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

commit08d8fe9c90081c3fb695a77173beb20134525a58
parent2773503777
authorLucas Galante <[email protected]>
date2026-06-02 13:24
Update system configuration and interface modules

 protocol/clear-inspector-v1.xml         |   6 +-
 protocol/river-window-management-v1.xml |   9 +
 src/borders.rs                          |   4 +-
 src/clearctl.rs                         |   4 +-
 src/config.rs                           |  11 +
 src/decorations.rs                      | 117 +++++++--
 src/input.rs                            |  86 ++++++-
 src/inspector_cli.rs                    | 251 ++++++++++++++++++-
 src/ipc.rs                              | 180 +++++++++++++-
 src/state.rs                            |  16 +-
 src/status.rs                           |   3 +-
 src/status_server.rs                    |  33 ++-
 src/tiling.rs                           |  16 +-
 src/types.rs                            |  15 ++
 src/wayland.rs                          | 208 +++++++++++++++-
 src/wm.rs                               | 422 ++++++++++++++++++++++++++++++--
 16 files changed, 1287 insertions(+), 94 deletions(-)

diff --git a/protocol/clear-inspector-v1.xml b/protocol/clear-inspector-v1.xml
index b321965..4dca611 100644
--- a/protocol/clear-inspector-v1.xml
+++ b/protocol/clear-inspector-v1.xml
@@ -27,7 +27,8 @@
         The state is serialized as a JSON string containing widget layout and state data.
       </description>
       <arg name="surface" type="object" interface="wl_surface"/>
-      <arg name="state" type="string" summary="JSON serialized state of the application"/>
+      <arg name="fd" type="fd" summary="JSON state file descriptor"/>
+      <arg name="len" type="uint" summary="JSON state length in bytes"/>
     </request>
 
     <request name="get_inspected_surfaces">
@@ -42,7 +43,8 @@
       <arg name="y" type="int" summary="absolute screen Y coordinate"/>
       <arg name="width" type="int" summary="surface width"/>
       <arg name="height" type="int" summary="surface height"/>
-      <arg name="state" type="string" summary="JSON state of the application"/>
+      <arg name="fd" type="fd" summary="JSON state file descriptor"/>
+      <arg name="len" type="uint" summary="JSON state length in bytes"/>
     </event>
 
     <event name="inspected_surface_done">
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 32d6db3..60040b2 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1130,6 +1130,15 @@
       </description>
       <arg name="opacity" type="uint" summary="opacity value from 0 to 0xffffffff"/>
     </request>
+
+    <request name="set_circular" since="4">
+      <description summary="set whether the window is circular">
+        Set whether the window is circular (1 for circular, 0 for rectangular).
+        This request modifies rendering state and may only be made as part of a
+        render sequence, see the river_window_manager_v1 description.
+      </description>
+      <arg name="circular" type="uint" summary="1 if circular, 0 otherwise"/>
+    </request>
   </interface>
 
   <interface name="river_decoration_v1" version="4">
diff --git a/src/borders.rs b/src/borders.rs
index 53d0657..5ab2e32 100644
--- a/src/borders.rs
+++ b/src/borders.rs
@@ -127,7 +127,9 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
             }
         };
 
-        if win.app_id.as_deref() == Some("clear-status-interface") {
+        if win.app_id.as_deref() == Some("clear-status-interface")
+            || win.app_id.as_deref().map_or(false, |aid| aid.contains("noborder"))
+        {
             width = 0;
         }
 
diff --git a/src/clearctl.rs b/src/clearctl.rs
index 4129218..6adcabb 100644
--- a/src/clearctl.rs
+++ b/src/clearctl.rs
@@ -31,11 +31,13 @@ fn usage(name: &str, to_stderr: bool) {
     print(&format!("usage: {} <command> [args...]", name));
     print("");
     print("commands:");
-    print("  layout <gap|gap_top|gap_left|gap_right|gap_bottom|offset|bar_height|border_width|fullscreen_border_width|border_color> <value>");
+    print("  layout <gap|gap_top|gap_left|gap_right|gap_bottom|offset|grid_gap|bar_height|border_width|fullscreen_border_width|border_color> <value>");
     print("  view <1-4>");
     print("  toggle <1-4>");
     print("  close");
+    print("  minimize");
     print("  focus-next");
+  print("  focus-window <app_id|title>");
     print("  expose");
     print("  windows");
     print("  exit");
diff --git a/src/config.rs b/src/config.rs
index 1d6690b..f73ea2b 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -78,6 +78,8 @@ pub struct LayoutConfig {
     pub border_font_size: i64,
     #[serde(default = "default_transition_duration")]
     pub transition_duration: i64,
+    #[serde(default = "default_grid_gap")]
+    pub grid_gap: i64,
 }
 
 impl Default for LayoutConfig {
@@ -99,10 +101,15 @@ impl Default for LayoutConfig {
             background_color: default_background_color(),
             border_font_size: default_border_font_size(),
             transition_duration: default_transition_duration(),
+            grid_gap: default_grid_gap(),
         }
     }
 }
 
+fn default_grid_gap() -> i64 {
+    18
+}
+
 fn default_gap() -> i64 {
     48
 }
@@ -255,6 +262,7 @@ pub struct ModeRuleConfig {
     pub title: Option<String>,
     pub single: Option<bool>,
     pub tag: Option<i64>,
+    pub circular: Option<bool>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -300,6 +308,7 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
     state.layout.floating_border_width = config.layout.floating_border_width as i32;
     state.layout.border_font_size = config.layout.border_font_size as i32;
     state.layout.transition_duration = config.layout.transition_duration as i32;
+    state.layout.grid_gap = config.layout.grid_gap 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;
@@ -356,6 +365,7 @@ pub fn parse_config(path: &str, cold_start: bool, state: &mut WindowManager) ->
             title_pattern: mr.title.clone(),
             single_instance: mr.single.unwrap_or(false),
             tag,
+            circular: mr.circular.unwrap_or(false),
         });
     }
 
@@ -671,6 +681,7 @@ mod tests {
         assert_eq!(lc.fullscreen_border_width, 0);
         assert_eq!(lc.border_color, "#3e3e3e");
         assert_eq!(lc.border_font_size, 11);
+        assert_eq!(lc.grid_gap, 18);
     }
 
     #[test]
diff --git a/src/decorations.rs b/src/decorations.rs
index ec48de7..1360b14 100644
--- a/src/decorations.rs
+++ b/src/decorations.rs
@@ -338,8 +338,15 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
     // or side borders for windows that do not have them (Popup, Fullscreen).
     for (wid, wp) in &mut state.window_proxies {
         if let Some(w) = state.wm.windows.iter().find(|win| win.id == *wid) {
-            let should_not_decorate = w.closed || w.app_id.as_deref() == Some("clear-status-interface") || w.tiling_mode == crate::types::TilingMode::Popup || w.tiling_mode == crate::types::TilingMode::Fullscreen;
-            let needs_sides = !should_not_decorate;
+            let is_minimized = w.minimized;
+            let should_not_decorate = w.closed
+                || w.app_id.as_deref() == Some("clear-status-interface")
+                || w.app_id.as_deref().map_or(false, |aid| aid.contains("noborder"))
+                || w.tiling_mode == crate::types::TilingMode::Popup
+                || 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());
 
             if should_not_decorate {
                 if let Some(dec) = wp.decoration.take() {
@@ -365,36 +372,64 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
         }
     }
 
+    struct DecorateInfo {
+        id: u64,
+        win_width: i32,
+        win_height: i32,
+        border_width: i32,
+        title: String,
+        bg_color: u32,
+        tiling_mode: crate::types::TilingMode,
+        is_minimized: bool,
+        minimized_idx: Option<usize>,
+        win_y: i32,
+    }
+
     // Calculate dynamic border colors
     let border_colors = crate::borders::compute_border_colors(&state.wm);
     let text_color = 0xFFE0E0E0u32;
 
     // Collect window IDs to modify so we don't violate the borrow checker
-    let windows_to_decorate: Vec<(u64, i32, i32, i32, String, u32, crate::types::TilingMode)> = state
+    let windows_to_decorate: Vec<DecorateInfo> = state
         .wm
         .windows
         .iter()
         .enumerate()
-        .filter(|(_, w)| !w.closed && w.app_id.as_deref() != Some("clear-status-interface") && w.tiling_mode != crate::types::TilingMode::Popup && w.tiling_mode != crate::types::TilingMode::Fullscreen)
+        .filter(|(_, w)| !w.closed && w.app_id.as_deref() != Some("clear-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;
+            let minimized_idx = if is_minimized {
+                state.wm.windows
+                    .iter()
+                    .filter(|win| !win.closed && win.minimized && (win.tags & active_tags) != 0 && win.app_id.as_deref() != Some("clear-status-interface"))
+                    .position(|win| win.id == w.id)
+            } else {
+                None
+            };
             let title = w.title.clone().unwrap_or_else(|| {
                 w.app_id.clone().unwrap_or_else(|| "Window".to_string())
             });
             let mode_idx = if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
-                state.wm.windows
+                let list: Vec<_> = state.wm.windows
                     .iter()
                     .filter(|win| !win.closed && win.app_id.as_deref() != Some("clear-status-interface") && win.tiling_mode != crate::types::TilingMode::Popup && (win.tags & active_tags) != 0)
-                    .position(|win| win.id == w.id)
-                    .unwrap_or(0)
+                    .collect();
+                let len = list.len();
+                let pos = list.iter().position(|win| win.id == w.id).unwrap_or(0);
+                if len > 0 { len - 1 - pos } else { 0 }
             } else {
-                state.wm.windows
+                let list: Vec<_> = state.wm.windows
                     .iter()
                     .filter(|win| !win.closed && win.app_id.as_deref() != Some("clear-status-interface") && (win.tags & active_tags) != 0 && win.tiling_mode == w.tiling_mode)
-                    .position(|win| win.id == w.id)
-                    .unwrap_or(0)
+                    .collect();
+                let len = list.len();
+                let pos = list.iter().position(|win| win.id == w.id).unwrap_or(0);
+                if len > 0 { len - 1 - pos } else { 0 }
             };
-            let indicator = if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
+            let indicator = if is_minimized {
+                "M"
+            } else if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
                 "EX"
             } else {
                 match w.tiling_mode {
@@ -405,7 +440,11 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
                     crate::types::TilingMode::Popup => "P",
                 }
             };
-            let title_with_idx = format!("[{}{}] {}", indicator, mode_idx, title);
+            let title_with_idx = if is_minimized {
+                format!("[{}] {}", indicator, title)
+            } else {
+                format!("[{}{}] {}", indicator, mode_idx, title)
+            };
 
             // Find matching computed border color for this window
             let bc = border_colors.iter().find(|b| b.window_idx == idx);
@@ -426,7 +465,9 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
             let bg_color = ((border_a as u32) << 24) | ((border_r as u32) << 16) | ((border_g as u32) << 8) | (border_b as u32);
 
             // Border width is mode-specific
-            let border_w = if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
+            let border_width = if is_minimized {
+                state.wm.layout.grid_border_width
+            } else if state.wm.expose_visual_active && w.tiling_mode != crate::types::TilingMode::Popup {
                 state.wm.layout.grid_border_width
             } else {
                 match w.tiling_mode {
@@ -437,11 +478,33 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
                     crate::types::TilingMode::Popup => 0,
                 }
             };
-            (w.id, w.width, w.height, border_w, title_with_idx, bg_color, w.tiling_mode)
+            DecorateInfo {
+                id: w.id,
+                win_width: w.width,
+                win_height: w.height,
+                border_width,
+                title: title_with_idx,
+                bg_color,
+                tiling_mode: w.tiling_mode,
+                is_minimized,
+                minimized_idx,
+                win_y: w.y,
+            }
         })
         .collect();
 
