Skip to main content

tui_lipan/app/input/
command_registry.rs

1//! Runtime command registry used by command palette style UIs.
2
3use 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/// Stable identifier for a registered command.
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
15pub struct CommandId(Arc<str>);
16
17impl CommandId {
18    /// Borrow this id as `&str`.
19    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/// One command entry in the runtime registry.
49#[derive(Clone)]
50pub struct CommandEntry {
51    /// Stable command id.
52    pub id: CommandId,
53    /// Label shown in command palettes.
54    pub label: Arc<str>,
55    /// Optional longer description.
56    pub description: Option<Arc<str>>,
57    /// Optional category/group label.
58    pub category: Option<Arc<str>>,
59    /// Optional keybinding hint shown on the right.
60    pub keybinding_hint: Option<Arc<str>>,
61    /// Whether this command is currently actionable.
62    pub enabled: bool,
63    pub(crate) scope: Option<ScopeId>,
64    /// Callback executed when command is run.
65    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    /// Create a command entry builder.
84    pub fn builder(id: impl Into<CommandId>) -> CommandBuilder {
85        CommandBuilder::new(id)
86    }
87}
88
89/// Builder for [`CommandEntry`].
90#[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    /// Create a builder with required id.
104    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    /// Set command label.
118    pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
119        self.label = label.into();
120        self
121    }
122
123    /// Set optional description.
124    pub fn description(mut self, description: impl Into<Arc<str>>) -> Self {
125        self.description = Some(description.into());
126        self
127    }
128
129    /// Set optional category.
130    pub fn category(mut self, category: impl Into<Arc<str>>) -> Self {
131        self.category = Some(category.into());
132        self
133    }
134
135    /// Set optional keybinding hint text.
136    pub fn keybinding(mut self, hint: impl Into<Arc<str>>) -> Self {
137        self.keybinding_hint = Some(hint.into());
138        self
139    }
140
141    /// Set optional keybinding hint text directly.
142    pub fn keybinding_hint_opt(mut self, hint: Option<Arc<str>>) -> Self {
143        self.keybinding_hint = hint;
144        self
145    }
146
147    /// Set keybinding hint derived from the configured keymap for an action.
148    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    /// Set initial enabled state.
156    pub fn enabled(mut self, enabled: bool) -> Self {
157        self.enabled = enabled;
158        self
159    }
160
161    /// Set command handler callback.
162    pub fn handler(mut self, handler: Callback<()>) -> Self {
163        self.handler = handler;
164        self
165    }
166
167    /// Build the final command entry.
168    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/// Shared runtime command registry.
183#[derive(Clone, Default)]
184pub struct CommandRegistry {
185    entries: Rc<RefCell<HashMap<CommandId, CommandEntry>>>,
186    generation: Rc<Cell<u64>>,
187}
188
189impl CommandRegistry {
190    /// Create an empty registry.
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Register a command (replaces same id).
196    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    /// Remove a command by id.
209    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    /// Enable/disable a registered command.
218    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    /// Execute a command by id when present and enabled.
232    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    /// Snapshot all registered entries.
249    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    /// Monotonic generation used by listeners to detect updates.
264    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}