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

commit8f634998733b77334c4c08b31ad50f6648fcefc8
parentd91a004288
authorLucas Galante <[email protected]>
date2026-06-13 21:29
Refactor client window manager: update Wayland, input, and decorations handling

 src/borders.rs     |  83 +++++++-----
 src/clearctl.rs    |   1 +
 src/config.rs      |  54 ++++++++
 src/decorations.rs | 106 ++++++++++++++-
 src/input.rs       |   4 +
 src/ipc.rs         |  37 +++++
 src/main.rs        |  21 ++-
 src/types.rs       |  34 +++++
 src/wayland.rs     | 358 +++++++++++++++++++++++++++++++++++-------------
 src/wm.rs          | 392 ++++++++++++++++++++++++++++++++++++++++++++++-------
 start-river.sh     |   7 +-
 11 files changed, 894 insertions(+), 203 deletions(-)

diff --git a/src/borders.rs b/src/borders.rs
index 6569861..5fbc3f7 100644
--- a/src/borders.rs
+++ b/src/borders.rs
@@ -51,7 +51,6 @@ pub struct WindowBorders {
 /// background_color and border_color based on stack depth.
 pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
     let mut results = Vec::new();
-    let all_edges = 0b1111u32;
 
     // Find the focused window ID from the first non-removed seat
     let focused_id = state
@@ -86,7 +85,7 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
         );
         */
 