-    for (wid, win_width, win_height, border_width, title, bg_color, tiling_mode) in windows_to_decorate {
+    for info in windows_to_decorate {
+        let wid = info.id;
+        let win_width = info.win_width;
+        let win_height = info.win_height;
+        let border_width = info.border_width;
+        let title = info.title;
+        let bg_color = info.bg_color;
+        let tiling_mode = info.tiling_mode;
+        let is_minimized = info.is_minimized;
+        let minimized_idx = info.minimized_idx;
+        let win_y = info.win_y;
+
         // Find or create decoration proxy
         let wp_idx = match state.window_proxies.iter().position(|(id, _)| *id == wid) {
             Some(idx) => idx,
@@ -452,7 +515,7 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
 
         let scale = (state.wm.output_scale.round() as i32).max(1);
 
-        if tiling_mode != crate::types::TilingMode::Popup && tiling_mode != crate::types::TilingMode::Fullscreen {
+        if tiling_mode != crate::types::TilingMode::Popup && tiling_mode != crate::types::TilingMode::Fullscreen && !is_minimized {
             let grab_w = if tiling_mode == crate::types::TilingMode::Floating {
                 border_width.max(10)
             } else {
@@ -505,7 +568,11 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
         // Determine titlebar dimensions:
         // Height equals border_width (or 16 if border_width is too small to display font)
         let logical_height = std::cmp::max(border_width, 16);
-        let logical_width = win_width + 2 * border_width;
+        let logical_width = if is_minimized {
+            160
+        } else {
+            win_width + 2 * border_width
+        };
         let dec_height = logical_height * scale;
         let dec_width = logical_width * scale;
 
@@ -534,7 +601,17 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
         let dec = wp.decoration.as_mut().unwrap();
 
         // Position decoration on top of the window top border
-        dec.decoration.set_offset(-border_width, -logical_height);
+        if is_minimized {
+            if let Some(idx) = minimized_idx {
+                let bar_height = state.wm.layout.bar_height;
+                let gap_top = state.wm.layout.gap_top;
+                let bubble_gap = 8;
+                let bubble_y = bar_height + gap_top + idx as i32 * (logical_height + bubble_gap);
+                dec.decoration.set_offset(0, bubble_y - win_y);
+            }
+        } else {
+            dec.decoration.set_offset(-border_width, -logical_height);
+        }
 
         if needs_new_buffer {
             eprintln!(
@@ -696,7 +773,9 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
             }
 
             // Commit surface rendering
-            dec.decoration.sync_next_commit();
+            if !is_minimized {
+                dec.decoration.sync_next_commit();
+            }
             if let Some(ref wl_buf) = dec.buffer {
                 dec.surface.attach(Some(wl_buf), 0, 0);
             }
diff --git a/src/input.rs b/src/input.rs
index 4d4656d..0e85dd8 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -83,6 +83,28 @@ struct InputEvent {
     value: i32,
 }
 
+const UI_SET_ABSBIT: libc::c_ulong = 0x40045567;
+const UI_ABS_SETUP: libc::c_ulong = 1075598596; // 0x40185568
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+struct InputAbsinfo {
+    value: i32,
+    minimum: i32,
+    maximum: i32,
+    fuzz: i32,
+    flat: i32,
+    resolution: i32,
+}
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+struct UinputAbsSetup {
+    code: u16,
+    _padding: u16,
+    absinfo: InputAbsinfo,
+}
+
 #[derive(Debug, Clone, serde::Serialize)]
 pub struct FingerState {
     pub slot: usize,
@@ -152,6 +174,47 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
         if libc::ioctl(fd, UI_SET_RELBIT, REL_HWHEEL as libc::c_int) < 0 {
             return Err(std::io::Error::last_os_error());
         }
+        if libc::ioctl(fd, UI_SET_EVBIT, EV_ABS as libc::c_int) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+        if libc::ioctl(fd, UI_SET_ABSBIT, ABS_X as libc::c_int) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+        let abs_x_setup = UinputAbsSetup {
+            code: ABS_X,
+            _padding: 0,
+            absinfo: InputAbsinfo {
+                value: 0,
+                minimum: 0,
+                maximum: 1920,
+                fuzz: 0,
+                flat: 0,
+                resolution: 1,
+            },
+        };
+        if libc::ioctl(fd, UI_ABS_SETUP, &abs_x_setup as *const UinputAbsSetup as *const libc::c_void) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+
+        if libc::ioctl(fd, UI_SET_ABSBIT, ABS_Y as libc::c_int) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+        let abs_y_setup = UinputAbsSetup {
+            code: ABS_Y,
+            _padding: 0,
+            absinfo: InputAbsinfo {
+                value: 0,
+                minimum: 0,
+                maximum: 1200,
+                fuzz: 0,
+                flat: 0,
+                resolution: 1,
+            },
+        };
+        if libc::ioctl(fd, UI_ABS_SETUP, &abs_y_setup as *const UinputAbsSetup as *const libc::c_void) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+
         if libc::ioctl(fd, UI_SET_EVBIT, EV_KEY as libc::c_int) < 0 {
             return Err(std::io::Error::last_os_error());
         }
@@ -212,13 +275,10 @@ fn write_raw_event(file: &mut std::fs::File, type_: u16, code: u16, value: i32)
     Ok(())
 }
 
-fn write_mouse_move(file: &mut std::fs::File, dx: i32, dy: i32) -> std::io::Result<()> {
-    if dx != 0 {
-        write_raw_event(file, EV_REL, REL_X, dx)?;
-    }
-    if dy != 0 {
-        write_raw_event(file, EV_REL, REL_Y, dy)?;
-    }
+
+fn write_mouse_absolute(file: &mut std::fs::File, x: i32, y: i32) -> std::io::Result<()> {
+    write_raw_event(file, EV_ABS, ABS_X, x)?;
+    write_raw_event(file, EV_ABS, ABS_Y, y)?;
     write_raw_event(file, EV_SYN, SYN_REPORT, 0)?;
     Ok(())
 }
@@ -619,8 +679,10 @@ pub fn run_input_daemon(
                                     }
                                 }
                                 InputDaemonMsg::SimulateMove { dx, dy } => {
-                                    let _ = write_mouse_move(&mut uinput_file, dx, dy);
                                     update_pointer_coords(dx, dy);
+                                    let px = POINTER_X.load(Ordering::SeqCst);
+                                    let py = POINTER_Y.load(Ordering::SeqCst);
+                                    let _ = write_mouse_absolute(&mut uinput_file, px, py);
                                 }
                                 InputDaemonMsg::SimulateButton { button, press } => {
                                     let val = if press { 1 } else { 0 };
@@ -834,8 +896,10 @@ pub fn run_input_daemon(
                                 state.accum_y -= steps_y as f32;
 
                                 if steps_x != 0 || steps_y != 0 {
-                                    let _ = write_mouse_move(&mut uinput_file, steps_x, steps_y);
                                     update_pointer_coords(steps_x, steps_y);
+                                    let px = POINTER_X.load(Ordering::SeqCst);
+                                    let py = POINTER_Y.load(Ordering::SeqCst);
+                                    let _ = write_mouse_absolute(&mut uinput_file, px, py);
                                 }
                             }
                         }
@@ -860,8 +924,10 @@ pub fn run_input_daemon(
                             state.accum_trackpad_y -= steps_y as f32;
 
                             if steps_x != 0 || steps_y != 0 {
-                                let _ = write_mouse_move(&mut uinput_file, steps_x, steps_y);
                                 update_pointer_coords(steps_x, steps_y);
+                                let px = POINTER_X.load(Ordering::SeqCst);
+                                let py = POINTER_Y.load(Ordering::SeqCst);
+                                let _ = write_mouse_absolute(&mut uinput_file, px, py);
                             }
                         }
                     }
diff --git a/src/inspector_cli.rs b/src/inspector_cli.rs
index 47bb034..8c62ae6 100644
--- a/src/inspector_cli.rs
+++ b/src/inspector_cli.rs
@@ -35,6 +35,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for InspectorState {
         qh: &QueueHandle<Self>,
     ) {
         if let wl_registry::Event::Global { name, interface, version } = event {
+            println!("Debug Global: interface='{}', version={}, name={}", interface, version, name);
             if interface == "zclear_inspector_v1" {
                 state.inspector = Some(registry.bind::<ZclearInspectorV1, _, _>(name, version, qh, ()));
             }
@@ -52,7 +53,13 @@ impl Dispatch<ZclearInspectorV1, ()> for InspectorState {
         _qh: &QueueHandle<Self>,
     ) {
         match event {
-            zclear_inspector_v1::Event::InspectedSurface { title, app_id, x, y, width, height, state: surface_state } => {
+            zclear_inspector_v1::Event::InspectedSurface { title, app_id, x, y, width, height, fd, len } => {
+                let mut surface_state = String::new();
+                if len > 0 {
+                    use std::io::Read;
+                    let file = std::fs::File::from(fd);
+                    let _ = file.take(len as u64).read_to_string(&mut surface_state);
+                }
                 state.surfaces.push(InspectedSurface {
                     title,
                     app_id,
@@ -70,7 +77,166 @@ impl Dispatch<ZclearInspectorV1, ()> for InspectorState {
     }
 }
 
+fn get_socket_path() -> String {
+    match std::env::var("WAYLAND_DISPLAY") {
+        Ok(display) => format!("/tmp/ccec-{}.sock", display),
+        Err(_) => "/tmp/ccec.sock".to_string(),
+    }
+}
+
+fn send_ipc_commands(commands: &[String]) -> Result<Vec<String>, std::io::Error> {
+    use std::io::{Read, Write};
+    use std::os::unix::net::UnixStream;
+    let mut stream = UnixStream::connect(get_socket_path())?;
+    
+    let mut combined_cmd = String::new();
+    for cmd in commands {
+        combined_cmd.push_str(cmd);
+    }
+    
+    stream.write_all(combined_cmd.as_bytes())?;
+    
+    let mut buf = [0u8; 4096];
+    let mut response = String::new();
+    loop {
+        match stream.read(&mut buf) {
+            Ok(0) => break,
+            Ok(n) => {
+                response.push_str(&String::from_utf8_lossy(&buf[..n]));
+            }
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
+            Err(e) => return Err(e),
+        }
+    }
+    
+    let replies = response.lines().map(|line| format!("{}\n", line)).collect();
+    Ok(replies)
+}
+
+fn find_widget_recursive(val: &Value, target_type: &str, target_label: Option<&str>) -> Option<(f64, f64, f64, f64)> {
+    if let Some(obj) = val.as_object() {
+        let type_match = obj.get("type")
+            .and_then(|t| t.as_str())
+            .map(|t| t == target_type)
+            .unwrap_or(false);
+            
+        let label_match = match target_label {
+            Some(lbl) => obj.get("label")
+                .and_then(|l| l.as_str())
+                .map(|l| l.eq_ignore_ascii_case(lbl) || l.to_lowercase().contains(&lbl.to_lowercase()))
+                .unwrap_or(false),
+            None => true,
+        };
+        
+        if type_match && label_match {
+            if let Some(rect_arr) = obj.get("rect").and_then(|r| r.as_array()) {
+                if rect_arr.len() == 4 {
+                    let rx = rect_arr[0].as_f64().unwrap_or(0.0);
+                    let ry = rect_arr[1].as_f64().unwrap_or(0.0);
+                    let rw = rect_arr[2].as_f64().unwrap_or(0.0);
+                    let rh = rect_arr[3].as_f64().unwrap_or(0.0);
+                    return Some((rx, ry, rw, rh));
+                }
+            }
+        }
+        
+        // Search children
+        if let Some(children) = obj.get("children").and_then(|c| c.as_array()) {
+            for child in children {
+                if let Some(coords) = find_widget_recursive(child, target_type, target_label) {
+                    return Some(coords);
+                }
+            }
+        }
+    } else if let Some(arr) = val.as_array() {
+        for item in arr {
+            if let Some(coords) = find_widget_recursive(item, target_type, target_label) {
+                return Some(coords);
+            }
+        }
+    }
+    None
+}
+
+fn print_usage(bin_name: &str) {
+    println!("Usage:");
+    println!("  {}                       List all inspected surfaces and their widget trees", bin_name);
+    println!("  {} click-widget --app-id <app_id> --type <widget_type> [--label <widget_label>]", bin_name);
+    println!();
+    println!("Options:");
+    println!("  -a, --app-id <app_id>       The Wayland application ID (e.g. clear-design-interface)");
+    println!("  -t, --type <widget_type>    The widget type (e.g. MenuItem, Button)");
+    println!("  -l, --label <widget_label>  Optional widget label to filter by");
+}
+
 fn main() {
+    let args: Vec<String> = std::env::args().collect();
+    let bin_name = args.get(0).map(|s| s.as_str()).unwrap_or("clear-inspector");
+    
+    let mut app_id = None;
+    let mut widget_type = None;
+    let mut widget_label = None;
+    let mut is_click_widget = false;
+
+    if args.len() > 1 {
+        if args[1] == "click-widget" {
+            is_click_widget = true;
+            let mut i = 2;
+            while i < args.len() {
+                match args[i].as_str() {
+                    "--app-id" | "-a" => {
+                        if i + 1 < args.len() {
+                            app_id = Some(args[i+1].clone());
+                            i += 2;
+                        } else {
+                            eprintln!("Error: missing value for --app-id");
+                            std::process::exit(1);
+                        }
+                    }
+                    "--type" | "-t" => {
+                        if i + 1 < args.len() {
+                            widget_type = Some(args[i+1].clone());
+                            i += 2;
+                        } else {
+                            eprintln!("Error: missing value for --type");
+                            std::process::exit(1);
+                        }
+                    }
+                    "--label" | "-l" => {
+                        if i + 1 < args.len() {
+                            widget_label = Some(args[i+1].clone());
+                            i += 2;
+                        } else {
+                            eprintln!("Error: missing value for --label");
+                            std::process::exit(1);
+                        }
+                    }
+                    "-h" | "--help" | "help" => {
+                        print_usage(bin_name);
+                        std::process::exit(0);
+                    }
+                    _ => {
+                        eprintln!("Error: unknown argument '{}'", args[i]);
+                        print_usage(bin_name);
+                        std::process::exit(1);
+                    }
+                }
+            }
+            if app_id.is_none() || widget_type.is_none() {
+                eprintln!("Error: --app-id and --type are required for click-widget command");
+                print_usage(bin_name);
+                std::process::exit(1);
+            }
+        } else if args[1] == "-h" || args[1] == "--help" || args[1] == "help" {
+            print_usage(bin_name);
+            std::process::exit(0);
+        } else {
+            eprintln!("Error: unknown command '{}'", args[1]);
+            print_usage(bin_name);
+            std::process::exit(1);
+        }
+    }
+
     let conn = match Connection::connect_to_env() {
         Ok(c) => c,
         Err(e) => {
@@ -79,8 +245,8 @@ fn main() {
         }
     };
 
-    let (_globals, mut event_queue) = registry_queue_init(&conn).unwrap();
-    let _qh = event_queue.handle();
+    let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
+    let qh = event_queue.handle();
 
     let mut state = InspectorState {
         inspector: None,
@@ -88,13 +254,10 @@ fn main() {
         surfaces: Vec::new(),
     };
 
-    // Populate registry globals
-    event_queue.roundtrip(&mut state).unwrap();
-
-    let inspector = match state.inspector.take() {
-        Some(ins) => ins,
-        None => {
-            eprintln!("Error: zclear_inspector_v1 global protocol not found on Wayland registry.");
+    let inspector = match globals.bind::<ZclearInspectorV1, _, _>(&qh, 1..=1, ()) {
+        Ok(ins) => ins,
+        Err(e) => {
+            eprintln!("Error: zclear_inspector_v1 global protocol not found on Wayland registry: {:?}", e);
             eprintln!("Ensure clear-river is running and supports the inspector protocol.");
             std::process::exit(1);
         }
@@ -108,6 +271,74 @@ fn main() {
         event_queue.blocking_dispatch(&mut state).unwrap();
     }
 
+    if is_click_widget {
+        let target_app_id = app_id.as_deref().unwrap();
+        let target_type = widget_type.as_deref().unwrap();
+        let matched_surface = state.surfaces.iter().find(|s| s.app_id.eq_ignore_ascii_case(target_app_id));
+        if let Some(surface) = matched_surface {
+            if let Ok(json_val) = serde_json::from_str::<Value>(&surface.state) {
+                let mut max_logical_w = 0.0;
+                if let Some(arr) = json_val.as_array() {
+                    for item in arr {
+                        if let Some(obj) = item.as_object() {
+                            if let Some(rect_arr) = obj.get("rect").and_then(|r| r.as_array()) {
+                                if rect_arr.len() == 4 {
+                                    let rx_val = rect_arr[0].as_f64().unwrap_or(0.0);
+                                    let rw_val = rect_arr[2].as_f64().unwrap_or(0.0);
+                                    if rx_val == 0.0 && rw_val > max_logical_w {
+                                        max_logical_w = rw_val;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                let scale = if max_logical_w > 0.0 {
+                    (surface.width as f64) / max_logical_w
+                } else {
+                    1.0
+                };
+
+                if let Some((rx, ry, rw, rh)) = find_widget_recursive(&json_val, target_type, widget_label.as_deref()) {
+                    let target_x = (surface.x as f64 + (rx + rw / 2.0) * scale) as i32;
+                    let target_y = (surface.y as f64 + (ry + rh / 2.0) * scale) as i32;
+                    println!("Found matching widget! Coordinate local bounds: [{}, {}, {}, {}], global target: ({}, {}), scale: {}", rx, ry, rw, rh, target_x, target_y, scale);
+                    
+                    let commands = vec![
+                        format!("focus-window {}\n", target_app_id),
+                        format!("pointer-move-to {} {}\n", target_x, target_y),
+                        format!("pointer-click left\n"),
+                    ];
+                    match send_ipc_commands(&commands) {
+                        Ok(replies) => {
+                            for r in replies {
+                                print!("{}", r);
+                            }
+                        }
+                        Err(e) => {
+                            eprintln!("Error sending IPC commands to ccec socket: {:?}", e);
+                            std::process::exit(1);
+                        }
+                    }
+                } else {
+                    eprintln!("Error: Widget of type '{}'{} not found in surface '{}'", 
+                        target_type, 
+                        widget_label.as_ref().map(|l| format!(" with label '{}'", l)).unwrap_or_default(),
+                        target_app_id
+                    );
+                    std::process::exit(1);
+                }
+            } else {
+                eprintln!("Error: Failed to parse widget state JSON for surface '{}'", target_app_id);
+                std::process::exit(1);
+            }
+        } else {
+            eprintln!("Error: Surface with app_id '{}' not found", target_app_id);
+            std::process::exit(1);
+        }
+        return;
+    }
+
     // Print results
     println!("Found {} active inspected surface(s):", state.surfaces.len());
     println!("{}", "=".repeat(60));
diff --git a/src/ipc.rs b/src/ipc.rs
index 5cd0b12..8580dca 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -78,6 +78,32 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
             }
             state.needs_render = true;
         }
+        "minimize" => {
+            if let Some(seat) = state.seats.first() {
+                if let Some(focused_id) = seat.focused_window_id {
+                    if let Some(window) = state.get_window_mut(focused_id) {
+                        window.minimized = true;
+                    }
+                    // Shift focus to the next visible window
+                    let active_tags = state.active_tags;
+                    let visible_ids: Vec<u64> = state
+                        .windows
+                        .iter()
+                        .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.app_id.as_deref() != Some("clear-status-interface"))
+                        .map(|w| w.id)
+                        .collect();
+                    let next_id = visible_ids.last().copied();
+                    for s in &mut state.seats {
+                        if !s.removed {
+                            s.focused_window_id = next_id;
+                        }
+                    }
+                }
+            }
+            state.needs_render = true;
+            state.needs_focus = true;
+            state.needs_status_update = true;
+        }
         "focus-next" => {
             // Focus the next visible window (wrapping) and move it to the
             // front of the cascade stack (end of windows vector).
@@ -87,7 +113,7 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
                 let visible_ids: Vec<u64> = state
                     .windows
                     .iter()
-                    .filter(|w| (w.tags & active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
+                    .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.app_id.as_deref() != Some("clear-status-interface"))
                     .map(|w| w.id)
                     .collect();
                 if visible_ids.len() > 1 {
@@ -106,6 +132,34 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
             state.needs_focus = true;
             state.needs_status_update = true;
         }
+        "focus-prev" => {
+            // Focus the previous visible window (wrapping) and move it to the
+            // front of the cascade stack (end of windows vector).
+            if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
+                let focused_id = seat.focused_window_id;
+                let active_tags = state.active_tags;
+                let visible_ids: Vec<u64> = state
+                    .windows
+                    .iter()
+                    .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.app_id.as_deref() != Some("clear-status-interface"))
+                    .map(|w| w.id)
+                    .collect();
+                if visible_ids.len() > 1 {
+                    if let Some(fid) = focused_id {
+                        if let Some(idx) = visible_ids.iter().position(|id| *id == fid) {
+                            let prev_idx = if idx == 0 { visible_ids.len() - 1 } else { idx - 1 };
+                            let prev_id = visible_ids[prev_idx];
+                            seat.focused_window_id = Some(prev_id);
+                            // Move newly focused window to front of cascade stack
+                            state.move_window_to_end(prev_id);
+                        }
+                    }
+                }
+            }
+            state.needs_render = true;
+            state.needs_focus = true;
+            state.needs_status_update = true;
+        }
         "exit" => {
             // Signal exit request
         }
@@ -113,7 +167,7 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
             crate::restart::wm_restart();
         }
         "reload" => {
-            crate::restart::wm_restart();
+            crate::restart::wm_reload(state);
         }
         "expose" => {
             let active = !state.expose_active;
@@ -254,6 +308,9 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
         "set-mode" => {
             handle_set_mode_command(rest, state);
         }
+        "apply-mode-sharing" => {
+            handle_apply_mode_sharing_command(rest, state);
+        }
         "bind" => {
             handle_bind_command(rest, state);
         }
@@ -288,6 +345,50 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
                 }
             }
         }
+        "focus-window" => {
+            let app_id_or_title = rest.trim();
+            if !app_id_or_title.is_empty() {
+                let mut found_id = None;
+                for win in &state.windows {
+                    if !win.closed && !win.minimized {
+                        if let Some(ref app_id) = win.app_id {
+                            if app_id.eq_ignore_ascii_case(app_id_or_title) {
+                                found_id = Some(win.id);
+                                break;
+                            }
+                        }
+                    }
+                }
+                if found_id.is_none() {
+                    for win in &state.windows {
+                        if !win.closed && !win.minimized {
+                            if let Some(ref title) = win.title {
+                                if title.to_lowercase().contains(&app_id_or_title.to_lowercase()) {
+                                    found_id = Some(win.id);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+                if let Some(wid) = found_id {
+                    if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
+                        seat.focused_window_id = Some(wid);
+                        state.move_window_to_end(wid);
+                        state.needs_focus = true;
+                        state.needs_render = true;
+                        state.needs_status_update = true;
+                        reply = format!("ok focused window {}\n", wid);
+                    } else {
+                        reply = "error: no active seat\n".to_string();
+                    }
+                } else {
+                    reply = "error: window not found\n".to_string();
+                }
+            } else {
+                reply = "error: usage: focus-window <app_id|title>\n".to_string();
+            }
+        }
         "pointer-location" => {
             let px = crate::input::POINTER_X.load(std::sync::atomic::Ordering::SeqCst);
             let py = crate::input::POINTER_Y.load(std::sync::atomic::Ordering::SeqCst);
@@ -511,6 +612,14 @@ fn handle_layout_command(rest: &str, state: &mut WindowManager) {
                 }
             }
         }
+        "grid_gap" => {
+            if let Ok(value) = value_str.parse::<i32>() {
+                state.layout.grid_gap = value;
+                if state.notifications_enable {
+                    crate::config::show_notification("ccec", &format!("Grid gap set to {}px", value));
+                }
+            }
+        }
         "grid_border_width" => {
             if let Ok(value) = value_str.parse::<i32>() {
                 state.layout.grid_border_width = value;
@@ -593,6 +702,7 @@ fn handle_mode_command(rest: &str, state: &mut WindowManager) {
         title_pattern,
         single_instance,
         tag,
+        circular: false,
     });
 }
 
@@ -616,6 +726,38 @@ fn handle_set_mode_command(rest: &str, state: &mut WindowManager) {
     }
 }
 
+/// Handle "apply-mode-sharing <mode>" command — apply <mode> to all windows sharing a tiling mode with the focused window.
+fn handle_apply_mode_sharing_command(rest: &str, state: &mut WindowManager) {
+    let mode_str = rest.trim();
+    if mode_str.is_empty() {
+        return;
+    }
+    let new_mode = parse_tiling_mode(mode_str);
+
+    // Find the focused window's current tiling mode
+    let old_mode = if let Some(window) = state.focused_window() {
+        window.tiling_mode
+    } else {
+        return;
+    };
+
+    let notifications_enable = state.notifications_enable;
+
+    // Iterate over all windows and update tiling mode for matching windows
+    for window in &mut state.windows {
+        if !window.closed && window.tiling_mode == old_mode {
+            window.tiling_mode = new_mode;
+            window.mode_locked = true;
+        }
+    }
+
+    if notifications_enable {
+        crate::config::show_notification("ccec", &format!("Applied tiling mode {} to all windows sharing mode {}", new_mode.as_str(), old_mode.as_str()));
+    }
+    state.needs_render = true;
+    state.needs_status_update = true;
+}
+
 /// Handle "bind <mods> <key> <action> [command]" command
 fn handle_bind_command(rest: &str, state: &mut WindowManager) {
     // Format: bind <mods> <key> <action> [command...]
@@ -1023,6 +1165,15 @@ mod tests {
         assert_eq!(state.layout.gap, 18);
     }
 
+    #[test]
+    fn test_ipc_layout_grid_gap() {
+        let mut state = WindowManager::default();
+        assert_eq!(state.layout.grid_gap, 18);
+
+        handle_ipc_command("layout grid_gap 24", &mut state);
+        assert_eq!(state.layout.grid_gap, 24);
+    }
+
     #[test]
     fn test_ipc_layout_gap_sides() {
         let mut state = WindowManager::default();
@@ -1217,6 +1368,31 @@ mod tests {
         assert_eq!(state.active_tags, 1); // Tag 1
     }
 
+    #[test]
+    fn test_ipc_focus_next_prev() {
+        let mut state = WindowManager::default();
+        // Setup 3 windows
+        state.windows.push(crate::types::Window { id: 1, tags: 1, ..Default::default() });
+        state.windows.push(crate::types::Window { id: 2, tags: 1, ..Default::default() });
+        state.windows.push(crate::types::Window { id: 3, tags: 1, ..Default::default() });
+        state.seats.push(crate::types::Seat { id: 1, focused_window_id: Some(1), ..Default::default() });
+
+        handle_ipc_command("focus-next", &mut state);
+        assert_eq!(state.seats[0].focused_window_id, Some(2));
+
+        handle_ipc_command("focus-next", &mut state);
+        assert_eq!(state.seats[0].focused_window_id, Some(1));
+
+        handle_ipc_command("focus-next", &mut state);
+        assert_eq!(state.seats[0].focused_window_id, Some(3));
+
+        handle_ipc_command("focus-prev", &mut state);
+        assert_eq!(state.seats[0].focused_window_id, Some(1));
+
+        handle_ipc_command("focus-prev", &mut state);
+        assert_eq!(state.seats[0].focused_window_id, Some(3));
+    }
+
     #[test]
     fn test_ipc_pointer_and_keys() {
         let mut state = WindowManager::default();
diff --git a/src/state.rs b/src/state.rs
index 8e41a94..ba244af 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -58,6 +58,7 @@ pub struct PersistentWindow {
     pub tags: u32,
     pub tiling_mode: TilingMode,
     pub mode_locked: bool,
+    pub minimized: bool,
 }
 
 /// Percent-encode spaces, tabs, newlines, and percent signs in a string.
@@ -137,8 +138,8 @@ pub fn write_state(wm: &WindowManager) {
             let mode_str = win.tiling_mode.as_str();
             let _ = writeln!(
                 f,
-                "window\t{}\t{}\t{}\t{}\t{}\t{}",
-                ident, app_id, title, win.tags, mode_str, win.mode_locked
+                "window\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
+                ident, app_id, title, win.tags, mode_str, win.mode_locked, win.minimized
             );
         }
 
@@ -204,6 +205,7 @@ pub fn read_state() -> Option<PersistentState> {
                 let tags = parts[3].parse::<u32>().unwrap_or(1);
                 let tiling_mode = parse_tiling_mode_str(parts[4]);
                 let mode_locked = parts[5] == "true";
+                let minimized = parts.get(6).map(|&s| s == "true").unwrap_or(false);
 
                 windows.push(PersistentWindow {
                     identifier,
@@ -212,6 +214,7 @@ pub fn read_state() -> Option<PersistentState> {
                     tags,
                     tiling_mode,
                     mode_locked,
+                    minimized,
                 });
             }
             continue;
@@ -309,6 +312,7 @@ pub fn apply_state(wm: &mut WindowManager, state: &PersistentState) {
                 win.tiling_mode = pw.tiling_mode;
                 win.mode_locked = true;
             }
+            win.minimized = pw.minimized;
         }
 
         // Post-restore normalization: Ensure blank steam_proton helper windows are untagged (tags = 0)
@@ -395,8 +399,8 @@ mod tests {
                 let title = w.title.as_deref().map(pct_encode).unwrap_or("-".to_string());
                 let _ = writeln!(
                     f,
-                    "window\t{}\t{}\t{}\t{}\t{}\t{}",
-                    ident, app_id, title, w.tags, w.tiling_mode.as_str(), w.mode_locked
+                    "window\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
+                    ident, app_id, title, w.tags, w.tiling_mode.as_str(), w.mode_locked, w.minimized
                 );
             }
         }
@@ -435,7 +439,8 @@ mod tests {
                     let tags = parts[3].parse::<u32>().unwrap_or(1);
                     let tiling_mode = parse_tiling_mode_str(parts[4]);
                     let mode_locked = parts[5] == "true";
-                    windows.push(PersistentWindow { identifier, app_id, title, tags, tiling_mode, mode_locked });
+                    let minimized = parts.get(6).map(|&s| s == "true").unwrap_or(false);
+                    windows.push(PersistentWindow { identifier, app_id, title, tags, tiling_mode, mode_locked, minimized });
                 }
             }
         }
@@ -461,6 +466,7 @@ mod tests {
         assert_eq!(pw.tags, 0b100);
         assert_eq!(pw.tiling_mode, TilingMode::Fullscreen);
         assert!(pw.mode_locked);
+        assert!(!pw.minimized);
 
         // Clean up
         let _ = fs::remove_file(&path);
diff --git a/src/status.rs b/src/status.rs
index 121eab2..b6599b4 100644
--- a/src/status.rs
+++ b/src/status.rs
@@ -55,7 +55,7 @@ pub fn write_status_files(state: &WindowManager) {
             };
             let _ = writeln!(
                 f,
-                "window app_id={} title={} mode={} decoration={} presentation={} tags={} x={} y={} w={} h={} has_parent={}",
+                "window app_id={} title={} mode={} decoration={} presentation={} tags={} x={} y={} w={} h={} has_parent={} minimized={}",
                 win.app_id.as_deref().unwrap_or("(null)"),
                 win.title.as_deref().unwrap_or("(null)"),
                 mode_str,
@@ -63,6 +63,7 @@ pub fn write_status_files(state: &WindowManager) {
                 presentation_str,
                 win.tags, win.x, win.y, win.width, win.height,
                 win.has_parent,
+                win.minimized,
             );
         }
 
diff --git a/src/status_server.rs b/src/status_server.rs
index 0a69de9..8b7fc67 100644
--- a/src/status_server.rs
+++ b/src/status_server.rs
@@ -268,15 +268,40 @@ pub fn build_status_update(wm: &crate::types::WindowManager) -> StatusUpdate {
     }
 }
 
+fn read_status_normal_color_from_config() -> String {
+    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/lsgalante".to_string());
+    let path = format!("{}/.config/ccec/config.toml", home);
+    if let Ok(content) = std::fs::read_to_string(&path) {
+        for line in content.lines() {
+            let trimmed = line.trim();
+            if let Some(rest) = trimmed.strip_prefix("status_normal_color") {
+                let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                let hex = rest.trim_end_matches('"').trim();
+                if hex.starts_with('#') {
+                    return hex.to_string();
+                } else if !hex.is_empty() {
+                    return format!("#{}", hex);
+                }
+            }
+        }
+    }
+    "#ccccd8".to_string()
+}
+
 /// Render tag state as a JSON string with pango markup, matching the format
 /// produced by the old ccec-tags.sh script.
 ///
 /// Colors:
-/// - Active + Focused: bright (#a8c0d8)
+/// - Active + Focused: bright (dynamic normal color, defaults to #ccccd8)
 /// - Focused only: dim (#666666)
 /// - Active only: medium (#888888)
 /// - Neither: dark (#444444)
 fn render_tags_json(active: u32, focused: u32, num_tags: u32) -> String {
+    let normal_color = read_status_normal_color_from_config();
+    render_tags_json_with_color(active, focused, num_tags, &normal_color)
+}
+
+fn render_tags_json_with_color(active: u32, focused: u32, num_tags: u32, normal_color: &str) -> String {
     let mut text = String::new();
     for i in 0..num_tags {
         let bit = 1u32 << i;
@@ -286,7 +311,7 @@ fn render_tags_json(active: u32, focused: u32, num_tags: u32) -> String {
         let is_focused = (focused & bit) != 0;
 
         let color = if is_focused && is_active {
-            "#a8c0d8"
+            normal_color
         } else if is_focused {
             "#666666"
         } else if is_active {
@@ -310,7 +335,7 @@ mod tests {
 
     #[test]
     fn test_render_tags_json_single_tag() {
-        let json = render_tags_json(1, 1, 4);
+        let json = render_tags_json_with_color(1, 1, 4, "#a8c0d8");
         // Tag 1 should be active+focused (#a8c0d8), tags 2-4 should be dark (#444444)
         assert!(json.contains("#a8c0d8"), "tag 1 should be bright: {}", json);
         assert!(
@@ -323,7 +348,7 @@ mod tests {
 
     #[test]
     fn test_render_tags_json_no_focus() {
-        let json = render_tags_json(1, 0, 4);
+        let json = render_tags_json_with_color(1, 0, 4, "#a8c0d8");
         // Tag 1 is active but not focused → #888888
         assert!(
             json.contains("#888888"),
diff --git a/src/tiling.rs b/src/tiling.rs
index 6a5297b..c66f0c5 100644
--- a/src/tiling.rs
+++ b/src/tiling.rs
@@ -39,8 +39,9 @@ pub fn tile_cascade(
     let height = screen_h - bar_height - gap_top - gap_bottom - (dec_h + bw) - cascade_offset * (n_cascade - 1);
     let width = if width < 1 { 1 } else { width };
     let height = if height < 1 { 1 } else { height };
-    let x = gap_left + bw + idx * cascade_offset;
-    let y = bar_height + gap_top + dec_h + idx * cascade_offset;
+    let pos_idx = n_cascade - 1 - idx;
+    let x = gap_left + bw + pos_idx * cascade_offset;
+    let y = bar_height + gap_top + dec_h + pos_idx * cascade_offset;
     (x, y, width, height)
 }
 
@@ -149,10 +150,15 @@ mod tests {
 
     #[test]
     fn test_tile_cascade_multiple() {
-        // 3 cascade windows, idx=2 (the back one)
+        // 3 cascade windows, idx=2 (the back one, highest index, should be up/left)
         let (x, y, _w, _h) = tile_cascade(1920, 1080, 18, 18, 18, 18, 18, 18, 32, 28, 3, 2);
-        assert_eq!(x, 36 + 2 * 32); // gap_left + bw + idx * offset
-        assert_eq!(y, 64 + 2 * 32); // bar + gap_top + bw + idx * offset
+        assert_eq!(x, 36); // gap_left + bw
+        assert_eq!(y, 64); // bar + gap_top + bw
+
+        // 3 cascade windows, idx=0 (the focused one, lowest index, should be down/right)
+        let (x, y, _w, _h) = tile_cascade(1920, 1080, 18, 18, 18, 18, 18, 18, 32, 28, 3, 0);
+        assert_eq!(x, 36 + 2 * 32); // gap_left + bw + 2 * offset
+        assert_eq!(y, 64 + 2 * 32); // bar + gap_top + bw + 2 * offset
     }
 
     #[test]
diff --git a/src/types.rs b/src/types.rs
index b906622..0d89512 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -33,6 +33,7 @@ pub enum Action {
     Spawn,
     Close,
     FocusNext,
+    FocusPrev,
     Move,
     Resize,
     Exit,
@@ -54,6 +55,7 @@ pub enum Action {
     SetTag3,
     SetTag4,
     Expose,
+    Minimize,
 }
 
 /// Layout parameters
@@ -81,6 +83,7 @@ pub struct Layout {
     pub background_a: u32,
     pub border_font_size: i32,
     pub transition_duration: i32,
+    pub grid_gap: i32,
 }
 
 impl Default for Layout {
@@ -108,6 +111,7 @@ impl Default for Layout {
             background_a: 0xFFFFFFFFu32,
             border_font_size: 11,
             transition_duration: 300,
+            grid_gap: 18,
         }
     }
 }
@@ -120,6 +124,7 @@ pub struct ModeRule {
     pub title_pattern: Option<String>,
     pub single_instance: bool,
     pub tag: i32,
+    pub circular: bool,
 }
 
 /// A pending keyboard binding waiting to be applied to seats
@@ -214,6 +219,7 @@ pub struct Window {
     pub fullscreen_requested: bool,
     pub maximize_requested: bool,
     pub minimize_requested: bool,
+    pub minimized: bool,
     pub tiling_mode: TilingMode,
     pub mode_locked: bool,
     /// Whether we've queued an xprop check for XWayland parent detection.
@@ -227,6 +233,7 @@ pub struct Window {
     pub anim_w: Option<f64>,
     pub anim_h: Option<f64>,
     pub anim_opacity: Option<f64>,
+    pub circular: bool,
 }
 
 impl Default for Window {
@@ -255,6 +262,7 @@ impl Default for Window {
             fullscreen_requested: false,
             maximize_requested: false,
             minimize_requested: false,
+            minimized: false,
             tiling_mode: TilingMode::Floating,
             mode_locked: false,
             needs_xprop_check: false,
@@ -264,6 +272,7 @@ impl Default for Window {
             anim_w: None,
             anim_h: None,
             anim_opacity: None,
+            circular: false,
         }
     }
 }
@@ -506,6 +515,8 @@ pub fn parse_action(s: &str) -> Action {
         Action::Exit
     } else if s == "focus-next" {
         Action::FocusNext
+    } else if s == "focus-prev" {
+        Action::FocusPrev
     } else if s == "move" {
         Action::Move
     } else if s == "resize" {
@@ -520,6 +531,8 @@ pub fn parse_action(s: &str) -> Action {
         Action::Restart
     } else if s == "fullscreen" {
         Action::Fullscreen
+    } else if s == "minimize" {
+        Action::Minimize
     } else if s.starts_with("spawn")
         && (s.len() == 5 || s.as_bytes()[5] == b' ' || s.as_bytes()[5] == b'-')
     {
@@ -659,6 +672,7 @@ mod tests {
         assert_eq!(parse_action("close"), Action::Close);
         assert_eq!(parse_action("exit"), Action::Exit);
         assert_eq!(parse_action("focus-next"), Action::FocusNext);
+        assert_eq!(parse_action("focus-prev"), Action::FocusPrev);
         assert_eq!(parse_action("move"), Action::Move);
         assert_eq!(parse_action("resize"), Action::Resize);
         assert_eq!(parse_action("layout-next"), Action::LayoutNext);
@@ -671,6 +685,7 @@ mod tests {
         assert_eq!(parse_action("toggle-2"), Action::Toggle2);
         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("unknown"), Action::None);
     }
 
diff --git a/src/wayland.rs b/src/wayland.rs
index 0018c61..67380e4 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -1055,6 +1055,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                     // and must happen during ManageStart.
                     crate::wm::render_borders(state);
                     crate::wm::render_opacity(state);
+                    crate::wm::render_circular(state);
 
                     // Update and render window title decorations on borders
                     crate::decorations::update_decorations(state, qhandle);
@@ -1519,13 +1520,39 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
             }
 
             river_window_v1::Event::MinimizeRequested { .. } => {
+                let mut minimized_id = None;
                 if let Some(window) = state.wm.get_window_mut(wid) {
                     window.minimize_requested = true;
+                    window.minimized = true;
+                    minimized_id = Some(wid);
                     eprintln!(
                         "[window] id={} (app_id={:?}) requested minimize",
                         wid, window.app_id
                     );
                 }
+
+                if let Some(wid) = minimized_id {
+                    // Shift focus to the next visible window if the minimized window was focused
+                    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(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.id != wid && w.app_id.as_deref() != Some("clear-status-interface"))
+                                .map(|w| w.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;
+                if let Some(ref wm) = state.window_manager {
+                    wm.manage_dirty();
+                }
             }
 
             river_window_v1::Event::ShowWindowMenuRequested { x, y } => {
@@ -1588,21 +1615,27 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                             wid,
                             target_app_id
                         );
+                        let already_focused = seat.focused_window_id == Some(wid);
                         seat.focused_window_id = Some(wid);
                         // Move clicked window to front of cascade stack
-                        state.wm.move_window_to_end(wid);
+                        let moved = state.wm.move_window_to_end(wid);
 
                         // Disable expose if it was active
+                        let mut expose_changed = false;
                         if state.wm.expose_active {
                             crate::wm::set_expose_active(&mut state.wm, false);
-                            if let Some(ref wm) = state.window_manager {
-                                wm.manage_dirty();
-                            }
+                            expose_changed = true;
                         }
 
                         state.wm.needs_render = true;
                         state.wm.needs_focus = true;
                         state.wm.needs_status_update = true;
+
+                        if !already_focused || moved || expose_changed {
+                            if let Some(ref wm) = state.window_manager {
+                                wm.manage_dirty();
+                            }
+                        }
                     }
                 }
             }
@@ -1613,6 +1646,21 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
                 if let Some(wid) = state.window_id_for_proxy(&river_window) {
                     if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
                         seat.hovered_window_id = Some(wid);
+
+                        if state.wm.expose_active {
+                            let already_focused = seat.focused_window_id == Some(wid);
+                            seat.focused_window_id = Some(wid);
+
+                            state.wm.needs_focus = true;
+                            state.wm.needs_render = true;
+                            state.wm.needs_status_update = true;
+
+                            if !already_focused {
+                                if let Some(ref wm) = state.window_manager {
+                                    wm.manage_dirty();
+                                }
+                            }
+                        }
                     }
                 }
             }
@@ -2131,7 +2179,7 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
                         .windows
                         .iter()
                         .filter(|w| {
-                            (w.tags & state.wm.active_tags) != 0 && !w.closed && w.id != focused_id && w.app_id.as_deref() != Some("clear-status-interface")
+                            (w.tags & state.wm.active_tags) != 0 && !w.closed && !w.minimized && w.id != focused_id && w.app_id.as_deref() != Some("clear-status-interface")
                         })
                         .map(|w| w.id)
                         .collect();
@@ -2145,6 +2193,33 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
             state.wm.needs_focus = true;
             state.wm.needs_status_update = true;
         }
+        Action::Minimize => {
+            // Get the focused window ID on this seat
+            let focused_id = state.wm.seats.iter().find(|s| s.id == seat_id).and_then(|s| s.focused_window_id);
+            if let Some(focused_id) = focused_id {
+                if let Some(w) = state.wm.get_window_mut(focused_id) {
+                    w.minimized = true;
+                }
+                // Shift focus to the next visible window
+                let active_tags = state.wm.active_tags;
+                let visible_ids: Vec<u64> = state.wm
+                    .windows
+                    .iter()
+                    .filter(|w| (w.tags & active_tags) != 0 && !w.closed && !w.minimized && w.id != focused_id && w.app_id.as_deref() != Some("clear-status-interface"))
+                    .map(|w| w.id)
+                    .collect();
+                let next_id = visible_ids.last().copied();
+                if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == seat_id) {
+                    seat.focused_window_id = next_id;
+                }
+            }
+            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();
+            }
+        }
         Action::FocusNext => {
             // Focus the next visible window (wrapping) of the same tiling mode,
             // and move it to the front of the cascade stack (end of windows vector).
@@ -2160,6 +2235,7 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
                     .filter(|w| {
                         (w.tags & state.wm.active_tags) != 0
                             && !w.closed
+                            && !w.minimized
                             && w.app_id.as_deref() != Some("clear-status-interface")
                             && (focused_mode.is_none() || Some(w.tiling_mode) == focused_mode)
                     })
@@ -2188,6 +2264,50 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
                 }
             }
         }
+        Action::FocusPrev => {
+            // Focus the previous visible window (wrapping) of the same tiling mode,
+            // and move it to the front of the cascade stack (end of windows vector).
+            if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
+                let focused_id = seat.focused_window_id;
+                let focused_mode = focused_id.and_then(|fid| {
+                    state.wm.windows.iter().find(|w| w.id == fid).map(|w| w.tiling_mode)
+                });
+                let visible_ids: Vec<u64> = state
+                    .wm
+                    .windows
+                    .iter()
+                    .filter(|w| {
+                        (w.tags & state.wm.active_tags) != 0
+                            && !w.closed
+                            && !w.minimized
+                            && w.app_id.as_deref() != Some("clear-status-interface")
+                            && (focused_mode.is_none() || Some(w.tiling_mode) == focused_mode)
+                    })
+                    .map(|w| w.id)
+                    .collect();
+                if let Some(fid) = focused_id {
+                    if let Some(idx) = visible_ids.iter().position(|id| *id == fid) {
+                        let prev_idx = if idx == 0 { visible_ids.len() - 1 } else { idx - 1 };
+                        let prev_id = visible_ids[prev_idx];
+                        seat.focused_window_id = Some(prev_id);
+                        // Move the newly focused window to the end of the
+                        // windows vector so it gets the front cascade position
+                        // (rightmost/bottommost) and brightest border.
+                        state.wm.move_window_to_end(prev_id);
+                        state.wm.needs_render = true;
+                        state.wm.needs_focus = true;
+                        state.wm.needs_status_update = true;
+                    }
+                } else if !visible_ids.is_empty() {
+                    let prev_id = visible_ids[visible_ids.len() - 1];
+                    seat.focused_window_id = Some(prev_id);
+                    state.wm.move_window_to_end(prev_id);
+                    state.wm.needs_render = true;
+                    state.wm.needs_focus = true;
+                    state.wm.needs_status_update = true;
+                }
+            }
+        }
         Action::Move | Action::Resize => {
             let op_type = if *action == Action::Move {
                 PointerOpType::Move
@@ -3373,7 +3493,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
         match event {
             wl_pointer::Event::Enter { serial, surface, surface_x, surface_y } => {
                 eprintln!("[pointer] enter surface={:?} x={} y={}", surface, surface_x, surface_y);
-                state.pointer_hovered_surface = Some(surface);
+                state.pointer_hovered_surface = Some(surface.clone());
                 state.last_pointer_surface_x = surface_x;
                 state.last_pointer_surface_y = surface_y;
 
@@ -3384,6 +3504,59 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
                 }
 
                 Self::update_cursor_shape_for_surface(state, proxy);
+
+                if state.wm.expose_active {
+                    let current_surface = &surface;
+                    let matched_window = state.window_proxies.iter().find_map(|(id, proxy)| {
+                        if let Some(dec) = &proxy.decoration {
+                            if &dec.surface == current_surface {
+                                return Some(*id);
+                            }
+                        }
+                        if let Some(dec) = &proxy.dec_left {
+                            if &dec.surface == current_surface {
+                                return Some(*id);
+                            }
+                        }
+                        if let Some(dec) = &proxy.dec_right {
+                            if &dec.surface == current_surface {
+                                return Some(*id);
+                            }
+                        }
+                        if let Some(dec) = &proxy.dec_bottom {
+                            if &dec.surface == current_surface {
+                                return Some(*id);
+                            }
+                        }
+                        None
+                    });
+
+                    if let Some(wid) = matched_window {
+                        let seat_id = state.seat_proxies.iter().find_map(|(sid, sp)| {
+                            if sp.wl_pointer.as_ref() == Some(proxy) {
+                                Some(*sid)
+                            } else {
+                                None
+                            }
+                        });
+
+                        if let Some(sid) = seat_id {
+                            if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
+                                let already_focused = seat.focused_window_id == Some(wid);
+                                seat.focused_window_id = Some(wid);
+                                state.wm.needs_focus = true;
+                                state.wm.needs_render = true;
+                                state.wm.needs_status_update = true;
+
+                                if !already_focused {
+                                    if let Some(ref wm) = state.window_manager {
+                                        wm.manage_dirty();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
             }
             wl_pointer::Event::Leave { .. } => {
                 eprintln!("[pointer] leave");
@@ -3486,12 +3659,33 @@ impl Dispatch<wl_pointer::WlPointer, ()> for AppState {
 
                                         if let Some(sid) = seat_id {
                                             if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
+                                                let already_focused = seat.focused_window_id == Some(wid);
                                                 seat.focused_window_id = Some(wid);
-                                                state.wm.move_window_to_end(wid);
+                                                let moved = state.wm.move_window_to_end(wid);
+                                                let mut expose_changed = false;
                                                 if state.wm.expose_active {
                                                     crate::wm::set_expose_active(&mut state.wm, false);
+                                                    expose_changed = true;
                                                 }
+                                                
+                                                let mut unminimized = false;
+                                                if let Some(w) = state.wm.get_window_mut(wid) {
+                                                    if w.minimized {
+                                                        w.minimized = false;
+                                                        w.minimize_requested = false;
+                                                        unminimized = true;
+                                                    }
                                                 }
+
+                                                state.wm.needs_focus = true;
+                                                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();
+                                                    }
+                                                }
+                                            }
                                                                  // Determine the specific PointerOpType based on coordinates on the matched surface
                                             let op_type = if let Some(w) = state.wm.windows.iter().find(|win| win.id == wid) {
                                                 let border_w = if state.wm.expose_active && w.tiling_mode != crate::types::TilingMode::Popup {
diff --git a/src/wm.rs b/src/wm.rs
index aa28e18..61c7eb9 100644
--- a/src/wm.rs
+++ b/src/wm.rs
@@ -9,7 +9,7 @@ use crate::borders::compute_border_colors;
 use crate::protocol::river_window_management::client::river_node_v1::RiverNodeV1;
 use crate::protocol::river_window_management::client::river_window_v1::Edges;
 use crate::tiling;
-use crate::types::{TilingMode, Window, WindowManager, NUM_TAGS};
+use crate::types::{TilingMode, Window, WindowManager, NUM_TAGS, ModeRule};
 use crate::wayland::AppState;
 use wayland_client::QueueHandle;
 
@@ -74,12 +74,68 @@ pub fn get_mode_for_window(wm: &WindowManager, win: &Window) -> Option<TilingMod
     Some(wm.global_layout)
 }
 
+fn matches_mode_rule(mode_rules: &[ModeRule], win: &Window) -> 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 {
+            return true;
+        }
+    }
+    false
+}
+
+fn get_circular_for_window(mode_rules: &[ModeRule], win: &Window) -> 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 {
+            return rule.circular;
+        }
+    }
+    false
+}
+
 /// 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) {
     // Enforce that clear-status-interface is assigned all tags so that it is always visible
     // and that blank steam_proton helper windows are always untagged so they remain hidden.
     for win in &mut wm.windows {
+        win.circular = get_circular_for_window(&wm.mode_rules, win);
+
         if win.app_id.as_deref() == Some("clear-status-interface") {
             win.tags = u32::MAX;
         }
@@ -94,6 +150,38 @@ pub fn assign_window_modes(wm: &mut WindowManager) {
         }
     }
 
+    // Have newly spawned windows adopt the window mode of the focused window, if there is one.
+    let focused_mode = wm.focused_window().map(|w| w.tiling_mode);
+    if let Some(f_mode) = focused_mode {
+        let mode_rules = &wm.mode_rules;
+        for win in &mut wm.windows {
+            if win.is_new
+                && win.app_id.as_deref() != Some("clear-status-interface")
+                && !win.has_parent
+                && !matches_mode_rule(mode_rules, win)
+            {
+                // Determine what normal fallback mode would be
+                let mut normal_fallback = wm.global_layout;
+                for tag_bit in 0..crate::types::NUM_TAGS {
+                    let tag_mask = 1u32 << tag_bit;
+                    if (win.tags & tag_mask) != 0 && wm.has_tag_layout[tag_bit] {
+                        normal_fallback = wm.tag_layouts[tag_bit];
+                        break;
+                    }
+                }
+
+                if f_mode != normal_fallback {
+                    win.mode_locked = true;
+                }
+                win.tiling_mode = f_mode;
+                eprintln!(
+                    "[mode] new window {} (app_id={:?}) inherits focus mode {:?}",
+                    win.id, win.app_id, f_mode
+                );
+            }
+        }
+    }
+
     // Collect assignments first (borrow checker: can't borrow wm mutably while iterating mode_rules)
     let assignments: Vec<(u64, TilingMode)> = wm
         .windows
@@ -251,10 +339,19 @@ fn compute_tiling(
     if wm.expose_active {
         let mut results = Vec::new();
         // Collect all active non-status-bar, non-popup windows on current tags
-        let expose_windows: Vec<&crate::types::Window> = wm.windows.iter()
-            .filter(|w| !w.closed && (w.tags & wm.active_tags) != 0 && w.app_id.as_deref() != Some("clear-status-interface") && w.tiling_mode != TilingMode::Popup)
+        let mut expose_windows: Vec<&crate::types::Window> = wm.windows.iter()
+            .filter(|w| !w.closed && !w.minimized && (w.tags & wm.active_tags) != 0 && w.app_id.as_deref() != Some("clear-status-interface") && w.tiling_mode != TilingMode::Popup)
             .collect();
 
+        // Sort expose_windows by current visual location (y first, then x)
+        expose_windows.sort_by(|a, b| {
+            if a.y != b.y {
+                a.y.cmp(&b.y)
+            } else {
+                a.x.cmp(&b.x)
+            }
+        });
+
         let n_expose = expose_windows.len() as i32;
         if n_expose > 0 {
             let cols = (n_expose as f64).sqrt().ceil() as i32;
@@ -338,19 +435,33 @@ fn compute_tiling(
         return results;
     }
 
-    // Count windows per tiling mode
-    let mut n_cascade = 0i32;
-    let mut n_grid = 0i32;
-    for win in &wm.windows {
-        if (win.tags & wm.active_tags) == 0 || win.closed {
-            continue;
-        }
-        match win.tiling_mode {
-            TilingMode::Cascade => n_cascade += 1,
-            TilingMode::Grid => n_grid += 1,
-            _ => {}
-        }
-    }
+    // Collect and sort grid windows by ID to ensure stable tiling layout positions
+    let mut grid_windows: Vec<&crate::types::Window> = wm
+        .windows
+        .iter()
+        .filter(|w| {
+            (w.tags & wm.active_tags) != 0
+                && !w.closed
+                && !w.minimized
+                && w.tiling_mode == TilingMode::Grid
+        })
+        .collect();
+    grid_windows.sort_by_key(|w| w.id);
+    let n_grid = grid_windows.len() as i32;
+
+    // Collect and reverse cascade windows (focused window gets index 0, next 1, etc.)
+    let mut cascade_windows: Vec<&crate::types::Window> = wm
+        .windows
+        .iter()
+        .filter(|w| {
+            (w.tags & wm.active_tags) != 0
+                && !w.closed
+                && !w.minimized
+                && w.tiling_mode == TilingMode::Cascade
+        })
+        .collect();
+    cascade_windows.reverse();
+    let n_cascade = cascade_windows.len() as i32;
 
     // Check for fullscreen window — prefer the focused window so FocusNext
     // cycles visible windows when fullscreen is used as a layout mode.
@@ -361,24 +472,22 @@ fn compute_tiling(
         .and_then(|s| s.focused_window_id)
         .filter(|&fid| {
             wm.get_window(fid).map_or(false, |w| {
-                (w.tags & wm.active_tags) != 0 && !w.closed && w.tiling_mode == TilingMode::Fullscreen && w.app_id.as_deref() != Some("clear-status-interface")
+                (w.tags & wm.active_tags) != 0 && !w.closed && !w.minimized && w.tiling_mode == TilingMode::Fullscreen && w.app_id.as_deref() != Some("clear-status-interface")
             })
         })
         .or_else(|| {
             // Fallback: first fullscreen window if no focused window qualifies
             wm.windows.iter().find(|w| {
-                (w.tags & wm.active_tags) != 0 && !w.closed && w.tiling_mode == TilingMode::Fullscreen && w.app_id.as_deref() != Some("clear-status-interface")
+                (w.tags & wm.active_tags) != 0 && !w.closed && !w.minimized && w.tiling_mode == TilingMode::Fullscreen && w.app_id.as_deref() != Some("clear-status-interface")
             }).map(|w| w.id)
         });
 
     // Compute tiling
     let mut results = Vec::new();
-    let mut idx_cascade = 0i32;
-    let mut idx_grid = 0i32;
     let mut idx_floating = 0i32;
 
     for win in &wm.windows {
-        if (win.tags & wm.active_tags) == 0 || win.closed {
+        if (win.tags & wm.active_tags) == 0 || win.closed || win.minimized {
             continue;
         }
 
@@ -409,6 +518,10 @@ fn compute_tiling(
                 }
             }
             TilingMode::Cascade => {
+                let idx = cascade_windows
+                    .iter()
+                    .position(|w| w.id == wid)
+                    .unwrap_or(0) as i32;
                 let (x, y, w, h) = tiling::tile_cascade(
                     screen_w,
                     screen_h,
@@ -421,15 +534,28 @@ fn compute_tiling(
                     cascade_offset,
                     bar_height,
                     n_cascade,
-                    idx_cascade,
+                    idx,
                 );
-                idx_cascade += 1;
                 (x, y, w, h)
             }
             TilingMode::Grid => {
-                let (x, y, w, h) =
-                    tiling::tile_grid(screen_w, screen_h, gap, gap_top, gap_left, gap_right, gap_bottom, wm.layout.grid_border_width, bar_height, n_grid, idx_grid);
-                idx_grid += 1;
+                 let idx = grid_windows
+                     .iter()
+                     .position(|w| w.id == wid)
+                     .unwrap_or(0) as i32;
+                 let (x, y, w, h) = tiling::tile_grid(
+                     screen_w,
+                     screen_h,
+                     wm.layout.grid_gap,
+                     gap_top,
+                     gap_left,
+                     gap_right,
+                     gap_bottom,
+                     wm.layout.grid_border_width,
+                     bar_height,
+                     n_grid,
+                     idx,
+                 );
                 (x, y, w, h)
             }
 
@@ -491,6 +617,29 @@ fn compute_tiling(
         results.push(TileResult { wid, x, y, w, h });
     }
 
+    // Layout minimized windows as bubbles stacked at the right edge
+    let minimized_windows: Vec<&crate::types::Window> = wm.windows.iter()
+        .filter(|w| !w.closed && w.minimized && (w.tags & wm.active_tags) != 0 && w.app_id.as_deref() != Some("clear-status-interface"))
+        .collect();
+
+    let bubble_width = 160;
+    let border_w = wm.layout.grid_border_width;
+
+    for (_idx, win) in minimized_windows.iter().enumerate() {
+        let wid = win.id;
+        let x = screen_w - wm.layout.gap_right - bubble_width - border_w;
+        let y = screen_h - 2;
+        let w = if win.width > 0 { win.width } else { bubble_width };
+        let h = if win.height > 0 { win.height } else { 100 };
+        results.push(TileResult {
+            wid,
+            x,
+            y,
+            w,
+            h,
+        });
+    }
+
     results
 }
 
@@ -526,7 +675,7 @@ fn apply_tiling(state: &mut AppState, results: &[TileResult]) {
             let is_cascade = win.tiling_mode == TilingMode::Cascade;
             let is_exposed = expose_active && win.tiling_mode != TilingMode::Popup && win.app_id.as_deref() != Some("clear-status-interface");
             let was_animating = win.anim_x.is_some() || win.anim_y.is_some() || win.anim_w.is_some() || win.anim_h.is_some() || win.anim_opacity.is_some();
-            let should_animate = is_cascade || is_exposed || was_animating;
+            let should_animate = (is_cascade || is_exposed || was_animating) && !win.minimized;
 
             if should_animate {
                 let curr_x = win.anim_x.unwrap_or(win.x as f64);
@@ -754,3 +903,222 @@ 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 {
+        if win.closed {
+            continue;
+        }
+        if let Some(wp) = state.get_window_proxy(win.id) {
+            let val = if win.circular { 1 } else { 0 };
+            wp.river_window.set_circular(val);
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::types::{Window, Seat, WindowManager, TilingMode, ModeRule};
+
+    #[test]
+    fn test_assign_window_modes_inherit_focus_mode_locked() {
+        let mut wm = WindowManager::default();
+        wm.global_layout = TilingMode::Cascade;
+        wm.tag_layouts[0] = TilingMode::Cascade;
+
+        // 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 window
+        wm.windows.push(Window {
+            id: 2,
+            is_new: true,
+            app_id: Some("kitty".to_string()),
+            ..Default::default()
+        });
+
+        assign_window_modes(&mut wm);
+
+        // Window 2 should inherit Grid mode and be locked because Grid != Cascade
+        let win2 = wm.get_window(2).unwrap();
+        assert_eq!(win2.tiling_mode, TilingMode::Grid);
+        assert!(win2.mode_locked);
+    }
+
+    #[test]
+    fn test_assign_window_modes_inherit_focus_mode_not_locked() {
+        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 window
+        wm.windows.push(Window {
+            id: 2,
+            is_new: true,
+            app_id: Some("kitty".to_string()),
+            ..Default::default()
+        });
+
+        assign_window_modes(&mut wm);
+
+        // Window 2 should inherit Grid mode but NOT be locked because Grid == Grid (normal fallback)
+        let win2 = wm.get_window(2).unwrap();
+        assert_eq!(win2.tiling_mode, TilingMode::Grid);
+        assert!(!win2.mode_locked);
+    }
+
+    #[test]
+    fn test_assign_window_modes_inherit_focus_has_parent() {
+        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 parented window
+        wm.windows.push(Window {
+            id: 2,
+            is_new: true,
+            app_id: Some("kitty".to_string()),
+            has_parent: true,
+            ..Default::default()
+        });
+
+        assign_window_modes(&mut wm);
+
+        // Window 2 should get Floating mode (parent fallback) and not inherit Grid
+        let win2 = wm.get_window(2).unwrap();
+        assert_eq!(win2.tiling_mode, TilingMode::Floating);
+    }
+
+    #[test]
+    fn test_assign_window_modes_inherit_focus_matches_rule() {
+        let mut wm = WindowManager::default();
+        wm.global_layout = TilingMode::Cascade;
+        wm.tag_layouts[0] = TilingMode::Cascade;
+        wm.mode_rules.push(ModeRule {
+            mode: TilingMode::Fullscreen,
+            app_id_pattern: "firefox".to_string(),
+            title_pattern: None,
+            single_instance: false,
+            tag: 0,
+            circular: false,
+        });
+
+        // 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 firefox window matching the mode rule
+        wm.windows.push(Window {
+            id: 2,
+            is_new: true,
+            app_id: Some("firefox".to_string()),
+            ..Default::default()
+        });
+
+        assign_window_modes(&mut wm);
+
+        // Window 2 should get Fullscreen mode from rule and not inherit Grid
+        let win2 = wm.get_window(2).unwrap();
+        assert_eq!(win2.tiling_mode, TilingMode::Fullscreen);
+    }
+
+    #[test]
+    fn test_expose_mode_sorting() {
+        let mut wm = WindowManager::default();
+        wm.expose_active = true;
+        wm.active_tags = 1;
+
+        // Push windows in unordered spatial positions, representing focus ordering
+        wm.windows.push(Window {
+            id: 10,
+            x: 1000,
+            y: 500,
+            tags: 1,
+            ..Default::default()
+        });
+        wm.windows.push(Window {
+            id: 20,
+            x: 0,
+            y: 500,
+            tags: 1,
+            ..Default::default()
+        });
+        wm.windows.push(Window {
+            id: 30,
+            x: 1000,
+            y: 0,
+            tags: 1,
+            ..Default::default()
+        });
+        wm.windows.push(Window {
+            id: 40,
+            x: 0,
+            y: 0,
+            tags: 1,
+            ..Default::default()
+        });
+
+        // Compute tiling in expose mode
+        let results = compute_tiling(&wm, 1920, 1080, 1920, 1080, 0, 0);
+
+        // Expected sorted order:
+        // 1. (0, 0) -> id 40
+        // 2. (1000, 0) -> id 30
+        // 3. (0, 500) -> id 20
+        // 4. (1000, 500) -> id 10
+        assert_eq!(results.len(), 4);
+        assert_eq!(results[0].wid, 40);
+        assert_eq!(results[1].wid, 30);
+        assert_eq!(results[2].wid, 20);
+        assert_eq!(results[3].wid, 10);
+    }
+}
+