rustfmt.toml sets group_imports, imports_granularity and imports_layout, which are nightly-only; agents formatted with stable rustfmt, which ignored them. This is a formatting-only pass with `cargo +nightly fmt`. Test Plan: - cargo clippy --workspace --all-targets -- -D warnings - cargo test --workspace
788 lines
26 KiB
Rust
788 lines
26 KiB
Rust
//! A binding maps some input trigger to an action. When the trigger
|
|
//! occurs, the action is performed. (ZIG/src/input/Binding.zig)
|
|
|
|
pub mod action;
|
|
pub mod set;
|
|
|
|
use std::{
|
|
collections::hash_map::DefaultHasher,
|
|
fmt,
|
|
hash::{Hash, Hasher},
|
|
};
|
|
|
|
pub use action::{
|
|
Action,
|
|
ActionKey,
|
|
AdjustSelection,
|
|
CloseTabMode,
|
|
CopyToClipboard,
|
|
CrashThread,
|
|
CursorKey,
|
|
GotoWindow,
|
|
InspectorMode,
|
|
NavigateSearch,
|
|
Scope,
|
|
SplitDirection,
|
|
SplitFocusDirection,
|
|
SplitResizeDirection,
|
|
SplitResizeParameter,
|
|
WriteScreen,
|
|
WriteScreenAction,
|
|
WriteScreenFormat,
|
|
};
|
|
pub use set::Set;
|
|
|
|
use crate::{
|
|
key,
|
|
key_mods::{self, Mods},
|
|
};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum Error {
|
|
#[error("invalid format")]
|
|
InvalidFormat,
|
|
#[error("invalid action")]
|
|
InvalidAction,
|
|
}
|
|
|
|
/// A binding: trigger, action and per-binding flags.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Binding {
|
|
/// The trigger that needs to be performed to execute the action.
|
|
pub trigger: Trigger,
|
|
|
|
/// The action to take if this binding matches
|
|
pub action: Action,
|
|
|
|
/// Boolean flags that can be set per binding.
|
|
pub flags: Flags,
|
|
}
|
|
|
|
/// Flags the full binding-scoped flags that can be set per binding.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct Flags {
|
|
/// True if this binding should consume the input when the
|
|
/// action is triggered.
|
|
pub consumed: bool,
|
|
|
|
/// True if this binding should be forwarded to all active surfaces
|
|
/// in the application.
|
|
pub all: bool,
|
|
|
|
/// True if this binding is global. Global bindings should work system-wide
|
|
/// and not just while Ghostty is focused. This may not work on all platforms.
|
|
/// See the keybind config documentation for more information.
|
|
pub global: bool,
|
|
|
|
/// True if this binding should only be triggered if the action can be
|
|
/// performed. If the action can't be performed then the binding acts as
|
|
/// if it doesn't exist.
|
|
pub performable: bool,
|
|
}
|
|
|
|
impl Default for Flags {
|
|
fn default() -> Self {
|
|
Flags {
|
|
consumed: true,
|
|
all: false,
|
|
global: false,
|
|
performable: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Flags {
|
|
/// Converts this to a C-compatible value (packed bits, `consumed` = bit 0).
|
|
pub fn cval(self) -> u8 {
|
|
u8::from(self.consumed)
|
|
| u8::from(self.all) << 1
|
|
| u8::from(self.global) << 2
|
|
| u8::from(self.performable) << 3
|
|
}
|
|
}
|
|
|
|
/// Elements yielded by the [`Parser`].
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum Elem {
|
|
/// A leader trigger in a sequence.
|
|
Leader(Trigger),
|
|
|
|
/// The final trigger and action in a sequence.
|
|
Binding(Binding),
|
|
|
|
/// A chained action `chain=<action>` that should be appended
|
|
/// to the previous binding. Note that any action is parsed, including
|
|
/// invalid actions for chains such as `unbind`. We expect downstream
|
|
/// consumers to validate that the action is valid for chaining.
|
|
Chain(Action),
|
|
}
|
|
|
|
/// Full binding parser. The binding parser is implemented as an iterator
|
|
/// which yields elements to support multi-key sequences.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Parser<'a> {
|
|
trigger_it: SequenceIterator<'a>,
|
|
action: Action,
|
|
flags: Flags,
|
|
chain: bool,
|
|
}
|
|
|
|
impl<'a> Parser<'a> {
|
|
pub fn new(raw_input: &'a str) -> Result<Parser<'a>, Error> {
|
|
let (flags, start_idx) = parse_flags(raw_input)?;
|
|
let input = &raw_input[start_idx..];
|
|
|
|
// Find the equal sign. This is more complicated than it seems on
|
|
// the surface because we need to ignore equal signs that are
|
|
// part of the trigger.
|
|
let eql_idx: usize = 'eql: {
|
|
// TODO: We should change this parser into a real state machine
|
|
// based parser that parses the trigger fully, then yields the
|
|
// action after. The loop below is a total mess.
|
|
let mut offset = 0usize;
|
|
let bytes = input.as_bytes();
|
|
while let Some(offset_idx) = input[offset..].find('=') {
|
|
// Find: '=+ctrl' or '==action'
|
|
let idx = offset + offset_idx;
|
|
if idx + 1 < bytes.len() && (bytes[idx + 1] == b'+' || bytes[idx + 1] == b'=') {
|
|
offset += offset_idx + 1;
|
|
continue;
|
|
}
|
|
|
|
// Looks like the real equal sign.
|
|
break 'eql idx;
|
|
}
|
|
|
|
return Err(Error::InvalidFormat);
|
|
};
|
|
|
|
// Detect chains. Chains must not have flags.
|
|
let chain = &input[..eql_idx] == "chain";
|
|
if chain && start_idx > 0 {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
|
|
// Sequence iterator goes up to the equal, action is after. We can
|
|
// parse the action now.
|
|
Ok(Parser {
|
|
trigger_it: SequenceIterator {
|
|
// This is kind of hacky but we put a dummy trigger
|
|
// for chained inputs. The `next` will never yield this
|
|
// because we have chain set.
|
|
input: if chain { "a" } else { &input[..eql_idx] },
|
|
i: 0,
|
|
},
|
|
action: Action::parse(&input[eql_idx + 1..])?,
|
|
flags,
|
|
chain,
|
|
})
|
|
}
|
|
|
|
pub fn next_elem(&mut self) -> Result<Option<Elem>, Error> {
|
|
// Get our trigger. If we're out of triggers then we're done.
|
|
let Some(trigger) = self.trigger_it.next_trigger()? else {
|
|
return Ok(None);
|
|
};
|
|
|
|
// If this is our last trigger then it is our final binding.
|
|
if !self.trigger_it.done() {
|
|
// Global/all bindings can't be sequences
|
|
if self.flags.global || self.flags.all {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
return Ok(Some(Elem::Leader(trigger)));
|
|
}
|
|
|
|
// If we're a chain then return it as-is.
|
|
if self.chain {
|
|
return Ok(Some(Elem::Chain(self.action.clone())));
|
|
}
|
|
|
|
// Out of triggers, yield the final action.
|
|
Ok(Some(Elem::Binding(Binding {
|
|
trigger,
|
|
action: self.action.clone(),
|
|
flags: self.flags,
|
|
})))
|
|
}
|
|
|
|
pub fn reset(&mut self) {
|
|
self.trigger_it.i = 0;
|
|
}
|
|
}
|
|
|
|
fn parse_flags(raw_input: &str) -> Result<(Flags, usize), Error> {
|
|
let mut flags = Flags::default();
|
|
|
|
let mut start_idx = 0usize;
|
|
let mut input = raw_input;
|
|
// Find the next prefix
|
|
while let Some(idx) = input.find(':') {
|
|
let prefix = &input[..idx];
|
|
|
|
// If the prefix is one of our flags then set it.
|
|
match prefix {
|
|
"all" => {
|
|
if flags.all {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
flags.all = true;
|
|
}
|
|
"global" => {
|
|
if flags.global {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
flags.global = true;
|
|
}
|
|
"unconsumed" => {
|
|
if !flags.consumed {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
flags.consumed = false;
|
|
}
|
|
"performable" => {
|
|
if flags.performable {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
flags.performable = true;
|
|
}
|
|
// If we don't recognize the prefix then we're done. We
|
|
// let any unknown prefix fallthrough to trigger-specific
|
|
// parsing in case there are trigger-specific prefixes
|
|
// (none currently but historically there was `physical:`
|
|
// at one point). Breaking here lets us always implement new
|
|
// prefixes.
|
|
_ => break,
|
|
}
|
|
|
|
// Move past the prefix
|
|
start_idx += idx + 1;
|
|
input = &input[idx + 1..];
|
|
}
|
|
|
|
Ok((flags, start_idx))
|
|
}
|
|
|
|
/// An iterator that yields each trigger in a sequence of triggers. For
|
|
/// example, the sequence "ctrl+a>ctrl+b" would yield "ctrl+a" and then
|
|
/// "ctrl+b".
|
|
#[derive(Debug, Clone)]
|
|
pub struct SequenceIterator<'a> {
|
|
/// The input of triggers. This is expected to be ONLY triggers. Things
|
|
/// like the "unconsumed:" prefix or action must be stripped before
|
|
/// passing to this iterator.
|
|
pub input: &'a str,
|
|
pub i: usize,
|
|
}
|
|
|
|
impl<'a> SequenceIterator<'a> {
|
|
pub fn new(input: &'a str) -> Self {
|
|
SequenceIterator { input, i: 0 }
|
|
}
|
|
|
|
/// Returns the next trigger in the sequence if there is no parsing error.
|
|
pub fn next_trigger(&mut self) -> Result<Option<Trigger>, Error> {
|
|
if self.done() {
|
|
return Ok(None);
|
|
}
|
|
let rem = &self.input[self.i..];
|
|
let idx = rem.find('>').unwrap_or(rem.len());
|
|
self.i += idx + 1;
|
|
Ok(Some(Trigger::parse(&rem[..idx])?))
|
|
}
|
|
|
|
/// Returns true if there are no more triggers to parse.
|
|
pub fn done(&self) -> bool {
|
|
self.i > self.input.len()
|
|
}
|
|
}
|
|
|
|
/// Parse a single, non-sequenced binding. To support sequences you must
|
|
/// use parse. This is a convenience function for single bindings aimed
|
|
/// primarily at tests.
|
|
///
|
|
/// This doesn't support `chain` either, since chaining requires some
|
|
/// stateful concept of a prior binding.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ParseSingleError {
|
|
Error(Error),
|
|
UnexpectedChain,
|
|
UnexpectedSequence,
|
|
}
|
|
|
|
impl From<Error> for ParseSingleError {
|
|
fn from(e: Error) -> Self {
|
|
ParseSingleError::Error(e)
|
|
}
|
|
}
|
|
|
|
pub fn parse_single(raw_input: &str) -> Result<Binding, ParseSingleError> {
|
|
let mut p = Parser::new(raw_input)?;
|
|
let elem = p.next_elem()?.ok_or(Error::InvalidFormat)?;
|
|
match elem {
|
|
Elem::Leader(_) => Err(ParseSingleError::UnexpectedSequence),
|
|
Elem::Binding(b) => Ok(b),
|
|
Elem::Chain(_) => Err(ParseSingleError::UnexpectedChain),
|
|
}
|
|
}
|
|
|
|
/// Returns true if lhs should be sorted before rhs
|
|
pub fn less_than(lhs: &Binding, rhs: &Binding) -> bool {
|
|
let count = |m: Mods| {
|
|
usize::from(m.super_) + usize::from(m.ctrl) + usize::from(m.shift) + usize::from(m.alt)
|
|
};
|
|
let lhs_count = count(lhs.trigger.mods);
|
|
let rhs_count = count(rhs.trigger.mods);
|
|
|
|
if lhs_count != rhs_count {
|
|
return lhs_count > rhs_count;
|
|
}
|
|
|
|
if lhs.trigger.mods.int() != rhs.trigger.mods.int() {
|
|
return lhs.trigger.mods.int() > rhs.trigger.mods.int();
|
|
}
|
|
|
|
let k = |t: &Trigger| -> i64 {
|
|
match t.key {
|
|
TriggerKey::Physical(k) => k as i64,
|
|
TriggerKey::Unicode(cp) => i64::from(cp),
|
|
// Zig reads the C-union tag's key here (undefined); treat as 0.
|
|
TriggerKey::CatchAll => 0,
|
|
}
|
|
};
|
|
k(&lhs.trigger) < k(&rhs.trigger)
|
|
}
|
|
|
|
/// The key part of a [`Trigger`].
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum TriggerKey {
|
|
/// key is the "physical" version. This is the same as mapped for
|
|
/// standard US keyboard layouts. For non-US keyboard layouts, this
|
|
/// is used to bind to a physical key location rather than a translated
|
|
/// key.
|
|
Physical(key::Key),
|
|
|
|
/// This is used for binding to keys that produce a certain unicode
|
|
/// codepoint. This is useful for binding to keys that don't have a
|
|
/// registered keycode with Ghostty.
|
|
Unicode(u32),
|
|
|
|
/// A catch-all key that matches any key press that is otherwise
|
|
/// unbound.
|
|
CatchAll,
|
|
}
|
|
|
|
/// Trigger is the associated key state that can trigger an action.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Trigger {
|
|
/// The key that has to be pressed for a binding to take action.
|
|
pub key: TriggerKey,
|
|
|
|
/// The key modifiers that must be active for this to match.
|
|
pub mods: Mods,
|
|
}
|
|
|
|
impl Default for Trigger {
|
|
fn default() -> Self {
|
|
Trigger {
|
|
key: TriggerKey::Physical(key::Key::Unidentified),
|
|
mods: Mods::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Backwards compatible key names from Ghostty 1.1.x. Ghostty 1.2+ doesn't
|
|
/// support these anymore since we moved to W3C key codes.
|
|
pub const BACKWARDS_COMPATIBLE_KEYS: &[(&str, TriggerKey)] = {
|
|
use TriggerKey::{Physical as P, Unicode as U};
|
|
use key::Key as K;
|
|
&[
|
|
("zero", U('0' as u32)),
|
|
("one", U('1' as u32)),
|
|
("two", U('2' as u32)),
|
|
("three", U('3' as u32)),
|
|
("four", U('4' as u32)),
|
|
("five", U('5' as u32)),
|
|
("six", U('6' as u32)),
|
|
("seven", U('7' as u32)),
|
|
("eight", U('8' as u32)),
|
|
("nine", U('9' as u32)),
|
|
("plus", U('+' as u32)),
|
|
("apostrophe", U('\'' as u32)),
|
|
("grave_accent", P(K::Backquote)),
|
|
("left_bracket", P(K::BracketLeft)),
|
|
("right_bracket", P(K::BracketRight)),
|
|
("up", P(K::ArrowUp)),
|
|
("down", P(K::ArrowDown)),
|
|
("left", P(K::ArrowLeft)),
|
|
("right", P(K::ArrowRight)),
|
|
("kp_0", P(K::Numpad0)),
|
|
("kp_1", P(K::Numpad1)),
|
|
("kp_2", P(K::Numpad2)),
|
|
("kp_3", P(K::Numpad3)),
|
|
("kp_4", P(K::Numpad4)),
|
|
("kp_5", P(K::Numpad5)),
|
|
("kp_6", P(K::Numpad6)),
|
|
("kp_7", P(K::Numpad7)),
|
|
("kp_8", P(K::Numpad8)),
|
|
("kp_9", P(K::Numpad9)),
|
|
("kp_add", P(K::NumpadAdd)),
|
|
("kp_subtract", P(K::NumpadSubtract)),
|
|
("kp_multiply", P(K::NumpadMultiply)),
|
|
("kp_divide", P(K::NumpadDivide)),
|
|
("kp_decimal", P(K::NumpadDecimal)),
|
|
("kp_enter", P(K::NumpadEnter)),
|
|
("kp_equal", P(K::NumpadEqual)),
|
|
("kp_separator", P(K::NumpadSeparator)),
|
|
("kp_left", P(K::NumpadLeft)),
|
|
("kp_right", P(K::NumpadRight)),
|
|
("kp_up", P(K::NumpadUp)),
|
|
("kp_down", P(K::NumpadDown)),
|
|
("kp_page_up", P(K::NumpadPageUp)),
|
|
("kp_page_down", P(K::NumpadPageDown)),
|
|
("kp_home", P(K::NumpadHome)),
|
|
("kp_end", P(K::NumpadEnd)),
|
|
("kp_insert", P(K::NumpadInsert)),
|
|
("kp_delete", P(K::NumpadDelete)),
|
|
("kp_begin", P(K::NumpadBegin)),
|
|
("left_shift", P(K::ShiftLeft)),
|
|
("right_shift", P(K::ShiftRight)),
|
|
("left_control", P(K::ControlLeft)),
|
|
("right_control", P(K::ControlRight)),
|
|
("left_alt", P(K::AltLeft)),
|
|
("right_alt", P(K::AltRight)),
|
|
("left_super", P(K::MetaLeft)),
|
|
("right_super", P(K::MetaRight)),
|
|
// Physical variants. This is a blunt approach to this but its
|
|
// glue for backwards compatibility so I'm not too worried about
|
|
// making this super nice.
|
|
("physical:zero", P(K::Digit0)),
|
|
("physical:one", P(K::Digit1)),
|
|
("physical:two", P(K::Digit2)),
|
|
("physical:three", P(K::Digit3)),
|
|
("physical:four", P(K::Digit4)),
|
|
("physical:five", P(K::Digit5)),
|
|
("physical:six", P(K::Digit6)),
|
|
("physical:seven", P(K::Digit7)),
|
|
("physical:eight", P(K::Digit8)),
|
|
("physical:nine", P(K::Digit9)),
|
|
("physical:apostrophe", P(K::Quote)),
|
|
("physical:grave_accent", P(K::Backquote)),
|
|
("physical:left_bracket", P(K::BracketLeft)),
|
|
("physical:right_bracket", P(K::BracketRight)),
|
|
("physical:up", P(K::ArrowUp)),
|
|
("physical:down", P(K::ArrowDown)),
|
|
("physical:left", P(K::ArrowLeft)),
|
|
("physical:right", P(K::ArrowRight)),
|
|
("physical:kp_0", P(K::Numpad0)),
|
|
("physical:kp_1", P(K::Numpad1)),
|
|
("physical:kp_2", P(K::Numpad2)),
|
|
("physical:kp_3", P(K::Numpad3)),
|
|
("physical:kp_4", P(K::Numpad4)),
|
|
("physical:kp_5", P(K::Numpad5)),
|
|
("physical:kp_6", P(K::Numpad6)),
|
|
("physical:kp_7", P(K::Numpad7)),
|
|
("physical:kp_8", P(K::Numpad8)),
|
|
("physical:kp_9", P(K::Numpad9)),
|
|
("physical:kp_add", P(K::NumpadAdd)),
|
|
("physical:kp_subtract", P(K::NumpadSubtract)),
|
|
("physical:kp_multiply", P(K::NumpadMultiply)),
|
|
("physical:kp_divide", P(K::NumpadDivide)),
|
|
("physical:kp_decimal", P(K::NumpadDecimal)),
|
|
("physical:kp_enter", P(K::NumpadEnter)),
|
|
("physical:kp_equal", P(K::NumpadEqual)),
|
|
("physical:kp_separator", P(K::NumpadSeparator)),
|
|
("physical:kp_left", P(K::NumpadLeft)),
|
|
("physical:kp_right", P(K::NumpadRight)),
|
|
("physical:kp_up", P(K::NumpadUp)),
|
|
("physical:kp_down", P(K::NumpadDown)),
|
|
("physical:kp_page_up", P(K::NumpadPageUp)),
|
|
("physical:kp_page_down", P(K::NumpadPageDown)),
|
|
("physical:kp_home", P(K::NumpadHome)),
|
|
("physical:kp_end", P(K::NumpadEnd)),
|
|
("physical:kp_insert", P(K::NumpadInsert)),
|
|
("physical:kp_delete", P(K::NumpadDelete)),
|
|
("physical:kp_begin", P(K::NumpadBegin)),
|
|
("physical:left_shift", P(K::ShiftLeft)),
|
|
("physical:right_shift", P(K::ShiftRight)),
|
|
("physical:left_control", P(K::ControlLeft)),
|
|
("physical:right_control", P(K::ControlRight)),
|
|
("physical:left_alt", P(K::AltLeft)),
|
|
("physical:right_alt", P(K::AltRight)),
|
|
("physical:left_super", P(K::MetaLeft)),
|
|
("physical:right_super", P(K::MetaRight)),
|
|
]
|
|
};
|
|
|
|
/// The codepoint we use for comparisons. Case folding can result
|
|
/// in more codepoints so we need to use a 3 element array.
|
|
///
|
|
/// Uses the full Unicode case folding table (uucode's `case_folding_full`,
|
|
/// generated by `ghostty-unicode`).
|
|
fn folded_codepoint(cp: u32) -> [u32; 3] {
|
|
// ASCII fast path
|
|
if let Some(c) = char::from_u32(cp)
|
|
&& c.is_ascii_alphabetic()
|
|
{
|
|
return [u32::from(c.to_ascii_lowercase()), 0, 0];
|
|
}
|
|
|
|
// Unicode slow path. Case folding can result in more codepoints.
|
|
ghostty_unicode::case_folding::fold_full(cp)
|
|
}
|
|
|
|
impl Trigger {
|
|
/// Parse a single trigger. The input is expected to be ONLY the trigger
|
|
/// (i.e. in the sequence `a=ignore` input is only `a`). The trigger may
|
|
/// not be part of a sequence (i.e. `a>b`). This parses exactly a single
|
|
/// trigger.
|
|
pub fn parse(input: &str) -> Result<Trigger, Error> {
|
|
if input.is_empty() {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
let mut result = Trigger::default();
|
|
let mut rem: &str = input;
|
|
'outer: while !rem.is_empty() {
|
|
let idx = rem.find('+').unwrap_or(rem.len());
|
|
let part = &rem[..idx];
|
|
rem = if idx >= rem.len() {
|
|
""
|
|
} else {
|
|
&rem[idx + 1..]
|
|
};
|
|
|
|
// Check if its a modifier
|
|
{
|
|
let slot: Option<&mut bool> = match part {
|
|
"shift" => Some(&mut result.mods.shift),
|
|
"ctrl" => Some(&mut result.mods.ctrl),
|
|
"alt" => Some(&mut result.mods.alt),
|
|
"super" => Some(&mut result.mods.super_),
|
|
"caps_lock" => Some(&mut result.mods.caps_lock),
|
|
"num_lock" => Some(&mut result.mods.num_lock),
|
|
_ => None,
|
|
};
|
|
if let Some(slot) = slot {
|
|
// Repeat not allowed
|
|
if *slot {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
*slot = true;
|
|
continue 'outer;
|
|
}
|
|
}
|
|
|
|
// Alias modifiers
|
|
for (alias, m) in key_mods::ALIAS {
|
|
if part == *alias {
|
|
// Repeat not allowed
|
|
let slot = match m {
|
|
key_mods::Mod::Shift => &mut result.mods.shift,
|
|
key_mods::Mod::Ctrl => &mut result.mods.ctrl,
|
|
key_mods::Mod::Alt => &mut result.mods.alt,
|
|
key_mods::Mod::Super => &mut result.mods.super_,
|
|
};
|
|
if *slot {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
*slot = true;
|
|
continue 'outer;
|
|
}
|
|
}
|
|
|
|
// Anything after this point is a key and we only support
|
|
// single keys.
|
|
if !result.is_key_unset() {
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
|
|
// If the part is empty it means that it is actually
|
|
// a literal `+`, which we treat as a Unicode character.
|
|
if part.is_empty() {
|
|
result.key = TriggerKey::Unicode('+' as u32);
|
|
continue;
|
|
}
|
|
|
|
// Check if its a key
|
|
if part != "unidentified"
|
|
&& let Some(k) = key::Key::from_name(part)
|
|
{
|
|
result.key = TriggerKey::Physical(k);
|
|
continue;
|
|
}
|
|
|
|
// If we're still unset and we have exactly one unicode
|
|
// character then we can use that as a key.
|
|
{
|
|
let mut it = part.chars();
|
|
if let (Some(c), None) = (it.next(), it.next()) {
|
|
result.key = TriggerKey::Unicode(c as u32);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Look for a matching w3c name next.
|
|
if let Some(w3c_key) = key::Key::from_w3c(part) {
|
|
result.key = TriggerKey::Physical(w3c_key);
|
|
continue;
|
|
}
|
|
|
|
// Check for catch_all. We do this near the end since its unlikely
|
|
// in most cases that we're setting a catch-all key.
|
|
if part == "catch_all" {
|
|
result.key = TriggerKey::CatchAll;
|
|
continue;
|
|
}
|
|
|
|
// If we're still unset then we look for backwards compatible
|
|
// keys with Ghostty 1.1.x. We do this last so its least likely
|
|
// to impact performance for modern users.
|
|
if let Some((_, old_key)) = BACKWARDS_COMPATIBLE_KEYS.iter().find(|e| e.0 == part) {
|
|
result.key = *old_key;
|
|
continue;
|
|
}
|
|
|
|
// We didn't recognize this value
|
|
return Err(Error::InvalidFormat);
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// Returns true if this trigger has no key set.
|
|
pub fn is_key_unset(&self) -> bool {
|
|
match self.key {
|
|
TriggerKey::Physical(v) => v == key::Key::Unidentified,
|
|
TriggerKey::Unicode(_) | TriggerKey::CatchAll => false,
|
|
}
|
|
}
|
|
|
|
/// Returns a hash code that can be used to uniquely identify this trigger.
|
|
pub fn hash(&self) -> u64 {
|
|
let mut h = DefaultHasher::new();
|
|
self.hash_incremental(&mut h);
|
|
h.finish()
|
|
}
|
|
|
|
/// Hash the trigger into the given hasher (case-folded for unicode keys).
|
|
pub fn hash_incremental<H: Hasher>(&self, h: &mut H) {
|
|
std::mem::discriminant(&self.key).hash(h);
|
|
match self.key {
|
|
TriggerKey::Physical(v) => v.hash(h),
|
|
TriggerKey::Unicode(cp) => folded_codepoint(cp).hash(h),
|
|
TriggerKey::CatchAll => {}
|
|
}
|
|
self.mods.binding().hash(h);
|
|
}
|
|
|
|
/// Returns true if two triggers are equal.
|
|
pub fn equal(&self, other: &Trigger) -> bool {
|
|
self == other
|
|
}
|
|
|
|
/// Returns true if two triggers are equal using folded codepoints.
|
|
pub fn folded_equal(&self, other: &Trigger) -> bool {
|
|
if self.mods != other.mods {
|
|
return false;
|
|
}
|
|
match (self.key, other.key) {
|
|
(TriggerKey::Physical(a), TriggerKey::Physical(b)) => a == b,
|
|
(TriggerKey::Unicode(a), TriggerKey::Unicode(b)) => {
|
|
folded_codepoint(a) == folded_codepoint(b)
|
|
}
|
|
(TriggerKey::CatchAll, TriggerKey::CatchAll) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Convert the trigger to a C API compatible trigger.
|
|
pub fn cval(&self) -> TriggerC {
|
|
match self.key {
|
|
TriggerKey::Physical(v) => TriggerC {
|
|
tag: TriggerCTag::Physical,
|
|
key: v as u32,
|
|
mods: self.mods,
|
|
},
|
|
TriggerKey::Unicode(v) => TriggerC {
|
|
tag: TriggerCTag::Unicode,
|
|
key: v,
|
|
mods: self.mods,
|
|
},
|
|
TriggerKey::CatchAll => TriggerC {
|
|
tag: TriggerCTag::CatchAll,
|
|
key: 0,
|
|
mods: self.mods,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// C API tag for triggers.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(i32)]
|
|
pub enum TriggerCTag {
|
|
Physical,
|
|
Unicode,
|
|
CatchAll,
|
|
}
|
|
|
|
/// The struct used for triggers in the C API (`Trigger.C`).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct TriggerC {
|
|
pub tag: TriggerCTag,
|
|
/// Physical key enum value or unicode codepoint (undefined for catch_all).
|
|
pub key: u32,
|
|
pub mods: Mods,
|
|
}
|
|
|
|
impl fmt::Display for Trigger {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Modifiers first
|
|
if self.mods.super_ {
|
|
f.write_str("super+")?;
|
|
}
|
|
if self.mods.ctrl {
|
|
f.write_str("ctrl+")?;
|
|
}
|
|
if self.mods.alt {
|
|
f.write_str("alt+")?;
|
|
}
|
|
if self.mods.shift {
|
|
f.write_str("shift+")?;
|
|
}
|
|
|
|
// Key
|
|
match self.key {
|
|
TriggerKey::Physical(k) => f.write_str(k.name()),
|
|
TriggerKey::Unicode(c) => {
|
|
// Zig `{u}` writes the codepoint as UTF-8.
|
|
match char::from_u32(c) {
|
|
Some(ch) => write!(f, "{ch}"),
|
|
None => f.write_str("\u{FFFD}"),
|
|
}
|
|
}
|
|
TriggerKey::CatchAll => f.write_str("catch_all"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Hash-map key wrapper for triggers: equality and hashing use the
|
|
/// case-folded comparison (`Trigger.bindingSetEqual`).
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct FoldedTrigger(pub Trigger);
|
|
|
|
impl PartialEq for FoldedTrigger {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.0.folded_equal(&other.0)
|
|
}
|
|
}
|
|
impl Eq for FoldedTrigger {}
|
|
impl Hash for FoldedTrigger {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.0.hash_incremental(state);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|