aboutsummaryrefslogtreecommitdiffstats
path: root/keybinds/Keybinds.qml
blob: be28e2f7735a655bb617ff9ecd10106eb9348f91 (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// 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.

import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import "Binds.js" as Binds

Scope {
    id: root

    property bool open: false
    property string monitor: "DP-1"
    property var groups: []
    // Non-empty while loading or after a failure; the grid shows only when
    // this is empty, so a stale list from the last opening never shows.
    property string status: "Loading..."

    readonly property var screenObj:
        Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0]

    function toggle() { root.open = !root.open; }
    function close() { root.open = false; }

    // Asked afresh on every opening, so the list is what Hyprland has loaded
    // right now: an edit to keybindings.lua shows after `hyprctl reload`, and
    // not before, which is when it becomes true.
    onOpenChanged: if (open) {
        root.status = "Loading...";
        // A Process already running ignores running = true.
        proc.running = false;
        proc.running = true;
    }

    Process {
        id: proc
        command: ["hyprctl", "binds", "-j"]
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    root.groups = Binds.group(JSON.parse(text));
                    root.status = "";
                } catch (e) {
                    root.status = "hyprctl binds -j gave nothing readable";
                }
            }
        }
    }

    // A config with no visible window exits, reporting nothing. This 1x1
    // click-through window holds the shell open while the overlay is hidden.
    PanelWindow {
        anchors { top: true; left: true }
        implicitWidth: 1
        implicitHeight: 1
        color: "transparent"
        exclusionMode: ExclusionMode.Ignore
        mask: Region {}
        WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
    }

    LazyLoader {
        active: root.open

        PanelWindow {
            screen: root.screenObj
            anchors { top: true; left: true; right: true; bottom: true }
            color: "transparent"
            exclusionMode: ExclusionMode.Ignore
            WlrLayershell.layer: WlrLayer.Overlay
            WlrLayershell.namespace: "quickshell-keybinds"
            WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive

            // Keys reach a focused item, not a window.
            Item {
                anchors.fill: parent
                focus: true
                Keys.onEscapePressed: root.close()

                Rectangle {
                    anchors.fill: parent
                    color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.82)

                    MouseArea {
                        anchors.fill: parent
                        onClicked: root.close()
                    }

                    Text {
                        anchors.centerIn: parent
                        visible: root.status !== ""
                        text: root.status
                        color: Theme.text
                        font.family: Theme.fontFamily
                        font.pixelSize: 22
                    }

                    // Groups fill a column down to the screen's height, then
                    // start the next one, so a long group never leaves a row
                    // of short ones sitting on empty space. The width is left
                    // to the content, which is what centres the block.
                    // ponytail: a single group taller than the screen clips;
                    // split it in keybindings.lua if one ever gets there.
                    Flow {
                        anchors.centerIn: parent
                        height: parent.height * 0.9
                        flow: Flow.TopToBottom
                        spacing: 28
                        visible: root.status === ""

                        Repeater {
                            model: root.groups

                            Column {
                                required property var modelData
                                width: 440
                                spacing: 4

                                Text {
                                    text: modelData.name
                                    color: Theme.accent
                                    font.family: Theme.fontFamily
                                    font.pixelSize: 18
                                    font.bold: true
                                    bottomPadding: 6
                                }

                                Repeater {
                                    model: modelData.rows

                                    // Fixed widths summing to the column's:
                                    // a Row sizes to its children.
                                    Row {
                                        required property var modelData
                                        spacing: 12

                                        Text {
                                            width: 240
                                            elide: Text.ElideLeft
                                            text: modelData.keys
                                            color: Theme.subtext
                                            font.family: Theme.fontFamily
                                            font.pixelSize: 14
                                        }
                                        Text {
                                            width: 188
                                            elide: Text.ElideRight
                                            text: modelData.label
                                            color: Theme.text
                                            font.family: Theme.fontFamily
                                            font.pixelSize: 14
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}