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