// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Io import Quickshell.Hyprland import QtQuick import "../.." import "Hypr.js" as H // Every bind Hyprland has loaded, joined to the hl.bind call in the Lua // config that made it, so a row shows its action and file:line, and a combo // written as a plain string can be changed from the page. hyprctl reports // every Lua action as an opaque __lua number: the source is the only place // the action is readable. Module { id: bm name: "shortcuts" label: "Keyboard shortcuts" // nf-fa-keyboard_o, present in Inconsolata Nerd Font. icon: "" settings: true group: "Hyprland" keywords: ["keybind", "bind", "hotkey", "combo", "rebind", "keybindings.lua", "hyprland", "clash"] readonly property string dir: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/hypr" // hyprland.lua and its sections, relative to dir. property var files: [] property var live: [] // [{keys, desc, src, bind}], see Hypr.js. property var rows: [] // file -> its text as last read or written. FileView.text() still // returns the old text right after setText(), so a second rebind in the // same file would undo the first. property var texts: ({}) function refresh() { lister.running = false; lister.running = true; binds.running = false; binds.running = true; } function join() { const fs = []; for (const v of views.instances) fs.push({ path: v.modelData, text: bm.texts[v.modelData] ?? "" }); bm.rows = H.rows(bm.live, H.sources(fs)); bm.searchItems = bm.rows.map((r, i) => ({ text: r.keys.join(" + ") + " " + r.desc, key: "bind:" + i })); } // A config edit anywhere, by this page or an editor, reloads Hyprland. property Connections reloads: Connections { target: Hyprland function onRawEvent(event) { if (event.name === "configreloaded") bm.refresh(); } } property Process lister: Process { command: ["sh", "-c", 'cd "$1" && ls hyprland.lua sections/*.lua', "sh", bm.dir] stdout: StdioCollector { onStreamFinished: { const l = this.text.split("\n").filter(x => x !== ""); if (JSON.stringify(l) !== JSON.stringify(bm.files)) bm.files = l; } } } property Process binds: Process { command: ["hyprctl", "binds", "-j"] stdout: StdioCollector { onStreamFinished: { try { bm.live = JSON.parse(this.text); } catch (e) { bm.live = []; } bm.join(); } } } property Variants views: Variants { model: bm.files FileView { required property string modelData path: bm.dir + "/" + modelData // Hyprland reloads on every change, so it must never read a // half-written file. atomicWrites: true watchChanges: true printErrors: false onFileChanged: reload() onLoaded: { bm.texts[modelData] = text(); bm.join(); } } } // Changes row's combo to combo, calling done(error) with "" on success. // Hyprland checks the combo first: an eval of a do-nothing bind fails on // an unknown modifier or keysym and changes nothing. One that passes is // registered live until the reload the write causes clears it. property var pending: null function rebind(row, combo, done) { const c = combo.trim(); if (H.parseCombo(c) === null) { done("Write it as modifiers and a key: SUPER + SHIFT + q. Modifiers are SUPER, CTRL, ALT, SHIFT."); return; } bm.pending = { row: row, combo: H.formatCombo(c), done: done }; probe.command = ["hyprctl", "eval", 'hl.bind("' + H.quote(bm.pending.combo) + '", function() end)']; probe.running = false; probe.running = true; } property Process probe: Process { stdout: StdioCollector { id: probeOut } onExited: code => { const p = bm.pending; bm.pending = null; if (!p) return; if (code !== 0) { p.done(probeOut.text.trim().replace(/^error: .*?:1: /, "") || "Hyprland refused it."); return; } const v = bm.views.instances.find(x => x.modelData === p.row.src.file); const t = v ? H.rebind(bm.texts[v.modelData] ?? "", p.row.src, p.combo) : null; if (t === null) { // Clears the probe's bind, which no write will. Quickshell.execDetached(["hyprctl", "reload", "config-only"]); p.done(p.row.src.file + " changed underneath, reloaded. Try again."); return; } bm.texts[v.modelData] = t; v.setText(t); bm.join(); p.done(""); } } page: Component { Page { title: "Keyboard shortcuts" BindsPage { width: parent.width; mod: bm } } } }