Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Implement clear-inspector-v1 protocol and add inspector CLI client
Cargo.lock | 26 +--
Cargo.toml | 8 +-
protocol/clear-inspector-v1.xml | 52 +++++
protocol/river-window-management-v1.xml | 9 +
src/borders.rs | 6 +-
src/clearctl.rs | 25 ++-
src/config.rs | 95 +++++----
src/decorations.rs | 14 +-
src/input.rs | 146 +++++++++++---
src/inspector_cli.rs | 140 +++++++++++++
src/ipc.rs | 346 ++++++++++++++++++++++++++------
src/ipc_server.rs | 37 +++-
src/lib.rs | 2 +-
src/main.rs | 103 ++++++----
src/paths.rs | 44 ++--
src/protocol.rs | 4 +
src/restart.rs | 31 +--
src/state.rs | 21 +-
src/status.rs | 10 +-
src/status_server.rs | 8 +-
src/tiling.rs | 182 +----------------
src/types.rs | 40 ++--
src/wayland.rs | 312 ++++++++++++++++++----------
src/wm.rs | 263 +++++++++++++++++++++---
start-river.sh | 20 +-
25 files changed, 1331 insertions(+), 613 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2f3cfd6..83920d2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -31,19 +31,7 @@ dependencies = [
]
[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "cfg_aliases"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
-
-[[package]]
-name = "clearwm"
+name = "ccec"
version = "0.1.0"
dependencies = [
"bitflags",
@@ -61,6 +49,18 @@ dependencies = [
"xkbcommon",
]
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
[[package]]
name = "downcast-rs"
version = "1.2.1"
diff --git a/Cargo.toml b/Cargo.toml
index 413bd84..5b2aa02 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,16 +1,20 @@
[package]
-name = "clearwm"
+name = "ccec"
version = "0.1.0"
edition = "2021"
[[bin]]
-name = "clearwm"
+name = "ccec"
path = "src/main.rs"
[[bin]]
name = "clearctl"
path = "src/clearctl.rs"
+[[bin]]
+name = "clear-inspector"
+path = "src/inspector_cli.rs"
+
[dependencies]
wayland-client = "0.31"
wayland-backend = "0.3"
diff --git a/protocol/clear-inspector-v1.xml b/protocol/clear-inspector-v1.xml
new file mode 100644
index 0000000..b321965
--- /dev/null
+++ b/protocol/clear-inspector-v1.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="clear_inspector_v1">
+ <copyright>
+ Copyright © 2026 Antigravity
+ </copyright>
+
+ <interface name="zclear_inspector_v1" version="1">
+ <description summary="query information about client surface states and layouts">
+ This interface allows clear-ui client applications to register their surfaces
+ and publish their internal widget trees and state to the compositor.
+ It also allows inspector tools to request the list of active surfaces and their states.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the inspector object"/>
+ </request>
+
+ <request name="register_client">
+ <description summary="register a wl_surface as a clear-ui client window">
+ Associate a client wl_surface with this inspector.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ </request>
+
+ <request name="update_state">
+ <description summary="update the published state of a registered client surface">
+ The state is serialized as a JSON string containing widget layout and state data.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ <arg name="state" type="string" summary="JSON serialized state of the application"/>
+ </request>
+
+ <request name="get_inspected_surfaces">
+ <description summary="request the list of all registered surfaces from the compositor"/>
+ </request>
+
+ <event name="inspected_surface">
+ <description summary="reports a registered surface's state and absolute coordinates"/>
+ <arg name="title" type="string" summary="window title"/>
+ <arg name="app_id" type="string" summary="application ID"/>
+ <arg name="x" type="int" summary="absolute screen X coordinate"/>
+ <arg name="y" type="int" summary="absolute screen Y coordinate"/>
+ <arg name="width" type="int" summary="surface width"/>
+ <arg name="height" type="int" summary="surface height"/>
+ <arg name="state" type="string" summary="JSON state of the application"/>
+ </event>
+
+ <event name="inspected_surface_done">
+ <description summary="indicates all inspected surfaces have been reported"/>
+ </event>
+ </interface>
+</protocol>
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index f835971..32d6db3 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1121,6 +1121,15 @@
<arg name="max_width" type="int" summary="maximum width"/>
<arg name="max_height" type="int" summary="maximum height"/>
</request>
+
+ <request name="set_opacity" since="4">
+ <description summary="set the window opacity">
+ Set the window opacity, from 0 (fully transparent) to 0xffffffff (fully opaque).
+ This request modifies rendering state and may only be made as part of a
+ render sequence, see the river_window_manager_v1 description.
+ </description>
+ <arg name="opacity" type="uint" summary="opacity value from 0 to 0xffffffff"/>
+ </request>
</interface>
<interface name="river_decoration_v1" version="4">
diff --git a/src/borders.rs b/src/borders.rs
index 4f916c5..53d0657 100644
--- a/src/borders.rs
+++ b/src/borders.rs
@@ -100,7 +100,7 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
let visible_mode: Vec<usize> = visible
.iter()
.cloned()
- .filter(|&i| state.expose_active || state.windows[i].tiling_mode == win.tiling_mode)
+ .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);
@@ -115,15 +115,13 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
(r, g, b, ALPHA)
};
- let mut width = if state.expose_active && win.tiling_mode != TilingMode::Popup {
+ let mut width = if state.expose_visual_active && win.tiling_mode != TilingMode::Popup {
state.layout.grid_border_width
} else {
match win.tiling_mode {
TilingMode::Cascade => state.layout.cascade_border_width,
TilingMode::Fullscreen => state.layout.fullscreen_border_width,
TilingMode::Grid => state.layout.grid_border_width,
- TilingMode::Vsplit => state.layout.vsplit_border_width,
- TilingMode::Hsplit => state.layout.hsplit_border_width,
TilingMode::Floating => state.layout.floating_border_width,
TilingMode::Popup => state.layout.border_width,
}
diff --git a/src/clearctl.rs b/src/clearctl.rs
index ede805a..4129218 100644
--- a/src/clearctl.rs
+++ b/src/clearctl.rs
@@ -1,4 +1,4 @@
-// clearctl — IPC client for clearwm
+// clearctl — IPC client for ccec
use std::env;
use std::fs;
@@ -8,15 +8,15 @@ use std::process;
fn get_socket_path() -> String {
match env::var("WAYLAND_DISPLAY") {
- Ok(display) => format!("/tmp/clearwm-{}.sock", display),
- Err(_) => "/tmp/clearwm.sock".to_string(),
+ Ok(display) => format!("/tmp/ccec-{}.sock", display),
+ Err(_) => "/tmp/ccec.sock".to_string(),
}
}
fn get_windows_path() -> String {
match env::var("WAYLAND_DISPLAY") {
- Ok(display) => format!("/tmp/clearwm-windows-{}", display),
- Err(_) => "/tmp/clearwm-windows".to_string(),
+ Ok(display) => format!("/tmp/ccec-windows-{}", display),
+ Err(_) => "/tmp/ccec-windows".to_string(),
}
}
@@ -49,8 +49,17 @@ fn usage(name: &str, to_stderr: bool) {
print(" pbind <mods> <button> <action>");
print(" retile");
print(" set-tag <1-4>");
- print(" mode <cascade|grid|vsplit|hsplit|fullscreen|floating|popup> <app_id> [title]");
- print(" tag-layout <1-4> <cascade|grid|vsplit|hsplit|fullscreen|floating|popup>");
+ print(" mode <cascade|grid|fullscreen|floating|popup> <app_id> [title]");
+ print(" tag-layout <1-4> <cascade|grid|fullscreen|floating|popup>");
+ print(" pointer-location");
+ print(" pointer-move-to <x> <y>");
+ print(" pointer-move-by <dx> <dy>");
+ print(" pointer-click <button>");
+ print(" pointer-press <button>");
+ print(" pointer-release <button>");
+ print(" keypress <key>");
+ print(" key-press <key>");
+ print(" key-release <key>");
}
fn main() {
@@ -69,7 +78,7 @@ fn main() {
if args[1] == "windows" {
match fs::read_to_string(get_windows_path()) {
Ok(content) => print!("{}", content),
- Err(_) => eprintln!("No windows info (clearwm may not be running)"),
+ Err(_) => eprintln!("No windows info (ccec may not be running)"),
}
return;
}
diff --git a/src/config.rs b/src/config.rs
index 78a8ba8..1d6690b 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,4 @@
-// TOML config parsing for clearwm
+// TOML config parsing for ccec
use serde::Deserialize;
use std::collections::HashMap;
@@ -68,10 +68,6 @@ pub struct LayoutConfig {
pub cascade_border_width: i64,
#[serde(default = "default_grid_border_width")]
pub grid_border_width: i64,
- #[serde(default = "default_vsplit_border_width")]
- pub vsplit_border_width: i64,
- #[serde(default = "default_hsplit_border_width")]
- pub hsplit_border_width: i64,
#[serde(default = "default_floating_border_width")]
pub floating_border_width: i64,
#[serde(default = "default_border_color", alias = "high_color")]
@@ -80,6 +76,8 @@ pub struct LayoutConfig {
pub background_color: String,
#[serde(default = "default_border_font_size")]
pub border_font_size: i64,
+ #[serde(default = "default_transition_duration")]
+ pub transition_duration: i64,
}
impl Default for LayoutConfig {
@@ -96,12 +94,11 @@ impl Default for LayoutConfig {
fullscreen_border_width: default_fullscreen_border_width(),
cascade_border_width: default_cascade_border_width(),
grid_border_width: default_grid_border_width(),
- vsplit_border_width: default_vsplit_border_width(),
- hsplit_border_width: default_hsplit_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(),
}
}
}
@@ -139,12 +136,6 @@ fn default_cascade_border_width() -> i64 {
fn default_grid_border_width() -> i64 {
6
}
-fn default_vsplit_border_width() -> i64 {
- 6
-}
-fn default_hsplit_border_width() -> i64 {
- 6
-}
fn default_floating_border_width() -> i64 {
6
}
@@ -157,6 +148,9 @@ fn default_background_color() -> String {
fn default_border_font_size() -> i64 {
11
}
+fn default_transition_duration() -> i64 {
+ 300
+}
#[derive(Debug, Deserialize, Default)]
pub struct OutputConfig {
@@ -303,10 +297,9 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
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.vsplit_border_width = config.layout.vsplit_border_width as i32;
- state.layout.hsplit_border_width = config.layout.hsplit_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;
if let Some((r, g, b, a)) = parse_hex_color(&config.layout.border_color) {
state.layout.border_r = r;
state.layout.border_g = g;
@@ -421,6 +414,15 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
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);
@@ -483,7 +485,7 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
}
});
if let Some(ref controller) = state.input_controller {
- let _ = controller.send((inertial_cfg, config.input.tap_to_click));
+ let _ = controller.send(crate::input::InputDaemonMsg::UpdateConfig(inertial_cfg, config.input.tap_to_click));
}
// [notifications]
@@ -571,22 +573,43 @@ pub fn parse_keysym(key_str: &str) -> u32 {
///
/// Closes all inherited FDs > 2 in the child via pre_exec so that
/// spawned Wayland clients (fuzzel, foot, etc.) never accidentally
-/// read from clearwm's Wayland socket fd. Also redirects stdout/stderr
-/// to /dev/null so child output doesn't pollute clearwm's log, and
-/// calls setsid() to detach from clearwm's process group.
+/// read from ccec's Wayland socket fd. Also redirects stdout/stderr
+/// to /dev/null so child output doesn't pollute ccec's log, and
+/// calls setsid() to detach from ccec'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/ccec-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/ccec-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(std::process::Stdio::null())
- .stderr(std::process::Stdio::null())
+ .stdout(stdout_cfg)
+ .stderr(stderr_cfg)
.pre_exec(|| {
// Close all inherited FDs > 2 to prevent the child from
- // accidentally reading clearwm's Wayland socket or status
+ // accidentally reading ccec'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 {
@@ -615,7 +638,7 @@ pub fn process_running(name: &str) -> bool {
pub fn show_notification(title: &str, body: &str) {
let title_escaped = title.replace('\'', "'\\''");
let body_escaped = body.replace('\'', "'\\''");
- let cmd = format!("notify-send -a clearwm '{}' '{}'", title_escaped, body_escaped);
+ let cmd = format!("notify-send -a ccec '{}' '{}'", title_escaped, body_escaped);
spawn_command_bg(&cmd);
}
@@ -652,31 +675,31 @@ mod tests {
#[test]
fn test_expand_env_vars_simple() {
- std::env::set_var("CLEARWM_TEST_VAR", "hello");
- assert_eq!(expand_env_vars("$CLEARWM_TEST_VAR"), "hello");
- std::env::remove_var("CLEARWM_TEST_VAR");
+ std::env::set_var("CCEC_TEST_VAR", "hello");
+ assert_eq!(expand_env_vars("$CCEC_TEST_VAR"), "hello");
+ std::env::remove_var("CCEC_TEST_VAR");
}
#[test]
fn test_expand_env_vars_braces() {
- std::env::set_var("CLEARWM_TEST_VAR", "world");
- assert_eq!(expand_env_vars("${CLEARWM_TEST_VAR}!"), "world!");
- std::env::remove_var("CLEARWM_TEST_VAR");
+ std::env::set_var("CCEC_TEST_VAR", "world");
+ assert_eq!(expand_env_vars("${CCEC_TEST_VAR}!"), "world!");
+ std::env::remove_var("CCEC_TEST_VAR");
}
#[test]
fn test_expand_env_vars_mid_string() {
- std::env::set_var("CLEARWM_TEST_HOME", "/home/user");
+ std::env::set_var("CCEC_TEST_HOME", "/home/user");
assert_eq!(
- expand_env_vars("$CLEARWM_TEST_HOME/.local/bin:$CLEARWM_TEST_HOME/bin"),
+ expand_env_vars("$CCEC_TEST_HOME/.local/bin:$CCEC_TEST_HOME/bin"),
"/home/user/.local/bin:/home/user/bin"
);
- std::env::remove_var("CLEARWM_TEST_HOME");
+ std::env::remove_var("CCEC_TEST_HOME");
}
#[test]
fn test_expand_env_vars_unset() {
- assert_eq!(expand_env_vars("$CLEARWM_NONEXISTENT_VAR"), "");
+ assert_eq!(expand_env_vars("$CCEC_NONEXISTENT_VAR"), "");
}
#[test]
@@ -770,9 +793,9 @@ exec = "echo reloaded"
assert_eq!(config.reload[0].exec, "pkill clear-input-daemon");
assert_eq!(config.reload[1].exec, "echo reloaded");
- // Also test integration via parse_config (we can write to a temporary file in /tmp or mock it,
- // but wait! we can write to a temporary file inside the workspace)
- let temp_path = "/home/lsgalante/Dropbox/Clear/clear-window-manager/scratch_config_test.toml";
+ // 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();
diff --git a/src/decorations.rs b/src/decorations.rs
index 5219988..ec48de7 100644
--- a/src/decorations.rs
+++ b/src/decorations.rs
@@ -1,4 +1,4 @@
-// Wayland decoration surface drawing and management for clearwm
+// Wayland decoration surface drawing and management for ccec
use std::ffi::CString;
use std::os::fd::RawFd;
@@ -188,7 +188,7 @@ fn draw_char(
/// Create a temporary shared memory file descriptor.
fn create_memfd(size: usize) -> Option<RawFd> {
- let name = CString::new("clearwm-decoration").ok()?;
+ let name = CString::new("ccec-decoration").ok()?;
let fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
if fd < 0 {
return None;
@@ -381,7 +381,7 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
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_active && w.tiling_mode != crate::types::TilingMode::Popup {
+ let mode_idx = if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
state.wm.windows
.iter()
.filter(|win| !win.closed && win.app_id.as_deref() != Some("clear-status-interface") && win.tiling_mode != crate::types::TilingMode::Popup && (win.tags & active_tags) != 0)
@@ -394,15 +394,13 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
.position(|win| win.id == w.id)
.unwrap_or(0)
};
- let indicator = if state.wm.expose_active && w.tiling_mode != crate::types::TilingMode::Popup {
+ let indicator = 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::Vsplit => "V",
- crate::types::TilingMode::Hsplit => "H",
crate::types::TilingMode::Fullscreen => "S",
crate::types::TilingMode::Popup => "P",
}
@@ -428,15 +426,13 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
let bg_color = ((border_a as u32) << 24) | ((border_r as u32) << 16) | ((border_g as u32) << 8) | (border_b as u32);
// Border width is mode-specific
- let border_w = if state.wm.expose_active && w.tiling_mode != crate::types::TilingMode::Popup {
+ let border_w = 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::Vsplit => state.wm.layout.vsplit_border_width,
- crate::types::TilingMode::Hsplit => state.wm.layout.hsplit_border_width,
crate::types::TilingMode::Floating => state.wm.layout.floating_border_width,
crate::types::TilingMode::Popup => 0,
}
diff --git a/src/input.rs b/src/input.rs
index 53b5887..4d4656d 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -4,16 +4,36 @@ 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);
+}
+
// 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;
@@ -86,14 +106,26 @@ fn eviocgabs(abs: u32) -> libc::c_ulong {
const ABS_MT_SLOT: u16 = 0x2f;
+#[derive(Debug, Clone)]
+pub enum InputDaemonMsg {
+ UpdateConfig(InertialConfig, bool),
+ SimulateMove { dx: i32, dy: i32 },
+ SimulateButton { button: u16, press: bool },
+ SimulateKey { keycode: u16, press: bool },
+ SimulateClick { button: u16 },
+ SimulateKeyPress { keycode: u16 },
+}
+
#[derive(Debug)]
enum CoordinatorMsg {
PhysicalMove { dx: i32, dy: i32, timestamp: Instant },
PhysicalTrackpadMove { dx: i32, dy: i32, timestamp: Instant },
PhysicalTrackpadLift { timestamp: Instant },
PhysicalScroll { dwx: i32, dwy: i32, timestamp: Instant },
- UpdateConfig((InertialConfig, bool)),
FingersReport(Vec<FingerState>),
+ DaemonMsg(InputDaemonMsg),
+ InternalReleaseButton { button: u16 },
+ InternalReleaseKey { keycode: u16 },
}
fn setup_uinput() -> std::io::Result<std::fs::File> {
@@ -120,6 +152,19 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
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 {
@@ -378,57 +423,62 @@ struct PhysicsState {
three_finger_gesture_triggered: bool,
}
-fn trigger_tap_to_click_ipc(tap: bool, ipc_tx: &std::sync::mpsc::Sender<String>, pipe_write: libc::c_int) {
+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);
- let _ = ipc_tx.send(cmd);
+ 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<String>, pipe_write: libc::c_int) {
+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);
- let _ = ipc_tx.send(cmd);
+ 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<String>, pipe_write: libc::c_int) {
+fn trigger_expose_ipc(ipc_tx: &std::sync::mpsc::Sender<crate::ipc_server::IpcRequest>, pipe_write: libc::c_int) {
let cmd = "expose".to_string();
- let _ = ipc_tx.send(cmd);
+ 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<String>, pipe_write: libc::c_int) {
+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();
- let _ = ipc_tx.send(cmd);
+ 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<String>, pipe_write: libc::c_int) {
+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();
- let _ = ipc_tx.send(cmd);
+ 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<String>, pipe_write: libc::c_int) {
+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();
- let _ = ipc_tx.send(cmd);
+ 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<(InertialConfig, bool)>,
- ipc_tx: std::sync::mpsc::Sender<String>,
+ 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()
@@ -472,8 +522,8 @@ pub fn run_input_daemon(
// Wait for initial config
let (initial_config, tap_to_click) = match event_queue_rx.recv().await {
- Some(res) => res,
- None => {
+ Some(InputDaemonMsg::UpdateConfig(res, tap)) => (res, tap),
+ _ => {
return Err(Box::from("Failed to receive initial input configuration") as Box<dyn std::error::Error>);
}
};
@@ -481,8 +531,8 @@ pub fn run_input_daemon(
// Config listener task
let tx_config = tx.clone();
tokio::spawn(async move {
- while let Some((config, tap)) = event_queue_rx.recv().await {
- let _ = tx_config.send(CoordinatorMsg::UpdateConfig((config, tap)));
+ while let Some(msg) = event_queue_rx.recv().await {
+ let _ = tx_config.send(CoordinatorMsg::DaemonMsg(msg));
}
});
@@ -559,14 +609,59 @@ pub fn run_input_daemon(
tokio::select! {
Some(msg) = rx.recv() => {
match msg {
- CoordinatorMsg::UpdateConfig((cfg, tap)) => {
- state.config = cfg;
- if state.tap_to_click != tap {
- state.tap_to_click = tap;
- trigger_tap_to_click_ipc(tap, &ipc_tx, pipe_write);
+ CoordinatorMsg::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::SimulateMove { dx, dy } => {
+ let _ = write_mouse_move(&mut uinput_file, dx, dy);
+ update_pointer_coords(dx, dy);
+ }
+ InputDaemonMsg::SimulateButton { button, press } => {
+ let val = if press { 1 } else { 0 };
+ let _ = write_raw_event(&mut uinput_file, EV_KEY, button, val);
+ let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ }
+ InputDaemonMsg::SimulateKey { keycode, press } => {
+ let val = if press { 1 } else { 0 };
+ let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, val);
+ let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ }
+ InputDaemonMsg::SimulateClick { button } => {
+ let _ = write_raw_event(&mut uinput_file, EV_KEY, button, 1);
+ let _ = write_raw_event(&mut uinput_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_file, EV_KEY, keycode, 1);
+ let _ = write_raw_event(&mut uinput_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 });
+ });
+ }
}
}
+ CoordinatorMsg::InternalReleaseButton { button } => {
+ let _ = write_raw_event(&mut uinput_file, EV_KEY, button, 0);
+ let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ }
+ CoordinatorMsg::InternalReleaseKey { keycode } => {
+ let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, 0);
+ let _ = write_raw_event(&mut uinput_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;
@@ -586,6 +681,7 @@ pub fn run_input_daemon(
}
}
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;
@@ -739,6 +835,7 @@ pub fn run_input_daemon(
if steps_x != 0 || steps_y != 0 {
let _ = write_mouse_move(&mut uinput_file, steps_x, steps_y);
+ update_pointer_coords(steps_x, steps_y);
}
}
}
@@ -764,6 +861,7 @@ pub fn run_input_daemon(
if steps_x != 0 || steps_y != 0 {
let _ = write_mouse_move(&mut uinput_file, steps_x, steps_y);
+ update_pointer_coords(steps_x, steps_y);
}
}
}
diff --git a/src/inspector_cli.rs b/src/inspector_cli.rs
new file mode 100644
index 0000000..47bb034
--- /dev/null
+++ b/src/inspector_cli.rs
@@ -0,0 +1,140 @@
+use wayland_client::{
+ globals::{registry_queue_init, GlobalListContents},
+ protocol::wl_registry,
+ Connection, Dispatch, QueueHandle,
+};
+use serde_json::Value;
+
+// Import generated client protocols from ccec crate
+mod protocol;
+use 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 {
+ 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>,
+ ) {
+ match event {
+ zclear_inspector_v1::Event::InspectedSurface { title, app_id, x, y, width, height, state: 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 main() {
+ 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(),
+ };
+
+ // Populate registry globals
+ event_queue.roundtrip(&mut state).unwrap();
+
+ let inspector = match state.inspector.take() {
+ Some(ins) => ins,
+ None => {
+ eprintln!("Error: zclear_inspector_v1 global protocol not found on Wayland registry.");
+ 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();
+ }
+
+ // 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!(" Widget 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/ipc.rs b/src/ipc.rs
index 7326bdc..5cd0b12 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -1,19 +1,57 @@
-// IPC command parser for clearwm
-// Ported from handle_ipc_command in clearwm.c
+// IPC command parser for ccec
+// Ported from handle_ipc_command in ccec.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,
+ 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("clear-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) {
+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;
+ return "\n".to_string();
}
// Split the command into tokens
@@ -21,6 +59,8 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) {
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() {
@@ -76,25 +116,32 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) {
crate::restart::wm_restart();
}
"expose" => {
- state.expose_active = !state.expose_active;
+ 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 {
- state.expose_active = false;
- let mut windows_to_focus = Vec::new();
- for seat in &state.seats {
- if seat.removed {
- continue;
- }
- if let Some(wid) = seat.hovered_window_id {
- windows_to_focus.push((seat.id, wid));
- }
- }
- for (seat_id, wid) in windows_to_focus {
- if let Some(seat) = state.seats.iter_mut().find(|s| s.id == seat_id) {
- seat.focused_window_id = Some(wid);
+ 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;
@@ -237,14 +284,110 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) {
}
} else if parts.len() == 1 && !parts[0].is_empty() {
if state.notifications_enable {
- crate::config::show_notification("clearwm", parts[0]);
+ crate::config::show_notification("ccec", parts[0]);
+ }
+ }
+ }
+ "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>()) {
+ let cur_x = crate::input::POINTER_X.load(std::sync::atomic::Ordering::SeqCst);
+ let cur_y = crate::input::POINTER_Y.load(std::sync::atomic::Ordering::SeqCst);
+ let dx = target_x - cur_x;
+ let dy = target_y - cur_y;
+ if let Some(ref controller) = state.input_controller {
+ let _ = controller.send(crate::input::InputDaemonMsg::SimulateMove { dx, dy });
+ }
+ } 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::SimulateMove { dx, dy });
+ }
+ } else {
+ reply = "error: invalid deltas\n".to_string();
}
+ } else {
+ reply = "error: usage: pointer-move-by <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();
}
}
_ => {
- // Unknown command, ignore
+ reply = "error: unknown command\n".to_string();
}
}
+
+ reply
}
/// Parse a tag number from a command like "view-1" or "view 1"
@@ -276,7 +419,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.gap = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Gap set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Gap set to {}px", value));
}
}
}
@@ -284,7 +427,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.gap_top = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Top gap set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Top gap set to {}px", value));
}
}
}
@@ -292,7 +435,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.gap_left = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Left gap set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Left gap set to {}px", value));
}
}
}
@@ -300,7 +443,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.gap_right = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Right gap set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Right gap set to {}px", value));
}
}
}
@@ -308,7 +451,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.gap_bottom = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Bottom gap set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Bottom gap set to {}px", value));
}
}
}
@@ -316,7 +459,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.cascade_offset = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Cascade offset set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Cascade offset set to {}px", value));
}
}
}
@@ -324,7 +467,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.bar_height = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Bar height set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Bar height set to {}px", value));
}
}
}
@@ -332,7 +475,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.border_width = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Border width set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Border width set to {}px", value));
}
}
}
@@ -340,7 +483,15 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.border_font_size = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Border font size set to {}px", value));
+ crate::config::show_notification("ccec", &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("ccec", &format!("Transition duration set to {}ms", value));
}
}
}
@@ -348,7 +499,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.fullscreen_border_width = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Fullscreen border width set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Fullscreen border width set to {}px", value));
}
}
}
@@ -356,7 +507,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.cascade_border_width = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Cascade border width set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Cascade border width set to {}px", value));
}
}
}
@@ -364,23 +515,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.grid_border_width = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Grid border width set to {}px", value));
- }
- }
- }
- "vsplit_border_width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.vsplit_border_width = value;
- if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Vsplit border width set to {}px", value));
- }
- }
- }
- "hsplit_border_width" => {
- if let Ok(value) = value_str.parse::<i32>() {
- state.layout.hsplit_border_width = value;
- if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Hsplit border width set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Grid border width set to {}px", value));
}
}
}
@@ -388,7 +523,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
if let Ok(value) = value_str.parse::<i32>() {
state.layout.floating_border_width = value;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Floating border width set to {}px", value));
+ crate::config::show_notification("ccec", &format!("Floating border width set to {}px", value));
}
}
}
@@ -399,7 +534,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
state.layout.border_b = b;
state.layout.border_a = a;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Border color set to {}", value_str));
+ crate::config::show_notification("ccec", &format!("Border color set to {}", value_str));
}
}
}
@@ -410,7 +545,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
state.layout.background_b = b;
state.layout.background_a = a;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Background color set to {}", value_str));
+ crate::config::show_notification("ccec", &format!("Background color set to {}", value_str));
}
}
}
@@ -474,7 +609,7 @@ fn handle_set_mode_command(rest: &str, state: &mut WindowManager) {
window.mode_locked = true;
if notifications_enable {
let win_title = window.title.as_deref().unwrap_or("Window");
- crate::config::show_notification("clearwm", &format!("Tiling mode set to {} for: {}", mode.as_str(), win_title));
+ crate::config::show_notification("ccec", &format!("Tiling mode set to {} for: {}", mode.as_str(), win_title));
}
state.needs_render = true;
state.needs_status_update = true;
@@ -551,7 +686,7 @@ fn handle_tag_layout_command(rest: &str, state: &mut WindowManager) {
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("clearwm", &format!("Tag {} layout set to {}", tag, mode.as_str()));
+ crate::config::show_notification("ccec", &format!("Tag {} layout set to {}", tag, mode.as_str()));
}
state.needs_render = true;
state.needs_status_update = true;
@@ -632,7 +767,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
}
if state.tap_to_click != old_val && state.notifications_enable {
crate::config::show_notification(
- "clearwm",
+ "ccec",
&format!(
"Tap-to-click {}",
if state.tap_to_click { "enabled" } else { "disabled" }
@@ -645,7 +780,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.accel_speed = Some(val);
state.tap_config_applied = false;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Acceleration speed set to {}", val));
+ crate::config::show_notification("ccec", &format!("Acceleration speed set to {}", val));
}
}
}
@@ -655,7 +790,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.accel_profile = Some(val.clone());
state.tap_config_applied = false;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Acceleration profile set to {}", val));
+ crate::config::show_notification("ccec", &format!("Acceleration profile set to {}", val));
}
}
}
@@ -677,7 +812,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.tap_config_applied = false;
if state.notifications_enable {
crate::config::show_notification(
- "clearwm",
+ "ccec",
&format!(
"Natural scroll {}",
if state.natural_scroll.unwrap_or(false) { "enabled" } else { "disabled" }
@@ -704,7 +839,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.tap_config_applied = false;
if state.notifications_enable {
crate::config::show_notification(
- "clearwm",
+ "ccec",
&format!(
"Disable-while-typing {}",
if state.dwt.unwrap_or(false) { "enabled" } else { "disabled" }
@@ -731,7 +866,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.tap_config_applied = false;
if state.notifications_enable {
crate::config::show_notification(
- "clearwm",
+ "ccec",
&format!(
"Disable-while-trackpointing {}",
if state.dwtp.unwrap_or(false) { "enabled" } else { "disabled" }
@@ -760,7 +895,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.trackpoint_accel_speed = Some(val);
state.tap_config_applied = false;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Trackpoint acceleration speed set to {}", val));
+ crate::config::show_notification("ccec", &format!("Trackpoint acceleration speed set to {}", val));
}
}
}
@@ -770,7 +905,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.trackpoint_accel_profile = Some(val.clone());
state.tap_config_applied = false;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Trackpoint acceleration profile set to {}", val));
+ crate::config::show_notification("ccec", &format!("Trackpoint acceleration profile set to {}", val));
}
}
}
@@ -780,7 +915,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.cursor_theme = Some(val.clone());
state.cursor_theme_applied = false;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Cursor theme set to {}", val));
+ crate::config::show_notification("ccec", &format!("Cursor theme set to {}", val));
}
}
}
@@ -789,7 +924,7 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
state.cursor_size = Some(val);
state.cursor_theme_applied = false;
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Cursor size set to {}", val));
+ crate::config::show_notification("ccec", &format!("Cursor size set to {}", val));
}
}
}
@@ -797,6 +932,50 @@ fn handle_input_command(rest: &str, state: &mut WindowManager) {
}
}
+
+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::*;
@@ -1037,4 +1216,43 @@ mod tests {
handle_ipc_command("view-prev", &mut state);
assert_eq!(state.active_tags, 1); // Tag 1
}
+
+ #[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/ipc_server.rs b/src/ipc_server.rs
index 2ad5b6d..caece55 100644
--- a/src/ipc_server.rs
+++ b/src/ipc_server.rs
@@ -4,17 +4,22 @@ 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<String>,
- pub tx: mpsc::Sender<String>,
+ 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::<String>();
+ let (tx, rx) = mpsc::channel::<IpcRequest>();
let tx_clone = tx.clone();
std::thread::Builder::new()
- .name("clearwm-ipc".into())
+ .name("ccec-ipc".into())
.spawn(move || {
ipc_server_main(tx, pipe_write);
})
@@ -23,7 +28,7 @@ pub fn spawn_ipc_server(pipe_write: libc::c_int) -> IpcReceiver {
IpcReceiver { rx, tx: tx_clone }
}
-fn ipc_server_main(tx: mpsc::Sender<String>, pipe_write: libc::c_int) {
+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);
@@ -75,19 +80,35 @@ fn ipc_server_main(tx: mpsc::Sender<String>, pipe_write: libc::c_int) {
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(cmd);
+ let _ = tx.send(IpcRequest {
+ command: cmd,
+ reply_tx: reply_tx.clone(),
+ });
sent = true;
+ cmd_count += 1;
}
}
if sent {
- let _ = stream.write_all(b"ok\n");
- dead.push(i);
+ // 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;
}
diff --git a/src/lib.rs b/src/lib.rs
index 8d13552..df105d1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
-// clearwm — Wayland window manager for river, written in Rust
+// ccec — Wayland window manager for river, written in Rust
pub mod borders;
pub mod config;
diff --git a/src/main.rs b/src/main.rs
index 83c813d..af90874 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,18 +1,18 @@
-// clearwm — Wayland window manager for river
-
-use clearwm::config::parse_config;
-use clearwm::ipc;
-use clearwm::ipc_server;
-use clearwm::restart;
-use clearwm::status_server;
-use clearwm::wayland::wayland_init;
+// ccec — Wayland window manager for river
+
+use ccec::config::parse_config;
+use ccec::ipc;
+use ccec::ipc_server;
+use ccec::restart;
+use ccec::status_server;
+use ccec::wayland::wayland_init;
use std::env;
use std::fs;
-use clearwm::paths;
+use ccec::paths;
-/// Write a crash/exit trace to /tmp/clearwm-death.log so we can diagnose
-/// why clearwm dies even when the normal log gets overwritten on restart.
+/// Write a crash/exit trace to /tmp/ccec-death.log so we can diagnose
+/// why ccec 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()
@@ -40,7 +40,7 @@ fn main() {
}
}));
- eprintln!("clearwm starting...");
+ eprintln!("ccec starting...");
// Start the status socket server thread (for waybar integration)
let status_sender = status_server::spawn_status_server();
@@ -66,7 +66,7 @@ fn main() {
}
// Connect to Wayland display and get initial state.
- // SIGUSR2 handler: dump backtrace to /tmp/clearwm-bt.txt for debugging busy loops
+ // SIGUSR2 handler: dump backtrace to /tmp/ccec-bt.txt for debugging busy loops
unsafe {
nix::sys::signal::sigaction(
nix::sys::signal::SIGUSR2,
@@ -101,16 +101,16 @@ fn main() {
let ipc_tx = ipc_server.tx;
// Setup channel for configuration updates:
- let (config_tx, config_rx) = tokio::sync::mpsc::unbounded_channel::<(clearwm::config::InertialConfig, bool)>();
+ let (config_tx, config_rx) = tokio::sync::mpsc::unbounded_channel::<ccec::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("clearwm-input-subsystem".into())
+ .name("ccec-input-subsystem".into())
.spawn(move || {
- if let Err(e) = clearwm::input::run_input_daemon(config_rx, ipc_tx_clone, pipe_write_clone) {
+ if let Err(e) = ccec::input::run_input_daemon(config_rx, ipc_tx_clone, pipe_write_clone) {
eprintln!("[input-subsystem] Fatal error: {:?}", e);
}
})
@@ -120,8 +120,8 @@ fn main() {
state.status_sender = Some(status_sender);
// Check if this is a restart
- let cold_start = if env::var("CLEARWM_RESTARTING").as_deref() == Ok("1") {
- env::remove_var("CLEARWM_RESTARTING");
+ let cold_start = if env::var("CCEC_RESTARTING").as_deref() == Ok("1") {
+ env::remove_var("CCEC_RESTARTING");
false
} else {
true
@@ -139,7 +139,7 @@ fn main() {
);
let config_start = std::time::Instant::now();
if let Ok(home) = env::var("HOME") {
- let config_path = format!("{}/.config/clearwm/config.toml", home);
+ let config_path = format!("{}/.config/ccec/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);
@@ -156,7 +156,7 @@ fn main() {
if state.wm.pending_scale_apply && state.wm.output_scale > 0.0 && !state.output_heads.is_empty()
{
let qh = event_queue.handle();
- clearwm::wayland::apply_output_scale(&mut state, &qh);
+ ccec::wayland::apply_output_scale(&mut state, &qh);
}
// Flush any queued requests from config loading (bindings, etc.)
@@ -165,6 +165,7 @@ fn main() {
// 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;
+ let mut last_animation_tick = std::time::Instant::now();
loop {
loop_count += 1;
if loop_count % 10000 == 0 {
@@ -202,22 +203,39 @@ fn main() {
nix::poll::PollFd::new(unsafe { std::os::fd::BorrowedFd::borrow_raw(pipe_read) }, nix::poll::PollFlags::POLLIN),
];
- match nix::poll::poll(&mut poll_fds, nix::poll::PollTimeout::NONE) {
- Ok(_) => {
- // 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);
- }
+ let timeout = if state.wm.animating {
+ let elapsed = last_animation_tick.elapsed();
+ let timeout_duration = if elapsed >= std::time::Duration::from_millis(16) {
+ std::time::Duration::ZERO
+ } else {
+ std::time::Duration::from_millis(16) - elapsed
+ };
+ nix::poll::PollTimeout::try_from(timeout_duration).unwrap()
+ } else {
+ nix::poll::PollTimeout::NONE
+ };
- // 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());
+ 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) => {
@@ -232,13 +250,22 @@ fn main() {
// 5. Dispatch read events
let _ = event_queue.dispatch_pending(&mut state);
+ // 6. If animating and frame budget elapsed, request next frame
+ if state.wm.animating && last_animation_tick.elapsed() >= std::time::Duration::from_millis(16) {
+ if let Some(ref wm) = state.window_manager {
+ wm.manage_dirty();
+ last_animation_tick = std::time::Instant::now();
+ }
+ }
+
// 6. Process pending IPC commands from the socket channel
let mut ipc_commands = false;
loop {
match ipc_rx.try_recv() {
- Ok(cmd) => {
- eprintln!("[main] IPC command: {}", cmd);
- ipc::handle_ipc_command(&cmd, &mut state.wm);
+ 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,
@@ -255,7 +282,7 @@ fn main() {
}
if !state.wm.tap_config_applied && !state.libinput_devices.is_empty() {
let qh = event_queue.handle();
- clearwm::wayland::apply_input_config(&mut state, &qh);
+ ccec::wayland::apply_input_config(&mut state, &qh);
}
}
diff --git a/src/paths.rs b/src/paths.rs
index cff52f3..de180f6 100644
--- a/src/paths.rs
+++ b/src/paths.rs
@@ -2,65 +2,65 @@ use std::env;
pub fn get_socket_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-{}.sock", display)
+ format!("/tmp/ccec-{}.sock", display)
} else {
- "/tmp/clearwm.sock".to_string()
+ "/tmp/ccec.sock".to_string()
}
}
pub fn get_status_socket_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-status-{}.sock", display)
+ format!("/tmp/ccec-status-{}.sock", display)
} else {
- "/tmp/clearwm-status.sock".to_string()
+ "/tmp/ccec-status.sock".to_string()
}
}
pub fn get_windows_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-windows-{}", display)
+ format!("/tmp/ccec-windows-{}", display)
} else {
- "/tmp/clearwm-windows".to_string()
+ "/tmp/ccec-windows".to_string()
}
}
pub fn get_tags_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-tags-{}", display)
+ format!("/tmp/ccec-tags-{}", display)
} else {
- "/tmp/clearwm-tags".to_string()
+ "/tmp/ccec-tags".to_string()
}
}
pub fn get_layout_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-layout-{}", display)
+ format!("/tmp/ccec-layout-{}", display)
} else {
- "/tmp/clearwm-layout".to_string()
+ "/tmp/ccec-layout".to_string()
}
}
pub fn get_title_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-title-{}", display)
+ format!("/tmp/ccec-title-{}", display)
} else {
- "/tmp/clearwm-title".to_string()
+ "/tmp/ccec-title".to_string()
}
}
pub fn get_death_log_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-death-{}.log", display)
+ format!("/tmp/ccec-death-{}.log", display)
} else {
- "/tmp/clearwm-death.log".to_string()
+ "/tmp/ccec-death.log".to_string()
}
}
pub fn get_bt_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-bt-{}.txt", display)
+ format!("/tmp/ccec-bt-{}.txt", display)
} else {
- "/tmp/clearwm-bt.txt".to_string()
+ "/tmp/ccec-bt.txt".to_string()
}
}
@@ -74,24 +74,24 @@ pub fn get_input_coords_socket_path() -> String {
pub fn get_log_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-{}.log", display)
+ format!("/tmp/ccec-{}.log", display)
} else {
- "/tmp/clearwm.log".to_string()
+ "/tmp/ccec.log".to_string()
}
}
pub fn get_prev_log_path() -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-{}-prev.log", display)
+ format!("/tmp/ccec-{}-prev.log", display)
} else {
- "/tmp/clearwm-prev.log".to_string()
+ "/tmp/ccec-prev.log".to_string()
}
}
pub fn get_xprop_path(wid: u64) -> String {
if let Ok(display) = env::var("WAYLAND_DISPLAY") {
- format!("/tmp/clearwm-xprop-{}-{}", wid, display)
+ format!("/tmp/ccec-xprop-{}-{}", wid, display)
} else {
- format!("/tmp/clearwm-xprop-{}", wid)
+ format!("/tmp/ccec-xprop-{}", wid)
}
}
diff --git a/src/protocol.rs b/src/protocol.rs
index 6049a23..8fd1d59 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -59,3 +59,7 @@ pub mod river_libinput_config {
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/restart.rs b/src/restart.rs
index 1d38e12..af84cc5 100644
--- a/src/restart.rs
+++ b/src/restart.rs
@@ -1,4 +1,4 @@
-// Restart and reload logic for clearwm
+// Restart and reload logic for ccec
use crate::config::parse_config;
use crate::types::WindowManager;
@@ -7,17 +7,17 @@ use crate::types::WindowManager;
///
/// 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
-/// clearwm instance. The parent (current process) exits immediately.
+/// ccec instance. The parent (current process) exits immediately.
///
-/// The CLEARWM_RESTARTING environment variable signals that this is a restart
+/// The CCEC_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
///
-/// clearwm is typically a session leader (PID == SID, started by River's
+/// ccec 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 clearwm
+/// `setsid()`, the child dies from SIGHUP before it can exec, and ccec
/// never comes back.
pub fn wm_restart() {
use std::time::Instant;
@@ -58,9 +58,9 @@ pub fn wm_restart() {
}
// Signal that this is a restart, not a cold start
- std::env::set_var("CLEARWM_RESTARTING", "1");
+ std::env::set_var("CCEC_RESTARTING", "1");
- // Persist state to ~/.cache/clearwm_state for the new instance to restore.
+ // Persist state to ~/.cache/ccec_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.
@@ -76,13 +76,13 @@ pub fn wm_restart() {
// 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/clearwm) over current_exe()
+ // We prefer the symlink path (~/.local/bin/ccec) 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/clearwm", home);
+ let symlink = format!("{}/.local/bin/ccec", home);
if std::path::Path::new(&symlink).exists() {
std::path::PathBuf::from(symlink)
} else {
@@ -95,7 +95,7 @@ pub fn wm_restart() {
let path_cstr = std::ffi::CString::new(exe_path.to_string_lossy().into_owned())
.unwrap_or_else(|_| std::process::exit(1));
- // Fork: child waits for parent to die, then execs fresh clearwm.
+ // Fork: child waits for parent to die, then execs fresh ccec.
// Parent exits so River tears down the old Wayland connection.
let pid = unsafe { libc::fork() };
if pid < 0 {
@@ -123,7 +123,7 @@ pub fn wm_restart() {
std::thread::sleep(std::time::Duration::from_millis(500));
}
- // Close inherited Wayland FDs so the new clearwm instance doesn't
+ // Close inherited Wayland FDs so the new ccec 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;
@@ -133,7 +133,7 @@ pub fn wm_restart() {
}
}
- // Exec the same binary — replaces this process with a fresh clearwm.
+ // Exec the same binary — replaces this process with a fresh ccec.
// 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).
for attempt in 0..3 {
@@ -205,6 +205,7 @@ pub fn wm_reload(state: &mut WindowManager) {
// 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();
@@ -216,7 +217,7 @@ pub fn wm_reload(state: &mut WindowManager) {
// Try TOML config first
let home = std::env::var("HOME").unwrap_or_default();
if !home.is_empty() {
- let config_path = format!("{}/.config/clearwm/config.toml", home);
+ let config_path = format!("{}/.config/ccec/config.toml", home);
if std::path::Path::new(&config_path).exists() {
match parse_config(&config_path, false, state) {
Ok(_) => {
@@ -225,12 +226,12 @@ pub fn wm_reload(state: &mut WindowManager) {
spawn_command_bg(cmd);
}
if state.notifications_enable {
- crate::config::show_notification("clearwm", "Configuration reloaded successfully");
+ crate::config::show_notification("ccec", "Configuration reloaded successfully");
}
}
Err(e) => {
if state.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Config reload failed:\n{}", e));
+ crate::config::show_notification("ccec", &format!("Config reload failed:\n{}", e));
}
}
}
diff --git a/src/state.rs b/src/state.rs
index 3188328..978d4ce 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1,7 +1,7 @@
-// Persistent state file for clearwm restart recovery.
+// Persistent state file for ccec restart recovery.
//
// Writes window tag assignments and global tag/layout state to
-// ~/.cache/clearwm_state so that it survives restarts. On startup,
+// ~/.cache/ccec_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.
@@ -21,12 +21,12 @@ use std::fs;
use std::io::{BufRead, Write};
use std::path::PathBuf;
-/// Get the state file path: ~/.cache/clearwm_state
+/// Get the state file path: ~/.cache/ccec_state
fn state_file_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
let mut path = PathBuf::from(home);
path.push(".cache");
- path.push("clearwm_state");
+ path.push("ccec_state");
path
}
@@ -224,8 +224,6 @@ fn parse_tiling_mode_str(s: &str) -> TilingMode {
match s {
"Cascade" => TilingMode::Cascade,
"Grid" => TilingMode::Grid,
- "Vsplit" => TilingMode::Vsplit,
- "Hsplit" => TilingMode::Hsplit,
"Fullscreen" => TilingMode::Fullscreen,
"Floating" => TilingMode::Floating,
"Popup" => TilingMode::Popup,
@@ -306,6 +304,13 @@ pub fn apply_state(wm: &mut WindowManager, state: &PersistentState) {
win.mode_locked = true;
}
}
+
+ // 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!(
@@ -342,9 +347,9 @@ mod tests {
#[test]
fn test_write_read_roundtrip() {
- let dir = std::env::temp_dir().join("clearwm_state_test");
+ let dir = std::env::temp_dir().join("ccec_state_test");
let _ = fs::create_dir_all(&dir);
- let path = dir.join("clearwm_state");
+ let path = dir.join("ccec_state");
let mut wm = WindowManager::default();
wm.active_tags = 0b1010; // tags 2 and 4
diff --git a/src/status.rs b/src/status.rs
index db9d43e..121eab2 100644
--- a/src/status.rs
+++ b/src/status.rs
@@ -12,7 +12,7 @@ pub fn write_status_files(state: &WindowManager) {
use crate::paths;
use std::io::Write;
- // /tmp/clearwm-tags: active_tags focused_tags num_tags
+ // /tmp/ccec-tags: active_tags focused_tags num_tags
if let Ok(mut f) = fs::File::create(paths::get_tags_path()) {
let _ = writeln!(
f,
@@ -21,7 +21,7 @@ pub fn write_status_files(state: &WindowManager) {
);
}
- // /tmp/clearwm-layout: focused window's tiling mode
+ // /tmp/ccec-layout: focused window's tiling mode
let mode_str = if state.expose_active {
"Expose"
} else {
@@ -35,7 +35,7 @@ pub fn write_status_files(state: &WindowManager) {
let _ = writeln!(f, "{}", mode_str);
}
- // /tmp/clearwm-windows: one line per window
+ // /tmp/ccec-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());
@@ -66,7 +66,7 @@ pub fn write_status_files(state: &WindowManager) {
);
}
- // /tmp/clearwm-title
+ // /tmp/ccec-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)"));
@@ -92,8 +92,6 @@ fn tiling_mode_str(mode: TilingMode) -> &'static str {
TilingMode::Floating => "Floating",
TilingMode::Cascade => "Cascade",
TilingMode::Grid => "Grid",
- TilingMode::Vsplit => "Vsplit",
- TilingMode::Hsplit => "Hsplit",
TilingMode::Fullscreen => "Fullscreen",
TilingMode::Popup => "Popup",
}
diff --git a/src/status_server.rs b/src/status_server.rs
index d734455..0a69de9 100644
--- a/src/status_server.rs
+++ b/src/status_server.rs
@@ -1,7 +1,7 @@
// Status socket server for waybar integration
//
// Runs in a dedicated thread. Waybar custom module scripts connect to
-// /tmp/clearwm-status.sock, send a subscription line ("tags", "layout",
+// /tmp/ccec-status.sock, send a subscription line ("tags", "layout",
// or "title"), and receive JSON lines whenever the status changes.
//
// The main loop sends updates through an mpsc channel — no blocking,
@@ -72,7 +72,7 @@ pub fn spawn_status_server() -> StatusSender {
let (tx, rx) = mpsc::channel::<StatusUpdate>();
std::thread::Builder::new()
- .name("clearwm-status".into())
+ .name("ccec-status".into())
.spawn(move || {
status_server_main(rx);
})
@@ -242,7 +242,7 @@ fn format_for_subscription(sub: Subscription, update: &StatusUpdate) -> String {
/// This is the same logic that write_status_files() uses, but produces
/// the data for the socket instead of writing to files.
pub fn build_status_update(wm: &crate::types::WindowManager) -> StatusUpdate {
- // Tags: generate the same pango-marked JSON that clearwm-tags.sh produces
+ // Tags: generate the same pango-marked JSON that ccec-tags.sh produces
let tags_json = render_tags_json(
wm.active_tags,
wm.focused_tags,
@@ -269,7 +269,7 @@ pub fn build_status_update(wm: &crate::types::WindowManager) -> StatusUpdate {
}
/// Render tag state as a JSON string with pango markup, matching the format
-/// produced by the old clearwm-tags.sh script.
+/// produced by the old ccec-tags.sh script.
///
/// Colors:
/// - Active + Focused: bright (#a8c0d8)
diff --git a/src/tiling.rs b/src/tiling.rs
index 47e8ebb..69c79cc 100644
--- a/src/tiling.rs
+++ b/src/tiling.rs
@@ -1,4 +1,4 @@
-// Tiling formulas ported from clearwm.c
+// Tiling formulas ported from ccec.c
/// Cascade depth factor: each depth step multiplies channels by this
pub const CASCADE_DEPTH_FACTOR: f64 = 0.80;
@@ -80,71 +80,7 @@ pub fn tile_grid(
(x, y, width, height)
}
-/// Tile a window in vsplit mode (vertical splits — windows side by side).
-///
-/// Each window gets an equal share of the horizontal space.
-///
-/// Screen edges use per-side gaps; inter-window spacing uses `gap`.
-/// n = total windows in vsplit
-/// width = (screen_w - gap_left - gap_right - (n - 1) * gap) / n - 2 * bw
-/// height = screen_h - gap_top - gap_bottom - bw * 2 - bar_height
-/// x = gap_left + bw + idx * (width + 2 * bw + gap)
-/// y = bar_height + gap_top + bw
-pub fn tile_vsplit(
- 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_vsplit: i32,
- idx: i32,
-) -> (i32, i32, i32, i32) {
- let n = if n_vsplit < 1 { 1 } else { n_vsplit };
- let width = (screen_w - gap_left - gap_right - (n - 1) * gap) / n - 2 * bw;
- let height = screen_h - gap_top - gap_bottom - bw * 2 - bar_height;
- let width = if width < 1 { 1 } else { width };
- let height = if height < 1 { 1 } else { height };
- let x = gap_left + bw + idx * (width + 2 * bw + gap);
- let y = bar_height + gap_top + bw;
- (x, y, width, height)
-}
-/// Tile a window in hsplit mode (horizontal splits — windows stacked vertically).
-///
-/// Each window gets an equal share of the vertical space.
-///
-/// Screen edges use per-side gaps; inter-window spacing uses `gap`.
-/// n = total windows in hsplit
-/// width = screen_w - gap_left - gap_right - bw * 2
-/// height = (screen_h - bar_height - gap_top - gap_bottom - (n - 1) * gap) / n - 2 * bw
-/// x = gap_left + bw
-/// y = bar_height + gap_top + bw + idx * (height + 2 * bw + gap)
-pub fn tile_hsplit(
- 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_hsplit: i32,
- idx: i32,
-) -> (i32, i32, i32, i32) {
- let n = if n_hsplit < 1 { 1 } else { n_hsplit };
- let width = screen_w - gap_left - gap_right - bw * 2;
- let height = (screen_h - bar_height - gap_top - gap_bottom - (n - 1) * gap) / n - 2 * bw;
- let width = if width < 1 { 1 } else { width };
- let height = if height < 1 { 1 } else { height };
- let x = gap_left + bw;
- let y = bar_height + gap_top + bw + idx * (height + 2 * bw + gap);
- (x, y, width, height)
-}
/// Tile a window in fullscreen mode — fills the screen minus gaps, bar, and borders.
///
@@ -336,123 +272,7 @@ mod tests {
assert_eq!(color, "#3e3e3e");
}
- #[test]
- fn test_tile_vsplit_single() {
- // Single vsplit window fills screen minus gaps/borders/bar
- let (x, y, w, h) = tile_vsplit(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 1, 0);
- assert_eq!(x, 36); // gap_left + bw
- assert_eq!(y, 64); // bar_height + gap_top + bw
- // w = (1920 - 18 - 18 - 0*18) / 1 - 2*18 = 1884 - 36 = 1848
- assert_eq!(w, 1848);
- // h = 1080 - 18 - 18 - 18*2 - 28 = 1080 - 100 = 980
- assert_eq!(h, 980);
- }
-
- #[test]
- fn test_tile_vsplit_two() {
- // Two vsplit windows side by side
- let (x0, y0, w0, h0) = tile_vsplit(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 2, 0);
- let (x1, y1, w1, h1) = tile_vsplit(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 2, 1);
-
- // Same dimensions
- assert_eq!(w0, w1);
- assert_eq!(h0, h1);
- // Same y (same row)
- assert_eq!(y0, y1);
- // Window 1 is to the right
- assert!(x1 > x0);
-
- // w = (1920 - 18 - 18 - 1*18) / 2 - 2*18 = (1920-54)/2 - 36 = 933 - 36 = 897
- assert_eq!(w0, 897);
- }
-
- #[test]
- fn test_tile_vsplit_minimum_size() {
- let (_, _, w, h) = tile_vsplit(100, 100, 18, 18, 18, 18, 18, 18, 28, 10, 5);
- assert!(w >= 1);
- assert!(h >= 1);
- }
-
- #[test]
- fn test_tile_vsplit_asymmetric_gaps() {
- // Asymmetric screen gaps with 2 vsplit windows
- let (x0, y0, w0, h0) = tile_vsplit(1920, 1080, 12, 10, 20, 30, 40, 6, 28, 2, 0);
- let (x1, _y1, w1, _h1) = tile_vsplit(1920, 1080, 12, 10, 20, 30, 40, 6, 28, 2, 1);
-
- // w = (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 + bw = 28 + 10 + 6 = 44
- assert_eq!(y0, 44);
- // h = 1080 - 10 - 40 - 6*2 - 28 = 1080 - 90 = 990
- assert_eq!(h0, 990);
-
- // x1 = gap_left + bw + 1*(917 + 2*6 + 12) = 26 + 941 = 967
- assert_eq!(x1, 967);
- }
- #[test]
- fn test_tile_hsplit_single() {
- // Single hsplit window fills screen minus gaps/borders/bar
- let (x, y, w, h) = tile_hsplit(1920, 1080, 18, 18, 18, 18, 18, 18, 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 = 1920 - 72 = 1848
- assert_eq!(w, 1848);
- // h = (1080 - 28 - 18 - 18 - 0*18) / 1 - 2*18 = 1016 - 36 = 980
- assert_eq!(h, 980);
- }
-
- #[test]
- fn test_tile_hsplit_two() {
- // Two hsplit windows stacked vertically
- let (x0, y0, w0, h0) = tile_hsplit(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 2, 0);
- let (x1, y1, w1, h1) = tile_hsplit(1920, 1080, 18, 18, 18, 18, 18, 18, 28, 2, 1);
-
- // Same dimensions
- assert_eq!(w0, w1);
- assert_eq!(h0, h1);
- // Same x (same column)
- assert_eq!(x0, x1);
- // Window 1 is below window 0
- assert!(y1 > y0);
-
- // h = (1080 - 28 - 18 - 18 - 1*18) / 2 - 2*18 = (1080-82)/2 - 36 = 499 - 36 = 463
- assert_eq!(h0, 463);
- }
-
- #[test]
- fn test_tile_hsplit_minimum_size() {
- let (_, _, w, h) = tile_hsplit(100, 100, 18, 18, 18, 18, 18, 18, 28, 10, 5);
- assert!(w >= 1);
- assert!(h >= 1);
- }
-
- #[test]
- fn test_tile_hsplit_asymmetric_gaps() {
- // Asymmetric screen gaps with 2 hsplit windows
- let (x0, y0, w0, h0) = tile_hsplit(1920, 1080, 12, 10, 20, 30, 40, 6, 28, 2, 0);
- let (_x1, y1, w1, h1) = tile_hsplit(1920, 1080, 12, 10, 20, 30, 40, 6, 28, 2, 1);
-
- // w = 1920 - 20 - 30 - 6*2 = 1920 - 62 = 1858
- assert_eq!(w0, 1858);
- assert_eq!(w0, w1);
-
- // x0 = gap_left + bw = 20 + 6 = 26
- assert_eq!(x0, 26);
- // y0 = bar_height + gap_top + bw = 28 + 10 + 6 = 44
- assert_eq!(y0, 44);
-
- // h = (1080 - 28 - 10 - 40 - 1*12) / 2 - 2*6 = (1080-90)/2 - 12 = 495 - 12 = 483
- assert_eq!(h0, 483);
- assert_eq!(h0, h1);
-
- // y1 = bar_height + gap_top + bw + 1*(483 + 2*6 + 12) = 44 + 507 = 551
- assert_eq!(y1, 551);
- }
#[test]
fn test_tile_fullscreen_basic() {
diff --git a/src/types.rs b/src/types.rs
index 7abc041..587a606 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,4 +1,4 @@
-// Core data structures for clearwm
+// Core data structures for ccec
use std::collections::HashMap;
@@ -10,8 +10,6 @@ pub enum TilingMode {
Floating,
Cascade,
Grid,
- Vsplit,
- Hsplit,
Fullscreen,
Popup,
}
@@ -22,8 +20,6 @@ impl TilingMode {
TilingMode::Floating => "Floating",
TilingMode::Cascade => "Cascade",
TilingMode::Grid => "Grid",
- TilingMode::Vsplit => "Vsplit",
- TilingMode::Hsplit => "Hsplit",
TilingMode::Fullscreen => "Fullscreen",
TilingMode::Popup => "Popup",
}
@@ -74,8 +70,6 @@ pub struct Layout {
pub fullscreen_border_width: i32,
pub cascade_border_width: i32,
pub grid_border_width: i32,
- pub vsplit_border_width: i32,
- pub hsplit_border_width: i32,
pub floating_border_width: i32,
pub border_r: u32,
pub border_g: u32,
@@ -86,6 +80,7 @@ pub struct Layout {
pub background_b: u32,
pub background_a: u32,
pub border_font_size: i32,
+ pub transition_duration: i32,
}
impl Default for Layout {
@@ -102,8 +97,6 @@ impl Default for Layout {
fullscreen_border_width: 0,
cascade_border_width: 6,
grid_border_width: 6,
- vsplit_border_width: 6,
- hsplit_border_width: 6,
floating_border_width: 6,
border_r: 0x3E3E3E3Eu32,
border_g: 0x3E3E3E3Eu32,
@@ -114,6 +107,7 @@ impl Default for Layout {
background_b: 0x0E0E0E0Eu32,
background_a: 0xFFFFFFFFu32,
border_font_size: 11,
+ transition_duration: 300,
}
}
}
@@ -228,6 +222,11 @@ pub struct Window {
pub needs_xprop_check: bool,
/// How many ManageStart cycles we've waited for the xprop result file.
pub xprop_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>,
}
impl Default for Window {
@@ -260,6 +259,11 @@ impl Default for Window {
mode_locked: false,
needs_xprop_check: false,
xprop_check_attempts: 0,
+ anim_x: None,
+ anim_y: None,
+ anim_w: None,
+ anim_h: None,
+ anim_opacity: None,
}
}
}
@@ -322,7 +326,7 @@ pub struct WindowManager {
/// 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/clearwm_state on the
+ /// When true, apply persisted state from ~/.cache/ccec_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,
@@ -347,9 +351,11 @@ pub struct WindowManager {
pub notifications_enable: bool,
/// Reload commands to execute on configuration reload
pub reload_commands: Vec<String>,
- pub input_controller: Option<tokio::sync::mpsc::UnboundedSender<(crate::config::InertialConfig, bool)>>,
+ pub 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,
}
impl Default for WindowManager {
@@ -398,6 +404,8 @@ impl Default for WindowManager {
input_controller: None,
trackpad_disabled: false,
expose_active: false,
+ expose_visual_active: false,
+ animating: false,
}
}
}
@@ -483,8 +491,6 @@ pub fn parse_tiling_mode(s: &str) -> TilingMode {
match s {
"cascade" => TilingMode::Cascade,
"grid" => TilingMode::Grid,
- "vsplit" => TilingMode::Vsplit,
- "hsplit" => TilingMode::Hsplit,
"fullscreen" => TilingMode::Fullscreen,
"floating" => TilingMode::Floating,
"popup" => TilingMode::Popup,
@@ -599,11 +605,13 @@ pub fn parse_modifiers(mod_str: &str) -> u32 {
/// Parse a button string ("left", "right", "middle", or numeric)
pub fn parse_button(s: &str) -> u32 {
- match s {
+ match s.trim() {
"left" => 0x110, // BTN_LEFT
"right" => 0x111, // BTN_RIGHT
"middle" => 0x112, // BTN_MIDDLE
- _ => s.parse().unwrap_or(0),
+ "side" => 0x113, // BTN_SIDE
+ "extra" => 0x114, // BTN_EXTRA
+ _ => s.trim().parse().unwrap_or(0),
}
}
@@ -640,8 +648,6 @@ mod tests {
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("vsplit"), TilingMode::Vsplit);
- assert_eq!(parse_tiling_mode("hsplit"), TilingMode::Hsplit);
assert_eq!(parse_tiling_mode("fullscreen"), TilingMode::Fullscreen);
assert_eq!(parse_tiling_mode("floating"), TilingMode::Floating);
assert_eq!(parse_tiling_mode("popup"), TilingMode::Popup);
diff --git a/src/wayland.rs b/src/wayland.rs
index fa60f62..0018c61 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -1,4 +1,4 @@
-// Wayland display connection, registry, and event dispatch for clearwm
+// Wayland display connection, registry, and event dispatch for ccec
use wayland_client::{
event_created_child, protocol::wl_registry, Connection, Dispatch, EventQueue, Proxy,
@@ -93,6 +93,25 @@ pub struct PendingBorderDrag {
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 {
@@ -367,19 +386,18 @@ impl AppState {
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::Vsplit => state.wm.layout.vsplit_border_width,
- crate::types::TilingMode::Hsplit => state.wm.layout.hsplit_border_width,
crate::types::TilingMode::Floating => state.wm.layout.floating_border_width,
crate::types::TilingMode::Popup => 0,
}
};
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 edge_thresh = (border_w as f64).max(8.0);
- if state.last_pointer_surface_y < edge_thresh {
+ 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) {
@@ -388,41 +406,50 @@ impl AppState {
Shape::NsResize
}
} else {
- if state.last_pointer_surface_x < CORNER_THRESHOLD {
- Shape::EwResize
- } else if state.last_pointer_surface_x > (total_width as f64 - CORNER_THRESHOLD) {
- Shape::EwResize
- } else {
- Shape::Default
- }
+ Shape::Default
}
}
"left" => {
- 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
+ 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::EwResize
+ Shape::Default
}
}
"right" => {
- 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
+ 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::EwResize
+ Shape::Default
}
}
"bottom" => {
let total_width = w.width + 2 * grab_w;
- 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
+ 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::NsResize
+ Shape::Default
}
}
_ => Shape::Default,
@@ -690,6 +717,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
});
}
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
@@ -802,7 +830,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
}
}
- // Apply persisted state from ~/.cache/clearwm_state on first ManageStart.
+ // Apply persisted state from ~/.cache/ccec_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.
@@ -937,6 +965,13 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
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();
@@ -967,10 +1002,17 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
}
})
.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 {
+ if let Some(ref wm) = state.window_manager {
+ wm.manage_dirty();
+ }
+ }
+
wm_proxy.manage_finish();
// Flush immediately so River can start the configure/render cycle
// without waiting for our blocking_dispatch to complete.
@@ -1012,6 +1054,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
// set_position and propose_dimensions are window management state
// and must happen during ManageStart.
crate::wm::render_borders(state);
+ crate::wm::render_opacity(state);
// Update and render window title decorations on borders
crate::decorations::update_decorations(state, qhandle);
@@ -1103,7 +1146,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
if state.wm.needs_status_update {
crate::status::write_status_files(&state.wm);
- // Persist state to ~/.cache/clearwm_state for restart recovery.
+ // Persist state to ~/.cache/ccec_state for restart recovery.
// Safe: just file I/O, no fork, no blocking.
crate::state::write_state(&state.wm);
@@ -1182,6 +1225,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
};
state.wm.outputs.push(output);
+ update_screen_bounds(&state.wm.outputs);
state.output_proxies.push((
id,
OutputProxy {
@@ -1274,6 +1318,16 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
);
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 {
@@ -1285,10 +1339,38 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
}
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;
- state.wm.needs_render = true;
+ 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 {
+ if let Some(ref wm) = state.window_manager {
+ wm.manage_dirty();
}
}
}
@@ -1318,7 +1400,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
// 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/clearwm-xprop-{wid} which
+ // The script writes results to /tmp/ccec-xprop-{wid} which
// is read on the next ManageStart cycle.
if !window.has_parent && window.pid > 0 {
let pid = window.pid;
@@ -1512,7 +1594,10 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
// Disable expose if it was active
if state.wm.expose_active {
- state.wm.expose_active = false;
+ crate::wm::set_expose_active(&mut state.wm, false);
+ if let Some(ref wm) = state.window_manager {
+ wm.manage_dirty();
+ }
}
state.wm.needs_render = true;
@@ -1655,6 +1740,7 @@ impl Dispatch<RiverOutputV1, ()> for AppState {
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
@@ -1705,6 +1791,7 @@ impl Dispatch<wl_output::WlOutput, ()> for AppState {
output.x = x;
output.y = y;
state.wm.needs_render = true;
+ update_screen_bounds(&state.wm.outputs);
}
}
}
@@ -1730,6 +1817,7 @@ impl Dispatch<wl_output::WlOutput, ()> for AppState {
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
@@ -1996,7 +2084,7 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
eprintln!("spawn: {}", cmd);
// Close inherited FDs > 2 in the child so that spawned
// Wayland clients (fuzzel, etc.) never accidentally read
- // from clearwm's Wayland socket fd. This prevents protocol
+ // from ccec'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 {
@@ -2136,7 +2224,17 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
if let Some((_, sp)) = state.seat_proxies.iter().find(|(sid, _)| *sid == seat_id) {
sp.river_seat.op_start_pointer();
}
- if let Some(win) = state.wm.get_window(wid) {
+ 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,
@@ -2200,8 +2298,8 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
minimize_requested: false,
tiling_mode: TilingMode::Fullscreen,
mode_locked,
- needs_xprop_check: false,
xprop_check_attempts: 0,
+ ..Default::default()
};
crate::wm::get_mode_for_window(&state.wm, &temp_win)
.unwrap_or(state.wm.global_layout)
@@ -2227,8 +2325,6 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
let cycle = [
TilingMode::Cascade,
TilingMode::Grid,
- TilingMode::Vsplit,
- TilingMode::Hsplit,
TilingMode::Fullscreen,
];
// Cycle the layout for the currently active tag(s) only.
@@ -2265,7 +2361,7 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
);
if state.wm.notifications_enable {
- crate::config::show_notification("clearwm", &format!("Layout set to {} for active tags", next.as_str()));
+ crate::config::show_notification("ccec", &format!("Layout set to {} for active tags", next.as_str()));
}
// Unlock windows that got their mode from the layout (not from mode_rules
@@ -2281,8 +2377,6 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
let cycle = [
TilingMode::Cascade,
TilingMode::Grid,
- TilingMode::Vsplit,
- TilingMode::Hsplit,
TilingMode::Fullscreen,
TilingMode::Floating,
];
@@ -2311,7 +2405,7 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
win.mode_locked = true;
if notifications_enable {
let win_title = win.title.as_deref().unwrap_or("Window");
- crate::config::show_notification("clearwm", &format!("Tiling mode set to {} for: {}", next.as_str(), win_title));
+ crate::config::show_notification("ccec", &format!("Tiling mode set to {} for: {}", next.as_str(), win_title));
}
state.wm.needs_render = true;
state.wm.needs_status_update = true;
@@ -2325,7 +2419,8 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
crate::restart::wm_restart();
}
Action::Expose => {
- state.wm.expose_active = !state.wm.expose_active;
+ 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;
}
@@ -2928,7 +3023,7 @@ pub fn wayland_init() -> Result<(Connection, EventQueue<AppState>, AppState), St
state.render_count
);
- eprintln!("clearwm: Wayland connection established");
+ eprintln!("ccec: Wayland connection established");
Ok((conn, event_queue, state))
}
@@ -3293,7 +3388,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
wl_pointer::Event::Leave { .. } => {
eprintln!("[pointer] leave");
state.pointer_hovered_surface = None;
- state.pending_border_drag = None;
Self::update_cursor_shape_for_surface(state, proxy);
}
wl_pointer::Event::Motion { surface_x, surface_y, .. } => {
@@ -3306,8 +3400,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
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 12.0 logical pixels to distinguish click vs drag
- if dist > 12.0 {
+ // 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.
@@ -3390,64 +3484,48 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
}
});
- let mut focus_changed = false;
if let Some(sid) = seat_id {
if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
- focus_changed = seat.focused_window_id != Some(wid);
seat.focused_window_id = Some(wid);
state.wm.move_window_to_end(wid);
if state.wm.expose_active {
- state.wm.expose_active = false;
+ crate::wm::set_expose_active(&mut state.wm, false);
}
- state.wm.needs_focus = true;
- state.wm.needs_render = true;
- state.wm.needs_status_update = true;
- if let Some(ref wm) = state.window_manager {
- wm.manage_dirty();
}
- }
-
- if !focus_changed {
- // Determine the specific PointerOpType based on coordinates on the matched surface
- let op_type = 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::Vsplit => state.wm.layout.vsplit_border_width,
- crate::types::TilingMode::Hsplit => state.wm.layout.hsplit_border_width,
- crate::types::TilingMode::Floating => state.wm.layout.floating_border_width,
- crate::types::TilingMode::Popup => 0,
- }
- };
- let grab_w = border_w.max(10);
-
- match surface_type {
- "top" => {
- let total_width = w.width + 2 * border_w;
- let edge_thresh = (border_w as f64).max(8.0);
- if state.last_pointer_surface_y < edge_thresh {
- if state.last_pointer_surface_x < CORNER_THRESHOLD {
- PointerOpType::ResizeTopLeft
- } else if state.last_pointer_surface_x > (total_width as f64 - CORNER_THRESHOLD) {
- PointerOpType::ResizeTopRight
- } else {
- PointerOpType::ResizeTop
- }
+ // Determine the specific PointerOpType based on coordinates on the matched surface
+ let op_type = 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,
+ }
+ };
+ let grab_w = border_w.max(10);
+ let logical_height = border_w.max(16);
+ 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 {
+ PointerOpType::ResizeTopLeft
+ } else if state.last_pointer_surface_x > (total_width as f64 - CORNER_THRESHOLD) {
+ PointerOpType::ResizeTopRight
} else {
- if state.last_pointer_surface_x < CORNER_THRESHOLD {
- PointerOpType::ResizeLeft
- } else if state.last_pointer_surface_x > (total_width as f64 - CORNER_THRESHOLD) {
- PointerOpType::ResizeRight
- } else {
- PointerOpType::Move
- }
+ PointerOpType::ResizeTop
}
+ } else {
+ PointerOpType::Move
}
- "left" => {
+ }
+ "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 {
PointerOpType::ResizeTopLeft
} else if state.last_pointer_surface_y > (w.height as f64 - CORNER_THRESHOLD) {
@@ -3455,8 +3533,13 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
} else {
PointerOpType::ResizeLeft
}
+ } else {
+ PointerOpType::Move
}
- "right" => {
+ }
+ "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 {
PointerOpType::ResizeTopRight
} else if state.last_pointer_surface_y > (w.height as f64 - CORNER_THRESHOLD) {
@@ -3464,9 +3547,14 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
} else {
PointerOpType::ResizeRight
}
+ } else {
+ PointerOpType::Move
}
- "bottom" => {
- let total_width = w.width + 2 * grab_w;
+ }
+ "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 {
PointerOpType::ResizeBottomLeft
} else if state.last_pointer_surface_x > (total_width as f64 - CORNER_THRESHOLD) {
@@ -3474,22 +3562,24 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
} else {
PointerOpType::ResizeBottom
}
+ } else {
+ PointerOpType::Move
}
- _ => PointerOpType::Move,
}
- } else {
- PointerOpType::Move
- };
-
- // 2. Store the pending drag info (threshold check is in Event::Motion)
- state.pending_border_drag = Some(PendingBorderDrag {
- window_id: wid,
- seat_id: sid,
- start_surface_x: state.last_pointer_surface_x,
- start_surface_y: state.last_pointer_surface_y,
- op_type,
- });
- }
+ _ => PointerOpType::Move,
+ }
+ } else {
+ PointerOpType::Move
+ };
+
+ // 2. Store the pending drag info (threshold check is in Event::Motion)
+ state.pending_border_drag = Some(PendingBorderDrag {
+ window_id: wid,
+ seat_id: sid,
+ start_surface_x: state.last_pointer_surface_x,
+ start_surface_y: state.last_pointer_surface_y,
+ op_type,
+ });
}
}
}
diff --git a/src/wm.rs b/src/wm.rs
index 917dabd..85fa139 100644
--- a/src/wm.rs
+++ b/src/wm.rs
@@ -78,10 +78,20 @@ pub fn get_mode_for_window(wm: &WindowManager, win: &Window) -> Option<TilingMod
/// Should be called during ManageStart before compute_tiling.
pub fn assign_window_modes(wm: &mut WindowManager) {
// Enforce that clear-status-interface is assigned all tags so that it is always visible
+ // and that blank steam_proton helper windows are always untagged so they remain hidden.
for win in &mut wm.windows {
if win.app_id.as_deref() == Some("clear-status-interface") {
win.tags = u32::MAX;
}
+
+ 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 {
+ if win.tags != 0 {
+ eprintln!("[mode] enforcing tags=0 for blank steam_proton helper window id={}", win.id);
+ win.tags = 0;
+ }
+ }
}
// Collect assignments first (borrow checker: can't borrow wm mutably while iterating mode_rules)
@@ -256,13 +266,13 @@ fn compute_tiling(
let row = idx / cols;
let col = idx % cols;
- 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 - 2 * bw;
+ let width = (screen_w - gap - gap - (cols - 1) * gap) / cols - 2 * bw;
+ let height = (screen_h - bar_height - gap - gap - (rows - 1) * gap) / rows - 2 * 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 + bw + row * (height + 2 * bw + gap);
+ let x = gap + bw + col * (width + 2 * bw + gap);
+ let y = bar_height + gap + bw + row * (height + 2 * bw + gap);
results.push(TileResult {
wid: win.id,
@@ -330,8 +340,6 @@ fn compute_tiling(
// Count windows per tiling mode
let mut n_cascade = 0i32;
let mut n_grid = 0i32;
- let mut n_vsplit = 0i32;
- let mut n_hsplit = 0i32;
for win in &wm.windows {
if (win.tags & wm.active_tags) == 0 || win.closed {
continue;
@@ -339,8 +347,6 @@ fn compute_tiling(
match win.tiling_mode {
TilingMode::Cascade => n_cascade += 1,
TilingMode::Grid => n_grid += 1,
- TilingMode::Vsplit => n_vsplit += 1,
- TilingMode::Hsplit => n_hsplit += 1,
_ => {}
}
}
@@ -368,8 +374,6 @@ fn compute_tiling(
let mut results = Vec::new();
let mut idx_cascade = 0i32;
let mut idx_grid = 0i32;
- let mut idx_vsplit = 0i32;
- let mut idx_hsplit = 0i32;
let mut idx_floating = 0i32;
for win in &wm.windows {
@@ -427,20 +431,7 @@ fn compute_tiling(
idx_grid += 1;
(x, y, w, h)
}
- TilingMode::Vsplit => {
- let (x, y, w, h) = tiling::tile_vsplit(
- screen_w, screen_h, gap, gap_top, gap_left, gap_right, gap_bottom, wm.layout.vsplit_border_width, bar_height, n_vsplit, idx_vsplit,
- );
- idx_vsplit += 1;
- (x, y, w, h)
- }
- TilingMode::Hsplit => {
- let (x, y, w, h) = tiling::tile_hsplit(
- screen_w, screen_h, gap, gap_top, gap_left, gap_right, gap_bottom, wm.layout.hsplit_border_width, bar_height, n_hsplit, idx_hsplit,
- );
- idx_hsplit += 1;
- (x, y, w, h)
- }
+
TilingMode::Floating => {
// Floating windows: don't tile them, but still propose
// dimensions so they don't end up at w=0 h=0 (which
@@ -504,7 +495,139 @@ fn compute_tiling(
/// Apply computed tiling results: set position and propose dimensions.
fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
+ let mut any_animating = false;
+
+ let focused_id = state.wm.seats.iter()
+ .find(|s| !s.removed)
+ .and_then(|s| s.focused_window_id);
+
+ let transition_duration = state.wm.layout.transition_duration;
+ let easing = if transition_duration <= 16 {
+ 1.0
+ } else {
+ 1.0 - 0.01f64.powf(16.0 / transition_duration as f64)
+ };
+
+ let expose_active = state.wm.expose_active;
+ let (screen_w, screen_h, _, _, _, _) = get_screen_geometry(&state.wm);
+ let fbw = state.wm.layout.floating_border_width;
+ let gap_left = state.wm.layout.gap_left;
+ let gap_top = state.wm.layout.gap_top;
+ let bar_height = state.wm.layout.bar_height;
+
for tr in results {
+ let mut final_x = tr.x;
+ let mut final_y = tr.y;
+ let mut final_w = tr.w;
+ let mut final_h = tr.h;
+
+ if let Some(win) = state.wm.get_window_mut(tr.wid) {
+ let is_cascade = win.tiling_mode == TilingMode::Cascade;
+ let is_exposed = expose_active && win.tiling_mode != TilingMode::Popup && win.app_id.as_deref() != Some("clear-status-interface");
+ let was_animating = win.anim_x.is_some() || win.anim_y.is_some() || win.anim_w.is_some() || win.anim_h.is_some() || win.anim_opacity.is_some();
+ let should_animate = is_cascade || is_exposed || was_animating;
+
+ if should_animate {
+ let curr_x = win.anim_x.unwrap_or(win.x as f64);
+ let curr_y = win.anim_y.unwrap_or(win.y as f64);
+ let curr_w = win.anim_w.unwrap_or(win.width as f64);
+ let curr_h = win.anim_h.unwrap_or(win.height as f64);
+ let curr_opacity = win.anim_opacity.unwrap_or(1.0);
+
+ let target_opacity = if is_exposed {
+ if Some(win.id) == focused_id { 1.0 } else { 0.75 }
+ } else if is_cascade {
+ if Some(win.id) == focused_id { 1.0 } else { 0.75 }
+ } else {
+ 1.0
+ };
+
+ if curr_w == 0.0 || win.is_new {
+ // New window: snap instantly
+ win.anim_x = Some(tr.x as f64);
+ win.anim_y = Some(tr.y as f64);
+ win.anim_w = Some(tr.w as f64);
+ win.anim_h = Some(tr.h as f64);
+ win.anim_opacity = Some(target_opacity);
+ } else {
+ let target_x = tr.x as f64;
+ let target_y = tr.y as f64;
+ let target_w = tr.w as f64;
+ let target_h = tr.h as f64;
+
+ let dx = target_x - curr_x;
+ let dy = target_y - curr_y;
+ let dw = target_w - curr_w;
+ let dh = target_h - curr_h;
+ let d_opacity = target_opacity - curr_opacity;
+
+ if dx.abs() > 0.5 || dy.abs() > 0.5 || dw.abs() > 0.5 || dh.abs() > 0.5 || d_opacity.abs() > 0.01 {
+ let next_x = curr_x + dx * easing;
+ let next_y = curr_y + dy * easing;
+ let next_w = curr_w + dw * easing;
+ let next_h = curr_h + dh * easing;
+ let next_opacity = curr_opacity + d_opacity * easing;
+
+ win.anim_x = Some(next_x);
+ win.anim_y = Some(next_y);
+ win.anim_w = Some(next_w);
+ win.anim_h = Some(next_h);
+ win.anim_opacity = Some(next_opacity);
+
+ final_x = next_x.round() as i32;
+ final_y = next_y.round() as i32;
+ // Propose the target size instantly during transitions to avoid configure storms and flickering
+ final_w = tr.w;
+ final_h = tr.h;
+
+ any_animating = true;
+ } else {
+ if is_cascade || is_exposed {
+ win.anim_x = Some(target_x);
+ win.anim_y = Some(target_y);
+ win.anim_w = Some(target_w);
+ win.anim_h = Some(target_h);
+ win.anim_opacity = Some(target_opacity);
+ } else {
+ win.anim_x = None;
+ win.anim_y = None;
+ win.anim_w = None;
+ win.anim_h = None;
+ win.anim_opacity = None;
+ }
+ }
+ }
+ } else {
+ win.anim_x = None;
+ win.anim_y = None;
+ win.anim_w = None;
+ win.anim_h = None;
+ win.anim_opacity = None;
+ }
+
+ // Update internal state
+ let is_floating_and_expose = expose_active && win.tiling_mode == TilingMode::Floating;
+ if !is_floating_and_expose {
+ win.x = final_x;
+ win.y = final_y;
+ win.width = final_w;
+ win.height = final_h;
+ } else {
+ // If it's a new floating window created during expose mode,
+ // initialize its position to the default floating position if unset.
+ if win.x == 0 && win.y == 0 {
+ let fw = if win.hint_min_width > 32 { win.hint_min_width } else { screen_w * 2 / 3 };
+ let fh = if win.hint_min_height > 32 { win.hint_min_height } else { screen_h * 2 / 3 };
+ let fx = gap_left + fbw;
+ let fy = gap_left + fbw + bar_height + gap_top;
+ win.x = fx;
+ win.y = fy;
+ win.width = fw;
+ win.height = fh;
+ }
+ }
+ }
+
// Set position via river_node_v1
if let Some(node) = state
.window_nodes
@@ -512,12 +635,12 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
.find(|(id, _)| *id == tr.wid)
.map(|(_, n)| n)
{
- node.set_position(tr.x, tr.y);
+ node.set_position(final_x, final_y);
}
// Propose dimensions via river_window_v1
if let Some(wp) = state.get_window_proxy(tr.wid) {
- wp.river_window.propose_dimensions(tr.w, tr.h);
+ wp.river_window.propose_dimensions(final_w, final_h);
// Tell the client to use server-side decoration.
// Per the River protocol, use_csd is the default when neither
// use_csd nor use_ssd is called. Calling use_ssd here ensures
@@ -526,15 +649,52 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
// (decoration_hint == only_supports_csd).
wp.river_window.use_ssd();
}
+ }
- // Update internal state
- if let Some(win) = state.wm.get_window_mut(tr.wid) {
- win.x = tr.x;
- win.y = tr.y;
- win.width = tr.w;
- win.height = tr.h;
+ state.wm.animating = any_animating;
+ if any_animating {
+ state.wm.needs_render = true;
+ }
+ state.wm.expose_visual_active = state.wm.expose_active;
+}
+
+/// Set the expose_active state and initialize transition animation states to prevent flickering and enable smooth animations.
+pub fn set_expose_active(wm: &mut WindowManager, active: bool) {
+ if wm.expose_active == active {
+ return;
+ }
+ let prior_expose_visual_active = wm.expose_visual_active;
+
+ let focused_id = wm.seats.iter()
+ .find(|s| !s.removed)
+ .and_then(|s| s.focused_window_id);
+
+ for win in &mut wm.windows {
+ if win.closed || win.app_id.as_deref() == Some("clear-status-interface") || win.tiling_mode == TilingMode::Popup {
+ continue;
+ }
+ let is_focused = Some(win.id) == focused_id;
+
+ // Ensure current geometry animation states are initialized
+ if win.anim_x.is_none() { win.anim_x = Some(win.x as f64); }
+ if win.anim_y.is_none() { win.anim_y = Some(win.y as f64); }
+ if win.anim_w.is_none() { win.anim_w = Some(win.width as f64); }
+ if win.anim_h.is_none() { win.anim_h = Some(win.height as f64); }
+
+ // Ensure opacity animation state is initialized to its current visual value
+ if win.anim_opacity.is_none() {
+ let current_opacity = if win.tiling_mode == TilingMode::Cascade && !is_focused {
+ 0.75
+ } else if prior_expose_visual_active && !is_focused {
+ 0.75
+ } else {
+ 1.0
+ };
+ win.anim_opacity = Some(current_opacity);
}
}
+
+ wm.expose_active = active;
}
/// Set border colors on all visible windows.
@@ -554,3 +714,42 @@ fn set_borders(state: &mut AppState) {
}
}
}
+
+/// Set opacity on all visible windows.
+/// This modifies rendering state and is called during RenderStart.
+/// Opacity is applied with the next render_finish.
+pub fn render_opacity(state: &mut AppState) {
+ let focused_id = state.wm.seats.iter()
+ .find(|s| !s.removed)
+ .and_then(|s| s.focused_window_id);
+
+ for win in &state.wm.windows {
+ if win.closed {
+ continue;
+ }
+ if let Some(wp) = state.get_window_proxy(win.id) {
+ let is_cascade = win.tiling_mode == TilingMode::Cascade;
+ let is_exposed = state.wm.expose_visual_active && win.tiling_mode != TilingMode::Popup && win.app_id.as_deref() != Some("clear-status-interface");
+ let was_animating = win.anim_opacity.is_some();
+ let should_fade = is_cascade || is_exposed || was_animating;
+
+ let opacity = if should_fade {
+ win.anim_opacity.unwrap_or_else(|| {
+ if is_exposed || is_cascade {
+ if Some(win.id) == focused_id {
+ 1.0
+ } else {
+ 0.75
+ }
+ } else {
+ 1.0
+ }
+ })
+ } else {
+ 1.0
+ };
+ let op_u32 = (opacity * u32::MAX as f64).round() as u32;
+ wp.river_window.set_opacity(op_u32);
+ }
+ }
+}
diff --git a/start-river.sh b/start-river.sh
index 81e451e..ce87e56 100755
--- a/start-river.sh
+++ b/start-river.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# Launch river with clearwm on this TTY
+# Launch river with ccec on this TTY
# Usage: Switch to a free TTY, log in, and run this script
LOGGING=false
@@ -15,26 +15,26 @@ export XCURSOR_THEME="crosshair-theme"
export XCURSOR_SIZE=24
export XCURSOR_PATH="/home/lsgalante/.local/share/icons:/home/lsgalante/.icons:/usr/share/icons"
-# Create the River init executable (clearwm launch script)
+# Create the River init executable (ccec launch script)
# This must exist before River starts, and /tmp is cleared on reboot.
if [ "$LOGGING" = true ]; then
- cat > /tmp/clearwm-rs-launch-river.sh << 'LAUNCH_EOF'
+ cat > /tmp/ccec-launch-river.sh << 'LAUNCH_EOF'
#!/bin/sh
-exec /home/lsgalante/.local/bin/clearwm 2>/tmp/clearwm-${WAYLAND_DISPLAY}.log
+exec /home/lsgalante/.local/bin/ccec 2>/tmp/ccec-${WAYLAND_DISPLAY}.log
LAUNCH_EOF
else
- cat > /tmp/clearwm-rs-launch-river.sh << 'LAUNCH_EOF'
+ cat > /tmp/ccec-launch-river.sh << 'LAUNCH_EOF'
#!/bin/sh
-exec /home/lsgalante/.local/bin/clearwm
+exec /home/lsgalante/.local/bin/ccec
LAUNCH_EOF
fi
-chmod +x /tmp/clearwm-rs-launch-river.sh
+chmod +x /tmp/ccec-launch-river.sh
-echo "Starting river with clearwm..."
+echo "Starting river with ccec..."
if [ "$LOGGING" = true ]; then
- echo "Logs: /tmp/river-clearwm.log + /tmp/clearwm-${WAYLAND_DISPLAY}.log"
+ echo "Logs: /tmp/river-ccec.log + /tmp/ccec-${WAYLAND_DISPLAY}.log"
else
echo "Logging disabled. Use --logging to enable."
fi
-exec river -c /tmp/clearwm-rs-launch-river.sh 2>/tmp/river-clearwm.log
+exec river -c /tmp/ccec-launch-river.sh 2>/tmp/river-ccec.log