2025-06-30 15:45:37 -05:00
|
|
|
|
// client/src/layout.rs - Wayland-only window placement utilities
|
2025-06-29 04:54:39 -05:00
|
|
|
|
#![forbid(unsafe_code)]
|
|
|
|
|
|
|
2025-12-01 11:38:51 -03:00
|
|
|
|
use serde_json::Value;
|
2025-06-29 04:54:39 -05:00
|
|
|
|
use std::process::Command;
|
|
|
|
|
|
use tracing::{info, warn};
|
|
|
|
|
|
|
|
|
|
|
|
/// The three layouts we cycle through.
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
|
pub enum Layout {
|
2025-12-01 11:38:51 -03:00
|
|
|
|
SideBySide, // two halves on the current monitor
|
|
|
|
|
|
FullLeft, // left eye full-screen, right hidden
|
|
|
|
|
|
FullRight, // right eye full-screen, left hidden
|
2025-06-29 04:54:39 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-29 22:39:17 -05:00
|
|
|
|
/// Move/resize a window titled “Lesavka-eye-{eye}” using `swaymsg`.
|
2025-06-29 04:54:39 -05:00
|
|
|
|
fn place_window(eye: u32, x: i32, y: i32, w: i32, h: i32) {
|
2025-06-29 22:39:17 -05:00
|
|
|
|
let title = format!("Lesavka-eye-{eye}");
|
2025-12-01 11:38:51 -03:00
|
|
|
|
let cmd = format!(r#"[title="^{title}$"] resize set {w} {h}; move position {x} {y}"#);
|
2025-06-29 04:54:39 -05:00
|
|
|
|
|
|
|
|
|
|
match Command::new("swaymsg").arg(cmd).status() {
|
|
|
|
|
|
Ok(st) if st.success() => info!("✅ placed eye{eye} {w}×{h}@{x},{y}"),
|
2025-12-01 11:38:51 -03:00
|
|
|
|
Ok(st) => warn!("⚠️ swaymsg exited with {st}"),
|
|
|
|
|
|
Err(e) => warn!("⚠️ swaymsg failed: {e}"),
|
2025-06-29 04:54:39 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-29 22:39:17 -05:00
|
|
|
|
/// Apply a layout on the **currently-focused** output.
|
2025-06-29 04:54:39 -05:00
|
|
|
|
/// 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,
|
2025-12-01 11:38:51 -03:00
|
|
|
|
Err(e) => {
|
|
|
|
|
|
warn!("get_outputs failed: {e}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-06-29 04:54:39 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let Ok(Value::Array(outputs)) = serde_json::from_slice::<Value>(&out) else {
|
2025-12-01 11:38:51 -03:00
|
|
|
|
warn!("unexpected JSON from swaymsg");
|
|
|
|
|
|
return;
|
2025-06-29 04:54:39 -05:00
|
|
|
|
};
|
2025-12-01 11:38:51 -03:00
|
|
|
|
let Some(rect) = outputs
|
|
|
|
|
|
.iter()
|
2025-06-29 04:54:39 -05:00
|
|
|
|
.find(|o| o.get("focused").and_then(Value::as_bool) == Some(true))
|
2025-12-01 11:38:51 -03:00
|
|
|
|
.and_then(|o| o.get("rect"))
|
|
|
|
|
|
else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
2025-06-29 04:54:39 -05:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
2025-12-01 11:38:51 -03:00
|
|
|
|
place_window(0, x, y, w_half, h);
|
|
|
|
|
|
place_window(1, x + w_half, y, w_half, h);
|
2025-06-29 04:54:39 -05:00
|
|
|
|
}
|
|
|
|
|
|
Layout::FullLeft => {
|
|
|
|
|
|
place_window(0, x, y, w, h);
|
2025-06-29 22:39:17 -05:00
|
|
|
|
place_window(1, x + w * 2, y + h * 2, 1, 1); // off-screen
|
2025-06-29 04:54:39 -05:00
|
|
|
|
}
|
|
|
|
|
|
Layout::FullRight => {
|
|
|
|
|
|
place_window(1, x, y, w, h);
|
|
|
|
|
|
place_window(0, x + w * 2, y + h * 2, 1, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|