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 TldrWarm,
31}
32
33pub struct CommandContext {
35 pub chat_view: Arc<Mutex<ChatView>>,
37 pub session_manager: Arc<Mutex<SessionManager>>,
39 pub session_id: String,
41 pub state: Arc<Mutex<TuiState>>,
43 pub messages: Arc<Mutex<Vec<limit_llm::Message>>>,
45 pub total_input_tokens: Arc<Mutex<u64>>,
47 pub total_output_tokens: Arc<Mutex<u64>>,
49 pub clipboard: Option<Arc<Mutex<crate::clipboard::ClipboardManager>>>,
51 pub project_path: std::path::PathBuf,
53}
54
55impl CommandContext {
56 #[allow(clippy::too_many_arguments)]
58 pub fn new(
59 chat_view: Arc<Mutex<ChatView>>,
60 session_manager: Arc<Mutex<SessionManager>>,
61 session_id: String,
62 state: Arc<Mutex<TuiState>>,
63 messages: Arc<Mutex<Vec<limit_llm::Message>>>,
64 total_input_tokens: Arc<Mutex<u64>>,
65 total_output_tokens: Arc<Mutex<u64>>,
66 clipboard: Option<Arc<Mutex<crate::clipboard::ClipboardManager>>>,
67 project_path: std::path::PathBuf,
68 ) -> Self {
69 Self {
70 chat_view,
71 session_manager,
72 session_id,
73 state,
74 messages,
75 total_input_tokens,
76 total_output_tokens,
77 clipboard,
78 project_path,
79 }
80 }
81
82 pub fn add_system_message(&self, text: String) {
84 let msg = Message::system(text);
85 self.chat_view.lock().unwrap().add_message(msg);
86 }
87
88 pub fn add_user_message(&self, text: String) {
90 let msg = Message::user(text);
91 self.chat_view.lock().unwrap().add_message(msg);
92 }
93
94 pub fn clear_chat(&self) {
96 self.chat_view.lock().unwrap().clear();
97 }
98}
99
100pub trait Command: Send + Sync {
102 fn name(&self) -> &str;
104
105 fn aliases(&self) -> Vec<&str> {
107 vec![]
108 }
109
110 fn description(&self) -> &str;
112
113 fn usage(&self) -> Vec<&str> {
115 vec![]
116 }
117
118 fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError>;
120}
121
122pub struct CommandRegistry {
124 commands: HashMap<String, Box<dyn Command>>,
125}
126
127impl CommandRegistry {
128 pub fn new() -> Self {
130 Self {
131 commands: HashMap::new(),
132 }
133 }
134
135 pub fn register(&mut self, command: Box<dyn Command>) {
137 let name = command.name().to_string();
138 self.commands.insert(name, command);
139 }
140
141 #[inline]
143 pub fn list_commands(&self) -> Vec<&dyn Command> {
144 self.commands.values().map(|c| c.as_ref()).collect()
145 }
146
147 pub fn parse_and_execute(
149 &self,
150 input: &str,
151 ctx: &mut CommandContext,
152 ) -> Result<Option<CommandResult>, CliError> {
153 let input = input.trim();
154
155 if !input.starts_with('/') {
157 return Ok(None);
158 }
159
160 let input = &input[1..]; let parts: Vec<&str> = input.splitn(2, ' ').collect();
162 let cmd_name = parts[0].to_lowercase();
163 let args = parts.get(1).copied().unwrap_or("");
164
165 for command in self.commands.values() {
167 if command.name() == cmd_name || command.aliases().contains(&cmd_name.as_str()) {
168 return Ok(Some(command.execute(args, ctx)?));
169 }
170 }
171
172 ctx.add_system_message(format!("Unknown command: /{}", cmd_name));
174 ctx.add_system_message("Type /help for available commands".to_string());
175 Ok(Some(CommandResult::Continue))
176 }
177}
178
179impl Default for CommandRegistry {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_command_registry_creation() {
191 let registry = CommandRegistry::new();
192 assert_eq!(registry.list_commands().len(), 0);
193 }
194
195 #[test]
196 fn test_command_registry_default() {
197 let registry = CommandRegistry::default();
198 assert_eq!(registry.list_commands().len(), 0);
199 }
200}