-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathshell.qml
More file actions
631 lines (569 loc) · 24.8 KB
/
Copy pathshell.qml
File metadata and controls
631 lines (569 loc) · 24.8 KB
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
//@ pragma UseQApplication
//@ pragma ShellId grootshell
//@ pragma IconTheme Papirus-Dark
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.config
import qs.services
import qs.components
import qs.modules.background
import qs.modules.bar
import qs.modules.border
import qs.modules.island
import qs.modules.launcher
import qs.modules.notifications
import qs.modules.osd
import qs.modules.translate
import qs.modules.network
import qs.modules.clipboard
import qs.modules.switcher
import qs.modules.keybinds
import qs.modules.settings
// grootshell.
//
// Three layer surfaces per screen:
//
// Background bottom layer, the wallpaper. Separate so a fullscreen window
// covers it the way it covers any other window.
// Bar top layer, anchored to the top edge, and the ONLY surface here
// with an exclusive zone. That zone is what stops tiled windows
// opening underneath the bar — see below.
// Overlay everything else — border and panels — in one fullscreen
// transparent surface.
//
// The bar is its own surface purely so it can reserve space. A layer surface
// anchored to all four edges has no meaningful exclusive zone (there is no edge
// to reserve *from*), so while the bar lived inside the overlay nothing told
// Hyprland it was there and windows tiled straight under it. Hyprland's gaps_out
// could have been widened at the top to compensate, but that would hard-code the
// bar's height into the compositor config, in a second place, where it could not
// follow Config.bar.height when it hot-reloads.
//
// The overlay keeps ExclusionMode.Ignore on purpose: the frame occupies the
// screen edges but must not reserve them, because gaps_out already insets tiled
// windows to clear it.
//
// The single-overlay shape for everything else is what lets panels look like
// they grow out of the frame instead of floating above it: they are siblings in
// one scene, sharing a coordinate space and a clip. The price is that a
// fullscreen transparent surface would eat every click on the desktop, which is
// what `mask` is for — only the rectangles listed there accept input, and
// everything else falls through to the window underneath.
ShellRoot {
id: root
// Touches the Theming singleton so it is actually constructed.
//
// QML creates singletons lazily, on first reference. Theming has no
// properties anyone reads — it only watches the wallpaper and shells out —
// so nothing referenced it, so it was never built, so the colours never
// regenerated. A service that exists purely for its side effects has to be
// named somewhere or it does not exist.
readonly property bool themingActive: Theming.running
Variants {
model: Quickshell.screens
Scope {
id: scope
required property ShellScreen modelData
// What the bar occupies: its own height plus the frame band above
// it. Panels in the overlay dock below this rather than to the bar
// itself, which now lives in a different window.
//
// NOT readonly, despite being a pure function of its inputs: a
// Behavior animates writes, and a read-only property is never
// written to — it is re-evaluated. Declaring this readonly makes the
// Behavior below a load-time error rather than a no-op, which takes
// the whole shell down.
property int barZone: GameMode.enabled ? 0 : Config.bar.height + Hypr.frameThickness
// How tall the bar's SURFACE is, as opposed to how much it reserves.
//
// The two are no longer the same. A tiled window does not begin at
// the bottom of the reserved strip — the compositor's gaps_out and
// its window border push it down another Hypr.windowInset — so the
// band of wallpaper a pill actually floats in is that much taller
// than the strip. Centring pills on the strip alone put every one of
// them high by half of it: about 7px of air above and 21 below.
//
// Making the surface cover the whole band, and reserving only part
// of it, is what lets the pills sit where they look right without a
// magic offset — and without being clipped by a surface that ends
// before they do.
readonly property int barSurface: GameMode.enabled ? 0 : scope.barZone + Hypr.windowInset
// Where anything hanging from the top of the screen starts: level
// with the top of a tiled window, so a side panel and the windows
// beside it share a line.
readonly property int topDock: scope.barSurface
Behavior on barZone {
enabled: Appearance.anim.enabled
NumberAnimation {
duration: Appearance.anim.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.emphasised
}
}
Background {
screen: scope.modelData
}
PanelWindow {
id: barWindow
screen: scope.modelData
color: "transparent"
anchors {
top: true
left: true
right: true
}
implicitHeight: scope.barSurface
visible: scope.barSurface > 0
// The surface is taller than the reservation, so the zone has to
// be stated rather than derived. Auto takes it from the anchored
// size, which was right while the two were the same number and
// would now reserve the pills' breathing room as well — pushing
// every window down by a gap that already exists.
exclusionMode: ExclusionMode.Normal
exclusiveZone: scope.barZone
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "grootshell-bar"
// The bar never types. Taking focus here would steal it from the
// window you are working in every time the pointer crossed it.
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
Bar {
anchors.fill: parent
screen: scope.modelData
}
}
// Space reserved for the side panels, so tiled windows move aside
// rather than being covered.
//
// One surface per panel rather than one per edge: each carries its
// own zone and the compositor sums them, so two right-edge panels
// open at once reserve the sum without any arithmetic here.
//
// Side panels only. The island drops from the bar in the middle of
// the screen, and a zone is a full-edge strip — reserving for it
// would push every window down to clear something that only covers
// the middle third.
// Breathing room between the frame and the windows inside it.
//
// The compositor leaves gaps_out around a tiled window and the frame
// is exactly gaps_out thick, so the two meet flush and the frame's
// rounded inner corners clip the window's corners. These reserve a
// little more, on the three edges that have no bar to do it for them,
// and Hyprland lays the windows out that much further in.
//
// Reserved rather than drawn: the frame stays the same size and in
// the same place, and it is the WINDOWS that move. Making the frame
// thicker instead would eat the same space from the other side and
// leave them just as flush.
// Written out three times rather than looped: a Repeater needs Item
// delegates and these are windows, so it silently creates nothing.
EdgeReservation {
reservationScreen: scope.modelData
edge: "left"
depth: Hypr.windowPadding
// Game mode collapses the frame, so there is nothing left to
// hold the windows away from.
reserve: !GameMode.enabled && Hypr.windowPadding > 0
}
EdgeReservation {
reservationScreen: scope.modelData
edge: "right"
depth: Hypr.windowPadding
reserve: !GameMode.enabled && Hypr.windowPadding > 0
}
EdgeReservation {
reservationScreen: scope.modelData
edge: "bottom"
depth: Hypr.windowPadding
reserve: !GameMode.enabled && Hypr.windowPadding > 0
}
EdgeReservation {
reservationScreen: scope.modelData
edge: "right"
depth: notificationCentre.depth
reserve: notificationCentre.open
}
EdgeReservation {
reservationScreen: scope.modelData
edge: "left"
depth: translatePanel.depth
reserve: translatePanel.open
}
EdgeReservation {
reservationScreen: scope.modelData
edge: "right"
depth: osd.depth
// wantsInput, not showing: the readout also flashes on every
// volume change, and reflowing the desktop for that would be
// absurd. This reserves only while the pointer is on it.
reserve: osd.wantsInput
}
PanelWindow {
id: overlay
screen: scope.modelData
color: "transparent"
anchors {
top: true
bottom: true
left: true
right: true
}
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "grootshell"
// Exclusive only while something that types is open. OnDemand
// would let a panel steal focus on hover, and None would make the
// launcher unable to read a keystroke.
WlrLayershell.keyboardFocus: ShellState.anyOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
// Escape closes whatever is open, from anywhere. The one binding
// that must always work, because a panel that has taken
// exclusive keyboard focus and cannot be dismissed is a hung
// desktop on a host with no local console.
Item {
anchors.fill: parent
focus: ShellState.anyOpen
Keys.onEscapePressed: ShellState.closeAll()
Border {
anchors.fill: parent
}
// Click-outside-to-close. Declared before the panels so it
// sits underneath them — a click that lands on a panel hits
// the panel, and anything else hits this.
//
// It has to be a real item in the mask, not just an absence:
// the mask is what decides whether the compositor gives this
// surface the click at all, so without a region covering the
// screen an outside click goes straight through to whatever
// window is behind and the panel stays open.
MouseArea {
id: scrim
anchors.fill: parent
visible: ShellState.anyOpen
// No visual. A dimming layer would be the obvious thing
// and is wrong here: every frame it darkens is a frame
// re-encoded for the stream, for an effect nobody asked
// for.
onClicked: ShellState.closeAll()
}
// Docked into the frame rather than floating over it. Each
// of these anchors to the screen edge it belongs to, inset
// past the bar's reserved zone at the top.
// Always on screen, because at rest it IS the clock pill —
// it only becomes the dashboard when opened. It owns its own
// top margin, which travels from the pill's resting line up
// to the screen edge as it grows.
Island {
id: island
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: island.topOffset
}
Launcher {
id: launcher
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
}
// Vertically centred: it is half height now, and pinning it
// under the bar would leave it hanging off the top with a
// gulf beneath.
TranslatePanel {
id: translatePanel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
// Top only. A DockedPanel sizes itself from span and depth,
// so anchoring the bottom as well would fight it.
NotificationCentre {
id: notificationCentre
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: scope.topDock
}
// Flush against the bar, deliberately. Both it and a
// DockedPanel paint Theme.frame, so the two merge into one
// surface — which is the caelestia look these are modelled
// on, notifications hanging off the top border rather than
// floating near it.
Toasts {
id: toasts
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: scope.topDock
}
Osd {
id: osd
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
WifiPopout {
id: wifiPopout
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: scope.topDock
}
EthernetPopout {
id: ethernetPopout
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: scope.topDock
}
ClipboardPanel {
id: clipboardPanel
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
}
Switcher {
id: switcher
anchors.centerIn: parent
}
DesktopSwitcher {
id: desktopSwitcher
anchors.centerIn: parent
}
KeybindsModal {
id: keybindsModal
anchors.centerIn: parent
}
SettingsPanel {
id: settingsPanel
anchors.centerIn: parent
}
}
// Everything that should accept a click. A panel that is closed
// contributes nothing, so the desktop underneath stays fully
// usable — which is the difference between a shell and a
// fullscreen window that happens to be mostly transparent.
//
// The bar is absent because it is no longer in this window; its
// own surface takes input across its whole area.
mask: Region {
Region {
item: scrim.visible ? scrim : null
}
Region {
item: island.visible ? island : null
}
Region {
item: launcher.visible ? launcher : null
}
Region {
item: translatePanel.visible ? translatePanel : null
}
Region {
item: notificationCentre.visible ? notificationCentre : null
}
Region {
item: toasts.visible ? toasts : null
}
Region {
item: wifiPopout.visible ? wifiPopout : null
}
Region {
item: ethernetPopout.visible ? ethernetPopout : null
}
Region {
item: clipboardPanel.visible ? clipboardPanel : null
}
Region {
item: switcher.visible ? switcher : null
}
Region {
item: desktopSwitcher.visible ? desktopSwitcher : null
}
Region {
item: keybindsModal.visible ? keybindsModal : null
}
Region {
item: settingsPanel.visible ? settingsPanel : null
}
// The OSD contributes two regions rather than none.
//
// Its trigger strip is always live: a border-width sliver
// beside the volume readout, which is what makes hovering
// the right edge open it. Hover only — that sliver lies on
// a window's resize edge, and taking a button there would
// quietly break dragging it.
//
// The body joins the mask only once something has opened it
// deliberately, so the readout that flashes up from a volume
// key still swallows nothing.
Region {
// Gated on the OSD root, which game mode hides — the
// stripped desktop should not be quietly holding a strip
// of the right edge away from a fullscreen game.
item: osd.visible ? osd.trigger : null
}
Region {
item: osd.wantsInput ? osd : null
}
}
}
}
}
// --- IPC ----------------------------------------------------------------
//
// The surface the Hyprland keybinds call, via `grootshell-ipc call <target>
// <fn>`. Names here and in the nixos repo's keybinds.nix have to agree; a
// typo on either side is a key that silently does nothing.
IpcHandler {
target: "launcher"
function toggle(): void {
ShellState.toggle("launcher");
}
function open(): void {
ShellState.open("launcher");
}
function close(): void {
ShellState.close("launcher");
}
}
IpcHandler {
target: "translate"
function toggle(): void {
ShellState.toggle("translate");
}
}
IpcHandler {
target: "island"
function toggle(): void {
ShellState.toggle("island");
}
// `grootshell-ipc call island tab media` — jump straight to a tab
// rather than opening and then clicking.
//
// Named `tab` and not `show`, which is what it was: `show` is also a
// subcommand of `qs ipc`, and the CLI matches the subcommand first. The
// function was unreachable — `call island show media` answered by
// printing the target listing, and `qs` was the one rejecting the
// argument, so nothing in the shell's log said a word about it.
function tab(tab: string): void {
ShellState.islandTab = tab;
ShellState.open("island");
}
}
IpcHandler {
target: "notifications"
function toggle(): void {
ShellState.toggle("notifications");
}
function clear(): void {
Notifs.clear();
}
function dnd(on: string): void {
Notifs.doNotDisturb = on === "true" || on === "1";
}
}
IpcHandler {
target: "clipboard"
function toggle(): void {
if (!ShellState.clipboard)
Clipboard.refresh();
ShellState.toggle("clipboard");
}
}
// The picker is a tab in the island now, so `toggle` opens that rather than
// a panel of its own. The target name stays — it is what the SUPER+P bind
// and anything scripted against the shell already call, and renaming it
// would break those for no gain.
IpcHandler {
target: "wallpaper"
function toggle(): void {
if (ShellState.island && ShellState.islandTab === "wallpaper") {
ShellState.close("island");
return;
}
ShellState.islandTab = "wallpaper";
ShellState.open("island");
}
function next(): void {
Wallpapers.next();
}
function set(path: string): void {
Wallpapers.set(path);
}
}
// Two targets, because there are two panels. `network` is gone rather than
// kept as an alias for one of them: it would have to pick, and a bind that
// opens wifi when it says network is worse than one that fails loudly.
IpcHandler {
target: "wifi"
function toggle(): void {
if (!ShellState.wifi)
Net.scan();
ShellState.toggle("wifi");
}
}
IpcHandler {
target: "ethernet"
function toggle(): void {
ShellState.toggle("ethernet");
}
}
IpcHandler {
target: "switcher"
function toggle(): void {
ShellState.toggle("switcher");
}
}
// The desktop switcher. `next` is what SUPER+Tab calls: it opens the panel
// if it is closed and advances the selection either way, so one bind does
// both jobs and holding SUPER while tapping Tab walks along the desktops.
//
// Advancing lives here rather than in the panel's own key handler because
// Hyprland processes its keybinds BEFORE forwarding to clients — SUPER+Tab
// fires even while the panel holds exclusive keyboard focus, so a Tab
// handler in the panel would move the selection a second time on every
// press.
IpcHandler {
target: "desktops"
function toggle(): void {
ShellState.toggle("desktops");
}
function next(): void {
if (!ShellState.desktops)
ShellState.open("desktops");
ShellState.desktopStep(1);
}
function previous(): void {
if (!ShellState.desktops)
ShellState.open("desktops");
ShellState.desktopStep(-1);
}
}
IpcHandler {
target: "keybinds"
function toggle(): void {
ShellState.toggle("keybinds");
}
}
IpcHandler {
target: "settings"
function toggle(): void {
ShellState.toggle("settings");
}
// `grootshell-ipc call settings open Weather` — straight to a group,
// the same jump the weather tab's "Open settings" button makes. Titles
// are as they appear in the rail; an unknown one just opens the panel.
function open(group: string): void {
ShellState.openSettings(group);
}
}
IpcHandler {
target: "theme"
function regenerate(): void {
Theming.regenerate();
}
}
IpcHandler {
target: "screenshot"
function region(): void {
ShellState.closeAll();
Screenshot.region();
}
function screen(): void {
ShellState.closeAll();
Screenshot.screen();
}
}
}