Tiling window manager for X11 with smart splits and Lua configuration
gar is the centerpiece of the gardesk desktop environment. Written in Rust with x11rb bindings, it provides a tiling window management experience with smart automatic splits, Lua scripting for configuration and keybindings, native integration with garbar status bar, and deep compositor integration via picom for visual effects including blur, shadows, rounded corners, and animations.
Automatic tiling with intelligent split direction based on window dimensions
Full scripting via mlua with gar.bind(), gar.set(), and gar.exec() APIs
Independent workspace management with EWMH compliance
RandR hotplug detection with per-monitor workspaces
picom integration for blur, shadows, rounded corners, and animations
Customizable gradient borders with direction control
Native status bar configuration via gar.bar Lua table
Seamless integration with garterm, garlaunch, garlock, garbg, and more
gar requires the following runtime dependencies:
Copy the example configuration to your config directory:
For startx-based sessions, add gar to your .xinitrc:
gar installs a desktop entry at /usr/share/xsessions/gar.desktop. Select "gar" from your display manager's session menu.
On first launch, use Mod+Return to open a terminal.
Mod is the Super/Windows key by default. Press Mod+Shift+r to reload config after making changes.
gar is configured via Lua script at ~/.config/gar/init.lua.
Use gar.set(key, value) for settings and
gar.bind(keys, action) for keybindings.
| Option | Type | Default | Description |
|---|---|---|---|
border_width | - | 2 | Window border width in pixels |
border_color_focused | - | "#5294e2" | Border color for focused window (hex) |
border_color_unfocused | - | "#2d2d2d" | Border color for unfocused windows (hex) |
gap_inner | - | 12 | Gap between adjacent windows in pixels |
gap_outer | - | 16 | Gap between windows and screen edge in pixels |
| Option | Type | Default | Description |
|---|---|---|---|
border_gradient_enabled | - | false | Enable gradient borders |
border_gradient_start_focused | - | "#9b59b6" | Start color for focused gradient |
border_gradient_end_focused | - | "#3498db" | End color for focused gradient |
border_gradient_start_unfocused | - | "#4a235a" | Start color for unfocused gradient |
border_gradient_end_unfocused | - | "#1a5276" | End color for unfocused gradient |
border_gradient_direction | - | "diagonal" | Gradient direction: "vertical", "horizontal", "diagonal" |
| Option | Type | Default | Description |
|---|---|---|---|
titlebar_enabled | - | false | Enable window title bars |
titlebar_height | - | 20 | Title bar height in pixels |
titlebar_color_focused | - | "#3d3d3d" | Title bar color for focused window |
titlebar_color_unfocused | - | "#2d2d2d" | Title bar color for unfocused windows |
titlebar_text_color | - | "#ffffff" | Title bar text color |
These settings auto-generate ~/.config/gar/picom.conf. Changes apply on reload.
| Option | Type | Default | Description |
|---|---|---|---|
compositor | - | "picom" | Compositor to use: "picom", "garchomp", or "none" |
corner_radius | - | 18 | Window corner radius in pixels (0 = square) |
blur_enabled | - | true | Enable window blur effect |
blur_method | - | "dual_kawase" | Blur algorithm: "gaussian", "dual_kawase", "box" |
blur_strength | - | 5 | Blur intensity (1-20 for dual_kawase) |
shadow_enabled | - | true | Enable drop shadows |
shadow_radius | - | 12 | Shadow blur radius |
shadow_opacity | - | 0.75 | Shadow opacity (0.0-1.0) |
shadow_offset_x | - | -7 | Horizontal shadow offset |
shadow_offset_y | - | -7 | Vertical shadow offset |
opacity_focused | - | 0.94 | Opacity for focused windows |
opacity_unfocused | - | 0.6 | Opacity for unfocused windows |
fade_enabled | - | true | Enable fade animations |
fade_delta | - | 3 | Fade animation speed |
| Option | Type | Default | Description |
|---|---|---|---|
animation_open | - | "appear" | Open animation: "slide-in", "fly-in", "appear", "none" |
animation_close | - | "disappear" | Close animation: "slide-out", "fly-out", "disappear", "none" |
animation_duration | - | 0.15 | Animation duration in seconds |
| Option | Type | Default | Description |
|---|---|---|---|
follow_window_on_move | - | true | Follow window when moving to another workspace |
focus_follows_mouse | - | false | Focus window under cursor automatically |
mouse_follows_focus | - | false | Warp mouse to focused window center |
| Option | Type | Default | Description |
|---|---|---|---|
screen_timeout_enabled | - | false | Enable automatic screen blanking |
screen_timeout | - | 300 | Seconds of inactivity before screen blanks |
| Option | Type | Default | Description |
|---|---|---|---|
monitor_order | - | auto-detected | Table of monitor names in left-to-right order |
bar_height | - | 0 | Reserved space for status bar (pixels) |
Match windows by class, instance, or title and apply automatic actions:
| Option | Type | Default | Description |
|---|---|---|---|
class | - | - | Match by WM_CLASS class name (e.g., "Firefox") |
instance | - | - | Match by WM_CLASS instance name |
title | - | - | Match by window title |
Available rule actions:
| Option | Type | Default | Description |
|---|---|---|---|
floating | - | nil | Force window to floating mode |
workspace | - | nil | Assign window to specific workspace |
-- Force dialogs to float
gar.rule({ class = "Dialog" }, { floating = true })
-- Send Spotify to workspace 9
gar.rule({ class = "Spotify" }, { workspace = 9 })
-- Firefox Picture-in-Picture floats
gar.rule({ title = "Picture-in-Picture" }, { floating = true }) Apply compositor effects to specific windows using picom match syntax:
| Option | Type | Default | Description |
|---|---|---|---|
match | - | - | picom match expression (required) |
corner_radius | - | nil | Per-window corner radius override |
opacity | - | nil | Per-window opacity override |
shadow | - | nil | Enable/disable shadow for this window |
blur_background | - | nil | Enable/disable blur for this window |
shader | - | nil | Custom GLSL shader path |
-- Transparent terminal with blur
gar.picom_rule({
match = "class_g = 'Alacritty'",
blur_background = true,
opacity = 0.92,
})
-- Firefox with different corner radius
gar.picom_rule({
match = "class_g = 'firefox'",
corner_radius = 8,
})
-- Disable shadow for popup menus
gar.picom_rule({
match = "window_type = 'popup_menu'",
shadow = false,
}) -- Appearance
gar.set("border_width", 2)
gar.set("border_color_focused", "#5294e2")
gar.set("border_color_unfocused", "#2d2d2d")
gar.set("gap_inner", 12)
gar.set("gap_outer", 16)
-- Gradient borders
gar.set("border_gradient_enabled", true)
gar.set("border_gradient_start_focused", "#9b59b6")
gar.set("border_gradient_end_focused", "#3498db")
gar.set("border_gradient_direction", "diagonal")
-- Visual effects
gar.set("corner_radius", 18)
gar.set("blur_enabled", true)
gar.set("shadow_enabled", true)
gar.set("opacity_focused", 0.94)
gar.set("opacity_unfocused", 0.6)
-- Autostart
gar.exec_once("garbg set ~/Pictures/wallpaper.png")
gar.exec_once("garlock daemon")
gar.exec_once("garclip daemon --foreground")
-- Keybindings
local mod = "mod"
gar.bind(mod .. "+Return", function()
gar.exec("alacritty || kitty || xterm")
end)
gar.bind(mod .. "+q", gar.close_window)
gar.bind(mod .. "+Shift+r", gar.reload)
-- Focus (vim keys)
gar.bind(mod .. "+h", gar.focus("left"))
gar.bind(mod .. "+l", gar.focus("right"))
gar.bind(mod .. "+k", gar.focus("up"))
gar.bind(mod .. "+j", gar.focus("down"))
-- Workspaces
for i = 1, 9 do
gar.bind(mod .. "+" .. i, gar.workspace(i))
gar.bind(mod .. "+shift+" .. i, gar.move_to_workspace(i))
end gar is a tiling window manager for X11 designed to be the foundation of the gardesk desktop environment. It automatically arranges windows in a non-overlapping layout, maximizing screen real estate while maintaining keyboard-driven navigation.
Key design principles:
gar is built with a modular architecture separating X11 communication, window management logic, input handling, and IPC into distinct components.
gar uses the x11rb crate for type-safe X11 protocol communication. On startup, it:
The main event loop handles X11 events, IPC commands, and signal interrupts:
Windows are organized in a binary tree structure per workspace. Each node is either:
This structure enables efficient resize operations and automatic layout rebalancing.
Configuration is powered by mlua with Lua 5.4. The gar global table provides:
gar.set(key, value) - Set configuration optiongar.bind(keys, fn) - Bind key combination to functiongar.exec(cmd) - Execute shell commandgar.exec_once(cmd) - Execute once per session (autostart)gar.focus(dir) - Focus window in directiongar.swap(dir) - Swap with neighborgar.workspace(n) - Switch to workspacegar uses a binary space partitioning approach where each new window splits the focused container.
When a new window is created, gar determines the split direction based on the focused container's aspect ratio:
This produces balanced layouts without manual direction specification.
Use Mod+Ctrl+h/j/k/l to resize windows. Resize operations adjust
the split ratio of the parent container, affecting both siblings proportionally.
Press Mod+e to reset all split ratios to 50/50, making all
windows in the current workspace equal size.
gar provides 10 workspaces per monitor, numbered 1-10 (0 key for workspace 10).
Switch workspaces with Mod+1 through Mod+0.
Each workspace maintains its own window tree independently.
Move the focused window to another workspace with Mod+Shift+1 through
Mod+Shift+0. If follow_window_on_move
is enabled, focus follows the window.
Use Mod+Tab and Mod+Shift+Tab
to cycle through workspaces sequentially.
gar supports multiple monitors with independent workspace sets per output.
Monitor configuration is detected via X RandR extension. gar responds to hotplug events for monitor connect/disconnect.
Use Mod+, and Mod+.
to move focus between monitors (previous/next).
Move windows between monitors with Mod+Shift+, and
Mod+Shift+..
Override automatic monitor detection order with monitor_order:
-- Specify left-to-right monitor order
gar.set("monitor_order", { "DP-1", "eDP-1", "HDMI-1" })
This affects how Mod+, and Mod+. navigate between monitors.
Toggle floating mode with Mod+f. Floating windows:
Mod+`
Fullscreen mode (Mod+Shift+f) temporarily removes a window from tiling
and maximizes it to the full monitor dimensions.
Automatically apply settings to windows based on their properties using gar.rule().
Match windows using WM_CLASS (class/instance) or window title:
-- Use xprop to find window class/instance
-- class = WM_CLASS second value
-- instance = WM_CLASS first value
gar.rule({ class = "Firefox" }, { floating = false })
gar.rule({ instance = "Navigator" }, { workspace = 2 })
gar.rule({ title = "Picture-in-Picture" }, { floating = true }) Available actions when a rule matches:
floating = true/false - Force floating or tiled modeworkspace = n - Send window to specific workspace-- Example: Music apps to workspace 9
gar.rule({ class = "Spotify" }, { workspace = 9 })
gar.rule({ class = "cider" }, { workspace = 9, floating = false })
gar supports multiple compositors for visual effects. Set with gar.set("compositor", "..."):
gar auto-generates ~/.config/gar/picom.conf from your Lua settings.
The configuration is regenerated on reload and picom is signaled to reread it.
Key picom settings controlled via gar.set():
corner_radius - Window corner roundingblur_enabled, blur_method, blur_strength - Background blurshadow_enabled, shadow_radius, shadow_opacity - Drop shadowsopacity_focused, opacity_unfocused - Window transparency
Apply compositor effects to specific windows with gar.picom_rule():
-- Terminal with blur
gar.picom_rule({
match = "class_g = 'Alacritty'",
blur_background = true,
opacity = 0.92,
})
-- Disable shadows for panels
gar.picom_rule({
match = "window_type = 'dock'",
shadow = false,
})
Match syntax uses picom expressions: class_g (class), class_i (instance),
name (title), window_type.
Window open/close animations require picom v12 or newer:
-- Animation settings
gar.set("animation_open", "fly-in") -- slide-in, fly-in, appear, none
gar.set("animation_close", "fly-out") -- slide-out, fly-out, disappear, none
gar.set("animation_duration", 0.15) -- seconds Apply GLSL shaders globally or per-window (requires picom GLX backend):
-- Global shader
gar.set("picom_shader", "~/.config/gar/shaders/focused-glow.glsl")
-- Per-window shader
gar.picom_rule({
match = "class_g = 'mpv'",
shader = "~/.config/gar/shaders/vibrance.glsl",
}) gar integrates deeply with the gardesk desktop environment components through Lua configuration tables and native IPC.
Define the gar.bar table to enable automatic garbar management:
gar.bar = {
height = 32,
position = "top",
background = "#1a1a1a",
foreground = "#ffffff",
modules_left = { "workspaces", "window_title" },
modules_right = { "cpu", "memory", "datetime" },
}
When gar.bar is present, gar spawns garbar automatically and reserves
screen space for it. See garbar documentation for full configuration.
Define the gar.terminal table for garterm configuration:
gar.terminal = {
shell = "/usr/bin/fish",
font = { family = "JetBrainsMono Nerd Font", size = 20.0 },
colors = { preset = "catpuccin-mocha" },
sessions = {
["dev"] = {
tabs = {
{ title = "Editor", cwd = "~/project", cmd = "nvim" },
{ title = "Server", cwd = "~/project", cmd = "npm run dev" },
},
},
},
} See garterm documentation for full configuration.
Define the gar.notification table to enable garnotify:
gar.notification = {
position = "top-right",
width = 300,
timeout = 5000,
} When present, gar spawns garnotify automatically for D-Bus notifications.
gar tracks processes spawned via gar.exec() and gar.exec_once().
On exit, gar sends SIGTERM to all spawned process groups.
gar.exec(cmd) - Run command every time (on keybind, etc.)gar.exec_once(cmd) - Run only once per session (autostart)
Both functions spawn processes as their own process group leaders, so killing the group terminates
child processes too (e.g., sh -c "garterm" kills both sh and garterm).
gar implements the Extended Window Manager Hints specification for interoperability with:
Supported EWMH atoms include:
_NET_SUPPORTED, _NET_SUPPORTING_WM_CHECK_NET_CLIENT_LIST, _NET_CLIENT_LIST_STACKING_NET_NUMBER_OF_DESKTOPS, _NET_CURRENT_DESKTOP_NET_ACTIVE_WINDOW, _NET_WM_STATE_NET_WM_WINDOW_TYPE, _NET_WORKAREA
These are the default keybindings from the example config. Mod is the Super/Windows key.
All keybindings can be customized in ~/.config/gar/init.lua.
| Keys | Action |
|---|---|
Mod+Return | Launch terminal (alacritty/kitty/foot/xterm) |
Mod+Shift+Return | Launch garterm |
Mod+q | Close focused window (graceful) |
Mod+Shift+q | Lock screen with garlock |
Mod+Shift+r | Reload configuration |
Mod+Shift+e | Exit gar (return to display manager) |
Mod+Shift+Escape | PANIC: Exit gar immediately |
Mod+h/j/k/l | Focus left/down/up/right (vim keys) |
Mod+Arrow | Focus in arrow direction |
Mod+Shift+h/j/k/l | Swap window left/down/up/right |
Mod+Shift+Arrow | Swap window in arrow direction |
Mod+Ctrl+h/j/k/l | Resize window |
Mod+Ctrl+Arrow | Resize window in arrow direction |
Mod+e | Equalize window sizes |
Mod+f | Toggle floating |
Mod+Shift+f | Toggle fullscreen |
Mod+` | Cycle floating windows |
Mod+1-9,0 | Switch to workspace 1-10 |
Mod+Shift+1-9,0 | Move window to workspace 1-10 |
Mod+Tab | Next workspace |
Mod+Shift+Tab | Previous workspace |
Mod+,/. | Focus prev/next monitor |
Mod+Shift+,/. | Move window to prev/next monitor |
Mod+Space | Launch garlaunch (drun mode) |
Mod+Shift+Space | Launch garlaunch (window mode) |
Mod+r | Launch garlaunch (run mode) |
Mod+p | Launch dmenu |
Mod+Escape | Lock screen (i3lock/swaylock/slock) |
Mod+Shift+v | Clipboard history (garclip-picker) |
Mod+Shift+p | Start wallpaper slideshow (garbg) |
Ctrl+Shift+3 | Screenshot with annotation (garshot) |
Ctrl+Shift+4 | Screenshot selection (garshot) |
Ctrl+Shift+5 | Screenshot full screen (garshot) |
Ctrl+Shift+6 | Screenshot active window (garshot) |
Print | Screenshot full screen (scrot/maim) |
Mod+Print | Screenshot selection (scrot/maim) |
XF86Audio* | Volume controls (pactl/pamixer) |
XF86MonBrightness* | Brightness controls (brightnessctl/light) |
Define custom keybindings with gar.bind():
-- Launch specific app
gar.bind("mod+b", function()
gar.exec("firefox")
end)
-- Screenshot with garshot
gar.bind("ctrl+shift+4", function()
gar.exec("garshot select")
end)
-- Custom workspace naming
gar.bind("mod+alt+1", function()
gar.workspace(1)
gar.exec("notify-send 'Workspace: Main'")
end) Control gar at runtime via the garctl command-line tool:
| Command | Description |
|---|---|
focus <direction> | Focus window in direction (left/right/up/down) |
swap <direction> | Swap focused window with neighbor |
resize <direction> <amount> | Resize focused window (default amount: 0.05) |
workspace <n> | Switch to workspace n (1-10) |
move-to-workspace <n> | Move focused window to workspace n |
toggle-floating | Toggle floating state of focused window |
equalize | Make all windows in workspace equal size |
close | Close focused window (graceful WM_DELETE_WINDOW) |
focus-monitor <target> | Focus monitor: next, prev, left, right, or name |
move-to-monitor <target> | Move window to monitor |
reload | Reload configuration from init.lua |
exit | Exit gar (return to display manager) |
Get state information from the running window manager:
| Command | Description |
|---|---|
get-workspaces | Get workspace information as JSON (id, name, focused, window counts) |
get-focused | Get focused window information (id, workspace, floating status) |
get-tree | Get full window tree structure as JSON |
get-monitors | Get monitor information (name, geometry, workspaces, primary) |
Built-in actions for use with gar.bind():
| Action | Description |
|---|---|
gar.close_window | Graceful window close (WM_DELETE_WINDOW) |
gar.force_close_window | Force kill window without asking |
gar.reload | Reload configuration |
gar.exit | Exit gar window manager |
gar.equalize | Equalize all split ratios to 50/50 |
gar.toggle_floating | Toggle floating state of focused window |
gar.toggle_fullscreen | Toggle fullscreen state |
gar.cycle_floating | Cycle focus through floating windows |
gar.focus(dir) | Focus window in direction (left/right/up/down) |
gar.swap(dir) | Swap focused window with neighbor in direction |
gar.resize(dir, amount) | Resize split in direction by amount (0.0-1.0) |
gar.workspace(n) | Switch to workspace n (1-10) |
gar.workspace_next() | Switch to next workspace |
gar.workspace_prev() | Switch to previous workspace |
gar.move_to_workspace(n) | Move focused window to workspace n |
gar.focus_monitor(target) | Focus monitor: "next", "prev", or name |
gar.move_to_monitor(target) | Move window to monitor |
gar listens on Unix socket at $XDG_RUNTIME_DIR/gar.sock (or /tmp/gar.sock).
Send JSON commands directly:
All responses follow this JSON format:
// Success response
{
"success": true,
"data": { ... } // optional payload
}
// Error response
{
"success": false,
"error": "Error message"
} Another window manager has already claimed SubstructureRedirect. Ensure no other WM is running.
Ensure picom is installed and started. gar doesn't auto-start picom.
Or add to your init.lua: gar.exec_once("picom")
Check for syntax errors in init.lua. View logs for errors:
garbar must be started separately or via gar.bar table:
Or define gar.bar = { ... } in init.lua for automatic management.
Check logs at /tmp/gar.log for detailed debug output.
Set RUST_LOG=debug for verbose logging.