⚠️ Work in Progress: Wayle is under active development. The bar and modules that are completed under the UI Components section are ready to use. This, however, is not a stable environment, things are subject to change.
A fast, configurable desktop environment shell for Wayland compositors. Built in Rust with Relm4 and focused on performance, modularity, and a great user experience. A successor to HyprPanel without the pain or dependency on Hyprland.
- Configuration System - Reactive TOML config with schema validation
- CLI Interface - Complete command-line management interface
- Documentation Generator - Auto-generated config docs from schemas
- MPRIS
- PulseAudio
- Network
- Bluetooth
- Battery
- Notification Daemon
- Power Profiles
- System Tray
- GTK4 Adapter
- Hyprland
- Cava
- Component Library - Base Relm4 widgets and containers
- Bar Modules:
- Battery
- Media
- Volume
- Network
- Bluetooth
- Clock
- Microphone
- System tray
- Notification
- Dashboard
- Netstat
- RAM
- CPU
- CPU Temp
- Storage
- Separator
- Power
- World clock
- Weather
- Idle Inhibit
- Keyboard input
- Hyprland Window title
- Hyprland submap
- Hyprsunset
- Hyprland workspaces
- Custom Modules
- Cava
- Updates
- Too much surface area and distro coupling
- Will be achievable easily via custom modules
- Audio Panel
- Network Panel
- Bluetooth Panel
- Battery Panel
- Media Panel
- Weather Panel
- Calendar Panel
- Dashboard
- Notifications Panel
- Notifications
- OSD
- Settings Dialog (WIP)
Configuration lives in ~/.config/wayle/config.toml with live reloading.
[styling]
theme-provider = "wayle"
[styling.palette]
bg = "#16161e"
fg = "#c0caf5"
primary = "#7aa2f7"
[bar]
scale = 1
location = "top"
rounding = "sm"
[[bar.layout]]
monitor = "*"
left = ["clock"]
center = ["media"]
right = ["battery"]
[modules.clock]
format = "%H:%M"
icon-show = true
label-show = trueConfig files can be split and imported for better organization:
# config.toml
imports = ["colors.toml", "modules/bar.toml"]
[bar]
location = "top"CLI commands can also be used to modify, get or reset any property:
wayle config get bar.scale
wayle config set bar.location bottom
wayle config reset bar.scaleOnce the project is finished, documentation will be added for all configurable
properties, in addition to having a settings GUI. Until then you can run the
following command to generate a reference config config.toml.example in your
config directory:
wayle config defaultEditor intellisense is available via JSON Schema. Install
Tombi
for VSCode or the tombi LSP for Neovim. The schema is generated automatically
on startup.
This will give you auto-complete, config validation and other nice QoL features for your config.toml (and other toml files).
wayle config schemaInstall Rust via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shClone the repository recursively and build:
git clone --recursive https://github.com/wayle-rs/wayle
cd wayle
cargo install --path crates/wayle-shell
cargo install --path wayleOnce Wayle is installed, you can set up the icons (temporary measure) and start it via:
wayle icons setup
wayle panel startWayle uses GTK symbolic icons that support CSS color theming.
To manually manage icons:
# Install bundled icons (automatic on first launch)
wayle icons setup
# Install additional icons from CDN sources
wayle icons install tabler home settings bell
wayle icons install simple-icons firefox spotify
# See all available sources
wayle icons install --helpIcons are installed to ~/.local/share/wayle/icons/ as GTK symbolic icons.
Custom modules run shell commands and display the output in the bar. Define one
in your config and add it to your layout with the custom- prefix:
[[bar.layout]]
monitor = "*"
right = ["custom-gpu-temp", "clock"]
[[modules.custom]]
id = "gpu-temp"
command = "nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits"
interval-ms = 5000
format = "{{ output }}°C"
icon-name = "ld-thermometer-symbolic"The command runs via sh -c. Plain text output is available as {{ output }}
in format. If the output starts with { or [, it's parsed as JSON and
fields are available directly: {{ temperature }}, {{ nested.value }}, etc.
Poll (default) runs the command every interval-ms milliseconds:
# default, can be omitted
mode = "poll"
# every 5 seconds
interval-ms = 5000Watch spawns the command once and updates the display on each line of
stdout. Good for commands that stream events like pactl subscribe or
inotifywait:
[[modules.custom]]
id = "volume"
mode = "watch"
command = '''
pactl subscribe | while read -r line; do
if [[ "$line" == *"sink"* ]]; then
vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oP '\d+(?=%)' | head -1)
echo "{\"percentage\": $vol}"
fi
done
'''
format = "{{ percentage }}%"
restart-policy = "on-failure"If a watch process exits, restart-policy controls what happens:
never(default) - stay deadon-exit- restart after any exiton-failure- restart only on non-zero exit codes
The restart delay starts at restart-interval-ms (default 1000ms) and doubles
on each rapid failure, capping at 30 seconds.
If your command outputs JSON with a percentage field (0-100), you can map it
to an array of icons. The array is divided evenly across the range:
[[modules.custom]]
id = "battery"
command = '''
cap=$(cat /sys/class/power_supply/BAT0/capacity)
echo "{\"percentage\": $cap}"
'''
interval-ms = 30000
format = "{{ percentage }}%"
icon-names = [
"ld-battery-warning-symbolic",
"ld-battery-low-symbolic",
"ld-battery-medium-symbolic",
"ld-battery-full-symbolic"
]4 icons means: 0-24% picks the first, 25-49% the second, 50-74% the third, 75-100% the fourth.
For state-based icons, output an alt field and use icon-map:
icon-map = { muted = "ld-volume-off-symbolic", default = "ld-volume-2-symbolic" }If both alt and percentage are present, icon-map wins. The full priority
is: icon-map[alt] > icon-names[percentage] > icon-map["default"] >
icon-name.
Each interaction type has its own command:
left-click = "pavucontrol"
scroll-up = "pactl set-sink-volume @DEFAULT_SINK@ +5%"
scroll-down = "pactl set-sink-volume @DEFAULT_SINK@ -5%"By default, the display won't update until the next poll. To refresh immediately
after an action, add on-action - its output updates the display right away:
on-action = '''
vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oP '\d+(?=%)' | head -1)
echo "{\"percentage\": $vol}"
'''Scroll events are debounced (50ms) so rapid scrolling doesn't fire dozens of
commands. Set interval-ms = 0 if you only want updates from on-action (no
polling at all).
When outputting JSON, these fields have special meaning:
| Field | Type | Effect |
|---|---|---|
text |
string | Replaces the format result for the label |
tooltip |
string | Replaces the tooltip-format result |
percentage |
number | 0-100, selects from icon-names |
alt |
string | Selects from icon-map |
class |
string/array | Adds CSS classes to the module |
All other fields are available in format and tooltip-format templates.
All fields for [[modules.custom]]
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | required | Unique ID, referenced in layout as custom-<id> |
command |
string | none | Shell command (sh -c). JSON auto-detected |
mode |
"poll" / "watch" |
"poll" |
Poll runs on interval, watch streams stdout |
interval-ms |
number | 5000 |
Poll interval. 0 = manual only. Ignored in watch mode |
restart-policy |
"never" / "on-exit" / "on-failure" |
"never" |
Watch mode only |
restart-interval-ms |
number | 1000 |
Watch mode restart delay (doubles on rapid failures, caps 30s) |
| Field | Type | Default | Description |
|---|---|---|---|
format |
string | "{{ output }}" |
Template for the label. Use {{ field }} for JSON fields |
tooltip-format |
string | none | Template for hover tooltip |
hide-if-empty |
bool | false |
Hide when output is empty, "0", or "false" |
class-format |
string | none | Template for dynamic CSS classes (space-separated) |
| Field | Type | Default | Description |
|---|---|---|---|
icon-name |
string | "" |
Static fallback icon |
icon-names |
string[] | none | Icons indexed by JSON percentage (0-100) |
icon-map |
table | none | Icons keyed by JSON alt. "default" key as fallback |
| Field | Type | Default | Description |
|---|---|---|---|
icon-show |
bool | true |
Show the icon |
icon-color |
color | "auto" |
Icon foreground color |
icon-bg-color |
color | "auto" |
Icon container background |
label-show |
bool | true |
Show the text label |
label-color |
color | "auto" |
Label text color |
label-max-length |
number | 0 |
Truncate after N chars (0 = no limit) |
button-bg-color |
color | theme default | Button background |
border-show |
bool | false |
Show border |
border-color |
color | "auto" |
Border color |
| Field | Type | Default | Description |
|---|---|---|---|
left-click |
string | "" |
Command on left click |
right-click |
string | "" |
Command on right click |
middle-click |
string | "" |
Command on middle click |
scroll-up |
string | "" |
Command on scroll up (50ms debounce) |
scroll-down |
string | "" |
Command on scroll down (50ms debounce) |
on-action |
string | none | Runs after any action, output updates display |
Color values: "auto", hex ("#ff0000"), or theme token ("red", "primary",
etc.).
Big thanks to @M70v for the Wayle logo contribution! Check out their work at https://www.instagram.com/m70v.art/.
MIT