Skip to main content

fret_runtime/
shortcut_routing_diagnostics.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use fret_core::{AppWindowId, FrameId, KeyCode, Modifiers};
5
6use crate::CommandId;
7
8/// Diagnostics-only trace entries that explain how keydown shortcuts were routed.
9///
10/// This store is intended to support structured explainability in `fretboard diag` without
11/// relying on ad-hoc logs or screenshots.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ShortcutRoutingPhase {
14    PreDispatch,
15    PostDispatch,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ShortcutRoutingOutcome {
20    /// Shortcut matching was skipped because the key was reserved for IME while composing.
21    ReservedForIme,
22    /// Shortcut matching was deferred, and the widget path consumed the event.
23    ConsumedByWidget,
24    /// A command was matched and dispatched via an `Effect::Command`.
25    CommandDispatched,
26    /// A command matched but was disabled (so the event fell through to normal dispatch).
27    CommandDisabled,
28    /// A key chord started or continued a multi-keystroke shortcut sequence.
29    SequenceContinuation,
30    /// A shortcut sequence failed to match and the captured keystrokes were replayed.
31    SequenceReplay,
32    /// No shortcut matched this chord.
33    NoMatch,
34    /// Shortcut matching was unavailable (no keymap service).
35    NoKeymap,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ShortcutRoutingDecision {
40    pub seq: u64,
41    pub frame_id: FrameId,
42    pub phase: ShortcutRoutingPhase,
43    pub key: KeyCode,
44    pub modifiers: Modifiers,
45    pub repeat: bool,
46    pub deferred: bool,
47    pub focus_is_text_input: bool,
48    pub ime_composing: bool,
49    pub pending_sequence_len: u32,
50    pub outcome: ShortcutRoutingOutcome,
51    pub command: Option<CommandId>,
52    pub command_enabled: Option<bool>,
53    pub key_contexts: Vec<Arc<str>>,
54}
55
56#[derive(Default)]
57pub struct WindowShortcutRoutingDiagnosticsStore {
58    next_seq: u64,
59    per_window: HashMap<AppWindowId, Vec<ShortcutRoutingDecision>>,
60}
61
62impl WindowShortcutRoutingDiagnosticsStore {
63    const MAX_ENTRIES_PER_WINDOW: usize = 128;
64
65    pub fn record(&mut self, window: AppWindowId, mut decision: ShortcutRoutingDecision) {
66        decision.seq = self.next_seq;
67        self.next_seq = self.next_seq.saturating_add(1);
68
69        let entries = self.per_window.entry(window).or_default();
70        entries.push(decision);
71        if entries.len() > Self::MAX_ENTRIES_PER_WINDOW {
72            let extra = entries.len().saturating_sub(Self::MAX_ENTRIES_PER_WINDOW);
73            entries.drain(0..extra);
74        }
75    }
76
77    pub fn snapshot_since(
78        &self,
79        window: AppWindowId,
80        since_seq: u64,
81        max_entries: usize,
82    ) -> Vec<ShortcutRoutingDecision> {
83        let Some(entries) = self.per_window.get(&window) else {
84            return Vec::new();
85        };
86        entries
87            .iter()
88            .filter(|e| e.seq >= since_seq)
89            .take(max_entries)
90            .cloned()
91            .collect()
92    }
93}