Skip to content

Commit

Permalink
Allow binding virtual key input to gestures
Browse files Browse the repository at this point in the history
Closes #136.
  • Loading branch information
chrisduerr committed Nov 23, 2023
1 parent 48ebca7 commit 9a6378a
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 28 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 25 additions & 6 deletions catacomb_ipc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,31 @@ pub enum IpcMessage {
start: GestureSector,
/// Termination sector of the gesture.
end: GestureSector,
/// Programm this gesture should spawn.
/// Program or keybinding this gesture should spawn.
program: String,
/// Arguments for this gesture's program.
#[cfg_attr(feature = "clap", clap(allow_hyphen_values = true, trailing_var_arg = true))]
arguments: Vec<String>,
},
/// Add a gesture keybinding.
BindGestureKey {
/// App ID regex.
///
/// The binding will be enabled when the focused window's App ID matches
/// the regex.
///
/// Use `*` to bind the gesture globally.
app_id: String,
/// Starting sector of the gesture.
start: GestureSector,
/// Termination sector of the gesture.
end: GestureSector,
/// Desired modifiers.
#[cfg_attr(feature = "clap", clap(long, short))]
mods: Option<Modifiers>,
/// X11 keysym for this binding.
key: ClapKeysym,
},
/// Remove a gesture.
UnbindGesture {
/// App ID regex of the gesture.
Expand All @@ -99,7 +118,7 @@ pub enum IpcMessage {
mods: Option<Modifiers>,
/// Base key for this binding.
key: ClapKeysym,
/// Programm this gesture should spawn.
/// Program this gesture should spawn.
program: String,
/// Arguments for this gesture's program.
#[cfg_attr(feature = "clap", clap(allow_hyphen_values = true, trailing_var_arg = true))]
Expand Down Expand Up @@ -323,10 +342,10 @@ pub enum AppIdMatcherVariant {
/// Modifier state for a key press.
#[derive(Deserialize, Serialize, PartialEq, Eq, Default, Copy, Clone, Debug)]
pub struct Modifiers {
control: bool,
shift: bool,
logo: bool,
alt: bool,
pub control: bool,
pub shift: bool,
pub logo: bool,
pub alt: bool,
}

#[cfg(feature = "smithay")]
Expand Down
10 changes: 8 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ pub struct GestureBinding {
pub app_id: AppIdMatcher,
pub start: GestureSector,
pub end: GestureSector,
pub program: String,
pub arguments: Vec<String>,
pub action: GestureBindingAction,
}

/// Action variants for gesture bindings.
#[derive(Clone, Debug)]
pub enum GestureBindingAction {
Cmd((String, Vec<String>)),
Key((u32, Modifiers)),
}

/// User-defined key action.
Expand Down
135 changes: 118 additions & 17 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use smithay::backend::input::{
AbsolutePositionEvent, ButtonState, Event, InputBackend, InputEvent, KeyState,
KeyboardKeyEvent, MouseButton, PointerButtonEvent, TouchEvent as _, TouchSlot,
};
use smithay::input::keyboard::{keysyms, FilterResult, KeysymHandle, ModifiersState};
use smithay::input::keyboard::{
keysyms, FilterResult, Keycode, KeysymHandle, ModifiersState, XkbContextHandler,
};
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{LoopHandle, RegistrationToken};
use smithay::utils::{Logical, Point, Rectangle, SERIAL_COUNTER};
Expand All @@ -16,7 +18,7 @@ use smithay::wayland::seat::TouchHandle;
use tracing::error;

use crate::catacomb::Catacomb;
use crate::config::GestureBinding;
use crate::config::{GestureBinding, GestureBindingAction};
use crate::daemon;
use crate::drawing::CatacombSurfaceData;
use crate::orientation::Orientation;
Expand Down Expand Up @@ -148,10 +150,7 @@ impl TouchState {
let mut gestures = self.matching_gestures(canvas, app_id, start, end);

if let Some(gesture) = gestures.next() {
return Some(TouchAction::UserGesture((
gesture.program.clone(),
gesture.arguments.clone(),
)));
return Some(TouchAction::UserGesture(gesture.action.clone()));
}
}

Expand Down Expand Up @@ -223,7 +222,7 @@ impl TouchStart {
#[derive(Debug)]
enum TouchAction {
HandleGesture(HandleGesture),
UserGesture((String, Vec<String>)),
UserGesture(GestureBindingAction),
Drag,
Tap,
}
Expand Down Expand Up @@ -347,7 +346,12 @@ impl Catacomb {
self.reset_idle_timer();

match event {
InputEvent::Keyboard { event, .. } => self.on_keyboard_input(&event),
InputEvent::Keyboard { event, .. } => {
let time = Event::time(&event) as u32;
let code = event.key_code();
let state = event.state();
self.on_keyboard_input(code, state, time);
},
InputEvent::PointerButton { event } if event.button() == Some(MouseButton::Left) => {
let slot = TouchSlot::from(POINTER_TOUCH_SLOT);
let position = self.touch_state.position;
Expand Down Expand Up @@ -541,7 +545,7 @@ impl Catacomb {
}
},
Some(TouchAction::HandleGesture(gesture)) => self.on_handle_gesture(gesture),
Some(TouchAction::UserGesture((program, args))) => self.on_user_gesture(program, args),
Some(TouchAction::UserGesture(action)) => self.on_user_gesture(action),
_ => (),
}

Expand All @@ -563,10 +567,16 @@ impl Catacomb {
}

/// Dispatch user gestures.
fn on_user_gesture(&mut self, program: String, args: Vec<String>) {
// Execute subcommand.
if let Err(err) = daemon::spawn(&program, &args) {
error!("Failed gesture command {program} {args:?}: {err}");
fn on_user_gesture(&mut self, action: GestureBindingAction) {
match action {
// Execute subcommand.
GestureBindingAction::Cmd((program, args)) => {
if let Err(err) = daemon::spawn(&program, &args) {
error!("Failed gesture command {program} {args:?}: {err}");
}
},
// Submit virtual key press.
GestureBindingAction::Key((key, mods)) => self.send_virtual_key(key, mods),
}

self.touch_state.cancel_velocity();
Expand Down Expand Up @@ -627,15 +637,12 @@ impl Catacomb {
}

/// Handle new keyboard input events.
fn on_keyboard_input<I: InputBackend>(&mut self, event: &impl KeyboardKeyEvent<I>) {
fn on_keyboard_input(&mut self, code: u32, state: KeyState, time: u32) {
let keyboard = match self.seat.get_keyboard() {
Some(keyboard) => keyboard,
None => return,
};
let serial = SERIAL_COUNTER.next_serial();
let time = Event::time(event) as u32;
let code = event.key_code();
let state = event.state();

// Get desired action for this key.
let action = keyboard.input(self, code, state, serial, time, |catacomb, mods, keysym| {
Expand Down Expand Up @@ -666,6 +673,73 @@ impl Catacomb {
}
}

/// Send virtual keyboard input.
fn send_virtual_key(&mut self, keysym: u32, mods: Modifiers) {
let keyboard = match self.seat.get_keyboard() {
Some(keyboard) => keyboard,
None => return,
};

// Try to convert the keysym to a keycode.
let keycodes = self.keysym_to_keycode(keysym);
let keycode = match keycodes.first() {
Some(keycode) => keycode,
None => return,
};

// Get currently pressed modifier keys.
let old_mods = keyboard.with_pressed_keysyms(|keysyms| {
keysyms
.iter()
.filter(|keysym| {
let keysym = keysym.modified_sym().raw();
(!mods.shift && (keysym | 1 == keysyms::KEY_Shift_L))
|| (!mods.alt && (keysym | 1 == keysyms::KEY_Alt_L))
|| (!mods.logo && (keysym | 1 == keysyms::KEY_Super_L))
|| (!mods.control && (keysym | 1 == keysyms::KEY_Control_L))
})
.map(|keysym| keysym.raw_code().raw())
.collect::<Vec<_>>()
});

// Get keycodes for missing modifiers.
let mut new_mods = Vec::new();
let current_mods = keyboard.modifier_state();
if mods.shift && !current_mods.shift {
new_mods.push(42);
}
if mods.control && !current_mods.ctrl {
new_mods.push(29);
}
if mods.alt && !current_mods.alt {
new_mods.push(56);
}
if mods.logo && !current_mods.logo {
new_mods.push(125);
}

// Set desired modifiers.
for old_mod in &old_mods {
self.on_keyboard_input(*old_mod, KeyState::Released, 0);
}
for new_mod in &new_mods {
self.on_keyboard_input(*new_mod, KeyState::Pressed, 0);
}

// Send the key itself.
let raw_keycode = keycode.raw() - 8;
self.on_keyboard_input(raw_keycode, KeyState::Pressed, 0);
self.on_keyboard_input(raw_keycode, KeyState::Released, 0);

// Restore previous modifier state.
for new_mod in &new_mods {
self.on_keyboard_input(*new_mod, KeyState::Released, 0);
}
for old_mod in &old_mods {
self.on_keyboard_input(*old_mod, KeyState::Pressed, 0);
}
}

/// Get keyboard action for a keysym.
fn keyboard_action(
catacomb: &mut Catacomb,
Expand Down Expand Up @@ -795,6 +869,33 @@ impl Catacomb {

TimeoutAction::Drop
}

/// Convert Keysym to Keycode.
fn keysym_to_keycode(&mut self, keysym: u32) -> Vec<Keycode> {
let keyboard = match self.seat.get_keyboard() {
Some(keyboard) => keyboard,
None => return Vec::new(),
};

let mut codes = Vec::new();

// Iterate over all keycodes with the current layout to check for matches.
keyboard.with_xkb_state(self, |context| {
let layout = context.active_layout();
context.keymap().key_for_each(|_keymap, keycode| {
let matches = context
.raw_syms_for_key_in_layout(keycode, layout)
.iter()
.any(|sym| sym.raw() == keysym);

if matches {
codes.push(keycode);
}
});
});

codes
}
}

/// Actions to be taken on keyboard input.
Expand Down
18 changes: 16 additions & 2 deletions src/ipc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use smithay::reexports::calloop::LoopHandle;
use tracing::warn;

use crate::catacomb::Catacomb;
use crate::config::{GestureBinding, KeyBinding};
use crate::config::{GestureBinding, GestureBindingAction, KeyBinding};
use crate::socket::SocketSource;

/// Create an IPC socket.
Expand Down Expand Up @@ -97,7 +97,21 @@ fn handle_message(buffer: &mut String, stream: UnixStream, catacomb: &mut Cataco
},
};

let gesture = GestureBinding { app_id, start, end, program, arguments };
let action = GestureBindingAction::Cmd((program, arguments));
let gesture = GestureBinding { app_id, start, end, action };
catacomb.touch_state.user_gestures.push(gesture);
},
IpcMessage::BindGestureKey { app_id, start, end, mods, key } => {
let app_id = match AppIdMatcher::try_from(app_id) {
Ok(app_id) => app_id,
Err(err) => {
warn!("ignoring invalid ipc message: binding has invalid App ID regex: {err}");
return;
},
};

let action = GestureBindingAction::Key((key.0, mods.unwrap_or_default()));
let gesture = GestureBinding { app_id, start, end, action };
catacomb.touch_state.user_gestures.push(gesture);
},
IpcMessage::UnbindGesture { app_id, start, end } => {
Expand Down

0 comments on commit 9a6378a

Please sign in to comment.