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: "Esc Enter",
82        description: "send the composed message (terminal-universal)",
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 STREAM_KEYS: &[Binding] = &[
107    Binding {
108        chord: "Ctrl+E",
109        description: "stream keys to focused agent's tmux pane (when detail focused)",
110    },
111    Binding {
112        chord: "Esc",
113        description: "exit stream-keys mode",
114    },
115];
116
117pub const APPROVALS: &[Binding] = &[
118    Binding {
119        chord: "a",
120        description: "open approvals modal (when pending)",
121    },
122    Binding {
123        chord: "y",
124        description: "approve focused",
125    },
126    Binding {
127        chord: "Shift-N",
128        description: "deny focused (Shift-gated)",
129    },
130    Binding {
131        chord: "j / k",
132        description: "cycle through pending approvals",
133    },
134];
135
136// T-131: per-row mailbox UX — row scroll (PR-1), filter+search
137// (PR-2), detail modal (PR-3), time indicator (PR-4). All gated on
138// `Pane::Mailbox` focused.
139pub const MAILBOX: &[Binding] = &[
140    Binding {
141        chord: "j / k / ↓ / ↑",
142        description: "row down / up (when mailbox focused)",
143    },
144    Binding {
145        chord: "PageDown / PageUp",
146        description: "jump a screen down / up",
147    },
148    Binding {
149        chord: "Home / End",
150        description: "jump to first / last row",
151    },
152    Binding {
153        chord: "← / →",
154        description: "cycle mailbox tabs",
155    },
156    Binding {
157        chord: "f",
158        description: "filter rows by sender substring (Esc reverts, Enter keeps)",
159    },
160    Binding {
161        chord: "/",
162        description: "search rows by body substring (Esc reverts, Enter keeps)",
163    },
164    Binding {
165        chord: "Enter",
166        description: "open detail modal on the selected row",
167    },
168    Binding {
169        chord: "Esc / q (in modal)",
170        description: "close the detail modal",
171    },
172];
173
174pub const SYSTEM: &[Binding] = &[
175    Binding {
176        chord: "?",
177        description: "this help overlay",
178    },
179    Binding {
180        chord: "t",
181        description: "open / reopen tutorial",
182    },
183    Binding {
184        chord: "q",
185        description: "quit (with confirm)",
186    },
187    Binding {
188        chord: "Esc",
189        description: "close modal / cancel",
190    },
191];
192
193pub const ALL_GROUPS: &[BindingGroup] = &[
194    BindingGroup {
195        title: "Navigation",
196        bindings: NAVIGATION,
197    },
198    BindingGroup {
199        title: "Layouts",
200        bindings: LAYOUTS,
201    },
202    BindingGroup {
203        title: "Compose",
204        bindings: COMPOSE,
205    },
206    BindingGroup {
207        title: "Stream keys",
208        bindings: STREAM_KEYS,
209    },
210    BindingGroup {
211        title: "Mailbox",
212        bindings: MAILBOX,
213    },
214    BindingGroup {
215        title: "Approvals",
216        bindings: APPROVALS,
217    },
218    BindingGroup {
219        title: "System",
220        bindings: SYSTEM,
221    },
222];
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn registry_covers_seven_groups() {
230        // T-108 added Stream-keys between Compose and Approvals;
231        // T-131 PR-4 added Mailbox between Stream-keys and
232        // Approvals. Update this count when groups change.
233        assert_eq!(ALL_GROUPS.len(), 7);
234    }
235
236    #[test]
237    fn registry_covers_central_chords() {
238        let bindings: Vec<&str> = ALL_GROUPS
239            .iter()
240            .flat_map(|g| g.bindings.iter().map(|b| b.chord))
241            .collect();
242        for must_have in [
243            "Tab",
244            "Ctrl+W",
245            "Ctrl+E",
246            "@",
247            "!",
248            "a",
249            "y",
250            "Shift-N",
251            "?",
252            "t",
253            "q",
254            "Esc Enter",
255            // T-131 PR-4: mailbox UX chords surfaced in the
256            // registry so the help overlay never lies about what's
257            // wired up.
258            "f",
259            "/",
260            "PageDown / PageUp",
261            "Home / End",
262            "Esc / q (in modal)",
263        ] {
264            assert!(
265                bindings.contains(&must_have),
266                "registry missing chord {must_have}"
267            );
268        }
269    }
270}