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}