aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules
AgeCommit message (Collapse)AuthorFilesLines
7 daysfeat(user): keyboard for the location suggestionsDanilo M.1-6/+29
Up and Down move through the list, wrapping, and light the row as hover does; Enter takes it; Escape closes the list. With no list open the keys pass on, so Escape still leaves the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(user): location suggestions from OpenStreetMapDanilo M.3-4/+110
There is no place database on this machine, and the user chose Nominatim over a tz city list for coverage. It is asked 1s after typing stops (its policy: one request a second, an identifying User-Agent, nothing personal sent), from three characters, in the desktop's language, and a reply to an older query is dropped. places() turns its JSON into 'place, region, country' labels and treats an error page as no suggestions; test_user.js covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(user): a Git section with the values git will carryDanilo M.1-16/+35
The one-line preview became a section: the git name and email as read-only fields that follow the Name and Email edits as they are typed, and the signing key, which is git's too, moved under them. A derived field is computed from the draft and never saved on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 daysfeat(shared): one faded glow for hovers across the shellsDanilo M.1-17/+37
Hover and attention effects were solid: a flat accent ring round the user page's avatar, a flat quarter-disc in the hot corner, and thick accent or white borders on window cards, keycaps and knobs. Glow.qml is a RectangularShadow in the accent, fading out from the shape's edge and in over 140ms, linked into desktop, window-switcher and keybinds like Theme.qml, and all five places use it. Selection borders in the appearance tabs stay: they mark state, not hover. The avatar also gains the hover cue in the first place: the hand cursor never showed under a hyprcursor-only theme, so without it the picture gave no sign it could be clicked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysstyle(user): a larger avatar and dropdowns in the themeDanilo M.1-5/+22
The avatar had room to spare at 96px; 160 fills the page better. The two ComboBoxes rendered in the default style's light colours on a dark page; a palette takes the theme into the control and its popup, and the background matches the text fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysstyle(user): the greeting is a header, not a cardDanilo M.1-3/+3
With a surface and a border it read as one more notification in the list under it. Only a faint hover tint remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfix(user): the avatar is ~/.user-icon.jpg, as hyprlock and SDDM useDanilo M.3-7/+32
The page showed AccountsService's icon, an older picture than the one the rest of the desktop uses: hyprlock, the udt SDDM theme (pointed at it by udt-accent) and the rofi power menu all read ~/.user-icon.jpg. get now prefers that file, and set converts the pick to JPEG there, through a temp file and a rename, before handing it to AccountsService so both agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfix(user): the page retries partial saves and survives leaving mid-saveDanilo M.3-7/+15
A key whose write half-failed (AccountsService took it, git did not) reloads as unchanged and Save went dead. A key with an error now counts as changed, and editing a field clears its error. Processes were parented to the page, so leaving it mid-save killed userctl between stores. They hang off the module now, passed in as owner (not the module's id, which would bind to itself). busy now clears when the reload lands, so Save cannot be clicked again before saved is fresh, and the fields and avatar picker are disabled while busy. Avatar paths with # or % broke the preview under a bare file:// prefix; each segment is now URI-encoded for the Image source (page and row), while draft.avatar stays a plain path for userctl. Dropped a redundant saved clear in load. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysdocs(user): the module's README and the busctl escaping noteDanilo M.1-0/+30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(user): a drawer page for the user's detailsDanilo M.2-0/+285
Name, email, avatar, language, location and the git signing key, each saved through userctl to every store that holds it. Only changed fields are sent; a field whose write failed keeps its edit and its error, so the next Save retries it. A failed userctl get reports a single error= line. The page shows it in red under the avatar, leaves saved and draft untouched, and disables Save until a later load succeeds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(desktop): a greeting row above the notificationsDanilo M.1-0/+102
Avatar, Hello <login> and the real name, re-read whenever the grid view shows again so an edit is visible on the way back from the page. Module gains tiled, so a module can own a page without a tile. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(user): helpers for the user pageDanilo M.2-0/+93
Parsing userctl's output and locale -a, and the git name preview, kept out of QML so node can test them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfix(user): userctl survives leading dashes and reports failed readsDanilo M.2-15/+37
busctl and kwriteconfig6 parse a value starting with '-' as an option, so a location like '-x' failed; '--' before the value ends option parsing (checked against both binaries). get ran without pipefail, so a failing busctl gave empty fields and exit 0, which the page would show as a blank form. It now reads all fields first and, on failure, prints one error= line and exits 1. keys listed revoked, expired or invalid primaries, keys that cannot sign, and could label a key with a revoked uid. It now skips those. Unsetting the signing key uses --unset-all, since git exits 5 on a multi-valued key with --unset. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(user): userctl reads and writes the user's detailsDanilo M.2-0/+258
One writer for the stores other programs read: AccountsService (and with it the passwd GECOS LibreOffice prefills from), KDE's emaildefaults and the global git config, where the surname goes in as an initial. busctl is asked for JSON because its plain output escapes non-ASCII as octal. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(perf): the performance page moves to the drawer, with ~/.cacheDanilo M.6-62/+161
The island's performance icon and panel are gone: the drawer page covers them. With only one host left, the panel drops its three-column grid for one card per row at a larger size, since the page scrolls; temperatures get a card of their own as tiles. The files move from island/perf to modules/perf. A cache card ports conky's: ~/.cache and its five largest children, measured with du each time the page opens (28ms over 2 GiB here) rather than read from conky's 15-minute sampler file. The largest is yellow, red past half the total, as conky had it. The warning dot now shows only on the drawer tile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysdocs(spec): user details in the drawerDanilo M.4-0/+979
A drawer row greets the login user and opens a page that edits name, email, avatar, language and location, written where other programs already read them: AccountsService (and through it the passwd GECOS LibreOffice reads), KDE's emaildefaults and the global git config, with the surname shortened to an initial for git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(sound): per-app volume on the drawer page, output to 150%Danilo M.3-84/+157
The island spec put per-app volume on the drawer page, and it was never built. Each application playing audio now gets a row with its name, what it is playing (two browser tabs are otherwise identical rows), a click-to-mute level and its own slider. Streams are matched on PwNodeType.AudioOutStream. Quickshell reports isSink true for a playback stream, so the obvious isStream && !isSink filter found nothing while Firefox played. Their properties are empty until bound, so the page carries a PwObjectTracker for them, which stops with the drawer. speech-dispatcher-dummy is always present and never audible, and is hidden. The output slider runs to 150%, the ceiling the volume keybind already uses (wpctl -l 1.5), with a tick at 100%. Input and apps stop at 100%: a boosted microphone only clips. The meter became an inline component shared by both, and wheel-up never lowers a level already past its maximum. Now playing is dropped from the page: the OSD already carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(updates): a page that reads at a glanceDanilo M.1-30/+106
The first cut was small type in one long list: 471 Slackware upgrades pushed everything below them out of sight, and the repo tag, the versions and the name ran together across the row. Type is 2px larger and rows taller. A section over ten rows starts folded behind a clickable heading with its count. The version being moved to is bright, the one being left dim, both in Inconsolata so digits line up. Every other row is shaded to carry the eye across, and the repo is a pill in one colour per repo, never red, green, yellow or the accent, which mean something elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysdocs(updates): the module's READMEDanilo M.1-0/+32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(updates): the drawer tile and pageDanilo M.2-0/+228
The tile counts what can be installed now (Slackware and the binary repo) and what only needs a build, and goes accent when there is something to install. It runs updatesctl while the drawer is open and not otherwise; a result over 30 minutes old is followed by a refresh that runs nvchecker, never after a refresh, so a failing nvchecker cannot loop. The page has one section per next action: Slackware upgrades, new and removed first, then upgrade here, awaiting build, upstream newer, app updates, the order the work is done in. Each row names the package, the repo it comes from and the one version jump its action covers. A source that could not be read says so under the section it empties instead of reading as nothing pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfix(updates): review findings in updatesctlDanilo M.2-12/+27
A git hash starting with a letter (3.1-a59215d) ordered behind its base, because any trailing alphabetic chunk counted as a pre-release; only alpha, beta, rc, pre and dev do now. Trailing zeros (1.14 against 1.14.0) compare equal instead of reading as an upstream bump. A bad line in the slackpkg blacklist is skipped rather than crashing the script and leaving the drawer with nothing. An AppImage keeps its key over a flatpak of the same name, as the spec orders them, and flatpak list has a timeout. A Slackware upgrade compares version and build, so an arch change alone is not a row. The spec's prose now matches the code: the upstream comparison includes the installed version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfix(updates): drop a redundant clause, test the mirror blacklistDanilo M.2-2/+3
The awaiting-build branch also required the build to be older than upstream, which cannot fail there: a build newer than installed was already taken as an upgrade, so the build is at most the installed version, which is behind upstream whenever a row is emitted. Removing it left every test passing, which is how it was found. The blacklisted testing/ entry sat after the main one in the fixture, so setdefault kept the main one and the mirror-side blacklist was never exercised; it now comes first, and dropping the check fails the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daystest(updates): a hung NFS read reports unreachableDanilo M.1-0/+12
A fifo with no writer blocks cat at open() the way a hard NFS mount blocks during a NAS outage. The repo section reports unreachable within the timeout and the mirror sections still fill. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(updates): Slackware upgrades, new and removed packagesDanilo M.2-1/+92
Compared mirror against installed, as upgrade-all does, not against slackpkg's local ChangeLog: `slackpkg update` replaces that without installing anything, so a diff against it reads clean after an update that was never applied. New and removed come from the mirror's ChangeLog, the newest Added or Removed line deciding, so a package added and later upgraded still counts as new. Blacklisted build tags and directories are skipped, as slackpkg skips them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(updates): sort tracked packages into the next actionDanilo M.2-1/+351
Each nvchecker stanza lands in one section: upgrade here when the binary repo's PACKAGES.TXT holds a newer build than installed, awaiting build when the recipe (the user's or SBo's) has caught up but the build has not, upstream newer when the recipe lags, app updates for AppImages and flatpaks. Built comes from PACKAGES.TXT rather than the .info, which is why gamescope and kitty no longer read as installable: the repo still held the old build. The repo is on a hard NFS mount, so it is read by a child the timeout can kill. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(updates): version order for updatesctlDanilo M.2-0/+117
nvtable compares versions for equality, which cannot tell an installed 2.1.278 from an SBo recipe of 2.1.267 that is older than it. vercmp orders numeric chunks as numbers and treats a trailing alphabetic chunk as a pre-release, so a kernel suffix or git hash orders as newer than its base and never reads as behind. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8 daysfeat(desktop): performance and clipboard pages, a scrolling tile gridDanilo M.6-0/+413
The tile grid sits in a Flickable capped at three rows, so the fourth row the new modules bring scrolls inside that box instead of eating the notification space; a thin accent bar in the margin shows only when a row is hidden. Tiles are reordered with the everyday ones first. Performance shows the island's PerfPanel in a Page, with a tile line of CPU and RAM and the warning dot. Perf.detailed is now a count of live PerfPanel instances instead of a binding in the status pill: the island and the drawer both host the panel, and a bool with two writers would let one clear it under the other. Clipboard reads cliphist when its page opens: search, copy on click or Enter, delete per row, clear all behind a 3 second confirm, image entries as thumbnails decoded once onto tmpfs. Entries reach sh as $1, never inside a command string. A copy closes the drawer through a new Page.dismiss signal the drawer connects like back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(status): quiet mode mutes the Claude and opencode soundsDanilo M.6-9/+156
A new registry mode, status.quiet, with a row on the status page. Both tools will play their sounds through notify-sound, which asks statusctl and plays nothing while quiet is on, so their popups stay and only the sound goes. Presentation sets quiet and restores it afterwards, like dnd and nolock: a sound during a call or a game is what quiet is for. notify-sound ignores any argument it has no sound for, because the opencode notifier runs its command on every event, session_started and user_message included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07docs: the island, and waybar goneDanilo M.2-53/+37
AGENTS.md describes the hybrid model (desktop/ hosts drawer and island) and gains the gotchas found while building the island: UseQApplication for tray menus, WheelHandler vs MouseArea, accent colliding with green, xkb layout switching, and the tray popup un-hovering the island. The Blur waybar measurement becomes history. READMEs drop their present-tense waybar references. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07chore: remove waybarDanilo M.2-71/+3
The island replaces both bars: clock and VM dots in the clock pill, volume, tray, layout flag and privacy in the status pill and its sliver, mail count in the drawer. The icon switch no longer regenerates or reloads waybar, and the mail pill's watcher script goes with it. Workspaces and the taskbar have no persistent view by decision: the flick and the expose cover them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07fix(mail): recount unread when the notmuch database commitsDanilo M.2-0/+28
Accounts counted only at shell start and when the mail page opened, so the drawer tile drifted behind the inbox for the rest of the session: it read 14 while notmuch and the waybar pill both said 16. The waybar pill never drifted because waybar-mail.sh watches the database; the drawer had no watcher. Accounts now runs its own inotifywait -m on the xapian directory (the directory, because Xapian replaces files on commit) and refreshes 300ms after a commit burst settles. The watcher respawns after 5s if it exits, since a dead one reproduces the drift with no visible error. This is the prerequisite for removing waybar: the drawer becomes the only place the unread count is shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05docs(ai): module README, drawer module listDanilo M.1-0/+51
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05feat(ai): drawer page for the local AI stackDanilo M.4-0/+334
Engines are read-only, since stopping llama-server or sd-server takes its web UI down too. Apps start and stop through their own controls, detached so a start outlives a quickshell reload. Fanfictioner has no start (a run needs a story) and stops by pid, children first, so its sd-cli runs go with it and nothing else does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05fix(ai): count a running sd-cli as the sd engineDanilo M.2-7/+32
fanfictioner runs sd-cli one-shot, not through sd-server, so the sd row read down while a CLI run held VRAM and generated. A running sd-cli now reads up and busy, its model marked (sd-cli), and outranks the web UI. The model is the first --diffusion-model/-m: sd-cli's prompt follows and is free text that may contain "-m". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05feat(ai): state sweep for the AI stackDanilo M.2-0/+242
Six TSV lines per sweep: llama and sd engines, four apps. llama's slot probe passes autoload=false, since a plain /slots?model= loads the model into VRAM. sd-server has no progress endpoint, so busy is its log having been written in the last 5s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03fix(status): watch for create, so a mode's first write is seenDanilo M.2-2/+25
Qt's atomic write builds the file as an unnamed O_TMPFILE. When the target does not exist yet it links that straight into place, and the only event naming the file is IN_CREATE: no close_write, no moved_to. statusctl watch did not listen for create, so the first write of each mode in a session went unseen. On a fresh session that is the write that matters: a Teams call started an hour in left the waybar pill reading off until some later rename happened to redraw it. The test reproduces the event with a hard link, which is what the linkat from Qt's temp file looks like to inotify. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03docs(status): ~/bin/statusctl is a symlink to the repo copyDanilo M.1-1/+1
It was a hand-installed copy, which can drift from the tested source silently. Replaced with a symlink after confirming the two were identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03feat(status): present during a Teams callDanilo M.1-1/+1
The auto-detector now sets presentation while Teams holds a mic stream, alongside the game checks. Teams runs for the whole session, so its process alone is no signal and would have kept lock, DND and the breaktimer off all day. A call is Teams holding a source-output, which pactl lists tagged with application.process.binary = "teams-for-linux"; verified matching during a live call. It reuses status.gaming rather than status.presentation, so a call ending clears only the automatic half and never a manual toggle. The statusctl tooltip for that state said "game running", which is now wrong, so it reads "auto: game or call". The file and waybar class keep the gaming name: renaming them touches four places for no behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29fix(sound): swap background cover when the track changesDanilo M.1-4/+9
The crossfade only brought a loaded cover to the front when the image's source compared equal to the wanted URL. It never did for a title with spaces or brackets: the URL carries the title as a percent-encoded fragment cache buster, and QUrl reads it back decoded, so "#Foo%20Bar" came back as "#Foo Bar". The load finished, loaded() bailed, and the old cover stayed. Confirmed with a standalone qml test. The slot being loaded is now tracked by index instead. Stale status changes cannot match: only the back image is ever reloaded, and a newer want resets it before assigning. Seen working on screen by the user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29feat(sound): crossfade album art, fade to plain without itDanilo M.2-59/+113
The OSD background held one Image, so a track change swapped covers in a single frame. Two images now take turns: the new cover loads into the hidden one and is brought to the front only once it reports Ready, then both fade over 300ms. Waiting for Ready is what keeps a slow or remote art server from fading through black. The blur, scrim, sheen and grain are grouped under one item whose opacity follows artOn. It goes false when the player has no art or the cover for the current track errors, so the panel fades back to its plain translucent look instead of snapping, or keeping a stale cover that reads as a fault. It goes true again only when a new cover has loaded, so returning from plain never flashes the previous one. The back image's source is cleared before being set, so a URL it already holds still reloads and reports a status. onWantChanged misses the value at load, hence the Component.onCompleted call. Not yet seen on screen: the shell reloaded it cleanly but the fade was not watched through a track change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29feat(sound): fake frost on the album-art scrimDanilo M.1-0/+40
The art background is opaque, so Hyprland's blur behind the OSD cannot show through it; making the art translucent to let it through would also thin the scrim that keeps light covers legible. Fake the frosted glass instead: a faint top-down sheen in Theme.text and a fixed grain, painted once by a Canvas and masked to the panel's rounded corners. The grain is a Canvas rather than a ShaderEffect because a shader needs a compiled .qsb beside it; the Canvas repaints only on resize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29feat(sound): album art as the OSD backgroundDanilo M.2-0/+56
When the current track has art, the volume OSD now draws it across the whole panel, blurred (MultiEffect blur 0.5, default blurMax) and masked to the panel's rounded corners, since clip only cuts to the bounding box. The thumbnail in the track row stays sharp on top. A dark scrim (Theme.base at 0.82) sits over the art. The palette is dark and the text light, so a light cover made the panel unreadable, and more blur did not fix that; only darkening did. The scrim is a sibling of the MultiEffect, not a child: nested inside it, it did not paint at all, which is why raising its alpha first appeared to change nothing. Without art the panel keeps its translucent, compositor-frosted look. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25fix(sound): collapse the OSD track row when the player goes awayDanilo M.1-0/+5
A Loader keeps the height of its last item after active goes false. When a browser tab playing media was closed, its MPRIS player vanished, Player.active dropped and the window shrank to 100px, but the Loader still reserved 46px plus the Column spacing on top. The volume row was pushed down and clipped at the bottom edge, which is the "stuck" OSD a hot reload used to cure: a fresh Loader starts at zero height. Hiding the Loader while inactive makes the Column skip it. Both the retained height and the collapse were confirmed with headless qml6 runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25feat(vm): notify when a started VM accepts sshDanilo M.1-0/+28
libvirt reports "running" as soon as QEMU is up, long before the guest has booted, so the panel had no way to say when a VM is actually usable. After a successful start, a detached script polls the VM's sshd and sends a notification once it accepts connections, or a warning after five minutes. Host and port come from `ssh -G <vm>`: a Host alias named after the VM may put sshd on a non-default port (buildsystem listens on 2290, and a probe of port 22 never succeeded). Without an alias `ssh -G` echoes the name back, and the guest agent's IPv4 address on port 22 is used. The probe is a plain `nc -z`, so no authentication or key is involved. Detached rather than a Process so several VMs can wait at once and the wait survives a shell reload. Only starts issued from the panel are watched; a VM started from virt-manager or the CLI is not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-21feat(status): report a running game as its own presentation stateDanilo M.3-3/+92
status.gaming is written by the shell's game detector and presentation is the OR of it and the manual file, but statusctl read only the manual half. A game therefore asserted the presentation effects while the waybar pill still read deactivated. Report gaming as a third state, for presentation only. The watch follows status.gaming as well as its own file, and emits only on a real change so an unchanged rewrite draws nothing. Toggle is refused while a game holds the mode: the detector reasserts the file within its poll, so the write would not stick. set stays allowed, the manual half is worth setting for when the game exits. gaming stays out of MODES. The detector owns that file and nothing here may write it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21feat(status): detect PCSX2 for game modeDanilo M.1-3/+3
PCSX2 joins gamescope, Steam and DuckStation in the game detector, so presentation is set automatically while a PS2 game runs and the idle lock, DND and breaktimer stay out of the way. The binary is /usr/bin/pcsx2-qt, the same -qt shape as duckstation-qt, so it takes the same pgrep -x exact name match and cannot self-match the way the bracketed steam pattern has to guard against. Verified against the real emulator rather than a stand-in: with pcsx2-qt running, status.gaming reads 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20feat(status): auto-present while DuckStation runsDanilo M.1-4/+6
Game detection probed only gamescope and Steam, so a standalone DuckStation session never turned presentation on. Add a third probe for the duckstation-qt process. pgrep -x rather than the bracketed pgrep -f the Steam path needs: this is an exact match on the process name, so it cannot match the sh running the check, and the binary name is under the 15 character comm limit so it is not truncated.
2026-09-20feat(desktop): dismissable bluetooth pairing error on the pageDanilo M.2-5/+52
A failed pairing showed the raw bluetoothctl output as a plain red line that only cleared on the next attempt, and the desktop notification is suppressed while the drawer is open, so the failure was easy to miss and impossible to dismiss. Show it in a red card with a close control that clears pairing.error. It has no timeout, and clearing the source means the next identical failure shows again rather than staying hidden.
2026-09-20feat(desktop): read gamepad battery from power_supplyDanilo M.3-2/+81
BluetoothRow only read BluetoothDevice.battery, which is BlueZ's org.bluez.Battery1. A gamepad never publishes that interface: a Sony pad exposes its charge as a kernel power_supply instead, so the percentage never appeared. Read every power_supply whose name ends in a MAC on a 30s timer while anything is connected, keyed by address. BlueZ still wins when it has a value.
2026-09-19feat(status): auto-present while a game runsDanilo M.1-1/+27
Presentation is worth having automatically while a game runs: it is a fullscreen app that already holds the screen, and the idle lock, DND and breaktimer are all unwanted during it. Detection is a 5s poll for a gamescope process or a Steam game binary, either one enough. The Steam pattern is bracketed so pgrep -f cannot match the sh running the check and report a game forever. The result goes to its own status.gaming file, and presentation becomes the OR of it and status.presentation. That separation is the point: a game ending clears only the gaming half, so a presentation set by hand survives a game session instead of being clobbered on exit.