| 1 | -- gar default configuration |
| 2 | -- Copy to ~/.config/gar/init.lua to customize |
| 3 | |
| 4 | -- Autostart applications (only run once per session) |
| 5 | -- gar.exec_once("random-bg cold") -- Pick random wallpaper from a themed directory |
| 6 | -- garbg daemon is managed by systemd (garbg.service) with internal X11 retry |
| 7 | gar.exec_once("garbg set ~/Pictures/background/cold/ --random") |
| 8 | |
| 9 | -- Screen locker daemon |
| 10 | gar.exec_once("garlock daemon") |
| 11 | |
| 12 | -- Clipboard manager daemon |
| 13 | gar.exec_once("garclip daemon --foreground") |
| 14 | |
| 15 | -- Uncomment the ones you want: |
| 16 | -- gar.exec_once("picom") -- Compositor (for transparency/shadows) |
| 17 | -- gar.exec_once("dunst") -- Notification daemon |
| 18 | -- gar.exec_once("nm-applet") -- NetworkManager tray icon |
| 19 | -- gar.exec_once("blueman-applet") -- Bluetooth tray icon |
| 20 | -- gar.exec_once("xss-lock -- i3lock -c 000000") -- Auto-lock on suspend |
| 21 | |
| 22 | -- Appearance |
| 23 | gar.set("border_width", 2) |
| 24 | gar.set("border_color_focused", "#5294e2") |
| 25 | gar.set("border_color_unfocused", "#2d2d2d") |
| 26 | gar.set("gap_inner", 12) |
| 27 | gar.set("gap_outer", 16) |
| 28 | |
| 29 | -- Visual Effects (picom compositor) |
| 30 | -- gar auto-generates ~/.config/gar/picom.conf from these settings |
| 31 | -- Changes take effect on reload (Mod+Shift+R) - picom is signaled automatically |
| 32 | gar.set("corner_radius", 18) -- 0 = square corners |
| 33 | gar.set("blur_enabled", true) |
| 34 | gar.set("blur_method", "dual_kawase") -- "gaussian", "dual_kawase", "box" |
| 35 | gar.set("blur_strength", 5) -- 1-20 for dual_kawase |
| 36 | gar.set("shadow_enabled", true) |
| 37 | gar.set("shadow_radius", 12) |
| 38 | gar.set("shadow_opacity", 0.75) |
| 39 | gar.set("shadow_offset_x", -7) |
| 40 | gar.set("shadow_offset_y", -7) |
| 41 | gar.set("opacity_focused", 0.94) |
| 42 | gar.set("opacity_unfocused", 0.6) -- Set to 0.9 to dim unfocused windows |
| 43 | gar.set("fade_enabled", true) |
| 44 | gar.set("fade_delta", 3) |
| 45 | |
| 46 | -- Title bars (disabled by default) |
| 47 | -- gar.set("titlebar_enabled", true) |
| 48 | -- gar.set("titlebar_height", 20) |
| 49 | -- gar.set("titlebar_color_focused", "#3d3d3d") |
| 50 | -- gar.set("titlebar_color_unfocused", "#2d2d2d") |
| 51 | -- gar.set("titlebar_text_color", "#ffffff") |
| 52 | |
| 53 | -- Border gradients (purple to blue theme) |
| 54 | -- direction options: "vertical", "horizontal", "diagonal" |
| 55 | gar.set("border_gradient_enabled", true) |
| 56 | gar.set("border_gradient_start_focused", "#9b59b6") -- Purple |
| 57 | gar.set("border_gradient_end_focused", "#3498db") -- Blue |
| 58 | gar.set("border_gradient_start_unfocused", "#4a235a") -- Dark purple |
| 59 | gar.set("border_gradient_end_unfocused", "#1a5276") -- Dark blue |
| 60 | gar.set("border_gradient_direction", "diagonal") |
| 61 | |
| 62 | -- Animations (picom v12+) |
| 63 | -- open options: "slide-in", "fly-in", "appear", "none" |
| 64 | -- close options: "slide-out", "fly-out", "disappear", "none" |
| 65 | gar.set("animation_open", "appear") |
| 66 | gar.set("animation_close", "disappear") |
| 67 | gar.set("animation_duration", 0.15) -- seconds |
| 68 | |
| 69 | -- Custom GLSL shader (picom, requires GLX backend) |
| 70 | -- Bundled shaders: ~/.config/gar/shaders/focused-glow.glsl, dim-unfocused.glsl |
| 71 | -- gar.set("picom_shader", "~/.config/gar/shaders/focused-glow.glsl") |
| 72 | |
| 73 | -- Per-window picom rules |
| 74 | -- NOTE: When using rules, picom ignores old-style options (inactive-opacity, shadow-exclude, etc.) |
| 75 | -- Only enable rules if you want full control via rules-based config |
| 76 | -- Available rule options: corner_radius, opacity, shadow, blur_background, shader |
| 77 | -- Match syntax: class_g = 'ClassName', class_i = 'instance', name = 'title', window_type = 'type' |
| 78 | -- gar.picom_rule({ |
| 79 | -- match = "class_g = 'Alacritty'", |
| 80 | -- blur_background = true, |
| 81 | -- opacity = 0.92, |
| 82 | -- }) |
| 83 | -- gar.picom_rule({ |
| 84 | -- match = "class_g = 'firefox'", |
| 85 | -- corner_radius = 8, |
| 86 | -- }) |
| 87 | |
| 88 | -- garbar status bar (native gar integration) |
| 89 | -- When this table is present, gar automatically spawns and manages garbar |
| 90 | gar.bar = { |
| 91 | height = 32, |
| 92 | position = "top", |
| 93 | background = "#1a1a1a", |
| 94 | foreground = "#ffffff", |
| 95 | opacity = 1.0, |
| 96 | fonts = { |
| 97 | "JetBrainsMono Nerd Font:size=10", |
| 98 | "Symbols Nerd Font:size=10", |
| 99 | }, |
| 100 | padding = { left = 8, right = 16, top = 0, bottom = 0 }, |
| 101 | |
| 102 | -- Module layout |
| 103 | modules_left = { "workspaces", "window_title" }, |
| 104 | modules_center = {}, |
| 105 | modules_right = { "almanta", "filesystem", "memory", "cpu", "battery", "wlan", "volume", "datetime" }, |
| 106 | |
| 107 | -- Module configurations |
| 108 | modules = { |
| 109 | workspaces = { |
| 110 | font_size = 11, |
| 111 | focused = { |
| 112 | foreground = "#ffffff", |
| 113 | background = "transparent", |
| 114 | underline = { width = 2, color = "#33ccff" }, |
| 115 | }, |
| 116 | unfocused = { |
| 117 | foreground = "#666666", |
| 118 | background = "transparent", |
| 119 | }, |
| 120 | urgent = { |
| 121 | foreground = "#ffffff", |
| 122 | background = "#ff5555", |
| 123 | }, |
| 124 | }, |
| 125 | window_title = { |
| 126 | max_length = 50, |
| 127 | empty_text = "Desktop", |
| 128 | }, |
| 129 | datetime = { |
| 130 | format = " %a %b %d %H:%M", |
| 131 | }, |
| 132 | cpu = { |
| 133 | format = " {usage}%", |
| 134 | }, |
| 135 | memory = { |
| 136 | format = " {percent}%", |
| 137 | }, |
| 138 | battery = { |
| 139 | device = "auto", |
| 140 | format_charging = " {percent}%", |
| 141 | format_discharging = " {percent}%", |
| 142 | format_full = " Full", |
| 143 | }, |
| 144 | -- Script modules (custom commands) |
| 145 | script = { |
| 146 | almanta = { |
| 147 | exec = [[ |
| 148 | HOST="espadon@almanta" |
| 149 | SSH_OPTS="-T -o BatchMode=yes -o ConnectTimeout=2 -o ConnectionAttempts=1" |
| 150 | if ! ssh $SSH_OPTS "$HOST" "echo ok" >/dev/null 2>&1; then |
| 151 | echo "almanta: down" |
| 152 | exit 0 |
| 153 | fi |
| 154 | load=$(ssh $SSH_OPTS "$HOST" "cut -d' ' -f1 /proc/loadavg" 2>/dev/null || echo "?") |
| 155 | mem=$(ssh $SSH_OPTS "$HOST" "free | grep '^Mem:' | awk '{printf \"%.0f\", (\$3/\$2)*100}'" 2>/dev/null || echo "?") |
| 156 | disk=$(ssh $SSH_OPTS "$HOST" "df / | tail -1 | awk '{print \$5}' | tr -d '%'" 2>/dev/null || echo "?") |
| 157 | echo "almanta L:$load M:$mem% D:$disk%" |
| 158 | ]], |
| 159 | interval = 30, |
| 160 | }, |
| 161 | filesystem = { |
| 162 | exec = [[df -h / | awk 'NR==2 {print " " $4}']], |
| 163 | interval = 60, |
| 164 | }, |
| 165 | wlan = { |
| 166 | exec = [[ |
| 167 | IFACE="wlp1s0f0" |
| 168 | if [ -d "/sys/class/net/$IFACE" ]; then |
| 169 | STATE=$(cat /sys/class/net/$IFACE/operstate 2>/dev/null) |
| 170 | if [ "$STATE" = "up" ]; then |
| 171 | ESSID=$(iwgetid -r 2>/dev/null || echo "") |
| 172 | IP=$(ip -4 addr show $IFACE 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1) |
| 173 | if [ -n "$ESSID" ]; then |
| 174 | echo " $ESSID $IP" |
| 175 | else |
| 176 | echo " connected" |
| 177 | fi |
| 178 | else |
| 179 | echo " offline" |
| 180 | fi |
| 181 | else |
| 182 | echo " N/A" |
| 183 | fi |
| 184 | ]], |
| 185 | interval = 5, |
| 186 | }, |
| 187 | volume = { |
| 188 | exec = [[ |
| 189 | if command -v pamixer >/dev/null 2>&1; then |
| 190 | if pamixer --get-mute | grep -q true; then |
| 191 | echo " muted" |
| 192 | else |
| 193 | VOL=$(pamixer --get-volume) |
| 194 | echo " $VOL%" |
| 195 | fi |
| 196 | elif command -v pactl >/dev/null 2>&1; then |
| 197 | VOL=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oP '\d+%' | head -1) |
| 198 | MUTE=$(pactl get-sink-mute @DEFAULT_SINK@ | grep -oP 'yes|no') |
| 199 | if [ "$MUTE" = "yes" ]; then |
| 200 | echo " muted" |
| 201 | else |
| 202 | echo " $VOL" |
| 203 | fi |
| 204 | else |
| 205 | echo " N/A" |
| 206 | fi |
| 207 | ]], |
| 208 | interval = 1, |
| 209 | }, |
| 210 | }, |
| 211 | }, |
| 212 | } |
| 213 | |
| 214 | -- Behavior |
| 215 | gar.set("follow_window_on_move", true) -- Follow window when using Mod+Shift+number |
| 216 | |
| 217 | -- Mod key: "mod" = Super/Win, "alt" = Alt |
| 218 | -- Use "mod" for real X session, "alt" for nested testing (Xephyr) |
| 219 | local mod = "mod" |
| 220 | |
| 221 | -- Terminal |
| 222 | gar.bind(mod .. "+Return", function() |
| 223 | gar.exec("alacritty || kitty || foot || xterm") |
| 224 | end) |
| 225 | |
| 226 | -- Close window |
| 227 | gar.bind(mod .. "+q", gar.close_window) |
| 228 | |
| 229 | -- Reload config |
| 230 | gar.bind(mod .. "+shift+r", gar.reload) |
| 231 | |
| 232 | -- Exit gar (return to display manager) |
| 233 | gar.bind(mod .. "+shift+e", gar.exit) |
| 234 | |
| 235 | -- Focus navigation (arrow keys) |
| 236 | gar.bind(mod .. "+Left", gar.focus("left")) |
| 237 | gar.bind(mod .. "+Right", gar.focus("right")) |
| 238 | gar.bind(mod .. "+Up", gar.focus("up")) |
| 239 | gar.bind(mod .. "+Down", gar.focus("down")) |
| 240 | |
| 241 | -- Focus navigation (vim keys) |
| 242 | gar.bind(mod .. "+h", gar.focus("left")) |
| 243 | gar.bind(mod .. "+l", gar.focus("right")) |
| 244 | gar.bind(mod .. "+k", gar.focus("up")) |
| 245 | gar.bind(mod .. "+j", gar.focus("down")) |
| 246 | |
| 247 | -- Swap windows |
| 248 | gar.bind(mod .. "+shift+Left", gar.swap("left")) |
| 249 | gar.bind(mod .. "+shift+Right", gar.swap("right")) |
| 250 | gar.bind(mod .. "+shift+Up", gar.swap("up")) |
| 251 | gar.bind(mod .. "+shift+Down", gar.swap("down")) |
| 252 | |
| 253 | gar.bind(mod .. "+shift+h", gar.swap("left")) |
| 254 | gar.bind(mod .. "+shift+l", gar.swap("right")) |
| 255 | gar.bind(mod .. "+shift+k", gar.swap("up")) |
| 256 | gar.bind(mod .. "+shift+j", gar.swap("down")) |
| 257 | |
| 258 | -- Resize |
| 259 | gar.bind(mod .. "+ctrl+Left", gar.resize("left", 0.05)) |
| 260 | gar.bind(mod .. "+ctrl+Right", gar.resize("right", 0.05)) |
| 261 | gar.bind(mod .. "+ctrl+Up", gar.resize("up", 0.05)) |
| 262 | gar.bind(mod .. "+ctrl+Down", gar.resize("down", 0.05)) |
| 263 | |
| 264 | gar.bind(mod .. "+ctrl+h", gar.resize("left", 0.05)) |
| 265 | gar.bind(mod .. "+ctrl+l", gar.resize("right", 0.05)) |
| 266 | gar.bind(mod .. "+ctrl+k", gar.resize("up", 0.05)) |
| 267 | gar.bind(mod .. "+ctrl+j", gar.resize("down", 0.05)) |
| 268 | |
| 269 | -- Equalize splits |
| 270 | gar.bind(mod .. "+e", gar.equalize) |
| 271 | |
| 272 | -- Toggle floating |
| 273 | gar.bind(mod .. "+f", gar.toggle_floating) |
| 274 | |
| 275 | -- Toggle fullscreen |
| 276 | gar.bind(mod .. "+shift+f", gar.toggle_fullscreen) |
| 277 | |
| 278 | -- Cycle through floating windows |
| 279 | gar.bind(mod .. "+grave", gar.cycle_floating) -- Mod+` (backtick) |
| 280 | |
| 281 | -- Workspaces |
| 282 | for i = 1, 9 do |
| 283 | gar.bind(mod .. "+" .. i, gar.workspace(i)) |
| 284 | gar.bind(mod .. "+shift+" .. i, gar.move_to_workspace(i)) |
| 285 | end |
| 286 | gar.bind(mod .. "+0", gar.workspace(10)) |
| 287 | gar.bind(mod .. "+shift+0", gar.move_to_workspace(10)) |
| 288 | |
| 289 | -- Multi-monitor (comma/period = prev/next) |
| 290 | gar.bind(mod .. "+comma", gar.focus_monitor("prev")) |
| 291 | gar.bind(mod .. "+period", gar.focus_monitor("next")) |
| 292 | gar.bind(mod .. "+shift+comma", gar.move_to_monitor("prev")) |
| 293 | gar.bind(mod .. "+shift+period", gar.move_to_monitor("next")) |
| 294 | |
| 295 | -- Workspace cycling |
| 296 | gar.bind(mod .. "+Tab", gar.workspace_next()) |
| 297 | gar.bind(mod .. "+shift+Tab", gar.workspace_prev()) |
| 298 | |
| 299 | -- Launchers (garlaunch) |
| 300 | local garlaunch = os.getenv("HOME") .. "/.cargo/bin/garlaunch" |
| 301 | gar.bind(mod .. "+space", function() |
| 302 | gar.exec(garlaunch .. " --mode drun 2>/tmp/garlaunch-debug.log") |
| 303 | end) |
| 304 | gar.bind(mod .. "+shift+space", function() |
| 305 | gar.exec(garlaunch .. " --mode window 2>/tmp/garlaunch-debug.log") |
| 306 | end) |
| 307 | gar.bind(mod .. "+r", function() |
| 308 | gar.exec(garlaunch .. " --mode run 2>/tmp/garlaunch-debug.log") |
| 309 | end) |
| 310 | |
| 311 | -- dmenu alternative (if rofi not available) |
| 312 | gar.bind(mod .. "+p", function() |
| 313 | gar.exec("dmenu_run") |
| 314 | end) |
| 315 | |
| 316 | -- Screenshot (requires scrot or maim) |
| 317 | gar.bind("Print", function() |
| 318 | gar.exec("scrot -e 'mv $f ~/Pictures/' || maim ~/Pictures/screenshot-$(date +%s).png") |
| 319 | end) |
| 320 | gar.bind(mod .. "+Print", function() |
| 321 | gar.exec("scrot -s -e 'mv $f ~/Pictures/' || maim -s ~/Pictures/screenshot-$(date +%s).png") |
| 322 | end) |
| 323 | |
| 324 | -- Screenshot (garshot) |
| 325 | gar.bind("ctrl+shift+3", function() |
| 326 | gar.exec("garshot select --annotate") -- Region selection with annotation editor |
| 327 | end) |
| 328 | gar.bind("ctrl+shift+4", function() |
| 329 | gar.exec("garshot select") -- Interactive region selection with blur overlay |
| 330 | end) |
| 331 | gar.bind("ctrl+shift+5", function() |
| 332 | gar.exec("garshot screen") -- Full screen capture |
| 333 | end) |
| 334 | gar.bind("ctrl+shift+6", function() |
| 335 | gar.exec("garshot window") -- Active window capture |
| 336 | end) |
| 337 | |
| 338 | -- Slideshow mode (Mod+Shift+P to avoid conflict with dmenu on Mod+P) |
| 339 | gar.bind(mod .. "+shift+p", function() gar.exec("garbg set ~/Pictures/background/cold/ --random --interval 2m") end) |
| 340 | |
| 341 | -- Lock screen (requires i3lock, swaylock, or slock) |
| 342 | gar.bind(mod .. "+Escape", function() |
| 343 | gar.exec("i3lock -c 000000 || swaylock -c 000000 || slock") |
| 344 | end) |
| 345 | |
| 346 | -- Lock screen with garlock (via daemon) |
| 347 | gar.bind(mod .. "+shift+q", function() |
| 348 | gar.exec("garlock lock") |
| 349 | end) |
| 350 | |
| 351 | -- Volume controls (requires pactl/pamixer) |
| 352 | gar.bind("XF86AudioRaiseVolume", function() |
| 353 | gar.exec("pactl set-sink-volume @DEFAULT_SINK@ +5% || pamixer -i 5") |
| 354 | end) |
| 355 | gar.bind("XF86AudioLowerVolume", function() |
| 356 | gar.exec("pactl set-sink-volume @DEFAULT_SINK@ -5% || pamixer -d 5") |
| 357 | end) |
| 358 | gar.bind("XF86AudioMute", function() |
| 359 | gar.exec("pactl set-sink-mute @DEFAULT_SINK@ toggle || pamixer -t") |
| 360 | end) |
| 361 | |
| 362 | -- Brightness controls (requires brightnessctl or light) |
| 363 | gar.bind("XF86MonBrightnessUp", function() |
| 364 | gar.exec("brightnessctl set +10% || light -A 10") |
| 365 | end) |
| 366 | gar.bind("XF86MonBrightnessDown", function() |
| 367 | gar.exec("brightnessctl set 10%- || light -U 10") |
| 368 | end) |
| 369 | |
| 370 | -- Clipboard history (garclip) |
| 371 | gar.bind(mod .. "+shift+v", function() |
| 372 | gar.exec("garclip-picker") |
| 373 | end) |