atagen

monolith

public
Star0
Watch0

Commit 4727706757b9

tarball

atagen <boss@atagen.co>2026-10-19 09:48 UTC

5 files changed+78-0
inhibit: let focused clients take the keyboard from binds

VM viewers and remote desktop clients could not receive Super or Alt combinations because monolith always consumed them. A keyboard shortcuts inhibitor activates without a prompt while its surface has keyboard focus and ends with it. Binds that run reload or quit still fire so a stuck client can be walked out of, and VT switches are never inhibited.
Modified file: crates/monolith/src/globals.rs+3 additions
@@ -20,6 +20,7 @@ use smithay::wayland::image_capture_source::{
2020 ImageCaptureSourceState, OutputCaptureSourceState, ToplevelCaptureSourceState,
2121 };
2222 use smithay::wayland::image_copy_capture::ImageCopyCaptureState;
23+use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitState;
2324 use smithay::wayland::output::OutputManagerState;
2425 use smithay::wayland::pointer_constraints::PointerConstraintsState;
2526 use smithay::wayland::presentation::PresentationState;
@@ -62,6 +63,7 @@ pub struct Protocols {
6263 pub toplevel_list: ForeignToplevelListState,
6364 pub xdg_foreign: XdgForeignState,
6465 _xdg_dialog: XdgDialogState,
66+ pub shortcuts_inhibit: KeyboardShortcutsInhibitState,
6567 _image_capture_source: ImageCaptureSourceState,
6668 _idle_inhibit: IdleInhibitManagerState,
6769 _xdg_decoration: XdgDecorationState,
@@ -128,6 +130,7 @@ impl Protocols {
128130 toplevel_list: ForeignToplevelListState::new::<State>(dh),
129131 xdg_foreign: XdgForeignState::new::<State>(dh),
130132 _xdg_dialog: XdgDialogState::new::<State>(dh),
133+ shortcuts_inhibit: KeyboardShortcutsInhibitState::new::<State>(dh),
131134 _image_capture_source: ImageCaptureSourceState::new(),
132135 _idle_inhibit: IdleInhibitManagerState::new::<State>(dh),
133136 _xdg_decoration: XdgDecorationState::new::<State>(dh),
Added file: crates/monolith/src/inhibit.rs+68 additions
@@ -0,0 +1,68 @@
1+//! Keyboard shortcut inhibitors. A client that asks gets its keys while it
2+//! has keyboard focus, with no prompt. Only binds that quit or reload still
3+//! fire, so a stuck inhibitor can always be walked out of, and VT switches
4+//! are never inhibited.
5+
6+use crate::focus::Focus;
7+use crate::state::State;
8+use monolith_ipc::Command;
9+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
10+use smithay::wayland::keyboard_shortcuts_inhibit::{
11+ KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
12+ KeyboardShortcutsInhibitorSeat,
13+};
14+use smithay::wayland::seat::WaylandFocus;
15+
16+pub fn escapes(cmd: &Command) -> bool {
17+ matches!(cmd, Command::Reload | Command::Quit)
18+}
19+
20+impl State {
21+ pub fn shortcuts_inhibited(&self) -> bool {
22+ self.input.inhibitor.as_ref().is_some_and(|i| i.is_active())
23+ }
24+
25+ /// Moves the active inhibitor to the new keyboard focus's, if it has one.
26+ pub fn follow_inhibitor(&mut self, focused: Option<&Focus>) {
27+ let surface = focused.and_then(|f| f.wl_surface());
28+ let next = surface
29+ .as_deref()
30+ .and_then(|s| self.input.seat.keyboard_shortcuts_inhibitor_for_surface(s));
31+ if self.input.inhibitor == next {
32+ return;
33+ }
34+ if let Some(old) = std::mem::replace(&mut self.input.inhibitor, next.clone()) {
35+ old.inactivate();
36+ }
37+ if let Some(next) = next {
38+ next.activate();
39+ }
40+ }
41+}
42+
43+impl KeyboardShortcutsInhibitHandler for State {
44+ fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
45+ &mut self.protocols.shortcuts_inhibit
46+ }
47+
48+ fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
49+ let focused = self
50+ .input
51+ .seat
52+ .get_keyboard()
53+ .and_then(|k| k.current_focus());
54+ if focused
55+ .and_then(|f| f.wl_surface().map(|s| s.into_owned()))
56+ .is_some_and(|s: WlSurface| &s == inhibitor.wl_surface())
57+ {
58+ inhibitor.activate();
59+ self.input.inhibitor = Some(inhibitor);
60+ }
61+ }
62+
63+ fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
64+ if self.input.inhibitor.as_ref() == Some(&inhibitor) {
65+ self.input.inhibitor = None;
66+ }
67+ }
68+}
Modified file: crates/monolith/src/input.rs+3 additions
@@ -210,6 +210,9 @@ impl State {
210210 Some(cmd) if state.services.lock.active() && !cmd.allowed_while_locked() => {
211211 FilterResult::Forward
212212 }
213+ Some(cmd) if state.shortcuts_inhibited() && !crate::inhibit::escapes(&cmd) => {
214+ FilterResult::Forward
215+ }
213216 Some(cmd) => {
214217 tracing::debug!(?cmd, "bind");
215218 state.input.suppressed.push(code);
Modified file: crates/monolith/src/main.rs+1 additions
@@ -13,6 +13,7 @@ mod frame;
1313 mod globals;
1414 mod idle;
1515 mod import;
16+mod inhibit;
1617 mod input;
1718 mod ipc;
1819 mod launch;
Modified file: crates/monolith/src/state.rs+3 additions
@@ -68,6 +68,7 @@ pub struct Input {
6868 /// Where the last absolute device event put the pointer, so the next
6969 /// one yields a relative delta even while the pointer is held in place.
7070 pub absolute: Option<Point<f64, Logical>>,
71+ pub inhibitor: Option<smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor>,
7172 }
7273
7374 /// What needs doing after this batch of events.
@@ -202,6 +203,7 @@ impl State {
202203 hover: None,
203204 pointer_output: None,
204205 absolute: None,
206+ inhibitor: None,
205207 },
206208 dirty: Dirty {
207209 redraw: true,
@@ -470,6 +472,7 @@ impl State {
470472 {
471473 keyboard.set_focus(self, target.clone(), SERIAL_COUNTER.next_serial());
472474 }
475+ self.follow_inhibitor(target.as_ref());
473476 self.sync_pointer_constraint();
474477 }