Skip to main content

rmux_core/
keys.rs

1//! tmux-compatible key code parsing and key table storage.
2#![allow(clippy::unusual_byte_groupings)]
3
4use crate::command_parser::{
5    parse_command_string, CommandParseError, CommandParser, ParsedCommands,
6};
7
8#[path = "keys/defaults.rs"]
9mod defaults;
10#[path = "keys/store.rs"]
11mod store;
12#[path = "keys/string_table.rs"]
13mod string_table;
14
15pub use store::{
16    KeyBinding, KeyBindingDisplay, KeyBindingSortOrder, KeyBindingStore, KeyBindingTable,
17    KeyBindingTableRef,
18};
19use string_table::{
20    decode_mouse_key, key_string_entry_for_key, key_string_search_table, mouse_key_name,
21};
22
23/// tmux-style 64-bit key code.
24pub type KeyCode = u64;
25
26/// Meta modifier bit.
27pub const KEYC_META: KeyCode = 0x0010_0000_0000_00;
28/// Ctrl modifier bit.
29pub const KEYC_CTRL: KeyCode = 0x0020_0000_0000_00;
30/// Shift modifier bit.
31pub const KEYC_SHIFT: KeyCode = 0x0040_0000_0000_00;
32/// Literal flag bit.
33pub const KEYC_LITERAL: KeyCode = 0x0100_0000_0000_00;
34/// Keypad flag bit.
35pub const KEYC_KEYPAD: KeyCode = 0x0200_0000_0000_00;
36/// Cursor flag bit.
37pub const KEYC_CURSOR: KeyCode = 0x0400_0000_0000_00;
38/// Implied-meta flag bit.
39pub const KEYC_IMPLIED_META: KeyCode = 0x0800_0000_0000_00;
40/// Build-modifiers flag bit.
41pub const KEYC_BUILD_MODIFIERS: KeyCode = 0x1000_0000_0000_00;
42/// Vi flag bit.
43pub const KEYC_VI: KeyCode = 0x2000_0000_0000_00;
44/// Sent flag bit.
45pub const KEYC_SENT: KeyCode = 0x4000_0000_0000_00;
46
47/// Key type mask.
48pub const KEYC_MASK_TYPE: KeyCode = 0x0000_ff00_0000_00;
49/// Modifier mask.
50pub const KEYC_MASK_MODIFIERS: KeyCode = 0x00ff_0000_0000_00;
51/// Flag mask.
52pub const KEYC_MASK_FLAGS: KeyCode = 0xff00_0000_0000_00;
53/// Key payload mask.
54pub const KEYC_MASK_KEY: KeyCode = 0x0000_ffff_ffff_ff;
55
56const KEYC_NUSER: u32 = 1000;
57
58/// No key.
59pub const KEYC_NONE: KeyCode = shift_type(KeyCodeType::Function);
60/// Unknown key.
61pub const KEYC_UNKNOWN: KeyCode = KEYC_NONE + 1;
62/// Any key catch-all.
63pub const KEYC_ANY: KeyCode = KEYC_NONE + 4;
64/// Backspace key.
65pub const KEYC_BSPACE: KeyCode = KEYC_NONE + 7;
66/// User key range base.
67pub const KEYC_USER: KeyCode = shift_type(KeyCodeType::User);
68
69const KEYC_FOCUS_IN: KeyCode = KEYC_NONE + 2;
70const KEYC_FOCUS_OUT: KeyCode = KEYC_NONE + 3;
71const KEYC_PASTE_START: KeyCode = KEYC_NONE + 5;
72const KEYC_PASTE_END: KeyCode = KEYC_NONE + 6;
73const KEYC_F1: KeyCode = KEYC_NONE + 8;
74const KEYC_F2: KeyCode = KEYC_NONE + 9;
75const KEYC_F3: KeyCode = KEYC_NONE + 10;
76const KEYC_F4: KeyCode = KEYC_NONE + 11;
77const KEYC_F5: KeyCode = KEYC_NONE + 12;
78const KEYC_F6: KeyCode = KEYC_NONE + 13;
79const KEYC_F7: KeyCode = KEYC_NONE + 14;
80const KEYC_F8: KeyCode = KEYC_NONE + 15;
81const KEYC_F9: KeyCode = KEYC_NONE + 16;
82const KEYC_F10: KeyCode = KEYC_NONE + 17;
83const KEYC_F11: KeyCode = KEYC_NONE + 18;
84const KEYC_F12: KeyCode = KEYC_NONE + 19;
85const KEYC_IC: KeyCode = KEYC_NONE + 20;
86const KEYC_DC: KeyCode = KEYC_NONE + 21;
87const KEYC_HOME: KeyCode = KEYC_NONE + 22;
88const KEYC_END: KeyCode = KEYC_NONE + 23;
89const KEYC_NPAGE: KeyCode = KEYC_NONE + 24;
90const KEYC_PPAGE: KeyCode = KEYC_NONE + 25;
91const KEYC_BTAB: KeyCode = KEYC_NONE + 26;
92const KEYC_UP: KeyCode = KEYC_NONE + 27;
93const KEYC_DOWN: KeyCode = KEYC_NONE + 28;
94const KEYC_LEFT: KeyCode = KEYC_NONE + 29;
95const KEYC_RIGHT: KeyCode = KEYC_NONE + 30;
96const KEYC_KP_SLASH: KeyCode = KEYC_NONE + 31;
97const KEYC_KP_STAR: KeyCode = KEYC_NONE + 32;
98const KEYC_KP_MINUS: KeyCode = KEYC_NONE + 33;
99const KEYC_KP_SEVEN: KeyCode = KEYC_NONE + 34;
100const KEYC_KP_EIGHT: KeyCode = KEYC_NONE + 35;
101const KEYC_KP_NINE: KeyCode = KEYC_NONE + 36;
102const KEYC_KP_PLUS: KeyCode = KEYC_NONE + 37;
103const KEYC_KP_FOUR: KeyCode = KEYC_NONE + 38;
104const KEYC_KP_FIVE: KeyCode = KEYC_NONE + 39;
105const KEYC_KP_SIX: KeyCode = KEYC_NONE + 40;
106const KEYC_KP_ONE: KeyCode = KEYC_NONE + 41;
107const KEYC_KP_TWO: KeyCode = KEYC_NONE + 42;
108const KEYC_KP_THREE: KeyCode = KEYC_NONE + 43;
109const KEYC_KP_ENTER: KeyCode = KEYC_NONE + 44;
110const KEYC_KP_ZERO: KeyCode = KEYC_NONE + 45;
111const KEYC_KP_PERIOD: KeyCode = KEYC_NONE + 46;
112const KEYC_REPORT_DARK_THEME: KeyCode = KEYC_NONE + 47;
113const KEYC_REPORT_LIGHT_THEME: KeyCode = KEYC_NONE + 48;
114const KEYC_MOUSE: KeyCode = KEYC_NONE + 49;
115/// Internal drag-in-progress sentinel key.
116pub const KEYC_DRAGGING: KeyCode = KEYC_NONE + 50;
117
118/// Default `list-keys` template.
119pub const LIST_KEYS_TEMPLATE: &str = "#{?notes_only,#{key_prefix} #{p|#{key_string_width}:key_string} #{?key_note,#{key_note},#{key_command}},bind-key #{?key_has_repeat,#{?key_repeat,-r,  },} -T #{p|#{key_table_width}:key_table} #{p|#{key_string_width}:key_string} #{key_command}}";
120
121/// Returns the key bits used for binding lookup.
122#[must_use]
123pub const fn key_code_lookup_bits(key: KeyCode) -> KeyCode {
124    key & (KEYC_MASK_KEY | KEYC_MASK_MODIFIERS)
125}
126
127/// Returns whether the key is a mouse-move key.
128#[must_use]
129pub fn key_code_is_mouse_move(key: KeyCode) -> bool {
130    matches!(
131        decode_mouse_key(key),
132        Some((MouseEventType::MouseMove, _, _))
133    )
134}
135
136/// Converts a canonical key name into a tmux key code.
137#[must_use]
138pub fn key_string_lookup_string(string: &str) -> Option<KeyCode> {
139    if string.eq_ignore_ascii_case("None") {
140        return Some(KEYC_NONE);
141    }
142    if string.eq_ignore_ascii_case("Any") {
143        return Some(KEYC_ANY);
144    }
145
146    if let Some(hex) = string.strip_prefix("0x") {
147        let value = u32::from_str_radix(hex, 16).ok()?;
148        if value < 32 {
149            return Some(KeyCode::from(value));
150        }
151        return char::from_u32(value).map(|character| character as KeyCode);
152    }
153
154    let mut modifiers = 0;
155    let mut rest = string;
156
157    if rest.starts_with('^') && rest.len() > 1 {
158        if rest.chars().count() == 2 {
159            let character = rest.chars().nth(1)?;
160            return Some(character.to_ascii_lowercase() as KeyCode | KEYC_CTRL);
161        }
162        modifiers |= KEYC_CTRL;
163        rest = &rest[1..];
164    }
165
166    modifiers |= parse_modifiers(&mut rest)?;
167    if rest.is_empty() {
168        return None;
169    }
170
171    if rest.is_ascii() {
172        let bytes = rest.as_bytes();
173        if bytes.len() == 1 {
174            let key = KeyCode::from(bytes[0]);
175            if key < 32 {
176                return None;
177            }
178            return Some(key | modifiers);
179        }
180    } else {
181        let mut chars = rest.chars();
182        let character = chars.next()?;
183        if chars.next().is_none() {
184            return Some(character as KeyCode | modifiers);
185        }
186    }
187
188    let mut key = key_string_search_table(rest)?;
189    if modifiers & KEYC_META == 0 {
190        key &= !KEYC_IMPLIED_META;
191    }
192    Some(key | modifiers)
193}
194
195/// Converts a key code into its canonical tmux string.
196#[must_use]
197pub fn key_string_lookup_key(key: KeyCode, with_flags: bool) -> String {
198    let saved = key;
199    let mut output = String::new();
200
201    if key & KEYC_LITERAL != 0 {
202        output.push(char::from_u32((key & 0xff) as u32).unwrap_or('\0'));
203        return maybe_append_flags(output, saved, with_flags);
204    }
205
206    if key & KEYC_CTRL != 0 {
207        output.push_str("C-");
208    }
209    if key & KEYC_META != 0 {
210        output.push_str("M-");
211    }
212    if key & KEYC_SHIFT != 0 {
213        output.push_str("S-");
214    }
215
216    let key = key & KEYC_MASK_KEY;
217    let suffix = match key {
218        KEYC_NONE => Some("None".to_owned()),
219        KEYC_UNKNOWN => Some("Unknown".to_owned()),
220        KEYC_ANY => Some("Any".to_owned()),
221        KEYC_FOCUS_IN => Some("FocusIn".to_owned()),
222        KEYC_FOCUS_OUT => Some("FocusOut".to_owned()),
223        KEYC_PASTE_START => Some("PasteStart".to_owned()),
224        KEYC_PASTE_END => Some("PasteEnd".to_owned()),
225        KEYC_REPORT_DARK_THEME => Some("ReportDarkTheme".to_owned()),
226        KEYC_REPORT_LIGHT_THEME => Some("ReportLightTheme".to_owned()),
227        KEYC_MOUSE => Some("Mouse".to_owned()),
228        KEYC_DRAGGING => Some("Dragging".to_owned()),
229        value if value == make_mouse_key(MouseEventType::MouseMove, 0, MouseLocation::Pane) => {
230            Some("MouseMovePane".to_owned())
231        }
232        value if value == make_mouse_key(MouseEventType::MouseMove, 0, MouseLocation::Status) => {
233            Some("MouseMoveStatus".to_owned())
234        }
235        value
236            if value == make_mouse_key(MouseEventType::MouseMove, 0, MouseLocation::StatusLeft) =>
237        {
238            Some("MouseMoveStatusLeft".to_owned())
239        }
240        value
241            if value
242                == make_mouse_key(MouseEventType::MouseMove, 0, MouseLocation::StatusRight) =>
243        {
244            Some("MouseMoveStatusRight".to_owned())
245        }
246        value
247            if value
248                == make_mouse_key(MouseEventType::MouseMove, 0, MouseLocation::StatusDefault) =>
249        {
250            Some("MouseMoveStatusDefault".to_owned())
251        }
252        value if value == make_mouse_key(MouseEventType::MouseMove, 0, MouseLocation::Border) => {
253            Some("MouseMoveBorder".to_owned())
254        }
255        value if is_user_key(value) => Some(format!("User{}", value - KEYC_USER)),
256        value => key_string_entry_for_key(value)
257            .map(|entry| entry.string.to_owned())
258            .or_else(|| mouse_key_name(value))
259            .or_else(|| {
260                if is_unicode_key(value) {
261                    char::from_u32(value as u32).map(|character| character.to_string())
262                } else if value > 255 {
263                    Some(format!("Invalid#{saved:#x}"))
264                } else if (33..=126).contains(&value) {
265                    Some((value as u8 as char).to_string())
266                } else if value == 127 {
267                    Some("C-?".to_owned())
268                } else if value >= 128 {
269                    Some(format!("\\{:o}", value))
270                } else {
271                    key_string_entry_for_key(value).map(|entry| entry.string.to_owned())
272                }
273            }),
274    };
275
276    if let Some(suffix) = suffix {
277        output.push_str(&suffix);
278    }
279    maybe_append_flags(output, saved, with_flags)
280}
281
282/// Converts a key code into bytes suitable for the legacy direct PTY path.
283#[must_use]
284pub fn key_code_to_bytes(key: KeyCode) -> Option<Vec<u8>> {
285    let key = key_code_lookup_bits(key);
286    if key == KEYC_NONE || key == KEYC_UNKNOWN || KEYC_IS_MOUSE(key) {
287        return None;
288    }
289
290    let base = key & KEYC_MASK_KEY;
291    if key & KEYC_CTRL != 0 {
292        if base == b'?' as u64 {
293            return Some(vec![0x7f]);
294        }
295        if base == b' ' as u64 {
296            return Some(vec![0x00]);
297        }
298        if (b'a' as u64..=b'z' as u64).contains(&base) {
299            return Some(vec![((base as u8) - b'a') + 1]);
300        }
301        if (b'A' as u64..=b'Z' as u64).contains(&base) {
302            return Some(vec![((base as u8) - b'A') + 1]);
303        }
304    }
305
306    match base {
307        value if value == b'\r' as u64 || value == b'\t' as u64 || value == 0x1b => {
308            Some(vec![value as u8])
309        }
310        value if value == KEYC_BSPACE => Some(vec![0x7f]),
311        value if value <= 0x7f => Some(vec![value as u8]),
312        value if is_unicode_key(value) => char::from_u32(value as u32).map(|character| {
313            let mut buffer = [0_u8; 4];
314            character.encode_utf8(&mut buffer).as_bytes().to_vec()
315        }),
316        _ => None,
317    }
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321#[repr(u64)]
322enum KeyCodeType {
323    Unicode = 0,
324    User = 1,
325    Function = 2,
326    MouseMove = 3,
327    MouseDown = 4,
328    MouseUp = 5,
329    MouseDrag = 6,
330    MouseDragEnd = 7,
331    WheelDown = 8,
332    WheelUp = 9,
333    SecondClick = 10,
334    DoubleClick = 11,
335    TripleClick = 12,
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339#[repr(u64)]
340enum MouseLocation {
341    Pane = 0,
342    Status = 1,
343    StatusLeft = 2,
344    StatusRight = 3,
345    StatusDefault = 4,
346    Border = 5,
347    ScrollbarUp = 6,
348    ScrollbarSlider = 7,
349    ScrollbarDown = 8,
350    Control0 = 9,
351    Control1 = 10,
352    Control2 = 11,
353    Control3 = 12,
354    Control4 = 13,
355    Control5 = 14,
356    Control6 = 15,
357    Control7 = 16,
358    Control8 = 17,
359    Control9 = 18,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363enum MouseEventType {
364    MouseMove,
365    MouseDown,
366    MouseUp,
367    MouseDrag,
368    MouseDragEnd,
369    WheelDown,
370    WheelUp,
371    SecondClick,
372    DoubleClick,
373    TripleClick,
374}
375
376const fn shift_type(kind: KeyCodeType) -> KeyCode {
377    (kind as KeyCode) << 32
378}
379
380const fn make_mouse_key(kind: MouseEventType, button: u64, location: MouseLocation) -> KeyCode {
381    shift_type(match kind {
382        MouseEventType::MouseMove => KeyCodeType::MouseMove,
383        MouseEventType::MouseDown => KeyCodeType::MouseDown,
384        MouseEventType::MouseUp => KeyCodeType::MouseUp,
385        MouseEventType::MouseDrag => KeyCodeType::MouseDrag,
386        MouseEventType::MouseDragEnd => KeyCodeType::MouseDragEnd,
387        MouseEventType::WheelDown => KeyCodeType::WheelDown,
388        MouseEventType::WheelUp => KeyCodeType::WheelUp,
389        MouseEventType::SecondClick => KeyCodeType::SecondClick,
390        MouseEventType::DoubleClick => KeyCodeType::DoubleClick,
391        MouseEventType::TripleClick => KeyCodeType::TripleClick,
392    }) | (button << 8)
393        | location as u64
394}
395
396const fn strip_flags(key: KeyCode) -> KeyCode {
397    key & !KEYC_MASK_FLAGS
398}
399
400const fn is_unicode_key(key: KeyCode) -> bool {
401    (key & KEYC_MASK_TYPE) == shift_type(KeyCodeType::Unicode) && (key & KEYC_MASK_KEY) > 0x7f
402}
403
404const fn is_user_key(key: KeyCode) -> bool {
405    (key & KEYC_MASK_TYPE) == shift_type(KeyCodeType::User)
406}
407
408#[allow(non_snake_case)]
409const fn KEYC_IS_MOUSE(key: KeyCode) -> bool {
410    (key & KEYC_MASK_KEY) == KEYC_MOUSE
411        || ((key & KEYC_MASK_TYPE) >= shift_type(KeyCodeType::MouseMove)
412            && (key & KEYC_MASK_TYPE) <= shift_type(KeyCodeType::TripleClick))
413}
414
415fn parse_modifiers(rest: &mut &str) -> Option<KeyCode> {
416    let mut modifiers = 0;
417    loop {
418        let bytes = rest.as_bytes();
419        if bytes.len() < 2 || bytes[1] != b'-' {
420            break;
421        }
422        match bytes[0].to_ascii_lowercase() {
423            b'c' => modifiers |= KEYC_CTRL,
424            b'm' => modifiers |= KEYC_META,
425            b's' => modifiers |= KEYC_SHIFT,
426            _ => return None,
427        }
428        *rest = &rest[2..];
429    }
430    Some(modifiers)
431}
432
433fn maybe_append_flags(mut output: String, saved: KeyCode, with_flags: bool) -> String {
434    if with_flags && (saved & KEYC_MASK_FLAGS) != 0 {
435        output.push('[');
436        if saved & KEYC_LITERAL != 0 {
437            output.push('L');
438        }
439        if saved & KEYC_KEYPAD != 0 {
440            output.push('K');
441        }
442        if saved & KEYC_CURSOR != 0 {
443            output.push('C');
444        }
445        if saved & KEYC_IMPLIED_META != 0 {
446            output.push('I');
447        }
448        if saved & KEYC_BUILD_MODIFIERS != 0 {
449            output.push('B');
450        }
451        if saved & KEYC_SENT != 0 {
452            output.push('S');
453        }
454        output.push(']');
455    }
456    output
457}
458
459/// Parses a `bind-key` command payload from raw argv-style tokens.
460pub fn parse_binding_command_tokens(
461    tokens: &[String],
462) -> Result<ParsedCommands, CommandParseError> {
463    if tokens.len() == 1 {
464        parse_command_string(&tokens[0])
465    } else {
466        CommandParser::new().parse_arguments(tokens)
467    }
468}
469
470#[cfg(test)]
471#[path = "keys/tests.rs"]
472mod tests;