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

commitd8ef070b52f994e9dead9f7a80de0f94640c8407
parent08d8fe9c90
authorLucas Galante <[email protected]>
date2026-06-03 15:28
Update and align interface features

 src/input.rs         | 149 +++++++++++++++++++++++++++++++++++++------------
 src/ipc.rs           |  96 +++++++++++++++++++++++++++-----
 src/status_server.rs |  68 ++++++++++++-----------
 src/types.rs         |   5 ++
 src/wayland.rs       | 154 +++++++++++++++++++++++++++++++++++++++++++++++++--
 5 files changed, 388 insertions(+), 84 deletions(-)

diff --git a/src/input.rs b/src/input.rs
index 0e85dd8..1e5aeed 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -28,6 +28,16 @@ pub fn update_pointer_coords(dx: i32, dy: i32) {
     POINTER_Y.store(current_y, Ordering::SeqCst);
 }
 
+pub fn set_pointer_coords(x: i32, y: i32) {
+    let screen_w = SCREEN_WIDTH.load(Ordering::SeqCst);
+    let screen_h = SCREEN_HEIGHT.load(Ordering::SeqCst);
+    let current_x = x.clamp(0, screen_w);
+    let current_y = y.clamp(0, screen_h);
+
+    POINTER_X.store(current_x, Ordering::SeqCst);
+    POINTER_Y.store(current_y, Ordering::SeqCst);
+}
+
 // IOCTL and Event constants
 const UI_DEV_CREATE: libc::c_ulong = 0x5501;
 const UI_DEV_SETUP: libc::c_ulong = 0x405C5503;
@@ -131,7 +141,8 @@ const ABS_MT_SLOT: u16 = 0x2f;
 #[derive(Debug, Clone)]
 pub enum InputDaemonMsg {
     UpdateConfig(InertialConfig, bool),
-    SimulateMove { dx: i32, dy: i32 },
+    SimulateMoveTo { x: i32, y: i32 },
+    SimulateMoveBy { dx: i32, dy: i32 },
     SimulateButton { button: u16, press: bool },
     SimulateKey { keycode: u16, press: bool },
     SimulateClick { button: u16 },
@@ -150,7 +161,7 @@ enum CoordinatorMsg {
     InternalReleaseKey { keycode: u16 },
 }
 
-fn setup_uinput() -> std::io::Result<std::fs::File> {
+fn setup_uinput_mouse() -> std::io::Result<std::fs::File> {
     let file = OpenOptions::new()
         .write(true)
         .custom_flags(libc::O_NONBLOCK)
@@ -174,6 +185,59 @@ 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_KEY as libc::c_int) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+        for key in 1..=511 {
+            if libc::ioctl(fd, UI_SET_KEYBIT, key as libc::c_int) < 0 {
+                return Err(std::io::Error::last_os_error());
+            }
+        }
+        for btn in 272..=276 {
+            if libc::ioctl(fd, UI_SET_KEYBIT, btn as libc::c_int) < 0 {
+                return Err(std::io::Error::last_os_error());
+            }
+        }
+    }
+
+    let mut setup = UinputSetup {
+        id: InputId {
+            bustype: 0x0006, // BUS_VIRTUAL
+            vendor: 0x1234,
+            product: 0x5678,
+            version: 1,
+        },
+        name: [0; 80],
+        ff_effects_max: 0,
+    };
+
+    let name_bytes = b"Clear Virtual Mouse";
+    setup.name[..name_bytes.len()].copy_from_slice(name_bytes);
+
+    unsafe {
+        let setup_ptr = &setup as *const UinputSetup as *const libc::c_void;
+        if libc::ioctl(fd, UI_DEV_SETUP, setup_ptr) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+        if libc::ioctl(fd, UI_DEV_CREATE) < 0 {
+            return Err(std::io::Error::last_os_error());
+        }
+    }
+
+    println!("[input-subsystem] Successfully created virtual uinput mouse device.");
+    Ok(file)
+}
+
+fn setup_uinput_abs() -> std::io::Result<std::fs::File> {
+    let file = OpenOptions::new()
+        .write(true)
+        .custom_flags(libc::O_NONBLOCK)
+        .open("/dev/uinput")?;
+
+    let fd = file.as_raw_fd();
+
+    unsafe {
         if libc::ioctl(fd, UI_SET_EVBIT, EV_ABS as libc::c_int) < 0 {
             return Err(std::io::Error::last_os_error());
         }
@@ -218,11 +282,6 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
         if libc::ioctl(fd, UI_SET_EVBIT, EV_KEY as libc::c_int) < 0 {
             return Err(std::io::Error::last_os_error());
         }
-        for key in 1..=511 {
-            if libc::ioctl(fd, UI_SET_KEYBIT, key as libc::c_int) < 0 {
-                return Err(std::io::Error::last_os_error());
-            }
-        }
         for btn in 272..=276 {
             if libc::ioctl(fd, UI_SET_KEYBIT, btn as libc::c_int) < 0 {
                 return Err(std::io::Error::last_os_error());
@@ -234,14 +293,14 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
         id: InputId {
             bustype: 0x0006, // BUS_VIRTUAL
             vendor: 0x1234,
-            product: 0x5678,
+            product: 0x5679,
             version: 1,
         },
         name: [0; 80],
         ff_effects_max: 0,
     };
 
-    let name_bytes = b"Clear Virtual Mouse";
+    let name_bytes = b"Clear Virtual Absolute Pointer";
     setup.name[..name_bytes.len()].copy_from_slice(name_bytes);
 
     unsafe {
@@ -254,7 +313,7 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
         }
     }
 
-    println!("[input-subsystem] Successfully created virtual uinput device.");
+    println!("[input-subsystem] Successfully created virtual uinput absolute pointer device.");
     Ok(file)
 }
 
@@ -283,6 +342,20 @@ fn write_mouse_absolute(file: &mut std::fs::File, x: i32, y: i32) -> std::io::Re
     Ok(())
 }
 
