Skip to main content

ui/
keys.rs

1//! Reading the keymap back, so a printed accelerator is the one that fires.
2//!
3//! Every chord a surface shows — a menu row's trailing `⌘N`, a toolbar
4//! button's tooltip — is a claim about the keymap, and a hand-typed string is
5//! a claim nothing checks. Bind `cmd-b` somewhere else and the button still
6//! says `⌘B`. So the label is resolved from the binding instead of written
7//! beside it: [`shortcut`] asks gpui what is bound right now, and an action
8//! with nothing bound to it prints nothing rather than a chord that is no
9//! longer true.
10//!
11//! Dispatch is untouched. Nothing here fires an action — a menu still runs
12//! what the app wires to it, and this only answers what the keyboard would
13//! have run.
14//!
15//! # Replacing what a component binds
16//!
17//! Every `init` in this crate is its `bindings()` bound, and that list is
18//! public. Three ways to change it, none of which is copying it out:
19//!
20//! ```ignore
21//! cx.bind_keys(input::bindings());                       // the defaults
22//! cx.bind_keys([KeyBinding::new("ctrl-w", input::DeleteWordLeft, ctx)]);
23//! cx.bind_keys([KeyBinding::new("cmd-z", NoAction, ctx)]);
24//! ```
25//!
26//! A later binding wins the dispatch, `gpui::NoAction` takes a chord away, and
27//! an app that wants neither skips `init` and binds a filtered `bindings()`.
28//! Whichever it does, what is printed follows — that is what the rest of this
29//! module is for.
30
31use gpui::{Action, KeyContext, KeybindingKeystroke, Modifiers, SharedString, Window};
32
33/// The chord bound to `action` for whatever holds focus right now, formatted
34/// for the platform.
35///
36/// The right question for a control that sits *with* the surface the binding
37/// belongs to — a formatting bar above a focused editor, a button in the panel
38/// it acts on. `None` when nothing is bound, or when what is focused is out of
39/// the binding's context.
40pub fn shortcut(action: &dyn Action, window: &Window) -> Option<SharedString> {
41    let binding = window.highest_precedence_binding_for_action(action)?;
42    Some(format(binding.keystrokes()))
43}
44
45/// The chord bound to `action` in a named key context, whatever holds focus.
46///
47/// The right question for a control that names a chord belonging to a surface
48/// that is not focused — a menu row printing the editor's `⌘B` while the menu
49/// itself holds focus. The context is the one the binding was scoped to:
50/// [`crate::input::KEY_CONTEXT`], `editor::CONTEXT`, and so on.
51pub fn shortcut_in(action: &dyn Action, context: &str, window: &Window) -> Option<SharedString> {
52    // `new_with_defaults` rather than `default`, because it sets the `os` key
53    // a binding is free to predicate on.
54    let mut key_context = KeyContext::new_with_defaults();
55    key_context.add(context.to_owned());
56    let binding = window.highest_precedence_binding_for_action_in_context(action, key_context)?;
57    Some(format(binding.keystrokes()))
58}
59
60/// Format a binding's keystrokes the way the platform writes them.
61///
62/// macOS gets the glyphs in the order Apple sets them — `⌃⌥⇧⌘`, modifiers
63/// before the key, nothing between — and every other platform gets
64/// `Ctrl+Shift+P`, with the platform key leading: `Win+Shift+S`. A
65/// two-keystroke chord is the two spelled out with a space between, which is
66/// how both platforms print a sequence.
67///
68/// Written here rather than taken from gpui's `Display` because that one
69/// orders `⌘` before `⇧` and leaves `enter`, `delete` and `space` spelled as
70/// words in among the glyphs.
71pub fn format(keystrokes: &[KeybindingKeystroke]) -> SharedString {
72    let mut out = String::new();
73    for keystroke in keystrokes {
74        if !out.is_empty() {
75            out.push(' ');
76        }
77        modifiers(keystroke.modifiers(), &mut out);
78        out.push_str(&key(keystroke.key()));
79    }
80    SharedString::from(out)
81}
82
83#[cfg(target_os = "macos")]
84fn modifiers(modifiers: &Modifiers, out: &mut String) {
85    // Apple's order, which is not the order the struct declares them in.
86    if modifiers.function {
87        out.push_str("fn");
88    }
89    if modifiers.control {
90        out.push('⌃');
91    }
92    if modifiers.alt {
93        out.push('⌥');
94    }
95    if modifiers.shift {
96        out.push('⇧');
97    }
98    if modifiers.platform {
99        out.push('⌘');
100    }
101}
102
103#[cfg(not(target_os = "macos"))]
104fn modifiers(modifiers: &Modifiers, out: &mut String) {
105    // The platform key leads here, where on macOS it trails: Windows prints
106    // its own chords `Win+Shift+S` and `Win+Ctrl+Shift+B`, and GNOME writes
107    // `Super+` first for the same reason. Apple's `⌘` last is Apple's order,
108    // and copying it here is how this printed a chord nobody else writes.
109    if modifiers.platform {
110        out.push_str(PLATFORM_MODIFIER);
111    }
112    if modifiers.control {
113        out.push_str("Ctrl+");
114    }
115    if modifiers.alt {
116        out.push_str("Alt+");
117    }
118    if modifiers.shift {
119        out.push_str("Shift+");
120    }
121}
122
123#[cfg(any(target_os = "linux", target_os = "freebsd"))]
124const PLATFORM_MODIFIER: &str = "Super+";
125#[cfg(target_os = "windows")]
126const PLATFORM_MODIFIER: &str = "Win+";
127#[cfg(not(any(
128    target_os = "macos",
129    target_os = "linux",
130    target_os = "freebsd",
131    target_os = "windows"
132)))]
133const PLATFORM_MODIFIER: &str = "Super+";
134
135/// The key itself. macOS spells most of the named keys as glyphs; everywhere
136/// else they stay words, title-cased so they sit beside `Ctrl+` as one label.
137#[cfg(target_os = "macos")]
138fn key(key: &str) -> String {
139    let glyph = match key {
140        "backspace" => "⌫",
141        "delete" => "⌦",
142        "enter" => "↩",
143        "tab" => "⇥",
144        "escape" => "⎋",
145        "up" => "↑",
146        "down" => "↓",
147        "left" => "←",
148        "right" => "→",
149        "pageup" => "⇞",
150        "pagedown" => "⇟",
151        "home" => "↖",
152        "end" => "↘",
153        "space" => "Space",
154        // A single character is the character, upper case — `cmd-b` prints
155        // `⌘B`. Anything longer is a name (`f1`, `capslock`) and is title-cased
156        // with the rest of the words.
157        key if key.chars().count() == 1 => return key.to_uppercase(),
158        key => return title_case(key),
159    };
160    glyph.to_owned()
161}
162
163#[cfg(not(target_os = "macos"))]
164fn key(key: &str) -> String {
165    match key {
166        key if key.chars().count() == 1 => key.to_uppercase(),
167        key => title_case(key),
168    }
169}
170
171fn title_case(key: &str) -> String {
172    let mut chars = key.chars();
173    match chars.next() {
174        Some(first) => first.to_uppercase().chain(chars).collect(),
175        None => String::new(),
176    }
177}