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}