git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitd91a0042888623936ad63f51ffc1c0a4138ed258
parent5c731261c9
authorLucas Galante <[email protected]>
date2026-06-10 22:00
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();