Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Fix monolithic window mapping, layer shell support, and rendering lifecycle assertions
Cargo.lock | 99 +-
Cargo.toml | 4 -
src/bin/cce.rs | 12 +-
src/{client => }/clearctl.rs | 26 +-
src/client/borders.rs | 210 --
src/client/clear_inspector.rs | 371 ---
src/client/config.rs | 909 -------
src/client/decorations.rs | 1276 ---------
src/client/input.rs | 1063 --------
src/client/ipc.rs | 1571 -----------
src/client/ipc_server.rs | 131 -
src/client/lib.rs | 22 -
src/client/paths.rs | 97 -
src/client/protocol.rs | 65 -
src/client/restart.rs | 279 --
src/client/run.rs | 328 ---
src/client/state.rs | 475 ----
src/client/status.rs | 100 -
src/client/tiling.rs | 311 ---
src/client/types.rs | 781 ------
src/client/wayland.rs | 4436 -------------------------------
src/client/wm.rs | 1538 -----------
src/lib.rs | 54 +-
src/server/config.rs | 693 +++++
src/server/cursor.rs | 67 +
src/server/ipc_server.rs | 84 +
src/server/keyboard_group.rs | 68 +-
src/server/run_server.rs | 61 +-
src/server/seat.rs | 51 +
src/server/server.rs | 8 +-
src/{client => server}/status_server.rs | 209 +-
src/server/tiling.rs | 117 +
src/server/window.rs | 28 +
src/server/window_manager.rs | 872 +++++-
34 files changed, 2123 insertions(+), 14293 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2378892..655f83d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -130,10 +130,6 @@ dependencies = [
"serde_json",
"tokio",
"toml",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "wayland-scanner",
"xkbcommon",
]
@@ -215,12 +211,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
-[[package]]
-name = "downcast-rs"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
-
[[package]]
name = "either"
version = "1.16.0"
@@ -415,12 +405,6 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
-[[package]]
-name = "linux-raw-sys"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
-
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -571,15 +555,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "quick-xml"
-version = "0.39.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "quote"
version = "1.0.45"
@@ -642,23 +617,10 @@ dependencies = [
"bitflags",
"errno",
"libc",
- "linux-raw-sys 0.4.15",
+ "linux-raw-sys",
"windows-sys 0.59.0",
]
-[[package]]
-name = "rustix"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys 0.12.1",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -865,63 +827,6 @@ 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"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
-dependencies = [
- "cc",
- "downcast-rs",
- "rustix 1.1.4",
- "smallvec",
- "wayland-sys",
-]
-
-[[package]]
-name = "wayland-client"
-version = "0.31.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
-dependencies = [
- "bitflags",
- "rustix 1.1.4",
- "wayland-backend",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-protocols"
-version = "0.32.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
-dependencies = [
- "bitflags",
- "wayland-backend",
- "wayland-client",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-scanner"
-version = "0.31.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
-dependencies = [
- "proc-macro2",
- "quick-xml",
- "quote",
-]
-
-[[package]]
-name = "wayland-sys"
-version = "0.31.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
-dependencies = [
- "pkg-config",
-]
-
[[package]]
name = "which"
version = "4.4.2"
@@ -931,7 +836,7 @@ dependencies = [
"either",
"home",
"once_cell",
- "rustix 0.38.44",
+ "rustix",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 62cd018..6172850 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,10 +4,6 @@ version = "0.1.0"
edition = "2021"
[dependencies]
-wayland-client = "0.31"
-wayland-backend = "0.3"
-wayland-protocols = { version = "0.32", features = ["client", "unstable", "staging"] }
-wayland-scanner = "0.31"
xkbcommon = "0.7"
toml = "0.8"
serde = { version = "1", features = ["derive"] }
diff --git a/src/bin/cce.rs b/src/bin/cce.rs
index 64cda9a..f49f0e3 100644
--- a/src/bin/cce.rs
+++ b/src/bin/cce.rs
@@ -5,18 +5,14 @@ fn main() {
if args.len() > 1 {
match args[1].as_str() {
"client" => {
- cce::run_client();
+ println!("cce: standalone client mode is deprecated in the monolithic architecture");
+ std::process::exit(0);
}
"control" => {
let mut control_args = vec!["clearctl".to_string()];
control_args.extend(args.iter().skip(2).cloned());
cce::run_clearctl(control_args);
}
- "inspect" | "inspector" => {
- let mut inspect_args = vec!["clear-inspector".to_string()];
- inspect_args.extend(args.iter().skip(2).cloned());
- cce::run_inspector(inspect_args);
- }
"--help" | "-h" | "help" => {
print_help();
}
@@ -33,8 +29,6 @@ fn print_help() {
println!("usage: cce <subcommand> [options]");
println!();
println!("subcommands:");
- println!(" (default) Start the compositor server and embedded client");
- println!(" client Start the standalone Wayland window manager client");
+ println!(" (default) Start the monolithic compositor and window manager");
println!(" control Run IPC control commands (e.g. cce control layout gap 10)");
- println!(" inspect Run the widget tree inspector");
}
diff --git a/src/client/clearctl.rs b/src/clearctl.rs
similarity index 94%
rename from src/client/clearctl.rs
rename to src/clearctl.rs
index 992598c..a8b811a 100644
--- a/src/client/clearctl.rs
+++ b/src/clearctl.rs
@@ -1,25 +1,25 @@
-// clearctl — IPC client for cce-client
-
+// clearctl — IPC client for cce
+
use std::env;
use std::fs;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::process;
-
+
fn get_socket_path() -> String {
match env::var("WAYLAND_DISPLAY") {
Ok(display) => format!("/tmp/cce-client-{}.sock", display),
Err(_) => "/tmp/cce-client.sock".to_string(),
}
}
-
+
fn get_windows_path() -> String {
match env::var("WAYLAND_DISPLAY") {
Ok(display) => format!("/tmp/cce-client-windows-{}", display),
Err(_) => "/tmp/cce-client-windows".to_string(),
}
}
-
+
fn usage(name: &str, to_stderr: bool) {
let print = |s: &str| {
if to_stderr {
@@ -37,7 +37,7 @@ fn usage(name: &str, to_stderr: bool) {
print(" close");
print(" minimize");
print(" focus-next");
- print(" focus-window <app_id|title>");
+ print(" focus-window <app_id|title>");
print(" expose");
print(" windows");
print(" exit");
@@ -64,27 +64,27 @@ fn usage(name: &str, to_stderr: bool) {
print(" key-press <key>");
print(" key-release <key>");
}
-
+
pub fn run_clearctl(args: Vec<String>) {
if args.len() < 2 {
usage(&args[0], true);
process::exit(1);
}
-
+
if args[1] == "--help" || args[1] == "-h" || args[1] == "help" {
usage(&args[0], false);
return;
}
-
+
// Special case: "windows" reads the status file directly
if args[1] == "windows" {
match fs::read_to_string(get_windows_path()) {
Ok(content) => print!("{}", content),
- Err(_) => eprintln!("No windows info (cce-client may not be running)"),
+ Err(_) => eprintln!("No windows info (cce may not be running)"),
}
return;
}
-
+
// Connect to IPC socket
let stream = match UnixStream::connect(get_socket_path()) {
Ok(s) => s,
@@ -93,7 +93,7 @@ pub fn run_clearctl(args: Vec<String>) {
process::exit(1);
}
};
-
+
let mut stream = stream;
// Build command string from args
let cmd = args[1..].join(" ") + "\n";
@@ -101,7 +101,7 @@ pub fn run_clearctl(args: Vec<String>) {
eprintln!("write: {}", e);
process::exit(1);
}
-
+
// Read response
let mut buf = [0u8; 4096];
loop {
diff --git a/src/client/borders.rs b/src/client/borders.rs
deleted file mode 100644
index 5fbc3f7..0000000
--- a/src/client/borders.rs
+++ /dev/null
@@ -1,210 +0,0 @@
-// Border color computation for cascade depth gradient and normal windows
-
-use crate::types::{TilingMode, WindowManager};
-
-/// Interpolate a single 8-bit channel toward black by (factor ^ depth).
-/// Returns a 32-bit value with the byte replicated across all 4 bytes,
-/// which is the format River expects (it divides by maxInt(u32) to get a float).
-pub fn interp_channel(fp_channel: u32, factor: f64, depth: i32) -> u32 {
- let base = (fp_channel & 0xFF) as u8;
- let mut f = 1.0_f64;
- for _ in 0..depth {
- f *= factor;
- }
- let val = (base as f64 * f) as u8;
- // Replicate byte across all 4 bytes: 0xVV -> 0xVVVVVVVV
- val as u32 * 0x01010101
-}
-
-/// Blend between two byte-replicated 32-bit channel values.
-/// factor=0.0 → pure bg, factor=1.0 → pure fg.
-fn blend_channel(bg_channel: u32, fg_channel: u32, factor: f64) -> u32 {
- let bg = (bg_channel & 0xFF) as u8 as f64;
- let fg = (fg_channel & 0xFF) as u8 as f64;
- let val = (bg + (fg - bg) * factor) as u8;
- val as u32 * 0x01010101
-}
-
-/// Alpha for borders: fully opaque, byte-replicated
-pub const ALPHA: u32 = 0xFFFFFFFF;
-
-/// Unfocused depth factor: each step away from the focused window reduces
-/// the blend factor by this multiplier, making the border color approach
-/// the background color.
-pub const UNFOCUSED_DEPTH_FACTOR: f64 = 0.70;
-
-/// Result of border color computation for a single window
-#[derive(Debug, Clone)]
-pub struct WindowBorders {
- pub window_idx: usize,
- pub edges: u32, // all edges = top | bottom | left | right
- pub width: i32,
- pub r: u32,
- pub g: u32,
- pub b: u32,
- pub a: u32,
-}
-
-/// Compute border colors for all visible windows.
-/// The focused window gets the configured border_color.
-/// Unfocused windows get a color interpolated between the desktop
-/// background_color and border_color based on stack depth.
-pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
- let mut results = Vec::new();
-
- // Find the focused window ID from the first non-removed seat
- let focused_id = state
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
-
- // Collect visible window indices (ordered from bottom to top of stack)
- let visible: Vec<usize> = state
- .windows
- .iter()
- .enumerate()
- .filter(|(_, w)| (w.tags & state.active_tags) != 0 && !w.closed)
- .map(|(i, _)| i)
- .collect();
-
- for (idx, win) in state.windows.iter().enumerate() {
- if (win.tags & state.active_tags) == 0 {
- continue;
- }
-
- let is_focused = focused_id.map_or(false, |fid| win.id == fid);
-
- /*
- eprintln!(
- "[borders] win={} app_id={:?} is_focused={} border_r=#{:08x} border_g=#{:08x} border_b=#{:08x}",
- win.id, win.app_id, is_focused,
- state.layout.border_r,
- state.layout.border_g,
- state.layout.border_b,
- );
- */
-
- let (r, g, b, mut a) = if win.tiling_mode == TilingMode::Popup {
- // Popup windows have a transparent border
- (0, 0, 0, 0)
- } else if is_focused {
- // Focused window: pure border color
- (
- state.layout.border_r,
- state.layout.border_g,
- state.layout.border_b,
- state.layout.border_a,
- )
- } else {
- // Unfocused window: blend background → border based on depth
- let visible_mode: Vec<usize> = visible
- .iter()
- .cloned()
- .filter(|&i| state.expose_visual_active || state.windows[i].tiling_mode == win.tiling_mode)
- .collect();
- let n_visible_mode = visible_mode.len();
- let pos = visible_mode.iter().position(|&i| i == idx).unwrap_or(0);
- let depth = (n_visible_mode - 1 - pos) as i32;
- let mut factor = 1.0_f64;
- for _ in 0..depth {
- factor *= UNFOCUSED_DEPTH_FACTOR;
- }
- let r = blend_channel(state.layout.background_r, state.layout.border_r, factor);
- let g = blend_channel(state.layout.background_g, state.layout.border_g, factor);
- let b = blend_channel(state.layout.background_b, state.layout.border_b, factor);
- let a = blend_channel(state.layout.background_a, state.layout.border_a, factor);
- (r, g, b, a)
- };
-
- if win.tiling_mode == TilingMode::SidePanel {
- let opacity_factor = state.layout.side_panel_border_opacity as f64 / 100.0;
- let base_alpha = (a & 0xFF) as f64 * opacity_factor;
- let val = base_alpha.round().clamp(0.0, 255.0) as u8;
- a = val as u32 * 0x01010101;
- }
-
- let edges = 0b0000u32;
- let width = 0;
-
- /*
- eprintln!(
- "[borders] -> r=#{:08x} g=#{:08x} b=#{:08x} a=#{:08x} width={}",
- r, g, b, a, width,
- );
- */
-
- results.push(WindowBorders {
- window_idx: idx,
- edges,
- width,
- r,
- g,
- b,
- a,
- });
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::types::{Window, ModeRule};
-
- #[test]
- fn test_interp_channel_depth0() {
- // Depth 0 should return the base color unchanged
- let result = interp_channel(0x5C5C5C5C, 0.80, 0);
- assert_eq!(result, 0x5C5C5C5C);
- }
-
- #[test]
- fn test_interp_channel_depth1() {
- let result = interp_channel(0x90909090, 0.80, 1);
- let expected = ((0x90 as f64 * 0.80) as u8) as u32 * 0x01010101;
- assert_eq!(result, expected);
- }
-
- #[test]
- fn test_border_edges_with_decorations() {
- let mut wm = WindowManager::default();
- wm.active_tags = 1;
-
- // Window with client decorations OK (should have edges = 0b0000)
- wm.windows.push(Window {
- id: 1,
- tiling_mode: TilingMode::Cascade,
- tags: 1,
- ..Default::default()
- });
-
- // Window with client decorations disabled via mode rule
- wm.mode_rules.push(ModeRule {
- mode: TilingMode::Cascade,
- app_id_pattern: "no-decorations".to_string(),
- title_pattern: None,
- single_instance: false,
- tag: 0,
- circular: false,
- ssd: Some(false), // Disable CSD
- });
- wm.windows.push(Window {
- id: 2,
- tiling_mode: TilingMode::Cascade,
- app_id: Some("no-decorations".to_string()),
- tags: 1,
- ..Default::default()
- });
-
- let results = compute_border_colors(&wm);
- assert_eq!(results.len(), 2);
-
- // Window 1: no compositor-drawn borders
- assert_eq!(results[0].edges, 0);
-
- // Window 2: no compositor-drawn borders either (all borders are handled in client)
- assert_eq!(results[1].edges, 0);
- }
-}
diff --git a/src/client/clear_inspector.rs b/src/client/clear_inspector.rs
deleted file mode 100644
index 2e2becc..0000000
--- a/src/client/clear_inspector.rs
+++ /dev/null
@@ -1,371 +0,0 @@
-use wayland_client::{
- globals::{registry_queue_init, GlobalListContents},
- protocol::wl_registry,
- Connection, Dispatch, QueueHandle,
-};
-use serde_json::Value;
-
-// Import generated client protocols from cce library
-use crate::protocol::clear_inspector::client::zclear_inspector_v1::{self, ZclearInspectorV1};
-
-struct InspectorState {
- inspector: Option<ZclearInspectorV1>,
- done: bool,
- surfaces: Vec<InspectedSurface>,
-}
-
-struct InspectedSurface {
- title: String,
- app_id: String,
- x: i32,
- y: i32,
- width: i32,
- height: i32,
- state: String,
-}
-
-impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for InspectorState {
- fn event(
- state: &mut Self,
- registry: &wl_registry::WlRegistry,
- event: wl_registry::Event,
- _data: &GlobalListContents,
- _conn: &Connection,
- qh: &QueueHandle<Self>,
- ) {
- if let wl_registry::Event::Global { name, interface, version } = event {
- println!("Debug Global: interface='{}', version={}, name={}", interface, version, name);
- if interface == "zclear_inspector_v1" {
- state.inspector = Some(registry.bind::<ZclearInspectorV1, _, _>(name, version, qh, ()));
- }
- }
- }
-}
-
-impl Dispatch<ZclearInspectorV1, ()> for InspectorState {
- fn event(
- state: &mut Self,
- _proxy: &ZclearInspectorV1,
- event: zclear_inspector_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- ) {
- #[allow(unreachable_patterns)]
- match event {
- zclear_inspector_v1::Event::InspectedSurface { title, app_id, x, y, width, height, fd, len } => {
- let mut surface_state = String::new();
- if len > 0 {
- use std::io::Read;
- let file = std::fs::File::from(fd);
- let _ = file.take(len as u64).read_to_string(&mut surface_state);
- }
- state.surfaces.push(InspectedSurface {
- title,
- app_id,
- x,
- y,
- width,
- height,
- state: surface_state,
- });
- }
- zclear_inspector_v1::Event::InspectedSurfaceDone => {
- state.done = true;
- }
- _ => {}
- }
- }
-}
-
-fn get_socket_path() -> String {
- match std::env::var("WAYLAND_DISPLAY") {
- Ok(display) => format!("/tmp/cce-client-{}.sock", display),
- Err(_) => "/tmp/cce-client.sock".to_string(),
- }
-}
-
-fn send_ipc_commands(commands: &[String]) -> Result<Vec<String>, std::io::Error> {
- use std::io::{Read, Write};
- use std::os::unix::net::UnixStream;
- let mut stream = UnixStream::connect(get_socket_path())?;
-
- let mut combined_cmd = String::new();
- for cmd in commands {
- combined_cmd.push_str(cmd);
- }
-
- stream.write_all(combined_cmd.as_bytes())?;
-
- let mut buf = [0u8; 4096];
- let mut response = String::new();
- loop {
- match stream.read(&mut buf) {
- Ok(0) => break,
- Ok(n) => {
- response.push_str(&String::from_utf8_lossy(&buf[..n]));
- }
- Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
- Err(e) => return Err(e),
- }
- }
-
- let replies = response.lines().map(|line| format!("{}\n", line)).collect();
- Ok(replies)
-}
-
-fn find_widget_recursive(val: &Value, target_type: &str, target_label: Option<&str>) -> Option<(f64, f64, f64, f64)> {
- if let Some(obj) = val.as_object() {
- let type_match = obj.get("type")
- .and_then(|t| t.as_str())
- .map(|t| t == target_type)
- .unwrap_or(false);
-
- let label_match = match target_label {
- Some(lbl) => obj.get("label")
- .and_then(|l| l.as_str())
- .map(|l| l.eq_ignore_ascii_case(lbl) || l.to_lowercase().contains(&lbl.to_lowercase()))
- .unwrap_or(false),
- None => true,
- };
-
- if type_match && label_match {
- if let Some(rect_arr) = obj.get("rect").and_then(|r| r.as_array()) {
- if rect_arr.len() == 4 {
- let rx = rect_arr[0].as_f64().unwrap_or(0.0);
- let ry = rect_arr[1].as_f64().unwrap_or(0.0);
- let rw = rect_arr[2].as_f64().unwrap_or(0.0);
- let rh = rect_arr[3].as_f64().unwrap_or(0.0);
- return Some((rx, ry, rw, rh));
- }
- }
- }
-
- // Search children
- if let Some(children) = obj.get("children").and_then(|c| c.as_array()) {
- for child in children {
- if let Some(coords) = find_widget_recursive(child, target_type, target_label) {
- return Some(coords);
- }
- }
- }
- } else if let Some(arr) = val.as_array() {
- for item in arr {
- if let Some(coords) = find_widget_recursive(item, target_type, target_label) {
- return Some(coords);
- }
- }
- }
- None
-}
-
-fn print_usage(bin_name: &str) {
- println!("Usage:");
- println!(" {} List all inspected surfaces and their widget trees", bin_name);
- println!(" {} click-widget --app-id <app_id> --type <widget_type> [--label <widget_label>]", bin_name);
- println!();
- println!("Options:");
- println!(" -a, --app-id <app_id> The Wayland application ID (e.g. clear-design-interface)");
- println!(" -t, --type <widget_type> The widget type (e.g. MenuItem, Button)");
- println!(" -l, --label <widget_label> Optional widget label to filter by");
-}
-
-pub fn run_inspector(args: Vec<String>) {
- let bin_name = args.get(0).map(|s| s.as_str()).unwrap_or("clear-inspector");
-
- let mut app_id = None;
- let mut widget_type = None;
- let mut widget_label = None;
- let mut is_click_widget = false;
-
- if args.len() > 1 {
- if args[1] == "click-widget" {
- is_click_widget = true;
- let mut i = 2;
- while i < args.len() {
- match args[i].as_str() {
- "--app-id" | "-a" => {
- if i + 1 < args.len() {
- app_id = Some(args[i+1].clone());
- i += 2;
- } else {
- eprintln!("Error: missing value for --app-id");
- std::process::exit(1);
- }
- }
- "--type" | "-t" => {
- if i + 1 < args.len() {
- widget_type = Some(args[i+1].clone());
- i += 2;
- } else {
- eprintln!("Error: missing value for --type");
- std::process::exit(1);
- }
- }
- "--label" | "-l" => {
- if i + 1 < args.len() {
- widget_label = Some(args[i+1].clone());
- i += 2;
- } else {
- eprintln!("Error: missing value for --label");
- std::process::exit(1);
- }
- }
- "-h" | "--help" | "help" => {
- print_usage(bin_name);
- std::process::exit(0);
- }
- _ => {
- eprintln!("Error: unknown argument '{}'", args[i]);
- print_usage(bin_name);
- std::process::exit(1);
- }
- }
- }
- if app_id.is_none() || widget_type.is_none() {
- eprintln!("Error: --app-id and --type are required for click-widget command");
- print_usage(bin_name);
- std::process::exit(1);
- }
- } else if args[1] == "-h" || args[1] == "--help" || args[1] == "help" {
- print_usage(bin_name);
- std::process::exit(0);
- } else {
- eprintln!("Error: unknown command '{}'", args[1]);
- print_usage(bin_name);
- std::process::exit(1);
- }
- }
-
- let conn = match Connection::connect_to_env() {
- Ok(c) => c,
- Err(e) => {
- eprintln!("Failed to connect to Wayland display socket: {:?}", e);
- std::process::exit(1);
- }
- };
-
- let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
- let qh = event_queue.handle();
-
- let mut state = InspectorState {
- inspector: None,
- done: false,
- surfaces: Vec::new(),
- };
-
- let inspector = match globals.bind::<ZclearInspectorV1, _, _>(&qh, 1..=1, ()) {
- Ok(ins) => ins,
- Err(e) => {
- eprintln!("Error: zclear_inspector_v1 global protocol not found on Wayland registry: {:?}", e);
- eprintln!("Ensure clear-river is running and supports the inspector protocol.");
- std::process::exit(1);
- }
- };
-
- // Request active inspected surfaces
- inspector.get_inspected_surfaces();
-
- // Dispatch until completed
- while !state.done {
- event_queue.blocking_dispatch(&mut state).unwrap();
- }
-
- if is_click_widget {
- let target_app_id = app_id.as_deref().unwrap();
- let target_type = widget_type.as_deref().unwrap();
- let matched_surface = state.surfaces.iter().find(|s| s.app_id.eq_ignore_ascii_case(target_app_id));
- if let Some(surface) = matched_surface {
- if let Ok(json_val) = serde_json::from_str::<Value>(&surface.state) {
- let mut max_logical_w = 0.0;
- if let Some(arr) = json_val.as_array() {
- for item in arr {
- if let Some(obj) = item.as_object() {
- if let Some(rect_arr) = obj.get("rect").and_then(|r| r.as_array()) {
- if rect_arr.len() == 4 {
- let rx_val = rect_arr[0].as_f64().unwrap_or(0.0);
- let rw_val = rect_arr[2].as_f64().unwrap_or(0.0);
- if rx_val == 0.0 && rw_val > max_logical_w {
- max_logical_w = rw_val;
- }
- }
- }
- }
- }
- }
- let scale = if max_logical_w > 0.0 {
- (surface.width as f64) / max_logical_w
- } else {
- 1.0
- };
-
- if let Some((rx, ry, rw, rh)) = find_widget_recursive(&json_val, target_type, widget_label.as_deref()) {
- let target_x = (surface.x as f64 + (rx + rw / 2.0) * scale) as i32;
- let target_y = (surface.y as f64 + (ry + rh / 2.0) * scale) as i32;
- println!("Found matching widget! Coordinate local bounds: [{}, {}, {}, {}], global target: ({}, {}), scale: {}", rx, ry, rw, rh, target_x, target_y, scale);
-
- let commands = vec![
- format!("focus-window {}\n", target_app_id),
- format!("pointer-move-to {} {}\n", target_x, target_y),
- format!("pointer-click left\n"),
- ];
- match send_ipc_commands(&commands) {
- Ok(replies) => {
- for r in replies {
- print!("{}", r);
- }
- }
- Err(e) => {
- eprintln!("Error sending IPC commands to cce-client socket: {:?}", e);
- std::process::exit(1);
- }
- }
- } else {
- eprintln!("Error: Element of type '{}'{} not found in surface '{}'",
- target_type,
- widget_label.as_ref().map(|l| format!(" with label '{}'", l)).unwrap_or_default(),
- target_app_id
- );
- std::process::exit(1);
- }
- } else {
- eprintln!("Error: Failed to parse widget state JSON for surface '{}'", target_app_id);
- std::process::exit(1);
- }
- } else {
- eprintln!("Error: Surface with app_id '{}' not found", target_app_id);
- std::process::exit(1);
- }
- return;
- }
-
- // Print results
- println!("Found {} active inspected surface(s):", state.surfaces.len());
- println!("{}", "=".repeat(60));
-
- for (idx, surface) in state.surfaces.iter().enumerate() {
- println!("Surface #{}:", idx + 1);
- println!(" Title: {}", surface.title);
- println!(" App ID: {}", surface.app_id);
- println!(" Position: (x: {}, y: {})", surface.x, surface.y);
- println!(" Dimensions: {}x{} px", surface.width, surface.height);
-
- // Pretty print widget state if it's JSON
- print!(" Element Tree: ");
- if let Ok(json_val) = serde_json::from_str::<Value>(&surface.state) {
- if let Ok(pretty_json) = serde_json::to_string_pretty(&json_val) {
- // Indent pretty JSON for clean display
- let indented = pretty_json.lines()
- .map(|line| format!(" {}", line))
- .collect::<Vec<String>>()
- .join("\n");
- println!("\n{}", indented);
- } else {
- println!("{}", surface.state);
- }
- } else {
- println!("{}", surface.state);
- }
- println!("{}", "=".repeat(60));
- }
-}
diff --git a/src/client/config.rs b/src/client/config.rs
deleted file mode 100644
index a5d118d..0000000
--- a/src/client/config.rs
+++ /dev/null
@@ -1,909 +0,0 @@
-// TOML config parsing for cce-client
-
-use serde::Deserialize;
-use std::collections::HashMap;
-use std::fs;
-
-use crate::types::{
- parse_action, parse_button, parse_hex_color, parse_modifiers, parse_tiling_mode, ModeRule,
- PendingPointerBinding, PendingXkbBinding, WindowManager, NUM_TAGS,
-};
-
-#[derive(Debug, Deserialize)]
-pub struct Config {
- #[serde(default)]
- pub layout: LayoutConfig,
- #[serde(default)]
- pub output: OutputConfig,
- #[serde(default)]
- pub input: InputConfig,
- #[serde(default)]
- pub repeat: RepeatConfig,
- #[serde(default)]
- pub startup: Vec<StartupEntryConfig>,
- #[serde(default)]
- pub env: HashMap<String, String>,
- #[serde(default)]
- pub keybind: Vec<KeybindConfig>,
- #[serde(default)]
- pub pointer_bind: Vec<PointerBindConfig>,
- #[serde(default)]
- pub mode_rule: Vec<ModeRuleConfig>,
- #[serde(default)]
- pub tag_layout: Vec<TagLayoutConfig>,
- #[serde(default)]
- pub notifications: NotificationsConfig,
- #[serde(default)]
- pub reload: Vec<ReloadEntryConfig>,
- #[serde(default)]
- pub inertial: Option<InertialConfig>,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct ReloadEntryConfig {
- pub exec: String,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct LayoutConfig {
- #[serde(default = "default_gap")]
- pub gap: i64,
- #[serde(default = "default_gap_top")]
- pub gap_top: i64,
- #[serde(default = "default_gap_left")]
- pub gap_left: i64,
- #[serde(default = "default_gap_right")]
- pub gap_right: i64,
- #[serde(default = "default_gap_bottom")]
- pub gap_bottom: i64,
- #[serde(default = "default_cascade_offset")]
- pub cascade_offset: i64,
- #[serde(default = "default_bar_height")]
- pub bar_height: i64,
- #[serde(default = "default_border_width")]
- pub border_width: i64,
- #[serde(default = "default_fullscreen_border_width")]
- pub fullscreen_border_width: i64,
- #[serde(default = "default_cascade_border_width")]
- pub cascade_border_width: i64,
- #[serde(default = "default_grid_border_width")]
- pub grid_border_width: i64,
- #[serde(default = "default_floating_border_width")]
- pub floating_border_width: i64,
- #[serde(default = "default_border_color", alias = "high_color")]
- pub border_color: String,
- #[serde(default = "default_background_color", alias = "low_color")]
- pub background_color: String,
- #[serde(default = "default_border_font_size")]
- pub border_font_size: i64,
- #[serde(default = "default_transition_duration")]
- pub transition_duration: i64,
- #[serde(default = "default_grid_gap")]
- pub grid_gap: i64,
- #[serde(default = "default_border_blur")]
- pub border_blur: bool,
- #[serde(default = "default_window_blur")]
- pub window_blur: bool,
- #[serde(default = "default_side_panel_behavior")]
- pub side_panel_behavior: String,
- #[serde(default = "default_side_panel_width")]
- pub side_panel_width: i64,
- #[serde(default = "default_side_panel_position")]
- pub side_panel_position: String,
- #[serde(default = "default_side_panel_border_gap")]
- pub side_panel_border_gap: i64,
- #[serde(default = "default_side_panel_border_opacity")]
- pub side_panel_border_opacity: i64,
-}
-
-impl Default for LayoutConfig {
- fn default() -> Self {
- LayoutConfig {
- gap: default_gap(),
- gap_top: default_gap_top(),
- gap_left: default_gap_left(),
- gap_right: default_gap_right(),
- gap_bottom: default_gap_bottom(),
- cascade_offset: default_cascade_offset(),
- bar_height: default_bar_height(),
- border_width: default_border_width(),
- fullscreen_border_width: default_fullscreen_border_width(),
- cascade_border_width: default_cascade_border_width(),
- grid_border_width: default_grid_border_width(),
- floating_border_width: default_floating_border_width(),
- border_color: default_border_color(),
- background_color: default_background_color(),
- border_font_size: default_border_font_size(),
- transition_duration: default_transition_duration(),
- grid_gap: default_grid_gap(),
- border_blur: default_border_blur(),
- window_blur: default_window_blur(),
- side_panel_behavior: default_side_panel_behavior(),
- side_panel_width: default_side_panel_width(),
- side_panel_position: default_side_panel_position(),
- side_panel_border_gap: default_side_panel_border_gap(),
- side_panel_border_opacity: default_side_panel_border_opacity(),
- }
- }
-}
-
-fn default_grid_gap() -> i64 {
- 18
-}
-
-fn default_border_blur() -> bool {
- false
-}
-
-fn default_window_blur() -> bool {
- false
-}
-
-fn default_side_panel_behavior() -> String {
- "inline".to_string()
-}
-
-fn default_side_panel_width() -> i64 {
- 360
-}
-
-fn default_side_panel_position() -> String {
- "left".to_string()
-}
-
-fn default_side_panel_border_gap() -> i64 {
- 0
-}
-
-fn default_side_panel_border_opacity() -> i64 {
- 100
-}
-
-fn default_gap() -> i64 {
- 48
-}
-fn default_gap_top() -> i64 {
- 48
-}
-fn default_gap_left() -> i64 {
- 48
-}
-fn default_gap_right() -> i64 {
- 48
-}
-fn default_gap_bottom() -> i64 {
- 48
-}
-fn default_cascade_offset() -> i64 {
- 20
-}
-fn default_bar_height() -> i64 {
- 24
-}
-fn default_border_width() -> i64 {
- 6
-}
-fn default_fullscreen_border_width() -> i64 {
- 0
-}
-fn default_cascade_border_width() -> i64 {
- 6
-}
-fn default_grid_border_width() -> i64 {
- 6
-}
-fn default_floating_border_width() -> i64 {
- 6
-}
-fn default_border_color() -> String {
- "#3e3e3e".to_string()
-}
-fn default_background_color() -> String {
- "#0a1a0e".to_string()
-}
-fn default_border_font_size() -> i64 {
- 11
-}
-fn default_transition_duration() -> i64 {
- 300
-}
-
-#[derive(Debug, Deserialize, Default)]
-pub struct OutputConfig {
- #[serde(default)]
- pub scale: f64,
-}
-
-#[derive(Debug, Deserialize, Default)]
-pub struct RepeatConfig {
- #[serde(default)]
- pub rate: i64,
- #[serde(default)]
- pub delay: i64,
-}
-
-#[derive(Debug, Deserialize, Default)]
-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>,
- pub cursor_theme: Option<String>,
- pub cursor_size: Option<u32>,
-}
-
-#[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")]
- pub enable: bool,
-}
-
-impl Default for NotificationsConfig {
- fn default() -> Self {
- Self { enable: true }
- }
-}
-
-fn default_true() -> bool {
- true
-}
-
-#[derive(Debug, Deserialize)]
-pub struct StartupEntryConfig {
- pub exec: String,
- #[serde(default)]
- pub once: bool,
- pub restart: Option<bool>,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct KeybindConfig {
- pub mods: String,
- pub key: String,
- pub action: String,
- pub command: Option<String>,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct PointerBindConfig {
- pub mods: String,
- pub button: String,
- pub action: String,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct ModeRuleConfig {
- pub mode: String,
- pub app_id: String,
- pub title: Option<String>,
- pub single: Option<bool>,
- pub tag: Option<i64>,
- pub circular: Option<bool>,
- pub ssd: Option<bool>,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct TagLayoutConfig {
- pub tag: i64,
- pub mode: String,
-}
-
-/// Parse the TOML config file and apply it to the WindowManager state.
-/// `cold_start` controls whether `once = true` startup entries are spawned.
-pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) -> Result<(), String> {
- let content = match fs::read_to_string(path) {
- Ok(c) => c,
- Err(e) => {
- let err_msg = format!("cannot open {}: {}", path, e);
- eprintln!("parse_config: {}", err_msg);
- return Err(err_msg);
- }
- };
-
- let config: Config = match toml::from_str(&content) {
- Ok(c) => c,
- Err(e) => {
- let err_msg = format!("TOML parse error: {}", e);
- eprintln!("parse_config: {}", err_msg);
- return Err(err_msg);
- }
- };
-
- // [layout] section
- eprintln!("[config] applying layout section...");
- state.layout.gap = config.layout.gap as i32;
- state.layout.gap_top = config.layout.gap_top as i32;
- state.layout.gap_left = config.layout.gap_left as i32;
- state.layout.gap_right = config.layout.gap_right as i32;
- state.layout.gap_bottom = config.layout.gap_bottom as i32;
- state.layout.cascade_offset = config.layout.cascade_offset as i32;
- state.layout.bar_height = config.layout.bar_height as i32;
- state.layout.border_width = config.layout.border_width as i32;
- state.layout.fullscreen_border_width = config.layout.fullscreen_border_width as i32;
- state.layout.cascade_border_width = config.layout.cascade_border_width as i32;
- state.layout.grid_border_width = config.layout.grid_border_width as i32;
- state.layout.floating_border_width = config.layout.floating_border_width as i32;
- state.layout.border_font_size = config.layout.border_font_size as i32;
- state.layout.transition_duration = config.layout.transition_duration as i32;
- state.layout.grid_gap = config.layout.grid_gap as i32;
- state.layout.border_blur = config.layout.border_blur;
- state.layout.window_blur = config.layout.window_blur;
- state.layout.side_panel_behavior = config.layout.side_panel_behavior.clone();
- state.layout.side_panel_width = config.layout.side_panel_width as i32;
- state.layout.side_panel_position = config.layout.side_panel_position.clone();
- state.layout.side_panel_border_gap = config.layout.side_panel_border_gap as i32;
- state.layout.side_panel_border_opacity = config.layout.side_panel_border_opacity as i32;
- if let Some((r, g, b, a)) = parse_hex_color(&config.layout.border_color) {
- state.layout.border_r = r;
- state.layout.border_g = g;
- state.layout.border_b = b;
- state.layout.border_a = a;
- }
- if let Some((r, g, b, a)) = parse_hex_color(&config.layout.background_color) {
- state.layout.background_r = r;
- state.layout.background_g = g;
- state.layout.background_b = b;
- state.layout.background_a = a;
- }
-
- // [[keybind]] array
- eprintln!("[config] processing {} keybinds...", config.keybind.len());
- for kb in &config.keybind {
- let mods = parse_modifiers(&kb.mods);
- let keysym = parse_keysym(&kb.key);
- let action = parse_action(&kb.action);
- let command = if action == crate::types::Action::Spawn || action == crate::types::Action::Toggle {
- kb.command.clone()
- } else {
- None
- };
-
- state.pending_bindings.push(PendingXkbBinding {
- mods,
- keysym,
- action,
- command,
- });
- }
-
- // Register default super+left and super+right bindings for side panel position if not overridden
- let super_mod = parse_modifiers("super");
- let left_sym = parse_keysym("Left");
- let right_sym = parse_keysym("Right");
-
- let has_super_left = state.pending_bindings.iter().any(|b| b.mods == super_mod && b.keysym == left_sym);
- if !has_super_left {
- state.pending_bindings.push(PendingXkbBinding {
- mods: super_mod,
- keysym: left_sym,
- action: crate::types::Action::SidePanelLeft,
- command: None,
- });
- }
-
- let has_super_right = state.pending_bindings.iter().any(|b| b.mods == super_mod && b.keysym == right_sym);
- if !has_super_right {
- state.pending_bindings.push(PendingXkbBinding {
- mods: super_mod,
- keysym: right_sym,
- action: crate::types::Action::SidePanelRight,
- command: None,
- });
- }
-
- // [[pointer_bind]] array
- for pb in &config.pointer_bind {
- let mods = parse_modifiers(&pb.mods);
- let button = parse_button(&pb.button);
- let action = parse_action(&pb.action);
-
- state.pending_pointer_bindings.push(PendingPointerBinding {
- mods,
- button,
- action,
- });
- }
-
- // [[mode_rule]] array
- for mr in &config.mode_rule {
- let mode = parse_tiling_mode(&mr.mode);
- let tag = mr.tag.unwrap_or(0) as i32;
- state.mode_rules.push(ModeRule {
- mode,
- app_id_pattern: mr.app_id.clone(),
- title_pattern: mr.title.clone(),
- single_instance: mr.single.unwrap_or(false),
- tag,
- circular: mr.circular.unwrap_or(false),
- ssd: mr.ssd,
- });
- }
-
- // [[tag_layout]] array
- for tl in &config.tag_layout {
- let tag = tl.tag as i32;
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- let mode = parse_tiling_mode(&tl.mode);
- state.tag_layouts[tag as usize - 1] = mode;
- state.has_tag_layout[tag as usize - 1] = true;
- }
- }
-
- // [env] — set environment variables BEFORE startup apps are processed,
- // so that PATH and other vars are available when spawn_command_bg runs.
- // Supports $VAR and ${VAR} expansion using the current environment.
- for (key, value) in &config.env {
- let expanded = expand_env_vars(value);
- std::env::set_var(key, &expanded);
- state.env_vars.insert(key.clone(), expanded);
- }
-
- // [[startup]] array — queue apps for spawning inside the render callback.
- // Spawning between blocking_dispatch calls corrupts the Wayland connection
- // because the fork inherits the socket fd, so we defer to render time.
- state.pending_startup_apps.clear();
- eprintln!(
- "[config] processing {} startup entries (cold_start={})...",
- config.startup.len(),
- cold_start
- );
- for (i, entry) in config.startup.iter().enumerate() {
- let name = extract_program_name(&entry.exec);
- let should_restart = entry.restart.unwrap_or(false);
-
- if entry.once && !cold_start && !should_restart {
- eprintln!(
- "[config] startup[{}]: exec=\"{}\" once=true → skipped (not cold start)",
- i, entry.exec
- );
- continue;
- }
-
- let running = process_running(&name);
- if should_restart && running {
- eprintln!(
- "[config] startup[{}]: exec=\"{}\" restart=true → killing existing process {}",
- i, entry.exec, name
- );
- let mut pkill_cmd = std::process::Command::new("pkill");
- if name.len() > 15 {
- pkill_cmd.arg("-f").arg(&name);
- } else {
- pkill_cmd.arg("-x").arg(&name);
- }
- match pkill_cmd.status() {
- Ok(status) => {
- eprintln!("[config] pkill status: {}", status);
- let start = std::time::Instant::now();
- while process_running(&name) && start.elapsed().as_secs_f64() < 1.0 {
- std::thread::sleep(std::time::Duration::from_millis(50));
- }
- eprintln!(
- "[config] process {} exited after pkill: {}",
- name,
- !process_running(&name)
- );
- }
- Err(e) => {
- eprintln!("[config] failed to execute pkill: {:?}", e);
- }
- }
- state.pending_startup_apps.push(entry.exec.clone());
- } else if running {
- eprintln!("[config] startup[{}]: exec=\"{}\" once={} → skipped (already running, pgrep -x {})", i, entry.exec, entry.once, name);
- } else {
- eprintln!(
- "[config] startup[{}]: exec=\"{}\" once={} → queued for spawn",
- i, entry.exec, entry.once
- );
- state.pending_startup_apps.push(entry.exec.clone());
- }
- }
-
- // [output] scale — applied via wlr-output-management protocol.
- // After storing the scale, set pending_scale_apply so that the next
- // output_manager done event triggers the configuration. This handles
- // both initial startup and VT-switch-back (where wlroots resets scale to 1).
- state.output_scale = if config.output.scale > 0.0 {
- state.pending_scale_apply = true;
- config.output.scale
- } else {
- 0.0
- };
-
- // [input] tap_to_click — applied via river-libinput-config protocol.
- // 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;
-
- if state.cursor_theme != config.input.cursor_theme || state.cursor_size != config.input.cursor_size {
- state.cursor_theme = config.input.cursor_theme.clone();
- state.cursor_size = config.input.cursor_size;
- state.cursor_theme_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(crate::input::InputDaemonMsg::UpdateConfig(inertial_cfg, config.input.tap_to_click));
- }
-
- // [notifications]
- state.notifications_enable = config.notifications.enable;
-
- // [[reload]] array
- state.reload_commands.clear();
- for entry in &config.reload {
- state.reload_commands.push(entry.exec.clone());
- }
-
- // Signal config-done
- state.config_done = true;
- Ok(())
-}
-
-/// Expand $VAR and ${VAR} references in a string using the current environment.
-/// Unset variables expand to empty strings. $$ is not expanded (not a shell).
-fn expand_env_vars(s: &str) -> String {
- let mut result = String::with_capacity(s.len());
- let chars: Vec<char> = s.chars().collect();
- let mut i = 0;
- while i < chars.len() {
- if chars[i] == '$' && i + 1 < chars.len() {
- if chars[i + 1] == '{' {
- // ${VAR} form
- if let Some(end) = chars[i + 2..].iter().position(|c| *c == '}') {
- let var_name: String = chars[i + 2..i + 2 + end].iter().collect();
- let val = std::env::var(&var_name).unwrap_or_default();
- result.push_str(&val);
- i = i + 2 + end + 1; // skip ${VAR}
- } else {
- result.push(chars[i]);
- i += 1;
- }
- } else if chars[i + 1].is_ascii_alphabetic() || chars[i + 1] == '_' {
- // $VAR form — name is [A-Za-z_][A-Za-z0-9_]*
- let start = i + 1;
- let mut end = start;
- while end < chars.len() && (chars[end].is_ascii_alphanumeric() || chars[end] == '_')
- {
- end += 1;
- }
- let var_name: String = chars[start..end].iter().collect();
- let val = std::env::var(&var_name).unwrap_or_default();
- result.push_str(&val);
- i = end;
- } else {
- // $ followed by non-identifier char (e.g. $$, $:, $@) — keep as-is
- result.push(chars[i]);
- i += 1;
- }
- } else {
- result.push(chars[i]);
- i += 1;
- }
- }
- result
-}
-
-/// Extract the program name (first word, basename) from a command string
-pub fn extract_program_name(cmd: &str) -> String {
- let cmd = cmd.trim_start();
- let first_word: String = cmd.chars().take_while(|c| !c.is_whitespace()).collect();
- if let Some(slash) = first_word.rfind('/') {
- first_word[slash + 1..].to_string()
- } else {
- first_word
- }
-}
-
-/// Parse a key name string into an xkb keysym value.
-/// Uses xkbcommon to resolve key names.
-pub fn parse_keysym(key_str: &str) -> u32 {
- let name = if key_str.starts_with("XKB_KEY_") {
- &key_str[8..]
- } else {
- key_str
- };
- // Use xkbcommon to parse the keysym
- xkbcommon::xkb::keysym_from_name(name, xkbcommon::xkb::KEYSYM_CASE_INSENSITIVE).into()
-}
-
-/// Spawn a command in the background.
-///
-/// Closes all inherited FDs > 2 in the child via pre_exec so that
-/// spawned Wayland clients (fuzzel, foot, etc.) never accidentally
-/// read from cce-client's Wayland socket fd. Also redirects stdout/stderr
-/// to /dev/null so child output doesn't pollute cce-client's log, and
-/// calls setsid() to detach from cce-client's process group.
-pub fn spawn_command_bg(cmd: &str) {
- use std::os::unix::process::CommandExt;
- let cmd = cmd.to_string();
-
- let stdout_cfg = if let Ok(f) = std::fs::OpenOptions::new()
- .create(true)
- .append(true)
- .open("/tmp/cce-client-spawned-apps.log")
- {
- std::process::Stdio::from(f)
- } else {
- std::process::Stdio::null()
- };
-
- let stderr_cfg = if let Ok(f) = std::fs::OpenOptions::new()
- .create(true)
- .append(true)
- .open("/tmp/cce-client-spawned-apps.log")
- {
- std::process::Stdio::from(f)
- } else {
- std::process::Stdio::null()
- };
-
- let _ = unsafe {
- std::process::Command::new("sh")
- .arg("-c")
- .arg(&cmd)
- .env_remove("WAYLAND_DEBUG")
- .stdout(stdout_cfg)
- .stderr(stderr_cfg)
- .pre_exec(|| {
- // Close all inherited FDs > 2 to prevent the child from
- // accidentally reading cce-client's Wayland socket or status
- // socket FDs. close() and setsid() are async-signal-safe.
- let max_fd = libc::sysconf(libc::_SC_OPEN_MAX) as libc::c_int;
- for fd in 3..max_fd {
- libc::close(fd);
- }
- libc::setsid();
- Ok(())
- })
- .spawn()
- };
-}
-pub fn process_running(name: &str) -> bool {
- let mut cmd = std::process::Command::new("pgrep");
- if name.len() > 15 {
- cmd.arg("-f").arg(name);
- } else {
- cmd.arg("-x").arg(name);
- }
- match cmd.output() {
- Ok(output) => output.status.success(),
- Err(_) => false,
- }
-}
-
-/// Show a system desktop notification via notify-send.
-pub fn show_notification(title: &str, body: &str) {
- let title_escaped = title.replace('\'', "'\\''");
- let body_escaped = body.replace('\'', "'\\''");
- let cmd = format!("notify-send -a cce-client '{}' '{}'", title_escaped, body_escaped);
- spawn_command_bg(&cmd);
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_extract_program_name() {
- assert_eq!(extract_program_name("ghostty"), "ghostty");
- assert_eq!(extract_program_name("ghostty -e something"), "ghostty");
- assert_eq!(
- extract_program_name("/home/lsgalante/.local/bin/tmux-startup --recreate main"),
- "tmux-startup"
- );
- assert_eq!(extract_program_name(" waybar"), "waybar");
- }
-
- #[test]
- fn test_config_layout_defaults() {
- let lc = LayoutConfig::default();
- assert_eq!(lc.gap, 48);
- assert_eq!(lc.gap_top, 48);
- assert_eq!(lc.gap_left, 48);
- assert_eq!(lc.gap_right, 48);
- assert_eq!(lc.gap_bottom, 48);
- assert_eq!(lc.cascade_offset, 20);
- assert_eq!(lc.bar_height, 24);
- assert_eq!(lc.border_width, 6);
- assert_eq!(lc.fullscreen_border_width, 0);
- assert_eq!(lc.border_color, "#3e3e3e");
- assert_eq!(lc.border_font_size, 11);
- assert_eq!(lc.grid_gap, 18);
- assert_eq!(lc.side_panel_width, 360);
- assert_eq!(lc.side_panel_behavior, "inline");
- assert_eq!(lc.side_panel_position, "left");
- assert_eq!(lc.side_panel_border_gap, 0);
- assert_eq!(lc.side_panel_border_opacity, 100);
- }
-
- #[test]
- fn test_expand_env_vars_simple() {
- std::env::set_var("CCE_CLIENT_TEST_VAR_SIMPLE", "hello");
- assert_eq!(expand_env_vars("$CCE_CLIENT_TEST_VAR_SIMPLE"), "hello");
- std::env::remove_var("CCE_CLIENT_TEST_VAR_SIMPLE");
- }
-
- #[test]
- fn test_expand_env_vars_braces() {
- std::env::set_var("CCE_CLIENT_TEST_VAR_BRACES", "world");
- assert_eq!(expand_env_vars("${CCE_CLIENT_TEST_VAR_BRACES}!"), "world!");
- std::env::remove_var("CCE_CLIENT_TEST_VAR_BRACES");
- }
-
- #[test]
- fn test_expand_env_vars_mid_string() {
- std::env::set_var("CCE_CLIENT_TEST_HOME", "/home/user");
- assert_eq!(
- expand_env_vars("$CCE_CLIENT_TEST_HOME/.local/bin:$CCE_CLIENT_TEST_HOME/bin"),
- "/home/user/.local/bin:/home/user/bin"
- );
- std::env::remove_var("CCE_CLIENT_TEST_HOME");
- }
-
- #[test]
- fn test_expand_env_vars_unset() {
- assert_eq!(expand_env_vars("$CCE_CLIENT_NONEXISTENT_VAR"), "");
- }
-
- #[test]
- fn test_expand_env_vars_no_vars() {
- assert_eq!(expand_env_vars("plain string"), "plain string");
- }
-
- #[test]
- fn test_expand_env_vars_dollar_non_identifier() {
- // $$ and $: should be kept as-is (not a shell)
- assert_eq!(expand_env_vars("$$"), "$$");
- assert_eq!(expand_env_vars("$:"), "$:");
- }
-
- #[test]
- fn test_expand_env_vars_path_append() {
- // Simulates the real use case: PATH = "$HOME/.local/bin:$PATH"
- let orig_path = std::env::var("PATH").unwrap_or_default();
- let home = std::env::var("HOME").unwrap_or_default();
- let expanded = expand_env_vars("$HOME/.local/bin:$PATH");
- assert!(expanded.starts_with(&format!("{}/.local/bin:", home)));
- assert!(expanded.contains(&orig_path));
- }
-
- #[test]
- fn test_notifications_config_parse() {
- let toml_str = r#"
-[notifications]
-enable = false
-"#;
- let config: Config = toml::from_str(toml_str).unwrap();
- assert!(!config.notifications.enable);
-
- let toml_str_empty = "";
- let config_empty: Config = toml::from_str(toml_str_empty).unwrap();
- assert!(config_empty.notifications.enable);
- }
-
- #[test]
- fn test_parse_keysym_favorites() {
- let sym = parse_keysym("XF86Favorites");
- assert!(sym != 0, "XF86Favorites keysym must be resolved successfully");
- }
-}
-
-#[cfg(test)]
-mod startup_format_tests {
- use super::*;
-
- #[test]
- fn test_startup_entry_format() {
- let toml_str = r#"
-[env]
-XDG_CURRENT_DESKTOP = "river"
-
-[[startup]]
-exec = "waybar"
-
-[[startup]]
-exec = "fuzzel"
-once = true
-
-[[startup]]
-exec = "cce-system-interface"
-once = true
-restart = true
-"#;
- let config: Config = toml::from_str(toml_str).expect("TOML parse failed");
- assert_eq!(config.startup.len(), 3);
- assert_eq!(config.startup[0].exec, "waybar");
- assert!(!config.startup[0].once);
- assert_eq!(config.startup[1].exec, "fuzzel");
- assert!(config.startup[1].once);
- assert_eq!(config.startup[2].exec, "cce-system-interface");
- assert!(config.startup[2].once);
- assert_eq!(config.startup[2].restart, Some(true));
- assert_eq!(config.env.get("XDG_CURRENT_DESKTOP").unwrap(), "river");
- }
-
- #[test]
- fn test_reload_entry_format() {
- let toml_str = r#"
-[[reload]]
-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-daemon");
- assert_eq!(config.reload[1].exec, "echo reloaded");
-
- // Also test integration via parse_config by writing to a temporary file
- let temp_path_buf = std::env::temp_dir().join("scratch_config_test.toml");
- let temp_path = temp_path_buf.to_str().unwrap();
- std::fs::write(temp_path, toml_str).unwrap();
-
- let mut wm = WindowManager::default();
- let parse_res = parse_config(temp_path, false, &mut wm);
- let _ = std::fs::remove_file(temp_path);
-
- parse_res.unwrap();
- assert_eq!(wm.reload_commands.len(), 2);
- assert_eq!(wm.reload_commands[0], "pkill clear-input-daemon");
- assert_eq!(wm.reload_commands[1], "echo reloaded");
- }
-}
diff --git a/src/client/decorations.rs b/src/client/decorations.rs
deleted file mode 100644
index e7beb5d..0000000
--- a/src/client/decorations.rs
+++ /dev/null
@@ -1,1276 +0,0 @@
-// Wayland decoration surface drawing and management for cce-client
-
-use std::ffi::CString;
-use std::os::fd::RawFd;
-use std::ptr;
-use wayland_client::protocol::{wl_buffer, wl_shm, wl_shm_pool, wl_surface};
-use wayland_client::QueueHandle;
-
-use crate::protocol::river_window_management::client::river_decoration_v1::RiverDecorationV1;
-use crate::wayland::AppState;
-
-fn resolve_window_border_font_path() -> Option<String> {
- use std::io::Read;
- let mut child = std::process::Command::new("fc-match")
- .args(&["-f", "%{file}", "window\\-borders"])
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::null())
- .spawn()
- .ok()?;
-
- let mut stdout = child.stdout.take()?;
- let mut output_str = String::new();
- let _ = stdout.read_to_string(&mut output_str);
- let _ = child.wait();
-
- let path = output_str.trim().to_string();
- if !path.is_empty() && std::path::Path::new(&path).exists() {
- return Some(path);
- }
- None
-}
-
-/// Struct tracking the Wayland decoration resources for a window.
-pub struct WindowDecoration {
- pub decoration: RiverDecorationV1,
- pub surface: wl_surface::WlSurface,
- pub buffer: Option<wl_buffer::WlBuffer>,
- pub pool: Option<wl_shm_pool::WlShmPool>,
- pub width: i32,
- pub height: i32,
- pub mapped_data: *mut u32,
- pub mapped_size: usize,
-}
-
-impl Drop for WindowDecoration {
- fn drop(&mut self) {
- if !self.mapped_data.is_null() {
- unsafe {
- libc::munmap(self.mapped_data as *mut libc::c_void, self.mapped_size);
- }
- }
- }
-}
-
-// Standard 8x8 bitmap font (IBM CP437 ASCII printable characters 32..=126)
-const FONT_DATA: &[u8; 760] = &[
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // (0x20)
- 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x00, // ! (0x21)
- 0x24, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, // " (0x22)
- 0x24, 0x24, 0x7e, 0x24, 0x7e, 0x24, 0x24, 0x00, // # (0x23)
- 0x08, 0x3e, 0x28, 0x3e, 0x0a, 0x3e, 0x08, 0x00, // $ (0x24)
- 0x00, 0x62, 0x64, 0x08, 0x13, 0x23, 0x00, 0x00, // % (0x25)
- 0x18, 0x24, 0x18, 0x2a, 0x24, 0x24, 0x1b, 0x00, // & (0x26)
- 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ' (0x27)
- 0x08, 0x10, 0x20, 0x20, 0x20, 0x10, 0x08, 0x00, // ( (0x28)
- 0x10, 0x08, 0x04, 0x04, 0x04, 0x08, 0x10, 0x00, // ) (0x29)
- 0x00, 0x24, 0x18, 0x3c, 0x18, 0x24, 0x00, 0x00, // * (0x2a)
- 0x00, 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, 0x00, // + (0x2b)
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x10, // , (0x2c)
- 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, // - (0x2d)
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, // . (0x2e)
- 0x00, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, // / (0x2f)
- 0x3c, 0x46, 0x4a, 0x52, 0x62, 0x3c, 0x00, 0x00, // 0 (0x30)
- 0x18, 0x28, 0x08, 0x08, 0x08, 0x3e, 0x00, 0x00, // 1 (0x31)
- 0x3c, 0x42, 0x02, 0x3c, 0x40, 0x7e, 0x00, 0x00, // 2 (0x32)
- 0x3c, 0x42, 0x1c, 0x02, 0x42, 0x3c, 0x00, 0x00, // 3 (0x33)
- 0x08, 0x18, 0x28, 0x48, 0x7e, 0x08, 0x00, 0x00, // 4 (0x34)
- 0x7e, 0x40, 0x7c, 0x02, 0x42, 0x3c, 0x00, 0x00, // 5 (0x35)
- 0x3c, 0x40, 0x7c, 0x42, 0x42, 0x3c, 0x00, 0x00, // 6 (0x36)
- 0x7e, 0x02, 0x04, 0x08, 0x10, 0x10, 0x00, 0x00, // 7 (0x37)
- 0x3c, 0x42, 0x3c, 0x42, 0x42, 0x3c, 0x00, 0x00, // 8 (0x38)
- 0x3c, 0x42, 0x3e, 0x02, 0x02, 0x3c, 0x00, 0x00, // 9 (0x39)
- 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, // : (0x3a)
- 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x10, 0x00, // ; (0x3b)
- 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, // < (0x3c)
- 0x00, 0x00, 0x3e, 0x00, 0x3e, 0x00, 0x00, 0x00, // = (0x3d)
- 0x20, 0x10, 0x08, 0x04, 0x08, 0x10, 0x20, 0x00, // > (0x3e)
- 0x3c, 0x42, 0x02, 0x0c, 0x00, 0x08, 0x08, 0x00, // ? (0x3f)
- 0x3c, 0x42, 0x5a, 0x5a, 0x5a, 0x3e, 0x00, 0x00, // @ (0x40)
- 0x18, 0x24, 0x42, 0x7e, 0x42, 0x42, 0x00, 0x00, // A (0x41)
- 0x7c, 0x42, 0x7c, 0x42, 0x42, 0x7c, 0x00, 0x00, // B (0x42)
- 0x3c, 0x42, 0x40, 0x40, 0x42, 0x3c, 0x00, 0x00, // C (0x43)
- 0x78, 0x44, 0x42, 0x42, 0x44, 0x78, 0x00, 0x00, // D (0x44)
- 0x7e, 0x40, 0x7c, 0x40, 0x40, 0x7e, 0x00, 0x00, // E (0x45)
- 0x7e, 0x40, 0x7c, 0x40, 0x40, 0x40, 0x00, 0x00, // F (0x46)
- 0x3c, 0x42, 0x40, 0x4e, 0x42, 0x3c, 0x00, 0x00, // G (0x47)
- 0x42, 0x42, 0x7e, 0x42, 0x42, 0x42, 0x00, 0x00, // H (0x48)
- 0x3e, 0x08, 0x08, 0x08, 0x08, 0x3e, 0x00, 0x00, // I (0x49)
- 0x02, 0x02, 0x02, 0x02, 0x42, 0x3c, 0x00, 0x00, // J (0x4a)
- 0x44, 0x48, 0x70, 0x48, 0x44, 0x42, 0x00, 0x00, // K (0x4b)
- 0x40, 0x40, 0x40, 0x40, 0x40, 0x7e, 0x00, 0x00, // L (0x4c)
- 0x42, 0x66, 0x5a, 0x42, 0x42, 0x42, 0x00, 0x00, // M (0x4d)
- 0x42, 0x62, 0x52, 0x4a, 0x46, 0x42, 0x00, 0x00, // N (0x4e)
- 0x3c, 0x42, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // O (0x4f)
- 0x7c, 0x42, 0x7c, 0x40, 0x40, 0x40, 0x00, 0x00, // P (0x50)
- 0x3c, 0x42, 0x42, 0x42, 0x4a, 0x3c, 0x02, 0x00, // Q (0x51)
- 0x7c, 0x42, 0x7c, 0x48, 0x44, 0x42, 0x00, 0x00, // R (0x52)
- 0x3c, 0x40, 0x3c, 0x02, 0x02, 0x3c, 0x00, 0x00, // S (0x53)
- 0x7e, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, // T (0x54)
- 0x42, 0x42, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // U (0x55)
- 0x42, 0x42, 0x42, 0x24, 0x24, 0x18, 0x00, 0x00, // V (0x56)
- 0x42, 0x42, 0x42, 0x5a, 0x5a, 0x24, 0x00, 0x00, // W (0x57)
- 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00, 0x00, // X (0x58)
- 0x42, 0x42, 0x24, 0x18, 0x08, 0x08, 0x00, 0x00, // Y (0x59)
- 0x7e, 0x02, 0x04, 0x08, 0x10, 0x7e, 0x00, 0x00, // Z (0x5a)
- 0x3c, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x00, 0x00, // [ (0x5b)
- 0x00, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, // \ (0x5c)
- 0x3c, 0x02, 0x02, 0x02, 0x02, 0x3c, 0x00, 0x00, // ] (0x5d)
- 0x08, 0x14, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, // ^ (0x5e)
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, // _ (0x5f)
- 0x08, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // ` (0x60)
- 0x00, 0x3c, 0x02, 0x3e, 0x42, 0x3e, 0x00, 0x00, // a (0x61)
- 0x40, 0x40, 0x7c, 0x42, 0x42, 0x7c, 0x00, 0x00, // b (0x62)
- 0x00, 0x3c, 0x40, 0x40, 0x42, 0x3c, 0x00, 0x00, // c (0x63)
- 0x02, 0x02, 0x3e, 0x42, 0x42, 0x3e, 0x00, 0x00, // d (0x64)
- 0x00, 0x3c, 0x42, 0x7e, 0x40, 0x3c, 0x00, 0x00, // e (0x65)
- 0x1c, 0x20, 0x78, 0x20, 0x20, 0x20, 0x00, 0x00, // f (0x66)
- 0x00, 0x3e, 0x42, 0x3e, 0x02, 0x3c, 0x00, 0x00, // g (0x67)
- 0x40, 0x40, 0x7c, 0x42, 0x42, 0x42, 0x00, 0x00, // h (0x68)
- 0x08, 0x00, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, // i (0x69)
- 0x02, 0x00, 0x02, 0x02, 0x02, 0x42, 0x3c, 0x00, // j (0x6a)
- 0x40, 0x44, 0x48, 0x70, 0x48, 0x44, 0x00, 0x00, // k (0x6b)
- 0x18, 0x08, 0x08, 0x08, 0x08, 0x3e, 0x00, 0x00, // l (0x6c)
- 0x00, 0x66, 0x5a, 0x42, 0x42, 0x42, 0x00, 0x00, // m (0x6d)
- 0x00, 0x7c, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, // n (0x6e)
- 0x00, 0x3c, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // o (0x6f)
- 0x00, 0x7c, 0x42, 0x7c, 0x40, 0x40, 0x00, 0x00, // p (0x70)
- 0x00, 0x3e, 0x42, 0x3e, 0x02, 0x02, 0x00, 0x00, // q (0x71)
- 0x00, 0x7c, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, // r (0x72)
- 0x00, 0x3e, 0x40, 0x3c, 0x02, 0x3c, 0x00, 0x00, // s (0x73)
- 0x08, 0x08, 0x3e, 0x08, 0x08, 0x0a, 0x04, 0x00, // t (0x74)
- 0x00, 0x42, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // u (0x75)
- 0x00, 0x42, 0x42, 0x42, 0x24, 0x18, 0x00, 0x00, // v (0x76)
- 0x00, 0x42, 0x42, 0x5a, 0x5a, 0x24, 0x00, 0x00, // w (0x77)
- 0x00, 0x42, 0x24, 0x18, 0x24, 0x42, 0x00, 0x00, // x (0x78)
- 0x00, 0x42, 0x42, 0x3e, 0x02, 0x3c, 0x00, 0x00, // y (0x79)
- 0x00, 0x7e, 0x04, 0x08, 0x10, 0x7e, 0x00, 0x00, // z (0x7a)
- 0x0c, 0x10, 0x10, 0x20, 0x10, 0x10, 0x0c, 0x00, // { (0x7b)
- 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, // | (0x7c)
- 0x30, 0x08, 0x08, 0x04, 0x08, 0x08, 0x30, 0x00, // } (0x7d)
- 0x00, 0x00, 0x00, 0x14, 0x28, 0x00, 0x00, 0x00, // ~ (0x7e)
-];
-
-/// Helper to draw a character at (x, y) with a given scale and BGRA color.
-fn draw_char(
- buf: &mut [u32],
- buf_w: i32,
- buf_h: i32,
- c: char,
- x: i32,
- y: i32,
- scale: i32,
- color: u32,
-) {
- let code = (c as usize).saturating_sub(32);
- if code >= 95 {
- return;
- }
- let glyph = &FONT_DATA[code * 8..(code + 1) * 8];
- for row in 0..8 {
- let row_byte = glyph[row];
- for col in 0..8 {
- let bit = (row_byte >> (7 - col)) & 1;
- if bit != 0 {
- for dy in 0..scale {
- for dx in 0..scale {
- let px = x + (col as i32) * scale + dx;
- let py = y + (row as i32) * scale + dy;
- if px >= 0 && px < buf_w && py >= 0 && py < buf_h {
- buf[(py * buf_w + px) as usize] = color;
- }
- }
- }
- }
- }
- }
-}
-
-/// Create a temporary shared memory file descriptor.
-fn create_memfd(size: usize) -> Option<RawFd> {
- let name = CString::new("cce-client-decoration").ok()?;
- let fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
- if fd < 0 {
- return None;
- }
- if unsafe { libc::ftruncate(fd, size as libc::off_t) } < 0 {
- unsafe { libc::close(fd); }
- return None;
- }
- Some(fd)
-}
-
-#[derive(Clone, Copy, PartialEq, Eq)]
-enum CornerType {
- TopLeft,
- TopRight,
- BottomLeft,
- BottomRight,
-}
-
-fn round_corner(
- buffer_slice: &mut [u32],
- dec_width: i32,
- dec_height: i32,
- cx: i32,
- cy: i32,
- r: i32,
- corner: CornerType,
-) {
- if r <= 0 {
- return;
- }
- let r_f = r as f32;
-
- let (x_range, y_range) = match corner {
- CornerType::TopLeft => (0..cx, 0..cy),
- CornerType::TopRight => (cx..dec_width, 0..cy),
- CornerType::BottomLeft => (0..cx, cy..dec_height),
- CornerType::BottomRight => (cx..dec_width, cy..dec_height),
- };
-
- for py in y_range {
- if py < 0 || py >= dec_height {
- continue;
- }
- let row_offset = (py * dec_width) as usize;
- for px in x_range.clone() {
- if px < 0 || px >= dec_width {
- continue;
- }
-
- let dx = (px - cx) as f32;
- let dy = (py - cy) as f32;
- let dist = (dx * dx + dy * dy).sqrt();
-
- if dist > r_f + 0.5 {
- buffer_slice[row_offset + px as usize] = 0x00000000;
- } else if dist > r_f - 0.5 {
- let alpha_scale = r_f + 0.5 - dist; // value between 0.0 and 1.0
- let index = row_offset + px as usize;
- let pixel = buffer_slice[index];
- let a = ((pixel >> 24) & 0xFF) as f32 * alpha_scale;
- let r_val = ((pixel >> 16) & 0xFF) as f32 * alpha_scale;
- let g = ((pixel >> 8) & 0xFF) as f32 * alpha_scale;
- let b = (pixel & 0xFF) as f32 * alpha_scale;
-
- buffer_slice[index] = ((a as u32) << 24)
- | ((r_val as u32) << 16)
- | ((g as u32) << 8)
- | (b as u32);
- }
- }
- }
-}
-
-fn update_border_decoration(
- _wid: u64,
- dec_opt: &mut Option<WindowDecoration>,
- compositor: &wayland_client::protocol::wl_compositor::WlCompositor,
- shm: &wayland_client::protocol::wl_shm::WlShm,
- river_window: &crate::protocol::river_window_management::client::river_window_v1::RiverWindowV1,
- qhandle: &QueueHandle<AppState>,
- offset_x: i32,
- offset_y: i32,
- logical_width: i32,
- logical_height: i32,
- scale: i32,
- border_blur: bool,
- bg_color: u32,
- rect_x: i32,
- rect_y: i32,
- rect_w: i32,
- rect_h: i32,
- border_width: i32,
- round_bottom: bool,
- side: &str,
- hover_info: Option<(f64, f64)>,
-) {
- let dec_width = logical_width * scale;
- let dec_height = logical_height * scale;
-
- let needs_new_buffer = match dec_opt {
- Some(dec) => dec.width != dec_width || dec.height != dec_height,
- None => true,
- };
-
- if dec_opt.is_none() {
- let surface: wl_surface::WlSurface = compositor.create_surface(qhandle, ());
- let decoration: RiverDecorationV1 = river_window.get_decoration_above(&surface, qhandle, ());
-
- *dec_opt = Some(WindowDecoration {
- decoration,
- surface,
- buffer: None,
- pool: None,
- width: 0,
- height: 0,
- mapped_data: ptr::null_mut(),
- mapped_size: 0,
- });
- }
-
- let dec = dec_opt.as_mut().unwrap();
- dec.decoration.set_offset(offset_x, offset_y);
- dec.decoration.set_blur(if border_blur { 1 } else { 0 });
-
- if needs_new_buffer {
- if !dec.mapped_data.is_null() {
- unsafe {
- libc::munmap(dec.mapped_data as *mut libc::c_void, dec.mapped_size);
- }
- dec.mapped_data = ptr::null_mut();
- }
-
- let stride = dec_width * 4;
- let size = (stride * dec_height) as usize;
-
- if let Some(fd) = create_memfd(size) {
- let mapped_data = unsafe {
- libc::mmap(
- ptr::null_mut(),
- size,
- libc::PROT_READ | libc::PROT_WRITE,
- libc::MAP_SHARED,
- fd,
- 0,
- ) as *mut u32
- };
-
- if mapped_data != libc::MAP_FAILED as *mut u32 {
- let borrowed_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
- let pool = shm.create_pool(borrowed_fd, size as i32, qhandle, ());
- let buffer = pool.create_buffer(
- 0,
- dec_width,
- dec_height,
- stride,
- wl_shm::Format::Argb8888,
- qhandle,
- (),
- );
-
- dec.pool = Some(pool);
- dec.buffer = Some(buffer);
- dec.width = dec_width;
- dec.height = dec_height;
- dec.mapped_data = mapped_data;
- dec.mapped_size = size;
- dec.surface.set_buffer_scale(scale);
- } else {
- eprintln!("[decorations] failed to mmap border decoration buffer");
- unsafe { libc::close(fd); }
- return;
- }
- unsafe { libc::close(fd); }
- } else {
- eprintln!("[decorations] failed to create memfd for border decoration");
- return;
- }
- }
-
- if let Some(ref buffer) = dec.buffer {
- if !dec.mapped_data.is_null() {
- let buffer_slice = unsafe {
- std::slice::from_raw_parts_mut(dec.mapped_data, (dec_width * dec_height) as usize)
- };
-
- // Clear to fully transparent
- for pixel in buffer_slice.iter_mut() {
- *pixel = 0x00000000;
- }
-
- let mut highlight_region: Option<HighlightRegion> = None;
- if let Some((hx, hy)) = hover_info {
- const CORNER_THRESHOLD: f64 = 16.0;
- match side {
- "left" => {
- let mid_x = (logical_width as f64) / 2.0;
- if hx < mid_x {
- if hy < CORNER_THRESHOLD {
- highlight_region = Some(HighlightRegion::TopLeft);
- } else if hy > (logical_height as f64 - CORNER_THRESHOLD) {
- highlight_region = Some(HighlightRegion::BottomLeft);
- } else {
- highlight_region = Some(HighlightRegion::Left);
- }
- }
- }
- "right" => {
- let mid_x = (logical_width as f64) / 2.0;
- if hx >= mid_x {
- if hy < CORNER_THRESHOLD {
- highlight_region = Some(HighlightRegion::TopRight);
- } else if hy > (logical_height as f64 - CORNER_THRESHOLD) {
- highlight_region = Some(HighlightRegion::BottomRight);
- } else {
- highlight_region = Some(HighlightRegion::Right);
- }
- }
- }
- "bottom" => {
- let mid_y = (logical_height as f64) / 2.0;
- if hy >= mid_y {
- if hx < CORNER_THRESHOLD {
- highlight_region = Some(HighlightRegion::BottomLeft);
- } else if hx > (logical_width as f64 - CORNER_THRESHOLD) {
- highlight_region = Some(HighlightRegion::BottomRight);
- } else {
- highlight_region = Some(HighlightRegion::Bottom);
- }
- }
- }
- _ => {}
- }
- }
-
- // Draw visual border color with 3D cylindrical shading and mitred joints
- let rx_start = rect_x * scale;
- let ry_start = rect_y * scale;
- let rx_end = (rect_x + rect_w) * scale;
- let ry_end = (rect_y + rect_h) * scale;
-
- let border_w_scaled = border_width * scale;
- let win_w_scaled = (rect_w - 2 * border_width) * scale;
- let grab_w_scaled = logical_height * scale;
-
- for py in ry_start..ry_end {
- if py >= 0 && py < dec_height {
- let row_offset = (py * dec_width) as usize;
- for px in rx_start..rx_end {
- if px >= 0 && px < dec_width {
- let s = if rect_w > rect_h {
- // Horizontal border (bottom)
- let logical_px = px - grab_w_scaled;
- let logical_py = py;
-
- if logical_px < 0 && logical_py < -logical_px {
- // Left border region (outer is at t = 0)
- let t_val = ((logical_px + border_w_scaled) as f32) / (border_w_scaled as f32 - 1.0).max(1.0);
- if t_val < 0.5 {
- 1.0 + 0.45 * (1.0 - 2.0 * t_val) * (1.0 - 2.0 * t_val)
- } else {
- 1.0
- }
- } else if logical_px > win_w_scaled && logical_py < (logical_px - win_w_scaled) {
- // Right border region (outer is at t = 0)
- let t_val = 1.0 - ((logical_px - win_w_scaled) as f32) / (border_w_scaled as f32 - 1.0).max(1.0);
- if t_val < 0.5 {
- 1.0 - 0.50 * (1.0 - 2.0 * t_val) * (1.0 - 2.0 * t_val)
- } else {
- 1.0
- }
- } else {
- // Bottom border region (outer is at t = 0)
- let t_val = 1.0 - (logical_py as f32) / (border_w_scaled as f32 - 1.0).max(1.0);
- if t_val < 0.5 {
- 1.0 - 0.50 * (1.0 - 2.0 * t_val) * (1.0 - 2.0 * t_val)
- } else {
- 1.0
- }
- }
- } else {
- // Vertical border (left/right)
- let t_val = ((px - rx_start) as f32) / (border_w_scaled as f32 - 1.0).max(1.0);
- if offset_x < 0 {
- // Left border: outer is at t = 0 (highlight)
- if t_val < 0.5 {
- 1.0 + 0.45 * (1.0 - 2.0 * t_val) * (1.0 - 2.0 * t_val)
- } else {
- 1.0
- }
- } else {
- // Right border: outer is at t = 1 (shadow)
- if t_val > 0.5 {
- 1.0 - 0.50 * (2.0 * t_val - 1.0) * (2.0 * t_val - 1.0)
- } else {
- 1.0
- }
- }
- };
-
- let a = ((bg_color >> 24) & 0xFF) as f32;
- let mut r_val = (((bg_color >> 16) & 0xFF) as f32 * s).round().min(255.0) as u32;
- let mut g = (((bg_color >> 8) & 0xFF) as f32 * s).round().min(255.0) as u32;
- let mut b = ((bg_color & 0xFF) as f32 * s).round().min(255.0) as u32;
-
- if let Some(region) = highlight_region {
- if is_pixel_in_highlight_region(side, region, px, py, logical_width, logical_height, scale) {
- r_val = (r_val as f32 * 0.6 + 255.0 * 0.4).round() as u32;
- g = (g as f32 * 0.6 + 255.0 * 0.4).round() as u32;
- b = (b as f32 * 0.6 + 255.0 * 0.4).round() as u32;
- }
- }
-
- buffer_slice[row_offset + px as usize] = ((a as u32) << 24)
- | (r_val << 16)
- | (g << 8)
- | b;
- }
- }
- }
- }
-
- if round_bottom {
- let r_val = (16.min(border_width)) * scale;
- let cy = ry_end - r_val;
-
- // Bottom Left
- let cx_l = rx_start + r_val;
- round_corner(buffer_slice, dec_width, dec_height, cx_l, cy, r_val, CornerType::BottomLeft);
-
- // Bottom Right
- let cx_r = rx_end - r_val;
- round_corner(buffer_slice, dec_width, dec_height, cx_r, cy, r_val, CornerType::BottomRight);
- }
- }
-
- dec.surface.attach(Some(buffer), 0, 0);
- dec.surface.damage(0, 0, dec_width, dec_height);
- dec.surface.commit();
- }
-}
-
-/// Main entry point to create or update decoration surfaces during rendering.
-pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
- // Check if we need to load or reload the font
- let current_path = resolve_window_border_font_path();
- if current_path != state.border_font_path {
- state.border_font_path = current_path.clone();
- state.border_font = current_path
- .and_then(|path| std::fs::read(&path).ok())
- .and_then(|data| fontdue::Font::from_bytes(data, fontdue::FontSettings::default()).ok());
- if state.border_font.is_some() {
- eprintln!("[decorations] loaded border font: {:?}", state.border_font_path);
- } else {
- eprintln!("[decorations] failed to load border font, falling back to built-in bitmap font");
- }
- }
-
- let active_tags = state.wm.active_tags;
-
- // We can only create decoration surfaces if the compositor and shm globals are bound
- let (compositor, shm) = match (&state.compositor, &state.shm) {
- (Some(c), Some(s)) => (c, s),
- _ => return,
- };
-
- // Clean up decorations for windows that should not be decorated,
- // or side borders for windows that do not have them (Popup, Fullscreen).
- for (wid, wp) in &mut state.window_proxies {
- if let Some(w) = state.wm.windows.iter().find(|win| win.id == *wid) {
- let is_minimized = w.minimized;
- let should_not_decorate = w.closed
- || w.app_id.as_deref() == Some("cce-status-interface")
- || w.app_id.as_deref().map_or(false, |aid| aid.contains("noborder"))
- || w.tiling_mode == crate::types::TilingMode::Popup
- || w.tiling_mode == crate::types::TilingMode::Fullscreen
- || w.circular;
- let needs_sides = !should_not_decorate && !is_minimized;
- // eprintln!("[decorations] window {} minimized={} needs_sides={} dec_right_exists={}", w.id, is_minimized, needs_sides, wp.dec_right.is_some());
-
- if should_not_decorate {
- if let Some(dec) = wp.decoration.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- }
-
- if !needs_sides {
- if let Some(dec) = wp.dec_left.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- if let Some(dec) = wp.dec_right.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- if let Some(dec) = wp.dec_bottom.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- }
- }
- }
-
- struct DecorateInfo {
- id: u64,
- win_width: i32,
- win_height: i32,
- border_width: i32,
- title: String,
- bg_color: u32,
- tiling_mode: crate::types::TilingMode,
- is_minimized: bool,
- minimized_idx: Option<usize>,
- win_y: i32,
- }
-
- // Calculate dynamic border colors
- let border_colors = crate::borders::compute_border_colors(&state.wm);
- let text_color = 0xFFE0E0E0u32;
-
- // Collect window IDs to modify so we don't violate the borrow checker
- let windows_to_decorate: Vec<DecorateInfo> = state
- .wm
- .windows
- .iter()
- .enumerate()
- .filter(|(_, w)| {
- !w.closed && w.app_id.as_deref() != Some("cce-status-interface") && !w.circular && !w.app_id.as_deref().map_or(false, |aid| aid.contains("noborder")) && (w.minimized || (w.tiling_mode != crate::types::TilingMode::Popup && w.tiling_mode != crate::types::TilingMode::Fullscreen))
- })
- .filter(|(_, w)| (w.tags & active_tags) != 0)
- .map(|(idx, w)| {
- let is_minimized = w.minimized;
- let minimized_idx = if is_minimized {
- state.wm.windows
- .iter()
- .filter(|win| !win.closed && win.minimized && (win.tags & active_tags) != 0 && win.app_id.as_deref() != Some("cce-status-interface"))
- .position(|win| win.id == w.id)
- } else {
- None
- };
- let title = w.title.clone().unwrap_or_else(|| {
- w.app_id.clone().unwrap_or_else(|| "Window".to_string())
- });
- let mode_idx = if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
- let list: Vec<_> = state.wm.windows
- .iter()
- .filter(|win| !win.closed && win.app_id.as_deref() != Some("cce-status-interface") && win.tiling_mode != crate::types::TilingMode::Popup && (win.tags & active_tags) != 0)
- .collect();
- let len = list.len();
- let pos = list.iter().position(|win| win.id == w.id).unwrap_or(0);
- if len > 0 { len - 1 - pos } else { 0 }
- } else {
- let list: Vec<_> = state.wm.windows
- .iter()
- .filter(|win| !win.closed && win.app_id.as_deref() != Some("cce-status-interface") && (win.tags & active_tags) != 0 && win.tiling_mode == w.tiling_mode)
- .collect();
- let len = list.len();
- let pos = list.iter().position(|win| win.id == w.id).unwrap_or(0);
- if len > 0 { len - 1 - pos } else { 0 }
- };
- let indicator = if is_minimized {
- "M"
- } else if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
- "EX"
- } else {
- match w.tiling_mode {
- crate::types::TilingMode::Floating => "F",
- crate::types::TilingMode::Cascade => "C",
- crate::types::TilingMode::Grid => "G",
- crate::types::TilingMode::Fullscreen => "S",
- crate::types::TilingMode::Popup => "P",
- crate::types::TilingMode::SidePanel => "SP",
- }
- };
- let title_with_idx = if is_minimized {
- format!("[{}] {}", indicator, title)
- } else {
- format!("[{}{}] {}", indicator, mode_idx, title)
- };
-
- // Find matching computed border color for this window
- let bc = border_colors.iter().find(|b| b.window_idx == idx);
- let (r, g, b, a) = match bc {
- Some(b) => (b.r, b.g, b.b, b.a),
- None => (
- state.wm.layout.border_r,
- state.wm.layout.border_g,
- state.wm.layout.border_b,
- state.wm.layout.border_a,
- ),
- };
-
- let border_r = (r & 0xFF) as u8;
- let border_g = (g & 0xFF) as u8;
- let border_b = (b & 0xFF) as u8;
- let border_a = (a & 0xFF) as u8;
-
- let r_premult = ((border_r as u32 * border_a as u32) / 255) as u8;
- let g_premult = ((border_g as u32 * border_a as u32) / 255) as u8;
- let b_premult = ((border_b as u32 * border_a as u32) / 255) as u8;
-
- let bg_color = ((border_a as u32) << 24)
- | ((r_premult as u32) << 16)
- | ((g_premult as u32) << 8)
- | (b_premult as u32);
-
- // Border width is mode-specific
- let border_width = if is_minimized {
- state.wm.layout.grid_border_width
- } else if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
- state.wm.layout.grid_border_width
- } else {
- match w.tiling_mode {
- crate::types::TilingMode::Cascade => state.wm.layout.cascade_border_width,
- crate::types::TilingMode::Fullscreen => state.wm.layout.fullscreen_border_width,
- crate::types::TilingMode::Grid => state.wm.layout.grid_border_width,
- crate::types::TilingMode::Floating => state.wm.layout.floating_border_width,
- crate::types::TilingMode::Popup => 0,
- crate::types::TilingMode::SidePanel => state.wm.layout.cascade_border_width,
- }
- };
- DecorateInfo {
- id: w.id,
- win_width: if w.committed_width > 0 { w.committed_width } else { w.width },
- win_height: if w.committed_height > 0 { w.committed_height } else { w.height },
- border_width,
- title: title_with_idx,
- bg_color,
- tiling_mode: w.tiling_mode,
- is_minimized,
- minimized_idx,
- win_y: w.y,
- }
- })
- .collect();
-
- for info in windows_to_decorate {
- let wid = info.id;
- let win_width = info.win_width;
- let win_height = info.win_height;
- let border_width = info.border_width;
- let title = info.title;
- let bg_color = info.bg_color;
- let tiling_mode = info.tiling_mode;
- let is_minimized = info.is_minimized;
- let minimized_idx = info.minimized_idx;
- let win_y = info.win_y;
-
- // Find or create decoration proxy
- let wp_idx = match state.window_proxies.iter().position(|(id, _)| *id == wid) {
- Some(idx) => idx,
- None => continue,
- };
-
- let wp = &mut state.window_proxies[wp_idx].1;
-
- let scale = (state.wm.output_scale.round() as i32).max(1);
-
- if tiling_mode != crate::types::TilingMode::Popup && tiling_mode != crate::types::TilingMode::Fullscreen && !is_minimized {
- let grab_w = if tiling_mode == crate::types::TilingMode::Floating {
- border_width.max(10)
- } else {
- border_width.max(1)
- };
-
- let hover_left = state.pointer_hovered_surface.as_ref()
- .filter(|surf| wp.dec_left.as_ref().map_or(false, |d| &d.surface == *surf))
- .map(|_| (state.last_pointer_surface_x, state.last_pointer_surface_y));
-
- update_border_decoration(
- wid,
- &mut wp.dec_left,
- compositor,
- shm,
- &wp.river_window,
- qhandle,
- -grab_w,
- 0,
- grab_w,
- win_height,
- scale,
- state.wm.layout.border_blur,
- bg_color,
- grab_w - border_width,
- 0,
- border_width,
- win_height,
- border_width,
- false,
- "left",
- hover_left,
- );
-
- let hover_right = state.pointer_hovered_surface.as_ref()
- .filter(|surf| wp.dec_right.as_ref().map_or(false, |d| &d.surface == *surf))
- .map(|_| (state.last_pointer_surface_x, state.last_pointer_surface_y));
-
- update_border_decoration(
- wid,
- &mut wp.dec_right,
- compositor,
- shm,
- &wp.river_window,
- qhandle,
- win_width,
- 0,
- grab_w,
- win_height,
- scale,
- state.wm.layout.border_blur,
- bg_color,
- 0,
- 0,
- border_width,
- win_height,
- border_width,
- false,
- "right",
- hover_right,
- );
-
- let hover_bottom = state.pointer_hovered_surface.as_ref()
- .filter(|surf| wp.dec_bottom.as_ref().map_or(false, |d| &d.surface == *surf))
- .map(|_| (state.last_pointer_surface_x, state.last_pointer_surface_y));
-
- update_border_decoration(
- wid,
- &mut wp.dec_bottom,
- compositor,
- shm,
- &wp.river_window,
- qhandle,
- -grab_w,
- win_height,
- win_width + 2 * grab_w,
- grab_w,
- scale,
- state.wm.layout.border_blur,
- bg_color,
- grab_w - border_width,
- 0,
- win_width + 2 * border_width,
- border_width,
- border_width,
- true,
- "bottom",
- hover_bottom,
- );
- }
-
- // Determine titlebar dimensions:
- // Height equals border_width (or 16 if border_width is too small to display font)
- let logical_height = std::cmp::max(border_width, 16);
- let logical_width = if is_minimized {
- 160
- } else {
- win_width + 2 * border_width
- };
- let dec_height = logical_height * scale;
- let dec_width = logical_width * scale;
-
- let needs_new_buffer = match &wp.decoration {
- Some(dec) => dec.width != dec_width || dec.height != dec_height,
- None => true,
- };
-
- if wp.decoration.is_none() {
- eprintln!("[decorations] creating decoration for window {}", wid);
- let surface: wl_surface::WlSurface = compositor.create_surface(qhandle, ());
- let decoration: RiverDecorationV1 = wp.river_window.get_decoration_above(&surface, qhandle, ());
-
- wp.decoration = Some(WindowDecoration {
- decoration,
- surface,
- buffer: None,
- pool: None,
- width: 0,
- height: 0,
- mapped_data: ptr::null_mut(),
- mapped_size: 0,
- });
- }
-
- let hover_top = state.pointer_hovered_surface.as_ref()
- .filter(|surf| wp.decoration.as_ref().map_or(false, |d| &d.surface == *surf))
- .map(|_| (state.last_pointer_surface_x, state.last_pointer_surface_y));
-
- let dec = wp.decoration.as_mut().unwrap();
-
- // Position decoration on top of the window top border
- if is_minimized {
- if let Some(idx) = minimized_idx {
- let bar_height = state.wm.layout.bar_height;
- let gap_top = state.wm.layout.gap_top;
- let bubble_gap = 8;
- let bubble_y = bar_height + gap_top + idx as i32 * (logical_height + bubble_gap);
- dec.decoration.set_offset(0, bubble_y - win_y);
- }
- } else {
- dec.decoration.set_offset(-border_width, -logical_height);
- }
-
- dec.decoration.set_blur(if state.wm.layout.border_blur { 1 } else { 0 });
-
- if needs_new_buffer {
- eprintln!(
- "[decorations] allocating buffer {}x{} for window {}",
- dec_width, dec_height, wid
- );
-
- // Clean up old mapping if it exists
- if !dec.mapped_data.is_null() {
- unsafe {
- libc::munmap(dec.mapped_data as *mut libc::c_void, dec.mapped_size);
- }
- dec.mapped_data = ptr::null_mut();
- }
-
- let stride = dec_width * 4;
- let size = (stride * dec_height) as usize;
-
- if let Some(fd) = create_memfd(size) {
- let mapped_data = unsafe {
- libc::mmap(
- ptr::null_mut(),
- size,
- libc::PROT_READ | libc::PROT_WRITE,
- libc::MAP_SHARED,
- fd,
- 0,
- ) as *mut u32
- };
-
- if mapped_data != libc::MAP_FAILED as *mut u32 {
- let borrowed_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
- let pool = shm.create_pool(borrowed_fd, size as i32, qhandle, ());
- let buffer = pool.create_buffer(
- 0,
- dec_width,
- dec_height,
- stride,
- wl_shm::Format::Argb8888,
- qhandle,
- (),
- );
-
- dec.pool = Some(pool);
- dec.buffer = Some(buffer);
- dec.width = dec_width;
- dec.height = dec_height;
- dec.mapped_data = mapped_data;
- dec.mapped_size = size;
- dec.surface.set_buffer_scale(scale);
- } else {
- eprintln!("[decorations] failed to mmap decoration buffer");
- unsafe { libc::close(fd); }
- continue;
- }
- unsafe { libc::close(fd); }
- } else {
- eprintln!("[decorations] failed to create memfd");
- continue;
- }
- }
-
- // Draw titlebar background and text
- if !dec.mapped_data.is_null() {
- let buffer_slice = unsafe {
- std::slice::from_raw_parts_mut(dec.mapped_data, (dec_width * dec_height) as usize)
- };
-
- // Clear background with 3D cylindrical shading and mitred corners (outer edge only)
- let border_w_scaled = border_width * scale;
- let titlebar_h_scaled = logical_height * scale;
- let win_w_scaled = (if is_minimized { 160 - 2 * border_width } else { win_width }) * scale;
-
- let mut highlight_region: Option<HighlightRegion> = None;
- if let Some((hx, hy)) = hover_top {
- let mid_y = (logical_height as f64) / 2.0;
- if hy < mid_y {
- const CORNER_THRESHOLD: f64 = 16.0;
- if hx < CORNER_THRESHOLD {
- highlight_region = Some(HighlightRegion::TopLeft);
- } else if hx > (logical_width as f64 - CORNER_THRESHOLD) {
- highlight_region = Some(HighlightRegion::TopRight);
- } else {
- highlight_region = Some(HighlightRegion::Top);
- }
- }
- }
-
- for py in 0..dec_height {
- let row_offset = (py * dec_width) as usize;
- for px in 0..dec_width {
- // Convert to coordinates relative to client area (0, 0)
- let logical_px = px - border_w_scaled;
- let logical_py = py - titlebar_h_scaled;
-
- // Determine which border region this pixel belongs to, using the exact diagonal slope:
- let slope = (titlebar_h_scaled as f32) / (border_w_scaled as f32).max(1.0);
- let s = if logical_px < 0 && logical_py > (logical_px as f32 * slope) as i32 {
- // Left border region (outer is at t = 0)
- let t_val = ((logical_px + border_w_scaled) as f32) / (border_w_scaled as f32 - 1.0).max(1.0);
- if t_val < 0.5 {
- 1.0 + 0.45 * (1.0 - 2.0 * t_val) * (1.0 - 2.0 * t_val)
- } else {
- 1.0
- }
- } else if logical_px > win_w_scaled && logical_py > (-((logical_px - win_w_scaled) as f32 * slope)) as i32 {
- // Right border region (outer is at t = 0)
- let t_val = 1.0 - ((logical_px - win_w_scaled) as f32) / (border_w_scaled as f32 - 1.0).max(1.0);
- if t_val < 0.5 {
- 1.0 - 0.50 * (1.0 - 2.0 * t_val) * (1.0 - 2.0 * t_val)
- } else {
- 1.0
- }
- } else {
- // Top border region (outer is at t = 0)
- let t_val = ((logical_py + titlebar_h_scaled) as f32) / (titlebar_h_scaled as f32 - 1.0).max(1.0);
- if t_val < 0.5 {
- 1.0 + 0.45 * (1.0 - 2.0 * t_val) * (1.0 - 2.0 * t_val)
- } else {
- 1.0
- }
- };
-
- let a = ((bg_color >> 24) & 0xFF) as f32;
- let mut r_val = (((bg_color >> 16) & 0xFF) as f32 * s).round().min(255.0) as u32;
- let mut g = (((bg_color >> 8) & 0xFF) as f32 * s).round().min(255.0) as u32;
- let mut b = ((bg_color & 0xFF) as f32 * s).round().min(255.0) as u32;
-
- if let Some(region) = highlight_region {
- if is_pixel_in_highlight_region("top", region, px, py, logical_width, logical_height, scale) {
- r_val = (r_val as f32 * 0.6 + 255.0 * 0.4).round() as u32;
- g = (g as f32 * 0.6 + 255.0 * 0.4).round() as u32;
- b = (b as f32 * 0.6 + 255.0 * 0.4).round() as u32;
- }
- }
-
- let base_pixel_color = ((a as u32) << 24) | (r_val << 16) | (g << 8) | b;
- let mut final_color = base_pixel_color;
-
- let lx = px as f32 / scale as f32;
- let ly = py as f32 / scale as f32;
-
- if !is_minimized && ly >= 0.0 && ly < logical_height as f32 {
- if lx >= (logical_width as f32 - 48.0) && lx < (logical_width as f32 - 32.0) {
- // Minimize button
- let is_hovered = hover_top.map_or(false, |(hx, hy)| {
- hx >= (logical_width as f64 - 48.0) && hx < (logical_width as f64 - 32.0)
- && hy >= 0.0 && hy < logical_height as f64
- });
- final_color = if is_hovered {
- blend_colors(base_pixel_color, 0x30FFFFFF)
- } else {
- base_pixel_color
- };
-
- let cx = logical_width as f32 - 40.0;
- let cy = logical_height as f32 / 2.0;
- if lx >= (cx - 4.0) && lx <= (cx + 4.0) && ly >= (cy + 2.0) && ly < (cy + 3.0) {
- final_color = text_color;
- }
- } else if lx >= (logical_width as f32 - 32.0) && lx < (logical_width as f32 - 16.0) {
- // Maximize button
- let is_hovered = hover_top.map_or(false, |(hx, hy)| {
- hx >= (logical_width as f64 - 32.0) && hx < (logical_width as f64 - 16.0)
- && hy >= 0.0 && hy < logical_height as f64
- });
- final_color = if is_hovered {
- blend_colors(base_pixel_color, 0x30FFFFFF)
- } else {
- base_pixel_color
- };
-
- let cx = logical_width as f32 - 24.0;
- let cy = logical_height as f32 / 2.0;
- let is_border_x = (lx >= cx - 4.0 && lx < cx - 3.0) || (lx > cx + 3.0 && lx <= cx + 4.0);
- let is_in_x = lx >= cx - 4.0 && lx <= cx + 4.0;
- let is_border_y = (ly >= cy - 4.0 && ly < cy - 3.0) || (ly > cy + 3.0 && ly <= cy + 4.0);
- let is_in_y = ly >= cy - 4.0 && ly <= cy + 4.0;
- if (is_border_x && is_in_y) || (is_border_y && is_in_x) {
- final_color = text_color;
- }
- } else if lx >= (logical_width as f32 - 16.0) && lx <= logical_width as f32 {
- // Close button
- let is_hovered = hover_top.map_or(false, |(hx, hy)| {
- hx >= (logical_width as f64 - 16.0) && hx <= logical_width as f64
- && hy >= 0.0 && hy < logical_height as f64
- });
- final_color = if is_hovered {
- blend_colors(base_pixel_color, 0x90E53935)
- } else {
- base_pixel_color
- };
-
- let cx = logical_width as f32 - 8.0;
- let cy = logical_height as f32 / 2.0;
- let dx = (lx - cx).abs();
- let dy = (ly - cy).abs();
- if dx <= 4.0 && dy <= 4.0 && (dx - dy).abs() < 1.0 {
- final_color = text_color;
- }
- }
- }
-
- buffer_slice[row_offset + px as usize] = final_color;
- }
- }
-
- // Draw window title text
- if let Some(ref font) = state.border_font {
- let font_size = if state.wm.layout.border_font_size > 0 {
- state.wm.layout.border_font_size as f32 * scale as f32
- } else {
- if logical_height >= 32 {
- 22.0 * scale as f32
- } else if logical_height >= 24 {
- 16.0 * scale as f32
- } else if logical_height >= 16 {
- 11.0 * scale as f32
- } else {
- (logical_height as f32 - 4.0).max(8.0) * scale as f32
- }
- };
-
- let line_metrics = font.horizontal_line_metrics(font_size).unwrap_or(fontdue::LineMetrics {
- ascent: font_size * 0.8,
- descent: -font_size * 0.2,
- line_gap: 0.0,
- new_line_size: font_size,
- });
- let baseline_y = (dec_height as f32 + line_metrics.ascent + line_metrics.descent) / 2.0;
-
- let mut text_x = 24.0f32 * scale as f32; // Margin from left
- for c in title.chars() {
- let (metrics, bitmap) = font.rasterize(c, font_size);
- if text_x + metrics.xmin as f32 + metrics.width as f32 > dec_width as f32 - 48.0 * scale as f32 {
- break;
- }
-
- let x_start = (text_x + metrics.xmin as f32).round() as i32;
- let y_start = (baseline_y - metrics.ymin as f32 - metrics.height as f32).round() as i32;
-
- for row in 0..metrics.height {
- for col in 0..metrics.width {
- let px = x_start + col as i32;
- let py = y_start + row as i32;
-
- if px >= 0 && px < dec_width && py >= 0 && py < dec_height {
- let alpha_coverage = bitmap[row * metrics.width + col] as u32;
- if alpha_coverage > 0 {
- let index = (py * dec_width + px) as usize;
- let dest_pixel = buffer_slice[index];
-
- let src_r = (text_color >> 16) & 0xFF;
- let src_g = (text_color >> 8) & 0xFF;
- let src_b = text_color & 0xFF;
- let src_a = (text_color >> 24) & 0xFF;
-
- let dest_r = (dest_pixel >> 16) & 0xFF;
- let dest_g = (dest_pixel >> 8) & 0xFF;
- let dest_b = dest_pixel & 0xFF;
- let dest_a = (dest_pixel >> 24) & 0xFF;
-
- let alpha = (alpha_coverage * src_a) / 255;
-
- let out_r = ((src_r * alpha) + (dest_r * (255 - alpha))) / 255;
- let out_g = ((src_g * alpha) + (dest_g * (255 - alpha))) / 255;
- let out_b = ((src_b * alpha) + (dest_b * (255 - alpha))) / 255;
- let out_a = dest_a + ((255 - dest_a) * alpha) / 255;
-
- buffer_slice[index] = (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b;
- }
- }
- }
- }
- text_x += metrics.advance_width;
- }
- } else {
- // Determine font scale (1x if height < 32, 2x if height >= 32)
- let drawing_scale = if logical_height >= 32 { 2 } else { 1 };
- let font_h = 8 * drawing_scale * scale;
-
- // Vertically center the text inside the titlebar
- let text_y = (dec_height - font_h) / 2;
- let mut text_x = 24 * scale; // Margin from left
-
- for c in title.chars() {
- if text_x + 8 * drawing_scale * scale > dec_width - 48 * scale {
- break; // Out of bounds
- }
- draw_char(buffer_slice, dec_width, dec_height, c, text_x, text_y, drawing_scale * scale, text_color);
- text_x += 8 * drawing_scale * scale;
- }
- }
-
- // Apply corner rounding to top-left and top-right of titlebar
- let r_top = 16 * scale;
- round_corner(buffer_slice, dec_width, dec_height, r_top, r_top, r_top, CornerType::TopLeft);
- round_corner(buffer_slice, dec_width, dec_height, dec_width - r_top, r_top, r_top, CornerType::TopRight);
-
- // Commit surface rendering
- if !is_minimized {
- dec.decoration.sync_next_commit();
- }
- if let Some(ref wl_buf) = dec.buffer {
- dec.surface.attach(Some(wl_buf), 0, 0);
- }
- dec.surface.commit();
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum HighlightRegion {
- TopLeft,
- Top,
- TopRight,
- Left,
- Right,
- BottomLeft,
- Bottom,
- BottomRight,
-}
-
-fn is_pixel_in_highlight_region(
- side: &str,
- region: HighlightRegion,
- px: i32,
- py: i32,
- logical_width: i32,
- logical_height: i32,
- scale: i32,
-) -> bool {
- let px_logical = px as f32 / scale as f32;
- let py_logical = py as f32 / scale as f32;
- const CORNER_THRESHOLD: f32 = 16.0;
-
- match (side, region) {
- ("left", HighlightRegion::TopLeft) => py_logical < CORNER_THRESHOLD,
- ("left", HighlightRegion::BottomLeft) => py_logical > (logical_height as f32 - CORNER_THRESHOLD),
- ("left", HighlightRegion::Left) => py_logical >= CORNER_THRESHOLD && py_logical <= (logical_height as f32 - CORNER_THRESHOLD),
-
- ("right", HighlightRegion::TopRight) => py_logical < CORNER_THRESHOLD,
- ("right", HighlightRegion::BottomRight) => py_logical > (logical_height as f32 - CORNER_THRESHOLD),
- ("right", HighlightRegion::Right) => py_logical >= CORNER_THRESHOLD && py_logical <= (logical_height as f32 - CORNER_THRESHOLD),
-
- ("bottom", HighlightRegion::BottomLeft) => px_logical < CORNER_THRESHOLD,
- ("bottom", HighlightRegion::BottomRight) => px_logical > (logical_width as f32 - CORNER_THRESHOLD),
- ("bottom", HighlightRegion::Bottom) => px_logical >= CORNER_THRESHOLD && px_logical <= (logical_width as f32 - CORNER_THRESHOLD),
-
- ("top", HighlightRegion::TopLeft) => px_logical < CORNER_THRESHOLD,
- ("top", HighlightRegion::TopRight) => px_logical > (logical_width as f32 - CORNER_THRESHOLD),
- ("top", HighlightRegion::Top) => px_logical >= CORNER_THRESHOLD && px_logical <= (logical_width as f32 - CORNER_THRESHOLD),
-
- _ => false,
- }
-}
-
-fn blend_colors(bg: u32, fg: u32) -> u32 {
- let fg_a = (fg >> 24) & 0xFF;
- if fg_a == 0 {
- return bg;
- }
- if fg_a == 255 {
- return fg;
- }
- let bg_a = (bg >> 24) & 0xFF;
- let bg_r = (bg >> 16) & 0xFF;
- let bg_g = (bg >> 8) & 0xFF;
- let bg_b = bg & 0xFF;
-
- let fg_r = (fg >> 16) & 0xFF;
- let fg_g = (fg >> 8) & 0xFF;
- let fg_b = fg & 0xFF;
-
- let out_r = ((fg_r * fg_a) + (bg_r * (255 - fg_a))) / 255;
- let out_g = ((fg_g * fg_a) + (bg_g * (255 - fg_a))) / 255;
- let out_b = ((fg_b * fg_a) + (bg_b * (255 - fg_a))) / 255;
- let out_a = bg_a + ((255 - bg_a) * fg_a) / 255;
-
- (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b
-}
diff --git a/src/client/input.rs b/src/client/input.rs
deleted file mode 100644
index 28b2401..0000000
--- a/src/client/input.rs
+++ /dev/null
@@ -1,1063 +0,0 @@
-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 std::sync::atomic::{AtomicI32, Ordering};
-use tokio::io::AsyncReadExt;
-use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
-use tokio::time::{sleep, Duration};
-use crate::config::InertialConfig;
-
-pub static POINTER_X: AtomicI32 = AtomicI32::new(0);
-pub static POINTER_Y: AtomicI32 = AtomicI32::new(0);
-pub static SCREEN_WIDTH: AtomicI32 = AtomicI32::new(1920);
-pub static SCREEN_HEIGHT: AtomicI32 = AtomicI32::new(1080);
-
-pub fn update_pointer_coords(dx: i32, dy: i32) {
- let screen_w = SCREEN_WIDTH.load(Ordering::SeqCst);
- let screen_h = SCREEN_HEIGHT.load(Ordering::SeqCst);
- let mut current_x = POINTER_X.load(Ordering::SeqCst);
- let mut current_y = POINTER_Y.load(Ordering::SeqCst);
-
- current_x = (current_x + dx).clamp(0, screen_w);
- current_y = (current_y + dy).clamp(0, screen_h);
-
- POINTER_X.store(current_x, Ordering::SeqCst);
- POINTER_Y.store(current_y, Ordering::SeqCst);
-}
-
-pub fn set_pointer_coords(x: i32, y: i32) {
- let screen_w = SCREEN_WIDTH.load(Ordering::SeqCst);
- let screen_h = SCREEN_HEIGHT.load(Ordering::SeqCst);
- let current_x = x.clamp(0, screen_w);
- let current_y = y.clamp(0, screen_h);
-
- POINTER_X.store(current_x, Ordering::SeqCst);
- POINTER_Y.store(current_y, Ordering::SeqCst);
-}
-
-// 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;
-const UI_SET_KEYBIT: libc::c_ulong = 0x40045565;
-
-// 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,
-}
-
-const UI_SET_ABSBIT: libc::c_ulong = 0x40045567;
-const UI_ABS_SETUP: libc::c_ulong = 1075598596; // 0x40185568
-
-#[repr(C)]
-#[derive(Debug, Clone, Copy)]
-struct InputAbsinfo {
- value: i32,
- minimum: i32,
- maximum: i32,
- fuzz: i32,
- flat: i32,
- resolution: i32,
-}
-
-#[repr(C)]
-#[derive(Debug, Clone, Copy)]
-struct UinputAbsSetup {
- code: u16,
- _padding: u16,
- absinfo: InputAbsinfo,
-}
-
-#[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,
- raw_x: Option<i32>,
- raw_y: Option<i32>,
-}
-
-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, Clone)]
-pub enum InputDaemonMsg {
- UpdateConfig(InertialConfig, bool),
- SimulateMoveTo { x: i32, y: i32 },
- SimulateMoveBy { dx: i32, dy: i32 },
- SimulateButton { button: u16, press: bool },
- SimulateKey { keycode: u16, press: bool },
- SimulateClick { button: u16 },
- SimulateKeyPress { keycode: u16 },
- SimulateScroll { dx: i32, dy: i32 },
-}
-
-#[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 },
- FingersReport(Vec<FingerState>),
- DaemonMsg(InputDaemonMsg),
- InternalReleaseButton { button: u16 },
- InternalReleaseKey { keycode: u16 },
-}
-
-fn setup_uinput_mouse() -> 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());
- }
-
- if libc::ioctl(fd, UI_SET_EVBIT, EV_KEY as libc::c_int) < 0 {
- return Err(std::io::Error::last_os_error());
- }
- for key in 1..=511 {
- if libc::ioctl(fd, UI_SET_KEYBIT, key as libc::c_int) < 0 {
- return Err(std::io::Error::last_os_error());
- }
- }
- for btn in 272..=276 {
- if libc::ioctl(fd, UI_SET_KEYBIT, btn 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 mouse device.");
- Ok(file)
-}
-
-fn setup_uinput_abs() -> 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_ABS as libc::c_int) < 0 {
- return Err(std::io::Error::last_os_error());
- }
- if libc::ioctl(fd, UI_SET_ABSBIT, ABS_X as libc::c_int) < 0 {
- return Err(std::io::Error::last_os_error());
- }
- let abs_x_setup = UinputAbsSetup {
- code: ABS_X,
- _padding: 0,
- absinfo: InputAbsinfo {
- value: 0,
- minimum: 0,
- maximum: 1920,
- fuzz: 0,
- flat: 0,
- resolution: 1,
- },
- };
- if libc::ioctl(fd, UI_ABS_SETUP, &abs_x_setup as *const UinputAbsSetup as *const libc::c_void) < 0 {
- return Err(std::io::Error::last_os_error());
- }
-
- if libc::ioctl(fd, UI_SET_ABSBIT, ABS_Y as libc::c_int) < 0 {
- return Err(std::io::Error::last_os_error());
- }
- let abs_y_setup = UinputAbsSetup {
- code: ABS_Y,
- _padding: 0,
- absinfo: InputAbsinfo {
- value: 0,
- minimum: 0,
- maximum: 1200,
- fuzz: 0,
- flat: 0,
- resolution: 1,
- },
- };
- if libc::ioctl(fd, UI_ABS_SETUP, &abs_y_setup as *const UinputAbsSetup as *const libc::c_void) < 0 {
- return Err(std::io::Error::last_os_error());
- }
-
- if libc::ioctl(fd, UI_SET_EVBIT, EV_KEY as libc::c_int) < 0 {
- return Err(std::io::Error::last_os_error());
- }
- for btn in 272..=276 {
- if libc::ioctl(fd, UI_SET_KEYBIT, btn 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: 0x5679,
- version: 1,
- },
- name: [0; 80],
- ff_effects_max: 0,
- };
-
- let name_bytes = b"Clear Virtual Absolute Pointer";
- 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 absolute pointer 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_absolute(file: &mut std::fs::File, x: i32, y: i32) -> std::io::Result<()> {
- write_raw_event(file, EV_ABS, ABS_X, x)?;
- write_raw_event(file, EV_ABS, ABS_Y, y)?;
- write_raw_event(file, EV_SYN, SYN_REPORT, 0)?;
- Ok(())
-}
-
-fn write_mouse_relative(file: &mut std::fs::File, dx: i32, dy: i32) -> std::io::Result<()> {
- if dx == 0 && dy == 0 {
- return Ok(());
- }
- 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 {
- 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(); 16];
- let mut current_slot = 0usize;
- let mut buf = [0u8; 24];
-
- let mut touch = TouchTracker {
- 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;
- let slot_idx = if event.code == ABS_X { 0 } else { current_slot };
- let is_primary = slots.iter().position(|s| s.active) == Some(slot_idx);
- let slot = &mut slots[slot_idx];
- if is_primary {
- if let Some(lx) = slot.raw_x {
- touch.dx += val - lx;
- }
- }
- slot.raw_x = Some(val);
- 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;
- let slot_idx = if event.code == ABS_Y { 0 } else { current_slot };
- let is_primary = slots.iter().position(|s| s.active) == Some(slot_idx);
- let slot = &mut slots[slot_idx];
- if is_primary {
- if let Some(ly) = slot.raw_y {
- touch.dy += val - ly;
- }
- }
- slot.raw_y = Some(val);
- slot.y = (val - min_y) as f32 / range_y;
- } else if event.code == ABS_MT_TRACKING_ID {
- if event.value >= 0 {
- slots[current_slot].active = true;
- touch.finger_down = true;
- } else {
- slots[current_slot].active = false;
- slots[current_slot].raw_x = None;
- slots[current_slot].raw_y = None;
- touch.finger_down = slots.iter().any(|s| s.active);
- }
- } else if event.code == ABS_MT_SLOT {
- current_slot = (event.value as usize).min(15);
- }
- } 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;
- for s in &mut slots {
- s.active = false;
- s.raw_x = None;
- s.raw_y = None;
- }
- }
- }
- } 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;
- for s in &mut slots {
- s.raw_x = None;
- s.raw_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,
- three_finger_start_x: Option<f32>,
- three_finger_start_y: Option<f32>,
- three_finger_gesture_triggered: bool,
-}
-
-fn send_ipc_cmd(ipc_tx: &std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>, cmd: String) {
- let (reply_tx, _) = std::sync::mpsc::channel();
- let _ = ipc_tx.send(crate::ipc_server::IpcRequest { command: cmd, reply_tx });
-}
-
-fn trigger_tap_to_click_ipc(tap: bool, ipc_tx: &std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>, pipe_write: libc::c_int) {
- let cmd = format!("input tap-to-click {}", tap);
- send_ipc_cmd(ipc_tx, 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<crate::ipc_server::IpcRequest>, pipe_write: libc::c_int) {
- let cmd = format!("input trackpad-disabled {}", disabled);
- send_ipc_cmd(ipc_tx, cmd);
- unsafe {
- libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
- }
-}
-
-fn trigger_expose_ipc(ipc_tx: &std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>, pipe_write: libc::c_int) {
- let cmd = "expose".to_string();
- send_ipc_cmd(ipc_tx, cmd);
- unsafe {
- libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
- }
-}
-
-fn trigger_expose_exit_ipc(ipc_tx: &std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>, pipe_write: libc::c_int) {
- let cmd = "expose-exit".to_string();
- send_ipc_cmd(ipc_tx, cmd);
- unsafe {
- libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
- }
-}
-
-fn trigger_view_next_ipc(ipc_tx: &std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>, pipe_write: libc::c_int) {
- let cmd = "view-next".to_string();
- send_ipc_cmd(ipc_tx, cmd);
- unsafe {
- libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
- }
-}
-
-fn trigger_view_prev_ipc(ipc_tx: &std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>, pipe_write: libc::c_int) {
- let cmd = "view-prev".to_string();
- send_ipc_cmd(ipc_tx, 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<InputDaemonMsg>,
- ipc_tx: std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>,
- 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_mouse_file = match setup_uinput_mouse() {
- Ok(f) => f,
- Err(e) => {
- eprintln!("[input-subsystem] FATAL: Could not initialize uinput mouse: {}.", e);
- return Err(Box::new(e) as Box<dyn std::error::Error>);
- }
- };
-
- let mut uinput_abs_file = match setup_uinput_abs() {
- Ok(f) => f,
- Err(e) => {
- eprintln!("[input-subsystem] FATAL: Could not initialize uinput absolute pointer: {}.", 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 = crate::paths::get_input_coords_socket_path();
- 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(InputDaemonMsg::UpdateConfig(res, tap)) => (res, tap),
- _ => {
- 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(msg) = event_queue_rx.recv().await {
- let _ = tx_config.send(CoordinatorMsg::DaemonMsg(msg));
- }
- });
-
- // 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,
- three_finger_start_x: None,
- three_finger_start_y: None,
- three_finger_gesture_triggered: false,
- };
-
- let mut tick_interval = tokio::time::interval(Duration::from_millis(16)); // ~60fps
-
- loop {
- tokio::select! {
- Some(msg) = rx.recv() => {
- match msg {
- CoordinatorMsg::DaemonMsg(daemon_msg) => {
- match daemon_msg {
- InputDaemonMsg::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);
- }
- }
- InputDaemonMsg::SimulateMoveTo { x, y } => {
- set_pointer_coords(x, y);
- let px = POINTER_X.load(Ordering::SeqCst);
- let py = POINTER_Y.load(Ordering::SeqCst);
- let _ = write_mouse_absolute(&mut uinput_abs_file, px, py);
- }
- InputDaemonMsg::SimulateMoveBy { dx, dy } => {
- update_pointer_coords(dx, dy);
- let _ = write_mouse_relative(&mut uinput_mouse_file, dx, dy);
- }
- InputDaemonMsg::SimulateButton { button, press } => {
- let val = if press { 1 } else { 0 };
- let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, val);
- let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
- }
- InputDaemonMsg::SimulateKey { keycode, press } => {
- let val = if press { 1 } else { 0 };
- let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, val);
- let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
- }
- InputDaemonMsg::SimulateClick { button } => {
- let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, 1);
- let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
- let tx_clone = tx.clone();
- tokio::spawn(async move {
- tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
- let _ = tx_clone.send(CoordinatorMsg::InternalReleaseButton { button });
- });
- }
- InputDaemonMsg::SimulateKeyPress { keycode } => {
- let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, 1);
- let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
- let tx_clone = tx.clone();
- tokio::spawn(async move {
- tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
- let _ = tx_clone.send(CoordinatorMsg::InternalReleaseKey { keycode });
- });
- }
- InputDaemonMsg::SimulateScroll { dx, dy } => {
- let _ = write_scroll(&mut uinput_mouse_file, dx, dy);
- }
- }
- }
- CoordinatorMsg::InternalReleaseButton { button } => {
- let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, 0);
- let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
- }
- CoordinatorMsg::InternalReleaseKey { keycode } => {
- let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, 0);
- let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
- }
- CoordinatorMsg::PhysicalMove { dx, dy, timestamp } => {
- update_pointer_coords(dx, dy);
- 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 } => {
- update_pointer_coords(dx, dy);
- 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);
- }
-
- // Detect 3-finger gestures (swipe up, down, left, right)
- if fingers.len() == 3 {
- let avg_x = (fingers[0].x + fingers[1].x + fingers[2].x) / 3.0;
- let avg_y = (fingers[0].y + fingers[1].y + fingers[2].y) / 3.0;
- if let (Some(start_x), Some(start_y)) = (state.three_finger_start_x, state.three_finger_start_y) {
- let dy_up = start_y - avg_y; // Y decreases as fingers move up
- let dy_down = avg_y - start_y; // Y increases as fingers move down
- let dx_right = avg_x - start_x; // X increases as fingers move right
- let dx_left = start_x - avg_x; // X decreases as fingers move left
-
- if dy_up > 0.15 && !state.three_finger_gesture_triggered {
- state.three_finger_gesture_triggered = true;
- println!("[input-subsystem] 3-finger swipe up gesture detected. Triggering Expose mode.");
- trigger_expose_ipc(&ipc_tx, pipe_write);
- } else if dy_down > 0.15 && !state.three_finger_gesture_triggered {
- state.three_finger_gesture_triggered = true;
- println!("[input-subsystem] 3-finger swipe down gesture detected. Triggering Expose exit.");
- trigger_expose_exit_ipc(&ipc_tx, pipe_write);
- } else if dx_right > 0.15 && !state.three_finger_gesture_triggered {
- state.three_finger_gesture_triggered = true;
- println!("[input-subsystem] 3-finger swipe right gesture detected. Switching to previous tag.");
- trigger_view_prev_ipc(&ipc_tx, pipe_write);
- } else if dx_left > 0.15 && !state.three_finger_gesture_triggered {
- state.three_finger_gesture_triggered = true;
- println!("[input-subsystem] 3-finger swipe left gesture detected. Switching to next tag.");
- trigger_view_next_ipc(&ipc_tx, pipe_write);
- }
- } else {
- state.three_finger_start_x = Some(avg_x);
- state.three_finger_start_y = Some(avg_y);
- }
- } else {
- state.three_finger_start_x = None;
- state.three_finger_start_y = None;
- state.three_finger_gesture_triggered = false;
- }
-
- 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 {
- update_pointer_coords(steps_x, steps_y);
- let _ = write_mouse_relative(&mut uinput_mouse_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 {
- update_pointer_coords(steps_x, steps_y);
- let _ = write_mouse_relative(&mut uinput_mouse_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_mouse_file, steps_x, steps_y);
- }
- }
- }
- }
- }
- }
- }
- });
-
- res
-}
diff --git a/src/client/ipc.rs b/src/client/ipc.rs
deleted file mode 100644
index b09f6df..0000000
--- a/src/client/ipc.rs
+++ /dev/null
@@ -1,1571 +0,0 @@
-// IPC command parser for cce-client
-// Ported from handle_ipc_command in cce-client.c
-
-use crate::config::{parse_keysym, spawn_command_bg};
-use crate::types::{
- parse_action, parse_button, parse_hex_color, parse_modifiers, parse_tiling_mode, Action,
- ModeRule, PendingPointerBinding, PendingXkbBinding, WindowManager, NUM_TAGS, TilingMode,
-};
-
-fn get_window_under_pointer(state: &WindowManager) -> Option<u64> {
- // 1. Check seat.hovered_window_id first
- for seat in &state.seats {
- if !seat.removed {
- if let Some(wid) = seat.hovered_window_id {
- return Some(wid);
- }
- }
- }
-
- // 2. Fallback: find it using POINTER_X/POINTER_Y and window geometries
- let px = crate::input::POINTER_X.load(std::sync::atomic::Ordering::SeqCst) as f64;
- let py = crate::input::POINTER_Y.load(std::sync::atomic::Ordering::SeqCst) as f64;
-
- let active_tags = state.active_tags;
- let mut best_wid = None;
-
- for win in &state.windows {
- if win.closed
- || (win.tags & active_tags) == 0
- || win.app_id.as_deref() == Some("cce-status-interface")
- || win.tiling_mode == TilingMode::Popup
- {
- continue;
- }
- let wx = win.anim_x.unwrap_or(win.x as f64);
- let wy = win.anim_y.unwrap_or(win.y as f64);
- let ww = win.anim_w.unwrap_or(win.width as f64);
- let wh = win.anim_h.unwrap_or(win.height as f64);
-
- if px >= wx && px <= wx + ww && py >= wy && py <= wy + wh {
- best_wid = Some(win.id);
- }
- }
-
- best_wid
-}
-
-/// Handle an IPC command string, modifying the window manager state.
-/// This is called from the Unix socket listener when clearctl sends a command.
-pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
- // Strip trailing newlines/spaces
- let cmd = cmd.trim_end_matches(|c| c == '\n' || c == '\r' || c == ' ');
- if cmd.is_empty() {
- return "\n".to_string();
- }
-
- // Split the command into tokens
- let tokens: Vec<&str> = cmd.splitn(2, ' ').collect();
- let tok = tokens[0];
- let rest = if tokens.len() > 1 { tokens[1] } else { "" };
-
- let mut reply = "ok\n".to_string();
-
- match tok {
- "spawn" => {
- if !rest.is_empty() {
- spawn_command_bg(rest);
- }
- }
- "close" => {
- // Close the focused window
- if let Some(seat) = state.seats.first() {
- if let Some(focused_id) = seat.focused_window_id {
- if let Some(window) = state.get_window_mut(focused_id) {
- window.closed = true;
- }
- }
- }
- state.needs_render = true;
- }
- "minimize" => {
- if let Some(seat) = state.seats.first() {
- if let Some(focused_id) = seat.focused_window_id {
- if let Some(window) = state.get_window_mut(focused_id) {
- window.minimized = true;
- }
- // Shift focus to the next visible window
- let active_tags = state.active_tags;
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- let next_id = visible_ids.last().copied();
- for s in &mut state.seats {
- if !s.removed {
- s.focused_window_id = next_id;
- }
- }
- }
- }
- state.needs_render = true;
- state.needs_focus = true;
- state.needs_status_update = true;
- }
- "focus-next" => {
- // Focus the next visible window (wrapping) and move it to the
- // front of the cascade stack (end of windows vector).
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- let focused_id = seat.focused_window_id;
- let active_tags = state.active_tags;
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- if visible_ids.len() > 1 {
- if let Some(fid) = focused_id {
- if let Some(idx) = visible_ids.iter().position(|id| *id == fid) {
- let next_idx = (idx + 1) % visible_ids.len();
- let next_id = visible_ids[next_idx];
- seat.focused_window_id = Some(next_id);
- // Move newly focused window to front of cascade stack
- state.move_window_to_end(next_id);
- }
- }
- }
- }
- state.needs_render = true;
- state.needs_focus = true;
- state.needs_status_update = true;
- }
- "focus-prev" => {
- // Focus the previous visible window (wrapping) and move it to the
- // front of the cascade stack (end of windows vector).
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- let focused_id = seat.focused_window_id;
- let active_tags = state.active_tags;
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- if visible_ids.len() > 1 {
- if let Some(fid) = focused_id {
- if let Some(idx) = visible_ids.iter().position(|id| *id == fid) {
- let prev_idx = if idx == 0 { visible_ids.len() - 1 } else { idx - 1 };
- let prev_id = visible_ids[prev_idx];
- seat.focused_window_id = Some(prev_id);
- // Move newly focused window to front of cascade stack
- state.move_window_to_end(prev_id);
- }
- }
- }
- }
- state.needs_render = true;
- state.needs_focus = true;
- state.needs_status_update = true;
- }
- "exit" => {
- // Signal exit request
- }
- "restart" => {
- crate::restart::wm_restart();
- }
- "reload" => {
- crate::restart::wm_reload(state);
- }
- "expose" => {
- let active = !state.expose_active;
- if !active {
- // Exiting expose mode! Focus the window under pointer.
- if let Some(wid) = get_window_under_pointer(state) {
- for seat in &mut state.seats {
- if !seat.removed {
- seat.focused_window_id = Some(wid);
- }
- }
- state.move_window_to_end(wid);
- state.needs_focus = true;
- }
- }
- crate::wm::set_expose_active(state, active);
- state.needs_render = true;
- state.needs_status_update = true;
- }
- "expose-exit" => {
- if state.expose_active {
- let hovered_id = get_window_under_pointer(state);
- crate::wm::set_expose_active(state, false);
- if let Some(wid) = hovered_id {
- for seat in &mut state.seats {
- if !seat.removed {
- seat.focused_window_id = Some(wid);
- }
- }
- state.move_window_to_end(wid);
- state.needs_focus = true;
- }
- state.needs_render = true;
- state.needs_status_update = true;
- } else {
- spawn_command_bg("clear-cloud --apps");
- }
- }
- "view-next" => {
- let current_tag_idx = (0..NUM_TAGS).find(|&i| (state.active_tags & (1 << i)) != 0).unwrap_or(0);
- let next_tag_idx = (current_tag_idx + 1) % NUM_TAGS;
- state.active_tags = 1 << next_tag_idx;
-
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- seat.focused_window_id = visible_ids.last().copied();
- }
- state.needs_render = true;
- state.needs_focus = true;
- state.needs_status_update = true;
- }
- "view-prev" => {
- let current_tag_idx = (0..NUM_TAGS).find(|&i| (state.active_tags & (1 << i)) != 0).unwrap_or(0);
- let prev_tag_idx = (current_tag_idx + NUM_TAGS - 1) % NUM_TAGS;
- state.active_tags = 1 << prev_tag_idx;
-
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- seat.focused_window_id = visible_ids.last().copied();
- }
- state.needs_render = true;
- state.needs_focus = true;
- state.needs_status_update = true;
- }
- "view" | _ if tok.starts_with("view") => {
- let tag = parse_tag_from_command(tok, "view", rest);
- if let Some(tag) = tag {
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- state.active_tags = 1 << (tag - 1);
-
- // Reassign focus to a visible window on the new tag
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- seat.focused_window_id = visible_ids.last().copied();
- }
- state.needs_render = true;
- state.needs_focus = true;
- state.needs_status_update = true;
- }
- }
- }
- "toggle" | _ if tok.starts_with("toggle") => {
- let tag = parse_tag_from_command(tok, "toggle", rest);
- if let Some(tag) = tag {
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- state.active_tags ^= 1 << (tag - 1);
-
- // If the focused window is no longer visible, reassign focus
- let focused_id = state
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
- let focused_still_visible = focused_id.map_or(false, |fid| {
- state
- .get_window(fid)
- .map_or(false, |w| (w.tags & state.active_tags) != 0 && !w.closed)
- });
- if !focused_still_visible {
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- seat.focused_window_id = visible_ids.last().copied();
- }
- state.needs_focus = true;
- }
- state.needs_render = true;
- state.needs_status_update = true;
- }
- }
- }
- "config-done" => {
- state.config_done = true;
- }
- "layout" => {
- handle_layout_command(rest, state);
- }
- "mode" => {
- handle_mode_command(rest, state);
- }
- "set-mode" => {
- handle_set_mode_command(rest, state);
- }
- "apply-mode-sharing" => {
- handle_apply_mode_sharing_command(rest, state);
- }
- "mode-next-shared" => {
- let cycle = [
- TilingMode::Cascade,
- TilingMode::Grid,
- TilingMode::Fullscreen,
- TilingMode::Floating,
- ];
- let focused_id = state
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
- if let Some(fid) = focused_id {
- let notifications_enable = state.notifications_enable;
- let current_mode = state.get_window(fid).map(|w| w.tiling_mode);
- if let Some(old_mode) = current_mode {
- let next = cycle
- .iter()
- .position(|m| *m == old_mode)
- .map(|i| cycle[(i + 1) % cycle.len()])
- .unwrap_or(TilingMode::Cascade);
-
- let active_tags = state.active_tags;
-
- let mut updated_count = 0;
- for win in &mut state.windows {
- if !win.closed
- && win.app_id.as_deref() != Some("cce-status-interface")
- && (win.tags & active_tags) != 0
- && win.tiling_mode == old_mode
- {
- win.tiling_mode = next;
- win.mode_locked = true;
- updated_count += 1;
- }
- }
-
- if notifications_enable && updated_count > 0 {
- crate::config::show_notification(
- "cce-client",
- &format!(
- "Tiling mode set to {} for all {} windows on active tag",
- next.as_str(),
- old_mode.as_str()
- ),
- );
- }
- state.needs_render = true;
- state.needs_status_update = true;
- }
- }
- }
- "bind" => {
- handle_bind_command(rest, state);
- }
- "pbind" => {
- handle_pbind_command(rest, state);
- }
- "retile" => {
- // Retile is handled by the WM layer - just set a flag
- // The actual tile_windows() call happens in wm.rs
- }
- "tag-layout" => {
- handle_tag_layout_command(rest, state);
- }
- "set-tag" => {
- handle_set_tag_command(rest, state);
- }
- "repeat" => {
- handle_repeat_command(rest, state);
- }
- "input" => {
- handle_input_command(rest, state);
- }
- "notify" => {
- let parts: Vec<&str> = rest.splitn(2, ' ').collect();
- if parts.len() == 2 {
- if state.notifications_enable {
- crate::config::show_notification(parts[0], parts[1]);
- }
- } else if parts.len() == 1 && !parts[0].is_empty() {
- if state.notifications_enable {
- crate::config::show_notification("cce-client", parts[0]);
- }
- }
- }
- "focus-window" => {
- let app_id_or_title = rest.trim();
- if !app_id_or_title.is_empty() {
- let mut found_id = None;
- for win in &state.windows {
- if !win.closed && !win.minimized {
- if let Some(ref app_id) = win.app_id {
- if app_id.eq_ignore_ascii_case(app_id_or_title) {
- found_id = Some(win.id);
- break;
- }
- }
- }
- }
- if found_id.is_none() {
- for win in &state.windows {
- if !win.closed && !win.minimized {
- if let Some(ref title) = win.title {
- if title.to_lowercase().contains(&app_id_or_title.to_lowercase()) {
- found_id = Some(win.id);
- break;
- }
- }
- }
- }
- }
- if let Some(wid) = found_id {
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- seat.focused_window_id = Some(wid);
- state.move_window_to_end(wid);
- state.needs_focus = true;
- state.needs_render = true;
- state.needs_status_update = true;
- reply = format!("ok focused window {}\n", wid);
- } else {
- reply = "error: no active seat\n".to_string();
- }
- } else {
- reply = "error: window not found\n".to_string();
- }
- } else {
- reply = "error: usage: focus-window <app_id|title>\n".to_string();
- }
- }
- "pointer-location" => {
- let px = crate::input::POINTER_X.load(std::sync::atomic::Ordering::SeqCst);
- let py = crate::input::POINTER_Y.load(std::sync::atomic::Ordering::SeqCst);
- reply = format!("{} {}\n", px, py);
- }
- "pointer-move-to" => {
- let parts: Vec<&str> = rest.split_whitespace().collect();
- if parts.len() == 2 {
- if let (Ok(target_x), Ok(target_y)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateMoveTo { x: target_x, y: target_y });
- }
- } else {
- reply = "error: invalid coordinates\n".to_string();
- }
- } else {
- reply = "error: usage: pointer-move-to <x> <y>\n".to_string();
- }
- }
- "pointer-move-by" => {
- let parts: Vec<&str> = rest.split_whitespace().collect();
- if parts.len() == 2 {
- if let (Ok(dx), Ok(dy)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateMoveBy { dx, dy });
- }
- } else {
- reply = "error: invalid deltas\n".to_string();
- }
- } else {
- reply = "error: usage: pointer-move-by <dx> <dy>\n".to_string();
- }
- }
- "pointer-scroll" => {
- let parts: Vec<&str> = rest.split_whitespace().collect();
- if parts.len() == 2 {
- if let (Ok(dx), Ok(dy)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateScroll { dx, dy });
- }
- } else {
- reply = "error: invalid deltas\n".to_string();
- }
- } else {
- reply = "error: usage: pointer-scroll <dx> <dy>\n".to_string();
- }
- }
- "pointer-click" => {
- let btn = parse_button(rest) as u16;
- if btn != 0 {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateClick { button: btn });
- }
- } else {
- reply = "error: invalid button\n".to_string();
- }
- }
- "pointer-press" => {
- let btn = parse_button(rest) as u16;
- if btn != 0 {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateButton { button: btn, press: true });
- }
- } else {
- reply = "error: invalid button\n".to_string();
- }
- }
- "pointer-release" => {
- let btn = parse_button(rest) as u16;
- if btn != 0 {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateButton { button: btn, press: false });
- }
- } else {
- reply = "error: invalid button\n".to_string();
- }
- }
- "keypress" => {
- if let Some(key) = parse_keycode(rest) {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateKeyPress { keycode: key });
- }
- } else {
- reply = "error: invalid key\n".to_string();
- }
- }
- "key-press" => {
- if let Some(key) = parse_keycode(rest) {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateKey { keycode: key, press: true });
- }
- } else {
- reply = "error: invalid key\n".to_string();
- }
- }
- "key-release" => {
- if let Some(key) = parse_keycode(rest) {
- if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateKey { keycode: key, press: false });
- }
- } else {
- reply = "error: invalid key\n".to_string();
- }
- }
- _ => {
- reply = "error: unknown command\n".to_string();
- }
- }
-
- reply
-}
-
-/// Parse a tag number from a command like "view-1" or "view 1"
-fn parse_tag_from_command(tok: &str, prefix: &str, rest: &str) -> Option<i32> {
- let after_prefix = &tok[prefix.len()..];
- if after_prefix.starts_with('-') {
- after_prefix[1..].parse::<i32>().ok()
- } else if !rest.is_empty() {
- rest.trim()
- .split_whitespace()
- .next()
- .and_then(|s| s.parse::<i32>().ok())
- } else {
- None
- }
-}
-
-/// Handle "layout <param> <value>" command
-fn handle_layout_command(rest: &str, state: &mut WindowManager) {
- let parts: Vec<&str> = rest.split_whitespace().collect();
- if parts.len() < 2 {
- return;
- }
- let param = parts[0];
- let value_str = parts[1];
-
- match param {
- "gap" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.gap = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Gap set to {}px", value));
- }
- }
- }
- "gap_top" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.gap_top = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Top gap set to {}px", value));
- }
- }
- }
- "gap_left" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.gap_left = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Left gap set to {}px", value));
- }
- }
- }
- "gap_right" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.gap_right = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Right gap set to {}px", value));
- }
- }
- }
- "gap_bottom" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.gap_bottom = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Bottom gap set to {}px", value));
- }
- }
- }
- "cascade_offset" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.cascade_offset = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Cascade offset set to {}px", value));
- }
- }
- }
- "bar_height" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.bar_height = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Bar height set to {}px", value));
- }
- }
- }
- "border_width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.border_width = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Border width set to {}px", value));
- }
- }
- }
- "border_font_size" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.border_font_size = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Border font size set to {}px", value));
- }
- }
- }
- "transition_duration" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.transition_duration = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Transition duration set to {}ms", value));
- }
- }
- }
- "fullscreen_border_width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.fullscreen_border_width = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Fullscreen border width set to {}px", value));
- }
- }
- }
- "cascade_border_width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.cascade_border_width = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Cascade border width set to {}px", value));
- }
- }
- }
- "grid_gap" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.grid_gap = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Grid gap set to {}px", value));
- }
- }
- }
- "grid_border_width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.grid_border_width = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Grid border width set to {}px", value));
- }
- }
- }
- "floating_border_width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.floating_border_width = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Floating border width set to {}px", value));
- }
- }
- }
- "border_color" | "high_color" => {
- if let Some((r, g, b, a)) = parse_hex_color(value_str) {
- state.layout.border_r = r;
- state.layout.border_g = g;
- state.layout.border_b = b;
- state.layout.border_a = a;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Border color set to {}", value_str));
- }
- }
- }
- "background_color" | "low_color" => {
- if let Some((r, g, b, a)) = parse_hex_color(value_str) {
- state.layout.background_r = r;
- state.layout.background_g = g;
- state.layout.background_b = b;
- state.layout.background_a = a;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Background color set to {}", value_str));
- }
- }
- }
- "side_panel_behavior" | "side-panel-behavior" => {
- state.layout.side_panel_behavior = value_str.to_string();
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Side panel behavior set to {}", value_str));
- }
- }
- "side_panel_width" | "side-panel-width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.side_panel_width = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Side panel width set to {}px", value));
- }
- }
- }
- "side_panel_position" | "side-panel-position" => {
- state.layout.side_panel_position = value_str.to_string();
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Side panel position set to {}", value_str));
- }
- }
- "side_panel_border_gap" | "side-panel-border-gap" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.side_panel_border_gap = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Side panel border gap set to {}px", value));
- }
- }
- }
- "side_panel_border_opacity" | "side-panel-border-opacity" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.side_panel_border_opacity = value;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Side panel border opacity set to {}%", value));
- }
- }
- }
- _ => {}
- }
- state.needs_render = true;
-}
-
-/// Handle "mode <mode> <app_id_pattern> [--single] [--tag N] [title_pattern]" command
-fn handle_mode_command(rest: &str, state: &mut WindowManager) {
- let mut parts: Vec<&str> = rest.split_whitespace().collect();
- if parts.len() < 2 {
- return;
- }
-
- let mode_str = parts[0];
- let app_id_pattern = parts[1].to_string();
-
- // Remove the first two tokens
- parts.drain(0..2);
-
- let mut title_pattern: Option<String> = None;
- let mut single_instance = false;
- let mut tag = 0i32;
-
- let mut i = 0;
- while i < parts.len() {
- if parts[i] == "--single" {
- single_instance = true;
- } else if parts[i] == "--tag" {
- if i + 1 < parts.len() {
- tag = parts[i + 1].parse::<i32>().unwrap_or(0);
- i += 1;
- }
- } else if title_pattern.is_none() {
- title_pattern = Some(parts[i].to_string());
- }
- i += 1;
- }
-
- let mode = parse_tiling_mode(mode_str);
- if let Some(existing) = state.mode_rules.iter_mut().find(|r| {
- r.app_id_pattern == app_id_pattern && r.title_pattern == title_pattern
- }) {
- existing.mode = mode;
- existing.single_instance = single_instance;
- existing.tag = tag;
- } else {
- state.mode_rules.push(ModeRule {
- mode,
- app_id_pattern,
- title_pattern,
- single_instance,
- tag,
- circular: false,
- ssd: None,
- });
- }
-}
-
-/// Handle "set-mode <mode>" command — set the focused window's tiling mode
-fn handle_set_mode_command(rest: &str, state: &mut WindowManager) {
- let mode_str = rest.trim();
- if mode_str.is_empty() {
- return;
- }
- let mode = parse_tiling_mode(mode_str);
- let notifications_enable = state.notifications_enable;
- if let Some(window) = state.focused_window_mut() {
- let old_mode = window.tiling_mode;
- window.tiling_mode = mode;
- window.mode_locked = true;
- if (mode == TilingMode::Floating || mode == TilingMode::Popup)
- && old_mode != TilingMode::Floating
- && old_mode != TilingMode::Popup
- {
- window.width = 0;
- window.height = 0;
- }
- if notifications_enable {
- let win_title = window.title.as_deref().unwrap_or("Window");
- crate::config::show_notification("cce-client", &format!("Tiling mode set to {} for: {}", mode.as_str(), win_title));
- }
- state.needs_render = true;
- state.needs_status_update = true;
- }
-}
-
-/// Handle "apply-mode-sharing <mode>" command — apply <mode> to all windows sharing a tiling mode with the focused window.
-fn handle_apply_mode_sharing_command(rest: &str, state: &mut WindowManager) {
- let mode_str = rest.trim();
- if mode_str.is_empty() {
- return;
- }
- let new_mode = parse_tiling_mode(mode_str);
-
- // Find the focused window's current tiling mode
- let old_mode = if let Some(window) = state.focused_window() {
- window.tiling_mode
- } else {
- return;
- };
-
- let notifications_enable = state.notifications_enable;
-
- // Iterate over all windows and update tiling mode for matching windows
- for window in &mut state.windows {
- if !window.closed && window.tiling_mode == old_mode {
- let win_old_mode = window.tiling_mode;
- window.tiling_mode = new_mode;
- window.mode_locked = true;
- if (new_mode == TilingMode::Floating || new_mode == TilingMode::Popup)
- && win_old_mode != TilingMode::Floating
- && win_old_mode != TilingMode::Popup
- {
- window.width = 0;
- window.height = 0;
- }
- }
- }
-
- if notifications_enable {
- crate::config::show_notification("cce-client", &format!("Applied tiling mode {} to all windows sharing mode {}", new_mode.as_str(), old_mode.as_str()));
- }
- state.needs_render = true;
- state.needs_status_update = true;
-}
-
-/// Handle "bind <mods> <key> <action> [command]" command
-fn handle_bind_command(rest: &str, state: &mut WindowManager) {
- // Format: bind <mods> <key> <action> [command...]
- let parts: Vec<&str> = rest.splitn(3, ' ').collect();
- if parts.len() < 3 {
- return;
- }
- let mod_str = parts[0];
- let key_str = parts[1];
- let action_and_cmd = parts[2];
-
- let mods = parse_modifiers(mod_str);
- let keysym = parse_keysym(key_str);
-
- // Split action from command
- let (action_str, command) = if let Some(space_pos) = action_and_cmd.find(' ') {
- let (a, c) = action_and_cmd.split_at(space_pos);
- (a, Some(c.trim_start().to_string()))
- } else {
- (action_and_cmd, None)
- };
-
- let action = parse_action(action_str);
- let command = if action == Action::Spawn || action == Action::Toggle {
- command
- } else {
- None
- };
-
- state.pending_bindings.push(PendingXkbBinding {
- mods,
- keysym,
- action,
- command,
- });
-}
-
-/// Handle "pbind <mods> <button> <action>" command
-fn handle_pbind_command(rest: &str, state: &mut WindowManager) {
- let parts: Vec<&str> = rest.split_whitespace().collect();
- if parts.len() < 3 {
- return;
- }
- let mod_str = parts[0];
- let button_str = parts[1];
- let action_str = parts[2];
-
- let mods = parse_modifiers(mod_str);
- let button = parse_button(button_str);
- let action = parse_action(action_str);
-
- state.pending_pointer_bindings.push(PendingPointerBinding {
- mods,
- button,
- action,
- });
-}
-
-/// Handle "tag-layout <tag> <mode>" command
-fn handle_tag_layout_command(rest: &str, state: &mut WindowManager) {
- let parts: Vec<&str> = rest.split_whitespace().collect();
- if parts.len() < 2 {
- return;
- }
- if let Ok(tag) = parts[0].parse::<i32>() {
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- let mode = parse_tiling_mode(parts[1]);
- state.tag_layouts[tag as usize - 1] = mode;
- state.has_tag_layout[tag as usize - 1] = true;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Tag {} layout set to {}", tag, mode.as_str()));
- }
- state.needs_render = true;
- state.needs_status_update = true;
- }
- }
-}
-
-/// Handle "set-tag <tag>" command — set the focused window's tag
-fn handle_set_tag_command(rest: &str, state: &mut WindowManager) {
- let tag_str = rest.trim();
- if let Ok(tag) = tag_str.parse::<i32>() {
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- // Read focused_id before any mutable borrow
- let focused_id = state
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
- if let Some(focused_id) = focused_id {
- // Set the window's tag
- let active_tags = state.active_tags;
- let window_left_active_tag =
- state.get_window_mut(focused_id).map_or(false, |window| {
- window.tags = 1 << (tag - 1);
- (window.tags & active_tags) == 0
- });
-
- // If the window is no longer on an active tag, shift focus
- if window_left_active_tag {
- let visible_ids: Vec<u64> = state
- .windows
- .iter()
- .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
- seat.focused_window_id = visible_ids.last().copied();
- }
- state.needs_focus = true;
- }
- state.needs_render = true;
- state.needs_status_update = true;
- }
- }
- }
-}
-
-/// Handle "repeat <rate> <delay>" command
-fn handle_repeat_command(_rest: &str, _state: &mut WindowManager) {
- // The actual repeat rate change is applied to input devices via the
- // Wayland protocol; this is a placeholder for the pure-logic layer.
- // The WM integration layer will read the rate/delay and call
- // river_input_device_v1_set_repeat_info()
-}
-
-fn handle_input_command(rest: &str, state: &mut WindowManager) {
- let tokens: Vec<&str> = rest.splitn(2, ' ').collect();
- let param = tokens[0];
- let value_str = if tokens.len() > 1 { tokens[1] } else { "" };
-
- match param {
- "tap-to-click" | "tap_to_click" => {
- let old_val = state.tap_to_click;
- match value_str {
- "true" | "1" | "enabled" => {
- state.tap_to_click = true;
- state.tap_config_applied = false; // re-apply
- }
- "false" | "0" | "disabled" => {
- state.tap_to_click = false;
- state.tap_config_applied = false; // re-apply
- }
- "toggle" => {
- state.tap_to_click = !state.tap_to_click;
- state.tap_config_applied = false; // re-apply
- }
- _ => {}
- }
- if state.tap_to_click != old_val && state.notifications_enable {
- crate::config::show_notification(
- "cce-client",
- &format!(
- "Tap-to-click {}",
- if state.tap_to_click { "enabled" } else { "disabled" }
- ),
- );
- }
- }
- "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("cce-client", &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("cce-client", &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(
- "cce-client",
- &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(
- "cce-client",
- &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(
- "cce-client",
- &format!(
- "Disable-while-trackpointing {}",
- if state.dwtp.unwrap_or(false) { "enabled" } else { "disabled" }
- ),
- );
- }
- }
- }
- "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);
- state.tap_config_applied = false;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &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("cce-client", &format!("Trackpoint acceleration profile set to {}", val));
- }
- }
- }
- "cursor-theme" | "cursor_theme" => {
- let val = value_str.trim().to_string();
- if !val.is_empty() {
- state.cursor_theme = Some(val.clone());
- state.cursor_theme_applied = false;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Cursor theme set to {}", val));
- }
- }
- }
- "cursor-size" | "cursor_size" => {
- if let Ok(val) = value_str.parse::<u32>() {
- state.cursor_size = Some(val);
- state.cursor_theme_applied = false;
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Cursor size set to {}", val));
- }
- }
- }
- _ => {}
- }
-}
-
-
-fn parse_keycode(key: &str) -> Option<u16> {
- let key = key.trim();
- if let Ok(val) = key.parse::<u16>() {
- return Some(val);
- }
- match key.to_lowercase().as_str() {
- "esc" | "escape" => Some(1),
- "1" => Some(2), "2" => Some(3), "3" => Some(4), "4" => Some(5),
- "5" => Some(6), "6" => Some(7), "7" => Some(8), "8" => Some(9),
- "9" => Some(10), "0" => Some(11),
- "minus" => Some(12), "equal" => Some(13), "backspace" => Some(14),
- "tab" => Some(15),
- "q" => Some(16), "w" => Some(17), "e" => Some(18), "r" => Some(19),
- "t" => Some(20), "y" => Some(21), "u" => Some(22), "i" => Some(23),
- "o" => Some(24), "p" => Some(25),
- "leftbrace" | "[" => Some(26), "rightbrace" | "]" => Some(27),
- "enter" | "return" => Some(28),
- "ctrl" | "leftctrl" => Some(29),
- "a" => Some(30), "s" => Some(31), "d" => Some(32), "f" => Some(33),
- "g" => Some(34), "h" => Some(35), "j" => Some(36), "k" => Some(37),
- "l" => Some(38), "semicolon" | ";" => Some(39), "apostrophe" | "'" => Some(40),
- "grave" | "`" => Some(41), "shift" | "leftshift" => Some(42),
- "backslash" | "\\" => Some(43),
- "z" => Some(44), "x" => Some(45), "c" => Some(46), "v" => Some(47),
- "b" => Some(48), "n" => Some(49), "m" => Some(50),
- "comma" | "," => Some(51), "dot" | "." => Some(52), "slash" | "/" => Some(53),
- "rightshift" => Some(54),
- "alt" | "leftalt" => Some(56), "space" => Some(57), "capslock" => Some(58),
- "f1" => Some(59), "f2" => Some(60), "f3" => Some(61), "f4" => Some(62),
- "f5" => Some(63), "f6" => Some(64), "f7" => Some(65), "f8" => Some(66),
- "f9" => Some(67), "f10" => Some(68),
- "f11" => Some(87), "f12" => Some(88),
- "rightctrl" => Some(97), "rightalt" => Some(100),
- "home" => Some(102), "up" => Some(103), "pageup" => Some(104),
- "left" => Some(105), "right" => Some(106), "end" => Some(107),
- "down" => Some(108), "pagedown" => Some(109), "insert" => Some(110),
- "delete" => Some(111),
- "super" | "hyper" | "meta" | "logo" | "leftmeta" | "leftsuper" => Some(125),
- "rightmeta" | "rightsuper" => Some(126),
- _ => None,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::types::{TilingMode, WindowManager};
-
- #[test]
- fn test_ipc_view() {
- let mut state = WindowManager::default();
- assert_eq!(state.active_tags, 1);
-
- handle_ipc_command("view-2", &mut state);
- assert_eq!(state.active_tags, 2);
-
- handle_ipc_command("view 3", &mut state);
- assert_eq!(state.active_tags, 4);
- }
-
- #[test]
- fn test_ipc_toggle() {
- let mut state = WindowManager::default();
- assert_eq!(state.active_tags, 1);
-
- handle_ipc_command("toggle-2", &mut state);
- assert_eq!(state.active_tags, 3); // tag 1 + tag 2
-
- handle_ipc_command("toggle-1", &mut state);
- assert_eq!(state.active_tags, 2); // tag 2 only
- }
-
- #[test]
- fn test_ipc_config_done() {
- let mut state = WindowManager::default();
- assert!(!state.config_done);
-
- handle_ipc_command("config-done", &mut state);
- assert!(state.config_done);
- }
-
- #[test]
- fn test_ipc_layout_gap() {
- let mut state = WindowManager::default();
- assert_eq!(state.layout.gap, 48);
-
- handle_ipc_command("layout gap 18", &mut state);
- assert_eq!(state.layout.gap, 18);
- }
-
- #[test]
- fn test_ipc_layout_grid_gap() {
- let mut state = WindowManager::default();
- assert_eq!(state.layout.grid_gap, 18);
-
- handle_ipc_command("layout grid_gap 24", &mut state);
- assert_eq!(state.layout.grid_gap, 24);
- }
-
- #[test]
- fn test_ipc_layout_gap_sides() {
- let mut state = WindowManager::default();
- assert_eq!(state.layout.gap_top, 48);
- assert_eq!(state.layout.gap_left, 48);
- assert_eq!(state.layout.gap_right, 48);
- assert_eq!(state.layout.gap_bottom, 48);
-
- handle_ipc_command("layout gap_top 10", &mut state);
- handle_ipc_command("layout gap_left 20", &mut state);
- handle_ipc_command("layout gap_right 30", &mut state);
- handle_ipc_command("layout gap_bottom 40", &mut state);
- assert_eq!(state.layout.gap_top, 10);
- assert_eq!(state.layout.gap_left, 20);
- assert_eq!(state.layout.gap_right, 30);
- assert_eq!(state.layout.gap_bottom, 40);
- }
-
- #[test]
- fn test_ipc_layout_border_color() {
- let mut state = WindowManager::default();
- handle_ipc_command("layout border_color #5c9060", &mut state);
- assert_eq!(state.layout.border_r, 0x5C5C5C5C);
- assert_eq!(state.layout.border_g, 0x90909090);
- assert_eq!(state.layout.border_b, 0x60606060);
- }
-
- #[test]
- fn test_ipc_mode_rule() {
- let mut state = WindowManager::default();
- handle_ipc_command("mode cascade ghostty", &mut state);
- assert_eq!(state.mode_rules.len(), 1);
- assert_eq!(state.mode_rules[0].mode, TilingMode::Cascade);
- assert_eq!(state.mode_rules[0].app_id_pattern, "ghostty");
- }
-
- #[test]
- fn test_ipc_mode_rule_update() {
- let mut state = WindowManager::default();
- handle_ipc_command("mode fullscreen cce-system-interface", &mut state);
- assert_eq!(state.mode_rules.len(), 1);
- assert_eq!(state.mode_rules[0].mode, TilingMode::Fullscreen);
-
- handle_ipc_command("mode cascade cce-system-interface", &mut state);
- assert_eq!(state.mode_rules.len(), 1);
- assert_eq!(state.mode_rules[0].mode, TilingMode::Cascade);
- }
-
- #[test]
- fn test_ipc_mode_rule_with_single() {
- let mut state = WindowManager::default();
- handle_ipc_command("mode cascade qutebrowser --single", &mut state);
- assert!(state.mode_rules[0].single_instance);
- }
-
- #[test]
- fn test_ipc_mode_rule_with_tag() {
- let mut state = WindowManager::default();
- handle_ipc_command("mode cascade ghostty --tag 2", &mut state);
- assert_eq!(state.mode_rules[0].tag, 2);
- }
-
- #[test]
- fn test_ipc_bind() {
- let mut state = WindowManager::default();
- handle_ipc_command("bind alt Return spawn ghostty", &mut state);
- assert_eq!(state.pending_bindings.len(), 1);
- assert_eq!(state.pending_bindings[0].action, Action::Spawn);
- assert_eq!(
- state.pending_bindings[0].command,
- Some("ghostty".to_string())
- );
- }
-
- #[test]
- fn test_ipc_pbind() {
- let mut state = WindowManager::default();
- handle_ipc_command("pbind alt left move", &mut state);
- assert_eq!(state.pending_pointer_bindings.len(), 1);
- assert_eq!(state.pending_pointer_bindings[0].action, Action::Move);
- assert_eq!(state.pending_pointer_bindings[0].button, 0x110); // BTN_LEFT
- }
-
- #[test]
- fn test_ipc_tag_layout() {
- let mut state = WindowManager::default();
- handle_ipc_command("tag-layout 2 grid", &mut state);
- assert!(state.has_tag_layout[1]);
- assert_eq!(state.tag_layouts[1], TilingMode::Grid);
- }
-
- #[test]
- fn test_ipc_set_tag() {
- let mut state = WindowManager::default();
- // Add a window and a seat with focus
- state.windows.push(crate::types::Window {
- id: 1,
- tags: 1,
- ..Default::default()
- });
- state.seats.push(crate::types::Seat {
- id: 1,
- focused_window_id: Some(1),
- ..Default::default()
- });
-
- handle_ipc_command("set-tag 3", &mut state);
- assert_eq!(state.windows[0].tags, 4); // 1 << 2
- }
-
- #[test]
- fn test_ipc_empty_command() {
- let mut state = WindowManager::default();
- handle_ipc_command("", &mut state);
- handle_ipc_command(" \n", &mut state);
- // Should not crash
- }
-
- #[test]
- fn test_ipc_layout_cascade_offset() {
- let mut state = WindowManager::default();
- handle_ipc_command("layout cascade_offset 32", &mut state);
- assert_eq!(state.layout.cascade_offset, 32);
- }
-
- #[test]
- fn test_ipc_layout_bar_height() {
- let mut state = WindowManager::default();
- handle_ipc_command("layout bar_height 28", &mut state);
- assert_eq!(state.layout.bar_height, 28);
- }
-
- #[test]
- fn test_ipc_layout_border_width() {
- let mut state = WindowManager::default();
- handle_ipc_command("layout border_width 18", &mut state);
- assert_eq!(state.layout.border_width, 18);
- }
-
- #[test]
- fn test_ipc_expose() {
- let mut state = WindowManager::default();
- assert!(!state.expose_active);
- handle_ipc_command("expose", &mut state);
- assert!(state.expose_active);
- handle_ipc_command("expose", &mut state);
- assert!(!state.expose_active);
- }
-
- #[test]
- fn test_ipc_expose_exit() {
- let mut state = WindowManager::default();
- state.seats.push(crate::types::Seat {
- id: 1,
- hovered_window_id: Some(42),
- focused_window_id: Some(10),
- ..Default::default()
- });
-
- // 1. When expose is not active, "expose-exit" should do nothing.
- assert!(!state.expose_active);
- handle_ipc_command("expose-exit", &mut state);
- assert!(!state.expose_active);
- assert_eq!(state.seats[0].focused_window_id, Some(10));
-
- // 2. When expose is active, "expose-exit" should deactivate it and focus the hovered window.
- state.expose_active = true;
- handle_ipc_command("expose-exit", &mut state);
- assert!(!state.expose_active);
- assert_eq!(state.seats[0].focused_window_id, Some(42));
- assert!(state.needs_focus);
- assert!(state.needs_render);
- }
-
- #[test]
- fn test_ipc_view_next_prev() {
- let mut state = WindowManager::default();
- state.active_tags = 1; // Tag 1 (1 << 0)
-
- // view-next should go: Tag 1 -> Tag 2 -> Tag 3 -> Tag 4 -> Tag 1
- handle_ipc_command("view-next", &mut state);
- assert_eq!(state.active_tags, 2); // Tag 2
-
- handle_ipc_command("view-next", &mut state);
- assert_eq!(state.active_tags, 4); // Tag 3
-
- handle_ipc_command("view-next", &mut state);
- assert_eq!(state.active_tags, 8); // Tag 4
-
- handle_ipc_command("view-next", &mut state);
- assert_eq!(state.active_tags, 1); // Tag 1 (wrap around)
-
- // view-prev should go: Tag 1 -> Tag 4 -> Tag 3 -> Tag 2 -> Tag 1
- handle_ipc_command("view-prev", &mut state);
- assert_eq!(state.active_tags, 8); // Tag 4 (wrap around)
-
- handle_ipc_command("view-prev", &mut state);
- assert_eq!(state.active_tags, 4); // Tag 3
-
- handle_ipc_command("view-prev", &mut state);
- assert_eq!(state.active_tags, 2); // Tag 2
-
- handle_ipc_command("view-prev", &mut state);
- assert_eq!(state.active_tags, 1); // Tag 1
- }
-
- #[test]
- fn test_ipc_focus_next_prev() {
- let mut state = WindowManager::default();
- // Setup 3 windows
- state.windows.push(crate::types::Window { id: 1, tags: 1, ..Default::default() });
- state.windows.push(crate::types::Window { id: 2, tags: 1, ..Default::default() });
- state.windows.push(crate::types::Window { id: 3, tags: 1, ..Default::default() });
- state.seats.push(crate::types::Seat { id: 1, focused_window_id: Some(1), ..Default::default() });
-
- handle_ipc_command("focus-next", &mut state);
- assert_eq!(state.seats[0].focused_window_id, Some(2));
-
- handle_ipc_command("focus-next", &mut state);
- assert_eq!(state.seats[0].focused_window_id, Some(1));
-
- handle_ipc_command("focus-next", &mut state);
- assert_eq!(state.seats[0].focused_window_id, Some(3));
-
- handle_ipc_command("focus-prev", &mut state);
- assert_eq!(state.seats[0].focused_window_id, Some(1));
-
- handle_ipc_command("focus-prev", &mut state);
- assert_eq!(state.seats[0].focused_window_id, Some(3));
- }
-
- #[test]
- fn test_ipc_pointer_and_keys() {
- let mut state = WindowManager::default();
-
- // Test pointer location query
- let location = handle_ipc_command("pointer-location", &mut state);
- assert!(location.ends_with("\n"));
- let coords: Vec<&str> = location.trim().split_whitespace().collect();
- assert_eq!(coords.len(), 2);
- assert_eq!(coords[0].parse::<i32>().is_ok(), true);
- assert_eq!(coords[1].parse::<i32>().is_ok(), true);
-
- // Test button parsing helpers
- assert_eq!(parse_button("left"), 272);
- assert_eq!(parse_button("right"), 273);
- assert_eq!(parse_button("middle"), 274);
- assert_eq!(parse_button("side"), 275);
- assert_eq!(parse_button("extra"), 276);
- assert_eq!(parse_button("280"), 280);
- assert_eq!(parse_button("invalid"), 0);
-
- // Test keycode parsing helpers
- assert_eq!(parse_keycode("escape"), Some(1));
- assert_eq!(parse_keycode("enter"), Some(28));
- assert_eq!(parse_keycode("a"), Some(30));
- assert_eq!(parse_keycode("30"), Some(30));
- assert_eq!(parse_keycode("invalid"), None);
-
- // Test some simulation commands return error if invalid or ok (without input controller it shouldn't crash)
- let r1 = handle_ipc_command("pointer-move-to invalid", &mut state);
- assert!(r1.starts_with("error:"));
- let r2 = handle_ipc_command("pointer-move-by 10", &mut state);
- assert!(r2.starts_with("error:"));
- let r3 = handle_ipc_command("pointer-click invalid", &mut state);
- assert!(r3.starts_with("error:"));
- let r4 = handle_ipc_command("keypress invalid", &mut state);
- assert!(r4.starts_with("error:"));
- }
-}
diff --git a/src/client/ipc_server.rs b/src/client/ipc_server.rs
deleted file mode 100644
index 858d1f3..0000000
--- a/src/client/ipc_server.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-use std::io::{Read, Write};
-use std::os::unix::net::{UnixListener, UnixStream};
-use std::sync::mpsc;
-
-use crate::paths;
-
-pub struct IpcRequest {
- pub command: String,
- pub reply_tx: mpsc::Sender<String>,
-}
-
-pub struct IpcReceiver {
- pub rx: mpsc::Receiver<IpcRequest>,
- pub tx: mpsc::Sender<IpcRequest>,
-}
-
-pub fn spawn_ipc_server(pipe_write: libc::c_int) -> IpcReceiver {
- let (tx, rx) = mpsc::channel::<IpcRequest>();
- let tx_clone = tx.clone();
-
- std::thread::Builder::new()
- .name("cce-client-ipc".into())
- .spawn(move || {
- ipc_server_main(tx, pipe_write);
- })
- .expect("failed to spawn IPC server thread");
-
- IpcReceiver { rx, tx: tx_clone }
-}
-
-fn ipc_server_main(tx: mpsc::Sender<IpcRequest>, pipe_write: libc::c_int) {
- let socket_path = paths::get_socket_path();
- let _ = std::fs::remove_file(&socket_path);
-
- let listener = match UnixListener::bind(&socket_path) {
- Ok(l) => l,
- Err(e) => {
- eprintln!("[ipc] failed to bind {}: {}", socket_path, e);
- return;
- }
- };
-
- if let Err(e) = listener.set_nonblocking(true) {
- eprintln!("[ipc] failed to set non-blocking: {}", e);
- return;
- }
-
- eprintln!("[ipc] listening on {}", socket_path);
-
- let mut streams: Vec<UnixStream> = Vec::new();
-
- loop {
- let mut activity = false;
- for _ in 0..5 {
- match listener.accept() {
- Ok((stream, _addr)) => {
- if let Err(e) = stream.set_nonblocking(true) {
- eprintln!("[ipc] failed to set non-blocking on client: {}", e);
- continue;
- }
- eprintln!("[ipc] new connection");
- streams.push(stream);
- activity = true;
- }
- Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
- Err(e) => {
- eprintln!("[ipc] accept error: {}", e);
- break;
- }
- }
- }
-
- let mut dead = Vec::new();
- for (i, stream) in streams.iter_mut().enumerate() {
- let mut buf = [0u8; 4096];
- match stream.read(&mut buf) {
- Ok(0) => {
- dead.push(i);
- }
- Ok(n) => {
- let s = String::from_utf8_lossy(&buf[..n]);
- let mut sent = false;
- let (reply_tx, reply_rx) = mpsc::channel();
- let mut cmd_count = 0;
- for line in s.lines() {
- let cmd = line.trim().to_string();
- if !cmd.is_empty() {
- let _ = tx.send(IpcRequest {
- command: cmd,
- reply_tx: reply_tx.clone(),
- });
- sent = true;
- cmd_count += 1;
- }
- }
- if sent {
- // Wake up main thread
- unsafe {
- libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
- }
-
- let mut response = String::new();
- for _ in 0..cmd_count {
- if let Ok(res) = reply_rx.recv_timeout(std::time::Duration::from_millis(1000)) {
- response.push_str(&res);
- } else {
- response.push_str("error: timeout or no response\n");
- }
- }
- let _ = stream.write_all(response.as_bytes());
- dead.push(i);
- }
- activity = true;
- }
- Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
- Err(e) => {
- eprintln!("[ipc] read error: {}", e);
- dead.push(i);
- }
- }
- }
-
- for i in dead.into_iter().rev() {
- streams.remove(i);
- }
-
- if !activity {
- std::thread::sleep(std::time::Duration::from_millis(20));
- }
- }
-}
diff --git a/src/client/lib.rs b/src/client/lib.rs
deleted file mode 100644
index ac40d31..0000000
--- a/src/client/lib.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-// cce-client — Wayland window manager for river, written in Rust
-
-pub mod borders;
-pub mod config;
-pub mod decorations;
-pub mod ipc;
-pub mod ipc_server;
-pub mod protocol;
-pub mod restart;
-pub mod state;
-pub mod status;
-pub mod status_server;
-pub mod tiling;
-pub mod types;
-#[allow(unreachable_patterns)] // wayland event match arms use _ => {} for forward-compat
-pub mod wayland;
-pub mod wm;
-pub mod input;
-pub mod paths;
-pub mod run;
-
-pub use run::run_client;
diff --git a/src/client/paths.rs b/src/client/paths.rs
deleted file mode 100644
index a74053a..0000000
--- a/src/client/paths.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use std::env;
-
-pub fn get_socket_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-{}.sock", display)
- } else {
- "/tmp/cce-client.sock".to_string()
- }
-}
-
-pub fn get_status_socket_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-status-{}.sock", display)
- } else {
- "/tmp/cce-client-status.sock".to_string()
- }
-}
-
-pub fn get_windows_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-windows-{}", display)
- } else {
- "/tmp/cce-client-windows".to_string()
- }
-}
-
-pub fn get_tags_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-tags-{}", display)
- } else {
- "/tmp/cce-client-tags".to_string()
- }
-}
-
-pub fn get_layout_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-layout-{}", display)
- } else {
- "/tmp/cce-client-layout".to_string()
- }
-}
-
-pub fn get_title_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-title-{}", display)
- } else {
- "/tmp/cce-client-title".to_string()
- }
-}
-
-pub fn get_death_log_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-death-{}.log", display)
- } else {
- "/tmp/cce-client-death.log".to_string()
- }
-}
-
-pub fn get_bt_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-bt-{}.txt", display)
- } else {
- "/tmp/cce-client-bt.txt".to_string()
- }
-}
-
-pub fn get_input_coords_socket_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clear-input-coords-{}.sock", display)
- } else {
- "/tmp/clear-input-coords.sock".to_string()
- }
-}
-
-pub fn get_log_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-{}.log", display)
- } else {
- "/tmp/cce-client.log".to_string()
- }
-}
-
-pub fn get_prev_log_path() -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-{}-prev.log", display)
- } else {
- "/tmp/cce-client-prev.log".to_string()
- }
-}
-
-pub fn get_xprop_path(wid: u64) -> String {
- if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/cce-client-xprop-{}-{}", wid, display)
- } else {
- format!("/tmp/cce-client-xprop-{}", wid)
- }
-}
diff --git a/src/client/protocol.rs b/src/client/protocol.rs
deleted file mode 100644
index 8fd1d59..0000000
--- a/src/client/protocol.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-// River Wayland protocol bindings generated from XML
-
-macro_rules! river_protocol {
- ($path:expr, [$($imports:path),*]) => {
- #[allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
- #[allow(non_upper_case_globals, non_snake_case, unused_imports, missing_docs, clippy::all)]
- pub mod generated {
- pub mod client {
- use wayland_client;
- use wayland_client::protocol::*;
- $(use $imports::{client::*};)*
-
- pub mod __interfaces {
- use wayland_client::protocol::__interfaces::*;
- $(use $imports::{client::__interfaces::*};)*
- wayland_scanner::generate_interfaces!($path);
- }
- use self::__interfaces::*;
-
- wayland_scanner::generate_client_code!($path);
- }
- }
- pub use self::generated::client;
- };
-}
-
-pub mod river_window_management {
- river_protocol!("protocol/river-window-management-v1.xml", []);
-}
-
-pub mod river_xkb_bindings {
- river_protocol!(
- "protocol/river-xkb-bindings-v1.xml",
- [crate::protocol::river_window_management::generated]
- );
-}
-
-pub mod river_layer_shell {
- river_protocol!(
- "protocol/river-layer-shell-v1.xml",
- [crate::protocol::river_window_management::generated]
- );
-}
-
-pub mod river_input_management {
- river_protocol!(
- "protocol/river-input-management-v1.xml",
- [crate::protocol::river_window_management::generated]
- );
-}
-
-pub mod river_libinput_config {
- river_protocol!(
- "protocol/river-libinput-config-v1.xml",
- [crate::protocol::river_input_management::generated]
- );
-}
-
-pub mod wlr_output_management {
- river_protocol!("protocol/wlr-output-management-unstable-v1.xml", []);
-}
-
-pub mod clear_inspector {
- river_protocol!("protocol/clear-inspector-v1.xml", []);
-}
diff --git a/src/client/restart.rs b/src/client/restart.rs
deleted file mode 100644
index 9cd1f2f..0000000
--- a/src/client/restart.rs
+++ /dev/null
@@ -1,279 +0,0 @@
-// Restart and reload logic for cce-client
-
-use crate::config::parse_config;
-use crate::types::WindowManager;
-
-/// Restart the window manager process.
-///
-/// This forks a child process that waits briefly for the parent to die
-/// (so River cleans up the old Wayland connection), then execs a fresh
-/// cce-client instance. The parent (current process) exits immediately.
-///
-/// The CCE_CLIENT_RESTARTING environment variable signals that this is a restart
-/// (not a cold start), so the new process skips cold_start_only apps.
-///
-/// ## Why setsid() is required
-///
-/// cce-client is typically a session leader (PID == SID, started by River's
-/// `-c` launch script). When a session leader exits, the kernel sends SIGHUP
-/// to all processes in that session — including the forked child. Without
-/// `setsid()`, the child dies from SIGHUP before it can exec, and cce-client
-/// never comes back.
-pub fn wm_restart() {
- use std::time::Instant;
-
- use crate::paths;
-
- // Write to death log before fork — this is the last chance to capture
- // why we're restarting, since fork+process::exit(0) silently kills the parent.
- if let Ok(mut f) = std::fs::OpenOptions::new()
- .create(true)
- .append(true)
- .open(paths::get_death_log_path())
- {
- use std::io::Write;
- let _ = writeln!(f, "wm_restart() called — about to fork+exit");
- let _ = f.sync_all(); // ensure it hits disk before process::exit(0)
- }
-
- static mut LAST_RESTART: Option<Instant> = None;
-
- // Throttle restarts (minimum 2 seconds between restarts)
- // Safety: single-threaded access; acceptable for this use case
- let now = Instant::now();
- let should_throttle = unsafe {
- if let Some(last) = LAST_RESTART {
- now.duration_since(last).as_secs_f64() < 2.0
- } else {
- false
- }
- };
-
- if should_throttle {
- std::thread::sleep(std::time::Duration::from_secs(2));
- }
-
- unsafe {
- LAST_RESTART = Some(now);
- }
-
- // Signal that this is a restart, not a cold start
- std::env::set_var("CCE_CLIENT_RESTARTING", "1");
-
- // Persist state to ~/.cache/cce_client_state for the new instance to restore.
- // Note: we can't pass &WindowManager here since wm_restart() has no access
- // to it. The state file is kept fresh by RenderStart's needs_status_update
- // path, so it should be reasonably up-to-date already.
-
- // Save the current log before River's launch script truncates it on restart.
- // This preserves the crash/reason for the restart.
- let _ = std::fs::copy(paths::get_log_path(), paths::get_prev_log_path());
-
- // Remove the IPC socket
- let _ = std::fs::remove_file(paths::get_socket_path());
-
- // Get the current executable path.
- // CString is required because execl() needs a null-terminated C string;
- // Rust's String::as_ptr() is NOT guaranteed to be null-terminated.
- //
- // We prefer the symlink path (~/.local/bin/cce-client) over current_exe()
- // because current_exe() resolves through /proc/self/exe to the real path,
- // which may be on a sync filesystem (Dropbox) that temporarily moves files.
- // The symlink is on the root filesystem and always available.
- let home = std::env::var("HOME").unwrap_or_default();
- let exe_path = if !home.is_empty() {
- let symlink = format!("{}/.local/bin/cce-client", home);
- if std::path::Path::new(&symlink).exists() {
- std::path::PathBuf::from(symlink)
- } else {
- std::env::current_exe().unwrap_or_else(|_| std::process::exit(1))
- }
- } else {
- std::env::current_exe().unwrap_or_else(|_| std::process::exit(1))
- };
- let _ = std::io::Write::write_fmt(&mut std::io::stderr(), format_args!("wm_restart: exe_path={}\n", exe_path.display()));
- let path_cstr = std::ffi::CString::new(exe_path.to_string_lossy().into_owned())
- .unwrap_or_else(|_| std::process::exit(1));
-
- // Prepare log path CString in the parent process before fork to be async-signal-safe.
- let log_path = paths::get_log_path();
- let log_path_cstr = std::ffi::CString::new(log_path)
- .unwrap_or_else(|_| std::process::exit(1));
-
- // Fork: child waits for parent to die, then execs fresh cce-client.
- // Parent exits so River tears down the old Wayland connection.
- let pid = unsafe { libc::fork() };
- if pid < 0 {
- // fork failed, just exit
- std::process::exit(1);
- } else if pid == 0 {
- // ── Child process ──
-
- // Create a new session IMMEDIATELY. Without this, the child stays
- // in the parent's session. When the parent (session leader) calls
- // process::exit(0), the kernel sends SIGHUP to every process in that
- // session — killing the child before it can exec.
- unsafe {
- libc::setsid();
- libc::signal(libc::SIGHUP, libc::SIG_IGN);
- }
-
- // Wait for parent to exit so River cleans up the old Wayland
- // connection before we try to connect fresh.
- let parent_pid = unsafe { libc::getppid() };
- std::thread::sleep(std::time::Duration::from_millis(500));
-
- // If parent is somehow still alive, wait a bit more
- if unsafe { libc::kill(parent_pid, 0) == 0 } {
- std::thread::sleep(std::time::Duration::from_millis(500));
- }
-
- // Close inherited Wayland FDs so the new cce-client instance doesn't
- // confuse River with stale connections.
- // (close everything except stdin/stdout/stderr)
- let max_fd = unsafe { libc::sysconf(libc::_SC_OPEN_MAX) } as i32;
- for fd in 3..max_fd {
- unsafe {
- libc::close(fd);
- }
- }
-
- // Reopen standard input to /dev/null to ensure a clean stdin fd
- let null_fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const libc::c_char, libc::O_RDONLY) };
- if null_fd >= 0 {
- unsafe {
- libc::dup2(null_fd, 0);
- libc::close(null_fd);
- }
- }
-
- // Reopen standard output and error to the log file to prevent EPIPE panics when parent shell/session exits
- let log_fd = unsafe {
- libc::open(
- log_path_cstr.as_ptr(),
- libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND,
- 0o644,
- )
- };
- if log_fd >= 0 {
- unsafe {
- libc::dup2(log_fd, 1);
- libc::dup2(log_fd, 2);
- libc::close(log_fd);
- }
- }
-
- // Exec the same binary — replaces this process with a fresh cce-client.
- // Retry up to 3 times with a short delay — the binary may be temporarily
- // unavailable if cargo build is replacing it mid-write (atomic rename).
- use std::io::Write;
- for attempt in 0..3 {
- let ret = unsafe {
- libc::execl(
- path_cstr.as_ptr(),
- path_cstr.as_ptr(),
- std::ptr::null::<i8>(),
- )
- };
- let errno = unsafe { *libc::__errno_location() };
- if attempt < 2 {
- let _ = writeln!(
- std::io::stderr(),
- "wm_restart: execl attempt {} failed (errno={} {}), retrying in 500ms...",
- attempt + 1,
- errno,
- std::io::Error::from_raw_os_error(errno)
- );
- std::thread::sleep(std::time::Duration::from_millis(500));
- let _ = ret; // suppress unused
- } else {
- // Final attempt failed — log and exit
- let _ = writeln!(
- std::io::stderr(),
- "wm_restart: execl failed after 3 attempts! errno={} ({})",
- errno,
- std::io::Error::from_raw_os_error(errno)
- );
- if let Ok(mut f) = std::fs::OpenOptions::new()
- .create(true)
- .append(true)
- .open(paths::get_death_log_path())
- {
- let _ = writeln!(
- f,
- "child: execl failed after 3 attempts! errno={} ({}) path={}",
- errno,
- std::io::Error::from_raw_os_error(errno),
- exe_path.display()
- );
- }
- let _ = ret; // suppress unused
- std::process::exit(1);
- }
- }
- } else {
- // ── Parent process ──
- // Exit immediately so River sees the Wayland connection drop
- // and tears down the old WM binding.
- std::process::exit(0);
- }
-}
-
-/// Reload the configuration file.
-///
-/// This clears mode rules, pending bindings, and seat bindings, then
-/// re-parses the config file.
-///
-/// Ported from C wm_reload().
-pub fn wm_reload(state: &mut WindowManager) {
- // Unlock mode on all windows so the new rules apply to them
- for window in &mut state.windows {
- window.mode_locked = false;
- }
-
- // Clear per-tag layout defaults
- state.tag_layouts = [crate::types::TilingMode::Cascade; crate::types::NUM_TAGS];
- state.has_tag_layout = [true; crate::types::NUM_TAGS];
-
- // Mark status for update after reload
- state.needs_status_update = true;
- state.tap_config_applied = false;
- state.startup_spawned = false;
-
- // Clear mode rules
- state.mode_rules.clear();
-
- // Clear pending bindings
- state.pending_bindings.clear();
- state.pending_pointer_bindings.clear();
-
- // Try TOML config first
- let home = std::env::var("HOME").unwrap_or_default();
- if !home.is_empty() {
- let config_path = format!("{}/.config/cce/config.toml", home);
- if std::path::Path::new(&config_path).exists() {
- match parse_config(&config_path, false, state) {
- Ok(_) => {
- for cmd in &state.reload_commands {
- let _ = std::io::Write::write_fmt(&mut std::io::stderr(), format_args!("[reload] executing reload command: {}\n", cmd));
- spawn_command_bg(cmd);
- }
- if state.notifications_enable {
- crate::config::show_notification("cce-client", "Configuration reloaded successfully");
- }
- }
- Err(e) => {
- if state.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Config reload failed:\n{}", e));
- }
- }
- }
- }
- }
-}
-
-/// Spawn a command in the background (re-export from config for convenience)
-pub use crate::config::spawn_command_bg;
-
-/// Check if a process with the given name is already running (re-export from config)
-pub use crate::config::process_running;
diff --git a/src/client/run.rs b/src/client/run.rs
deleted file mode 100644
index 2cee73f..0000000
--- a/src/client/run.rs
+++ /dev/null
@@ -1,328 +0,0 @@
-// cce-client — Wayland window manager run loop
-
-use crate::config::parse_config;
-use crate::ipc;
-use crate::ipc_server;
-use crate::restart;
-use crate::status_server;
-use crate::wayland::{wayland_init, AppState};
-use crate::paths;
-use std::env;
-use std::fs;
-use wayland_client::EventQueue;
-
-/// Write a crash/exit trace to /tmp/cce-client-death.log so we can diagnose
-/// why cce-client dies even when the normal log gets overwritten on restart.
-fn log_death(msg: &str) {
- use std::io::Write;
- if let Ok(mut f) = std::fs::OpenOptions::new()
- .create(true)
- .append(true)
- .open(paths::get_death_log_path())
- {
- let _ = writeln!(f, "{}", msg);
- let _ = f.sync_all();
- }
- let _ = writeln!(std::io::stderr(), "{}", msg);
-}
-
-pub fn run_client() {
- // Install a panic hook that writes to a separate log file before aborting.
- std::panic::set_hook(Box::new(|info| {
- let msg = format!("[PANIC] {}", info);
- use std::io::Write;
- if let Ok(mut f) = std::fs::OpenOptions::new()
- .create(true)
- .append(true)
- .open(paths::get_death_log_path())
- {
- let _ = writeln!(f, "{}", msg);
- let _ = f.sync_all();
- }
- let _ = writeln!(std::io::stderr(), "{}", msg);
- }));
-
- eprintln!("cce-client starting...");
-
- // Start the status socket server thread (for waybar integration)
- let status_sender = status_server::spawn_status_server();
-
- // Ignore SIGPIPE — when the Wayland compositor closes the connection
- // (protocol error, unresponsive timeout, etc.), writes to the socket
- // generate SIGPIPE. Without ignoring it, the process is killed before
- // conn.flush() or blocking_dispatch() can return an error, masking the
- // real cause of the disconnect.
- unsafe {
- nix::libc::signal(nix::libc::SIGPIPE, nix::libc::SIG_IGN);
- }
-
- // Set up SIGCHLD handler to reap child processes
- let sa = nix::sys::signal::SigAction::new(
- nix::sys::signal::SigHandler::Handler(sigchld_handler),
- nix::sys::signal::SaFlags::SA_RESTART,
- nix::sys::signal::SigSet::empty(),
- );
- unsafe {
- nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGCHLD, &sa)
- .expect("failed to set SIGCHLD handler");
- }
-
- // Connect to Wayland display and get initial state.
- // SIGUSR2 handler: dump backtrace to /tmp/cce-client-bt.txt for debugging busy loops
- unsafe {
- nix::sys::signal::sigaction(
- nix::sys::signal::SIGUSR2,
- &nix::sys::signal::SigAction::new(
- nix::sys::signal::SigHandler::Handler(dump_backtrace),
- nix::sys::signal::SaFlags::empty(),
- nix::sys::signal::SigSet::empty(),
- ),
- )
- .expect("failed to set SIGUSR2 handler");
- }
-
- let (_conn, mut event_queue, mut state) = match wayland_init() {
- Ok(c) => c,
- Err(e) => {
- log_death(&format!("fatal: {}", e));
- std::process::exit(1);
- }
- };
-
- // Create a pipe to wake up the main loop when IPC commands arrive
- let mut pipe_fds = [0; 2];
- unsafe {
- libc::pipe(pipe_fds.as_mut_ptr());
- }
- let pipe_read = pipe_fds[0];
- let pipe_write = pipe_fds[1];
-
- // Start the IPC server thread (for clearctl and cce-system-interface)
- 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::<crate::input::InputDaemonMsg>();
- 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("cce-client-input-subsystem".into())
- .spawn(move || {
- if let Err(e) = crate::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);
-
- // Check if this is a restart
- let cold_start = if env::var("CCE_CLIENT_RESTARTING").as_deref() == Ok("1") {
- env::remove_var("CCE_CLIENT_RESTARTING");
- false
- } else {
- true
- };
-
- // Don't remove WAYLAND_DEBUG — we need protocol debug logging to diagnose crashes
- // env::remove_var("WAYLAND_DEBUG");
-
- // Load config immediately, before entering the event loop.
- // This ensures bindings are registered before any render_start arrives.
- // Following the tinyrwm pattern: do setup, then simple blocking_dispatch loop.
- eprintln!(
- "[init] about to load config, render_count={}",
- state.render_count
- );
- let config_start = std::time::Instant::now();
- if let Ok(home) = env::var("HOME") {
- let config_path = format!("{}/.config/cce/config.toml", home);
- if fs::metadata(&config_path).is_ok() {
- if let Err(e) = parse_config(&config_path, cold_start, &mut state.wm) {
- eprintln!("[init] failed to load config: {}", e);
- }
- }
- }
- eprintln!(
- "[init] config loaded in {:?}, render_count={}",
- config_start.elapsed(),
- state.render_count
- );
-
- // Apply output scale immediately if heads were discovered during init roundtrips.
- if state.wm.pending_scale_apply && state.wm.output_scale > 0.0 && !state.output_heads.is_empty()
- {
- let qh = event_queue.handle();
- crate::wayland::apply_output_scale(&mut state, &qh);
- }
-
- // Flush any queued requests from config loading (bindings, etc.)
- eprintln!("[init] flushed, entering main loop");
-
- // Main loop — using poll to block on both Wayland socket and IPC wake-up pipe.
- // All work (including spawning) happens inside Dispatch callbacks.
- let mut loop_count: u64 = 0;
- loop {
- loop_count += 1;
- if loop_count % 10000 == 0 {
- eprintln!("[main] loop iteration {}", loop_count);
- }
-
- // 1. Dispatch any already pending events in the queue
- let _ = event_queue.dispatch_pending(&mut state);
-
- // 2. Flush outgoing requests to the compositor
- if let Err(e) = event_queue.flush() {
- log_death(&format!("flush error: {:?}", e));
- let _ = std::fs::copy(paths::get_log_path(), paths::get_prev_log_path());
- if !state.wm.exit_requested {
- restart::wm_restart();
- }
- break;
- }
-
- // 3. Prepare to read Wayland events
- let read_guard = match event_queue.prepare_read() {
- Some(g) => g,
- None => {
- // If None, events are already in the queue, dispatch them immediately
- let _ = event_queue.dispatch_pending(&mut state);
- continue;
- }
- };
-
- // 4. Poll both the Wayland display FD and the IPC wake-up pipe
- use std::os::fd::{AsFd, AsRawFd};
- let wl_fd = event_queue.as_fd().as_raw_fd();
- let mut poll_fds = [
- nix::poll::PollFd::new(unsafe { std::os::fd::BorrowedFd::borrow_raw(wl_fd) }, nix::poll::PollFlags::POLLIN),
- nix::poll::PollFd::new(unsafe { std::os::fd::BorrowedFd::borrow_raw(pipe_read) }, nix::poll::PollFlags::POLLIN),
- ];
-
- let timeout = if state.wm.animating {
- let elapsed = state.wm.last_animation_tick.elapsed();
- let target = std::time::Duration::from_millis(32);
- let timeout_duration = if elapsed >= target {
- std::time::Duration::ZERO
- } else {
- target - elapsed
- };
- nix::poll::PollTimeout::try_from(timeout_duration).unwrap()
- } else {
- nix::poll::PollTimeout::NONE
- };
-
- match nix::poll::poll(&mut poll_fds, timeout) {
- Ok(num_events) => {
- if num_events > 0 {
- // If Wayland FD is readable, read the events
- if poll_fds[0].revents().unwrap_or(nix::poll::PollFlags::empty()).contains(nix::poll::PollFlags::POLLIN) {
- let _ = read_guard.read();
- } else {
- // Otherwise drop the read guard to release the lock
- std::mem::drop(read_guard);
- }
-
- // If IPC pipe is readable, drain it
- if poll_fds[1].revents().unwrap_or(nix::poll::PollFlags::empty()).contains(nix::poll::PollFlags::POLLIN) {
- let mut buf = [0u8; 128];
- unsafe {
- libc::read(pipe_read, buf.as_mut_ptr() as *mut libc::c_void, buf.len());
- }
- }
- } else {
- // Timeout (num_events == 0) — drop the read guard to release the lock
- std::mem::drop(read_guard);
- }
- }
- Err(e) => {
- std::mem::drop(read_guard);
- if e != nix::errno::Errno::EINTR {
- log_death(&format!("poll error: {:?}", e));
- break;
- }
- }
- }
-
- // 5. Dispatch read events
- let _ = event_queue.dispatch_pending(&mut state);
-
- // 6. If animating and frame budget elapsed, request next frame (fallback only)
- if state.wm.animating && state.wm.last_animation_tick.elapsed() >= std::time::Duration::from_millis(32) {
- state.manage_dirty();
- }
-
- // 6. Process pending IPC commands from the socket channel
- let mut ipc_commands = false;
- loop {
- match ipc_rx.try_recv() {
- Ok(req) => {
- eprintln!("[main] IPC command: {}", req.command);
- let reply = ipc::handle_ipc_command(&req.command, &mut state.wm);
- let _ = req.reply_tx.send(reply);
- ipc_commands = true;
- }
- Err(std::sync::mpsc::TryRecvError::Empty) => break,
- Err(std::sync::mpsc::TryRecvError::Disconnected) => {
- eprintln!("[main] IPC server disconnected");
- break;
- }
- }
- }
- // Force a render sequence if we processed IPC commands
- if ipc_commands {
- state.manage_dirty();
- if !state.wm.tap_config_applied && !state.libinput_devices.is_empty() {
- let qh = event_queue.handle();
- crate::wayland::apply_input_config(&mut state, &qh);
- }
- }
-
- if state.exit_requested || state.wm.exit_requested {
- log_death(&format!(
- "main loop exit: exit_requested={} wm.exit_requested={}",
- state.exit_requested, state.wm.exit_requested
- ));
- break;
- }
- }
- let _ = fs::remove_file(paths::get_socket_path());
- if !state.wm.exit_requested {
- // Save log before restart overwrites it
- let _ = std::fs::copy(paths::get_log_path(), paths::get_prev_log_path());
- restart::wm_restart();
- }
- log_death("main loop exited cleanly");
-}
-
-extern "C" fn sigchld_handler(_sig: nix::libc::c_int) {
- use nix::sys::wait::WaitStatus;
- while let Ok(status) = nix::sys::wait::waitpid(
- nix::unistd::Pid::from_raw(-1),
- Some(nix::sys::wait::WaitPidFlag::WNOHANG),
- ) {
- match status {
- WaitStatus::StillAlive => break, // no more children to reap
- _ => continue, // reaped a child, check for more
- }
- }
-}
-
-extern "C" fn dump_backtrace(_sig: nix::libc::c_int) {
- use std::io::Write;
- let bt = std::backtrace::Backtrace::capture();
- if let Ok(mut f) = std::fs::OpenOptions::new()
- .create(true)
- .write(true)
- .truncate(true)
- .open(paths::get_bt_path())
- {
- let _ = writeln!(f, "SIGUSR2 backtrace:\n{:?}", bt);
- let _ = f.sync_all();
- }
-}
diff --git a/src/client/state.rs b/src/client/state.rs
deleted file mode 100644
index 94e52c2..0000000
--- a/src/client/state.rs
+++ /dev/null
@@ -1,475 +0,0 @@
-// Persistent state file for cce-client restart recovery.
-//
-// Writes window tag assignments and global tag/layout state to
-// ~/.cache/cce_client_state so that it survives restarts. On startup,
-// the state file is read and applied to re-advertised windows
-// matched by their River identifier (stable across WM restarts)
-// or app_id+title as a fallback.
-//
-// What is persisted:
-// - active_tags bitmask
-// - Per-tag layout overrides (tag_layouts + has_tag_layout)
-// - Per-window: tags, tiling_mode (only if mode_locked), identifier, app_id, title
-//
-// What is NOT persisted:
-// - Focus (seat.focused_window_id)
-// - Window positions/dimensions (recomputed on startup)
-// - Transient flags (is_new, needs_*, etc.)
-
-use crate::types::{TilingMode, WindowManager, NUM_TAGS};
-use std::fs;
-use std::io::{BufRead, Write};
-use std::path::PathBuf;
-
-/// Get the state file path: ~/.cache/cce_client_state
-fn state_file_path() -> PathBuf {
- if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
- let mut path = PathBuf::from(runtime_dir);
- path.push("cce_client_state");
- path
- } else {
- let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
- let mut path = PathBuf::from(home);
- path.push(".cache");
- path.push("cce_client_state");
- path
- }
-}
-
-/// Persistent state snapshot (plain-text, line-oriented).
-///
-/// Format:
-/// active_tags=<u32>
-/// tag_layout=<tag_index> <mode_str> <has_layout_bool>
-/// window <identifier> <app_id> <title> <tags> <mode_str> <mode_locked>
-///
-/// Fields are tab-separated. App_id and title use URL-style percent-encoding
-/// for spaces, tabs, and newlines so they never break the line format.
-pub struct PersistentState {
- pub active_tags: u32,
- pub tag_layouts: Vec<(usize, TilingMode, bool)>,
- pub windows: Vec<PersistentWindow>,
-}
-
-pub struct PersistentWindow {
- pub identifier: Option<String>,
- pub app_id: Option<String>,
- pub title: Option<String>,
- pub tags: u32,
- pub tiling_mode: TilingMode,
- pub mode_locked: bool,
- pub minimized: bool,
-}
-
-/// Percent-encode spaces, tabs, newlines, and percent signs in a string.
-fn pct_encode(s: &str) -> String {
- let mut out = String::with_capacity(s.len());
- for c in s.chars() {
- match c {
- ' ' => out.push_str("%20"),
- '\t' => out.push_str("%09"),
- '\n' => out.push_str("%0A"),
- '\r' => out.push_str("%0D"),
- '%' => out.push_str("%25"),
- _ => out.push(c),
- }
- }
- out
-}
-
-/// Decode a percent-encoded string.
-fn pct_decode(s: &str) -> String {
- let mut out = String::with_capacity(s.len());
- let mut chars = s.chars();
- while let Some(c) = chars.next() {
- if c == '%' {
- let hex: String = chars.by_ref().take(2).collect();
- if let Ok(byte) = u8::from_str_radix(&hex, 16) {
- out.push(byte as char);
- } else {
- out.push('%');
- out.push_str(&hex);
- }
- } else {
- out.push(c);
- }
- }
- out
-}
-
-/// Write the current WM state to the state file.
-/// Safe to call inside Dispatch callbacks — just file I/O, no fork.
-pub fn write_state(wm: &WindowManager) {
- let path = state_file_path();
-
- // Ensure ~/.cache/ exists
- if let Some(parent) = path.parent() {
- let _ = fs::create_dir_all(parent);
- }
-
- if let Ok(mut f) = fs::File::create(&path) {
- let _ = writeln!(f, "active_tags={}", wm.active_tags);
-
- for tag_bit in 0..NUM_TAGS {
- let mode_str = wm.tag_layouts[tag_bit].as_str();
- let has = wm.has_tag_layout[tag_bit];
- let _ = writeln!(f, "tag_layout\t{}\t{}\t{}", tag_bit, mode_str, has);
- }
-
- for win in &wm.windows {
- if win.closed {
- continue;
- }
- let ident = win
- .identifier
- .as_deref()
- .map(pct_encode)
- .unwrap_or_else(|| "-".to_string());
- let app_id = win
- .app_id
- .as_deref()
- .map(pct_encode)
- .unwrap_or_else(|| "-".to_string());
- let title = win
- .title
- .as_deref()
- .map(pct_encode)
- .unwrap_or_else(|| "-".to_string());
- let mode_str = win.tiling_mode.as_str();
- let _ = writeln!(
- f,
- "window\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
- ident, app_id, title, win.tags, mode_str, win.mode_locked, win.minimized
- );
- }
-
- let _ = f.sync_all();
- }
-}
-
-/// Read the persisted state from disk. Returns None if the file doesn't
-/// exist or can't be parsed.
-pub fn read_state() -> Option<PersistentState> {
- let path = state_file_path();
- let file = fs::File::open(&path).ok()?;
- let reader = std::io::BufReader::new(file);
-
- let mut active_tags: Option<u32> = None;
- let mut tag_layouts = Vec::new();
- let mut windows = Vec::new();
-
- for line in reader.lines() {
- let line = line.ok()?;
- let line = line.trim();
- if line.is_empty() || line.starts_with('#') {
- continue;
- }
-
- if let Some(rest) = line.strip_prefix("active_tags=") {
- active_tags = rest.parse::<u32>().ok();
- continue;
- }
-
- if let Some(rest) = line.strip_prefix("tag_layout") {
- let rest = rest.trim_start();
- let parts: Vec<&str> = rest.split('\t').collect();
- if parts.len() >= 3 {
- if let Ok(idx) = parts[0].parse::<usize>() {
- let mode = parse_tiling_mode_str(parts[1]);
- let has = parts[2] == "true";
- tag_layouts.push((idx, mode, has));
- }
- }
- continue;
- }
-
- if let Some(rest) = line.strip_prefix("window") {
- let rest = rest.trim_start();
- let parts: Vec<&str> = rest.split('\t').collect();
- if parts.len() >= 6 {
- let identifier = if parts[0] == "-" {
- None
- } else {
- Some(pct_decode(parts[0]))
- };
- let app_id = if parts[1] == "-" {
- None
- } else {
- Some(pct_decode(parts[1]))
- };
- let title = if parts[2] == "-" {
- None
- } else {
- Some(pct_decode(parts[2]))
- };
- let tags = parts[3].parse::<u32>().unwrap_or(1);
- let tiling_mode = parse_tiling_mode_str(parts[4]);
- let mode_locked = parts[5] == "true";
- let minimized = parts.get(6).map(|&s| s == "true").unwrap_or(false);
-
- windows.push(PersistentWindow {
- identifier,
- app_id,
- title,
- tags,
- tiling_mode,
- mode_locked,
- minimized,
- });
- }
- continue;
- }
- }
-
- Some(PersistentState {
- active_tags: active_tags.unwrap_or(1),
- tag_layouts,
- windows,
- })
-}
-
-/// Parse a tiling mode string (case-insensitive).
-fn parse_tiling_mode_str(s: &str) -> TilingMode {
- match s {
- "Cascade" => TilingMode::Cascade,
- "Grid" => TilingMode::Grid,
- "Fullscreen" => TilingMode::Fullscreen,
- "Floating" => TilingMode::Floating,
- "Popup" => TilingMode::Popup,
- "Side Panel" | "SidePanel" | "side-panel" => TilingMode::SidePanel,
- _ => TilingMode::Cascade,
- }
-}
-
-/// Apply persisted state to the window manager.
-/// Called on startup after config is loaded but before the main loop.
-///
-/// - Restores active_tags (so the correct tag view is shown).
-/// - Restores per-tag layout overrides.
-/// - For each live window, looks up its persisted entry by identifier
-/// (preferred) or app_id+title (fallback) and restores:
-/// - tags (which tag the window is on)
-/// - tiling_mode + mode_locked (if the mode was user-set)
-pub fn apply_state(wm: &mut WindowManager, state: &PersistentState) {
- // Restore active_tags
- wm.active_tags = state.active_tags;
-
- // Restore per-tag layouts
- for (idx, mode, has) in &state.tag_layouts {
- if *idx < NUM_TAGS {
- wm.tag_layouts[*idx] = *mode;
- wm.has_tag_layout[*idx] = *has;
- }
- }
-
- // Apply per-window state from persisted entries.
- // Build a lookup: identifier -> PersistentWindow, and (app_id, title) -> PersistentWindow.
- // Identifier is the primary key (stable across restarts).
- // App_id+title is a fallback for windows without identifiers.
- let mut by_identifier: Vec<&PersistentWindow> = Vec::new();
- let mut by_app_id_title: Vec<&PersistentWindow> = Vec::new();
-
- for pw in &state.windows {
- if pw.identifier.is_some() {
- by_identifier.push(pw);
- }
- if pw.app_id.is_some() {
- by_app_id_title.push(pw);
- }
- }
-
- for win in &mut wm.windows {
- if win.closed {
- continue;
- }
-
- // Try identifier match first
- let matched = if let Some(ref ident) = win.identifier {
- by_identifier.iter().find(|pw| {
- pw.identifier.as_deref() == Some(ident.as_str())
- })
- } else {
- None
- };
-
- // Fallback: app_id + title match
- let matched = matched.or_else(|| {
- if let Some(ref aid) = win.app_id {
- by_app_id_title.iter().find(|pw| {
- pw.app_id.as_deref() == Some(aid.as_str())
- && pw.title == win.title
- })
- } else {
- None
- }
- });
-
- if let Some(pw) = matched {
- // Restore tag assignment
- win.tags = pw.tags;
-
- // Restore locked tiling mode (user explicitly set this)
- if pw.mode_locked {
- win.tiling_mode = pw.tiling_mode;
- win.mode_locked = true;
- }
- win.minimized = pw.minimized;
- }
-
- // Post-restore normalization: Ensure blank steam_proton helper windows are untagged (tags = 0)
- let is_proton = win.app_id.as_deref() == Some("steam_proton");
- let is_blank = win.title.is_none() || win.title.as_deref().map_or(true, |t| t.is_empty());
- if is_proton && is_blank {
- win.tags = 0;
- }
- }
-
- eprintln!(
- "[state] restored: active_tags={}, tag_layouts={:?}, window_entries={}",
- state.active_tags,
- state
- .tag_layouts
- .iter()
- .filter(|(_, _, has)| *has)
- .count(),
- state.windows.len()
- );
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_pct_encode_decode_roundtrip() {
- let cases = vec![
- "",
- "hello",
- "hello world",
- "foo\tbar\nbaz",
- "100%",
- "org.qutebrowser.qutebrowser",
- "zed-industries/awesome-gpui: Awesome projects!",
- ];
- for case in cases {
- assert_eq!(pct_decode(&pct_encode(case)), case);
- }
- }
-
- #[test]
- fn test_write_read_roundtrip() {
- let dir = std::env::temp_dir().join("cce_client_state_test");
- let _ = fs::create_dir_all(&dir);
- let path = dir.join("cce_client_state");
-
- let mut wm = WindowManager::default();
- wm.active_tags = 0b1010; // tags 2 and 4
- wm.tag_layouts[1] = TilingMode::Grid;
- wm.has_tag_layout[1] = true;
-
- // Add a window
- let mut win = crate::types::Window::default();
- win.id = 42;
- win.identifier = Some("river-window-123".to_string());
- win.app_id = Some("org.qutebrowser.qutebrowser".to_string());
- win.title = Some("Test Page - qutebrowser".to_string());
- win.tags = 0b100; // tag 3
- win.tiling_mode = TilingMode::Fullscreen;
- win.mode_locked = true;
- wm.windows.push(win);
-
- // Write
- if let Some(parent) = path.parent() {
- let _ = fs::create_dir_all(parent);
- }
- if let Ok(mut f) = fs::File::create(&path) {
- // Manually write using the same format as write_state
- let _ = writeln!(f, "active_tags={}", wm.active_tags);
- for tag_bit in 0..NUM_TAGS {
- let _ = writeln!(
- f,
- "tag_layout\t{}\t{}\t{}",
- tag_bit,
- wm.tag_layouts[tag_bit].as_str(),
- wm.has_tag_layout[tag_bit]
- );
- }
- for w in &wm.windows {
- let ident = w.identifier.as_deref().map(pct_encode).unwrap_or("-".to_string());
- let app_id = w.app_id.as_deref().map(pct_encode).unwrap_or("-".to_string());
- let title = w.title.as_deref().map(pct_encode).unwrap_or("-".to_string());
- let _ = writeln!(
- f,
- "window\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
- ident, app_id, title, w.tags, w.tiling_mode.as_str(), w.mode_locked, w.minimized
- );
- }
- }
-
- // Read and verify via the public read_state() path.
- // We set HOME to our temp dir so state_file_path() finds the test file.
- // Instead, use a direct parse approach:
- let file = fs::File::open(&path).unwrap();
- let reader = std::io::BufReader::new(file);
- let mut active_tags: Option<u32> = None;
- let mut tag_layouts = Vec::new();
- let mut windows = Vec::new();
- for line_r in reader.lines() {
- let line = line_r.unwrap();
- let line = line.trim();
- if line.is_empty() { continue; }
- if let Some(rest) = line.strip_prefix("active_tags=") {
- active_tags = rest.parse::<u32>().ok();
- } else if let Some(rest) = line.strip_prefix("tag_layout") {
- let rest = rest.trim_start();
- let parts: Vec<&str> = rest.split('\t').collect();
- if parts.len() >= 3 {
- if let Ok(idx) = parts[0].parse::<usize>() {
- let mode = parse_tiling_mode_str(parts[1]);
- let has = parts[2] == "true";
- tag_layouts.push((idx, mode, has));
- }
- }
- } else if let Some(rest) = line.strip_prefix("window") {
- let rest = rest.trim_start();
- let parts: Vec<&str> = rest.split('\t').collect();
- if parts.len() >= 6 {
- let identifier = if parts[0] == "-" { None } else { Some(pct_decode(parts[0])) };
- let app_id = if parts[1] == "-" { None } else { Some(pct_decode(parts[1])) };
- let title = if parts[2] == "-" { None } else { Some(pct_decode(parts[2])) };
- let tags = parts[3].parse::<u32>().unwrap_or(1);
- let tiling_mode = parse_tiling_mode_str(parts[4]);
- let mode_locked = parts[5] == "true";
- let minimized = parts.get(6).map(|&s| s == "true").unwrap_or(false);
- windows.push(PersistentWindow { identifier, app_id, title, tags, tiling_mode, mode_locked, minimized });
- }
- }
- }
- let state = PersistentState {
- active_tags: active_tags.unwrap_or(1),
- tag_layouts,
- windows,
- };
- assert_eq!(state.active_tags, 0b1010);
- assert_eq!(state.tag_layouts.len(), NUM_TAGS);
- // Tag layout at index 1 should be Grid + has=true
- let tl1 = state.tag_layouts.iter().find(|(idx, _, _)| *idx == 1);
- assert!(tl1.is_some());
- let (_, mode, has) = tl1.unwrap();
- assert_eq!(*mode, TilingMode::Grid);
- assert!(has);
-
- assert_eq!(state.windows.len(), 1);
- let pw = &state.windows[0];
- assert_eq!(pw.identifier.as_deref(), Some("river-window-123"));
- assert_eq!(pw.app_id.as_deref(), Some("org.qutebrowser.qutebrowser"));
- assert_eq!(pw.title.as_deref(), Some("Test Page - qutebrowser"));
- assert_eq!(pw.tags, 0b100);
- assert_eq!(pw.tiling_mode, TilingMode::Fullscreen);
- assert!(pw.mode_locked);
- assert!(!pw.minimized);
-
- // Clean up
- let _ = fs::remove_file(&path);
- }
-}
diff --git a/src/client/status.rs b/src/client/status.rs
deleted file mode 100644
index df2fdc8..0000000
--- a/src/client/status.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-// Status file writing and waybar signaling
-
-use crate::types::{TilingMode, WindowManager};
-use std::fs;
-use std::process::Command;
-
-pub const NUM_TAGS: u32 = 4;
-
-/// Write status files only (no pkill signaling). Safe to call inside
-/// Dispatch callbacks — no fork, no blocking, just file I/O.
-pub fn write_status_files(state: &WindowManager) {
- use crate::paths;
- use std::io::Write;
-
- // /tmp/cce-client-tags: active_tags focused_tags num_tags
- if let Ok(mut f) = fs::File::create(paths::get_tags_path()) {
- let _ = writeln!(
- f,
- "{} {} {}",
- state.active_tags, state.focused_tags, NUM_TAGS
- );
- }
-
- // /tmp/cce-client-layout: focused window's tiling mode
- let mode_str = if state.expose_active {
- "Expose"
- } else {
- state
- .focused_window()
- .map(|w| tiling_mode_str(w.tiling_mode))
- .unwrap_or("none")
- };
-
- if let Ok(mut f) = fs::File::create(paths::get_layout_path()) {
- let _ = writeln!(f, "{}", mode_str);
- }
-
- // /tmp/cce-client-windows: one line per window
- if let Ok(mut f) = fs::File::create(paths::get_windows_path()) {
- let focused_title = state.focused_window().map(|w| w.title.clone());
-
- for win in &state.windows {
- let mode_str = tiling_mode_str(win.tiling_mode);
- let decoration_str = match win.decoration_hint {
- 0 => "only_csd",
- 1 => "prefers_csd",
- 2 => "prefers_ssd",
- 3 => "no_preference",
- _ => "unknown",
- };
- let presentation_str = match win.presentation_hint {
- 0 => "vsync",
- 1 => "async",
- _ => "unknown",
- };
- let _ = writeln!(
- f,
- "window app_id={} title={} mode={} decoration={} presentation={} tags={} x={} y={} w={} h={} has_parent={} minimized={}",
- win.app_id.as_deref().unwrap_or("(null)"),
- win.title.as_deref().unwrap_or("(null)"),
- mode_str,
- decoration_str,
- presentation_str,
- win.tags, win.x, win.y, win.width, win.height,
- win.has_parent,
- win.minimized,
- );
- }
-
- // /tmp/cce-client-title
- if let Some(title) = focused_title {
- if let Ok(mut tf) = fs::File::create(paths::get_title_path()) {
- let _ = writeln!(tf, "{}", title.as_deref().unwrap_or("(null)"));
- }
- }
- }
-}
-
-/// Write all status files and signal waybar.
-/// WARNING: The pkill calls block the event loop. Do NOT call this
-/// inside a Dispatch callback. Use write_status_files() instead.
-pub fn update_status_files(state: &WindowManager) {
- write_status_files(state);
-
- // Signal waybar
- let _ = Command::new("pkill").args(["-RTMIN+8", "waybar"]).output();
- let _ = Command::new("pkill").args(["-RTMIN+9", "waybar"]).output();
- let _ = Command::new("pkill").args(["-RTMIN+10", "waybar"]).output();
-}
-
-fn tiling_mode_str(mode: TilingMode) -> &'static str {
- match mode {
- TilingMode::Floating => "Floating",
- TilingMode::Cascade => "Cascade",
- TilingMode::Grid => "Grid",
- TilingMode::Fullscreen => "Fullscreen",
- TilingMode::Popup => "Popup",
- TilingMode::SidePanel => "Side Panel",
- }
-}
diff --git a/src/client/tiling.rs b/src/client/tiling.rs
deleted file mode 100644
index 52ecc93..0000000
--- a/src/client/tiling.rs
+++ /dev/null
@@ -1,311 +0,0 @@
-// Tiling formulas ported from cce-client.c
-
-/// Cascade depth factor: each depth step multiplies channels by this
-pub const CASCADE_DEPTH_FACTOR: f64 = 0.80;
-
-/// Full alpha, byte-replicated for River's color format
-pub const CASCADE_ALPHA: u32 = 0xFFFFFFFFu32;
-
-/// Cascade base green (focused window) in fixed-point format
-/// These come from the layout.border_r/g/b values, not hardcoded constants.
-/// The C code uses #5c9060 as the default cascade base but it's actually
-/// read from config. We just define the factor here.
-
-/// Tile a window in cascade mode.
-///
-/// Returns (x, y, width, height) for the window at the given cascade index.
-///
-/// Screen edges use per-side gaps; inter-window offset is cascade_offset.
-/// width = screen_w - gap_left - gap_right - bw*2 - cascade_offset*(n_cascade - 1)
-/// height = screen_h - gap_top - gap_bottom - bw*2 - cascade_offset*(n_cascade - 1)
-/// x = gap_left + bw + idx * cascade_offset
-/// y = bar_height + gap_top + bw + idx * cascade_offset
-pub fn tile_cascade(
- screen_w: i32,
- screen_h: i32,
- _gap: i32,
- gap_top: i32,
- gap_left: i32,
- gap_right: i32,
- gap_bottom: i32,
- bw: i32,
- cascade_offset: i32,
- bar_height: i32,
- n_cascade: i32,
- idx: i32,
-) -> (i32, i32, i32, i32) {
- let dec_h = std::cmp::max(bw, 16);
- let width = screen_w - gap_left - gap_right - bw * 2 - cascade_offset * (n_cascade - 1);
- let height = screen_h - bar_height - gap_top - gap_bottom - (dec_h + bw) - cascade_offset * (n_cascade - 1);
- let width = if width < 1 { 1 } else { width };
- let height = if height < 1 { 1 } else { height };
- let pos_idx = n_cascade - 1 - idx;
- let x = gap_left + bw + pos_idx * cascade_offset;
- let y = bar_height + gap_top + dec_h + pos_idx * cascade_offset;
- (x, y, width, height)
-}
-
-/// Tile a window in grid mode.
-///
-/// Uses a 2-column grid layout.
-///
-/// Screen edges use per-side gaps; inter-window spacing uses `gap`.
-/// cols = 2
-/// rows = (n_grid + cols - 1) / cols
-/// width = (screen_w - gap_left - gap_right - (cols - 1) * gap) / cols - 2 * bw
-/// height = (screen_h - gap_top - gap_bottom - (rows - 1) * gap) / rows - (dec_h + bw)
-/// x = gap_left + bw + col * (width + 2 * bw + gap)
-/// y = bar_height + gap_top + dec_h + row * (height + (dec_h + bw) + gap)
-pub fn tile_grid(
- screen_w: i32,
- screen_h: i32,
- gap: i32,
- gap_top: i32,
- gap_left: i32,
- gap_right: i32,
- gap_bottom: i32,
- bw: i32,
- bar_height: i32,
- n_grid: i32,
- idx: i32,
-) -> (i32, i32, i32, i32) {
- let cols = 2i32;
- let row = idx / cols;
- let col = idx % cols;
- let rows = (n_grid + cols - 1) / cols;
- let dec_h = std::cmp::max(bw, 16);
- let width = (screen_w - gap_left - gap_right - (cols - 1) * gap) / cols - 2 * bw;
- let height = (screen_h - bar_height - gap_top - gap_bottom - (rows - 1) * gap) / rows - (dec_h + bw);
- let width = if width < 1 { 1 } else { width };
- let height = if height < 1 { 1 } else { height };
- let x = gap_left + bw + col * (width + 2 * bw + gap);
- let y = bar_height + gap_top + dec_h + row * (height + (dec_h + bw) + gap);
- (x, y, width, height)
-}
-
-
-
-/// Tile a window in fullscreen mode — fills the screen minus gaps, bar, and borders.
-///
-/// Only the focused window is visible; other fullscreen windows are skipped.
-/// A fullscreen window occupies the entire screen (0, 0, screen_w, screen_h).
-pub fn tile_fullscreen(
- screen_w: i32,
- screen_h: i32,
- _gap_top: i32,
- _gap_left: i32,
- _gap_right: i32,
- _gap_bottom: i32,
- _bw: i32,
- _bar_height: i32,
-) -> (i32, i32, i32, i32) {
- (0, 0, screen_w, screen_h)
-}
-
-/// Interpolate a byte-replicated 32-bit channel (0xVVVVVVVV) by factor^depth.
-///
-/// depth 0 returns the base color unchanged; each step darkens by factor.
-/// Returns byte-replicated 32-bit value for River's color format.
-///
-/// Ported from C: interp_channel()
-pub fn interp_channel(fp_channel: u32, factor: f64, depth: i32) -> u32 {
- let base = (fp_channel & 0xFF) as u8;
- let f = factor.powi(depth);
- let val = ((base as f64) * f) as u8;
- val as u32 * 0x01010101
-}
-
-/// Compute a "#RRGGBB" hex color string for a given cascade depth.
-///
-/// Takes the base border color channels in byte-replicated format and
-/// interpolates them toward black by CASCADE_DEPTH_FACTOR^depth.
-///
-/// Ported from C: cascade_hex_color()
-pub fn cascade_hex_color(r: u32, g: u32, b: u32, depth: i32) -> String {
- let ri = interp_channel(r, CASCADE_DEPTH_FACTOR, depth);
- let gi = interp_channel(g, CASCADE_DEPTH_FACTOR, depth);
- let bi = interp_channel(b, CASCADE_DEPTH_FACTOR, depth);
- // Extract the low byte for display
- let rv = ri & 0xFF;
- let gv = gi & 0xFF;
- let bv = bi & 0xFF;
- format!("#{:02x}{:02x}{:02x}", rv, gv, bv)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_tile_cascade_single() {
- // Single cascade window should fill the screen minus gaps/borders
- let (x, y, w, h) = tile_cascade(1920, 1080, 18, 18, 18, 18, 18, 18, 32, 28, 1, 0);
- assert_eq!(x, 36); // gap_left + bw
- assert_eq!(y, 64); // bar_height + gap_top + bw
- // w = 1920 - 18 - 18 - 18*2 - 32*0 = 1920 - 72 = 1848
- assert_eq!(w, 1848);
- // h = 1080 - 28 (bar) - 18 - 18 - 18*2 - 32*0 = 1080 - 100 = 980
- assert_eq!(h, 980);
- }
-
- #[test]
- fn test_tile_cascade_multiple() {
- // 3 cascade windows, idx=2 (the back one, highest index, should be up/left)
- let (x, y, _w, _h) = tile_cascade(1920, 1080, 18, 18, 18, 18, 18, 18, 32, 28, 3, 2);
- assert_eq!(x, 36); // gap_left + bw
- assert_eq!(y, 64); // bar + gap_top + bw
-
- // 3 cascade windows, idx=0 (the focused one, lowest index, should be down/right)
- let (x, y, _w, _h) = tile_cascade(1920, 1080, 18, 18, 18, 18, 18, 18, 32, 28, 3, 0);
- assert_eq!(x, 36 + 2 * 32); // gap_left + bw + 2 * offset
- assert_eq!(y, 64 + 2 * 32); // bar + gap_top + bw + 2 * offset
- }
-
- #[test]
- fn test_tile_cascade_minimum_size() {
- // Very small screen, many windows — should clamp to 1
- let (_, _, w, h) = tile_cascade(100, 100, 18, 18, 18, 18, 18, 18, 32, 28, 10, 5);
- assert!(w >= 1);
- assert!(h >= 1);
- }
-
- #[test]
- fn test_tile_cascade_asymmetric_gaps() {
- // Asymmetric screen gaps: top=10, left=20, right=30, bottom=40
- let (x, y, w, h) = tile_cascade(1920, 1080, 12, 10, 20, 30, 40, 6, 24, 28, 1, 0);
- assert_eq!(x, 20 + 6); // gap_left + bw = 26
- assert_eq!(y, 28 + 10 + 16); // bar_height + gap_top + dec_h = 54
- // w = 1920 - 20 - 30 - 6*2 = 1920 - 62 = 1858
- assert_eq!(w, 1858);
- // h = 1080 - 28 (bar) - 10 - 40 - (16 + 6) = 1080 - 100 = 980
- assert_eq!(h, 980);
- }
-
- #[test]
- fn test_tile_grid_two_windows() {
- let (x0, y0, w0, h0) = tile_grid(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 2, 0);
- let (x1, y1, w1, h1) = tile_grid(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 2, 1);
-
- // Both windows should have same dimensions
- assert_eq!(w0, w1);
- assert_eq!(h0, h1);
-
- // Window 1 is to the right of window 0
- assert!(x1 > x0);
- assert_eq!(y0, y1); // Same row
-
- // Width = (1920 - 18 - 18 - 1*18) / 2 - 2*18 = (1920-54)/2 - 36 = 933 - 36 = 897
- assert_eq!(w0, 897);
- // rows=1, Height = (1080 - 28 (bar) - 18 - 18 - 0*18) / 1 - 2*18 = 1016 - 36 = 980
- assert_eq!(h0, 980);
- }
-
- #[test]
- fn test_tile_grid_four_windows() {
- // 4 windows in 2x2 grid
- let (x0, y0, _, _) = tile_grid(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 4, 0);
- let (x1, y1, _, _) = tile_grid(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 4, 1);
- let (x2, y2, _, _) = tile_grid(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 4, 2);
- let (x3, y3, _, _) = tile_grid(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 4, 3);
-
- // Row 0: windows 0,1
- assert!(y0 == y1);
- assert!(x0 < x1);
- // Row 1: windows 2,3
- assert!(y2 == y3);
- assert!(x2 < x3);
- // Row 1 below row 0
- assert!(y2 > y0);
- }
-
- #[test]
- fn test_tile_grid_asymmetric_gaps() {
- // Asymmetric screen gaps with 2 windows in 1 row
- let (x0, y0, w0, h0) = tile_grid(1920, 1080, 12, 10, 20, 30, 40, 6, 28, 2, 0);
- let (x1, _y1, w1, _h1) = tile_grid(1920, 1080, 12, 10, 20, 30, 40, 6, 28, 2, 1);
-
- // Width = (1920 - 20 - 30 - 1*12) / 2 - 2*6 = (1920-62)/2 - 12 = 929 - 12 = 917
- assert_eq!(w0, 917);
- assert_eq!(w0, w1);
-
- // x0 = gap_left + bw = 20 + 6 = 26
- assert_eq!(x0, 26);
- // y0 = bar_height + gap_top + dec_h = 28 + 10 + 16 = 54
- assert_eq!(y0, 54);
- // rows=1, Height = (1080 - 28 (bar) - 10 - 40 - 0*12) / 1 - (16 + 6) = 1002 - 22 = 980
- assert_eq!(h0, 980);
-
- // x1 = gap_left + bw + 1*(917 + 2*6 + 12) = 26 + 941 = 967
- assert_eq!(x1, 967);
- }
-
- #[test]
- fn test_interp_channel_depth_zero() {
- // depth 0 should return the base color unchanged
- let result = interp_channel(0x5C5C5C5C, CASCADE_DEPTH_FACTOR, 0);
- assert_eq!(result, 0x5C5C5C5C);
- }
-
- #[test]
- fn test_interp_channel_depth_one() {
- let result = interp_channel(0x90909090, CASCADE_DEPTH_FACTOR, 1);
- // 0x90 * 0.80 = 144 * 0.80 = 115.2 → 115 = 0x73
- assert_eq!(result & 0xFF, 0x73);
- }
-
- #[test]
- fn test_interp_channel_depth_two() {
- let result = interp_channel(0x60606060, CASCADE_DEPTH_FACTOR, 2);
- // 0x60 * 0.80^2 = 96 * 0.64 = 61.44 → 61 = 0x3D
- assert_eq!(result & 0xFF, 0x3D);
- }
-
- #[test]
- fn test_cascade_hex_color_depth_zero() {
- let color = cascade_hex_color(0x5C5C5C5C, 0x90909090, 0x60606060, 0);
- assert_eq!(color, "#5c9060");
- }
-
- #[test]
- fn test_cascade_hex_color_depth_one() {
- let color = cascade_hex_color(0x5C5C5C5C, 0x90909090, 0x60606060, 1);
- // 0x5C*0.80=0x49, 0x90*0.80=0x73, 0x60*0.80=0x4C
- assert_eq!(color, "#49734c");
- }
-
- #[test]
- fn test_cascade_hex_color_dark_gray() {
- // Using #3e3e3e as base (normal border color)
- let color = cascade_hex_color(0x3E3E3E3E, 0x3E3E3E3E, 0x3E3E3E3E, 0);
- assert_eq!(color, "#3e3e3e");
- }
-
-
-
- #[test]
- fn test_tile_fullscreen_basic() {
- let (x, y, w, h) = tile_fullscreen(1920, 1080, 18, 18, 18, 18, 6, 28);
- assert_eq!(x, 0);
- assert_eq!(y, 0);
- assert_eq!(w, 1920);
- assert_eq!(h, 1080);
- }
-
- #[test]
- fn test_tile_fullscreen_asymmetric_gaps() {
- let (x, y, w, h) = tile_fullscreen(1920, 1080, 10, 20, 30, 40, 6, 28);
- assert_eq!(x, 0);
- assert_eq!(y, 0);
- assert_eq!(w, 1920);
- assert_eq!(h, 1080);
- }
-
- #[test]
- fn test_tile_fullscreen_minimum_size() {
- let (x, y, w, h) = tile_fullscreen(50, 50, 18, 18, 18, 18, 6, 28);
- assert_eq!(x, 0);
- assert_eq!(y, 0);
- assert_eq!(w, 50);
- assert_eq!(h, 50);
- }
-}
diff --git a/src/client/types.rs b/src/client/types.rs
deleted file mode 100644
index a0acb46..0000000
--- a/src/client/types.rs
+++ /dev/null
@@ -1,781 +0,0 @@
-// Core data structures for cce-client
-
-use std::collections::HashMap;
-
-pub const NUM_TAGS: usize = 4;
-
-/// Tiling mode for a window
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TilingMode {
- Floating,
- Cascade,
- Grid,
- Fullscreen,
- Popup,
- SidePanel,
-}
-
-impl TilingMode {
- pub fn as_str(&self) -> &'static str {
- match self {
- TilingMode::Floating => "Floating",
- TilingMode::Cascade => "Cascade",
- TilingMode::Grid => "Grid",
- TilingMode::Fullscreen => "Fullscreen",
- TilingMode::Popup => "Popup",
- TilingMode::SidePanel => "Side Panel",
- }
- }
-}
-
-/// Actions that can be triggered by keybindings or IPC
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Action {
- None,
- Spawn,
- Toggle,
- Close,
- FocusNext,
- FocusPrev,
- Move,
- Resize,
- Exit,
- Fullscreen,
- LayoutNext,
- ModeNext,
- ModeNextShared,
- Reload,
- Restart,
- View1,
- View2,
- View3,
- View4,
- Toggle1,
- Toggle2,
- Toggle3,
- Toggle4,
- SetTag1,
- SetTag2,
- SetTag3,
- SetTag4,
- Expose,
- Minimize,
- SidePanelLeft,
- SidePanelRight,
-}
-
-/// Layout parameters
-#[derive(Debug, Clone)]
-pub struct Layout {
- pub gap: i32, // inter-window spacing
- pub gap_top: i32, // screen edge inset, top (above bar)
- pub gap_left: i32, // screen edge inset, left
- pub gap_right: i32, // screen edge inset, right
- pub gap_bottom: i32, // screen edge inset, bottom
- pub cascade_offset: i32,
- pub bar_height: i32,
- pub border_width: i32,
- pub fullscreen_border_width: i32,
- pub cascade_border_width: i32,
- pub grid_border_width: i32,
- pub floating_border_width: i32,
- pub border_r: u32,
- pub border_g: u32,
- pub border_b: u32,
- pub border_a: u32,
- pub background_r: u32,
- pub background_g: u32,
- pub background_b: u32,
- pub background_a: u32,
- pub border_font_size: i32,
- pub transition_duration: i32,
- pub grid_gap: i32,
- pub border_blur: bool,
- pub window_blur: bool,
- pub side_panel_behavior: String,
- pub side_panel_width: i32,
- pub side_panel_position: String,
- pub side_panel_border_gap: i32,
- pub side_panel_border_opacity: i32,
-}
-
-impl Default for Layout {
- fn default() -> Self {
- Layout {
- gap: 48,
- gap_top: 48,
- gap_left: 48,
- gap_right: 48,
- gap_bottom: 48,
- cascade_offset: 20,
- bar_height: 24,
- border_width: 6,
- fullscreen_border_width: 0,
- cascade_border_width: 6,
- grid_border_width: 6,
- floating_border_width: 6,
- border_r: 0x3E3E3E3Eu32,
- border_g: 0x3E3E3E3Eu32,
- border_b: 0x3E3E3E3Eu32,
- border_a: 0xFFFFFFFFu32,
- background_r: 0x0A0A0A0Au32,
- background_g: 0x1A1A1A1Au32,
- background_b: 0x0E0E0E0Eu32,
- background_a: 0xFFFFFFFFu32,
- border_font_size: 11,
- transition_duration: 300,
- grid_gap: 18,
- border_blur: false,
- window_blur: false,
- side_panel_behavior: "inline".to_string(),
- side_panel_width: 360,
- side_panel_position: "left".to_string(),
- side_panel_border_gap: 0,
- side_panel_border_opacity: 100,
- }
- }
-}
-
-/// A rule that matches windows by app_id/title and assigns a tiling mode
-#[derive(Debug, Clone)]
-pub struct ModeRule {
- pub mode: TilingMode,
- pub app_id_pattern: String,
- pub title_pattern: Option<String>,
- pub single_instance: bool,
- pub tag: i32,
- pub circular: bool,
- pub ssd: Option<bool>,
-}
-
-/// A pending keyboard binding waiting to be applied to seats
-#[derive(Debug, Clone)]
-pub struct PendingXkbBinding {
- pub mods: u32,
- pub keysym: u32,
- pub action: Action,
- pub command: Option<String>,
-}
-
-/// A pending pointer binding waiting to be applied to seats
-#[derive(Debug, Clone)]
-pub struct PendingPointerBinding {
- pub mods: u32,
- pub button: u32,
- pub action: Action,
-}
-
-/// User data attached to XKB/pointer binding proxies, carrying the action
-/// to execute when the binding is triggered.
-#[derive(Debug, Clone)]
-pub struct BindingUserData {
- pub seat_id: u64,
- pub action: Action,
- pub command: Option<String>,
-}
-
-/// An input device (keyboard or other)
-#[derive(Debug, Clone)]
-pub struct InputDevice {
- pub is_keyboard: bool,
-}
-
-/// A managed output (monitor)
-#[derive(Debug, Clone)]
-pub struct Output {
- pub id: u64,
- pub removed: bool,
- pub x: i32,
- pub y: i32,
- pub width: i32,
- pub height: i32,
- pub usable_x: i32,
- pub usable_y: i32,
- pub usable_width: i32,
- pub usable_height: i32,
- pub wl_output_name: Option<u32>,
-}
-
-impl Default for Output {
- fn default() -> Self {
- Output {
- id: 0,
- removed: false,
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- usable_x: 0,
- usable_y: 0,
- usable_width: 0,
- usable_height: 0,
- wl_output_name: None,
- }
- }
-}
-
-/// A managed window
-#[derive(Debug, Clone)]
-pub struct Window {
- pub id: u64,
- pub is_new: bool,
- pub closed: bool,
- pub tags: u32,
- pub x: i32,
- pub y: i32,
- pub width: i32,
- pub height: i32,
- pub committed_width: i32,
- pub committed_height: i32,
- pub app_id: Option<String>,
- pub title: Option<String>,
- pub identifier: Option<String>,
- pub parent_id: Option<u64>,
- pub has_parent: bool,
- pub pid: u32,
- pub hint_min_width: i32,
- pub hint_min_height: i32,
- pub hint_max_width: i32,
- pub hint_max_height: i32,
- pub decoration_hint: u32,
- pub presentation_hint: u32,
- pub fullscreen_requested: bool,
- pub maximize_requested: bool,
- pub minimize_requested: bool,
- pub minimized: bool,
- pub tiling_mode: TilingMode,
- pub mode_locked: bool,
- /// Whether we've queued an xprop check for XWayland parent detection.
- /// River doesn't forward WM_TRANSIENT_FOR for XWayland windows, so we
- /// check via xprop as a fallback.
- pub needs_xprop_check: bool,
- /// How many ManageStart cycles we've waited for the xprop result file.
- pub xprop_check_attempts: u8,
- /// How many ManageStart cycles we've waited for window metadata (app_id/title).
- pub metadata_check_attempts: u8,
- pub anim_x: Option<f64>,
- pub anim_y: Option<f64>,
- pub anim_w: Option<f64>,
- pub anim_h: Option<f64>,
- pub anim_opacity: Option<f64>,
- pub circular: bool,
- pub size_hint_applied: bool,
- pub last_borders: Option<(u32, i32, u32, u32, u32, u32)>,
- pub last_opacity: Option<u32>,
- pub last_circular: Option<u32>,
- pub last_blur: Option<u32>,
-}
-
-impl Default for Window {
- fn default() -> Self {
- Window {
- id: 0,
- is_new: true,
- closed: false,
- tags: 1,
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- committed_width: 0,
- committed_height: 0,
- app_id: None,
- title: None,
- identifier: None,
- parent_id: None,
- has_parent: false,
- pid: 0,
- hint_min_width: 0,
- hint_min_height: 0,
- hint_max_width: 0,
- hint_max_height: 0,
- decoration_hint: 3, // no_preference
- presentation_hint: 0, // vsync
- fullscreen_requested: false,
- maximize_requested: false,
- minimize_requested: false,
- minimized: false,
- tiling_mode: TilingMode::Floating,
- mode_locked: false,
- needs_xprop_check: false,
- xprop_check_attempts: 0,
- metadata_check_attempts: 0,
- anim_x: None,
- anim_y: None,
- anim_w: None,
- anim_h: None,
- anim_opacity: None,
- circular: false,
- size_hint_applied: false,
- last_borders: None,
- last_opacity: None,
- last_circular: None,
- last_blur: None,
- }
- }
-}
-
-/// A seat (input device group)
-#[derive(Debug, Clone)]
-pub struct Seat {
- pub id: u64,
- pub is_new: bool,
- pub removed: bool,
- pub focused_window_id: Option<u64>,
- pub hovered_window_id: Option<u64>,
- pub interacted_window_id: Option<u64>,
- pub pending_action: Action,
- pub pending_command: Option<String>,
-}
-
-impl Default for Seat {
- fn default() -> Self {
- Seat {
- id: 0,
- is_new: true,
- removed: false,
- focused_window_id: None,
- hovered_window_id: None,
- interacted_window_id: None,
- pending_action: Action::None,
- pending_command: None,
- }
- }
-}
-
-/// The main window manager state
-#[derive(Debug, Clone)]
-pub struct WindowManager {
- pub outputs: Vec<Output>,
- pub windows: Vec<Window>,
- pub seats: Vec<Seat>,
- pub pending_bindings: Vec<PendingXkbBinding>,
- pub pending_pointer_bindings: Vec<PendingPointerBinding>,
- pub mode_rules: Vec<ModeRule>,
- pub layout: Layout,
- pub active_tags: u32,
- pub focused_tags: u32,
- pub config_done: bool,
- pub in_manage_sequence: bool,
- pub needs_render: bool,
- pub needs_focus: bool,
- pub needs_status_update: bool,
- pub exit_requested: bool,
- pub global_layout: TilingMode,
- pub tag_layouts: [TilingMode; NUM_TAGS],
- pub has_tag_layout: [bool; NUM_TAGS],
- pub input_devices: Vec<InputDevice>,
- pub env_vars: HashMap<String, String>,
- pub pending_startup_apps: Vec<String>,
- pub startup_spawned: bool,
- pub output_scale: f64,
- /// When true, apply configured output_scale via wlr-output-management
- /// on the next output_manager done event. Set by config load and
- /// by VT-switch-back (where wlroots resets scale to 1).
- pub pending_scale_apply: bool,
- /// When true, apply persisted state from ~/.cache/cce_client_state on the
- /// next ManageStart cycle (after windows have been re-advertised).
- /// Set to true on startup/restart, consumed after application.
- pub needs_state_restore: bool,
- /// Counter for how many ManageStart cycles we've waited for window metadata
- /// before applying persisted state. Reset to 0 after state is applied.
- 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,
- pub cursor_theme: Option<String>,
- pub cursor_size: Option<u32>,
- pub cursor_theme_applied: bool,
- /// Whether system notifications are enabled
- 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::input::InputDaemonMsg>>,
- pub trackpad_disabled: bool,
- pub expose_active: bool,
- pub expose_visual_active: bool,
- pub animating: bool,
- pub last_animation_tick: std::time::Instant,
- pub last_frame_time: std::time::Instant,
-}
-
-impl Default for WindowManager {
- fn default() -> Self {
- WindowManager {
- outputs: Vec::new(),
- windows: Vec::new(),
- seats: Vec::new(),
- pending_bindings: Vec::new(),
- pending_pointer_bindings: Vec::new(),
- mode_rules: Vec::new(),
- layout: Layout::default(),
- active_tags: 1,
- focused_tags: 0,
- config_done: false,
- in_manage_sequence: false,
- needs_render: true, // render on first frame
- needs_focus: true, // focus on first frame
- needs_status_update: true, // update status files on first cycle
- exit_requested: false,
- global_layout: TilingMode::Cascade,
- tag_layouts: [TilingMode::Cascade; NUM_TAGS],
- has_tag_layout: [true; NUM_TAGS],
- input_devices: Vec::new(),
- env_vars: HashMap::new(),
- pending_startup_apps: Vec::new(),
- startup_spawned: false,
- output_scale: 0.0,
- pending_scale_apply: false,
- 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,
- cursor_theme: None,
- cursor_size: None,
- cursor_theme_applied: false,
- notifications_enable: true,
- reload_commands: Vec::new(),
- input_controller: None,
- trackpad_disabled: false,
- expose_active: false,
- expose_visual_active: false,
- animating: false,
- last_animation_tick: std::time::Instant::now(),
- last_frame_time: std::time::Instant::now(),
- }
- }
-}
-
-impl WindowManager {
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Find a window by its ID
- pub fn get_window(&self, id: u64) -> Option<&Window> {
- self.windows.iter().find(|w| w.id == id)
- }
-
- /// Find a window by its ID (mutable)
- pub fn get_window_mut(&mut self, id: u64) -> Option<&mut Window> {
- self.windows.iter_mut().find(|w| w.id == id)
- }
-
- /// Find the first seat that has a focused window
- pub fn first_seat_with_focus(&self) -> Option<&Seat> {
- self.seats.iter().find(|s| s.focused_window_id.is_some())
- }
-
- /// Get the focused window for the first seat that has one
- pub fn focused_window(&self) -> Option<&Window> {
- let seat = self.first_seat_with_focus()?;
- let wid = seat.focused_window_id?;
- self.get_window(wid)
- }
-
- /// Get the focused window (mutable)
- pub fn focused_window_mut(&mut self) -> Option<&mut Window> {
- let (wid, _) = {
- let seat = self.first_seat_with_focus()?;
- (seat.focused_window_id?, seat.id)
- };
- self.get_window_mut(wid)
- }
-
- /// Move a window to the end of the windows vector.
- /// This makes it the last cascade window (front of visual stack,
- /// rightmost/bottommost position, brightest border).
- /// Returns true if the window was moved, false if not found or already last.
- pub fn move_window_to_end(&mut self, id: u64) -> bool {
- let idx = match self.windows.iter().position(|w| w.id == id) {
- Some(i) => i,
- None => return false,
- };
- // Already last?
- if idx == self.windows.len() - 1 {
- return false;
- }
- let win = self.windows.remove(idx);
- self.windows.push(win);
- true
- }
-}
-
-/// Parse a hex color string like "#RRGGBB" or "#RRGGBBAA" into
-/// byte-replicated 32-bit channel values for River's color format.
-/// River divides each u32 by maxInt(u32) to get a float, so each byte
-/// must be replicated: 0xVV -> 0xVVVVVVVV.
-pub fn parse_hex_color(s: &str) -> Option<(u32, u32, u32, u32)> {
- let s = s.strip_prefix('#')?;
- if s.len() != 6 && s.len() != 8 {
- return None;
- }
- let r = u8::from_str_radix(&s[0..2], 16).ok()? as u32;
- let g = u8::from_str_radix(&s[2..4], 16).ok()? as u32;
- let b = u8::from_str_radix(&s[4..6], 16).ok()? as u32;
- let a = if s.len() == 8 {
- u8::from_str_radix(&s[6..8], 16).ok()? as u32
- } else {
- 0xFFu32
- };
- // Byte-replicate: 0xVV -> 0xVVVVVVVV
- Some((r * 0x01010101, g * 0x01010101, b * 0x01010101, a * 0x01010101))
-}
-
-/// Parse a tiling mode string
-pub fn parse_tiling_mode(s: &str) -> TilingMode {
- match s {
- "cascade" => TilingMode::Cascade,
- "grid" => TilingMode::Grid,
- "fullscreen" => TilingMode::Fullscreen,
- "floating" => TilingMode::Floating,
- "popup" => TilingMode::Popup,
- "side-panel" | "side_panel" | "side panel" | "Side Panel" => TilingMode::SidePanel,
- _ => TilingMode::Floating,
- }
-}
-
-/// Parse an action string
-pub fn parse_action(s: &str) -> Action {
- if s == "close" {
- Action::Close
- } else if s == "exit" {
- Action::Exit
- } else if s == "focus-next" {
- Action::FocusNext
- } else if s == "focus-prev" {
- Action::FocusPrev
- } else if s == "move" {
- Action::Move
- } else if s == "resize" {
- Action::Resize
- } else if s == "layout-next" {
- Action::LayoutNext
- } else if s == "mode-next" {
- Action::ModeNext
- } else if s == "mode-next-shared" {
- Action::ModeNextShared
- } else if s == "reload" {
- Action::Reload
- } else if s == "restart" {
- Action::Restart
- } else if s == "fullscreen" {
- Action::Fullscreen
- } else if s == "minimize" {
- Action::Minimize
- } else if s.starts_with("spawn")
- && (s.len() == 5 || s.as_bytes()[5] == b' ' || s.as_bytes()[5] == b'-')
- {
- Action::Spawn
- } else if s.starts_with("view") {
- let rest = &s[4..];
- let tag_str = rest
- .strip_prefix('-')
- .or_else(|| rest.strip_prefix(' '))
- .unwrap_or(rest);
- if let Ok(tag) = tag_str.parse::<i32>() {
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- return match tag {
- 1 => Action::View1,
- 2 => Action::View2,
- 3 => Action::View3,
- 4 => Action::View4,
- _ => Action::None,
- };
- }
- }
- Action::None
- } else if s.starts_with("toggle") {
- let rest = &s[6..];
- let tag_str = rest
- .strip_prefix('-')
- .or_else(|| rest.strip_prefix(' '))
- .unwrap_or(rest);
- if let Ok(tag) = tag_str.parse::<i32>() {
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- return match tag {
- 1 => Action::Toggle1,
- 2 => Action::Toggle2,
- 3 => Action::Toggle3,
- 4 => Action::Toggle4,
- _ => Action::None,
- };
- }
- }
- if s == "toggle" {
- Action::Toggle
- } else {
- Action::None
- }
- } else if s.starts_with("set-tag") {
- let rest = &s[7..];
- let tag_str = rest
- .strip_prefix('-')
- .or_else(|| rest.strip_prefix(' '))
- .unwrap_or(rest);
- if let Ok(tag) = tag_str.parse::<i32>() {
- if tag >= 1 && tag <= NUM_TAGS as i32 {
- return match tag {
- 1 => Action::SetTag1,
- 2 => Action::SetTag2,
- 3 => Action::SetTag3,
- 4 => Action::SetTag4,
- _ => Action::None,
- };
- }
- }
- Action::None
- } else if s == "expose" {
- Action::Expose
- } else if s == "side-panel-left" || s == "side_panel_left" {
- Action::SidePanelLeft
- } else if s == "side-panel-right" || s == "side_panel_right" {
- Action::SidePanelRight
- } else {
- Action::None
- }
-}
-
-/// Parse modifier string like "alt+shift" into a bitmask
-pub fn parse_modifiers(mod_str: &str) -> u32 {
- let mut mods = 0u32;
- if mod_str.contains("super") || mod_str.contains("mod4") {
- mods |= 0x40; // RIVER_SEAT_V1_MODIFIERS_MOD4
- }
- if mod_str.contains("shift") {
- mods |= 0x01; // RIVER_SEAT_V1_MODIFIERS_SHIFT
- }
- if mod_str.contains("ctrl") {
- mods |= 0x04; // RIVER_SEAT_V1_MODIFIERS_CTRL
- }
- if mod_str.contains("alt") || mod_str.contains("mod1") {
- mods |= 0x08; // RIVER_SEAT_V1_MODIFIERS_MOD1
- }
- mods
-}
-
-/// Parse a button string ("left", "right", "middle", or numeric)
-pub fn parse_button(s: &str) -> u32 {
- match s.trim() {
- "left" => 0x110, // BTN_LEFT
- "right" => 0x111, // BTN_RIGHT
- "middle" => 0x112, // BTN_MIDDLE
- "side" => 0x113, // BTN_SIDE
- "extra" => 0x114, // BTN_EXTRA
- _ => s.trim().parse().unwrap_or(0),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_hex_color_rgb() {
- let (r, g, b, a) = parse_hex_color("#3e3e3e").unwrap();
- assert_eq!(r, 0x3E3E3E3E);
- assert_eq!(g, 0x3E3E3E3E);
- assert_eq!(b, 0x3E3E3E3E);
- assert_eq!(a, 0xFFFFFFFF);
- }
-
- #[test]
- fn test_parse_hex_color_rgba() {
- let (r, g, b, a) = parse_hex_color("#5c9060ff").unwrap();
- assert_eq!(r, 0x5C5C5C5C);
- assert_eq!(g, 0x90909090);
- assert_eq!(b, 0x60606060);
- assert_eq!(a, 0xFFFFFFFF);
- }
-
- #[test]
- fn test_parse_hex_color_invalid() {
- assert!(parse_hex_color("3e3e3e").is_none());
- assert!(parse_hex_color("#3e3e").is_none());
- assert!(parse_hex_color("#3e3e3e3e3e").is_none());
- }
-
- #[test]
- fn test_parse_tiling_mode() {
- assert_eq!(parse_tiling_mode("cascade"), TilingMode::Cascade);
- assert_eq!(parse_tiling_mode("grid"), TilingMode::Grid);
- assert_eq!(parse_tiling_mode("fullscreen"), TilingMode::Fullscreen);
- assert_eq!(parse_tiling_mode("floating"), TilingMode::Floating);
- assert_eq!(parse_tiling_mode("popup"), TilingMode::Popup);
- assert_eq!(parse_tiling_mode("side-panel"), TilingMode::SidePanel);
- assert_eq!(parse_tiling_mode("Side Panel"), TilingMode::SidePanel);
- assert_eq!(parse_tiling_mode("unknown"), TilingMode::Floating);
- }
-
- #[test]
- fn test_parse_action() {
- assert_eq!(parse_action("close"), Action::Close);
- assert_eq!(parse_action("exit"), Action::Exit);
- assert_eq!(parse_action("focus-next"), Action::FocusNext);
- assert_eq!(parse_action("focus-prev"), Action::FocusPrev);
- assert_eq!(parse_action("move"), Action::Move);
- assert_eq!(parse_action("resize"), Action::Resize);
- assert_eq!(parse_action("layout-next"), Action::LayoutNext);
- assert_eq!(parse_action("mode-next"), Action::ModeNext);
- assert_eq!(parse_action("mode-next-shared"), Action::ModeNextShared);
- assert_eq!(parse_action("reload"), Action::Reload);
- assert_eq!(parse_action("restart"), Action::Restart);
- assert_eq!(parse_action("spawn"), Action::Spawn);
- assert_eq!(parse_action("spawn something"), Action::Spawn);
- assert_eq!(parse_action("toggle"), Action::Toggle);
- assert_eq!(parse_action("view-1"), Action::View1);
- assert_eq!(parse_action("view-4"), Action::View4);
- assert_eq!(parse_action("toggle-2"), Action::Toggle2);
- assert_eq!(parse_action("set-tag-3"), Action::SetTag3);
- assert_eq!(parse_action("expose"), Action::Expose);
- assert_eq!(parse_action("minimize"), Action::Minimize);
- assert_eq!(parse_action("side-panel-left"), Action::SidePanelLeft);
- assert_eq!(parse_action("side-panel-right"), Action::SidePanelRight);
- assert_eq!(parse_action("unknown"), Action::None);
- }
-
- #[test]
- fn test_parse_modifiers() {
- assert_eq!(parse_modifiers("alt"), 0x08);
- assert_eq!(parse_modifiers("alt+shift"), 0x08 | 0x01);
- assert_eq!(parse_modifiers("ctrl"), 0x04);
- assert_eq!(parse_modifiers("super"), 0x40);
- assert_eq!(parse_modifiers(""), 0x00);
- }
-
- #[test]
- fn test_parse_button() {
- assert_eq!(parse_button("left"), 0x110);
- assert_eq!(parse_button("right"), 0x111);
- assert_eq!(parse_button("middle"), 0x112);
- assert_eq!(parse_button("272"), 272);
- }
-
- #[test]
- fn test_wm_default() {
- let wm = WindowManager::default();
- assert_eq!(wm.active_tags, 1);
- assert_eq!(wm.global_layout, TilingMode::Cascade);
- assert!(!wm.config_done);
- assert_eq!(wm.layout.gap, 48);
- assert_eq!(wm.layout.gap_top, 48);
- assert_eq!(wm.layout.gap_left, 48);
- assert_eq!(wm.layout.gap_right, 48);
- assert_eq!(wm.layout.gap_bottom, 48);
- }
-}
diff --git a/src/client/wayland.rs b/src/client/wayland.rs
deleted file mode 100644
index 1692cd6..0000000
--- a/src/client/wayland.rs
+++ /dev/null
@@ -1,4436 +0,0 @@
-// Wayland display connection, registry, and event dispatch for cce-client
-
-use wayland_client::{
- event_created_child, protocol::wl_registry, Connection, Dispatch, EventQueue, Proxy,
- QueueHandle,
-};
-
-use crate::protocol::river_input_management::client::{
- river_input_device_v1::{self, RiverInputDeviceV1},
- river_input_manager_v1::{self, RiverInputManagerV1},
-};
-use crate::protocol::river_libinput_config::client::{
- river_libinput_config_v1::{self, RiverLibinputConfigV1},
- river_libinput_device_v1::{self, RiverLibinputDeviceV1},
- river_libinput_result_v1::{self, RiverLibinputResultV1},
-};
-use crate::protocol::river_layer_shell::client::{
- river_layer_shell_output_v1::{self, RiverLayerShellOutputV1},
- river_layer_shell_v1::{self, RiverLayerShellV1},
-};
-use crate::protocol::river_window_management::client::{
- river_decoration_v1::{self, RiverDecorationV1},
- river_node_v1::{self, RiverNodeV1},
- river_output_v1::{self, RiverOutputV1},
- river_pointer_binding_v1::{self, RiverPointerBindingV1},
- river_seat_v1::{self, Modifiers, RiverSeatV1},
- river_window_manager_v1::{self, RiverWindowManagerV1},
- river_window_v1::{self, RiverWindowV1},
-};
-use crate::protocol::river_xkb_bindings::client::{
- river_xkb_binding_v1::{self, RiverXkbBindingV1},
- river_xkb_bindings_seat_v1::{self, RiverXkbBindingsSeatV1},
- river_xkb_bindings_v1::{self, RiverXkbBindingsV1},
-};
-use crate::protocol::wlr_output_management::client::{
- zwlr_output_configuration_head_v1::{self, ZwlrOutputConfigurationHeadV1},
- zwlr_output_configuration_v1::{self, ZwlrOutputConfigurationV1},
- zwlr_output_head_v1::{self, ZwlrOutputHeadV1},
- zwlr_output_manager_v1::{self, ZwlrOutputManagerV1},
- zwlr_output_mode_v1::{self, ZwlrOutputModeV1},
-};
-
-use crate::types::{Action, BindingUserData, Output, Seat, TilingMode, Window, WindowManager};
-use wayland_client::protocol::{
- wl_buffer, wl_compositor, wl_output, wl_pointer, wl_seat, wl_shm, wl_shm_pool, wl_surface,
-};
-use wayland_protocols::wp::cursor_shape::v1::client::{
- wp_cursor_shape_device_v1::{self, WpCursorShapeDeviceV1, Shape},
- wp_cursor_shape_manager_v1::{self, WpCursorShapeManagerV1},
-};
-
-// Interface name constants (from river protocol XML)
-const IFACE_WINDOW_MANAGER: &str = "river_window_manager_v1";
-const IFACE_CURSOR_SHAPE_MANAGER: &str = "wp_cursor_shape_manager_v1";
-const IFACE_XKB_BINDINGS: &str = "river_xkb_bindings_v1";
-const IFACE_LAYER_SHELL: &str = "river_layer_shell_v1";
-const IFACE_INPUT_MANAGER: &str = "river_input_manager_v1";
-const IFACE_LIBINPUT_CONFIG: &str = "river_libinput_config_v1";
-const IFACE_WLR_OUTPUT_MANAGER: &str = "zwlr_output_manager_v1";
-
-const CORNER_THRESHOLD: f64 = 16.0;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum PointerOpType {
- Move,
- Resize,
- ResizeLeft,
- ResizeRight,
- ResizeBottom,
- ResizeTop,
- ResizeBottomLeft,
- ResizeBottomRight,
- ResizeTopLeft,
- ResizeTopRight,
-}
-
-#[derive(Debug, Clone)]
-pub struct PointerOp {
- pub window_id: u64,
- pub op_type: PointerOpType,
- pub start_x: i32,
- pub start_y: i32,
- pub start_width: i32,
- pub start_height: i32,
-}
-
-#[derive(Debug, Clone)]
-pub struct PendingBorderDrag {
- pub window_id: u64,
- pub seat_id: u64,
- pub start_surface_x: f64,
- pub start_surface_y: f64,
- pub op_type: PointerOpType,
-}
-
-fn update_screen_bounds(outputs: &[crate::types::Output]) {
- let mut max_x = 1920;
- let mut max_y = 1080;
- for out in outputs {
- if !out.removed {
- let right = out.x + out.width;
- let bottom = out.y + out.height;
- if right > max_x {
- max_x = right;
- }
- if bottom > max_y {
- max_y = bottom;
- }
- }
- }
- crate::input::SCREEN_WIDTH.store(max_x, std::sync::atomic::Ordering::SeqCst);
- crate::input::SCREEN_HEIGHT.store(max_y, std::sync::atomic::Ordering::SeqCst);
-}
-
-/// Wayland proxy objects stored alongside each Window, so we can
-/// call protocol methods (set_position, propose_dimensions, etc.) on it.
-pub struct WindowProxy {
- pub river_window: RiverWindowV1,
- pub decoration: Option<crate::decorations::WindowDecoration>,
- pub dec_left: Option<crate::decorations::WindowDecoration>,
- pub dec_right: Option<crate::decorations::WindowDecoration>,
- pub dec_bottom: Option<crate::decorations::WindowDecoration>,
-}
-
-/// Wayland proxy objects stored alongside each Seat.
-pub struct SeatProxy {
- pub river_seat: RiverSeatV1,
- pub xkb_bindings_seat: Option<RiverXkbBindingsSeatV1>,
- pub wl_seat: Option<wl_seat::WlSeat>,
- pub wl_pointer: Option<wl_pointer::WlPointer>,
- pub cursor_shape_device: Option<WpCursorShapeDeviceV1>,
- pub last_pointer_enter_serial: u32,
-}
-
-/// Wayland proxy objects stored alongside each Output.
-pub struct OutputProxy {
- pub river_output: RiverOutputV1,
- pub layer_shell_output: Option<RiverLayerShellOutputV1>,
-}
-
-/// Tracked wl_output proxy and its physical properties.
-pub struct WlOutputInfo {
- pub name: u32,
- pub wl_output: wl_output::WlOutput,
- pub width: i32,
- pub height: i32,
- pub x: i32,
- pub y: i32,
-}
-
-/// The full app state combining logic state + protocol proxy storage.
-pub struct AppState {
- pub wm: WindowManager,
- pub wl_outputs: Vec<WlOutputInfo>,
-
- // Protocol objects (None until bound via registry)
- pub registry: Option<wl_registry::WlRegistry>,
- pub window_manager: Option<RiverWindowManagerV1>,
- pub xkb_bindings: Option<RiverXkbBindingsV1>,
- pub layer_shell: Option<RiverLayerShellV1>,
- pub input_manager: Option<RiverInputManagerV1>,
- pub cursor_shape_manager: Option<WpCursorShapeManagerV1>,
- pub compositor: Option<wl_compositor::WlCompositor>,
- pub shm: Option<wl_shm::WlShm>,
-
- // Whether we got all required globals
- pub has_window_manager: bool,
- pub has_xkb_bindings: bool,
-
- // Proxy objects indexed by window/seat/output ID
- pub window_proxies: Vec<(u64, WindowProxy)>,
- pub seat_proxies: Vec<(u64, SeatProxy)>,
- pub output_proxies: Vec<(u64, OutputProxy)>,
-
- // River node proxies for window positioning (created via get_node request)
- pub window_nodes: Vec<(u64, RiverNodeV1)>,
-
- // Active binding proxies — must be held alive for bindings to remain registered.
- // On reload, these are cleared (which destroys the old protocol objects) before
- // new bindings are created.
- pub xkb_binding_proxies: Vec<RiverXkbBindingV1>,
- pub pointer_binding_proxies: Vec<RiverPointerBindingV1>,
-
- // Next ID counter for new windows/outputs/seats
- pub next_id: u64,
-
- // Exit flag
- pub exit_requested: bool,
-
- // Debug: render cycle counter
- pub render_count: u32,
-
- // --- wlr-output-management protocol state ---
- pub output_manager: Option<ZwlrOutputManagerV1>,
- /// Latest serial from the output manager's done event.
- /// Required to create a valid configuration.
- pub output_serial: u32,
- /// Discovered output heads: (head_proxy, name, enabled, current_scale)
- /// The head proxy must be kept alive to reference it in configurations.
- pub output_heads: Vec<OutputHeadInfo>,
- /// Pending configuration (alive until succeeded/failed/cancelled)
- pub output_config: Option<ZwlrOutputConfigurationV1>,
-
- // --- Status socket sender for waybar ---
- pub status_sender: Option<crate::status_server::StatusSender>,
-
- // --- Libinput config protocol state ---
- pub libinput_config: Option<RiverLibinputConfigV1>,
- /// Tracked libinput devices with their tap state
- pub libinput_devices: Vec<LibinputDeviceInfo>,
- /// The surface the pointer is currently hovering over
- pub pointer_hovered_surface: Option<wl_surface::WlSurface>,
- pub active_pointer_op: Option<PointerOp>,
- pub pointer_op_release_pending: bool,
- pub pending_border_drag: Option<PendingBorderDrag>,
- pub pending_pointer_op_type: Option<PointerOpType>,
- pub last_pointer_surface_x: f64,
- pub last_pointer_surface_y: f64,
- pub input_device_names: std::collections::HashMap<u32, String>,
- pub border_font: Option<fontdue::Font>,
- pub border_font_path: Option<String>,
-}
-
-/// Info tracked for each libinput device discovered via river_libinput_config_v1
-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.
-pub struct OutputHeadInfo {
- pub proxy: ZwlrOutputHeadV1,
- pub name: String,
- pub enabled: bool,
- pub scale: f64,
-}
-
-impl AppState {
- pub fn new() -> Self {
- AppState {
- wm: WindowManager::new(),
- wl_outputs: Vec::new(),
- registry: None,
- window_manager: None,
- xkb_bindings: None,
- layer_shell: None,
- input_manager: None,
- cursor_shape_manager: None,
- compositor: None,
- shm: None,
- has_window_manager: false,
- has_xkb_bindings: false,
- window_proxies: Vec::new(),
- seat_proxies: Vec::new(),
- output_proxies: Vec::new(),
- window_nodes: Vec::new(),
- xkb_binding_proxies: Vec::new(),
- pointer_binding_proxies: Vec::new(),
- next_id: 1,
- exit_requested: false,
- render_count: 0,
- output_manager: None,
- output_serial: 0,
- output_heads: Vec::new(),
- output_config: None,
- status_sender: None,
- libinput_config: None,
- libinput_devices: Vec::new(),
- pointer_hovered_surface: None,
- active_pointer_op: None,
- pointer_op_release_pending: false,
- pending_border_drag: None,
- pending_pointer_op_type: None,
- last_pointer_surface_x: 0.0,
- last_pointer_surface_y: 0.0,
- input_device_names: std::collections::HashMap::new(),
- border_font: None,
- border_font_path: None,
- }
- }
-
- fn alloc_id(&mut self) -> u64 {
- let id = self.next_id;
- self.next_id += 1;
- id
- }
-
- pub fn manage_dirty(&mut self) {
- if let Some(ref wm) = self.window_manager {
- wm.manage_dirty();
- self.wm.last_animation_tick = std::time::Instant::now();
- }
- }
-
- pub fn get_window_proxy(&self, id: u64) -> Option<&WindowProxy> {
- self.window_proxies
- .iter()
- .find(|(wid, _)| *wid == id)
- .map(|(_, p)| p)
- }
-
- pub fn get_seat_proxy(&self, id: u64) -> Option<&SeatProxy> {
- self.seat_proxies
- .iter()
- .find(|(sid, _)| *sid == id)
- .map(|(_, p)| p)
- }
-
- pub fn get_output_proxy(&self, id: u64) -> Option<&OutputProxy> {
- self.output_proxies
- .iter()
- .find(|(oid, _)| *oid == id)
- .map(|(_, p)| p)
- }
-
- /// Find the internal window ID for a given RiverWindowV1 proxy.
- fn window_id_for_proxy(&self, proxy: &RiverWindowV1) -> Option<u64> {
- let pid = proxy.id().protocol_id();
- self.window_proxies
- .iter()
- .find(|(_, wp)| wp.river_window.id().protocol_id() == pid)
- .map(|(id, _)| *id)
- }
-
- /// Find the internal seat ID for a given RiverSeatV1 proxy.
- fn seat_id_for_proxy(&self, proxy: &RiverSeatV1) -> Option<u64> {
- let pid = proxy.id().protocol_id();
- self.seat_proxies
- .iter()
- .find(|(_, sp)| sp.river_seat.id().protocol_id() == pid)
- .map(|(id, _)| *id)
- }
-
- fn update_cursor_shape_for_surface(state: &mut Self, pointer: &wl_pointer::WlPointer) {
- let seat_info = state.seat_proxies.iter().find_map(|(_, sp)| {
- if sp.wl_pointer.as_ref() == Some(pointer) {
- Some((sp.cursor_shape_device.clone(), sp.last_pointer_enter_serial))
- } else {
- None
- }
- });
-
- let (device, serial) = match seat_info {
- Some((Some(dev), ser)) => (dev, ser),
- _ => return,
- };
-
- let mut shape = Shape::Default;
-
- if let Some(ref current_surface) = state.pointer_hovered_surface {
- let matched_window = state.window_proxies.iter().find_map(|(id, proxy)| {
- if let Some(dec) = &proxy.decoration {
- if &dec.surface == current_surface {
- return Some((*id, "top"));
- }
- }
- if let Some(dec) = &proxy.dec_left {
- if &dec.surface == current_surface {
- return Some((*id, "left"));
- }
- }
- if let Some(dec) = &proxy.dec_right {
- if &dec.surface == current_surface {
- return Some((*id, "right"));
- }
- }
- if let Some(dec) = &proxy.dec_bottom {
- if &dec.surface == current_surface {
- return Some((*id, "bottom"));
- }
- }
- None
- });
-
- if let Some((wid, surface_type)) = matched_window {
- if let Some(w) = state.wm.windows.iter().find(|win| win.id == wid) {
- let border_w = if state.wm.expose_active && w.tiling_mode != crate::types::TilingMode::Popup {
- state.wm.layout.grid_border_width
- } else {
- match w.tiling_mode {
- crate::types::TilingMode::Cascade => state.wm.layout.cascade_border_width,
- crate::types::TilingMode::Fullscreen => state.wm.layout.fullscreen_border_width,
- crate::types::TilingMode::Grid => state.wm.layout.grid_border_width,
- crate::types::TilingMode::Floating => state.wm.layout.floating_border_width,
- crate::types::TilingMode::Popup => 0,
- crate::types::TilingMode::SidePanel => state.wm.layout.cascade_border_width,
- }
- };
- let grab_w = border_w.max(10);
-
- let logical_height = border_w.max(16);
- shape = match surface_type {
- "top" => {
- let total_width = w.width + 2 * border_w;
- let mid_y = (logical_height as f64) / 2.0;
- if state.last_pointer_surface_y < mid_y {
- if state.last_pointer_surface_x < CORNER_THRESHOLD {
- Shape::NwseResize
- } else if state.last_pointer_surface_x > (total_width as f64 - CORNER_THRESHOLD) {
- Shape::NeswResize
- } else {
- Shape::NsResize
- }
- } else {
- Shape::Default
- }
- }
- "left" => {
- let mid_x = (grab_w as f64) / 2.0;
- if state.last_pointer_surface_x < mid_x {
- if state.last_pointer_surface_y < CORNER_THRESHOLD {
- Shape::NwseResize
- } else if state.last_pointer_surface_y > (w.height as f64 - CORNER_THRESHOLD) {
- Shape::NeswResize
- } else {
- Shape::EwResize
- }
- } else {
- Shape::Default
- }
- }
- "right" => {
- let mid_x = (grab_w as f64) / 2.0;
- if state.last_pointer_surface_x >= mid_x {
- if state.last_pointer_surface_y < CORNER_THRESHOLD {
- Shape::NeswResize
- } else if state.last_pointer_surface_y > (w.height as f64 - CORNER_THRESHOLD) {
- Shape::NwseResize
- } else {
- Shape::EwResize
- }
- } else {
- Shape::Default
- }
- }
- "bottom" => {
- let total_width = w.width + 2 * grab_w;
- let mid_y = (grab_w as f64) / 2.0;
- if state.last_pointer_surface_y >= mid_y {
- if state.last_pointer_surface_x < CORNER_THRESHOLD {
- Shape::NeswResize
- } else if state.last_pointer_surface_x > (total_width as f64 - CORNER_THRESHOLD) {
- Shape::NwseResize
- } else {
- Shape::NsResize
- }
- } else {
- Shape::Default
- }
- }
- _ => Shape::Default,
- };
- }
- }
- }
-
- device.set_shape(serial, shape);
- }
-}
-
-// --- Dispatch implementations ---
-
-/// User data for the registry
-pub struct RegistryData;
-
-impl Dispatch<wl_registry::WlRegistry, RegistryData> for AppState {
- fn event(
- state: &mut Self,
- registry: &wl_registry::WlRegistry,
- event: wl_registry::Event,
- _data: &RegistryData,
- _conn: &Connection,
- qhandle: &QueueHandle<Self>,
- ) {
- match event {
- wl_registry::Event::Global {
- name,
- interface,
- version,
- } => {
- if interface == IFACE_WINDOW_MANAGER {
- if version >= 4 {
- eprintln!("registry: binding {} v{}", IFACE_WINDOW_MANAGER, version);
- // Use the Proxy trait's bind method via the interface
- let wm: RiverWindowManagerV1 =
- registry.bind::<RiverWindowManagerV1, _, _>(name, 4, qhandle, ());
- state.window_manager = Some(wm);
- state.has_window_manager = true;
- } else {
- eprintln!(
- "warning: {} version {} < 4, skipping",
- IFACE_WINDOW_MANAGER, version
- );
- }
- } else if interface == IFACE_XKB_BINDINGS {
- let bind_ver = std::cmp::min(version, 2);
- eprintln!("registry: binding {} v{}", IFACE_XKB_BINDINGS, bind_ver);
- let xb: RiverXkbBindingsV1 =
- registry.bind::<RiverXkbBindingsV1, _, _>(name, bind_ver, qhandle, ());
- state.xkb_bindings = Some(xb);
- state.has_xkb_bindings = true;
- } else if interface == IFACE_LAYER_SHELL {
- eprintln!("registry: binding {}", IFACE_LAYER_SHELL);
- let ls: RiverLayerShellV1 =
- registry.bind::<RiverLayerShellV1, _, _>(name, 1, qhandle, ());
- state.layer_shell = Some(ls);
- } else if interface == IFACE_INPUT_MANAGER {
- eprintln!("registry: binding {}", IFACE_INPUT_MANAGER);
- let im: RiverInputManagerV1 =
- registry.bind::<RiverInputManagerV1, _, _>(name, 1, qhandle, ());
- state.input_manager = Some(im);
- } else if interface == IFACE_LIBINPUT_CONFIG {
- eprintln!("registry: binding {}", IFACE_LIBINPUT_CONFIG);
- let lc: RiverLibinputConfigV1 =
- registry.bind::<RiverLibinputConfigV1, _, _>(name, 1, qhandle, ());
- state.libinput_config = Some(lc);
- } else if interface == IFACE_WLR_OUTPUT_MANAGER {
- eprintln!("registry: binding {}", IFACE_WLR_OUTPUT_MANAGER);
- let om: ZwlrOutputManagerV1 =
- registry.bind::<ZwlrOutputManagerV1, _, _>(name, 4, qhandle, ());
- state.output_manager = Some(om);
- } else if interface == IFACE_CURSOR_SHAPE_MANAGER {
- eprintln!("registry: binding {}", IFACE_CURSOR_SHAPE_MANAGER);
- let csm: WpCursorShapeManagerV1 =
- registry.bind::<WpCursorShapeManagerV1, _, _>(name, 1, qhandle, ());
- state.cursor_shape_manager = Some(csm);
- } else if interface == "wl_compositor" {
- eprintln!("registry: binding wl_compositor name={}", name);
- let comp: wl_compositor::WlCompositor =
- registry.bind::<wl_compositor::WlCompositor, _, _>(name, 4, qhandle, ());
- state.compositor = Some(comp);
- } else if interface == "wl_shm" {
- eprintln!("registry: binding wl_shm name={}", name);
- let shm: wl_shm::WlShm =
- registry.bind::<wl_shm::WlShm, _, _>(name, 1, qhandle, ());
- state.shm = Some(shm);
- } else if interface == "wl_output" {
- eprintln!("registry: binding wl_output name={}", name);
- let wl_out: wl_output::WlOutput =
- registry.bind::<wl_output::WlOutput, _, _>(name, 4, qhandle, ());
- state.wl_outputs.push(WlOutputInfo {
- name,
- wl_output: wl_out,
- width: 0,
- height: 0,
- x: 0,
- y: 0,
- });
- }
- }
- wl_registry::Event::GlobalRemove { name: _ } => {}
- _ => {}
- }
- }
-}
-
-// --- RiverWindowManagerV1 events ---
-
-impl Dispatch<RiverWindowManagerV1, ()> for AppState {
- event_created_child!(AppState, RiverWindowManagerV1, [
- river_window_manager_v1::EVT_WINDOW_OPCODE => (RiverWindowV1, ()),
- river_window_manager_v1::EVT_OUTPUT_OPCODE => (RiverOutputV1, ()),
- river_window_manager_v1::EVT_SEAT_OPCODE => (RiverSeatV1, ()),
- ]);
-
- fn event(
- state: &mut Self,
- wm_proxy: &RiverWindowManagerV1,
- event: river_window_manager_v1::Event,
- _data: &(),
- conn: &Connection,
- qhandle: &QueueHandle<Self>,
- ) {
- match event {
- river_window_manager_v1::Event::Unavailable => {
- eprintln!("error: another window manager is already running");
- state.exit_requested = true;
- state.wm.exit_requested = true;
- }
-
- river_window_manager_v1::Event::Finished => {
- if state.wm.exit_requested {
- eprintln!("river sent finished, exiting");
- state.exit_requested = true;
- } else {
- eprintln!("river sent finished unexpectedly, restarting");
- // Write to death log before fork+exit loses all traces
- if let Ok(mut f) = std::fs::OpenOptions::new()
- .create(true)
- .append(true)
- .open(crate::paths::get_death_log_path())
- {
- use std::io::Write;
- let _ = writeln!(
- f,
- "river sent Finished event — protocol error likely, in_manage={}, render_count={}",
- state.wm.in_manage_sequence,
- state.render_count
- );
- let _ = f.sync_all();
- }
- crate::restart::wm_restart();
- }
- }
-
- river_window_manager_v1::Event::ManageStart => {
- let ms_start = std::time::Instant::now();
- state.wm.in_manage_sequence = true;
- state.wm.focused_tags = 0;
- state.wm.needs_render = true;
-
- // Remove closed windows and their proxy objects.
- // Collect IDs first, then clean up proxies, then remove from windows vector.
- // This matches tinyrwm's remove_windows() pattern.
- let closed_ids: Vec<(u64, Option<String>)> = state
- .wm
- .windows
- .iter()
- .filter(|w| w.closed)
- .map(|w| (w.id, w.app_id.clone()))
- .collect();
- // Destroy proxy objects for closed windows.
- // Also clean up seat state (focused_window_id, hovered_window_id,
- // interacted_window_id) that references closed windows, matching
- // tinyrwm's remove_windows() pattern which cleans up seat ops
- // referencing closed window proxies.
- for (closed_id, closed_app_id) in &closed_ids {
- state.window_proxies.retain_mut(|(id, wp)| {
- if id == closed_id {
- if let Some(dec) = wp.decoration.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- if let Some(dec) = wp.dec_left.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- if let Some(dec) = wp.dec_right.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- if let Some(dec) = wp.dec_bottom.take() {
- dec.decoration.destroy();
- dec.surface.destroy();
- }
- wp.river_window.destroy();
- false
- } else {
- true
- }
- });
- state.window_nodes.retain(|(id, node)| {
- if id == closed_id {
- node.destroy();
- false
- } else {
- true
- }
- });
- // Clear seat references to this closed window.
- // If the focused window closed, reassign focus to another
- // visible window (matching tinyrwm's focus_top pattern).
- let active_tags = state.wm.active_tags;
- for seat in &mut state.wm.seats {
- if seat.focused_window_id == Some(*closed_id) {
- // Pick the last visible window (cascade front)
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| {
- !w.closed && (w.tags & active_tags) != 0 && w.id != *closed_id && w.app_id.as_deref() != Some("cce-status-interface")
- })
- .map(|w| w.id)
- .collect();
- seat.focused_window_id = visible_ids.last().copied();
- if seat.focused_window_id.is_some() {
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- eprintln!(
- "[manage] focused window {} (app_id={:?}) closed, reassigned to {:?}",
- closed_id, closed_app_id, seat.focused_window_id
- );
- }
- if seat.hovered_window_id == Some(*closed_id) {
- seat.hovered_window_id = None;
- }
- if seat.interacted_window_id == Some(*closed_id) {
- seat.interacted_window_id = None;
- }
- }
- }
- state.wm.windows.retain(|w| !w.closed);
-
- // Remove removed outputs — destroy protocol proxies first
- // (matching tinyrwm's remove_outputs pattern).
- let removed_output_ids: Vec<u64> = state
- .wm
- .outputs
- .iter()
- .filter(|o| o.removed)
- .map(|o| o.id)
- .collect();
- for removed_id in &removed_output_ids {
- state.output_proxies.retain(|(id, op)| {
- if id == removed_id {
- op.river_output.destroy();
- false
- } else {
- true
- }
- });
- }
- state.wm.outputs.retain(|o| !o.removed);
- update_screen_bounds(&state.wm.outputs);
-
- // Remove removed seats — destroy protocol proxies first
- // (matching tinyrwm's remove_seats pattern which destroys
- // seat proxy and all binding proxies).
- let removed_seat_ids: Vec<u64> = state
- .wm
- .seats
- .iter()
- .filter(|s| s.removed)
- .map(|s| s.id)
- .collect();
- for removed_id in &removed_seat_ids {
- state.seat_proxies.retain(|(id, sp)| {
- if id == removed_id {
- sp.river_seat.destroy();
- false
- } else {
- true
- }
- });
- }
- state.wm.seats.retain(|s| !s.removed);
-
- // Set default layer shell on first output
- if let Some(first_output) = state.wm.outputs.first() {
- if let Some(op) = state.get_output_proxy(first_output.id) {
- if let Some(ref lso) = op.layer_shell_output {
- lso.set_default();
- }
- }
- }
-
- // Enforce single_instance mode rules
- enforce_single_instance(&mut state.wm);
-
- // Apply pending bindings to seats
- apply_pending_bindings(state, qhandle);
-
- // Manage seats (set focused tags)
- for seat in &state.wm.seats {
- if let Some(focused_id) = seat.focused_window_id {
- if let Some(fw) = state.wm.get_window(focused_id) {
- state.wm.focused_tags |= fw.tags;
- }
- }
- }
-
- // Check xprop results for XWayland parent detection.
- // River doesn't send the Parent event for XWayland windows,
- // so we detect WM_TRANSIENT_FOR / _NET_WM_WINDOW_TYPE_DIALOG|UTILITY
- // via an async xprop check spawned in the UnreliablePid handler.
- // This must run before assign_window_modes so has_parent is set
- // before mode assignment decides Floating vs tiled.
- {
- let mut xprop_parent_changed = false;
- let max_attempts: u8 = 10;
- for window in &mut state.wm.windows {
- if !window.needs_xprop_check || window.has_parent || window.closed {
- continue;
- }
- window.xprop_check_attempts += 1;
- let path = crate::paths::get_xprop_path(window.id);
- if let Ok(content) = std::fs::read_to_string(&path) {
- let _ = std::fs::remove_file(&path);
- // Each line: title|window_type_line
- // Match by title against our window's title.
- let my_title = window.title.as_deref().unwrap_or("");
- for line in content.lines() {
- let parts: Vec<&str> = line.splitn(2, '|').collect();
- if parts.len() < 2 {
- continue;
- }
- let x11_title = parts[0];
- let wtype = parts[1];
- if x11_title != my_title {
- continue;
- }
- let is_dialog =
- wtype.contains("DIALOG") || wtype.contains("UTILITY");
- // Only use the window type hint, not WM_TRANSIENT_FOR alone.
- // Transient-for is unreliable: some apps (e.g. Houdini)
- // set WM_TRANSIENT_FOR on their main window (transient to the
- // splash screen). The window type hint is the reliable signal
- // that a window is actually a dialog/utility child.
- if is_dialog {
- window.has_parent = true;
- xprop_parent_changed = true;
- eprintln!(
- "[xprop] id={} (app_id={:?}) detected XWayland dialog/utility window",
- window.id, window.app_id
- );
- }
- }
- window.needs_xprop_check = false;
- } else if window.xprop_check_attempts >= max_attempts {
- // Give up — xprop file never appeared (native Wayland window?)
- window.needs_xprop_check = false;
- eprintln!(
- "[xprop] id={} (app_id={:?}) giving up after {} attempts",
- window.id, window.app_id, max_attempts
- );
- }
- }
- // If any window gained has_parent, trigger a re-manage so
- // assign_window_modes picks it up on the next cycle.
- if xprop_parent_changed {
- state.manage_dirty();
- }
- }
-
- // Apply persisted state from ~/.cache/cce_client_state on first ManageStart.
- // This restores window tag assignments, active_tags, tag_layouts, and
- // locked tiling modes from the previous session. Must run before
- // assign_window_modes so restored tags/modes take effect.
- //
- // We retry across multiple ManageStart cycles because window metadata
- // (identifier, app_id, title) arrives as separate events AFTER the
- // Window creation event. On the first ManageStart, these fields may
- // still be None, so matching would fail. We keep needs_state_restore
- // true until we've had at least one window with metadata (or after
- // 5 cycles, giving up gracefully).
- if state.wm.needs_state_restore {
- if let Some(pstate) = crate::state::read_state() {
- // Check if any window has metadata yet
- let has_metadata = state.wm.windows.iter().any(|w| {
- w.identifier.is_some() || w.app_id.is_some()
- });
- if has_metadata {
- crate::state::apply_state(&mut state.wm, &pstate);
- state.wm.needs_state_restore = false;
- eprintln!("[state] restored state on ManageStart (windows have metadata)");
- } else {
- // Increment a counter; give up after 5 cycles
- state.wm.state_restore_attempts += 1;
- if state.wm.state_restore_attempts >= 5 {
- // No windows with metadata yet — probably a fresh start
- // with no existing windows. Apply global state only
- // (active_tags, tag_layouts) and stop retrying.
- state.wm.active_tags = pstate.active_tags;
- for (idx, mode, has) in &pstate.tag_layouts {
- if *idx < crate::types::NUM_TAGS {
- state.wm.tag_layouts[*idx] = *mode;
- state.wm.has_tag_layout[*idx] = *has;
- }
- }
- state.wm.needs_state_restore = false;
- eprintln!("[state] no windows with metadata after 5 cycles, applied global state only");
- }
- }
- } else {
- // No state file — fresh start
- state.wm.needs_state_restore = false;
- }
- }
-
- // Assign tiling modes to windows based on mode_rules / tag_layouts / global_layout.
- // Must happen before manage_windows so tiling computation uses the correct modes.
- crate::wm::assign_window_modes(&mut state.wm);
-
- // Window management: set position + propose dimensions.
- // These modify window management state and can ONLY be called
- // during a manage sequence (per River protocol spec).
- crate::wm::manage_windows(state, qhandle);
-
- // Auto-focus: new windows on the current tags get keyboard focus.
- // This runs after manage_windows so the new window is tiled and
- // has a valid position/proxy, but before focus application so
- // needs_focus will trigger the actual focus_window() call.
- {
- let active_tags = state.wm.active_tags;
- // Find the last new window on active tags (cascade-front).
- // Multiple new windows can appear in one manage cycle (e.g.
- // spawning several apps at once); focusing the last one is
- // consistent with FocusNext and WindowInteraction which also
- // pick visible_ids.last().
- let new_focused_id = state
- .wm
- .windows
- .iter()
- .filter(|w| {
- w.is_new
- && !w.closed
- && (w.tags & active_tags) != 0
- && w.app_id.as_deref() != Some("cce-status-interface")
- && w.app_id.as_deref() != Some("clear-notification-daemon")
- && w.app_id.as_deref() != Some("cce-notification-daemon")
- })
- .map(|w| w.id)
- .last();
- if let Some(new_id) = new_focused_id {
- for seat in &mut state.wm.seats {
- if seat.removed {
- continue;
- }
- seat.focused_window_id = Some(new_id);
- }
- // Move to cascade-front position (last in windows vec)
- state.wm.move_window_to_end(new_id);
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- let app_id = state.wm.get_window(new_id).and_then(|w| w.app_id.clone());
- eprintln!(
- "[focus] auto-focusing new window id={} (app_id={:?})",
- new_id, app_id
- );
- }
- // Clear is_new on all windows once metadata is resolved, or after a timeout
- for window in &mut state.wm.windows {
- if window.app_id.is_none() && window.title.is_none() && window.metadata_check_attempts < 10 {
- window.metadata_check_attempts += 1;
- continue;
- }
- window.is_new = false;
- }
- }
-
- // Focus management: focus the focused window on each seat.
- // focus_window() modifies window management state.
- // Only call when needs_focus is set to avoid stealing focus from
- // layer-shell clients (like fuzzel) that trigger manage sequences.
- if state.wm.needs_focus {
- for seat in &state.wm.seats {
- if seat.removed {
- continue;
- }
- if let Some(focused_id) = seat.focused_window_id {
- if let Some((_id, sp)) =
- state.seat_proxies.iter().find(|(id, _)| *id == seat.id)
- {
- if let Some(wp) = state.get_window_proxy(focused_id) {
- let app_id = state
- .wm
- .get_window(focused_id)
- .and_then(|w| w.app_id.clone());
- eprintln!(
- "[focus] calling focus_window for id={} (app_id={:?})",
- focused_id, app_id
- );
- sp.river_seat.focus_window(&wp.river_window);
- }
- }
- } else {
- // No focused window — clear focus
- if let Some((_id, sp)) =
- state.seat_proxies.iter().find(|(id, _)| *id == seat.id)
- {
- eprintln!("[focus] calling clear_focus");
- sp.river_seat.clear_focus();
- }
- }
- }
- state.wm.needs_focus = false;
- }
-
- if state.pointer_op_release_pending {
- state.pointer_op_release_pending = false;
- if let Some(ref op) = state.active_pointer_op {
- if op.op_type != PointerOpType::Move {
- if let Some(wp) = state.get_window_proxy(op.window_id) {
- wp.river_window.inform_resize_end();
- }
- }
- }
- state.active_pointer_op = None;
- for (_sid, sp) in &state.seat_proxies {
- sp.river_seat.op_end();
- }
- for seat in &mut state.wm.seats {
- seat.interacted_window_id = None;
- }
- state.wm.needs_render = true;
- }
-
- // Execute pending actions from key/pointer bindings (tinyrwm pattern).
- // Like tinyrwm, we defer action execution to ManageStart so all
- // state mutations happen during the manage sequence.
- // Collect pending actions first to avoid borrow checker issues
- // (execute_action borrows state mutably).
- let pending: Vec<(u64, Action, Option<String>)> = state
- .wm
- .seats
- .iter_mut()
- .filter_map(|seat| {
- if seat.pending_action != Action::None {
- let action = seat.pending_action;
- let command = seat.pending_command.take();
- seat.pending_action = Action::None;
- Some((seat.id, action, command))
- } else {
- None
- }
- })
- .collect();
- let has_pending = !pending.is_empty();
- for (seat_id, action, command) in pending {
- execute_action(state, seat_id, &action, command.as_deref());
- }
-
- if has_pending {
- state.manage_dirty();
- }
-
- wm_proxy.manage_finish();
- // Flush immediately so River can start the configure/render cycle
- // without waiting for our blocking_dispatch to complete.
- if let Err(e) = conn.flush() {
- eprintln!("[manage] FATAL: flush after manage_finish failed: {:?}", e);
- }
- state.wm.in_manage_sequence = false;
- // eprintln!("[manage] ManageStart done in {:?}", ms_start.elapsed());
- // NOTE: Do NOT call update_status_files() here — it calls
- // pkill with .output() which blocks the event loop.
- }
-
- river_window_manager_v1::Event::RenderStart => {
- state.render_count += 1;
- /*
- eprintln!(
- "[render] RenderStart #{} needs_render={}",
- state.render_count, state.wm.needs_render
- );
- */
-
- // Show/hide windows based on tag visibility.
- // This is rendering state and must happen during a render sequence.
- let active_tags = state.wm.active_tags;
- for window in &state.wm.windows {
- if window.closed {
- continue;
- }
- let visible = (window.tags & active_tags) != 0;
- if let Some(wp) = state.get_window_proxy(window.id) {
- if visible {
- wp.river_window.show();
- } else {
- wp.river_window.hide();
- }
- }
- }
-
- if state.wm.needs_render {
- // Only do rendering state here: borders, place_top/place_bottom.
- // set_position and propose_dimensions are window management state
- // and must happen during ManageStart.
- crate::wm::render_borders(state);
- crate::wm::render_opacity(state);
- crate::wm::render_circular(state);
- crate::wm::render_blur(state);
-
- // Update and render window title decorations on borders
- crate::decorations::update_decorations(state, qhandle);
-
- // Raise and stack windows according to z-axis logic:
- // 1. cce-status-interface at the absolute bottom (score = 0)
- // 2. Unfocused tiled/fullscreen windows (score = 1)
- // 3. Focused tiled/fullscreen window (score = 2)
- // 4. Unfocused floating windows (score = 3)
- // 5. Focused floating window (score = 4)
- let active_tags = state.wm.active_tags;
- let focused_id = state.wm.seats.iter().find(|s| !s.removed).and_then(|s| s.focused_window_id);
-
- let has_fullscreen = state.wm.windows.iter().any(|w| {
- w.tiling_mode == crate::types::TilingMode::Fullscreen
- && (w.tags & active_tags) != 0
- && !w.closed
- && !w.minimized
- && w.app_id.as_deref() != Some("cce-status-interface")
- });
-
- let get_window_score = |win: &crate::types::Window| -> i32 {
- if win.app_id.as_deref() == Some("cce-status-interface") {
- if has_fullscreen {
- 0
- } else {
- 5
- }
- } else if win.tiling_mode == crate::types::TilingMode::Popup {
- 10
- } else if win.tiling_mode == crate::types::TilingMode::SidePanel {
- if state.wm.layout.side_panel_behavior == "above" {
- if Some(win.id) == focused_id {
- 8
- } else {
- 6
- }
- } else {
- if Some(win.id) == focused_id {
- 4
- } else {
- 2
- }
- }
- } else if win.tiling_mode != crate::types::TilingMode::Floating {
- if Some(win.id) == focused_id {
- 4
- } else {
- 2
- }
- } else {
- if Some(win.id) == focused_id {
- 8
- } else {
- 6
- }
- }
- };
-
- let mut nodes_to_place: Vec<(i32, usize, u64, Option<String>, &RiverNodeV1)> = Vec::new();
- for &(wid, ref node) in &state.window_nodes {
- if let Some((idx, win)) = state.wm.windows.iter().enumerate().find(|(_, w)| w.id == wid) {
- if win.closed {
- continue;
- }
- let visible = (win.tags & active_tags) != 0;
- if visible {
- let score = get_window_score(win);
- nodes_to_place.push((score, idx, win.id, win.app_id.clone(), node));
- }
- }
- }
-
- // Sort ascending by score, then by original window list index
- nodes_to_place.sort_by_key(|&(score, idx, _, _, _)| (score, idx));
-
- for &(score, _, id, ref app_id, node) in &nodes_to_place {
- /*
- eprintln!(
- "[render] placing node id={} (app_id={:?}) at top with score {}",
- id, app_id, score
- );
- */
- node.place_top();
- }
-
- state.wm.needs_render = false;
- }
-
- wm_proxy.render_finish();
- if state.wm.animating {
- state.manage_dirty();
- }
- // Flush immediately so River receives render_finish without waiting
- // for blocking_dispatch to complete. River has a 3-second unresponsive
- // timeout, and if we don't flush promptly, River will kill us.
- if let Err(e) = conn.flush() {
- eprintln!(
- "[render] FATAL: flush after render_finish #{} failed: {:?}",
- state.render_count, e
- );
- }
- // eprintln!("[render] render_finish #{} flushed", state.render_count);
- // Spawn startup apps inside the callback, like tinyrwm does.
- // Spawning between blocking_dispatch calls corrupts the Wayland
- // connection state because the fork inherits the socket fd.
- if !state.wm.startup_spawned {
- let apps: Vec<String> = state.wm.pending_startup_apps.drain(..).collect();
- for cmd in &apps {
- eprintln!("[init] spawning startup app: {}", cmd);
- crate::config::spawn_command_bg(cmd);
- }
- state.wm.startup_spawned = true;
- }
-
- // Write status files after all manage+render state is settled.
- // Uses the deferred flag pattern: handlers set needs_status_update,
- // we check it here inside the Dispatch callback.
- // write_status_files() does only file I/O — no fork, no pkill.
- if state.wm.needs_status_update {
- crate::status::write_status_files(&state.wm);
-
- // Persist state to ~/.cache/cce_client_state for restart recovery.
- // Safe: just file I/O, no fork, no blocking.
- crate::state::write_state(&state.wm);
-
- // Push the same data through the status socket so waybar
- // gets updates in real-time without needing signal-based pkill.
- if let Some(ref sender) = state.status_sender {
- let update = crate::status_server::build_status_update(&state.wm);
- sender.send(update);
- }
-
- state.wm.needs_status_update = false;
- }
-
- // Re-apply input config if it was changed via IPC
- if !state.wm.tap_config_applied && !state.libinput_devices.is_empty() {
- crate::wayland::apply_input_config(state, qhandle);
- }
-
- if !state.wm.cursor_theme_applied {
- let theme = state.wm.cursor_theme.clone().unwrap_or_else(|| "default".to_string());
- let size = state.wm.cursor_size.unwrap_or(24);
- for (_, sp) in &state.seat_proxies {
- sp.river_seat.set_xcursor_theme(theme.clone(), size);
- }
- state.wm.cursor_theme_applied = true;
- }
- }
-
- // Window event: field is `id` (the new RiverWindowV1 proxy)
- river_window_manager_v1::Event::Window { id: river_window } => {
- let already_tracked = state.window_proxies.iter().any(|(_, wp)| {
- wp.river_window.id().protocol_id() == river_window.id().protocol_id()
- });
- if already_tracked {
- eprintln!("wm_handle_window: duplicate skipped");
- return;
- }
-
- let id = state.alloc_id();
- let mut window = Window::default();
- window.id = id;
- window.is_new = true;
- window.tags = state.wm.active_tags;
-
- state.wm.windows.push(window);
- state
- .window_proxies
- .push((id, WindowProxy {
- river_window,
- decoration: None,
- dec_left: None,
- dec_right: None,
- dec_bottom: None,
- }));
- eprintln!("[window] new window id={} (app_id pending)", id);
- }
-
- // Output event: field is `id` (the new RiverOutputV1 proxy)
- river_window_manager_v1::Event::Output { id: river_output } => {
- let already_tracked = state.output_proxies.iter().any(|(_, op)| {
- op.river_output.id().protocol_id() == river_output.id().protocol_id()
- });
- if already_tracked {
- eprintln!("wm_handle_output: duplicate skipped");
- return;
- }
-
- let id = state.alloc_id();
- let mut output = Output::default();
- output.id = id;
-
- let lso = if let Some(ref ls) = state.layer_shell {
- Some(ls.get_output(&river_output, qhandle, ()))
- } else {
- None
- };
-
- state.wm.outputs.push(output);
- update_screen_bounds(&state.wm.outputs);
- state.output_proxies.push((
- id,
- OutputProxy {
- river_output,
- layer_shell_output: lso,
- },
- ));
- eprintln!("wm_handle_output: new output id={}", id);
- }
-
- // Seat event: field is `id` (the new RiverSeatV1 proxy)
- river_window_manager_v1::Event::Seat { id: river_seat } => {
- let already_tracked = state.seat_proxies.iter().any(|(_, sp)| {
- sp.river_seat.id().protocol_id() == river_seat.id().protocol_id()
- });
- if already_tracked {
- return;
- }
-
- let id = state.alloc_id();
- let mut seat = Seat::default();
- seat.id = id;
- seat.is_new = true;
-
- let cursor_theme = state.wm.cursor_theme.clone().unwrap_or_else(|| "default".to_string());
- let cursor_size = state.wm.cursor_size.unwrap_or(24);
- river_seat.set_xcursor_theme(cursor_theme, cursor_size);
-
- state.wm.seats.push(seat);
- state.seat_proxies.push((
- id,
- SeatProxy {
- river_seat,
- xkb_bindings_seat: None,
- wl_seat: None,
- wl_pointer: None,
- cursor_shape_device: None,
- last_pointer_enter_serial: 0,
- },
- ));
- eprintln!("wm_handle_seat: new seat id={}", id);
- }
-
- river_window_manager_v1::Event::SessionLocked => {}
- river_window_manager_v1::Event::SessionUnlocked => {}
-
- _ => {}
- }
- }
-}
-
-// --- RiverWindowV1 events ---
-
-impl Dispatch<RiverWindowV1, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &RiverWindowV1,
- event: river_window_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- let wid = match state.window_id_for_proxy(proxy) {
- Some(id) => id,
- None => return,
- };
-
- match event {
- river_window_v1::Event::Closed => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.closed = true;
- eprintln!("window id={} (app_id={:?}) closed", wid, window.app_id);
- }
- }
-
- river_window_v1::Event::Dimensions { width, height } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.committed_width = width;
- window.committed_height = height;
- let changed = window.width != width || window.height != height;
- if changed {
- eprintln!(
- "[window] id={} (app_id={:?}) Event::Dimensions: {}x{} (was {}x{})",
- wid, window.app_id, width, height, window.width, window.height
- );
- window.width = width;
- window.height = height;
- state.wm.needs_render = true;
- state.manage_dirty();
- }
- }
- }
-
- river_window_v1::Event::AppId { app_id } => {
- let mut re_eval = false;
- if let Some(window) = state.wm.get_window_mut(wid) {
- if window.app_id != app_id {
- eprintln!(
- "[window] id={} app_id: {:?} -> {:?}",
- wid, window.app_id, app_id
- );
- window.app_id = app_id;
- re_eval = !window.mode_locked;
-
- // Check if this is a blank steam_proton helper window
- let is_proton = window.app_id.as_deref() == Some("steam_proton");
- let is_blank = window.title.is_none() || window.title.as_deref().map_or(true, |t| t.is_empty());
- if is_proton && is_blank {
- if window.tags != 0 {
- window.tags = 0;
- re_eval = true;
- }
- }
- }
- }
- if re_eval {
- state.wm.needs_render = true;
- state.manage_dirty();
- }
- }
-
- river_window_v1::Event::Title { title } => {
- let mut re_eval = false;
- let mut needs_render = false;
- let active_tags = state.wm.active_tags;
- if let Some(window) = state.wm.get_window_mut(wid) {
- if window.title != title {
- window.title = title;
- needs_render = true;
-
- // Check if we need to update/restore tags based on title
- let is_proton = window.app_id.as_deref() == Some("steam_proton");
- let is_blank = window.title.is_none() || window.title.as_deref().map_or(true, |t| t.is_empty());
- if is_proton {
- if is_blank {
- if window.tags != 0 {
- window.tags = 0;
- re_eval = true;
- }
- } else {
- if window.tags == 0 {
- window.tags = active_tags;
- re_eval = true;
- }
- }
- }
- }
- }
- if needs_render {
- state.wm.needs_render = true;
- }
- if re_eval {
- state.manage_dirty();
- }
- }
-
- river_window_v1::Event::DecorationHint { hint } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- // Store the raw u32 value from the WEnum
- window.decoration_hint = hint.into();
- }
- }
-
- river_window_v1::Event::PresentationHint { hint } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.presentation_hint = hint.into();
- }
- }
-
- river_window_v1::Event::Identifier { identifier } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.identifier = Some(identifier);
- }
- }
-
- river_window_v1::Event::UnreliablePid { unreliable_pid } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.pid = unreliable_pid as u32;
- // Spawn an async xprop check for XWayland parent detection.
- // River doesn't forward WM_TRANSIENT_FOR for XWayland windows,
- // so we check via xdotool + xprop as a fallback.
- // The script writes results to /tmp/cce-client-xprop-{wid} which
- // is read on the next ManageStart cycle.
- if !window.has_parent && window.pid > 0 {
- let pid = window.pid;
- let id = window.id;
- let path = crate::paths::get_xprop_path(id);
- let cmd = format!(
- "for xid in $(xdotool search --pid {pid} 2>/dev/null); do \
- t=$(xdotool getwindowname $xid 2>/dev/null); \
- wt=$(xprop -id $xid _NET_WM_WINDOW_TYPE 2>/dev/null); \
- printf '%s|%s\\n' \"$t\" \"$wt\"; \
- done > {path}",
- pid = pid,
- path = path
- );
- crate::config::spawn_command_bg(&cmd);
- window.needs_xprop_check = true;
- eprintln!(
- "[window] id={} (app_id={:?}) spawned xprop check for pid={}",
- wid, window.app_id, pid
- );
- }
- }
- }
-
- river_window_v1::Event::DimensionsHint {
- min_width,
- min_height,
- max_width,
- max_height,
- } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.hint_min_width = min_width;
- window.hint_min_height = min_height;
- window.hint_max_width = max_width;
- window.hint_max_height = max_height;
- eprintln!(
- "[window] id={} (app_id={:?}) dimensions_hint: min={}x{} max={}x{}",
- wid, window.app_id, min_width, min_height, max_width, max_height
- );
-
- if !window.size_hint_applied && min_width > 32 {
- window.size_hint_applied = true;
- state.wm.needs_render = true;
- state.manage_dirty();
- }
- }
- }
-
- river_window_v1::Event::Parent { parent } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- let had_parent = window.has_parent;
- match &parent {
- Some(parent_proxy) => {
- window.has_parent = true;
- // Look up our internal ID for the parent proxy
- let parent_id = state
- .window_proxies
- .iter()
- .find(|(_, wp)| {
- wp.river_window.id().protocol_id()
- == parent_proxy.id().protocol_id()
- })
- .map(|(id, _)| *id);
- window.parent_id = parent_id;
- eprintln!(
- "[window] id={} (app_id={:?}) has parent (internal_id={:?})",
- wid, window.app_id, parent_id
- );
- }
- None => {
- window.has_parent = false;
- window.parent_id = None;
- }
- }
- // Parent status changed: re-assign mode (child windows float)
- if window.has_parent != had_parent && !window.mode_locked {
- state.manage_dirty();
- }
- }
- }
-
- river_window_v1::Event::FullscreenRequested { .. } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.fullscreen_requested = true;
- eprintln!(
- "[window] id={} (app_id={:?}) requested fullscreen",
- wid, window.app_id
- );
- state.manage_dirty();
- }
- }
-
- river_window_v1::Event::ExitFullscreenRequested { .. } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.fullscreen_requested = false;
- eprintln!(
- "[window] id={} (app_id={:?}) requested exit fullscreen",
- wid, window.app_id
- );
- state.manage_dirty();
- }
- }
-
- river_window_v1::Event::MaximizeRequested { .. } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.maximize_requested = true;
- eprintln!(
- "[window] id={} (app_id={:?}) requested maximize",
- wid, window.app_id
- );
- }
- }
-
- river_window_v1::Event::UnmaximizeRequested { .. } => {
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.maximize_requested = false;
- eprintln!(
- "[window] id={} (app_id={:?}) requested unmaximize",
- wid, window.app_id
- );
- }
- }
-
- river_window_v1::Event::MinimizeRequested { .. } => {
- let mut minimized_id = None;
- if let Some(window) = state.wm.get_window_mut(wid) {
- window.minimize_requested = true;
- window.minimized = true;
- minimized_id = Some(wid);
- eprintln!(
- "[window] id={} (app_id={:?}) requested minimize",
- wid, window.app_id
- );
- }
-
- if let Some(wid) = minimized_id {
- // Shift focus to the next visible window if the minimized window was focused
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- if seat.focused_window_id == Some(wid) {
- let active_tags = state.wm.active_tags;
- let visible_ids: Vec<u64> = state.wm
- .windows
- .iter()
- .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.id != wid && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- seat.focused_window_id = visible_ids.last().copied();
- }
- }
- }
-
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- state.manage_dirty();
- }
-
- river_window_v1::Event::ShowWindowMenuRequested { x, y } => {
- eprintln!(
- "[window] id={} show_window_menu_requested at ({}, {}) — ignored",
- wid, x, y
- );
- }
-
- river_window_v1::Event::PointerMoveRequested { seat } => {
- if let Some(_sid) = state.seat_id_for_proxy(&seat) {
- let mut window_found = false;
- let mut needs_render = false;
- if let Some(win) = state.wm.get_window_mut(wid) {
- if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
- if win.tiling_mode != TilingMode::Floating {
- win.tiling_mode = TilingMode::Floating;
- needs_render = true;
- }
- win.mode_locked = true;
- window_found = true;
- }
- }
- if window_found {
- if needs_render {
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- seat.op_start_pointer();
- if let Some(win) = state.wm.get_window_mut(wid) {
- win.anim_x = None;
- win.anim_y = None;
- win.anim_w = None;
- win.anim_h = None;
- win.anim_opacity = None;
- state.active_pointer_op = Some(PointerOp {
- window_id: wid,
- op_type: PointerOpType::Move,
- start_x: win.x,
- start_y: win.y,
- start_width: win.width,
- start_height: win.height,
- });
- }
- }
- }
- }
-
- river_window_v1::Event::PointerResizeRequested { seat, edges } => {
- if let Some(_sid) = state.seat_id_for_proxy(&seat) {
- let edges_u32: u32 = edges.into();
- let op_type = match edges_u32 {
- 1 => PointerOpType::ResizeTop,
- 2 => PointerOpType::ResizeBottom,
- 4 => PointerOpType::ResizeLeft,
- 8 => PointerOpType::ResizeRight,
- 5 => PointerOpType::ResizeTopLeft,
- 9 => PointerOpType::ResizeTopRight,
- 6 => PointerOpType::ResizeBottomLeft,
- 10 => PointerOpType::ResizeBottomRight,
- _ => PointerOpType::Resize,
- };
-
- let mut window_found = false;
- let mut needs_render = false;
- let side_panel_pos = state.wm.layout.side_panel_position.clone();
- if let Some(win) = state.wm.get_window_mut(wid) {
- if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
- let is_valid_side_panel_resize = win.tiling_mode == TilingMode::SidePanel
- && if side_panel_pos == "right" {
- matches!(
- op_type,
- PointerOpType::Resize
- | PointerOpType::ResizeLeft
- | PointerOpType::ResizeTopLeft
- | PointerOpType::ResizeBottomLeft
- )
- } else {
- matches!(
- op_type,
- PointerOpType::Resize
- | PointerOpType::ResizeRight
- | PointerOpType::ResizeTopRight
- | PointerOpType::ResizeBottomRight
- )
- };
- if !is_valid_side_panel_resize && win.tiling_mode != TilingMode::Floating {
- win.tiling_mode = TilingMode::Floating;
- needs_render = true;
- }
- win.mode_locked = true;
- window_found = true;
- }
- }
- if window_found {
- if needs_render {
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- seat.op_start_pointer();
- if let Some(win) = state.wm.get_window_mut(wid) {
- win.anim_x = None;
- win.anim_y = None;
- win.anim_w = None;
- win.anim_h = None;
- win.anim_opacity = None;
-
- state.active_pointer_op = Some(PointerOp {
- window_id: wid,
- op_type,
- start_x: win.x,
- start_y: win.y,
- start_width: win.width,
- start_height: win.height,
- });
- }
- }
- }
- }
-
- _ => {}
- }
- }
-}
-
-// --- RiverSeatV1 events ---
-
-impl Dispatch<RiverSeatV1, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &RiverSeatV1,
- event: river_seat_v1::Event,
- _data: &(),
- _conn: &Connection,
- qhandle: &QueueHandle<Self>,
- ) {
- let sid = match state.seat_id_for_proxy(proxy) {
- Some(id) => id,
- None => return,
- };
-
- match event {
- river_seat_v1::Event::Removed => {
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
- seat.removed = true;
- }
- }
-
- river_seat_v1::Event::WindowInteraction {
- window: river_window,
- } => {
- if let Some(wid) = state.window_id_for_proxy(&river_window) {
- let target_app_id = state.wm.get_window(wid).and_then(|w| w.app_id.clone());
- if target_app_id.as_deref() == Some("cce-status-interface") {
- // Do not focus status bar!
- return;
- }
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
- eprintln!(
- "[focus] WindowInteraction: seat={} focused_window_id={} -> {} (app_id={:?})",
- sid,
- seat.focused_window_id.unwrap_or(0),
- wid,
- target_app_id
- );
- let already_focused = seat.focused_window_id == Some(wid);
- seat.focused_window_id = Some(wid);
- // Move clicked window to front of cascade stack
- let moved = state.wm.move_window_to_end(wid);
-
- // Disable expose if it was active
- let mut expose_changed = false;
- if state.wm.expose_active {
- crate::wm::set_expose_active(&mut state.wm, false);
- expose_changed = true;
- }
-
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
-
- if !already_focused || moved || expose_changed {
- state.manage_dirty();
- }
- }
- }
- }
-
- river_seat_v1::Event::PointerEnter {
- window: river_window,
- } => {
- if let Some(wid) = state.window_id_for_proxy(&river_window) {
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
- seat.hovered_window_id = Some(wid);
-
- if state.wm.expose_active {
- let already_focused = seat.focused_window_id == Some(wid);
- seat.focused_window_id = Some(wid);
-
- state.wm.needs_focus = true;
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
-
- if !already_focused {
- state.manage_dirty();
- }
- }
- }
- }
- }
-
- river_seat_v1::Event::PointerLeave => {
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
- seat.hovered_window_id = None;
- }
- }
-
- river_seat_v1::Event::WlSeat { name } => {
- if let Some(ref registry) = state.registry {
- if let Some((_, seat_proxy)) = state.seat_proxies.iter_mut().find(|(_, sp)| sp.river_seat == *proxy) {
- if seat_proxy.wl_seat.is_none() {
- let wl_seat = registry.bind::<wl_seat::WlSeat, _, _>(name, 2, qhandle, ());
- seat_proxy.wl_seat = Some(wl_seat);
- }
- }
- }
- }
-
- river_seat_v1::Event::OpDelta { dx, dy } => {
- if let Some(ref op) = state.active_pointer_op {
- let wid = op.window_id;
- let bw = state.wm.layout.cascade_border_width;
- if let Some(win) = state.wm.get_window_mut(wid) {
- match op.op_type {
- PointerOpType::Move => {
- win.x = op.start_x + dx;
- win.y = op.start_y + dy;
- }
- PointerOpType::Resize | PointerOpType::ResizeRight => {
- win.width = std::cmp::max(50, op.start_width + dx);
- if win.tiling_mode == TilingMode::SidePanel {
- win.hint_min_width = win.width + bw * 2;
- }
- }
- PointerOpType::ResizeLeft => {
- let target_width = std::cmp::max(50, op.start_width - dx);
- let actual_dx = op.start_width - target_width;
- win.x = op.start_x + actual_dx;
- win.width = target_width;
- if win.tiling_mode == TilingMode::SidePanel {
- win.hint_min_width = win.width + bw * 2;
- }
- }
- PointerOpType::ResizeBottom => {
- win.height = std::cmp::max(50, op.start_height + dy);
- }
- PointerOpType::ResizeTop => {
- let target_height = std::cmp::max(50, op.start_height - dy);
- let actual_dy = op.start_height - target_height;
- win.y = op.start_y + actual_dy;
- win.height = target_height;
- }
- PointerOpType::ResizeBottomRight => {
- win.width = std::cmp::max(50, op.start_width + dx);
- win.height = std::cmp::max(50, op.start_height + dy);
- if win.tiling_mode == TilingMode::SidePanel {
- win.hint_min_width = win.width + bw * 2;
- }
- }
- PointerOpType::ResizeBottomLeft => {
- let target_width = std::cmp::max(50, op.start_width - dx);
- let actual_dx = op.start_width - target_width;
- win.x = op.start_x + actual_dx;
- win.width = target_width;
- win.height = std::cmp::max(50, op.start_height + dy);
- if win.tiling_mode == TilingMode::SidePanel {
- win.hint_min_width = win.width + bw * 2;
- }
- }
- PointerOpType::ResizeTopLeft => {
- let target_width = std::cmp::max(50, op.start_width - dx);
- let actual_dx = op.start_width - target_width;
- win.x = op.start_x + actual_dx;
- win.width = target_width;
-
- let target_height = std::cmp::max(50, op.start_height - dy);
- let actual_dy = op.start_height - target_height;
- win.y = op.start_y + actual_dy;
- win.height = target_height;
- if win.tiling_mode == TilingMode::SidePanel {
- win.hint_min_width = win.width + bw * 2;
- }
- }
- PointerOpType::ResizeTopRight => {
- win.width = std::cmp::max(50, op.start_width + dx);
-
- let target_height = std::cmp::max(50, op.start_height - dy);
- let actual_dy = op.start_height - target_height;
- win.y = op.start_y + actual_dy;
- win.height = target_height;
- if win.tiling_mode == TilingMode::SidePanel {
- win.hint_min_width = win.width + bw * 2;
- }
- }
- }
- }
- state.manage_dirty();
- }
- }
- river_seat_v1::Event::OpRelease => {
- state.pointer_op_release_pending = true;
- state.manage_dirty();
- }
-
- _ => {}
- }
- }
-}
-
-// --- RiverOutputV1 events ---
-
-impl Dispatch<RiverOutputV1, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &RiverOutputV1,
- event: river_output_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- let oid = state
- .output_proxies
- .iter()
- .find(|(_, op)| op.river_output.id() == proxy.id())
- .map(|(id, _)| *id);
-
- match event {
- river_output_v1::Event::WlOutput { name } => {
- if let Some(oid) = oid {
- if let Some(output) = state.wm.outputs.iter_mut().find(|o| o.id == oid) {
- output.wl_output_name = Some(name);
- // Also try to copy dimensions from WlOutputInfo if already populated
- if let Some(info) = state.wl_outputs.iter().find(|info| info.name == name) {
- if info.width > 0 && info.height > 0 {
- output.width = info.width;
- output.height = info.height;
- output.x = info.x;
- output.y = info.y;
- state.wm.needs_render = true;
- update_screen_bounds(&state.wm.outputs);
- eprintln!(
- "river_output linked to wl_output name={} dimensions (copied): {}x{} at ({},{})",
- name, info.width, info.height, info.x, info.y
- );
- }
- }
- }
- }
- }
- _ => {}
- }
- }
-}
-
-// --- wl_output::WlOutput events ---
-
-impl Dispatch<wl_output::WlOutput, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &wl_output::WlOutput,
- event: wl_output::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- // Find registry name
- let name = state
- .wl_outputs
- .iter()
- .find(|info| info.wl_output.id() == proxy.id())
- .map(|info| info.name);
-
- let Some(name) = name else { return };
-
- match event {
- wl_output::Event::Geometry {
- x,
- y,
- ..
- } => {
- if let Some(info) = state.wl_outputs.iter_mut().find(|info| info.name == name) {
- info.x = x;
- info.y = y;
- }
- // Also update matched Output in WindowManager
- if let Some(output) = state.wm.outputs.iter_mut().find(|o| o.wl_output_name == Some(name)) {
- if output.x != x || output.y != y {
- output.x = x;
- output.y = y;
- state.wm.needs_render = true;
- update_screen_bounds(&state.wm.outputs);
- }
- }
- }
- wl_output::Event::Mode {
- flags,
- width,
- height,
- ..
- } => {
- let is_current = match flags {
- wayland_client::WEnum::Value(mode) => mode.contains(wl_output::Mode::Current),
- _ => false,
- };
-
- if is_current {
- if let Some(info) = state.wl_outputs.iter_mut().find(|info| info.name == name) {
- info.width = width;
- info.height = height;
- }
- // Also update matched Output in WindowManager
- if let Some(output) = state.wm.outputs.iter_mut().find(|o| o.wl_output_name == Some(name)) {
- if output.width != width || output.height != height {
- output.width = width;
- output.height = height;
- state.wm.needs_render = true;
- update_screen_bounds(&state.wm.outputs);
- eprintln!(
- "wl_output name={} updated current mode dimensions: {}x{}",
- name, width, height
- );
- }
- }
- }
- }
- _ => {}
- }
- }
-}
-
-// --- RiverLayerShellV1 (no events) ---
-
-impl Dispatch<RiverLayerShellV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &RiverLayerShellV1,
- _event: river_layer_shell_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- }
-}
-
-// --- RiverLayerShellOutputV1 events ---
-
-impl Dispatch<RiverLayerShellOutputV1, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &RiverLayerShellOutputV1,
- event: river_layer_shell_output_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- let oid = state
- .output_proxies
- .iter()
- .find(|(_, op)| {
- op.layer_shell_output.as_ref().map_or(false, |lso| {
- lso.id().protocol_id() == proxy.id().protocol_id()
- })
- })
- .map(|(id, _)| *id);
-
- let Some(oid) = oid else { return };
-
- match event {
- river_layer_shell_output_v1::Event::NonExclusiveArea {
- x,
- y,
- width,
- height,
- } => {
- if let Some(output) = state.wm.outputs.iter_mut().find(|o| o.id == oid) {
- if output.usable_width != width
- || output.usable_height != height
- || output.usable_x != x
- || output.usable_y != y
- {
- output.usable_x = x;
- output.usable_y = y;
- output.usable_width = width;
- output.usable_height = height;
- state.wm.needs_render = true;
- eprintln!(
- "output id={} usable area: {}x{} at ({},{})",
- oid, width, height, x, y
- );
- }
- }
- }
- _ => {}
- }
- }
-}
-
-// --- RiverInputManagerV1 events ---
-
-impl Dispatch<RiverInputManagerV1, ()> for AppState {
- event_created_child!(AppState, RiverInputManagerV1, [
- river_input_manager_v1::EVT_INPUT_DEVICE_OPCODE => (RiverInputDeviceV1, ()),
- ]);
-
- fn event(
- state: &mut Self,
- _proxy: &RiverInputManagerV1,
- event: river_input_manager_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- river_input_manager_v1::Event::InputDevice { id: _input_device } => {
- use crate::types::InputDevice;
- let dev = InputDevice { is_keyboard: false };
- state.wm.input_devices.push(dev);
- eprintln!("input_device discovered");
- }
- river_input_manager_v1::Event::Finished => {}
- _ => {}
- }
- }
-}
-
-// --- RiverInputDeviceV1 events ---
-
-impl Dispatch<RiverInputDeviceV1, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &RiverInputDeviceV1,
- event: river_input_device_v1::Event,
- _data: &(),
- _conn: &Connection,
- qhandle: &QueueHandle<Self>,
- ) {
- match event {
- river_input_device_v1::Event::Type { _type: dev_type } => {
- if let Some(dev) = state.wm.input_devices.last_mut() {
- // dev_type is WEnum<Type>; check for Keyboard variant
- dev.is_keyboard = matches!(
- dev_type,
- wayland_client::WEnum::Value(river_input_device_v1::Type::Keyboard)
- );
- }
- }
- 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()
- } 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 => {}
- _ => {}
- }
- }
-}
-
-// --- RiverPointerBindingV1 events ---
-
-impl Dispatch<RiverPointerBindingV1, BindingUserData> for AppState {
- fn event(
- state: &mut Self,
- _proxy: &RiverPointerBindingV1,
- event: river_pointer_binding_v1::Event,
- data: &BindingUserData,
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- river_pointer_binding_v1::Event::Pressed => {
- eprintln!(
- "[binding] pointer pressed: action={:?} seat_id={}",
- data.action, data.seat_id
- );
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == data.seat_id) {
- seat.pending_action = data.action.clone();
- seat.pending_command = data.command.clone();
- }
- // Trigger a manage sequence so the pending action is processed
- state.manage_dirty();
- }
- _ => {}
- }
- }
-}
-
-// --- XKB bindings ---
-
-impl Dispatch<RiverXkbBindingsV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &RiverXkbBindingsV1,
- _event: river_xkb_bindings_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- }
-}
-
-impl Dispatch<RiverXkbBindingV1, BindingUserData> for AppState {
- fn event(
- state: &mut Self,
- _proxy: &RiverXkbBindingV1,
- event: river_xkb_binding_v1::Event,
- data: &BindingUserData,
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- river_xkb_binding_v1::Event::Pressed => {
- eprintln!(
- "[binding] xkb pressed: action={:?} command={:?} seat_id={}",
- data.action, data.command, data.seat_id
- );
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == data.seat_id) {
- seat.pending_action = data.action.clone();
- seat.pending_command = data.command.clone();
- }
- // Trigger a manage sequence so the pending action is processed
- state.manage_dirty();
- }
- river_xkb_binding_v1::Event::Released => {}
- river_xkb_binding_v1::Event::StopRepeat => {}
- _ => {}
- }
- }
-}
-
-impl Dispatch<RiverXkbBindingsSeatV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &RiverXkbBindingsSeatV1,
- _event: river_xkb_bindings_seat_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- }
-}
-
-// --- RiverNodeV1 (no events, used for set_position) ---
-
-impl Dispatch<RiverNodeV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &RiverNodeV1,
- _event: river_node_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- // river_node_v1 has no events
- }
-}
-
-// --- Helper functions ---
-
-/// Execute an action triggered by a keybinding or pointer binding.
-fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Action, command: Option<&str>) {
- use crate::types::Action;
- match action {
- Action::None => {}
- Action::SidePanelLeft => {
- let focused = state.wm.focused_window();
- let is_side_panel = focused.map_or(false, |w| w.tiling_mode == TilingMode::SidePanel);
- if is_side_panel {
- state.wm.layout.side_panel_position = "left".to_string();
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- state.manage_dirty();
- }
- }
- Action::SidePanelRight => {
- let focused = state.wm.focused_window();
- let is_side_panel = focused.map_or(false, |w| w.tiling_mode == TilingMode::SidePanel);
- if is_side_panel {
- state.wm.layout.side_panel_position = "right".to_string();
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- state.manage_dirty();
- }
- }
- Action::Spawn => {
- if let Some(cmd) = command {
- eprintln!("spawn: {}", cmd);
- // Close inherited FDs > 2 in the child so that spawned
- // Wayland clients (fuzzel, etc.) never accidentally read
- // from cce-client's Wayland socket fd. This prevents protocol
- // corruption and the CPU spin loop that results from it.
- use std::os::unix::process::CommandExt;
- let _ = unsafe {
- std::process::Command::new("sh")
- .arg("-c")
- .arg(cmd)
- .env_remove("WAYLAND_DEBUG")
- .stdout(std::process::Stdio::null())
- .stderr(std::process::Stdio::null())
- .pre_exec(|| {
- let max_fd = libc::sysconf(libc::_SC_OPEN_MAX) as libc::c_int;
- for fd in 3..max_fd {
- libc::close(fd);
- }
- libc::setsid();
- Ok(())
- })
- .spawn()
- };
- }
- }
- Action::Toggle => {
- if let Some(cmd) = command {
- let prog_name = crate::config::extract_program_name(cmd);
- // Look for an open window whose app_id (or title fallback) matches prog_name
- let open_window_id = state.wm.windows.iter().find(|w| {
- if w.closed {
- return false;
- }
- if let Some(ref aid) = w.app_id {
- let aid_lower = aid.to_lowercase();
- let prog_lower = prog_name.to_lowercase();
- aid_lower == prog_lower || aid_lower.contains(&prog_lower) || prog_lower.contains(&aid_lower)
- } else if let Some(ref title) = w.title {
- let title_lower = title.to_lowercase();
- let prog_lower = prog_name.to_lowercase();
- title_lower.contains(&prog_lower)
- } else {
- false
- }
- }).map(|w| w.id);
-
- if let Some(win_id) = open_window_id {
- eprintln!("toggle: close window id={}", win_id);
- if let Some(wp) = state.get_window_proxy(win_id) {
- wp.river_window.close();
- }
- // Shift focus to the next visible window if the closed window was focused.
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- if seat.focused_window_id == Some(win_id) {
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| {
- (w.tags & state.wm.active_tags) != 0 && !w.closed && !w.minimized && w.id != win_id && w.app_id.as_deref() != Some("cce-status-interface")
- })
- .map(|w| w.id)
- .collect();
- seat.focused_window_id = visible_ids.last().copied();
- }
- }
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- } else {
- eprintln!("toggle: spawn command={}", cmd);
- use std::os::unix::process::CommandExt;
- let _ = unsafe {
- std::process::Command::new("sh")
- .arg("-c")
- .arg(cmd)
- .env_remove("WAYLAND_DEBUG")
- .stdout(std::process::Stdio::null())
- .stderr(std::process::Stdio::null())
- .pre_exec(|| {
- let max_fd = libc::sysconf(libc::_SC_OPEN_MAX) as libc::c_int;
- for fd in 3..max_fd {
- libc::close(fd);
- }
- libc::setsid();
- Ok(())
- })
- .spawn()
- };
- }
- }
- }
- Action::Close => {
- // Ask the compositor to close the focused window by calling
- // close() on its River protocol proxy. This matches tinyrwm's
- // approach: close() sends a request to River, which asks the
- // client to close. When the client actually closes, River sends
- // Event::Closed, which sets window.closed = true. Then on the
- // next ManageStart, remove_windows() drops it from the vector.
- //
- // close() modifies window management state and can only be called
- // during a manage sequence — which it is, since execute_action
- // runs inside ManageStart.
- if let Some(seat) = state.wm.seats.first() {
- if let Some(focused_id) = seat.focused_window_id {
- // Send the close request to the compositor
- if let Some(wp) = state.get_window_proxy(focused_id) {
- wp.river_window.close();
- }
- // Shift focus to the next visible window (excluding the one we just closed).
- // The window isn't closed=true yet (that happens when River sends Event::Closed),
- // so we exclude it by ID instead.
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| {
- (w.tags & state.wm.active_tags) != 0 && !w.closed && !w.minimized && w.id != focused_id && w.app_id.as_deref() != Some("cce-status-interface")
- })
- .map(|w| w.id)
- .collect();
- let next_id = visible_ids.last().copied();
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- seat.focused_window_id = next_id;
- }
- }
- }
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- Action::Minimize => {
- // Get the focused window ID on this seat
- let focused_id = state.wm.seats.iter().find(|s| s.id == seat_id).and_then(|s| s.focused_window_id);
- if let Some(focused_id) = focused_id {
- if let Some(w) = state.wm.get_window_mut(focused_id) {
- w.minimized = true;
- }
- // Shift focus to the next visible window
- let active_tags = state.wm.active_tags;
- let visible_ids: Vec<u64> = state.wm
- .windows
- .iter()
- .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.id != focused_id && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- let next_id = visible_ids.last().copied();
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == seat_id) {
- seat.focused_window_id = next_id;
- }
- }
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- state.manage_dirty();
- }
- Action::FocusNext => {
- // Focus the next visible window (wrapping) of the same tiling mode,
- // and move it to the front of the cascade stack (end of windows vector).
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- let focused_id = seat.focused_window_id;
- let focused_mode = focused_id.and_then(|fid| {
- state.wm.windows.iter().find(|w| w.id == fid).map(|w| w.tiling_mode)
- });
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| {
- (w.tags & state.wm.active_tags) != 0
- && !w.closed
- && !w.minimized
- && w.app_id.as_deref() != Some("cce-status-interface")
- && (focused_mode.is_none() || Some(w.tiling_mode) == focused_mode)
- })
- .map(|w| w.id)
- .collect();
- if let Some(fid) = focused_id {
- if let Some(idx) = visible_ids.iter().position(|id| *id == fid) {
- let next_idx = (idx + 1) % visible_ids.len();
- let next_id = visible_ids[next_idx];
- seat.focused_window_id = Some(next_id);
- // Move the newly focused window to the end of the
- // windows vector so it gets the front cascade position
- // (rightmost/bottommost) and brightest border.
- state.wm.move_window_to_end(next_id);
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- } else if !visible_ids.is_empty() {
- let next_id = visible_ids[visible_ids.len() - 1];
- seat.focused_window_id = Some(next_id);
- state.wm.move_window_to_end(next_id);
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- }
- }
- Action::FocusPrev => {
- // Focus the previous visible window (wrapping) of the same tiling mode,
- // and move it to the front of the cascade stack (end of windows vector).
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- let focused_id = seat.focused_window_id;
- let focused_mode = focused_id.and_then(|fid| {
- state.wm.windows.iter().find(|w| w.id == fid).map(|w| w.tiling_mode)
- });
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| {
- (w.tags & state.wm.active_tags) != 0
- && !w.closed
- && !w.minimized
- && w.app_id.as_deref() != Some("cce-status-interface")
- && (focused_mode.is_none() || Some(w.tiling_mode) == focused_mode)
- })
- .map(|w| w.id)
- .collect();
- if let Some(fid) = focused_id {
- if let Some(idx) = visible_ids.iter().position(|id| *id == fid) {
- let prev_idx = if idx == 0 { visible_ids.len() - 1 } else { idx - 1 };
- let prev_id = visible_ids[prev_idx];
- seat.focused_window_id = Some(prev_id);
- // Move the newly focused window to the end of the
- // windows vector so it gets the front cascade position
- // (rightmost/bottommost) and brightest border.
- state.wm.move_window_to_end(prev_id);
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- } else if !visible_ids.is_empty() {
- let prev_id = visible_ids[visible_ids.len() - 1];
- seat.focused_window_id = Some(prev_id);
- state.wm.move_window_to_end(prev_id);
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- }
- }
- Action::Move | Action::Resize => {
- let op_type = if *action == Action::Move {
- PointerOpType::Move
- } else {
- state.pending_pointer_op_type.take().unwrap_or(PointerOpType::Resize)
- };
-
- let target_wid = if let Some(seat) = state.wm.seats.iter().find(|s| s.id == seat_id) {
- seat.interacted_window_id.or(seat.focused_window_id)
- } else {
- None
- };
-
- if let Some(wid) = target_wid {
- let mut window_found = false;
- let mut needs_render = false;
- let side_panel_pos = state.wm.layout.side_panel_position.clone();
- if let Some(win) = state.wm.get_window_mut(wid) {
- if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
- let is_valid_side_panel_resize = win.tiling_mode == TilingMode::SidePanel
- && if side_panel_pos == "right" {
- matches!(
- op_type,
- PointerOpType::Resize
- | PointerOpType::ResizeLeft
- | PointerOpType::ResizeTopLeft
- | PointerOpType::ResizeBottomLeft
- )
- } else {
- matches!(
- op_type,
- PointerOpType::Resize
- | PointerOpType::ResizeRight
- | PointerOpType::ResizeTopRight
- | PointerOpType::ResizeBottomRight
- )
- };
- if !is_valid_side_panel_resize && win.tiling_mode != TilingMode::Floating {
- win.tiling_mode = TilingMode::Floating;
- needs_render = true;
- }
- win.mode_locked = true;
- window_found = true;
- }
- }
-
- if window_found {
- if needs_render {
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- if let Some((_, sp)) = state.seat_proxies.iter().find(|(sid, _)| *sid == seat_id) {
- sp.river_seat.op_start_pointer();
- }
- if op_type != PointerOpType::Move {
- if let Some(wp) = state.get_window_proxy(wid) {
- wp.river_window.inform_resize_start();
- }
- }
- if let Some(win) = state.wm.get_window_mut(wid) {
- win.anim_x = None;
- win.anim_y = None;
- win.anim_w = None;
- win.anim_h = None;
- win.anim_opacity = None;
- state.active_pointer_op = Some(PointerOp {
- window_id: wid,
- op_type,
- start_x: win.x,
- start_y: win.y,
- start_width: win.width,
- start_height: win.height,
- });
- }
- }
- }
- }
- Action::Exit => {
- state.wm.exit_requested = true;
- state.exit_requested = true;
- if let Some(ref wm) = state.window_manager {
- wm.exit_session();
- }
- }
- Action::Fullscreen => {
- if let Some(seat) = state.wm.seats.first() {
- if let Some(focused_id) = seat.focused_window_id {
- // Read current state and compute target mode before mutable borrow
- let is_fullscreen = state
- .wm
- .get_window(focused_id)
- .map(|w| w.tiling_mode == TilingMode::Fullscreen)
- .unwrap_or(false);
-
- let resolved_mode = if is_fullscreen {
- // Exiting fullscreen: snapshot the window info we need for mode resolution
- let (app_id, title, tags, mode_locked) = state
- .wm
- .get_window(focused_id)
- .map(|w| (w.app_id.clone(), w.title.clone(), w.tags, w.mode_locked))
- .unwrap_or((None, None, 1, false));
- // Build a temporary Window for mode resolution
- let temp_win = Window {
- id: focused_id,
- is_new: false,
- closed: false,
- tags,
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- app_id,
- title,
- identifier: None,
- parent_id: None,
- has_parent: false,
- pid: 0,
- hint_min_width: 0,
- hint_min_height: 0,
- hint_max_width: 0,
- hint_max_height: 0,
- decoration_hint: 3,
- presentation_hint: 0,
- fullscreen_requested: false,
- maximize_requested: false,
- minimize_requested: false,
- tiling_mode: TilingMode::Fullscreen,
- mode_locked,
- xprop_check_attempts: 0,
- ..Default::default()
- };
- crate::wm::get_mode_for_window(&state.wm, &temp_win)
- .unwrap_or(state.wm.global_layout)
- } else {
- TilingMode::Fullscreen
- };
-
- if let Some(window) = state.wm.get_window_mut(focused_id) {
- if is_fullscreen {
- window.tiling_mode = resolved_mode;
- window.mode_locked = false;
- } else {
- window.tiling_mode = TilingMode::Fullscreen;
- window.mode_locked = true;
- }
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- }
- }
- }
- }
- Action::LayoutNext => {
- let cycle = [
- TilingMode::Cascade,
- TilingMode::Grid,
- TilingMode::Fullscreen,
- ];
- // Cycle the layout for the currently active tag(s) only.
- // Determine the "current" mode from the first active tag's layout
- // (or global_layout if no tag_layout is set for it), then advance.
- let active_tags = state.wm.active_tags;
- let first_tag_bit = (0..crate::types::NUM_TAGS).find(|b| (active_tags & (1u32 << b)) != 0);
- let current = if let Some(bit) = first_tag_bit {
- if state.wm.has_tag_layout[bit] {
- state.wm.tag_layouts[bit]
- } else {
- state.wm.global_layout
- }
- } else {
- state.wm.global_layout
- };
- let next = cycle
- .iter()
- .position(|m| *m == current)
- .map(|i| cycle[(i + 1) % cycle.len()])
- .unwrap_or(TilingMode::Cascade);
-
- // Set the layout for every currently active tag.
- for tag_bit in 0..crate::types::NUM_TAGS {
- if (active_tags & (1u32 << tag_bit)) != 0 {
- state.wm.tag_layouts[tag_bit] = next;
- state.wm.has_tag_layout[tag_bit] = true;
- }
- }
- eprintln!(
- "layout-next: tag layout set to {} for active_tags=0b{:b}",
- next.as_str(),
- active_tags
- );
-
- if state.wm.notifications_enable {
- crate::config::show_notification("cce-client", &format!("Layout set to {} for active tags", next.as_str()));
- }
-
- // Unlock windows that got their mode from the layout (not from mode_rules
- // or manual set-mode) so assign_window_modes will reassign them.
- // Windows with mode_locked=true were explicitly set by the user and stay.
- // Windows matched by mode_rules will get reassigned to the same rule mode.
- // Only windows that fell through to tag_layouts/global_layout will change.
-
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- }
- Action::ModeNext => {
- let cycle = [
- TilingMode::Cascade,
- TilingMode::Grid,
- TilingMode::Fullscreen,
- TilingMode::Floating,
- ];
- let focused_id = state
- .wm
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
- if let Some(fid) = focused_id {
- let notifications_enable = state.wm.notifications_enable;
- if let Some(win) = state.wm.get_window_mut(fid) {
- let next = cycle
- .iter()
- .position(|m| *m == win.tiling_mode)
- .map(|i| cycle[(i + 1) % cycle.len()])
- .unwrap_or(TilingMode::Cascade);
- eprintln!(
- "mode-next: window {} ({:?}) {} -> {}",
- fid,
- win.app_id,
- win.tiling_mode.as_str(),
- next.as_str()
- );
- win.tiling_mode = next;
- win.mode_locked = true;
- if notifications_enable {
- let win_title = win.title.as_deref().unwrap_or("Window");
- crate::config::show_notification("cce-client", &format!("Tiling mode set to {} for: {}", next.as_str(), win_title));
- }
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- }
- }
- }
- Action::ModeNextShared => {
- let cycle = [
- TilingMode::Cascade,
- TilingMode::Grid,
- TilingMode::Fullscreen,
- TilingMode::Floating,
- ];
- let focused_id = state
- .wm
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
- if let Some(fid) = focused_id {
- let notifications_enable = state.wm.notifications_enable;
- let current_mode = state.wm.get_window(fid).map(|w| w.tiling_mode);
- if let Some(old_mode) = current_mode {
- let next = cycle
- .iter()
- .position(|m| *m == old_mode)
- .map(|i| cycle[(i + 1) % cycle.len()])
- .unwrap_or(TilingMode::Cascade);
-
- let active_tags = state.wm.active_tags;
-
- let mut updated_count = 0;
- for win in &mut state.wm.windows {
- if !win.closed
- && win.app_id.as_deref() != Some("cce-status-interface")
- && (win.tags & active_tags) != 0
- && win.tiling_mode == old_mode
- {
- win.tiling_mode = next;
- win.mode_locked = true;
- updated_count += 1;
- }
- }
-
- eprintln!(
- "mode-next-shared: updated {} windows (current tag, mode {}) -> {}",
- updated_count,
- old_mode.as_str(),
- next.as_str()
- );
-
- if notifications_enable && updated_count > 0 {
- crate::config::show_notification(
- "cce-client",
- &format!(
- "Tiling mode set to {} for all {} windows on active tag",
- next.as_str(),
- old_mode.as_str()
- ),
- );
- }
-
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- }
- }
- }
- Action::Reload => {
- crate::restart::wm_restart();
- }
- Action::Restart => {
- crate::restart::wm_restart();
- }
- Action::Expose => {
- let active = !state.wm.expose_active;
- crate::wm::set_expose_active(&mut state.wm, active);
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- }
- Action::View1 | Action::View2 | Action::View3 | Action::View4 => {
- let tag = match action {
- Action::View1 => 1,
- Action::View2 => 2,
- Action::View3 => 3,
- Action::View4 => 4,
- _ => return,
- };
- state.wm.active_tags = 1 << (tag - 1);
-
- // Reassign focus to a visible window on the new tag
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- seat.focused_window_id = visible_ids.last().copied();
- }
-
- state.wm.needs_render = true;
- state.wm.needs_focus = true;
- state.wm.needs_status_update = true;
- }
- Action::Toggle1 | Action::Toggle2 | Action::Toggle3 | Action::Toggle4 => {
- let tag = match action {
- Action::Toggle1 => 1,
- Action::Toggle2 => 2,
- Action::Toggle3 => 3,
- Action::Toggle4 => 4,
- _ => return,
- };
- state.wm.active_tags ^= 1 << (tag - 1);
-
- // If the focused window is no longer visible, reassign focus
- let focused_id = state
- .wm
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
- let focused_still_visible = focused_id.map_or(false, |fid| {
- state
- .wm
- .get_window(fid)
- .map_or(false, |w| (w.tags & state.wm.active_tags) != 0 && !w.closed)
- });
- if !focused_still_visible {
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- seat.focused_window_id = visible_ids.last().copied();
- }
- state.wm.needs_focus = true;
- }
-
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- }
- Action::SetTag1 | Action::SetTag2 | Action::SetTag3 | Action::SetTag4 => {
- let tag = match action {
- Action::SetTag1 => 1,
- Action::SetTag2 => 2,
- Action::SetTag3 => 3,
- Action::SetTag4 => 4,
- _ => return,
- };
- // Read focused_id before any mutable borrow
- let focused_id = state
- .wm
- .seats
- .iter()
- .find(|s| !s.removed)
- .and_then(|s| s.focused_window_id);
- if let Some(focused_id) = focused_id {
- // Set the window's tag
- let active_tags = state.wm.active_tags;
- let window_left_active_tag =
- state.wm.get_window_mut(focused_id).map_or(false, |window| {
- window.tags = 1 << (tag - 1);
- (window.tags & active_tags) == 0
- });
-
- // If the window is no longer on an active tag, shift focus
- if window_left_active_tag {
- let visible_ids: Vec<u64> = state
- .wm
- .windows
- .iter()
- .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("cce-status-interface"))
- .map(|w| w.id)
- .collect();
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
- seat.focused_window_id = visible_ids.last().copied();
- }
- state.wm.needs_focus = true;
- }
-
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- }
- }
- }
-}
-
-fn enforce_single_instance(wm: &mut WindowManager) {
- for rule in &wm.mode_rules {
- if !rule.single_instance {
- continue;
- }
- let mut first_matched = false;
- for window in &mut wm.windows {
- if window.closed {
- continue;
- }
- let has_app_id = window.app_id.as_deref().map_or(false, |s| !s.is_empty());
- let match_app = rule.app_id_pattern == "*"
- || window
- .app_id
- .as_deref()
- .map_or(false, |aid| aid.contains(&rule.app_id_pattern))
- || (!has_app_id && window.title.as_deref().map_or(false, |t| {
- let normalize = |s: &str| -> String {
- s.to_lowercase().replace(|c: char| c == '-' || c == '_', " ")
- };
- normalize(t).contains(&normalize(&rule.app_id_pattern))
- }));
- let match_title = rule.title_pattern.as_deref() == Some("*")
- || rule.title_pattern.is_none()
- || window.title.as_deref().map_or(false, |t| {
- t.contains(rule.title_pattern.as_deref().unwrap_or(""))
- });
- if match_app && match_title {
- if first_matched {
- window.tiling_mode = TilingMode::Floating;
- window.mode_locked = true;
- }
- first_matched = true;
- }
- }
- }
-}
-
-/// Convert our u32 modifier bitmask to the generated Modifiers bitflags.
-fn u32_to_modifiers(mods: u32) -> Modifiers {
- Modifiers::from_bits_truncate(mods)
-}
-
-fn apply_pending_bindings(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
- // Clear old binding proxies (destroying them unregisters the bindings)
- state.xkb_binding_proxies.clear();
- state.pointer_binding_proxies.clear();
-
- // Create xkb binding seats for any seats that don't have one yet
- if state.xkb_bindings.is_some() {
- for (_sid, sp) in &mut state.seat_proxies {
- if sp.xkb_bindings_seat.is_none() {
- if let Some(ref xb) = state.xkb_bindings {
- sp.xkb_bindings_seat = Some(xb.get_seat(&sp.river_seat, qhandle, ()));
- }
- }
- }
- }
-
- // Apply xkb bindings to each seat
- let bindings: Vec<_> = state.wm.pending_bindings.drain(..).collect();
- /*
- eprintln!(
- "[bindings] applying {} xkb bindings to {} seats",
- bindings.len(),
- state.seat_proxies.len()
- );
- */
- for pb in &bindings {
- for (sid, sp) in &state.seat_proxies {
- if let Some(ref xb) = state.xkb_bindings {
- if let Some(ref _xbs) = sp.xkb_bindings_seat {
- let modifiers = u32_to_modifiers(pb.mods);
- let binding_data = BindingUserData {
- seat_id: *sid,
- action: pb.action.clone(),
- command: pb.command.clone(),
- };
- let binding = xb.get_xkb_binding(
- &sp.river_seat,
- pb.keysym,
- modifiers,
- qhandle,
- binding_data,
- );
- binding.enable();
- // Store the proxy so it stays alive (binding would be unregistered on drop)
- state.xkb_binding_proxies.push(binding);
- }
- }
- }
- }
-
- // Apply pointer bindings to each seat
- let ptr_bindings: Vec<_> = state.wm.pending_pointer_bindings.drain(..).collect();
- for ppb in &ptr_bindings {
- for (sid, sp) in &state.seat_proxies {
- let modifiers = u32_to_modifiers(ppb.mods);
- let binding_data = BindingUserData {
- seat_id: *sid,
- action: ppb.action.clone(),
- command: None,
- };
- let pb =
- sp.river_seat
- .get_pointer_binding(ppb.button, modifiers, qhandle, binding_data);
- // Store the proxy so it stays alive
- state.pointer_binding_proxies.push(pb);
- }
- }
-}
-
-// --- wlr-output-management dispatch implementations ---
-
-/// Helper: convert an f64 scale value to the wl_fixed_t format
-/// used by the Wayland protocol set_scale method.
-/// wayland-scanner already converts wl_fixed -> f64 for events,
-/// but set_scale takes f64 directly (the scanner handles the conversion).
-fn _f64_to_wl_fixed(v: f64) -> i32 {
- (v * 256.0) as i32
-}
-
-impl Dispatch<ZwlrOutputManagerV1, ()> for AppState {
- event_created_child!(AppState, ZwlrOutputManagerV1, [
- zwlr_output_manager_v1::EVT_HEAD_OPCODE => (ZwlrOutputHeadV1, ()),
- ]);
-
- fn event(
- state: &mut Self,
- _proxy: &ZwlrOutputManagerV1,
- event: zwlr_output_manager_v1::Event,
- _data: &(),
- _conn: &Connection,
- qhandle: &QueueHandle<Self>,
- ) {
- match event {
- zwlr_output_manager_v1::Event::Head { head: _ } => {
- eprintln!("[output-mgmt] Head event (new head advertised)");
- }
- zwlr_output_manager_v1::Event::Done { serial } => {
- eprintln!(
- "[output-mgmt] Done serial={} pending_scale_apply={} output_scale={} heads={}",
- serial,
- state.wm.pending_scale_apply,
- state.wm.output_scale,
- state.output_heads.len()
- );
- for h in &state.output_heads {
- eprintln!(
- "[output-mgmt] head {} enabled={} scale={}",
- h.name, h.enabled, h.scale
- );
- }
- state.output_serial = serial;
-
- // If config requested scale application and we have heads, apply it
- if state.wm.pending_scale_apply && state.wm.output_scale > 0.0 {
- apply_output_scale(state, qhandle);
- }
- }
- zwlr_output_manager_v1::Event::Finished => {
- eprintln!(
- "[output-mgmt] manager finished, heads before clear: {}",
- state.output_heads.len()
- );
- state.output_manager = None;
- }
- _ => {}
- }
- }
-}
-
-impl Dispatch<ZwlrOutputHeadV1, ()> for AppState {
- event_created_child!(AppState, ZwlrOutputHeadV1, [
- zwlr_output_head_v1::EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()),
- ]);
-
- fn event(
- state: &mut Self,
- proxy: &ZwlrOutputHeadV1,
- event: zwlr_output_head_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- // Find or create the head entry by proxy ID
- let pid = proxy.id().protocol_id();
-
- match event {
- zwlr_output_head_v1::Event::Name { name } => {
- // Check if we already track this head
- if let Some(head) = state
- .output_heads
- .iter_mut()
- .find(|h| h.proxy.id().protocol_id() == pid)
- {
- head.name = name.clone();
- } else {
- state.output_heads.push(OutputHeadInfo {
- proxy: proxy.clone(),
- name,
- enabled: false,
- scale: 1.0,
- });
- }
- eprintln!(
- "[output-mgmt] head name={}",
- state
- .output_heads
- .iter()
- .find(|h| h.proxy.id().protocol_id() == pid)
- .map(|h| h.name.as_str())
- .unwrap_or("?")
- );
- }
- zwlr_output_head_v1::Event::Description { description } => {
- let _ = description; // not needed for scale config
- }
- zwlr_output_head_v1::Event::PhysicalSize { width, height } => {
- let _ = (width, height);
- }
- zwlr_output_head_v1::Event::Enabled { enabled } => {
- let head_name = state
- .output_heads
- .iter()
- .find(|h| h.proxy.id().protocol_id() == pid)
- .map(|h| h.name.as_str())
- .unwrap_or("?");
- eprintln!(
- "[output-mgmt] head {} Enabled enabled={} pending_scale_apply={}",
- head_name, enabled, state.wm.pending_scale_apply
- );
- if let Some(head) = state
- .output_heads
- .iter_mut()
- .find(|h| h.proxy.id().protocol_id() == pid)
- {
- head.enabled = enabled != 0;
- }
- }
- zwlr_output_head_v1::Event::Scale { scale } => {
- let head_name = state
- .output_heads
- .iter()
- .find(|h| h.proxy.id().protocol_id() == pid)
- .map(|h| h.name.as_str())
- .unwrap_or("?");
- eprintln!(
- "[output-mgmt] head {} Scale={} output_scale={} pending_scale_apply={}",
- head_name, scale, state.wm.output_scale, state.wm.pending_scale_apply
- );
- // wayland-scanner converts wl_fixed to f64 automatically
- if let Some(head) = state
- .output_heads
- .iter_mut()
- .find(|h| h.proxy.id().protocol_id() == pid)
- {
- // Detect VT-switch-back: compositor reports a scale that
- // differs from our configured output_scale. This covers
- // both: (a) the old head had scale=X and compositor sends 1.0,
- // and (b) the head was re-created after Finished (so its
- // internal scale was reset to 1.0) and compositor sends 1.0
- // instead of the configured scale.
- if state.wm.output_scale > 0.0
- && head.enabled
- && (scale - state.wm.output_scale).abs() > 0.01
- {
- eprintln!(
- "[output-mgmt] scale mismatch (got {}, configured {}), will re-apply",
- scale, state.wm.output_scale
- );
- state.wm.pending_scale_apply = true;
- }
- head.scale = scale;
- }
- }
- zwlr_output_head_v1::Event::Finished => {
- let head_name = state
- .output_heads
- .iter()
- .find(|h| h.proxy.id().protocol_id() == pid)
- .map(|h| h.name.as_str())
- .unwrap_or("?");
- eprintln!(
- "[output-mgmt] head {} Finished (removing from heads)",
- head_name
- );
- state
- .output_heads
- .retain(|h| h.proxy.id().protocol_id() != pid);
- }
- zwlr_output_head_v1::Event::CurrentMode { mode: _ } => {}
- zwlr_output_head_v1::Event::Position { x, y } => {
- let _ = (x, y);
- }
- zwlr_output_head_v1::Event::Transform { transform } => {
- let _ = transform;
- }
- zwlr_output_head_v1::Event::Make { make } => {
- let _ = make;
- }
- zwlr_output_head_v1::Event::Model { model } => {
- let _ = model;
- }
- zwlr_output_head_v1::Event::SerialNumber { serial_number } => {
- let _ = serial_number;
- }
- zwlr_output_head_v1::Event::AdaptiveSync { state: _ } => {}
- _ => {}
- }
- }
-}
-
-impl Dispatch<ZwlrOutputModeV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &ZwlrOutputModeV1,
- event: zwlr_output_mode_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- zwlr_output_mode_v1::Event::Size { width, height } => {
- let _ = (width, height);
- }
- zwlr_output_mode_v1::Event::Refresh { refresh } => {
- let _ = refresh;
- }
- zwlr_output_mode_v1::Event::Preferred => {}
- zwlr_output_mode_v1::Event::Finished => {}
- _ => {}
- }
- }
-}
-
-impl Dispatch<ZwlrOutputConfigurationV1, ()> for AppState {
- event_created_child!(AppState, ZwlrOutputConfigurationV1, [
- zwlr_output_configuration_v1::REQ_ENABLE_HEAD_OPCODE => (ZwlrOutputConfigurationHeadV1, ()),
- ]);
-
- fn event(
- state: &mut Self,
- _proxy: &ZwlrOutputConfigurationV1,
- event: zwlr_output_configuration_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- zwlr_output_configuration_v1::Event::Succeeded => {
- eprintln!("[output-mgmt] configuration succeeded");
- state.output_config = None;
- }
- zwlr_output_configuration_v1::Event::Failed => {
- eprintln!("[output-mgmt] configuration failed");
- state.output_config = None;
- }
- zwlr_output_configuration_v1::Event::Cancelled => {
- eprintln!("[output-mgmt] configuration cancelled (stale serial, will retry)");
- state.output_config = None;
- // Re-queue: the done event will fire again with a fresh serial
- state.wm.pending_scale_apply = true;
- }
- _ => {}
- }
- }
-}
-
-impl Dispatch<ZwlrOutputConfigurationHeadV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &ZwlrOutputConfigurationHeadV1,
- _event: zwlr_output_configuration_head_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- // zwlr_output_configuration_head_v1 has no events — it's request-only
- }
-}
-
-/// Apply the configured output scale via the wlr-output-management protocol.
-/// Called when a `done` event arrives and `pending_scale_apply` is true.
-pub fn apply_output_scale(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
- let Some(ref om) = state.output_manager else {
- eprintln!("[output-mgmt] no output_manager, cannot apply scale");
- return;
- };
-
- if state.output_heads.is_empty() {
- eprintln!("[output-mgmt] no heads discovered yet, deferring scale apply");
- return;
- }
-
- // Check if any enabled head actually needs a scale change
- let target_scale = state.wm.output_scale;
- let needs_change = state
- .output_heads
- .iter()
- .any(|h| h.enabled && (h.scale - target_scale).abs() > 0.01);
- if !needs_change {
- eprintln!(
- "[output-mgmt] all heads already at target scale {}",
- target_scale
- );
- state.wm.pending_scale_apply = false;
- return;
- }
-
- eprintln!(
- "[output-mgmt] applying scale {} to {} head(s)",
- target_scale,
- state.output_heads.len()
- );
-
- let serial = state.output_serial;
-
- // Destroy any existing pending configuration
- if let Some(ref config) = state.output_config {
- config.destroy();
- }
- state.output_config = None;
-
- // Create a new configuration object
- let config = om.create_configuration(serial, qhandle, ());
-
- // For each head: enable it and set the scale
- for head in &state.output_heads {
- if head.enabled {
- let config_head = config.enable_head(&head.proxy, qhandle, ());
- config_head.set_scale(target_scale);
- } else {
- config.disable_head(&head.proxy);
- }
- }
-
- config.apply();
-
- // Store the config proxy so it stays alive until succeeded/failed/cancelled
- state.output_config = Some(config);
- state.wm.pending_scale_apply = false;
-}
-
-/// Connect to the Wayland display and set up the event queue.
-/// Returns the Connection, EventQueue, and AppState after the initial registry roundtrip.
-pub fn wayland_init() -> Result<(Connection, EventQueue<AppState>, AppState), String> {
- let conn = Connection::connect_to_env()
- .map_err(|e| format!("failed to connect to Wayland: {:?}", e))?;
-
- let mut event_queue = conn.new_event_queue::<AppState>();
- let qh = event_queue.handle();
-
- let registry = conn.display().get_registry(&qh, RegistryData);
-
- // Do initial roundtrip to receive global events and bind protocols
- let mut state = AppState::new();
- state.registry = Some(registry);
- eprintln!("[init] first roundtrip starting...");
- let rt1 = std::time::Instant::now();
- event_queue
- .roundtrip(&mut state)
- .map_err(|e| format!("initial roundtrip failed: {:?}", e))?;
- eprintln!(
- "[init] first roundtrip done in {:?}, render_count={}",
- rt1.elapsed(),
- state.render_count
- );
-
- // Check we got the required protocols
- if !state.has_window_manager {
- return Err("river_window_manager_v1 not available".to_string());
- }
- if !state.has_xkb_bindings {
- return Err("river_xkb_bindings_v1 not available".to_string());
- }
-
- // Second roundtrip for input device events
- eprintln!("[init] second roundtrip starting...");
- let rt2 = std::time::Instant::now();
- event_queue
- .roundtrip(&mut state)
- .map_err(|e| format!("second roundtrip failed: {:?}", e))?;
- eprintln!(
- "[init] second roundtrip done in {:?}, render_count={}",
- rt2.elapsed(),
- state.render_count
- );
-
- eprintln!("cce-client: Wayland connection established");
-
- Ok((conn, event_queue, state))
-}
-
-// --- RiverLibinputConfigV1 events ---
-
-impl Dispatch<RiverLibinputConfigV1, ()> for AppState {
- event_created_child!(AppState, RiverLibinputConfigV1, [
- river_libinput_config_v1::EVT_LIBINPUT_DEVICE_OPCODE => (RiverLibinputDeviceV1, ()),
- ]);
-
- fn event(
- state: &mut Self,
- _proxy: &RiverLibinputConfigV1,
- event: river_libinput_config_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- river_libinput_config_v1::Event::LibinputDevice { id: device } => {
- eprintln!("[libinput] device discovered");
- 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;
- }
- river_libinput_config_v1::Event::Finished => {}
- _ => {}
- }
- }
-}
-
-// --- RiverLibinputDeviceV1 events ---
-
-impl Dispatch<RiverLibinputDeviceV1, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &RiverLibinputDeviceV1,
- event: river_libinput_device_v1::Event,
- _data: &(),
- _conn: &Connection,
- 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.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()) {
- dev.tap_finger_count = finger_count;
- eprintln!("[libinput] tap support: {} fingers", finger_count);
- }
- 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::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::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");
- state.libinput_devices.retain(|d| d.device.id().protocol_id() != proxy.id().protocol_id());
- state.wm.tap_config_applied = false;
- }
- _ => {}
- }
- }
-}
-
-// --- RiverLibinputResultV1 events ---
-
-impl Dispatch<RiverLibinputResultV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &RiverLibinputResultV1,
- event: river_libinput_result_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- river_libinput_result_v1::Event::Success => {
- eprintln!("[libinput] config applied successfully");
- }
- river_libinput_result_v1::Event::Unsupported => {
- eprintln!("[libinput] config unsupported by device");
- }
- river_libinput_result_v1::Event::Invalid => {
- eprintln!("[libinput] config invalid");
- }
- }
- }
-}
-
-/// 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 name and support events (if they have a corresponding input_device).
- let all_info_received = state.libinput_devices.iter().all(|d| {
- if d.input_device.is_none() {
- true
- } else {
- 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;
- }
-
- // 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.config_applied {
- continue;
- }
-
- if dev_info.input_device.is_none() {
- dev_info.config_applied = true;
- continue;
- }
-
- 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 {
- 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, ());
- }
-
- // 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, ());
- }
- }
- }
- // 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;
- }
-
- 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] all input configurations applied to all devices");
- }
-}
-
-// --- wl_seat events ---
-
-impl Dispatch<wl_seat::WlSeat, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &wl_seat::WlSeat,
- event: wl_seat::Event,
- _data: &(),
- _conn: &Connection,
- qhandle: &QueueHandle<Self>,
- ) {
- match event {
- wl_seat::Event::Capabilities { capabilities } => {
- let has_pointer = match capabilities {
- wayland_client::WEnum::Value(caps) => caps.contains(wl_seat::Capability::Pointer),
- _ => false,
- };
- if let Some((_, seat_proxy)) = state.seat_proxies.iter_mut().find(|(_, sp)| {
- sp.wl_seat.as_ref() == Some(proxy)
- }) {
- if has_pointer && seat_proxy.wl_pointer.is_none() {
- let wl_pointer = proxy.get_pointer(qhandle, ());
-
- if let Some(ref csm) = state.cursor_shape_manager {
- let device = csm.get_pointer(&wl_pointer, qhandle, ());
- device.set_shape(0, Shape::Default);
- seat_proxy.cursor_shape_device = Some(device);
- }
-
- seat_proxy.wl_pointer = Some(wl_pointer);
- } else if !has_pointer && seat_proxy.wl_pointer.is_some() {
- seat_proxy.cursor_shape_device = None;
- if let Some(pointer) = seat_proxy.wl_pointer.take() {
- pointer.release();
- }
- }
- }
- }
- _ => {}
- }
- }
-}
-
-// --- wl_pointer events ---
-
-impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
- fn event(
- state: &mut Self,
- proxy: &wl_pointer::WlPointer,
- event: wl_pointer::Event,
- _data: &(),
- _conn: &Connection,
- _qhandle: &QueueHandle<Self>,
- ) {
- match event {
- wl_pointer::Event::Enter { serial, surface, surface_x, surface_y } => {
- eprintln!("[pointer] enter surface={:?} x={} y={}", surface, surface_x, surface_y);
- state.pointer_hovered_surface = Some(surface.clone());
- state.last_pointer_surface_x = surface_x;
- state.last_pointer_surface_y = surface_y;
-
- if let Some((_, seat_proxy)) = state.seat_proxies.iter_mut().find(|(_, sp)| {
- sp.wl_pointer.as_ref() == Some(proxy)
- }) {
- seat_proxy.last_pointer_enter_serial = serial;
- }
-
- Self::update_cursor_shape_for_surface(state, proxy);
-
- let is_decoration = state.window_proxies.iter().any(|(_, wp)| {
- wp.decoration.as_ref().map_or(false, |d| &d.surface == &surface)
- || wp.dec_left.as_ref().map_or(false, |d| &d.surface == &surface)
- || wp.dec_right.as_ref().map_or(false, |d| &d.surface == &surface)
- || wp.dec_bottom.as_ref().map_or(false, |d| &d.surface == &surface)
- });
- if is_decoration {
- state.wm.needs_render = true;
- }
-
- if state.wm.expose_active {
- let current_surface = &surface;
- let matched_window = state.window_proxies.iter().find_map(|(id, proxy)| {
- if let Some(dec) = &proxy.decoration {
- if &dec.surface == current_surface {
- return Some(*id);
- }
- }
- if let Some(dec) = &proxy.dec_left {
- if &dec.surface == current_surface {
- return Some(*id);
- }
- }
- if let Some(dec) = &proxy.dec_right {
- if &dec.surface == current_surface {
- return Some(*id);
- }
- }
- if let Some(dec) = &proxy.dec_bottom {
- if &dec.surface == current_surface {
- return Some(*id);
- }
- }
- None
- });
-
- if let Some(wid) = matched_window {
- let seat_id = state.seat_proxies.iter().find_map(|(sid, sp)| {
- if sp.wl_pointer.as_ref() == Some(proxy) {
- Some(*sid)
- } else {
- None
- }
- });
-
- if let Some(sid) = seat_id {
- if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
- let already_focused = seat.focused_window_id == Some(wid);
- seat.focused_window_id = Some(wid);
- state.wm.needs_focus = true;
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
-
- if !already_focused {
- state.manage_dirty();
- }
- }
- }
- }
- }
- }
- wl_pointer::Event::Leave { .. } => {
- eprintln!("[pointer] leave");
- state.pointer_hovered_surface = None;
- Self::update_cursor_shape_for_surface(state, proxy);
- state.wm.needs_render = true;
- }
- wl_pointer::Event::Motion { surface_x, surface_y, .. } => {
- state.last_pointer_surface_x = surface_x;
- state.last_pointer_surface_y = surface_y;
-
- Self::update_cursor_shape_for_surface(state, proxy);
-
- if let Some(ref current_surface) = state.pointer_hovered_surface {
- let is_decoration = state.window_proxies.iter().any(|(_, wp)| {
- wp.decoration.as_ref().map_or(false, |d| &d.surface == current_surface)
- || wp.dec_left.as_ref().map_or(false, |d| &d.surface == current_surface)
- || wp.dec_right.as_ref().map_or(false, |d| &d.surface == current_surface)
- || wp.dec_bottom.as_ref().map_or(false, |d| &d.surface == current_surface)
- });
- if is_decoration {
- state.wm.needs_render = true;
- }
- }
-
- if let Some(pending) = state.pending_border_drag.clone() {
- let dx = surface_x - pending.start_surface_x;
- let dy = surface_y - pending.start_surface_y;
- let dist = (dx * dx + dy * dy).sqrt();
- // Threshold of 4.0 logical pixels to distinguish click vs drag
- if dist > 4.0 {
- state.pending_border_drag = None;
-
- // Start the drag! Set the window to Floating and lock it.
- let mut needs_render = false;
- let side_panel_pos = state.wm.layout.side_panel_position.clone();
- if let Some(win) = state.wm.get_window_mut(pending.window_id) {
- if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
- let is_valid_side_panel_resize = win.tiling_mode == TilingMode::SidePanel
- && if side_panel_pos == "right" {
- matches!(
- pending.op_type,
- PointerOpType::Resize
- | PointerOpType::ResizeLeft
- | PointerOpType::ResizeTopLeft
- | PointerOpType::ResizeBottomLeft
- )
- } else {
- matches!(
- pending.op_type,
- PointerOpType::
diff truncated