futucli/cmd/
command_catalog.rs1use 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
93include!(concat!(env!("OUT_DIR"), "/generated_cli_catalog.rs"));
94
95pub fn catalog_rows(group: Option<&str>, search: Option<&str>) -> Result<Vec<CommandCatalogRow>> {
96 let group_filter = group.map(CommandGroup::from_str).transpose()?;
97 let search_filter = search.map(|s| s.trim().to_ascii_lowercase());
98 let summaries = command_summaries_by_name();
99
100 let rows = ALL_COMMANDS
101 .iter()
102 .filter(|entry| group_filter.is_none_or(|group| entry.group == group))
103 .filter_map(|entry| {
104 let summary = summaries
105 .get(entry.name)
106 .map(|summary| compact_summary(summary))
107 .unwrap_or_default();
108 let haystack = format!(
109 "{} {} {} {}",
110 entry.name,
111 entry.group.as_str(),
112 entry.group.label(),
113 summary
114 )
115 .to_ascii_lowercase();
116 if search_filter
117 .as_ref()
118 .is_some_and(|needle| !haystack.contains(needle))
119 {
120 return None;
121 }
122 Some(CommandCatalogRow {
123 group: entry.group.as_str(),
124 label: entry.group.label(),
125 command: entry.name.to_string(),
126 summary,
127 })
128 })
129 .collect();
130
131 Ok(rows)
132}
133
134fn command_summaries_by_name() -> BTreeMap<String, String> {
135 Cli::command()
136 .get_subcommands()
137 .map(|cmd| {
138 let summary = cmd.get_about().map(ToString::to_string).unwrap_or_default();
139 (cmd.get_name().to_string(), summary)
140 })
141 .collect()
142}
143
144fn compact_summary(raw: &str) -> String {
145 const MAX_CHARS: usize = 96;
146 let normalized = raw.split_whitespace().collect::<Vec<_>>().join(" ");
147 if normalized.chars().count() <= MAX_CHARS {
148 return normalized;
149 }
150
151 let mut out = normalized.chars().take(MAX_CHARS - 1).collect::<String>();
152 out.push('…');
153 out
154}
155
156pub fn run(group: Option<&str>, search: Option<&str>, output: OutputFormat) -> Result<()> {
157 let rows = catalog_rows(group, search)?;
158 output.print_rows(&rows, &rows)?;
159 Ok(())
160}