use std::io::{BufRead, BufReader};
use std::process::{Child, ChildStdout, Command, Stdio};
use serde_json::Value;
const BIN: &str = "aerospace";
const WORKSPACE_FORMAT: &str =
"%{workspace}%{workspace-is-focused}%{workspace-is-visible}%{monitor-id}";
const WINDOW_FORMAT: &str =
"%{window-id}%{workspace}%{app-name}%{app-bundle-id}%{window-title}%{window-is-fullscreen}";
fn query(args: &[&str]) -> Option<Value> {
let out = Command::new(BIN).args(args).output().ok()?;
if !out.status.success() {
tracing::warn!(
args = ?args,
stderr = %String::from_utf8_lossy(&out.stderr).trim(),
"aerospace query failed"
);
return None;
}
serde_json::from_slice(&out.stdout).ok()
}
pub fn list_workspaces() -> Option<Value> {
query(&[
"list-workspaces",
"--all",
"--format",
WORKSPACE_FORMAT,
"--json",
])
}
pub fn list_windows() -> Option<Value> {
query(&["list-windows", "--all", "--format", WINDOW_FORMAT, "--json"])
}
pub fn switch_workspace(name: &str) {
match Command::new(BIN).args(["workspace", name]).output() {
Ok(out) if out.status.success() => {}
Ok(out) => tracing::warn!(
workspace = %name,
stderr = %String::from_utf8_lossy(&out.stderr).trim(),
"workspace switch rejected"
),
Err(e) => tracing::error!(workspace = %name, error = %e, "could not run aerospace"),
}
}
pub fn subscribe() -> std::io::Result<(Child, BufReader<ChildStdout>)> {
let mut child = Command::new(BIN)
.args(["subscribe", "--all"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let stdout = child.stdout.take().expect("stdout was piped");
Ok((child, BufReader::new(stdout)))
}
pub fn pump_events(reader: BufReader<ChildStdout>, mut on_event: impl FnMut()) {
for line in reader.lines() {
match line {
Ok(_) => on_event(),
Err(e) => {
tracing::error!(error = %e, "event stream ended");
return;
}
}
}
}