initial commit
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
# att_menu
|
||||
|
||||
A bottom desktop-environment panel for wlroots-based compositors, built on
|
||||
[Quickshell](https://quickshell.org). Three sections, nothing else:
|
||||
|
||||
| Section | Contents |
|
||||
| ------- | ----------------------------------------------------------------- |
|
||||
| Left | Application menu built from `.desktop` entries, with search |
|
||||
| Middle | Open windows (task list), click to focus, middle-click to close |
|
||||
| Right | Wifi, bluetooth, and a session menu: log out, suspend, restart, shut down |
|
||||
|
||||
No clock, no tray, no status readouts — it is designed to sit alongside a
|
||||
separate status bar.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A wlroots-based compositor supporting `wlr-layer-shell-unstable-v1` and
|
||||
`wlr-foreign-toplevel-management-unstable-v1` (sway, Hyprland, river, niri,
|
||||
labwc, Wayfire, dwl, …). On river 0.4, additionally attwm — the window
|
||||
manager owns focus there, so the task list needs its socket
|
||||
(`attwmctl focus-window`); everything else works on plain river
|
||||
- Nix with flakes enabled
|
||||
- BlueZ (`bluetoothd`) for the bluetooth section — optional; without it, or on a
|
||||
machine with no adapter, the button is simply absent
|
||||
- NetworkManager for the wifi section, likewise optional. A NetworkManager
|
||||
configured with `wifi.backend=iwd` is fine — and is what this was built
|
||||
against — since the connection still goes through iwd underneath
|
||||
|
||||
The window list uses the foreign-toplevel protocol rather than any single
|
||||
compositor's IPC, so it works the same everywhere — except under river 0.4,
|
||||
where it uses attwm's socket instead, because foreign-toplevel cannot focus a
|
||||
window there. See the note at the end.
|
||||
|
||||
## Running
|
||||
|
||||
Try it without installing:
|
||||
|
||||
```sh
|
||||
nix run .
|
||||
```
|
||||
|
||||
Iterate on the config from the working tree, with hot reload on file save:
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
qs --path .
|
||||
```
|
||||
|
||||
Install into a profile:
|
||||
|
||||
```sh
|
||||
nix profile install .
|
||||
att_menu
|
||||
```
|
||||
|
||||
### Autostart
|
||||
|
||||
sway (`~/.config/sway/config`):
|
||||
|
||||
```
|
||||
exec att_menu
|
||||
bindsym $mod+d exec att_menu ipc call launcher toggle
|
||||
```
|
||||
|
||||
Hyprland (`~/.config/hypr/hyprland.conf`):
|
||||
|
||||
```
|
||||
exec-once = att_menu
|
||||
bind = SUPER, D, exec, att_menu ipc call launcher toggle
|
||||
```
|
||||
|
||||
river 0.4 with attwm (`~/.config/river/init`) — attwm owns key bindings, so the
|
||||
launcher binding goes in its `config.zig` as a `spawn` action:
|
||||
|
||||
```sh
|
||||
att_menu &
|
||||
exec attwm
|
||||
```
|
||||
|
||||
Either order works: whichever starts first waits for the other.
|
||||
|
||||
The `launcher`, `wifi`, `bluetooth` and `power` IPC targets each accept
|
||||
`toggle`, `open` and `close`. When opened by keybind the menu appears on the screen holding the
|
||||
focused window. Only one menu is open at a time, so `close` closes whichever it
|
||||
is.
|
||||
|
||||
## Behaviour
|
||||
|
||||
**Placement.** The panel is a layer-shell surface anchored to the left, right and
|
||||
bottom edges, on the `Top` layer. Because it is anchored rather than sized, the
|
||||
compositor resizes it on any output change — resolution, scale, or **rotation** —
|
||||
and it stays on the bottom edge in every orientation. It reserves an exclusive
|
||||
zone so maximised windows do not sit underneath.
|
||||
|
||||
One panel is created per screen and they follow monitor hotplug.
|
||||
|
||||
**Window list.** Left click focuses a window, or minimises it if it is already
|
||||
focused. Middle click closes it. The focused window is underlined, and a window
|
||||
that is not on screen — minimised, or on an unviewed tag under attwm — is dimmed.
|
||||
By default each panel shows only the windows on its own screen (see
|
||||
`windowsPerScreen`).
|
||||
|
||||
Under attwm there is nothing to minimise into: a window is on a tag or it is
|
||||
not. Left click therefore always means focus, and clicking a window parked on
|
||||
another tag brings that tag into view.
|
||||
|
||||
**Launcher.** Type to search across name, generic name, keywords, comment and
|
||||
desktop-entry id, ranked by match quality. Category chips filter the list when
|
||||
the search box is empty. `Up`/`Down` move, `Enter` launches, `Escape` closes, and
|
||||
clicking away closes.
|
||||
|
||||
The launcher is a full-screen overlay layer surface rather than an
|
||||
`xdg-popup`, which is what makes click-away dismissal and keyboard focus work on
|
||||
any wlroots compositor without compositor-specific focus grabs.
|
||||
|
||||
**Wifi.** The button shows signal strength in the arcs and the SSID in use next
|
||||
to them, struck through when the radio is off. The menu lists the networks in
|
||||
range — connected first, then saved, then by strength — with the switch in the
|
||||
header powering the radio. Clicking a network connects it, clicking the
|
||||
connected one disconnects, and joining a secured network that is not already
|
||||
saved opens a passphrase field at the bottom of the menu (`Enter` joins,
|
||||
`Escape` cancels). Right-click a saved network to forget it, which is also the
|
||||
way out if a wrong passphrase gets saved. `Up`/`Down` move, `Enter` activates.
|
||||
|
||||
Scanning only runs while the menu is open, so the panel is not keeping the radio
|
||||
busy in the background. Networks are ordered by signal in quarters rather than by
|
||||
the raw value, so rows do not reshuffle under the cursor as strength drifts.
|
||||
|
||||
Quickshell speaks to NetworkManager, which is the only backend it implements.
|
||||
On a system where NetworkManager is configured with `wifi.backend=iwd`, iwd is
|
||||
still what associates — this just avoids racing NetworkManager's own policy by
|
||||
driving iwd behind its back.
|
||||
|
||||
**Bluetooth.** The button shows adapter state at a glance — struck through when
|
||||
the radio is off, accented while something is connected — followed by the
|
||||
battery level of the first connected device that reports one. The menu lists the
|
||||
devices bluetoothd already knows about (paired or bonded), connected ones first;
|
||||
clicking a row connects it, clicking a connected row disconnects it, and the
|
||||
switch in the header powers the adapter. `Up`/`Down` move, `Enter` toggles the
|
||||
selected device, `Escape` closes.
|
||||
|
||||
It is deliberately not a pairing UI: discovery is never turned on, so nothing
|
||||
transient appears in the list. Pair new devices with `bluetoothctl` or a
|
||||
settings app once, and they show up here afterwards. Battery levels come from
|
||||
BlueZ, so they appear only for devices that report them (most headsets, some
|
||||
mice) and only while connected.
|
||||
|
||||
The whole section hides itself on machines with no bluetooth adapter, or when
|
||||
bluetoothd is not running.
|
||||
|
||||
**Session actions.** One button opens a menu holding log out, suspend, restart
|
||||
and shut down, in whatever order `powerActions` lists them. Actions marked
|
||||
`dangerous` (log out, restart, shut down) ask for confirmation first; suspend
|
||||
does not. `Up`/`Down` move, `Enter` runs, `Escape` closes — and the menu opens
|
||||
with the first *non*-destructive action selected, so `Enter` straight after
|
||||
opening can never shut the machine down.
|
||||
|
||||
Logging out is compositor-specific and is detected at runtime — Hyprland, sway,
|
||||
niri and river are recognised, with `loginctl terminate-session` as the
|
||||
fallback. On river it goes through `attwmctl exit-session` when attwm is
|
||||
answering, since river 0.4 has no `riverctl`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Everything tunable lives in two singletons; edit and restart (or just save, when
|
||||
running with `qs --path .`).
|
||||
|
||||
- **`config/Theme.qml`** — colours, sizes, fonts, animation timings. `barHeight`
|
||||
is here.
|
||||
- **`config/Config.qml`** — behaviour:
|
||||
- `layer`, `namespace`, `reserveSpace` — layer-shell placement. The namespace
|
||||
(`att_menu`) is what compositor rules target, e.g. sway's `layer_effects`.
|
||||
- `windowsPerScreen` — set `false` to mirror every window onto every panel.
|
||||
- `windowButtonMinWidth` / `windowButtonMaxWidth`
|
||||
- `launcherWidth` / `launcherHeight` — clamped to the screen, so a rotated or
|
||||
small output still gets a usable menu.
|
||||
- `terminal` — argv prefix for entries with `Terminal=true`. Honours
|
||||
`$TERMINAL`; otherwise change the `["foot"]` fallback to your terminal.
|
||||
- `showWifi` — set `false` to drop the wifi button entirely.
|
||||
- `wifiLabelMaxWidth` — cap on the SSID shown on the panel.
|
||||
- `wifiMenuWidth` / `wifiMenuHeight` — also clamped to the screen.
|
||||
- `showBluetooth` — set `false` to drop the bluetooth button entirely.
|
||||
- `bluetoothMenuWidth` / `bluetoothMenuHeight` — also clamped to the screen.
|
||||
- `confirmDangerous` — set `false` to skip the confirmation dialog.
|
||||
- `powerMenuWidth` — width of the session menu.
|
||||
- `powerActions` — the rows in the session menu. Reorder, remove, or add entries;
|
||||
each has a `label`, an icon name, a fallback `glyph`, an argv `command`, and
|
||||
a `dangerous` flag.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
shell.qml Root: one Bar per screen, plus the menu IPC handlers
|
||||
config/Theme.qml Colours, sizing, fonts
|
||||
config/Config.qml Behaviour, power actions, compositor detection
|
||||
services/Session.qml Which menu is open, and on which screen
|
||||
services/AppLauncher.qml Desktop-entry launching
|
||||
services/Attwm.qml attwm's IPC socket, when there is one
|
||||
modules/Bar.qml The panel window and its three sections
|
||||
modules/AppMenu.qml Left: launcher button
|
||||
modules/Launcher.qml The application menu overlay
|
||||
modules/AppEntry.qml One row in the menu
|
||||
modules/WindowList.qml Middle: task list
|
||||
modules/WindowButton.qml One task button
|
||||
modules/WifiMenu.qml Right: wifi button
|
||||
modules/WifiPanel.qml The network list overlay
|
||||
modules/WifiNetworkEntry.qml One network row
|
||||
modules/BluetoothMenu.qml Right: bluetooth button
|
||||
modules/BluetoothPanel.qml The device list overlay
|
||||
modules/BluetoothDeviceEntry.qml One device row
|
||||
modules/PowerMenu.qml Right: session button, and running the actions
|
||||
modules/PowerPanel.qml The session menu overlay
|
||||
modules/ConfirmDialog.qml Confirmation for destructive actions
|
||||
components/AppIcon.qml Icon resolution with fallbacks
|
||||
components/PanelButton.qml Shared hover/press surface
|
||||
components/BluetoothGlyph.qml The bluetooth rune, drawn
|
||||
components/WifiGlyph.qml The wifi fan, drawn
|
||||
components/ToggleSwitch.qml On/off switch
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **Why the window list has two sources.** river 0.4 is non-monolithic: it ships
|
||||
no window management policy and hands the job to an external client speaking
|
||||
`river-window-management-v1`, which for this setup is attwm. river still
|
||||
advertises `wlr-foreign-toplevel-management`, so a task list built on it shows
|
||||
the right titles and tracks focus — but that protocol's `activate` request has
|
||||
nowhere to go. Focus is set solely by the window manager through
|
||||
`river_seat_v1.focus_window`, and the window management protocol has no
|
||||
`activate_requested` event to forward the request through, so river drops it
|
||||
and a click does nothing at all. `services/Attwm.qml` therefore takes over
|
||||
under river, sourcing the list from attwm's socket and focusing with
|
||||
`focus-window <id>`. Matching the two protocols up is not needed: river's
|
||||
window identifier is shared between them — which is just as well, since
|
||||
Quickshell's `Toplevel` does not expose it, and app id plus title is not
|
||||
unique (two terminals in the same directory).
|
||||
- Quickshell's `Socket` cannot be reconnected in place. A connect attempt against
|
||||
a socket that does not exist yet fails silently — no state change is reported —
|
||||
and re-asserting `connected` afterwards does nothing. So the socket lives in a
|
||||
`Loader` that gets thrown away and rebuilt on a timer, which is what lets the
|
||||
panel and attwm be started in either order.
|
||||
- Quickshell's `DesktopEntry.execute()` intentionally ignores `Terminal=true`
|
||||
and does not strip `Exec` field codes, so entries would be launched with a
|
||||
literal `%U` argument. `services/AppLauncher.qml` handles both instead.
|
||||
- Icons are resolved through the Qt icon theme, falling back to the last segment
|
||||
of a reverse-DNS app id (`org.qutebrowser.qutebrowser` → `qutebrowser`), then
|
||||
to a lettered tile. The packaged build adds Adwaita and hicolor to
|
||||
`XDG_DATA_DIRS` so the session-action icons resolve on minimal systems.
|
||||
- The bluetooth rune and the wifi fan are drawn with `QtQuick.Shapes` instead of
|
||||
pulled from the icon theme. Themes ship them only as *symbolic*
|
||||
(black-on-transparent) SVGs, which Qt renders as-is — they would be invisible
|
||||
on the dark panel. Drawing them also makes them recolourable, which is how
|
||||
adapter state is shown, and lets the wifi arcs light up one at a time.
|
||||
- The flake pins nixpkgs to the current NixOS stable release, `nixos-26.05`.
|
||||
Reference in New Issue
Block a user