1use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Scope {
9 Global,
10 Cluster,
11 Monitor,
12 Tokens,
13 Topics,
14 Messages,
16 Profiles,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Action {
21 Quit,
22 GoTab(u8),
23 TabNext,
24 TabPrev,
25 PaneNext,
26 PanePrev,
27 Up,
28 Down,
29 PageUp,
30 PageDown,
31 First,
32 Last,
33 Left,
34 Right,
35 Enter,
36 Esc,
37 Filter,
38 Sort,
39 SortReverse,
40 Refresh,
41 Pause,
42 Palette,
43 Language,
44 Help,
45 Submit,
46 Detail,
47 Drain,
48 Return,
49 FlushNode,
50 FlushAll,
51 StopCluster,
52 Monitor,
53 WindowWider,
54 WindowNarrower,
55 PauseChart,
56 ChartFull,
57 Scale,
58 Export,
59 SwitchProfile,
60 TokenNew,
61 TokenEdit,
62 TokenClone,
63 TokenRevoke,
64 TopicNew,
65 TopicEdit,
66 TopicDelete,
67 TopicDead,
68 TopicSummary,
69 TopicCharts,
70 TopicMessages,
71 MsgPublish,
72 MsgReceive,
73 MsgAck,
74 MsgAckAll,
75 MsgRelease,
76 MsgVisibility,
77 MsgPrune,
78 ProfileUse,
79 ProfileNew,
80 ProfileEdit,
81 ProfileDefault,
82 ProfileRemove,
83 ProfileTest,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum Key {
88 Char(char),
89 Code(KeyCode),
90 Ctrl(char),
91 Alt(KeyCode),
92}
93
94pub fn ctrl_of(event: &KeyEvent) -> bool {
97 event.modifiers.contains(KeyModifiers::CONTROL) && !event.modifiers.contains(KeyModifiers::ALT)
98}
99
100pub fn alt_of(event: &KeyEvent) -> bool {
102 event.modifiers.contains(KeyModifiers::ALT) && !event.modifiers.contains(KeyModifiers::CONTROL)
103}
104
105impl Key {
106 fn matches(self, event: &KeyEvent) -> bool {
107 let ctrl = ctrl_of(event);
108 let alt = alt_of(event);
109 match self {
110 Key::Char(c) => matches!(event.code, KeyCode::Char(e) if e == c) && !ctrl && !alt,
111 Key::Code(code) => event.code == code && !ctrl && !alt,
112 Key::Ctrl(c) => {
113 ctrl && matches!(event.code, KeyCode::Char(e) if e.eq_ignore_ascii_case(&c))
114 }
115 Key::Alt(code) => alt && event.code == code,
116 }
117 }
118}
119
120pub struct Binding {
121 pub action: Action,
122 pub keys: &'static [Key],
123 pub scope: Scope,
124 pub label: &'static str,
126 pub help: Option<&'static str>,
128}
129
130const fn bind(
131 action: Action,
132 scope: Scope,
133 keys: &'static [Key],
134 label: &'static str,
135 help: Option<&'static str>,
136) -> Binding {
137 Binding {
138 action,
139 keys,
140 scope,
141 label,
142 help,
143 }
144}
145
146use Key::{Alt, Char, Code, Ctrl};
147use Scope::{Cluster, Global, Messages, Monitor, Profiles, Tokens, Topics};
148
149pub const BINDINGS: &[Binding] = &[
150 bind(
151 Action::GoTab(0),
152 Global,
153 &[Char('1')],
154 "1…4",
155 Some("keys.tab"),
156 ),
157 bind(Action::GoTab(1), Global, &[Char('2')], "2", None),
158 bind(Action::GoTab(2), Global, &[Char('3')], "3", None),
159 bind(Action::GoTab(3), Global, &[Char('4')], "4", None),
160 bind(
161 Action::TabNext,
162 Global,
163 &[Char(']'), Alt(KeyCode::Right)],
164 "] · Alt+→",
165 Some("keys.tab-next"),
166 ),
167 bind(
168 Action::TabPrev,
169 Global,
170 &[Char('['), Alt(KeyCode::Left)],
171 "[ · Alt+←",
172 Some("keys.tab-prev"),
173 ),
174 bind(
175 Action::PaneNext,
176 Global,
177 &[Code(KeyCode::Tab)],
178 "Tab",
179 Some("keys.pane-next"),
180 ),
181 bind(
182 Action::PanePrev,
183 Global,
184 &[Code(KeyCode::BackTab)],
185 "Shift+Tab",
186 Some("keys.pane-prev"),
187 ),
188 bind(
189 Action::Up,
190 Global,
191 &[Code(KeyCode::Up), Char('k')],
192 "↑ k",
193 Some("keys.up"),
194 ),
195 bind(
196 Action::Down,
197 Global,
198 &[Code(KeyCode::Down), Char('j')],
199 "↓ j",
200 Some("keys.down"),
201 ),
202 bind(
203 Action::PageUp,
204 Global,
205 &[Code(KeyCode::PageUp), Ctrl('u')],
206 "PgUp Ctrl+u",
207 Some("keys.page-up"),
208 ),
209 bind(
210 Action::PageDown,
211 Global,
212 &[Code(KeyCode::PageDown), Ctrl('d')],
213 "PgDn Ctrl+d",
214 Some("keys.page-down"),
215 ),
216 bind(
217 Action::First,
218 Global,
219 &[Code(KeyCode::Home), Char('g')],
220 "Home g",
221 Some("keys.first"),
222 ),
223 bind(
224 Action::Last,
225 Global,
226 &[Code(KeyCode::End), Char('G')],
227 "End G",
228 Some("keys.last"),
229 ),
230 bind(
231 Action::Left,
232 Global,
233 &[Code(KeyCode::Left)],
234 "←",
235 Some("keys.left"),
236 ),
237 bind(
238 Action::Right,
239 Global,
240 &[Code(KeyCode::Right)],
241 "→",
242 Some("keys.right"),
243 ),
244 bind(
245 Action::Enter,
246 Global,
247 &[Code(KeyCode::Enter)],
248 "Enter",
249 Some("keys.enter"),
250 ),
251 bind(
252 Action::Esc,
253 Global,
254 &[Code(KeyCode::Esc)],
255 "Esc",
256 Some("keys.esc"),
257 ),
258 bind(
259 Action::Filter,
260 Global,
261 &[Char('/')],
262 "/",
263 Some("keys.filter"),
264 ),
265 bind(Action::Sort, Global, &[Char('s')], "s", Some("keys.sort")),
266 bind(
267 Action::SortReverse,
268 Global,
269 &[Char('S')],
270 "S",
271 Some("keys.sort-reverse"),
272 ),
273 bind(
274 Action::Refresh,
275 Global,
276 &[Char('r')],
277 "r",
278 Some("keys.refresh"),
279 ),
280 bind(Action::Pause, Global, &[Char('p')], "p", Some("keys.pause")),
281 bind(
282 Action::Palette,
283 Global,
284 &[Char(':')],
285 ":",
286 Some("keys.palette"),
287 ),
288 bind(
289 Action::Language,
290 Global,
291 &[Char('l')],
292 "l",
293 Some("keys.language"),
294 ),
295 bind(
296 Action::Help,
297 Global,
298 &[Char('h'), Char('?'), Code(KeyCode::F(1))],
299 "h F1",
300 Some("keys.help"),
301 ),
302 bind(
303 Action::SwitchProfile,
304 Global,
305 &[Char('o')],
306 "o",
307 Some("keys.profile-switch"),
308 ),
309 bind(
310 Action::Quit,
311 Global,
312 &[Char('q'), Ctrl('c')],
313 "q Ctrl+c",
314 Some("keys.quit"),
315 ),
316 bind(
317 Action::Submit,
318 Global,
319 &[Ctrl('s')],
320 "Ctrl+s",
321 Some("keys.submit"),
322 ),
323 bind(
324 Action::Detail,
325 Cluster,
326 &[Char('i')],
327 "i",
328 Some("keys.cluster.detail"),
329 ),
330 bind(
331 Action::Drain,
332 Cluster,
333 &[Char('d')],
334 "d",
335 Some("keys.cluster.drain"),
336 ),
337 bind(
338 Action::Return,
339 Cluster,
340 &[Char('u')],
341 "u",
342 Some("keys.cluster.return"),
343 ),
344 bind(
345 Action::FlushNode,
346 Cluster,
347 &[Char('b')],
348 "b",
349 Some("keys.cluster.flush-node"),
350 ),
351 bind(
352 Action::FlushAll,
353 Cluster,
354 &[Char('B')],
355 "B",
356 Some("keys.cluster.flush-all"),
357 ),
358 bind(
359 Action::StopCluster,
360 Cluster,
361 &[Char('S')],
362 "S",
363 Some("keys.cluster.stop"),
364 ),
365 bind(
366 Action::Monitor,
367 Cluster,
368 &[Char('m')],
369 "m",
370 Some("keys.cluster.monitor"),
371 ),
372 bind(
373 Action::WindowWider,
374 Monitor,
375 &[Char('+'), Char('=')],
376 "+",
377 Some("keys.monitor.wider"),
378 ),
379 bind(
380 Action::WindowNarrower,
381 Monitor,
382 &[Char('-'), Char('_')],
383 "-",
384 Some("keys.monitor.narrower"),
385 ),
386 bind(
387 Action::PauseChart,
388 Monitor,
389 &[Char(' ')],
390 "Space",
391 Some("keys.monitor.pause"),
392 ),
393 bind(
394 Action::ChartFull,
395 Monitor,
396 &[Code(KeyCode::Enter)],
397 "Enter",
398 Some("keys.monitor.full"),
399 ),
400 bind(
401 Action::Scale,
402 Monitor,
403 &[Char('z')],
404 "z",
405 Some("keys.monitor.scale"),
406 ),
407 bind(
408 Action::Export,
409 Monitor,
410 &[Char('e')],
411 "e",
412 Some("keys.monitor.export"),
413 ),
414 bind(
415 Action::TokenNew,
416 Tokens,
417 &[Char('n')],
418 "n",
419 Some("keys.tokens.new"),
420 ),
421 bind(
422 Action::TokenEdit,
423 Tokens,
424 &[Char('e')],
425 "e",
426 Some("keys.tokens.edit"),
427 ),
428 bind(
429 Action::TokenClone,
430 Tokens,
431 &[Char('c')],
432 "c",
433 Some("keys.tokens.clone"),
434 ),
435 bind(
436 Action::TokenRevoke,
437 Tokens,
438 &[Code(KeyCode::Delete), Char('x')],
439 "Del x",
440 Some("keys.tokens.revoke"),
441 ),
442 bind(
443 Action::TopicNew,
444 Topics,
445 &[Char('n')],
446 "n",
447 Some("keys.topics.new"),
448 ),
449 bind(
450 Action::TopicEdit,
451 Topics,
452 &[Char('e')],
453 "e",
454 Some("keys.topics.edit"),
455 ),
456 bind(
457 Action::TopicDelete,
458 Topics,
459 &[Code(KeyCode::Delete), Char('d')],
460 "Del d",
461 Some("keys.topics.delete"),
462 ),
463 bind(
464 Action::TopicDead,
465 Topics,
466 &[Char('v')],
467 "v",
468 Some("keys.topics.dead"),
469 ),
470 bind(
471 Action::TopicSummary,
472 Topics,
473 &[Char('i')],
474 "i",
475 Some("keys.topics.summary"),
476 ),
477 bind(
478 Action::TopicCharts,
479 Topics,
480 &[Char('g')],
481 "g",
482 Some("keys.topics.charts"),
483 ),
484 bind(
485 Action::TopicMessages,
486 Topics,
487 &[Char('x')],
488 "x",
489 Some("keys.topics.messages"),
490 ),
491 bind(
492 Action::MsgPublish,
493 Messages,
494 &[Char('p')],
495 "p",
496 Some("keys.messages.publish"),
497 ),
498 bind(
499 Action::MsgReceive,
500 Messages,
501 &[Char('r')],
502 "r",
503 Some("keys.messages.receive"),
504 ),
505 bind(
506 Action::MsgAck,
507 Messages,
508 &[Char('a')],
509 "a",
510 Some("keys.messages.ack"),
511 ),
512 bind(
513 Action::MsgAckAll,
514 Messages,
515 &[Char('A')],
516 "A",
517 Some("keys.messages.ack-all"),
518 ),
519 bind(
520 Action::MsgRelease,
521 Messages,
522 &[Char('v')],
523 "v",
524 Some("keys.messages.release"),
525 ),
526 bind(
527 Action::MsgPrune,
528 Messages,
529 &[Char('P')],
530 "P",
531 Some("keys.messages.prune"),
532 ),
533 bind(
534 Action::ProfileUse,
535 Profiles,
536 &[Char('c')],
537 "c",
538 Some("keys.profiles.use"),
539 ),
540 bind(
541 Action::ProfileNew,
542 Profiles,
543 &[Char('n')],
544 "n",
545 Some("keys.profiles.new"),
546 ),
547 bind(
548 Action::ProfileEdit,
549 Profiles,
550 &[Char('e')],
551 "e",
552 Some("keys.profiles.edit"),
553 ),
554 bind(
555 Action::ProfileDefault,
556 Profiles,
557 &[Char('d')],
558 "d",
559 Some("keys.profiles.default"),
560 ),
561 bind(
562 Action::ProfileRemove,
563 Profiles,
564 &[Code(KeyCode::Delete), Char('x')],
565 "Del x",
566 Some("keys.profiles.remove"),
567 ),
568 bind(
569 Action::ProfileTest,
570 Profiles,
571 &[Char('t')],
572 "t",
573 Some("keys.profiles.test"),
574 ),
575];
576
577pub fn resolve(scope: Scope, event: &KeyEvent) -> Option<Action> {
579 let find = |wanted: Scope| {
580 BINDINGS
581 .iter()
582 .filter(|b| b.scope == wanted)
583 .find(|b| b.keys.iter().any(|k| k.matches(event)))
584 .map(|b| b.action)
585 };
586 find(scope).or_else(|| {
587 if scope == Scope::Global {
588 None
589 } else {
590 find(Scope::Global)
591 }
592 })
593}
594
595pub fn help_for(scope: Scope) -> Vec<&'static Binding> {
597 let own = BINDINGS
598 .iter()
599 .filter(|b| b.scope == scope && b.help.is_some());
600 let shadowed = |g: &Binding| {
601 BINDINGS.iter().any(|b| {
602 b.scope == scope
603 && b.scope != Scope::Global
604 && b.keys.iter().any(|k| g.keys.contains(k))
605 })
606 };
607 let global = BINDINGS.iter().filter(|b| {
608 b.scope == Scope::Global && b.help.is_some() && !(scope != Scope::Global && shadowed(b))
609 });
610 own.chain(global).collect()
611}
612
613pub fn binding_of(action: Action) -> Option<&'static Binding> {
615 BINDINGS.iter().find(|b| b.action == action)
616}
617
618#[cfg(test)]
619mod tests;