### Linux tray setup: connect to session bus, register StatusNotifierItem and menu paths Source: https://docs.rs/ldtray/latest/src/ldtray/platform/linux/mod.rs.html Initializes a private session-bus connection, requests a well-known StatusNotifierItem name (falling back to the unique name), builds the tray state, registers the item and menu object paths, and installs signal match rules for watcher and notification callbacks. Returns an error if the bus connection or object registration fails. The code relies on libdbus FFI functions and assumes the caller has already initialized the DBusError and state pointer. ```rust // SAFETY: err is a correctly sized DBusError; conn checked for null. let conn = unsafe { (dbus.dbus_error_init)(&mut err); (dbus.dbus_bus_get_private)(DBUS_BUS_SESSION, &mut err) }; if conn.is_null() { let message = err.message(); unsafe { (dbus.dbus_error_free)(&mut err) }; return Err(Error::Backend(format!( "cannot connect to the session bus: {message}" ))); } unsafe { (dbus.dbus_connection_set_exit_on_disconnect)(conn, FALSE) }; // Prefer the conventional well-known name; fall back to the unique name. let pid = std::process::id(); let wk = cstring(&format!("org.kde.StatusNotifierItem-{pid}-1")); let reply = unsafe { (dbus.dbus_bus_request_name)(conn, wk.as_ptr(), DBUS_NAME_FLAG_DO_NOT_QUEUE, &mut err) }; let service = if reply == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER || reply == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER { wk } else { let unique = unsafe { (dbus.dbus_bus_get_unique_name)(conn) }; if unique.is_null() { wk } else { unsafe { CStr::from_ptr(unique) }.to_owned() } }; unsafe { (dbus.dbus_error_free)(&mut err) }; let id = std::env::current_exe() .ok() .and_then(|p| p.file_name().map(|s| s.to_string_lossy().into_owned())) .filter(|s| !s.is_empty()) .unwrap_or_else(|| "ldtray".to_string()); let title = if init.tooltip.is_empty() { id.clone() } else { init.tooltip.clone() }; let (icon_w, icon_h, icon_argb) = rgba_to_argb(&init.icon); let menu_model = menu::DbusMenu::build(init.menu.as_ref(), 1); let mut state = Box::new(State { dbus, conn, service, id: cstring(&id), title: cstring(&title), status: cstring("Active"), category: cstring("ApplicationStatus"), menu_path: cstring("/MenuBar"), icon_w, icon_h, icon_argb, menu_model, notify_ids: HashSet::new(), pending: Vec::new(), }); let state_ptr = (&mut *state as *mut State) as *mut c_void; // Register the item and the dbusmenu object. libdbus copies each vtable, // so locals are fine. let item_vtable = object_vtable(item_message_handler); let menu_vtable = object_vtable(menu_message_handler); let registered = unsafe { let item_ok = (state.dbus.dbus_connection_try_register_object_path)( conn, ITEM_PATH.as_ptr(), &item_vtable, state_ptr, &mut err, ); if item_ok == FALSE { FALSE } else { (state.dbus.dbus_connection_try_register_object_path)( conn, MENU_PATH.as_ptr(), &menu_vtable, state_ptr, &mut err, ) } }; if registered == FALSE { let message = err.message(); unsafe { (state.dbus.dbus_error_free)(&mut err) }; // `state` drops here, closing the connection. return Err(Error::Backend(format!( "failed to export tray objects: {message}" ))); } // Notice a watcher that starts *after* us (daemon booted before the // panel), and receive notification action callbacks. let watcher_rule = c"type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.kde.StatusNotifierWatcher'"; let action_rule = c"type='signal',interface='org.freedesktop.Notifications',member='ActionInvoked'"; let closed_rule = c"type='signal',interface='org.freedesktop.Notifications',member='NotificationClosed'"; unsafe { for rule in [watcher_rule, action_rule, closed_rule] { (state.dbus.dbus_bus_add_match)(conn, rule.as_ptr(), &mut err); (state.dbus.dbus_error_free)(&mut err); } (state.dbus.dbus_connection_add_filter)(conn, signal_filter, state_ptr, None); } // Best-effort registration with whatever host is already present. unsafe { state.register_with_watcher() }; ``` -------------------------------- ### ldtray usage example Source: https://docs.rs/ldtray/latest/src/ldtray/lib.rs.html Shows how to create a tray icon with a menu, handle menu events, and gracefully handle the case where no tray is available. The example uses Icon::from_rgba, Menu::new, Tray::new, and TrayHandle methods. ```rust use ldtray::{Tray, TrayConfig, Icon, Menu, MenuItem, Event, Notification}; # fn main() -> ldtray::Result<()> { let icon = Icon::from_rgba(1, 1, vec![255, 0, 0, 255])?; let menu = Menu::new() .item(MenuItem::button(1, "Say hi")) .item(MenuItem::separator()) .item(MenuItem::button(2, "Quit")); let tray = match Tray::new(TrayConfig::new(icon).tooltip("demo").menu(menu)) { Ok(tray) => tray, Err(err) => { eprintln!("no tray available ({err}); continuing headless"); return Ok(()); } }; let handle = tray.handle(); tray.run(move |event| match event { Event::Menu(id) if id.0 == 1 => { let _ = handle.notify(Notification::new("demo", "hi there")); } Event::Menu(id) if id.0 == 2 => { let _ = handle.quit(); } other => println!("event: {other:?}"), }) # } ``` -------------------------------- ### Menu::new() example from examples/basic.rs Source: https://docs.rs/ldtray/latest/ldtray/struct.Menu.html Shows creating a menu, tray configuration, error handling for headless servers, notification actions, and running the event loop with menu and quit handling. ```rust 14fn main() { 15 // A 16x16 solid red icon. 16 let side = 16u32; 17 let mut rgba = Vec::with_capacity((side * side * 4) as usize); 18 for _ in 0..side * side { 19 rgba.extend_from_slice(&[220, 40, 40, 255]); 20 } 21 let icon = Icon::from_rgba(side, side, rgba).expect("valid icon"); 22 let notify_icon = icon.clone(); 23 24 let menu = Menu::new() 25 .item(MenuItem::button(SAY_HI, "Say hi")) 26 .item(MenuItem::checkbox(TOGGLE, "Toggle me", false)) 27 .item(MenuItem::separator()) 28 .item(MenuItem::button(QUIT, "Quit")); 29 30 let config = TrayConfig::new(icon).tooltip("ldtray example").menu(menu); 31 32 let tray = match Tray::new(config) { 33 Ok(tray) => tray, 34 Err(err) => { 35 eprintln!("tray unavailable: {err}"); 36 eprintln!("(this is expected on a headless server — exiting cleanly)"); 37 return; 38 } 39 }; 40 41 let handle = tray.handle(); 42 println!("tray is up — right-click it for the menu, or Ctrl-C to abort"); 43 44 let result = tray.run(move |event| { 45 println!("event: {event:?}"); 46 match event { 47 Event::Menu(id) if id.0 == SAY_HI => { 48 // Actions are shown on Linux; ignored (message still shown) 49 // elsewhere. 50 let _ = handle.notify( 51 Notification::new("ldtray", "Hello from the tray!") 52 .with_icon(notify_icon.clone()) 53 .action(100, "Wave back"), 54 ); 55 } 56 Event::NotificationAction(action) => { 57 println!("notification action clicked: {}", action.0); 58 } 59 Event::Menu(id) if id.0 == QUIT => { 60 let _ = handle.quit(); 61 } 62 _ => {} 63 } 64 }); 65 66 if let Err(err) = result { 67 eprintln!("event loop ended with error: {err}"); 68 } 69} ``` -------------------------------- ### Reply to GetLayout (start) Source: https://docs.rs/ldtray/latest/src/ldtray/platform/linux/mod.rs.html Starts the reply for GetLayout. Reads the parent and depth arguments from the incoming call, defaulting parent to 0. ```rust unsafe fn reply_get_layout(&self, call: *mut DBusMessage) -> c_int { let (parent, depth) = unsafe { let mut it = msg::iter_init(&self.dbus, call); let parent = msg::read_i32(&self.dbus, &mut it).unwrap_or(0); msg::advance(&self.dbus, &mut it); ``` -------------------------------- ### Menu::new() example from examples/smoke.rs Source: https://docs.rs/ldtray/latest/ldtray/struct.Menu.html Shows building a menu with items, checkbox, separator, submenu, and quit button using `Menu::new()`. ```rust 26fn demo_menu() -> Menu { 27 Menu::new() 28 .item(MenuItem::button(1, "One")) 29 .item(MenuItem::checkbox(2, "Toggle", true)) 30 .item(MenuItem::separator()) 31 .item(MenuItem::submenu("More", [MenuItem::button(3, "Nested")])) 32 .item(MenuItem::button(9, "Quit")) 33} ``` -------------------------------- ### Minimal end-to-end ldtray example with red icon and menu Source: https://docs.rs/ldtray/latest/src/basic/basic.rs.html Minimal end-to-end example for the ldtray crate. Creates a 16x16 red icon, builds a menu with a button, checkbox, separator, and quit item, and runs the tray event loop. If no tray is available (headless server, missing libraries) it prints why and exits 0, demonstrating graceful degradation. The example prints every event, shows a notification with an action when "Say hi" is clicked, and exits on "Quit". ```rust 1//! Minimal end-to-end example. 2//! 3//! Shows a red tray icon with a small menu, prints every event, pops a 4//! notification when "Say hi" is clicked, and exits on "Quit". If no tray is 5//! available (headless server, missing libraries) it prints why and exits 0 — 6//! demonstrating the graceful-degradation contract. 7 8use ldtray::{Event, Icon, Menu, MenuItem, Notification, Tray, TrayConfig}; 9 10const SAY_HI: u32 = 1; 11const TOGGLE: u32 = 2; 12const QUIT: u32 = 3; 13 14fn main() { 15 // A 16x16 solid red icon. 16 let side = 16u32; 17 let mut rgba = Vec::with_capacity((side * side * 4) as usize); 18 for _ in 0..side * side { 19 rgba.extend_from_slice(&[220, 40, 40, 255]); 20 } 21 let icon = Icon::from_rgba(side, side, rgba).expect("valid icon"); 22 let notify_icon = icon.clone(); 23 24 let menu = Menu::new() 25 .item(MenuItem::button(SAY_HI, "Say hi")) 26 .item(MenuItem::checkbox(TOGGLE, "Toggle me", false)) 27 .item(MenuItem::separator()) 28 .item(MenuItem::button(QUIT, "Quit")); 29 30 let config = TrayConfig::new(icon).tooltip("ldtray example").menu(menu); 31 32 let tray = match Tray::new(config) { 33 Ok(tray) => tray, 34 Err(err) => { 35 eprintln!("tray unavailable: {err}"); 36 eprintln!("(this is expected on a headless server — exiting cleanly)"); 37 return; 38 } 39 }; 40 41 let handle = tray.handle(); 42 println!("tray is up — right-click it for the menu, or Ctrl-C to abort"); 43 44 let result = tray.run(move |event| { 45 println!("event: {event:?}"); 46 match event { 47 Event::Menu(id) if id.0 == SAY_HI => { 48 // Actions are shown on Linux; ignored (message still shown) 49 // elsewhere. 50 let _ = handle.notify( 51 Notification::new("ldtray", "Hello from the tray!") 52 .with_icon(notify_icon.clone()) 53 .action(100, "Wave back"), 54 ); 55 } 56 Event::NotificationAction(action) => { 57 println!("notification action clicked: {}", action.0); 58 } 59 Event::Menu(id) if id.0 == QUIT => { 60 let _ = handle.quit(); 61 } 62 _ => {} 63 } 64 }); 65 66 if let Err(err) = result { 67 eprintln!("event loop ended with error: {err}"); 68 } 69} ``` -------------------------------- ### Example calling a_crate::a_func() in examples/ex.rs Source: https://docs.rs/ldtray/latest/scrape-examples-help.html Example of a separate file that calls the public function. This code will be scraped and included in the documentation for a_func. ```rust // examples/ex.rs fn main() { a_crate::a_func(); } ``` -------------------------------- ### Smoke test using set_tooltip Source: https://docs.rs/ldtray/latest/ldtray/struct.TrayHandle.html Example from examples/smoke.rs (line 66) showing set_tooltip usage. It updates the tooltip from a spawned thread and then continues with other mutating methods. The example also demonstrates handling headless environments and printing events. ```rust 35fn main() { 36 // Hard stop so a hang shows up as a CI failure rather than a 6-hour job. 37 thread::spawn(|| { 38 thread::sleep(Duration::from_secs(20)); 39 eprintln!("SMOKE_TIMEOUT: event loop did not finish"); 40 std::process::exit(3); 41 }); 42 43 let config = TrayConfig::new(demo_icon()) 44 .tooltip("ldtray smoke") 45 .menu(demo_menu()); 46 47 let tray = match Tray::new(config) { 48 Ok(tray) => tray, 49 Err(err) => { 50 // Expected on a headless box (e.g. Linux CI: no session bus). On a 51 // GUI OS the platform libraries always exist, so treat a failure 52 // there as a real regression. 53 if cfg!(any(target_os = "windows", target_os = "macos")) { 54 eprintln!("SMOKE_FAIL: Tray::new failed on a GUI OS: {err}"); 55 std::process::exit(1); 56 } 57 eprintln!("SMOKE_SKIP: no tray in this environment: {err}"); 58 return; 59 } 60 }; 61 62 // Drive every mutating method from another thread, then stop the loop. 63 let handle = tray.handle(); 64 let driver = thread::spawn(move || { 65 thread::sleep(Duration::from_millis(400)); 66 let _ = handle.set_tooltip("updated tooltip"); 67 let _ = handle.set_icon(demo_icon()); 68 let _ = handle.set_menu(demo_menu()); 69 let _ = handle.notify(Notification::new("ldtray", "smoke").with_icon(demo_icon())); 70 thread::sleep(Duration::from_millis(400)); 71 let _ = handle.quit(); 72 }); 73 let mut events = 0usize; let result = tray.run(|event: Event| { events += 1; println!("event: {event:?}"); }); let _ = driver.join(); match result { Ok(()) => { println!("SMOKE_OK: ran cleanly ({events} events)"); } Err(err) => { eprintln!("SMOKE_FAIL: event loop ended with error: {err}"); std::process::exit(2); } } } ``` -------------------------------- ### Tray::run Source: https://docs.rs/ldtray Starts the tray event loop, delivering events to the provided callback. Blocks the current thread. ```APIDOC ## Tray::run ### Description Starts the tray event loop, delivering events to the provided callback. Blocks the current thread. ### Method Method ### Parameters - **callback** (FnMut(Event)) - Required - Callback invoked for each user event. ### Returns - **Result<()>** - Result indicating success or failure. ### Example ```rust tray.run(move |event| match event { Event::Menu(id) if id.0 == 1 => { /* ... */ } _ => {} }) ``` ``` -------------------------------- ### Smoke test TrayConfig with background driver Source: https://docs.rs/ldtray/latest/ldtray/struct.TrayConfig.html A smoke test example showing TrayConfig initialization and using a background thread to drive tray updates and quit the event loop. ```rust fn main() { // Hard stop so a hang shows up as a CI failure rather than a 6-hour job. thread::spawn(|| { thread::sleep(Duration::from_secs(20)); eprintln!("SMOKE_TIMEOUT: event loop did not finish"); std::process::exit(3); }); let config = TrayConfig::new(demo_icon()) .tooltip("ldtray smoke") .menu(demo_menu()); let tray = match Tray::new(config) { Ok(tray) => tray, Err(err) => { // Expected on a headless box (e.g. Linux CI: no session bus). On a // GUI OS the platform libraries always exist, so treat a failure // there as a real regression. if cfg!(any(target_os = "windows", target_os = "macos")) { eprintln!("SMOKE_FAIL: Tray::new failed on a GUI OS: {err}"); std::process::exit(1); } eprintln!("SMOKE_SKIP: no tray in this environment: {err}"); return; } }; // Drive every mutating method from another thread, then stop the loop. let handle = tray.handle(); let driver = thread::spawn(move || { thread::sleep(Duration::from_millis(400)); let _ = handle.set_tooltip("updated tooltip"); let _ = handle.set_icon(demo_icon()); let _ = handle.set_menu(demo_menu()); let _ = handle.notify(Notification::new("ldtray", "smoke").with_icon(demo_icon())); thread::sleep(Duration::from_millis(400)); let _ = handle.quit(); }); let mut events = 0usize; let result = tray.run(|event: Event| { events += 1; println!("event: {event:?}"); }); let _ = driver.join(); match result { Ok(()) => { println!("SMOKE_OK: ran cleanly ({events} events)"); } Err(err) => { eprintln!("SMOKE_FAIL: event loop ended with error: {err}"); std::process::exit(2); } } } ``` -------------------------------- ### Tray::spawn Source: https://docs.rs/ldtray Starts the tray event loop in a background thread, returning immediately. Events are delivered to the callback. ```APIDOC ## Tray::spawn ### Description Starts the tray event loop in a background thread, returning immediately. Events are delivered to the callback. ### Method Method ### Parameters - **callback** (FnMut(Event)) - Required - Callback invoked for each user event. ### Returns - **Result<()>** - Result indicating success or failure. ### Example ```rust tray.spawn(move |event| { /* ... */ }) ``` ``` -------------------------------- ### Define TrayConfig, Command, Tray, and event_loop in tray.rs Source: https://docs.rs/ldtray/latest/src/ldtray/tray.rs.html Defines the TrayConfig builder, the Command enum, the Tray struct, and the start of the event_loop function. Tray::new loads platform libraries and may return Error::Unsupported or Error::LibraryLoad; Tray::run blocks on the calling thread, while Tray::spawn runs on a background thread and returns Error::Unsupported on macOS. ```rust use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; use std::time::Duration; use crate::error::{Error, Result}; use crate::event::Event; use crate::icon::Icon; use crate::menu::Menu; use crate::notification::Notification; use crate::platform::{self, Backend, Init}; /// How long the event loop blocks in the platform pump between checks of the /// command channel. Bounds the latency of [`TrayHandle`] updates. const PUMP_INTERVAL: Duration = Duration::from_millis(100); /// Initial configuration for a [`Tray`]. #[derive(Clone, Debug)] pub struct TrayConfig { icon: Icon, tooltip: String, menu: Option, } impl TrayConfig { /// Start a configuration with the required tray icon. pub fn new(icon: Icon) -> TrayConfig { TrayConfig { icon, tooltip: String::new(), menu: None, } } /// Builder: set the hover tooltip text. pub fn tooltip(mut self, tooltip: impl Into) -> TrayConfig { self.tooltip = tooltip.into(); self } /// Builder: set the context menu. pub fn menu(mut self, menu: Menu) -> TrayConfig { self.menu = Some(menu); self } } /// Commands sent from a [`TrayHandle`] to the running event loop. enum Command { SetIcon(Icon), SetTooltip(String), SetMenu(Option), Notify(Notification), Quit, } /// A live tray icon. /// /// Construct it with [`Tray::new`] (which loads the platform libraries and may /// fail gracefully), then hand control to the event loop with either /// [`Tray::run`] (blocking, current thread) or [`Tray::spawn`] (background /// thread). Obtain [`TrayHandle`]s with [`Tray::handle`] to mutate the tray from /// any thread. pub struct Tray { backend: Box, tx: Sender, rx: Receiver, } impl Tray { /// Creates the tray, loading the platform GUI libraries at runtime. /// /// Returns [`Error::Unsupported`] or [`Error::LibraryLoad`] when no tray is /// available (headless server, missing libraries, unknown OS). Daemons /// should treat these as non-fatal and continue without a tray. pub fn new(config: TrayConfig) -> Result { let backend = platform::new_backend(Init { icon: config.icon, tooltip: config.tooltip, menu: config.menu, })?; let (tx, rx) = mpsc::channel(); Ok(Tray { backend, tx, rx }) } /// Returns a cloneable, `Send + Sync` handle for updating the tray from any /// thread while the event loop runs. pub fn handle(&self) -> TrayHandle { TrayHandle { tx: self.tx.clone(), } } /// Runs the event loop on the calling thread, blocking until /// [`TrayHandle::quit`] is called (or the backend closes). /// /// This is the portable path: it is correct on every platform, including /// macOS, where tray work must happen on the main thread. pub fn run(self, mut callback: impl FnMut(Event)) -> Result<()> { let Tray { mut backend, tx: _keep_alive, rx, } = self; event_loop(backend.as_mut(), &rx, &mut callback) } /// Runs the event loop on a newly spawned background thread and returns a /// handle to control it. /// /// Not available on macOS (tray work must run on the main thread there); on /// that platform this returns [`Error::Unsupported`] — use [`Tray::run`] /// from the main thread instead. pub fn spawn(self, mut callback: impl FnMut(Event) + Send + 'static) -> Result { if !self.backend.can_spawn() { return Err(Error::Unsupported); } let handle = self.handle(); std::thread::Builder::new() .name("ldtray".into()) .spawn(move || { let Tray { mut backend, tx: _keep_alive, rx, } = self; let _ = event_loop(backend.as_mut(), &rx, &mut callback); }) .map_err(|e| Error::Backend(format!("failed to spawn tray thread: {e}")))?; Ok(handle) } } /// The core loop: drain any queued commands, then pump platform events for a /// bounded interval, forwarding them to `callback`. Returns when a /// [`Command::Quit`] is received. fn event_loop( backend: &mut dyn Backend, rx: &Receiver, callback: &mut dyn FnMut(Event), ) -> Result<()> { loop { ``` -------------------------------- ### Public function in src/lib.rs Source: https://docs.rs/ldtray/latest/scrape-examples-help.html Example of a public function that can be scraped by Rustdoc. This is the item that will be documented and for which examples will be searched. ```rust // src/lib.rs pub fn a_func() {} ``` -------------------------------- ### Smoke test using TrayHandle methods Source: https://docs.rs/ldtray/latest/ldtray/struct.TrayHandle.html Example from examples/smoke.rs (line 67) showing how to use TrayHandle methods from another thread. It creates a Tray, spawns a driver thread that calls set_tooltip, set_icon, set_menu, notify, and quit, then runs the event loop and prints events. On headless environments (Linux CI) it skips; on GUI OSes a failure is treated as a regression. ```rust 35fn main() { 36 // Hard stop so a hang shows up as a CI failure rather than a 6-hour job. 37 thread::spawn(|| { 38 thread::sleep(Duration::from_secs(20)); 39 eprintln!("SMOKE_TIMEOUT: event loop did not finish"); 40 std::process::exit(3); 41 }); 42 43 let config = TrayConfig::new(demo_icon()) 44 .tooltip("ldtray smoke") 45 .menu(demo_menu()); 46 47 let tray = match Tray::new(config) { 48 Ok(tray) => tray, 49 Err(err) => { 50 // Expected on a headless box (e.g. Linux CI: no session bus). On a 51 // GUI OS the platform libraries always exist, so treat a failure 52 // there as a real regression. 53 if cfg!(any(target_os = "windows", target_os = "macos")) { 54 eprintln!("SMOKE_FAIL: Tray::new failed on a GUI OS: {err}"); 55 std::process::exit(1); 56 } 57 eprintln!("SMOKE_SKIP: no tray in this environment: {err}"); 58 return; 59 } 60 }; 61 62 // Drive every mutating method from another thread, then stop the loop. 63 let handle = tray.handle(); 64 let driver = thread::spawn(move || { 65 thread::sleep(Duration::from_millis(400)); 66 let _ = handle.set_tooltip("updated tooltip"); 67 let _ = handle.set_icon(demo_icon()); 68 let _ = handle.set_menu(demo_menu()); 69 let _ = handle.notify(Notification::new("ldtray", "smoke").with_icon(demo_icon())); 70 thread::sleep(Duration::from_millis(400)); 71 let _ = handle.quit(); 72 }); 73 74 let mut events = 0usize; 75 let result = tray.run(|event: Event| { 76 events += 1; 77 println!("event: {event:?}"); 78 }); 79 let _ = driver.join(); 80 81 match result { 82 Ok(()) => { 83 println!("SMOKE_OK: ran cleanly ({events} events)"); 84 } 85 Err(err) => { 86 eprintln!("SMOKE_FAIL: event loop ended with error: {err}"); 87 std::process::exit(2); 88 } 89 } 90} ``` -------------------------------- ### Send desktop notification via org.freedesktop.Notifications.Notify Source: https://docs.rs/ldtray/latest/src/ldtray/platform/linux/notify.rs.html Implements the `org.freedesktop.Notifications.Notify` method call. It builds a DBus message with app name, replaces_id=0, empty app_icon, summary, body, actions array, and optional image-data hint. For notifications without actions it sends fire-and-forget; with actions it blocks up to 2000 ms to get the returned id. The id is 0 if no daemon replies. ```rust 1//! Desktop notifications via `org.freedesktop.Notifications.Notify`. 2//! 3//! A method call to the session notification daemon. An attached icon is passed 4//! inline through the `image-data` hint (raw RGBA, which is exactly our 5//! [`Icon`](crate::Icon) format — no conversion needed, unlike the SNI pixmap). 6//! Action buttons are sent in the `actions` array with the [`ActionId`] as the 7//! key, so the daemon's `ActionInvoked` signal maps straight back. 8//! 9//! [`ActionId`]: crate::ActionId 10 11use std::ffi::CStr; 12use std::os::raw::{c_int, c_void}; 13 14use super::dbus::*; 15use super::{cstring, msg}; 16use crate::error::{Error, Result}; use crate::notification::Notification; const NOTIFY_NAME: &CStr = c"org.freedesktop.Notifications"; const NOTIFY_PATH: &CStr = c"/org/freedesktop/Notifications"; /// Sends `notification` to the notification daemon and returns the id the daemon /// assigned it (0 if there is no daemon or it sent no reply). The id lets the /// caller route `ActionInvoked` signals back to this notification. Best effort: /// if no daemon is present the call is simply dropped by the bus. pub(super) unsafe fn send( d: &DBus, conn: *mut DBusConnection, app_name: &CStr, notification: &Notification, ) -> Result { let summary = cstring(¬ification.title); let body = cstring(¬ification.body); unsafe { let msg = (d.dbus_message_new_method_call)( NOTIFY_NAME.as_ptr(), NOTIFY_PATH.as_ptr(), NOTIFY_NAME.as_ptr(), c"Notify".as_ptr(), ); if msg.is_null() { return Err(Error::Backend("failed to build Notify message".into())); } let mut it = DBusMessageIter::uninit(); (d.dbus_message_iter_init_append)(msg, &mut it); msg::append_str(d, &mut it, app_name); // app_name msg::append_u32(d, &mut it, 0); // replaces_id (0 = new) msg::append_str(d, &mut it, c""); // app_icon (we use image-data instead) msg::append_str(d, &mut it, &summary); // summary msg::append_str(d, &mut it, &body); // body // actions: `as` of alternating (key, label). The key is the ActionId as // a decimal string, so ActionInvoked maps straight back to it. let mut actions = DBusMessageIter::uninit(); (d.dbus_message_iter_open_container)(&mut it, DBUS_TYPE_ARRAY, c"s".as_ptr(), &mut actions); for (id, label) in ¬ification.actions { let key = cstring(&id.0.to_string()); let label = cstring(label); msg::append_str(d, &mut actions, &key); msg::append_str(d, &mut actions, &label); } (d.dbus_message_iter_close_container)(&mut it, &mut actions); // hints: `a{sv}`, optionally carrying the inline image. let mut hints = DBusMessageIter::uninit(); (d.dbus_message_iter_open_container)( &mut it, DBUS_TYPE_ARRAY, c"{sv}".as_ptr(), &mut hints, ); if let Some(icon) = ¬ification.icon { append_image_data( d, &mut hints, icon.width as i32, icon.height as i32, &icon.rgba, ); } (d.dbus_message_iter_close_container)(&mut it, &mut hints); msg::append_i32(d, &mut it, -1); // expire_timeout (-1 = default) // With actions we need the returned id to route callbacks, so we make a // blocking call; plain notifications stay fire-and-forget (cheaper). if notification.actions.is_empty() { (d.dbus_connection_send)(conn, msg, std::ptr::null_mut()); (d.dbus_message_unref)(msg); (d.dbus_connection_flush)(conn); Ok(0) } else { let mut err = DBusError::zeroed(); (d.dbus_error_init)(&mut err); let reply = (d.dbus_connection_send_with_reply_and_block)(conn, msg, 2000, &mut err); (d.dbus_message_unref)(msg); let mut id = 0u32; if !reply.is_null() { let mut rit = DBusMessageIter::uninit(); if (d.dbus_message_iter_init)(reply, &mut rit) == TRUE { if let Some(v) = msg::read_u32(d, &mut rit) { id = v; } } (d.dbus_message_unref)(reply); } (d.dbus_error_free)(&mut err); Ok(id) } } } /// Appends the `image-data` hint: `{ "image-data": (iiibiiay) }`, where the ``` -------------------------------- ### Create a solid red icon and set up tray (examples/basic.rs) Source: https://docs.rs/ldtray/latest/ldtray/struct.Icon.html Creates a 16x16 solid red icon, then builds a tray with a menu and runs the event loop. Handles menu events and notifications. ```rust fn main() { // A 16x16 solid red icon. let side = 16u32; let mut rgba = Vec::with_capacity((side * side * 4) as usize); for _ in 0..side * side { rgba.extend_from_slice(&[220, 40, 40, 255]); } let icon = Icon::from_rgba(side, side, rgba).expect("valid icon"); let notify_icon = icon.clone(); let menu = Menu::new() .item(MenuItem::button(SAY_HI, "Say hi")) .item(MenuItem::checkbox(TOGGLE, "Toggle me", false)) .item(MenuItem::separator()) .item(MenuItem::button(QUIT, "Quit")); let config = TrayConfig::new(icon).tooltip("ldtray example").menu(menu); let tray = match Tray::new(config) { Ok(tray) => tray, Err(err) => { eprintln!("tray unavailable: {err}"); eprintln!("(this is expected on a headless server — exiting cleanly)"); return; } }; let handle = tray.handle(); println!("tray is up — right-click it for the menu, or Ctrl-C to abort"); let result = tray.run(move |event| { println!("event: {event:?}"); match event { Event::Menu(id) if id.0 == SAY_HI => { // Actions are shown on Linux; ignored (message still shown) // elsewhere. let _ = handle.notify( Notification::new("ldtray", "Hello from the tray!") .with_icon(notify_icon.clone()) .action(100, "Wave back"), ); } Event::NotificationAction(action) => { println!("notification action clicked: {}", action.0); } Event::Menu(id) if id.0 == QUIT => { let _ = handle.quit(); } _ => {} } }); if let Err(err) = result { eprintln!("event loop ended with error: {err}"); } } ``` -------------------------------- ### Define demo menu Source: https://docs.rs/ldtray/latest/src/smoke/smoke.rs.html Constructs a sample menu with buttons, a checkbox, a separator, and a submenu. ```rust fn demo_menu() -> Menu { Menu::new() .item(MenuItem::button(1, "One")) .item(MenuItem::checkbox(2, "Toggle", true)) .item(MenuItem::separator()) .item(MenuItem::submenu("More", [MenuItem::button(3, "Nested")])) .item(MenuItem::button(9, "Quit")) } ``` -------------------------------- ### Handle D-Bus Get method call Source: https://docs.rs/ldtray/latest/src/ldtray/platform/linux/mod.rs.html Handles a D-Bus Get method call by reading the interface and property name, then returning the property value as a variant. Sends an InvalidArgs error if fewer than two strings are provided, and returns NEED_MEMORY if the reply allocation fails. ```rust let args = unsafe { msg::read_leading_strings(&self.dbus, call, 2) }; if args.len() < 2 { return unsafe { self.send_error( call, c"org.freedesktop.DBus.Error.InvalidArgs", c"expected interface and property", ) }; } match self.item_property(&args[1]) { Some(value) => unsafe { let reply = (self.dbus.dbus_message_new_method_return)(call); if reply.is_null() { return DBUS_HANDLER_RESULT_NEED_MEMORY; } let mut it = DBusMessageIter::uninit(); (self.dbus.dbus_message_iter_init_append)(reply, &mut it); msg::append_variant(&self.dbus, &mut it, &value); self.send(reply); DBUS_HANDLER_RESULT_HANDLED }, None => unsafe { self.send_error( ``` -------------------------------- ### Create and run a ldtray tray icon with menu and event handling Source: https://docs.rs/ldtray/latest/index.html Shows the full ldtray workflow: build an icon and menu, create a tray with Tray::new, handle the headless error by returning Ok, then run the event loop and respond to menu clicks with notifications or quit. ```rust use ldtray::{Tray, TrayConfig, Icon, Menu, MenuItem, Event, Notification}; let icon = Icon::from_rgba(1, 1, vec![255, 0, 0, 255])?; let menu = Menu::new() .item(MenuItem::button(1, "Say hi")) .item(MenuItem::separator()) .item(MenuItem::button(2, "Quit")); let tray = match Tray::new(TrayConfig::new(icon).tooltip("demo").menu(menu)) { Ok(tray) => tray, Err(err) => { eprintln!("no tray available ({err}); continuing headless"); return Ok(()); } }; let handle = tray.handle(); tray.run(move |event| match event { Event::Menu(id) if id.0 == 1 => { let _ = handle.notify(Notification::new("demo", "hi there")); } Event::Menu(id) if id.0 == 2 => { let _ = handle.quit(); } other => println!("event: {other:?}"), }) ``` -------------------------------- ### Tray::new Source: https://docs.rs/ldtray Creates a new tray icon with the given configuration. Returns an error if the platform tray library cannot be loaded. ```APIDOC ## Tray::new ### Description Creates a new tray icon with the given configuration. Returns an error if the platform tray library cannot be loaded. ### Method Constructor ### Parameters - **config** (TrayConfig) - Required - Initial configuration for the tray. ### Returns - **Result** - A live tray icon or an error. ### Example ```rust let tray = Tray::new(TrayConfig::new(icon).tooltip("demo").menu(menu))?; ``` ``` -------------------------------- ### Backend implementation for LinuxBackend (set_icon, set_tooltip, set_menu) Source: https://docs.rs/ldtray/latest/src/ldtray/platform/linux/mod.rs.html The `Backend` implementation for `LinuxBackend` updates the tray state and emits D-Bus signals. `set_icon` converts the icon to ARGB and emits `NewIcon`; `set_tooltip` updates the title and emits `NewToolTip` and `NewTitle`; `set_menu` rebuilds the D-Bus menu model with an incremented revision. ```rust impl Backend for LinuxBackend { fn set_icon(&mut self, icon: &Icon) -> Result<()> { let (w, h, argb) = rgba_to_argb(icon); self.state.icon_w = w; self.state.icon_h = h; self.state.icon_argb = argb; unsafe { self.state.emit(c"NewIcon") }; Ok(()) } fn set_tooltip(&mut self, text: &str) -> Result<()> { self.state.title = cstring(text); unsafe { self.state.emit(c"NewToolTip"); self.state.emit(c"NewTitle"); } Ok(()) } fn set_menu(&mut self, menu: Option<&Menu>) -> Result<()> { let revision = self.state.menu_model.revision.wrapping_add(1); self.state.menu_model = menu::DbusMenu::build(menu, revision); } } ``` -------------------------------- ### TrayConfig::new Source: https://docs.rs/ldtray Creates a new tray configuration with the given icon. ```APIDOC ## TrayConfig::new ### Description Creates a new tray configuration with the given icon. ### Method Constructor ### Parameters - **icon** (Icon) - Required - The icon to display. ### Returns - **TrayConfig** - The created configuration. ### Example ```rust let config = TrayConfig::new(icon); ``` ``` -------------------------------- ### Create a solid blue icon (examples/smoke.rs) Source: https://docs.rs/ldtray/latest/ldtray/struct.Icon.html Builds a 16x16 solid blue icon using Icon::from_rgba. The rgba vector is filled with the color [10, 120, 220, 255] for each pixel. ```rust fn demo_icon() -> Icon { let side = 16u32; let mut rgba = Vec::with_capacity((side * side * 4) as usize); for _ in 0..side * side { rgba.extend_from_slice(&[10, 120, 220, 255]); } Icon::from_rgba(side, side, rgba).expect("valid icon") } ``` -------------------------------- ### pub fn menu(self, menu: Menu) -> TrayConfig Source: https://docs.rs/ldtray/latest/ldtray/struct.TrayConfig.html Sets the context menu for the tray icon. This builder method consumes the `TrayConfig` and returns a new `TrayConfig` with the specified menu attached. ```APIDOC ## pub fn menu(self, menu: Menu) -> TrayConfig ### Description Sets the context menu for the tray icon. This builder method consumes the `TrayConfig` and returns a new `TrayConfig` with the specified menu attached. ### Method Builder method (Rust) ### Signature `pub fn menu(self, menu: Menu) -> TrayConfig` ### Parameters - **menu** (`Menu`) - Required - The context menu to set for the tray icon. ### Returns - `TrayConfig` - The updated `TrayConfig` with the menu set. ### Example ```rust let menu = Menu::new() .item(MenuItem::button(SAY_HI, "Say hi")) .item(MenuItem::checkbox(TOGGLE, "Toggle me", false)) .item(MenuItem::separator()) .item(MenuItem::button(QUIT, "Quit")); let config = TrayConfig::new(icon).tooltip("ldtray example").menu(menu); ``` ``` -------------------------------- ### Get dbusmenu property values Source: https://docs.rs/ldtray/latest/src/ldtray/platform/linux/mod.rs.html Returns the dbusmenu object's own properties: Version (3), Status ("normal"), and TextDirection ("ltr"). Returns None for unknown property names. ```rust fn menu_property(&self, name: &str) -> Option> { use msg::Variant; Some(match name { "Version" => Variant::UInt32(3), "Status" => Variant::Str(c"normal"), "TextDirection" => Variant::Str(c"ltr"), _ => return None, }) } ``` -------------------------------- ### set_menu Source: https://docs.rs/ldtray/latest/ldtray/struct.TrayHandle.html Replaces the context menu with a new menu. ```APIDOC ## set_menu ### Description Replaces the context menu with a new menu. ### Method N/A (Rust method) ### Endpoint N/A (Rust method) ### Parameters - **menu** (Menu) - Required - The new menu to set as the context menu. ### Request Example N/A ### Response #### Success Response - Returns `Result<()>` - Ok on success, Err on failure. #### Response Example N/A ``` -------------------------------- ### TrayHandle::set_menu Source: https://docs.rs/ldtray/latest/src/ldtray/tray.rs.html Replaces the context menu. Sends a SetMenu command with the provided menu to the event loop. ```APIDOC ## TrayHandle::set_menu ### Description Replaces the context menu with the provided menu. ### Method pub fn set_menu(&self, menu: Menu) -> Result<()> ### Parameters - **menu** (Menu) - Required - The new context menu. ### Returns - **Result<()>** - Ok(()) on success, or Error::Disconnected if the event loop has stopped. ``` -------------------------------- ### Menu::new Source: https://docs.rs/ldtray Creates an empty menu. ```APIDOC ## Menu::new ### Description Creates an empty menu. ### Method Constructor ### Returns - **Menu** - An empty menu. ### Example ```rust let menu = Menu::new(); ``` ```