use anyhow::{Context, Result, anyhow};
use serde_json::{Value, json};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;
const REPLY_TIMEOUT: Duration = Duration::from_secs(5);
pub fn socket_path() -> Result<String> {
if let Ok(explicit) = std::env::var("UMBRIEL_SOCKET") {
return Ok(explicit);
}
let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
.context("neither UMBRIEL_SOCKET nor XDG_RUNTIME_DIR is set")?;
let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string());
Ok(format!("{runtime_dir}/umbriel-{display}.sock"))
}
fn request(payload: Value) -> Result<Value> {
let path = socket_path()?;
let stream = UnixStream::connect(&path)
.with_context(|| format!("cannot reach umbriel at {path}; is the compositor running?"))?;
stream.set_read_timeout(Some(REPLY_TIMEOUT))?;
stream.set_write_timeout(Some(REPLY_TIMEOUT))?;
let mut outgoing = stream.try_clone()?;
writeln!(outgoing, "{payload}")?;
outgoing.flush()?;
let mut line = String::new();
BufReader::new(stream).read_line(&mut line)?;
let reply: Value =
serde_json::from_str(&line).with_context(|| format!("unparseable reply: {line}"))?;
if let Some(failure) = reply.get("err") {
let detail = failure
.as_str()
.map(str::to_string)
.unwrap_or_else(|| failure.to_string());
return Err(anyhow!("umbriel rejected the request: {detail}"));
}
reply
.get("ok")
.cloned()
.ok_or_else(|| anyhow!("reply carried neither ok nor err: {line}"))
}
pub fn query(command: &str) -> Result<Value> {
request(json!({ "cmd": command }))
}
pub fn dispatch(action: &str) -> Result<Value> {
request(json!({ "cmd": "msg", "arg": action }))
}
pub fn windows() -> Result<Vec<Value>> {
Ok(query("windows")?.as_array().cloned().unwrap_or_default())
}
pub struct WindowPlacement {
pub geometry: String,
pub origin: (i64, i64),
pub shown_on_screen: bool,
}
pub fn window_placement(window_id: &str) -> Result<WindowPlacement> {
let matching = windows()?
.into_iter()
.find(|entry| entry.get("id").and_then(Value::as_str) == Some(window_id))
.ok_or_else(|| anyhow!("no window with id {window_id}"))?;
let number = |key: &str| -> Result<i64> {
matching
.get(key)
.and_then(Value::as_i64)
.ok_or_else(|| anyhow!("window {window_id} has no {key}"))
};
let (x, y) = (number("x")?, number("y")?);
Ok(WindowPlacement {
geometry: format!("{x},{y} {}x{}", number("w")?, number("h")?),
origin: (x, y),
shown_on_screen: matching
.get("visible")
.and_then(Value::as_bool)
.unwrap_or(true),
})
}