Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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");
}
}