tui_commander/
commander.rs1use 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 #[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}