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}
50
51impl CommandContext {
52    /// Create a new command context
53    #[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    /// Add a system message to the chat
77    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    /// Add a user message to the chat
83    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    /// Clear the chat view
89    pub fn clear_chat(&self) {
90        self.chat_view.lock().unwrap().clear();
91    }
92}
93
94/// Trait for implementing commands
95pub trait Command: Send + Sync {
96    /// Get the command name (e.g., "help", "session")
97    fn name(&self) -> &str;
98
99    /// Get command aliases (e.g., ["?", "h"] for help)
100    fn aliases(&self) -> Vec<&str> {
101        vec![]
102    }
103
104    /// Get command description for help text
105    fn description(&self) -> &str;
106
107    /// Get usage examples
108    fn usage(&self) -> Vec<&str> {
109        vec![]
110    }
111
112    /// Execute the command
113    fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError>;
114}
115
116/// Registry for managing commands
117pub struct CommandRegistry {
118    commands: HashMap<String, Box<dyn Command>>,
119}
120
121impl CommandRegistry {
122    /// Create a new empty registry
123    pub fn new() -> Self {
124        Self {
125            commands: HashMap::new(),
126        }
127    }
128
129    /// Register a command
130    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    /// Get all registered commands
136    #[inline]
137    pub fn list_commands(&self) -> Vec<&dyn Command> {
138        self.commands.values().map(|c| c.as_ref()).collect()
139    }
140
141    /// Parse and execute a command string
142    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        // Must start with /
150        if !input.starts_with('/') {
151            return Ok(None);
152        }
153
154        let input = &input[1..]; // Remove /
155        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        // Find command by name or check if command handles it
160        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        // Unknown command
167        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}