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

commitc50e0c8617a665b78f60c7d34ad340363bce3417
parentb733a82127
authorLucas Galante <[email protected]>
date2026-05-25 20:36
Update system configuration and interface modules

 src/config.rs  |  21 +++++-
 src/ipc.rs     | 119 ++++++++++++++++++++++++++++++++
 src/restart.rs |   1 +
 src/types.rs   |  14 ++++
 src/wayland.rs | 214 ++++++++++++++++++++++++++++++++++++++++++++++-----------
 5 files changed, 325 insertions(+), 44 deletions(-)

diff --git a/src/config.rs b/src/config.rs
index e6d13e6..74a02cc 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -168,6 +168,13 @@ pub struct RepeatConfig {
 pub struct InputConfig {
     #[serde(default)]
     pub tap_to_click: bool,
+    pub accel_speed: Option<f64>,
+    pub accel_profile: Option<String>,
+    pub natural_scroll: Option<bool>,
+    pub dwt: Option<bool>,
+    pub dwtp: Option<bool>,
+    pub trackpoint_accel_speed: Option<f64>,
+    pub trackpoint_accel_profile: Option<String>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -383,6 +390,14 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
     // The tap config will be applied when libinput devices are discovered
     // (in the RiverLibinputDeviceV1 TapSupport event handler).
     state.tap_to_click = config.input.tap_to_click;
+    state.accel_speed = config.input.accel_speed;
+    state.accel_profile = config.input.accel_profile.clone();
+    state.natural_scroll = config.input.natural_scroll;
+    state.dwt = config.input.dwt;
+    state.dwtp = config.input.dwtp;
+    state.trackpoint_accel_speed = config.input.trackpoint_accel_speed;
+    state.trackpoint_accel_profile = config.input.trackpoint_accel_profile.clone();
+    state.tap_config_applied = false;
 
     // [notifications]
     state.notifications_enable = config.notifications.enable;
@@ -647,14 +662,14 @@ once = true
     fn test_reload_entry_format() {
         let toml_str = r#"
 [[reload]]
-exec = "pkill clear-input-manager"
+exec = "pkill clear-input-daemon"
 
 [[reload]]
 exec = "echo reloaded"
 "#;
         let config: Config = toml::from_str(toml_str).expect("TOML parse failed");
         assert_eq!(config.reload.len(), 2);
-        assert_eq!(config.reload[0].exec, "pkill clear-input-manager");
+        assert_eq!(config.reload[0].exec, "pkill clear-input-daemon");
         assert_eq!(config.reload[1].exec, "echo reloaded");
 
         // Also test integration via parse_config (we can write to a temporary file in /tmp or mock it,
@@ -668,7 +683,7 @@ exec = "echo reloaded"
 
         parse_res.unwrap();
         assert_eq!(wm.reload_commands.len(), 2);
-        assert_eq!(wm.reload_commands[0], "pkill clear-input-manager");
+        assert_eq!(wm.reload_commands[0], "pkill clear-input-daemon");
         assert_eq!(wm.reload_commands[1], "echo reloaded");
     }
 }
diff --git a/src/ipc.rs b/src/ipc.rs
index f332f6a..e7126fb 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -568,6 +568,125 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
                 );
             }
         }
+        "accel-speed" | "accel_speed" => {
+            if let Ok(val) = value_str.parse::<f64>() {
+                state.accel_speed = Some(val);
+                state.tap_config_applied = false;
+                if state.notifications_enable {
+                    crate::config::show_notification("clearwm", &format!("Acceleration speed set to {}", val));
+                }
+            }
+        }
+        "accel-profile" | "accel_profile" => {
+            let val = value_str.trim().to_string();
+            if !val.is_empty() {
+                state.accel_profile = Some(val.clone());
+                state.tap_config_applied = false;
+                if state.notifications_enable {
+                    crate::config::show_notification("clearwm", &format!("Acceleration profile set to {}", val));
+                }
+            }
+        }
+        "natural-scroll" | "natural_scroll" => {
+            let old_val = state.natural_scroll;
+            match value_str {
+                "true" | "1" | "enabled" => {
+                    state.natural_scroll = Some(true);
+                }
+                "false" | "0" | "disabled" => {
+                    state.natural_scroll = Some(false);
+                }
+                "toggle" => {
+                    state.natural_scroll = Some(!state.natural_scroll.unwrap_or(false));
+                }
+                _ => {}
+            }
+            if state.natural_scroll != old_val {
+                state.tap_config_applied = false;
+                if state.notifications_enable {
+                    crate::config::show_notification(
+                        "clearwm",
+                        &format!(
+                            "Natural scroll {}",
+                            if state.natural_scroll.unwrap_or(false) { "enabled" } else { "disabled" }
+                        ),
+                    );
+                }
+            }
+        }
+        "dwt" => {
+            let old_val = state.dwt;
+            match value_str {
+                "true" | "1" | "enabled" => {
+                    state.dwt = Some(true);
+                }
+                "false" | "0" | "disabled" => {
+                    state.dwt = Some(false);
+                }
+                "toggle" => {
+                    state.dwt = Some(!state.dwt.unwrap_or(false));
+                }
+                _ => {}
+            }
+            if state.dwt != old_val {
+                state.tap_config_applied = false;
+                if state.notifications_enable {
+                    crate::config::show_notification(
+                        "clearwm",
+                        &format!(
+                            "Disable-while-typing {}",
+                            if state.dwt.unwrap_or(false) { "enabled" } else { "disabled" }
+                        ),
+                    );
+                }
+            }
+        }
+        "dwtp" => {
+            let old_val = state.dwtp;
+            match value_str {
+                "true" | "1" | "enabled" => {
+                    state.dwtp = Some(true);
+                }
+                "false" | "0" | "disabled" => {
+                    state.dwtp = Some(false);
+                }
+                "toggle" => {
+                    state.dwtp = Some(!state.dwtp.unwrap_or(false));
+                }
+                _ => {}
+            }
+            if state.dwtp != old_val {
+                state.tap_config_applied = false;
+                if state.notifications_enable {
+                    crate::config::show_notification(
+                        "clearwm",
+                        &format!(
+                            "Disable-while-trackpointing {}",
+                            if state.dwtp.unwrap_or(false) { "enabled" } else { "disabled" }
+                        ),
+                    );
+                }
+            }
+        }
+        "trackpoint-accel-speed" | "trackpoint_accel_speed" => {
+            if let Ok(val) = value_str.parse::<f64>() {
+                state.trackpoint_accel_speed = Some(val);
+                state.tap_config_applied = false;
+                if state.notifications_enable {
+                    crate::config::show_notification("clearwm", &format!("Trackpoint acceleration speed set to {}", val));
+                }
+            }
+        }
+        "trackpoint-accel-profile" | "trackpoint_accel_profile" => {
+            let val = value_str.trim().to_string();
+            if !val.is_empty() {
+                state.trackpoint_accel_profile = Some(val.clone());
+                state.tap_config_applied = false;
+                if state.notifications_enable {
+                    crate::config::show_notification("clearwm", &format!("Trackpoint acceleration profile set to {}", val));
+                }
+            }
+        }
         _ => {}
     }
 }
