Skip to main content

teamctl_ui/
help.rs

1//! `?` help overlay — keymap registry + grouped binding list.
2//!
3//! The registry is the single source of truth for "what chords
4//! this UI accepts." Both the help-overlay renderer and the
5//! statusline's contextual hints read from this slice; the event
6//! loop in `app.rs` references the same chord constants so the
7//! help text never lies about what's wired up.
8
9#[derive(Debug, Clone, Copy)]
10pub struct Binding {
11    pub chord: &'static str,
12    pub description: &'static str,
13}
14
15#[derive(Debug, Clone, Copy)]
16pub struct BindingGroup {
17    pub title: &'static str,
18    pub bindings: &'static [Binding],
19}
20
21pub const NAVIGATION: &[Binding] = &[
22    Binding {
23        chord: "Tab",
24        description: "cycle pane focus forward",
25    },
26    Binding {
27        chord: "Shift+Tab",
28        description: "cycle pane focus backward",
29    },
30    Binding {
31        chord: "j / k / ↓ / ↑",
32        description: "navigate within focused pane",
33    },
34    Binding {
35        chord: "[ / ]",
36        description: "walk mailbox tabs (when mailbox focused)",
37    },
38    Binding {
39        chord: "Enter",
40        description: "open / drill in",
41    },
42];
43
44pub const LAYOUTS: &[Binding] = &[
45    Binding {
46        chord: "Ctrl+W",
47        description: "toggle Wall layout",
48    },
49    Binding {
50        chord: "Ctrl+M",
51        description: "toggle Mailbox-first layout",
52    },
53    Binding {
54        chord: "Ctrl+|",
55        description: "split detail pane vertically",
56    },
57    Binding {
58        chord: "Ctrl+-",
59        description: "split detail pane horizontally",
60    },
61    Binding {
62        chord: "Ctrl+H/J/K/L",
63        description: "vim window-motion across splits",
64    },
65    Binding {
66        chord: "Ctrl+W q / Ctrl+Q",
67        description: "close focused split",
68    },
69];
70
71pub const COMPOSE: &[Binding] = &[
72    Binding {
73        chord: "@",
74        description: "DM the focused agent",
75    },
76    Binding {
77        chord: "!",
78        description: "broadcast to a channel (picker)",
79    },
80    Binding {
81        chord: "Alt+Enter",
82        description: "send the composed message",
83    },
84    Binding {
85        chord: "Esc Esc",
86        description: "cancel compose",
87    },
88    Binding {
89        chord: ":wq / :q",
90        description: "ex-command send / cancel",
91    },
92    Binding {
93        chord: "i / a / o",
94        description: "enter insert mode",
95    },
96    Binding {
97        chord: "w / b / e",
98        description: "word motions in normal mode",
99    },
100    Binding {
101        chord: "dd / yy / p",
102        description: "line ops in normal mode",
103    },
104];
105
106pub const APPROVALS: &[Binding] = &[
107    Binding {
108        chord: "a",
109        description: "open approvals modal (when pending)",
110    },
111    Binding {
112        chord: "y",
113        description: "approve focused",
114    },
115    Binding {
116        chord: "Shift-N",
117        description: "deny focused (Shift-gated)",
118    },
119    Binding {
120        chord: "j / k",
121        description: "cycle through pending approvals",
122    },
123];
124
125pub const SYSTEM: &[Binding] = &[
126    Binding {
127        chord: "?",
128        description: "this help overlay",
129    },
130    Binding {
131        chord: "t",
132        description: "open / reopen tutorial",
133    },
134    Binding {
135        chord: "q",
136        description: "quit (with confirm)",
137    },
138    Binding {
139        chord: "Esc",
140        description: "close modal / cancel",
141    },
142];
143
144pub const ALL_GROUPS: &[BindingGroup] = &[
145    BindingGroup {
146        title: "Navigation",
147        bindings: NAVIGATION,
148    },
149    BindingGroup {
150        title: "Layouts",
151        bindings: LAYOUTS,
152    },
153    BindingGroup {
154        title: "Compose",
155        bindings: COMPOSE,
156    },
157    BindingGroup {
158        title: "Approvals",
159        bindings: APPROVALS,
160    },
161    BindingGroup {
162        title: "System",
163        bindings: SYSTEM,
164    },
165];
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn registry_covers_five_groups() {
173        assert_eq!(ALL_GROUPS.len(), 5);
174    }
175
176    #[test]
177    fn registry_covers_central_chords() {
178        let bindings: Vec<&str> = ALL_GROUPS
179            .iter()
180            .flat_map(|g| g.bindings.iter().map(|b| b.chord))
181            .collect();
182        for must_have in [
183            "Tab",
184            "Ctrl+W",
185            "@",
186            "!",
187            "a",
188            "y",
189            "Shift-N",
190            "?",
191            "t",
192            "q",
193            "Alt+Enter",
194        ] {
195            assert!(
196                bindings.contains(&must_have),
197                "registry missing chord {must_have}"
198            );
199        }
200    }
201}