+fn write_mouse_relative(file: &mut std::fs::File, dx: i32, dy: i32) -> std::io::Result<()> {
+    if dx == 0 && dy == 0 {
+        return Ok(());
+    }
+    if dx != 0 {
+        write_raw_event(file, EV_REL, REL_X, dx)?;
+    }
+    if dy != 0 {
+        write_raw_event(file, EV_REL, REL_Y, dy)?;
+    }
+    write_raw_event(file, EV_SYN, SYN_REPORT, 0)?;
+    Ok(())
+}
+
 fn write_scroll(file: &mut std::fs::File, dwx: i32, dwy: i32) -> std::io::Result<()> {
     if dwx != 0 {
         write_raw_event(file, EV_REL, REL_HWHEEL, dwx)?;
@@ -548,10 +621,18 @@ pub fn run_input_daemon(
     let res: Result<(), Box<dyn std::error::Error>> = rt.block_on(async move {
         println!("[input-subsystem] Starting Clear Input Subsystem...");
 
-        let mut uinput_file = match setup_uinput() {
+        let mut uinput_mouse_file = match setup_uinput_mouse() {
+            Ok(f) => f,
+            Err(e) => {
+                eprintln!("[input-subsystem] FATAL: Could not initialize uinput mouse: {}.", e);
+                return Err(Box::new(e) as Box<dyn std::error::Error>);
+            }
+        };
+
+        let mut uinput_abs_file = match setup_uinput_abs() {
             Ok(f) => f,
             Err(e) => {
-                eprintln!("[input-subsystem] FATAL: Could not initialize /dev/uinput: {}.", e);
+                eprintln!("[input-subsystem] FATAL: Could not initialize uinput absolute pointer: {}.", e);
                 return Err(Box::new(e) as Box<dyn std::error::Error>);
             }
         };
@@ -678,25 +759,29 @@ pub fn run_input_daemon(
                                         trigger_tap_to_click_ipc(tap, &ipc_tx, pipe_write);
                                     }
                                 }
-                                InputDaemonMsg::SimulateMove { dx, dy } => {
-                                    update_pointer_coords(dx, dy);
+                                InputDaemonMsg::SimulateMoveTo { x, y } => {
+                                    set_pointer_coords(x, y);
                                     let px = POINTER_X.load(Ordering::SeqCst);
                                     let py = POINTER_Y.load(Ordering::SeqCst);
-                                    let _ = write_mouse_absolute(&mut uinput_file, px, py);
+                                    let _ = write_mouse_absolute(&mut uinput_abs_file, px, py);
+                                }
+                                InputDaemonMsg::SimulateMoveBy { dx, dy } => {
+                                    update_pointer_coords(dx, dy);
+                                    let _ = write_mouse_relative(&mut uinput_mouse_file, dx, dy);
                                 }
                                 InputDaemonMsg::SimulateButton { button, press } => {
                                     let val = if press { 1 } else { 0 };
-                                    let _ = write_raw_event(&mut uinput_file, EV_KEY, button, val);
-                                    let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, val);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
                                 }
                                 InputDaemonMsg::SimulateKey { keycode, press } => {
                                     let val = if press { 1 } else { 0 };
-                                    let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, val);
-                                    let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, val);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
                                 }
                                 InputDaemonMsg::SimulateClick { button } => {
-                                    let _ = write_raw_event(&mut uinput_file, EV_KEY, button, 1);
-                                    let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, 1);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
                                     let tx_clone = tx.clone();
                                     tokio::spawn(async move {
                                         tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
@@ -704,8 +789,8 @@ pub fn run_input_daemon(
                                     });
                                 }
                                 InputDaemonMsg::SimulateKeyPress { keycode } => {
-                                    let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, 1);
-                                    let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, 1);
+                                    let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
                                     let tx_clone = tx.clone();
                                     tokio::spawn(async move {
                                         tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
@@ -715,12 +800,12 @@ pub fn run_input_daemon(
                             }
                         }
                         CoordinatorMsg::InternalReleaseButton { button } => {
-                            let _ = write_raw_event(&mut uinput_file, EV_KEY, button, 0);
-                            let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+                            let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, 0);
+                            let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
                         }
                         CoordinatorMsg::InternalReleaseKey { keycode } => {
-                            let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, 0);
-                            let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+                            let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, 0);
+                            let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
                         }
                         CoordinatorMsg::PhysicalMove { dx, dy, timestamp } => {
                             update_pointer_coords(dx, dy);
@@ -897,9 +982,7 @@ pub fn run_input_daemon(
 
                                 if steps_x != 0 || steps_y != 0 {
                                     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);
+                                    let _ = write_mouse_relative(&mut uinput_mouse_file, steps_x, steps_y);
                                 }
                             }
                         }
