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