summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bar.rs192
-rw-r--r--src/button_manager.rs8
-rw-r--r--src/config.rs28
-rw-r--r--src/icon.rs28
-rw-r--r--src/main.rs1
-rw-r--r--src/shared_state.rs4
-rw-r--r--src/state.rs37
-rw-r--r--src/tray.rs615
-rw-r--r--src/wm_info_provider.rs11
9 files changed, 914 insertions, 10 deletions
diff --git a/src/bar.rs b/src/bar.rs
index 102978d..84bbecc 100644
--- a/src/bar.rs
+++ b/src/bar.rs
@@ -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)
}