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    /// Enable TLDR warm for project
30    TldrWarm,
31}
32
33/// Context provided to commands for execution
34pub struct CommandContext {
35    /// Chat view for displaying messages
36    pub chat_view: Arc<Mutex<ChatView>>,
37    /// Session manager for session operations
38    pub session_manager: Arc<Mutex<SessionManager>>,
39    /// Current session ID
40    pub session_id: String,
41    /// Current TUI state
42    pub state: Arc<Mutex<TuiState>>,
43    /// Conversation messages (LLM format)
44    pub messages: Arc<Mutex<Vec<limit_llm::Message>>>,
45    /// Total input tokens
46    pub total_input_tokens: Arc<Mutex<u64>>,
47    /// Total output tokens
48    pub total_output_tokens: Arc<Mutex<u64>>,
49    /// Clipboard manager (optional)
50    pub clipboard: Option<Arc<Mutex<crate::clipboard::ClipboardManager>>>,
51    /// Current project path (working directory)
52    pub project_path: std::path::PathBuf,
53}
54
55impl CommandContext {
56    /// Create a new command context
57    #[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    /// Add a system message to the chat
83    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    /// Add a user message to the chat
89    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    /// Clear the chat view
95    pub fn clear_chat(&self) {
96        self.chat_view.lock().unwrap().clear();
97    }
98}
99
100/// Trait for implementing commands
101pub trait Command: Send + Sync {
102    /// Get the command name (e.g., "help", "session")
103    fn name(&self) -> &str;
104
105    /// Get command aliases (e.g., ["?", "h"] for help)
106    fn aliases(&self) -> Vec<&str> {
107        vec![]
108    }
109
110    /// Get command description for help text
111    fn description(&self) -> &str;
112
113    /// Get usage examples
114    fn usage(&self) -> Vec<&str> {
115        vec![]
116    }
117
118    /// Execute the command
119    fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError>;
120}
121
122/// Registry for managing commands
123pub struct CommandRegistry {
124    commands: HashMap<String, Box<dyn Command>>,
125}
126
127impl CommandRegistry {
128    /// Create a new empty registry
129    pub fn new() -> Self {
130        Self {
131            commands: HashMap::new(),
132        }
133    }
134
135    /// Register a command
136    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    /// Get all registered commands
142    #[inline]
143    pub fn list_commands(&self) -> Vec<&dyn Command> {
144        self.commands.values().map(|c| c.as_ref()).collect()
145    }
146
147    /// Parse and execute a command string
148    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        // Must start with /
156        if !input.starts_with('/') {
157            return Ok(None);
158        }
159
160        let input = &input[1..]; // Remove /
161        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        // Find command by name or check if command handles it
166        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        // Unknown command
173        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}