diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bar.rs | 192 | ||||
| -rw-r--r-- | src/button_manager.rs | 8 | ||||
| -rw-r--r-- | src/config.rs | 28 | ||||
| -rw-r--r-- | src/icon.rs | 28 | ||||
| -rw-r--r-- | src/main.rs | 1 | ||||
| -rw-r--r-- | src/shared_state.rs | 4 | ||||
| -rw-r--r-- | src/state.rs | 37 | ||||
| -rw-r--r-- | src/tray.rs | 615 | ||||
| -rw-r--r-- | src/wm_info_provider.rs | 11 |
9 files changed, 914 insertions, 10 deletions
@@ -14,6 +14,7 @@ use crate::protocol::*; use crate::shared_state::SharedState; use crate::state::State; use crate::text::{self, ComputedText, RenderOptions}; +use crate::tray::TrayCommand; use crate::wm_info_provider::{FocusedWindow, Tag}; pub struct Bar { @@ -39,6 +40,14 @@ pub struct Bar { layout_name_computed: Option<ComputedText>, mode_computed: Option<ComputedText>, focused_window_computed: Option<ComputedText>, + tray_btns: ButtonManager<TrayBtn>, + tray_toggle_computed: Option<ComputedText>, +} + +#[derive(Clone)] +enum TrayBtn { + Toggle, + Item(String), } #[derive(Debug, PartialEq)] @@ -60,7 +69,7 @@ impl Bar { surface, Some(output.wl), state.shared_state.config.layer.into(), - c"i3bar-river".into(), + c"i3bar-mango".into(), layer_surface_cb, ); @@ -87,6 +96,8 @@ impl Bar { layout_name_computed: None, mode_computed: None, focused_window_computed: None, + tray_btns: ButtonManager::default(), + tray_toggle_computed: None, } } @@ -128,7 +139,7 @@ impl Bar { button: PointerBtn, seat: WlSeat, x: f64, - _y: f64, + y: f64, ) -> anyhow::Result<()> { if let Some(tag_id) = self.tags_btns.click(x) { ss.wm_info_provider @@ -136,6 +147,31 @@ impl Bar { } else if self.tags_btns.is_between(x) { ss.wm_info_provider .click_on_tag(conn, &self.output, seat, None, button); + } else if let Some(tray_btn) = self.tray_btns.click(x).cloned() { + match tray_btn { + TrayBtn::Toggle => { + ss.tray_expanded = !ss.tray_expanded; + self.tray_toggle_computed = None; + self.frame(conn, ss); + } + TrayBtn::Item(key) => { + if let Some(tray) = &ss.tray { + let cmd = match button { + PointerBtn::Left => Some(TrayCommand::Activate { key }), + PointerBtn::Middle => Some(TrayCommand::SecondaryActivate { key }), + PointerBtn::Right => Some(TrayCommand::ContextMenu { + key, + x: x as i32, + y: y as i32, + }), + _ => None, + }; + if let Some(cmd) = cmd { + tray.send(cmd); + } + } + } + } } else if let Some((name, instance)) = self.blocks_btns.click(x) { if let Some(cmd) = &mut ss.status_cmd { cmd.send_click_event(&i3bar_protocol::Event { @@ -381,6 +417,14 @@ impl Bar { } } + // Display the tray + let tray_width = if ss.config.tray.enabled { + self.render_tray(&cairo_ctx, ss, width_f, height_f) + } else { + self.tray_btns.clear(); + 0.0 + }; + // Display the blocks render_blocks( &cairo_ctx, @@ -388,7 +432,7 @@ impl Bar { ss.blocks_cache.get_computed(), &mut self.blocks_btns, offset_left, - width_f, + width_f - tray_width, height_f, ); @@ -417,6 +461,148 @@ impl Bar { self.surface.commit(conn); } + fn render_tray( + &mut self, + context: &cairo::Context, + ss: &mut SharedState, + full_width: f64, + full_height: f64, + ) -> f64 { + self.tray_btns.clear(); + if ss.tray_items.is_empty() { + return 0.0; + } + + let cfg = ss.config.tray; + let expanded = ss.tray_expanded; + let icon_size = full_height - 4.0; + let spacing = 8.0; + let bar_height = self.height; + + let toggle = self.tray_toggle_computed.get_or_insert_with(|| { + ComputedText::new( + if expanded { "\u{25BE}" } else { "\u{25B8}" }, + text::Attributes { + font: &ss.config.font, + padding_left: 8.0, + padding_right: 8.0, + min_width: None, + align: Default::default(), + markup: false, + }, + ) + }); + + struct Rendered { + key: String, + icon: Option<cairo::ImageSurface>, + text: Option<ComputedText>, + width: f64, + } + + let mut rendered = Vec::new(); + if expanded { + for item in &ss.tray_items { + let label = match (cfg.show_name, cfg.show_description && !item.tooltip.is_empty()) { + (true, true) if item.tooltip != item.title => { + format!("{} \u{2014} {}", item.title, item.tooltip) + } + (true, _) => item.title.clone(), + (false, true) => item.tooltip.clone(), + (false, false) => String::new(), + }; + let text = if label.is_empty() { + None + } else { + Some(ComputedText::new( + &label, + text::Attributes { + font: &ss.config.font, + padding_left: 4.0, + padding_right: 4.0, + min_width: None, + align: Default::default(), + markup: false, + }, + )) + }; + let icon = if cfg.show_icon { + item.pixmap.clone().or_else(|| { + ss.icon_cache + .get_tray_icon(&item.icon_name, &item.icon_theme_path, bar_height) + .cloned() + }) + } else { + None + }; + let mut width = text.as_ref().map_or(0.0, |t| t.width); + if icon.is_some() { + width += icon_size + 4.0; + } + if width > 0.0 { + rendered.push(Rendered { + key: item.key.clone(), + icon, + text, + width, + }); + } + } + } + + let items_width: f64 = rendered.iter().map(|r| r.width + spacing).sum(); + let total_width = items_width + toggle.width; + let mut x = full_width - total_width; + + for r in rendered { + let mut item_x = x; + if let Some(icon) = &r.icon { + let scale = icon_size / (icon.width() as f64).max(icon.height() as f64); + let icon_w = icon.width() as f64 * scale; + let icon_h = icon.height() as f64 * scale; + context.save().unwrap(); + context.translate(item_x + (icon_size - icon_w) / 2.0, (full_height - icon_h) / 2.0); + context.scale(scale, scale); + context.set_source_surface(icon, 0.0, 0.0).unwrap(); + context.paint().unwrap(); + context.restore().unwrap(); + item_x += icon_size + 4.0; + } + if let Some(text) = &r.text { + text.render( + context, + RenderOptions { + x_offset: item_x, + bar_height: full_height, + fg_color: ss.config.color, + bg_color: None, + r_left: 0.0, + r_right: 0.0, + overlap: 0.0, + }, + ); + } + self.tray_btns.push(x, r.width, TrayBtn::Item(r.key)); + x += r.width + spacing; + } + + toggle.render( + context, + RenderOptions { + x_offset: x, + bar_height: full_height, + fg_color: ss.config.color, + bg_color: None, + r_left: 0.0, + r_right: 0.0, + overlap: 0.0, + }, + ); + self.tray_btns.push(x, toggle.width, TrayBtn::Toggle); + + total_width + } + pub fn show(&mut self, conn: &mut Connection<State>, shared_state: &SharedState) { assert!(!self.mapped); diff --git a/src/button_manager.rs b/src/button_manager.rs index 13f9124..9915460 100644 --- a/src/button_manager.rs +++ b/src/button_manager.rs @@ -1,6 +1,12 @@ -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ButtonManager<T = usize>(Vec<(f64, f64, T)>); +impl<T> Default for ButtonManager<T> { + fn default() -> Self { + Self(Vec::new()) + } +} + impl<T> ButtonManager<T> { pub fn push(&mut self, x_offset: f64, width: f64, elem: T) { self.0.push((x_offset, width, elem)); diff --git a/src/config.rs b/src/config.rs index e5c04e7..f93d7a8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,6 +50,8 @@ pub struct Config { pub blend: bool, pub show_mode: bool, pub start_hidden: bool, + // tray + pub tray: TrayConfig, // wm-specific pub wm: WmConfig, // overrides @@ -98,6 +100,8 @@ impl Default for Config { show_mode: true, start_hidden: false, + tray: TrayConfig::default(), + wm: WmConfig { river: RiverConfig { max_tag: 9 }, }, @@ -148,7 +152,7 @@ fn config_dir() -> Option<PathBuf> { fn config_path() -> Option<PathBuf> { let mut path = config_dir()?; - path.push("i3bar-river"); + path.push("i3bar-mango"); path.push("config.toml"); path.exists().then_some(path) } @@ -189,6 +193,28 @@ impl From<Layer> for zwlr_layer_shell_v1::Layer { } } +#[derive(Deserialize, Debug, Clone, Copy)] +#[serde(deny_unknown_fields, default)] +pub struct TrayConfig { + pub enabled: bool, + pub expanded: bool, + pub show_icon: bool, + pub show_name: bool, + pub show_description: bool, +} + +impl Default for TrayConfig { + fn default() -> Self { + Self { + enabled: true, + expanded: false, + show_icon: true, + show_name: true, + show_description: false, + } + } +} + #[derive(Debug, Deserialize)] pub struct WmConfig { pub river: RiverConfig, diff --git a/src/icon.rs b/src/icon.rs index 4dae1a9..1173629 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -17,6 +17,34 @@ impl IconCache { .or_insert_with(|| resolve_icon(app_id, bar_height)) .as_ref() } + + /// Get a tray icon by name, preferring the given SNI IconThemePath. + pub fn get_tray_icon( + &mut self, + name: &str, + theme_path: &str, + bar_height: u32, + ) -> Option<&cairo::ImageSurface> { + if name.is_empty() { + return None; + } + self.map + .entry(format!("{theme_path}:{name}")) + .or_insert_with(|| resolve_tray_icon(name, theme_path, bar_height)) + .as_ref() + } +} + +fn resolve_tray_icon(name: &str, theme_path: &str, bar_height: u32) -> Option<cairo::ImageSurface> { + if !theme_path.is_empty() { + let path = PathBuf::from(theme_path).join(format!("{name}.png")); + if let Ok(mut file) = std::fs::File::open(path) + && let Ok(surface) = cairo::ImageSurface::create_from_png(&mut file) + { + return Some(surface); + } + } + resolve_icon(name, bar_height) } fn resolve_icon(app_id: &str, bar_height: u32) -> Option<cairo::ImageSurface> { diff --git a/src/main.rs b/src/main.rs index b43443c..a4751d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ mod shared_state; mod state; mod status_cmd; mod text; +mod tray; mod utils; mod wm_info_provider; diff --git a/src/shared_state.rs b/src/shared_state.rs index c484d24..3bf05e5 100644 --- a/src/shared_state.rs +++ b/src/shared_state.rs @@ -5,6 +5,7 @@ use crate::{ config::Config, icon::IconCache, status_cmd::StatusCmd, + tray::{TrayHandle, TrayItem}, wm_info_provider::{self, WmInfoProvider}, }; @@ -17,6 +18,9 @@ pub struct SharedState { pub blocks_cache: BlocksCache, pub icon_cache: IconCache, pub wm_info_provider: Box<dyn WmInfoProvider>, + pub tray: Option<TrayHandle>, + pub tray_items: Vec<TrayItem>, + pub tray_expanded: bool, } impl SharedState { diff --git a/src/state.rs b/src/state.rs index ccd0d52..2853731 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,6 +5,7 @@ use crate::protocol::*; use crate::wm_info_provider; use std::fmt::Display; +use std::io; use std::os::unix::io::{AsRawFd, RawFd}; use std::path::Path; @@ -15,6 +16,9 @@ use wayrs_utils::cursor::{CursorImage, CursorShape, CursorTheme, ThemedPointer}; use wayrs_utils::seats::{SeatHandler, Seats}; use wayrs_utils::shm_alloc::ShmAlloc; +use crate::event_loop::Action; +use crate::tray::TrayHandle; + use crate::{ bar::Bar, config::Config, i3bar_protocol::Block, pointer_btn::PointerBtn, shared_state::SharedState, status_cmd::StatusCmd, @@ -83,6 +87,29 @@ impl State { let wm_info_provider = wm_info_provider::bind(conn, &config.wm); wm_info_provider.register(event_loop); + let tray = if config.tray.enabled { + match io::pipe() { + Ok((tray_read, tray_write)) => { + let tray = TrayHandle::spawn(tray_write.into()); + let fd = tray_read.as_raw_fd(); + event_loop.register_with_fd(fd, move |ctx| { + let mut buf = [0u8; 64]; + while io::Read::read(&mut &tray_read, &mut buf).is_ok_and(|n| n == 64) {} + ctx.state.tray_updated(ctx.conn); + Ok(Action::Keep) + }); + Some(tray) + } + Err(e) => { + eprintln!("tray: failed to create wake pipe: {e}"); + None + } + } + } else { + None + }; + let tray_expanded = config.tray.expanded; + let mut this = Self { wl_compositor, layer_shell: conn.bind_singleton(1..=4).unwrap(), @@ -105,6 +132,9 @@ impl State { blocks_cache: BlocksCache::default(), icon_cache: Default::default(), wm_info_provider, + tray, + tray_items: Vec::new(), + tray_expanded, }, cursor_theme, @@ -144,6 +174,13 @@ impl State { } } + pub fn tray_updated(&mut self, conn: &mut Connection<Self>) { + if let Some(tray) = &self.shared_state.tray { + self.shared_state.tray_items = tray.items(); + } + self.draw_all(conn); + } + pub fn status_cmd_fd(&self) -> Option<RawFd> { self.shared_state .status_cmd diff --git a/src/tray.rs b/src/tray.rs new file mode 100644 index 0000000..69fa9e2 --- /dev/null +++ b/src/tray.rs @@ -0,0 +1,615 @@ +use std::os::fd::{AsRawFd, OwnedFd}; +use std::sync::Arc; + +use parking_lot::Mutex; +use std::time::Duration; + +use futures_channel::mpsc; +use futures_lite::stream::StreamExt; +use pangocairo::cairo; +use zbus::object_server::SignalEmitter; +use zbus::{Connection, interface, proxy}; + +#[derive(Clone)] +pub struct TrayItem { + pub key: String, + pub title: String, + pub icon_name: String, + pub icon_theme_path: String, + pub tooltip: String, + pub pixmap: Option<cairo::ImageSurface>, +} + +unsafe impl Send for TrayItem {} + +pub enum TrayCommand { + Activate { key: String }, + SecondaryActivate { key: String }, + ContextMenu { key: String, x: i32, y: i32 }, +} + +pub struct TrayHandle { + items: Arc<Mutex<Vec<TrayItem>>>, + commands: mpsc::UnboundedSender<TrayCommand>, +} + +impl TrayHandle { + pub fn spawn(wake: OwnedFd) -> TrayHandle { + let items = Arc::new(Mutex::new(Vec::new())); + let (commands, commands_rx) = mpsc::unbounded(); + let worker_items = items.clone(); + std::thread::spawn(move || worker(wake, worker_items, commands_rx)); + TrayHandle { items, commands } + } + + pub fn items(&self) -> Vec<TrayItem> { + self.items.lock().clone() + } + + pub fn send(&self, cmd: TrayCommand) { + let _ = self.commands.unbounded_send(cmd); + } +} + +#[derive(Clone, Copy)] +enum SignalKind { + Title, + Icon, + Status, + ToolTip, + IconThemePath, +} + +enum Event { + Register { + bus_name: String, + path: String, + announced: String, + }, + Vanished(String), + Signal { + key: String, + kind: SignalKind, + }, +} + +enum Input { + Event(Event), + Command(TrayCommand), +} + +enum Method { + Activate, + SecondaryActivate, + ContextMenu, +} + +#[derive(Default)] +struct WatcherShared { + registered: Vec<String>, + hosts: Vec<String>, +} + +struct WatcherService { + shared: Arc<Mutex<WatcherShared>>, + events: mpsc::UnboundedSender<Event>, +} + +#[interface(name = "org.kde.StatusNotifierWatcher")] +impl WatcherService { + async fn register_status_notifier_item( + &self, + service: &str, + #[zbus(header)] header: zbus::message::Header<'_>, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> zbus::fdo::Result<()> { + let sender = header.sender().map(|name| name.as_str()).unwrap_or(""); + let (bus_name, path) = parse_registration(service, sender); + let announced = bus_name.clone(); + { + let mut shared = self.shared.lock(); + if shared.registered.contains(&announced) { + return Ok(()); + } + shared.registered.push(announced.clone()); + } + emitter.status_notifier_item_registered(&announced).await?; + self.registered_status_notifier_items_changed(&emitter).await?; + let _ = self.events.unbounded_send(Event::Register { + bus_name, + path, + announced, + }); + Ok(()) + } + + async fn register_status_notifier_host( + &self, + service: &str, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> zbus::fdo::Result<()> { + { + let mut shared = self.shared.lock(); + if shared.hosts.iter().any(|host| host == service) { + return Ok(()); + } + shared.hosts.push(service.to_string()); + } + emitter.status_notifier_host_registered().await?; + self.is_status_notifier_host_registered_changed(&emitter).await?; + Ok(()) + } + + #[zbus(property)] + fn registered_status_notifier_items(&self) -> Vec<String> { + self.shared.lock().registered.clone() + } + + #[zbus(property)] + fn is_status_notifier_host_registered(&self) -> bool { + !self.shared.lock().hosts.is_empty() + } + + #[zbus(property(emits_changed_signal = "const"))] + fn protocol_version(&self) -> i32 { + 0 + } + + #[zbus(signal)] + async fn status_notifier_item_registered( + emitter: &SignalEmitter<'_>, + service: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn status_notifier_item_unregistered( + emitter: &SignalEmitter<'_>, + service: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn status_notifier_host_registered(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; + + #[zbus(signal)] + async fn status_notifier_host_unregistered(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; +} + +#[proxy( + interface = "org.kde.StatusNotifierItem", + gen_blocking = false, + assume_defaults = false +)] +trait StatusNotifierItem { + fn activate(&self, x: i32, y: i32) -> zbus::Result<()>; + fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>; + fn context_menu(&self, x: i32, y: i32) -> zbus::Result<()>; + + #[zbus(property(emits_changed_signal = "false"))] + fn id(&self) -> zbus::Result<String>; + + #[zbus(property(emits_changed_signal = "false"))] + fn title(&self) -> zbus::Result<String>; + + #[zbus(property(emits_changed_signal = "false"))] + fn status(&self) -> zbus::Result<String>; + + #[zbus(property(emits_changed_signal = "false"))] + fn icon_name(&self) -> zbus::Result<String>; + + #[zbus(property(emits_changed_signal = "false"))] + fn icon_theme_path(&self) -> zbus::Result<String>; + + #[zbus(property(emits_changed_signal = "false"))] + fn tool_tip(&self) -> zbus::Result<(String, Vec<(i32, i32, Vec<u8>)>, String, String)>; + + #[zbus(property(emits_changed_signal = "false"))] + fn icon_pixmap(&self) -> zbus::Result<Vec<(i32, i32, Vec<u8>)>>; + + #[zbus(signal)] + fn new_title(&self); + + #[zbus(signal)] + fn new_icon(&self); + + #[zbus(signal)] + fn new_status(&self); + + #[zbus(signal)] + fn new_tool_tip(&self); + + #[zbus(signal)] + fn new_icon_theme_path(&self); +} + +struct ActiveItem { + key: String, + bus_name: String, + unique_name: String, + announced: String, + status: String, + id: String, + proxy: StatusNotifierItemProxy<'static>, + item: TrayItem, + _tasks: Vec<zbus::Task<()>>, +} + +fn worker( + wake: OwnedFd, + snapshot: Arc<Mutex<Vec<TrayItem>>>, + mut commands: mpsc::UnboundedReceiver<TrayCommand>, +) { + loop { + match futures_lite::future::block_on(run(&wake, &snapshot, &mut commands)) { + Ok(()) => break, + Err(err) => eprintln!("i3bar-mango: tray: {err}; retrying in 5s"), + } + std::thread::sleep(Duration::from_secs(5)); + } +} + +async fn run( + wake: &OwnedFd, + snapshot: &Arc<Mutex<Vec<TrayItem>>>, + commands: &mut mpsc::UnboundedReceiver<TrayCommand>, +) -> zbus::Result<()> { + let shared = Arc::new(Mutex::new(WatcherShared::default())); + let (events_tx, mut events_rx) = mpsc::unbounded::<Event>(); + + let conn = zbus::connection::Builder::session()? + .serve_at( + "/StatusNotifierWatcher", + WatcherService { + shared: shared.clone(), + events: events_tx.clone(), + }, + )? + .name("org.kde.StatusNotifierWatcher")? + .name(format!( + "org.kde.StatusNotifierHost-i3bar-mango-{}", + std::process::id() + ))? + .build() + .await?; + + let dbus = zbus::fdo::DBusProxy::new(&conn).await?; + + let owner_stream = dbus.receive_name_owner_changed().await?; + let tx = events_tx.clone(); + conn.executor() + .spawn( + async move { + futures_lite::pin!(owner_stream); + while let Some(signal) = owner_stream.next().await { + if let Ok(args) = signal.args() { + if args.new_owner().is_none() { + let _ = tx.unbounded_send(Event::Vanished(args.name().to_string())); + } + } + } + }, + "tray-name-owner", + ) + .detach(); + + let mut items: Vec<ActiveItem> = Vec::new(); + + loop { + let input = futures_lite::future::or( + async { events_rx.next().await.map(Input::Event) }, + async { commands.next().await.map(Input::Command) }, + ) + .await; + + match input { + None => return Ok(()), + Some(Input::Command(cmd)) => { + let (key, method, x, y) = match cmd { + TrayCommand::Activate { key } => (key, Method::Activate, 0, 0), + TrayCommand::SecondaryActivate { key } => (key, Method::SecondaryActivate, 0, 0), + TrayCommand::ContextMenu { key, x, y } => (key, Method::ContextMenu, x, y), + }; + if let Some(active) = items.iter().find(|item| item.key == key) { + let proxy = active.proxy.clone(); + conn.executor() + .spawn( + async move { + let result = match method { + Method::Activate => proxy.activate(0, 0).await, + Method::SecondaryActivate => { + proxy.secondary_activate(0, 0).await + } + Method::ContextMenu => proxy.context_menu(x, y).await, + }; + if let Err(err) = result { + eprintln!("i3bar-mango: tray: command failed: {err}"); + } + }, + "tray-command", + ) + .detach(); + } + } + Some(Input::Event(Event::Register { + bus_name, + path, + announced, + })) => { + let key = format!("{bus_name}/{path}"); + if items.iter().any(|item| item.key == key) { + continue; + } + let builder = StatusNotifierItemProxy::builder(&conn) + .destination(bus_name.clone()) + .and_then(|builder| builder.path(path.clone())); + let proxy = match builder { + Ok(builder) => match builder.build().await { + Ok(proxy) => proxy, + Err(err) => { + eprintln!("i3bar-mango: tray: failed to reach {key}: {err}"); + unregister(&conn, &shared, &announced).await; + continue; + } + }, + Err(err) => { + eprintln!("i3bar-mango: tray: invalid registration {key}: {err}"); + unregister(&conn, &shared, &announced).await; + continue; + } + }; + let id = match proxy.id().await { + Ok(id) => id, + Err(err) => { + eprintln!("i3bar-mango: tray: failed to read properties of {key}: {err}"); + unregister(&conn, &shared, &announced).await; + continue; + } + }; + let title = proxy.title().await.unwrap_or_default(); + let icon_name = proxy.icon_name().await.unwrap_or_default(); + let icon_theme_path = proxy.icon_theme_path().await.unwrap_or_default(); + let status = proxy.status().await.unwrap_or_default(); + let tooltip = proxy + .tool_tip() + .await + .map(|tooltip| strip_html(&tooltip.3)) + .unwrap_or_default(); + let pixmap = match proxy.icon_pixmap().await { + Ok(pixmaps) => best_pixmap(&pixmaps) + .and_then(|(width, height, data)| pixmap_to_surface(*width, *height, data)), + Err(_) => None, + }; + let title = if title.is_empty() { id.clone() } else { title }; + let unique_name = if bus_name.starts_with(':') { + bus_name.clone() + } else { + match zbus::names::BusName::try_from(bus_name.as_str()) { + Ok(name) => dbus + .get_name_owner(name) + .await + .map(|owner| owner.to_string()) + .unwrap_or_default(), + Err(_) => String::new(), + } + }; + let tasks = spawn_signal_tasks(&conn, &proxy, &key, &events_tx).await; + items.push(ActiveItem { + key: key.clone(), + bus_name, + unique_name, + announced, + status, + id, + proxy, + item: TrayItem { + key, + title, + icon_name, + icon_theme_path, + tooltip, + pixmap, + }, + _tasks: tasks, + }); + update_snapshot(&items, snapshot, wake); + } + Some(Input::Event(Event::Vanished(name))) => { + let removed = if let Some(pos) = items + .iter() + .position(|item| item.bus_name == name || item.unique_name == name) + { + Some(items.remove(pos).announced) + } else { + let is_host = shared.lock().hosts.iter().any(|host| *host == name); + if is_host { + let mut state = shared.lock(); + state.hosts.retain(|host| *host != name); + if let Ok(iface) = conn + .object_server() + .interface::<_, WatcherService>("/StatusNotifierWatcher") + .await + { + let _ = iface.status_notifier_host_unregistered().await; + let guard = iface.get().await; + let _ = guard + .is_status_notifier_host_registered_changed(iface.signal_emitter()) + .await; + } + None + } else if shared.lock().registered.iter().any(|n| *n == name) { + Some(name) + } else { + None + } + }; + if let Some(announced) = removed { + unregister(&conn, &shared, &announced).await; + update_snapshot(&items, snapshot, wake); + } + } + Some(Input::Event(Event::Signal { key, kind })) => { + let Some(index) = items.iter().position(|item| item.key == key) else { + continue; + }; + let active = &mut items[index]; + match kind { + SignalKind::Title => { + if let Ok(title) = active.proxy.title().await { + active.item.title = if title.is_empty() { + active.id.clone() + } else { + title + }; + } + } + SignalKind::Icon => { + if let Ok(icon_name) = active.proxy.icon_name().await { + active.item.icon_name = icon_name; + } + if let Ok(pixmaps) = active.proxy.icon_pixmap().await { + active.item.pixmap = best_pixmap(&pixmaps).and_then( + |(width, height, data)| pixmap_to_surface(*width, *height, data), + ); + } + } + SignalKind::Status => { + if let Ok(status) = active.proxy.status().await { + active.status = status; + } + } + SignalKind::ToolTip => { + if let Ok(tooltip) = active.proxy.tool_tip().await { + active.item.tooltip = strip_html(&tooltip.3); + } + } + SignalKind::IconThemePath => { + if let Ok(path) = active.proxy.icon_theme_path().await { + active.item.icon_theme_path = path; + } + } + } + update_snapshot(&items, snapshot, wake); + } + } + } +} + +async fn unregister(conn: &Connection, shared: &Arc<Mutex<WatcherShared>>, announced: &str) { + { + let mut state = shared.lock(); + let Some(pos) = state.registered.iter().position(|name| name == announced) else { + return; + }; + state.registered.remove(pos); + } + if let Ok(iface) = conn + .object_server() + .interface::<_, WatcherService>("/StatusNotifierWatcher") + .await + { + let _ = iface.status_notifier_item_unregistered(announced).await; + let guard = iface.get().await; + let _ = guard + .registered_status_notifier_items_changed(iface.signal_emitter()) + .await; + } +} + +async fn spawn_signal_tasks( + conn: &Connection, + proxy: &StatusNotifierItemProxy<'static>, + key: &str, + events: &mpsc::UnboundedSender<Event>, +) -> Vec<zbus::Task<()>> { + let mut tasks = Vec::new(); + macro_rules! forward { + ($stream:expr, $kind:expr) => { + if let Ok(stream) = $stream.await { + let key = key.to_string(); + let events = events.clone(); + tasks.push(conn.executor().spawn( + async move { + futures_lite::pin!(stream); + while stream.next().await.is_some() { + let _ = events.unbounded_send(Event::Signal { + key: key.clone(), + kind: $kind, + }); + } + }, + "tray-signal", + )); + } + }; + } + forward!(proxy.receive_new_title(), SignalKind::Title); + forward!(proxy.receive_new_icon(), SignalKind::Icon); + forward!(proxy.receive_new_status(), SignalKind::Status); + forward!(proxy.receive_new_tool_tip(), SignalKind::ToolTip); + forward!(proxy.receive_new_icon_theme_path(), SignalKind::IconThemePath); + tasks +} + +fn update_snapshot(items: &[ActiveItem], snapshot: &Arc<Mutex<Vec<TrayItem>>>, wake: &OwnedFd) { + let visible: Vec<TrayItem> = items + .iter() + .filter(|item| item.status != "Passive") + .map(|item| item.item.clone()) + .collect(); + *snapshot.lock() = visible; + let result = unsafe { libc::write(wake.as_raw_fd(), [1u8].as_ptr().cast(), 1) }; + if result < 0 { + eprintln!( + "i3bar-mango: tray: wake pipe write failed: {}", + std::io::Error::last_os_error() + ); + } +} + +fn parse_registration(arg: &str, sender: &str) -> (String, String) { + if arg.starts_with('/') { + (sender.to_string(), arg.to_string()) + } else if let Some((name, path)) = arg.split_once('/') { + (name.to_string(), format!("/{path}")) + } else { + (arg.to_string(), "/StatusNotifierItem".to_string()) + } +} + +fn best_pixmap<'a>(pixmaps: &'a [(i32, i32, Vec<u8>)]) -> Option<&'a (i32, i32, Vec<u8>)> { + pixmaps + .iter() + .filter(|(width, height, data)| { + *width > 0 && *height > 0 && data.len() >= (*width * *height * 4) as usize + }) + .min_by_key(|(width, height, _)| { + let size = (*width).max(*height); + if size >= 24 { size } else { i32::MAX - size } + }) +} + +fn pixmap_to_surface(width: i32, height: i32, data: &[u8]) -> Option<cairo::ImageSurface> { + let stride = width * 4; + let mut buffer = vec![0u8; (height * stride) as usize]; + for (src, dst) in data.chunks_exact(4).zip(buffer.chunks_exact_mut(4)) { + let a = u32::from(src[0]); + let r = (u32::from(src[1]) * a + 127) / 255; + let g = (u32::from(src[2]) * a + 127) / 255; + let b = (u32::from(src[3]) * a + 127) / 255; + dst.copy_from_slice(&((a << 24) | (r << 16) | (g << 8) | b).to_ne_bytes()); + } + cairo::ImageSurface::create_for_data(buffer, cairo::Format::ARgb32, width, height, stride).ok() +} + +fn strip_html(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + let mut in_tag = false; + for ch in text.chars() { + match ch { + '<' => in_tag = true, + '>' if in_tag => in_tag = false, + _ if !in_tag => out.push(ch), + _ => {} + } + } + out.trim().to_string() +} diff --git a/src/wm_info_provider.rs b/src/wm_info_provider.rs index 67e5f83..f46c4e0 100644 --- a/src/wm_info_provider.rs +++ b/src/wm_info_provider.rs @@ -62,7 +62,13 @@ pub trait WmInfoProvider: Any { } } +#[cfg_attr(not(feature = "river"), allow(unused_variables))] pub fn bind(conn: &mut Connection<State>, config: &WmConfig) -> Box<dyn WmInfoProvider> { + #[cfg(feature = "mango")] + if let Some(mango) = MangoInfoProvider::new() { + return Box::new(mango); + } + #[cfg(feature = "river")] if let Some(river) = RiverInfoProvider::bind(conn, config) { return Box::new(river); @@ -78,11 +84,6 @@ pub fn bind(conn: &mut Connection<State>, config: &WmConfig) -> Box<dyn WmInfoPr return Box::new(niri); } - #[cfg(feature = "mango")] - if let Some(mango) = MangoInfoProvider::new() { - return Box::new(mango); - } - Box::new(DummyInfoProvider) } |
