#![allow(clippy::collapsible_else_if)] use std::io; use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; use std::os::unix::net::UnixStream; use std::process::{Command, Stdio}; use super::*; use crate::event_loop; use crate::utils::read_to_vec; pub struct MangoInfoProvider { mmsg_sock: UnixStream, mmsg_buf: Vec, all_monitors: Vec, } #[derive(serde::Deserialize, Debug, Clone)] struct MangoTag { index: u32, is_active: bool, is_urgent: bool, client_count: u32, } #[derive(serde::Deserialize, Debug, Clone)] struct MangoMonitor { name: String, active: bool, tags: Vec, layout_symbol: String, #[serde(default)] active_client: Option, } // When no client is focused, the compositor sends // {"id":null,"title":null,"appid":null} instead of omitting the field. #[derive(serde::Deserialize, Debug, Clone)] struct MangoActiveClient { #[allow(dead_code)] id: Option, title: Option, appid: Option, } #[derive(serde::Deserialize, Debug)] struct MangoIpc { monitors: Vec, } impl MangoInfoProvider { pub fn new() -> Option { let child = Command::new("mmsg") .arg("watch") .arg("all-monitors") .stdout(Stdio::piped()) .spawn().ok()?; let stdout = child.stdout.expect("couldn't get stdout"); let sock = unsafe { UnixStream::from_raw_fd(stdout.into_raw_fd()) }; sock.set_nonblocking(true).ok()?; Some(Self { mmsg_sock: sock, mmsg_buf: Vec::new(), all_monitors: Vec::new(), }) } fn next_event(&mut self) -> io::Result { loop { if let Some(i) = memchr::memchr(b'\n', &self.mmsg_buf) { let event = String::from_utf8_lossy(&self.mmsg_buf[..i]).into_owned(); self.mmsg_buf.drain(..=i); return Ok(serde_json::from_str(&event)?); } if read_to_vec(&self.mmsg_sock, &mut self.mmsg_buf)? == 0 { return Err(io::Error::new( io::ErrorKind::BrokenPipe, "mmsg child process disconnected", )); } } } } impl WmInfoProvider for MangoInfoProvider { fn register(&self, ev: &mut EventLoop) { ev.register_with_fd(self.mmsg_sock.as_raw_fd(), |ctx| { loop { let mango = ctx.state.shared_state.get_mango().unwrap(); let mut layout_updated = false; let focused_window_updated; match mango.next_event() { Ok(event) => { if let Some(i) = event.monitors.iter().position(|m| m.active) { if mango.all_monitors.get(i).map(|m| &m.layout_symbol) != event.monitors.get(i).map(|m| &m.layout_symbol) { layout_updated = true; } } focused_window_updated = event.monitors.iter().any(|new_monitor| { let old_client = mango .all_monitors .iter() .find(|m| m.name == new_monitor.name) .and_then(|m| m.active_client.as_ref()); old_client.map(|c| (&c.title, &c.appid)) != new_monitor.active_client.as_ref().map(|c| (&c.title, &c.appid)) }); mango.all_monitors = event.monitors; } Err(e) if e.kind() == io::ErrorKind::WouldBlock => break, Err(e) => { ctx.state.set_error(ctx.conn, "mango", e); return Ok(event_loop::Action::Unregister); } } ctx.state.tags_updated(ctx.conn, None); if layout_updated { ctx.state.layout_name_updated(ctx.conn, None); } if focused_window_updated { ctx.state.focused_window_updated(ctx.conn, None); } } Ok(event_loop::Action::Keep) }); } fn get_tags(&self, output: &Output) -> Vec { self.all_monitors .iter() .find(|c| c.name == output.name) .map_or_else( Vec::new, |monitor| { monitor .tags .iter() .enumerate() .map(|(i, mango_tag)| Tag { id: mango_tag.index, name: (i + 1).to_string(), is_focused: mango_tag.is_active, is_active: mango_tag.client_count > 0, is_urgent: mango_tag.is_urgent, }) .collect() }, ) } fn get_layout_name(&self, output: &Output) -> Option { self.all_monitors .iter() .find(|c| c.name == output.name) .map_or_else( || { eprintln!("Couldn't find layout_name for monitor {}", output.name); None }, |monitor| { Some(monitor.layout_symbol.clone()) }) } fn get_mode_name(&self, _: &Output) -> Option { None } fn get_focused_window(&self, output: &Output) -> Option { self.all_monitors .iter() .find(|c| c.name == output.name) .and_then(|monitor| monitor.active_client.as_ref()) .and_then(|client| { Some(FocusedWindow { app_id: client.appid.clone()?, title: client.title.clone().unwrap_or_default(), }) }) } fn click_on_tag( &mut self, _conn: &mut Connection, _output: &Output, _seat: WlSeat, tag_id: Option, _btn: PointerBtn, ) { if let Some(id) = tag_id { let _ = Command::new("mmsg") .arg("dispatch") .arg(format!("view,{}", id)) .stdout(Stdio::piped()) .spawn(); } } } #[cfg(test)] mod tests { use super::*; // Real output shape of `mmsg watch all-monitors` (mangowm 0.15.4) #[test] fn parse_all_monitors_event() { let event = r#"{"monitors":[{"name":"DP-1","active":true,"is_hdr":false,"is_vrr":false,"x":1920,"y":0,"width":1920,"height":1080,"scale":1,"layout_index":0,"layout_symbol":"T","last_open_surface":"i3bar-river","tags":[{"index":1,"is_active":true,"is_urgent":false,"layout":"T","client_count":1}],"active_tags":[1],"active_client":{"id":3,"title":"some title","appid":"emacs"},"keymode":"default","keyboardlayout":"English (US)"}]}"#; let ipc: MangoIpc = serde_json::from_str(event).unwrap(); let client = ipc.monitors[0].active_client.as_ref().unwrap(); assert_eq!(client.appid.as_deref(), Some("emacs")); assert_eq!(client.title.as_deref(), Some("some title")); } // With no focused client the compositor sends nulls instead of omitting the field #[test] fn parse_all_monitors_event_no_focused_client() { let event = r#"{"monitors":[{"name":"DP-1","active":true,"layout_symbol":"T","tags":[{"index":1,"is_active":false,"is_urgent":false,"client_count":0}],"active_client":{"id":null,"title":null,"appid":null}}]}"#; let ipc: MangoIpc = serde_json::from_str(event).unwrap(); let client = ipc.monitors[0].active_client.as_ref().unwrap(); assert_eq!(client.appid, None); assert_eq!(client.title, None); } // Older mangowc versions (<0.14.x) have no active_client field at all #[test] fn parse_all_monitors_event_without_active_client() { let event = r#"{"monitors":[{"name":"DP-1","active":true,"layout_symbol":"T","tags":[{"index":1,"is_active":false,"is_urgent":false,"client_count":0}]}]}"#; let ipc: MangoIpc = serde_json::from_str(event).unwrap(); assert!(ipc.monitors[0].active_client.is_none()); } }