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

commitf1550c20f28e1136add6b302f898a00935f4c28f
parent0235e2f94d
authorLucas Galante <[email protected]>
date2026-08-28 12:54
feat(ccectl): move-window-left/right/up/down

The relative counterpart to `move-window <square>`: step the focused tiled
window one grid cell, swapping with whatever tiled window holds the
destination. The decision is the policy crate's `actions::move_tiled`; this
is just the four dispatch arms and the usage line.

Worth having beyond scripting: these actions are otherwise reachable only
through a keybinding, and keybindings cannot be driven from here —
`ccectl keypress` calls wlr_seat_keyboard_notify_key, which hands the key
to the focused CLIENT and never passes the compositor's binding matcher.
So without a verb there is no way to exercise the behaviour in a headless
session at all, which is how the last two keybound features shipped
unverified.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/cce_ctl.rs               |  2 ++
 src/server/window_manager.rs | 13 +++++++++++++
 2 files changed, 15 insertions(+)

diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index e871cd5..41e69e9 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -54,6 +54,8 @@ fn usage(name: &str, to_stderr: bool) {
     print("  close-window <app_id|id> [title-substring]  # close a specific window");
     print("  center-window [<app_id>]   # pan focused/named window on-screen; replies x= y= w= h=");
     print("  move-window <square> [<app_id>] # put focused/named window on a desktop square (e.g. C-9)");
+    print("  move-window-left|-right|-up|-down    # step the focused TILED window one grid");
+    print("                              # cell; swaps with the tiled window already there");
     print("  place-next <app_id> <x> <y> # one-shot: next map of app_id lands near this layout pos");
     print("  place-next-cell <app_id> <x> <y> # one-shot: next map covers the grid square at this pos,");
     print("                              # keeping its size and growing away from its neighbours");
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 2ee985b..18e8288 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -3762,6 +3762,19 @@ impl WindowManager {
                 self.execute_action(&crate::config::Action::FocusPrev, None);
                 "ok\n".to_string()
             }
+            // Relative grid steps for the focused TILED window, swapping with
+            // whatever tiled window holds the destination. Absolute placement
+            // is "move-window <square>", above.
+            "move-window-left" | "move-window-right" | "move-window-up" | "move-window-down" => {
+                let a = match action {
+                    "move-window-left" => crate::config::Action::MoveWindowLeft,
+                    "move-window-right" => crate::config::Action::MoveWindowRight,
+                    "move-window-up" => crate::config::Action::MoveWindowUp,
+                    _ => crate::config::Action::MoveWindowDown,
+                };
+                self.execute_action(&a, None);
+                "ok\n".to_string()
+            }
             "focus-up" => {
                 self.execute_action(&crate::config::Action::FocusUp, None);
                 "ok\n".to_string()