1use egui::{KeyboardShortcut, ModifierNames, Modifiers, Vec2};
2use eyre::Result;
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::SystemState;
6use crate::message::{Message, MessageTarget};
7use crate::wave_data::{PER_SCROLL_EVENT, SCROLL_EVENTS_PER_PAGE};
8
9const NUMBER_OF_SHORTCUTS: usize = 32; #[derive(Clone, Copy, Debug)]
13pub enum ShortcutAction {
14 OpenFile,
15 SwitchFile,
16 Redo,
17 Undo,
18 ToggleSidePanel,
19 ToggleToolbar,
20 GoToEnd,
21 GoToStart,
22 SaveStateFile,
23 GoToTop,
24 GoToBottom,
25 ItemFocus,
26 GroupNew,
27 SelectAll,
28 SelectToggle,
29 ReloadWaveform,
30 ZoomIn,
31 ZoomOut,
32 UiZoomIn,
33 UiZoomOut,
34 ScrollUp,
35 ScrollDown,
36 DeleteSelected,
37 MarkerAdd,
38 ToggleMenu,
39 ShowCommandPrompt,
40 RenameItem,
41 DividerAdd,
42 ZoomToFit,
43 ZoomToCursor,
44 GoToTime,
45 FocusVariableNameFilter,
46}
47
48#[derive(Clone, Debug)]
50struct DispatchEntry {
51 action: ShortcutAction,
52 priority: u8,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct SurferShortcuts {
57 #[serde(with = "keyboard_shortcuts_serde")]
58 pub open_file: Vec<KeyboardShortcut>,
59 #[serde(with = "keyboard_shortcuts_serde")]
60 pub switch_file: Vec<KeyboardShortcut>,
61 #[serde(with = "keyboard_shortcuts_serde")]
62 pub undo: Vec<KeyboardShortcut>,
63 #[serde(with = "keyboard_shortcuts_serde")]
64 pub redo: Vec<KeyboardShortcut>,
65 #[serde(with = "keyboard_shortcuts_serde")]
66 pub toggle_side_panel: Vec<KeyboardShortcut>,
67 #[serde(with = "keyboard_shortcuts_serde")]
68 pub toggle_toolbar: Vec<KeyboardShortcut>,
69 #[serde(with = "keyboard_shortcuts_serde")]
70 pub goto_end: Vec<KeyboardShortcut>,
71 #[serde(with = "keyboard_shortcuts_serde")]
72 pub goto_start: Vec<KeyboardShortcut>,
73 #[serde(with = "keyboard_shortcuts_serde")]
74 pub save_state_file: Vec<KeyboardShortcut>,
75 #[serde(with = "keyboard_shortcuts_serde")]
76 pub goto_top: Vec<KeyboardShortcut>,
77 #[serde(with = "keyboard_shortcuts_serde")]
78 pub goto_bottom: Vec<KeyboardShortcut>,
79 #[serde(with = "keyboard_shortcuts_serde")]
80 pub group_new: Vec<KeyboardShortcut>,
81 #[serde(with = "keyboard_shortcuts_serde")]
82 pub item_focus: Vec<KeyboardShortcut>,
83 #[serde(with = "keyboard_shortcuts_serde")]
84 pub select_all: Vec<KeyboardShortcut>,
85 #[serde(with = "keyboard_shortcuts_serde")]
86 pub select_toggle: Vec<KeyboardShortcut>,
87 #[serde(with = "keyboard_shortcuts_serde")]
88 pub reload_waveform: Vec<KeyboardShortcut>,
89 #[serde(with = "keyboard_shortcuts_serde")]
90 pub zoom_in: Vec<KeyboardShortcut>,
91 #[serde(with = "keyboard_shortcuts_serde")]
92 pub zoom_out: Vec<KeyboardShortcut>,
93 #[serde(with = "keyboard_shortcuts_serde")]
94 pub ui_zoom_in: Vec<KeyboardShortcut>,
95 #[serde(with = "keyboard_shortcuts_serde")]
96 pub ui_zoom_out: Vec<KeyboardShortcut>,
97 #[serde(with = "keyboard_shortcuts_serde")]
98 pub scroll_up: Vec<KeyboardShortcut>,
99 #[serde(with = "keyboard_shortcuts_serde")]
100 pub scroll_down: Vec<KeyboardShortcut>,
101 #[serde(with = "keyboard_shortcuts_serde")]
102 pub delete_selected: Vec<KeyboardShortcut>,
103 #[serde(with = "keyboard_shortcuts_serde")]
104 pub marker_add: Vec<KeyboardShortcut>,
105 #[serde(with = "keyboard_shortcuts_serde")]
106 pub toggle_menu: Vec<KeyboardShortcut>,
107 #[serde(with = "keyboard_shortcuts_serde")]
108 pub show_command_prompt: Vec<KeyboardShortcut>,
109 #[serde(with = "keyboard_shortcuts_serde")]
110 pub rename_item: Vec<KeyboardShortcut>,
111 #[serde(with = "keyboard_shortcuts_serde")]
112 pub divider_add: Vec<KeyboardShortcut>,
113 #[serde(with = "keyboard_shortcuts_serde")]
114 pub zoom_to_fit: Vec<KeyboardShortcut>,
115 #[serde(with = "keyboard_shortcuts_serde")]
116 pub zoom_to_cursor: Vec<KeyboardShortcut>,
117 #[serde(with = "keyboard_shortcuts_serde")]
118 pub go_to_time: Vec<KeyboardShortcut>,
119 #[serde(with = "keyboard_shortcuts_serde")]
120 pub focus_variable_name_filter: Vec<KeyboardShortcut>,
121
122 #[serde(skip)]
123 cached_dispatch_table: Vec<DispatchEntry>,
124}
125
126pub fn deserialize_shortcuts<'de, D>(deserializer: D) -> Result<SurferShortcuts, D::Error>
127where
128 D: Deserializer<'de>,
129{
130 let mut shortcuts = SurferShortcuts::deserialize(deserializer)?;
131 shortcuts.cached_dispatch_table = shortcuts.build_dispatch_table();
132 Ok(shortcuts)
133}
134
135impl SurferShortcuts {
136 #[must_use]
137 pub fn format_shortcut(&self, action: ShortcutAction) -> String {
138 #[cfg(any(not(target_os = "macos"), test))]
141 let is_mac = false;
142 #[cfg(all(target_os = "macos", not(test)))]
143 let is_mac = true;
144 self.shortcuts_for_action(action)
145 .iter()
146 .map(|kb| kb.format(&ModifierNames::NAMES, is_mac))
147 .collect::<Vec<String>>()
148 .join("/")
149 }
150
151 fn build_dispatch_table(&self) -> Vec<DispatchEntry> {
152 let mut dispatch_table = Vec::with_capacity(NUMBER_OF_SHORTCUTS);
154
155 dispatch_table.extend_from_slice(&[
157 DispatchEntry {
158 action: ShortcutAction::OpenFile,
159 priority: modifier_priority(&self.open_file),
160 },
161 DispatchEntry {
162 action: ShortcutAction::SwitchFile,
163 priority: modifier_priority(&self.switch_file),
164 },
165 DispatchEntry {
166 action: ShortcutAction::Redo,
167 priority: modifier_priority(&self.redo),
168 },
169 DispatchEntry {
170 action: ShortcutAction::Undo,
171 priority: modifier_priority(&self.undo),
172 },
173 DispatchEntry {
174 action: ShortcutAction::ToggleSidePanel,
175 priority: modifier_priority(&self.toggle_side_panel),
176 },
177 DispatchEntry {
178 action: ShortcutAction::ToggleToolbar,
179 priority: modifier_priority(&self.toggle_toolbar),
180 },
181 DispatchEntry {
182 action: ShortcutAction::GoToEnd,
183 priority: modifier_priority(&self.goto_end),
184 },
185 DispatchEntry {
186 action: ShortcutAction::GoToStart,
187 priority: modifier_priority(&self.goto_start),
188 },
189 DispatchEntry {
190 action: ShortcutAction::SaveStateFile,
191 priority: modifier_priority(&self.save_state_file),
192 },
193 DispatchEntry {
194 action: ShortcutAction::GoToTop,
195 priority: modifier_priority(&self.goto_top),
196 },
197 DispatchEntry {
198 action: ShortcutAction::GoToBottom,
199 priority: modifier_priority(&self.goto_bottom),
200 },
201 DispatchEntry {
202 action: ShortcutAction::GroupNew,
203 priority: modifier_priority(&self.group_new),
204 },
205 DispatchEntry {
206 action: ShortcutAction::ItemFocus,
207 priority: modifier_priority(&self.item_focus),
208 },
209 DispatchEntry {
210 action: ShortcutAction::SelectAll,
211 priority: modifier_priority(&self.select_all),
212 },
213 DispatchEntry {
214 action: ShortcutAction::SelectToggle,
215 priority: modifier_priority(&self.select_toggle),
216 },
217 DispatchEntry {
218 action: ShortcutAction::ReloadWaveform,
219 priority: modifier_priority(&self.reload_waveform),
220 },
221 DispatchEntry {
222 action: ShortcutAction::ZoomIn,
223 priority: modifier_priority(&self.zoom_in),
224 },
225 DispatchEntry {
226 action: ShortcutAction::ZoomOut,
227 priority: modifier_priority(&self.zoom_out),
228 },
229 DispatchEntry {
230 action: ShortcutAction::UiZoomIn,
231 priority: modifier_priority(&self.ui_zoom_in),
232 },
233 DispatchEntry {
234 action: ShortcutAction::UiZoomOut,
235 priority: modifier_priority(&self.ui_zoom_out),
236 },
237 DispatchEntry {
238 action: ShortcutAction::ScrollUp,
239 priority: modifier_priority(&self.scroll_up),
240 },
241 DispatchEntry {
242 action: ShortcutAction::ScrollDown,
243 priority: modifier_priority(&self.scroll_down),
244 },
245 DispatchEntry {
246 action: ShortcutAction::DeleteSelected,
247 priority: modifier_priority(&self.delete_selected),
248 },
249 DispatchEntry {
250 action: ShortcutAction::MarkerAdd,
251 priority: modifier_priority(&self.marker_add),
252 },
253 DispatchEntry {
254 action: ShortcutAction::ToggleMenu,
255 priority: modifier_priority(&self.toggle_menu),
256 },
257 DispatchEntry {
258 action: ShortcutAction::ShowCommandPrompt,
259 priority: modifier_priority(&self.show_command_prompt),
260 },
261 DispatchEntry {
262 action: ShortcutAction::RenameItem,
263 priority: modifier_priority(&self.rename_item),
264 },
265 DispatchEntry {
266 action: ShortcutAction::DividerAdd,
267 priority: modifier_priority(&self.divider_add),
268 },
269 DispatchEntry {
270 action: ShortcutAction::ZoomToFit,
271 priority: modifier_priority(&self.zoom_to_fit),
272 },
273 DispatchEntry {
274 action: ShortcutAction::ZoomToCursor,
275 priority: modifier_priority(&self.zoom_to_cursor),
276 },
277 DispatchEntry {
278 action: ShortcutAction::GoToTime,
279 priority: modifier_priority(&self.go_to_time),
280 },
281 DispatchEntry {
282 action: ShortcutAction::FocusVariableNameFilter,
283 priority: modifier_priority(&self.focus_variable_name_filter),
284 },
285 ]);
286
287 debug_assert!(
288 dispatch_table.len() == NUMBER_OF_SHORTCUTS,
289 "Dispatch table length does not match the number of defined shortcut actions. Update the NUMBER_OF_SHORTCUTS constant accordingly."
290 );
291
292 dispatch_table.sort_by_key(|entry| entry.priority);
294 dispatch_table
295 }
296
297 fn shortcuts_for_action(&self, action: ShortcutAction) -> &[KeyboardShortcut] {
299 match action {
300 ShortcutAction::OpenFile => &self.open_file,
301 ShortcutAction::SwitchFile => &self.switch_file,
302 ShortcutAction::Undo => &self.undo,
303 ShortcutAction::Redo => &self.redo,
304 ShortcutAction::ToggleSidePanel => &self.toggle_side_panel,
305 ShortcutAction::ToggleToolbar => &self.toggle_toolbar,
306 ShortcutAction::GoToEnd => &self.goto_end,
307 ShortcutAction::GoToStart => &self.goto_start,
308 ShortcutAction::SaveStateFile => &self.save_state_file,
309 ShortcutAction::GoToTop => &self.goto_top,
310 ShortcutAction::GoToBottom => &self.goto_bottom,
311 ShortcutAction::ItemFocus => &self.item_focus,
312 ShortcutAction::GroupNew => &self.group_new,
313 ShortcutAction::SelectAll => &self.select_all,
314 ShortcutAction::SelectToggle => &self.select_toggle,
315 ShortcutAction::ReloadWaveform => &self.reload_waveform,
316 ShortcutAction::ZoomIn => &self.zoom_in,
317 ShortcutAction::ZoomOut => &self.zoom_out,
318 ShortcutAction::UiZoomIn => &self.ui_zoom_in,
319 ShortcutAction::UiZoomOut => &self.ui_zoom_out,
320 ShortcutAction::ScrollUp => &self.scroll_up,
321 ShortcutAction::ScrollDown => &self.scroll_down,
322 ShortcutAction::DeleteSelected => &self.delete_selected,
323 ShortcutAction::MarkerAdd => &self.marker_add,
324 ShortcutAction::ToggleMenu => &self.toggle_menu,
325 ShortcutAction::ShowCommandPrompt => &self.show_command_prompt,
326 ShortcutAction::RenameItem => &self.rename_item,
327 ShortcutAction::DividerAdd => &self.divider_add,
328 ShortcutAction::ZoomToFit => &self.zoom_to_fit,
329 ShortcutAction::ZoomToCursor => &self.zoom_to_cursor,
330 ShortcutAction::GoToTime => &self.go_to_time,
331 ShortcutAction::FocusVariableNameFilter => &self.focus_variable_name_filter,
332 }
333 }
334
335 fn execute_action(&self, action: ShortcutAction, msgs: &mut Vec<Message>, state: &SystemState) {
337 match action {
338 ShortcutAction::OpenFile => {
339 msgs.push(Message::OpenFileDialog(crate::file_dialog::OpenMode::Open));
340 }
341 ShortcutAction::SwitchFile => {
342 msgs.push(Message::OpenFileDialog(
343 crate::file_dialog::OpenMode::Switch,
344 ));
345 }
346 ShortcutAction::Redo => {
347 msgs.push(Message::Redo(state.get_count()));
348 }
349 ShortcutAction::Undo => {
350 msgs.push(Message::Undo(state.get_count()));
351 }
352 ShortcutAction::ToggleSidePanel => {
353 msgs.push(Message::SetSidePanelVisible(!state.show_hierarchy()));
354 }
355 ShortcutAction::ToggleToolbar => {
356 msgs.push(Message::SetToolbarVisible(!state.show_toolbar()));
357 }
358 ShortcutAction::GoToEnd => {
359 msgs.push(Message::GoToEnd {
360 viewport_idx: state
361 .user
362 .waves
363 .as_ref()
364 .map_or(0, |waves| waves.last_active_viewport_idx),
365 });
366 }
367 ShortcutAction::GoToStart => {
368 msgs.push(Message::GoToStart {
369 viewport_idx: state
370 .user
371 .waves
372 .as_ref()
373 .map_or(0, |waves| waves.last_active_viewport_idx),
374 });
375 }
376 ShortcutAction::SaveStateFile => {
377 msgs.push(Message::SaveStateFile(state.user.state_file.clone()));
378 }
379 ShortcutAction::GoToTop => {
380 msgs.push(Message::ScrollToItem(0));
381 }
382 ShortcutAction::GoToBottom => {
383 if let Some(waves) = &state.user.waves
384 && waves.displayed_items.len() > 1
385 {
386 msgs.push(Message::ScrollToItem(waves.displayed_items.len() - 1));
387 }
388 }
389 ShortcutAction::GroupNew => {
390 msgs.push(Message::GroupNew {
391 name: None,
392 before: None,
393 items: None,
394 });
395 msgs.push(Message::ShowCommandPrompt("item_rename ".to_owned(), None));
396 }
397 ShortcutAction::ItemFocus => {
398 msgs.push(Message::ShowCommandPrompt("item_focus ".to_string(), None));
399 }
400 ShortcutAction::SelectAll => {
401 msgs.push(Message::ItemSelectAll);
402 }
403 ShortcutAction::SelectToggle => {
404 msgs.push(Message::ToggleItemSelected(None));
405 }
406 ShortcutAction::ReloadWaveform => {
407 msgs.push(Message::ReloadWaveform(
408 state.user.config.behavior.keep_during_reload,
409 ));
410 }
411 ShortcutAction::ZoomIn => {
412 msgs.push(Message::CanvasZoom {
413 mouse_ptr: None,
414 delta: 0.5,
415 viewport_idx: state
416 .user
417 .waves
418 .as_ref()
419 .map_or(0, |waves| waves.last_active_viewport_idx),
420 });
421 }
422 ShortcutAction::ZoomOut => {
423 msgs.push(Message::CanvasZoom {
424 mouse_ptr: None,
425 delta: 2.0,
426 viewport_idx: state
427 .user
428 .waves
429 .as_ref()
430 .map_or(0, |waves| waves.last_active_viewport_idx),
431 });
432 }
433 ShortcutAction::UiZoomIn => {
434 let mut next_factor = f32::INFINITY;
435 for factor in &state.user.config.layout.zoom_factors {
436 if *factor > state.ui_zoom_factor() && *factor < next_factor {
437 next_factor = *factor;
438 }
439 }
440 if next_factor != f32::INFINITY {
441 msgs.push(Message::SetUIZoomFactor(next_factor));
442 }
443 }
444 ShortcutAction::UiZoomOut => {
445 let mut next_factor = 0f32;
446 for factor in &state.user.config.layout.zoom_factors {
447 if *factor < state.ui_zoom_factor() && *factor > next_factor {
448 next_factor = *factor;
449 }
450 }
451 if next_factor > 0f32 {
452 msgs.push(Message::SetUIZoomFactor(next_factor));
453 }
454 }
455 ShortcutAction::ScrollUp => {
456 msgs.push(Message::CanvasScroll {
457 delta: Vec2 {
458 x: 0.,
459 y: -PER_SCROLL_EVENT * SCROLL_EVENTS_PER_PAGE,
460 },
461 viewport_idx: 0,
462 });
463 }
464 ShortcutAction::ScrollDown => {
465 msgs.push(Message::CanvasScroll {
466 delta: Vec2 {
467 x: 0.,
468 y: PER_SCROLL_EVENT * SCROLL_EVENTS_PER_PAGE,
469 },
470 viewport_idx: 0,
471 });
472 }
473 ShortcutAction::DeleteSelected => {
474 msgs.push(Message::RemoveVisibleItems(MessageTarget::CurrentSelection));
475 }
476 ShortcutAction::MarkerAdd => {
477 if let Some(waves) = state.user.waves.as_ref()
478 && let Some(cursor) = waves.cursor.as_ref()
479 {
480 let marker_exists = waves
482 .markers
483 .values()
484 .any(|marker_time| marker_time == cursor);
485 if !marker_exists {
486 msgs.push(Message::AddMarker {
487 time: cursor.clone(),
488 name: None,
489 move_focus: state.user.config.layout.move_focus_on_inserted_marker(),
490 });
491 }
492 }
493 }
494 ShortcutAction::ToggleMenu => {
495 msgs.push(Message::SetMenuVisible(!state.show_menu()));
496 }
497 ShortcutAction::ShowCommandPrompt => {
498 msgs.push(Message::ShowCommandPrompt(String::new(), None));
499 }
500 ShortcutAction::RenameItem => {
501 if let Some(waves) = &state.user.waves
502 && waves.focused_item.is_some()
503 {
504 msgs.push(Message::ShowCommandPrompt("item_rename ".to_owned(), None));
505 }
506 }
507 ShortcutAction::DividerAdd => {
508 msgs.push(Message::AddDivider(None, None));
509 }
510 ShortcutAction::ZoomToFit => {
511 msgs.push(Message::ZoomToFit {
512 viewport_idx: state
513 .user
514 .waves
515 .as_ref()
516 .map_or(0, |waves| waves.last_active_viewport_idx),
517 });
518 }
519 ShortcutAction::ZoomToCursor => {
520 msgs.push(Message::ZoomToCursor {
521 delta: 0.5,
522 viewport_idx: 0,
523 });
524 }
525 ShortcutAction::GoToTime => {
526 msgs.push(Message::SetRequestTextEditFocus(
527 crate::toolbar::TOOLBAR_TIME_ID.to_string(),
528 true,
529 ));
530 }
531 ShortcutAction::FocusVariableNameFilter => {
532 msgs.push(Message::SetRequestTextEditFocus(
533 crate::variable_filter::VARIABLE_FILTER_ID.to_string(),
534 true,
535 ));
536 }
537 }
538 }
539
540 pub fn process(&self, ctx: &egui::Context, msgs: &mut Vec<Message>, state: &SystemState) {
542 for entry in &self.cached_dispatch_table {
544 if self
545 .shortcuts_for_action(entry.action)
546 .iter()
547 .any(|shortcut| ctx.input_mut(|i| i.consume_shortcut(shortcut)))
548 {
549 self.execute_action(entry.action, msgs, state);
550 }
551 }
552 }
553}
554
555fn modifier_priority(shortcuts: &[KeyboardShortcut]) -> u8 {
565 shortcuts
566 .iter()
567 .find_map(|shortcut| {
568 let has_shift = shortcut.modifiers.contains(Modifiers::SHIFT);
569 let has_alt = shortcut.modifiers.contains(Modifiers::ALT);
570
571 match (has_shift, has_alt) {
572 (true, true) => Some(0), (_, true) => Some(1), (true, _) => Some(2), _ => None,
576 }
577 })
578 .unwrap_or(3) }
580
581mod keyboard_shortcuts_serde {
583 use egui::Key;
584 use serde::{Deserializer, Serializer};
585
586 use super::{Deserialize, KeyboardShortcut, Modifiers, Result, Serialize};
587
588 pub fn serialize<S>(shortcuts: &[KeyboardShortcut], serializer: S) -> Result<S::Ok, S::Error>
589 where
590 S: Serializer,
591 {
592 let bindings: Vec<String> = shortcuts
593 .iter()
594 .map(|s| format_binding(s.modifiers, s.logical_key))
595 .collect();
596 bindings.serialize(serializer)
597 }
598
599 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<KeyboardShortcut>, D::Error>
600 where
601 D: Deserializer<'de>,
602 {
603 let bindings: Vec<String> = Vec::deserialize(deserializer)?;
604 bindings
605 .iter()
606 .map(|s| parse_binding(s).map_err(serde::de::Error::custom))
607 .collect()
608 }
609
610 fn format_binding(modifiers: Modifiers, logical_key: Key) -> String {
611 const MODIFIER_NAMES: &[(Modifiers, &str)] = &[
612 (Modifiers::CTRL, "Ctrl"),
613 (Modifiers::SHIFT, "Shift"),
614 (Modifiers::ALT, "Alt"),
615 (Modifiers::MAC_CMD, "Mac_cmd"),
616 (Modifiers::COMMAND, "Command"),
617 ];
618
619 let mut parts = Vec::with_capacity(6);
621
622 for (modifier, name) in MODIFIER_NAMES {
623 if modifiers.contains(*modifier) {
624 parts.push(*name);
625 }
626 }
627 let key_name = format!("{logical_key:?}");
628 parts.push(&key_name);
629 parts.join("+")
630 }
631
632 fn parse_binding(binding: &str) -> Result<KeyboardShortcut, String> {
633 const MODIFIER_MAP: &[(&str, Modifiers)] = &[
634 ("ctrl", Modifiers::CTRL),
635 ("shift", Modifiers::SHIFT),
636 ("alt", Modifiers::ALT),
637 ("mac_cmd", Modifiers::MAC_CMD),
638 ("command", Modifiers::COMMAND),
639 ("cmd", Modifiers::COMMAND),
640 ];
641
642 let parts: Vec<&str> = binding.split('+').map(str::trim).collect();
643
644 let (modifier_parts, key_str) = match parts.as_slice() {
646 [modifiers @ .., key] => (modifiers, *key),
647 [] => return Err("Empty binding".to_string()),
648 };
649
650 let logical_key =
651 Key::from_name(key_str).ok_or_else(|| format!("Unknown key: {key_str}"))?;
652
653 let modifiers = modifier_parts
655 .iter()
656 .try_fold(Modifiers::NONE, |acc, &modifier_str| {
657 let lower = modifier_str.to_lowercase();
658 MODIFIER_MAP
659 .iter()
660 .find(|(name, _)| name == &lower)
661 .map(|(_, mod_bit)| acc | *mod_bit)
662 .ok_or_else(|| format!("Unknown modifier: {modifier_str}"))
663 })?;
664
665 Ok(KeyboardShortcut::new(modifiers, logical_key))
666 }
667}