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: "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 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
136pub const SYSTEM: &[Binding] = &[
137 Binding {
138 chord: "?",
139 description: "this help overlay",
140 },
141 Binding {
142 chord: "t",
143 description: "open / reopen tutorial",
144 },
145 Binding {
146 chord: "q",
147 description: "quit (with confirm)",
148 },
149 Binding {
150 chord: "Esc",
151 description: "close modal / cancel",
152 },
153];
154
155pub const ALL_GROUPS: &[BindingGroup] = &[
156 BindingGroup {
157 title: "Navigation",
158 bindings: NAVIGATION,
159 },
160 BindingGroup {
161 title: "Layouts",
162 bindings: LAYOUTS,
163 },
164 BindingGroup {
165 title: "Compose",
166 bindings: COMPOSE,
167 },
168 BindingGroup {
169 title: "Stream keys",
170 bindings: STREAM_KEYS,
171 },
172 BindingGroup {
173 title: "Approvals",
174 bindings: APPROVALS,
175 },
176 BindingGroup {
177 title: "System",
178 bindings: SYSTEM,
179 },
180];
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn registry_covers_six_groups() {
188 assert_eq!(ALL_GROUPS.len(), 6);
191 }
192
193 #[test]
194 fn registry_covers_central_chords() {
195 let bindings: Vec<&str> = ALL_GROUPS
196 .iter()
197 .flat_map(|g| g.bindings.iter().map(|b| b.chord))
198 .collect();
199 for must_have in [
200 "Tab",
201 "Ctrl+W",
202 "Ctrl+E",
203 "@",
204 "!",
205 "a",
206 "y",
207 "Shift-N",
208 "?",
209 "t",
210 "q",
211 "Alt+Enter",
212 ] {
213 assert!(
214 bindings.contains(&must_have),
215 "registry missing chord {must_have}"
216 );
217 }
218 }
219}