# 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 `. 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`.