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"
|
"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]
|
# [shell-integration]
|
||||||
|
|
||||||
Behaviour driven by *OSC 7* (cwd) and *OSC 133* (prompt) marks.
|
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.
|
/// The resolved binding tables for a session.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Bindings {
|
pub struct Bindings {
|
||||||
keys: Vec<(Chord, Action)>,
|
keys: Vec<(Chord, Action)>,
|
||||||
text: Vec<(Chord, Vec<u8>)>,
|
text: Vec<(Chord, Vec<u8>)>,
|
||||||
|
mouse: Vec<(MouseChord, Action)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bindings {
|
impl Bindings {
|
||||||
|
|
@ -119,6 +177,7 @@ impl Bindings {
|
||||||
pub fn from_config(
|
pub fn from_config(
|
||||||
key_bindings: &HashMap<String, String>,
|
key_bindings: &HashMap<String, String>,
|
||||||
text_bindings: &HashMap<String, String>,
|
text_bindings: &HashMap<String, String>,
|
||||||
|
mouse_bindings: &HashMap<String, String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut keys: Vec<(Chord, Action)> = Vec::new();
|
let mut keys: Vec<(Chord, Action)> = Vec::new();
|
||||||
let mut add = |chord: &str, action: Action| {
|
let mut add = |chord: &str, action: Action| {
|
||||||
|
|
@ -149,7 +208,30 @@ impl Bindings {
|
||||||
text.push((c, unescape(value)));
|
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.
|
/// The action bound to this key event, if any.
|
||||||
|
|
@ -167,6 +249,14 @@ impl Bindings {
|
||||||
.find(|(c, _)| c.matches(event, mods))
|
.find(|(c, _)| c.matches(event, mods))
|
||||||
.map(|(_, t)| t.as_slice())
|
.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).
|
/// Built-in default key bindings (chord, action name).
|
||||||
|
|
@ -190,6 +280,10 @@ const DEFAULT_BINDINGS: &[(&str, &str)] = &[
|
||||||
("Ctrl+Shift+U", "unicode-input"),
|
("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.
|
/// Map a key token to a keysym: a single character, or a named special key.
|
||||||
fn keysym_from_token(token: &str) -> Option<Keysym> {
|
fn keysym_from_token(token: &str) -> Option<Keysym> {
|
||||||
let mut chars = token.chars();
|
let mut chars = token.chars();
|
||||||
|
|
@ -292,7 +386,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_copy_binding_matches_case_insensitively() {
|
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 {
|
let mods = Modifiers {
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
shift: true,
|
shift: true,
|
||||||
|
|
@ -309,7 +403,7 @@ mod tests {
|
||||||
let mut kb = HashMap::new();
|
let mut kb = HashMap::new();
|
||||||
kb.insert("Ctrl+Shift+C".to_string(), "none".to_string());
|
kb.insert("Ctrl+Shift+C".to_string(), "none".to_string());
|
||||||
kb.insert("Ctrl+y".to_string(), "copy".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 {
|
let cs = Modifiers {
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
shift: true,
|
shift: true,
|
||||||
|
|
@ -324,7 +418,7 @@ mod tests {
|
||||||
fn text_binding_unescapes() {
|
fn text_binding_unescapes() {
|
||||||
let mut tb = HashMap::new();
|
let mut tb = HashMap::new();
|
||||||
tb.insert("Ctrl+Shift+Return".to_string(), "\\x1b\\r".to_string());
|
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 {
|
let mods = Modifiers {
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
shift: true,
|
shift: true,
|
||||||
|
|
@ -332,4 +426,32 @@ mod tests {
|
||||||
};
|
};
|
||||||
assert_eq!(b.text(&key(Keysym::Return), mods), Some(&[0x1b, b'\r'][..]));
|
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>,
|
pub key_bindings: std::collections::HashMap<String, String>,
|
||||||
/// Chord → literal text to send (supports `\e \n \r \t \\ \xNN`).
|
/// Chord → literal text to send (supports `\e \n \r \t \\ \xNN`).
|
||||||
pub text_bindings: std::collections::HashMap<String, String>,
|
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).
|
/// `[cursor]`: the default cursor presentation (DECSCUSR may override at runtime).
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::bindings::MouseButton;
|
||||||
|
|
||||||
impl CompositorHandler for App {
|
impl CompositorHandler for App {
|
||||||
fn scale_factor_changed(
|
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 {
|
impl PointerHandler for App {
|
||||||
fn pointer_frame(
|
fn pointer_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -372,13 +383,17 @@ impl PointerHandler for App {
|
||||||
self.pressed_button = Some(code);
|
self.pressed_button = Some(code);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match *button {
|
// A configured `[mouse-bindings]` action (Middle defaults to
|
||||||
BTN_LEFT => {
|
// primary paste) fires before the built-in left-drag select.
|
||||||
self.press_cell = self.cell_at(self.pointer_pos.0, self.pointer_pos.1);
|
if let Some(mb) = mouse_button(*button)
|
||||||
self.pointer_press(*time);
|
&& let Some(action) = self.bindings.mouse_action(mb, self.modifiers)
|
||||||
}
|
{
|
||||||
BTN_MIDDLE => self.paste_primary(),
|
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, .. } => {
|
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")?;
|
.context("create shm slot pool")?;
|
||||||
|
|
||||||
let bindings =
|
let bindings = crate::bindings::Bindings::from_config(
|
||||||
crate::bindings::Bindings::from_config(&config.key_bindings, &config.text_bindings);
|
&config.key_bindings,
|
||||||
|
&config.text_bindings,
|
||||||
|
&config.mouse_bindings,
|
||||||
|
);
|
||||||
let font_size = config.main.font_size;
|
let font_size = config.main.font_size;
|
||||||
|
|
||||||
let mut app = App {
|
let mut app = App {
|
||||||
|
|
@ -934,8 +937,11 @@ impl App {
|
||||||
/// Re-read the config file and apply it in place (SIGUSR1).
|
/// Re-read the config file and apply it in place (SIGUSR1).
|
||||||
fn reload_config(&mut self) {
|
fn reload_config(&mut self) {
|
||||||
let new = Config::load(self.config_path.as_deref());
|
let new = Config::load(self.config_path.as_deref());
|
||||||
self.bindings =
|
self.bindings = crate::bindings::Bindings::from_config(
|
||||||
crate::bindings::Bindings::from_config(&new.key_bindings, &new.text_bindings);
|
&new.key_bindings,
|
||||||
|
&new.text_bindings,
|
||||||
|
&new.mouse_bindings,
|
||||||
|
);
|
||||||
let font_changed = new.main.font != self.config.main.font
|
let font_changed = new.main.font != self.config.main.font
|
||||||
|| new.main.font_size != self.config.main.font_size;
|
|| new.main.font_size != self.config.main.font_size;
|
||||||
if font_changed {
|
if font_changed {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue