| 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 | -- System tray with quick settings panel |
| 16 | gar.exec_once("gartray daemon") |
| 17 | |
| 18 | -- Polkit authentication agent (needed for power actions via D-Bus) |
| 19 | gar.exec_once("/usr/libexec/kf6/polkit-kde-authentication-agent-1") |
| 20 | |
| 21 | -- Uncomment the ones you want: |
| 22 | -- gar.exec_once("picom") -- Compositor (for transparency/shadows) |
| 23 | -- gar.exec_once("dunst") -- Notification daemon |
| 24 | -- gar.exec_once("nm-applet") -- NetworkManager tray icon |
| 25 | -- gar.exec_once("blueman-applet") -- Bluetooth tray icon |
| 26 | -- gar.exec_once("xss-lock -- i3lock -c 000000") -- Auto-lock on suspend |
| 27 | |
| 28 | -- Appearance |
| 29 | gar.set("border_width", 2) |
| 30 | gar.set("border_color_focused", "#5294e2") |
| 31 | gar.set("border_color_unfocused", "#2d2d2d") |
| 32 | gar.set("gap_inner", 12) |
| 33 | gar.set("gap_outer", 16) |
| 34 | |
| 35 | -- Visual Effects (picom compositor) |
| 36 | -- gar auto-generates ~/.config/gar/picom.conf from these settings |
| 37 | -- Changes take effect on reload (Mod+Shift+R) - picom is signaled automatically |
| 38 | gar.set("corner_radius", 18) -- 0 = square corners |
| 39 | gar.set("blur_enabled", true) |
| 40 | gar.set("blur_method", "dual_kawase") -- "gaussian", "dual_kawase", "box" |
| 41 | gar.set("blur_strength", 5) -- 1-20 for dual_kawase |
| 42 | gar.set("shadow_enabled", true) |
| 43 | gar.set("shadow_radius", 12) |
| 44 | gar.set("shadow_opacity", 0.75) |
| 45 | gar.set("shadow_offset_x", -7) |
| 46 | gar.set("shadow_offset_y", -7) |
| 47 | gar.set("opacity_focused", 0.94) |
| 48 | gar.set("opacity_unfocused", 0.6) -- Set to 0.9 to dim unfocused windows |
| 49 | gar.set("fade_enabled", true) |
| 50 | gar.set("fade_delta", 3) |
| 51 | |
| 52 | -- Title bars (disabled by default) |
| 53 | -- gar.set("titlebar_enabled", true) |
| 54 | -- gar.set("titlebar_height", 20) |
| 55 | -- gar.set("titlebar_color_focused", "#3d3d3d") |
| 56 | -- gar.set("titlebar_color_unfocused", "#2d2d2d") |
| 57 | -- gar.set("titlebar_text_color", "#ffffff") |
| 58 | |
| 59 | -- Border gradients (purple to blue theme) |
| 60 | -- direction options: "vertical", "horizontal", "diagonal" |
| 61 | gar.set("border_gradient_enabled", true) |
| 62 | gar.set("border_gradient_start_focused", "#9b59b6") -- Purple |
| 63 | gar.set("border_gradient_end_focused", "#3498db") -- Blue |
| 64 | gar.set("border_gradient_start_unfocused", "#4a235a") -- Dark purple |
| 65 | gar.set("border_gradient_end_unfocused", "#1a5276") -- Dark blue |
| 66 | gar.set("border_gradient_direction", "diagonal") |
| 67 | |
| 68 | -- Animations (picom v12+) |
| 69 | -- open options: "slide-in", "fly-in", "appear", "none" |
| 70 | -- close options: "slide-out", "fly-out", "disappear", "none" |
| 71 | gar.set("animation_open", "appear") |
| 72 | gar.set("animation_close", "disappear") |
| 73 | gar.set("animation_duration", 0.15) -- seconds |
| 74 | |
| 75 | -- Custom GLSL shader (picom, requires GLX backend) |
| 76 | -- Bundled shaders: ~/.config/gar/shaders/focused-glow.glsl, dim-unfocused.glsl |
| 77 | -- gar.set("picom_shader", "~/.config/gar/shaders/focused-glow.glsl") |
| 78 | |
| 79 | -- Per-window picom rules |
| 80 | -- NOTE: When using rules, picom ignores old-style options (inactive-opacity, shadow-exclude, etc.) |
| 81 | -- Only enable rules if you want full control via rules-based config |
| 82 | -- Available rule options: corner_radius, opacity, shadow, blur_background, shader |
| 83 | -- Match syntax: class_g = 'ClassName', class_i = 'instance', name = 'title', window_type = 'type' |
| 84 | -- gar.picom_rule({ |
| 85 | -- match = "class_g = 'Alacritty'", |
| 86 | -- blur_background = true, |
| 87 | -- opacity = 0.92, |
| 88 | -- }) |
| 89 | -- gar.picom_rule({ |
| 90 | -- match = "class_g = 'firefox'", |
| 91 | -- corner_radius = 8, |
| 92 | -- }) |
| 93 | |
| 94 | -- garbar status bar (native gar integration) |
| 95 | -- When this table is present, gar automatically spawns and manages garbar |
| 96 | gar.bar = { |
| 97 | height = 32, |
| 98 | position = "top", |
| 99 | background = "#1a1a1a", |
| 100 | foreground = "#ffffff", |
| 101 | opacity = 1.0, |
| 102 | fonts = { |
| 103 | "JetBrainsMono Nerd Font:size=10", |
| 104 | "Symbols Nerd Font:size=10", |
| 105 | }, |
| 106 | padding = { left = 8, right = 16, top = 0, bottom = 0 }, |
| 107 | |
| 108 | -- Module layout |
| 109 | modules_left = { "workspaces", "window_title" }, |
| 110 | modules_center = {}, |
| 111 | modules_right = { "almanta", "filesystem", "memory", "cpu", "battery", "wlan", "volume", "tray", "datetime", "quick_settings" }, |
| 112 | |
| 113 | -- Module configurations |
| 114 | modules = { |
| 115 | workspaces = { |
| 116 | font_size = 11, |
| 117 | focused = { |
| 118 | foreground = "#ffffff", |
| 119 | background = "transparent", |
| 120 | underline = { width = 2, color = "#33ccff" }, |
| 121 | }, |
| 122 | unfocused = { |
| 123 | foreground = "#666666", |
| 124 | background = "transparent", |
| 125 | }, |
| 126 | urgent = { |
| 127 | foreground = "#ffffff", |
| 128 | background = "#ff5555", |
| 129 | }, |
| 130 | }, |
| 131 | window_title = { |
| 132 | max_length = 50, |
| 133 | empty_text = "Desktop", |
| 134 | }, |
| 135 | datetime = { |
| 136 | format = " %a %b %d %H:%M", |
| 137 | }, |
| 138 | cpu = { |
| 139 | format = " {usage}%", |
| 140 | }, |
| 141 | memory = { |
| 142 | format = " {percent}%", |
| 143 | }, |
| 144 | battery = { |
| 145 | device = "auto", |
| 146 | format_charging = " {percent}%", |
| 147 | format_discharging = " {percent}%", |
| 148 | format_full = " Full", |
| 149 | }, |
| 150 | -- Script modules (custom commands) |
| 151 | script = { |
| 152 | almanta = { |
| 153 | exec = [[ |
| 154 | HOST="espadon@almanta" |
| 155 | SSH_OPTS="-T -o BatchMode=yes -o ConnectTimeout=2 -o ConnectionAttempts=1" |
| 156 | if ! ssh $SSH_OPTS "$HOST" "echo ok" >/dev/null 2>&1; then |
| 157 | echo "almanta: down" |
| 158 | exit 0 |
| 159 | fi |
| 160 | load=$(ssh $SSH_OPTS "$HOST" "cut -d' ' -f1 /proc/loadavg" 2>/dev/null || echo "?") |
| 161 | mem=$(ssh $SSH_OPTS "$HOST" "free | grep '^Mem:' | awk '{printf \"%.0f\", (\$3/\$2)*100}'" 2>/dev/null || echo "?") |
| 162 | disk=$(ssh $SSH_OPTS "$HOST" "df / | tail -1 | awk '{print \$5}' | tr -d '%'" 2>/dev/null || echo "?") |
| 163 | echo "almanta L:$load M:$mem% D:$disk%" |
| 164 | ]], |
| 165 | interval = 30, |
| 166 | }, |
| 167 | filesystem = { |
| 168 | exec = [[df -h / | awk 'NR==2 {print " " $4}']], |
| 169 | interval = 60, |
| 170 | }, |
| 171 | wlan = { |
| 172 | exec = [[ |
| 173 | IFACE="wlp1s0f0" |
| 174 | if [ -d "/sys/class/net/$IFACE" ]; then |
| 175 | STATE=$(cat /sys/class/net/$IFACE/operstate 2>/dev/null) |
| 176 | if [ "$STATE" = "up" ]; then |
| 177 | ESSID=$(iwgetid -r 2>/dev/null || echo "") |
| 178 | IP=$(ip -4 addr show $IFACE 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1) |
| 179 | if [ -n "$ESSID" ]; then |
| 180 | echo " $ESSID $IP" |
| 181 | else |
| 182 | echo " connected" |
| 183 | fi |
| 184 | else |
| 185 | echo " offline" |
| 186 | fi |
| 187 | else |
| 188 | echo " N/A" |
| 189 | fi |
| 190 | ]], |
| 191 | interval = 5, |
| 192 | }, |
| 193 | volume = { |
| 194 | exec = [[ |
| 195 | if command -v pamixer >/dev/null 2>&1; then |
| 196 | if pamixer --get-mute | grep -q true; then |
| 197 | echo " muted" |
| 198 | else |
| 199 | VOL=$(pamixer --get-volume) |
| 200 | echo " $VOL%" |
| 201 | fi |
| 202 | elif command -v pactl >/dev/null 2>&1; then |
| 203 | VOL=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oP '\d+%' | head -1) |
| 204 | MUTE=$(pactl get-sink-mute @DEFAULT_SINK@ | grep -oP 'yes|no') |
| 205 | if [ "$MUTE" = "yes" ]; then |
| 206 | echo " muted" |
| 207 | else |
| 208 | echo " $VOL" |
| 209 | fi |
| 210 | else |
| 211 | echo " N/A" |
| 212 | fi |
| 213 | ]], |
| 214 | interval = 1, |
| 215 | }, |
| 216 | }, |
| 217 | }, |
| 218 | } |
| 219 | |
| 220 | -- Behavior |
| 221 | gar.set("follow_window_on_move", true) -- Follow window when using Mod+Shift+number |
| 222 | |
| 223 | -- Mod key: "mod" = Super/Win, "alt" = Alt |
| 224 | -- Use "mod" for real X session, "alt" for nested testing (Xephyr) |
| 225 | local mod = "mod" |
| 226 | |
| 227 | -- Terminal (fallback) |
| 228 | gar.bind(mod .. "+Return", function() |
| 229 | gar.exec("alacritty || kitty || foot || xterm") |
| 230 | end) |
| 231 | |
| 232 | -- garterm with fish shell |
| 233 | gar.bind(mod .. "+shift+Return", function() |
| 234 | gar.exec("garterm") |
| 235 | end) |
| 236 | |
| 237 | -- CPR-Music dev workspace via session |
| 238 | gar.bind(mod .. "+alt+Return", function() |
| 239 | gar.exec("garterm") |
| 240 | gar.exec("sleep 0.3 && gartermctl load-session cpr-music") |
| 241 | end) |
| 242 | |
| 243 | -------------------------------------------------------------------------------- |
| 244 | -- GARTERM CONFIGURATION |
| 245 | -------------------------------------------------------------------------------- |
| 246 | -- garterm reads this table for shell, font, colors, sessions, and keybinds |
| 247 | |
| 248 | gar.terminal = { |
| 249 | -- Default shell |
| 250 | shell = "/usr/bin/fish", |
| 251 | |
| 252 | -- Font settings |
| 253 | font = { |
| 254 | family = "JetBrainsMono Nerd Font", |
| 255 | size = 20.0, |
| 256 | }, |
| 257 | |
| 258 | -- Color scheme |
| 259 | colors = { |
| 260 | preset = "catpuccin-mocha", |
| 261 | }, |
| 262 | |
| 263 | -- Tab bar configuration |
| 264 | tab_bar = { |
| 265 | height = 24, -- Tab bar height in pixels |
| 266 | position = "top", -- "top" or "bottom" |
| 267 | show_single_tab = false, -- Show tab bar even with one tab |
| 268 | max_tab_width = 200, -- Maximum width per tab in pixels |
| 269 | tab_padding = 16, -- Horizontal padding inside each tab |
| 270 | shorten_paths = true, -- Shorten paths: ~/P/a/src style |
| 271 | |
| 272 | -- Colors as {R, G, B, A} with values 0.0 to 1.0 |
| 273 | background = {0.08, 0.08, 0.12, 1.0}, -- Tab bar background |
| 274 | active_bg = {0.15, 0.15, 0.20, 1.0}, -- Active tab background |
| 275 | inactive_bg = {0.10, 0.10, 0.14, 1.0},-- Inactive tab background |
| 276 | active_fg = {1.0, 1.0, 1.0, 1.0}, -- Active tab text color |
| 277 | inactive_fg = {0.7, 0.7, 0.7, 1.0}, -- Inactive tab text color |
| 278 | }, |
| 279 | |
| 280 | -- Session definitions (load with gartermctl load-session <name>) |
| 281 | sessions = { |
| 282 | -- CPR-Music development workspace |
| 283 | ["cpr-music"] = { |
| 284 | tabs = { |
| 285 | { |
| 286 | title = "Frontend", |
| 287 | cwd = "~/GithubOrgs/espadonne/CPR-Music", |
| 288 | cmd = "npm run dev", |
| 289 | }, |
| 290 | { |
| 291 | title = "Backend", |
| 292 | cwd = "~/GithubOrgs/mfwolffe/CPR-Music-Backend", |
| 293 | cmd = "source .venv/bin/activate.fish && python manage.py runserver", |
| 294 | }, |
| 295 | { |
| 296 | title = "Editor", |
| 297 | cwd = "~/GithubOrgs/espadonne/CPR-Music", |
| 298 | cmd = "fackr", |
| 299 | }, |
| 300 | }, |
| 301 | }, |
| 302 | }, |
| 303 | |
| 304 | -- Keybinds within garterm (Lua function callbacks) |
| 305 | keybinds = { |
| 306 | -- Quick session loading |
| 307 | ["alt+m"] = { action = "load_session", session = "cpr-music"}, |
| 308 | |
| 309 | -- Open fackr in current pane |
| 310 | ["alt+e"] = function() |
| 311 | gar.terminal.send_text(nil, "fackr \n") |
| 312 | end, |
| 313 | |
| 314 | -- Open fackr in new tab |
| 315 | ["alt+shift+e"] = function() |
| 316 | gar.terminal.new_tab({}) |
| 317 | gar.terminal.send_text(nil, "fackr \n") |
| 318 | end, |
| 319 | }, |
| 320 | } |
| 321 | |
| 322 | -- Close window |
| 323 | gar.bind(mod .. "+q", gar.close_window) |
| 324 | |
| 325 | -- Reload config |
| 326 | gar.bind(mod .. "+shift+r", gar.reload) |
| 327 | |
| 328 | -- Exit gar (return to display manager) |
| 329 | gar.bind(mod .. "+shift+e", gar.exit) |
| 330 | |
| 331 | -- Focus navigation (arrow keys) |
| 332 | gar.bind(mod .. "+Left", gar.focus("left")) |
| 333 | gar.bind(mod .. "+Right", gar.focus("right")) |
| 334 | gar.bind(mod .. "+Up", gar.focus("up")) |
| 335 | gar.bind(mod .. "+Down", gar.focus("down")) |
| 336 | |
| 337 | -- Focus navigation (vim keys) |
| 338 | gar.bind(mod .. "+h", gar.focus("left")) |
| 339 | gar.bind(mod .. "+l", gar.focus("right")) |
| 340 | gar.bind(mod .. "+k", gar.focus("up")) |
| 341 | gar.bind(mod .. "+j", gar.focus("down")) |
| 342 | |
| 343 | -- Swap windows |
| 344 | gar.bind(mod .. "+shift+Left", gar.swap("left")) |
| 345 | gar.bind(mod .. "+shift+Right", gar.swap("right")) |
| 346 | gar.bind(mod .. "+shift+Up", gar.swap("up")) |
| 347 | gar.bind(mod .. "+shift+Down", gar.swap("down")) |
| 348 | |
| 349 | gar.bind(mod .. "+shift+h", gar.swap("left")) |
| 350 | gar.bind(mod .. "+shift+l", gar.swap("right")) |
| 351 | gar.bind(mod .. "+shift+k", gar.swap("up")) |
| 352 | gar.bind(mod .. "+shift+j", gar.swap("down")) |
| 353 | |
| 354 | -- Resize |
| 355 | gar.bind(mod .. "+ctrl+Left", gar.resize("left", 0.05)) |
| 356 | gar.bind(mod .. "+ctrl+Right", gar.resize("right", 0.05)) |
| 357 | gar.bind(mod .. "+ctrl+Up", gar.resize("up", 0.05)) |
| 358 | gar.bind(mod .. "+ctrl+Down", gar.resize("down", 0.05)) |
| 359 | |
| 360 | gar.bind(mod .. "+ctrl+h", gar.resize("left", 0.05)) |
| 361 | gar.bind(mod .. "+ctrl+l", gar.resize("right", 0.05)) |
| 362 | gar.bind(mod .. "+ctrl+k", gar.resize("up", 0.05)) |
| 363 | gar.bind(mod .. "+ctrl+j", gar.resize("down", 0.05)) |
| 364 | |
| 365 | -- Equalize splits |
| 366 | gar.bind(mod .. "+e", gar.equalize) |
| 367 | |
| 368 | -- Toggle floating |
| 369 | gar.bind(mod .. "+f", gar.toggle_floating) |
| 370 | |
| 371 | -- Toggle fullscreen |
| 372 | gar.bind(mod .. "+shift+f", gar.toggle_fullscreen) |
| 373 | |
| 374 | -- Cycle through floating windows |
| 375 | gar.bind(mod .. "+grave", gar.cycle_floating) -- Mod+` (backtick) |
| 376 | |
| 377 | -- Workspaces |
| 378 | for i = 1, 9 do |
| 379 | gar.bind(mod .. "+" .. i, gar.workspace(i)) |
| 380 | gar.bind(mod .. "+shift+" .. i, gar.move_to_workspace(i)) |
| 381 | end |
| 382 | gar.bind(mod .. "+0", gar.workspace(10)) |
| 383 | gar.bind(mod .. "+shift+0", gar.move_to_workspace(10)) |
| 384 | |
| 385 | -- Multi-monitor (comma/period = prev/next) |
| 386 | gar.bind(mod .. "+comma", gar.focus_monitor("prev")) |
| 387 | gar.bind(mod .. "+period", gar.focus_monitor("next")) |
| 388 | gar.bind(mod .. "+shift+comma", gar.move_to_monitor("prev")) |
| 389 | gar.bind(mod .. "+shift+period", gar.move_to_monitor("next")) |
| 390 | |
| 391 | -- Workspace cycling |
| 392 | gar.bind(mod .. "+Tab", gar.workspace_next()) |
| 393 | gar.bind(mod .. "+shift+Tab", gar.workspace_prev()) |
| 394 | |
| 395 | -- Launchers (garlaunch) |
| 396 | local garlaunch = os.getenv("HOME") .. "/.cargo/bin/garlaunch" |
| 397 | gar.bind(mod .. "+space", function() |
| 398 | gar.exec(garlaunch .. " --mode drun 2>/tmp/garlaunch-debug.log") |
| 399 | end) |
| 400 | gar.bind(mod .. "+shift+space", function() |
| 401 | gar.exec(garlaunch .. " --mode window 2>/tmp/garlaunch-debug.log") |
| 402 | end) |
| 403 | gar.bind(mod .. "+r", function() |
| 404 | gar.exec(garlaunch .. " --mode run 2>/tmp/garlaunch-debug.log") |
| 405 | end) |
| 406 | |
| 407 | -- dmenu alternative (if rofi not available) |
| 408 | gar.bind(mod .. "+p", function() |
| 409 | gar.exec("dmenu_run") |
| 410 | end) |
| 411 | |
| 412 | -- Screenshot (requires scrot or maim) |
| 413 | gar.bind("Print", function() |
| 414 | gar.exec("scrot -e 'mv $f ~/Pictures/' || maim ~/Pictures/screenshot-$(date +%s).png") |
| 415 | end) |
| 416 | gar.bind(mod .. "+Print", function() |
| 417 | gar.exec("scrot -s -e 'mv $f ~/Pictures/' || maim -s ~/Pictures/screenshot-$(date +%s).png") |
| 418 | end) |
| 419 | |
| 420 | -- Screenshot (garshot) |
| 421 | gar.bind("ctrl+shift+3", function() |
| 422 | gar.exec("garshot select --annotate") -- Region selection with annotation editor |
| 423 | end) |
| 424 | gar.bind("ctrl+shift+4", function() |
| 425 | gar.exec("garshot select") -- Interactive region selection with blur overlay |
| 426 | end) |
| 427 | gar.bind("ctrl+shift+5", function() |
| 428 | gar.exec("garshot screen") -- Full screen capture |
| 429 | end) |
| 430 | gar.bind("ctrl+shift+6", function() |
| 431 | gar.exec("garshot window") -- Active window capture |
| 432 | end) |
| 433 | |
| 434 | -- Slideshow mode (Mod+Shift+P to avoid conflict with dmenu on Mod+P) |
| 435 | gar.bind(mod .. "+shift+p", function() gar.exec("garbg set ~/Pictures/background/cold/ --random --interval 2m") end) |
| 436 | |
| 437 | -- Lock screen (requires i3lock, swaylock, or slock) |
| 438 | gar.bind(mod .. "+Escape", function() |
| 439 | gar.exec("i3lock -c 000000 || swaylock -c 000000 || slock") |
| 440 | end) |
| 441 | |
| 442 | -- Lock screen with garlock (via daemon) |
| 443 | gar.bind(mod .. "+shift+q", function() |
| 444 | gar.exec("garlock lock") |
| 445 | end) |
| 446 | |
| 447 | -- Volume controls (requires pactl/pamixer) |
| 448 | gar.bind("XF86AudioRaiseVolume", function() |
| 449 | gar.exec("pactl set-sink-volume @DEFAULT_SINK@ +5% || pamixer -i 5") |
| 450 | end) |
| 451 | gar.bind("XF86AudioLowerVolume", function() |
| 452 | gar.exec("pactl set-sink-volume @DEFAULT_SINK@ -5% || pamixer -d 5") |
| 453 | end) |
| 454 | gar.bind("XF86AudioMute", function() |
| 455 | gar.exec("pactl set-sink-mute @DEFAULT_SINK@ toggle || pamixer -t") |
| 456 | end) |
| 457 | |
| 458 | -- Brightness controls (requires brightnessctl or light) |
| 459 | gar.bind("XF86MonBrightnessUp", function() |
| 460 | gar.exec("brightnessctl set +10% || light -A 10") |
| 461 | end) |
| 462 | gar.bind("XF86MonBrightnessDown", function() |
| 463 | gar.exec("brightnessctl set 10%- || light -U 10") |
| 464 | end) |
| 465 | |
| 466 | -- Clipboard history (garclip) |
| 467 | gar.bind(mod .. "+shift+v", function() |
| 468 | gar.exec("garclip-picker") |
| 469 | end) |