1use std::cell::{Cell, RefCell};
4use std::collections::HashMap;
5use std::fmt;
6use std::rc::Rc;
7use std::sync::Arc;
8
9use crate::app::input::key_dispatch::CommandConflictPolicy;
10use crate::app::input::keymap::{Action, Keymap};
11use crate::callback::Callback;
12use crate::callback::ScopeId;
13use crate::core::event::KeyEvent;
14use crate::input::{ChordMatcher, ChordResult, KeyBinding, KeyBindings};
15
16#[derive(Clone, Debug, Eq, Hash, PartialEq)]
18pub struct CommandId(Arc<str>);
19
20impl CommandId {
21 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25}
26
27impl From<&str> for CommandId {
28 fn from(value: &str) -> Self {
29 Self(Arc::from(value))
30 }
31}
32
33impl From<String> for CommandId {
34 fn from(value: String) -> Self {
35 Self(Arc::from(value))
36 }
37}
38
39impl From<Arc<str>> for CommandId {
40 fn from(value: Arc<str>) -> Self {
41 Self(value)
42 }
43}
44
45impl fmt::Display for CommandId {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 f.write_str(self.as_str())
48 }
49}
50
51#[derive(Clone)]
53pub struct CommandEntry {
54 pub id: CommandId,
56 pub label: Arc<str>,
58 pub description: Option<Arc<str>>,
60 pub category: Option<Arc<str>>,
62 pub keybinding_hint: Option<Arc<str>>,
64 pub shortcuts: KeyBindings,
66 pub priority: i32,
68 pub enabled: bool,
70 pub(crate) scope: Option<ScopeId>,
71 pub handler: Callback<()>,
73}
74
75impl fmt::Debug for CommandEntry {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.debug_struct("CommandEntry")
78 .field("id", &self.id)
79 .field("label", &self.label)
80 .field("description", &self.description)
81 .field("category", &self.category)
82 .field("keybinding_hint", &self.keybinding_hint)
83 .field("shortcuts", &self.shortcuts)
84 .field("priority", &self.priority)
85 .field("enabled", &self.enabled)
86 .field("scope", &self.scope)
87 .finish()
88 }
89}
90
91impl CommandEntry {
92 pub fn builder(id: impl Into<CommandId>) -> CommandBuilder {
94 CommandBuilder::new(id)
95 }
96}
97
98#[derive(Clone)]
100pub struct CommandBuilder {
101 id: CommandId,
102 label: Arc<str>,
103 description: Option<Arc<str>>,
104 category: Option<Arc<str>>,
105 keybinding_hint: Option<Arc<str>>,
106 shortcut_bindings: Vec<KeyBinding>,
107 priority: i32,
108 enabled: bool,
109 scope: Option<ScopeId>,
110 handler: Callback<()>,
111}
112
113impl CommandBuilder {
114 pub fn new(id: impl Into<CommandId>) -> Self {
116 Self {
117 id: id.into(),
118 label: Arc::from(""),
119 description: None,
120 category: None,
121 keybinding_hint: None,
122 shortcut_bindings: Vec::new(),
123 priority: 0,
124 enabled: true,
125 scope: None,
126 handler: Callback::new(|_| {}),
127 }
128 }
129
130 pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
132 self.label = label.into();
133 self
134 }
135
136 pub fn description(mut self, description: impl Into<Arc<str>>) -> Self {
138 self.description = Some(description.into());
139 self
140 }
141
142 pub fn category(mut self, category: impl Into<Arc<str>>) -> Self {
144 self.category = Some(category.into());
145 self
146 }
147
148 pub fn keybinding_hint(mut self, hint: impl Into<Arc<str>>) -> Self {
150 self.keybinding_hint = Some(hint.into());
151 self
152 }
153
154 pub fn keybinding_hint_opt(mut self, hint: Option<Arc<str>>) -> Self {
156 self.keybinding_hint = hint;
157 self
158 }
159
160 pub fn keybinding_from_keymap(mut self, keymap: &Keymap, action: Action) -> Self {
162 self.keybinding_hint = keymap
163 .binding_for_action(action)
164 .map(|binding| Arc::<str>::from(binding.canonical_lowercase()));
165 self
166 }
167
168 pub fn shortcut(mut self, binding: KeyBinding) -> Self {
170 self.shortcut_bindings.push(binding);
171 self
172 }
173
174 pub fn shortcuts(mut self, bindings: KeyBindings) -> Self {
176 self.shortcut_bindings.extend(bindings.iter().cloned());
177 self
178 }
179
180 pub fn priority(mut self, priority: i32) -> Self {
182 self.priority = priority;
183 self
184 }
185
186 pub fn enabled(mut self, enabled: bool) -> Self {
188 self.enabled = enabled;
189 self
190 }
191
192 pub fn handler(mut self, handler: Callback<()>) -> Self {
194 self.handler = handler;
195 self
196 }
197
198 pub fn build(self) -> CommandEntry {
200 CommandEntry {
201 id: self.id,
202 label: self.label,
203 description: self.description,
204 category: self.category,
205 keybinding_hint: self.keybinding_hint,
206 shortcuts: KeyBindings::from_bindings(self.shortcut_bindings),
207 priority: self.priority,
208 enabled: self.enabled,
209 scope: self.scope,
210 handler: self.handler,
211 }
212 }
213}
214
215#[derive(Clone, Default)]
217pub struct CommandRegistry {
218 entries: Rc<RefCell<HashMap<CommandId, CommandEntry>>>,
219 order: Rc<RefCell<Vec<CommandId>>>,
220 generation: Rc<Cell<u64>>,
221}
222
223impl CommandRegistry {
224 pub fn new() -> Self {
226 Self::default()
227 }
228
229 pub fn register(&self, entry: CommandEntry) {
231 let id = entry.id.clone();
232 let mut entries = self.entries.borrow_mut();
233 let mut order = self.order.borrow_mut();
234 let replacing = entries.contains_key(&id);
235 entries.insert(id.clone(), entry);
236 if !replacing {
237 order.push(id);
238 }
239 drop(entries);
240 drop(order);
241 self.bump_generation();
242 }
243
244 pub(crate) fn register_for_scope(&self, scope: ScopeId, entry: CommandEntry) {
245 self.register(CommandEntry {
246 scope: Some(scope),
247 ..entry
248 });
249 }
250
251 pub fn unregister(&self, id: impl Into<CommandId>) -> Option<CommandEntry> {
253 let id = id.into();
254 let removed = self.entries.borrow_mut().remove(&id);
255 if removed.is_some() {
256 self.order.borrow_mut().retain(|existing| existing != &id);
257 self.bump_generation();
258 }
259 removed
260 }
261
262 pub fn set_enabled(&self, id: impl Into<CommandId>, enabled: bool) -> bool {
264 let id = id.into();
265 let mut entries = self.entries.borrow_mut();
266 let Some(entry) = entries.get_mut(&id) else {
267 return false;
268 };
269 if entry.enabled != enabled {
270 entry.enabled = enabled;
271 self.bump_generation();
272 }
273 true
274 }
275
276 pub fn execute(&self, id: impl Into<CommandId>) -> bool {
278 let id = id.into();
279 let to_run = {
280 let entries = self.entries.borrow();
281 let Some(entry) = entries.get(&id) else {
282 return false;
283 };
284 if !entry.enabled {
285 return false;
286 }
287 entry.handler.clone()
288 };
289 to_run.emit(());
290 true
291 }
292
293 pub fn matching_enabled_shortcuts(
295 &self,
296 key: KeyEvent,
297 policy: CommandConflictPolicy,
298 ) -> Vec<CommandId> {
299 let entries = self.entries.borrow();
300 let order = self.order.borrow();
301 let mut matches = Vec::new();
302
303 for id in order.iter() {
304 let Some(entry) = entries.get(id) else {
305 continue;
306 };
307 if !entry.enabled {
308 continue;
309 }
310 if entry
311 .shortcuts
312 .iter()
313 .any(|binding| binding.step_count() == 1 && binding.matches_sequence(&[key]))
314 {
315 matches.push(id.clone());
316 }
317 }
318
319 resolve_shortcut_conflicts(matches, &entries, policy)
320 }
321
322 pub(crate) fn shortcut_entries(&self) -> Vec<(KeyBinding, CommandId)> {
323 let entries = self.entries.borrow();
324 let order = self.order.borrow();
325 let mut out = Vec::new();
326 for id in order.iter() {
327 let Some(entry) = entries.get(id) else {
328 continue;
329 };
330 if !entry.enabled {
331 continue;
332 }
333 for binding in entry.shortcuts.iter().cloned() {
334 out.push((binding, id.clone()));
335 }
336 }
337 out
338 }
339
340 pub(crate) fn ordered_entries(&self) -> Vec<CommandEntry> {
341 let entries = self.entries.borrow();
342 self.order
343 .borrow()
344 .iter()
345 .filter_map(|id| entries.get(id).cloned())
346 .collect()
347 }
348
349 pub fn entries(&self) -> Vec<CommandEntry> {
351 self.ordered_entries()
352 }
353
354 pub(crate) fn unregister_scope(&self, scope: ScopeId) {
355 let before = self.entries.borrow().len();
356 self.entries
357 .borrow_mut()
358 .retain(|_, entry| entry.scope != Some(scope));
359 self.order
360 .borrow_mut()
361 .retain(|id| self.entries.borrow().contains_key(id));
362 if self.entries.borrow().len() != before {
363 self.bump_generation();
364 }
365 }
366
367 pub fn generation(&self) -> u64 {
369 self.generation.get()
370 }
371
372 fn bump_generation(&self) {
373 self.generation
374 .set(self.generation.get().wrapping_add(1).max(1));
375 }
376}
377
378fn resolve_shortcut_conflicts(
379 matches: Vec<CommandId>,
380 entries: &HashMap<CommandId, CommandEntry>,
381 policy: CommandConflictPolicy,
382) -> Vec<CommandId> {
383 if matches.len() <= 1 {
384 return matches;
385 }
386
387 match policy {
388 CommandConflictPolicy::FirstRegistered => vec![matches[0].clone()],
389 CommandConflictPolicy::HighestPriority => {
390 let winner = matches.into_iter().max_by(|left, right| {
391 let left_priority = entries.get(left).map(|entry| entry.priority).unwrap_or(0);
392 let right_priority = entries.get(right).map(|entry| entry.priority).unwrap_or(0);
393 left_priority.cmp(&right_priority)
394 });
395 winner.into_iter().collect()
396 }
397 }
398}
399
400#[derive(Clone, Debug, Eq, PartialEq)]
402pub(crate) enum CommandShortcutResult {
403 None,
404 Pending,
405 Matched(CommandId),
406 Mismatch,
407}
408
409pub(crate) struct CommandShortcutRuntime {
411 matcher: ChordMatcher<CommandId>,
412 registry_generation: u64,
413 conflict_policy: CommandConflictPolicy,
414}
415
416impl CommandShortcutRuntime {
417 pub(crate) fn new(registry: &CommandRegistry, conflict_policy: CommandConflictPolicy) -> Self {
418 Self {
419 matcher: ChordMatcher::new(registry.shortcut_entries()),
420 registry_generation: registry.generation(),
421 conflict_policy,
422 }
423 }
424
425 pub(crate) fn sync_registry(&mut self, registry: &CommandRegistry) {
426 let generation = registry.generation();
427 if self.registry_generation == generation {
428 return;
429 }
430 self.matcher = ChordMatcher::new(registry.shortcut_entries());
431 self.registry_generation = generation;
432 self.matcher.reset();
433 }
434
435 pub(crate) fn reset(&mut self) {
436 self.matcher.reset();
437 }
438
439 pub(crate) fn feed(
440 &mut self,
441 key: KeyEvent,
442 registry: &CommandRegistry,
443 ) -> CommandShortcutResult {
444 self.sync_registry(registry);
445 let was_pending = self.matcher.is_pending();
446 match self.matcher.feed(&key) {
447 ChordResult::None => {
448 if was_pending {
449 CommandShortcutResult::Mismatch
450 } else {
451 CommandShortcutResult::None
452 }
453 }
454 ChordResult::Pending => CommandShortcutResult::Pending,
455 ChordResult::Matched(id) => {
456 if !was_pending {
457 let resolved = registry.matching_enabled_shortcuts(key, self.conflict_policy);
458 return resolved
459 .into_iter()
460 .next()
461 .map(CommandShortcutResult::Matched)
462 .unwrap_or(CommandShortcutResult::None);
463 }
464 let resolved = resolve_shortcut_conflicts(
465 vec![(*id).clone()],
466 ®istry.entries.borrow(),
467 self.conflict_policy,
468 );
469 if let Some(winner) = resolved.into_iter().next() {
470 CommandShortcutResult::Matched(winner)
471 } else {
472 CommandShortcutResult::None
473 }
474 }
475 }
476 }
477
478 pub(crate) fn is_pending(&self) -> bool {
479 self.matcher.is_pending()
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use std::cell::Cell;
486 use std::rc::Rc;
487 use std::str::FromStr;
488
489 use super::{
490 CommandBuilder, CommandId, CommandRegistry, CommandShortcutResult, CommandShortcutRuntime,
491 };
492 use crate::app::input::key_dispatch::CommandConflictPolicy;
493 use crate::callback::Callback;
494 use crate::core::event::{KeyCode, KeyEvent, KeyMods};
495 use crate::input::KeyBinding;
496
497 fn ctrl_key_for_test(ch: char) -> KeyEvent {
498 KeyEvent {
499 code: KeyCode::Char(ch),
500 mods: KeyMods {
501 ctrl: true,
502 ..KeyMods::default()
503 },
504 }
505 }
506
507 fn key_event_for_test(ch: char) -> KeyEvent {
508 KeyEvent {
509 code: KeyCode::Char(ch),
510 mods: KeyMods::default(),
511 }
512 }
513
514 #[test]
515 fn register_replaces_existing_entry_by_id() {
516 let registry = CommandRegistry::new();
517 let hit_a = Rc::new(Cell::new(false));
518 let hit_b = Rc::new(Cell::new(false));
519
520 let hit_a_cb = Rc::clone(&hit_a);
521 registry.register(
522 CommandBuilder::new("app.test")
523 .label("A")
524 .handler(Callback::new(move |_| hit_a_cb.set(true)))
525 .build(),
526 );
527
528 let hit_b_cb = Rc::clone(&hit_b);
529 registry.register(
530 CommandBuilder::new("app.test")
531 .label("B")
532 .handler(Callback::new(move |_| hit_b_cb.set(true)))
533 .build(),
534 );
535
536 assert_eq!(registry.entries().len(), 1);
537 assert!(registry.execute("app.test"));
538 assert!(!hit_a.get());
539 assert!(hit_b.get());
540 }
541
542 #[test]
543 fn generation_increments_on_mutations() {
544 let registry = CommandRegistry::new();
545 assert_eq!(registry.generation(), 0);
546
547 registry.register(CommandBuilder::new("a").label("A").build());
548 let g1 = registry.generation();
549 assert!(g1 > 0);
550
551 assert!(registry.set_enabled("a", false));
552 let g2 = registry.generation();
553 assert!(g2 > g1);
554
555 assert!(registry.unregister("a").is_some());
556 let g3 = registry.generation();
557 assert!(g3 > g2);
558 }
559
560 #[test]
561 fn execute_runs_handler_when_enabled() {
562 let registry = CommandRegistry::new();
563 let called = Rc::new(Cell::new(false));
564 let called_cb = Rc::clone(&called);
565
566 registry.register(
567 CommandBuilder::new("run")
568 .label("Run")
569 .handler(Callback::new(move |_| called_cb.set(true)))
570 .build(),
571 );
572
573 assert!(registry.execute("run"));
574 assert!(called.get());
575 }
576
577 #[test]
578 fn disabled_command_does_not_execute_until_enabled() {
579 let registry = CommandRegistry::new();
580 let called = Rc::new(Cell::new(false));
581 let called_cb = Rc::clone(&called);
582
583 registry.register(
584 CommandBuilder::new("toggle")
585 .label("Toggle")
586 .enabled(false)
587 .handler(Callback::new(move |_| called_cb.set(true)))
588 .build(),
589 );
590
591 assert!(!registry.execute("toggle"));
592 assert!(!called.get());
593
594 assert!(registry.set_enabled("toggle", true));
595 assert!(registry.execute("toggle"));
596 assert!(called.get());
597 }
598
599 #[test]
600 fn command_shortcut_conflict_is_stable_first_registered_by_default() {
601 let registry = CommandRegistry::new();
602 registry.register(
603 CommandBuilder::new("first")
604 .shortcut(KeyBinding::from_str("ctrl-k").unwrap())
605 .build(),
606 );
607 registry.register(
608 CommandBuilder::new("second")
609 .shortcut(KeyBinding::from_str("ctrl-k").unwrap())
610 .build(),
611 );
612
613 let matches = registry.matching_enabled_shortcuts(
614 ctrl_key_for_test('k'),
615 CommandConflictPolicy::FirstRegistered,
616 );
617 assert_eq!(matches, vec![CommandId::from("first")]);
618 }
619
620 #[test]
621 fn command_shortcut_conflict_can_use_highest_priority() {
622 let registry = CommandRegistry::new();
623 registry.register(
624 CommandBuilder::new("low")
625 .priority(0)
626 .shortcut(KeyBinding::from_str("ctrl-k").unwrap())
627 .build(),
628 );
629 registry.register(
630 CommandBuilder::new("high")
631 .priority(10)
632 .shortcut(KeyBinding::from_str("ctrl-k").unwrap())
633 .build(),
634 );
635
636 let matches = registry.matching_enabled_shortcuts(
637 ctrl_key_for_test('k'),
638 CommandConflictPolicy::HighestPriority,
639 );
640 assert_eq!(matches, vec![CommandId::from("high")]);
641 }
642
643 #[test]
644 fn command_shortcut_runtime_supports_chords_and_resets_on_generation_change() {
645 let registry = CommandRegistry::new();
646 registry.register(
647 CommandBuilder::new("mux.detach")
648 .shortcut(KeyBinding::from_str("ctrl-a d").unwrap())
649 .build(),
650 );
651 let mut runtime =
652 CommandShortcutRuntime::new(®istry, CommandConflictPolicy::FirstRegistered);
653 assert!(matches!(
654 runtime.feed(ctrl_key_for_test('a'), ®istry),
655 CommandShortcutResult::Pending
656 ));
657 registry.unregister("mux.detach");
658 assert!(matches!(
659 runtime.feed(key_event_for_test('d'), ®istry),
660 CommandShortcutResult::None
661 ));
662 }
663}