It is the system settings app for this desktop, so it is named the way one is. The window is "Settings", the titlebar is "Settings", the wordmark above the search field is gone, the sidebar's last row is "About", and the status line reads "Desktop is healthy". Prose followed the same rule. Forty-odd strings explained what "Panama" does -- "Panama never animates while idle", "Restore Panama defaults", "Panama looks in ~/Pictures/Wallpapers" -- which is how a product describes itself, not how a settings panel describes a setting. They now say what happens. No user-visible "Panama" remains anywhere in the app. Two consequences worth naming: The SUPER+I shortcut's description is user-visible, because the Shortcuts page is generated from it, so that is renamed too. Rebindings are keyed by shipped chord rather than description, so no existing override is orphaned by this. Three contracts matched the window by title and one matched that shortcut by description; all four are updated. There are no Hyprland window rules keyed on the title, so nothing about the window's placement changes. The desktop entry is now Name=Settings, but the FILE keeps its panama-settings name, as does the icon: the dock pins applications by desktop id, and renaming the file would silently unpin it. dock-pins-contract covers exactly that. The dated design docs under docs/superpowers keep the old name. They are a record of what was decided when, and editing them to agree with the present would make them lie about the past. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
285 lines
19 KiB
Lua
285 lines
19 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" })
|
|
bind(mod .. " + SHIFT + P", 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).
|
|
bind(mod .. " + Tab", hl.dsp.window.cycle_next({ next = true }), { description = "Next window" })
|
|
bind(mod .. " + SHIFT + Tab", hl.dsp.window.cycle_next({ next = false }), { description = "Previous window" })
|
|
-- 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.
|
|
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
|