#![allow(clippy::collapsible_else_if)] use std::io::{self, Write}; use std::os::fd::AsRawFd; use std::os::unix::net::UnixStream; use std::path::PathBuf; use serde::de::DeserializeOwned; use super::*; use crate::event_loop; use crate::utils::read_to_vec; pub struct HyprlandInfoProvider { ipc: Ipc, workspaces: Vec, active_id: u32, } impl HyprlandInfoProvider { pub fn new() -> Option { let his = std::env::var("HYPRLAND_INSTANCE_SIGNATURE").ok()?; let ipc = Ipc::new(&his)?; Some(Self { workspaces: ipc.query_sorted_workspaces().ok()?, active_id: ipc.query_json::("j/activeworkspace").ok()?.id, ipc, }) } fn set_workspace(&self, id: u32) { let _ = self.ipc.exec(&format!("/dispatch workspace {id}")); } } impl WmInfoProvider for HyprlandInfoProvider { fn register(&self, event_loop: &mut EventLoop) { event_loop.register_with_fd(self.ipc.sock2.as_raw_fd(), |ctx| { match hyprland_cb(ctx.conn, ctx.state) { Ok(()) => Ok(event_loop::Action::Keep), Err(e) => { ctx.state.set_error(ctx.conn, "hyprland", e); Ok(event_loop::Action::Unregister) } } }); } fn get_tags(&self, output: &Output) -> Vec { self.workspaces .iter() .filter(|ws| ws.monitor == output.name) .map(|ws| Tag { id: ws.id, name: ws.name.clone(), is_focused: ws.id == self.active_id, is_active: true, is_urgent: false, }) .collect() } fn click_on_tag( &mut self, _: &mut Connection, output: &Output, _: WlSeat, tag_id: Option, btn: PointerBtn, ) { match btn { PointerBtn::Left => { if let Some(tag_id) = tag_id { self.set_workspace(tag_id); } } PointerBtn::WheelUp | PointerBtn::WheelDown => { if let Some(active_i) = self .workspaces .iter() .position(|ws| ws.monitor == output.name && self.active_id == ws.id) { if btn == PointerBtn::WheelUp { if let Some(prev) = self.workspaces[..active_i] .iter() .rfind(|ws| ws.monitor == output.name) { self.set_workspace(prev.id); } } else { if let Some(next) = self.workspaces[active_i..] .iter() .skip(1) .find(|ws| ws.monitor == output.name) { self.set_workspace(next.id); } } } } _ => (), } } fn as_any(&mut self) -> &mut dyn Any { self } } fn hyprland_cb(conn: &mut Connection, state: &mut State) -> io::Result<()> { let hyprland = state.shared_state.get_hyprland().unwrap(); let mut updated = false; loop { match hyprland.ipc.next_event() { Ok(event) => { if let Some(active_ws) = event.strip_prefix("workspace>>") { let ws = hyprland .workspaces .iter() .find(|ws| ws.name == active_ws) .ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidData, "Unknown workspace") })?; hyprland.active_id = ws.id; updated = true; } else if let Some(data) = event.strip_prefix("focusedmon>>") { let (_monitor, active_ws) = data.split_once(',').ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidData, "Too few fields in data") })?; let ws = hyprland .workspaces .iter() .find(|ws| ws.name == active_ws) .ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidData, "Unknown workspace") })?; hyprland.active_id = ws.id; updated = true; } else if event.contains("workspace>>") { hyprland.workspaces = hyprland.ipc.query_sorted_workspaces()?; updated = true; } } Err(e) if e.kind() == io::ErrorKind::WouldBlock => break, Err(e) => return Err(e), } } if updated { state.tags_updated(conn, None); } Ok(()) } struct Ipc { sock1_path: PathBuf, sock2: UnixStream, sock2_buf: Vec, } impl Ipc { fn new(his: &str) -> Option { let mut path = PathBuf::from(std::env::var("XDG_RUNTIME_DIR").ok()?); path.push("hypr"); if !path.exists() { path.push("/tmp/hypr"); } path.push(his); let sock1_path = path.join(".socket.sock"); let sock2_path = path.join(".socket2.sock"); let sock2 = UnixStream::connect(sock2_path).ok()?; sock2.set_nonblocking(true).ok()?; Some(Self { sock1_path, sock2, sock2_buf: Vec::new(), }) } fn exec(&self, cmd: &str) -> io::Result<()> { let mut sock = UnixStream::connect(&self.sock1_path)?; sock.write_all(cmd.as_bytes())?; sock.flush()?; Ok(()) } fn query_json(&self, cmd: &str) -> io::Result { let mut sock = UnixStream::connect(&self.sock1_path)?; sock.write_all(cmd.as_bytes())?; sock.flush()?; serde_json::from_reader(&mut sock).map_err(Into::into) } fn query_sorted_workspaces(&self) -> io::Result> { let mut workspaces = self.query_json::>("j/workspaces")?; workspaces.sort_unstable_by_key(|x| x.id); Ok(workspaces) } fn next_event(&mut self) -> io::Result { loop { if let Some(i) = memchr::memchr(b'\n', &self.sock2_buf) { let event = String::from_utf8_lossy(&self.sock2_buf[..i]).into_owned(); self.sock2_buf.drain(..=i); return Ok(event); } if read_to_vec(&self.sock2, &mut self.sock2_buf)? == 0 { return Err(io::Error::new( io::ErrorKind::BrokenPipe, "hyprland socked disconnected", )); } } } } #[derive(Debug, serde::Deserialize)] struct IpcWorkspace { id: u32, name: String, monitor: String, }