Skip to main content

limit_cli/tui/commands/
registry.rs

1//! Command registry and trait definition
2//!
3//! Provides the core command system architecture.
4
5use 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/// Result of executing a command
13#[derive(Debug, Clone, PartialEq)]
14pub enum CommandResult {
15    /// Command executed successfully, continue running
16    Continue,
17    /// Exit the application
18    Exit,
19    /// Clear the chat view
20    ClearChat,
21    /// Add a message to the chat
22    Message(String),
23    /// Create a new session (returned by /session new)
24    NewSession,
25    /// Load a session (returned by /session load)
26    LoadSession(String),
27    /// Share/export session
28    Share(String),
29}
30
31/// Context provided to commands for execution
32pub struct CommandContext {
33    /// Chat view for displaying messages
34    pub chat_view: Arc<Mutex<ChatView>>,
35    /// Session manager for session operations
36    pub session_manager: Arc<Mutex<SessionManager>>,
37    /// Current session ID
38    pub session_id: String,
39    /// Current TUI state
40    pub state: Arc<Mutex<TuiState>>,
41    /// Conversation messages (LLM format)
42    pub messages: Arc<Mutex<Vec<limit_llm::Message>>>,
43    /// Total input tokens
44    pub total_input_tokens: Arc<Mutex<u64>>,
45    /// Total output tokens
46    pub total_output_tokens: Arc<Mutex<u64>>,
47    /// Clipboard manager (optional)
48    pub clipboard: Option<Arc<Mutex<crate::clipboard::ClipboardManager>>>,
49    /// Current project path (working directory)
50    pub project_path: std::path::PathBuf,
51}
52
53impl CommandContext {
54    /// Create a new command context
55    #[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    /// Add a system message to the chat
81    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    /// Add a user message to the chat
87    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    /// Clear the chat view
93    pub fn clear_chat(&self) {
94        self.chat_view.lock().unwrap().clear();
95    }
96}
97
98/// Trait for implementing commands
99pub trait Command: Send + Sync {
100    /// Get the command name (e.g., "help", "session")
101    fn name(&self) -> &str;
102
103    /// Get command aliases (e.g., ["?", "h"] for help)
104    fn aliases(&self) -> Vec<&str> {
105        vec![]
106    }
107
108    /// Get command description for help text
109    fn description(&self) -> &str;
110
111    /// Get usage examples
112    fn usage(&self) -> Vec<&str> {
113        vec![]
114    }
115
116    /// Execute the command
117    fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError>;
118}
119
120/// Registry for managing commands
121pub struct CommandRegistry {
122    commands: HashMap<String, Box<dyn Command>>,
123}
124
125impl CommandRegistry {
126    /// Create a new empty registry
127    pub fn new() -> Self {
128        Self {
129            commands: HashMap::new(),
130        }
131    }
132
133    /// Register a command
134    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    /// Get all registered commands
140    #[inline]
141    pub fn list_commands(&self) -> Vec<&dyn Command> {
142        self.commands.values().map(|c| c.as_ref()).collect()
143    }
144
145    /// Parse and execute a command string
146    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        // Must start with /
154        if !input.starts_with('/') {
155            return Ok(None);
156        }
157
158        let input = &input[1..]; // Remove /
159        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        // Find command by name or check if command handles it
164        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        // Unknown command
171        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}