1#![allow(dead_code)]
2
3use std::sync::Arc;
4
5use crate::core::event::KeyEvent;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub(crate) enum FocusKind {
9 Widget,
10 Terminal,
11}
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum KeyDispatchPolicy {
16 WidgetFirst,
18 AppCommandsFirst,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum TerminalKeyPolicy {
25 FrameworkFirst,
27 AppCommandsThenTerminal,
29 TerminalFirst,
31 TerminalOnly,
33}
34
35#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
37pub enum CommandConflictPolicy {
38 #[default]
40 FirstRegistered,
41 HighestPriority,
43}
44
45#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
47pub enum ChordMismatchPolicy {
48 #[default]
50 SwallowPrefixReplayCurrent,
51 ForwardPrefixAndCurrent,
53 CancelOnly,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub(crate) enum CommandDispatchState {
59 None,
60 Pending,
61 Mismatch,
62 Matched(Arc<str>),
63}
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub(crate) enum FrameworkDispatch {
67 None,
68 Handled,
69 Quit,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub(crate) enum DispatchOutcome {
74 Unhandled,
75 Widget,
76 Bubble,
77 Command,
78 CommandPending,
79 Framework,
80 FrameworkQuit,
81 TerminalPreflight,
82 Terminal,
83 AmbientScroll,
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub(crate) struct DispatchRequest {
88 key: KeyEvent,
89 focus: FocusKind,
90 key_policy: KeyDispatchPolicy,
91 terminal_policy: TerminalKeyPolicy,
92 chord_mismatch_policy: ChordMismatchPolicy,
93}
94
95impl DispatchRequest {
96 pub(crate) fn new(key: KeyEvent, focus: FocusKind) -> Self {
97 Self {
98 key,
99 focus,
100 key_policy: KeyDispatchPolicy::WidgetFirst,
101 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
102 chord_mismatch_policy: ChordMismatchPolicy::default(),
103 }
104 }
105
106 pub(crate) fn key_policy(mut self, policy: KeyDispatchPolicy) -> Self {
107 self.key_policy = policy;
108 self
109 }
110
111 pub(crate) fn terminal_policy(mut self, policy: TerminalKeyPolicy) -> Self {
112 self.terminal_policy = policy;
113 self
114 }
115
116 pub(crate) fn chord_mismatch_policy(mut self, policy: ChordMismatchPolicy) -> Self {
117 self.chord_mismatch_policy = policy;
118 self
119 }
120}
121
122pub(crate) trait DispatchOps {
123 fn continue_command_chord(&mut self, key: KeyEvent) -> CommandDispatchState;
124 fn reset_command_chord(&mut self);
125 fn dispatch_widget(&mut self, key: KeyEvent) -> bool;
126 fn dispatch_bubble(&mut self, key: KeyEvent) -> bool;
127 fn dispatch_command(&mut self, key: KeyEvent) -> bool;
128 fn dispatch_framework(&mut self, key: KeyEvent) -> FrameworkDispatch;
129 fn dispatch_terminal_preflight(&mut self, key: KeyEvent) -> TerminalPreflightDispatch;
130 fn dispatch_terminal(&mut self, key: KeyEvent) -> bool;
131 fn dispatch_ambient_scroll(&mut self, key: KeyEvent) -> bool;
132}
133
134pub(crate) fn dispatch_key(
135 request: DispatchRequest,
136 ops: &mut impl DispatchOps,
137) -> DispatchOutcome {
138 match request.focus {
139 FocusKind::Widget => dispatch_widget_focus(request, ops),
140 FocusKind::Terminal => dispatch_terminal_focus(request, ops),
141 }
142}
143
144fn dispatch_widget_focus(request: DispatchRequest, ops: &mut impl DispatchOps) -> DispatchOutcome {
145 if let Some(outcome) = handle_command_chord(request, ops) {
146 return outcome;
147 }
148
149 match request.key_policy {
150 KeyDispatchPolicy::WidgetFirst => {
151 dispatch_widget_bubble_command_framework(request.key, ops)
152 }
153 KeyDispatchPolicy::AppCommandsFirst => {
154 if ops.dispatch_command(request.key) {
155 DispatchOutcome::Command
156 } else {
157 dispatch_widget_bubble_framework(request.key, ops)
158 }
159 }
160 }
161}
162
163fn handle_command_chord(
164 request: DispatchRequest,
165 ops: &mut impl DispatchOps,
166) -> Option<DispatchOutcome> {
167 match ops.continue_command_chord(request.key) {
168 CommandDispatchState::Pending => Some(DispatchOutcome::CommandPending),
169 CommandDispatchState::Matched(_) => Some(DispatchOutcome::Command),
170 CommandDispatchState::Mismatch => match request.chord_mismatch_policy {
171 ChordMismatchPolicy::SwallowPrefixReplayCurrent
172 | ChordMismatchPolicy::ForwardPrefixAndCurrent => None,
173 ChordMismatchPolicy::CancelOnly => Some(DispatchOutcome::Unhandled),
174 },
175 CommandDispatchState::None => None,
176 }
177}
178
179fn dispatch_terminal_focus(
180 request: DispatchRequest,
181 ops: &mut impl DispatchOps,
182) -> DispatchOutcome {
183 match request.terminal_policy {
184 TerminalKeyPolicy::FrameworkFirst => match ops.dispatch_framework(request.key) {
185 FrameworkDispatch::Handled => DispatchOutcome::Framework,
186 FrameworkDispatch::Quit => DispatchOutcome::FrameworkQuit,
187 FrameworkDispatch::None => dispatch_terminal_forward(request.key, ops),
188 },
189 TerminalKeyPolicy::AppCommandsThenTerminal => {
190 match ops.dispatch_terminal_preflight(request.key) {
191 TerminalPreflightDispatch::Consumed => {
192 ops.reset_command_chord();
193 return DispatchOutcome::TerminalPreflight;
194 }
195 TerminalPreflightDispatch::Forward => {
196 return dispatch_terminal_forward(request.key, ops);
197 }
198 TerminalPreflightDispatch::NotApplicable
199 | TerminalPreflightDispatch::NotConsumed => {}
200 }
201 if let Some(outcome) = handle_command_chord(request, ops) {
202 return outcome;
203 }
204 if ops.dispatch_command(request.key) {
205 return DispatchOutcome::Command;
206 }
207 if let Some(outcome) = dispatch_terminal_then_bubble_framework(request.key, ops) {
208 return outcome;
209 }
210 DispatchOutcome::Unhandled
211 }
212 TerminalKeyPolicy::TerminalFirst => {
213 match ops.dispatch_terminal_preflight(request.key) {
214 TerminalPreflightDispatch::Consumed => {
215 ops.reset_command_chord();
216 return DispatchOutcome::TerminalPreflight;
217 }
218 TerminalPreflightDispatch::Forward => {
219 return dispatch_terminal_forward(request.key, ops);
220 }
221 TerminalPreflightDispatch::NotApplicable
222 | TerminalPreflightDispatch::NotConsumed => {}
223 }
224 if ops.dispatch_terminal(request.key) {
225 return DispatchOutcome::Terminal;
226 }
227 if ops.dispatch_command(request.key) {
228 return DispatchOutcome::Command;
229 }
230 if ops.dispatch_bubble(request.key) {
231 return DispatchOutcome::Bubble;
232 }
233 dispatch_framework_only(request.key, ops)
234 }
235 TerminalKeyPolicy::TerminalOnly => match ops.dispatch_terminal_preflight(request.key) {
236 TerminalPreflightDispatch::Consumed => {
237 ops.reset_command_chord();
238 DispatchOutcome::TerminalPreflight
239 }
240 TerminalPreflightDispatch::Forward => dispatch_terminal_forward(request.key, ops),
241 TerminalPreflightDispatch::NotApplicable | TerminalPreflightDispatch::NotConsumed => {
242 dispatch_terminal_forward(request.key, ops)
243 }
244 },
245 }
246}
247
248fn dispatch_widget_bubble_command_framework(
249 key: KeyEvent,
250 ops: &mut impl DispatchOps,
251) -> DispatchOutcome {
252 if ops.dispatch_widget(key) {
253 DispatchOutcome::Widget
254 } else if ops.dispatch_bubble(key) {
255 DispatchOutcome::Bubble
256 } else if ops.dispatch_command(key) {
257 DispatchOutcome::Command
258 } else {
259 dispatch_framework_ambient(key, ops)
260 }
261}
262
263fn dispatch_widget_bubble_framework(key: KeyEvent, ops: &mut impl DispatchOps) -> DispatchOutcome {
264 if ops.dispatch_widget(key) {
265 DispatchOutcome::Widget
266 } else if ops.dispatch_bubble(key) {
267 DispatchOutcome::Bubble
268 } else {
269 dispatch_framework_ambient(key, ops)
270 }
271}
272
273fn dispatch_framework_ambient(key: KeyEvent, ops: &mut impl DispatchOps) -> DispatchOutcome {
274 match ops.dispatch_framework(key) {
275 FrameworkDispatch::Handled => DispatchOutcome::Framework,
276 FrameworkDispatch::Quit => DispatchOutcome::FrameworkQuit,
277 FrameworkDispatch::None if ops.dispatch_ambient_scroll(key) => {
278 DispatchOutcome::AmbientScroll
279 }
280 FrameworkDispatch::None => DispatchOutcome::Unhandled,
281 }
282}
283
284fn dispatch_terminal_forward(key: KeyEvent, ops: &mut impl DispatchOps) -> DispatchOutcome {
285 if ops.dispatch_terminal(key) {
286 DispatchOutcome::Terminal
287 } else {
288 DispatchOutcome::Unhandled
289 }
290}
291
292fn dispatch_terminal_then_bubble_framework(
293 key: KeyEvent,
294 ops: &mut impl DispatchOps,
295) -> Option<DispatchOutcome> {
296 if ops.dispatch_terminal(key) {
297 return Some(DispatchOutcome::Terminal);
298 }
299 if ops.dispatch_bubble(key) {
300 return Some(DispatchOutcome::Bubble);
301 }
302 match ops.dispatch_framework(key) {
303 FrameworkDispatch::Handled => Some(DispatchOutcome::Framework),
304 FrameworkDispatch::Quit => Some(DispatchOutcome::FrameworkQuit),
305 FrameworkDispatch::None => None,
306 }
307}
308
309fn dispatch_framework_only(key: KeyEvent, ops: &mut impl DispatchOps) -> DispatchOutcome {
310 match ops.dispatch_framework(key) {
311 FrameworkDispatch::Handled => DispatchOutcome::Framework,
312 FrameworkDispatch::Quit => DispatchOutcome::FrameworkQuit,
313 FrameworkDispatch::None => DispatchOutcome::Unhandled,
314 }
315}
316
317#[derive(Clone, Copy, Debug, Eq, PartialEq)]
318pub(crate) enum TerminalPreflightDispatch {
319 Consumed,
320 Forward,
321 NotApplicable,
322 NotConsumed,
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::core::event::{KeyCode, KeyEvent, KeyMods};
329
330 #[derive(Default)]
331 struct FakeOps {
332 widget_handles: bool,
333 bubble_handles: bool,
334 terminal_preflight_handles: bool,
335 terminal_preflight_not_consumed: bool,
336 terminal_handles: bool,
337 command_chord_match: Option<&'static str>,
338 command_match: Option<&'static str>,
339 command_pending: bool,
340 command_mismatch: bool,
341 framework_quit: bool,
342 framework_handles: bool,
343 ambient_handles: bool,
344 calls: Vec<&'static str>,
345 }
346
347 impl FakeOps {
348 fn from_case(case: &Case) -> Self {
349 Self {
350 widget_handles: case.widget_handles,
351 bubble_handles: case.bubble_handles,
352 terminal_preflight_handles: case.terminal_preflight_handles,
353 terminal_preflight_not_consumed: case.terminal_preflight_not_consumed,
354 terminal_handles: case.terminal_handles,
355 command_chord_match: case.command_chord_match,
356 command_match: case.command_match,
357 command_pending: case.command_pending,
358 command_mismatch: case.command_mismatch,
359 framework_quit: case.framework_quit,
360 framework_handles: case.framework_handles,
361 ambient_handles: case.ambient_handles,
362 calls: Vec::new(),
363 }
364 }
365 }
366
367 impl DispatchOps for FakeOps {
368 fn continue_command_chord(&mut self, _key: KeyEvent) -> CommandDispatchState {
369 self.calls.push("command-chord");
370 if self.command_pending {
371 return CommandDispatchState::Pending;
372 }
373 if self.command_mismatch {
374 return CommandDispatchState::Mismatch;
375 }
376 self.command_chord_match
377 .map(|id| CommandDispatchState::Matched(id.into()))
378 .unwrap_or(CommandDispatchState::None)
379 }
380
381 fn reset_command_chord(&mut self) {
382 self.calls.push("reset-chord");
383 }
384
385 fn dispatch_widget(&mut self, _key: KeyEvent) -> bool {
386 self.calls.push("widget");
387 self.widget_handles
388 }
389
390 fn dispatch_bubble(&mut self, _key: KeyEvent) -> bool {
391 self.calls.push("bubble");
392 self.bubble_handles
393 }
394
395 fn dispatch_command(&mut self, _key: KeyEvent) -> bool {
396 self.calls.push("command");
397 self.command_match.is_some()
398 }
399
400 fn dispatch_framework(&mut self, _key: KeyEvent) -> FrameworkDispatch {
401 self.calls.push("framework");
402 if self.framework_quit {
403 FrameworkDispatch::Quit
404 } else if self.framework_handles {
405 FrameworkDispatch::Handled
406 } else {
407 FrameworkDispatch::None
408 }
409 }
410
411 fn dispatch_terminal_preflight(&mut self, _key: KeyEvent) -> TerminalPreflightDispatch {
412 self.calls.push("terminal-preflight");
413 if self.terminal_preflight_handles {
414 TerminalPreflightDispatch::Consumed
415 } else if self.terminal_preflight_not_consumed {
416 TerminalPreflightDispatch::NotConsumed
417 } else {
418 TerminalPreflightDispatch::NotApplicable
419 }
420 }
421
422 fn dispatch_terminal(&mut self, _key: KeyEvent) -> bool {
423 self.calls.push("terminal");
424 self.terminal_handles
425 }
426
427 fn dispatch_ambient_scroll(&mut self, _key: KeyEvent) -> bool {
428 self.calls.push("ambient");
429 self.ambient_handles
430 }
431 }
432
433 fn ctrl(ch: char) -> KeyEvent {
434 KeyEvent {
435 code: KeyCode::Char(ch),
436 mods: KeyMods {
437 ctrl: true,
438 ..KeyMods::default()
439 },
440 }
441 }
442
443 struct Case {
444 name: &'static str,
445 focus: FocusKind,
446 key_policy: KeyDispatchPolicy,
447 terminal_policy: TerminalKeyPolicy,
448 widget_handles: bool,
449 bubble_handles: bool,
450 terminal_preflight_handles: bool,
451 terminal_preflight_not_consumed: bool,
452 terminal_handles: bool,
453 command_chord_match: Option<&'static str>,
454 command_match: Option<&'static str>,
455 command_pending: bool,
456 command_mismatch: bool,
457 framework_quit: bool,
458 framework_handles: bool,
459 ambient_handles: bool,
460 expected_calls: &'static [&'static str],
461 expected: DispatchOutcome,
462 }
463
464 #[test]
465 fn dispatch_matrix_covers_focus_policy_and_consumption_paths() {
466 let cases = [
467 Case {
468 name: "widget-first widget consumes",
469 focus: FocusKind::Widget,
470 key_policy: KeyDispatchPolicy::WidgetFirst,
471 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
472 widget_handles: true,
473 bubble_handles: false,
474 terminal_preflight_handles: false,
475 terminal_preflight_not_consumed: false,
476 terminal_handles: false,
477 command_chord_match: None,
478 command_match: Some("app.save"),
479 command_pending: false,
480 command_mismatch: false,
481 framework_quit: true,
482 framework_handles: false,
483 ambient_handles: false,
484 expected_calls: &["command-chord", "widget"],
485 expected: DispatchOutcome::Widget,
486 },
487 Case {
488 name: "widget-first bubble consumes before command",
489 focus: FocusKind::Widget,
490 key_policy: KeyDispatchPolicy::WidgetFirst,
491 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
492 widget_handles: false,
493 bubble_handles: true,
494 terminal_preflight_handles: false,
495 terminal_preflight_not_consumed: false,
496 terminal_handles: false,
497 command_chord_match: None,
498 command_match: Some("app.save"),
499 command_pending: false,
500 command_mismatch: false,
501 framework_quit: true,
502 framework_handles: false,
503 ambient_handles: false,
504 expected_calls: &["command-chord", "widget", "bubble"],
505 expected: DispatchOutcome::Bubble,
506 },
507 Case {
508 name: "widget-first command after bubble miss",
509 focus: FocusKind::Widget,
510 key_policy: KeyDispatchPolicy::WidgetFirst,
511 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
512 widget_handles: false,
513 bubble_handles: false,
514 terminal_preflight_handles: false,
515 terminal_preflight_not_consumed: false,
516 terminal_handles: false,
517 command_chord_match: None,
518 command_match: Some("app.save"),
519 command_pending: false,
520 command_mismatch: false,
521 framework_quit: true,
522 framework_handles: false,
523 ambient_handles: false,
524 expected_calls: &["command-chord", "widget", "bubble", "command"],
525 expected: DispatchOutcome::Command,
526 },
527 Case {
528 name: "widget-first framework fallback",
529 focus: FocusKind::Widget,
530 key_policy: KeyDispatchPolicy::WidgetFirst,
531 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
532 widget_handles: false,
533 bubble_handles: false,
534 terminal_preflight_handles: false,
535 terminal_preflight_not_consumed: false,
536 terminal_handles: false,
537 command_chord_match: None,
538 command_match: None,
539 command_pending: false,
540 command_mismatch: false,
541 framework_quit: true,
542 framework_handles: false,
543 ambient_handles: false,
544 expected_calls: &["command-chord", "widget", "bubble", "command", "framework"],
545 expected: DispatchOutcome::FrameworkQuit,
546 },
547 Case {
548 name: "widget-first ambient fallback",
549 focus: FocusKind::Widget,
550 key_policy: KeyDispatchPolicy::WidgetFirst,
551 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
552 widget_handles: false,
553 bubble_handles: false,
554 terminal_preflight_handles: false,
555 terminal_preflight_not_consumed: false,
556 terminal_handles: false,
557 command_chord_match: None,
558 command_match: None,
559 command_pending: false,
560 command_mismatch: false,
561 framework_quit: false,
562 framework_handles: false,
563 ambient_handles: true,
564 expected_calls: &[
565 "command-chord",
566 "widget",
567 "bubble",
568 "command",
569 "framework",
570 "ambient",
571 ],
572 expected: DispatchOutcome::AmbientScroll,
573 },
574 Case {
575 name: "app-commands-first command before widget",
576 focus: FocusKind::Widget,
577 key_policy: KeyDispatchPolicy::AppCommandsFirst,
578 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
579 widget_handles: true,
580 bubble_handles: false,
581 terminal_preflight_handles: false,
582 terminal_preflight_not_consumed: false,
583 terminal_handles: false,
584 command_chord_match: None,
585 command_match: Some("app.save"),
586 command_pending: false,
587 command_mismatch: false,
588 framework_quit: false,
589 framework_handles: false,
590 ambient_handles: false,
591 expected_calls: &["command-chord", "command"],
592 expected: DispatchOutcome::Command,
593 },
594 Case {
595 name: "completed widget command chord consumes before widget",
596 focus: FocusKind::Widget,
597 key_policy: KeyDispatchPolicy::WidgetFirst,
598 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
599 widget_handles: true,
600 bubble_handles: true,
601 terminal_preflight_handles: false,
602 terminal_preflight_not_consumed: false,
603 terminal_handles: false,
604 command_chord_match: Some("app.save"),
605 command_match: None,
606 command_pending: false,
607 command_mismatch: false,
608 framework_quit: true,
609 framework_handles: false,
610 ambient_handles: false,
611 expected_calls: &["command-chord"],
612 expected: DispatchOutcome::Command,
613 },
614 Case {
615 name: "widget framework handled fallback",
616 focus: FocusKind::Widget,
617 key_policy: KeyDispatchPolicy::WidgetFirst,
618 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
619 widget_handles: false,
620 bubble_handles: false,
621 terminal_preflight_handles: false,
622 terminal_preflight_not_consumed: false,
623 terminal_handles: false,
624 command_chord_match: None,
625 command_match: None,
626 command_pending: false,
627 command_mismatch: false,
628 framework_quit: false,
629 framework_handles: true,
630 ambient_handles: true,
631 expected_calls: &["command-chord", "widget", "bubble", "command", "framework"],
632 expected: DispatchOutcome::Framework,
633 },
634 Case {
635 name: "terminal framework-first quit",
636 focus: FocusKind::Terminal,
637 key_policy: KeyDispatchPolicy::WidgetFirst,
638 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
639 widget_handles: false,
640 bubble_handles: false,
641 terminal_preflight_handles: false,
642 terminal_preflight_not_consumed: false,
643 terminal_handles: true,
644 command_chord_match: None,
645 command_match: Some("mux.detach"),
646 command_pending: false,
647 command_mismatch: false,
648 framework_quit: true,
649 framework_handles: false,
650 ambient_handles: false,
651 expected_calls: &["framework"],
652 expected: DispatchOutcome::FrameworkQuit,
653 },
654 Case {
655 name: "terminal framework-first handled",
656 focus: FocusKind::Terminal,
657 key_policy: KeyDispatchPolicy::WidgetFirst,
658 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
659 widget_handles: false,
660 bubble_handles: false,
661 terminal_preflight_handles: false,
662 terminal_preflight_not_consumed: false,
663 terminal_handles: true,
664 command_chord_match: None,
665 command_match: Some("mux.detach"),
666 command_pending: false,
667 command_mismatch: false,
668 framework_quit: false,
669 framework_handles: true,
670 ambient_handles: false,
671 expected_calls: &["framework"],
672 expected: DispatchOutcome::Framework,
673 },
674 Case {
675 name: "terminal app-then-terminal preflight wins",
676 focus: FocusKind::Terminal,
677 key_policy: KeyDispatchPolicy::WidgetFirst,
678 terminal_policy: TerminalKeyPolicy::AppCommandsThenTerminal,
679 widget_handles: false,
680 bubble_handles: false,
681 terminal_preflight_handles: true,
682 terminal_preflight_not_consumed: false,
683 terminal_handles: true,
684 command_chord_match: None,
685 command_match: Some("mux.copy"),
686 command_pending: false,
687 command_mismatch: false,
688 framework_quit: true,
689 framework_handles: false,
690 ambient_handles: false,
691 expected_calls: &["terminal-preflight", "reset-chord"],
692 expected: DispatchOutcome::TerminalPreflight,
693 },
694 Case {
695 name: "terminal app-then-terminal command wins after preflight miss",
696 focus: FocusKind::Terminal,
697 key_policy: KeyDispatchPolicy::WidgetFirst,
698 terminal_policy: TerminalKeyPolicy::AppCommandsThenTerminal,
699 widget_handles: false,
700 bubble_handles: false,
701 terminal_preflight_handles: false,
702 terminal_preflight_not_consumed: false,
703 terminal_handles: true,
704 command_chord_match: None,
705 command_match: Some("mux.detach"),
706 command_pending: false,
707 command_mismatch: false,
708 framework_quit: true,
709 framework_handles: false,
710 ambient_handles: false,
711 expected_calls: &["terminal-preflight", "command-chord", "command"],
712 expected: DispatchOutcome::Command,
713 },
714 Case {
715 name: "terminal app command chord consumes after preflight miss",
716 focus: FocusKind::Terminal,
717 key_policy: KeyDispatchPolicy::WidgetFirst,
718 terminal_policy: TerminalKeyPolicy::AppCommandsThenTerminal,
719 widget_handles: false,
720 bubble_handles: false,
721 terminal_preflight_handles: false,
722 terminal_preflight_not_consumed: false,
723 terminal_handles: true,
724 command_chord_match: Some("mux.detach"),
725 command_match: None,
726 command_pending: false,
727 command_mismatch: false,
728 framework_quit: true,
729 framework_handles: false,
730 ambient_handles: false,
731 expected_calls: &["terminal-preflight", "command-chord"],
732 expected: DispatchOutcome::Command,
733 },
734 Case {
735 name: "terminal preflight not-consumed forwards to terminal",
736 focus: FocusKind::Terminal,
737 key_policy: KeyDispatchPolicy::WidgetFirst,
738 terminal_policy: TerminalKeyPolicy::TerminalFirst,
739 widget_handles: false,
740 bubble_handles: false,
741 terminal_preflight_handles: false,
742 terminal_preflight_not_consumed: true,
743 terminal_handles: true,
744 command_chord_match: None,
745 command_match: None,
746 command_pending: false,
747 command_mismatch: false,
748 framework_quit: true,
749 framework_handles: false,
750 ambient_handles: false,
751 expected_calls: &["terminal-preflight", "terminal"],
752 expected: DispatchOutcome::Terminal,
753 },
754 Case {
755 name: "terminal-first terminal consumes",
756 focus: FocusKind::Terminal,
757 key_policy: KeyDispatchPolicy::WidgetFirst,
758 terminal_policy: TerminalKeyPolicy::TerminalFirst,
759 widget_handles: false,
760 bubble_handles: false,
761 terminal_preflight_handles: false,
762 terminal_preflight_not_consumed: false,
763 terminal_handles: true,
764 command_chord_match: None,
765 command_match: Some("mux.detach"),
766 command_pending: false,
767 command_mismatch: false,
768 framework_quit: true,
769 framework_handles: false,
770 ambient_handles: false,
771 expected_calls: &["terminal-preflight", "terminal"],
772 expected: DispatchOutcome::Terminal,
773 },
774 Case {
775 name: "terminal-only never command/framework",
776 focus: FocusKind::Terminal,
777 key_policy: KeyDispatchPolicy::WidgetFirst,
778 terminal_policy: TerminalKeyPolicy::TerminalOnly,
779 widget_handles: false,
780 bubble_handles: false,
781 terminal_preflight_handles: false,
782 terminal_preflight_not_consumed: false,
783 terminal_handles: true,
784 command_chord_match: None,
785 command_match: Some("mux.detach"),
786 command_pending: false,
787 command_mismatch: false,
788 framework_quit: true,
789 framework_handles: false,
790 ambient_handles: false,
791 expected_calls: &["terminal-preflight", "terminal"],
792 expected: DispatchOutcome::Terminal,
793 },
794 Case {
795 name: "command chord pending consumes",
796 focus: FocusKind::Widget,
797 key_policy: KeyDispatchPolicy::WidgetFirst,
798 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
799 widget_handles: true,
800 bubble_handles: false,
801 terminal_preflight_handles: false,
802 terminal_preflight_not_consumed: false,
803 terminal_handles: false,
804 command_chord_match: None,
805 command_match: None,
806 command_pending: true,
807 command_mismatch: false,
808 framework_quit: false,
809 framework_handles: false,
810 ambient_handles: false,
811 expected_calls: &["command-chord"],
812 expected: DispatchOutcome::CommandPending,
813 },
814 Case {
815 name: "command chord mismatch replays current to widget",
816 focus: FocusKind::Widget,
817 key_policy: KeyDispatchPolicy::WidgetFirst,
818 terminal_policy: TerminalKeyPolicy::FrameworkFirst,
819 widget_handles: true,
820 bubble_handles: false,
821 terminal_preflight_handles: false,
822 terminal_preflight_not_consumed: false,
823 terminal_handles: false,
824 command_chord_match: None,
825 command_match: None,
826 command_pending: false,
827 command_mismatch: true,
828 framework_quit: false,
829 framework_handles: false,
830 ambient_handles: false,
831 expected_calls: &["command-chord", "widget"],
832 expected: DispatchOutcome::Widget,
833 },
834 ];
835
836 for case in cases {
837 let mut ops = FakeOps::from_case(&case);
838 let outcome = dispatch_key(
839 DispatchRequest::new(ctrl('s'), case.focus)
840 .key_policy(case.key_policy)
841 .terminal_policy(case.terminal_policy),
842 &mut ops,
843 );
844 assert_eq!(ops.calls, case.expected_calls, "{} calls", case.name);
845 assert_eq!(outcome, case.expected, "{} outcome", case.name);
846 }
847 }
848}