tui_commander/
commander.rs

1use std::collections::HashMap;
2use std::ops::Deref;
3
4use crate::command::CommandBox;
5use crate::Command;
6
7pub struct Commander<Context> {
8    command_builders: HashMap<&'static str, CommandFuncs<Context>>,
9    command_str: String,
10    search_engine: nucleo_matcher::Matcher,
11}
12
13impl<Context> Commander<Context>
14where
15    Context: crate::Context,
16{
17    pub fn builder() -> CommanderBuilder<Context> {
18        CommanderBuilder {
19            case_sensitive: false,
20            command_builders: HashMap::new(),
21        }
22    }
23
24    pub fn suggestions(&mut self) -> Vec<String> {
25        let all_commands = self.all_command_names();
26
27        if let Some((command, _args)) = self.get_command_args() {
28            let command = command.to_string();
29            self.suggestions_for_command(command, all_commands)
30                .collect::<Vec<String>>()
31        } else {
32            all_commands
33        }
34    }
35
36    pub fn is_unknown_command(&mut self) -> bool {
37        self.suggestions().is_empty()
38    }
39
40    pub fn set_input(&mut self, input: String) {
41        self.command_str = input;
42    }
43
44    /// Alias for Self::set_input(String::new())
45    #[inline]
46    pub fn reset_input(&mut self) {
47        self.set_input(String::new());
48    }
49
50    pub fn execute(&mut self, context: &mut Context) -> Result<(), CommanderError> {
51        let Some((command, args)) = self.get_currently_matching_command_and_args() else {
52            return Err(CommanderError::EmptyCommand);
53        };
54
55        let Some(command_funcs) = self.command_builders.get(command.deref()) else {
56            return Err(CommanderError::UnknownCommand(self.command_str.clone()));
57        };
58
59        let commandbox = (command_funcs.builder)(&command)?;
60        commandbox
61            .0
62            .execute(args, context)
63            .map_err(CommanderError::Command)
64    }
65
66    fn all_command_names(&self) -> Vec<String> {
67        self.command_names()
68            .map(ToString::to_string)
69            .collect::<Vec<String>>()
70    }
71
72    fn suggestions_for_command(
73        &mut self,
74        command: String,
75        all_commands: Vec<String>,
76    ) -> impl Iterator<Item = String> {
77        nucleo_matcher::pattern::Pattern::new(
78            command.as_ref(),
79            nucleo_matcher::pattern::CaseMatching::Ignore,
80            nucleo_matcher::pattern::Normalization::Never,
81            nucleo_matcher::pattern::AtomKind::Fuzzy,
82        )
83        .match_list(all_commands, &mut self.search_engine)
84        .into_iter()
85        .map(|tpl| tpl.0)
86        .map(|s| s.to_string())
87    }
88
89    fn command_names(&self) -> impl Iterator<Item = &str> {
90        self.command_builders.keys().copied()
91    }
92
93    fn find_command_funcs_for_command(
94        &self,
95        command: &str,
96    ) -> Result<&CommandFuncs<Context>, CommanderError> {
97        self.command_builders
98            .get(command)
99            .ok_or_else(|| CommanderError::UnknownCommand(self.command_str.clone()))
100    }
101
102    fn get_command_args(&self) -> Option<(&str, Vec<&str>)> {
103        let mut it = self.command_str.split(' ');
104        let command = it.next()?;
105        let args = it.collect();
106        Some((command, args))
107    }
108
109    fn get_currently_matching_command_and_args(&mut self) -> Option<(String, Vec<String>)> {
110        let (command, args) = self.get_command_args()?;
111        let args = args.into_iter().map(String::from).collect();
112
113        self.suggestions_for_command(command.to_string(), self.all_command_names())
114            .next()
115            .map(|command| (command, args))
116    }
117
118    pub(crate) fn current_args_are_valid(&self) -> Result<bool, CommanderError> {
119        let Some((command, args)) = self.get_command_args() else {
120            return Err(CommanderError::EmptyCommand);
121        };
122
123        let funcs = self.find_command_funcs_for_command(command)?;
124        Ok((funcs.arg_validator)(&args))
125    }
126}
127
128#[derive(Debug, thiserror::Error)]
129pub enum CommanderError {
130    #[error("Command execution errored")]
131    Command(Box<dyn std::error::Error + Send + Sync + 'static>),
132
133    #[error("Empty command string")]
134    EmptyCommand,
135
136    #[error("Unknown command {}", .0)]
137    UnknownCommand(String),
138}
139
140struct CommandFuncs<Context> {
141    builder: CommandBuilderFn<Context>,
142    arg_validator: CommandArgValidatorFn,
143}
144
145type CommandBuilderFn<Context> = Box<dyn Fn(&str) -> Result<CommandBox<Context>, CommanderError>>;
146type CommandArgValidatorFn = Box<dyn Fn(&[&str]) -> bool>;
147
148pub struct CommanderBuilder<Context> {
149    case_sensitive: bool,
150    command_builders: HashMap<&'static str, CommandFuncs<Context>>,
151}
152
153impl<Context> CommanderBuilder<Context> {
154    pub fn with_case_sensitive(mut self, b: bool) -> Self {
155        self.case_sensitive = b;
156        self
157    }
158
159    pub fn with_command<C>(mut self) -> Self
160    where
161        C: Command<Context> + Send + Sync + 'static,
162        Context: 'static,
163    {
164        fn command_builder<C, Context>(input: &str) -> Result<CommandBox<Context>, CommanderError>
165        where
166            C: Command<Context> + Send + Sync + 'static,
167            Context: 'static,
168        {
169            C::build_from_command_name_str(input)
170                .map(|c| CommandBox(Box::new(c) as Box<dyn Command<Context>>))
171                .map_err(CommanderError::Command)
172        }
173
174        fn arg_validator<C, Context>(args: &[&str]) -> bool
175        where
176            C: Command<Context> + Send + Sync + 'static,
177            Context: 'static,
178        {
179            C::args_are_valid(args)
180        }
181
182        self.command_builders.insert(
183            C::name(),
184            CommandFuncs {
185                builder: Box::new(command_builder::<C, Context>),
186                arg_validator: Box::new(arg_validator::<C, Context>),
187            },
188        );
189        self
190    }
191
192    pub fn build(mut self) -> Commander<Context> {
193        self.command_builders.shrink_to_fit();
194        let search_engine = nucleo_matcher::Matcher::new({
195            let mut config = nucleo_matcher::Config::DEFAULT;
196            config.ignore_case = !self.case_sensitive;
197            config.prefer_prefix = true;
198            config
199        });
200
201        Commander {
202            command_builders: self.command_builders,
203            search_engine,
204            command_str: String::new(),
205        }
206    }
207}