Add named keyboard binding modes (#32)

Add named keyboard shortcut maps with an Escape exit and a panel indicator. Keep press/release ownership across transitions, validate remapped Escape keys, and cancel stale repeats and focus previews.

Give the indicator ownership of the panel while a mode is active. Close popovers, collapse Morph, and keep Live Activities data current without resizing the hidden panel. Document configuration and transition behavior.

Validation: 1,519 workspace Rust tests passed; 70 Python tests passed. Workspace build, formatting, Clippy, and Settings configuration validation passed. Opt-in native desktop checks were not rerun because Weston is absent.

Related: #5.
This commit is contained in:
Samuel Onoja authored and GitHub committed 2026-10-12 23:22:02 +01:00
1 parent 6b0d903ad0
commit 7d58eeb65d
28 files changed
+1258 -35

No files matched your search

+73 -9
View File
@@ -25,6 +25,9 @@ use crate::process::spawn_client;
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
#[cfg(test)]
pub(crate) mod binding_mode_tests;
pub(crate) struct LockedPointerHint {
pub(crate) surface: WlSurface,
pub(crate) local: Point<f64, Logical>,
@@ -110,6 +113,9 @@ impl Ferese {
}
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
if self.input_capture.active() {
self.set_binding_mode("default");
}
let seat = self.seat.clone();
self.idle_notifier_state.notify_activity(&seat);
if !matches!(
@@ -286,15 +292,59 @@ impl Ferese {
}
}
if state == KeyState::Released {
data.binding_mode_pressed.remove(&keycode);
data.binding_mode_repeats.remove(&keycode);
data.portal_shortcuts.release(keycode, Event::time(&event));
if data.intercepted_keys.remove(&keycode) {
return FilterResult::Intercept(());
}
if data.input_capture.captures(1) {
data.input_capture.push(serde_json::json!({"type":"key", "key":keycode.raw().saturating_sub(8), "pressed":false}));
}
// Ownership comes from the press, never from the
// current mode or modifiers at release time.
return FilterResult::Forward;
}
let raw_symbols = keysym.raw_syms().into_iter().map(|symbol| symbol.raw()).collect::<Vec<_>>();
if !data.binding_mode_pressed.insert(keycode) {
// A held key belongs to the recipient of its first
// press, even when a mode or configuration changes.
return if data.intercepted_keys.contains(&keycode) {
if !data.session_lock.active() && !data.input_capture.active()
&& !data.seat.keyboard_shortcuts_inhibited()
&& let Some(binding) = data.binding_mode_repeats.get(&keycode)
.filter(|binding| binding.matches(keycode, &raw_symbols,
modifiers.logo, modifiers.ctrl, modifiers.alt, modifiers.shift))
.cloned()
{
let action = binding.action;
if let BindingAction::FocusMru(reverse) = action {
data.cycle_focus(reverse, modifiers.alt);
} else {
data.execute_binding(action);
}
}
FilterResult::Intercept(())
} else {
FilterResult::Forward
};
}
if data.session_lock.active() {
return FilterResult::Forward;
}
let symbol = keysym.modified_sym().raw();
if data.binding_modes.active().is_some() && symbol == keysyms::KEY_Escape {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
if emergency_shortcut_escape(symbol, modifiers.ctrl, modifiers.alt)
&& let Some(inhibitor) = data.active_shortcuts_inhibitor.take()
{
inhibitor.inactivate();
}
data.set_binding_mode("default");
}
return FilterResult::Intercept(());
}
if data.focus_cycle.is_some() && symbol == keysyms::KEY_Escape {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
@@ -362,12 +412,7 @@ impl Ferese {
return FilterResult::Intercept(());
}
let raw_symbols = keysym
.raw_syms()
.into_iter()
.map(|symbol| symbol.raw())
.collect::<Vec<_>>();
let action = data.bindings.action_for_key(
let binding = data.binding_modes.bindings(&data.bindings).binding_for_key(
keycode,
&raw_symbols,
modifiers.logo,
@@ -377,7 +422,7 @@ impl Ferese {
);
if data.focus_cycle.is_some()
&& state == KeyState::Pressed
&& !matches!(action, Some(BindingAction::FocusMru(_)))
&& !matches!(binding.map(|binding| &binding.action), Some(BindingAction::FocusMru(_)))
&& !matches!(symbol,
keysyms::KEY_Alt_L | keysyms::KEY_Alt_R
| keysyms::KEY_Shift_L | keysyms::KEY_Shift_R
@@ -387,8 +432,9 @@ impl Ferese {
data.intercepted_keys.insert(keycode);
return FilterResult::Intercept(());
}
let Some(action) = action else {
let Some(binding) = binding else {
if state == KeyState::Pressed
&& data.binding_modes.active().is_none()
&& data.portal_shortcuts.press(
keycode,
&raw_symbols,
@@ -404,7 +450,10 @@ impl Ferese {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
let action = action.clone();
let action = binding.action.clone();
if !matches!(action, BindingAction::BindingMode(_)) {
data.binding_mode_repeats.insert(keycode, binding.clone());
}
if let BindingAction::FocusMru(reverse) = action {
data.cycle_focus(reverse, modifiers.alt);
@@ -1083,6 +1132,7 @@ impl Ferese {
fn execute_binding(&mut self, action: BindingAction) {
match action {
BindingAction::None => {}
BindingAction::BindingMode(name) => self.set_binding_mode(&name),
BindingAction::Spawn(argv) => {
if let Some(child) =
spawn_client(self, argv.iter(), crate::private_client::ClientCapabilities::default())
@@ -1128,6 +1178,20 @@ impl Ferese {
}
}
}
pub(crate) fn set_binding_mode(&mut self, name: &str) {
if name != "default" && (self.session_lock.active() || self.input_capture.active()) {
return;
}
if name == "default" {
self.binding_mode_repeats.clear();
}
if self.binding_modes.set(name) {
self.binding_mode_repeats.clear();
self.cancel_focus_cycle();
self.send_shell_snapshots();
}
}
}
fn surface_tree_position(