gar
Tiling Window Manager
An event-driven X11 tiling window manager with a binary tree layout engine. gar gives you full control over your windows with intuitive keyboard-driven navigation and a powerful Lua configuration system.
Features
- Binary tree layout with smart splits
- Full Lua 5.4 scripting for configuration
- Multi-monitor support with per-monitor workspaces
- EWMH compliant for compatibility with bars and tools
- Unix socket IPC for runtime control via garctl
- Customizable gaps, borders, and colors
- Window rules for automatic placement
- Keyboard and mouse input handling
- Integration with picom compositor
- Session management with systemd
Quick Start Guide
Get up and running with gar in just a few steps. This guide covers installation, basic configuration, and essential keybindings.
1. Installation
Install gar using the unified installer or build from source:
2. First Launch
Start gar from your display manager or directly from a TTY:
Or select "gar" from your display manager's session menu.
3. Essential Keybindings
The default configuration uses Super (Windows key) as the modifier:
| Keybinding | Action |
|---|---|
| Super + Return | Open terminal |
| Super + q | Close focused window |
| Super + h/j/k/l | Focus left/down/up/right |
| Super + Shift + h/l | Swap window left/right |
| Super + 1-9 | Switch to workspace |
| Super + Shift + 1-9 | Move window to workspace |
| Super + Shift + r | Reload configuration |
| Super + Shift + Escape | Exit gar |
4. Configuration File
Edit your config at ~/.config/gar/init.lua. Changes apply after reload (Super + Shift + r).
5. Control with garctl
Use garctl to control gar from scripts or the command line:
Configuration Reference
gar uses Lua for configuration, providing full scripting capabilities. The config file is located at ~/.config/gar/init.lua.
Minimal Configuration
-- Minimal gar configuration
local mod = "mod" -- Super/Windows key
-- Appearance
gar.set("border_width", 2)
gar.set("border_color_focused", "#5294e2")
gar.set("border_color_unfocused", "#2d2d2d")
gar.set("gap_inner", 8)
gar.set("gap_outer", 8)
-- Essential keybindings
gar.bind(mod .. "+Return", function() gar.exec("alacritty") end)
gar.bind(mod .. "+q", gar.close_window)
gar.bind(mod .. "+shift+r", gar.reload)
gar.bind(mod .. "+shift+Escape", gar.exit)
-- Navigation (vim-style)
gar.bind(mod .. "+h", gar.focus("left"))
gar.bind(mod .. "+j", gar.focus("down"))
gar.bind(mod .. "+k", gar.focus("up"))
gar.bind(mod .. "+l", gar.focus("right"))
-- Workspaces 1-9
for i = 1, 9 do
gar.bind(mod .. "+" .. i, gar.workspace(i))
gar.bind(mod .. "+shift+" .. i, gar.move_to_workspace(i))
end Full Configuration Example
-- gar configuration
local mod = "mod" -- Use "alt" for testing in Xephyr
-- ═══════════════════════════════════════════
-- Appearance
-- ═══════════════════════════════════════════
-- Borders
gar.set("border_width", 2)
gar.set("border_color_focused", "#5294e2")
gar.set("border_color_unfocused", "#2d2d2d")
gar.set("border_color_urgent", "#ff5555")
-- Gaps
gar.set("gap_inner", 8)
gar.set("gap_outer", 8)
-- Title bars (optional)
gar.set("titlebar_enabled", false)
gar.set("titlebar_height", 20)
-- ═══════════════════════════════════════════
-- Behavior
-- ═══════════════════════════════════════════
gar.set("follow_window_on_move", true)
gar.set("mouse_follows_focus", false)
-- ═══════════════════════════════════════════
-- Picom Compositor
-- ═══════════════════════════════════════════
gar.set("corner_radius", 12)
gar.set("blur_enabled", true)
gar.set("blur_method", "dual_kawase")
gar.set("blur_strength", 5)
gar.set("shadow_enabled", true)
gar.set("opacity_focused", 1.0)
gar.set("opacity_unfocused", 0.95)
-- Animations (picom v12+)
gar.set("animation_open", "fly-in")
gar.set("animation_close", "fly-out")
gar.set("animation_duration", 0.2)
-- ═══════════════════════════════════════════
-- Keybindings
-- ═══════════════════════════════════════════
-- Launchers
gar.bind(mod .. "+Return", function()
gar.exec("alacritty || kitty || foot || xterm")
end)
gar.bind(mod .. "+space", function()
gar.exec("garlaunch")
end)
-- Window management
gar.bind(mod .. "+q", gar.close_window)
gar.bind(mod .. "+shift+q", gar.force_close_window)
gar.bind(mod .. "+f", gar.toggle_fullscreen)
gar.bind(mod .. "+shift+space", gar.toggle_floating)
gar.bind(mod .. "+e", gar.equalize)
-- Navigation
gar.bind(mod .. "+h", gar.focus("left"))
gar.bind(mod .. "+j", gar.focus("down"))
gar.bind(mod .. "+k", gar.focus("up"))
gar.bind(mod .. "+l", gar.focus("right"))
-- Swap windows
gar.bind(mod .. "+shift+h", gar.swap("left"))
gar.bind(mod .. "+shift+j", gar.swap("down"))
gar.bind(mod .. "+shift+k", gar.swap("up"))
gar.bind(mod .. "+shift+l", gar.swap("right"))
-- Resize splits
gar.bind(mod .. "+ctrl+h", gar.resize("left", 0.05))
gar.bind(mod .. "+ctrl+j", gar.resize("down", 0.05))
gar.bind(mod .. "+ctrl+k", gar.resize("up", 0.05))
gar.bind(mod .. "+ctrl+l", gar.resize("right", 0.05))
-- 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.bind(mod .. "+0", gar.workspace(10))
gar.bind(mod .. "+shift+0", gar.move_to_workspace(10))
-- Multi-monitor
gar.bind(mod .. "+comma", gar.focus_monitor("prev"))
gar.bind(mod .. "+period", gar.focus_monitor("next"))
gar.bind(mod .. "+shift+comma", gar.move_to_monitor("prev"))
gar.bind(mod .. "+shift+period", gar.move_to_monitor("next"))
-- Session
gar.bind(mod .. "+shift+r", gar.reload)
gar.bind(mod .. "+shift+Escape", gar.exit)
-- Screenshots
gar.bind("Print", function()
gar.exec("scrot ~/Pictures/screenshot-%Y%m%d-%H%M%S.png")
end)
-- ═══════════════════════════════════════════
-- Window Rules
-- ═══════════════════════════════════════════
-- Float dialogs and utilities
gar.rule({ class = "Pavucontrol" }, { floating = true })
gar.rule({ class = "Nm-connection-editor" }, { floating = true })
gar.rule({ title = "File Upload" }, { floating = true })
-- Assign apps to workspaces
gar.rule({ class = "Firefox" }, { workspace = 2 })
gar.rule({ class = "Slack" }, { workspace = 4 })
gar.rule({ class = "Spotify" }, { workspace = 9 })
-- ═══════════════════════════════════════════
-- Startup Applications
-- ═══════════════════════════════════════════
gar.exec_once("nm-applet")
gar.exec_once("blueman-applet") Configuration Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
| border_width | int | 2 | Border width in pixels |
| border_color_focused | hex | #5294e2 | Focused window border |
| border_color_unfocused | hex | #2d2d2d | Unfocused window border |
| gap_inner | int | 8 | Gap between windows |
| gap_outer | int | 8 | Gap at screen edges |
| follow_window_on_move | bool | true | Follow window when moving to workspace |
| corner_radius | int | 12 | Window corner radius (picom) |
| blur_enabled | bool | true | Enable blur effect |
Overview
gar is an event-driven X11 tiling window manager written in Rust. It uses a binary space partition (BSP) tree layout engine to automatically arrange windows, while providing full control through Lua scripting.
Architecture
- Layout Engine: BSP tree with smart splitting (wider containers split vertically, taller split horizontally)
- Configuration: Embedded Lua 5.4 via mlua
- X11 Backend: x11rb for zero-copy event handling
- IPC: JSON over Unix domain sockets
- EWMH: Full compliance for status bar integration
File Locations
| ~/.config/gar/init.lua | Configuration file |
| ~/.config/gar/picom.conf | Auto-generated picom config |
| /tmp/gar.log | Log file |
| $XDG_RUNTIME_DIR/gar.sock | IPC socket |
Keybindings
Use gar.bind(keyspec, action) to register keybindings. The keyspec format is "modifier+key".
-- Simple keybinding with action
gar.bind("mod+q", gar.close_window)
-- Keybinding with Lua callback
gar.bind("mod+Return", function()
gar.exec("alacritty")
end)
-- Parameterized action
gar.bind("mod+h", gar.focus("left"))
-- Multiple modifiers
gar.bind("mod+shift+q", gar.force_close_window) Modifier Keys
| Modifier | Aliases | Key |
|---|---|---|
| mod | super, mod4 | Super/Windows key |
| alt | mod1 | Alt key |
| shift | - | Shift key |
| ctrl | control | Control key |
Available Actions
Simple Actions
| gar.close_window | Close focused window |
| gar.force_close_window | Force kill window |
| gar.toggle_floating | Toggle floating state |
| gar.toggle_fullscreen | Toggle fullscreen |
| gar.equalize | Reset all splits to 50/50 |
| gar.reload | Reload configuration |
| gar.exit | Exit gar |
Parameterized Actions
| gar.focus(dir) | Focus window in direction (left/right/up/down) |
| gar.swap(dir) | Swap with window in direction |
| gar.resize(dir, amt) | Resize split (amount default: 0.05) |
| gar.workspace(n) | Switch to workspace n (1-10) |
| gar.move_to_workspace(n) | Move window to workspace |
| gar.focus_monitor(target) | Focus monitor (next/prev/name) |
| gar.move_to_monitor(target) | Move window to monitor |
Appearance
Configure the visual appearance of windows using gar.set().
Borders
-- Border settings
gar.set("border_width", 2) -- Pixels
gar.set("border_color_focused", "#5294e2") -- Focused window
gar.set("border_color_unfocused", "#2d2d2d") -- Unfocused windows
gar.set("border_color_urgent", "#ff5555") -- Urgent windows
-- Gradient borders (requires titlebar_enabled)
gar.set("border_gradient_enabled", true)
gar.set("border_gradient_start_focused", "#5294e2")
gar.set("border_gradient_end_focused", "#1a5fb4")
gar.set("border_gradient_direction", "vertical") -- or "horizontal", "diagonal" Gaps
-- Gap settings
gar.set("gap_inner", 8) -- Between windows (pixels)
gar.set("gap_outer", 8) -- At screen edges (pixels)
-- Set to 0 for a compact look
gar.set("gap_inner", 0)
gar.set("gap_outer", 0) Title Bars
Title bars are optional frame windows drawn by gar. They enable gradient borders and window titles.
-- Enable title bars
gar.set("titlebar_enabled", true)
gar.set("titlebar_height", 20)
gar.set("titlebar_color_focused", "#3d3d3d")
gar.set("titlebar_color_unfocused", "#2d2d2d")
gar.set("titlebar_text_color", "#ffffff") Window Rules
Automatically configure windows based on their class, instance, or title.
-- Syntax: gar.rule(match_criteria, actions)
-- Float specific windows
gar.rule({ class = "Pavucontrol" }, { floating = true })
gar.rule({ title = "File Upload" }, { floating = true })
-- Assign to workspaces
gar.rule({ class = "Firefox" }, { workspace = 2 })
gar.rule({ class = "Spotify" }, { workspace = 9, floating = true })
-- Match by instance
gar.rule({ instance = "dialog" }, { floating = true }) Match Criteria
class- WM_CLASS (case-insensitive substring)instance- Window instancetitle- Window title (_NET_WM_NAME)
Actions
floating = true/false- Float by defaultworkspace = n- Open on workspace n
Workspaces
gar provides 10 workspaces (numbered 1-10). Each monitor has its own set of workspaces in multi-monitor setups.
-- Switch to workspace
gar.bind("mod+1", gar.workspace(1))
gar.bind("mod+2", gar.workspace(2))
-- Move window to workspace
gar.bind("mod+shift+1", gar.move_to_workspace(1))
-- Workspace navigation
gar.bind("mod+bracketleft", gar.workspace_prev())
gar.bind("mod+bracketright", gar.workspace_next())
-- Loop for workspaces 1-9
for i = 1, 9 do
gar.bind("mod+" .. i, gar.workspace(i))
gar.bind("mod+shift+" .. i, gar.move_to_workspace(i))
end Multi-Monitor
gar supports multiple monitors with i3-style workspace assignment. Each workspace belongs to one monitor, and workspaces are per-monitor.
-- Focus monitor
gar.bind("mod+comma", gar.focus_monitor("prev"))
gar.bind("mod+period", gar.focus_monitor("next"))
gar.bind("mod+m", gar.focus_monitor("HDMI-1")) -- By name
-- Move window to monitor
gar.bind("mod+shift+comma", gar.move_to_monitor("prev"))
gar.bind("mod+shift+period", gar.move_to_monitor("next")) Monitor Detection
gar uses RandR to detect monitors. Use xrandr to see monitor names and configure layouts.
Picom Integration
gar auto-generates a picom configuration at ~/.config/gar/picom.conf based on your Lua settings.
-- Compositor settings
gar.set("corner_radius", 12)
gar.set("blur_enabled", true)
gar.set("blur_method", "dual_kawase") -- or "gaussian", "box"
gar.set("blur_strength", 5) -- 1-20 for dual_kawase
-- Shadows
gar.set("shadow_enabled", true)
gar.set("shadow_radius", 12)
gar.set("shadow_opacity", 0.75)
-- Opacity
gar.set("opacity_focused", 1.0)
gar.set("opacity_unfocused", 0.95)
-- Animations (picom v12+)
gar.set("animation_open", "fly-in") -- "slide-in", "appear", "none"
gar.set("animation_close", "fly-out") -- "slide-out", "disappear", "none"
gar.set("animation_duration", 0.2)
-- Per-window rules
gar.picom_rule({
match = "class_g = 'Firefox'",
corner_radius = 0,
opacity = 1.0
}) Common Workflows
Practical examples of using gar with other gardesk components for common desktop tasks.
Desktop Session Startup
Configure your init.lua to launch components on startup:
-- ~/.config/gar/init.lua
-- Start garbar automatically (gar spawns it if configured)
gar.bar = {}
-- Startup applications (run once per session)
gar.exec_once("garbg daemon") -- Wallpaper daemon
gar.exec_once("nm-applet") -- Network applet
gar.exec_once("blueman-applet") -- Bluetooth
gar.exec_once("dunst") -- Notifications
-- Set initial wallpaper
gar.exec_once("garbg set ~/Pictures/wallpaper.png") Quick Application Launcher
Use garlaunch for fast application launching with fuzzy search:
-- Bind garlaunch modes
gar.bind("mod+space", function() gar.exec("garlaunch") end)
gar.bind("mod+r", function() gar.exec("garlaunch --mode run") end)
gar.bind("mod+w", function() gar.exec("garlaunch --mode window") end) Lock Screen on Idle
Combine garlock with xidlehook for automatic screen locking:
Screenshot Workflow
-- Full screen screenshot
gar.bind("Print", function()
gar.exec("scrot ~/Pictures/screenshot-%Y%m%d-%H%M%S.png")
end)
-- Selection screenshot
gar.bind("shift+Print", function()
gar.exec("scrot -s ~/Pictures/screenshot-%Y%m%d-%H%M%S.png")
end)
-- Active window screenshot
gar.bind("mod+Print", function()
gar.exec("scrot -u ~/Pictures/screenshot-%Y%m%d-%H%M%S.png")
end) Volume Control
-- Media keys
gar.bind("XF86AudioRaiseVolume", function()
gar.exec("pactl set-sink-volume @DEFAULT_SINK@ +5%")
end)
gar.bind("XF86AudioLowerVolume", function()
gar.exec("pactl set-sink-volume @DEFAULT_SINK@ -5%")
end)
gar.bind("XF86AudioMute", function()
gar.exec("pactl set-sink-mute @DEFAULT_SINK@ toggle")
end) Scratchpad Terminal
Create a toggleable floating terminal:
-- Float terminal by class
gar.rule({ class = "scratchpad" }, { floating = true })
-- Toggle scratchpad (launch or focus)
gar.bind("mod+grave", function()
gar.exec("alacritty --class scratchpad")
end) IPC Protocol
gar exposes a Unix domain socket for inter-process communication. Use garctl or send JSON directly.
Socket Location
Command Reference
| Command | Arguments | Description |
|---|---|---|
| focus | left|right|up|down | Focus window in direction |
| swap | left|right|up|down | Swap windows |
| resize | direction [amount] | Resize split |
| workspace | 1-10 | Switch workspace |
| move_to_workspace | 1-10 | Move window |
| close | - | Close focused window |
| toggle_floating | - | Toggle floating state |
| reload | - | Reload config |
| exit | - | Exit gar |
| get_workspaces | - | Query workspace info |
| get_focused | - | Query focused window |
| get_tree | - | Query window tree |
| get_monitors | - | Query monitors |
JSON Protocol
Send JSON objects over the socket for scripting:
{"command": "focus", "args": {"direction": "left"}}
{"command": "workspace", "args": {"number": 3}}
{"command": "resize", "args": {"direction": "right", "amount": 0.05}}
{"command": "get_workspaces"} // Success
{"success": true, "data": null}
// Success with data
{"success": true, "data": [
{"id": 1, "name": "1", "focused": true, "tiled_count": 2}
]}
// Error
{"success": false, "error": "No focused window"} Scripting Example
#!/bin/bash
# Get current workspace number
SOCK="${XDG_RUNTIME_DIR:-/tmp}/gar.sock"
echo '{"command": "get_workspaces"}' | nc -U "$SOCK" | \
jq -r '.data[] | select(.focused) | .id' Troubleshooting
gar won't start
Check the log file for errors:
Keybindings not working
Verify your config loads without errors:
Use xev to verify key names match your keymap.
Windows not tiling correctly
Some apps set incorrect window types. Check with:
Add a window rule to force tiling or floating.
garctl: connection refused
The IPC socket may not exist:
Picom effects not working
Check picom is running with the right config:
Ensure your GPU supports GLX. Use glxinfo | grep "direct rendering".
Testing in Xephyr
Test safely without replacing your current WM:
Use alt as modifier in Xephyr to avoid conflicts.
Multi-monitor issues
Verify monitor configuration:
Configure monitors with xrandr or arandr before starting gar.