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, Keystroke, 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/// A chord nothing is bound to, written the way this platform writes it.
61///
62/// For a label that names a chord the keymap does not own: a menu describing
63/// another app, a printed cheat sheet, a demo. Where there *is* a binding,
64/// [`shortcut`] is the one to reach for — it cannot drift, and this can.
65///
66/// The syntax is gpui's own, so `secondary-s` is the primary accelerator —
67/// `⌘S` on macOS, `Ctrl+S` elsewhere — while `cmd-s`, `super-s` and `win-s`
68/// all name the platform key itself and print `⌘S` against `Win+S`. Whitespace
69/// separates the keystrokes of a sequence, and one that will not parse is
70/// dropped rather than printed wrong.
71pub fn printed(chord: &str) -> SharedString {
72    let keystrokes = chord
73        .split_whitespace()
74        .filter_map(|keystroke| Keystroke::parse(keystroke).ok())
75        .map(KeybindingKeystroke::from_keystroke)
76        .collect::<Vec<_>>();
77    format(&keystrokes)
78}
79
80/// Format a binding's keystrokes the way the platform writes them.
81///
82/// macOS gets the glyphs in the order Apple sets them — `⌃⌥⇧⌘`, modifiers
83/// before the key, nothing between — and every other platform gets
84/// `Ctrl+Shift+P`, with the platform key leading: `Win+Shift+S`. A
85/// two-keystroke chord is the two spelled out with a space between, which is
86/// how both platforms print a sequence.
87///
88/// Written here rather than taken from gpui's `Display` because that one
89/// orders `⌘` before `⇧` and leaves `enter`, `delete` and `space` spelled as
90/// words in among the glyphs.
91pub fn format(keystrokes: &[KeybindingKeystroke]) -> SharedString {
92    let mut out = String::new();
93    for keystroke in keystrokes {
94        if !out.is_empty() {
95            out.push(' ');
96        }
97        modifiers(keystroke.modifiers(), &mut out);
98        out.push_str(&key(keystroke.key()));
99    }
100    SharedString::from(out)
101}
102
103#[cfg(target_os = "macos")]
104fn modifiers(modifiers: &Modifiers, out: &mut String) {
105    // Apple's order, which is not the order the struct declares them in.
106    if modifiers.function {
107        out.push_str("fn");
108    }
109    if modifiers.control {
110        out.push('⌃');
111    }
112    if modifiers.alt {
113        out.push('⌥');
114    }
115    if modifiers.shift {
116        out.push('⇧');
117    }
118    if modifiers.platform {
119        out.push('⌘');
120    }
121}
122
123#[cfg(not(target_os = "macos"))]
124fn modifiers(modifiers: &Modifiers, out: &mut String) {
125    // The platform key leads here, where on macOS it trails: Windows prints
126    // its own chords `Win+Shift+S` and `Win+Ctrl+Shift+B`, and GNOME writes
127    // `Super+` first for the same reason. Apple's `⌘` last is Apple's order,
128    // and copying it here is how this printed a chord nobody else writes.
129    if modifiers.platform {
130        out.push_str(PLATFORM_MODIFIER);
131    }
132    if modifiers.control {
133        out.push_str("Ctrl+");
134    }
135    if modifiers.alt {
136        out.push_str("Alt+");
137    }
138    if modifiers.shift {
139        out.push_str("Shift+");
140    }
141}
142
143#[cfg(any(target_os = "linux", target_os = "freebsd"))]
144const PLATFORM_MODIFIER: &str = "Super+";
145#[cfg(target_os = "windows")]
146const PLATFORM_MODIFIER: &str = "Win+";
147#[cfg(not(any(
148    target_os = "macos",
149    target_os = "linux",
150    target_os = "freebsd",
151    target_os = "windows"
152)))]
153const PLATFORM_MODIFIER: &str = "Super+";
154
155/// The key itself. macOS spells most of the named keys as glyphs; everywhere
156/// else they stay words, title-cased so they sit beside `Ctrl+` as one label.
157#[cfg(target_os = "macos")]
158fn key(key: &str) -> String {
159    let glyph = match key {
160        "backspace" => "⌫",
161        "delete" => "⌦",
162        "enter" => "↩",
163        "tab" => "⇥",
164        "escape" => "⎋",
165        "up" => "↑",
166        "down" => "↓",
167        "left" => "←",
168        "right" => "→",
169        "pageup" => "⇞",
170        "pagedown" => "⇟",
171        "home" => "↖",
172        "end" => "↘",
173        "space" => "Space",
174        // A single character is the character, upper case — `cmd-b` prints
175        // `⌘B`. Anything longer is a name (`f1`, `capslock`) and is title-cased
176        // with the rest of the words.
177        key if key.chars().count() == 1 => return key.to_uppercase(),
178        key => return title_case(key),
179    };
180    glyph.to_owned()
181}
182
183#[cfg(not(target_os = "macos"))]
184fn key(key: &str) -> String {
185    match key {
186        key if key.chars().count() == 1 => key.to_uppercase(),
187        key => title_case(key),
188    }
189}
190
191fn title_case(key: &str) -> String {
192    let mut chars = key.chars();
193    match chars.next() {
194        Some(first) => first.to_uppercase().chain(chars).collect(),
195        None => String::new(),
196    }
197}