-        let (r, g, b, a) = if win.tiling_mode == TilingMode::Popup {
+        let (r, g, b, mut a) = if win.tiling_mode == TilingMode::Popup {
             // Popup windows have a transparent border
             (0, 0, 0, 0)
         } else if is_focused {
@@ -118,39 +117,15 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
             (r, g, b, a)
         };
 
-        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::Floating => state.layout.floating_border_width,
-                TilingMode::Popup => 0,
-                TilingMode::SidePanel => state.layout.cascade_border_width,
-            }
-        };
-
-        if win.app_id.as_deref() == Some("cce-status-interface")
-            || win.app_id.as_deref().map_or(false, |aid| aid.contains("noborder"))
-        {
-            width = 0;
+        if win.tiling_mode == TilingMode::SidePanel {
+            let opacity_factor = state.layout.side_panel_border_opacity as f64 / 100.0;
+            let base_alpha = (a & 0xFF) as f64 * opacity_factor;
+            let val = base_alpha.round().clamp(0.0, 255.0) as u8;
+            a = val as u32 * 0x01010101;
         }
 
-        let has_titlebar = !win.closed
-            && !win.minimized
-            && win.app_id.as_deref() != Some("cce-status-interface")
-            && !win.app_id.as_deref().map_or(false, |aid| aid.contains("noborder"))
-            && win.tiling_mode != TilingMode::Popup
-            && win.tiling_mode != TilingMode::Fullscreen
-            && !win.circular;
-
-        let edges = if has_titlebar {
-            // Disable compositor-drawn borders entirely; all borders are drawn by client decorations.
-            0b0000u32
-        } else {
-            all_edges
-        };
+        let edges = 0b0000u32;
+        let width = 0;
 
         /*
         eprintln!(
@@ -176,6 +151,7 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::types::{Window, ModeRule};
 
     #[test]
     fn test_interp_channel_depth0() {
@@ -190,4 +166,45 @@ mod tests {
         let expected = ((0x90 as f64 * 0.80) as u8) as u32 * 0x01010101;
         assert_eq!(result, expected);
     }
+
+    #[test]
+    fn test_border_edges_with_decorations() {
+        let mut wm = WindowManager::default();
+        wm.active_tags = 1;
+
+        // Window with client decorations OK (should have edges = 0b0000)
+        wm.windows.push(Window {
+            id: 1,
+            tiling_mode: TilingMode::Cascade,
+            tags: 1,
+            ..Default::default()
+        });
+
+        // Window with client decorations disabled via mode rule
+        wm.mode_rules.push(ModeRule {
+            mode: TilingMode::Cascade,
+            app_id_pattern: "no-decorations".to_string(),
+            title_pattern: None,
+            single_instance: false,
+            tag: 0,
+            circular: false,
+            ssd: Some(false), // Disable CSD
+        });
+        wm.windows.push(Window {
+            id: 2,
+            tiling_mode: TilingMode::Cascade,
+            app_id: Some("no-decorations".to_string()),
+            tags: 1,
+            ..Default::default()
+        });
+
+        let results = compute_border_colors(&wm);
+        assert_eq!(results.len(), 2);
+
+        // Window 1: no compositor-drawn borders
+        assert_eq!(results[0].edges, 0);
+
+        // Window 2: no compositor-drawn borders either (all borders are handled in client)
+        assert_eq!(results[1].edges, 0);
+    }
 }
diff --git a/src/clearctl.rs b/src/clearctl.rs
index 25bb34e..b40d0b5 100644
--- a/src/clearctl.rs
+++ b/src/clearctl.rs
@@ -56,6 +56,7 @@ fn usage(name: &str, to_stderr: bool) {
     print("  pointer-location");
     print("  pointer-move-to <x> <y>");
     print("  pointer-move-by <dx> <dy>");
+    print("  pointer-scroll <dx> <dy>");
     print("  pointer-click <button>");
     print("  pointer-press <button>");
     print("  pointer-release <button>");
diff --git a/src/config.rs b/src/config.rs
index abb41be..a5d118d 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -88,6 +88,12 @@ pub struct LayoutConfig {
     pub side_panel_behavior: String,
     #[serde(default = "default_side_panel_width")]
     pub side_panel_width: i64,
+    #[serde(default = "default_side_panel_position")]
+    pub side_panel_position: String,
+    #[serde(default = "default_side_panel_border_gap")]
+    pub side_panel_border_gap: i64,
+    #[serde(default = "default_side_panel_border_opacity")]
+    pub side_panel_border_opacity: i64,
 }
 
 impl Default for LayoutConfig {
@@ -114,6 +120,9 @@ impl Default for LayoutConfig {
             window_blur: default_window_blur(),
             side_panel_behavior: default_side_panel_behavior(),
             side_panel_width: default_side_panel_width(),
+            side_panel_position: default_side_panel_position(),
+            side_panel_border_gap: default_side_panel_border_gap(),
+            side_panel_border_opacity: default_side_panel_border_opacity(),
         }
     }
 }
@@ -138,6 +147,18 @@ fn default_side_panel_width() -> i64 {
     360
 }
 
+fn default_side_panel_position() -> String {
+    "left".to_string()
+}
+
+fn default_side_panel_border_gap() -> i64 {
+    0
+}
+
+fn default_side_panel_border_opacity() -> i64 {
+    100
+}
+
 fn default_gap() -> i64 {
     48
 }
@@ -291,6 +312,7 @@ pub struct ModeRuleConfig {
     pub single: Option<bool>,
     pub tag: Option<i64>,
     pub circular: Option<bool>,
+    pub ssd: Option<bool>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -341,6 +363,9 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
     state.layout.window_blur = config.layout.window_blur;
     state.layout.side_panel_behavior = config.layout.side_panel_behavior.clone();
     state.layout.side_panel_width = config.layout.side_panel_width as i32;
+    state.layout.side_panel_position = config.layout.side_panel_position.clone();
+    state.layout.side_panel_border_gap = config.layout.side_panel_border_gap as i32;
+    state.layout.side_panel_border_opacity = config.layout.side_panel_border_opacity as i32;
     if let Some((r, g, b, a)) = parse_hex_color(&config.layout.border_color) {
         state.layout.border_r = r;
         state.layout.border_g = g;
@@ -374,6 +399,31 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
         });
     }
 
+    // Register default super+left and super+right bindings for side panel position if not overridden
+    let super_mod = parse_modifiers("super");
+    let left_sym = parse_keysym("Left");
+    let right_sym = parse_keysym("Right");
+
+    let has_super_left = state.pending_bindings.iter().any(|b| b.mods == super_mod && b.keysym == left_sym);
+    if !has_super_left {
+        state.pending_bindings.push(PendingXkbBinding {
+            mods: super_mod,
+            keysym: left_sym,
+            action: crate::types::Action::SidePanelLeft,
+            command: None,
+        });
+    }
+
+    let has_super_right = state.pending_bindings.iter().any(|b| b.mods == super_mod && b.keysym == right_sym);
+    if !has_super_right {
+        state.pending_bindings.push(PendingXkbBinding {
+            mods: super_mod,
+            keysym: right_sym,
+            action: crate::types::Action::SidePanelRight,
+            command: None,
+        });
+    }
+
     // [[pointer_bind]] array
     for pb in &config.pointer_bind {
         let mods = parse_modifiers(&pb.mods);
@@ -398,6 +448,7 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
             single_instance: mr.single.unwrap_or(false),
             tag,
             circular: mr.circular.unwrap_or(false),
+            ssd: mr.ssd,
         });
     }
 
@@ -716,6 +767,9 @@ mod tests {
         assert_eq!(lc.grid_gap, 18);
         assert_eq!(lc.side_panel_width, 360);
         assert_eq!(lc.side_panel_behavior, "inline");
+        assert_eq!(lc.side_panel_position, "left");
+        assert_eq!(lc.side_panel_border_gap, 0);
+        assert_eq!(lc.side_panel_border_opacity, 100);
     }
 
     #[test]
diff --git a/src/decorations.rs b/src/decorations.rs
index 929cbff..e7beb5d 100644
--- a/src/decorations.rs
+++ b/src/decorations.rs
@@ -616,7 +616,9 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
         .windows
         .iter()
         .enumerate()
-        .filter(|(_, w)| !w.closed && w.app_id.as_deref() != Some("cce-status-interface") && !w.circular && !w.app_id.as_deref().map_or(false, |aid| aid.contains("noborder")) && (w.minimized || (w.tiling_mode != crate::types::TilingMode::Popup && w.tiling_mode != crate::types::TilingMode::Fullscreen)))
+        .filter(|(_, w)| {
+            !w.closed && w.app_id.as_deref() != Some("cce-status-interface") && !w.circular && !w.app_id.as_deref().map_or(false, |aid| aid.contains("noborder")) && (w.minimized || (w.tiling_mode != crate::types::TilingMode::Popup && w.tiling_mode != crate::types::TilingMode::Fullscreen))
+        })
         .filter(|(_, w)| (w.tags & active_tags) != 0)
         .map(|(idx, w)| {
             let is_minimized = w.minimized;
@@ -711,8 +713,8 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
             };
             DecorateInfo {
                 id: w.id,
-                win_width: w.width,
-                win_height: w.height,
+                win_width: if w.committed_width > 0 { w.committed_width } else { w.width },
+                win_height: if w.committed_height > 0 { w.committed_height } else { w.height },
                 border_width,
                 title: title_with_idx,
                 bg_color,
@@ -1027,7 +1029,74 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
                         }
                     }
 
-                    buffer_slice[row_offset + px as usize] = ((a as u32) << 24) | (r_val << 16) | (g << 8) | b;
+                    let base_pixel_color = ((a as u32) << 24) | (r_val << 16) | (g << 8) | b;
+                    let mut final_color = base_pixel_color;
+
+                    let lx = px as f32 / scale as f32;
+                    let ly = py as f32 / scale as f32;
+
+                    if !is_minimized && ly >= 0.0 && ly < logical_height as f32 {
+                        if lx >= (logical_width as f32 - 48.0) && lx < (logical_width as f32 - 32.0) {
+                            // Minimize button
+                            let is_hovered = hover_top.map_or(false, |(hx, hy)| {
+                                hx >= (logical_width as f64 - 48.0) && hx < (logical_width as f64 - 32.0)
+                                    && hy >= 0.0 && hy < logical_height as f64
+                            });
+                            final_color = if is_hovered {
+                                blend_colors(base_pixel_color, 0x30FFFFFF)
+                            } else {
+                                base_pixel_color
+                            };
+
+                            let cx = logical_width as f32 - 40.0;
+                            let cy = logical_height as f32 / 2.0;
+                            if lx >= (cx - 4.0) && lx <= (cx + 4.0) && ly >= (cy + 2.0) && ly < (cy + 3.0) {
+                                final_color = text_color;
+                            }
+                        } else if lx >= (logical_width as f32 - 32.0) && lx < (logical_width as f32 - 16.0) {
+                            // Maximize button
+                            let is_hovered = hover_top.map_or(false, |(hx, hy)| {
+                                hx >= (logical_width as f64 - 32.0) && hx < (logical_width as f64 - 16.0)
+                                    && hy >= 0.0 && hy < logical_height as f64
+                            });
+                            final_color = if is_hovered {
+                                blend_colors(base_pixel_color, 0x30FFFFFF)
+                            } else {
+                                base_pixel_color
+                            };
+
+                            let cx = logical_width as f32 - 24.0;
+                            let cy = logical_height as f32 / 2.0;
+                            let is_border_x = (lx >= cx - 4.0 && lx < cx - 3.0) || (lx > cx + 3.0 && lx <= cx + 4.0);
+                            let is_in_x = lx >= cx - 4.0 && lx <= cx + 4.0;
+                            let is_border_y = (ly >= cy - 4.0 && ly < cy - 3.0) || (ly > cy + 3.0 && ly <= cy + 4.0);
+                            let is_in_y = ly >= cy - 4.0 && ly <= cy + 4.0;
+                            if (is_border_x && is_in_y) || (is_border_y && is_in_x) {
+                                final_color = text_color;
+                            }
+                        } else if lx >= (logical_width as f32 - 16.0) && lx <= logical_width as f32 {
+                            // Close button
+                            let is_hovered = hover_top.map_or(false, |(hx, hy)| {
+                                hx >= (logical_width as f64 - 16.0) && hx <= logical_width as f64
+                                    && hy >= 0.0 && hy < logical_height as f64
+                            });
+                            final_color = if is_hovered {
+                                blend_colors(base_pixel_color, 0x90E53935)
+                            } else {
+                                base_pixel_color
+                            };
+
+                            let cx = logical_width as f32 - 8.0;
+                            let cy = logical_height as f32 / 2.0;
+                            let dx = (lx - cx).abs();
+                            let dy = (ly - cy).abs();
+                            if dx <= 4.0 && dy <= 4.0 && (dx - dy).abs() < 1.0 {
+                                final_color = text_color;
+                            }
+                        }
+                    }
+
+                    buffer_slice[row_offset + px as usize] = final_color;
                 }
             }
 
@@ -1058,7 +1127,7 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
                 let mut text_x = 24.0f32 * scale as f32; // Margin from left
                 for c in title.chars() {
                     let (metrics, bitmap) = font.rasterize(c, font_size);
-                    if text_x + metrics.xmin as f32 + metrics.width as f32 > dec_width as f32 {
+                    if text_x + metrics.xmin as f32 + metrics.width as f32 > dec_width as f32 - 48.0 * scale as f32 {
                         break;
                     }
 
@@ -1110,7 +1179,7 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
                 let mut text_x = 24 * scale; // Margin from left
 
                 for c in title.chars() {
-                    if text_x + 8 * drawing_scale * scale > dec_width {
+                    if text_x + 8 * drawing_scale * scale > dec_width - 48 * scale {
                         break; // Out of bounds
                     }
                     draw_char(buffer_slice, dec_width, dec_height, c, text_x, text_y, drawing_scale * scale, text_color);
@@ -1180,3 +1249,28 @@ fn is_pixel_in_highlight_region(
         _ => false,
     }
 }
+
+fn blend_colors(bg: u32, fg: u32) -> u32 {
+    let fg_a = (fg >> 24) & 0xFF;
+    if fg_a == 0 {
+        return bg;
+    }
+    if fg_a == 255 {
+        return fg;
+    }
+    let bg_a = (bg >> 24) & 0xFF;
+    let bg_r = (bg >> 16) & 0xFF;
+    let bg_g = (bg >> 8) & 0xFF;
+    let bg_b = bg & 0xFF;
+
+    let fg_r = (fg >> 16) & 0xFF;
+    let fg_g = (fg >> 8) & 0xFF;
+    let fg_b = fg & 0xFF;
+
+    let out_r = ((fg_r * fg_a) + (bg_r * (255 - fg_a))) / 255;
+    let out_g = ((fg_g * fg_a) + (bg_g * (255 - fg_a))) / 255;
+    let out_b = ((fg_b * fg_a) + (bg_b * (255 - fg_a))) / 255;
+    let out_a = bg_a + ((255 - bg_a) * fg_a) / 255;
+
+    (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b
+}
diff --git a/src/input.rs b/src/input.rs
index 1e5aeed..28b2401 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -147,6 +147,7 @@ pub enum InputDaemonMsg {
     SimulateKey { keycode: u16, press: bool },
     SimulateClick { button: u16 },
     SimulateKeyPress { keycode: u16 },
+    SimulateScroll { dx: i32, dy: i32 },
 }
 
 #[derive(Debug)]
@@ -797,6 +798,9 @@ pub fn run_input_daemon(
                                         let _ = tx_clone.send(CoordinatorMsg::InternalReleaseKey { keycode });
                                     });
                                 }
+                                InputDaemonMsg::SimulateScroll { dx, dy } => {
+                                    let _ = write_scroll(&mut uinput_mouse_file, dx, dy);
+                                }
                             }
                         }
                         CoordinatorMsg::InternalReleaseButton { button } => {
diff --git a/src/ipc.rs b/src/ipc.rs
index 2c35ac5..b09f6df 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -476,6 +476,20 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
                 reply = "error: usage: pointer-move-by <dx> <dy>\n".to_string();
             }
         }
+        "pointer-scroll" => {
+            let parts: Vec<&str> = rest.split_whitespace().collect();
+            if parts.len() == 2 {
+                if let (Ok(dx), Ok(dy)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
+                    if let Some(ref controller) = state.input_controller {
+                        let _ = controller.send(crate::input::InputDaemonMsg::SimulateScroll { dx, dy });
+                    }
+                } else {
+                    reply = "error: invalid deltas\n".to_string();
+                }
+            } else {
+                reply = "error: usage: pointer-scroll <dx> <dy>\n".to_string();
+            }
+        }
         "pointer-click" => {
             let btn = parse_button(rest) as u16;
             if btn != 0 {
@@ -722,6 +736,28 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
                 }
             }
         }
+        "side_panel_position" | "side-panel-position" => {
+            state.layout.side_panel_position = value_str.to_string();
+            if state.notifications_enable {
+                crate::config::show_notification("cce-client", &format!("Side panel position set to {}", value_str));
+            }
+        }
+        "side_panel_border_gap" | "side-panel-border-gap" => {
+            if let Ok(value) = value_str.parse::<i32>() {
+                state.layout.side_panel_border_gap = value;
+                if state.notifications_enable {
+                    crate::config::show_notification("cce-client", &format!("Side panel border gap set to {}px", value));
+                }
+            }
+        }
+        "side_panel_border_opacity" | "side-panel-border-opacity" => {
+            if let Ok(value) = value_str.parse::<i32>() {
+                state.layout.side_panel_border_opacity = value;
+                if state.notifications_enable {
+                    crate::config::show_notification("cce-client", &format!("Side panel border opacity set to {}%", value));
+                }
+            }
+        }
         _ => {}
     }
     state.needs_render = true;
@@ -774,6 +810,7 @@ fn handle_mode_command(rest: &str, state: &mut WindowManager) {
             single_instance,
             tag,
             circular: false,
+            ssd: None,
         });
     }
 }
diff --git a/src/main.rs b/src/main.rs
index 6835448..2abc99e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -167,7 +167,6 @@ 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 {
@@ -206,11 +205,12 @@ fn main() {
         ];
 
         let timeout = if state.wm.animating {
-            let elapsed = last_animation_tick.elapsed();
-            let timeout_duration = if elapsed >= std::time::Duration::from_millis(16) {
+            let elapsed = state.wm.last_animation_tick.elapsed();
+            let target = std::time::Duration::from_millis(32);
+            let timeout_duration = if elapsed >= target {
                 std::time::Duration::ZERO
             } else {
-                std::time::Duration::from_millis(16) - elapsed
+                target - elapsed
             };
             nix::poll::PollTimeout::try_from(timeout_duration).unwrap()
         } else {
@@ -252,12 +252,9 @@ 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. If animating and frame budget elapsed, request next frame (fallback only)
+        if state.wm.animating && state.wm.last_animation_tick.elapsed() >= std::time::Duration::from_millis(32) {
+            state.manage_dirty();
         }
 
         // 6. Process pending IPC commands from the socket channel
@@ -279,9 +276,7 @@ fn main() {
         }
         // Force a render sequence if we processed IPC commands
         if ipc_commands {
-            if let Some(ref wm) = state.window_manager {
-                wm.manage_dirty();
-            }
+            state.manage_dirty();
             if !state.wm.tap_config_applied && !state.libinput_devices.is_empty() {
                 let qh = event_queue.handle();
                 cce_client::wayland::apply_input_config(&mut state, &qh);
diff --git a/src/types.rs b/src/types.rs
index d115128..a0acb46 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -60,6 +60,8 @@ pub enum Action {
     SetTag4,
     Expose,
     Minimize,
+    SidePanelLeft,
+    SidePanelRight,
 }
 
 /// Layout parameters
@@ -92,6 +94,9 @@ pub struct Layout {
     pub window_blur: bool,
     pub side_panel_behavior: String,
     pub side_panel_width: i32,
+    pub side_panel_position: String,
+    pub side_panel_border_gap: i32,
+    pub side_panel_border_opacity: i32,
 }
 
 impl Default for Layout {
@@ -124,6 +129,9 @@ impl Default for Layout {
             window_blur: false,
             side_panel_behavior: "inline".to_string(),
             side_panel_width: 360,
+            side_panel_position: "left".to_string(),
+            side_panel_border_gap: 0,
+            side_panel_border_opacity: 100,
         }
     }
 }
@@ -137,6 +145,7 @@ pub struct ModeRule {
     pub single_instance: bool,
     pub tag: i32,
     pub circular: bool,
+    pub ssd: Option<bool>,
 }
 
 /// A pending keyboard binding waiting to be applied to seats
@@ -216,6 +225,8 @@ pub struct Window {
     pub y: i32,
     pub width: i32,
     pub height: i32,
+    pub committed_width: i32,
+    pub committed_height: i32,
     pub app_id: Option<String>,
     pub title: Option<String>,
     pub identifier: Option<String>,
@@ -240,6 +251,8 @@ 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,
+    /// How many ManageStart cycles we've waited for window metadata (app_id/title).
+    pub metadata_check_attempts: u8,
     pub anim_x: Option<f64>,
     pub anim_y: Option<f64>,
     pub anim_w: Option<f64>,
@@ -247,6 +260,10 @@ pub struct Window {
     pub anim_opacity: Option<f64>,
     pub circular: bool,
     pub size_hint_applied: bool,
+    pub last_borders: Option<(u32, i32, u32, u32, u32, u32)>,
+    pub last_opacity: Option<u32>,
+    pub last_circular: Option<u32>,
+    pub last_blur: Option<u32>,
 }
 
 impl Default for Window {
@@ -260,6 +277,8 @@ impl Default for Window {
             y: 0,
             width: 0,
             height: 0,
+            committed_width: 0,
+            committed_height: 0,
             app_id: None,
             title: None,
             identifier: None,
@@ -280,6 +299,7 @@ impl Default for Window {
             mode_locked: false,
             needs_xprop_check: false,
             xprop_check_attempts: 0,
+            metadata_check_attempts: 0,
             anim_x: None,
             anim_y: None,
             anim_w: None,
@@ -287,6 +307,10 @@ impl Default for Window {
             anim_opacity: None,
             circular: false,
             size_hint_applied: false,
+            last_borders: None,
+            last_opacity: None,
+            last_circular: None,
+            last_blur: None,
         }
     }
 }
@@ -379,6 +403,8 @@ pub struct WindowManager {
     pub expose_active: bool,
     pub expose_visual_active: bool,
     pub animating: bool,
+    pub last_animation_tick: std::time::Instant,
+    pub last_frame_time: std::time::Instant,
 }
 
 impl Default for WindowManager {
@@ -429,6 +455,8 @@ impl Default for WindowManager {
             expose_active: false,
             expose_visual_active: false,
             animating: false,
+            last_animation_tick: std::time::Instant::now(),
+            last_frame_time: std::time::Instant::now(),
         }
     }
 }
@@ -614,6 +642,10 @@ pub fn parse_action(s: &str) -> Action {
         Action::None
     } else if s == "expose" {
         Action::Expose
+    } else if s == "side-panel-left" || s == "side_panel_left" {
+        Action::SidePanelLeft
+    } else if s == "side-panel-right" || s == "side_panel_right" {
+        Action::SidePanelRight
     } else {
         Action::None
     }
@@ -712,6 +744,8 @@ mod tests {
         assert_eq!(parse_action("set-tag-3"), Action::SetTag3);
         assert_eq!(parse_action("expose"), Action::Expose);
         assert_eq!(parse_action("minimize"), Action::Minimize);
+        assert_eq!(parse_action("side-panel-left"), Action::SidePanelLeft);
+        assert_eq!(parse_action("side-panel-right"), Action::SidePanelRight);
         assert_eq!(parse_action("unknown"), Action::None);
     }
 
diff --git a/src/wayland.rs b/src/wayland.rs
index 75e5e44..1692cd6 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -297,6 +297,13 @@ impl AppState {
         id
     }
 
+    pub fn manage_dirty(&mut self) {
+        if let Some(ref wm) = self.window_manager {
+            wm.manage_dirty();
+            self.wm.last_animation_tick = std::time::Instant::now();
+        }
+    }
+
     pub fn get_window_proxy(&self, id: u64) -> Option<&WindowProxy> {
         self.window_proxies
             .iter()
@@ -825,9 +832,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                     // If any window gained has_parent, trigger a re-manage so
                     // assign_window_modes picks it up on the next cycle.
                     if xprop_parent_changed {
-                        if let Some(ref wm) = state.window_manager {
-                            wm.manage_dirty();
-                        }
+                        state.manage_dirty();
                     }
                 }
 
@@ -927,8 +932,12 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                             new_id, app_id
                         );
                     }
-                    // Clear is_new on all windows (only matters once)
+                    // Clear is_new on all windows once metadata is resolved, or after a timeout
                     for window in &mut state.wm.windows {
+                        if window.app_id.is_none() && window.title.is_none() && window.metadata_check_attempts < 10 {
+                            window.metadata_check_attempts += 1;
+                            continue;
+                        }
                         window.is_new = false;
                     }
                 }
@@ -1016,9 +1025,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                 }
 
                 if has_pending {
-                    if let Some(ref wm) = state.window_manager {
-                        wm.manage_dirty();
-                    }
+                    state.manage_dirty();
                 }
 
                 wm_proxy.manage_finish();
@@ -1080,36 +1087,48 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                     let active_tags = state.wm.active_tags;
                     let focused_id = state.wm.seats.iter().find(|s| !s.removed).and_then(|s| s.focused_window_id);
 
+                    let has_fullscreen = state.wm.windows.iter().any(|w| {
+                        w.tiling_mode == crate::types::TilingMode::Fullscreen
+                            && (w.tags & active_tags) != 0
+                            && !w.closed
+                            && !w.minimized
+                            && w.app_id.as_deref() != Some("cce-status-interface")
+                    });
+
                     let get_window_score = |win: &crate::types::Window| -> i32 {
                         if win.app_id.as_deref() == Some("cce-status-interface") {
-                            0
+                            if has_fullscreen {
+                                0
+                            } else {
+                                5
+                            }
                         } else if win.tiling_mode == crate::types::TilingMode::Popup {
-                            5
+                            10
                         } else if win.tiling_mode == crate::types::TilingMode::SidePanel {
                             if state.wm.layout.side_panel_behavior == "above" {
                                 if Some(win.id) == focused_id {
-                                    4
+                                    8
                                 } else {
-                                    3
+                                    6
                                 }
                             } else {
                                 if Some(win.id) == focused_id {
-                                    2
+                                    4
                                 } else {
-                                    1
+                                    2
                                 }
                             }
                         } else if win.tiling_mode != crate::types::TilingMode::Floating {
                             if Some(win.id) == focused_id {
-                                2
+                                4
                             } else {
-                                1
+                                2
                             }
                         } else {
                             if Some(win.id) == focused_id {
-                                4
+                                8
                             } else {
-                                3
+                                6
                             }
                         }
                     };
@@ -1145,6 +1164,9 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                 }
 
                 wm_proxy.render_finish();
+                if state.wm.animating {
+                    state.manage_dirty();
+                }
                 // Flush immediately so River receives render_finish without waiting
                 // for blocking_dispatch to complete. River has a 3-second unresponsive
                 // timeout, and if we don't flush promptly, River will kill us.
@@ -1331,6 +1353,8 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
 
             river_window_v1::Event::Dimensions { width, height } => {
                 if let Some(window) = state.wm.get_window_mut(wid) {
+                    window.committed_width = width;
+                    window.committed_height = height;
                     let changed = window.width != width || window.height != height;
                     if changed {
                         eprintln!(
@@ -1340,9 +1364,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                         window.width = width;
                         window.height = height;
                         state.wm.needs_render = true;
-                        if let Some(ref wm) = state.window_manager {
-                            wm.manage_dirty();
-                        }
+                        state.manage_dirty();
                     }
                 }
             }
@@ -1371,9 +1393,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                 }
                 if re_eval {
                     state.wm.needs_render = true;
-                    if let Some(ref wm) = state.window_manager {
-                        wm.manage_dirty();
-                    }
+                    state.manage_dirty();
                 }
             }
 
@@ -1408,9 +1428,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                     state.wm.needs_render = true;
                 }
                 if re_eval {
-                    if let Some(ref wm) = state.window_manager {
-                        wm.manage_dirty();
-                    }
+                    state.manage_dirty();
                 }
             }
 
@@ -1483,9 +1501,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                     if !window.size_hint_applied && min_width > 32 {
                         window.size_hint_applied = true;
                         state.wm.needs_render = true;
-                        if let Some(ref wm) = state.window_manager {
-                            wm.manage_dirty();
-                        }
+                        state.manage_dirty();
                     }
                 }
             }
@@ -1518,9 +1534,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                     }
                     // Parent status changed: re-assign mode (child windows float)
                     if window.has_parent != had_parent && !window.mode_locked {
-                        if let Some(ref wm) = state.window_manager {
-                            wm.manage_dirty();
-                        }
+                        state.manage_dirty();
                     }
                 }
             }
@@ -1532,6 +1546,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                         "[window] id={} (app_id={:?}) requested fullscreen",
                         wid, window.app_id
                     );
+                    state.manage_dirty();
                 }
             }
 
@@ -1542,6 +1557,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                         "[window] id={} (app_id={:?}) requested exit fullscreen",
                         wid, window.app_id
                     );
+                    state.manage_dirty();
                 }
             }
 
@@ -1596,9 +1612,7 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                 state.wm.needs_render = true;
                 state.wm.needs_focus = true;
                 state.wm.needs_status_update = true;
-                if let Some(ref wm) = state.window_manager {
-                    wm.manage_dirty();
-                }
+                state.manage_dirty();
             }
 
             river_window_v1::Event::ShowWindowMenuRequested { x, y } => {
@@ -1665,17 +1679,28 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
 
                     let mut window_found = false;
                     let mut needs_render = false;
+                    let side_panel_pos = state.wm.layout.side_panel_position.clone();
                     if let Some(win) = state.wm.get_window_mut(wid) {
                         if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
-                            let is_right_resize_on_side_panel = win.tiling_mode == TilingMode::SidePanel
-                                && matches!(
-                                    op_type,
-                                    PointerOpType::Resize
-                                        | PointerOpType::ResizeRight
-                                        | PointerOpType::ResizeTopRight
-                                        | PointerOpType::ResizeBottomRight
-                                );
-                            if !is_right_resize_on_side_panel && win.tiling_mode != TilingMode::Floating {
+                            let is_valid_side_panel_resize = win.tiling_mode == TilingMode::SidePanel
+                                && if side_panel_pos == "right" {
+                                    matches!(
+                                        op_type,
+                                        PointerOpType::Resize
+                                            | PointerOpType::ResizeLeft
+                                            | PointerOpType::ResizeTopLeft
+                                            | PointerOpType::ResizeBottomLeft
+                                    )
+                                } else {
+                                    matches!(
+                                        op_type,
+                                        PointerOpType::Resize
+                                            | PointerOpType::ResizeRight
+                                            | PointerOpType::ResizeTopRight
+                                            | PointerOpType::ResizeBottomRight
+                                    )
+                                };
+                            if !is_valid_side_panel_resize && win.tiling_mode != TilingMode::Floating {
                                 win.tiling_mode = TilingMode::Floating;
                                 needs_render = true;
                             }
@@ -1772,9 +1797,7 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                         state.wm.needs_status_update = true;
 
                         if !already_focused || moved || expose_changed {
-                            if let Some(ref wm) = state.window_manager {
-                                wm.manage_dirty();
-                            }
+                            state.manage_dirty();
                         }
                     }
                 }
@@ -1796,9 +1819,7 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                             state.wm.needs_status_update = true;
 
                             if !already_focused {
-                                if let Some(ref wm) = state.window_manager {
-                                    wm.manage_dirty();
-                                }
+                                state.manage_dirty();
                             }
                         }
                     }
@@ -1843,6 +1864,9 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                                 let actual_dx = op.start_width - target_width;
                                 win.x = op.start_x + actual_dx;
                                 win.width = target_width;
+                                if win.tiling_mode == TilingMode::SidePanel {
+                                    win.hint_min_width = win.width + bw * 2;
+                                }
                             }
                             PointerOpType::ResizeBottom => {
                                 win.height = std::cmp::max(50, op.start_height + dy);
@@ -1866,6 +1890,9 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                                 win.x = op.start_x + actual_dx;
                                 win.width = target_width;
                                 win.height = std::cmp::max(50, op.start_height + dy);
+                                if win.tiling_mode == TilingMode::SidePanel {
+                                    win.hint_min_width = win.width + bw * 2;
+                                }
                             }
                             PointerOpType::ResizeTopLeft => {
                                 let target_width = std::cmp::max(50, op.start_width - dx);
@@ -1877,6 +1904,9 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                                 let actual_dy = op.start_height - target_height;
                                 win.y = op.start_y + actual_dy;
                                 win.height = target_height;
+                                if win.tiling_mode == TilingMode::SidePanel {
+                                    win.hint_min_width = win.width + bw * 2;
+                                }
                             }
                             PointerOpType::ResizeTopRight => {
                                 win.width = std::cmp::max(50, op.start_width + dx);
@@ -1891,16 +1921,12 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                             }
                         }
                     }
-                    if let Some(ref wm) = state.window_manager {
-                        wm.manage_dirty();
-                    }
+                    state.manage_dirty();
                 }
             }
             river_seat_v1::Event::OpRelease => {
                 state.pointer_op_release_pending = true;
-                if let Some(ref wm) = state.window_manager {
-                    wm.manage_dirty();
-                }
+                state.manage_dirty();
             }
 
             _ => {}
@@ -2189,9 +2215,7 @@ impl Dispatch<RiverPointerBindingV1, BindingUserData> for AppState {
                     seat.pending_command = data.command.clone();
                 }
                 // Trigger a manage sequence so the pending action is processed
-                if let Some(ref wm) = state.window_manager {
-                    wm.manage_dirty();
-                }
+                state.manage_dirty();
             }
             _ => {}
         }
@@ -2232,9 +2256,7 @@ impl Dispatch<RiverXkbBindingV1, BindingUserData> for AppState {
                     seat.pending_command = data.command.clone();
                 }
                 // Trigger a manage sequence so the pending action is processed
-                if let Some(ref wm) = state.window_manager {
-                    wm.manage_dirty();
-                }
+                state.manage_dirty();
             }
             river_xkb_binding_v1::Event::Released => {}
             river_xkb_binding_v1::Event::StopRepeat => {}
@@ -2277,6 +2299,26 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
     use crate::types::Action;
     match action {
         Action::None => {}
+        Action::SidePanelLeft => {
+            let focused = state.wm.focused_window();
+            let is_side_panel = focused.map_or(false, |w| w.tiling_mode == TilingMode::SidePanel);
+            if is_side_panel {
+                state.wm.layout.side_panel_position = "left".to_string();
+                state.wm.needs_render = true;
+                state.wm.needs_status_update = true;
+                state.manage_dirty();
+            }
+        }
+        Action::SidePanelRight => {
+            let focused = state.wm.focused_window();
+            let is_side_panel = focused.map_or(false, |w| w.tiling_mode == TilingMode::SidePanel);
+            if is_side_panel {
+                state.wm.layout.side_panel_position = "right".to_string();
+                state.wm.needs_render = true;
+                state.wm.needs_status_update = true;
+                state.manage_dirty();
+            }
+        }
         Action::Spawn => {
             if let Some(cmd) = command {
                 eprintln!("spawn: {}", cmd);
@@ -2433,9 +2475,7 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
             state.wm.needs_render = true;
             state.wm.needs_focus = true;
             state.wm.needs_status_update = true;
-            if let Some(ref wm) = state.window_manager {
-                wm.manage_dirty();
-            }
+            state.manage_dirty();
         }
         Action::FocusNext => {
             // Focus the next visible window (wrapping) of the same tiling mode,
@@ -2541,17 +2581,28 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
             if let Some(wid) = target_wid {
                 let mut window_found = false;
                 let mut needs_render = false;
+                let side_panel_pos = state.wm.layout.side_panel_position.clone();
                 if let Some(win) = state.wm.get_window_mut(wid) {
                     if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
-                        let is_right_resize_on_side_panel = win.tiling_mode == TilingMode::SidePanel
-                            && matches!(
-                                op_type,
-                                PointerOpType::Resize
-                                    | PointerOpType::ResizeRight
-                                    | PointerOpType::ResizeTopRight
-                                    | PointerOpType::ResizeBottomRight
-                            );
-                        if !is_right_resize_on_side_panel && win.tiling_mode != TilingMode::Floating {
+                        let is_valid_side_panel_resize = win.tiling_mode == TilingMode::SidePanel
+                            && if side_panel_pos == "right" {
+                                matches!(
+                                    op_type,
+                                    PointerOpType::Resize
+                                        | PointerOpType::ResizeLeft
+                                        | PointerOpType::ResizeTopLeft
+                                        | PointerOpType::ResizeBottomLeft
+                                )
+                            } else {
+                                matches!(
+                                    op_type,
+                                    PointerOpType::Resize
+                                        | PointerOpType::ResizeRight
+                                        | PointerOpType::ResizeTopRight
+                                        | PointerOpType::ResizeBottomRight
+                                )
+                            };
+                        if !is_valid_side_panel_resize && win.tiling_mode != TilingMode::Floating {
                             win.tiling_mode = TilingMode::Floating;
                             needs_render = true;
                         }
@@ -3847,9 +3898,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
                                 state.wm.needs_status_update = true;
 
                                 if !already_focused {
-                                    if let Some(ref wm) = state.window_manager {
-                                        wm.manage_dirty();
-                                    }
+                                    state.manage_dirty();
                                 }
                             }
                         }
@@ -3890,17 +3939,28 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
 
                         // Start the drag! Set the window to Floating and lock it.
                         let mut needs_render = false;
+                        let side_panel_pos = state.wm.layout.side_panel_position.clone();
                         if let Some(win) = state.wm.get_window_mut(pending.window_id) {
                             if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
-                                let is_right_resize_on_side_panel = win.tiling_mode == TilingMode::SidePanel
-                                    && matches!(
-                                        pending.op_type,
-                                        PointerOpType::Resize
-                                            | PointerOpType::ResizeRight
-                                            | PointerOpType::ResizeTopRight
-                                            | PointerOpType::ResizeBottomRight
-                                    );
-                                if !is_right_resize_on_side_panel && win.tiling_mode != TilingMode::Floating {
+                                let is_valid_side_panel_resize = win.tiling_mode == TilingMode::SidePanel
+                                    && if side_panel_pos == "right" {
+                                        matches!(
+                                            pending.op_type,
+                                            PointerOpType::Resize
+                                                | PointerOpType::ResizeLeft
+                                                | PointerOpType::ResizeTopLeft
+                                                | PointerOpType::ResizeBottomLeft
+                                        )
+                                    } else {
+                                        matches!(
+                                            pending.op_type,
+                                            PointerOpType::Resize
+                                                | PointerOpType::ResizeRight
+                                                | PointerOpType::ResizeTopRight
+                                                | PointerOpType::ResizeBottomRight
+                                        )
+                                    };
+                                if !is_valid_side_panel_resize && win.tiling_mode != TilingMode::Floating {
                                     win.tiling_mode = TilingMode::Floating;
                                     needs_render = true;
                                 }
@@ -3925,9 +3985,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
                             } else {
                                 crate::types::Action::Resize
                             };
-                            if let Some(ref wm) = state.window_manager {
-                                wm.manage_dirty();
-                            }
+                            state.manage_dirty();
                         }
                     }
                 }
@@ -3964,6 +4022,120 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
                             eprintln!("[pointer] matched_window={:?}", matched_window);
 
                             if let Some((wid, surface_type)) = matched_window {
+                                if let Some(w) = state.wm.windows.iter().find(|win| win.id == wid) {
+                                    if !w.minimized && surface_type == "top" {
+                                        let border_w = if state.wm.expose_active && w.tiling_mode != crate::types::TilingMode::Popup {
+                                            state.wm.layout.grid_border_width
+                                        } else {
+                                            match w.tiling_mode {
+                                                crate::types::TilingMode::Cascade => state.wm.layout.cascade_border_width,
+                                                crate::types::TilingMode::Fullscreen => state.wm.layout.fullscreen_border_width,
+                                                crate::types::TilingMode::Grid => state.wm.layout.grid_border_width,
+                                                crate::types::TilingMode::Floating => state.wm.layout.floating_border_width,
+                                                crate::types::TilingMode::Popup => 0,
+                                                crate::types::TilingMode::SidePanel => state.wm.layout.cascade_border_width,
+                                            }
+                                        };
+                                        let logical_width = w.width + 2 * border_w;
+                                        let logical_height = std::cmp::max(border_w, 16);
+
+                                        if state.last_pointer_surface_y >= 0.0 && state.last_pointer_surface_y < logical_height as f64 {
+                                            let click_x = state.last_pointer_surface_x;
+                                            let mut action_handled = false;
+
+                                            if click_x >= (logical_width as f64 - 48.0) && click_x < (logical_width as f64 - 32.0) {
+                                                // Minimize
+                                                eprintln!("[pointer] Minimize button clicked on window {}", wid);
+                                                if let Some(window) = state.wm.get_window_mut(wid) {
+                                                    window.minimize_requested = true;
+                                                    window.minimized = true;
+                                                }
+                                                // Shift focus
+                                                if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
+                                                    if seat.focused_window_id == Some(wid) {
+                                                        let active_tags = state.wm.active_tags;
+                                                        let visible_ids: Vec<u64> = state.wm
+                                                            .windows
+                                                            .iter()
+                                                            .filter(|win| (win.tags & active_tags) != 0 && !win.closed && !win.minimized && win.id != wid && win.app_id.as_deref() != Some("cce-status-interface"))
+                                                            .map(|win| win.id)
+                                                            .collect();
+                                                        seat.focused_window_id = visible_ids.last().copied();
+                                                    }
+                                                }
+                                                state.wm.needs_render = true;
+                                                state.wm.needs_focus = true;
+                                                state.wm.needs_status_update = true;
+                                                state.manage_dirty();
+                                                action_handled = true;
+                                            } else if click_x >= (logical_width as f64 - 32.0) && click_x < (logical_width as f64 - 16.0) {
+                                                // Maximize / Restore
+                                                eprintln!("[pointer] Maximize button clicked on window {}", wid);
+                                                let is_fullscreen = state.wm.get_window(wid)
+                                                    .map(|win| win.tiling_mode == TilingMode::Fullscreen)
+                                                    .unwrap_or(false);
+                                                let resolved_mode = if is_fullscreen {
+                                                    let (app_id, title, tags, mode_locked) = state.wm.get_window(wid)
+                                                        .map(|win| (win.app_id.clone(), win.title.clone(), win.tags, win.mode_locked))
+                                                        .unwrap_or((None, None, 1, false));
+                                                    let temp_win = Window {
+                                                        id: wid,
+                                                        is_new: false,
+                                                        closed: false,
+                                                        tags,
+                                                        app_id,
+                                                        title,
+                                                        tiling_mode: TilingMode::Fullscreen,
+                                                        mode_locked,
+                                                        ..Default::default()
+                                                    };
+                                                    crate::wm::get_mode_for_window(&state.wm, &temp_win).unwrap_or(state.wm.global_layout)
+                                                } else {
+                                                    TilingMode::Fullscreen
+                                                };
+                                                if let Some(window) = state.wm.get_window_mut(wid) {
+                                                    if is_fullscreen {
+                                                        window.tiling_mode = resolved_mode;
+                                                        window.mode_locked = false;
+                                                    } else {
+                                                        window.tiling_mode = TilingMode::Fullscreen;
+                                                        window.mode_locked = true;
+                                                    }
+                                                    state.wm.needs_render = true;
+                                                    state.wm.needs_status_update = true;
+                                                    state.manage_dirty();
+                                                }
+                                                action_handled = true;
+                                            } else if click_x >= (logical_width as f64 - 16.0) && click_x <= logical_width as f64 {
+                                                // Close
+                                                eprintln!("[pointer] Close button clicked on window {}", wid);
+                                                if let Some(wp) = state.get_window_proxy(wid) {
+                                                    wp.river_window.close();
+                                                }
+                                                action_handled = true;
+                                            }
+
+                                            if action_handled {
+                                                // Focus the window first (if not minimized/closed)
+                                                if click_x < (logical_width as f64 - 48.0) || click_x >= (logical_width as f64 - 32.0) {
+                                                    if let Some(sid) = state.seat_proxies.iter().find_map(|(sid, sp)| {
+                                                        if sp.wl_pointer.as_ref() == Some(proxy) { Some(*sid) } else { None }
+                                                    }) {
+                                                        if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
+                                                            seat.focused_window_id = Some(wid);
+                                                            state.wm.needs_focus = true;
+                                                            state.wm.needs_render = true;
+                                                            state.wm.needs_status_update = true;
+                                                            state.manage_dirty();
+                                                        }
+                                                    }
+                                                }
+                                                return; // Avoid triggering drag or focus logic below
+                                            }
+                                        }
+                                    }
+                                }
+
                                 let window_tiling_mode = state.wm.windows.iter().find(|w| w.id == wid).map(|w| w.tiling_mode);
                                 if let Some(mode) = window_tiling_mode {
                                     if mode != TilingMode::Fullscreen {
@@ -4000,9 +4172,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
                                                 state.wm.needs_render = true;
                                                 state.wm.needs_status_update = true;
                                                 if !already_focused || moved || unminimized || expose_changed {
-                                                    if let Some(ref wm) = state.window_manager {
-                                                        wm.manage_dirty();
-                                                    }
+                                                    state.manage_dirty();
                                                 }
                                             }
                                                                  // Determine the specific PointerOpType based on coordinates on the matched surface
@@ -4117,9 +4287,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
                                 seat.interacted_window_id = None;
                             }
                             state.wm.needs_render = true;
-                            if let Some(ref wm) = state.window_manager {
-                                wm.manage_dirty();
-                            }
+                            state.manage_dirty();
                         }
                     }
                 }
diff --git a/src/wm.rs b/src/wm.rs
index 23294ae..fbbf0a7 100644
--- a/src/wm.rs
+++ b/src/wm.rs
@@ -32,6 +32,11 @@ pub fn get_mode_for_window(wm: &WindowManager, win: &Window) -> Option<TilingMod
         return None;
     }
 
+    // If the client requested fullscreen, automatically place it in Fullscreen mode.
+    if win.fullscreen_requested {
+        return Some(TilingMode::Fullscreen);
+    }
+
     // 0. Windows with a parent (dialogs, file pickers, etc.) always float.
     if win.has_parent {
         return Some(TilingMode::Floating);
@@ -131,6 +136,36 @@ fn get_circular_for_window(mode_rules: &[ModeRule], win: &Window) -> bool {
     false
 }
 
+pub fn get_ssd_override_for_window(mode_rules: &[ModeRule], win: &Window) -> Option<bool> {
+    for rule in 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))
+            || (!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| {
+                t.contains(rule.title_pattern.as_deref().unwrap_or(""))
+            });
+
+        if match_app && match_title {
+            if rule.ssd.is_some() {
+                return rule.ssd;
+            }
+        }
+    }
+    None
+}
+
+
 /// Assign tiling modes to all windows that aren't mode_locked.
 /// Should be called during ManageStart before compute_tiling.
 pub fn assign_window_modes(wm: &mut WindowManager) {
@@ -165,6 +200,10 @@ pub fn assign_window_modes(wm: &mut WindowManager) {
                 && !win.has_parent
                 && !matches_mode_rule(mode_rules, win)
             {
+                if win.app_id.is_none() && win.title.is_none() {
+                    continue;
+                }
+
                 // Determine what normal fallback mode would be
                 let mut normal_fallback = wm.global_layout;
                 for tag_bit in 0..crate::types::NUM_TAGS {
@@ -505,18 +544,24 @@ fn compute_tiling(
             && w.tiling_mode == TilingMode::SidePanel
     });
 
+    let g = wm.layout.side_panel_border_gap;
     let shift_x = if let Some(panel_win) = side_panel_win {
         if wm.layout.side_panel_behavior == "above" {
             0
-        } else if panel_win.hint_min_width > 32 {
-            std::cmp::max(wm.layout.side_panel_width, panel_win.hint_min_width)
         } else {
-            wm.layout.side_panel_width
+            let target_w = if panel_win.hint_min_width > 32 {
+                std::cmp::max(wm.layout.side_panel_width, panel_win.hint_min_width)
+            } else {
+                wm.layout.side_panel_width
+            };
+            target_w + g
         }
     } else {
         0
     };
 
+    let tiled_screen_w = screen_w - shift_x;
+
     // Compute tiling
     let mut results = Vec::new();
     let mut idx_floating = 0i32;
@@ -538,7 +583,14 @@ fn compute_tiling(
                 };
                 let bw = wm.layout.cascade_border_width;
                 let dec_h = std::cmp::max(bw, 16);
-                (phys_x + bw, bar_height + phys_y + dec_h, target_w - bw * 2, screen_h - bar_height - (dec_h + bw))
+                let sp_x = if wm.layout.side_panel_position == "right" {
+                    phys_x + screen_w - target_w - g + bw
+                } else {
+                    phys_x + g + bw
+                };
+                let sp_y = bar_height + phys_y + dec_h + g;
+                let sp_h = (screen_h - bar_height - (dec_h + bw) - 2 * g).max(1);
+                (sp_x, sp_y, target_w - bw * 2, sp_h)
             }
             TilingMode::Fullscreen => {
                 if fullscreen_id == Some(wid) || win.app_id.as_deref() == Some("cce-status-interface") {
@@ -568,7 +620,7 @@ fn compute_tiling(
                     .position(|w| w.id == wid)
                     .unwrap_or(0) as i32;
                 let (x, y, w, h) = tiling::tile_cascade(
-                    screen_w,
+                    tiled_screen_w,
                     screen_h,
                     gap,
                     gap_top,
@@ -589,7 +641,7 @@ fn compute_tiling(
                      .position(|w| w.id == wid)
                      .unwrap_or(0) as i32;
                  let (x, y, w, h) = tiling::tile_grid(
-                     screen_w,
+                     tiled_screen_w,
                      screen_h,
                      wm.layout.grid_gap,
                      gap_top,
@@ -611,30 +663,63 @@ fn compute_tiling(
                 // Use the window's existing dimensions, or a reasonable
                 // default if unset.
                 let fbw = wm.layout.floating_border_width;
-                let fw = if win.width > 0 {
+                let mut fw = if win.width > 0 {
                     win.width
                 } else if win.hint_min_width > 32 {
                     win.hint_min_width
                 } else {
                     screen_w * 2 / 3
                 };
-                let fh = if win.height > 0 {
+                let mut fh = if win.height > 0 {
                     win.height
                 } else if win.hint_min_height > 32 {
                     win.hint_min_height
                 } else {
                     screen_h * 2 / 3
                 };
-                let fx = if win.x != 0 || win.y != 0 {
+
+                // Clamp dimensions to the screen's usable region
+                let max_w = screen_w - gap_left - gap_right;
+                let max_h = screen_h - bar_height - gap_top - gap_bottom;
+                fw = fw.clamp(1, max_w.max(1));
+                fh = fh.clamp(1, max_h.max(1));
+
+                let mut fx = if win.x != 0 || win.y != 0 {
                     win.x
                 } else {
-                    gap_left + fbw + cascade_offset * idx_floating
+                    gap_left + fbw + cascade_offset * idx_floating + phys_x
                 };
-                let fy = if win.x != 0 || win.y != 0 {
+                let mut fy = if win.x != 0 || win.y != 0 {
                     win.y
                 } else {
-                    gap_left + fbw + bar_height + gap_top + cascade_offset * idx_floating
+                    gap_left + fbw + bar_height + gap_top + cascade_offset * idx_floating + phys_y
+                };
+
+                // Clamp position so the window is fully inside usability limits.
+                // We use the actual committed size (if set) to determine the clamping bounds
+                // so that we correctly clamp windows that refuse to resize to the layout target (e.g. Steam).
+                let actual_fw = if win.committed_width > 0 { win.committed_width } else { fw };
+                let actual_fh = if win.committed_height > 0 { win.committed_height } else { fh };
+
+                let min_x = phys_x + gap_left;
+                let max_x = phys_x + screen_w - gap_right - actual_fw;
+                let min_y = phys_y + bar_height + gap_top;
+                let max_y = phys_y + screen_h - gap_bottom - actual_fh;
+
+                let (clamp_min_x, clamp_max_x) = if max_x >= min_x {
+                    (min_x, max_x)
+                } else {
+                    (max_x, min_x)
                 };
+                fx = fx.clamp(clamp_min_x, clamp_max_x);
+
+                let (clamp_min_y, clamp_max_y) = if max_y >= min_y {
+                    (min_y, max_y)
+                } else {
+                    (max_y, min_y)
+                };
+                fy = fy.clamp(clamp_min_y, clamp_max_y);
+
                 idx_floating += 1;
                 (fx, fy, fw, fh)
             }
@@ -664,7 +749,11 @@ fn compute_tiling(
             && mode != TilingMode::Popup
             && win.app_id.as_deref() != Some("cce-status-interface")
         {
-            x + shift_x
+            if wm.layout.side_panel_position == "right" {
+                x
+            } else {
+                x + shift_x
+            }
         } else {
             x
         };
@@ -705,11 +794,31 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
         .find(|s| !s.removed)
         .and_then(|s| s.focused_window_id);
 
+    let was_animating = state.wm.animating;
+    let dt = if was_animating {
+        let elapsed = state.wm.last_frame_time.elapsed().as_secs_f64();
+        // Cap dt to 0.1s to avoid huge snapping jumps on massive frame drops/system pauses
+        if elapsed > 0.1 {
+            0.1
+        } else {
+            elapsed
+        }
+    } else {
+        // Reset last_frame_time to now so that subsequent frames measure the actual elapsed time.
+        state.wm.last_frame_time = std::time::Instant::now();
+        0.016
+    };
+
+    if was_animating {
+        state.wm.last_frame_time = std::time::Instant::now();
+    }
+
     let transition_duration = state.wm.layout.transition_duration;
-    let easing = if transition_duration <= 16 {
+    let easing = if transition_duration <= 8 {
         1.0
     } else {
-        1.0 - 0.01f64.powf(16.0 / transition_duration as f64)
+        let dt_ms = dt * 1000.0;
+        1.0 - 0.01f64.powf(dt_ms / transition_duration as f64)
     };
 
     let expose_active = state.wm.expose_active;
@@ -739,6 +848,8 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
             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 || is_side_panel_present || win.tiling_mode == TilingMode::SidePanel || was_animating) && !win.minimized;
 
+
+
             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);
@@ -827,6 +938,7 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
                     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.
@@ -855,10 +967,12 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
 
         // Propose dimensions via river_window_v1
         if let Some(wp) = state.get_window_proxy(tr.wid) {
-            let is_floating_or_popup = if let Some(win) = state.wm.get_window(tr.wid) {
-                (win.tiling_mode == TilingMode::Floating || win.tiling_mode == TilingMode::Popup) && win.width == 0
+            let (is_floating_or_popup, is_tiled) = if let Some(win) = state.wm.get_window(tr.wid) {
+                let is_float = (win.tiling_mode == TilingMode::Floating || win.tiling_mode == TilingMode::Popup) && win.width == 0;
+                let is_tile = win.tiling_mode == TilingMode::Cascade || win.tiling_mode == TilingMode::Grid || win.tiling_mode == TilingMode::SidePanel;
+                (is_float, is_tile)
             } else {
-                false
+                (false, false)
             };
 
             if is_floating_or_popup {
@@ -866,6 +980,13 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
             } else {
                 wp.river_window.propose_dimensions(final_w, final_h);
             }
+
+            if is_tiled {
+                wp.river_window.set_tiled(Edges::all());
+            } else {
+                wp.river_window.set_tiled(Edges::empty());
+            }
+
             // 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
@@ -880,6 +1001,9 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
     if any_animating {
         state.wm.needs_render = true;
     }
+    if was_animating && !any_animating {
+        state.wm.needs_status_update = true;
+    }
     state.wm.expose_visual_active = state.wm.expose_active;
 }
 
@@ -927,15 +1051,25 @@ fn set_borders(state: &mut AppState) {
     let border_colors = compute_border_colors(&state.wm);
 
     for bc in &border_colors {
-        let wid = match state.wm.windows.get(bc.window_idx) {
-            Some(w) => w.id,
+        let (wid, is_diff) = match state.wm.windows.get_mut(bc.window_idx) {
+            Some(win) => {
+                let current_val = (bc.edges, bc.width, bc.r, bc.g, bc.b, bc.a);
+                if win.last_borders != Some(current_val) {
+                    win.last_borders = Some(current_val);
+                    (win.id, true)
+                } else {
+                    (win.id, false)
+                }
+            }
             None => continue,
         };
 
-        if let Some(wp) = state.get_window_proxy(wid) {
-            let edges = Edges::from_bits_truncate(bc.edges);
-            wp.river_window
-                .set_borders(edges, bc.width, bc.r, bc.g, bc.b, bc.a);
+        if is_diff {
+            if let Some(wp) = state.get_window_proxy(wid) {
+                let edges = Edges::from_bits_truncate(bc.edges);
+                wp.river_window
+                    .set_borders(edges, bc.width, bc.r, bc.g, bc.b, bc.a);
+            }
         }
     }
 }
@@ -948,32 +1082,40 @@ pub fn render_opacity(state: &mut AppState) {
         .find(|s| !s.removed)
         .and_then(|s| s.focused_window_id);
 
-    for win in &state.wm.windows {
+    let mut updates = Vec::new();
+    for win in &mut 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("cce-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 {
+        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("cce-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
-            };
-            let op_u32 = (opacity * u32::MAX as f64).round() as u32;
+                } else {
+                    1.0
+                }
+            })
+        } else {
+            1.0
+        };
+        let op_u32 = (opacity * u32::MAX as f64).round() as u32;
+        if win.last_opacity != Some(op_u32) {
+            win.last_opacity = Some(op_u32);
+            updates.push((win.id, op_u32));
+        }
+    }
+
+    for (wid, op_u32) in updates {
+        if let Some(wp) = state.get_window_proxy(wid) {
             wp.river_window.set_opacity(op_u32);
         }
     }
@@ -982,12 +1124,20 @@ pub fn render_opacity(state: &mut AppState) {
 /// Apply whether windows are circular.
 /// This modifies rendering state and is called during RenderStart.
 pub fn render_circular(state: &mut AppState) {
-    for win in &state.wm.windows {
+    let mut updates = Vec::new();
+    for win in &mut state.wm.windows {
         if win.closed {
             continue;
         }
-        if let Some(wp) = state.get_window_proxy(win.id) {
-            let val = if win.circular { 1 } else { 0 };
+        let val = if win.circular { 1u32 } else { 0u32 };
+        if win.last_circular != Some(val) {
+            win.last_circular = Some(val);
+            updates.push((win.id, val));
+        }
+    }
+
+    for (wid, val) in updates {
+        if let Some(wp) = state.get_window_proxy(wid) {
             wp.river_window.set_circular(val);
         }
     }
@@ -996,12 +1146,24 @@ pub fn render_circular(state: &mut AppState) {
 /// Apply window backdrop blur.
 /// This modifies rendering state and is called during RenderStart.
 pub fn render_blur(state: &mut AppState) {
-    for win in &state.wm.windows {
+    let mut updates = Vec::new();
+    for win in &mut state.wm.windows {
         if win.closed {
             continue;
         }
-        if let Some(wp) = state.get_window_proxy(win.id) {
-            let val = if state.wm.layout.window_blur { 1 } else { 0 };
+        let val = if state.wm.layout.window_blur && win.app_id.as_deref() != Some("cce-status-interface") {
+            1u32
+        } else {
+            0u32
+        };
+        if win.last_blur != Some(val) {
+            win.last_blur = Some(val);
+            updates.push((win.id, val));
+        }
+    }
+
+    for (wid, val) in updates {
+        if let Some(wp) = state.get_window_proxy(wid) {
             wp.river_window.set_blur(val);
         }
     }
@@ -1129,6 +1291,7 @@ mod tests {
             single_instance: false,
             tag: 0,
             circular: false,
+            ssd: None,
         });
 
         // Spawn a focused window that is in Grid mode
@@ -1244,5 +1407,132 @@ mod tests {
         assert_eq!(results[2].wid, 20);
         assert_eq!(results[3].wid, 10);
     }
+
+    #[test]
+    fn test_auto_fullscreen() {
+        let mut wm = WindowManager::default();
+        wm.global_layout = TilingMode::Cascade;
+        wm.tag_layouts[0] = TilingMode::Cascade;
+
+        wm.windows.push(Window {
+            id: 1,
+            fullscreen_requested: true,
+            is_new: false,
+            ..Default::default()
+        });
+
+        assign_window_modes(&mut wm);
+
+        let win1 = wm.get_window(1).unwrap();
+        assert_eq!(win1.tiling_mode, TilingMode::Fullscreen);
+    }
+
+    #[test]
+    fn test_floating_clamping() {
+        let mut wm = WindowManager::default();
+        wm.global_layout = TilingMode::Floating;
+        wm.active_tags = 1;
+        wm.layout.gap_left = 10;
+        wm.layout.gap_right = 10;
+        wm.layout.gap_top = 10;
+        wm.layout.gap_bottom = 10;
+        wm.layout.bar_height = 20;
+
+        // Large window that would overflow screen (1920x1080 screen)
+        wm.windows.push(Window {
+            id: 1,
+            tiling_mode: TilingMode::Floating,
+            x: 26,
+            y: 50,
+            width: 1920,
+            height: 1200,
+            tags: 1,
+            ..Default::default()
+        });
+
+        let results = compute_tiling(&wm, 1920, 1080, 1920, 1080, 0, 0);
+        assert_eq!(results.len(), 1);
+        let res = &results[0];
+
+        // Usable boundaries:
+        // max_w = 1920 - 10 - 10 = 1900
+        // max_h = 1080 - 20 (bar) - 10 - 10 = 1040
+        // width and height must be clamped to max_w and max_h respectively
+        assert_eq!(res.w, 1900);
+        assert_eq!(res.h, 1040);
+
+        // Coordinates:
+        // fx must be clamped to [min_x, max_x]
+        // min_x = 0 + 10 = 10
+        // max_x = 0 + 1920 - 10 - fw = 1910 - 1900 = 10
+        // So fx must be exactly 10!
+        assert_eq!(res.x, 10);
+
+        // fy must be clamped to [min_y, max_y]
+        // min_y = 0 + 20 (bar) + 10 = 30
+        // max_y = 0 + 1080 - 10 - fh = 1070 - 1040 = 30
+        // So fy must be exactly 30!
+        assert_eq!(res.y, 30);
+    }
+
+    #[test]
+    fn test_metadata_race_condition() {
+        let mut wm = WindowManager::default();
+        wm.global_layout = TilingMode::Cascade;
+        wm.tag_layouts[0] = TilingMode::Cascade;
+        wm.mode_rules.push(ModeRule {
+            mode: TilingMode::Floating,
+            app_id_pattern: "steam_proton".to_string(),
+            title_pattern: None,
+            single_instance: false,
+            tag: 0,
+            circular: false,
+            ssd: None,
+        });
+
+        // Focus window (Cascade mode)
+        wm.windows.push(Window {
+            id: 1,
+            tiling_mode: TilingMode::Cascade,
+            is_new: false,
+            ..Default::default()
+        });
+        wm.seats.push(Seat {
+            id: 1,
+            focused_window_id: Some(1),
+            ..Default::default()
+        });
+
+        // New window starts blank (is_new = true, app_id = None, title = None)
+        wm.windows.push(Window {
+            id: 2,
+            is_new: true,
+            app_id: None,
+            title: None,
+            ..Default::default()
+        });
+
+        // 1. First assign_window_modes run (blank window): inheritance should be skipped, not locked
+        assign_window_modes(&mut wm);
+        {
+            let win2 = wm.get_window(2).unwrap();
+            assert_eq!(win2.tiling_mode, TilingMode::Cascade);
+            assert!(!win2.mode_locked);
+        }
+
+        // 2. Metadata arrives (app_id = Some("steam_proton"))
+        {
+            let win2 = wm.get_window_mut(2).unwrap();
+            win2.app_id = Some("steam_proton".to_string());
+        }
+
+        // 3. Second assign_window_modes run: rule should match and resolve to Floating
+        assign_window_modes(&mut wm);
+        {
+            let win2 = wm.get_window(2).unwrap();
+            assert_eq!(win2.tiling_mode, TilingMode::Floating);
+            assert!(!win2.mode_locked);
+        }
+    }
 }
 
diff --git a/start-river.sh b/start-river.sh
index bd9dbf8..2a008fa 100755
--- a/start-river.sh
+++ b/start-river.sh
@@ -30,11 +30,8 @@ LAUNCH_EOF
 fi
 chmod +x /tmp/cce-client-launch-river.sh
  
-echo "Starting river with cce-client..."
 if [ "$LOGGING" = true ]; then
-    echo "Logs: /tmp/river-cce-client.log + /tmp/cce-client-${WAYLAND_DISPLAY}.log"
+    exec river -c /tmp/cce-client-launch-river.sh 2>/tmp/river-cce-client.log
 else
-    echo "Logging disabled. Use --logging to enable."
+    exec river -c /tmp/cce-client-launch-river.sh 2>/dev/null
 fi
- 
-exec river -c /tmp/cce-client-launch-river.sh 2>/tmp/river-cce-client.log