-- ─────────────────────────────────────────────────────────────────────────────
-- 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 = "Panama 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
