aboutsummaryrefslogtreecommitdiffstats
AgeCommit message (Collapse)AuthorFilesLines
21 hoursfeat(appearance): launch wallhaven-dl instead of the TinEye searchHEADmasterDanilo M.2-42/+14
The reverse image search never worked in practice: the clipboard-and-paste route to TinEye was the only one left after scripted uploads hit bot and consent walls, and it still left finding and replacing the file by hand. wallhaven-dl, a separate app, does both the search for a larger copy and general wallpaper search and download, so the button now launches it and the TinEye code is removed rather than hidden. The button is always shown, not only for a low-res staged image, since the app is useful beyond upscaling. It launches through gtk-launch and the app's desktop entry, which needs no ~/bin on quickshell's PATH and keeps the app's taskbar icon, and closes the panel so the window does not open under the overlay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hoursfeat(appearance): find a larger version of a low-res wallpaperDanilo M.2-0/+47
When the image staged for the targeted screen gets a yellow or red quality note, a button under the mock screens copies it to the clipboard and opens TinEye in the browser, where Ctrl+V runs the reverse search and the matches can be sorted by size. It acts on the staged image, not the hovered one, because the pointer leaves the thumbnail to reach it. The image goes through the clipboard because a search engine takes no local file in a URL, and scripted uploads did not get through: TinEye answered 403 to curl and 405 with a browser user agent, and Google Lens redirected to its consent page, dropping the image. The paste in the browser is the upload, and only on the user's click. The copy is converted to PNG with magick. Copied as itself, a JPEG put only image/jpeg on the clipboard and Firefox pasted nothing into the page; it hands web content image/png. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hoursfeat(appearance): wallpapers by folder, system ones, quality bordersDanilo M.3-64/+321
The wallpaper tab showed every image under ~/Pictures/wallpapers in one grid. It now groups them by top-level folder, each behind a header that opens and closes it, and adds the distribution's /usr/share/wallpapers: its loose images as "system", its Plasma packages as "Plasma". A package carries one picture at several resolutions under contents/images/, some with a dark version under contents/images_dark/, so it shows once, the dark version where there is one and the largest of those, labelled with the package name rather than "5120x2880.png". Groups are keyed apart from their names, so a local folder called "system" does not share open state with the system group. Open state lives in the Wallpapers singleton, because the panel is destroyed on close. The list is one flat ListView of rows, a header or a row of thumbnails, so only rows in view (plus cacheBuffer) are instantiated and decode. A first version made each folder one delegate, which loaded every image of a folder as soon as it opened; the system group alone is 727. The rows live in a ListModel and a toggle inserts or removes only that folder's rows: assigning a fresh JS array instead resets the view, which loses the scroll position and re-decodes everything visible. cacheBuffer is clamped at 0 because the column's width reads negative before layout and the ListView warns on each negative value. The scan's find calls go through a small shell function rather than eval. Each thumbnail's 2px border tells how it will look on the targeted screen. `file -N -0` reads every image's size from its header after each scan, ~0.6s for 1161; the NUL keeps names containing ": " intact, and the size is the last WxH in the description because a JPEG's "density 72x72" comes first. wallp runs swaybg -m fill, which scales to cover, so the measure is the enlargement factor: <=1 green, <=1.33 yellow, else red, in the udt palette colours. A note under the mock screens, in that colour, gives the size, the factor and why it will look soft or pixelated, for the hovered image or else the staged one. The staged image keeps its accent border. Screen sizes are fixed, as the mock's already were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
24 hoursfeat(notifications): window border on the balloonsDanilo M.2-5/+95
The balloons now wear the Hyprland window border: 2px, the udt gradient read from ~/.cache/udt/udt-border.lua (the file look_and_feel.lua reads, watched so a wallpaper change follows), rotating, with the windows' corners. A Rectangle border takes one colour and its radius is circular, while the windows use rounding 15 at rounding_power 4, a superellipse. So the body and the border are both drawn by a Shape from an SVG path whose corners are the cubic fit to |x|^4 + |y|^4 = 1, and the border is a ring filled odd-even, which keeps the translucent body from showing the gradient through. One angle drives every balloon, so they turn together, and the animation runs only while the stack is mapped. Hyprland's borderangle (speed 30) reads as 3s a turn, which looked slow on a balloon that size, so the turn is 1.5s, tuned by eye. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 hoursdocs(agents): llama-server /slots autoloads the modelDanilo M.1-0/+6
A slot query for an unloaded model loads it into VRAM, so any status probe against the router needs autoload=false. Found when a probe during the ai module's design reloaded Gemma. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 hoursdocs(ai): module README, drawer module listDanilo M.2-1/+53
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 hoursfeat(ai): drawer page for the local AI stackDanilo M.5-0/+336
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>
45 hoursfix(ai): count a running sd-cli as the sd engineDanilo M.3-8/+37
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>
45 hoursfeat(ai): state sweep for the AI stackDanilo M.3-3/+245
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>
45 hoursdocs(ai): implementation plan for the AI moduleDanilo M.1-0/+836
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
45 hoursdocs(ai): design for the AI stack drawer moduleDanilo M.1-0/+94
Engines (llama.cpp, sd.cpp) read-only, apps get start/stop through their existing controls. Records that /slots?model= autoloads an unloaded model unless autoload=false is passed, found when a design probe reloaded one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 daysfix(status): watch for create, so a mode's first write is seenDanilo M.3-5/+29
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>
4 daysdocs(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>
4 daysfeat(status): present during a Teams callDanilo M.2-2/+6
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>
8 daysdocs(agents): hot reload does not follow symlinksDanilo M.1-0/+7
An edit to shared/Notify.qml was never picked up by the running shells, while an edit in the same pass to a component's own file did reload, so the shell looked current and ran the old sanitize. Record that shared/ edits need a forced reload of each linking component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(notifications): render bodies as MarkdownDanilo M.3-3/+10
Balloon and drawer bodies switch from RichText to Qt's MarkdownText (CommonMark plus GitHub extensions). Raw HTML still goes through Qt's HTML importer, so body-markup senders keep working. Markdown images fetch their destination the same way an <img> does, which the existing sanitize filter would miss since it only inspects tags. sanitize now escapes every "![" so the syntax renders as literal text, local or remote; parsing CommonMark destinations and reference definitions to allow local ones is not worth the grammar. Verified in the shell log: before the filter reached the running shell, a test body fetched https://example.org/a.png; after, a second one did not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfix(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>
8 daysfeat(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>
8 daysfeat(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>
8 daysfeat(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>
8 daysfix(appearance): name btop and Joplin in the restart noticeDanilo M.1-1/+1
Both now follow the scheme but only read their theme at startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(appearance): recap tab first, consumer tabs by display nameDanilo M.3-5/+13
install.sh's tab is the overview of a whole run, so it leads as "General recap"; the consumer tabs follow, labelled GRUB, SDDM and Homepage. File names stay the producers' short ids, and one without a label shows under its own name, so a new producer still needs no edit here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysrefactor(appearance): let install.sh fill its own steps tabDanilo M.1-5/+4
install.sh now hands its output to the steps window on every run, so the Theme tab stops piping it through udt-steps, which would write the tab a second time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 daysfeat(appearance): steps window, icon regen, cursor out of hypr-themeDanilo M.7-37/+380
StepsWindow shows unified-desktop-theme's pending manual steps, one tab per consumer, centred on DP-3 on the top layer with on-demand focus so it can stay open while commands are pasted elsewhere. udt-steps writes the files and calls the new `steps` IPC verb; indented lines become command blocks with a copy button. The Theme tab pipes install.sh's output into it. The icon switch now also reruns udt-accent (SDDM power icons) and waybar-theme-udt's install.sh (taskbar theme, module icons), chained after the gsettings write both read. Cursor changes write ~/.config/udt/cursor.lua instead of editing hypr-theme's tracked environment.lua. The theme notice and the Udt.qml comment claimed install.sh reloads nothing; it reloads Hyprland, waybar, kitty and conky now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 daysfix(notifications): theme the action picker with udtDanilo M.1-1/+2
notify-actions.sh called rofi with no -theme, so clicking a notification that carries actions opened rofi in its stock theme instead of the udt one every other menu uses. Pass ~/.config/rofi/udt/menu.rasi, the small fixed-options layout the screenshot and notes menus already use. menu.rasi shows only the listview, so the "Notification" prompt and the filter input are not drawn; a notification's actions are few enough that picking from the list is enough. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 daysfix(assistant): retry the event socket with a fresh SocketDanilo M.2-9/+22
Quickshell 0.3.1 keeps the QLocalSocket of a failed connect, so setting connected = true again never retries: an overlay started before the assistant (at login, or by a restart) stayed dark for good. The Socket now lives in a LazyLoader that the retry timer turns off and on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 daysfeat(assistant): overlay for the voice assistantDanilo M.7-0/+345
Reads desktop-assistant's event socket, JSON lines on a Unix socket the assistant owns: state, context use, the running tool and audio levels. A circle shows the name and used/total context, a wrench and the tool name while one runs, and pulsing dots while it transcribes or thinks. Around it three sine ripples follow the loudness, the user's voice and the reply in two palette colours from Theme, so a udt change recolours them live. It fades out a configurable delay after the reply. The ripple is driven by FrameAnimation, not a Timer: a 16 ms Timer drifted against the display refresh and visibly stuttered. Standalone like the other components; it reconnects every 2 s while the assistant is down, and the assistant runs the same without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfix(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>
12 daysfix(keybinds): wrap long labels instead of eliding themDanilo M.1-1/+1
A label is the only place the reminder says what a bind does, so cutting it short loses the part that matters, as with the column moves' scrolling-layout note. A long one now takes a second line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(keybinds): draw the macropad keycaps as they lookDanilo M.2-21/+57
Each key showed its plain action as text. It now carries the real cap's legends, Nerd Font glyphs checked by name in Inconsolata, with the shifted or Alt legend below and the cap in the palette colour nearest the real one. Hovering still lists every action. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(theme): read blue, teal, crust, peach and mauveDanilo M.1-0/+12
For the keybind reminder's macropad keycaps. blue reads termblue, not blue: udt-palette's blue is the active-border role, magenta under candy-night, and termblue is the terminal's actual blue. The defaults are Macchiato's, as for the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(keybinds): draw the macropad beside the listDanilo M.5-84/+233
The pad glyph on each row was too small to read. The left fifth of the overlay now draws the pad at size, each key labelled with its plain action; hovering a key or an encoder lists everything it does, held keys and turn direction spelled out. padActions() in Binds.js inverts the grouped rows into that per-key list, and PAD's turn values became words for it. The panel is pinned at a fixed height: centred, it moved whenever the hovered key's list changed length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(keybinds): show where a bind sits on the macropadDanilo M.4-5/+120
The macropad's firmware sends ordinary combos, so the overlay listed them as keyboard shortcuts nobody presses. A PAD table in Binds.js maps each combo to its key (1-12) or encoder (top or bottom, turned or pressed) and what is held with it, and the row draws the pad small with that key lit. The workspace row is built in a loop, as it is in keybindings.lua. Every entry was checked against hyprctl binds -j. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(keybinds): show mnemonic names for obscure keysDanilo M.3-10/+38
Hyprland's key names are its own (mouse:272, XF86Calculator), not what is printed on the keyboard or how the key is pressed, which defeats a reminder. A fixed map in Binds.js renames them for display only; an unlisted key shows as before. SUPER reads as Win for the same reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfix(keybinds): fill columns top to bottomDanilo M.1-2/+9
Groups flowed left to right in rows, so each row was as tall as its longest group: short groups sat above empty space and the block ran past the top and bottom of the screen. Columns filled to the screen's height pack the groups and keep everything visible. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(keybinds): live keybind reminder overlayDanilo M.6-3/+234
Modelled on window-switcher: a Scope with a 1x1 keepalive PanelWindow plus a LazyLoader'd overlay, opened via IpcHandler (SUPER+k is bound in a different repo). hyprctl binds -j is asked fresh on every opening so the list matches whatever Hyprland has loaded, with Binds.group() from task 1 turning it into the grid's {name, rows} groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfix(keybinds): survive group names shadowing Object.prototypeDanilo M.2-1/+9
A description like "toString: X" made byName["toString"] resolve to the inherited Object.prototype.toString instead of undefined, so the group was never created and .rows.push threw. Object.create(null) gives byName no prototype to shadow. Also note in a comment that CAPS/MOD2/MOD3/MOD5 modmask bits are not decoded and silently drop from the combo string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(keybinds): group hyprctl binds by descriptionDanilo M.2-0/+95
Binds.js parses hyprctl binds -j output into display groups for the upcoming keybind reminder overlay. Group name and row label come from the "Group: Label" convention in each bind's description; a bind without one still surfaces under "Undescribed" rather than vanishing, so a gap in the Hyprland config stays visible. Pure functions with no QML dependency, so test_binds.js checks them directly with node, stripping the .pragma library line the QML runtime requires but node does not understand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysdocs(keybinds): implementation planDanilo M.1-0/+587
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysdocs(keybinds): design for a live keybind reminderDanilo M.1-0/+64
The list comes from hyprctl binds -j on every opening, so it is always what Hyprland has loaded, loop-generated binds included. Labels come from each bind's description option, since hyprctl reports Lua actions as an opaque __lua handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat: reload.sh restarts quickshell componentsDanilo M.2-0/+44
The volume OSD drew only its "Output 72%" label, with the icon, bar and device line clipped below the panel edge. The code and the theme were not at fault: a hot reload fixed it, and a debug line logged just after showed the volume row laid out correctly at the top of the panel. The stuck state lived in the running process, so the fix is a restart. quickshell has no reload subcommand and no IPC target for it, and a hot reload needs a real content change to a file. reload.sh kills and restarts each named component, or every directory holding a shell.qml, and prints the qs count afterwards, since the process is qs and not quickshell. It must run from the user's terminal. A detached qs started from an agent's tool call dies with the call, which would leave the desktop without its shells; AGENTS.md now says so under Reloading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 daysfeat(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.2-7/+7
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.2-8/+11
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.2-2/+77
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.
2026-09-19feat(kdeconnect): open the KDE Connect window from the pageDanilo M.3-2/+12
The drawer absorbed the indicator's device actions but not the settings window, so there was no path to the full KDE Connect app. Add an Open app button beside Refresh, launching kdeconnect-app through execDetached so the window outlives a quickshell reload.
2026-09-19feat(desktop): press feedback on the shared buttonsDanilo M.3-5/+13
The hand-rolled buttons only reacted to hover, so a press gave no visual confirmation until the action's outcome appeared. kdeconnect made this obvious: its row buttons shell out to kdeconnect-cli, and nothing on screen acknowledged the click. Button.qml now fills solid accent while the mouse is held (red for danger), with the label flipped to base for contrast. TransportButton gets the same treatment, and the window-switcher close button lightens on press. Toggles, tiles and tabs are left alone: their own state change is already the feedback.