tui_lipan/app/input/
command_registry.rs1use std::cell::{Cell, RefCell};
4use std::collections::HashMap;
5use std::fmt;
6use std::rc::Rc;
7use std::sync::Arc;
8
9use crate::app::input::keymap::{Action, Keymap};
10use crate::callback::Callback;
11use crate::callback::ScopeId;
12
13#[derive(Clone, Debug, Eq, Hash, PartialEq)]
15pub struct CommandId(Arc<str>);
16
17impl CommandId {
18 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22}
23
24impl From<&str> for CommandId {
25 fn from(value: &str) -> Self {
26 Self(Arc::from(value))
27 }
28}
29
30impl From<String> for CommandId {
31 fn from(value: String) -> Self {
32 Self(Arc::from(value))
33 }
34}
35
36impl From<Arc<str>> for CommandId {
37 fn from(value: Arc<str>) -> Self {
38 Self(value)
39 }
40}
41
42impl fmt::Display for CommandId {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str(self.as_str())
45 }
46}
47
48#[derive(Clone)]
50pub struct CommandEntry {
51 pub id: CommandId,
53 pub label: Arc<str>,
55 pub description: Option<Arc<str>>,
57 pub category: Option<Arc<str>>,
59 pub keybinding_hint: Option<Arc<str>>,
61 pub enabled: bool,
63 pub(crate) scope: Option<ScopeId>,
64 pub handler: Callback<()>,
66}
67
68impl fmt::Debug for CommandEntry {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.debug_struct("CommandEntry")
71 .field("id", &self.id)
72 .field("label", &self.label)
73 .field("description", &self.description)
74 .field("category", &self.category)
75 .field("keybinding_hint", &self.keybinding_hint)
76 .field("enabled", &self.enabled)
77 .field("scope", &self.scope)
78 .finish()
79 }
80}
81
82impl CommandEntry {
83 pub fn builder(id: impl Into<CommandId>) -> CommandBuilder {
85 CommandBuilder::new(id)
86 }
87}
88
89#[derive(Clone)]
91pub struct CommandBuilder {
92 id: CommandId,
93 label: Arc<str>,
94 description: Option<Arc<str>>,
95 category: Option<Arc<str>>,
96 keybinding_hint: Option<Arc<str>>,
97 enabled: bool,
98 scope: Option<ScopeId>,
99 handler: Callback<()>,
100}
101
102impl CommandBuilder {
103 pub fn new(id: impl Into<CommandId>) -> Self {
105 Self {
106 id: id.into(),
107 label: Arc::from(""),
108 description: None,
109 category: None,
110 keybinding_hint: None,
111 enabled: true,
112 scope: None,
113 handler: Callback::new(|_| {}),
114 }
115 }
116
117 pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
119 self.label = label.into();
120 self
121 }
122
123 pub fn description(mut self, description: impl Into<Arc<str>>) -> Self {
125 self.description = Some(description.into());
126 self
127 }
128
129 pub fn category(mut self, category: impl Into<Arc<str>>) -> Self {
131 self.category = Some(category.into());
132 self
133 }
134
135 pub fn keybinding(mut self, hint: impl Into<Arc<str>>) -> Self {
137 self.keybinding_hint = Some(hint.into());
138 self
139 }
140
141 pub fn keybinding_hint_opt(mut self, hint: Option<Arc<str>>) -> Self {
143 self.keybinding_hint = hint;
144 self
145 }
146
147 pub fn keybinding_from_keymap(mut self, keymap: &Keymap, action: Action) -> Self {
149 self.keybinding_hint = keymap
150 .binding_for_action(action)
151 .map(|binding| Arc::<str>::from(binding.canonical_lowercase()));
152 self
153 }
154
155 pub fn enabled(mut self, enabled: bool) -> Self {
157 self.enabled = enabled;
158 self
159 }
160
161 pub fn handler(mut self, handler: Callback<()>) -> Self {
163 self.handler = handler;
164 self
165 }
166
167 pub fn build(self) -> CommandEntry {
169 CommandEntry {
170 id: self.id,
171 label: self.label,
172 description: self.description,
173 category: self.category,
174 keybinding_hint: self.keybinding_hint,
175 enabled: self.enabled,
176 scope: self.scope,
177 handler: self.handler,
178 }
179 }
180}
181
182#[derive(Clone, Default)]
184pub struct CommandRegistry {
185 entries: Rc<RefCell<HashMap<CommandId, CommandEntry>>>,
186 generation: Rc<Cell<u64>>,
187}
188
189impl CommandRegistry {
190 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn register(&self, entry: CommandEntry) {
197 self.entries.borrow_mut().insert(entry.id.clone(), entry);
198 self.bump_generation();
199 }
200
201 pub(crate) fn register_for_scope(&self, scope: ScopeId, entry: CommandEntry) {
202 self.register(CommandEntry {
203 scope: Some(scope),
204 ..entry
205 });
206 }
207
208 pub fn unregister(&self, id: impl Into<CommandId>) -> Option<CommandEntry> {
210 let removed = self.entries.borrow_mut().remove(&id.into());
211 if removed.is_some() {
212 self.bump_generation();
213 }
214 removed
215 }
216
217 pub fn set_enabled(&self, id: impl Into<CommandId>, enabled: bool) -> bool {
219 let id = id.into();
220 let mut entries = self.entries.borrow_mut();
221 let Some(entry) = entries.get_mut(&id) else {
222 return false;
223 };
224 if entry.enabled != enabled {
225 entry.enabled = enabled;
226 self.bump_generation();
227 }
228 true
229 }
230
231 pub fn execute(&self, id: impl Into<CommandId>) -> bool {
233 let id = id.into();
234 let to_run = {
235 let entries = self.entries.borrow();
236 let Some(entry) = entries.get(&id) else {
237 return false;
238 };
239 if !entry.enabled {
240 return false;
241 }
242 entry.handler.clone()
243 };
244 to_run.emit(());
245 true
246 }
247
248 pub fn entries(&self) -> Vec<CommandEntry> {
250 self.entries.borrow().values().cloned().collect()
251 }
252
253 pub(crate) fn unregister_scope(&self, scope: ScopeId) {
254 let before = self.entries.borrow().len();
255 self.entries
256 .borrow_mut()
257 .retain(|_, entry| entry.scope != Some(scope));
258 if self.entries.borrow().len() != before {
259 self.bump_generation();
260 }
261 }
262
263 pub fn generation(&self) -> u64 {
265 self.generation.get()
266 }
267
268 fn bump_generation(&self) {
269 self.generation
270 .set(self.generation.get().wrapping_add(1).max(1));
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use std::cell::Cell;
277 use std::rc::Rc;
278
279 use super::{CommandBuilder, CommandRegistry};
280 use crate::callback::Callback;
281
282 #[test]
283 fn register_replaces_existing_entry_by_id() {
284 let registry = CommandRegistry::new();
285 let hit_a = Rc::new(Cell::new(false));
286 let hit_b = Rc::new(Cell::new(false));
287
288 let hit_a_cb = Rc::clone(&hit_a);
289 registry.register(
290 CommandBuilder::new("app.test")
291 .label("A")
292 .handler(Callback::new(move |_| hit_a_cb.set(true)))
293 .build(),
294 );
295
296 let hit_b_cb = Rc::clone(&hit_b);
297 registry.register(
298 CommandBuilder::new("app.test")
299 .label("B")
300 .handler(Callback::new(move |_| hit_b_cb.set(true)))
301 .build(),
302 );
303
304 assert_eq!(registry.entries().len(), 1);
305 assert!(registry.execute("app.test"));
306 assert!(!hit_a.get());
307 assert!(hit_b.get());
308 }
309
310 #[test]
311 fn generation_increments_on_mutations() {
312 let registry = CommandRegistry::new();
313 assert_eq!(registry.generation(), 0);
314
315 registry.register(CommandBuilder::new("a").label("A").build());
316 let g1 = registry.generation();
317 assert!(g1 > 0);
318
319 assert!(registry.set_enabled("a", false));
320 let g2 = registry.generation();
321 assert!(g2 > g1);
322
323 assert!(registry.unregister("a").is_some());
324 let g3 = registry.generation();
325 assert!(g3 > g2);
326 }
327
328 #[test]
329 fn execute_runs_handler_when_enabled() {
330 let registry = CommandRegistry::new();
331 let called = Rc::new(Cell::new(false));
332 let called_cb = Rc::clone(&called);
333
334 registry.register(
335 CommandBuilder::new("run")
336 .label("Run")
337 .handler(Callback::new(move |_| called_cb.set(true)))
338 .build(),
339 );
340
341 assert!(registry.execute("run"));
342 assert!(called.get());
343 }
344
345 #[test]
346 fn disabled_command_does_not_execute_until_enabled() {
347 let registry = CommandRegistry::new();
348 let called = Rc::new(Cell::new(false));
349 let called_cb = Rc::clone(&called);
350
351 registry.register(
352 CommandBuilder::new("toggle")
353 .label("Toggle")
354 .enabled(false)
355 .handler(Callback::new(move |_| called_cb.set(true)))
356 .build(),
357 );
358
359 assert!(!registry.execute("toggle"));
360 assert!(!called.get());
361
362 assert!(registry.set_enabled("toggle", true));
363 assert!(registry.execute("toggle"));
364 assert!(called.get());
365 }
366}