Skip to main content

rmux_core/keys/
store.rs

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