aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/README.md
blob: 1c7d781a123f610a2e5f06d2ad6aa6348d8a8f8f (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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# desktop

One drawer, left of DP-1, hosting the things a desktop lets you adjust.
Reached over IPC (`ipc call drawer toggle`), from SUPER pressed and released
alone, a Hyprland release bind.
The same process also hosts the island, see `island/README.md`, and the
assistant canvas on SUPER+Home, see `assistant/README.md`.

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 settings window

The drawer is the quick area: live state, and things toggled often. Pages
that are set once and left alone live in a settings window instead, a normal
Hyprland window rather than a layer surface, with those pages listed down its
left. A module joins it with `settings: true`, which also drops its tile. The
gear beside the user header opens it, as do SUPER+Return and IPC:

    qs -p ./desktop ipc call settings open autostart
    qs -p ./desktop ipc call settings toggle

The sidebar's edge drags to resize it, between 160 and 420px; the width is
saved in the preferences file on release.

`open` takes a page name; an unknown or empty one opens the landing page,
a short About. A page's back arrow and Escape return there; Escape on the
landing page closes the window. Ctrl+Q closes it from anywhere.

The search above the list matches each page's label, its module's
`keywords`, and the rows inside it: every git value, every autostart line.
A row hit opens its page scrolled to that row, flashed in the accent. Enter
opens the first result, Escape clears.

Pages load only when opened, so a module keeps its own copy of its rows,
`searchItems` (`[{text, key}]`), and the window calls its `refresh()` on
every open. `key` is the `objectName` of the row in the page; `Page.reveal()`
looks for it, waiting up to 3s for the page's data to arrive, scrolls to it
and flashes it, so a page needs no highlight code of its own.

## The modules

    modules/sound/       output and input volume, the OSD, the player
    modules/network/     wired and wifi, radio, scan, join, forget
    modules/bluetooth/   adapter, scan, pair, connect, forget, trust
    modules/kdeconnect/  devices, battery, pairing, ping, clipboard, share, mount
    modules/mail/        unread per account, threads, the watcher dot
    modules/vm/          libvirt state, live stats, snapshots
    modules/appearance/  wallpaper, theme, icons, sunset, idle: settings pages
    modules/ai/          local AI engines, start/stop for the apps using them
    modules/perf/        CPU, temperatures, GPU, network, disks, ~/.cache
    modules/user/        the greeting row and the user's details page
    modules/clipboard/   cliphist history: search, copy, delete, clear all
    modules/git/         the global git config, in the settings window
    modules/autostart/   Hyprland's autostart.lua, in the settings window

Sound, mail, vm, network, bluetooth, kdeconnect and ai each carry their own README.
So does appearance, for its five settings pages.

## 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.
- `settings`: the page lives in the settings window, not the drawer, and the
  module has no tile.
- `keywords`: for a settings page, the words its search should find besides
  the label.
- `activate()`: what a tile with no page does when clicked.
- `alwaysActive`: declares whether the module's service is meant to run while
  the drawer is closed. The drawer does not read it, the module implements its
  own lifetime. 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.

A tile with no page is the smallest complete example: a name, an icon, and an
`activate()`, with no page and no service. Sound, mail, vm, network, bluetooth and kdeconnect each add a service
and a page on top of that same shape; `modules/appearance/ThemeModule.qml` is
a small settings page.

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

`alwaysActive` is declarative metadata: it documents whether a module's service
is meant to run while the drawer is closed. The drawer does not read or enforce
it. Each module implements its own service lifetime, and the property records
that choice rather than driving it. Sound, mail, network and bluetooth are
`alwaysActive: true`; vm and kdeconnect are false.

Sound's OSD has to answer a volume keypress with no drawer open, which is the
whole point of it, so its PipeWire service is instantiated eagerly and 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 is
instantiated eagerly 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.

vm is not only its poll, however: its lifecycle event stream runs
unconditionally, which is what keeps the VM list and the tile's dots current
while the page is closed. Pages are lazily loaded in every module.

## Geometry

The panel is 600px wide and full height, on `DP-1`. 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 used to claim
an exclusive zone at the top of the screen, and respecting it put the drawer
below the bar without this code knowing the bar's height. Waybar is gone, so
the drawer now starts at the top edge of `DP-1`; `Normal` stays so a bar added
later is respected. The island is an overlay with no exclusive zone, so it
floats over the drawer's top edge. 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
`Flow` grid of three columns inside a `Flickable` capped at three rows: a
fourth row scrolls inside that box rather than pushing into the notification
space, and a thin accent bar in the margin shows only when a row is hidden.
The order is Sound, Network, Bluetooth, KDE Connect, Mail, Status,
Performance, Clipboard, VMs, AI, the everyday ones first.

A page can close the whole drawer by emitting `dismiss` from its `Page`, as
the clipboard does after a copy; `back` returns to the grid.

## Hyprland

Nothing here is in the repo's control, and the drawer works without 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 these the surfaces still render, just flat
translucent.

`SUPER+v` reaches the vm page with `ipc call drawer open vm`, and the drawer
toggles with `ipc call drawer toggle`. The island's sound panel opens the
drawer's sound page through the same `show` the IPC uses.

The drawer is the only place network and Bluetooth are managed. Waybar's own
indicators for both went with waybar.

The drawer is the only KDE Connect surface, so the `kdeconnect-indicator`
autostart line was removed in the same change. The `kdeconnectd` daemon is a
separate system autostart and stays; the phone-to-PC clipboard is the daemon's,
not the indicator's.

## Theme

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