1use std::collections::BTreeMap;
7use std::str::FromStr;
8
9use anyhow::{Result, bail};
10use clap::CommandFactory;
11use serde::Serialize;
12use tabled::Tabled;
13
14use crate::cli::Cli;
15use crate::output::OutputFormat;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
18#[serde(rename_all = "kebab-case")]
19pub enum CommandGroup {
20 Common,
21 Quote,
22 Trade,
23 Account,
24 Key,
25 System,
26 Research,
27 Advanced,
28}
29
30impl CommandGroup {
31 pub const fn as_str(self) -> &'static str {
32 match self {
33 Self::Common => "common",
34 Self::Quote => "quote",
35 Self::Trade => "trade",
36 Self::Account => "account",
37 Self::Key => "key",
38 Self::System => "system",
39 Self::Research => "research",
40 Self::Advanced => "advanced",
41 }
42 }
43
44 pub const fn label(self) -> &'static str {
45 match self {
46 Self::Common => "常用",
47 Self::Quote => "行情",
48 Self::Trade => "交易",
49 Self::Account => "账户",
50 Self::Key => "密钥/密码",
51 Self::System => "系统/诊断",
52 Self::Research => "财务/研究",
53 Self::Advanced => "高级",
54 }
55 }
56}
57
58impl FromStr for CommandGroup {
59 type Err = anyhow::Error;
60
61 fn from_str(value: &str) -> Result<Self> {
62 let normalized = value.trim().to_ascii_lowercase();
63 match normalized.as_str() {
64 "common" | "basic" | "常用" => Ok(Self::Common),
65 "quote" | "qot" | "行情" => Ok(Self::Quote),
66 "trade" | "trd" | "交易" => Ok(Self::Trade),
67 "account" | "acc" | "账户" => Ok(Self::Account),
68 "key" | "keys" | "auth" | "密钥" | "密码" => Ok(Self::Key),
69 "system" | "sys" | "diagnostic" | "diagnostics" | "诊断" => Ok(Self::System),
70 "research" | "financial" | "finance" | "财务" | "研究" => Ok(Self::Research),
71 "advanced" | "adv" | "debug" | "高级" => Ok(Self::Advanced),
72 _ => bail!(
73 "unknown command group `{value}`; valid groups: common, quote, trade, account, key, system, research, advanced"
74 ),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy)]
80pub struct CommandCatalogEntry {
81 pub name: &'static str,
82 pub group: CommandGroup,
83}
84
85#[derive(Debug, Clone, Serialize, Tabled)]
86pub struct CommandCatalogRow {
87 pub group: &'static str,
88 pub label: &'static str,
89 pub command: String,
90 pub summary: String,
91}
92
93macro_rules! entry {
94 ($name:literal, $group:ident) => {
95 CommandCatalogEntry {
96 name: $name,
97 group: CommandGroup::$group,
98 }
99 };
100}
101
102pub const ALL_COMMANDS: &[CommandCatalogEntry] = &[
103 entry!("ping", Common),
104 entry!("commands", Common),
105 entry!("version", System),
106 entry!("lang-pack", System),
107 entry!("repl", Common),
108 entry!("quote", Quote),
109 entry!("snapshot", Quote),
110 entry!("sub", Quote),
111 entry!("kline", Quote),
112 entry!("orderbook", Quote),
113 entry!("ticker", Quote),
114 entry!("rt", Quote),
115 entry!("static", Quote),
116 entry!("static-warmup", Quote),
117 entry!("broker", Quote),
118 entry!("plate-list", Quote),
119 entry!("plate-stocks", Quote),
120 entry!("reference", Quote),
121 entry!("capital-flow", Quote),
122 entry!("capital-distribution", Quote),
123 entry!("market-state", Quote),
124 entry!("owner-plate", Quote),
125 entry!("trading-days", Quote),
126 entry!("rehab", Quote),
127 entry!("suspend", Quote),
128 entry!("user-security", Quote),
129 entry!("user-security-groups", Quote),
130 entry!("warrant", Quote),
131 entry!("ipo-list", Quote),
132 entry!("ipo-calendar", Quote),
133 entry!("future-info", Quote),
134 entry!("stock-filter", Quote),
135 entry!("risk-free-rate", Quote),
136 entry!("spread-table", Quote),
137 entry!("ticker-statistic", Quote),
138 entry!("ticker-statistic-detail", Quote),
139 entry!("query-subscription", Quote),
140 entry!("unsubscribe", Quote),
141 entry!("account", Account),
142 entry!("funds", Account),
143 entry!("position", Account),
144 entry!("account-flag", Account),
145 entry!("cash-log", Account),
146 entry!("cash-detail", Account),
147 entry!("biz-group", Account),
148 entry!("margin-info", Account),
149 entry!("bond-total-asset", Account),
150 entry!("bond-single-asset", Account),
151 entry!("bond-position-list", Account),
152 entry!("place-order", Trade),
153 entry!("modify-order", Trade),
154 entry!("cancel-order", Trade),
155 entry!("reconfirm-order", Trade),
156 entry!("history-orders", Trade),
157 entry!("history-deals", Trade),
158 entry!("order", Trade),
159 entry!("deal", Trade),
160 entry!("unlock-trade", Trade),
161 entry!("max-qtys", Trade),
162 entry!("trade-check", Trade),
163 entry!("combo-max-trd-qtys", Trade),
164 entry!("combo-order", Trade),
165 entry!("margin-ratio", Trade),
166 entry!("order-fee", Trade),
167 entry!("cancel-all-order", Trade),
168 entry!("sub-acc-push", Trade),
169 entry!("unsub-acc-push", Trade),
170 entry!("acc-cash-flow", Trade),
171 entry!("bond-answer-state", Trade),
172 entry!("bond-trade-reminder", Trade),
173 entry!("set-trade-pwd", Key),
174 entry!("clear-trade-pwd", Key),
175 entry!("set-login-pwd", Key),
176 entry!("clear-login-pwd", Key),
177 entry!("gen-key", Key),
178 entry!("bind-key", Key),
179 entry!("machine-id", Key),
180 entry!("list-keys", Key),
181 entry!("revoke-key", Key),
182 entry!("push-subscriber-info", System),
183 entry!("static-status", System),
184 entry!("global-state", System),
185 entry!("user-info", System),
186 entry!("quote-rights", System),
187 entry!("quote-capability", System),
188 entry!("delay-statistics", System),
189 entry!("token-state", System),
190 entry!("used-quota", System),
191 entry!("surface", System),
192 entry!("history-kl-quota", System),
193 entry!("daemon-status", System),
194 entry!("doctor", System),
195 entry!("daemon-shutdown", System),
196 entry!("daemon-reload", System),
197 entry!("company-profile", Research),
198 entry!("company-executives", Research),
199 entry!("company-executive-background", Research),
200 entry!("company-operational-efficiency", Research),
201 entry!("financials-earnings-price-move", Research),
202 entry!("financials-earnings-price-history", Research),
203 entry!("financial-calendar", Research),
204 entry!("financial-calendar-target", Research),
205 entry!("earnings-calendar", Research),
206 entry!("macro-indicator-list", Research),
207 entry!("macro-indicator-history", Research),
208 entry!("fed-watch-target-rate", Research),
209 entry!("fed-watch-dot-plot", Research),
210 entry!("earnings-beat-rank", Research),
211 entry!("dividend-rank", Research),
212 entry!("dividend-calendar", Research),
213 entry!("economic-calendar", Research),
214 entry!("us-pre-market-rank", Research),
215 entry!("us-after-hours-rank", Research),
216 entry!("us-overnight-rank", Research),
217 entry!("top-movers-rank", Research),
218 entry!("hot-list", Research),
219 entry!("short-selling-rank", Research),
220 entry!("period-change-rank", Research),
221 entry!("high-dividend-soe-rank", Research),
222 entry!("institution-list", Research),
223 entry!("institution-profile", Research),
224 entry!("institution-distribution", Research),
225 entry!("institution-holding-change", Research),
226 entry!("institution-holding-list", Research),
227 entry!("ark-fund-holding", Research),
228 entry!("ark-stock-dynamic", Research),
229 entry!("ark-active-transaction", Research),
230 entry!("rating-change", Research),
231 entry!("industrial-chain-list", Research),
232 entry!("industrial-chain-detail", Research),
233 entry!("industrial-chain-by-plate", Research),
234 entry!("industrial-plate-info", Research),
235 entry!("industrial-plate-stock", Research),
236 entry!("heat-map-data", Research),
237 entry!("rise-fall-distribution", Research),
238 entry!("financials-statements", Research),
239 entry!("financials-revenue-breakdown", Research),
240 entry!("research-analyst-consensus", Research),
241 entry!("research-rating-summary", Research),
242 entry!("research-morningstar-report", Research),
243 entry!("valuation-detail", Research),
244 entry!("valuation-plate-stock-list", Research),
245 entry!("technical-unusual", Research),
246 entry!("financial-unusual", Research),
247 entry!("corporate-actions-buybacks", Research),
248 entry!("corporate-actions-dividends", Research),
249 entry!("corporate-actions-stock-splits", Research),
250 entry!("daily-short-volume", Research),
251 entry!("short-interest", Research),
252 entry!("top-ten-buy-sell-brokers", Research),
253 entry!("shareholders-overview", Research),
254 entry!("shareholders-holding-changes", Research),
255 entry!("shareholders-holder-detail", Research),
256 entry!("shareholders-institutional", Research),
257 entry!("insider-holder-list", Research),
258 entry!("insider-trade-list", Research),
259 entry!("option-screen", Advanced),
260 entry!("warrant-screen", Advanced),
261 entry!("stock-screen", Advanced),
262 entry!("derivative-unusual", Advanced),
263 entry!("option-volatility", Advanced),
264 entry!("option-exercise-probability", Advanced),
265 entry!("option-quote", Advanced),
266 entry!("option-strategy", Advanced),
267 entry!("option-strategy-analysis", Advanced),
268 entry!("option-strategy-spread", Advanced),
269 entry!("option-chain", Advanced),
270 entry!("holding-change", Advanced),
271 entry!("modify-user-security", Advanced),
272 entry!("code-change", Advanced),
273 entry!("set-price-reminder", Advanced),
274 entry!("price-reminder", Advanced),
275 entry!("option-expiration-date", Advanced),
276];
277
278pub fn catalog_rows(group: Option<&str>, search: Option<&str>) -> Result<Vec<CommandCatalogRow>> {
279 let group_filter = group.map(CommandGroup::from_str).transpose()?;
280 let search_filter = search.map(|s| s.trim().to_ascii_lowercase());
281 let summaries = command_summaries_by_name();
282
283 let rows = ALL_COMMANDS
284 .iter()
285 .filter(|entry| group_filter.is_none_or(|group| entry.group == group))
286 .filter_map(|entry| {
287 let summary = summaries
288 .get(entry.name)
289 .map(|summary| compact_summary(summary))
290 .unwrap_or_default();
291 let haystack = format!(
292 "{} {} {} {}",
293 entry.name,
294 entry.group.as_str(),
295 entry.group.label(),
296 summary
297 )
298 .to_ascii_lowercase();
299 if search_filter
300 .as_ref()
301 .is_some_and(|needle| !haystack.contains(needle))
302 {
303 return None;
304 }
305 Some(CommandCatalogRow {
306 group: entry.group.as_str(),
307 label: entry.group.label(),
308 command: entry.name.to_string(),
309 summary,
310 })
311 })
312 .collect();
313
314 Ok(rows)
315}
316
317fn command_summaries_by_name() -> BTreeMap<String, String> {
318 Cli::command()
319 .get_subcommands()
320 .map(|cmd| {
321 let summary = cmd.get_about().map(ToString::to_string).unwrap_or_default();
322 (cmd.get_name().to_string(), summary)
323 })
324 .collect()
325}
326
327fn compact_summary(raw: &str) -> String {
328 const MAX_CHARS: usize = 96;
329 let normalized = raw.split_whitespace().collect::<Vec<_>>().join(" ");
330 if normalized.chars().count() <= MAX_CHARS {
331 return normalized;
332 }
333
334 let mut out = normalized.chars().take(MAX_CHARS - 1).collect::<String>();
335 out.push('…');
336 out
337}
338
339pub fn run(group: Option<&str>, search: Option<&str>, output: OutputFormat) -> Result<()> {
340 let rows = catalog_rows(group, search)?;
341 output.print_rows(&rows, &rows)?;
342 Ok(())
343}