### 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