-- Hyprland Configuration -- Translated from DWM config.def.h and sxhkdrc -- Craig Jennings -- ============================================================================ -- Monitor Configuration -- ============================================================================ -- Generated by hyprlang2lua. Review TODOs before reloading Hyprland. -- hyprlang2lua polyfills — runtime helpers reproducing -- hyprlang behaviour the typed Lua API doesn't expose directly. local function hl_source_glob(pattern) -- 'source = path/*.conf' had hyprlang glob and inline-expand the -- matches. require() can't glob, so we shell out to ls (matching -- the user's brace-expansion behaviour) and dofile each result. -- Paths with spaces or shell metacharacters in the directory -- portion will misparse; typical ~/.config/hypr/ layouts don't -- hit this. Swap to lfs.dir() or find -name if you need fancier. local p = io.popen("ls " .. pattern .. " 2>/dev/null") if not p then return end for f in p:lines() do local chunk, err = loadfile(f) if chunk then chunk() else io.stderr:write("hl_source_glob: " .. tostring(err) .. "\n") end end p:close() end hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto", }) -- Waybar's strip (6px top margin + 54px bar) is reserved statically by -- waybar-reserve, and waybar runs with "exclusive": false. The bar's own -- exclusive zone would vanish and reappear on every SIGUSR2 reload (the -- collapse mechanism) and on hide/crash/relaunch, snapping every tiled window -- up and back down. The static reservation holds the clients in place; only -- the bar itself changes. `exec` (not exec-once) reruns it on every config -- reload, which is exactly when Hyprland resets dynamic reservations. The -- script is idempotent, and a catch-all `monitor=,addreserved,...` rule can't -- replace it (empty-name addreserved silently no-ops). -- -- Run three times over ~0.6s, not once: on reload Hyprland clears the -- reservation AND re-fires this exec, and the two race. A single run that -- fires before the clear no-ops (reserved still looks correct), the clear then -- wins, and the non-exclusive bar drops off-screen. Re-applying past the clear -- window makes the restore reliable; the script is idempotent so extra runs are -- free. Applying a monitor rule (e.g. the DP-4 pin) also clears the reservation, -- so this covers a reload that re-asserts monitors too. -- ============================================================================ -- Startup Applications -- ============================================================================ -- Portal and D-Bus setup FIRST, then waybar (needs portal for appearance query) -- Start hyprland-session.target FIRST: it pulls up graphical-session.target, -- which xdg-desktop-portal 1.22+ hard-requires (Requisite=). A bare-exec Hyprland -- session has no session manager to raise that target, so without this the portal -- fails its dependency at every login (screen-share + file pickers dead). -- 'systemctl start' blocks until active, so the ';' sequence guarantees the target -- is up before the portal restart runs. -- -- Portal restart (not start) reconnects stale portals on Hyprland restart. -- Backend portals (GTK, Hyprland) restart BEFORE the main portal to avoid a 50s -- GTK settings proxy timeout; the sequence keeps that ordering. Separated by ';' -- not '&&' so a failing portal restart can't stop waybar from launching — waybar -- degrades gracefully without the portal (only the appearance query is missed), -- and gating the bar behind the portal left the desktop bar-less whenever -- xdg-desktop-portal failed its dependency at login. Waybar stays gated on its -- own config generation (waybar-active-config && waybar). -- Core services -- Desktop appearance -- `settings restore` replays the remembered toggles and reapplies the stored -- wallpaper. It replaced `waypaper --restore` on 2026-08-14: waypaper keeps -- its own config.ini and the settings store keeps another, neither knew about -- the other, and the login replay always won — so a wallpaper chosen in the -- panel came back as whatever the shell had last set. The store is the only -- one of the two that can hold a sun pair, a video or a projected face, so it -- owns the restore. set-wallpaper records into it for choices made outside -- the panel. -- -- waypaper --restore stays as the fallback, not the owner. If the stored -- wallpaper cannot be applied (an image deleted, a drive not mounted yet), -- `settings restore` exits 3 and waypaper's independent copy still puts -- something on the screen. Dropping it outright would trade this bug for a -- bare desktop. -- -- The wallpaper half only. The toggle half runs from its own exec-once further -- down, after hypridle and dunst — caffeine *is* "hypridle isn't running" and -- DND *is* dunst's pause level, so replaying them here would spend the whole -- re-assert budget correcting backings that have not launched yet, and would -- replay them a second time besides. This slot exists for awww's timing, not -- theirs. -- Background services -- hypridle is reaped on both exit paths, because it outlives its compositor -- otherwise. An orphaned daemon keeps firing idle actions at whatever session -- is live next, and it holds its old logind session scope open (the scope can't -- close while a process sits in it), so orphans accumulate one per abnormal -- session death. On 2026-07-22 velox reached five concurrent hypridle daemons; -- two of them racing to lock produced "Cannot re-lock" and a session wedged -- locked with no client able to draw a password prompt — recoverable only from -- another console. exec-shutdown covers a clean compositor exit; the pkill in -- exec-once covers the paths where it never runs (crash, SIGKILL, TTY logout). hl.on("hyprland.shutdown", function() hl.exec_cmd("pkill -x hypridle") end) -- hypridle.conf is rendered here rather than tracked, because its contents -- are this machine's stage times and hibernate setting. Tracking the render -- meant every panel change dirtied the repo, and whichever machine -- committed last imposed its policy on the others: a desktop ended up -- carrying a laptop's suspend-then-hibernate line that it cannot run. -- Rendering at session start makes the store the only source of truth and -- the file a build artifact. -- -- hypridle-start owns the render, the fallback, and the ordering between -- them, because that ordering is subtle enough to get wrong in a config -- line nothing can test: a render can fail on purpose (a damaged store, to -- avoid overwriting a real policy with defaults), and a fallback that -- fired there would perform exactly the overwrite the render refused. -- Replay the toggles that have no durable state of their own. Caffeine *is* -- "hypridle isn't running" and DND *is* dunst's pause level, so the two -- exec-once lines above (and dunst's) recreate both at a fixed default every -- start — a deliberately-set caffeine was silently discarded on every login. -- Ordered after those launches so it corrects a backing that exists; it also -- re-asserts for a few seconds, which covers a backing that comes up late. -- Logged like its neighbours: a silent exec-once failure here would look -- exactly like the bug it fixes, and gammastep is the standing proof that a -- launch dying quietly at session start can go unnoticed for a long time. -- Pyprland (scratchpads, magnify, etc.) -- Tray apps. wait-for-tray blocks until waybar's systray host is up (a fixed -- sleep can't cover a slow cold-start waybar), so these register their icons -- instead of opening as windows. Caps at ~30s, then launches anyway. -- QT_FONT_DPI bumps the bridge's QML UI font (qt6ct General font is ignored by Qt Quick) -- ============================================================================ -- Environment Variables -- ============================================================================ hl.env("XCURSOR_SIZE", "24") hl.env("XCURSOR_THEME", "Bibata-Modern-Ice") hl.env("XDG_CURRENT_DESKTOP", "Hyprland") hl.env("XDG_SESSION_TYPE", "wayland") hl.env("XDG_SESSION_DESKTOP", "Hyprland") hl.env("_JAVA_AWT_WM_NONREPARENTING", "1") -- ============================================================================ -- Appearance (matching DWM colors) -- ============================================================================ -- DWM colors: gray1=#222222, gray2=#444444, gray3=#bbbbbb, gray4=#eeeeee, cyan=#daa520 hl.config({ general = { gaps_in = 25, gaps_out = 30, border_size = 2, col = { active_border = "rgba(daa520ff)", inactive_border = "rgba(444444ff)", nogroup_border_active = "rgba(daa520ff)", nogroup_border = "rgba(444444ff)", }, -- Pyprland 3.4+ applies `group deny` to scratchpads, which routes their -- border through col.nogroup_border* instead of col.*_border. Without -- these overrides Hyprland's defaults paint scratchpads bright magenta. layout = "master", resize_on_border = true, }, }) hl.config({ decoration = { rounding = 10, dim_inactive = true, dim_strength = 0.4, dim_special = 0.2, blur = { enabled = false, }, shadow = { enabled = false, }, }, }) hl.config({ animations = { enabled = true, }, }) hl.curve("myBezier", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } }) hl.animation({ leaf = "windows", enabled = true, speed = 2, bezier = "myBezier", }) hl.animation({ leaf = "windowsOut", enabled = true, speed = 2, bezier = "default", style = "popin 80%", }) hl.animation({ leaf = "fade", enabled = true, speed = 2, bezier = "default", }) hl.animation({ leaf = "workspaces", enabled = true, speed = 2, bezier = "default", }) hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 2, bezier = "default", style = "slidevert", }) -- ============================================================================ -- Layout (master-stack like DWM tile) -- ============================================================================ hl.config({ master = { new_status = "master", new_on_top = true, mfact = 0.55, }, }) hl.config({ dwindle = { preserve_split = true, }, }) -- ============================================================================ -- Input -- ============================================================================ hl.config({ cursor = { no_warps = true, inactive_timeout = 2.0, }, }) hl.config({ input = { kb_layout = "us", kb_options = "ctrl:nocaps", numlock_by_default = true, follow_mouse = 0, -- 0, not the default 1: with follow_mouse off we never want focus to follow -- the cursor. At 1, focus still jumps to the window under the pointer when it -- crosses a floating<->tiled boundary, so launching a floating scratchpad (or -- the org-capture popup) re-enabled focus-follows-mouse onto tiled windows. float_switch_override_focus = 0, mouse_refocus = false, natural_scroll = true, touchpad = { natural_scroll = false, }, }, }) -- ============================================================================ -- Misc -- ============================================================================ hl.config({ misc = { force_default_wallpaper = 0, disable_hyprland_logo = true, -- false so apps can't pull focus via activation requests. New windows still -- focus on open (separate path); this stops e.g. a browser yanking focus -- back off a freshly opened emacs frame. focus_on_activate = false, -- Let a fresh lock client adopt a session whose previous one died. The -- default (off) is the strict reading of ext-session-lock: a dead lock -- client leaves the session locked forever and refuses every replacement -- ("Cannot re-lock"), so the screen stays up with nothing able to draw a -- password prompt and the only way back in is another console. That is a -- hard lockout, and it cost a session on velox 2026-07-22. On means a -- replacement hyprlock re-attaches and prompts normally. The screen stays -- locked either way — this decides whether the lock is recoverable, never -- whether it holds. allow_session_lock_restore = true, }, }) -- ============================================================================ -- Debug (temporary - disable when stable) -- ============================================================================ hl.config({ debug = { disable_logs = false, }, }) -- ============================================================================ -- XWayland -- ============================================================================ hl.config({ xwayland = { force_zero_scaling = true, }, }) -- ============================================================================ -- Window Rules (Hyprland 0.53+ syntax: match:CONDITION, RULE) -- ============================================================================ -- Floating windows (from DWM rules) hl.window_rule({ match = { class = "^(xdg-desktop-portal-gtk)$", }, float = true, }) hl.window_rule({ match = { class = "^(Gimp)$", }, float = true, }) hl.window_rule({ match = { class = "^(caffeine)$", }, float = true, }) hl.window_rule({ match = { class = "^(qalculate-gtk)$", }, float = true, }) hl.window_rule({ match = { title = "^(Event Tester)$", }, float = true, }) -- net / bluetooth instrument-console panels. Normal floating windows (formerly -- gtk4-layer-shell overlays) so they drag to move and corner-drag to resize. -- Opened top-right to match their old anchored spot: the panel is right-aligned -- with a 44px gap, so x = 100% - (window width + 44). net is 420 wide, bt 380. hl.window_rule({ match = { class = "^(net\\.cjennings\\.netpanel)$", }, float = true, }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.netpanel)$", }, move = "100%-464 50", }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.btpanel)$", }, float = true, }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.btpanel)$", }, move = "100%-424 50", }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.audiopanel)$", }, float = true, }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.audiopanel)$", }, move = "100%-444 50", }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.timerpanel)$", }, float = true, }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.timerpanel)$", }, move = "100%-444 50", }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.settingspanel)$", }, float = true, }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.settingspanel)$", }, move = "100%-584 50", }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.weatherpanel)$", }, float = true, }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.weatherpanel)$", }, move = "100%-464 50", }) -- maintenance console: the wide board (960), same right-aligned convention. hl.window_rule({ match = { class = "^(net\\.cjennings\\.maintpanel)$", }, float = true, }) hl.window_rule({ match = { class = "^(net\\.cjennings\\.maintpanel)$", }, move = "100%-1004 50", }) -- org-capture popup frame (quick-capture script names the frame) -- Size is per-host in /conf.d/local.conf: native window rules ignore -- percentages (only pyprland honors them), so the popup is sized in absolute -- pixels matching that host's terminal scratchpad. No size rule here means a -- host without an override falls back to the script's char-cell geometry. hl.window_rule({ match = { title = "^(org-capture)$", }, float = true, }) hl.window_rule({ match = { title = "^(org-capture)$", }, center = true, }) -- dirvish popup frame (dirvish-popup script names the frame). No stay_focused — -- it's a file manager that launches files into other apps, so focus must be free -- to follow; q (cj/dirvish-popup-quit) closes the frame. hl.window_rule({ match = { title = "^(dirvish)$", }, float = true, }) hl.window_rule({ match = { title = "^(dirvish)$", }, size = "1100 700", }) hl.window_rule({ match = { title = "^(dirvish)$", }, center = true, }) -- NOTE: center windowrules removed 2026-03-04 per pyprland maintainer suggestion -- Testing whether pyprland handles scratchpad re-centering natively (issue #211) -- Gaming hl.window_rule({ match = { class = "^(Civ5XP)$", }, fullscreen = true, }) -- ============================================================================ -- Key Bindings -- ============================================================================ local mod = "SUPER" -- Terminal and core apps (from DWM) hl.bind(mod .. " + T", hl.dsp.exec_cmd("foot")) hl.bind(mod .. " + E", hl.dsp.exec_cmd("emacsclient -c -a \"\" || emacs")) -- Standalone emacs: its own process, not a frame on the daemon, so killing it -- takes no other frame with it. init.el guards server-start on server-running-p, -- so while the daemon holds the socket this process leaves it alone. With no -- daemon up it finds no server and becomes one -- the guard lives in init.el, and -- a keybind can't override it, since --eval runs after init. hl.bind(mod .. " + SHIFT + E", hl.dsp.exec_cmd("emacs")) hl.bind(mod .. " + N", hl.dsp.exec_cmd("quick-capture")) hl.bind(mod .. " + W", hl.dsp.exec_cmd("$BROWSER")) hl.bind(mod .. " + F", hl.dsp.exec_cmd("dirvish-popup")) hl.bind(mod .. " + SHIFT + F", hl.dsp.exec_cmd("layout-cycle float-toggle")) -- From sxhkdrc hl.bind(mod .. " + SPACE", hl.dsp.exec_cmd("fuzzel-toggle")) hl.bind(mod .. " + SHIFT + W", hl.dsp.exec_cmd("$ALTBROWSER")) hl.bind(mod .. " + P", hl.dsp.exec_cmd("media-toggle-all")) hl.bind(mod .. " + SHIFT + L", hl.dsp.exec_cmd("calibre")) hl.bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("toggle-touchpad")) -- Window management (from DWM) -- Layout-aware navigation (works across master, scrolling) hl.bind(mod .. " + J", hl.dsp.exec_cmd("layout-navigate next")) hl.bind(mod .. " + K", hl.dsp.exec_cmd("layout-navigate prev")) hl.bind(mod .. " + SHIFT + J", hl.dsp.exec_cmd("layout-navigate next move")) hl.bind(mod .. " + SHIFT + K", hl.dsp.exec_cmd("layout-navigate prev move")) hl.bind(mod .. " + H", hl.dsp.exec_cmd("layout-resize shrink")) hl.bind(mod .. " + L", hl.dsp.exec_cmd("layout-resize grow")) -- Swap focused window with master, then force focus onto the master slot. -- swapwithmaster's own `master` focus param doesn't stick when invoked from -- the master, so focusmaster master pins focus afterward. The 50ms sleep is -- load-bearing: swapwithmaster fires an async focus event ~1-2ms after it -- returns; without the delay that event lands AFTER focusmaster and flips -- focus back to the detail. The sleep lets the swap's focus settle so -- focusmaster runs last and wins. Proven via instrumented capture (19/19). hl.bind(mod .. " + RETURN", hl.dsp.layout("swapwithmaster && sleep 0.05 && hyprctl dispatch layoutmsg focusmaster master")) hl.bind(mod .. " + G", hl.dsp.window.center()) hl.bind(mod .. " + TAB", hl.dsp.focus({ workspace = "previous" })) hl.bind(mod .. " + SHIFT + C", hl.dsp.window.close()) -- Layouts: master -> monocle -- Cycle with Shift+arrows, or jump directly with Shift+T/M -- (scrolling layout disabled until frame-fit + wrap-around work lands) hl.bind(mod .. " + SHIFT + RIGHT", hl.dsp.exec_cmd("layout-cycle next")) hl.bind(mod .. " + SHIFT + LEFT", hl.dsp.exec_cmd("layout-cycle prev")) hl.bind(mod .. " + SHIFT + T", hl.dsp.exec_cmd("hyprctl keyword general:layout master && hyprctl keyword master:orientation left")) hl.bind(mod .. " + SHIFT + M", hl.dsp.exec_cmd("hyprctl keyword general:layout monocle")) hl.bind(mod .. " + SHIFT + SPACE", hl.dsp.window.float({ action = "toggle" })) -- Master layout adjustments hl.bind(mod .. " + U", hl.dsp.layout("addmaster")) hl.bind(mod .. " + D", hl.dsp.layout("removemaster")) -- Stash windows (hide to special workspace) -- O = stash focused / Alt+O = stash others / Shift+O = restore all hl.bind(mod .. " + O", hl.dsp.exec_cmd("stash-window")) hl.bind(mod .. " + ALT + O", hl.dsp.exec_cmd("stash-others")) hl.bind(mod .. " + SHIFT + O", hl.dsp.exec_cmd("stash-restore")) -- Gaps between windows only; window-gaps leaves the monitor-edge gap -- (general:gaps_out) fixed, so widening/narrowing moves the space between -- windows, not the screen-edge margin. hl.bind(mod .. " + MINUS", hl.dsp.exec_cmd("window-gaps narrow")) hl.bind(mod .. " + EQUAL", hl.dsp.exec_cmd("window-gaps widen")) hl.bind(mod .. " + SHIFT + EQUAL", hl.dsp.exec_cmd("window-gaps reset")) hl.bind(mod .. " + SHIFT + MINUS", hl.dsp.exec_cmd("window-gaps zero")) -- Auto-dim toggle (D = dim). Same action as clicking the waybar custom/dim icon. hl.bind(mod .. " + SHIFT + D", hl.dsp.exec_cmd("dim-toggle")) hl.bind(mod .. " + SHIFT + G", hl.dsp.exec_cmd("settings-panel")) -- Caffeine (keep-awake) toggle. Same action as clicking the waybar -- custom/caffeine icon — flips the hypridle daemon so the screen will / won't -- lock. Stays on $mod+I ($mod+C is taken by hyprpicker; no free caffeine key). hl.bind(mod .. " + I", hl.dsp.exec_cmd("caffeine-toggle")) -- Airplane mode (low-power: wifi off + CPU/brightness/services). A deliberate -- keybind, not a bar click — engaging it disconnects you, so it shouldn't be a -- misclick away. The custom/net module shows the state; this toggles it. -- On Super+Shift+X ("X" = everything off); Super+Shift+A toggles push-to-talk. hl.bind(mod .. " + SHIFT + X", hl.dsp.exec_cmd("airplane-mode")) -- Toggle bar visibility, or relaunch waybar if it crashed (no exec-once respawn). hl.bind(mod .. " + B", hl.dsp.exec_cmd("waybar-toggle")) -- Collapse / expand the left or right side of the bar to its base set -- (same action as clicking the side's arrowhead). [ = left, ] = right. hl.bind(mod .. " + bracketleft", hl.dsp.exec_cmd("waybar-collapse left")) hl.bind(mod .. " + bracketright", hl.dsp.exec_cmd("waybar-collapse right")) -- Fullscreen hl.bind(mod .. " + F11", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" })) -- Workspaces 1-9 (from DWM TAGKEYS) hl.bind(mod .. " + 1", hl.dsp.focus({ workspace = 1 })) hl.bind(mod .. " + 2", hl.dsp.focus({ workspace = 2 })) hl.bind(mod .. " + 3", hl.dsp.focus({ workspace = 3 })) hl.bind(mod .. " + 4", hl.dsp.focus({ workspace = 4 })) hl.bind(mod .. " + 5", hl.dsp.focus({ workspace = 5 })) hl.bind(mod .. " + 6", hl.dsp.focus({ workspace = 6 })) hl.bind(mod .. " + 7", hl.dsp.focus({ workspace = 7 })) hl.bind(mod .. " + 8", hl.dsp.focus({ workspace = 8 })) hl.bind(mod .. " + 9", hl.dsp.focus({ workspace = 9 })) -- Move window to workspace (from DWM tag) hl.bind(mod .. " + SHIFT + 1", hl.dsp.window.move({ workspace = 1, follow = false })) hl.bind(mod .. " + SHIFT + 2", hl.dsp.window.move({ workspace = 2, follow = false })) hl.bind(mod .. " + SHIFT + 3", hl.dsp.window.move({ workspace = 3, follow = false })) hl.bind(mod .. " + SHIFT + 4", hl.dsp.window.move({ workspace = 4, follow = false })) hl.bind(mod .. " + SHIFT + 5", hl.dsp.window.move({ workspace = 5, follow = false })) hl.bind(mod .. " + SHIFT + 6", hl.dsp.window.move({ workspace = 6, follow = false })) hl.bind(mod .. " + SHIFT + 7", hl.dsp.window.move({ workspace = 7, follow = false })) hl.bind(mod .. " + SHIFT + 8", hl.dsp.window.move({ workspace = 8, follow = false })) hl.bind(mod .. " + SHIFT + 9", hl.dsp.window.move({ workspace = 9, follow = false })) -- Monitor focus (from DWM focusmon) hl.bind(mod .. " + COMMA", hl.dsp.focus({ monitor = -1 })) hl.bind(mod .. " + PERIOD", hl.dsp.focus({ monitor = "+1" })) hl.bind(mod .. " + SHIFT + COMMA", hl.dsp.window.move({ monitor = "-1" })) hl.bind(mod .. " + SHIFT + PERIOD", hl.dsp.window.move({ monitor = "+1" })) -- ============================================================================ -- Scratchpads (via pyprland) -- ============================================================================ -- Configured in ~/.config/hypr/pyprland.toml -- Uses normal workspaces (not special), so new windows won't be captured hl.bind(mod .. " + SHIFT + RETURN", hl.dsp.exec_cmd("pypr toggle term")) hl.bind(mod .. " + A", hl.dsp.exec_cmd("audio-panel")) hl.bind(mod .. " + R", hl.dsp.exec_cmd("pypr toggle monitor")) hl.bind(mod .. " + SHIFT + N", hl.dsp.exec_cmd("net panel")) hl.bind(mod .. " + SLASH", hl.dsp.exec_cmd("pypr toggle music")) -- Magnify (zoom) -- mod+Z zooms and enters the "zoom" submap; inside it, Escape or mod+Z -- unzooms and returns to the normal keymap. Exit forces `pypr zoom 1` -- (factor 1) so submap state and zoom state can't desync. Note: while -- zoomed, other Hyprland binds pause until you exit the submap. hl.bind(mod .. " + Z", hl.dsp.exec_cmd("pypr zoom")) hl.bind(mod .. " + Z", hl.dsp.submap("zoom")) hl.define_submap("zoom", function() hl.bind("ESCAPE", hl.dsp.exec_cmd("pypr zoom 1")) hl.bind("ESCAPE", hl.dsp.submap("reset")) hl.bind(mod .. " + Z", hl.dsp.exec_cmd("pypr zoom 1")) hl.bind(mod .. " + Z", hl.dsp.submap("reset")) end) -- Calculator (not a scratchpad, just launches app) hl.bind(mod .. " + X", hl.dsp.exec_cmd("calc-toggle")) hl.bind(mod .. " + C", hl.dsp.exec_cmd("hyprpicker -a")) hl.bind(mod .. " + CONTROL + C", hl.dsp.exec_cmd("clock-panel toggle")) -- Media/hardware keys hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("pactl set-sink-volume @DEFAULT_SINK@ +5%"), { locked = true, repeating = true }) hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("pactl set-sink-volume @DEFAULT_SINK@ -5%"), { locked = true, repeating = true }) hl.bind("XF86AudioMute", hl.dsp.exec_cmd("audio quick-mute"), { locked = true }) hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl s +10%"), { locked = true, repeating = true }) hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl s 10%-"), { locked = true, repeating = true }) -- Microphone mute toggle (waybar pulseaudio#mic indicator follows via PipeWire events). -- On the hardware mic-mute key. Super+Shift+A used to duplicate this; it now -- toggles push-to-talk mode instead (mic-toggle stays reachable on the hw key). hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("mic-toggle"), { locked = true }) -- Push-to-talk toggle: enter PTT (mic muted, hold key armed) / exit (restore). -- The hold key is configurable (audio config ptt_key = Control_R or mouse:NNN). hl.bind(mod .. " + SHIFT + A", hl.dsp.exec_cmd("audio ptt-toggle")) -- Bluetooth panel (blueman retired in favor of the bt panel) hl.bind(mod .. " + SHIFT + B", hl.dsp.exec_cmd("bt-panel")) -- Screenshots (grim + slurp + fuzzel menu) -- Shift+S captures the whole desktop with no pointer interaction, so it -- works on scratchpads and popups that region-select would dismiss hl.bind(mod .. " + S", hl.dsp.exec_cmd("screenshot region")) hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("screenshot fullscreen")) hl.bind("CTRL" .. mod .. " + S", hl.dsp.exec_cmd("screenshot fullscreen")) -- Lock screen hl.bind(mod .. " + ESCAPE", hl.dsp.exec_cmd("hyprlock")) -- Audio mute cycle (M for Mute): one key walks volume/mic through all four -- on/off combinations. The waybar pulseaudio modules follow via PipeWire. hl.bind(mod .. " + M", hl.dsp.exec_cmd("audio-cycle")) -- Exit/session hl.bind(mod .. " + SHIFT + Q", hl.dsp.exec_cmd("pgrep -x wlogout || wlogout-menu")) -- mod+Shift+Backspace no longer exits outright — it enters the "exitconfirm" -- submap, where a second Backspace confirms and anything else backs out. -- The bare bind killed two live sessions on 2026-07-22: it sits one key from -- the mod+Shift cluster (Q wlogout, C killactive, Return terminal, G settings) -- and took the whole session down with no prompt. wlogout owns the normal -- exit path; this stays as the keyboard escape hatch for when wlogout won't -- come up, so it's gated rather than removed. hl.bind(mod .. " + SHIFT + BACKSPACE", hl.dsp.submap("exitconfirm")) hl.define_submap("exitconfirm", function() hl.bind("BACKSPACE", hl.dsp.exit()) hl.bind("ESCAPE", hl.dsp.submap("reset")) hl.bind("catchall", hl.dsp.submap("reset")) end) hl.bind(mod .. " + SHIFT + ESCAPE", hl.dsp.exec_cmd("hyprctl reload")) hl.bind("CTRL + ALT" .. mod .. " + K", hl.dsp.exec_cmd("hyprctl kill")) -- Mouse bindings (from DWM buttons) hl.bind(mod .. " + mouse:272", hl.dsp.window.drag()) hl.bind(mod .. " + mouse:273", hl.dsp.window.resize()) hl.bind(mod .. " + SHIFT + mouse:272", hl.dsp.window.resize()) -- ============================================================================ -- Machine-local overrides -- ============================================================================ -- Sourced last so machine-specific settings (monitor scale, gaps, keybinds) -- override the defaults above. See conf.d/local.conf. -- Source: $HOME/.config/hypr/conf.d/*.conf (glob; resolved at runtime). Each matched .conf must be converted to .lua. hl_source_glob("$HOME/.config/hypr/conf.d/*.lua") hl.on("hyprland.start", function() hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE") hl.exec_cmd("systemctl --user start hyprland-session.target; systemctl --user restart xdg-desktop-portal-hyprland xdg-desktop-portal-gtk; systemctl --user restart xdg-desktop-portal; waybar-active-config && waybar -c \"$XDG_RUNTIME_DIR/waybar/config\" -s ~/.config/waybar/style.css 2>&1 | grep -v \"LIBDBUSMENU-GLIB-WARNING\" > ~/.local/var/log/waybar-$(date +%Y-%m-%d-%H%M%S).log") hl.exec_cmd("/usr/lib/polkit-kde-authentication-agent-1") hl.exec_cmd("/usr/bin/gnome-keyring-daemon --start --components=pkcs11,secrets,ssh") hl.exec_cmd("dunst > ~/.local/var/log/dunst-$(date +%Y-%m-%d-%H%M%S).log 2>&1") hl.exec_cmd("awww-daemon & sleep 1 && { settings restore-wallpaper || waypaper --restore; }") hl.exec_cmd("touchpad-auto") hl.exec_cmd("pkill -x hypridle; hypridle-start > ~/.local/var/log/hypridle-$(date +%Y-%m-%d-%H%M%S).log 2>&1") hl.exec_cmd("/usr/lib/geoclue-2.0/demos/agent") hl.exec_cmd("gammastep > ~/.local/var/log/gammastep-$(date +%Y-%m-%d-%H%M%S).log 2>&1") hl.exec_cmd("mpd") hl.exec_cmd("settings restore > ~/.local/var/log/settings-restore-$(date +%Y-%m-%d-%H%M%S).log 2>&1") hl.exec_cmd("pypr > ~/.local/var/log/pypr-$(date +%Y-%m-%d-%H%M%S).log 2>&1") hl.exec_cmd("hypr-refocus-scratchpad") hl.exec_cmd("wait-for-tray && signal-desktop --start-in-tray --ozone-platform=wayland") hl.exec_cmd("env QT_FONT_DPI=108 protonmail-bridge --no-window") end) hl.on("config.reloaded", function() hl.exec_cmd("for i in 1 2 3; do sleep 0.2; waybar-reserve; done") end)