Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:33:18.865Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

keybinding.rs

782 lines · 24.7 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.