返回 CodeWhale
traits.rs
根目录 / crates / tui / src / commands / traits.rs
1 //! Command traits and registry support.
2
3 use std::borrow::Cow;
4 use std::collections::HashMap;
5
6 use crate::localization::{Locale, MessageId, tr};
7 use crate::tui::app::App;
8
9 use super::CommandResult;
10
11 #[derive(Debug, Clone, Copy)]
12 pub struct CommandInfo {
13 pub name: &'static str,
14 pub aliases: &'static [&'static str],
15 pub usage: &'static str,
16 pub description_id: MessageId,
17 }
18
19 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
20 pub enum CommandDiscovery {
21 Primary,
22 Advanced,
23 Compatibility,
24 }
25
26 pub(crate) const ADVANCED_DISCOVERY_COMMANDS: &[&str] = &[
27 "anchor",
28 "balance",
29 "cache",
30 "change",
31 "context",
32 "diff",
33 "edit",
34 "goal",
35 "hf",
36 "hooks",
37 "lsp",
38 "modeldb",
39 "models",
40 "network",
41 "plugin",
42 "preview-request",
43 "profile",
44 "purge",
45 "relay",
46 "rename",
47 "rlm",
48 "settings",
49 "share",
50 "rail",
51 "status",
52 "system",
53 "theme",
54 "tokens",
55 "tools",
56 "translate",
57 "trust",
58 "verbose",
59 "workspace",
60 ];
61
62 pub(crate) const COMPATIBILITY_DISCOVERY_COMMANDS: &[&str] = &["subagents"];
63
64 /// Built-in commands that the palette pastes into the composer instead of
65 /// executing, even though they have no *required* argument.
66 ///
67 /// Prefer keeping this empty. Every name here must be a registered canonical
68 /// command name — see `palette_paste_only_names_are_registered` in the
69 /// command palette tests.
70 pub(crate) const PALETTE_PASTE_ONLY: &[&str] = &[];
71
72 impl CommandDiscovery {
73 pub fn show_at_root(self) -> bool {
74 matches!(self, CommandDiscovery::Primary)
75 }
76 }
77
78 impl CommandInfo {
79 pub fn requires_argument(&self) -> bool {
80 self.usage.contains('<') || self.usage.contains('[')
81 }
82
83 pub fn requires_required_argument(&self) -> bool {
84 let mut optional_depth = 0usize;
85 for ch in self.usage.chars() {
86 match ch {
87 '[' => optional_depth += 1,
88 ']' => optional_depth = optional_depth.saturating_sub(1),
89 '<' if optional_depth == 0 => return true,
90 _ => {}
91 }
92 }
93 false
94 }
95
96 /// Whether the slash menu / composer should leave a trailing space so the
97 /// user can type arguments immediately. `/change` is bare-useful (opens
98 /// the latest changelog) even though its usage documents an optional
99 /// version, so it is the only historical carve-out.
100 pub fn composer_wants_trailing_space(&self) -> bool {
101 self.name != "change" && self.requires_argument()
102 }
103
104 /// Whether the command palette should run this command immediately when
105 /// selected, instead of pasting it into the composer.
106 ///
107 /// Default: run anything that does not require a mandatory positional
108 /// argument (including optional-arg commands that open a picker when bare).
109 /// [`PALETTE_PASTE_ONLY`] is the explicit opt-out for side-effectful or
110 /// multi-step no-arg commands that should still paste for confirmation.
111 pub fn palette_runs_directly(&self) -> bool {
112 if self.requires_required_argument() {
113 return false;
114 }
115 !PALETTE_PASTE_ONLY.contains(&self.name)
116 }
117
118 pub fn palette_command(&self) -> String {
119 if self.requires_argument() {
120 format!("/{} ", self.name)
121 } else {
122 format!("/{}", self.name)
123 }
124 }
125
126 pub fn description_for(&self, locale: Locale) -> Cow<'static, str> {
127 tr(locale, self.description_id)
128 }
129
130 pub fn palette_description_for(&self, locale: Locale) -> String {
131 let desc = self.description_for(locale);
132 if self.aliases.is_empty() {
133 desc.to_string()
134 } else {
135 format!("{} aliases: {}", desc, self.aliases.join(", "))
136 }
137 }
138
139 pub fn discovery(&self) -> CommandDiscovery {
140 if COMPATIBILITY_DISCOVERY_COMMANDS.contains(&self.name) {
141 CommandDiscovery::Compatibility
142 } else if ADVANCED_DISCOVERY_COMMANDS.contains(&self.name) {
143 CommandDiscovery::Advanced
144 } else {
145 CommandDiscovery::Primary
146 }
147 }
148
149 pub fn show_in_empty_discovery(&self) -> bool {
150 self.discovery().show_at_root()
151 }
152
153 pub fn show_in_slash_completion(&self, prefix: &str) -> bool {
154 !prefix.trim_start_matches('/').trim().is_empty() || self.show_in_empty_discovery()
155 }
156 }
157
158 pub trait Command: Send + Sync {
159 fn info(&self) -> &'static CommandInfo;
160 fn execute(&self, app: &mut App, args: Option<&str>) -> CommandResult;
161 }
162
163 pub trait CommandGroup: Send + Sync {
164 fn commands(&self) -> &'static [Box<dyn Command>];
165 }
166
167 pub(crate) type CommandHandler = fn(&mut App, Option<&str>) -> CommandResult;
168
169 /// Trait implemented by focused built-in command modules.
170 ///
171 /// A command module owns its metadata and exposes a static execution function
172 /// that the group registry can wire into [`FunctionCommand`].
173 pub trait RegisterCommand {
174 fn info() -> &'static CommandInfo;
175 fn execute(app: &mut App, arg: Option<&str>) -> CommandResult;
176 }
177
178 pub(crate) struct FunctionCommand {
179 info: &'static CommandInfo,
180 handler: CommandHandler,
181 }
182
183 impl FunctionCommand {
184 pub(crate) const fn new(info: &'static CommandInfo, handler: CommandHandler) -> Self {
185 Self { info, handler }
186 }
187 }
188
189 impl Command for FunctionCommand {
190 fn info(&self) -> &'static CommandInfo {
191 self.info
192 }
193
194 fn execute(&self, app: &mut App, args: Option<&str>) -> CommandResult {
195 (self.handler)(app, args)
196 }
197 }
198
199 pub struct CommandRegistry {
200 commands: Vec<&'static dyn Command>,
201 name_to_index: HashMap<&'static str, usize>,
202 }
203
204 impl CommandRegistry {
205 pub fn empty() -> Self {
206 Self {
207 commands: Vec::new(),
208 name_to_index: HashMap::new(),
209 }
210 }
211
212 pub fn register(&mut self, command: &'static dyn Command) {
213 let index = self.commands.len();
214 let info = command.info();
215 self.name_to_index.insert(info.name, index);
216 for alias in info.aliases {
217 self.name_to_index.insert(alias, index);
218 }
219 self.commands.push(command);
220 }
221
222 pub fn register_group(&mut self, group: &dyn CommandGroup) {
223 for command in group.commands() {
224 self.register(command.as_ref());
225 }
226 }
227
228 pub fn get(&self, name: &str) -> Option<&dyn Command> {
229 let name = name.strip_prefix('/').unwrap_or(name);
230 self.name_to_index
231 .get(name)
232 .and_then(|index| self.commands.get(*index))
233 .copied()
234 }
235
236 pub fn get_info(&self, name: &str) -> Option<&'static CommandInfo> {
237 self.get(name).map(Command::info)
238 }
239
240 pub fn iter(&self) -> impl Iterator<Item = &dyn Command> {
241 self.commands.iter().copied()
242 }
243
244 pub fn infos(&self) -> Vec<&'static CommandInfo> {
245 self.iter().map(Command::info).collect()
246 }
247 }
248
248 lines RUST