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