.pragma library // 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. // hyprctl binds -j into display groups. Pure, so node can check it: // see test_binds.js. // Modmask bits, in the order they are written in keybindings.lua. // ponytail: CAPS/MOD2/MOD3/MOD5 bits are not decoded and vanish from the // combo; add them here if a bind ever uses one. // SUPER shows as Win, the key it is on. const MODS = [[64, "Win"], [4, "CTRL"], [8, "ALT"], [1, "SHIFT"]]; function mods(mask) { return MODS.filter(m => mask & m[0]).map(m => m[1]); } // Display names for keys whose Hyprland name is hard to remember. A key not // listed shows as Hyprland names it. The Fn combos are where the XF86 keys // sit on this keyboard, which is how they are pressed. const NAMES = Object.assign(Object.create(null), { "mouse:272": "LMB", "mouse:273": "RMB", "mouse_up": "Mouse wheel up", "mouse_down": "Mouse wheel down", "XF86AudioRaiseVolume": "Volume +", "XF86AudioLowerVolume": "Volume -", "XF86AudioMute": "Mute", "XF86AudioPlay": "Play/Pause", "XF86AudioPrev": "Previous song", "XF86AudioNext": "Next song", "XF86Calculator": "Fn+F9", "XF86Mail": "Fn+F10", "XF86Search": "Fn+F11", }); function keys(bind) { return mods(bind.modmask).concat([NAMES[bind.key] ?? bind.key]).join(" + "); } // Where a bind sits on the macropad, keyed by the combo as keys() writes it, // so a renamed key above must be renamed here too. key is 1-12, left to // right and top to bottom; knob is the top or bottom encoder with how it is // turned or pressed; mod is what is held with it. The pad's firmware sends these combos. const PAD = Object.assign(Object.create(null), { "Win + w": { key: 1 }, "Win + SHIFT + w": { key: 1, mod: "Shift" }, "Win + r": { key: 2 }, "Win + SHIFT + r": { key: 2, mod: "Shift" }, "Win + g": { key: 3 }, "CTRL + SHIFT + l": { key: 4 }, "CTRL + ALT + SHIFT + l": { key: 4, mod: "Alt" }, "ALT + Return": { key: 5 }, "ALT + SHIFT + Return": { key: 5, mod: "Shift" }, "ALT + h": { key: 6 }, "Win + CTRL + r": { key: 7 }, "ALT + w": { key: 8 }, "Volume -": { knob: "top", turn: "Turn left" }, "Volume +": { knob: "top", turn: "Turn right" }, "Mute": { knob: "top", turn: "Press" }, "Win + Volume -": { knob: "top", turn: "Turn left", mod: "Win" }, "Win + Volume +": { knob: "top", turn: "Turn right", mod: "Win" }, "Previous song": { knob: "bottom", turn: "Turn left" }, "Next song": { knob: "bottom", turn: "Turn right" }, "Play/Pause": { knob: "bottom", turn: "Press" }, "Win + Previous song": { knob: "bottom", turn: "Turn left", mod: "Win" }, "Win + Next song": { knob: "bottom", turn: "Turn right", mod: "Win" }, }); // The bottom row is workspaces 1-4, 5-8 with Shift, and moves the window // there with Win held on the keyboard. for (let i = 1; i <= 4; i++) { PAD["CTRL + F" + i] = { key: 8 + i }; PAD["CTRL + SHIFT + F" + i] = { key: 8 + i, mod: "Shift" }; PAD["Win + CTRL + F" + i] = { key: 8 + i, mod: "Win" }; PAD["Win + CTRL + SHIFT + F" + i] = { key: 8 + i, mod: "Win + Shift" }; } // A description reads "Group: Label". A bind without one still shows, last, // so a gap in the config is visible rather than silently missing. function group(binds) { const groups = []; const byName = Object.create(null); const undescribed = []; for (const b of binds) { const d = b.description || ""; if (!d) { undescribed.push({ keys: keys(b), label: "no description" }); continue; } const i = d.indexOf(": "); const name = i < 0 ? "Other" : d.slice(0, i); const label = i < 0 ? d : d.slice(i + 2); if (!byName[name]) { byName[name] = { name: name, rows: [] }; groups.push(byName[name]); } const row = { keys: keys(b), label: label }; if (PAD[row.keys]) row.pad = PAD[row.keys]; byName[name].rows.push(row); } if (undescribed.length) groups.push({ name: "Undescribed", rows: undescribed }); return groups; } // Everything each pad key or encoder does, from group()'s output: "k1" to // "k12", "top" and "bottom", each a list of { how, label } in list order. function padActions(groups) { const out = Object.create(null); for (const g of groups) { for (const r of g.rows) { if (!r.pad) continue; const id = r.pad.key ? "k" + r.pad.key : r.pad.knob; if (!out[id]) out[id] = []; out[id].push({ how: [r.pad.mod, r.pad.turn || "Press"].filter(Boolean).join(" + "), label: r.label, }); } } return out; }