clone
ssh://git@git.notashelf.dev:33/notashelf/beer.gitCommit 3e49e94f5682
tarballunverified5 files changed+177-15
@@ -123,6 +123,22 @@ _unicode-input_. "Ctrl+`" = "none" ``` +# [mouse-bindings]++A table of _button-chord_ = _action_ entries, merged over the built-in+defaults. A chord is optional modifiers and a button joined by _+_, e.g.+_"Shift+Middle"_. Buttons are _Left_, _Middle_, and _Right_. The actions are+the same set as *[key-bindings]*. An action of _"none"_ unbinds the chord. The+left button's select/drag is a built-in gesture and is not rebindable.++The only default is _"Middle" = "paste-primary"_.++```+[mouse-bindings]+"Right" = "paste"+"Middle" = "none"+```+ # [shell-integration] Behaviour driven by *OSC 7* (cwd) and *OSC 133* (prompt) marks.@@ -106,11 +106,69 @@ impl Chord { } } +/// A mouse button that a `[mouse-bindings]` chord can name.+#[derive(Clone, Copy, PartialEq, Eq, Debug)]+pub enum MouseButton {+ Left,+ Middle,+ Right,+}++/// A parsed mouse chord: a button plus the modifiers that must be held exactly.+#[derive(Clone, Copy, PartialEq, Eq, Debug)]+struct MouseChord {+ button: MouseButton,+ ctrl: bool,+ shift: bool,+ alt: bool,+ logo: bool,+}++impl MouseChord {+ /// Parse a chord like `Shift+Middle`; the final non-modifier token is the+ /// button. Returns `None` if no button is recognized.+ fn parse(spec: &str) -> Option<Self> {+ let (mut ctrl, mut shift, mut alt, mut logo) = (false, false, false, false);+ let mut button = None;+ for token in spec.split('+').map(str::trim).filter(|t| !t.is_empty()) {+ match token.to_ascii_lowercase().as_str() {+ "ctrl" | "control" => ctrl = true,+ "shift" => shift = true,+ "alt" | "mod1" | "meta" => alt = true,+ "super" | "logo" | "cmd" | "mod4" => logo = true,+ "left" | "button1" => button = Some(MouseButton::Left),+ "middle" | "button2" => button = Some(MouseButton::Middle),+ "right" | "button3" => button = Some(MouseButton::Right),+ other => {+ tracing::warn!("unknown mouse button {other:?} in binding");+ return None;+ }+ }+ }+ Some(Self {+ button: button?,+ ctrl,+ shift,+ alt,+ logo,+ })+ }++ fn matches(&self, button: MouseButton, mods: Modifiers) -> bool {+ self.button == button+ && mods.ctrl == self.ctrl+ && mods.shift == self.shift+ && mods.alt == self.alt+ && mods.logo == self.logo+ }+}+ /// The resolved binding tables for a session. #[derive(Clone, Debug, Default)] pub struct Bindings { keys: Vec<(Chord, Action)>, text: Vec<(Chord, Vec<u8>)>,+ mouse: Vec<(MouseChord, Action)>, } impl Bindings {@@ -119,6 +177,7 @@ impl Bindings { pub fn from_config( key_bindings: &HashMap<String, String>, text_bindings: &HashMap<String, String>,+ mouse_bindings: &HashMap<String, String>, ) -> Self { let mut keys: Vec<(Chord, Action)> = Vec::new(); let mut add = |chord: &str, action: Action| {@@ -149,7 +208,30 @@ impl Bindings { text.push((c, unescape(value))); } }- Self { keys, text }++ let mut mouse: Vec<(MouseChord, Action)> = Vec::new();+ let mut add_mouse = |chord: &str, action: Option<Action>| {+ if let Some(c) = MouseChord::parse(chord) {+ mouse.retain(|(existing, _)| *existing != c);+ if let Some(a) = action {+ mouse.push((c, a));+ }+ }+ };+ for (chord, action) in DEFAULT_MOUSE_BINDINGS {+ add_mouse(chord, Action::parse(action));+ }+ for (chord, action) in mouse_bindings {+ if action == "none" {+ add_mouse(chord, None);+ } else if let Some(a) = Action::parse(action) {+ add_mouse(chord, Some(a));+ } else {+ tracing::warn!("unknown mouse-binding action {action:?}");+ }+ }++ Self { keys, text, mouse } } /// The action bound to this key event, if any.@@ -167,6 +249,14 @@ impl Bindings { .find(|(c, _)| c.matches(event, mods)) .map(|(_, t)| t.as_slice()) }++ /// The action bound to this mouse button + modifiers, if any.+ pub fn mouse_action(&self, button: MouseButton, mods: Modifiers) -> Option<Action> {+ self.mouse+ .iter()+ .find(|(c, _)| c.matches(button, mods))+ .map(|(_, a)| *a)+ } } /// Built-in default key bindings (chord, action name).@@ -190,6 +280,10 @@ const DEFAULT_BINDINGS: &[(&str, &str)] = &[ ("Ctrl+Shift+U", "unicode-input"), ]; +/// Built-in default mouse bindings (chord, action name). Left-button select and+/// drag are built-in gestures, not bindings, so only the other buttons appear.+const DEFAULT_MOUSE_BINDINGS: &[(&str, &str)] = &[("Middle", "paste-primary")];+ /// Map a key token to a keysym: a single character, or a named special key. fn keysym_from_token(token: &str) -> Option<Keysym> { let mut chars = token.chars();@@ -292,7 +386,7 @@ mod tests { #[test] fn default_copy_binding_matches_case_insensitively() {- let b = Bindings::from_config(&HashMap::new(), &HashMap::new());+ let b = Bindings::from_config(&HashMap::new(), &HashMap::new(), &HashMap::new()); let mods = Modifiers { ctrl: true, shift: true,@@ -309,7 +403,7 @@ mod tests { let mut kb = HashMap::new(); kb.insert("Ctrl+Shift+C".to_string(), "none".to_string()); kb.insert("Ctrl+y".to_string(), "copy".to_string());- let b = Bindings::from_config(&kb, &HashMap::new());+ let b = Bindings::from_config(&kb, &HashMap::new(), &HashMap::new()); let cs = Modifiers { ctrl: true, shift: true,@@ -324,7 +418,7 @@ mod tests { fn text_binding_unescapes() { let mut tb = HashMap::new(); tb.insert("Ctrl+Shift+Return".to_string(), "\\x1b\\r".to_string());- let b = Bindings::from_config(&HashMap::new(), &tb);+ let b = Bindings::from_config(&HashMap::new(), &tb, &HashMap::new()); let mods = Modifiers { ctrl: true, shift: true,@@ -332,4 +426,32 @@ mod tests { }; assert_eq!(b.text(&key(Keysym::Return), mods), Some(&[0x1b, b'\r'][..])); }++ #[test]+ fn mouse_bindings_default_override_and_unbind() {+ // Default: middle button pastes the primary selection.+ let b = Bindings::from_config(&HashMap::new(), &HashMap::new(), &HashMap::new());+ assert_eq!(+ b.mouse_action(MouseButton::Middle, NONE),+ Some(Action::PastePrimary)+ );+ assert_eq!(b.mouse_action(MouseButton::Right, NONE), None);++ // Config rebinds right to paste and unbinds the middle default.+ let mut mb = HashMap::new();+ mb.insert("Right".to_string(), "paste".to_string());+ mb.insert("Middle".to_string(), "none".to_string());+ let b = Bindings::from_config(&HashMap::new(), &HashMap::new(), &mb);+ assert_eq!(+ b.mouse_action(MouseButton::Right, NONE),+ Some(Action::Paste)+ );+ assert_eq!(b.mouse_action(MouseButton::Middle, NONE), None);+ // Modifiers must match exactly.+ let shift = Modifiers {+ shift: true,+ ..NONE+ };+ assert_eq!(b.mouse_action(MouseButton::Right, shift), None);+ } }@@ -25,6 +25,9 @@ pub struct Config { pub key_bindings: std::collections::HashMap<String, String>, /// Chord → literal text to send (supports `\e \n \r \t \\ \xNN`). pub text_bindings: std::collections::HashMap<String, String>,+ /// Mouse chord (e.g. `"Middle"`, `"Shift+Right"`) → action. Merged over the+ /// defaults; `"none"` unbinds. Left-button select/drag stays built in.+ pub mouse_bindings: std::collections::HashMap<String, String>, } /// `[cursor]`: the default cursor presentation (DECSCUSR may override at runtime).@@ -1,4 +1,5 @@ use super::*;+use crate::bindings::MouseButton; impl CompositorHandler for App { fn scale_factor_changed(@@ -330,6 +331,16 @@ fn button_code(button: u32) -> Option<u8> { } } +/// Map a Wayland button code to a bindable [`MouseButton`].+fn mouse_button(button: u32) -> Option<MouseButton> {+ match button {+ BTN_LEFT => Some(MouseButton::Left),+ BTN_MIDDLE => Some(MouseButton::Middle),+ BTN_RIGHT => Some(MouseButton::Right),+ _ => None,+ }+}+ impl PointerHandler for App { fn pointer_frame( &mut self,@@ -372,13 +383,17 @@ impl PointerHandler for App { self.pressed_button = Some(code); continue; }- match *button {- BTN_LEFT => {- self.press_cell = self.cell_at(self.pointer_pos.0, self.pointer_pos.1);- self.pointer_press(*time);- }- BTN_MIDDLE => self.paste_primary(),- _ => {}+ // A configured `[mouse-bindings]` action (Middle defaults to+ // primary paste) fires before the built-in left-drag select.+ if let Some(mb) = mouse_button(*button)+ && let Some(action) = self.bindings.mouse_action(mb, self.modifiers)+ {+ self.dispatch_action(action);+ continue;+ }+ if *button == BTN_LEFT {+ self.press_cell = self.cell_at(self.pointer_pos.0, self.pointer_pos.1);+ self.pointer_press(*time); } } PointerEventKind::Release { button, .. } => {@@ -217,8 +217,11 @@ pub fn run(config: Config, config_path: Option<std::path::PathBuf>) -> anyhow::R ) .context("create shm slot pool")?; - let bindings =- crate::bindings::Bindings::from_config(&config.key_bindings, &config.text_bindings);+ let bindings = crate::bindings::Bindings::from_config(+ &config.key_bindings,+ &config.text_bindings,+ &config.mouse_bindings,+ ); let font_size = config.main.font_size; let mut app = App {@@ -934,8 +937,11 @@ impl App { /// Re-read the config file and apply it in place (SIGUSR1). fn reload_config(&mut self) { let new = Config::load(self.config_path.as_deref());- self.bindings =- crate::bindings::Bindings::from_config(&new.key_bindings, &new.text_bindings);+ self.bindings = crate::bindings::Bindings::from_config(+ &new.key_bindings,+ &new.text_bindings,+ &new.mouse_bindings,+ ); let font_changed = new.main.font != self.config.main.font || new.main.font_size != self.config.main.font_size; if font_changed {