-- ───────────────────────────────────────────────────────────────────────────── -- Keybindings -- -- Ported 1:1 from the GNOME + Forge setup this replaces. Where Hyprland has no -- equivalent, the deviation is called out in a comment rather than silently -- dropped. -- -- The mental model, unchanged from Forge: -- SUPER -> act on windows (focus / move / swap / resize) -- ALT -> act on workspaces -- SUPER + CTRL -> change layout structure (split, float, swap) -- ───────────────────────────────────────────────────────────────────────────── local mod = "SUPER" -- Programs, matched to the GNOME custom keybindings and media-key settings. local terminal = "kitty" local editor = "kitty nvim ." local browser = "helium-browser-bin" local files = "nautilus --new-window" local calculator = "gnome-calculator" local mail = "flatpak run org.mozilla.thunderbird_esr" -- Panama owns the Hyprland and shell controls. GNOME Settings remains installed -- and searchable in Vicinae for its hardware/account panels. local settings = "qs ipc call settings toggle" local sysmonitor = "flatpak run io.missioncenter.MissionCenter" -- Vicinae is the Raycast-style launcher. `vicinae toggle` shows/hides the -- window against the already-running server (started in autostart.lua). local launcher = "vicinae toggle" -- Quickshell IPC targets. See quickshell/shell.qml for the handlers. local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end -- ── Applications ──────────────────────────────────────────────────────────── hl.bind(mod .. " + T", hl.dsp.exec_cmd(terminal), { description = "Terminal" }) hl.bind(mod .. " + N", hl.dsp.exec_cmd(editor), { description = "Neovim" }) hl.bind(mod .. " + W", hl.dsp.exec_cmd(browser), { description = "Browser" }) hl.bind(mod .. " + F", hl.dsp.exec_cmd(files), { description = "Files" }) hl.bind(mod .. " + C", hl.dsp.exec_cmd(calculator), { description = "Calculator" }) hl.bind(mod .. " + E", hl.dsp.exec_cmd(mail), { description = "Mail" }) hl.bind(mod .. " + I", hl.dsp.exec_cmd(settings), { description = "Panama Settings" }) hl.bind("CTRL + SHIFT + Escape", hl.dsp.exec_cmd(sysmonitor), { description = "System monitor" }) -- ── Launcher ──────────────────────────────────────────────────────────────── -- All three keys open the same launcher, on purpose: SUPER+A and SUPER+R were -- the GNOME app-grid and run-dialog shortcuts, and SUPER+SPACE is here as a -- third option to settle on. Vicinae covers apps, calculator, files, clipboard, -- emoji and window switching, so a separate run dialog and app grid are gone. hl.bind(mod .. " + A", hl.dsp.exec_cmd(launcher), { description = "Launcher" }) hl.bind(mod .. " + R", hl.dsp.exec_cmd(launcher), { description = "Launcher" }) hl.bind(mod .. " + Space", hl.dsp.exec_cmd(launcher), { description = "Launcher" }) -- Emergency fallback launcher. Vicinae runs as a systemd user service and the -- bar is Quickshell; if either fails to come up, this is how you start an -- application without dropping to a TTY. Depends on nothing but wofi itself. hl.bind(mod .. " + SHIFT + R", hl.dsp.exec_cmd("wofi"), { description = "Fallback launcher" }) -- Clipboard history and emoji, straight into the relevant launcher view. -- Deeplink form is the one from vicinae's own Hyprland quickstart. hl.bind(mod .. " + V", hl.dsp.exec_cmd("vicinae vicinae://launch/clipboard/history"), { description = "Clipboard history" }) hl.bind(mod .. " + Period", hl.dsp.exec_cmd("vicinae vicinae://launch/emoji/search"), { description = "Emoji picker" }) -- ── Shell surfaces (Quickshell) ───────────────────────────────────────────── -- SUPER+S was GNOME's quick settings; kept. hl.bind(mod .. " + S", hl.dsp.exec_cmd(qs("quicksettings", "toggle")), { description = "Quick settings" }) -- Start a 45-minute focus session on the current workspace, or reveal its -- Signal Glass controls if one is already running. hl.bind(mod .. " + SHIFT + F", hl.dsp.exec_cmd(qs("focus", "reveal")), { description = "Focus session" }) -- Workspace overview. GNOME put this on a bare SUPER tap; tap-detection on a -- modifier misfires when you're fast with SUPER+key combos, so it lives on a -- real chord instead. SUPER+grave was Forge's "cycle windows of same app", -- which Hyprland has no equivalent for. hl.bind(mod .. " + grave", hl.dsp.exec_cmd(qs("overview", "toggle")), { description = "Overview" }) -- Notification centre. hl.bind(mod .. " + B", hl.dsp.exec_cmd(qs("notifications", "toggle")), { description = "Notifications" }) -- Screenshot / screen record. One key, then pick screen / window / region and -- whether to capture or record -- reproducing GNOME's Print-screen UI. hl.bind("Print", hl.dsp.exec_cmd(qs("capture", "open")), { description = "Screenshot / record" }) -- The GNOME direct-capture variants, kept as shortcuts past the picker. hl.bind("SHIFT + Print", hl.dsp.exec_cmd(qs("capture", "screenNow")), { description = "Screenshot: whole screen" }) hl.bind("ALT + Print", hl.dsp.exec_cmd(qs("capture", "windowNow")), { description = "Screenshot: window" }) -- Local OCR and QR/barcode recognition through the same region picker. This -- opens directly in Selection + Read mode; Print still exposes every mode. hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd(qs("screen-intelligence", "open")), { description = "Screen Intelligence" }) -- Colour picker: copies the hex under the cursor to the clipboard. hl.bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("hyprpicker -a -f hex"), { description = "Colour picker" }) -- ── Window management ─────────────────────────────────────────────────────── hl.bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" }) hl.bind(mod .. " + U", hl.dsp.window.fullscreen({ mode = "fullscreen" }), { description = "Fullscreen" }) -- Forge: window-toggle-float / window-toggle-always-float. -- "Always float" has no Hyprland equivalent (it wrote a persistent rule); pin -- is the nearest useful thing -- the window floats above every workspace. hl.bind(mod .. " + CTRL + C", hl.dsp.window.float({ action = "toggle" }), { description = "Toggle float" }) hl.bind(mod .. " + CTRL + SHIFT + C", hl.dsp.window.pin({ action = "toggle" }), { description = "Pin window" }) -- Forge: con-split-layout-toggle / con-split-horizontal / con-split-vertical. hl.bind(mod .. " + CTRL + G", hl.dsp.layout("togglesplit"), { description = "Toggle split direction" }) hl.bind(mod .. " + CTRL + Z", hl.dsp.layout("preselect r"), { description = "Next window splits right" }) hl.bind(mod .. " + CTRL + V", hl.dsp.layout("preselect d"), { description = "Next window splits down" }) -- Forge: window-shrink / window-expand / window-reset-sizes. hl.bind(mod .. " + bracketleft", hl.dsp.layout("splitratio -0.05"), { repeating = true, description = "Shrink" }) hl.bind(mod .. " + bracketright", hl.dsp.layout("splitratio +0.05"), { repeating = true, description = "Expand" }) hl.bind(mod .. " + equal", hl.dsp.layout("splitratio exact 0.5"), { description = "Reset split" }) -- Focus (Forge: window-focus-*). Both vim keys and arrows, as in Forge. hl.bind(mod .. " + H", hl.dsp.focus({ direction = "l" }), { description = "Focus left" }) hl.bind(mod .. " + J", hl.dsp.focus({ direction = "d" }), { description = "Focus down" }) hl.bind(mod .. " + K", hl.dsp.focus({ direction = "u" }), { description = "Focus up" }) hl.bind(mod .. " + L", hl.dsp.focus({ direction = "r" }), { description = "Focus right" }) hl.bind(mod .. " + left", hl.dsp.focus({ direction = "l" })) hl.bind(mod .. " + down", hl.dsp.focus({ direction = "d" })) hl.bind(mod .. " + up", hl.dsp.focus({ direction = "u" })) hl.bind(mod .. " + right", hl.dsp.focus({ direction = "r" })) -- Move (Forge: window-move-*). hl.bind(mod .. " + SHIFT + H", hl.dsp.window.move({ direction = "l" }), { description = "Move window left" }) hl.bind(mod .. " + SHIFT + J", hl.dsp.window.move({ direction = "d" }), { description = "Move window down" }) hl.bind(mod .. " + SHIFT + K", hl.dsp.window.move({ direction = "u" }), { description = "Move window up" }) hl.bind(mod .. " + SHIFT + L", hl.dsp.window.move({ direction = "r" }), { description = "Move window right" }) -- Swap (Forge: window-swap-*). hl.bind(mod .. " + CTRL + H", hl.dsp.window.swap({ direction = "l" }), { description = "Swap left" }) hl.bind(mod .. " + CTRL + J", hl.dsp.window.swap({ direction = "d" }), { description = "Swap down" }) hl.bind(mod .. " + CTRL + K", hl.dsp.window.swap({ direction = "u" }), { description = "Swap up" }) hl.bind(mod .. " + CTRL + L", hl.dsp.window.swap({ direction = "r" }), { description = "Swap right" }) -- Resize (Forge: window-resize--). -- -- Forge resized one named edge at a time. Hyprland resizes the active window -- along an axis and lets the layout decide which edge actually moves, so the -- eight Forge keys collapse onto four behaviours. The pairing is kept -- consistent with the original: Y/B/O/M are horizontal, I/P/U/N are vertical, -- and "increase" always grows while "decrease" always shrinks. local step = 60 hl.bind(mod .. " + SHIFT + Y", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" }) hl.bind(mod .. " + SHIFT + O", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" }) hl.bind(mod .. " + SHIFT + B", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" }) hl.bind(mod .. " + SHIFT + M", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" }) hl.bind(mod .. " + SHIFT + I", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" }) hl.bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" }) hl.bind(mod .. " + SHIFT + P", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" }) hl.bind(mod .. " + SHIFT + N", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" }) -- Window cycling (GNOME: cycle-windows on SUPER+Tab). hl.bind(mod .. " + Tab", hl.dsp.window.cycle_next({ next = true }), { description = "Next window" }) hl.bind(mod .. " + SHIFT + Tab", hl.dsp.window.cycle_next({ next = false }), { description = "Previous window" }) -- Jump back to the previously focused window. hl.bind(mod .. " + SHIFT + grave", hl.dsp.focus({ last = true }), { description = "Last window" }) -- Mouse: drag to move, right-drag to resize. hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) -- ── Workspaces ────────────────────────────────────────────────────────────── -- ALT is the workspace modifier, matching the GNOME setup. -- -- Plain relative selectors ("+1" / "-1") reproduce GNOME's dynamic workspaces: -- moving right past the last workspace creates a new one, and moving left from -- the first clamps instead of wrapping. hl.bind("ALT + H", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" }) hl.bind("ALT + L", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" }) hl.bind("ALT + SHIFT + H", hl.dsp.window.move({ workspace = "-1" }), { description = "Move window to workspace left" }) hl.bind("ALT + SHIFT + L", hl.dsp.window.move({ workspace = "+1" }), { description = "Move window to workspace right" }) -- GNOME also had these on CTRL+ALT+Up/Down. hl.bind("CTRL + ALT + up", hl.dsp.focus({ workspace = "-1" })) hl.bind("CTRL + ALT + down", hl.dsp.focus({ workspace = "+1" })) -- Direct jump. ALT+0 is workspace 10. for i = 1, 10 do local key = i % 10 hl.bind("ALT + " .. key, hl.dsp.focus({ workspace = i }), { description = "Workspace " .. i }) hl.bind("ALT + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }), { description = "Move window to workspace " .. i }) end -- Scroll the mouse wheel over the desktop with SUPER held to change workspace. -- (Scrolling the workspace indicator in the bar does the same; that's handled -- in quickshell/modules/bar/Workspaces.qml.) hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" })) hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" })) -- Minimise, as far as Hyprland has one. -- -- Hyprland has no minimise: it receives the request (the binary has -- setSetMinimized handlers for xdg, XWayland and foreign-toplevel) but exposes -- no dispatcher, no config option and not even an event to hook, so titlebar -- minimise buttons are inert and cannot be made to work. A tiling WM has no -- iconified state and no taskbar to restore from. -- -- The scratchpad is the honest equivalent: the window goes away, and the same -- key brings it back. Bound to X to match the muscle memory it replaces. hl.bind(mod .. " + X", hl.dsp.workspace.toggle_special("scratch"), { description = "Toggle scratchpad (restore minimised)" }) hl.bind(mod .. " + SHIFT + X", hl.dsp.window.move({ workspace = "special:scratch" }), { description = "Minimise to scratchpad" }) -- ── Session ───────────────────────────────────────────────────────────────── -- GNOME's lock was SUPER+L, which is "focus right" here, so lock moves to -- CTRL+ALT+L -- the other binding most people already have in muscle memory. hl.bind("CTRL + ALT + L", hl.dsp.exec_cmd("loginctl lock-session"), { description = "Lock" }) hl.bind("CTRL + ALT + Delete", hl.dsp.exec_cmd(qs("powermenu", "toggle")), { description = "Power menu" }) -- ── Media and volume ──────────────────────────────────────────────────────── -- locked = true keeps these working on the lock screen, as they do in GNOME. -- 6% steps match the GNOME volume-step setting. hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 6%+"), { locked = true, repeating = true }) hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 6%-"), { locked = true, repeating = true }) hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true }) hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true }) -- Fine-grained steps, matching GNOME's shift/alt volume modifiers. hl.bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 1%+"), { locked = true, repeating = true }) hl.bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-"), { locked = true, repeating = true }) hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true }) hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) hl.bind("XF86AudioStop", hl.dsp.exec_cmd("playerctl stop"), { locked = true }) hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true }) hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true }) -- Hardware keys GNOME mapped that have obvious equivalents. hl.bind("XF86Tools", hl.dsp.exec_cmd(settings)) hl.bind("XF86Calculator", hl.dsp.exec_cmd(calculator)) hl.bind("XF86Explorer", hl.dsp.exec_cmd(files)) hl.bind("XF86WWW", hl.dsp.exec_cmd(browser)) hl.bind("XF86Mail", hl.dsp.exec_cmd(mail)) hl.bind("XF86Search", hl.dsp.exec_cmd(launcher)) return true