// client/src/layout.rs – Wayland-only window placement utilities #![forbid(unsafe_code)] use std::process::Command; use tracing::{info, warn}; use serde_json::Value; /// The three layouts we cycle through. #[derive(Clone, Copy)] pub enum Layout { SideBySide, // two halves on the current monitor FullLeft, // left eye full-screen, right hidden FullRight, // right eye full-screen, left hidden } /// Move/resize a window titled “Lesavka-eye-{eye}” using `swaymsg`. fn place_window(eye: u32, x: i32, y: i32, w: i32, h: i32) { let title = format!("Lesavka-eye-{eye}"); let cmd = format!( r#"[title="^{title}$"] resize set {w} {h}; move position {x} {y}"# ); match Command::new("swaymsg").arg(cmd).status() { Ok(st) if st.success() => info!("✅ placed eye{eye} {w}×{h}@{x},{y}"), Ok(st) => warn!("⚠️ swaymsg exited with {st}"), Err(e) => warn!("⚠️ swaymsg failed: {e}"), } } /// Apply a layout on the **currently-focused** output. /// No `?` operators → the function can stay `-> ()`. pub fn apply(layout: Layout) { let out = match Command::new("swaymsg") .args(["-t", "get_outputs", "-r"]) .output() { Ok(o) => o.stdout, Err(e) => { warn!("get_outputs failed: {e}"); return; } }; let Ok(Value::Array(outputs)) = serde_json::from_slice::(&out) else { warn!("unexpected JSON from swaymsg"); return; }; let Some(rect) = outputs.iter() .find(|o| o.get("focused").and_then(Value::as_bool) == Some(true)) .and_then(|o| o.get("rect")) else { return; }; // helper to read an i64 → i32 with defaults let g = |k: &str| rect.get(k).and_then(Value::as_i64).unwrap_or(0) as i32; let (x, y, w, h) = (g("x"), g("y"), g("width"), g("height")); match layout { Layout::SideBySide => { let w_half = w / 2; place_window(0, x, y, w_half, h); place_window(1, x + w_half, y, w_half, h); } Layout::FullLeft => { place_window(0, x, y, w, h); place_window(1, x + w * 2, y + h * 2, 1, 1); // off-screen } Layout::FullRight => { place_window(1, x, y, w, h); place_window(0, x + w * 2, y + h * 2, 1, 1); } } }