Skip to main content

tear_types/
keybind.rs

1//! Keybinding model — typed key chords + actions.
2//!
3//! Operators author bindings declaratively in the shikumi config
4//! (`~/.config/tear/tear.yaml`). The same vocabulary applies to mado:
5//! when mado embeds tear-core at tier 3, the multiplexer's bindings
6//! and mado's bindings come from a unified table so muscle memory
7//! transfers between the two apps.
8
9use std::collections::BTreeMap;
10
11use serde::{Deserialize, Serialize};
12
13/// A keybinding belongs to a named table — most live in the default
14/// `"root"` table but tmux operators recognise `"prefix"` (active
15/// after `C-b`) and `"copy"` (modal copy mode). Tear honours the
16/// same names so dropped-in tmux configs feel native.
17#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct KeyTableName(pub String);
20
21impl Default for KeyTableName {
22    fn default() -> Self {
23        Self::root()
24    }
25}
26
27impl KeyTableName {
28    pub fn root() -> Self {
29        Self("root".into())
30    }
31    pub fn prefix() -> Self {
32        Self("prefix".into())
33    }
34    pub fn copy() -> Self {
35        Self("copy".into())
36    }
37}
38
39/// A single key chord — modifiers + key. The string is the
40/// canonical lowercase form: `"ctrl+a"`, `"alt+left"`, `"super+l"`,
41/// `"f10"`. tmux's `C-a`, `M-Left` shorthand normalises through here.
42#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct KeyChord(pub String);
45
46impl KeyChord {
47    /// Build a KeyChord from a tmux-style shorthand like `"C-a"`.
48    /// Returns the canonical normalised form.
49    pub fn from_tmux(s: &str) -> Self {
50        let mut parts: Vec<String> = Vec::new();
51        let mut key = String::new();
52        for seg in s.split('-') {
53            match seg {
54                "C" | "c" => parts.push("ctrl".into()),
55                "M" | "m" => parts.push("alt".into()),
56                "S" | "s" => parts.push("shift".into()),
57                "D" | "d" => parts.push("super".into()),
58                other => key = other.to_ascii_lowercase(),
59            }
60        }
61        parts.push(key);
62        Self(parts.join("+"))
63    }
64}
65
66/// Action a keybinding fires. Variants intentionally mirror tmux
67/// command names so a tmux user's mental model carries over.
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
69#[serde(tag = "kind", rename_all = "kebab-case")]
70pub enum Action {
71    /// Create a new pane by splitting the active pane.
72    SplitPane { direction: crate::Direction },
73    /// Send keys to the active pane as if typed.
74    SendKeys { keys: String },
75    /// Run a tear/tmux command, e.g. `"new-window -n logs"`.
76    Command { cmd: String },
77    /// Switch focus to a pane by relative direction.
78    SelectPane { direction: crate::Direction },
79    /// Move to the next/previous window.
80    NextWindow,
81    PreviousWindow,
82    /// Create a new window in the active session.
83    NewWindow,
84    /// Kill the active pane / window / session.
85    KillPane,
86    KillWindow,
87    KillSession,
88    /// Detach the current client.
89    Detach,
90    /// Reload the shikumi config — operator-driven hot-reload trigger
91    /// in addition to the file-watcher path.
92    ReloadConfig,
93    /// Enter named key table — tmux's modal copy-mode etc.
94    EnterTable { table: KeyTableName },
95}
96
97/// Single binding row in a [`KeyTable`].
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99pub struct KeyBind {
100    pub chord: KeyChord,
101    pub action: Action,
102    /// Free-form note rendered by `tear keybinds list` — operators
103    /// often forget what they bound; this is the affordance.
104    #[serde(default)]
105    pub note: String,
106}
107
108/// A named set of bindings. Tables are looked up by name at chord-
109/// dispatch time; the "root" table is the implicit default.
110#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
111pub struct KeyTable {
112    pub name: KeyTableName,
113    pub bindings: Vec<KeyBind>,
114}
115
116/// The full keybinding store: every table by name. Lives on
117/// [`crate::TearTheme`] / the shikumi config so a reload swaps the
118/// whole map atomically.
119pub type KeyTableMap = BTreeMap<KeyTableName, KeyTable>;
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn from_tmux_normalises_chord_shorthand() {
127        assert_eq!(KeyChord::from_tmux("C-a").0, "ctrl+a");
128        assert_eq!(KeyChord::from_tmux("M-Left").0, "alt+left");
129        assert_eq!(KeyChord::from_tmux("C-M-x").0, "ctrl+alt+x");
130    }
131}