Krolyxon
Commit 0155f71

Branches

Refs →

budctlcratesbudctl-guisrckeys.rs

181 lines · 6.8 KB · Rust

1//! The keyboard layer: vim-like keys on top of a mouse-first window.
2//!
3//! A cursor moves over the things a page can change. It stays hidden until a key is pressed and
4//! hides again on a click, so mouse users never see it. This module only turns key presses into
5//! [`Action`]s; `App::update` decides what an action does on the current page.
6
7use iced::keyboard::key::Named;
8use iced::keyboard::{Key, Modifiers};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Action {
12 Down,
13 Up,
14 First,
15 Last,
16 /// The previous choice, or off.
17 Left,
18 /// The next choice, or on, or open what the item leads to.
19 Right,
20 /// Toggle, step to the next choice, or open.
21 Activate,
22 NextPage,
23 PreviousPage,
24 /// A page by its place in the sidebar, from 0.
25 Page(usize),
26 /// Read the buds again.
27 Refresh,
28 /// Show or hide the list of keys.
29 Help,
30 /// Close the list of keys, else hide the cursor.
31 Escape,
32 Quit,
33}
34
35/// The keys, as the list of keys shows them.
36pub const HELP: [(&str, &str); 11] = [
37 ("j k ↓ ↑", "move the cursor"),
38 ("h l ← →", "previous / next choice, off / on"),
39 ("Enter Space", "toggle, next choice, or open"),
40 ("gg G", "first / last"),
41 ("J K Ctrl+Tab", "next / previous page"),
42 ("gt gT", "next / previous page"),
43 ("1 … 5", "go to a page"),
44 ("r", "read the buds again"),
45 ("Esc", "hide the cursor"),
46 ("?", "these keys"),
47 ("q", "quit"),
48];
49
50/// Turns key presses into actions. It remembers a `g`, for `gg`, `gt` and `gT`.
51#[derive(Debug, Default)]
52pub struct Keys {
53 after_g: bool,
54}
55
56impl Keys {
57 pub fn press(&mut self, key: &Key, modifiers: Modifiers) -> Option<Action> {
58 let after_g = std::mem::take(&mut self.after_g);
59 let shift = modifiers.shift();
60 match key {
61 Key::Named(named) => Some(match named {
62 Named::ArrowDown => Action::Down,
63 Named::ArrowUp => Action::Up,
64 Named::ArrowLeft => Action::Left,
65 Named::ArrowRight => Action::Right,
66 Named::Tab if modifiers.control() && shift => Action::PreviousPage,
67 Named::Tab if modifiers.control() => Action::NextPage,
68 Named::Tab if shift => Action::Up,
69 Named::Tab => Action::Down,
70 Named::Enter | Named::Space => Action::Activate,
71 Named::Home => Action::First,
72 Named::End => Action::Last,
73 Named::Escape => Action::Escape,
74 _ => return None,
75 }),
76 Key::Character(c) if modifiers.control() => match c.as_str() {
77 "j" => Some(Action::NextPage),
78 "k" => Some(Action::PreviousPage),
79 "q" => Some(Action::Quit),
80 _ => None,
81 },
82 Key::Character(_) if modifiers.alt() || modifiers.logo() => None,
83 Key::Character(c) => match c.as_str() {
84 "g" if after_g => Some(Action::First),
85 "t" if after_g => Some(Action::NextPage),
86 "T" if after_g => Some(Action::PreviousPage),
87 "g" => {
88 self.after_g = true;
89 None
90 }
91 "j" => Some(Action::Down),
92 "k" => Some(Action::Up),
93 "h" => Some(Action::Left),
94 "l" => Some(Action::Right),
95 "G" => Some(Action::Last),
96 "J" => Some(Action::NextPage),
97 "K" => Some(Action::PreviousPage),
98 " " => Some(Action::Activate),
99 "r" => Some(Action::Refresh),
100 "?" => Some(Action::Help),
101 "q" => Some(Action::Quit),
102 digit => match digit.parse::<usize>() {
103 Ok(n @ 1..=9) => Some(Action::Page(n - 1)),
104 _ => None,
105 },
106 },
107 Key::Unidentified => None,
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 fn press(keys: &mut Keys, key: &str) -> Option<Action> {
117 let (key, modifiers) = match key {
118 "Down" => (Key::Named(Named::ArrowDown), Modifiers::empty()),
119 "Enter" => (Key::Named(Named::Enter), Modifiers::empty()),
120 "Space" => (Key::Named(Named::Space), Modifiers::empty()),
121 "Esc" => (Key::Named(Named::Escape), Modifiers::empty()),
122 "Tab" => (Key::Named(Named::Tab), Modifiers::empty()),
123 "S-Tab" => (Key::Named(Named::Tab), Modifiers::SHIFT),
124 "C-Tab" => (Key::Named(Named::Tab), Modifiers::CTRL),
125 "C-S-Tab" => (Key::Named(Named::Tab), Modifiers::CTRL | Modifiers::SHIFT),
126 "C-j" => (Key::Character("j".into()), Modifiers::CTRL),
127 "A-j" => (Key::Character("j".into()), Modifiers::ALT),
128 c if c.chars().next().is_some_and(char::is_uppercase) => (Key::Character(c.into()), Modifiers::SHIFT),
129 c => (Key::Character(c.into()), Modifiers::empty()),
130 };
131 keys.press(&key, modifiers)
132 }
133
134 #[test]
135 fn single_keys() {
136 let mut keys = Keys::default();
137 let cases = [
138 ("j", Some(Action::Down)),
139 ("Down", Some(Action::Down)),
140 ("Tab", Some(Action::Down)),
141 ("k", Some(Action::Up)),
142 ("S-Tab", Some(Action::Up)),
143 ("h", Some(Action::Left)),
144 ("l", Some(Action::Right)),
145 ("Enter", Some(Action::Activate)),
146 ("Space", Some(Action::Activate)),
147 ("G", Some(Action::Last)),
148 ("J", Some(Action::NextPage)),
149 ("K", Some(Action::PreviousPage)),
150 ("C-Tab", Some(Action::NextPage)),
151 ("C-S-Tab", Some(Action::PreviousPage)),
152 ("C-j", Some(Action::NextPage)),
153 ("3", Some(Action::Page(2))),
154 ("0", None),
155 ("r", Some(Action::Refresh)),
156 ("?", Some(Action::Help)),
157 ("Esc", Some(Action::Escape)),
158 ("q", Some(Action::Quit)),
159 ("x", None),
160 ("A-j", None),
161 ];
162 for (key, action) in cases {
163 assert_eq!(press(&mut keys, key), action, "{key}");
164 }
165 }
166
167 #[test]
168 fn g_waits_for_the_next_key() {
169 let mut keys = Keys::default();
170 assert_eq!(press(&mut keys, "g"), None);
171 assert_eq!(press(&mut keys, "g"), Some(Action::First));
172 assert_eq!(press(&mut keys, "g"), None);
173 assert_eq!(press(&mut keys, "t"), Some(Action::NextPage));
174 assert_eq!(press(&mut keys, "g"), None);
175 assert_eq!(press(&mut keys, "T"), Some(Action::PreviousPage));
176 // Anything else after a g counts on its own, and the g is forgotten.
177 assert_eq!(press(&mut keys, "g"), None);
178 assert_eq!(press(&mut keys, "j"), Some(Action::Down));
179 assert_eq!(press(&mut keys, "t"), None);
180 }
181}