mod commands;
mod errors;
pub use commands::*;
pub use errors::*;
use anyhow::anyhow;
use byteorder::{LittleEndian, WriteBytesExt};
use lazy_static::lazy_static;
use std::{
io::{Read, Write},
os::unix::net::UnixStream,
path::PathBuf,
};
lazy_static! {
static ref SOCKET_PATH: PathBuf = {
PathBuf::from(format!(
"/tmp/yabai_{}.socket",
std::env::var("USER").unwrap()
))
};
}
pub fn send(message: &str) -> anyhow::Result<Option<String>> {
send_raw(&format!(
"{}\0\0",
message.trim().split(' ').collect::<Vec<&str>>().join("\0")
))
}
fn send_raw(command: &str) -> anyhow::Result<Option<String>> {
let mut buffer = Vec::new();
let mut stream = UnixStream::connect(SOCKET_PATH.as_path())?;
stream.write_u32::<LittleEndian>(command.len() as u32)?;
stream.write_all(command.as_bytes())?;
let bytes = stream.read_to_end(&mut buffer)?;
if bytes == 0 {
return Ok(None);
}
if buffer[0] == 0x07 {
let rest = buffer[1..].to_vec();
let error_message = String::from_utf8(rest)?;
let error = YabaiError::CommandError {
command: command.to_string(),
message: error_message,
};
return Err(anyhow!(error));
}
Ok(Some(String::from_utf8(buffer)?))
}
pub fn send_command(command: &Command) -> anyhow::Result<Option<String>> {
let result = match command {
Command::FocusSpace { option } => match option {
FocusSpaceOption::Space { space } => send(&format!("space --focus {}", space))?,
named_option => send(&format!("space --focus {named_option}"))?,
},
Command::RotateSpace { rotation } => send(&format!("space --rotate {}", rotation))?,
Command::BalanceSpace {} => send("space --balance")?,
Command::MoveActiveWindowToSpace { space } => send(&format!("window --space {}", space))?,
Command::FocusWindow { window } => send(&format!("window --focus {}", window))?,
Command::FocusWindowDirection { direction } => {
send(&format!("window --focus {}", direction))?
}
Command::SwapWindowDirection { direction } => {
send(&format!("window --swap {}", direction))?
}
Command::WarpWindowDirection { direction } => {
send(&format!("window --warp {}", direction))?
}
Command::ToggleWindowFloating {} => send("window --toggle float")?,
Command::ToggleZoomFullscreen {} => send("window --toggle zoom-fullscreen")?,
};
Ok(result)
}
pub fn query_spaces() -> anyhow::Result<Vec<SpaceInfo>> {
let result = send("query --spaces")?;
match result {
Some(str) => Ok(serde_json::from_str::<Vec<SpaceInfo>>(&str)?),
None => Err(anyhow!("No result from yabai query --spaces")),
}
}
pub fn query_displays() -> anyhow::Result<Vec<DisplayInfo>> {
let result = send("query --displays")?;
match result {
Some(str) => Ok(serde_json::from_str::<Vec<DisplayInfo>>(&str)?),
None => Err(anyhow!("No result from yabai query --displays")),
}
}
pub fn query_windows() -> anyhow::Result<Vec<WindowInfo>> {
let result = send("query --windows")?;
match result {
Some(str) => Ok(serde_json::from_str::<Vec<WindowInfo>>(&str)?),
None => Err(anyhow!("No result from yabai query --windows")),
}
}
pub fn focus_window(window: u32) -> anyhow::Result<Option<String>> {
send_command(&Command::FocusWindow { window })
}
pub fn focus_space(space: u32) -> anyhow::Result<Option<String>> {
send_command(&Command::FocusSpace {
option: FocusSpaceOption::Space { space },
})
}