1pub mod registry;
2
3use crate::tui::custom_commands::CustomCommand;
4use std::fmt;
5use std::str::FromStr;
6use steer_core::app::conversation::{AppCommandType as CoreCommand, SlashCommandError};
7use strum::{Display, EnumIter, IntoEnumIterator};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum TuiCommandError {
13 #[error("Unknown command: {0}")]
14 UnknownCommand(String),
15 #[error(transparent)]
16 CoreParseError(#[from] SlashCommandError),
17}
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum TuiCommand {
22 ReloadFiles,
24 Theme(Option<String>),
26 Auth,
28 Help(Option<String>),
30 EditingMode(Option<String>),
32 Custom(CustomCommand),
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Display)]
39#[strum(serialize_all = "kebab-case")]
40pub enum TuiCommandType {
41 ReloadFiles,
42 Theme,
43 Auth,
44 Help,
45 EditingMode,
46}
47
48impl TuiCommandType {
49 pub fn command_name(&self) -> String {
51 match self {
52 TuiCommandType::ReloadFiles => self.to_string(),
53 TuiCommandType::Theme => self.to_string(),
54 TuiCommandType::Auth => self.to_string(),
55 TuiCommandType::Help => self.to_string(),
56 TuiCommandType::EditingMode => self.to_string(),
57 }
58 }
59
60 pub fn description(&self) -> &'static str {
62 match self {
63 TuiCommandType::ReloadFiles => "Reload file cache in the TUI",
64 TuiCommandType::Theme => "Change or list available themes",
65 TuiCommandType::Auth => "Manage authentication settings",
66 TuiCommandType::Help => "Show help information",
67 TuiCommandType::EditingMode => "Switch between editing modes (simple/vim)",
68 }
69 }
70
71 pub fn usage(&self) -> String {
73 match self {
74 TuiCommandType::ReloadFiles => format!("/{}", self.command_name()),
75 TuiCommandType::Theme => format!("/{} [theme_name]", self.command_name()),
76 TuiCommandType::Auth => format!("/{}", self.command_name()),
77 TuiCommandType::Help => format!("/{} [command]", self.command_name()),
78 TuiCommandType::EditingMode => format!("/{} [simple|vim]", self.command_name()),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Display)]
86#[strum(serialize_all = "kebab-case")]
87pub enum CoreCommandType {
88 Model,
89 Clear,
90 Compact,
91}
92
93impl CoreCommandType {
94 pub fn command_name(&self) -> String {
96 match self {
97 CoreCommandType::Model => self.to_string(),
98 CoreCommandType::Clear => self.to_string(),
99 CoreCommandType::Compact => self.to_string(),
100 }
101 }
102
103 pub fn description(&self) -> &'static str {
105 match self {
106 CoreCommandType::Model => "Show or change the current model",
107 CoreCommandType::Clear => "Clear conversation history and tool approvals",
108 CoreCommandType::Compact => "Summarise older messages to save context space",
109 }
110 }
111
112 pub fn usage(&self) -> String {
114 match self {
115 CoreCommandType::Model => format!("/{} [model_name]", self.command_name()),
116 CoreCommandType::Clear => format!("/{}", self.command_name()),
117 CoreCommandType::Compact => format!("/{}", self.command_name()),
118 }
119 }
120
121 pub fn to_core_command(&self, args: &[&str]) -> Option<CoreCommand> {
124 match self {
125 CoreCommandType::Model => {
126 let target = if args.is_empty() {
127 None
128 } else {
129 Some(args.join(" "))
130 };
131 Some(CoreCommand::Model { target })
132 }
133 CoreCommandType::Clear => Some(CoreCommand::Clear),
134 CoreCommandType::Compact => Some(CoreCommand::Compact),
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq)]
141pub enum AppCommand {
142 Tui(TuiCommand),
144 Core(CoreCommand),
146}
147
148impl TuiCommand {
149 fn parse_without_slash(command: &str) -> Result<Self, TuiCommandError> {
151 let parts: Vec<&str> = command.split_whitespace().collect();
152 let cmd_name = parts.first().copied().unwrap_or("");
153
154 for cmd_type in TuiCommandType::iter() {
156 if cmd_name == cmd_type.command_name() {
157 return match cmd_type {
158 TuiCommandType::ReloadFiles => Ok(TuiCommand::ReloadFiles),
159 TuiCommandType::Theme => {
160 let theme_name = parts.get(1).map(|s| s.to_string());
161 Ok(TuiCommand::Theme(theme_name))
162 }
163 TuiCommandType::Auth => Ok(TuiCommand::Auth),
164 TuiCommandType::Help => {
165 let command_name = parts.get(1).map(|s| s.to_string());
166 Ok(TuiCommand::Help(command_name))
167 }
168 TuiCommandType::EditingMode => {
169 let mode_name = parts.get(1).map(|s| s.to_string());
170 Ok(TuiCommand::EditingMode(mode_name))
171 }
172 };
173 }
174 }
175
176 Err(TuiCommandError::UnknownCommand(command.to_string()))
177 }
178
179 pub fn as_command_str(&self) -> String {
181 match self {
182 TuiCommand::ReloadFiles => TuiCommandType::ReloadFiles.command_name().to_string(),
183 TuiCommand::Theme(None) => TuiCommandType::Theme.command_name().to_string(),
184 TuiCommand::Theme(Some(name)) => {
185 format!("{} {}", TuiCommandType::Theme.command_name(), name)
186 }
187 TuiCommand::Auth => TuiCommandType::Auth.command_name().to_string(),
188 TuiCommand::Help(None) => TuiCommandType::Help.command_name().to_string(),
189 TuiCommand::Help(Some(cmd)) => {
190 format!("{} {}", TuiCommandType::Help.command_name(), cmd)
191 }
192 TuiCommand::EditingMode(None) => TuiCommandType::EditingMode.command_name().to_string(),
193 TuiCommand::EditingMode(Some(mode)) => {
194 format!("{} {}", TuiCommandType::EditingMode.command_name(), mode)
195 }
196 TuiCommand::Custom(cmd) => cmd.name().to_string(),
197 }
198 }
199}
200
201impl AppCommand {
202 pub fn parse(input: &str) -> Result<Self, TuiCommandError> {
204 let command = input.trim();
206 let command = command.strip_prefix('/').unwrap_or(command);
207
208 let parts: Vec<&str> = command.split_whitespace().collect();
209 let cmd_name = parts.first().copied().unwrap_or("");
210
211 for tui_type in TuiCommandType::iter() {
213 if cmd_name == tui_type.command_name() {
214 return TuiCommand::parse_without_slash(command).map(AppCommand::Tui);
215 }
216 }
217
218 for core_type in CoreCommandType::iter() {
220 if cmd_name == core_type.command_name() {
221 let args: Vec<&str> = parts.into_iter().skip(1).collect();
222 if let Some(core_cmd) = core_type.to_core_command(&args) {
223 return Ok(AppCommand::Core(core_cmd));
224 } else {
225 return Err(TuiCommandError::UnknownCommand(command.to_string()));
226 }
227 }
228 }
229
230 Err(TuiCommandError::UnknownCommand(command.to_string()))
233 }
234
235 pub fn as_command_str(&self) -> String {
237 match self {
238 AppCommand::Tui(tui_cmd) => format!("/{}", tui_cmd.as_command_str()),
239 AppCommand::Core(core_cmd) => core_cmd.to_string(),
240 }
241 }
242}
243
244impl fmt::Display for TuiCommand {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 write!(f, "/{}", self.as_command_str())
247 }
248}
249
250impl fmt::Display for AppCommand {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 write!(f, "{}", self.as_command_str())
253 }
254}
255
256impl FromStr for AppCommand {
257 type Err = TuiCommandError;
258
259 fn from_str(s: &str) -> Result<Self, Self::Err> {
260 Self::parse(s)
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_parse_tui_commands() {
270 assert!(matches!(
271 AppCommand::parse("/reload-files").unwrap(),
272 AppCommand::Tui(TuiCommand::ReloadFiles)
273 ));
274 assert!(matches!(
275 AppCommand::parse("/theme").unwrap(),
276 AppCommand::Tui(TuiCommand::Theme(None))
277 ));
278 assert!(matches!(
279 AppCommand::parse("/theme gruvbox").unwrap(),
280 AppCommand::Tui(TuiCommand::Theme(Some(_)))
281 ));
282 }
283
284 #[test]
285 fn test_parse_core_commands() {
286 assert!(matches!(
287 AppCommand::parse("/help").unwrap(),
288 AppCommand::Tui(TuiCommand::Help(None))
289 ));
290 assert!(matches!(
291 AppCommand::parse("/clear").unwrap(),
292 AppCommand::Core(CoreCommand::Clear)
293 ));
294 assert!(matches!(
295 AppCommand::parse("/model opus").unwrap(),
296 AppCommand::Core(CoreCommand::Model { .. })
297 ));
298 }
299
300 #[test]
301 fn test_display() {
302 assert_eq!(
303 AppCommand::Tui(TuiCommand::ReloadFiles).to_string(),
304 "/reload-files"
305 );
306 assert_eq!(AppCommand::Tui(TuiCommand::Help(None)).to_string(), "/help");
307 }
308
309 #[test]
310 fn test_error_formatting() {
311 let err = AppCommand::parse("/unknown-tui-cmd").unwrap_err();
313 assert_eq!(err.to_string(), "Unknown command: unknown-tui-cmd");
314 }
315
316 #[test]
317 fn test_tui_command_from_str() {
318 let cmd = "/reload-files".parse::<AppCommand>().unwrap();
319 assert!(matches!(cmd, AppCommand::Tui(TuiCommand::ReloadFiles)));
320 }
321}