An accessible, compositor-agnostic system tray for Linux.
WayTray implements the StatusNotifierItem (SNI) specification with a daemon + client architecture. Unlike traditional system trays that sit permanently in a panel, WayTray's client opens as a regular window on demand, making it fully accessible to screen readers like Orca.
- Compositor agnostic: Works on any Wayland compositor (or X11)
- Accessible: Full keyboard navigation and screen reader support
- Left/Right arrows to navigate between items
- Enter/Space to activate items
- Shift+F10 or Menu key for context menus
- Escape to close
- Modular: Built-in modules for system tray, battery, clock, and more
- Configurable: TOML configuration for modules, ordering, and notifications
- Daemon + client architecture: Daemon caches items; client displays them on demand
- Real-time updates: Items refresh automatically when status changes
- Desktop notifications: Battery warnings and other alerts via freedesktop notifications
WayTray is written in Rust. Install Rust and the required development libraries for your distribution:
Debian/Ubuntu:
sudo apt install rustc cargo build-essential pkg-config libgtk-4-dev libgstreamer1.0-devFedora:
sudo dnf install rust cargo gcc pkg-config gtk4-devel gstreamer1-develArch:
sudo pacman -S rust base-devel gtk4 gstreamerIf the packaged Rust version doesn't work, install via rustup instead:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh- GTK4
- A D-Bus session bus (standard on most Linux desktops)
pactlfor pipewire and privacy modules (frompulseaudio-utilsorpipewire-pulse)
git clone https://github.com/destructatron/waytray
cd waytray
cargo build --releaseBinaries will be at:
target/release/waytray-daemontarget/release/waytray
./target/release/waytray-daemonThe daemon registers as a StatusNotifierHost and begins caching tray items from applications (Discord, Spotify, nm-applet, etc.).
./target/release/waytrayA window appears showing all current tray items. Interact with them using keyboard or mouse, then close the window when done.
To start the daemon automatically at login:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/waytray.service << 'EOF'
[Unit]
Description=WayTray System Tray Daemon
[Service]
ExecStart=%h/.local/bin/waytray-daemon
Restart=on-failure
[Install]
WantedBy=default.target
EOF
# Copy binary to ~/.local/bin (or adjust the path above)
cp target/release/waytray-daemon ~/.local/bin/
# Enable and start
systemctl --user enable --now waytrayThen bind waytray to a keyboard shortcut in your compositor for quick access.
WayTray is configured via a TOML file at ~/.config/waytray/config.toml. If the file doesn't exist, it is created with defaults.
Hot Reload: The daemon automatically watches the config file and reloads module settings when it changes. No restart required for most configuration changes.
[modules]
# Module display order (left to right)
# Modules not listed appear after these
order = ["tray", "pipewire", "privacy", "power_profiles", "battery", "system", "gpu", "network", "weather", "clock", "scripts"]
[modules.tray]
enabled = true
[modules.battery]
enabled = true
low_threshold = 20 # Notify at this percentage
critical_threshold = 10 # Critical notification at this percentage
notify_full_charge = false # Notify when fully charged
[modules.clock]
enabled = true
format = "%H:%M" # Time format (strftime)
date_format = "%A, %B %d, %Y" # Tooltip date format
[modules.system]
enabled = true
show_cpu = true
show_memory = true
show_temperature = false
show_top_cpu_process = false # Show top CPU process in tooltip
show_top_memory_process = false # Show top memory process in tooltip
interval_seconds = 5
[modules.weather]
enabled = true
location = "" # Empty = auto-detect from IP
units = "celsius" # or "fahrenheit"
interval_seconds = 1800 # 30 minutes
[modules.network]
enabled = true
interface = "" # Empty = auto-detect default route
show_ip = false
show_speed = true
interval_seconds = 2
[modules.pipewire]
enabled = true
show_volume = true # Show volume % in label
max_volume = 100 # Cap volume (100 = normal, 150 = boost)
scroll_step = 5 # Volume change per action
show_microphone = true # Show microphone control item
show_mic_volume = true # Show mic volume % in label
mic_max_volume = 100 # Cap mic volume (100 = normal, 150 = boost)
mic_scroll_step = 5 # Mic volume change per action
[modules.privacy]
enabled = true
interval_seconds = 2 # Polling interval
show_when_idle = false # Show item when no apps are using the mic
[modules.power_profiles]
enabled = true # Requires power-profiles-daemon
[modules.gpu]
enabled = true
show_temperature = false # Show GPU temperature in tooltip
show_top_process = false # Show top GPU memory process (NVIDIA only)
interval_seconds = 5
[notifications]
enabled = true
timeout_ms = 5000 # 0 = no timeoutDisplays system tray items from applications (Discord, Spotify, nm-applet, etc.) using the StatusNotifierItem protocol.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the tray module |
Displays battery status and sends notifications for low/critical/full states. Uses UPower via D-Bus. Optionally plays sounds using GStreamer.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the battery module |
low_threshold |
u8 | 20 |
Battery percentage for low warning |
critical_threshold |
u8 | 10 |
Battery percentage for critical warning |
notify_full_charge |
bool | false |
Send notification when fully charged |
low_sound |
string | null |
Sound file to play on low battery (optional) |
critical_sound |
string | null |
Sound file to play on critical battery (optional) |
full_sound |
string | null |
Sound file to play when fully charged (optional) |
Sound files: Paths can use ~ for home directory. Supports any format GStreamer can play (WAV, OGG, MP3, etc.).
Displays the current time with configurable format.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the clock module |
format |
string | "%H:%M" |
Time format (strftime) |
date_format |
string | "%A, %B %d, %Y" |
Tooltip date format |
Format examples:
%H:%M- 24-hour time (14:30)%I:%M %p- 12-hour with AM/PM (2:30 PM)%H:%M:%S- With seconds (14:30:45)
Displays CPU usage, memory usage, and CPU temperature. Reads from /proc/stat, /proc/meminfo, and /sys/class/thermal.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the system module |
show_cpu |
bool | true |
Show CPU usage percentage |
show_memory |
bool | true |
Show memory usage percentage |
show_temperature |
bool | false |
Show CPU temperature |
show_top_cpu_process |
bool | false |
Show top CPU process in CPU tooltip |
show_top_memory_process |
bool | false |
Show top memory process in memory tooltip |
interval_seconds |
u64 | 5 |
Update interval in seconds |
Displays current weather conditions using wttr.in (no API key required). Shows temperature as the label with detailed conditions in the tooltip.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the weather module |
location |
string | "" |
City name (e.g., "London", "New York"). Empty = auto-detect from IP |
units |
string | "celsius" |
Temperature units: "celsius" or "fahrenheit" |
interval_seconds |
u64 | 1800 |
Update interval (default 30 minutes) |
Display:
- Label: Temperature (e.g., "15°C")
- Tooltip: Conditions, feels like, humidity, location
- Icon: Weather-appropriate theme icon
Displays network connection status and transfer speeds. Reads from /sys/class/net and /proc/net/route.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the network module |
interface |
string | "" |
Network interface to monitor. Empty = auto-detect default route interface |
show_ip |
bool | false |
Show IP address in tooltip |
show_speed |
bool | true |
Show upload/download speeds |
interval_seconds |
u64 | 2 |
Update interval in seconds |
Display:
- Label: Upload/download speeds (e.g., "↓1.2MB/s ↑50KB/s")
- Tooltip: Interface name, IP address, speeds
- Icon:
network-wireless,network-wired, ornetwork-offlinebased on interface type and status
Displays audio output volume and microphone input controls. Uses pactl to communicate with PulseAudio or PipeWire (via pipewire-pulse).
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the pipewire module |
show_volume |
bool | true |
Show volume percentage in label |
max_volume |
u32 | 100 |
Maximum volume cap (100 = normal, up to 150 for boost) |
scroll_step |
u32 | 5 |
Volume change percentage per action |
show_microphone |
bool | true |
Show microphone control item |
show_mic_volume |
bool | true |
Show microphone volume percentage in label |
mic_max_volume |
u32 | 100 |
Maximum mic volume cap (100 = normal, up to 150 for boost) |
mic_scroll_step |
u32 | 5 |
Mic volume change percentage per action |
Display (Output):
- Label: Volume percentage (e.g., "75%") or "Muted"
- Tooltip: Volume %, mute status, output device name
- Icon:
audio-volume-muted,audio-volume-low,audio-volume-medium, oraudio-volume-high
Display (Microphone):
- Label: Volume percentage (e.g., "75%") or "Muted"
- Tooltip: Volume %, mute status, input device name
- Icon:
microphone-sensitivity-muted,microphone-sensitivity-low,microphone-sensitivity-medium, ormicrophone-sensitivity-high
Actions:
- Enter/Click: Toggle mute
- Up/Down arrows: Adjust volume when focused on output or microphone item
Requirements: Requires pactl command (from pulseaudio-utils on Debian/Ubuntu or included with pipewire-pulse).
Shows which applications are currently using the microphone. Uses pactl list source-outputs and filters out corked streams.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the privacy module |
interval_seconds |
u64 | 2 |
Update interval in seconds |
show_when_idle |
bool | false |
Show an idle item when no apps are using the mic |
Display:
- Label: Active app name (or "App +N" if multiple)
- Tooltip: Full list of active apps, or "No apps are using the microphone."
- Icon:
microphone-sensitivity-highwhen active,microphone-sensitivity-mutedwhen idle
Requirements: Requires pactl command (from pulseaudio-utils on Debian/Ubuntu or included with pipewire-pulse).
Displays and controls system power profile via power-profiles-daemon. Allows switching between power-saver, balanced, and performance modes.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the power profiles module |
Display:
- Label: Current profile name (e.g., "Balanced")
- Tooltip: Profile name, degraded reason (if any)
- Icon:
power-profile-power-saver-symbolic,power-profile-balanced-symbolic, orpower-profile-performance-symbolic
Actions:
- Enter/Click: Cycle to next profile (Power Saver → Balanced → Performance → ...)
- Individual profile actions available in context menu
Requirements: Requires power-profiles-daemon to be installed and running. Available on most modern Linux distributions.
Displays GPU usage percentage and optionally temperature. Supports NVIDIA (via nvidia-smi) and AMD (via sysfs).
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable the GPU module |
show_temperature |
bool | false |
Show GPU temperature in tooltip |
show_top_process |
bool | false |
Show top GPU memory process in tooltip (NVIDIA only) |
interval_seconds |
u64 | 5 |
Update interval in seconds |
Display:
- Label: GPU usage percentage (e.g., "GPU 45%")
- Tooltip: Usage %, temperature (if enabled), top process (if enabled)
- Icon:
video-display(ordialog-warningif temperature ≥80°C)
Supported GPUs:
- NVIDIA: Full support via
nvidia-smi(usage, temperature, top process) - AMD: Usage and temperature via sysfs (
/sys/class/drm/card*/device/) - Intel: Detection only (usage monitoring requires elevated permissions)
Requirements: For NVIDIA GPUs, requires the proprietary NVIDIA driver with nvidia-smi available.
Run custom scripts and display their output. Each script must be explicitly enabled for security. Multiple scripts can be configured using TOML array-of-tables syntax.
| Option | Type | Default | Description |
|---|---|---|---|
id |
string | required | Unique identifier for this script |
path |
string | required | Path to the script to execute |
enabled |
bool | false |
Must be true to run (security measure) |
mode |
string | "interval" |
Execution mode (see below) |
interval_seconds |
u64 | 30 |
Update interval (only for interval mode) |
icon |
string | null |
Default icon (can be overridden by script output) |
Execution Modes:
| Mode | Description |
|---|---|
once |
Run script once when module loads |
watch |
Spawn as long-running process, monitor stdout for updates (each line triggers update) |
interval |
Run script at regular intervals |
on_connect |
Run script when module starts and on config reload |
Output Formats:
Scripts can output in two formats (auto-detected based on whether output starts with {):
- JSON format - for structured output with actions:
{
"label": "Display text",
"tooltip": "Tooltip text",
"icon": "icon-name",
"actions": [
{"id": "Activate", "command": "/path/to/click-handler.sh"},
{"id": "ScrollUp", "command": "/path/to/scroll-up.sh"},
{"id": "ScrollDown", "command": "/path/to/scroll-down.sh"}
]
}- Line-based format - simple text output:
Label text (first line)
Tooltip text (optional second line)
Example Configuration:
# Simple uptime display
[[modules.scripts]]
id = "uptime"
path = "/home/user/scripts/uptime.sh"
enabled = true
mode = "interval"
interval_seconds = 60
icon = "computer"
# Disk usage with click action
[[modules.scripts]]
id = "disk"
path = "/home/user/scripts/disk-usage.sh"
enabled = true
mode = "interval"
interval_seconds = 300
icon = "drive-harddisk"
# Long-running counter (watch mode)
[[modules.scripts]]
id = "counter"
path = "/home/user/scripts/counter.sh"
enabled = true
mode = "watch"Security: Scripts are disabled by default and must have enabled = true explicitly set. The script path must exist or a warning is logged and the script is skipped.
Example scripts are available in the examples/scripts/ directory.
Global notification settings.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable/disable all notifications |
timeout_ms |
u32 | 5000 |
Notification timeout (0 = never) |
┌─────────────────────────────────────────────────────────┐
│ waytray-daemon (always running) │
│ ├─ Module system │
│ │ ├─ Tray module (SNI host for app tray items) │
│ │ ├─ Battery module (UPower D-Bus) │
│ │ ├─ Clock module (time display) │
│ │ ├─ System module (CPU/memory from /proc) │
│ │ ├─ GPU module (nvidia-smi / sysfs) │
│ │ ├─ Network module (interface stats from /sys) │
│ │ ├─ Pipewire module (audio volume via pactl) │
│ │ ├─ Privacy module (mic usage via pactl) │
│ │ ├─ Power Profiles module (power-profiles-daemon) │
│ │ ├─ Weather module (wttr.in API) │
│ │ └─ Scripts module (custom user scripts) │
│ ├─ StatusNotifierWatcher (fallback if none exists) │
│ ├─ Notification service (desktop notifications) │
│ └─ org.waytray.Daemon interface for clients │
└─────────────────────────────────────────────────────────┘
↕ D-Bus
┌─────────────────────────────────────────────────────────┐
│ waytray (GTK4 client, invoked on demand) │
│ ├─ Queries daemon for module items │
│ ├─ Displays accessible horizontal item list │
│ └─ Forwards user actions to daemon │
└─────────────────────────────────────────────────────────┘
MIT