diff --git a/src/restart.rs b/src/restart.rs
index 3465266..4a7a5e7 100644
--- a/src/restart.rs
+++ b/src/restart.rs
@@ -202,6 +202,7 @@ pub fn wm_reload(state: &mut WindowManager) {
 
     // Mark status for update after reload
     state.needs_status_update = true;
+    state.tap_config_applied = false;
 
     // Clear mode rules
     state.mode_rules.clear();
diff --git a/src/types.rs b/src/types.rs
index a67427b..898e61c 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -328,6 +328,13 @@ pub struct WindowManager {
     pub state_restore_attempts: u8,
     /// Whether tap-to-click is enabled on touchpad devices
     pub tap_to_click: bool,
+    pub accel_speed: Option<f64>,
+    pub accel_profile: Option<String>,
+    pub natural_scroll: Option<bool>,
+    pub dwt: Option<bool>,
+    pub dwtp: Option<bool>,
+    pub trackpoint_accel_speed: Option<f64>,
+    pub trackpoint_accel_profile: Option<String>,
     /// Whether tap-to-click config has been applied to libinput devices yet
     pub tap_config_applied: bool,
     /// Whether system notifications are enabled
@@ -366,6 +373,13 @@ impl Default for WindowManager {
             needs_state_restore: true,
             state_restore_attempts: 0,
             tap_to_click: false,
+            accel_speed: None,
+            accel_profile: None,
+            natural_scroll: None,
+            dwt: None,
+            dwtp: None,
+            trackpoint_accel_speed: None,
+            trackpoint_accel_profile: None,
             tap_config_applied: false,
             notifications_enable: true,
             reload_commands: Vec::new(),
diff --git a/src/wayland.rs b/src/wayland.rs
index 5422dd2..169ad98 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -159,10 +159,17 @@ pub struct LibinputDeviceInfo {
     pub device: RiverLibinputDeviceV1,
     /// Device name (from the river_input_device_v1 name event)
     pub name: String,
+    pub input_device: Option<RiverInputDeviceV1>,
+    pub name_received: bool,
     /// Number of fingers supported for tap (0 = unsupported)
     pub tap_finger_count: i32,
     /// Whether we've received enough events to apply tap config
     pub tap_info_received: bool,
+    pub accel_profiles_support: Option<u32>,
+    pub natural_scroll_supported: Option<bool>,
+    pub dwt_supported: Option<bool>,
+    pub dwtp_supported: Option<bool>,
+    pub config_applied: bool,
 }
 
 /// Tracked info for a wlr-output-management head.
@@ -678,7 +685,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                         .wm
                         .windows
                         .iter()
-                        .filter(|w| w.is_new && !w.closed && (w.tags & active_tags) != 0 && w.app_id.as_deref() != Some("clear-status-interface") && w.app_id.as_deref() != Some("clear-notifier"))
+                        .filter(|w| w.is_new && !w.closed && (w.tags & active_tags) != 0 && w.app_id.as_deref() != Some("clear-status-interface") && w.app_id.as_deref() != Some("clear-notification-daemon"))
                         .map(|w| w.id)
                         .last();
                     if let Some(new_id) = new_focused_id {
@@ -912,9 +919,9 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                     state.wm.needs_status_update = false;
                 }
 
-                // Re-apply tap-to-click config if it was changed via IPC
+                // Re-apply input config if it was changed via IPC
                 if !state.wm.tap_config_applied && !state.libinput_devices.is_empty() {
-                    crate::wayland::apply_tap_config(state, qhandle);
+                    crate::wayland::apply_input_config(state, qhandle);
                 }
             }
 
@@ -1543,11 +1550,11 @@ impl Dispatch<RiverInputManagerV1, ()> for AppState {
 impl Dispatch<RiverInputDeviceV1, ()> for AppState {
     fn event(
         state: &mut Self,
-        _proxy: &RiverInputDeviceV1,
+        proxy: &RiverInputDeviceV1,
         event: river_input_device_v1::Event,
         _data: &(),
         _conn: &Connection,
-        _qhandle: &QueueHandle<Self>,
+        qhandle: &QueueHandle<Self>,
     ) {
         match event {
             river_input_device_v1::Event::Type { _type: dev_type } => {
@@ -1561,6 +1568,18 @@ impl Dispatch<RiverInputDeviceV1, ()> for AppState {
             }
             river_input_device_v1::Event::Name { name } => {
                 eprintln!("input_device name: {}", name);
+                if let Some(dev) = state.libinput_devices.iter_mut().find(|d| {
+                    if let Some(ref id) = d.input_device {
+                        id.id().protocol_id() == proxy.id().protocol_id()
+                    } else {
+                        false
+                    }
+                }) {
+                    dev.name = name.clone();
+                    dev.name_received = true;
+                    eprintln!("[libinput] associated name \"{}\" with device", name);
+                }
+                crate::wayland::apply_input_config(state, qhandle);
             }
             river_input_device_v1::Event::Removed => {}
             _ => {}
@@ -2593,8 +2612,15 @@ impl Dispatch<RiverLibinputConfigV1, ()> for AppState {
                 state.libinput_devices.push(LibinputDeviceInfo {
                     device,
                     name: String::new(),
+                    input_device: None,
+                    name_received: false,
                     tap_finger_count: -1, // not yet received
                     tap_info_received: false,
+                    accel_profiles_support: None,
+                    natural_scroll_supported: None,
+                    dwt_supported: None,
+                    dwtp_supported: None,
+                    config_applied: false,
                 });
                 state.wm.tap_config_applied = false;
             }
@@ -2616,21 +2642,45 @@ impl Dispatch<RiverLibinputDeviceV1, ()> for AppState {
         qhandle: &QueueHandle<Self>,
     ) {
         match event {
+            river_libinput_device_v1::Event::InputDevice { device } => {
+                if let Some(dev) = state.libinput_devices.iter_mut().find(|d| d.device.id().protocol_id() == proxy.id().protocol_id()) {
+                    dev.input_device = Some(device);
+                }
+            }
             river_libinput_device_v1::Event::TapSupport { finger_count } => {
                 if let Some(dev) = state.libinput_devices.iter_mut().find(|d| d.device.id().protocol_id() == proxy.id().protocol_id()) {
                     dev.tap_finger_count = finger_count;
                     eprintln!("[libinput] tap support: {} fingers", finger_count);
                 }
-                // Apply tap config directly when info is received, in case we are waking up
-                crate::wayland::apply_tap_config(state, qhandle);
+                crate::wayland::apply_input_config(state, qhandle);
+            }
+            river_libinput_device_v1::Event::AccelProfilesSupport { profiles } => {
+                if let Some(dev) = state.libinput_devices.iter_mut().find(|d| d.device.id().protocol_id() == proxy.id().protocol_id()) {
+                    dev.accel_profiles_support = Some(profiles.into());
+                    eprintln!("[libinput] accel profiles support: {:?}", profiles);
+                }
+                crate::wayland::apply_input_config(state, qhandle);
+            }
+            river_libinput_device_v1::Event::NaturalScrollSupport { supported } => {
+                if let Some(dev) = state.libinput_devices.iter_mut().find(|d| d.device.id().protocol_id() == proxy.id().protocol_id()) {
+                    dev.natural_scroll_supported = Some(supported != 0);
+                    eprintln!("[libinput] natural scroll support: {}", supported != 0);
+                }
+                crate::wayland::apply_input_config(state, qhandle);
             }
-            river_libinput_device_v1::Event::TapDefault { state: tap_state } => {
-                let _ = tap_state;
-                eprintln!("[libinput] tap default received");
+            river_libinput_device_v1::Event::DwtSupport { supported } => {
+                if let Some(dev) = state.libinput_devices.iter_mut().find(|d| d.device.id().protocol_id() == proxy.id().protocol_id()) {
+                    dev.dwt_supported = Some(supported != 0);
+                    eprintln!("[libinput] dwt support: {}", supported != 0);
+                }
+                crate::wayland::apply_input_config(state, qhandle);
             }
-            river_libinput_device_v1::Event::TapCurrent { state: tap_state } => {
-                let _ = tap_state;
-                eprintln!("[libinput] tap current received");
+            river_libinput_device_v1::Event::DwtpSupport { supported } => {
+                if let Some(dev) = state.libinput_devices.iter_mut().find(|d| d.device.id().protocol_id() == proxy.id().protocol_id()) {
+                    dev.dwtp_supported = Some(supported != 0);
+                    eprintln!("[libinput] dwtp support: {}", supported != 0);
+                }
+                crate::wayland::apply_input_config(state, qhandle);
             }
             river_libinput_device_v1::Event::Removed => {
                 eprintln!("[libinput] device removed");
@@ -2667,55 +2717,137 @@ impl Dispatch<RiverLibinputResultV1, ()> for AppState {
     }
 }
 
-/// Apply tap-to-click configuration to all libinput devices that support it.
-/// Called after device events arrive and after config changes.
-pub fn apply_tap_config(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
+/// Apply input configuration to all libinput devices that support it.
+pub fn apply_input_config(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
     if state.wm.tap_config_applied {
         return;
     }
 
-    // Wait until ALL discovered devices have received their tap_support event.
-    // Devices arrive one at a time; if we mark tap_config_applied after only
-    // the first device (which may not support tap), we'll miss the touchpad.
-    let all_info_received = state.libinput_devices.iter().all(|d| d.tap_finger_count >= 0);
+    // Wait until ALL discovered devices have received their name and support events.
+    let all_info_received = state.libinput_devices.iter().all(|d| {
+        d.name_received &&
+        d.tap_finger_count >= 0 &&
+        d.accel_profiles_support.is_some() &&
+        d.natural_scroll_supported.is_some() &&
+        d.dwt_supported.is_some() &&
+        d.dwtp_supported.is_some()
+    });
     if !all_info_received {
         return;
     }
 
-    let tap_to_click = state.wm.tap_to_click;
+    // Reset config_applied for all devices to force setting them
+    for dev_info in &mut state.libinput_devices {
+        dev_info.config_applied = false;
+    }
 
     for dev_info in &mut state.libinput_devices {
-        if dev_info.tap_info_received {
-            continue; // Already applied to this device
-        }
-        if dev_info.tap_finger_count == 0 {
-            // Device doesn't support tap-to-click
-            dev_info.tap_info_received = true;
+        if dev_info.config_applied {
             continue;
         }
 
-        // Device supports tap — apply config
-        let tap_state = if tap_to_click {
-            river_libinput_device_v1::TapState::Enabled
+        let is_trackpoint = dev_info.name.to_lowercase().contains("trackpoint");
+
+        // 1. Tap to click
+        if dev_info.tap_finger_count > 0 {
+            let tap_state = if state.wm.tap_to_click {
+                river_libinput_device_v1::TapState::Enabled
+            } else {
+                river_libinput_device_v1::TapState::Disabled
+            };
+            eprintln!(
+                "[libinput] setting tap={} on device ({})",
+                if state.wm.tap_to_click { "enabled" } else { "disabled" },
+                &dev_info.name
+            );
+            dev_info.device.set_tap(tap_state, qhandle, ());
+        }
+
+        // 2. Accel speed
+        let speed = if is_trackpoint {
+            state.wm.trackpoint_accel_speed.or(state.wm.accel_speed)
         } else {
-            river_libinput_device_v1::TapState::Disabled
+            state.wm.accel_speed
         };
+        if let Some(s) = speed {
+            let s = s.clamp(-1.0, 1.0);
+            let speed_bytes = s.to_ne_bytes().to_vec();
+            eprintln!("[libinput] setting accel_speed={} on device ({})", s, &dev_info.name);
+            dev_info.device.set_accel_speed(speed_bytes, qhandle, ());
+        }
 
-        eprintln!(
-            "[libinput] setting tap={} on device ({})",
-            if tap_to_click { "enabled" } else { "disabled" },
-            if dev_info.name.is_empty() { "unnamed" } else { &dev_info.name }
-        );
+        // 3. Accel profile
+        let profile_str = if is_trackpoint {
+            state.wm.trackpoint_accel_profile.as_ref().or(state.wm.accel_profile.as_ref())
+        } else {
+            state.wm.accel_profile.as_ref()
+        };
+        if let Some(profile_name) = profile_str {
+            let profile = match profile_name.as_str() {
+                "flat" => Some(river_libinput_device_v1::AccelProfile::Flat),
+                "adaptive" => Some(river_libinput_device_v1::AccelProfile::Adaptive),
+                "none" => Some(river_libinput_device_v1::AccelProfile::None),
+                "custom" => Some(river_libinput_device_v1::AccelProfile::Custom),
+                _ => None,
+            };
+            if let Some(p) = profile {
+                eprintln!("[libinput] setting accel_profile={:?} on device ({})", p, &dev_info.name);
+                dev_info.device.set_accel_profile(p, qhandle, ());
+            }
+        }
+
+        // 4. Natural scroll
+        if let Some(supported) = dev_info.natural_scroll_supported {
+            if supported {
+                if let Some(natural) = state.wm.natural_scroll {
+                    let ns_state = if natural {
+                        river_libinput_device_v1::NaturalScrollState::Enabled
+                    } else {
+                        river_libinput_device_v1::NaturalScrollState::Disabled
+                    };
+                    eprintln!("[libinput] setting natural_scroll={} on device ({})", natural, &dev_info.name);
+                    dev_info.device.set_natural_scroll(ns_state, qhandle, ());
+                }
+            }
+        }
+
+        // 5. Disable while typing (dwt)
+        if let Some(supported) = dev_info.dwt_supported {
+            if supported {
+                if let Some(dwt) = state.wm.dwt {
+                    let dwt_state = if dwt {
+                        river_libinput_device_v1::DwtState::Enabled
+                    } else {
+                        river_libinput_device_v1::DwtState::Disabled
+                    };
+                    eprintln!("[libinput] setting dwt={} on device ({})", dwt, &dev_info.name);
+                    dev_info.device.set_dwt(dwt_state, qhandle, ());
+                }
+            }
+        }
+
+        // 6. Disable while trackpointing (dwtp)
+        if let Some(supported) = dev_info.dwtp_supported {
+            if supported {
+                if let Some(dwtp) = state.wm.dwtp {
+                    let dwtp_state = if dwtp {
+                        river_libinput_device_v1::DwtpState::Enabled
+                    } else {
+                        river_libinput_device_v1::DwtpState::Disabled
+                    };
+                    eprintln!("[libinput] setting dwtp={} on device ({})", dwtp, &dev_info.name);
+                    dev_info.device.set_dwtp(dwtp_state, qhandle, ());
+                }
+            }
+        }
 
-        dev_info.device.set_tap(tap_state, qhandle, ());
-        dev_info.tap_info_received = true;
+        dev_info.config_applied = true;
     }
 
-    // Only mark as fully applied once all devices have been configured
-    let all_done = state.libinput_devices.iter().all(|d| d.tap_info_received);
+    let all_done = state.libinput_devices.iter().all(|d| d.config_applied);
     if all_done && !state.libinput_devices.is_empty() {
         state.wm.tap_config_applied = true;
-        eprintln!("[libinput] tap config applied to all devices");
+        eprintln!("[libinput] all input configurations applied to all devices");
     }
 }