use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Hyprland {
command: PathBuf,
event: PathBuf,
}
impl Hyprland {
pub fn connect() -> Option<Self> {
let dir = std::env::var_os("XDG_RUNTIME_DIR")?;
let signature = std::env::var_os("HYPRLAND_INSTANCE_SIGNATURE")?;
let base = PathBuf::from(dir).join("hypr").join(signature);
let command = base.join(".socket.sock");
let event = base.join(".socket2.sock");
if !command.exists() || !event.exists() {
return None;
}
Some(Self { command, event })
}
fn command(&self, command: &str) -> Option<String> {
let mut stream = UnixStream::connect(&self.command).ok()?;
stream.write_all(command.as_bytes()).ok()?;
stream.shutdown(std::net::Shutdown::Write).ok()?;
let mut response = String::new();
stream.read_to_string(&mut response).ok()?;
Some(response)
}
pub fn active_workspace(&self) -> Option<String> {
let raw = self.command("j/activeworkspace")?;
let value: serde_json::Value = serde_json::from_str(&raw).ok()?;
value.get("name")?.as_str().map(str::to_owned)
}
pub fn workspace_names(&self) -> Vec<String> {
let raw = self.command("j/workspaces").unwrap_or_default();
let list: Vec<serde_json::Value> = serde_json::from_str(&raw).unwrap_or_default();
let mut named: Vec<(i64, String)> = list
.iter()
.filter_map(|workspace| {
let id = workspace.get("id")?.as_i64()?;
let name = workspace.get("name")?.as_str()?;
Some((id, name.to_owned()))
})
.collect();
named.sort_by_key(|(id, _)| *id);
named.into_iter().map(|(_, name)| name).collect()
}
pub fn dispatch_workspace(&self, name: &str) {
let classic = self.command(&format!("dispatch workspace {name}"));
if classic.as_deref().is_some_and(|response| response.trim() == "ok") {
return;
}
let _ = self.command(&format!("dispatch hl.dsp.focus({{ workspace = \"{name}\" }})"));
}
pub fn spawn_watcher(&self, on_change: impl Fn(String) + Send + 'static) {
let path = self.event.clone();
std::thread::spawn(move || {
let Ok(stream) = UnixStream::connect(&path) else { return };
for line in BufReader::new(stream).lines() {
let Ok(line) = line else { break };
let workspace = line
.strip_prefix("workspace>>")
.or_else(|| line.strip_prefix("focusedmon>>").and_then(|rest| rest.split_once(',').map(|(_, ws)| ws)));
if let Some(name) = workspace {
on_change(name.to_owned());
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn connect_returns_none_without_hyprland_env() {
if std::env::var_os("HYPRLAND_INSTANCE_SIGNATURE").is_none() {
assert!(Hyprland::connect().is_none());
}
}
}