Async status bar daemon with native gar integration and modular architecture
garbar is the status bar component of the gardesk ecosystem. Built in Rust with async Tokio runtime, Cairo/Pango rendering, and deep integration with the gar window manager. Features include EWMH workspace tracking, multi-monitor support, XEmbed and StatusNotifierItem tray protocols, and unified configuration through gar's Lua config system. Control the bar at runtime via garbarctl CLI or Unix socket IPC.
Non-blocking module updates with tokio::select event loop
workspaces, window_title, cpu, memory, battery, datetime, tray, script, quick_settings
Hardware-accelerated text and gradient rendering
Shared Lua configuration via gar.bar table
Independent bar per monitor with RandR hotplug detection
XEmbed (legacy) and StatusNotifierItem (modern D-Bus) protocols
garbarctl CLI and JSON over Unix socket
Real-time workspace and window tracking via X11 properties
garbar requires the following runtime dependencies:
When using gar, garbar starts automatically via systemd user services. The gar-session.target pulls in garbar.service, ensuring the bar is available when gar's X11 session is ready.
# ~/.config/systemd/user/garbar.service
[Unit]
Description=garbar status bar daemon
PartOf=gar-session.target
After=gar.service
[Service]
ExecStart=/usr/bin/garbar daemon
Restart=on-failure
Environment="GAR_BAR_CONFIG=%h/.cache/gar/bar-config.json"
[Install]
WantedBy=gar-session.target When gar starts, it activates gar-session.target, which starts garbar. When gar exits, the target deactivates and garbar stops gracefully.
If running garbar standalone (without gar), start the daemon directly:
Configure garbar directly in your gar init.lua file. When gar starts, it serializes the gar.bar table to JSON and passes it to garbar.
-- ~/.config/gar/init.lua
gar.bar = (
height = 32,
position = "top",
background = "#1e1e2e",
foreground = "#cdd6f4",
modules_left = ( "workspaces", "window_title" ),
modules_center = (),
modules_right = ( "cpu", "memory", "datetime" ),
modules = (
workspaces = (
show_empty = false,
focused = (
foreground = "#ffffff",
underline = ( width = 2, color = "#89b4fa" ),
),
),
datetime = (
format = "%H:%M",
format_alt = "%Y-%m-%d %H:%M:%S",
),
),
) garbar configuration is defined in the gar.bar Lua table in ~/.config/gar/init.lua. When gar starts, it serializes this table to JSON and passes it to garbar. This unified configuration approach ensures gar and garbar stay in sync without configuration drift.
| Option | Type | Default | Description |
|---|---|---|---|
height | - | 32 | Bar height in pixels |
position | - | "top" | Bar position: "top" or "bottom" |
background | - | "#1a1a1a" | Background color (hex) or gradient config |
foreground | - | "#ffffff" | Default text color (hex) |
opacity | - | 1.0 | Window opacity 0.0 to 1.0 |
fonts | - | JetBrainsMono 10 | Font stack array for text rendering |
modules_left | - | workspaces, window_title | Modules to display on left side |
modules_center | - | (empty) | Modules to display in center |
modules_right | - | cpu, memory, battery, datetime | Modules to display on right side |
| Option | Type | Default | Description |
|---|---|---|---|
padding.top | - | 0 | Top padding in pixels |
padding.right | - | 16 | Right padding in pixels |
padding.bottom | - | 0 | Bottom padding in pixels |
padding.left | - | 8 | Left padding in pixels |
| Option | Type | Default | Description |
|---|---|---|---|
margin.top | - | 0 | Top margin in pixels |
margin.right | - | 0 | Right margin in pixels |
margin.bottom | - | 0 | Bottom margin in pixels |
margin.left | - | 0 | Left margin in pixels |
| Option | Type | Default | Description |
|---|---|---|---|
border.width | - | 0 | Border width in pixels |
border.color | - | "" | Border color (hex) |
border.radius | - | 0 | Corner radius in pixels |
| Option | Type | Default | Description |
|---|---|---|---|
shadow.enabled | - | false | Enable drop shadow |
shadow.color | - | "#00000080" | Shadow color with alpha (hex) |
shadow.blur | - | 8 | Shadow blur radius in pixels |
shadow.offset.x | - | 0 | Horizontal shadow offset |
shadow.offset.y | - | 0 | Vertical shadow offset |
| Option | Type | Default | Description |
|---|---|---|---|
animations.enabled | - | true | Enable module transitions |
animations.duration | - | 150 | Animation duration in milliseconds |
animations.easing | - | "ease-out-cubic" | Easing function name |
| Option | Type | Default | Description |
|---|---|---|---|
separator.text | - | "│" | Separator character between modules |
separator.foreground | - | "#555555" | Separator color (hex) |
separator.padding | - | 8px horizontal | Separator padding config |
The background option can be a solid color string or a gradient configuration object.
| Option | Type | Default | Description |
|---|---|---|---|
type | - | "gradient" | Gradient type: "gradient" (linear) or "radial" |
direction | - | "horizontal" | Linear direction: "horizontal", "vertical", "diagonal" |
stops | - | - | Array of gradient stops with position and color |
center | - | (0.5, 0.5) | Radial center point (0.0-1.0 normalized) |
radius | - | 1.0 | Radial radius (normalized to smaller dimension) |
gar.bar = (
background = (
type = "gradient",
direction = "horizontal",
stops = (
( position = 0.0, color = "#1e1e2e" ),
( position = 0.5, color = "#313244" ),
( position = 1.0, color = "#1e1e2e" ),
),
),
) gar.bar = (
background = (
type = "radial",
center = ( 0.5, 0.5 ),
radius = 1.5,
stops = (
( position = 0.0, color = "#45475a" ),
( position = 1.0, color = "#1e1e2e" ),
),
),
) Per-module configuration lives under the modules table. Each module has its own set of options.
| Option | Type | Default | Description |
|---|---|---|---|
show_empty | - | false | Show indicators for empty workspaces |
show_urgent | - | true | Show urgent workspace state |
pin_workspaces | - | false | Pin workspaces to specific positions |
focused.background | - | "transparent" | Focused workspace background |
focused.foreground | - | "#ffffff" | Focused workspace text color |
focused.underline | - | 2px #33ccff | Underline config for focused state |
unfocused.background | - | "transparent" | Unfocused workspace background |
unfocused.foreground | - | "#666666" | Unfocused workspace text color |
urgent.background | - | "#ff5555" | Urgent workspace background |
urgent.foreground | - | "#ffffff" | Urgent workspace text color |
font_size | - | inherit | Override font size for workspace labels |
| Option | Type | Default | Description |
|---|---|---|---|
max_length | - | 50 | Maximum title length before truncation |
ellipsis | - | "..." | Truncation suffix |
empty_text | - | "Desktop" | Text when no window focused |
show_icon | - | true | Show window icon if available |
icon_spacing | - | 8 | Space between icon and title |
| Option | Type | Default | Description |
|---|---|---|---|
format | - | " {usage}%" | Display format with placeholders |
interval | - | 2 | Update interval in seconds |
warning_threshold | - | 70 | Usage % to trigger warning color |
critical_threshold | - | 90 | Usage % to trigger critical color |
warning_foreground | - | "#ffaa00" | Warning state text color |
critical_foreground | - | "#ff5555" | Critical state text color |
graph.enabled | - | false | Show CPU usage graph |
graph.width | - | 50 | Graph width in pixels |
graph.style | - | "bars" | Graph style: "bars", "line", "area" |
| Option | Type | Default | Description |
|---|---|---|---|
format | - | " {percent}%" | Display format with placeholders |
format_alt | - | " {used}/{total}" | Alternate format (click to toggle) |
interval | - | 5 | Update interval in seconds |
warning_threshold | - | 80 | Usage % to trigger warning color |
critical_threshold | - | 95 | Usage % to trigger critical color |
| Option | Type | Default | Description |
|---|---|---|---|
device | - | "auto" | Battery device path or "auto" |
format_charging | - | " {percent}%" | Format when charging |
format_discharging | - | "{icon} {percent}%" | Format when discharging |
format_full | - | " Full" | Format when fully charged |
icons | - | 5 battery icons | Array of threshold/icon pairs |
low_threshold | - | 15 | Percentage to trigger low warning |
low_animation | - | "blink" | Animation when low: "blink" or "none" |
low_foreground | - | "#ff5555" | Text color when battery low |
| Option | Type | Default | Description |
|---|---|---|---|
format | - | " %a %b %d %H:%M" | strftime format string |
format_alt | - | " %Y-%m-%d %H:%M:%S" | Alternate format (click to toggle) |
interval | - | 1 | Update interval in seconds |
tooltip | - | true | Show tooltip on hover |
| Option | Type | Default | Description |
|---|---|---|---|
icon_size | - | 18 | Tray icon size in pixels |
spacing | - | 8 | Space between tray icons |
padding.left | - | 4 | Left padding for tray area |
padding.right | - | 4 | Right padding for tray area |
| Option | Type | Default | Description |
|---|---|---|---|
enabled | - | true | Enable quick settings trigger |
icon | - | "gear" | Icon when inactive |
icon_active | - | "gear" | Icon when panel is open |
foreground | - | "#abb2bf" | Icon color |
active_foreground | - | "#61afef" | Icon color when active |
| Option | Type | Default | Description |
|---|---|---|---|
exec | - | - | Command or script path to execute |
interval | - | 30 | Update interval in seconds |
tail | - | false | Tail mode: read continuously |
format | - | "" | Output format string |
click_left | - | "" | Command on left click |
click_middle | - | "" | Command on middle click |
click_right | - | "" | Command on right click |
scroll_up | - | "" | Command on scroll up |
scroll_down | - | "" | Command on scroll down |
font_size | - | inherit | Override font size |
-- ~/.config/gar/init.lua
gar.bar = (
height = 32,
position = "top",
background = "#1e1e2e",
foreground = "#cdd6f4",
opacity = 0.95,
fonts = (
"JetBrainsMono Nerd Font:size=10",
"DejaVu Sans Mono:size=10",
"monospace:size=10",
),
padding = ( left = 8, right = 16, top = 0, bottom = 0 ),
margin = ( left = 0, right = 0, top = 0, bottom = 0 ),
border = ( width = 0, color = "", radius = 0 ),
animations = ( enabled = true, duration = 150, easing = "ease-out-cubic" ),
separator = (
text = "|",
foreground = "#45475a",
padding = ( left = 8, right = 8 ),
),
modules_left = ( "workspaces", "window_title" ),
modules_center = (),
modules_right = ( "cpu", "memory", "battery", "tray", "datetime" ),
modules = (
workspaces = (
show_empty = false,
show_urgent = true,
focused = (
background = "transparent",
foreground = "#cdd6f4",
underline = ( width = 2, color = "#89b4fa" ),
),
unfocused = (
background = "transparent",
foreground = "#585b70",
),
urgent = (
background = "#f38ba8",
foreground = "#1e1e2e",
),
),
window_title = (
max_length = 50,
ellipsis = "...",
empty_text = "Desktop",
show_icon = true,
),
cpu = (
format = " (usage)%",
interval = 2,
warning_threshold = 70,
critical_threshold = 90,
warning_foreground = "#f9e2af",
critical_foreground = "#f38ba8",
),
memory = (
format = " (percent)%",
format_alt = " (used)/(total)",
interval = 5,
warning_threshold = 80,
critical_threshold = 95,
),
battery = (
device = "auto",
format_charging = " (percent)%",
format_discharging = "(icon) (percent)%",
format_full = " Full",
low_threshold = 15,
low_animation = "blink",
low_foreground = "#f38ba8",
),
datetime = (
format = " %a %b %d %H:%M",
format_alt = " %Y-%m-%d %H:%M:%S",
interval = 1,
tooltip = true,
),
tray = (
icon_size = 18,
spacing = 8,
padding = ( left = 4, right = 4 ),
),
),
) garbar is an async status bar daemon built in Rust for the gardesk desktop environment. It renders a configurable status bar with modules for workspace indicators, window titles, system monitoring, date/time, and system tray icons. garbar integrates deeply with the gar window manager through shared configuration, EWMH property communication, and coordinated lifecycle management.
The bar uses Cairo for rendering and Pango for text layout, providing high-quality antialiased graphics with support for gradients, rounded corners, and shadows. The async Tokio runtime enables non-blocking module updates, ensuring the UI remains responsive even when modules perform I/O operations like reading /proc or querying D-Bus.
The DaemonState struct is the central coordinator for garbar. It owns:
garbar uses tokio::select! to multiplex multiple event sources in a single async loop:
while running (
tokio::select! (
// Handle Unix signals (SIGTERM, SIGHUP)
signal = signal_handler.recv() => handle_signal(signal),
// Handle X11 events (Expose, ButtonPress, PropertyNotify)
event = conn.next_event() => handle_x11_event(event),
// Handle SNI D-Bus events (item registered/unregistered)
event = sni_event_rx.recv() => handle_sni_event(event),
// Periodic module updates (50ms tick rate)
_ = update_interval.tick() => (
poll_ipc_commands(),
refresh_sni_items(),
modules.update_all(),
draw_bar(),
),
)
) The 50ms tick rate provides snappy workspace updates while keeping CPU usage low. Event-driven modules (workspaces, window_title) subscribe to X11 PropertyNotify events for instant updates.
Each frame follows this rendering pipeline:
Modules implement the Module trait with these methods:
trait Module: Send + Sync (
fn name(&self) -> &'static str;
fn output(&self) -> ModuleOutput;
fn interval(&self) -> u64; // 0 = event-driven only
fn update(&mut self);
fn on_click(&mut self, button: u8, block_index: usize, x: i16, y: i16);
fn on_scroll(&mut self, up: bool, block_index: usize, x: i16, y: i16);
) The ModuleRegistry manages module instantiation and provides async methods for collecting outputs and dispatching events. Each module runs in the main thread but can spawn background tasks for I/O-heavy operations.
garbar shares configuration with gar through the gar.bar Lua table. When gar starts:
This approach eliminates configuration drift between components. When you change gar.bar in init.lua and run garctl reload, gar re-serializes the config and signals garbar to reload.
gar and garbar communicate through Extended Window Manager Hints (EWMH) X11 properties:
gar manages garbar's lifecycle through systemd user services:
garbar creates independent bar windows for each monitor:
Each bar window is created with specific EWMH properties:
Struts reserve screen space so windows don't overlap the bar:
// _NET_WM_STRUT = (left, right, top, bottom)
// top = y_position + bar_height
strut = (0, 0, y + height, 0)
// _NET_WM_STRUT_PARTIAL includes X range for per-monitor struts
strut_partial = (
0, 0, y + height, 0, // left, right, top, bottom
0, 0, 0, 0, // left_start_y, left_end_y, right_start_y, right_end_y
x, x + width - 1, 0, 0, // top_start_x, top_end_x, bottom_start_x, bottom_end_x
) garbar supports transparency through the _NET_WM_WINDOW_OPACITY property:
// Opacity is a 32-bit cardinal, 0xFFFFFFFF = 100% opaque
let opacity_value = (opacity * u32::MAX as f64) as u32;
change_property32(window, _NET_WM_WINDOW_OPACITY, CARDINAL, opacity_value) The compositor (like garbg or picom) reads this property and applies alpha blending. Without a compositor, the bar is fully opaque regardless of this setting.
garbar implements both legacy and modern system tray protocols to support the widest range of applications.
XEmbed is the legacy X11 system tray protocol. garbar acts as a system tray manager by:
Applications using XEmbed include: nm-applet, blueman-applet, and older Qt applications.
StatusNotifierItem is the modern D-Bus-based tray protocol. garbar acts as both a StatusNotifierWatcher and StatusNotifierHost:
Applications using SNI include: Discord, Slack, modern Qt applications, and GNOME applications with the AppIndicator extension.
garbar ships with nine built-in modules. Each module independently manages its state and provides output as styled text blocks. Modules can respond to click and scroll events for interactivity.
Displays workspace indicators with distinct styling for focused, visible, unfocused, and urgent states. Integrates with gar (and i3/sway) via the i3-IPC protocol for real-time workspace state updates.
modules = (
workspaces = (
show_empty = false,
show_urgent = true,
pin_workspaces = false,
focused = (
background = "transparent",
foreground = "#ffffff",
underline = ( width = 2, color = "#89b4fa" ),
),
unfocused = (
background = "transparent",
foreground = "#666666",
),
urgent = (
background = "#f38ba8",
foreground = "#1e1e2e",
),
),
) Displays the title of the currently focused window. Updates instantly when focus changes by watching the _NET_ACTIVE_WINDOW property on the root window.
modules = (
window_title = (
max_length = 50,
ellipsis = "...",
empty_text = "Desktop",
show_icon = true,
icon_spacing = 8,
),
) Real-time CPU usage monitoring. Reads /proc/stat to calculate usage percentage with optional exponential moving average smoothing to prevent jittery display.
modules = (
cpu = (
format = " (usage)%",
interval = 2,
warning_threshold = 70,
critical_threshold = 90,
warning_foreground = "#f9e2af",
critical_foreground = "#f38ba8",
graph = (
enabled = true,
width = 50,
style = "bars",
color = "#89b4fa",
),
),
) RAM usage monitoring with percentage and absolute value display options. Click to toggle between formats.
modules = (
memory = (
format = " (percent)%",
format_alt = " (used)/(total)",
interval = 5,
warning_threshold = 80,
critical_threshold = 95,
warning_foreground = "#f9e2af",
critical_foreground = "#f38ba8",
),
) Battery status with charging/discharging indicators, percentage, and time remaining estimates. Automatically selects the first battery device or uses the configured device path.
modules = (
battery = (
device = "auto",
format_charging = " (percent)%",
format_discharging = "(icon) (percent)%",
format_full = " Full",
icons = (
( threshold = 10, icon = "" ),
( threshold = 25, icon = "" ),
( threshold = 50, icon = "" ),
( threshold = 75, icon = "" ),
( threshold = 100, icon = "" ),
),
low_threshold = 15,
low_animation = "blink",
low_foreground = "#f38ba8",
),
) Date and time display with strftime format strings. Click to toggle between primary and alternate formats.
modules = (
datetime = (
format = " %a %b %d %H:%M",
format_alt = " %Y-%m-%d %H:%M:%S",
interval = 1,
tooltip = true,
),
) System tray hosting both XEmbed (legacy) and StatusNotifierItem (modern) icons. XEmbed icons are embedded as X11 child windows; SNI icons are rendered directly via Cairo.
modules = (
tray = (
icon_size = 18,
spacing = 8,
padding = ( left = 4, right = 4 ),
),
) Execute custom shell scripts and display their output. Enables integration with external tools and custom status indicators.
modules = (
-- Named script modules use script:name syntax in modules_right
script = (
weather = (
exec = "~/scripts/weather.sh",
interval = 600,
format = "",
click_left = "xdg-open https://weather.com",
),
spotify = (
exec = "playerctl metadata --format '(artist) - (title)'",
interval = 5,
click_left = "playerctl play-pause",
scroll_up = "playerctl next",
scroll_down = "playerctl previous",
),
),
)
-- Reference in modules_right as "script:weather", "script:spotify"
modules_right = ( "script:weather", "script:spotify", "datetime" ) Trigger for the gartray quick settings panel. Displays a gear icon that changes color when the panel is open.
modules = (
quick_settings = (
enabled = true,
icon = "gear",
icon_active = "gear",
foreground = "#abb2bf",
active_foreground = "#61afef",
),
) garbarctl is the command-line control tool for garbar. It communicates with the running daemon over a Unix domain socket using JSON protocol.
| Option | Type | Default | Description |
|---|---|---|---|
show | - | - | Make the bar visible |
hide | - | - | Hide the bar |
toggle | - | - | Toggle bar visibility |
reload | - | - | Reload configuration from disk |
quit | - | - | Gracefully stop the daemon |
status | - | - | Get current bar status as JSON |
update MODULE | - | - | Force update a specific module |
Bind garbarctl commands to keys in gar's init.lua:
gar.keybinds = (
-- Toggle bar visibility with Super+B
( mods = ( "Mod4" ), key = "b", action = "spawn", args = "garbarctl toggle" ),
-- Reload bar config with Super+Shift+B
( mods = ( "Mod4", "Shift" ), key = "b", action = "spawn", args = "garbarctl reload" ),
) garbar's IPC uses JSON over a Unix domain socket at $XDG_RUNTIME_DIR/garbar.sock. Each message is a single JSON object terminated by a newline.
$XDG_RUNTIME_DIR/garbar.sock
# Typically: /run/user/1000/garbar.sock Commands are JSON objects with a "command" field and optional additional fields:
// Simple commands
("command": "show")
("command": "hide")
("command": "toggle")
("command": "reload")
("command": "quit")
("command": "status")
// Update specific module
("command": "update_module", "module": "cpu") Responses are JSON objects with success status and optional data:
// Success response
("success": true)
// Success with data (status command)
(
"success": true,
"data": (
"visible": true,
"width": 1920,
"height": 32,
"modules_left": ["workspaces", "window_title"],
"modules_center": [],
"modules_right": ["cpu", "memory", "datetime"]
)
)
// Error response
("success": false, "error": "Module not found: invalid_module") #!/usr/bin/env python3
import socket
import json
import os
sock_path = os.path.join(os.environ.get('XDG_RUNTIME_DIR', '/tmp'), 'garbar.sock')
def send_command(cmd):
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(sock_path)
sock.sendall((json.dumps(cmd) + '\n').encode())
response = sock.recv(4096).decode()
return json.loads(response)
# Toggle bar visibility
result = send_command(("command": "toggle"))
print("Success:", result["success"])
# Get status
status = send_command(("command": "status"))
print("Visible:", status["data"]["visible"]) garbar responds to Unix signals for lifecycle management:
The workspaces module communicates with gar (and i3/sway) using the i3-IPC protocol over a Unix socket.
garbar searches for the i3-IPC socket in this order:
Check if another instance is running and verify the socket/PID file:
After editing gar's init.lua, reload both gar and garbar:
Verify gar is setting EWMH properties and the i3-IPC socket exists:
Check _NET_ACTIVE_WINDOW property and verify garbar is subscribed to root window events:
Check if garbar has acquired the system tray selection and D-Bus is working:
Verify the bar window exists and has correct properties:
Transparency requires a compositor. Check if one is running and the opacity property is set:
Check RandR monitor configuration and verify garbar created bars for each monitor:
Check for modules with very short intervals or issues with subscription loops:
Verify the configured fonts are installed and Pango can find them:
Enable verbose logging to diagnose issues:
journalctl --user -u garbar.servicegarbar --verbose daemon