Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat(input): bind touchpad gestures from input.kdl; ccectl pointer-swipe
The gesture table (matched in the cursor's swipe/pinch handlers) was
fed only by config.kdl `gesture_bind` nodes and the legacy
`window_manager { toggle_overview "swipe_down" }`. Now a
cce-window-manager entry in input.kdl whose chord parses as a gesture
(`focus_left "swipe3_left"`, `super+pinch_out`) lands there too, ahead
of both legacy sources, so the file that holds every other binding can
hold these. A fingerless gesture binds three and four fingers; a repeat
warns like a repeated key chord.
`ccectl pointer-swipe <fingers> <dx> <dy> [steps]` injects a whole swipe
the way pointer-pinch does, so the table can be driven in a headless
shadow: verified swipe3 left/right stepping focus, swipe4 staying
unbound, and the legacy swipe_down overview still firing.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/cce_ctl.rs | 3 +++
src/server/config.rs | 38 ++++++++++++++++++++++++++++++++++----
src/server/cursor.rs | 43 +++++++++++++++++++++++++++++++++++++++++++
src/server/window_manager.rs | 14 ++++++++++++++
4 files changed, 94 insertions(+), 4 deletions(-)
diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index fe160d2..b86e860 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -91,6 +91,9 @@ fn usage(name: &str, to_stderr: bool) {
print(" two-finger swipe, ended by finger-stop; natural = from");
print(" a natural-scrolling touchpad, deltas already flipped)");
print(" pointer-scroll finger-stop");
+ print(" pointer-swipe <fingers> <dx> <dy> [steps]");
+ print(" (a 3/4-finger touchpad swipe; drives input.kdl gesture");
+ print(" chords such as focus_left \"swipe3_left\")");
print(" pointer-pinch <scale> [rotation] [steps] | begin | update <scale> [rotation] | end");
print(" touchpad-view-regions <x11:ID|id|app_id> clear | <x,y,w,h> ...");
print(" (limit a touchpad_view_apps drag to these window-local");
diff --git a/src/server/config.rs b/src/server/config.rs
index f070484..4253084 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -2578,13 +2578,12 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
state.keybinds.clear();
let mut table = cce_window_manager::bindings::BindingTable::new();
+ // Gesture entries from the same domain (`focus_left "swipe3_left"`);
+ // they go ahead of config.kdl's `gesture_bind` nodes below.
+ let mut input_gesture_binds: Vec<GestureBind> = Vec::new();
// Primary source: the `cce-window-manager` domain of input.kdl.
for entry in &wm_domain_entries {
- let Some(chord) = cce_window_manager::bindings::parse_chord(&entry.chord) else {
- eprintln!("[WARNING] input.kdl: invalid chord {:?} for {}", entry.chord, entry.name);
- continue;
- };
let Some(action) = Action::from_name(&entry.name) else {
eprintln!("[WARNING] input.kdl: unknown window-manager action {:?}", entry.name);
continue;
@@ -2603,6 +2602,33 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
}
_ => None,
};
+ // A touchpad gesture rides in the chord slot: `swipe3_left`,
+ // `super+pinch_out`. The fingerless spelling binds three AND four
+ // fingers, as `toggle_overview "swipe_down"` always has.
+ if let Some(g) = cce_window_manager::bindings::parse_gesture(&entry.chord) {
+ let fingers: Vec<u32> = g.fingers.map(|n| vec![n]).unwrap_or_else(|| vec![3, 4]);
+ for fingers in fingers {
+ let dup = input_gesture_binds.iter().any(|b| {
+ b.mods == g.mods && b.gesture_type == g.kind.as_str() && b.fingers == fingers && b.direction == g.direction
+ });
+ if dup {
+ eprintln!("[WARNING] input.kdl: {:?} is bound more than once", entry.chord);
+ }
+ input_gesture_binds.push(GestureBind {
+ mods: g.mods,
+ gesture_type: g.kind.as_str().to_string(),
+ fingers,
+ direction: g.direction.clone(),
+ action,
+ command: command.clone(),
+ });
+ }
+ continue;
+ }
+ let Some(chord) = cce_window_manager::bindings::parse_chord(&entry.chord) else {
+ eprintln!("[WARNING] input.kdl: invalid chord {:?} for {}", entry.chord, entry.name);
+ continue;
+ };
let keysym = parse_keysym(&chord.key);
if keysym == 0 {
eprintln!("[WARNING] input.kdl: unknown key {:?} in chord {:?}", chord.key, entry.chord);
@@ -2680,7 +2706,11 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
});
}
+ // Gesture table, first match wins in the cursor's swipe/pinch handlers:
+ // input.kdl entries, then config.kdl `gesture_bind` nodes, then the
+ // legacy `window_manager { toggle_overview "swipe_down" }`.
state.gesture_binds.clear();
+ state.gesture_binds.extend(input_gesture_binds);
for gb in &config.gesture_bind {
let mods = gb.mods.as_ref().map(|m| parse_modifiers(m)).unwrap_or(0);
let action = parse_action(&gb.action);
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index a835820..5ee1246 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -1095,6 +1095,49 @@ impl Cursor {
ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
}
+ /// Inject a whole touchpad swipe: begin with `fingers`, `steps` updates
+ /// that together move the gesture centre by (`dx`, `dy`), end. Drives
+ /// the gesture-bind table (`swipe3_left` in input.kdl) headlessly;
+ /// the pointer-gestures forward to clients runs too.
+ pub unsafe fn inject_swipe(&mut self, fingers: u32, dx: f64, dy: f64, steps: u32) {
+ let time = crate::util::msec_timestamp();
+ let mut begin = ffi::wlr_pointer_swipe_begin_event {
+ pointer: std::ptr::null_mut(),
+ time_msec: time,
+ fingers,
+ };
+ handle_swipe_begin(
+ &mut self.swipe_begin_listener as *mut ffi::wl_listener,
+ &mut begin as *mut ffi::wlr_pointer_swipe_begin_event as *mut std::ffi::c_void,
+ );
+ ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
+ let steps = steps.max(1);
+ for i in 1..=steps {
+ let mut update = ffi::wlr_pointer_swipe_update_event {
+ pointer: std::ptr::null_mut(),
+ time_msec: time + i,
+ fingers,
+ dx: dx / steps as f64,
+ dy: dy / steps as f64,
+ };
+ handle_swipe_update(
+ &mut self.swipe_update_listener as *mut ffi::wl_listener,
+ &mut update as *mut ffi::wlr_pointer_swipe_update_event as *mut std::ffi::c_void,
+ );
+ ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
+ }
+ let mut end = ffi::wlr_pointer_swipe_end_event {
+ pointer: std::ptr::null_mut(),
+ time_msec: time + steps + 1,
+ cancelled: false,
+ };
+ handle_swipe_end(
+ &mut self.swipe_end_listener as *mut ffi::wl_listener,
+ &mut end as *mut ffi::wlr_pointer_swipe_end_event as *mut std::ffi::c_void,
+ );
+ ffi::wlr_seat_pointer_notify_frame((*self.seat).wlr_seat);
+ }
+
pub unsafe fn inject_pinch(&mut self, scale: f64, rotation: f64, steps: u32) {
let time = crate::util::msec_timestamp();
let mut begin = ffi::wlr_pointer_pinch_begin_event {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 886ca43..6f45fac 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -5444,6 +5444,20 @@ impl WindowManager {
"error: invalid dy or dx\n".to_string()
}
}
+ "pointer-swipe" => {
+ // pointer-swipe <fingers> <dx> <dy> [steps]
+ if parts.len() < 4 { return "error: usage: pointer-swipe <fingers> <dx> <dy> [steps]\n".to_string(); }
+ let fingers = parts[1].parse::<u32>();
+ let dx = parts[2].parse::<f64>();
+ let dy = parts[3].parse::<f64>();
+ let steps = parts.get(4).map(|v| v.parse::<u32>()).unwrap_or(Ok(10));
+ if let (Ok(fingers), Ok(dx), Ok(dy), Ok(steps)) = (fingers, dx, dy, steps) {
+ self.for_each_cursor(|cursor| cursor.inject_swipe(fingers, dx, dy, steps));
+ "ok\n".to_string()
+ } else {
+ "error: invalid swipe arguments\n".to_string()
+ }
+ }
"pointer-pinch" => {
// pointer-pinch <scale> [rotation-degrees] [steps]
// pointer-pinch begin | update <scale> [rotation] | end (paced by the caller)