Skip to main content

taba_cli/tui/keymap/
mod.rs

1//! The one table of keys (doc 34, section 5): action ⇄ keys. The program reads its keys from here, and the help
2//! (`?`), the footer and the command palette (`:`) are generated from the same table, so they never disagree.
3
4use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5
6/// Where a binding applies. A screen's scope is looked up before `Global`, so it may shadow a global key.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Scope {
9    Global,
10    Cluster,
11    Monitor,
12    Tokens,
13    Topics,
14    /// The Messages view of an open topic.
15    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
94/// Ctrl on its own. Windows reports AltGr (how `/`, `?` and `@` are typed on many layouts, ABNT2 among them) as Ctrl+Alt
95/// together: that is a typed character, never a Ctrl shortcut.
96pub fn ctrl_of(event: &KeyEvent) -> bool {
97    event.modifiers.contains(KeyModifiers::CONTROL) && !event.modifiers.contains(KeyModifiers::ALT)
98}
99
100/// Alt on its own (see [`ctrl_of`]).
101pub 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    /// The keys as the help shows them (the same in every language).
125    pub label: &'static str,
126    /// The catalog key of the action's description; `None` keeps the binding out of the help and the palette.
127    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
577/// The action of a key in a screen: its own scope first, then the global one.
578pub 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
595/// What a scope's help lists: the scope's own bindings, then the global ones that are not shadowed.
596pub 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
613/// The first binding of an action, for the footer.
614pub fn binding_of(action: Action) -> Option<&'static Binding> {
615    BINDINGS.iter().find(|b| b.action == action)
616}
617
618#[cfg(test)]
619mod tests;