Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: add IPC server and title-based fallback matching for window rules
.gitignore | 3 ++
src/borders.rs | 13 ++++++++
src/ipc.rs | 1 +
src/ipc_server.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib.rs | 1 +
src/main.rs | 29 +++++++++++++++++
src/wayland.rs | 9 +++++-
src/wm.rs | 12 ++++++-
8 files changed, 159 insertions(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore
index ea8c4bf..95c7048 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
/target
+.antigravitycli/
+.gemini/
+
diff --git a/src/borders.rs b/src/borders.rs
index 6647ada..be0d493 100644
--- a/src/borders.rs
+++ b/src/borders.rs
@@ -77,6 +77,14 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
let is_focused = focused_id.map_or(false, |fid| win.id == fid);
+ eprintln!(
+ "[borders] win={} app_id={:?} is_focused={} border_r=#{:08x} border_g=#{:08x} border_b=#{:08x}",
+ win.id, win.app_id, is_focused,
+ state.layout.border_r,
+ state.layout.border_g,
+ state.layout.border_b,
+ );
+
let (r, g, b, a) = if is_focused {
// Focused window: pure border color
(
@@ -108,6 +116,11 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
TilingMode::Floating => state.layout.floating_border_width,
};
+ eprintln!(
+ "[borders] -> r=#{:08x} g=#{:08x} b=#{:08x} a=#{:08x} width={}",
+ r, g, b, a, width,
+ );
+
results.push(WindowBorders {
window_idx: idx,
edges: all_edges,
diff --git a/src/ipc.rs b/src/ipc.rs
index d9d210c..fdd370c 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -280,6 +280,7 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
}
_ => {}
}
+ state.needs_render = true;
}
/// Handle "mode <mode> <app_id_pattern> [--single] [--tag N] [title_pattern]" command
diff --git a/src/ipc_server.rs b/src/ipc_server.rs
new file mode 100644
index 0000000..4a07a07
--- /dev/null
+++ b/src/ipc_server.rs
@@ -0,0 +1,93 @@
+use std::io::Read;
+use std::os::unix::net::{UnixListener, UnixStream};
+use std::sync::mpsc;
+
+pub const IPC_SOCKET_PATH: &str = "/tmp/clearwm.sock";
+
+pub struct IpcReceiver {
+ pub rx: mpsc::Receiver<String>,
+}
+
+pub fn spawn_ipc_server() -> IpcReceiver {
+ let (tx, rx) = mpsc::channel::<String>();
+
+ std::thread::Builder::new()
+ .name("clearwm-ipc".into())
+ .spawn(move || {
+ ipc_server_main(tx);
+ })
+ .expect("failed to spawn IPC server thread");
+
+ IpcReceiver { rx }
+}
+
+fn ipc_server_main(tx: mpsc::Sender<String>) {
+ let _ = std::fs::remove_file(IPC_SOCKET_PATH);
+
+ let listener = match UnixListener::bind(IPC_SOCKET_PATH) {
+ Ok(l) => l,
+ Err(e) => {
+ eprintln!("[ipc] failed to bind {}: {}", IPC_SOCKET_PATH, e);
+ return;
+ }
+ };
+
+ if let Err(e) = listener.set_nonblocking(true) {
+ eprintln!("[ipc] failed to set non-blocking: {}", e);
+ return;
+ }
+
+ eprintln!("[ipc] listening on {}", IPC_SOCKET_PATH);
+
+ let mut streams: Vec<UnixStream> = Vec::new();
+
+ loop {
+ for _ in 0..5 {
+ match listener.accept() {
+ Ok((stream, _addr)) => {
+ if let Err(e) = stream.set_nonblocking(true) {
+ eprintln!("[ipc] failed to set non-blocking on client: {}", e);
+ continue;
+ }
+ eprintln!("[ipc] new connection");
+ streams.push(stream);
+ }
+ Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
+ Err(e) => {
+ eprintln!("[ipc] accept error: {}", e);
+ break;
+ }
+ }
+ }
+
+ let mut dead = Vec::new();
+ for (i, stream) in streams.iter_mut().enumerate() {
+ let mut buf = [0u8; 4096];
+ match stream.read(&mut buf) {
+ Ok(0) => {
+ dead.push(i);
+ }
+ Ok(n) => {
+ let s = String::from_utf8_lossy(&buf[..n]);
+ for line in s.lines() {
+ let cmd = line.trim().to_string();
+ if !cmd.is_empty() {
+ let _ = tx.send(cmd);
+ }
+ }
+ }
+ Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
+ Err(e) => {
+ eprintln!("[ipc] read error: {}", e);
+ dead.push(i);
+ }
+ }
+ }
+
+ for i in dead.into_iter().rev() {
+ streams.remove(i);
+ }
+
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 6f49b62..84a0af0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@
pub mod borders;
pub mod config;
pub mod ipc;
+pub mod ipc_server;
pub mod protocol;
pub mod restart;
pub mod state;
diff --git a/src/main.rs b/src/main.rs
index 2f667b9..429650e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,8 @@
// 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;
@@ -85,6 +87,9 @@ fn main() {
}
};
+ // Start the IPC server thread (for clearctl and clear-system-interface)
+ let ipc_rx = ipc_server::spawn_ipc_server().rx;
+
// Store the status sender in the app state so RenderStart can push updates
state.status_sender = Some(status_sender);
@@ -157,6 +162,30 @@ fn main() {
break;
}
eprintln!("[main] flush ok, looping");
+
+ // Process pending IPC commands from the socket
+ 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);
+ ipc_commands = true;
+ }
+ Err(std::sync::mpsc::TryRecvError::Empty) => break,
+ Err(std::sync::mpsc::TryRecvError::Disconnected) => {
+ eprintln!("[main] IPC server disconnected");
+ break;
+ }
+ }
+ }
+ // Force a render sequence so the new rendering state (border
+ // colors, widths, etc.) is sent to River and displayed.
+ if ipc_commands {
+ if let Some(ref wm) = state.window_manager {
+ wm.manage_dirty();
+ }
+ }
}
Err(e) => {
log_death(&format!(
diff --git a/src/wayland.rs b/src/wayland.rs
index 18e7628..4874ed1 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -1850,11 +1850,18 @@ fn enforce_single_instance(wm: &mut WindowManager) {
if window.closed {
continue;
}
+ let has_app_id = window.app_id.as_deref().map_or(false, |s| !s.is_empty());
let match_app = rule.app_id_pattern == "*"
|| window
.app_id
.as_deref()
- .map_or(false, |aid| aid.contains(&rule.app_id_pattern));
+ .map_or(false, |aid| aid.contains(&rule.app_id_pattern))
+ || (!has_app_id && window.title.as_deref().map_or(false, |t| {
+ let normalize = |s: &str| -> String {
+ s.to_lowercase().replace(|c: char| c == '-' || c == '_', " ")
+ };
+ normalize(t).contains(&normalize(&rule.app_id_pattern))
+ }));
let match_title = rule.title_pattern.as_deref() == Some("*")
|| rule.title_pattern.is_none()
|| window.title.as_deref().map_or(false, |t| {
diff --git a/src/wm.rs b/src/wm.rs
index f9c0fda..7d4241a 100644
--- a/src/wm.rs
+++ b/src/wm.rs
@@ -32,11 +32,21 @@ pub fn get_mode_for_window(wm: &WindowManager, win: &Window) -> Option<TilingMod
// 1. Check mode_rules for a match on app_id/title
for rule in &wm.mode_rules {
+ let has_app_id = win.app_id.as_deref().map_or(false, |s| !s.is_empty());
let match_app = rule.app_id_pattern == "*"
|| win
.app_id
.as_deref()
- .map_or(false, |aid| aid.contains(&rule.app_id_pattern));
+ .map_or(false, |aid| aid.contains(&rule.app_id_pattern))
+ // Fallback: if the window has no app_id (None or empty), try
+ // matching the app_id_pattern against the window title. This
+ // handles apps that never set a Wayland app_id (e.g. clear-colors).
+ || (!has_app_id && win.title.as_deref().map_or(false, |t| {
+ let normalize = |s: &str| -> String {
+ s.to_lowercase().replace(|c: char| c == '-' || c == '_', " ")
+ };
+ normalize(t).contains(&normalize(&rule.app_id_pattern))
+ }));
let match_title = rule.title_pattern.as_deref() == Some("*")
|| rule.title_pattern.is_none()
|| win.title.as_deref().map_or(false, |t| {