aboutsummaryrefslogtreecommitdiffstats
path: root/keybinds/Binds.js
blob: 1f809cf5d5c987dc1d3c4c6ed473d9d2922ac01d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
.pragma library
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// 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;
}