Skip to main content

okf_studio/
keymap.rs

1//! The keybinding table: context → key → action, with a primary/alias flag.
2//!
3//! Dispatch, the hint bar, and the help overlay are all generated from this
4//! one table, so they can never drift. Standard keys (arrows, `F2`, `Del`,
5//! `Enter`) are the primary bindings the UI advertises; the vim-style letters
6//! are complementary aliases bound on top, never instead.
7
8use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9
10/// Where a binding applies. Contexts are consulted from most to least
11/// specific: the focused pane's context, then [`Context::Concept`] when a
12/// concept is selected, then [`Context::Global`].
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Context {
15    /// Always active.
16    Global,
17    /// Explorer with the tree pane focused.
18    Tree,
19    /// Explorer with the document pane focused.
20    Viewer,
21    /// The graph workspace.
22    Graph,
23    /// The mission-control workspace.
24    Trust,
25    /// The computations workspace.
26    Computations,
27    /// Verbs on the currently selected concept, any workspace.
28    Concept,
29}
30
31/// Every dispatchable studio action.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum Action {
34    /// Switch to workspace `0..=3`.
35    SwitchTab(usize),
36    /// Open the omnisearch / command palette.
37    OpenPalette,
38    /// Open the diagnostics overlay.
39    OpenDiagnostics,
40    /// Open the help cheatsheet.
41    OpenHelp,
42    /// Force a bundle reload.
43    Reload,
44    /// Open the selected file in `$EDITOR`.
45    OpenEditor,
46    /// Quit studio.
47    Quit,
48    /// Move selection / scroll up.
49    Up,
50    /// Move selection / scroll down.
51    Down,
52    /// Collapse / focus left.
53    Left,
54    /// Expand / focus right.
55    Right,
56    /// Page up.
57    PageUp,
58    /// Page down.
59    PageDown,
60    /// Half page down (vim `Ctrl+D`).
61    HalfDown,
62    /// Half page up (vim `Ctrl+U`).
63    HalfUp,
64    /// Jump to start.
65    Home,
66    /// Jump to end.
67    End,
68    /// Activate the selection (open / follow / filter).
69    Activate,
70    /// Navigate back in history.
71    Back,
72    /// Navigate forward in history.
73    Forward,
74    /// Focus the next link in the document.
75    NextLink,
76    /// Focus the previous link in the document.
77    PrevLink,
78    /// Toggle raw-YAML frontmatter view.
79    ToggleRawYaml,
80    /// Open the outline jump list.
81    Outline,
82    /// Cycle the inspector tab.
83    CycleInspector,
84    /// Stamp a verification on the selected concept.
85    Verify,
86    /// Extend the selected concept's `stale_after`.
87    ExtendStale,
88    /// Move / rename the selection (rename section on a heading).
89    MoveOrRename,
90    /// Remove the selected concept.
91    Remove,
92    /// Merge the selected concept into another.
93    Merge,
94    /// Split the focused section into a new concept.
95    SplitSection,
96    /// Create a new concept here.
97    NewConcept,
98    /// Zoom the graph in.
99    ZoomIn,
100    /// Zoom the graph out.
101    ZoomOut,
102    /// Select the next graph node.
103    NextNode,
104    /// Select the previous graph node.
105    PrevNode,
106    /// Toggle / widen egocentric focus mode.
107    FocusMode,
108    /// Cycle the node coloring dimension.
109    CycleColor,
110    /// Toggle external source nodes.
111    ToggleSources,
112    /// Toggle broken-target nodes.
113    ToggleBroken,
114    /// Toggle derivation edges.
115    ToggleDerivations,
116    /// Pause / resume the layout simulation.
117    PauseLayout,
118    /// Filter graph nodes by fuzzy query.
119    GraphFilter,
120    /// Cycle the layout algorithm.
121    CycleLayout,
122    /// Cycle panel focus within the workspace.
123    CyclePanel,
124    /// Cycle the attention-queue sort.
125    CycleSort,
126    /// Copy the invocation sketch (OSC 52).
127    CopySketch,
128    /// Preview and apply all safe fixes (palette-only).
129    FixAll,
130    /// Pin the evaluation date (palette-only).
131    PinToday,
132    /// Open the full activity log (palette-only).
133    OpenLog,
134}
135
136/// One row of the keybinding table.
137pub struct Binding {
138    /// The context the binding applies in.
139    pub context: Context,
140    /// The key code.
141    pub code: KeyCode,
142    /// Required modifiers (`SHIFT` is ignored for `Char` codes).
143    pub mods: KeyModifiers,
144    /// The action dispatched.
145    pub action: Action,
146    /// Whether this is the advertised binding (vs. a vim-style alias).
147    pub primary: bool,
148    /// Display label for the hint bar and help overlay.
149    pub label: &'static str,
150    /// One-line description.
151    pub help: &'static str,
152}
153
154const fn b(
155    context: Context,
156    code: KeyCode,
157    mods: KeyModifiers,
158    action: Action,
159    primary: bool,
160    label: &'static str,
161    help: &'static str,
162) -> Binding {
163    Binding {
164        context,
165        code,
166        mods,
167        action,
168        primary,
169        label,
170        help,
171    }
172}
173
174const NONE: KeyModifiers = KeyModifiers::NONE;
175const CTRL: KeyModifiers = KeyModifiers::CONTROL;
176
177/// The full binding table, in help-overlay order.
178#[must_use]
179pub const fn bindings() -> &'static [Binding] {
180    BINDINGS
181}
182
183#[allow(clippy::too_many_lines)]
184const BINDINGS: &[Binding] = {
185    use Action as A;
186    use Context as C;
187    use KeyCode as K;
188    &[
189        // Global.
190        b(
191            C::Global,
192            K::Char('1'),
193            NONE,
194            A::SwitchTab(0),
195            true,
196            "1",
197            "Explorer workspace",
198        ),
199        b(
200            C::Global,
201            K::Char('2'),
202            NONE,
203            A::SwitchTab(1),
204            true,
205            "2",
206            "Graph workspace",
207        ),
208        b(
209            C::Global,
210            K::Char('3'),
211            NONE,
212            A::SwitchTab(2),
213            true,
214            "3",
215            "Trust workspace",
216        ),
217        b(
218            C::Global,
219            K::Char('4'),
220            NONE,
221            A::SwitchTab(3),
222            true,
223            "4",
224            "Computations workspace",
225        ),
226        b(
227            C::Global,
228            K::Char('/'),
229            NONE,
230            A::OpenPalette,
231            true,
232            "/",
233            "Omnisearch (> for commands)",
234        ),
235        b(
236            C::Global,
237            K::Char('p'),
238            CTRL,
239            A::OpenPalette,
240            false,
241            "Ctrl+P",
242            "Command palette",
243        ),
244        b(
245            C::Global,
246            K::Char('!'),
247            NONE,
248            A::OpenDiagnostics,
249            true,
250            "!",
251            "Diagnostics & fixes",
252        ),
253        b(
254            C::Global,
255            K::Char('?'),
256            NONE,
257            A::OpenHelp,
258            true,
259            "?",
260            "Help / cheatsheet",
261        ),
262        b(
263            C::Global,
264            K::Char('R'),
265            NONE,
266            A::Reload,
267            true,
268            "R",
269            "Reload bundle",
270        ),
271        b(
272            C::Global,
273            K::Char('e'),
274            NONE,
275            A::OpenEditor,
276            true,
277            "e",
278            "Open in $EDITOR",
279        ),
280        b(C::Global, K::Char('q'), NONE, A::Quit, true, "q", "Quit"),
281        // Concept verbs.
282        b(
283            C::Concept,
284            K::Char('v'),
285            NONE,
286            A::Verify,
287            true,
288            "v",
289            "Stamp verification",
290        ),
291        b(
292            C::Concept,
293            K::Char('x'),
294            NONE,
295            A::ExtendStale,
296            true,
297            "x",
298            "Extend stale_after",
299        ),
300        b(
301            C::Concept,
302            K::F(2),
303            NONE,
304            A::MoveOrRename,
305            true,
306            "F2",
307            "Move / rename",
308        ),
309        b(
310            C::Concept,
311            K::Char('m'),
312            NONE,
313            A::MoveOrRename,
314            false,
315            "m",
316            "Move / rename",
317        ),
318        b(
319            C::Concept,
320            K::Delete,
321            NONE,
322            A::Remove,
323            true,
324            "Del",
325            "Remove concept",
326        ),
327        b(
328            C::Concept,
329            K::Char('M'),
330            NONE,
331            A::Merge,
332            true,
333            "M",
334            "Merge into…",
335        ),
336        b(
337            C::Concept,
338            K::Char('n'),
339            NONE,
340            A::NewConcept,
341            true,
342            "n",
343            "New concept here",
344        ),
345        // Tree pane.
346        b(C::Tree, K::Up, NONE, A::Up, true, "↑", "Previous row"),
347        b(C::Tree, K::Down, NONE, A::Down, true, "↓", "Next row"),
348        b(
349            C::Tree,
350            K::Char('k'),
351            NONE,
352            A::Up,
353            false,
354            "k",
355            "Previous row",
356        ),
357        b(C::Tree, K::Char('j'), NONE, A::Down, false, "j", "Next row"),
358        b(
359            C::Tree,
360            K::Left,
361            NONE,
362            A::Left,
363            true,
364            "←",
365            "Collapse directory",
366        ),
367        b(
368            C::Tree,
369            K::Right,
370            NONE,
371            A::Right,
372            true,
373            "→",
374            "Expand / focus document",
375        ),
376        b(
377            C::Tree,
378            K::Char('h'),
379            NONE,
380            A::Left,
381            false,
382            "h",
383            "Collapse directory",
384        ),
385        b(
386            C::Tree,
387            K::Char('l'),
388            NONE,
389            A::Right,
390            false,
391            "l",
392            "Expand / focus document",
393        ),
394        b(
395            C::Tree,
396            K::Enter,
397            NONE,
398            A::Activate,
399            true,
400            "⏎",
401            "Open selection",
402        ),
403        b(C::Tree, K::Home, NONE, A::Home, true, "Home", "First row"),
404        b(C::Tree, K::End, NONE, A::End, true, "End", "Last row"),
405        b(C::Tree, K::PageUp, NONE, A::PageUp, true, "PgUp", "Page up"),
406        b(
407            C::Tree,
408            K::PageDown,
409            NONE,
410            A::PageDown,
411            true,
412            "PgDn",
413            "Page down",
414        ),
415        b(
416            C::Tree,
417            K::Char('d'),
418            NONE,
419            A::Remove,
420            false,
421            "d",
422            "Remove concept",
423        ),
424        b(
425            C::Tree,
426            K::Char('i'),
427            NONE,
428            A::CycleInspector,
429            true,
430            "i",
431            "Cycle inspector tab",
432        ),
433        // Document viewer.
434        b(C::Viewer, K::Up, NONE, A::Up, true, "↑", "Scroll up"),
435        b(C::Viewer, K::Down, NONE, A::Down, true, "↓", "Scroll down"),
436        b(
437            C::Viewer,
438            K::Char('k'),
439            NONE,
440            A::Up,
441            false,
442            "k",
443            "Scroll up",
444        ),
445        b(
446            C::Viewer,
447            K::Char('j'),
448            NONE,
449            A::Down,
450            false,
451            "j",
452            "Scroll down",
453        ),
454        b(
455            C::Viewer,
456            K::PageUp,
457            NONE,
458            A::PageUp,
459            true,
460            "PgUp",
461            "Page up",
462        ),
463        b(
464            C::Viewer,
465            K::PageDown,
466            NONE,
467            A::PageDown,
468            true,
469            "PgDn",
470            "Page down",
471        ),
472        b(
473            C::Viewer,
474            K::Char('u'),
475            CTRL,
476            A::HalfUp,
477            false,
478            "Ctrl+U",
479            "Half page up",
480        ),
481        b(
482            C::Viewer,
483            K::Char('d'),
484            CTRL,
485            A::HalfDown,
486            false,
487            "Ctrl+D",
488            "Half page down",
489        ),
490        b(C::Viewer, K::Home, NONE, A::Home, true, "Home", "Top"),
491        b(C::Viewer, K::End, NONE, A::End, true, "End", "Bottom"),
492        b(C::Viewer, K::Char('g'), NONE, A::Home, false, "g", "Top"),
493        b(C::Viewer, K::Char('G'), NONE, A::End, false, "G", "Bottom"),
494        b(
495            C::Viewer,
496            K::Tab,
497            NONE,
498            A::NextLink,
499            true,
500            "Tab",
501            "Focus next link",
502        ),
503        b(
504            C::Viewer,
505            K::BackTab,
506            NONE,
507            A::PrevLink,
508            true,
509            "S-Tab",
510            "Focus previous link",
511        ),
512        b(
513            C::Viewer,
514            K::Enter,
515            NONE,
516            A::Activate,
517            true,
518            "⏎",
519            "Follow focused link",
520        ),
521        b(C::Viewer, K::Backspace, NONE, A::Back, true, "⌫", "Back"),
522        b(
523            C::Viewer,
524            K::Char('o'),
525            CTRL,
526            A::Back,
527            false,
528            "Ctrl+O",
529            "Back",
530        ),
531        b(
532            C::Viewer,
533            K::Char('i'),
534            CTRL,
535            A::Forward,
536            false,
537            "Ctrl+I",
538            "Forward",
539        ),
540        b(
541            C::Viewer,
542            K::Char('y'),
543            NONE,
544            A::ToggleRawYaml,
545            true,
546            "y",
547            "Toggle raw YAML",
548        ),
549        b(
550            C::Viewer,
551            K::Char('o'),
552            NONE,
553            A::Outline,
554            true,
555            "o",
556            "Outline jump list",
557        ),
558        b(
559            C::Viewer,
560            K::Char('i'),
561            NONE,
562            A::CycleInspector,
563            true,
564            "i",
565            "Cycle inspector tab",
566        ),
567        b(C::Viewer, K::Left, NONE, A::Left, true, "←", "Focus tree"),
568        b(
569            C::Viewer,
570            K::Char('s'),
571            NONE,
572            A::SplitSection,
573            true,
574            "s",
575            "Split section out",
576        ),
577        // Graph.
578        b(C::Graph, K::Up, NONE, A::Up, true, "↑", "Pan up"),
579        b(C::Graph, K::Down, NONE, A::Down, true, "↓", "Pan down"),
580        b(C::Graph, K::Left, NONE, A::Left, true, "←", "Pan left"),
581        b(C::Graph, K::Right, NONE, A::Right, true, "→", "Pan right"),
582        b(C::Graph, K::Char('k'), NONE, A::Up, false, "k", "Pan up"),
583        b(
584            C::Graph,
585            K::Char('j'),
586            NONE,
587            A::Down,
588            false,
589            "j",
590            "Pan down",
591        ),
592        b(
593            C::Graph,
594            K::Char('h'),
595            NONE,
596            A::Left,
597            false,
598            "h",
599            "Pan left",
600        ),
601        b(
602            C::Graph,
603            K::Char('l'),
604            NONE,
605            A::Right,
606            false,
607            "l",
608            "Pan right",
609        ),
610        b(
611            C::Graph,
612            K::Char('+'),
613            NONE,
614            A::ZoomIn,
615            true,
616            "+",
617            "Zoom in",
618        ),
619        b(
620            C::Graph,
621            K::Char('='),
622            NONE,
623            A::ZoomIn,
624            false,
625            "=",
626            "Zoom in",
627        ),
628        b(
629            C::Graph,
630            K::Char('-'),
631            NONE,
632            A::ZoomOut,
633            true,
634            "-",
635            "Zoom out",
636        ),
637        b(
638            C::Graph,
639            K::Tab,
640            NONE,
641            A::NextNode,
642            true,
643            "Tab",
644            "Next node",
645        ),
646        b(
647            C::Graph,
648            K::Char('n'),
649            NONE,
650            A::NextNode,
651            false,
652            "n",
653            "Next node",
654        ),
655        b(
656            C::Graph,
657            K::BackTab,
658            NONE,
659            A::PrevNode,
660            true,
661            "S-Tab",
662            "Previous node",
663        ),
664        b(
665            C::Graph,
666            K::Char('p'),
667            NONE,
668            A::PrevNode,
669            false,
670            "p",
671            "Previous node",
672        ),
673        b(
674            C::Graph,
675            K::Enter,
676            NONE,
677            A::Activate,
678            true,
679            "⏎",
680            "Open node in Explorer",
681        ),
682        b(
683            C::Graph,
684            K::Char('f'),
685            NONE,
686            A::FocusMode,
687            true,
688            "f",
689            "Focus mode (k-hop)",
690        ),
691        b(
692            C::Graph,
693            K::Char('c'),
694            NONE,
695            A::CycleColor,
696            true,
697            "c",
698            "Cycle node coloring",
699        ),
700        b(
701            C::Graph,
702            K::Char('s'),
703            NONE,
704            A::ToggleSources,
705            true,
706            "s",
707            "Toggle source nodes",
708        ),
709        b(
710            C::Graph,
711            K::Char('b'),
712            NONE,
713            A::ToggleBroken,
714            true,
715            "b",
716            "Toggle broken targets",
717        ),
718        b(
719            C::Graph,
720            K::Char('d'),
721            NONE,
722            A::ToggleDerivations,
723            true,
724            "d",
725            "Toggle derivation edges",
726        ),
727        b(
728            C::Graph,
729            K::Char(' '),
730            NONE,
731            A::PauseLayout,
732            true,
733            "Space",
734            "Pause / resume layout",
735        ),
736        b(
737            C::Graph,
738            K::Char('/'),
739            NONE,
740            A::GraphFilter,
741            true,
742            "/",
743            "Filter nodes",
744        ),
745        b(
746            C::Graph,
747            K::Char('L'),
748            NONE,
749            A::CycleLayout,
750            true,
751            "L",
752            "Cycle layout",
753        ),
754        // Mission control.
755        b(C::Trust, K::Up, NONE, A::Up, true, "↑", "Previous row"),
756        b(C::Trust, K::Down, NONE, A::Down, true, "↓", "Next row"),
757        b(
758            C::Trust,
759            K::Char('k'),
760            NONE,
761            A::Up,
762            false,
763            "k",
764            "Previous row",
765        ),
766        b(
767            C::Trust,
768            K::Char('j'),
769            NONE,
770            A::Down,
771            false,
772            "j",
773            "Next row",
774        ),
775        b(
776            C::Trust,
777            K::Enter,
778            NONE,
779            A::Activate,
780            true,
781            "⏎",
782            "Open / filter",
783        ),
784        b(
785            C::Trust,
786            K::Tab,
787            NONE,
788            A::CyclePanel,
789            true,
790            "Tab",
791            "Cycle panel focus",
792        ),
793        b(
794            C::Trust,
795            K::Char('s'),
796            NONE,
797            A::CycleSort,
798            true,
799            "s",
800            "Cycle queue sort",
801        ),
802        b(
803            C::Trust,
804            K::Char('d'),
805            NONE,
806            A::Remove,
807            false,
808            "d",
809            "Remove concept",
810        ),
811        b(C::Trust, K::Home, NONE, A::Home, true, "Home", "First row"),
812        b(C::Trust, K::End, NONE, A::End, true, "End", "Last row"),
813        // Computations.
814        b(C::Computations, K::Up, NONE, A::Up, true, "↑", "Previous"),
815        b(C::Computations, K::Down, NONE, A::Down, true, "↓", "Next"),
816        b(
817            C::Computations,
818            K::Char('k'),
819            NONE,
820            A::Up,
821            false,
822            "k",
823            "Previous",
824        ),
825        b(
826            C::Computations,
827            K::Char('j'),
828            NONE,
829            A::Down,
830            false,
831            "j",
832            "Next",
833        ),
834        b(
835            C::Computations,
836            K::Tab,
837            NONE,
838            A::CyclePanel,
839            true,
840            "Tab",
841            "Focus contracts / playground",
842        ),
843        b(
844            C::Computations,
845            K::Enter,
846            NONE,
847            A::Activate,
848            true,
849            "⏎",
850            "Open in Explorer",
851        ),
852        b(
853            C::Computations,
854            K::Char('y'),
855            CTRL,
856            A::CopySketch,
857            true,
858            "Ctrl+Y",
859            "Copy invocation sketch",
860        ),
861    ]
862};
863
864/// Whether a key event matches a binding. `SHIFT` is ignored for `Char`
865/// codes, since the char itself already encodes the case.
866fn matches(binding: &Binding, key: &KeyEvent) -> bool {
867    if binding.code != key.code {
868        return false;
869    }
870    let ignore = if matches!(key.code, KeyCode::Char(_)) {
871        KeyModifiers::SHIFT
872    } else {
873        KeyModifiers::NONE
874    };
875    (key.modifiers - ignore) == binding.mods
876}
877
878/// Looks up an action in exactly one context.
879#[must_use]
880pub fn lookup(context: Context, key: &KeyEvent) -> Option<Action> {
881    bindings()
882        .iter()
883        .find(|binding| binding.context == context && matches(binding, key))
884        .map(|binding| binding.action)
885}
886
887/// Resolves a key against a context chain, most specific first.
888#[must_use]
889pub fn resolve(contexts: &[Context], key: &KeyEvent) -> Option<Action> {
890    contexts.iter().find_map(|&c| lookup(c, key))
891}
892
893/// The primary bindings for a context, in table order — the hint bar's feed.
894pub fn hints(context: Context) -> impl Iterator<Item = &'static Binding> {
895    bindings()
896        .iter()
897        .filter(move |binding| binding.context == context && binding.primary)
898}