Skip to main content

oxios_cli/
commands.rs

1//! Meta-command parsing for the CLI channel.
2//!
3//! Recognises dot-commands that control the interactive session:
4//! `.quit`, `.help`, `.reset`, `.model`, `.persona`, `.clear`.
5
6/// A parsed meta-command.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum MetaCommand {
9    /// Quit the session.
10    Quit,
11    /// Show help text.
12    Help,
13    /// Reset the current session / conversation.
14    Reset,
15    /// Switch the active model. Carries the model name, if provided.
16    Model(Option<String>),
17    /// Switch the active persona. Carries the persona name, if provided.
18    Persona(Option<String>),
19    /// Clear the terminal screen.
20    Clear,
21}
22
23impl MetaCommand {
24    /// Attempt to parse a line as a meta-command.
25    ///
26    /// Returns `Some(MetaCommand)` if the line starts with `.`,
27    /// or `None` if it is a regular user message.
28    pub fn parse(line: &str) -> Option<Self> {
29        let trimmed = line.trim();
30        if !trimmed.starts_with('.') {
31            return None;
32        }
33
34        let parts: Vec<&str> = trimmed.splitn(2, whitespace_or_end).collect();
35        let cmd = parts[0];
36        let arg = parts
37            .get(1)
38            .map(|s| s.trim().to_string())
39            .filter(|s| !s.is_empty());
40
41        match cmd {
42            ".quit" | ".exit" | ".q" => Some(Self::Quit),
43            ".help" | ".h" | ".?" => Some(Self::Help),
44            ".reset" | ".r" => Some(Self::Reset),
45            ".model" | ".m" => Some(Self::Model(arg)),
46            ".persona" | ".p" => Some(Self::Persona(arg)),
47            ".clear" | ".cls" => Some(Self::Clear),
48            _ => None,
49        }
50    }
51
52    /// Returns the help text shown by `.help`.
53    pub fn help_text() -> &'static str {
54        r#"Oxios CLI — Meta-commands:
55  .quit, .exit, .q   Exit the session
56  .help, .h, .?      Show this help
57  .reset, .r          Reset the current session
58  .model, .m [NAME]   Show or switch the active model
59  .persona, .p [NAME] Show or switch the active persona
60  .clear, .cls        Clear the terminal screen
61"#
62    }
63}
64
65/// Helper: find the first whitespace or end-of-string.
66fn whitespace_or_end(c: char) -> bool {
67    c.is_whitespace()
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn parse_quit() {
76        assert_eq!(MetaCommand::parse(".quit"), Some(MetaCommand::Quit));
77        assert_eq!(MetaCommand::parse(".exit"), Some(MetaCommand::Quit));
78        assert_eq!(MetaCommand::parse(".q"), Some(MetaCommand::Quit));
79    }
80
81    #[test]
82    fn parse_help() {
83        assert_eq!(MetaCommand::parse(".help"), Some(MetaCommand::Help));
84        assert_eq!(MetaCommand::parse(".h"), Some(MetaCommand::Help));
85    }
86
87    #[test]
88    fn parse_model_with_arg() {
89        assert_eq!(
90            MetaCommand::parse(".model gpt-4o"),
91            Some(MetaCommand::Model(Some("gpt-4o".into())))
92        );
93    }
94
95    #[test]
96    fn parse_model_no_arg() {
97        assert_eq!(MetaCommand::parse(".model"), Some(MetaCommand::Model(None)));
98    }
99
100    #[test]
101    fn parse_persona_with_arg() {
102        assert_eq!(
103            MetaCommand::parse(".persona coder"),
104            Some(MetaCommand::Persona(Some("coder".into())))
105        );
106    }
107
108    #[test]
109    fn not_a_command() {
110        assert_eq!(MetaCommand::parse("hello world"), None);
111        assert_eq!(MetaCommand::parse("exit"), None); // no dot prefix
112        assert_eq!(MetaCommand::parse("quit"), None);
113    }
114}