Krolyxon
Commit 92a131a

Branches

Refs →

budctlcratesbudctl-guisrckeys.rs

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