A headless system tray host (StatusNotifierItem daemon) designed to work with external launchers like fuzzel, rofi, or dmenu — inspired by the cliphist + fuzzel architecture.
| Project | Role |
|---|---|
| tray-tui | Original TUI system tray — stripped down to become this project |
| system-tray | D-Bus StatusNotifierItem backend that does all the heavy lifting |
| cliphist | Architectural inspiration: daemon + socket + picker |
This project is a complete rewrite of tray-tui — all TUI/frontend code removed, replaced with a Unix socket daemon + CLI designed for composability with fuzzy pickers.
D-Bus Session Bus
│ Apps (Discord, Dropbox, copyq...) register tray icons
▼
tray-host daemon (background process)
├── system_tray::Client → D-Bus StatusNotifierWatcher
├── Host → in-memory tray item cache
└── Unix socket → $XDG_RUNTIME_DIR/tray-host.sock
▲
│
tray-host pick ← the only command you need
cargo install --git https://github.com/sorubedo/tray-tui
git clone https://github.com/sorubedo/tray-tui
cd tray-tui
cargo build --release
Add to your compositor autostart, or run manually:
tray-host daemon &
tray-host pick
That's it. pick handles the full flow:
- Lists tray icons → spawns fuzzel for selection
- If the icon has a menu → spawns fuzzel again for menu selection
- Sends the click via D-Bus to the application
- If no menu → sends a default (left-click) activation
Use a different picker:
tray-host pick --picker "rofi -dmenu -show-icons"
Optional config at $XDG_CONFIG_HOME/tray-host/config.toml:
sorting = false # sort tray items alphabeticallyuse tray_host::Host;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let host = Host::new().await?;
for (addr, title) in host.list_items() {
println!("{addr}\t{title}");
}
Ok(())
}MIT — see LICENSE.