forked from NotAShelf/beer
config: add a [mouse-bindings] table for rebindable buttons
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I8b4bfdd5c594a29f06c73aed0f7b07a46a6a6964
This commit is contained in:
parent
fb590c1645
commit
3e49e94f56
5 changed files with 177 additions and 15 deletions
|
|
@ -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.
|
||||
|
|
|
|||
130
src/bindings.rs
130
src/bindings.rs
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue