1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::{
4 error::Error,
5 fmt::{Display, Write},
6};
7
8/// A keystroke and associated metadata generated by the platform
9#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
10pub struct Keystroke {
11 /// the state of the modifier keys at the time the keystroke was generated
12 pub modifiers: Modifiers,
13
14 /// key is the character printed on the key that was pressed
15 /// e.g. for option-s, key is "s"
16 pub key: String,
17
18 /// key_char is the character that could have been typed when
19 /// this binding was pressed.
20 /// e.g. for s this is "s", for option-s "ร", and cmd-s None
21 pub key_char: Option<String>,
22}
23
24/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
25/// markdown to display it.
26#[derive(Debug)]
27pub struct InvalidKeystrokeError {
28 /// The invalid keystroke.
29 pub keystroke: String,
30}
31
32impl Error for InvalidKeystrokeError {}
33
34impl Display for InvalidKeystrokeError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(
37 f,
38 "Invalid keystroke \"{}\". {}",
39 self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
40 )
41 }
42}
43
44/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
45pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
46 (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
47 followed by a key, separated by `-`.";
48
49impl Keystroke {
50 /// When matching a key we cannot know whether the user intended to type
51 /// the key_char or the key itself. On some non-US keyboards keys we use in our
52 /// bindings are behind option (for example `$` is typed `alt-รง` on a Czech keyboard),
53 /// and on some keyboards the IME handler converts a sequence of keys into a
54 /// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
55 ///
56 /// This method assumes that `self` was typed and `target' is in the keymap, and checks
57 /// both possibilities for self against the target.
58 pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
59 if let Some(key_char) = self
60 .key_char
61 .as_ref()
62 .filter(|key_char| key_char != &&self.key)
63 {
64 let ime_modifiers = Modifiers {
65 control: self.modifiers.control,
66 platform: self.modifiers.platform,
67 ..Default::default()
68 };
69
70 if &target.key == key_char && target.modifiers == ime_modifiers {
71 return true;
72 }
73 }
74
75 target.modifiers == self.modifiers && target.key == self.key
76 }
77
78 /// key syntax is:
79 /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
80 /// key_char syntax is only used for generating test events,
81 /// secondary means "cmd" on macOS and "ctrl" on other platforms
82 /// when matching a key with an key_char set will be matched without it.
83 pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
84 let mut control = false;
85 let mut alt = false;
86 let mut shift = false;
87 let mut platform = false;
88 let mut function = false;
89 let mut key = None;
90 let mut key_char = None;
91
92 let mut components = source.split('-').peekable();
93 while let Some(component) = components.next() {
94 match component {
95 "ctrl" => control = true,
96 "alt" => alt = true,
97 "shift" => shift = true,
98 "fn" => function = true,
99 "secondary" => {
100 if cfg!(target_os = "macos") {
101 platform = true
102 } else {
103 control = true
104 };
105 }
106 "cmd" | "super" | "win" => platform = true,
107 _ => {
108 if let Some(next) = components.peek() {
109 if next.is_empty() && source.ends_with('-') {
110 key = Some(String::from("-"));
111 break;
112 } else if next.len() > 1 && next.starts_with('>') {
113 key = Some(String::from(component));
114 key_char = Some(String::from(&next[1..]));
115 components.next();
116 } else {
117 return Err(InvalidKeystrokeError {
118 keystroke: source.to_owned(),
119 });
120 }
121 } else {
122 key = Some(String::from(component));
123 }
124 }
125 }
126 }
127
128 // Allow for the user to specify a keystroke modifier as the key itself
129 // This sets the `key` to the modifier, and disables the modifier
130 if key.is_none() {
131 if shift {
132 key = Some("shift".to_string());
133 shift = false;
134 } else if control {
135 key = Some("control".to_string());
136 control = false;
137 } else if alt {
138 key = Some("alt".to_string());
139 alt = false;
140 } else if platform {
141 key = Some("platform".to_string());
142 platform = false;
143 } else if function {
144 key = Some("function".to_string());
145 function = false;
146 }
147 }
148
149 let key = key.ok_or_else(|| InvalidKeystrokeError {
150 keystroke: source.to_owned(),
151 })?;
152
153 Ok(Keystroke {
154 modifiers: Modifiers {
155 control,
156 alt,
157 shift,
158 platform,
159 function,
160 },
161 key,
162 key_char: key_char,
163 })
164 }
165
166 /// Produces a representation of this key that Parse can understand.
167 pub fn unparse(&self) -> String {
168 let mut str = String::new();
169 if self.modifiers.function {
170 str.push_str("fn-");
171 }
172 if self.modifiers.control {
173 str.push_str("ctrl-");
174 }
175 if self.modifiers.alt {
176 str.push_str("alt-");
177 }
178 if self.modifiers.platform {
179 #[cfg(target_os = "macos")]
180 str.push_str("cmd-");
181
182 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
183 str.push_str("super-");
184
185 #[cfg(target_os = "windows")]
186 str.push_str("win-");
187 }
188 if self.modifiers.shift {
189 str.push_str("shift-");
190 }
191 str.push_str(&self.key);
192 str
193 }
194
195 /// Returns true if this keystroke left
196 /// the ime system in an incomplete state.
197 pub fn is_ime_in_progress(&self) -> bool {
198 self.key_char.is_none()
199 && (is_printable_key(&self.key) || self.key.is_empty())
200 && !(self.modifiers.platform
201 || self.modifiers.control
202 || self.modifiers.function
203 || self.modifiers.alt)
204 }
205
206 /// Returns a new keystroke with the key_char filled.
207 /// This is used for dispatch_keystroke where we want users to
208 /// be able to simulate typing "space", etc.
209 pub fn with_simulated_ime(mut self) -> Self {
210 if self.key_char.is_none()
211 && !self.modifiers.platform
212 && !self.modifiers.control
213 && !self.modifiers.function
214 && !self.modifiers.alt
215 {
216 self.key_char = match self.key.as_str() {
217 "space" => Some(" ".into()),
218 "tab" => Some("\t".into()),
219 "enter" => Some("\n".into()),
220 key if !is_printable_key(key) || key.is_empty() => None,
221 key => {
222 if self.modifiers.shift {
223 Some(key.to_uppercase())
224 } else {
225 Some(key.into())
226 }
227 }
228 }
229 }
230 self
231 }
232}
233
234fn is_printable_key(key: &str) -> bool {
235 !matches!(
236 key,
237 "f1" | "f2"
238 | "f3"
239 | "f4"
240 | "f5"
241 | "f6"
242 | "f7"
243 | "f8"
244 | "f9"
245 | "f10"
246 | "f11"
247 | "f12"
248 | "f13"
249 | "f14"
250 | "f15"
251 | "f16"
252 | "f17"
253 | "f18"
254 | "f19"
255 | "f20"
256 | "f21"
257 | "f22"
258 | "f23"
259 | "f24"
260 | "f25"
261 | "f26"
262 | "f27"
263 | "f28"
264 | "f29"
265 | "f30"
266 | "f31"
267 | "f32"
268 | "f33"
269 | "f34"
270 | "f35"
271 | "backspace"
272 | "delete"
273 | "left"
274 | "right"
275 | "up"
276 | "down"
277 | "pageup"
278 | "pagedown"
279 | "insert"
280 | "home"
281 | "end"
282 | "back"
283 | "forward"
284 | "escape"
285 )
286}
287
288impl std::fmt::Display for Keystroke {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 if self.modifiers.control {
291 f.write_char('^')?;
292 }
293 if self.modifiers.alt {
294 f.write_char('โฅ')?;
295 }
296 if self.modifiers.platform {
297 #[cfg(target_os = "macos")]
298 f.write_char('โ')?;
299
300 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
301 f.write_char('โ')?;
302
303 #[cfg(target_os = "windows")]
304 f.write_char('โ')?;
305 }
306 if self.modifiers.shift {
307 f.write_char('โง')?;
308 }
309 let key = match self.key.as_str() {
310 "backspace" => 'โซ',
311 "up" => 'โ',
312 "down" => 'โ',
313 "left" => 'โ',
314 "right" => 'โ',
315 "tab" => 'โฅ',
316 "escape" => 'โ',
317 "shift" => 'โง',
318 "control" => 'โ',
319 "alt" => 'โฅ',
320 "platform" => 'โ',
321 key => {
322 if key.len() == 1 {
323 key.chars().next().unwrap().to_ascii_uppercase()
324 } else {
325 return f.write_str(key);
326 }
327 }
328 };
329 f.write_char(key)
330 }
331}
332
333/// The state of the modifier keys at some point in time
334#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
335pub struct Modifiers {
336 /// The control key
337 #[serde(default)]
338 pub control: bool,
339
340 /// The alt key
341 /// Sometimes also known as the 'meta' key
342 #[serde(default)]
343 pub alt: bool,
344
345 /// The shift key
346 #[serde(default)]
347 pub shift: bool,
348
349 /// The command key, on macos
350 /// the windows key, on windows
351 /// the super key, on linux
352 #[serde(default)]
353 pub platform: bool,
354
355 /// The function key
356 #[serde(default)]
357 pub function: bool,
358}
359
360impl Modifiers {
361 /// Returns whether any modifier key is pressed.
362 pub fn modified(&self) -> bool {
363 self.control || self.alt || self.shift || self.platform || self.function
364 }
365
366 /// Whether the semantically 'secondary' modifier key is pressed.
367 ///
368 /// On macOS, this is the command key.
369 /// On Linux and Windows, this is the control key.
370 pub fn secondary(&self) -> bool {
371 #[cfg(target_os = "macos")]
372 {
373 self.platform
374 }
375
376 #[cfg(not(target_os = "macos"))]
377 {
378 self.control
379 }
380 }
381
382 /// Returns how many modifier keys are pressed.
383 pub fn number_of_modifiers(&self) -> u8 {
384 self.control as u8
385 + self.alt as u8
386 + self.shift as u8
387 + self.platform as u8
388 + self.function as u8
389 }
390
391 /// Returns [`Modifiers`] with no modifiers.
392 pub fn none() -> Modifiers {
393 Default::default()
394 }
395
396 /// Returns [`Modifiers`] with just the command key.
397 pub fn command() -> Modifiers {
398 Modifiers {
399 platform: true,
400 ..Default::default()
401 }
402 }
403
404 /// A Returns [`Modifiers`] with just the secondary key pressed.
405 pub fn secondary_key() -> Modifiers {
406 #[cfg(target_os = "macos")]
407 {
408 Modifiers {
409 platform: true,
410 ..Default::default()
411 }
412 }
413
414 #[cfg(not(target_os = "macos"))]
415 {
416 Modifiers {
417 control: true,
418 ..Default::default()
419 }
420 }
421 }
422
423 /// Returns [`Modifiers`] with just the windows key.
424 pub fn windows() -> Modifiers {
425 Modifiers {
426 platform: true,
427 ..Default::default()
428 }
429 }
430
431 /// Returns [`Modifiers`] with just the super key.
432 pub fn super_key() -> Modifiers {
433 Modifiers {
434 platform: true,
435 ..Default::default()
436 }
437 }
438
439 /// Returns [`Modifiers`] with just control.
440 pub fn control() -> Modifiers {
441 Modifiers {
442 control: true,
443 ..Default::default()
444 }
445 }
446
447 /// Returns [`Modifiers`] with just alt.
448 pub fn alt() -> Modifiers {
449 Modifiers {
450 alt: true,
451 ..Default::default()
452 }
453 }
454
455 /// Returns [`Modifiers`] with just shift.
456 pub fn shift() -> Modifiers {
457 Modifiers {
458 shift: true,
459 ..Default::default()
460 }
461 }
462
463 /// Returns [`Modifiers`] with command + shift.
464 pub fn command_shift() -> Modifiers {
465 Modifiers {
466 shift: true,
467 platform: true,
468 ..Default::default()
469 }
470 }
471
472 /// Returns [`Modifiers`] with command + shift.
473 pub fn control_shift() -> Modifiers {
474 Modifiers {
475 shift: true,
476 control: true,
477 ..Default::default()
478 }
479 }
480
481 /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
482 pub fn is_subset_of(&self, other: &Modifiers) -> bool {
483 (other.control || !self.control)
484 && (other.alt || !self.alt)
485 && (other.shift || !self.shift)
486 && (other.platform || !self.platform)
487 && (other.function || !self.function)
488 }
489}