Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/keymap.rs
1use crate::actions::StandardAction;
2use crate::AppContext;
3use crate::{Action, Tracked};
4use anyhow::anyhow;
5use lazy_static::lazy_static;
6use serde::{Deserialize, Serialize};
7use std::borrow::Cow;
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::{
10 any::Any,
11 collections::{HashMap, HashSet},
12 fmt,
13 sync::Arc,
14};
15use titlecase::titlecase;
16 
17mod context;
18mod matcher;
19 
20use crate::platform::OperatingSystem;
21pub use context::{macros, Context, ContextPredicate};
22pub use matcher::{IsBindingValid, MatchResult, Matcher};
23 
24#[derive(Default)]
25pub struct Keymap {
26 fixed_bindings: Vec<FixedBinding>,
27 editable_bindings: Vec<Tracked<EditableBinding>>,
28 /// A mapping from binding name to indices in `editable_bindings` of bindings with
29 /// that name, stored in the order they were registered.
30 editable_bindings_by_name: HashMap<&'static str, Vec<usize>>,
31 
32 // We store a copy of the bindings, filtered down to only ones that are
33 // triggered by a custom action. This is done to optimize the lookups
34 // of custom action bindings that are performed on macOS in response to
35 // a `[WarpDelegate menuNeedsUpdate]` selector.
36 fixed_custom_action_bindings: Vec<FixedBinding>,
37 editable_custom_action_bindings: Vec<Tracked<EditableBinding>>,
38}
39 
40// Custom actions should be identified by a unique integer called their tag.
41pub type CustomTag = isize;
42 
43#[derive(PartialEq, Eq, Hash, Debug, Clone)]
44pub enum Trigger {
45 Keystrokes(Vec<Keystroke>), // trigger when keys are pressed
46 Standard(StandardAction), // trigger when a StandardAction is dispatched
47 Custom(CustomTag), // trigger when a Custom action (identified by its CustomTag) is dispatched
48 Empty, // empty trigger (cannot actually be matched)
49}
50 
51impl Trigger {
52 pub fn is_empty(&self) -> bool {
53 matches!(self, Trigger::Empty)
54 }
55}
56 
57/// The context in which a binding description should be shown
58#[derive(Debug, Clone, Copy)]
59pub enum DescriptionContext {
60 /// The default context (could be a command-palette or menu, depending on the app)
61 Default,
62 
63 /// A custom, app-specific context specified by string
64 Custom(&'static str),
65}
66 
67/// Closure that can override a [`BindingDescription`] from live app state. See
68/// [`BindingDescription::with_dynamic_override`].
69pub type DynamicDescriptionResolver = Arc<dyn Fn(&AppContext) -> Option<String> + Send + Sync>;
70 
71#[derive(Default, Clone)]
72/// A description of the binding. Supports a single default context and
73/// multiple custom contexts. Custom contexts are effectively overrides.
74///
75/// May also carry a [`Self::with_dynamic_override`] resolver for bindings
76/// whose label depends on live `&AppContext` state.
77pub struct BindingDescription {
78 // The default description. If not overridden, it will be used in all
79 // contexts.
80 description: String,
81 
82 // A map of custom description contexts to custom descriptions
83 custom: Option<HashMap<&'static str, String>>,
84 
85 // Optional dynamic override. The manual `PartialEq`/`Debug` impls below
86 // intentionally ignore this field because equality is only consumed by
87 // description deduplication that runs against already-materialized
88 // `CommandBinding`s.
89 dynamic_override: Option<DynamicDescriptionResolver>,
90}
91 
92impl PartialEq for BindingDescription {
93 fn eq(&self, other: &Self) -> bool {
94 self.description == other.description && self.custom == other.custom
95 }
96}
97impl Eq for BindingDescription {}
98 
99impl fmt::Debug for BindingDescription {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.debug_struct("BindingDescription")
102 .field("description", &self.description)
103 .field("custom", &self.custom)
104 .field(
105 "dynamic_override",
106 &self.dynamic_override.as_ref().map(|_| "<dynamic>"),
107 )
108 .finish()
109 }
110}
111 
112impl BindingDescription {
113 pub fn new<S: Into<String>>(description: S) -> Self {
114 BindingDescription {
115 description: titlecase(&description.into()),
116 ..Default::default()
117 }
118 }
119 
120 pub fn new_preserve_case<S: Into<String>>(description: S) -> Self {
121 BindingDescription {
122 description: description.into(),
123 ..Default::default()
124 }
125 }
126 
127 pub fn with_custom_description<S: Into<String>>(
128 mut self,
129 context: DescriptionContext,
130 description: S,
131 ) -> Self {
132 if let DescriptionContext::Custom(key) = context {
133 self.custom
134 .get_or_insert_with(HashMap::new)
135 .insert(key, description.into());
136 } else {
137 debug_assert!(false, "Expected custom description");
138 }
139 self
140 }
141 
142 /// Attach a dynamic override for this description at materialization time.
143 /// Returning `None` falls back to the static description for the requested
144 /// context.
145 ///
146 /// The static value passed to [`Self::new`] is retained as the fallback
147 /// for read paths that have no `&AppContext` (see [`Self::in_context`]).
148 pub fn with_dynamic_override<F>(mut self, resolver: F) -> Self
149 where
150 F: Fn(&AppContext) -> Option<String> + Send + Sync + 'static,
151 {
152 self.dynamic_override = Some(Arc::new(resolver));
153 self
154 }
155 
156 /// True if this description has an attached dynamic override.
157 pub fn has_dynamic_override(&self) -> bool {
158 self.dynamic_override.is_some()
159 }
160 
161 /// Returns the description for the given context, applying the dynamic
162 /// override if one is attached and returns `Some`. Prefer this over
163 /// [`Self::in_context`] anywhere `&AppContext` is in scope.
164 pub fn resolve(&self, ctx: &AppContext, context: DescriptionContext) -> Cow<'_, str> {
165 match &self.dynamic_override {
166 Some(f) => match f(ctx) {
167 Some(description) => Cow::Owned(titlecase(&description)),
168 None => Cow::Borrowed(self.in_context(context)),
169 },
170 None => Cow::Borrowed(self.in_context(context)),
171 }
172 }
173 
174 /// Returns a static description with dynamic overrides resolved and removed.
175 pub fn materialized(&self, ctx: &AppContext) -> Self {
176 let mut description = BindingDescription::new_preserve_case(
177 self.resolve(ctx, DescriptionContext::Default).into_owned(),
178 );
179 
180 if let Some(custom) = &self.custom {
181 description.custom = Some(
182 custom
183 .keys()
184 .map(|&key| {
185 (
186 key,
187 self.resolve(ctx, DescriptionContext::Custom(key))
188 .into_owned(),
189 )
190 })
191 .collect(),
192 );
193 }
194 
195 description
196 }
197 
198 /// Returns the static description for the given context. Does **not**
199 /// invoke any attached dynamic override, so callers that have access
200 /// to `&AppContext` should use [`Self::resolve`] instead. This method
201 /// remains available for read paths that operate on
202 /// already-materialized descriptions, or that genuinely cannot plumb
203 /// a context through.
204 pub fn in_context(&self, context: DescriptionContext) -> &str {
205 match (context, &self.custom) {
206 (DescriptionContext::Custom(key), Some(map)) => map
207 .get(key)
208 .map(|s| s.as_str())
209 .unwrap_or_else(|| self.description.as_str()),
210 _ => self.description.as_str(),
211 }
212 }
213}
214 
215impl<S: Into<String>> From<S> for BindingDescription {
216 fn from(description: S) -> Self {
217 BindingDescription::new(description)
218 }
219}
220 
221/// A predicate that determines whether or not a binding is enabled. By default, all bindings are
222/// enabled. Disabling a binding hides it completely - its context predicate never applies, it
223/// should not be shown in keymap settings, and it cannot be triggered.
224///
225/// ## Enabled vs. Context Predicates
226/// Context predicates configure whether or not a binding is available based on keymap contexts.
227/// For example, many bindings are predicated on a particular view being focused. It's also common
228/// to predicate bindings on view state, such as whether or not there's a text selection. Even if
229/// its context predicate is false, the binding is still registered in the total set of bindings.
230///
231/// Enabled predicates dynamically decide if a binding is registered or not. They're similar to
232/// conditionally calling [`strato_ui::app::AppContext::register_editable_bindings`], except
233/// that the condition is re-evaluated at runtime. The main use for enabled predicates is to check
234/// feature flags that might change post-initialization. If a feature is disabled, any bindings
235/// related to it should be as well. Once the feature is enabled, the UI framework will start
236/// checking the bindings' context predicates, and they can be shown in keymap settings.
237pub type EnabledPredicate = fn() -> bool;
238 
239/// A lens into a binding, used to match keyboard events
240/// to their appropriate actions.
241#[derive(Copy, Clone, Debug)]
242pub struct BindingLens<'a> {
243 pub name: &'a str,
244 pub trigger: &'a Trigger,
245 pub action: &'a Arc<dyn Action>,
246 context_predicate: &'a ContextPredicate,
247 // BindingLens does not have an enabled predicate because we never construct a BindingLens for
248 // disabled bindings.
249 pub description: Option<&'a BindingDescription>,
250 /// The original trigger for the binding. If `None`, the current trigger (set in `self.trigger`)
251 /// and the original trigger are the same.
252 pub original_trigger: Option<&'a Trigger>,
253 pub group: Option<&'static str>,
254 pub id: BindingId,
255}
256 
257/// A unique identifier for a Binding within the application.
258///
259/// Used so that bindings can be uniquely identified even if data within them (such as their
260/// trigger) changes.
261#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
262pub struct BindingId(pub usize);
263 
264static NEXT_BINDING_ID: AtomicUsize = AtomicUsize::new(0);
265 
266impl BindingId {
267 /// Constructs a new globally-unique Binding ID.
268 #[allow(clippy::new_without_default)]
269 pub fn new() -> BindingId {
270 let raw = NEXT_BINDING_ID.fetch_add(1, Ordering::Relaxed);
271 BindingId(raw)
272 }
273}
274 
275/// This action can't be reconfigured with a custom key binding trigger
276#[derive(Clone)]
277pub struct FixedBinding {
278 trigger: Trigger,
279 action: Arc<dyn Action>,
280 command_description: Option<BindingDescription>,
281 context_predicate: ContextPredicate,
282 enabled_predicate: Option<EnabledPredicate>,
283 group: Option<&'static str>,
284 /// A unique identifier that identifies this binding.
285 id: BindingId,
286}
287 
288/// An action which is explicitly registered with the key map
289///
290/// This action can have its key binding trigger overridden by setting a value
291/// for `custom_trigger`.
292#[derive(Clone)]
293pub struct EditableBinding {
294 name: &'static str,
295 description: BindingDescription,
296 action: Arc<dyn Action>,
297 context_predicate: ContextPredicate,
298 enabled_predicate: Option<EnabledPredicate>,
299 trigger: Trigger,
300 custom_trigger: Option<Trigger>,
301 group: Option<&'static str>,
302 /// A unique identifier that identifies this binding.
303 id: BindingId,
304}
305 
306/// A lens into an editable binding, allowing for the trigger to be updated where necessary
307pub struct EditableBindingLens<'a> {
308 pub name: &'static str,
309 pub description: &'a BindingDescription,
310 pub action: &'a Arc<dyn Action>,
311 context: &'a ContextPredicate,
312 enabled: Option<EnabledPredicate>,
313 pub trigger: &'a Trigger,
314 /// The original trigger, if a custom one is overriding it
315 pub original_trigger: Option<&'a Trigger>,
316 pub group: Option<&'static str>,
317 pub id: BindingId,
318}
319 
320#[derive(Clone, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
321pub struct Keystroke {
322 pub ctrl: bool,
323 pub alt: bool,
324 pub shift: bool,
325 pub cmd: bool,
326 pub meta: bool,
327 pub key: String,
328}
329 
330/// In the user-visible settings schema (and in the TOML settings file), a
331/// `Keystroke` is represented as a compact string like `"cmd-shift-a"`, not
332/// as an object with per-modifier booleans. Serde continues to use the
333/// default struct form for cloud sync and other in-memory consumers.
334#[cfg(feature = "schema_gen")]
335impl schemars::JsonSchema for Keystroke {
336 fn schema_name() -> std::borrow::Cow<'static, str> {
337 std::borrow::Cow::Borrowed("Keystroke")
338 }
339 
340 fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
341 gen.subschema_for::<String>()
342 }
343}
344 
345pub trait ActionArg {
346 fn boxed_clone(&self) -> Box<dyn Any>;
347}
348 
349impl<T> ActionArg for T
350where
351 T: 'static + Any + Clone,
352{
353 fn boxed_clone(&self) -> Box<dyn Any> {
354 Box::new(self.clone())
355 }
356}
357 
358impl Keymap {
359 #[cfg(test)]
360 pub fn new(fixed_bindings: Vec<FixedBinding>) -> Self {
361 Self {
362 fixed_bindings,
363 ..Default::default()
364 }
365 }
366 
367 /// Returns the earliest-registered currently-enabled binding with the given name.
368 pub fn get_binding_by_name(&self, name: &str) -> Option<BindingLens<'_>> {
369 let indices = self.editable_bindings_by_name.get(name)?;
370 indices.iter().find_map(|idx| {
371 let binding = self.editable_bindings.get(*idx)?;
372 let binding = binding.as_lens();
373 binding.is_enabled().then_some(binding.as_binding())
374 })
375 }
376 
377 /// Add new fixed bindings to the keymap
378 ///
379 /// These bindings are internal and cannot be changed once they are added
380 fn register_fixed_bindings<T: IntoIterator<Item = FixedBinding>>(&mut self, bindings: T) {
381 let start_idx = self.fixed_bindings.len();
382 self.fixed_bindings.extend(bindings);
383 for binding in &self.fixed_bindings[start_idx..] {
384 if matches!(binding.trigger(), Trigger::Custom(_)) {
385 self.fixed_custom_action_bindings.push(binding.clone());
386 }
387 }
388 }
389 
390 /// Add editable bindings to the keymap
391 ///
392 /// Editable Bindings have a name identifier which can be used to override their key bindings
393 /// via the `set_custom_trigger` method.
394 fn register_editable_bindings<A: IntoIterator<Item = EditableBinding>>(&mut self, actions: A) {
395 let start_idx = self.editable_bindings.len();
396 self.editable_bindings
397 .extend(actions.into_iter().map(Tracked::new));
398 for (idx, binding) in self.editable_bindings.iter().enumerate().skip(start_idx) {
399 if matches!(binding.trigger, Trigger::Custom(_)) {
400 self.editable_custom_action_bindings
401 .push(Tracked::new((*binding).clone()));
402 }
403 self.editable_bindings_by_name
404 .entry(binding.name)
405 .or_default()
406 .push(idx);
407 }
408 }
409 
410 /// Updates the custom trigger for a given editable binding.
411 fn update_custom_trigger(&mut self, name: &str, trigger: Option<Trigger>) {
412 for binding in self
413 .editable_custom_action_bindings
414 .iter_mut()
415 .filter(|b| b.name == name)
416 {
417 binding.custom_trigger = trigger.clone();
418 }
419 for binding in self.editable_bindings.iter_mut().filter(|b| b.name == name) {
420 binding.custom_trigger = trigger.clone();
421 }
422 }
423 
424 /// Fetch an iterator of editable bindings
425 ///
426 /// The triggers for those actions will be overwritten by any custom triggers
427 ///
428 /// Items will be returned in the reverse order they were registered, the most recently
429 /// registered editable binding will have the highest precedence
430 fn editable_bindings(&self) -> impl Iterator<Item = EditableBindingLens<'_>> {
431 self.editable_bindings
432 .iter()
433 .rev()
434 .map(|binding| binding.as_lens())
435 .filter(|binding| binding.is_enabled())
436 }
437 
438 /// Fetch an iterator of `BindingLens` objects, with the editable key bindings
439 /// modified by the custom bindings, where appropriate.
440 ///
441 /// Editable bindings will be returned first, followed by any fixed bindings in the reverse
442 /// order they were added.
443 fn bindings(&self) -> impl Iterator<Item = BindingLens<'_>> {
444 self.editable_bindings()
445 .map(|lens| lens.as_binding())
446 .chain(
447 self.fixed_bindings
448 .iter()
449 .rev()
450 .filter(|binding| binding.is_enabled())
451 .map(FixedBinding::as_lens),
452 )
453 }
454 
455 fn editable_custom_action_bindings(&self) -> impl Iterator<Item = EditableBindingLens<'_>> {
456 self.editable_custom_action_bindings
457 .iter()
458 .rev()
459 .map(|binding| binding.as_lens())
460 .filter(|binding| binding.is_enabled())
461 }
462 
463 pub(crate) fn custom_action_bindings(&self) -> impl Iterator<Item = BindingLens<'_>> {
464 self.editable_custom_action_bindings()
465 .map(|lens| lens.as_binding())
466 .chain(
467 self.fixed_custom_action_bindings
468 .iter()
469 .rev()
470 .filter(|binding| binding.is_enabled())
471 .map(FixedBinding::as_lens),
472 )
473 }
474}
475 
476/// Struct that stores distinct keybindings depending on the platform the application is running on.
477pub struct PerPlatformKeystroke {
478 /// The binding that should be used on mac.
479 pub mac: &'static str,
480 /// The binding that should be used on linux and windows.
481 pub linux_and_windows: &'static str,
482}
483 
484impl FixedBinding {
485 /// Constructs a new [`FixedBinding`] with separate bindings for mac and non-mac platforms.
486 pub fn new_per_platform(
487 keystroke: PerPlatformKeystroke,
488 action: impl Action,
489 context_predicate: ContextPredicate,
490 ) -> Self {
491 let keystroke = if OperatingSystem::get().is_mac() {
492 keystroke.mac
493 } else {
494 keystroke.linux_and_windows
495 };
496 Self::new(keystroke, action, context_predicate)
497 }
498 
499 /// Create a Key Binding for a Typed Action with the given keystrokes
500 pub fn new<A>(
501 keystrokes: impl AsRef<str>,
502 action: A,
503 context_predicate: ContextPredicate,
504 ) -> Self
505 where
506 A: Action,
507 {
508 let keys = keystrokes
509 .as_ref()
510 .split_whitespace()
511 .map(|key| Keystroke::parse(key).expect("Key Binding should be valid"))
512 .collect();
513 Self {
514 trigger: Trigger::Keystrokes(keys),
515 action: Arc::new(action),
516 command_description: None,
517 context_predicate,
518 enabled_predicate: None,
519 group: None,
520 id: BindingId::new(),
521 }
522 }
523 
524 /// Create an empty binding for a typed action
525 pub fn empty<D, A>(description: D, action: A, context_predicate: ContextPredicate) -> Self
526 where
527 A: Action,
528 D: Into<BindingDescription>,
529 {
530 Self {
531 trigger: Trigger::Empty,
532 action: Arc::new(action),
533 command_description: Some(description.into()),
534 context_predicate,
535 enabled_predicate: None,
536 group: None,
537 id: BindingId::new(),
538 }
539 }
540 
541 /// Create a Standard Action binding for a Typed action
542 pub fn standard<A>(
543 saction: StandardAction,
544 action: A,
545 context_predicate: ContextPredicate,
546 ) -> Self
547 where
548 A: Action,
549 {
550 Self {
551 trigger: Trigger::Standard(saction),
552 action: Arc::new(action),
553 command_description: None,
554 context_predicate,
555 enabled_predicate: None,
556 group: None,
557 id: BindingId::new(),
558 }
559 }
560 
561 /// Create a Custom Action (identified by its `CustomTag`) binding for a Typed Action
562 pub fn custom<T, A, D>(
563 caction: T,
564 action: A,
565 description: D,
566 context_predicate: ContextPredicate,
567 ) -> Self
568 where
569 T: Into<CustomTag>,
570 A: Action,
571 D: Into<BindingDescription>,
572 {
573 Self {
574 trigger: Trigger::Custom(caction.into()),
575 action: Arc::new(action),
576 command_description: Some(description.into()),
577 context_predicate,
578 enabled_predicate: None,
579 group: None,
580 id: BindingId::new(),
581 }
582 }
583 
584 /// Sets the group for which this binding is a part of. This can be used to group bindings
585 /// when reading all bindings from the [`Keymap`] (see [`Keymap::bindings`]).
586 pub fn with_group(mut self, group: &'static str) -> Self {
587 self.group = Some(group);
588 self
589 }
590 
591 /// Set a predicate for globally enabling/disabling this binding (by default, bindings are
592 /// always enabled). See [`EnabledPredicate`] on when to use this instead of a context
593 /// predicate.
594 pub fn with_enabled(mut self, enabled: EnabledPredicate) -> Self {
595 self.enabled_predicate = Some(enabled);
596 self
597 }
598 
599 pub fn trigger(&self) -> &Trigger {
600 &self.trigger
601 }
602 
603 pub fn action(&self) -> &dyn Action {
604 &self.action
605 }
606 
607 pub fn with_command_description<S: Into<BindingDescription>>(mut self, description: S) -> Self {
608 self.command_description = Some(description.into());
609 self
610 }
611 
612 /// Determine if this binding is globally enabled. This must not be cached.
613 ///
614 /// See [`EnabledPredicate`] on why a binding might be disabled.
615 fn is_enabled(&self) -> bool {
616 self.enabled_predicate.is_none_or(|predicate| predicate())
617 }
618 
619 /// Create a lens into this Binding's data
620 fn as_lens(&self) -> BindingLens<'_> {
621 BindingLens {
622 name: Default::default(),
623 trigger: &self.trigger,
624 action: &self.action,
625 context_predicate: &self.context_predicate,
626 description: self.command_description.as_ref(),
627 original_trigger: None,
628 group: self.group,
629 id: self.id,
630 }
631 }
632}
633 
634impl EditableBinding {
635 pub fn new<D, A>(name: &'static str, description: D, action: A) -> Self
636 where
637 D: Into<BindingDescription>,
638 A: Action,
639 {
640 // Note: Explicitly not supporting registering legacy actions, as they will be removed
641 // when the conversion to editable bindings is complete
642 EditableBinding {
643 name,
644 description: description.into(),
645 action: Arc::new(action),
646 context_predicate: ContextPredicate::Just(true),
647 enabled_predicate: None,
648 group: None,
649 trigger: Trigger::Empty,
650 custom_trigger: None,
651 id: BindingId::new(),
652 }
653 }
654 
655 pub fn with_context_predicate(mut self, context: ContextPredicate) -> Self {
656 self.context_predicate = context;
657 self
658 }
659 
660 /// Set a predicate for globally enabling/disabling this binding (by default, bindings are
661 /// always enabled). See [`EnabledPredicate`] on when to use this instead of a context
662 /// predicate.
663 pub fn with_enabled(mut self, enabled: EnabledPredicate) -> Self {
664 self.enabled_predicate = Some(enabled);
665 self
666 }
667 
668 /// Sets the group for which this binding is a part of. This can be used to group bindings
669 /// when reading all bindings from the [`Keymap`] (see [`Keymap::editable_bindings`]).
670 pub fn with_group(mut self, group: &'static str) -> Self {
671 self.group = Some(group);
672 self
673 }
674 
675 /// Sets the binding to that of `binding` if the current operating system is
676 /// [`OperatingSystem::Mac`]. Noops otherwise.
677 pub fn with_mac_key_binding<K>(self, binding: K) -> Self
678 where
679 K: AsRef<str>,
680 {
681 if OperatingSystem::get() == OperatingSystem::Mac {
682 self.with_key_binding(binding)
683 } else {
684 self
685 }
686 }
687 
688 /// Sets the binding to that of `binding` if the current operating system is
689 /// [`OperatingSystem::Linux`] or [`OperatingSystem::Windows`]. Noops otherwise.
690 pub fn with_linux_or_windows_key_binding<K>(self, binding: K) -> Self
691 where
692 K: AsRef<str>,
693 {
694 if matches!(
695 OperatingSystem::get(),
696 OperatingSystem::Linux | OperatingSystem::Windows
697 ) {
698 self.with_key_binding(binding)
699 } else {
700 self
701 }
702 }
703 
704 pub fn with_key_binding<K>(mut self, binding: K) -> Self
705 where
706 K: AsRef<str>,
707 {
708 let keystrokes = binding
709 .as_ref()
710 .split_whitespace()
711 .map(|key| Keystroke::parse(key).expect("Invalid keystroke"))
712 .collect();
713 self.trigger = Trigger::Keystrokes(keystrokes);
714 self
715 }
716 
717 pub fn with_standard_action(mut self, binding: StandardAction) -> Self {
718 self.trigger = Trigger::Standard(binding);
719 self
720 }
721 
722 pub fn with_custom_action<C>(mut self, binding: C) -> Self
723 where
724 C: Into<CustomTag>,
725 {
726 self.trigger = Trigger::Custom(binding.into());
727 self
728 }
729 
730 fn as_lens(&self) -> EditableBindingLens<'_> {
731 let (trigger, original_trigger) = if let Some(custom_trigger) = self.custom_trigger.as_ref()
732 {
733 (custom_trigger, Some(&self.trigger))
734 } else {
735 (&self.trigger, None)
736 };
737 EditableBindingLens {
738 name: self.name,
739 description: &self.description,
740 action: &self.action,
741 context: &self.context_predicate,
742 enabled: self.enabled_predicate,
743 trigger,
744 original_trigger,
745 group: self.group,
746 id: self.id,
747 }
748 }
749}
750 
751impl<'a> EditableBindingLens<'a> {
752 /// Create a lens into the binding information for the underlying `EditableBinding`
753 ///
754 /// Will return `None` if there is no `trigger` since there is no associated key binding
755 fn as_binding(&self) -> BindingLens<'a> {
756 BindingLens {
757 name: self.name,
758 trigger: self.trigger,
759 action: self.action,
760 context_predicate: self.context,
761 description: Some(self.description),
762 original_trigger: self.original_trigger,
763 group: self.group,
764 id: self.id,
765 }
766 }
767 
768 /// Determine if this binding is globally enabled. This must not be cached.
769 ///
770 /// See [`EnabledPredicate`] on why a binding might be disabled.
771 fn is_enabled(&self) -> bool {
772 self.enabled.is_none_or(|predicate| predicate())
773 }
774 
775 /// Determine if this action applies to the given context
776 pub fn in_context(&self, context: &Context) -> bool {
777 self.context.eval(context)
778 }
779}
780 
781lazy_static! {
782 /// List of the valid special key names, used when parsing Keystrokes
783 pub static ref VALID_SPECIAL_KEYS: HashSet<&'static str> = HashSet::from([
784 "up",
785 "down",
786 "left",
787 "right",
788 "home",
789 "end",
790 "pageup",
791 "pagedown",
792 "backspace",
793 "enter",
794 "insert",
795 "delete",
796 "escape",
797 "tab",
798 "numpadenter",
799 "f1",
800 "f2",
801 "f3",
802 "f4",
803 "f5",
804 "f6",
805 "f7",
806 "f8",
807 "f9",
808 "f10",
809 "f11",
810 "f12",
811 "f13",
812 "f14",
813 "f15",
814 "f16",
815 "f17",
816 "f18",
817 "f19",
818 "f20",
819 ]);
820}
821 
822impl Keystroke {
823 pub fn is_valid_key(key_name: &str) -> bool {
824 key_name.chars().count() == 1 || Self::is_valid_special_key(key_name)
825 }
826 
827 pub fn has_any_modifier(&self) -> bool {
828 self.ctrl || self.alt || self.shift || self.cmd || self.meta
829 }
830 
831 pub fn is_unmodified(&self) -> bool {
832 !self.has_any_modifier()
833 }
834 
835 pub fn is_unmodified_key(&self, key: &str) -> bool {
836 self.key == key && self.is_unmodified()
837 }
838 
839 pub fn is_unmodified_enter(&self) -> bool {
840 (self.key == "enter" || self.key == "numpadenter") && self.is_unmodified()
841 }
842 
843 pub fn is_shift_tab(&self) -> bool {
844 self.key == "tab" && self.shift && !self.ctrl && !self.alt && !self.cmd && !self.meta
845 }
846 
847 /// Returns whether the `key` is the name of a valid special key. A key is considered "special"
848 /// if it is the name of a nonprintable physical key on the keyboard, such as `backspace` or
849 /// `enter`.
850 pub fn is_valid_special_key(key_name: &str) -> bool {
851 VALID_SPECIAL_KEYS.contains(key_name)
852 }
853 
854 /// Attempts to create a new [`Keystroke`] from the given source string. The source string is
855 /// assumed to be a string that contains a sequence of characters separated by `-`.
856 ///
857 /// ## Supported Modifiers
858 /// The following modifiers are supported:
859 /// * `cmd`: The command key on Mac.
860 /// * `cmdorctrl`: Represents "cmd" on Mac and "ctrl" on Linux and Windows.
861 /// * `ctrl`
862 /// * `shift`
863 /// * `alt`
864 /// * `meta`
865 ///
866 ///
867 /// ## Supported Keycodes
868 /// The following key codes are supported:
869 /// * `0-9`
870 /// * `a-z`
871 /// * `A-Z`
872 /// * `f1`-`f20`
873 /// * Various Punctuation: `)`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `:`, `;`, `:`, `+`,
874 /// `=`, `<`, `,`, `_`, `-`, `>`, `.`, `?`, `/`, `~`, `` ` ``, `{`, `]`, `[`, `|`,`\`, `}`.
875 /// * `space`
876 /// * `up`, `down`, `left`, `right`
877 /// * `home` and `end`
878 /// * `pageup` and `pagedown`
879 /// * `backspace`
880 /// * `enter`
881 /// * `insert`
882 /// * `delete`
883 /// * `escape`
884 /// * `tab`
885 /// * `numpadenter`
886 pub fn parse(source: impl AsRef<str>) -> anyhow::Result<Self> {
887 let source = source.as_ref();
888 let mut ctrl = false;
889 let mut alt = false;
890 let mut shift = false;
891 let mut cmd = false;
892 let mut meta = false;
893 let mut key = None;
894 
895 let mut components = source.split('-').peekable();
896 while let Some(component) = components.next() {
897 match component {
898 "ctrl" => ctrl = true,
899 "alt" => alt = true,
900 "shift" => shift = true,
901 "cmd" => cmd = true,
902 "meta" => meta = true,
903 "cmdorctrl" => {
904 if OperatingSystem::get() == OperatingSystem::Mac {
905 cmd = true
906 } else {
907 ctrl = true
908 }
909 }
910 "space" => key = Some(String::from(" ")),
911 _ => {
912 if let Some(component) = components.peek() {
913 if component.is_empty() && source.ends_with('-') {
914 key = Some(String::from("-"));
915 break;
916 } else {
917 return Err(anyhow!("Invalid keystroke `{}`", source));
918 }
919 } else if Self::is_valid_key(component) {
920 key = Some(component.into());
921 } else {
922 return Err(anyhow!("Unknown key `{}`", component));
923 }
924 }
925 }
926 }
927 
928 // Make sure that we aren't accidentally registering a keybinding
929 // with shift + lowercase (e.g. shift-r), which will never actually be
930 // sent (since the OS sends ctrl-R in cases like this)
931 if cfg!(debug_assertions) {
932 let stroke = match &key {
933 Some(key) if key.chars().count() == 1 => {
934 Some(key.chars().next().expect("Character should exist"))
935 }
936 _ => None,
937 };
938 match stroke {
939 Some(stroke) if shift && stroke.is_lowercase() => {
940 panic!("Invalid keystroke - shift + letter should be uppercase: {source}")
941 }
942 Some(stroke) if !shift && stroke.is_uppercase() => panic!(
943 "Invalid keystroke - without shift, letter should be lowercase: {source}"
944 ),
945 _ => (),
946 };
947 }
948 
949 Ok(Keystroke {
950 ctrl,
951 alt,
952 shift,
953 cmd,
954 meta,
955 key: key.ok_or_else(|| anyhow!("Invalid keystroke: key is unset"))?,
956 })
957 }
958 
959 pub fn normalized(&self) -> String {
960 let mut s = String::new();
961 if self.ctrl {
962 s.push_str("ctrl-");
963 }
964 if self.alt {
965 s.push_str("alt-");
966 }
967 if self.shift {
968 s.push_str("shift-");
969 }
970 if self.cmd {
971 s.push_str("cmd-");
972 }
973 if self.meta {
974 s.push_str("meta-");
975 }
976 s.push_str(match self.key.as_str() {
977 " " => "space",
978 k => k,
979 });
980 
981 s
982 }
983 
984 /// Returns the keybinding string using special characters to present ctrl/alt/shift/cmd keys.
985 /// Can be used for displaying the key shortcuts in the UI. Use `normalized` when defining an
986 /// actual trigger for the action.
987 pub fn displayed(&self) -> String {
988 let mut s = Vec::new();
989 if self.ctrl {
990 let character = if OperatingSystem::get().is_mac() {
991 "⌃"
992 } else {
993 "Ctrl"
994 };
995 s.push(character.into());
996 }
997 if self.alt {
998 let character = if OperatingSystem::get().is_mac() {
999 "⌥"
1000 } else {
1001 "Alt"
1002 };
1003 s.push(character.into());
1004 }
1005 if self.shift {
1006 let character = if OperatingSystem::get().is_mac() {
1007 "⇧"
1008 } else {
1009 "Shift"
1010 };
1011 s.push(character.into());
1012 }
1013 if self.cmd {
1014 let character = if OperatingSystem::get().is_mac() {
1015 "⌘"
1016 } else {
1017 "Logo"
1018 };
1019 s.push(character.into());
1020 }
1021 if self.meta {
1022 s.push("Meta".into());
1023 }
1024 
1025 // Always treat the key as uppercase--this matches how operating systems and most
1026 // applications display keybindings.
1027 s.push(match self.key.as_str() {
1028 "up" => "↑".into(),
1029 "down" => "↓".into(),
1030 "left" => "←".into(),
1031 "right" => "→".into(),
1032 "\t" => "Tab".into(),
1033 " " => "Space".into(),
1034 "enter" => "⏎".into(),
1035 "backspace" => "⌫".into(),
1036 key => {
1037 // Capitalize the first letter of the key name
1038 key.chars()
1039 .next()
1040 .map(|c| c.to_ascii_uppercase())
1041 .into_iter()
1042 .chain(key.chars().skip(1))
1043 .collect::<String>()
1044 }
1045 });
1046 
1047 if OperatingSystem::get().is_mac() {
1048 // On mac, we want to display compactly as "⌘I"
1049 s.join("")
1050 } else {
1051 // On windows and linux, we want to display "Ctrl Shift I" instead of "CtrlShiftI"
1052 s.join(" ")
1053 }
1054 }
1055}
1056 
1057#[cfg(test)]
1058#[path = "keymap_test.rs"]
1059mod tests;
1060