From 198f374a18b5c8a0e21f7d41125393aa89523f5c Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 21 Jul 2026 14:07:42 +0200 Subject: mango: show focused window next to tags, fix mmsg lookup, 0.15.x compat - new focused-window block ({icon} {app_id} - {title}) driven by the active_client field of mmsg watch all-monitors (mangowm >= 0.14) - resolve program icons via freedesktop icon themes/.desktop files (new icon module, cairo png feature, cached per app_id) - new show_focused_window config option (default true) - run mmsg from PATH instead of hardcoded /usr/bin/mmsg (broken on NixOS) - handle null active_client fields (no focused client) without failing to parse the whole event - drop stale TODO comment and unused variable in mango provider - add .justfile run recipe exporting MANGO_INSTANCE_SIGNATURE --- src/wm_info_provider/mango.rs | 81 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 6 deletions(-) (limited to 'src/wm_info_provider/mango.rs') diff --git a/src/wm_info_provider/mango.rs b/src/wm_info_provider/mango.rs index fe5759e..5476f5e 100644 --- a/src/wm_info_provider/mango.rs +++ b/src/wm_info_provider/mango.rs @@ -29,6 +29,18 @@ struct MangoMonitor { 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, } @@ -40,7 +52,7 @@ struct MangoIpc { impl MangoInfoProvider { pub fn new() -> Option { - let child = Command::new("/usr/bin/mmsg") + let child = Command::new("mmsg") .arg("watch") .arg("all-monitors") .stdout(Stdio::piped()) @@ -80,6 +92,7 @@ impl WmInfoProvider for MangoInfoProvider { 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) { @@ -87,6 +100,15 @@ impl WmInfoProvider for MangoInfoProvider { 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, @@ -99,6 +121,9 @@ impl WmInfoProvider for MangoInfoProvider { 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) }); @@ -107,7 +132,8 @@ impl WmInfoProvider for MangoInfoProvider { self.all_monitors .iter() .find(|c| c.name == output.name) - .map_or_default( + .map_or_else( + Vec::new, |monitor| { monitor .tags @@ -124,9 +150,7 @@ impl WmInfoProvider for MangoInfoProvider { }, ) } - // TODO fn get_layout_name(&self, output: &Output) -> Option { - // self.layout_symbol.clone() self.all_monitors .iter() .find(|c| c.name == output.name) @@ -142,17 +166,29 @@ impl WmInfoProvider for MangoInfoProvider { 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, + _conn: &mut Connection, _output: &Output, _seat: WlSeat, tag_id: Option, _btn: PointerBtn, ) { if let Some(id) = tag_id { - let _ = Command::new("/usr/bin/mmsg") + let _ = Command::new("mmsg") .arg("dispatch") .arg(format!("view,{}", id)) .stdout(Stdio::piped()) @@ -160,3 +196,36 @@ impl WmInfoProvider for MangoInfoProvider { } } } + +#[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()); + } +} -- cgit v1.3.1