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

@@ -844,3 +844,135 @@ fn window_rule_minimum_size_replaces_the_client_advertised_minimum() {
(800, 600)
);
}
#[test]
fn resize_binding_mode_repeats_resize_a_mapped_window_and_stop_on_escape() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::resize_binding_mode_repeats_resize_a_mapped_window_and_stop_on_escape",
) {
return;
}
use crate::input::binding_mode_tests::key;
let (mut events, mut state, _) = fixture();
state
.reload_config_source("binding-mode resize { binding h resize left; }".into())
.unwrap();
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.toggle_focused_floating();
state
.workspaces
.set_floating_rect(id, Rect::new(120., 100., 224., 128.))
.unwrap();
state.relayout_window(id);
state.set_binding_mode("resize");
key(&mut state, 43, true);
assert_eq!(
state.workspaces.placement(id),
Some(WindowPlacement::Floating {
rect: Rect::new(120., 100., 192., 128.)
})
);
key(&mut state, 43, true);
assert_eq!(
state.workspaces.placement(id),
Some(WindowPlacement::Floating {
rect: Rect::new(120., 100., 160., 128.)
})
);
key(&mut state, 9, true);
key(&mut state, 9, false);
key(&mut state, 43, true);
assert_eq!(
state.workspaces.placement(id),
Some(WindowPlacement::Floating {
rect: Rect::new(120., 100., 160., 128.)
})
);
key(&mut state, 43, false);
assert!(state.intercepted_keys.is_empty());
}
#[test]
fn resize_binding_mode_repeats_require_the_original_modifiers() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::resize_binding_mode_repeats_require_the_original_modifiers",
) {
return;
}
use crate::input::binding_mode_tests::key;
let (mut events, mut state, _) = fixture();
state
.reload_config_source("binding-mode resize { binding h resize left; }".into())
.unwrap();
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.toggle_focused_floating();
state
.workspaces
.set_floating_rect(id, Rect::new(120., 100., 320., 128.))
.unwrap();
state.relayout_window(id);
state.set_binding_mode("resize");
key(&mut state, 43, true);
let resized = state.workspaces.placement(id);
key(&mut state, 133, true);
key(&mut state, 43, true);
assert_eq!(
state.workspaces.placement(id),
resized,
"Super+h is not a resize-mode chord"
);
assert!(
state
.intercepted_keys
.contains(&smithay::input::keyboard::Keycode::new(43))
);
key(&mut state, 133, false);
key(&mut state, 43, true);
assert_eq!(
state.workspaces.placement(id),
Some(WindowPlacement::Floating {
rect: Rect::new(120., 100., 256., 128.),
})
);
key(&mut state, 43, false);
assert!(state.intercepted_keys.is_empty());
}
#[test]
fn reloading_mode_bindings_cancels_the_modes_focus_preview() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::reloading_mode_bindings_cancels_the_modes_focus_preview",
) {
return;
}
use crate::input::binding_mode_tests::key;
let (mut events, mut state, _) = fixture();
let source = "binding-mode cycle { binding Alt+Tab focus-mru-next; }";
state.reload_config_source(source.into()).unwrap();
let (_first, _first_wire) = window(&mut state, &mut events, 0xff112233);
let (_second, _second_wire) = window(&mut state, &mut events, 0xff223344);
let origin = state.focused_window;
state.set_binding_mode("cycle");
key(&mut state, 64, true);
key(&mut state, 23, true);
assert!(state.focus_cycle.is_some());
state
.reload_config_source(format!("{source}\ninput {{ repeat-rate 30; }}"))
.unwrap();
assert_eq!(state.binding_modes.active(), None);
assert!(
state.focus_cycle.is_none(),
"the exited mode cannot keep its focus preview"
);
assert_eq!(state.focused_window, origin);
assert!(
state
.intercepted_keys
.contains(&smithay::input::keyboard::Keycode::new(23))
);
key(&mut state, 23, false);
key(&mut state, 64, false);
assert!(state.intercepted_keys.is_empty());
}