feat(input): port keyboard, keybinding and mouse input handling
Ports ZIG/src/input (except macOS keymaps) into ghostty-input: - key, key_mods (incl. RemapSet), the physical keycode table - key_encode: legacy, modifyOtherKeys and kitty keyboard protocol encoders, function key tables - Binding: all keybinding actions with their user-visible docs (for +list-actions), triggers, sets with sequences, chains, leader keys and key tables, and formatting back to config strings - command palette defaults, mouse, paste safety checks, kitty helpers, links, helpgen action docs Deviations (docs/rewrite/notes/ghostty-input.md): Mods.super is named super_; case folding uses char::to_lowercase plus Ghostty's special cases instead of uucode tables; chain parents are trigger paths instead of pointers; a Zig bug where chained leaves inside sequences corrupted the shared prefix when formatting is fixed (covered by a test). Integration left: building encode/paste Options from terminal modes, and implementing the EntryFormatter trait in ghostty-config. Test Plan: - cargo test -p ghostty-input (220 tests; 8 macOS-only skipped) - cargo clippy -p ghostty-input --all-targets -- -D warnings
This commit is contained in:
1 parent
3323c3f525
commit
3afe938b95
21 files changed
+13413
-1
No files matched your search
@@ -0,0 +1,787 @@
|
||||
//! 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;
|
||||
use std::fmt;
|
||||
use std::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;
|
||||
use crate::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.
|
||||
///
|
||||
/// DEVIATION: Zig uses uucode's full case folding table. Here we use
|
||||
/// `char::to_lowercase` (single-char results only) plus a few simple-folding
|
||||
/// special cases; multi-codepoint foldings return the codepoint as-is exactly
|
||||
/// like Zig does.
|
||||
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.
|
||||
// If more codepoints are produced then we return the codepoint
|
||||
// as-is which isn't correct but until we have a failing test
|
||||
// then I don't want to handle this.
|
||||
let Some(c) = char::from_u32(cp) else {
|
||||
return [cp, 0, 0];
|
||||
};
|
||||
// Simple-fold differences from lowercase.
|
||||
match c {
|
||||
'\u{00B5}' => return [0x03BC, 0, 0],
|
||||
'\u{017F}' => return ['s' as u32, 0, 0],
|
||||
'\u{03C2}' => return [0x03C3, 0, 0],
|
||||
_ => {}
|
||||
}
|
||||
let mut it = c.to_lowercase();
|
||||
match (it.next(), it.next()) {
|
||||
(Some(l), None) => [l as u32, 0, 0],
|
||||
_ => [cp, 0, 0],
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
Reference in new issue
Block a user