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 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}