@@ -925,9 +1008,7 @@ pub fn run_input_daemon(
 
                             if steps_x != 0 || steps_y != 0 {
                                 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);
+                                let _ = write_mouse_relative(&mut uinput_mouse_file, steps_x, steps_y);
                             }
                         }
                     }
@@ -964,7 +1045,7 @@ pub fn run_input_daemon(
                                 state.accum_scroll_y -= steps_y as f32;
 
                                 if steps_x != 0 || steps_y != 0 {
-                                    let _ = write_scroll(&mut uinput_file, steps_x, steps_y);
+                                    let _ = write_scroll(&mut uinput_mouse_file, steps_x, steps_y);
                                 }
                             }
                         }
diff --git a/src/ipc.rs b/src/ipc.rs
index 8580dca..982403f 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -311,6 +311,58 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
         "apply-mode-sharing" => {
             handle_apply_mode_sharing_command(rest, state);
         }
+        "mode-next-shared" => {
+            let cycle = [
+                TilingMode::Cascade,
+                TilingMode::Grid,
+                TilingMode::Fullscreen,
+                TilingMode::Floating,
+            ];
+            let focused_id = state
+                .seats
+                .iter()
+                .find(|s| !s.removed)
+                .and_then(|s| s.focused_window_id);
+            if let Some(fid) = focused_id {
+                let notifications_enable = state.notifications_enable;
+                let current_mode = state.get_window(fid).map(|w| w.tiling_mode);
+                if let Some(old_mode) = current_mode {
+                    let next = cycle
+                        .iter()
+                        .position(|m| *m == old_mode)
+                        .map(|i| cycle[(i + 1) % cycle.len()])
+                        .unwrap_or(TilingMode::Cascade);
+
+                    let active_tags = state.active_tags;
+
+                    let mut updated_count = 0;
+                    for win in &mut state.windows {
+                        if !win.closed
+                            && win.app_id.as_deref() != Some("clear-status-interface")
+                            && (win.tags & active_tags) != 0
+                            && win.tiling_mode == old_mode
+                        {
+                            win.tiling_mode = next;
+                            win.mode_locked = true;
+                            updated_count += 1;
+                        }
+                    }
+
+                    if notifications_enable && updated_count > 0 {
+                        crate::config::show_notification(
+                            "ccec",
+                            &format!(
+                                "Tiling mode set to {} for all {} windows on active tag",
+                                next.as_str(),
+                                old_mode.as_str()
+                            ),
+                        );
+                    }
+                    state.needs_render = true;
+                    state.needs_status_update = true;
+                }
+            }
+        }
         "bind" => {
             handle_bind_command(rest, state);
         }
