git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit55d81888c0f9d234ddf4be5463f86eed86b24826
parent61a7955aee
authorLucas Galante <[email protected]>
date2026-06-16 08:31
Add config reload action, bind it to super+shift+r, and add trackpad gesture config support

 .antigravityignore           |   6 +++
 src/server/config.rs         |  60 ++++++++++++++++++++-
 src/server/cursor.rs         | 124 +++++++++++++++++++++++++++++++++++++++++++
 src/server/server.rs         |   1 +
 src/server/window_manager.rs |  17 ++++++
 5 files changed, 207 insertions(+), 1 deletion(-)

diff --git a/.antigravityignore b/.antigravityignore
new file mode 100644
index 0000000..54cf910
--- /dev/null
+++ b/.antigravityignore
@@ -0,0 +1,6 @@
+/target
+.antigravitycli/
+.gemini/
+*.log
+check_errors.txt
+Cargo.lock
diff --git a/src/server/config.rs b/src/server/config.rs
index 1ba3c81..3200652 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -102,6 +102,7 @@ pub enum Action {
     Move,
     Resize,
     Exit,
+    Reload,
     Fullscreen,
     LayoutNext,
     ModeNext,
@@ -139,6 +140,18 @@ pub struct PointerBindConfig {
     pub action: String,
 }
 
+#[derive(Debug, Deserialize, Clone)]
+pub struct GestureBindConfig {
+    #[serde(default)]
+    pub mods: Option<String>,
+    #[serde(rename = "type")]
+    pub gesture_type: String,
+    pub fingers: u32,
+    pub direction: String,
+    pub action: String,
+    pub command: Option<String>,
+}
+
 #[derive(Debug, Deserialize, Clone)]
 pub struct StartupConfig {
     pub exec: String,
@@ -163,6 +176,16 @@ pub struct PointerBind {
     pub action: Action,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct GestureBind {
+    pub mods: u32,
+    pub gesture_type: String,
+    pub fingers: u32,
+    pub direction: String,
+    pub action: Action,
+    pub command: Option<String>,
+}
+
 #[derive(Debug, Deserialize, Clone)]
 pub struct OutputConfig {
     #[serde(default = "default_scale")]
@@ -213,6 +236,8 @@ pub struct Config {
     pub device: Vec<InputDeviceConfigRule>,
     #[serde(default)]
     pub input: Option<InputConfig>,
+    #[serde(default)]
+    pub gesture_bind: Vec<GestureBindConfig>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -404,6 +429,8 @@ pub fn parse_action(s: &str) -> Action {
         Action::Close
     } else if s == "exit" {
         Action::Exit
+    } else if s == "reload" {
+        Action::Reload
     } else if s == "focus-next" {
         Action::FocusNext
     } else if s == "focus-prev" {
@@ -673,6 +700,17 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
         });
     }
 
+    let super_shift_mod = parse_modifiers("super+shift");
+    let r_sym = parse_keysym("r");
+    if !state.keybinds.iter().any(|b| b.mods == super_shift_mod && b.keysym == r_sym) {
+        state.keybinds.push(Keybind {
+            mods: super_shift_mod,
+            keysym: r_sym,
+            action: Action::Reload,
+            command: None,
+        });
+    }
+
     state.pointer_binds.clear();
     for pb in &config.pointer_bind {
         let mods = parse_modifiers(&pb.mods);
@@ -685,6 +723,25 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
         });
     }
 
+    state.gesture_binds.clear();
+    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);
+        let command = if action == Action::Spawn || action == Action::Toggle {
+            gb.command.clone()
+        } else {
+            None
+        };
+        state.gesture_binds.push(GestureBind {
+            mods,
+            gesture_type: gb.gesture_type.clone(),
+            fingers: gb.fingers,
+            direction: gb.direction.clone(),
+            action,
+            command,
+        });
+    }
+
     state.mode_rules.clear();
     for rule in config.mode_rule {
         state.mode_rules.push(ModeRule {
@@ -712,9 +769,10 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
     }
 
     log::info!(
-        "Parsed config: {} keybinds, {} pointer binds, {} startup programs",
+        "Parsed config: {} keybinds, {} pointer binds, {} gesture binds, {} startup programs",
         state.keybinds.len(),
         state.pointer_binds.len(),
+        state.gesture_binds.len(),
         state.startup.len()
     );
 
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 90aed1f..3c073b7 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -41,6 +41,11 @@ pub struct Cursor {
 
     pub hold_begin_listener: ffi::wl_listener,
     pub hold_end_listener: ffi::wl_listener,
+
+    pub gesture_dx: f64,
+    pub gesture_dy: f64,
+    pub gesture_scale: f64,
+    pub gesture_triggered: bool,
 }
 
 impl Default for Cursor {
@@ -79,6 +84,11 @@ impl Default for Cursor {
 
             hold_begin_listener: unsafe { std::mem::zeroed() },
             hold_end_listener: unsafe { std::mem::zeroed() },
+
+            gesture_dx: 0.0,
+            gesture_dy: 0.0,
+            gesture_scale: 1.0,
+            gesture_triggered: false,
         }
     }
 }
@@ -1031,6 +1041,10 @@ unsafe extern "C" fn handle_swipe_begin(listener: *mut ffi::wl_listener, data: *
     let seat = &mut *cursor.seat;
     seat.handle_activity();
 
+    cursor.gesture_dx = 0.0;
+    cursor.gesture_dy = 0.0;
+    cursor.gesture_triggered = false;
+
     let server = seat.server;
     let pointer_gestures = (*server).input_manager.pointer_gestures;
     if !pointer_gestures.is_null() {
@@ -1050,6 +1064,56 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
     let seat = &mut *cursor.seat;
     seat.handle_activity();
 
+    if cursor.gesture_triggered {
+        return;
+    }
+
+    cursor.gesture_dx += (*event).dx;
+    cursor.gesture_dy += (*event).dy;
+
+    let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
+    let modifiers = if !wlr_keyboard.is_null() {
+        ffi::wlr_keyboard_get_modifiers(wlr_keyboard)
+    } else {
+        0
+    };
+
+    let mut matched_action = crate::config::Action::None;
+    let mut matched_command = None;
+
+    for gb in &(*seat.server).wm.gesture_binds {
+        if gb.gesture_type == "swipe" && gb.fingers == (*event).fingers && gb.mods == modifiers {
+            let matched = match gb.direction.as_str() {
+                "left" => cursor.gesture_dx < -150.0,
+                "right" => cursor.gesture_dx > 150.0,
+                "up" => cursor.gesture_dy < -150.0,
+                "down" => cursor.gesture_dy > 150.0,
+                _ => false,
+            };
+            if matched {
+                matched_action = gb.action;
+                matched_command = gb.command.clone();
+                break;
+            }
+        }
+    }
+
+    if matched_action != crate::config::Action::None {
+        cursor.gesture_triggered = true;
+        (*seat.server).wm.execute_action(&matched_action, matched_command.as_deref());
+
+        let pointer_gestures = (*seat.server).input_manager.pointer_gestures;
+        if !pointer_gestures.is_null() {
+            ffi::wlr_pointer_gestures_v1_send_swipe_end(
+                pointer_gestures,
+                seat.wlr_seat,
+                (*event).time_msec,
+                true, // cancelled: true
+            );
+        }
+        return;
+    }
+
     let server = seat.server;
     let pointer_gestures = (*server).input_manager.pointer_gestures;
     if !pointer_gestures.is_null() {
@@ -1070,6 +1134,11 @@ unsafe extern "C" fn handle_swipe_end(listener: *mut ffi::wl_listener, data: *mu
     let seat = &mut *cursor.seat;
     seat.handle_activity();
 
+    if cursor.gesture_triggered {
+        cursor.gesture_triggered = false;
+        return;
+    }
+
     let server = seat.server;
     let pointer_gestures = (*server).input_manager.pointer_gestures;
     if !pointer_gestures.is_null() {
@@ -1089,6 +1158,9 @@ unsafe extern "C" fn handle_pinch_begin(listener: *mut ffi::wl_listener, data: *
     let seat = &mut *cursor.seat;
     seat.handle_activity();
 
+    cursor.gesture_scale = 1.0;
+    cursor.gesture_triggered = false;
+
     let server = seat.server;
     let pointer_gestures = (*server).input_manager.pointer_gestures;
     if !pointer_gestures.is_null() {
@@ -1108,6 +1180,53 @@ unsafe extern "C" fn handle_pinch_update(listener: *mut ffi::wl_listener, data:
     let seat = &mut *cursor.seat;
     seat.handle_activity();
 
+    if cursor.gesture_triggered {
+        return;
+    }
+
+    cursor.gesture_scale = (*event).scale;
+
+    let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
+    let modifiers = if !wlr_keyboard.is_null() {
+        ffi::wlr_keyboard_get_modifiers(wlr_keyboard)
+    } else {
+        0
+    };
+
+    let mut matched_action = crate::config::Action::None;
+    let mut matched_command = None;
+
+    for gb in &(*seat.server).wm.gesture_binds {
+        if gb.gesture_type == "pinch" && gb.fingers == (*event).fingers && gb.mods == modifiers {
+            let matched = match gb.direction.as_str() {
+                "in" => cursor.gesture_scale < 0.7,
+                "out" => cursor.gesture_scale > 1.3,
+                _ => false,
+            };
+            if matched {
+                matched_action = gb.action;
+                matched_command = gb.command.clone();
+                break;
+            }
+        }
+    }
+
+    if matched_action != crate::config::Action::None {
+        cursor.gesture_triggered = true;
+        (*seat.server).wm.execute_action(&matched_action, matched_command.as_deref());
+
+        let pointer_gestures = (*seat.server).input_manager.pointer_gestures;
+        if !pointer_gestures.is_null() {
+            ffi::wlr_pointer_gestures_v1_send_pinch_end(
+                pointer_gestures,
+                seat.wlr_seat,
+                (*event).time_msec,
+                true, // cancelled: true
+            );
+        }
+        return;
+    }
+
     let server = seat.server;
     let pointer_gestures = (*server).input_manager.pointer_gestures;
     if !pointer_gestures.is_null() {
@@ -1130,6 +1249,11 @@ unsafe extern "C" fn handle_pinch_end(listener: *mut ffi::wl_listener, data: *mu
     let seat = &mut *cursor.seat;
     seat.handle_activity();
 
+    if cursor.gesture_triggered {
+        cursor.gesture_triggered = false;
+        return;
+    }
+
     let server = seat.server;
     let pointer_gestures = (*server).input_manager.pointer_gestures;
     if !pointer_gestures.is_null() {
diff --git a/src/server/server.rs b/src/server/server.rs
index 692998e..a3eace8 100644
--- a/src/server/server.rs
+++ b/src/server/server.rs
@@ -798,6 +798,7 @@ impl Default for Server {
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.mode_rules, Vec::new());
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.keybinds, Vec::new());
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.pointer_binds, Vec::new());
+            std::ptr::write(&mut (*server.as_mut_ptr()).wm.gesture_binds, Vec::new());
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.ipc_rx, None);
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.startup, Vec::new());
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.status_sender, None);
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index fcef2a1..919e6a9 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -62,6 +62,7 @@ pub struct WindowManager {
     pub mode_rules: Vec<crate::config::ModeRule>,
     pub keybinds: Vec<crate::config::Keybind>,
     pub pointer_binds: Vec<crate::config::PointerBind>,
+    pub gesture_binds: Vec<crate::config::GestureBind>,
     pub ipc_rx: Option<std::sync::mpsc::Receiver<crate::ipc_server::IpcRequest>>,
     pub ipc_timer: *mut ffi::wl_event_source,
     pub startup: Vec<crate::config::StartupConfig>,
@@ -118,6 +119,7 @@ impl WindowManager {
         self.mode_rules = Vec::new();
         self.keybinds = Vec::new();
         self.pointer_binds = Vec::new();
+        self.gesture_binds = Vec::new();
         self.ipc_rx = None;
         self.ipc_timer = std::ptr::null_mut();
         self.startup = Vec::new();
@@ -1262,6 +1264,21 @@ impl WindowManager {
                     }
                 }
             }
+            Action::Reload => {
+                log::info!("monolithic execute_action: Reload requested");
+                if let Some(path) = crate::config::default_config_path() {
+                    match crate::config::parse_config(&path, self) {
+                        Ok(()) => {
+                            self.dirty_windowing();
+                        }
+                        Err(e) => {
+                            log::error!("failed to reload config: {}", e);
+                        }
+                    }
+                } else {
+                    log::error!("no config file found to reload");
+                }
+            }
             Action::Exit => {
                 log::info!("monolithic execute_action: Exit requested");
                 ffi::wl_display_terminate((*self.server).wl_server);