Skip to main content

rmux_core/keys/
store.rs

1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3
4use unicode_width::UnicodeWidthStr;
5
6use crate::command_parser::{
7    parse_command_string, CommandArgument, CommandParseError, ParsedCommand, ParsedCommands,
8};
9
10use super::{
11    defaults, key_string_lookup_key, key_string_lookup_string, strip_flags, KeyCode,
12    KEYC_MASK_MODIFIERS,
13};
14
15/// Sort orders accepted by `list-keys -O`.
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
17pub enum KeyBindingSortOrder {
18    /// Sort by key code.
19    #[default]
20    Key,
21    /// Sort by modifier bits.
22    Modifier,
23    /// Sort by table name.
24    Name,
25}
26
27impl KeyBindingSortOrder {
28    /// Parses a tmux sort-order token.
29    #[must_use]
30    pub fn parse(value: &str) -> Option<Self> {
31        if value.eq_ignore_ascii_case("key") || value.eq_ignore_ascii_case("index") {
32            Some(Self::Key)
33        } else if value.eq_ignore_ascii_case("modifier") {
34            Some(Self::Modifier)
35        } else if value.eq_ignore_ascii_case("name") || value.eq_ignore_ascii_case("title") {
36            Some(Self::Name)
37        } else {
38            None
39        }
40    }
41}
42
43/// One bound key in a table.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct KeyBinding {
46    key: KeyCode,
47    note: Option<String>,
48    repeat: bool,
49    commands: ParsedCommands,
50}
51
52impl KeyBinding {
53    /// Returns the bound key code.
54    #[must_use]
55    pub const fn key(&self) -> KeyCode {
56        self.key
57    }
58
59    /// Returns the optional binding note.
60    #[must_use]
61    pub fn note(&self) -> Option<&str> {
62        self.note.as_deref()
63    }
64
65    /// Returns whether the binding repeats.
66    #[must_use]
67    pub const fn repeat(&self) -> bool {
68        self.repeat
69    }
70
71    /// Returns the parsed tmux command list.
72    #[must_use]
73    pub const fn commands(&self) -> &ParsedCommands {
74        &self.commands
75    }
76}
77
78/// One key table.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct KeyBindingTable {
81    name: String,
82    references: usize,
83    active: BTreeMap<KeyCode, KeyBinding>,
84    defaults: BTreeMap<KeyCode, KeyBinding>,
85}
86
87impl KeyBindingTable {
88    /// Returns the table name.
89    #[must_use]
90    pub fn name(&self) -> &str {
91        &self.name
92    }
93
94    /// Returns the current reference count.
95    #[must_use]
96    pub const fn references(&self) -> usize {
97        self.references
98    }
99
100    /// Returns the active binding tree.
101    #[must_use]
102    pub const fn active(&self) -> &BTreeMap<KeyCode, KeyBinding> {
103        &self.active
104    }
105
106    /// Returns the default binding tree snapshot.
107    #[must_use]
108    pub const fn defaults(&self) -> &BTreeMap<KeyCode, KeyBinding> {
109        &self.defaults
110    }
111}
112
113/// A listed key binding paired with derived display fields.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct KeyBindingDisplay {
116    table_name: String,
117    binding: KeyBinding,
118    key_string: String,
119    command_string: String,
120    default_index: Option<usize>,
121}
122
123impl KeyBindingDisplay {
124    /// Returns the table name.
125    #[must_use]
126    pub fn table_name(&self) -> &str {
127        &self.table_name
128    }
129
130    /// Returns the binding.
131    #[must_use]
132    pub const fn binding(&self) -> &KeyBinding {
133        &self.binding
134    }
135
136    /// Returns the canonical key string.
137    #[must_use]
138    pub fn key_string(&self) -> &str {
139        &self.key_string
140    }
141
142    /// Returns the canonical command string.
143    #[must_use]
144    pub fn command_string(&self) -> &str {
145        &self.command_string
146    }
147}
148
149/// A mutable table reference operation result.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct KeyBindingTableRef {
152    name: String,
153    created: bool,
154}
155
156impl KeyBindingTableRef {
157    /// Returns the table name.
158    #[must_use]
159    pub fn name(&self) -> &str {
160        &self.name
161    }
162
163    /// Returns whether the table was created.
164    #[must_use]
165    pub const fn created(&self) -> bool {
166        self.created
167    }
168}
169
170/// Global key table store with tmux-style default snapshots.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct KeyBindingStore {
173    tables: BTreeMap<String, KeyBindingTable>,
174}
175
176impl Default for KeyBindingStore {
177    fn default() -> Self {
178        Self::with_defaults().expect("embedded default bindings must parse")
179    }
180}
181
182impl KeyBindingStore {
183    /// Creates an empty key store with no tables.
184    #[must_use]
185    pub fn new() -> Self {
186        Self {
187            tables: BTreeMap::new(),
188        }
189    }
190
191    /// Creates a store populated with the frozen tmux default bindings.
192    pub fn with_defaults() -> Result<Self, CommandParseError> {
193        let mut store = Self::new();
194        for default in defaults::DEFAULT_BINDING_STRINGS {
195            let parsed = parse_command_string(default)?;
196            for command in parsed.commands() {
197                store.apply_parsed_default(command)?;
198            }
199        }
200        store.snapshot_defaults();
201        Ok(store)
202    }
203
204    /// Returns the named table, when present.
205    #[must_use]
206    pub fn table(&self, name: &str) -> Option<&KeyBindingTable> {
207        self.tables.get(name)
208    }
209
210    /// Iterates every table in name order.
211    pub fn tables(&self) -> impl Iterator<Item = &KeyBindingTable> {
212        self.tables.values()
213    }
214
215    /// Finds or creates a table and increments its reference count.
216    pub fn get_table(&mut self, name: &str, create: bool) -> Option<KeyBindingTableRef> {
217        if let Some(table) = self.tables.get_mut(name) {
218            table.references = table.references.saturating_add(1);
219            return Some(KeyBindingTableRef {
220                name: name.to_owned(),
221                created: false,
222            });
223        }
224        if !create {
225            return None;
226        }
227
228        self.tables.insert(
229            name.to_owned(),
230            KeyBindingTable {
231                name: name.to_owned(),
232                references: 1,
233                active: BTreeMap::new(),
234                defaults: BTreeMap::new(),
235            },
236        );
237        Some(KeyBindingTableRef {
238            name: name.to_owned(),
239            created: true,
240        })
241    }
242
243    /// Drops one reference from a table and removes it if it is fully empty.
244    pub fn unref_table(&mut self, name: &str) {
245        let should_remove = if let Some(table) = self.tables.get_mut(name) {
246            table.references = table.references.saturating_sub(1);
247            table.references == 0 && table.active.is_empty() && table.defaults.is_empty()
248        } else {
249            false
250        };
251        if should_remove {
252            self.tables.remove(name);
253        }
254    }
255
256    /// Adds or updates a binding in a table.
257    pub fn add_binding(
258        &mut self,
259        table_name: &str,
260        key: KeyCode,
261        note: Option<String>,
262        repeat: bool,
263        commands: Option<ParsedCommands>,
264    ) -> bool {
265        let table = self.ensure_table_mut(table_name);
266        let key = strip_flags(key);
267        if commands.is_none() {
268            if let Some(binding) = table.active.get_mut(&key) {
269                if let Some(note) = note {
270                    binding.note = Some(note);
271                }
272                if repeat {
273                    binding.repeat = true;
274                }
275                return true;
276            }
277            return false;
278        }
279
280        table.active.insert(
281            key,
282            KeyBinding {
283                key,
284                note,
285                repeat,
286                commands: commands.expect("checked commands presence"),
287            },
288        );
289        true
290    }
291
292    /// Removes one active binding from a table.
293    pub fn remove_binding(&mut self, table_name: &str, key: KeyCode) -> bool {
294        let key = strip_flags(key);
295        let Some(table) = self.tables.get_mut(table_name) else {
296            return false;
297        };
298        let removed = table.active.remove(&key).is_some();
299        self.remove_table_if_empty(table_name);
300        removed
301    }
302
303    /// Restores one active binding from the default snapshot.
304    pub fn reset_binding(&mut self, table_name: &str, key: KeyCode) {
305        let key = strip_flags(key);
306        let Some(table) = self.tables.get_mut(table_name) else {
307            return;
308        };
309        if let Some(default) = table.defaults.get(&key).cloned() {
310            table.active.insert(key, default);
311        } else {
312            table.active.remove(&key);
313        }
314        self.remove_table_if_empty(table_name);
315    }
316
317    /// Removes every binding from a table.
318    pub fn remove_table(&mut self, table_name: &str) -> bool {
319        let Some(table) = self.tables.get_mut(table_name) else {
320            return false;
321        };
322        let removed = !table.active.is_empty();
323        table.active.clear();
324        self.remove_table_if_empty(table_name);
325        removed
326    }
327
328    /// Restores every binding in a table from the default snapshot.
329    pub fn reset_table(&mut self, table_name: &str) {
330        let Some(table) = self.tables.get_mut(table_name) else {
331            return;
332        };
333        if table.defaults.is_empty() {
334            self.remove_table_if_empty(table_name);
335            return;
336        }
337        table.active = table.defaults.clone();
338    }
339
340    /// Returns a binding from the active tree.
341    #[must_use]
342    pub fn get_binding(&self, table_name: &str, key: KeyCode) -> Option<&KeyBinding> {
343        self.tables
344            .get(table_name)
345            .and_then(|table| table.active.get(&strip_flags(key)))
346    }
347
348    /// Returns a binding from the default snapshot.
349    #[must_use]
350    pub fn get_default_binding(&self, table_name: &str, key: KeyCode) -> Option<&KeyBinding> {
351        self.tables
352            .get(table_name)
353            .and_then(|table| table.defaults.get(&strip_flags(key)))
354    }
355
356    /// Returns every binding as display rows sorted for `list-keys`.
357    #[must_use]
358    pub fn list_bindings(
359        &self,
360        table_name: Option<&str>,
361        sort_order: KeyBindingSortOrder,
362        reversed: bool,
363    ) -> Vec<KeyBindingDisplay> {
364        let mut bindings = if let Some(table_name) = table_name {
365            self.tables
366                .get(table_name)
367                .into_iter()
368                .flat_map(|table| {
369                    table
370                        .active
371                        .values()
372                        .cloned()
373                        .flat_map(|binding| display_bindings(table, binding))
374                        .collect::<Vec<_>>()
375                })
376                .collect::<Vec<_>>()
377        } else {
378            self.tables
379                .values()
380                .flat_map(|table| {
381                    table
382                        .active
383                        .values()
384                        .cloned()
385                        .flat_map(|binding| display_bindings(table, binding))
386                        .collect::<Vec<_>>()
387                })
388                .collect::<Vec<_>>()
389        };
390
391        bindings.sort_by(|left, right| {
392            let ordering = compare_binding_display(left, right, sort_order);
393            if reversed {
394                ordering.reverse()
395            } else {
396                ordering
397            }
398        });
399        bindings
400    }
401
402    /// Returns whether any listed binding repeats.
403    #[must_use]
404    pub fn has_repeat(bindings: &[KeyBindingDisplay]) -> bool {
405        bindings.iter().any(|binding| binding.binding.repeat)
406    }
407
408    /// Returns the maximum display width for listed key strings.
409    #[must_use]
410    pub fn key_string_width(bindings: &[KeyBindingDisplay]) -> usize {
411        bindings
412            .iter()
413            .map(|binding| UnicodeWidthStr::width(binding.key_string.as_str()))
414            .max()
415            .unwrap_or(0)
416    }
417
418    /// Returns the maximum display width for listed table names.
419    #[must_use]
420    pub fn key_table_width(bindings: &[KeyBindingDisplay]) -> usize {
421        bindings
422            .iter()
423            .map(|binding| UnicodeWidthStr::width(binding.table_name.as_str()))
424            .max()
425            .unwrap_or(0)
426    }
427
428    fn apply_parsed_default(&mut self, command: &ParsedCommand) -> Result<(), CommandParseError> {
429        let (table, key, note, repeat, commands) = parse_bind_command(command)?;
430        let _ = self.add_binding(&table, key, note, repeat, Some(commands));
431        Ok(())
432    }
433
434    fn snapshot_defaults(&mut self) {
435        for table in self.tables.values_mut() {
436            if table.defaults.is_empty() {
437                table.defaults = table.active.clone();
438            }
439        }
440    }
441
442    fn ensure_table_mut(&mut self, name: &str) -> &mut KeyBindingTable {
443        self.tables
444            .entry(name.to_owned())
445            .or_insert_with(|| KeyBindingTable {
446                name: name.to_owned(),
447                references: 0,
448                active: BTreeMap::new(),
449                defaults: BTreeMap::new(),
450            })
451    }
452
453    fn remove_table_if_empty(&mut self, table_name: &str) {
454        let should_remove = self.tables.get(table_name).is_some_and(|table| {
455            table.references == 0 && table.active.is_empty() && table.defaults.is_empty()
456        });
457        if should_remove {
458            self.tables.remove(table_name);
459        }
460    }
461}
462
463fn compare_binding_display(
464    left: &KeyBindingDisplay,
465    right: &KeyBindingDisplay,
466    sort_order: KeyBindingSortOrder,
467) -> Ordering {
468    let ordering = match sort_order {
469        KeyBindingSortOrder::Key => compare_key_sort(left, right),
470        KeyBindingSortOrder::Modifier => {
471            (left.binding.key & KEYC_MASK_MODIFIERS).cmp(&(right.binding.key & KEYC_MASK_MODIFIERS))
472        }
473        KeyBindingSortOrder::Name => left
474            .table_name
475            .to_ascii_lowercase()
476            .cmp(&right.table_name.to_ascii_lowercase()),
477    };
478    ordering
479        .then_with(|| {
480            left.table_name
481                .to_ascii_lowercase()
482                .cmp(&right.table_name.to_ascii_lowercase())
483        })
484        .then_with(|| left.binding.key.cmp(&right.binding.key))
485        .then_with(|| left.key_string.cmp(&right.key_string))
486        .then_with(|| left.command_string.cmp(&right.command_string))
487}
488
489fn compare_key_sort(left: &KeyBindingDisplay, right: &KeyBindingDisplay) -> Ordering {
490    match (left.default_index, right.default_index) {
491        (Some(left), Some(right)) => left.cmp(&right),
492        (Some(_), None) => Ordering::Less,
493        (None, Some(_)) => Ordering::Greater,
494        (None, None) => left.binding.key.cmp(&right.binding.key),
495    }
496}
497
498fn display_bindings(table: &KeyBindingTable, binding: KeyBinding) -> Vec<KeyBindingDisplay> {
499    let default_displays = table
500        .defaults
501        .get(&binding.key)
502        .filter(|default| *default == &binding)
503        .map(|_| defaults::list_keys_displays(&table.name, binding.key))
504        .unwrap_or_default();
505
506    if default_displays.is_empty() {
507        return vec![KeyBindingDisplay {
508            table_name: table.name.clone(),
509            key_string: key_string_lookup_key(binding.key, false),
510            command_string: binding.commands.to_tmux_binding_string(),
511            default_index: None,
512            binding,
513        }];
514    }
515
516    default_displays
517        .into_iter()
518        .map(|display| KeyBindingDisplay {
519            table_name: table.name.clone(),
520            binding: binding.clone(),
521            key_string: display.key_string,
522            command_string: display.command_string.to_owned(),
523            default_index: Some(display.index),
524        })
525        .collect()
526}
527
528fn parse_bind_command(
529    command: &ParsedCommand,
530) -> Result<(String, KeyCode, Option<String>, bool, ParsedCommands), CommandParseError> {
531    let mut index = 0;
532    let mut table_name: Option<String> = None;
533    let mut note = None;
534    let mut repeat = false;
535    let arguments = command.arguments();
536
537    while let Some(argument) = arguments.get(index) {
538        let Some(value) = argument.as_string() else {
539            break;
540        };
541        match value {
542            "-T" => {
543                index += 1;
544                table_name = Some(
545                    arguments
546                        .get(index)
547                        .and_then(|argument| argument.as_string())
548                        .ok_or_else(|| CommandParseError::new(1, "bind-key missing -T key-table"))?
549                        .to_owned(),
550                );
551            }
552            _ if value.starts_with("-T") && value.len() > 2 => {
553                table_name = Some(value[2..].to_owned());
554            }
555            "-n" => table_name = Some("root".to_owned()),
556            "-N" => {
557                index += 1;
558                note = Some(
559                    arguments
560                        .get(index)
561                        .and_then(|argument| argument.as_string())
562                        .ok_or_else(|| CommandParseError::new(1, "bind-key missing -N note"))?
563                        .to_owned(),
564                );
565            }
566            _ if value.starts_with("-N") && value.len() > 2 => {
567                note = Some(value[2..].to_owned());
568            }
569            "-r" => repeat = true,
570            _ if value.starts_with('-') && value.len() > 1 => {
571                return Err(CommandParseError::new(
572                    1,
573                    format!("unsupported default bind-key flag: {value}"),
574                ));
575            }
576            _ => break,
577        }
578        index += 1;
579    }
580
581    let key_string = arguments
582        .get(index)
583        .and_then(|argument| argument.as_string())
584        .ok_or_else(|| CommandParseError::new(1, "bind-key missing key"))?;
585    index += 1;
586    let key = key_string_lookup_string(key_string)
587        .ok_or_else(|| CommandParseError::new(1, format!("unknown key: {key_string}")))?;
588
589    let commands = match arguments.get(index) {
590        Some(CommandArgument::Commands(commands)) => commands.clone(),
591        Some(CommandArgument::String(command)) => parse_command_string(command)?,
592        None => {
593            return Err(CommandParseError::new(
594                1,
595                "default bind-key must include a command list",
596            ))
597        }
598    };
599
600    Ok((
601        table_name.unwrap_or_else(|| "prefix".to_owned()),
602        key,
603        note,
604        repeat,
605        commands,
606    ))
607}