SUPER+SHIFT+P was bound to both the colour picker and a window-resize action; moved the resize bind to SUPER+SHIFT+Comma, next to its existing N alias, and updated the README's keymap table to match. workspaceBackAndForth and allowWorkspaceCycles were declared in the preference schema but read nowhere, so they had no effect regardless of what a user set them to; wired both into keybinds.lua's workspace config, which is where they actually apply. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
311 lines
20 KiB
Lua
311 lines
20 KiB
Lua
-- ─────────────────────────────────────────────────────────────────────────────
|
|
-- 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 prefs = require("prefs")
|
|
|
|
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"
|
|
local osd = function(action)
|
|
return "$HOME/.config/quickshell/scripts/panama-osd " .. action
|
|
end
|
|
|
|
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
|
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
|
|
|
-- ── Applications ────────────────────────────────────────────────────────────
|
|
-- ── User rebinding ──────────────────────────────────────────────────────────
|
|
-- Every bind below goes through `bind` rather than `hl.bind` directly, so a
|
|
-- chord can be replaced from Panama Settings without this file changing.
|
|
--
|
|
-- Overrides are keyed by the bind's SHIPPED chord, and ONLY the chord is taken
|
|
-- from settings -- the action is always the Lua value written here. A stored
|
|
-- override can therefore move a shortcut but can never make one do something
|
|
-- else, which is the property that makes reading them from a JSON file the
|
|
-- user can edit safe.
|
|
--
|
|
-- Keyed by chord rather than by description because chords are unique and
|
|
-- descriptions are not: "Calculator" is both SUPER+C and the XF86Calculator
|
|
-- hardware key, and keying by description moved both of them onto the same new
|
|
-- chord, silently costing the hardware key.
|
|
--
|
|
-- An override whose chord is not a plausible chord is ignored and the shipped
|
|
-- one is used, so a hand-edited settings file cannot cost you a keymap.
|
|
|
|
local overrides = prefs.get("keybindOverrides", {})
|
|
|
|
local function valid_chord(chord)
|
|
if type(chord) ~= "string" or chord == "" or #chord > 64 then
|
|
return false
|
|
end
|
|
-- "SUPER + SHIFT + K", "XF86AudioPlay", "Print", "SUPER + mouse:272"
|
|
return chord:match("^[%w_+%s:]+$") ~= nil
|
|
end
|
|
|
|
local function bind(chord, action, opts)
|
|
local override = overrides[chord]
|
|
if valid_chord(override) then
|
|
chord = override
|
|
end
|
|
return hl.bind(chord, action, opts)
|
|
end
|
|
|
|
bind(mod .. " + T", hl.dsp.exec_cmd(terminal), { description = "Terminal" })
|
|
bind(mod .. " + N", hl.dsp.exec_cmd(editor), { description = "Neovim" })
|
|
bind(mod .. " + W", hl.dsp.exec_cmd(browser), { description = "Browser" })
|
|
bind(mod .. " + F", hl.dsp.exec_cmd(files), { description = "Files" })
|
|
bind(mod .. " + C", hl.dsp.exec_cmd(calculator), { description = "Calculator" })
|
|
bind(mod .. " + E", hl.dsp.exec_cmd(mail), { description = "Mail" })
|
|
bind(mod .. " + I", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
|
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.
|
|
bind(mod .. " + A", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
|
bind(mod .. " + R", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
|
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.
|
|
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.
|
|
bind(mod .. " + V", hl.dsp.exec_cmd("vicinae vicinae://launch/clipboard/history"),
|
|
{ description = "Clipboard history" })
|
|
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.
|
|
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.
|
|
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.
|
|
bind(mod .. " + grave", hl.dsp.exec_cmd(qs("overview", "toggle")), { description = "Overview" })
|
|
|
|
-- Notification centre.
|
|
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.
|
|
bind("Print", hl.dsp.exec_cmd(qs("capture", "open")), { description = "Screenshot / record" })
|
|
-- The GNOME direct-capture variants, kept as shortcuts past the picker.
|
|
bind("SHIFT + Print", hl.dsp.exec_cmd(qs("capture", "screenNow")), { description = "Screenshot: whole screen" })
|
|
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.
|
|
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.
|
|
bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("hyprpicker -a -f hex"), { description = "Colour picker" })
|
|
|
|
-- ── Window management ───────────────────────────────────────────────────────
|
|
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
|
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.
|
|
bind(mod .. " + CTRL + C", hl.dsp.window.float({ action = "toggle" }), { description = "Toggle float" })
|
|
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.
|
|
bind(mod .. " + CTRL + G", hl.dsp.layout("togglesplit"), { description = "Toggle split direction" })
|
|
bind(mod .. " + CTRL + Z", hl.dsp.layout("preselect r"), { description = "Next window splits right" })
|
|
bind(mod .. " + CTRL + V", hl.dsp.layout("preselect d"), { description = "Next window splits down" })
|
|
|
|
-- Forge: window-shrink / window-expand / window-reset-sizes.
|
|
bind(mod .. " + bracketleft", hl.dsp.layout("splitratio -0.05"), { repeating = true, description = "Shrink" })
|
|
bind(mod .. " + bracketright", hl.dsp.layout("splitratio +0.05"), { repeating = true, description = "Expand" })
|
|
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.
|
|
bind(mod .. " + H", hl.dsp.focus({ direction = "l" }), { description = "Focus left" })
|
|
bind(mod .. " + J", hl.dsp.focus({ direction = "d" }), { description = "Focus down" })
|
|
bind(mod .. " + K", hl.dsp.focus({ direction = "u" }), { description = "Focus up" })
|
|
bind(mod .. " + L", hl.dsp.focus({ direction = "r" }), { description = "Focus right" })
|
|
bind(mod .. " + left", hl.dsp.focus({ direction = "l" }), { description = "Focus left" })
|
|
bind(mod .. " + down", hl.dsp.focus({ direction = "d" }), { description = "Focus down" })
|
|
bind(mod .. " + up", hl.dsp.focus({ direction = "u" }), { description = "Focus up" })
|
|
bind(mod .. " + right", hl.dsp.focus({ direction = "r" }), { description = "Focus right" })
|
|
|
|
-- Move (Forge: window-move-*).
|
|
bind(mod .. " + SHIFT + H", hl.dsp.window.move({ direction = "l" }), { description = "Move window left" })
|
|
bind(mod .. " + SHIFT + J", hl.dsp.window.move({ direction = "d" }), { description = "Move window down" })
|
|
bind(mod .. " + SHIFT + K", hl.dsp.window.move({ direction = "u" }), { description = "Move window up" })
|
|
bind(mod .. " + SHIFT + L", hl.dsp.window.move({ direction = "r" }), { description = "Move window right" })
|
|
|
|
-- Swap (Forge: window-swap-*).
|
|
bind(mod .. " + CTRL + H", hl.dsp.window.swap({ direction = "l" }), { description = "Swap left" })
|
|
bind(mod .. " + CTRL + J", hl.dsp.window.swap({ direction = "d" }), { description = "Swap down" })
|
|
bind(mod .. " + CTRL + K", hl.dsp.window.swap({ direction = "u" }), { description = "Swap up" })
|
|
bind(mod .. " + CTRL + L", hl.dsp.window.swap({ direction = "r" }), { description = "Swap right" })
|
|
|
|
-- Resize (Forge: window-resize-<edge>-<increase|decrease>).
|
|
--
|
|
-- 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
|
|
bind(mod .. " + SHIFT + Y", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
|
bind(mod .. " + SHIFT + O", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
|
bind(mod .. " + SHIFT + B", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
|
bind(mod .. " + SHIFT + M", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
|
bind(mod .. " + SHIFT + I", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
|
bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
|
-- SUPER+SHIFT+P was double-bound with the colour picker above; moved to
|
|
-- Comma, which continues the bottom-row cluster (B/M/N) this axis already
|
|
-- uses rather than landing on an arbitrary free key.
|
|
bind(mod .. " + SHIFT + Comma", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
|
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), now with an overlay
|
|
-- showing what you are choosing between.
|
|
--
|
|
-- The gesture needs three binds, not two. Tab steps the selection, and the
|
|
-- switch is only COMMITTED when the modifier is released -- which is the sole
|
|
-- way the compositor can tell the gesture is finished. That release bind is on
|
|
-- the bare modifier, so it fires on EVERY Super release in the session; the
|
|
-- handler returns immediately when no switch is open, which is why this is
|
|
-- affordable.
|
|
--
|
|
-- The release bind carries no description on purpose: it is not a shortcut
|
|
-- anyone would look up or rebind, and the Shortcuts page lists what it finds.
|
|
bind(mod .. " + Tab", hl.dsp.exec_cmd(qs("switcher", "next")), { description = "Next window" })
|
|
bind(mod .. " + SHIFT + Tab", hl.dsp.exec_cmd(qs("switcher", "previous")), { description = "Previous window" })
|
|
bind(mod, hl.dsp.exec_cmd(qs("switcher", "commit")), { release = true, description = "Commit window switch" })
|
|
-- Jump back to the previously focused window.
|
|
bind(mod .. " + SHIFT + grave", hl.dsp.focus({ last = true }), { description = "Last window" })
|
|
|
|
-- Mouse: drag to move, right-drag to resize.
|
|
bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window with pointer" })
|
|
bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true, description = "Resize window with pointer" })
|
|
|
|
-- ── 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.
|
|
|
|
-- Behaviour for the relative/cyclic binds below. These are Hyprland's own
|
|
-- `binds:` options -- not part of general/dwindle -- and have no other home
|
|
-- in the config, so they are read here rather than in looks.lua.
|
|
hl.config({
|
|
binds = {
|
|
workspace_back_and_forth = prefs.get("workspaceBackAndForth", false),
|
|
allow_workspace_cycles = prefs.get("allowWorkspaceCycles", false),
|
|
},
|
|
})
|
|
|
|
bind("ALT + H", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
|
bind("ALT + L", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
|
bind("ALT + SHIFT + H", hl.dsp.window.move({ workspace = "-1" }), { description = "Move window to workspace left" })
|
|
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.
|
|
bind("CTRL + ALT + up", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
|
bind("CTRL + ALT + down", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
|
|
|
-- Direct jump. ALT+0 is workspace 10.
|
|
for i = 1, 10 do
|
|
local key = i % 10
|
|
bind("ALT + " .. key, hl.dsp.focus({ workspace = i }), { description = "Workspace " .. i })
|
|
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.)
|
|
bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
|
bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
|
|
|
-- 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.
|
|
bind(mod .. " + X", hl.dsp.workspace.toggle_special("scratch"), { description = "Toggle scratchpad (restore minimised)" })
|
|
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.
|
|
bind("CTRL + ALT + L", hl.dsp.exec_cmd("loginctl lock-session"), { description = "Lock" })
|
|
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.
|
|
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 6")), { locked = true, repeating = true , description = "Volume up" })
|
|
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 6")), { locked = true, repeating = true , description = "Volume down" })
|
|
bind("XF86AudioMute", hl.dsp.exec_cmd(osd("volume toggle")), { locked = true , description = "Mute" })
|
|
bind("XF86AudioMicMute", hl.dsp.exec_cmd(osd("microphone toggle")), { locked = true , description = "Mute microphone" })
|
|
|
|
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
|
bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 1")), { locked = true, repeating = true , description = "Volume up (fine)" })
|
|
bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 1")), { locked = true, repeating = true , description = "Volume down (fine)" })
|
|
|
|
bind("XF86AudioPlay", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
|
bind("XF86AudioPause", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
|
bind("XF86AudioNext", hl.dsp.exec_cmd(osd("media next")), { locked = true , description = "Next track" })
|
|
bind("XF86AudioPrev", hl.dsp.exec_cmd(osd("media previous")), { locked = true , description = "Previous track" })
|
|
bind("XF86AudioStop", hl.dsp.exec_cmd(osd("media stop")), { locked = true , description = "Stop playback" })
|
|
|
|
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(osd("brightness up 5")), { locked = true, repeating = true , description = "Brightness up" })
|
|
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(osd("brightness down 5")), { locked = true, repeating = true , description = "Brightness down" })
|
|
|
|
-- Hardware keys GNOME mapped that have obvious equivalents.
|
|
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
|
bind("XF86Calculator", hl.dsp.exec_cmd(calculator), { description = "Calculator" })
|
|
bind("XF86Explorer", hl.dsp.exec_cmd(files), { description = "Files" })
|
|
bind("XF86WWW", hl.dsp.exec_cmd(browser), { description = "Browser" })
|
|
bind("XF86Mail", hl.dsp.exec_cmd(mail), { description = "Mail" })
|
|
bind("XF86Search", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
|
|
|
return true
|