@@ -398,12 +450,8 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
             let parts: Vec<&str> = rest.split_whitespace().collect();
             if parts.len() == 2 {
                 if let (Ok(target_x), Ok(target_y)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
-                    let cur_x = crate::input::POINTER_X.load(std::sync::atomic::Ordering::SeqCst);
-                    let cur_y = crate::input::POINTER_Y.load(std::sync::atomic::Ordering::SeqCst);
-                    let dx = target_x - cur_x;
-                    let dy = target_y - cur_y;
                     if let Some(ref controller) = state.input_controller {
-                        let _ = controller.send(crate::input::InputDaemonMsg::SimulateMove { dx, dy });
+                        let _ = controller.send(crate::input::InputDaemonMsg::SimulateMoveTo { x: target_x, y: target_y });
                     }
                 } else {
                     reply = "error: invalid coordinates\n".to_string();
@@ -417,7 +465,7 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
             if parts.len() == 2 {
                 if let (Ok(dx), Ok(dy)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
                     if let Some(ref controller) = state.input_controller {
-                        let _ = controller.send(crate::input::InputDaemonMsg::SimulateMove { dx, dy });
+                        let _ = controller.send(crate::input::InputDaemonMsg::SimulateMoveBy { dx, dy });
                     }
                 } else {
                     reply = "error: invalid deltas\n".to_string();
@@ -696,14 +744,22 @@ fn handle_mode_command(rest: &str, state: &mut WindowManager) {
     }
 
     let mode = parse_tiling_mode(mode_str);
-    state.mode_rules.push(ModeRule {
-        mode,
-        app_id_pattern,
-        title_pattern,
-        single_instance,
-        tag,
-        circular: false,
-    });
+    if let Some(existing) = state.mode_rules.iter_mut().find(|r| {
+        r.app_id_pattern == app_id_pattern && r.title_pattern == title_pattern
+    }) {
+        existing.mode = mode;
+        existing.single_instance = single_instance;
+        existing.tag = tag;
+    } else {
+        state.mode_rules.push(ModeRule {
+            mode,
+            app_id_pattern,
+            title_pattern,
+            single_instance,
+            tag,
+            circular: false,
+        });
+    }
 }
 
 /// Handle "set-mode <mode>" command — set the focused window's tiling mode
@@ -1210,6 +1266,18 @@ mod tests {
         assert_eq!(state.mode_rules[0].app_id_pattern, "ghostty");
     }
 
+    #[test]
+    fn test_ipc_mode_rule_update() {
+        let mut state = WindowManager::default();
+        handle_ipc_command("mode fullscreen clear-system-interface", &mut state);
+        assert_eq!(state.mode_rules.len(), 1);
+        assert_eq!(state.mode_rules[0].mode, TilingMode::Fullscreen);
+
+        handle_ipc_command("mode cascade clear-system-interface", &mut state);
+        assert_eq!(state.mode_rules.len(), 1);
+        assert_eq!(state.mode_rules[0].mode, TilingMode::Cascade);
+    }
+
     #[test]
     fn test_ipc_mode_rule_with_single() {
         let mut state = WindowManager::default();
diff --git a/src/status_server.rs b/src/status_server.rs
index 8b7fc67..6e79e1a 100644
--- a/src/status_server.rs
+++ b/src/status_server.rs
@@ -107,6 +107,7 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
 
     loop {
         let mut activity = false;
+        let mut has_new_update = false;
 
         // Accept new connections (non-blocking)
         for _ in 0..5 {
@@ -154,6 +155,7 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
                 Ok(update) => {
                     latest = Some(update);
                     activity = true;
+                    has_new_update = true;
                 }
                 Err(mpsc::TryRecvError::Empty) => break,
                 Err(mpsc::TryRecvError::Disconnected) => {
@@ -164,41 +166,43 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
             }
         }
 
-        // If we got an update, push it to all clients
-        if let Some(ref update) = latest {
-            let mut dead_clients = Vec::new();
-
-            for (i, client) in clients.iter_mut().enumerate() {
-                let msg = format_for_subscription(client.subscription, update);
-                match client
-                    .stream
-                    .write_all(msg.as_bytes())
-                    .and_then(|_| client.stream.write_all(b"\n"))
-                {
-                    Ok(_) => {}
-                    Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
-                        // Client not ready to receive — skip for now
-                    }
-                    Err(ref e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
-                        eprintln!(
-                            "[status] client {:?} disconnected (broken pipe)",
-                            client.subscription
-                        );
-                        dead_clients.push(i);
-                    }
-                    Err(e) => {
-                        eprintln!(
-                            "[status] write error to client {:?}: {}",
-                            client.subscription, e
-                        );
-                        dead_clients.push(i);
+        // If we got a new update, push it to all clients
+        if has_new_update {
+            if let Some(ref update) = latest {
+                let mut dead_clients = Vec::new();
+
+                for (i, client) in clients.iter_mut().enumerate() {
+                    let msg = format_for_subscription(client.subscription, update);
+                    match client
+                        .stream
+                        .write_all(msg.as_bytes())
+                        .and_then(|_| client.stream.write_all(b"\n"))
+                    {
+                        Ok(_) => {}
+                        Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
+                            // Client not ready to receive — skip for now
+                        }
+                        Err(ref e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
+                            eprintln!(
+                                "[status] client {:?} disconnected (broken pipe)",
+                                client.subscription
+                            );
+                            dead_clients.push(i);
+                        }
+                        Err(e) => {
+                            eprintln!(
+                                "[status] write error to client {:?}: {}",
+                                client.subscription, e
+                            );
+                            dead_clients.push(i);
+                        }
                     }
                 }
-            }
 
-            // Remove dead clients (iterate in reverse to preserve indices)
-            for i in dead_clients.into_iter().rev() {
-                clients.remove(i);
+                // Remove dead clients (iterate in reverse to preserve indices)
+                for i in dead_clients.into_iter().rev() {
+                    clients.remove(i);
+                }
             }
         }
 
diff --git a/src/types.rs b/src/types.rs
index 0d89512..6b2a585 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -40,6 +40,7 @@ pub enum Action {
     Fullscreen,
     LayoutNext,
     ModeNext,
+    ModeNextShared,
     Reload,
     Restart,
     View1,
@@ -525,6 +526,8 @@ pub fn parse_action(s: &str) -> Action {
         Action::LayoutNext
     } else if s == "mode-next" {
         Action::ModeNext
+    } else if s == "mode-next-shared" {
+        Action::ModeNextShared
     } else if s == "reload" {
         Action::Reload
     } else if s == "restart" {
@@ -676,6 +679,8 @@ mod tests {
         assert_eq!(parse_action("move"), Action::Move);
         assert_eq!(parse_action("resize"), Action::Resize);
         assert_eq!(parse_action("layout-next"), Action::LayoutNext);
+        assert_eq!(parse_action("mode-next"), Action::ModeNext);
+        assert_eq!(parse_action("mode-next-shared"), Action::ModeNextShared);
         assert_eq!(parse_action("reload"), Action::Reload);
         assert_eq!(parse_action("restart"), Action::Restart);
         assert_eq!(parse_action("spawn"), Action::Spawn);
diff --git a/src/wayland.rs b/src/wayland.rs
index 67380e4..1c83e74 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -1562,12 +1562,97 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
                 );
             }
 
-            river_window_v1::Event::PointerMoveRequested { .. } => {
-                // Will handle pointer ops later
+            river_window_v1::Event::PointerMoveRequested { seat } => {
+                if let Some(_sid) = state.seat_id_for_proxy(&seat) {
+                    let mut window_found = false;
+                    let mut needs_render = false;
+                    if let Some(win) = state.wm.get_window_mut(wid) {
+                        if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
+                            if win.tiling_mode != TilingMode::Floating {
+                                win.tiling_mode = TilingMode::Floating;
+                                needs_render = true;
+                            }
+                            win.mode_locked = true;
+                            window_found = true;
+                        }
+                    }
+                    if window_found {
+                        if needs_render {
+                            state.wm.needs_render = true;
+                            state.wm.needs_focus = true;
+                            state.wm.needs_status_update = true;
+                        }
+                        seat.op_start_pointer();
+                        if let Some(win) = state.wm.get_window_mut(wid) {
+                            win.anim_x = None;
+                            win.anim_y = None;
+                            win.anim_w = None;
+                            win.anim_h = None;
+                            win.anim_opacity = None;
+                            state.active_pointer_op = Some(PointerOp {
+                                window_id: wid,
+                                op_type: PointerOpType::Move,
+                                start_x: win.x,
+                                start_y: win.y,
+                                start_width: win.width,
+                                start_height: win.height,
+                            });
+                        }
+                    }
+                }
             }
 
-            river_window_v1::Event::PointerResizeRequested { .. } => {
-                // Will handle pointer ops later
+            river_window_v1::Event::PointerResizeRequested { seat, edges } => {
+                if let Some(_sid) = state.seat_id_for_proxy(&seat) {
+                    let mut window_found = false;
+                    let mut needs_render = false;
+                    if let Some(win) = state.wm.get_window_mut(wid) {
+                        if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
+                            if win.tiling_mode != TilingMode::Floating {
+                                win.tiling_mode = TilingMode::Floating;
+                                needs_render = true;
+                            }
+                            win.mode_locked = true;
+                            window_found = true;
+                        }
+                    }
+                    if window_found {
+                        if needs_render {
+                            state.wm.needs_render = true;
+                            state.wm.needs_focus = true;
+                            state.wm.needs_status_update = true;
+                        }
+                        seat.op_start_pointer();
+                        if let Some(win) = state.wm.get_window_mut(wid) {
+                            win.anim_x = None;
+                            win.anim_y = None;
+                            win.anim_w = None;
+                            win.anim_h = None;
+                            win.anim_opacity = None;
+                            
+                            let edges_u32: u32 = edges.into();
+                            let op_type = match edges_u32 {
+                                1 => PointerOpType::ResizeTop,
+                                2 => PointerOpType::ResizeBottom,
+                                4 => PointerOpType::ResizeLeft,
+                                8 => PointerOpType::ResizeRight,
+                                5 => PointerOpType::ResizeTopLeft,
+                                9 => PointerOpType::ResizeTopRight,
+                                6 => PointerOpType::ResizeBottomLeft,
+                                10 => PointerOpType::ResizeBottomRight,
+                                _ => PointerOpType::Resize,
+                            };
+                            state.active_pointer_op = Some(PointerOp {
+                                window_id: wid,
+                                op_type,
+                                start_x: win.x,
+                                start_y: win.y,
+                                start_width: win.width,
+                                start_height: win.height,
+                            });
+                        }
+                    }
+                }
             }
 
             _ => {}
@@ -2532,6 +2617,67 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
                 }
             }
         }
+        Action::ModeNextShared => {
+            let cycle = [
+                TilingMode::Cascade,
+                TilingMode::Grid,
+                TilingMode::Fullscreen,
+                TilingMode::Floating,
+            ];
+            let focused_id = state
+                .wm
+                .seats
+                .iter()
+                .find(|s| !s.removed)
+                .and_then(|s| s.focused_window_id);
+            if let Some(fid) = focused_id {
+                let notifications_enable = state.wm.notifications_enable;
+                let current_mode = state.wm.get_window(fid).map(|w| w.tiling_mode);
+                if let Some(old_mode) = current_mode {
+                    let next = cycle
+                        .iter()
+                        .position(|m| *m == old_mode)
+                        .map(|i| cycle[(i + 1) % cycle.len()])
+                        .unwrap_or(TilingMode::Cascade);
+
+                    let active_tags = state.wm.active_tags;
+
+                    let mut updated_count = 0;
+                    for win in &mut state.wm.windows {
+                        if !win.closed
+                            && win.app_id.as_deref() != Some("clear-status-interface")
+                            && (win.tags & active_tags) != 0
+                            && win.tiling_mode == old_mode
+                        {
+                            win.tiling_mode = next;
+                            win.mode_locked = true;
+                            updated_count += 1;
+                        }
+                    }
+
+                    eprintln!(
+                        "mode-next-shared: updated {} windows (current tag, mode {}) -> {}",
+                        updated_count,
+                        old_mode.as_str(),
+                        next.as_str()
+                    );
+
+                    if notifications_enable && updated_count > 0 {
+                        crate::config::show_notification(
+                            "ccec",
+                            &format!(
+                                "Tiling mode set to {} for all {} windows on active tag",
+                                next.as_str(),
+                                old_mode.as_str()
+                            ),
+                        );
+                    }
+
+                    state.wm.needs_render = true;
+                    state.wm.needs_status_update = true;
+                }
+            }
+        }
         Action::Reload => {
             crate::restart::wm_restart();
         }