Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: report and command desktop squares; Tiled drags snap to squares
- seat.rs: a move drag of a Tiled window goes through
snap::snap_move_tiled (hard snap to the nearest square) instead of the
magnetic snap_move, so it can only come to rest on whole squares and no
longer demotes itself to Floating by ending a drag mid-cell. Floating
windows keep the magnetic snap, which is how they become Tiled.
- window_manager.rs: "windows" / "windows --json" now report each window's
square as cell=C-9 (or cell=C-9:D-9 for a block).
- window_manager.rs: new "move-window <square> [app_id|id]" IPC command
places the focused or named window on a named square, carrying the saved
floating anchor with it so a later un-tile restores at the new square
rather than the old one. Replies with the resulting cell and virtual pos.
- cce_ctl.rs: usage line for it (the client forwards argv verbatim).
Co-Authored-By: Claude Opus 5 <[email protected]>
src/cce_ctl.rs | 1 +
src/server/seat.rs | 25 ++++++++++-----
src/server/window_manager.rs | 74 +++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 92 insertions(+), 8 deletions(-)
diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index f26b883..851b564 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -53,6 +53,7 @@ fn usage(name: &str, to_stderr: bool) {
print(" focus-window <app_id>");
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(" place-next <app_id> <x> <y> # one-shot: next map of app_id lands near this layout pos");
print(" overview");
print(" windows [--json] # list windows; --json emits one JSON object per line");
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 8d254a9..031e92e 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1185,13 +1185,24 @@ impl Seat {
let vx = op.start_win_virtual_x + virtual_dx;
let vy = op.start_win_virtual_y + virtual_dy;
- let (vx, vy) = crate::policy::snap::snap_move(
- vx,
- vy,
- (*win).box_geom.width as f64,
- (*win).box_geom.height as f64,
- &sp,
- );
+ // A Tiled window only ever occupies whole squares,
+ // so its drag snaps hard to the nearest one. The
+ // magnetic snap below is for Floating windows,
+ // which use it to decide whether they land aligned
+ // (and so become Tiled) at op_end.
+ let (vx, vy) = if (*self.server).wm.get_mode_for_window(win)
+ == crate::tiling::TilingMode::Tiled
+ {
+ crate::policy::snap::snap_move_tiled(vx, vy, &sp)
+ } else {
+ crate::policy::snap::snap_move(
+ vx,
+ vy,
+ (*win).box_geom.width as f64,
+ (*win).box_geom.height as f64,
+ &sp,
+ )
+ };
(*win).virtual_x = vx;
(*win).virtual_y = vy;
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 1c958fe..09e621a 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -3100,6 +3100,65 @@ impl WindowManager {
(*target).close();
format!("ok {}\n", title)
}
+ "move-window" => {
+ // move-window <square> [app_id|id] — put a window on a named
+ // desktop square (chess style, e.g. "C-9"). With no app_id the
+ // focused window moves. A Tiled window keeps the SHAPE of its
+ // block and is re-anchored with its top-left on that square;
+ // a Floating window keeps its size.
+ if parts.len() < 2 {
+ return "error: usage: move-window <square> [app_id|id]\n".to_string();
+ }
+ let sp = self.layout.snap_params();
+ let Some((col, row)) = crate::policy::cells::parse_square(parts[1]) else {
+ return format!(
+ "error: '{}' is not a square (expected e.g. A1, C-9, -B2)\n",
+ parts[1]
+ );
+ };
+ let target: *mut Window = if parts.len() >= 3 {
+ self.find_window_by_query(&parts[2..].join(" "))
+ } else if let Some(seat) = self.first_seat() {
+ match (*seat).focused {
+ crate::seat::Focus::Window(w) => w,
+ _ => std::ptr::null_mut(),
+ }
+ } else {
+ std::ptr::null_mut()
+ };
+ if target.is_null() {
+ return "error: window not found\n".to_string();
+ }
+ if (*target).minimized {
+ (*target).minimized = false;
+ }
+
+ let (x, y, _, _) = crate::policy::cells::square_rect(
+ col,
+ row,
+ sp.cell_size,
+ sp.gap_width,
+ sp.cell_inset,
+ );
+ (*target).virtual_x = x;
+ (*target).virtual_y = y;
+ // Saved floating geometry follows the window, so a later
+ // Tiled -> Floating transition restores it at the new square
+ // rather than yanking it back to where it used to live.
+ (*target).saved_floating_virtual_x = x;
+ (*target).saved_floating_virtual_y = y;
+ self.dirty_windowing();
+
+ let cell = crate::policy::cells::window_span_label(
+ x,
+ y,
+ (*target).box_geom.width as f64,
+ (*target).box_geom.height as f64,
+ sp.cell_size,
+ sp.gap_width,
+ );
+ format!("ok cell={} vx={:.1} vy={:.1}\n", cell, x, y)
+ }
"center-window" | "bring-window" => {
// Pan the desktop so the target window (given app_id/id, or the
// focused window if omitted) is centered in the output, then focus
@@ -3295,10 +3354,21 @@ impl WindowManager {
}
let mut out = String::new();
+ let sp = self.layout.snap_params();
for &w in self.windows.iter() {
if !w.is_null() && !(*w).closed && !matches!((*w).state, crate::window::WindowState::Closing | crate::window::WindowState::Init) {
let app_id = (*w).get_app_id_string().unwrap_or_default();
let title = (*w).get_title_string().unwrap_or_default();
+ // Which desktop square(s) the window sits on, chess
+ // style: "C-9" for one, "C-9:D-9" for a block.
+ let cell = crate::policy::cells::window_span_label(
+ (*w).virtual_x,
+ (*w).virtual_y,
+ (*w).box_geom.width as f64,
+ (*w).box_geom.height as f64,
+ sp.cell_size,
+ sp.gap_width,
+ );
if as_json {
out.push_str(&serde_json::json!({
"id": (*w).ref_key.index,
@@ -3311,6 +3381,7 @@ impl WindowManager {
"h": (*w).box_geom.height,
"vx": (*w).virtual_x,
"vy": (*w).virtual_y,
+ "cell": cell,
"minimized": (*w).minimized,
"has_parent": (*w).has_parent,
"focused": w == focused_window,
@@ -3319,7 +3390,7 @@ impl WindowManager {
out.push('\n');
} else {
out.push_str(&format!(
- "window id={} app_id={} title=\"{}\" mode={} x={} y={} w={} h={} vx={:.1} vy={:.1} minimized={} has_parent={} focused={} ssd={}\n",
+ "window id={} app_id={} title=\"{}\" mode={} x={} y={} w={} h={} vx={:.1} vy={:.1} cell={} minimized={} has_parent={} focused={} ssd={}\n",
(*w).ref_key.index,
app_id,
title,
@@ -3330,6 +3401,7 @@ impl WindowManager {
(*w).box_geom.height,
(*w).virtual_x,
(*w).virtual_y,
+ cell,
(*w).minimized,
(*w).has_parent,
w == focused_window,