limit_cli/tui/commands/
registry.rs1use crate::error::CliError;
6use crate::session::SessionManager;
7use crate::tui::TuiState;
8use limit_tui::components::{ChatView, Message};
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum CommandResult {
15 Continue,
17 Exit,
19 ClearChat,
21 Message(String),
23 NewSession,
25 LoadSession(String),
27 Share(String),
29}
30
31pub struct CommandContext {
33 pub chat_view: Arc<Mutex<ChatView>>,
35 pub session_manager: Arc<Mutex<SessionManager>>,
37 pub session_id: String,
39 pub state: Arc<Mutex<TuiState>>,
41 pub messages: Arc<Mutex<Vec<limit_llm::Message>>>,
43 pub total_input_tokens: Arc<Mutex<u64>>,
45 pub total_output_tokens: Arc<Mutex<u64>>,
47 pub clipboard: Option<Arc<Mutex<crate::clipboard::ClipboardManager>>>,
49}
50
51impl CommandContext {
52 #[allow(clippy::too_many_arguments)]
54 pub fn new(
55 chat_view: Arc<Mutex<ChatView>>,
56 session_manager: Arc<Mutex<SessionManager>>,
57 session_id: String,
58 state: Arc<Mutex<TuiState>>,
59 messages: Arc<Mutex<Vec<limit_llm::Message>>>,
60 total_input_tokens: Arc<Mutex<u64>>,
61 total_output_tokens: Arc<Mutex<u64>>,
62 clipboard: Option<Arc<Mutex<crate::clipboard::ClipboardManager>>>,
63 ) -> Self {
64 Self {
65 chat_view,
66 session_manager,
67 session_id,
68 state,
69 messages,
70 total_input_tokens,
71 total_output_tokens,
72 clipboard,
73 }
74 }
75
76 pub fn add_system_message(&self, text: String) {
78 let msg = Message::system(text);
79 self.chat_view.lock().unwrap().add_message(msg);
80 }
81
82 pub fn add_user_message(&self, text: String) {
84 let msg = Message::user(text);
85 self.chat_view.lock().unwrap().add_message(msg);
86 }
87
88 pub fn clear_chat(&self) {
90 self.chat_view.lock().unwrap().clear();
91 }
92}
93
94pub trait Command: Send + Sync {
96 fn name(&self) -> &str;
98
99 fn aliases(&self) -> Vec<&str> {
101 vec![]
102 }
103
104 fn description(&self) -> &str;
106
107 fn usage(&self) -> Vec<&str> {
109 vec![]
110 }
111
112 fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError>;
114}
115
116pub struct CommandRegistry {
118 commands: HashMap<String, Box<dyn Command>>,
119}
120
121impl CommandRegistry {
122 pub fn new() -> Self {
124 Self {
125 commands: HashMap::new(),
126 }
127 }
128
129 pub fn register(&mut self, command: Box<dyn Command>) {
131 let name = command.name().to_string();
132 self.commands.insert(name, command);
133 }
134
135 #[inline]
137 pub fn list_commands(&self) -> Vec<&dyn Command> {
138 self.commands.values().map(|c| c.as_ref()).collect()
139 }
140
141 pub fn parse_and_execute(
143 &self,
144 input: &str,
145 ctx: &mut CommandContext,
146 ) -> Result<Option<CommandResult>, CliError> {
147 let input = input.trim();
148
149 if !input.starts_with('/') {
151 return Ok(None);
152 }
153
154 let input = &input[1..]; let parts: Vec<&str> = input.splitn(2, ' ').collect();
156 let cmd_name = parts[0].to_lowercase();
157 let args = parts.get(1).copied().unwrap_or("");
158
159 for command in self.commands.values() {
161 if command.name() == cmd_name || command.aliases().contains(&cmd_name.as_str()) {
162 return Ok(Some(command.execute(args, ctx)?));
163 }
164 }
165
166 ctx.add_system_message(format!("Unknown command: /{}", cmd_name));
168 ctx.add_system_message("Type /help for available commands".to_string());
169 Ok(Some(CommandResult::Continue))
170 }
171}
172
173impl Default for CommandRegistry {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_command_registry_creation() {
185 let registry = CommandRegistry::new();
186 assert_eq!(registry.list_commands().len(), 0);
187 }
188
189 #[test]
190 fn test_command_registry_default() {
191 let registry = CommandRegistry::default();
192 assert_eq!(registry.list_commands().len(), 0);
193 }
194}