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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
// 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.Wayland
import QtQuick
Scope {
id: root
property bool open: false
property string monitor: "DP-1"
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; }
// Hyprland.toplevels reads empty until it is asked, and a window opened
// since the last showing would otherwise be missing from the grid.
onOpenChanged: if (open) Windows.refresh();
// A config with no visible window exits, reporting nothing. This 1x1
// click-through window is what holds the shell open while the overlay
// is hidden, which is almost always.
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 {
id: win
screen: root.screenObj
anchors { top: true; left: true; right: true; bottom: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "window-switcher"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
// Keys reach a focused item, not a window: Keys.onEscapePressed on
// the PanelWindow above never fires. This item is what listens.
Item {
id: keyHandler
anchors.fill: parent
focus: true
// The LazyLoader destroys the overlay when it closes and
// builds a new one on the next opening, verified by a build
// counter: two openings, two constructions. So this starts at
// 0 every time on its own, and there is nothing to reset. A
// Component.onCompleted assigning 0 here would be a no-op.
property int selectedIndex: 0
function commit() {
const list = Windows.windows;
if (selectedIndex >= 0 && selectedIndex < list.length) {
Windows.focus(list[selectedIndex]);
root.close();
}
}
function move(delta) {
const n = Windows.windows.length;
if (n === 0) return;
selectedIndex = (selectedIndex + delta + n) % n;
}
Keys.onEscapePressed: root.close()
Keys.onReturnPressed: commit()
Keys.onEnterPressed: commit()
Keys.onLeftPressed: move(-1)
Keys.onRightPressed: move(1)
Keys.onUpPressed: move(-grid.columns)
Keys.onDownPressed: move(grid.columns)
Keys.onTabPressed: move(1)
Keys.onBacktabPressed: move(-1)
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()
}
// Nothing to switch to. A full-screen dim with nothing in
// it reads as a hang, so it says so instead.
Column {
anchors.centerIn: parent
spacing: 14
visible: Windows.windows.length === 0
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Nothing here but hopes and dreams..."
color: Theme.text
font.family: Theme.fontFamily
font.pixelSize: 26
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "go create something beautiful!"
color: Theme.overlay
font.family: Theme.fontFamily
font.pixelSize: 32
}
}
Grid {
id: grid
anchors.centerIn: parent
visible: Windows.windows.length > 0
readonly property int count: Windows.windows.length
readonly property int gap: 30
readonly property int maxCardWidth: 640
// Columns are the window count capped at six, so one
// card sits dead centre and two straddle the middle.
columns: Math.max(1, Math.min(count, 6))
// Cards grow to fill their row, up to a ceiling.
// Without the ceiling a lone preview becomes a
// full-screen mirror of the window it stands for.
readonly property int rows: Math.ceil(count / columns)
// The three text lines and the spacing above them,
// which the box height has to leave room for.
readonly property int textHeight: 54
// Clamped by the height as well as the width. Without
// the second term a busy desktop pushes rows off the
// top and bottom, and a centred Grid has no way to
// scroll to them, so the cards shrink instead.
readonly property int cardWidth: Math.min(
maxCardWidth,
Math.floor((parent.width * 0.92 - (columns - 1) * gap) / columns),
Math.floor(((parent.height * 0.92 - (rows - 1) * gap) / rows - textHeight) * 16 / 10))
spacing: gap
Repeater {
model: Windows.windows
WindowCard {
required property int index
required property var modelData
toplevel: modelData
cardWidth: grid.cardWidth
selected: index === keyHandler.selectedIndex
onActivated: {
keyHandler.selectedIndex = index;
keyHandler.commit();
}
// Closing is a side errand, so the overlay
// stays open: a dismissal would mean reopening
// to close a second window. The card goes when
// Hyprland says it went.
onCloseRequested: Windows.closeWindow(modelData)
}
}
}
}
}
}
}
}
|