1#[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: "enter stream-keys mode — forward keys to focused agent's tmux pane (when detail focused)",
110 },
111 Binding {
112 chord: "Ctrl+E",
113 description: "exit stream-keys mode (Esc forwards to the pane, e.g. to Claude Code)",
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
136pub 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 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 "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}