tl_cli/chat/
command.rs

1use inquire::autocompletion::{Autocomplete, Replacement};
2
3// Available slash commands: (command, description)
4const SLASH_COMMANDS: &[(&str, &str)] = &[
5    ("/config", "Show current configuration"),
6    ("/help", "Show available commands"),
7    ("/quit", "Exit chat mode"),
8];
9
10/// Slash command autocompleter
11#[derive(Clone, Default)]
12pub struct SlashCommandCompleter;
13
14impl Autocomplete for SlashCommandCompleter {
15    fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
16        if !input.starts_with('/') {
17            return Ok(vec![]);
18        }
19
20        let suggestions: Vec<String> = SLASH_COMMANDS
21            .iter()
22            .filter(|(cmd, _)| cmd.starts_with(input))
23            .map(|(cmd, desc)| format!("{cmd}  {desc}"))
24            .collect();
25
26        Ok(suggestions)
27    }
28
29    fn get_completion(
30        &mut self,
31        _input: &str,
32        highlighted_suggestion: Option<String>,
33    ) -> Result<Replacement, inquire::CustomUserError> {
34        let replacement =
35            highlighted_suggestion.map(|s| s.split_whitespace().next().unwrap_or("").to_string());
36        Ok(replacement)
37    }
38}
39
40/// Slash command types
41#[derive(Debug, Clone)]
42pub enum SlashCommand {
43    Config,
44    Help,
45    Quit,
46    Unknown(String),
47}
48
49/// Input types
50#[derive(Debug)]
51pub enum Input {
52    Text(String),
53    Command(SlashCommand),
54    Empty,
55}
56
57pub fn parse_input(input: &str) -> Input {
58    let input = input.trim();
59
60    if input.is_empty() {
61        return Input::Empty;
62    }
63
64    input
65        .strip_prefix('/')
66        .map_or_else(|| Input::Text(input.to_string()), parse_slash_command)
67}
68
69fn parse_slash_command(cmd: &str) -> Input {
70    let parts: Vec<&str> = cmd.split_whitespace().collect();
71
72    match parts.first().copied() {
73        Some("config") => Input::Command(SlashCommand::Config),
74        Some("help") => Input::Command(SlashCommand::Help),
75        Some("quit" | "exit" | "q") => Input::Command(SlashCommand::Quit),
76        _ => Input::Command(SlashCommand::Unknown(parts.join(" "))),
77    }
78}
79
80#[cfg(test)]
81#[allow(clippy::unwrap_used)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_parse_empty_input() {
87        assert!(matches!(parse_input(""), Input::Empty));
88        assert!(matches!(parse_input("   "), Input::Empty));
89    }
90
91    #[test]
92    fn test_parse_text_input() {
93        match parse_input("Hello, world!") {
94            Input::Text(text) => assert_eq!(text, "Hello, world!"),
95            _ => panic!("Expected Input::Text"),
96        }
97    }
98
99    #[test]
100    fn test_parse_config_command() {
101        assert!(matches!(
102            parse_input("/config"),
103            Input::Command(SlashCommand::Config)
104        ));
105    }
106
107    #[test]
108    fn test_parse_help_command() {
109        assert!(matches!(
110            parse_input("/help"),
111            Input::Command(SlashCommand::Help)
112        ));
113    }
114
115    #[test]
116    fn test_parse_quit_commands() {
117        assert!(matches!(
118            parse_input("/quit"),
119            Input::Command(SlashCommand::Quit)
120        ));
121        assert!(matches!(
122            parse_input("/exit"),
123            Input::Command(SlashCommand::Quit)
124        ));
125        assert!(matches!(
126            parse_input("/q"),
127            Input::Command(SlashCommand::Quit)
128        ));
129    }
130
131    #[test]
132    fn test_parse_unknown_command() {
133        match parse_input("/unknown") {
134            Input::Command(SlashCommand::Unknown(cmd)) => assert_eq!(cmd, "unknown"),
135            _ => panic!("Expected Input::Command(SlashCommand::Unknown)"),
136        }
137    }
138
139    // SlashCommandCompleter tests
140
141    #[test]
142    fn test_completer_no_suggestions_for_regular_text() {
143        let mut completer = SlashCommandCompleter;
144        let suggestions = completer.get_suggestions("hello").unwrap();
145        assert!(suggestions.is_empty());
146    }
147
148    #[test]
149    fn test_completer_suggestions_for_slash() {
150        let mut completer = SlashCommandCompleter;
151        let suggestions = completer.get_suggestions("/").unwrap();
152        assert_eq!(suggestions.len(), 3); // /config, /help, /quit
153    }
154
155    #[test]
156    fn test_completer_suggestions_filter_by_prefix() {
157        let mut completer = SlashCommandCompleter;
158
159        let suggestions = completer.get_suggestions("/c").unwrap();
160        assert_eq!(suggestions.len(), 1);
161        assert!(suggestions[0].starts_with("/config"));
162
163        let suggestions = completer.get_suggestions("/q").unwrap();
164        assert_eq!(suggestions.len(), 1);
165        assert!(suggestions[0].starts_with("/quit"));
166    }
167
168    #[test]
169    fn test_completer_completion() {
170        let mut completer = SlashCommandCompleter;
171        let suggestion = "/config  Show current configuration".to_string();
172        let completion = completer.get_completion("/c", Some(suggestion)).unwrap();
173        assert_eq!(completion, Some("/config".to_string()));
174    }
175
176    #[test]
177    fn test_completer_completion_none() {
178        let mut completer = SlashCommandCompleter;
179        let completion = completer.get_completion("/x", None).unwrap();
180        assert!(completion.is_none());
181    }
182}