aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/README.md
blob: e443ca06ac3d0a410922ad9015609f4df8927e10 (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
# desktop

One drawer, left of DP-1, hosting the things a desktop lets you adjust.
Reached from a launcher at the left end of waybar.

The drawer opens as a grid of tiles, and clicking a tile either opens that
module's page or fires an action. A module's back arrow, and Escape while a
page is showing, return to the grid; Escape on the grid, or a click outside,
closes the drawer. It takes keyboard focus while it is open, so those keys
reach it.

## Running it

    qs -p ./desktop

    qs -p ./desktop ipc call drawer toggle      # the grid
    qs -p ./desktop ipc call drawer open mail   # straight to a page

`open` always takes a page name, because quickshell 0.3.1 IPC requires every
declared argument to be present and a defaulted parameter registers as a type
the IPC layer rejects. The page-less grid entry is therefore the zero-argument
`toggle`. `ipc call drawer close` closes it.

## The modules

    modules/sound/       output and input volume, the OSD, the player
    modules/mail/        unread per account, threads, the watcher dot
    modules/vm/          libvirt state, live stats, snapshots
    modules/appearance/  a tile that opens the separate appearance shell

Sound, mail and vm each carry their own README. Appearance is a tile onto the
separate appearance shell, whose README covers the picker it opens.

## Writing a module

A module is a `Module` (from `Module.qml` in this directory) declaring what the
drawer needs to show it:

- `name`: identifies the module, both for grid ordering and for IPC, where it
  is the page name `ipc call drawer open <name>` takes.
- `icon` and `label`: the glyph and the text on the tile.
- `tileContent`: a `Component` for the short state line under the label, or
  null for a tile that says nothing beyond its name.
- `page`: a `Component` for the full-height page behind the tile, or null.
- `activate()`: what a tile with no page does when clicked.
- `alwaysActive`: whether the module's background service runs while the
  drawer is closed, covered below.

A module provides a tile, a page, both, or neither. The drawer does not care
which: it reads those properties and does the obvious thing. A page is wrapped
in `Page.qml` for the header and back arrow, and a tile with no page is
fire-and-forget, so its click calls `activate()` and closes the drawer.

`modules/appearance/AppearanceModule.qml` is the smallest complete example: a
name, an icon, a label, and an `activate()` that launches the separate
appearance shell. It has no page and no service. Sound, mail and vm each add a
service and a page on top of that same shape.

Module files live under `desktop/modules/<name>/` and reference root types
(`Module`, `Page`, `Button`, `Theme`), so each needs `import "../.."`. A plain
directory import does not reach the parent. The moved `TransportButton.qml` and
`Stat.qml` gained that import for the same reason.

## alwaysActive

Sound and mail are `alwaysActive: true`; vm is false. The property governs the
module's background service, not its page: pages are lazily loaded either way.

Sound's OSD has to answer a volume keypress with no drawer open, which is the
whole point of it, so its PipeWire service runs for the session. Mail's unread
count outlives the drawer and its config watcher makes a newly added account
appear without a restart, so its service runs too. The vm module's 2s stats
poll exists only to paint a page nobody is looking at, so it starts and stops
with the page.

That last line is not the whole story for vm: its lifecycle event stream runs
unconditionally, which is what keeps the VM list and the tile's dots current
while the page is closed. `alwaysActive` gates the poll, not everything the
module does.

## Geometry

The panel is 600px wide and full height, on `DP-1`. Waybar runs there and the
launcher sits at its left end, while conky holds the right side of the screen,
so the drawer belongs on the left. It falls back to the first screen when
`DP-1` is not connected, so the drawer is never invisible.

The window uses `ExclusionMode.Normal`, not `Ignore`. Waybar claims an
exclusive zone at the top of the screen, so respecting it puts the drawer below
the bar without this code knowing the bar's height, and the bar stays visible
and clickable while the drawer is open. The whole surface is an overlay so a
click outside closes it.

The top of the panel is an empty reserved area for the notification engine. It
is an empty `Item` claiming the space, not a placeholder graphic, so the grid
below it sits where it will sit once notifications arrive. The bottom is a
fixed, never-scrolled `Flow` grid: three columns at 180px minimum, wrapping
and adding rows up to a 3x3 ceiling for the modules that exist.

## Hyprland and waybar

Nothing here is in the repo's control, and the drawer works without it. Two
pieces of outside configuration belong to it.

Frosting is the compositor's, matched on the namespace this window sets. The
drawer sets `quickshell-desktop`, so it needs:

    hl.layer_rule({
        name         = "blur-desktop",
        match        = { namespace = "^(quickshell-desktop)$" },
        blur         = true,
        xray         = false,
        ignore_alpha = 0.1,
    })

The sound module's OSD owns its own window and keeps its old
`quickshell-volume-osd` namespace, so the existing `blur-volume-osd` rule still
matches and stays. Without any of these the surfaces still render, just flat
translucent.

The waybar launcher is a static `custom` module with no `exec`, so it cannot
show whether the drawer is open. Its `on-click` runs the zero-argument toggle:

    "on-click": "qs -p ~/Programming/GIT/quickshell/desktop ipc call drawer toggle"

The mail module's waybar count keeps its own `custom` module, whose click
deep-links to the mail page with `ipc call drawer open mail`. `SUPER+v` reaches
the vm page the same way, with `ipc call drawer open vm`. The full list of
edits to the live Hyprland and waybar configuration is Task 10 of the plan in
`docs/superpowers/plans/2026-09-14-desktop-shell.md`.

## Theme

`Theme.qml` is a symlink to `shared/Theme.qml`, as in every component here.