Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Update Wayland client window decorations, borders, and restart handling
src/borders.rs | 4 ++++
src/decorations.rs | 2 +-
src/main.rs | 6 ++++--
src/restart.rs | 42 +++++++++++++++++++++++++++++++++++++-----
src/wayland.rs | 27 ++++++++++++++++++++-------
src/wm.rs | 42 ++++++++++++++++++++++++++++++++++++++++++
6 files changed, 108 insertions(+), 15 deletions(-)
diff --git a/src/borders.rs b/src/borders.rs
index f08bd75..6569861 100644
--- a/src/borders.rs
+++ b/src/borders.rs
@@ -76,6 +76,7 @@ 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,
@@ -83,6 +84,7 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
state.layout.border_g,
state.layout.border_b,
);
+ */
let (r, g, b, a) = if win.tiling_mode == TilingMode::Popup {
// Popup windows have a transparent border
@@ -150,10 +152,12 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
all_edges
};
+ /*
eprintln!(
"[borders] -> r=#{:08x} g=#{:08x} b=#{:08x} a=#{:08x} width={}",
r, g, b, a, width,
);
+ */
results.push(WindowBorders {
window_idx: idx,
diff --git a/src/decorations.rs b/src/decorations.rs
index 6dd4e7e..929cbff 100644
--- a/src/decorations.rs
+++ b/src/decorations.rs
@@ -567,7 +567,7 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
|| w.tiling_mode == crate::types::TilingMode::Fullscreen
|| w.circular;
let needs_sides = !should_not_decorate && !is_minimized;
- eprintln!("[decorations] window {} minimized={} needs_sides={} dec_right_exists={}", w.id, is_minimized, needs_sides, wp.dec_right.is_some());
+ // eprintln!("[decorations] window {} minimized={} needs_sides={} dec_right_exists={}", w.id, is_minimized, needs_sides, wp.dec_right.is_some());
if should_not_decorate {
if let Some(dec) = wp.decoration.take() {
diff --git a/src/main.rs b/src/main.rs
index dc9da97..6835448 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,15 +21,15 @@ fn log_death(msg: &str) {
.open(paths::get_death_log_path())
{
let _ = writeln!(f, "{}", msg);
+ let _ = f.sync_all();
}
- eprintln!("{}", msg);
+ let _ = writeln!(std::io::stderr(), "{}", msg);
}
fn main() {
// Install a panic hook that writes to a separate log file before aborting.
std::panic::set_hook(Box::new(|info| {
let msg = format!("[PANIC] {}", info);
- eprintln!("{}", msg);
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
@@ -37,7 +37,9 @@ fn main() {
.open(paths::get_death_log_path())
{
let _ = writeln!(f, "{}", msg);
+ let _ = f.sync_all();
}
+ let _ = writeln!(std::io::stderr(), "{}", msg);
}));
eprintln!("cce-client starting...");
diff --git a/src/restart.rs b/src/restart.rs
index 2481bcf..9cd1f2f 100644
--- a/src/restart.rs
+++ b/src/restart.rs
@@ -91,10 +91,15 @@ pub fn wm_restart() {
} else {
std::env::current_exe().unwrap_or_else(|_| std::process::exit(1))
};
- eprintln!("wm_restart: exe_path={}", exe_path.display());
+ let _ = std::io::Write::write_fmt(&mut std::io::stderr(), format_args!("wm_restart: exe_path={}\n", exe_path.display()));
let path_cstr = std::ffi::CString::new(exe_path.to_string_lossy().into_owned())
.unwrap_or_else(|_| std::process::exit(1));
+ // Prepare log path CString in the parent process before fork to be async-signal-safe.
+ let log_path = paths::get_log_path();
+ let log_path_cstr = std::ffi::CString::new(log_path)
+ .unwrap_or_else(|_| std::process::exit(1));
+
// Fork: child waits for parent to die, then execs fresh cce-client.
// Parent exits so River tears down the old Wayland connection.
let pid = unsafe { libc::fork() };
@@ -133,9 +138,35 @@ pub fn wm_restart() {
}
}
+ // Reopen standard input to /dev/null to ensure a clean stdin fd
+ let null_fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const libc::c_char, libc::O_RDONLY) };
+ if null_fd >= 0 {
+ unsafe {
+ libc::dup2(null_fd, 0);
+ libc::close(null_fd);
+ }
+ }
+
+ // Reopen standard output and error to the log file to prevent EPIPE panics when parent shell/session exits
+ let log_fd = unsafe {
+ libc::open(
+ log_path_cstr.as_ptr(),
+ libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND,
+ 0o644,
+ )
+ };
+ if log_fd >= 0 {
+ unsafe {
+ libc::dup2(log_fd, 1);
+ libc::dup2(log_fd, 2);
+ libc::close(log_fd);
+ }
+ }
+
// Exec the same binary — replaces this process with a fresh cce-client.
// Retry up to 3 times with a short delay — the binary may be temporarily
// unavailable if cargo build is replacing it mid-write (atomic rename).
+ use std::io::Write;
for attempt in 0..3 {
let ret = unsafe {
libc::execl(
@@ -146,7 +177,8 @@ pub fn wm_restart() {
};
let errno = unsafe { *libc::__errno_location() };
if attempt < 2 {
- eprintln!(
+ let _ = writeln!(
+ std::io::stderr(),
"wm_restart: execl attempt {} failed (errno={} {}), retrying in 500ms...",
attempt + 1,
errno,
@@ -156,7 +188,8 @@ pub fn wm_restart() {
let _ = ret; // suppress unused
} else {
// Final attempt failed — log and exit
- eprintln!(
+ let _ = writeln!(
+ std::io::stderr(),
"wm_restart: execl failed after 3 attempts! errno={} ({})",
errno,
std::io::Error::from_raw_os_error(errno)
@@ -166,7 +199,6 @@ pub fn wm_restart() {
.append(true)
.open(paths::get_death_log_path())
{
- use std::io::Write;
let _ = writeln!(
f,
"child: execl failed after 3 attempts! errno={} ({}) path={}",
@@ -223,7 +255,7 @@ pub fn wm_reload(state: &mut WindowManager) {
match parse_config(&config_path, false, state) {
Ok(_) => {
for cmd in &state.reload_commands {
- eprintln!("[reload] executing reload command: {}", cmd);
+ let _ = std::io::Write::write_fmt(&mut std::io::stderr(), format_args!("[reload] executing reload command: {}\n", cmd));
spawn_command_bg(cmd);
}
if state.notifications_enable {
diff --git a/src/wayland.rs b/src/wayland.rs
index 0aa3e9d..75e5e44 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -900,7 +900,14 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
.wm
.windows
.iter()
- .filter(|w| w.is_new && !w.closed && (w.tags & active_tags) != 0 && w.app_id.as_deref() != Some("cce-status-interface") && w.app_id.as_deref() != Some("clear-notification-daemon"))
+ .filter(|w| {
+ w.is_new
+ && !w.closed
+ && (w.tags & active_tags) != 0
+ && w.app_id.as_deref() != Some("cce-status-interface")
+ && w.app_id.as_deref() != Some("clear-notification-daemon")
+ && w.app_id.as_deref() != Some("cce-notification-daemon")
+ })
.map(|w| w.id)
.last();
if let Some(new_id) = new_focused_id {
@@ -1021,17 +1028,19 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
eprintln!("[manage] FATAL: flush after manage_finish failed: {:?}", e);
}
state.wm.in_manage_sequence = false;
- eprintln!("[manage] ManageStart done in {:?}", ms_start.elapsed());
+ // eprintln!("[manage] ManageStart done in {:?}", ms_start.elapsed());
// NOTE: Do NOT call update_status_files() here — it calls
// pkill with .output() which blocks the event loop.
}
river_window_manager_v1::Event::RenderStart => {
state.render_count += 1;
+ /*
eprintln!(
"[render] RenderStart #{} needs_render={}",
state.render_count, state.wm.needs_render
);
+ */
// Show/hide windows based on tag visibility.
// This is rendering state and must happen during a render sequence.
@@ -1123,10 +1132,12 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
nodes_to_place.sort_by_key(|&(score, idx, _, _, _)| (score, idx));
for &(score, _, id, ref app_id, node) in &nodes_to_place {
+ /*
eprintln!(
"[render] placing node id={} (app_id={:?}) at top with score {}",
id, app_id, score
);
+ */
node.place_top();
}
@@ -1143,7 +1154,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
state.render_count, e
);
}
- eprintln!("[render] render_finish #{} flushed", state.render_count);
+ // eprintln!("[render] render_finish #{} flushed", state.render_count);
// Spawn startup apps inside the callback, like tinyrwm does.
// Spawning between blocking_dispatch calls corrupts the Wayland
// connection state because the fork inherits the socket fd.
@@ -1321,11 +1332,11 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
river_window_v1::Event::Dimensions { width, height } => {
if let Some(window) = state.wm.get_window_mut(wid) {
let changed = window.width != width || window.height != height;
- eprintln!(
- "[window] id={} (app_id={:?}) Event::Dimensions: {}x{} (was {}x{}) changed={}",
- wid, window.app_id, width, height, window.width, window.height, changed
- );
if changed {
+ eprintln!(
+ "[window] id={} (app_id={:?}) Event::Dimensions: {}x{} (was {}x{})",
+ wid, window.app_id, width, height, window.width, window.height
+ );
window.width = width;
window.height = height;
state.wm.needs_render = true;
@@ -2992,11 +3003,13 @@ fn apply_pending_bindings(state: &mut AppState, qhandle: &QueueHandle<AppState>)
// Apply xkb bindings to each seat
let bindings: Vec<_> = state.wm.pending_bindings.drain(..).collect();
+ /*
eprintln!(
"[bindings] applying {} xkb bindings to {} seats",
bindings.len(),
state.seat_proxies.len()
);
+ */
for pb in &bindings {
for (sid, sp) in &state.seat_proxies {
if let Some(ref xb) = state.xkb_bindings {
diff --git a/src/wm.rs b/src/wm.rs
index f280d9e..23294ae 100644
--- a/src/wm.rs
+++ b/src/wm.rs
@@ -22,6 +22,9 @@ pub fn get_mode_for_window(wm: &WindowManager, win: &Window) -> Option<TilingMod
if win.app_id.as_deref() == Some("cce-status-interface") {
return Some(TilingMode::Fullscreen);
}
+ if win.app_id.as_deref() == Some("cce-notification-daemon") || win.app_id.as_deref() == Some("clear-notification-daemon") {
+ return Some(TilingMode::Popup);
+ }
// If the user manually locked the mode (via set-mode, fullscreen toggle, etc.),
// don't override it.
@@ -157,6 +160,8 @@ pub fn assign_window_modes(wm: &mut WindowManager) {
for win in &mut wm.windows {
if win.is_new
&& win.app_id.as_deref() != Some("cce-status-interface")
+ && win.app_id.as_deref() != Some("cce-notification-daemon")
+ && win.app_id.as_deref() != Some("clear-notification-daemon")
&& !win.has_parent
&& !matches_mode_rule(mode_rules, win)
{
@@ -227,6 +232,7 @@ struct TileResult {
pub fn manage_windows(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
let (screen_w, screen_h, phys_w, phys_h, phys_x, phys_y) = get_screen_geometry(&state.wm);
+ /*
eprintln!(
"[manage] windows={} outputs={} screen={}x{} (phys={}x{} at {},{})",
state.wm.windows.len(),
@@ -238,6 +244,7 @@ pub fn manage_windows(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
phys_x,
phys_y
);
+ */
// Ensure each window has a river_node_v1 proxy for positioning
ensure_window_nodes(state, qhandle);
@@ -1152,6 +1159,41 @@ mod tests {
assert_eq!(win2.tiling_mode, TilingMode::Fullscreen);
}
+ #[test]
+ fn test_assign_window_modes_notification_daemon_no_inherit() {
+ let mut wm = WindowManager::default();
+ wm.global_layout = TilingMode::Grid;
+ wm.tag_layouts[0] = TilingMode::Grid;
+
+ // Spawn a focused window that is in Grid mode
+ wm.windows.push(Window {
+ id: 1,
+ tiling_mode: TilingMode::Grid,
+ is_new: false,
+ ..Default::default()
+ });
+ wm.seats.push(Seat {
+ id: 1,
+ focused_window_id: Some(1),
+ ..Default::default()
+ });
+
+ // Spawn a new notification daemon window
+ wm.windows.push(Window {
+ id: 2,
+ is_new: true,
+ app_id: Some("cce-notification-daemon".to_string()),
+ ..Default::default()
+ });
+
+ assign_window_modes(&mut wm);
+
+ // Window 2 should get Popup mode (hardcoded default for notification daemon) and not inherit Grid
+ let win2 = wm.get_window(2).unwrap();
+ assert_eq!(win2.tiling_mode, TilingMode::Popup);
+ assert!(!win2.mode_locked);
+ }
+
#[test]
fn test_expose_mode_sorting() {
let mut wm = WindowManager::default();