Skip to repository content782 lines · 24.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:35:59.416Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
keybinding.rs
1use std::rc::Rc;
2
3use crate::PlatformStyle;
4use crate::utils::capitalize;
5use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
6use gpui::{
7 Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
8 Modifiers, Window, relative,
9};
10use itertools::Itertools;
11
12#[derive(Debug)]
13enum Source {
14 Action {
15 action: Box<dyn Action>,
16 focus_handle: Option<FocusHandle>,
17 },
18 Keystrokes {
19 /// A keybinding consists of a set of keystrokes,
20 /// where each keystroke is a key and a set of modifier keys.
21 /// More than one keystroke produces a chord.
22 ///
23 /// This should always contain at least one keystroke.
24 keystrokes: Rc<[KeybindingKeystroke]>,
25 },
26}
27
28impl Clone for Source {
29 fn clone(&self) -> Self {
30 match self {
31 Source::Action {
32 action,
33 focus_handle,
34 } => Source::Action {
35 action: action.boxed_clone(),
36 focus_handle: focus_handle.clone(),
37 },
38 Source::Keystrokes { keystrokes } => Source::Keystrokes {
39 keystrokes: keystrokes.clone(),
40 },
41 }
42 }
43}
44
45#[derive(Clone, Debug, IntoElement, RegisterComponent)]
46pub struct KeyBinding {
47 source: Source,
48 size: Option<AbsoluteLength>,
49 /// The [`PlatformStyle`] to use when displaying this keybinding.
50 platform_style: PlatformStyle,
51 /// Determines whether the keybinding is meant for vim mode.
52 vim_mode: bool,
53 /// Indicates whether the keybinding is currently disabled.
54 disabled: bool,
55}
56
57struct VimStyle(bool);
58impl Global for VimStyle {}
59
60impl KeyBinding {
61 /// Returns the highest precedence keybinding for an action. This is the last binding added to
62 /// the keymap. User bindings are added after built-in bindings so that they take precedence.
63 pub fn for_action(action: &dyn Action, cx: &App) -> Self {
64 Self::new(action, None, cx)
65 }
66
67 /// Like `for_action`, but lets you specify the context from which keybindings are matched.
68 pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self {
69 Self::new(action, Some(focus.clone()), cx)
70 }
71 pub fn has_binding(&self, window: &Window) -> bool {
72 match &self.source {
73 Source::Action {
74 action,
75 focus_handle: Some(focus),
76 } => window
77 .highest_precedence_binding_for_action_in(action.as_ref(), focus)
78 .or_else(|| window.highest_precedence_binding_for_action(action.as_ref()))
79 .is_some(),
80 _ => false,
81 }
82 }
83
84 pub fn set_vim_mode(cx: &mut App, enabled: bool) {
85 cx.set_global(VimStyle(enabled));
86 }
87
88 fn is_vim_mode(cx: &App) -> bool {
89 cx.try_global::<VimStyle>().is_some_and(|g| g.0)
90 }
91
92 pub fn new(action: &dyn Action, focus_handle: Option<FocusHandle>, cx: &App) -> Self {
93 Self {
94 source: Source::Action {
95 action: action.boxed_clone(),
96 focus_handle,
97 },
98 size: None,
99 vim_mode: KeyBinding::is_vim_mode(cx),
100 platform_style: PlatformStyle::platform(),
101 disabled: false,
102 }
103 }
104
105 pub fn from_keystrokes(keystrokes: Rc<[KeybindingKeystroke]>, vim_mode: bool) -> Self {
106 Self {
107 source: Source::Keystrokes { keystrokes },
108 size: None,
109 vim_mode,
110 platform_style: PlatformStyle::platform(),
111 disabled: false,
112 }
113 }
114
115 /// Sets the [`PlatformStyle`] for this [`KeyBinding`].
116 pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
117 self.platform_style = platform_style;
118 self
119 }
120
121 /// Sets the size for this [`KeyBinding`].
122 pub fn size(mut self, size: impl Into<AbsoluteLength>) -> Self {
123 self.size = Some(size.into());
124 self
125 }
126
127 /// Sets whether this keybinding is currently disabled.
128 /// Disabled keybinds will be rendered in a dimmed state.
129 pub fn disabled(mut self, disabled: bool) -> Self {
130 self.disabled = disabled;
131 self
132 }
133
134 fn vim_mode(mut self, vim_mode: bool) -> Self {
135 self.vim_mode = vim_mode;
136 self
137 }
138
139 /// Resolves this keybinding's keystrokes the same way rendering does
140 /// (matching the visible accelerator). Returns `None` when there is no
141 /// binding.
142 fn resolve_keystrokes(&self, window: &Window, cx: &App) -> Option<Vec<KeybindingKeystroke>> {
143 let keystrokes = match &self.source {
144 Source::Action {
145 action,
146 focus_handle,
147 } => {
148 let binding = focus_handle
149 .clone()
150 .or_else(|| window.focused(cx))
151 .and_then(|focus| {
152 window.highest_precedence_binding_for_action_in(action.as_ref(), &focus)
153 })
154 .or_else(|| window.highest_precedence_binding_for_action(action.as_ref()))?;
155 binding.keystrokes().to_vec()
156 }
157 Source::Keystrokes { keystrokes } => keystrokes.to_vec(),
158 };
159 (!keystrokes.is_empty()).then_some(keystrokes)
160 }
161
162 /// Resolves this keybinding to a human-readable shortcut string in the same
163 /// platform format shown to sighted users (e.g. `"Ctrl-S"`, `"Command-S"`,
164 /// or `"Ctrl-K Ctrl-S"` for a chord), matching the visible accelerator. This
165 /// is the format AccessKit's `keyboard_shortcut` property expects. Returns
166 /// `None` when there is no binding.
167 pub fn keyboard_shortcut_text(&self, window: &Window, cx: &App) -> Option<SharedString> {
168 let keystrokes = self.resolve_keystrokes(window, cx)?;
169 let text = keystrokes
170 .iter()
171 .map(|keystroke| {
172 keystroke_text(
173 keystroke.modifiers(),
174 keystroke.key(),
175 self.platform_style,
176 self.vim_mode,
177 )
178 })
179 .join(" ");
180 Some(text.into())
181 }
182}
183
184fn render_key(
185 key: &str,
186 color: Option<Color>,
187 platform_style: PlatformStyle,
188 size: impl Into<Option<AbsoluteLength>>,
189) -> AnyElement {
190 let key_icon = icon_for_key(key, platform_style);
191 match key_icon {
192 Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
193 None => {
194 let key = capitalize(key);
195 Key::new(&key, color).size(size).into_any_element()
196 }
197 }
198}
199
200impl RenderOnce for KeyBinding {
201 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
202 let render_keybinding = |keystrokes: &[KeybindingKeystroke]| {
203 let color = self.disabled.then_some(Color::Disabled);
204
205 h_flex()
206 .debug_selector(|| {
207 format!(
208 "KEY_BINDING-{}",
209 keystrokes
210 .iter()
211 .map(|k| k.key().to_string())
212 .collect::<Vec<_>>()
213 .join(" ")
214 )
215 })
216 .gap(DynamicSpacing::Base04.rems(cx))
217 .flex_none()
218 .children(keystrokes.iter().map(|keystroke| {
219 h_flex()
220 .flex_none()
221 .py_0p5()
222 .rounded_xs()
223 .text_color(cx.theme().colors().text_muted)
224 .children(render_keybinding_keystroke(
225 keystroke,
226 color,
227 self.size,
228 PlatformStyle::platform(),
229 self.vim_mode,
230 ))
231 }))
232 .into_any_element()
233 };
234
235 match self.source {
236 Source::Action {
237 action,
238 focus_handle,
239 } => focus_handle
240 .or_else(|| window.focused(cx))
241 .and_then(|focus| {
242 window.highest_precedence_binding_for_action_in(action.as_ref(), &focus)
243 })
244 .or_else(|| window.highest_precedence_binding_for_action(action.as_ref()))
245 .map(|binding| render_keybinding(binding.keystrokes())),
246 Source::Keystrokes { keystrokes } => Some(render_keybinding(keystrokes.as_ref())),
247 }
248 .unwrap_or_else(|| gpui::Empty.into_any_element())
249 }
250}
251
252pub fn render_keybinding_keystroke(
253 keystroke: &KeybindingKeystroke,
254 color: Option<Color>,
255 size: impl Into<Option<AbsoluteLength>>,
256 platform_style: PlatformStyle,
257 vim_mode: bool,
258) -> Vec<AnyElement> {
259 let use_text = vim_mode
260 || matches!(
261 platform_style,
262 PlatformStyle::Linux | PlatformStyle::Windows
263 );
264 let size = size.into();
265
266 if use_text {
267 let element = Key::new(
268 keystroke_text(
269 keystroke.modifiers(),
270 keystroke.key(),
271 platform_style,
272 vim_mode,
273 ),
274 color,
275 )
276 .size(size)
277 .into_any_element();
278 vec![element]
279 } else {
280 let mut elements = Vec::new();
281 elements.extend(render_modifiers(
282 keystroke.modifiers(),
283 platform_style,
284 color,
285 size,
286 true,
287 ));
288 elements.push(render_key(keystroke.key(), color, platform_style, size));
289 elements
290 }
291}
292
293fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
294 match key {
295 "left" => Some(IconName::ArrowLeft),
296 "right" => Some(IconName::ArrowRight),
297 "up" => Some(IconName::ArrowUp),
298 "down" => Some(IconName::ArrowDown),
299 "backspace" => Some(IconName::Backspace),
300 "delete" => Some(IconName::Backspace),
301 "return" => Some(IconName::Return),
302 "enter" => Some(IconName::Return),
303 "tab" => Some(IconName::Tab),
304 "space" => Some(IconName::Space),
305 "escape" => Some(IconName::Escape),
306 "pagedown" => Some(IconName::PageDown),
307 "pageup" => Some(IconName::PageUp),
308 "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
309 "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
310 "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
311 "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
312 "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
313 _ => None,
314 }
315}
316
317pub fn render_modifiers(
318 modifiers: &Modifiers,
319 platform_style: PlatformStyle,
320 color: Option<Color>,
321 size: Option<AbsoluteLength>,
322 trailing_separator: bool,
323) -> impl Iterator<Item = AnyElement> {
324 #[derive(Clone)]
325 enum KeyOrIcon {
326 Key(&'static str),
327 Plus,
328 Icon(IconName),
329 }
330
331 struct Modifier {
332 enabled: bool,
333 mac: KeyOrIcon,
334 linux: KeyOrIcon,
335 windows: KeyOrIcon,
336 }
337
338 let table = {
339 use KeyOrIcon::*;
340
341 [
342 Modifier {
343 enabled: modifiers.function,
344 mac: Icon(IconName::Control),
345 linux: Key("Fn"),
346 windows: Key("Fn"),
347 },
348 Modifier {
349 enabled: modifiers.control,
350 mac: Icon(IconName::Control),
351 linux: Key("Ctrl"),
352 windows: Key("Ctrl"),
353 },
354 Modifier {
355 enabled: modifiers.alt,
356 mac: Icon(IconName::Option),
357 linux: Key("Alt"),
358 windows: Key("Alt"),
359 },
360 Modifier {
361 enabled: modifiers.platform,
362 mac: Icon(IconName::Command),
363 linux: Key("Super"),
364 windows: Key("Win"),
365 },
366 Modifier {
367 enabled: modifiers.shift,
368 mac: Icon(IconName::Shift),
369 linux: Key("Shift"),
370 windows: Key("Shift"),
371 },
372 ]
373 };
374
375 let filtered = table
376 .into_iter()
377 .filter(|modifier| modifier.enabled)
378 .collect::<Vec<_>>();
379
380 let platform_keys = filtered
381 .into_iter()
382 .map(move |modifier| match platform_style {
383 PlatformStyle::Mac => Some(modifier.mac),
384 PlatformStyle::Linux => Some(modifier.linux),
385 PlatformStyle::Windows => Some(modifier.windows),
386 });
387
388 let separator = match platform_style {
389 PlatformStyle::Mac => None,
390 PlatformStyle::Linux => Some(KeyOrIcon::Plus),
391 PlatformStyle::Windows => Some(KeyOrIcon::Plus),
392 };
393
394 let platform_keys = itertools::intersperse(platform_keys, separator.clone());
395
396 platform_keys
397 .chain(if modifiers.modified() && trailing_separator {
398 Some(separator)
399 } else {
400 None
401 })
402 .flatten()
403 .map(move |key_or_icon| match key_or_icon {
404 KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
405 KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
406 KeyOrIcon::Plus => "+".into_any_element(),
407 })
408}
409
410#[derive(IntoElement)]
411pub struct Key {
412 key: SharedString,
413 color: Option<Color>,
414 size: Option<AbsoluteLength>,
415}
416
417impl RenderOnce for Key {
418 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
419 let single_char = self.key.len() == 1;
420 let size = self
421 .size
422 .unwrap_or_else(|| TextSize::default().rems(cx).into());
423
424 div()
425 .py_0()
426 .map(|this| {
427 if single_char {
428 this.w(size).flex().flex_none().justify_center()
429 } else {
430 this.px_0p5()
431 }
432 })
433 .h(size)
434 .text_size(size)
435 .line_height(relative(1.))
436 .text_color(self.color.unwrap_or(Color::Muted).color(cx))
437 .child(self.key)
438 }
439}
440
441impl Key {
442 pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
443 Self {
444 key: key.into(),
445 color,
446 size: None,
447 }
448 }
449
450 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
451 self.size = size.into();
452 self
453 }
454}
455
456#[derive(IntoElement)]
457pub struct KeyIcon {
458 icon: IconName,
459 color: Option<Color>,
460 size: Option<AbsoluteLength>,
461}
462
463impl RenderOnce for KeyIcon {
464 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
465 let size = self.size.unwrap_or(IconSize::Small.rems().into());
466
467 Icon::new(self.icon)
468 .size(IconSize::Custom(size.to_rems(window.rem_size())))
469 .color(self.color.unwrap_or(Color::Muted))
470 }
471}
472
473impl KeyIcon {
474 pub fn new(icon: IconName, color: Option<Color>) -> Self {
475 Self {
476 icon,
477 color,
478 size: None,
479 }
480 }
481
482 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
483 self.size = size.into();
484 self
485 }
486}
487
488/// Returns a textual representation of the key binding for the given [`Action`].
489pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
490 let key_binding = window.highest_precedence_binding_for_action(action)?;
491 Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx))
492}
493
494pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
495 let platform_style = PlatformStyle::platform();
496 let vim_enabled = KeyBinding::is_vim_mode(cx);
497 keystrokes
498 .iter()
499 .map(|keystroke| {
500 keystroke_text(
501 &keystroke.modifiers,
502 &keystroke.key,
503 platform_style,
504 vim_enabled,
505 )
506 })
507 .join(" ")
508}
509
510pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
511 let platform_style = PlatformStyle::platform();
512 let vim_enabled = KeyBinding::is_vim_mode(cx);
513 keystrokes
514 .iter()
515 .map(|keystroke| {
516 keystroke_text(
517 keystroke.modifiers(),
518 keystroke.key(),
519 platform_style,
520 vim_enabled,
521 )
522 })
523 .join(" ")
524}
525
526pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String {
527 let platform_style = PlatformStyle::platform();
528 keystroke_text(modifiers, key, platform_style, KeyBinding::is_vim_mode(cx))
529}
530
531/// Returns a textual representation of the given [`Keystroke`].
532fn keystroke_text(
533 modifiers: &Modifiers,
534 key: &str,
535 platform_style: PlatformStyle,
536 vim_mode: bool,
537) -> String {
538 let mut text = String::new();
539 let delimiter = '-';
540
541 if modifiers.function {
542 match vim_mode {
543 false => text.push_str("Fn"),
544 true => text.push_str("fn"),
545 }
546
547 text.push(delimiter);
548 }
549
550 if modifiers.control {
551 match (platform_style, vim_mode) {
552 (PlatformStyle::Mac, false) => text.push_str("Control"),
553 (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
554 (_, true) => text.push_str("ctrl"),
555 }
556
557 text.push(delimiter);
558 }
559
560 if modifiers.platform {
561 match (platform_style, vim_mode) {
562 (PlatformStyle::Mac, false) => text.push_str("Command"),
563 (PlatformStyle::Mac, true) => text.push_str("cmd"),
564 (PlatformStyle::Linux, false) => text.push_str("Super"),
565 (PlatformStyle::Linux, true) => text.push_str("super"),
566 (PlatformStyle::Windows, false) => text.push_str("Win"),
567 (PlatformStyle::Windows, true) => text.push_str("win"),
568 }
569
570 text.push(delimiter);
571 }
572
573 if modifiers.alt {
574 match (platform_style, vim_mode) {
575 (PlatformStyle::Mac, false) => text.push_str("Option"),
576 (PlatformStyle::Mac, true) => text.push_str("option"),
577 (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
578 (_, true) => text.push_str("alt"),
579 }
580
581 text.push(delimiter);
582 }
583
584 if modifiers.shift {
585 match (platform_style, vim_mode) {
586 (_, false) => text.push_str("Shift"),
587 (_, true) => text.push_str("shift"),
588 }
589 text.push(delimiter);
590 }
591
592 if vim_mode {
593 text.push_str(key)
594 } else {
595 let key = match key {
596 "pageup" => "PageUp",
597 "pagedown" => "PageDown",
598 key => &capitalize(key),
599 };
600 text.push_str(key);
601 }
602
603 text
604}
605
606impl Component for KeyBinding {
607 fn scope() -> ComponentScope {
608 ComponentScope::Typography
609 }
610
611 fn name() -> &'static str {
612 "KeyBinding"
613 }
614
615 fn description() -> &'static str {
616 "A component that displays a key binding, \
617 supporting different platform styles and vim mode."
618 }
619
620 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
621 fn keybinding(input: &str) -> KeyBinding {
622 let keystrokes: Rc<[KeybindingKeystroke]> = input
623 .split_whitespace()
624 .filter_map(|chunk| Keystroke::parse(chunk).ok())
625 .map(KeybindingKeystroke::from_keystroke)
626 .collect::<Vec<_>>()
627 .into();
628 KeyBinding::from_keystrokes(keystrokes, false)
629 }
630
631 v_flex()
632 .gap_6()
633 .children(vec![
634 example_group_with_title(
635 "Platform Styles",
636 vec![
637 single_example(
638 "Mac Style",
639 keybinding("cmd-s")
640 .platform_style(PlatformStyle::Mac)
641 .into_any_element(),
642 ),
643 single_example(
644 "Linux Style",
645 keybinding("ctrl-s")
646 .platform_style(PlatformStyle::Linux)
647 .into_any_element(),
648 ),
649 single_example(
650 "Windows Style",
651 keybinding("ctrl-s")
652 .platform_style(PlatformStyle::Windows)
653 .into_any_element(),
654 ),
655 ],
656 ),
657 example_group_with_title(
658 "Vim Mode Style",
659 vec![
660 single_example(
661 "Simple",
662 keybinding("s")
663 .platform_style(PlatformStyle::Mac)
664 .vim_mode(true)
665 .into_any_element(),
666 ),
667 single_example(
668 "With Modifiers",
669 keybinding("ctrl-s")
670 .platform_style(PlatformStyle::Linux)
671 .vim_mode(true)
672 .into_any_element(),
673 ),
674 single_example(
675 "With other special key",
676 keybinding("ctrl-escape")
677 .platform_style(PlatformStyle::Windows)
678 .vim_mode(true)
679 .into_any_element(),
680 ),
681 ],
682 ),
683 ])
684 .into_any_element()
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn test_text_for_keystroke() {
694 let keystroke = Keystroke::parse("cmd-c").unwrap();
695 assert_eq!(
696 keystroke_text(
697 &keystroke.modifiers,
698 &keystroke.key,
699 PlatformStyle::Mac,
700 false
701 ),
702 "Command-C".to_string()
703 );
704 assert_eq!(
705 keystroke_text(
706 &keystroke.modifiers,
707 &keystroke.key,
708 PlatformStyle::Linux,
709 false
710 ),
711 "Super-C".to_string()
712 );
713 assert_eq!(
714 keystroke_text(
715 &keystroke.modifiers,
716 &keystroke.key,
717 PlatformStyle::Windows,
718 false
719 ),
720 "Win-C".to_string()
721 );
722
723 let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
724 assert_eq!(
725 keystroke_text(
726 &keystroke.modifiers,
727 &keystroke.key,
728 PlatformStyle::Mac,
729 false
730 ),
731 "Control-Option-Delete".to_string()
732 );
733 assert_eq!(
734 keystroke_text(
735 &keystroke.modifiers,
736 &keystroke.key,
737 PlatformStyle::Linux,
738 false
739 ),
740 "Ctrl-Alt-Delete".to_string()
741 );
742 assert_eq!(
743 keystroke_text(
744 &keystroke.modifiers,
745 &keystroke.key,
746 PlatformStyle::Windows,
747 false
748 ),
749 "Ctrl-Alt-Delete".to_string()
750 );
751
752 let keystroke = Keystroke::parse("shift-pageup").unwrap();
753 assert_eq!(
754 keystroke_text(
755 &keystroke.modifiers,
756 &keystroke.key,
757 PlatformStyle::Mac,
758 false
759 ),
760 "Shift-PageUp".to_string()
761 );
762 assert_eq!(
763 keystroke_text(
764 &keystroke.modifiers,
765 &keystroke.key,
766 PlatformStyle::Linux,
767 false,
768 ),
769 "Shift-PageUp".to_string()
770 );
771 assert_eq!(
772 keystroke_text(
773 &keystroke.modifiers,
774 &keystroke.key,
775 PlatformStyle::Windows,
776 false
777 ),
778 "Shift-PageUp".to_string()
779 );
780 }
781}
782