Skip to main content

futucli/cmd/
command_catalog.rs

1//! Human-friendly futucli command catalog.
2//!
3//! This is a discovery layer only. Existing root command names remain the
4//! stable CLI contract; grouping metadata must not affect dispatch semantics.
5
6use 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}