Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Update system configuration and interface modules
Cargo.lock | 151 +++++++++++
Cargo.toml | 2 +
src/config.rs | 48 ++++
src/input.rs | 728 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ipc.rs | 15 ++
src/ipc_server.rs | 4 +-
src/lib.rs | 2 +
src/main.rs | 20 +-
src/types.rs | 4 +
src/wayland.rs | 24 +-
10 files changed, 995 insertions(+), 3 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bfd1f8f..8216187 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,12 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
[[package]]
name = "cc"
version = "1.2.62"
@@ -38,6 +44,8 @@ dependencies = [
"libc",
"nix",
"serde",
+ "serde_json",
+ "tokio",
"toml",
"wayland-backend",
"wayland-client",
@@ -90,6 +98,12 @@ dependencies = [
"hashbrown",
]
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
[[package]]
name = "libc"
version = "0.2.186"
@@ -102,6 +116,15 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
[[package]]
name = "memchr"
version = "2.8.0"
@@ -117,6 +140,17 @@ dependencies = [
"libc",
]
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
[[package]]
name = "nix"
version = "0.29.0"
@@ -129,6 +163,35 @@ dependencies = [
"libc",
]
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -162,6 +225,15 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
[[package]]
name = "rustix"
version = "1.1.4"
@@ -175,6 +247,12 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -205,6 +283,19 @@ dependencies = [
"syn",
]
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
[[package]]
name = "serde_spanned"
version = "0.6.9"
@@ -220,12 +311,32 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
[[package]]
name = "syn"
version = "2.0.117"
@@ -237,6 +348,34 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "toml"
version = "0.8.23"
@@ -284,6 +423,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
[[package]]
name = "wayland-backend"
version = "0.3.15"
@@ -381,3 +526,9 @@ name = "xkeysym"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
index b453b40..89449af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,3 +22,5 @@ serde = { version = "1", features = ["derive"] }
nix = { version = "0.29", features = ["signal", "process", "fs", "poll"] }
libc = "0.2"
bitflags = "2"
+tokio = { version = "1.35", features = ["full"] }
+serde_json = "1.0"
diff --git a/src/config.rs b/src/config.rs
index 74a02cc..4f30e19 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -35,6 +35,8 @@ pub struct Config {
pub notifications: NotificationsConfig,
#[serde(default)]
pub reload: Vec<ReloadEntryConfig>,
+ #[serde(default)]
+ pub inertial: Option<InertialConfig>,
}
#[derive(Debug, Deserialize)]
@@ -177,6 +179,34 @@ pub struct InputConfig {
pub trackpoint_accel_profile: Option<String>,
}
+#[derive(Debug, Deserialize, Clone)]
+pub struct InertialConfig {
+ #[serde(default = "default_true")]
+ pub inertial_scroll: bool,
+ #[serde(default = "default_scroll_friction")]
+ pub scroll_friction: u16,
+ #[serde(default = "default_false")]
+ pub inertial_pointer: bool,
+ #[serde(default = "default_pointer_friction")]
+ pub pointer_friction: u16,
+ #[serde(default = "default_false")]
+ pub inertial_trackpad: bool,
+ #[serde(default = "default_trackpad_friction")]
+ pub trackpad_friction: u16,
+ #[serde(default = "default_speed")]
+ pub pointer_speed: f64,
+ #[serde(default = "default_speed")]
+ pub scroll_speed: f64,
+ #[serde(default = "default_speed")]
+ pub trackpad_speed: f64,
+}
+
+fn default_scroll_friction() -> u16 { 90 }
+fn default_pointer_friction() -> u16 { 95 }
+fn default_trackpad_friction() -> u16 { 95 }
+fn default_speed() -> f64 { 1.0 }
+fn default_false() -> bool { false }
+
#[derive(Debug, Deserialize)]
pub struct NotificationsConfig {
#[serde(default = "default_true")]
@@ -399,6 +429,24 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
state.trackpoint_accel_profile = config.input.trackpoint_accel_profile.clone();
state.tap_config_applied = false;
+ // Send InertialConfig and tap_to_click state to input subsystem daemon
+ let inertial_cfg = config.inertial.clone().unwrap_or_else(|| {
+ InertialConfig {
+ inertial_scroll: true,
+ scroll_friction: 90,
+ inertial_pointer: false,
+ pointer_friction: 95,
+ inertial_trackpad: false,
+ trackpad_friction: 95,
+ pointer_speed: 1.0,
+ scroll_speed: 1.0,
+ trackpad_speed: 1.0,
+ }
+ });
+ if let Some(ref controller) = state.input_controller {
+ let _ = controller.send((inertial_cfg, config.input.tap_to_click));
+ }
+
// [notifications]
state.notifications_enable = config.notifications.enable;
diff --git a/src/input.rs b/src/input.rs
new file mode 100644
index 0000000..7029458
--- /dev/null
+++ b/src/input.rs
@@ -0,0 +1,728 @@
+use std::collections::HashSet;
+use std::fs::{self, OpenOptions};
+use std::os::unix::fs::OpenOptionsExt;
+use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
+use std::time::Instant;
+use tokio::io::AsyncReadExt;
+use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
+use tokio::time::{sleep, Duration};
+use crate::config::InertialConfig;
+
+// IOCTL and Event constants
+const UI_DEV_CREATE: libc::c_ulong = 0x5501;
+const UI_DEV_SETUP: libc::c_ulong = 0x405C5503;
+const UI_SET_EVBIT: libc::c_ulong = 0x40045564;
+const UI_SET_RELBIT: libc::c_ulong = 0x40045566;
+
+// Linux input event codes
+const EV_SYN: u16 = 0x00;
+const EV_KEY: u16 = 0x01;
+const EV_REL: u16 = 0x02;
+const EV_ABS: u16 = 0x03;
+
+const REL_X: u16 = 0x00;
+const REL_Y: u16 = 0x01;
+const REL_HWHEEL: u16 = 0x06;
+const REL_WHEEL: u16 = 0x08;
+
+const ABS_X: u16 = 0x00;
+const ABS_Y: u16 = 0x01;
+const ABS_MT_POSITION_X: u16 = 0x35;
+const ABS_MT_POSITION_Y: u16 = 0x36;
+const ABS_MT_TRACKING_ID: u16 = 0x39;
+
+const BTN_TOUCH: u16 = 0x14a;
+
+const SYN_REPORT: u16 = 0x00;
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+struct InputId {
+ bustype: u16,
+ vendor: u16,
+ product: u16,
+ version: u16,
+}
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+struct UinputSetup {
+ id: InputId,
+ name: [u8; 80],
+ ff_effects_max: u32,
+}
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+struct InputEvent {
+ tv_sec: i64,
+ tv_usec: i64,
+ type_: u16,
+ code: u16,
+ value: i32,
+}
+
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct FingerState {
+ pub slot: usize,
+ pub x: f32,
+ pub y: f32,
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+struct SlotState {
+ active: bool,
+ x: f32,
+ y: f32,
+}
+
+fn eviocgabs(abs: u32) -> libc::c_ulong {
+ let size = std::mem::size_of::<libc::input_absinfo>() as u64;
+ ((2u64 << 30) | ((b'E' as u64) << 8) | ((0x40 + abs) as u64) | (size << 16)) as libc::c_ulong
+}
+
+const ABS_MT_SLOT: u16 = 0x2f;
+
+#[derive(Debug)]
+enum CoordinatorMsg {
+ PhysicalMove { dx: i32, dy: i32, timestamp: Instant },
+ PhysicalTrackpadMove { dx: i32, dy: i32, timestamp: Instant },
+ PhysicalTrackpadLift { timestamp: Instant },
+ PhysicalScroll { dwx: i32, dwy: i32, timestamp: Instant },
+ UpdateConfig((InertialConfig, bool)),
+ FingersReport(Vec<FingerState>),
+}
+
+fn setup_uinput() -> std::io::Result<std::fs::File> {
+ let file = OpenOptions::new()
+ .write(true)
+ .custom_flags(libc::O_NONBLOCK)
+ .open("/dev/uinput")?;
+
+ let fd = file.as_raw_fd();
+
+ unsafe {
+ if libc::ioctl(fd, UI_SET_EVBIT, EV_REL as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ if libc::ioctl(fd, UI_SET_RELBIT, REL_X as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ if libc::ioctl(fd, UI_SET_RELBIT, REL_Y as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ if libc::ioctl(fd, UI_SET_RELBIT, REL_WHEEL as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ if libc::ioctl(fd, UI_SET_RELBIT, REL_HWHEEL as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ }
+
+ let mut setup = UinputSetup {
+ id: InputId {
+ bustype: 0x0006, // BUS_VIRTUAL
+ vendor: 0x1234,
+ product: 0x5678,
+ version: 1,
+ },
+ name: [0; 80],
+ ff_effects_max: 0,
+ };
+
+ let name_bytes = b"Clear Virtual Mouse";
+ setup.name[..name_bytes.len()].copy_from_slice(name_bytes);
+
+ unsafe {
+ let setup_ptr = &setup as *const UinputSetup as *const libc::c_void;
+ if libc::ioctl(fd, UI_DEV_SETUP, setup_ptr) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ if libc::ioctl(fd, UI_DEV_CREATE) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ }
+
+ println!("[input-subsystem] Successfully created virtual uinput device.");
+ Ok(file)
+}
+
+fn write_raw_event(file: &mut std::fs::File, type_: u16, code: u16, value: i32) -> std::io::Result<()> {
+ let event = InputEvent {
+ tv_sec: 0,
+ tv_usec: 0,
+ type_,
+ code,
+ value,
+ };
+ let buf = unsafe {
+ std::slice::from_raw_parts(&event as *const InputEvent as *const u8, std::mem::size_of::<InputEvent>())
+ };
+ use std::io::Write;
+ file.write_all(buf)?;
+ file.flush()?;
+ Ok(())
+}
+
+fn write_mouse_move(file: &mut std::fs::File, dx: i32, dy: i32) -> std::io::Result<()> {
+ if dx != 0 {
+ write_raw_event(file, EV_REL, REL_X, dx)?;
+ }
+ if dy != 0 {
+ write_raw_event(file, EV_REL, REL_Y, dy)?;
+ }
+ write_raw_event(file, EV_SYN, SYN_REPORT, 0)?;
+ Ok(())
+}
+
+fn write_scroll(file: &mut std::fs::File, dwx: i32, dwy: i32) -> std::io::Result<()> {
+ if dwx != 0 {
+ write_raw_event(file, EV_REL, REL_HWHEEL, dwx)?;
+ }
+ if dwy != 0 {
+ write_raw_event(file, EV_REL, REL_WHEEL, dwy)?;
+ }
+ write_raw_event(file, EV_SYN, SYN_REPORT, 0)?;
+ Ok(())
+}
+
+struct TouchTracker {
+ last_x: Option<i32>,
+ last_y: Option<i32>,
+ dx: i32,
+ dy: i32,
+ finger_down: bool,
+}
+
+async fn read_device_loop(
+ path: PathBuf,
+ tx: UnboundedSender<CoordinatorMsg>,
+) -> std::io::Result<()> {
+ println!("[input-subsystem] Started monitoring {:?}", path);
+ let mut file = tokio::fs::File::open(&path).await?;
+ let fd = file.as_raw_fd();
+
+ let mut min_x = 0;
+ let mut max_x = 3528;
+ let mut min_y = 0;
+ let mut max_y = 2006;
+
+ let mut absinfo = libc::input_absinfo {
+ value: 0, minimum: 0, maximum: 0, fuzz: 0, flat: 0, resolution: 0
+ };
+ unsafe {
+ if libc::ioctl(fd, eviocgabs(ABS_MT_POSITION_X as u32), &mut absinfo) >= 0 {
+ min_x = absinfo.minimum;
+ max_x = absinfo.maximum;
+ } else if libc::ioctl(fd, eviocgabs(ABS_X as u32), &mut absinfo) >= 0 {
+ min_x = absinfo.minimum;
+ max_x = absinfo.maximum;
+ }
+ if libc::ioctl(fd, eviocgabs(ABS_MT_POSITION_Y as u32), &mut absinfo) >= 0 {
+ min_y = absinfo.minimum;
+ max_y = absinfo.maximum;
+ } else if libc::ioctl(fd, eviocgabs(ABS_Y as u32), &mut absinfo) >= 0 {
+ min_y = absinfo.minimum;
+ max_y = absinfo.maximum;
+ }
+ }
+
+ let range_x = (max_x - min_x).max(1) as f32;
+ let range_y = (max_y - min_y).max(1) as f32;
+
+ let mut slots = [SlotState::default(); 5];
+ let mut current_slot = 0usize;
+ let mut buf = [0u8; 24];
+
+ let mut touch = TouchTracker {
+ last_x: None,
+ last_y: None,
+ dx: 0,
+ dy: 0,
+ finger_down: false,
+ };
+
+ loop {
+ file.read_exact(&mut buf).await?;
+ let event: InputEvent = unsafe { std::ptr::read_unaligned(buf.as_ptr() as *const InputEvent) };
+ let now = Instant::now();
+
+ if event.type_ == EV_REL {
+ if event.code == REL_X {
+ let _ = tx.send(CoordinatorMsg::PhysicalMove { dx: event.value, dy: 0, timestamp: now });
+ } else if event.code == REL_Y {
+ let _ = tx.send(CoordinatorMsg::PhysicalMove { dx: 0, dy: event.value, timestamp: now });
+ } else if event.code == REL_WHEEL {
+ let _ = tx.send(CoordinatorMsg::PhysicalScroll { dwx: 0, dwy: event.value, timestamp: now });
+ } else if event.code == REL_HWHEEL {
+ let _ = tx.send(CoordinatorMsg::PhysicalScroll { dwx: event.value, dwy: 0, timestamp: now });
+ }
+ } else if event.type_ == EV_ABS {
+ if event.code == ABS_X || event.code == ABS_MT_POSITION_X {
+ let val = event.value;
+ if let Some(lx) = touch.last_x {
+ touch.dx += val - lx;
+ }
+ touch.last_x = Some(val);
+ slots[current_slot].x = (val - min_x) as f32 / range_x;
+ } else if event.code == ABS_Y || event.code == ABS_MT_POSITION_Y {
+ let val = event.value;
+ if let Some(ly) = touch.last_y {
+ touch.dy += val - ly;
+ }
+ touch.last_y = Some(val);
+ slots[current_slot].y = (val - min_y) as f32 / range_y;
+ } else if event.code == ABS_MT_TRACKING_ID {
+ if event.value >= 0 {
+ touch.finger_down = true;
+ slots[current_slot].active = true;
+ } else {
+ touch.finger_down = false;
+ touch.last_x = None;
+ touch.last_y = None;
+ slots[current_slot].active = false;
+ }
+ } else if event.code == ABS_MT_SLOT {
+ current_slot = (event.value as usize).min(4);
+ }
+ } else if event.type_ == EV_KEY {
+ if event.code == BTN_TOUCH {
+ if event.value == 1 {
+ touch.finger_down = true;
+ slots[0].active = true;
+ } else {
+ touch.finger_down = false;
+ touch.last_x = None;
+ touch.last_y = None;
+ for s in &mut slots {
+ s.active = false;
+ }
+ }
+ }
+ } else if event.type_ == EV_SYN {
+ if event.code == SYN_REPORT {
+ let active_fingers: Vec<FingerState> = slots.iter().enumerate()
+ .filter(|(_, slot)| slot.active)
+ .map(|(i, slot)| FingerState { slot: i, x: slot.x, y: slot.y })
+ .collect();
+ let _ = tx.send(CoordinatorMsg::FingersReport(active_fingers));
+
+ if touch.finger_down {
+ if touch.dx != 0 || touch.dy != 0 {
+ let scaled_dx = (touch.dx as f32 * 0.15) as i32;
+ let scaled_dy = (touch.dy as f32 * 0.15) as i32;
+ if scaled_dx != 0 || scaled_dy != 0 {
+ let _ = tx.send(CoordinatorMsg::PhysicalTrackpadMove { dx: scaled_dx, dy: scaled_dy, timestamp: now });
+ }
+ touch.dx = 0;
+ touch.dy = 0;
+ }
+ } else {
+ let _ = tx.send(CoordinatorMsg::PhysicalTrackpadLift { timestamp: now });
+ touch.dx = 0;
+ touch.dy = 0;
+ touch.last_x = None;
+ touch.last_y = None;
+ }
+ }
+ }
+ }
+}
+
+struct PhysicsState {
+ config: InertialConfig,
+
+ last_move_time: Instant,
+ vel_x: f32,
+ vel_y: f32,
+ anim_vel_x: f32,
+ anim_vel_y: f32,
+ is_animating_pointer: bool,
+ accum_x: f32,
+ accum_y: f32,
+
+ last_trackpad_move_time: Instant,
+ trackpad_vel_x: f32,
+ trackpad_vel_y: f32,
+ anim_trackpad_vel_x: f32,
+ anim_trackpad_vel_y: f32,
+ is_animating_trackpad: bool,
+ accum_trackpad_x: f32,
+ accum_trackpad_y: f32,
+ last_physical_scroll_time: Instant,
+
+ last_scroll_time: Instant,
+ scroll_vel_x: f32,
+ scroll_vel_y: f32,
+ anim_scroll_vel_x: f32,
+ anim_scroll_vel_y: f32,
+ is_animating_scroll: bool,
+ accum_scroll_x: f32,
+ accum_scroll_y: f32,
+
+ tap_to_click: bool,
+ trackpad_disabled_by_scroll: bool,
+}
+
+fn trigger_tap_to_click_ipc(tap: bool, ipc_tx: &std::sync::mpsc::Sender<String>, pipe_write: libc::c_int) {
+ let cmd = format!("input tap-to-click {}", tap);
+ let _ = ipc_tx.send(cmd);
+ unsafe {
+ libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
+ }
+}
+
+fn trigger_trackpad_disabled_ipc(disabled: bool, ipc_tx: &std::sync::mpsc::Sender<String>, pipe_write: libc::c_int) {
+ let cmd = format!("input trackpad-disabled {}", disabled);
+ let _ = ipc_tx.send(cmd);
+ unsafe {
+ libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
+ }
+}
+
+pub fn run_input_daemon(
+ mut event_queue_rx: tokio::sync::mpsc::UnboundedReceiver<(InertialConfig, bool)>,
+ ipc_tx: std::sync::mpsc::Sender<String>,
+ pipe_write: libc::c_int,
+) -> Result<(), Box<dyn std::error::Error>> {
+ let rt = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()?;
+
+ let res: Result<(), Box<dyn std::error::Error>> = rt.block_on(async move {
+ println!("[input-subsystem] Starting Clear Input Subsystem...");
+
+ let mut uinput_file = match setup_uinput() {
+ Ok(f) => f,
+ Err(e) => {
+ eprintln!("[input-subsystem] FATAL: Could not initialize /dev/uinput: {}.", e);
+ return Err(Box::new(e) as Box<dyn std::error::Error>);
+ }
+ };
+
+ let (tx, mut rx): (UnboundedSender<CoordinatorMsg>, UnboundedReceiver<CoordinatorMsg>) = unbounded_channel();
+
+ // Broadcast coordinate socket setup
+ let (broadcast_tx, _) = tokio::sync::broadcast::channel::<String>(32);
+ let socket_path = "/tmp/clear-input-coords.sock";
+ let _ = fs::remove_file(socket_path);
+ let listener = tokio::net::UnixListener::bind(socket_path)?;
+ let b_tx = broadcast_tx.clone();
+ tokio::spawn(async move {
+ loop {
+ if let Ok((mut stream, _)) = listener.accept().await {
+ let mut rx = b_tx.subscribe();
+ tokio::spawn(async move {
+ use tokio::io::AsyncWriteExt;
+ while let Ok(msg) = rx.recv().await {
+ if stream.write_all(format!("{}\n", msg).as_bytes()).await.is_err() {
+ break;
+ }
+ }
+ });
+ }
+ }
+ });
+
+ // Wait for initial config
+ let (initial_config, tap_to_click) = match event_queue_rx.recv().await {
+ Some(res) => res,
+ None => {
+ return Err(Box::from("Failed to receive initial input configuration") as Box<dyn std::error::Error>);
+ }
+ };
+
+ // Config listener task
+ let tx_config = tx.clone();
+ tokio::spawn(async move {
+ while let Some((config, tap)) = event_queue_rx.recv().await {
+ let _ = tx_config.send(CoordinatorMsg::UpdateConfig((config, tap)));
+ }
+ });
+
+ // Event device discovery loop
+ let tx_devices = tx.clone();
+ tokio::spawn(async move {
+ let mut active_devices = HashSet::new();
+ loop {
+ if let Ok(entries) = fs::read_dir("/dev/input/by-path") {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
+ if name.ends_with("-event-mouse") {
+ if let Ok(canonical) = fs::canonicalize(&path) {
+ if !active_devices.contains(&canonical) {
+ active_devices.insert(canonical.clone());
+ let tx_c = tx_devices.clone();
+ let device_path = canonical.clone();
+ tokio::spawn(async move {
+ if let Err(e) = read_device_loop(device_path.clone(), tx_c).await {
+ eprintln!("[input-subsystem] Reader loop exited for {:?}: {}", device_path, e);
+ }
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+ sleep(Duration::from_secs(5)).await;
+ }
+ });
+
+ // Central coordinator and physics ticks
+ trigger_tap_to_click_ipc(tap_to_click, &ipc_tx, pipe_write);
+
+ let mut state = PhysicsState {
+ config: initial_config,
+ last_move_time: Instant::now(),
+ vel_x: 0.0,
+ vel_y: 0.0,
+ anim_vel_x: 0.0,
+ anim_vel_y: 0.0,
+ is_animating_pointer: false,
+ accum_x: 0.0,
+ accum_y: 0.0,
+ last_trackpad_move_time: Instant::now(),
+ trackpad_vel_x: 0.0,
+ trackpad_vel_y: 0.0,
+ anim_trackpad_vel_x: 0.0,
+ anim_trackpad_vel_y: 0.0,
+ is_animating_trackpad: false,
+ accum_trackpad_x: 0.0,
+ accum_trackpad_y: 0.0,
+ last_physical_scroll_time: Instant::now(),
+ last_scroll_time: Instant::now(),
+ scroll_vel_x: 0.0,
+ scroll_vel_y: 0.0,
+ anim_scroll_vel_x: 0.0,
+ anim_scroll_vel_y: 0.0,
+ is_animating_scroll: false,
+ accum_scroll_x: 0.0,
+ accum_scroll_y: 0.0,
+ tap_to_click,
+ trackpad_disabled_by_scroll: false,
+ };
+
+ let mut tick_interval = tokio::time::interval(Duration::from_millis(16)); // ~60fps
+
+ loop {
+ tokio::select! {
+ Some(msg) = rx.recv() => {
+ match msg {
+ CoordinatorMsg::UpdateConfig((cfg, tap)) => {
+ state.config = cfg;
+ if state.tap_to_click != tap {
+ state.tap_to_click = tap;
+ trigger_tap_to_click_ipc(tap, &ipc_tx, pipe_write);
+ }
+ }
+ CoordinatorMsg::PhysicalMove { dx, dy, timestamp } => {
+ let dt = timestamp.duration_since(state.last_move_time).as_secs_f32();
+ state.last_move_time = timestamp;
+
+ state.is_animating_pointer = false;
+ state.anim_vel_x = 0.0;
+ state.anim_vel_y = 0.0;
+
+ if dt > 0.1 {
+ state.vel_x = 0.0;
+ state.vel_y = 0.0;
+ } else if dt > 0.001 {
+ let inst_vx = dx as f32 / dt;
+ let inst_vy = dy as f32 / dt;
+ let alpha = 0.35;
+ state.vel_x = alpha * inst_vx + (1.0 - alpha) * state.vel_x;
+ state.vel_y = alpha * inst_vy + (1.0 - alpha) * state.vel_y;
+ }
+ }
+ CoordinatorMsg::PhysicalTrackpadMove { dx, dy, timestamp } => {
+ let dt = timestamp.duration_since(state.last_trackpad_move_time).as_secs_f32();
+ state.last_trackpad_move_time = timestamp;
+
+ state.is_animating_trackpad = false;
+ state.anim_trackpad_vel_x = 0.0;
+ state.anim_trackpad_vel_y = 0.0;
+
+ if dt > 0.1 {
+ state.trackpad_vel_x = 0.0;
+ state.trackpad_vel_y = 0.0;
+ } else if dt > 0.001 {
+ let inst_vx = dx as f32 / dt;
+ let inst_vy = dy as f32 / dt;
+ let alpha = 0.35;
+ state.trackpad_vel_x = alpha * inst_vx + (1.0 - alpha) * state.trackpad_vel_x;
+ state.trackpad_vel_y = alpha * inst_vy + (1.0 - alpha) * state.trackpad_vel_y;
+ }
+ }
+ CoordinatorMsg::PhysicalTrackpadLift { timestamp } => {
+ if state.trackpad_disabled_by_scroll {
+ state.trackpad_disabled_by_scroll = false;
+ trigger_trackpad_disabled_ipc(false, &ipc_tx, pipe_write);
+ }
+ let time_since_scroll = timestamp.duration_since(state.last_physical_scroll_time).as_secs_f32();
+ if time_since_scroll > 0.15 {
+ if state.config.inertial_trackpad && !state.is_animating_trackpad {
+ let time_since_last = timestamp.duration_since(state.last_trackpad_move_time).as_secs_f32();
+ if time_since_last < 0.05 {
+ let speed = (state.trackpad_vel_x * state.trackpad_vel_x + state.trackpad_vel_y * state.trackpad_vel_y).sqrt();
+ if speed > 150.0 {
+ state.is_animating_trackpad = true;
+ state.anim_trackpad_vel_x = state.trackpad_vel_x;
+ state.anim_trackpad_vel_y = state.trackpad_vel_y;
+ state.accum_trackpad_x = 0.0;
+ state.accum_trackpad_y = 0.0;
+ }
+ }
+ }
+ } else {
+ state.trackpad_vel_x = 0.0;
+ state.trackpad_vel_y = 0.0;
+ }
+ }
+ CoordinatorMsg::PhysicalScroll { dwx, dwy, timestamp } => {
+ if !state.trackpad_disabled_by_scroll {
+ state.trackpad_disabled_by_scroll = true;
+ trigger_trackpad_disabled_ipc(true, &ipc_tx, pipe_write);
+ }
+ let dt = timestamp.duration_since(state.last_scroll_time).as_secs_f32();
+ state.last_scroll_time = timestamp;
+ state.last_physical_scroll_time = timestamp;
+
+ state.is_animating_scroll = false;
+ state.anim_scroll_vel_x = 0.0;
+ state.anim_scroll_vel_y = 0.0;
+
+ if dt > 0.15 {
+ state.scroll_vel_x = 0.0;
+ state.scroll_vel_y = 0.0;
+ } else if dt > 0.001 {
+ let inst_vx = dwx as f32 / dt;
+ let inst_vy = dwy as f32 / dt;
+ let alpha = 0.45;
+ state.scroll_vel_x = alpha * inst_vx + (1.0 - alpha) * state.scroll_vel_x;
+ state.scroll_vel_y = alpha * inst_vy + (1.0 - alpha) * state.scroll_vel_y;
+ }
+ }
+ CoordinatorMsg::FingersReport(fingers) => {
+ if fingers.is_empty() && state.trackpad_disabled_by_scroll {
+ state.trackpad_disabled_by_scroll = false;
+ trigger_trackpad_disabled_ipc(false, &ipc_tx, pipe_write);
+ }
+ if let Ok(serialized) = serde_json::to_string(&fingers) {
+ let _ = broadcast_tx.send(serialized);
+ }
+ }
+ }
+ }
+ _ = tick_interval.tick() => {
+ let now = Instant::now();
+ let tick_dt = 0.016;
+
+ // --- Pointer physics tick ---
+ if state.config.inertial_pointer {
+ if !state.is_animating_pointer {
+ let time_since_last = now.duration_since(state.last_move_time).as_secs_f32();
+ if time_since_last >= 0.02 && time_since_last < 0.1 {
+ let speed = (state.vel_x * state.vel_x + state.vel_y * state.vel_y).sqrt();
+ if speed > 150.0 {
+ state.is_animating_pointer = true;
+ state.anim_vel_x = state.vel_x;
+ state.anim_vel_y = state.vel_y;
+ state.accum_x = 0.0;
+ state.accum_y = 0.0;
+ }
+ }
+ } else {
+ let friction_coef = state.config.pointer_friction as f32 / 100.0;
+ state.anim_vel_x *= friction_coef;
+ state.anim_vel_y *= friction_coef;
+
+ let speed = (state.anim_vel_x * state.anim_vel_x + state.anim_vel_y * state.anim_vel_y).sqrt();
+ if speed < 12.0 {
+ state.is_animating_pointer = false;
+ } else {
+ state.accum_x += state.anim_vel_x * tick_dt * state.config.pointer_speed as f32;
+ state.accum_y += state.anim_vel_y * tick_dt * state.config.pointer_speed as f32;
+
+ let steps_x = state.accum_x.trunc() as i32;
+ let steps_y = state.accum_y.trunc() as i32;
+ state.accum_x -= steps_x as f32;
+ state.accum_y -= steps_y as f32;
+
+ if steps_x != 0 || steps_y != 0 {
+ let _ = write_mouse_move(&mut uinput_file, steps_x, steps_y);
+ }
+ }
+ }
+ }
+
+ // --- Trackpad physics tick ---
+ if state.config.inertial_trackpad && state.is_animating_trackpad {
+ let friction_coef = state.config.trackpad_friction as f32 / 100.0;
+ state.anim_trackpad_vel_x *= friction_coef;
+ state.anim_trackpad_vel_y *= friction_coef;
+
+ let speed = (state.anim_trackpad_vel_x * state.anim_trackpad_vel_x + state.anim_trackpad_vel_y * state.anim_trackpad_vel_y).sqrt();
+ if speed < 12.0 {
+ state.is_animating_trackpad = false;
+ } else {
+ state.accum_trackpad_x += state.anim_trackpad_vel_x * tick_dt * state.config.trackpad_speed as f32;
+ state.accum_trackpad_y += state.anim_trackpad_vel_y * tick_dt * state.config.trackpad_speed as f32;
+
+ let steps_x = state.accum_trackpad_x.trunc() as i32;
+ let steps_y = state.accum_trackpad_y.trunc() as i32;
+ state.accum_trackpad_x -= steps_x as f32;
+ state.accum_trackpad_y -= steps_y as f32;
+
+ if steps_x != 0 || steps_y != 0 {
+ let _ = write_mouse_move(&mut uinput_file, steps_x, steps_y);
+ }
+ }
+ }
+
+ // --- Scroll physics tick ---
+ if state.config.inertial_scroll {
+ if !state.is_animating_scroll {
+ let time_since_last = now.duration_since(state.last_scroll_time).as_secs_f32();
+ if time_since_last >= 0.03 && time_since_last < 0.15 {
+ let speed = (state.scroll_vel_x * state.scroll_vel_x + state.scroll_vel_y * state.scroll_vel_y).sqrt();
+ if speed > 5.0 {
+ state.is_animating_scroll = true;
+ state.anim_scroll_vel_x = state.scroll_vel_x;
+ state.anim_scroll_vel_y = state.scroll_vel_y;
+ state.accum_scroll_x = 0.0;
+ state.accum_scroll_y = 0.0;
+ }
+ }
+ } else {
+ let friction_coef = state.config.scroll_friction as f32 / 100.0;
+ state.anim_scroll_vel_x *= friction_coef;
+ state.anim_scroll_vel_y *= friction_coef;
+
+ let speed = (state.anim_scroll_vel_x * state.anim_scroll_vel_x + state.anim_scroll_vel_y * state.anim_scroll_vel_y).sqrt();
+ if speed < 0.5 {
+ state.is_animating_scroll = false;
+ } else {
+ state.accum_scroll_x += state.anim_scroll_vel_x * tick_dt * state.config.scroll_speed as f32;
+ state.accum_scroll_y += state.anim_scroll_vel_y * tick_dt * state.config.scroll_speed as f32;
+
+ let steps_x = state.accum_scroll_x.trunc() as i32;
+ let steps_y = state.accum_scroll_y.trunc() as i32;
+ state.accum_scroll_x -= steps_x as f32;
+ state.accum_scroll_y -= steps_y as f32;
+
+ if steps_x != 0 || steps_y != 0 {
+ let _ = write_scroll(&mut uinput_file, steps_x, steps_y);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ res
+}
diff --git a/src/ipc.rs b/src/ipc.rs
index e7126fb..e51ee61 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -668,6 +668,21 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
}
}
}
+ "trackpad-disabled" | "trackpad_disabled" => {
+ let old_val = state.trackpad_disabled;
+ match value_str {
+ "true" | "1" | "enabled" => {
+ state.trackpad_disabled = true;
+ }
+ "false" | "0" | "disabled" => {
+ state.trackpad_disabled = false;
+ }
+ _ => {}
+ }
+ if state.trackpad_disabled != old_val {
+ state.tap_config_applied = false;
+ }
+ }
"trackpoint-accel-speed" | "trackpoint_accel_speed" => {
if let Ok(val) = value_str.parse::<f64>() {
state.trackpoint_accel_speed = Some(val);
diff --git a/src/ipc_server.rs b/src/ipc_server.rs
index c88b819..a73dcd4 100644
--- a/src/ipc_server.rs
+++ b/src/ipc_server.rs
@@ -6,10 +6,12 @@ pub const IPC_SOCKET_PATH: &str = "/tmp/clearwm.sock";
pub struct IpcReceiver {
pub rx: mpsc::Receiver<String>,
+ pub tx: mpsc::Sender<String>,
}
pub fn spawn_ipc_server(pipe_write: libc::c_int) -> IpcReceiver {
let (tx, rx) = mpsc::channel::<String>();
+ let tx_clone = tx.clone();
std::thread::Builder::new()
.name("clearwm-ipc".into())
@@ -18,7 +20,7 @@ pub fn spawn_ipc_server(pipe_write: libc::c_int) -> IpcReceiver {
})
.expect("failed to spawn IPC server thread");
- IpcReceiver { rx }
+ IpcReceiver { rx, tx: tx_clone }
}
fn ipc_server_main(tx: mpsc::Sender<String>, pipe_write: libc::c_int) {
diff --git a/src/lib.rs b/src/lib.rs
index 87da6c3..9ed148a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -15,3 +15,5 @@ pub mod types;
#[allow(unreachable_patterns)] // wayland event match arms use _ => {} for forward-compat
pub mod wayland;
pub mod wm;
+pub mod input;
+
diff --git a/src/main.rs b/src/main.rs
index 947aef0..634ef99 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -96,7 +96,25 @@ fn main() {
let pipe_write = pipe_fds[1];
// Start the IPC server thread (for clearctl and clear-system-interface)
- let ipc_rx = ipc_server::spawn_ipc_server(pipe_write).rx;
+ let ipc_server = ipc_server::spawn_ipc_server(pipe_write);
+ let ipc_rx = ipc_server.rx;
+ let ipc_tx = ipc_server.tx;
+
+ // Setup channel for configuration updates:
+ let (config_tx, config_rx) = tokio::sync::mpsc::unbounded_channel::<(clearwm::config::InertialConfig, bool)>();
+ state.wm.input_controller = Some(config_tx);
+
+ // Spawn the input subsystem background thread:
+ let pipe_write_clone = pipe_write;
+ let ipc_tx_clone = ipc_tx.clone();
+ std::thread::Builder::new()
+ .name("clearwm-input-subsystem".into())
+ .spawn(move || {
+ if let Err(e) = clearwm::input::run_input_daemon(config_rx, ipc_tx_clone, pipe_write_clone) {
+ eprintln!("[input-subsystem] Fatal error: {:?}", e);
+ }
+ })
+ .expect("failed to spawn input subsystem thread");
// Store the status sender in the app state so RenderStart can push updates
state.status_sender = Some(status_sender);
diff --git a/src/types.rs b/src/types.rs
index 898e61c..a9c5211 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -341,6 +341,8 @@ pub struct WindowManager {
pub notifications_enable: bool,
/// Reload commands to execute on configuration reload
pub reload_commands: Vec<String>,
+ pub input_controller: Option<tokio::sync::mpsc::UnboundedSender<(crate::config::InertialConfig, bool)>>,
+ pub trackpad_disabled: bool,
}
impl Default for WindowManager {
@@ -383,6 +385,8 @@ impl Default for WindowManager {
tap_config_applied: false,
notifications_enable: true,
reload_commands: Vec::new(),
+ input_controller: None,
+ trackpad_disabled: false,
}
}
}
diff --git a/src/wayland.rs b/src/wayland.rs
index 169ad98..6e4085b 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -152,6 +152,7 @@ pub struct AppState {
pub libinput_devices: Vec<LibinputDeviceInfo>,
/// The surface the pointer is currently hovering over
pub pointer_hovered_surface: Option<wl_surface::WlSurface>,
+ pub input_device_names: std::collections::HashMap<u32, String>,
}
/// Info tracked for each libinput device discovered via river_libinput_config_v1
@@ -212,6 +213,7 @@ impl AppState {
libinput_config: None,
libinput_devices: Vec::new(),
pointer_hovered_surface: None,
+ input_device_names: std::collections::HashMap::new(),
}
}
@@ -1568,6 +1570,7 @@ impl Dispatch<RiverInputDeviceV1, ()> for AppState {
}
river_input_device_v1::Event::Name { name } => {
eprintln!("input_device name: {}", name);
+ state.input_device_names.insert(proxy.id().protocol_id(), name.clone());
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()
@@ -2644,8 +2647,15 @@ impl Dispatch<RiverLibinputDeviceV1, ()> for AppState {
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);
+ dev.input_device = Some(device.clone());
+ let dev_id = device.id().protocol_id();
+ if let Some(name) = state.input_device_names.get(&dev_id) {
+ dev.name = name.clone();
+ dev.name_received = true;
+ eprintln!("[libinput] associated name \"{}\" with device on InputDevice event", name);
+ }
}
+ crate::wayland::apply_input_config(state, qhandle);
}
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()) {
@@ -2840,6 +2850,18 @@ pub fn apply_input_config(state: &mut AppState, qhandle: &QueueHandle<AppState>)
}
}
}
+ // 7. Send events (enable/disable trackpad)
+ let is_touchpad = dev_info.name.to_lowercase().contains("touchpad");
+ let send_events_mode = if state.wm.trackpad_disabled && is_touchpad {
+ river_libinput_device_v1::SendEventsModes::Disabled
+ } else {
+ river_libinput_device_v1::SendEventsModes::Enabled
+ };
+ eprintln!(
+ "[libinput] setting send_events={:?} on device ({})",
+ send_events_mode, &dev_info.name
+ );
+ dev_info.device.set_send_events(send_events_mode, qhandle, ());
dev_info.config_applied = true;
}