Skip to content

Commit

Permalink
implement keyboard-shortcuts-inhibit and wlr-virtual-pointer (#630)
Browse files Browse the repository at this point in the history
* stub keyboard-shortcuts-inhibit and virtual-pointer impls

* implement keyboard-shortcuts-inhibit

* implement virtual-pointer

* deal with supressed key release edge-case; add allow-inhibiting property

* add toggle-keyboard-shortcuts-inhibit bind

* add InputBackend extensions; use Device::output() for absolute pos events

* add a `State` parameter to the backend exts and better document future intent

* Add some tests for is_inhibiting_shortcuts

---------

Co-authored-by: Ivan Molodetskikh <[email protected]>
  • Loading branch information
sodiboo and YaLTeR authored Jan 18, 2025
1 parent bd559a2 commit 0584dd2
Show file tree
Hide file tree
Showing 8 changed files with 889 additions and 22 deletions.
49 changes: 48 additions & 1 deletion niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,7 @@ pub struct Bind {
pub repeat: bool,
pub cooldown: Option<Duration>,
pub allow_when_locked: bool,
pub allow_inhibiting: bool,
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
Expand Down Expand Up @@ -1278,6 +1279,7 @@ pub enum Action {
id: u64,
write_to_disk: bool,
},
ToggleKeyboardShortcutsInhibit,
CloseWindow,
#[knuffel(skip)]
CloseWindowById(u64),
Expand Down Expand Up @@ -3015,6 +3017,7 @@ where
let mut cooldown = None;
let mut allow_when_locked = false;
let mut allow_when_locked_node = None;
let mut allow_inhibiting = true;
for (name, val) in &node.properties {
match &***name {
"repeat" => {
Expand All @@ -3029,6 +3032,9 @@ where
allow_when_locked = knuffel::traits::DecodeScalar::decode(val, ctx)?;
allow_when_locked_node = Some(name);
}
"allow-inhibiting" => {
allow_inhibiting = knuffel::traits::DecodeScalar::decode(val, ctx)?;
}
name_str => {
ctx.emit_error(DecodeError::unexpected(
name,
Expand All @@ -3050,6 +3056,7 @@ where
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
};

if let Some(child) = children.next() {
Expand All @@ -3072,12 +3079,19 @@ where
}
}

// The toggle-inhibit action must always be uninhibitable.
// Otherwise, it would be impossible to trigger it.
if matches!(action, Action::ToggleKeyboardShortcutsInhibit) {
allow_inhibiting = false;
}

Ok(Self {
key,
action,
repeat,
cooldown,
allow_when_locked,
allow_inhibiting,
})
}
Err(e) => {
Expand Down Expand Up @@ -3463,14 +3477,16 @@ mod tests {
}
binds {
Mod+Escape { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+Escape allow-inhibiting=true { toggle-keyboard-shortcuts-inhibit; }
Mod+T allow-when-locked=true { spawn "alacritty"; }
Mod+Q { close-window; }
Mod+Shift+H { focus-monitor-left; }
Mod+Ctrl+Shift+L { move-window-to-monitor-right; }
Mod+Comma { consume-window-into-column; }
Mod+1 { focus-workspace 1; }
Mod+Shift+1 { focus-workspace "workspace-1"; }
Mod+Shift+E { quit skip-confirmation=true; }
Mod+Shift+E allow-inhibiting=false { quit skip-confirmation=true; }
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
}
Expand Down Expand Up @@ -3779,6 +3795,28 @@ mod tests {
},
],
binds: Binds(vec![
Bind {
key: Key {
trigger: Trigger::Keysym(Keysym::Escape),
modifiers: Modifiers::COMPOSITOR,
},
action: Action::ToggleKeyboardShortcutsInhibit,
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: false,
},
Bind {
key: Key {
trigger: Trigger::Keysym(Keysym::Escape),
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
action: Action::ToggleKeyboardShortcutsInhibit,
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: false,
},
Bind {
key: Key {
trigger: Trigger::Keysym(Keysym::t),
Expand All @@ -3788,6 +3826,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: true,
allow_inhibiting: true,
},
Bind {
key: Key {
Expand All @@ -3798,6 +3837,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
},
Bind {
key: Key {
Expand All @@ -3808,6 +3848,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
},
Bind {
key: Key {
Expand All @@ -3818,6 +3859,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
},
Bind {
key: Key {
Expand All @@ -3828,6 +3870,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
},
Bind {
key: Key {
Expand All @@ -3838,6 +3881,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
},
Bind {
key: Key {
Expand All @@ -3850,6 +3894,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
},
Bind {
key: Key {
Expand All @@ -3860,6 +3905,7 @@ mod tests {
repeat: true,
cooldown: None,
allow_when_locked: false,
allow_inhibiting: false,
},
Bind {
key: Key {
Expand All @@ -3870,6 +3916,7 @@ mod tests {
repeat: true,
cooldown: Some(Duration::from_millis(150)),
allow_when_locked: false,
allow_inhibiting: true,
},
]),
switch_events: SwitchBinds {
Expand Down
10 changes: 10 additions & 0 deletions resources/default-config.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,16 @@ binds {
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }

// Applications such as remote-desktop clients and software KVM switches may
// request that niri stops processing the keyboard shortcuts defined here
// so they may, for example, forward the key presses as-is to a remote machine.
// It's a good idea to bind an escape hatch to toggle the inhibitor,
// so a buggy application can't hold your session hostage.
//
// The allow-inhibiting=false property can be applied to other binds as well,
// which ensures niri always processes them, even when an inhibitor is active.
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }

// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
Expand Down
69 changes: 62 additions & 7 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::time::Duration;

use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::DrmNode;
use smithay::backend::input::TabletToolDescriptor;
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
Expand All @@ -35,6 +35,9 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler;
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::keyboard_shortcuts_inhibit::{
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
use smithay::wayland::security_context::{
Expand All @@ -59,11 +62,12 @@ use smithay::wayland::xdg_activation::{
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
delegate_input_method_manager, delegate_keyboard_shortcuts_inhibit, delegate_output,
delegate_pointer_constraints, delegate_pointer_gestures, delegate_presentation,
delegate_primary_selection, delegate_relative_pointer, delegate_seat,
delegate_security_context, delegate_session_lock, delegate_single_pixel_buffer,
delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter,
delegate_virtual_keyboard_manager, delegate_xdg_activation,
};

pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
Expand All @@ -75,10 +79,15 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
use crate::protocols::virtual_pointer::{
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
VirtualPointerMotionEvent,
};
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
use crate::{
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
delegate_output_management, delegate_screencopy,
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
};

pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
Expand Down Expand Up @@ -243,7 +252,28 @@ impl InputMethodHandler for State {
}
}

impl KeyboardShortcutsInhibitHandler for State {
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
&mut self.niri.keyboard_shortcuts_inhibit_state
}

fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
inhibitor.activate();
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.insert(inhibitor.wl_surface().clone(), inhibitor);
}

fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
self.niri
.keyboard_shortcuts_inhibiting_surfaces
.remove(&inhibitor.wl_surface().clone());
}
}

delegate_input_method_manager!(State);
delegate_keyboard_shortcuts_inhibit!(State);
delegate_virtual_keyboard_manager!(State);

impl SelectionHandler for State {
Expand Down Expand Up @@ -562,6 +592,31 @@ impl ScreencopyHandler for State {
}
delegate_screencopy!(State);

impl VirtualPointerHandler for State {
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
&mut self.niri.virtual_pointer_state
}

fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
}

fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
self.process_input_event(
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
);
}

fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
}

fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
}
}
delegate_virtual_pointer!(State);

impl DrmLeaseHandler for State {
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
self.backend
Expand Down
51 changes: 51 additions & 0 deletions src/input/backend_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use ::input as libinput;
use smithay::backend::input;
use smithay::backend::winit::WinitVirtualDevice;
use smithay::output::Output;

use crate::niri::State;
use crate::protocols::virtual_pointer::VirtualPointer;

pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
type NiriDevice: NiriInputDevice;
}
impl<T: input::InputBackend> NiriInputBackend for T
where
Self::Device: NiriInputDevice,
{
type NiriDevice = Self::Device;
}

pub trait NiriInputDevice: input::Device {
// FIXME: this should maybe be per-event, not per-device,
// but it's not clear that this matters in practice?
// it might be more obvious once we implement it for libinput
fn output(&self, state: &State) -> Option<Output>;
}

impl NiriInputDevice for libinput::Device {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: Allow specifying the output per-device?
None
}
}

impl NiriInputDevice for WinitVirtualDevice {
fn output(&self, _state: &State) -> Option<Output> {
// FIXME: we should be returning the single output that the winit backend creates,
// but for now, that will cause issues because the output is normally upside down,
// so we apply Transform::Flipped180 to it and that would also cause
// the cursor position to be flipped, which is not what we want.
//
// instead, we just return None and rely on the fact that it has only one output.
// doing so causes the cursor to be placed in *global* output coordinates,
// which are not flipped, and happen to be what we want.
None
}
}

impl NiriInputDevice for VirtualPointer {
fn output(&self, _: &State) -> Option<Output> {
self.output().cloned()
}
}
Loading

0 comments on commit 0584dd2

Please sign in to comment.