1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
use crate::monitor::{Monitor, MonitorId};
use crate::window::WindowId;
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Modifiers: u8 {
const SHIFT = 0b0000_0001;
const CTRL = 0b0000_0010;
const ALT = 0b0000_0100;
const SUPER = 0b0000_1000;
}
}
impl std::fmt::Display for Modifiers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.contains(Modifiers::CTRL) {
write!(f, "Ctrl+")?;
}
if self.contains(Modifiers::SHIFT) {
write!(f, "Shift+")?;
}
if self.contains(Modifiers::ALT) {
write!(f, "Alt+")?;
}
if self.contains(Modifiers::SUPER) {
write!(f, "Mod4+")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseButton {
Left,
Right,
Middle,
Other(u8),
}
/// A key combination, e.g. "Mod4+Shift+Return", used both as the canonical
/// string form for Lua keybindings and as the lookup key at dispatch time.
pub fn key_combo_string(modifiers: Modifiers, key_name: &str) -> String {
format!("{modifiers}{key_name}")
}
/// Parses a `"Mod4+Shift+Return"`-style combo string into modifiers plus the
/// bare key name, accepting the modifier tokens in *any* order.
///
/// This matters because [`key_combo_string`]/[`Modifiers`]'s `Display` only
/// ever produce one fixed order (Ctrl, Shift, Alt, Mod4) - but every
/// shipped keybinding is written the conventional "Mod4+Shift+x" way
/// (Super first, matching Hyprland's own `SUPER, SHIFT, x` convention).
/// `srd.bind` used to store the combo string exactly as the Lua config
/// wrote it, and dispatch always looked it up by the canonical
/// Ctrl/Shift/Alt/Mod4 order built from the real keypress - so any
/// binding combining more than one modifier in a different order than that
/// fixed one could never fire: X11 grabbed the physical key correctly
/// (`grab_keybindings` already parsed order-independently, duplicating
/// this logic) but dispatch found nothing to run, and on Wayland the combo
/// was not even recognized as bound at all, so the keypress was forwarded
/// straight to the focused client instead of reaching srdwm. Confirmed
/// against the shipped `keybindings.lua`: every multi-modifier binding
/// there (`Mod4+Shift+*`, `Mod4+Ctrl+k`, `Alt+Shift+Tab`, ...) is written
/// Super/Alt-first, which never matched the canonical order. Returns
/// `None` for an empty combo (no key name at all).
pub fn parse_key_combo(combo: &str) -> Option<(Modifiers, &str)> {
let parts: Vec<&str> = combo.split('+').collect();
let (key_name, mod_parts) = parts.split_last()?;
let mut modifiers = Modifiers::empty();
for m in mod_parts {
modifiers |= match *m {
"Ctrl" => Modifiers::CTRL,
"Shift" => Modifiers::SHIFT,
"Alt" => Modifiers::ALT,
"Mod4" | "Super" => Modifiers::SUPER,
_ => Modifiers::empty(),
};
}
Some((modifiers, key_name))
}
/// Re-orders a combo string into the canonical form [`key_combo_string`]
/// produces, regardless of what order its modifiers were written in.
/// Unparseable input (empty string) is returned unchanged, so a caller that
/// can't do anything better with it still has *something* to store/log.
pub fn canonicalize_key_combo(combo: &str) -> String {
match parse_key_combo(combo) {
Some((modifiers, key_name)) => key_combo_string(modifiers, key_name),
None => combo.to_string(),
}
}
#[derive(Debug, Clone)]
pub enum Event {
WindowCreated(WindowId),
WindowDestroyed(WindowId),
WindowTitleChanged(WindowId, String),
WindowMoved { id: WindowId, x: i32, y: i32 },
WindowResized { id: WindowId, width: u32, height: u32 },
WindowFocused(WindowId),
WindowUnfocused(WindowId),
KeyPress { key_name: String, modifiers: Modifiers },
KeyRelease { key_name: String, modifiers: Modifiers },
MouseButtonPress { button: MouseButton, x: i32, y: i32 },
MouseButtonRelease { button: MouseButton, x: i32, y: i32 },
MouseMotion { x: i32, y: i32 },
MonitorAdded(Monitor),
MonitorRemoved(MonitorId),
/// The laptop lid was closed or opened. Emitted by the udev backend from
/// libinput switch events; `closed` is true when the lid is shut.
///
/// Exposed to config as `srd.on_lid("closed"/"open", fn)` so a session
/// can lock and suspend, which is otherwise impossible: a laptop that
/// does nothing on lid-close is a real problem, not a nicety.
LidSwitch { closed: bool },
/// `WindowManager::current_workspace` changed. Carries no id: every
/// consumer that cares (`main.rs`'s `sync()`) re-reads whichever
/// workspace is current now rather than trusting a stale snapshot from
/// whenever this event was queued.
///
/// Exists purely so `sync()` actually runs after a switch - without a
/// `dirty`-setting event, `WindowManager::switch_workspace` alone only
/// changes core's own bookkeeping; nothing shows or hides a single
/// window for the new workspace until `sync()` runs, which only
/// happens when a polled event sets `dirty`. Every switch path (a
/// keybinding, `SUPER`+scroll, the `ext_workspace_v1` protocol's
/// `activate` request) needs this pushed after it changes
/// `current_workspace`, or the switch is invisible.
WorkspaceChanged,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn super_first_combo_canonicalizes_to_dispatch_order() {
// The shipped keybindings.lua writes every combo Super-first
// ("Mod4+Shift+m"), matching Hyprland's own convention - but
// `key_combo_string`'s Display order is fixed Ctrl/Shift/Alt/Mod4.
// A binding registered under its literal Lua string could never be
// found by a real keypress, which always dispatches through the
// canonical order. This is exactly the bug `canonicalize_key_combo`
// exists to close.
assert_eq!(canonicalize_key_combo("Mod4+Shift+m"), "Shift+Mod4+m");
assert_eq!(canonicalize_key_combo("Mod4+Ctrl+k"), "Ctrl+Mod4+k");
assert_eq!(canonicalize_key_combo("Alt+Shift+Tab"), "Shift+Alt+Tab");
}
#[test]
fn already_canonical_combo_is_unchanged() {
assert_eq!(canonicalize_key_combo("Ctrl+Mod4+k"), "Ctrl+Mod4+k");
}
#[test]
fn single_modifier_combo_is_unaffected() {
// No ordering ambiguity with one modifier - this case always
// worked, before and after the fix.
assert_eq!(canonicalize_key_combo("Mod4+Return"), "Mod4+Return");
}
#[test]
fn parse_key_combo_accepts_modifiers_in_any_order() {
let (mods, key) = parse_key_combo("Mod4+Shift+m").unwrap();
assert_eq!(key, "m");
assert!(mods.contains(Modifiers::SUPER) && mods.contains(Modifiers::SHIFT));
let (mods2, key2) = parse_key_combo("Shift+Mod4+m").unwrap();
assert_eq!(key2, "m");
assert_eq!(mods, mods2);
}
#[test]
fn parse_key_combo_with_no_modifiers_is_bare_key() {
let (mods, key) = parse_key_combo("Return").unwrap();
assert_eq!(key, "Return");
assert_eq!(mods, Modifiers::empty());
}
}
|