aboutsummaryrefslogtreecommitdiffstats
AgeCommit message (Collapse)AuthorFilesLines
6 hoursfix(notifications): check every img src and uncache previewHEADmasterDanilo M.2-7/+14
The sanitizer matched the first \bsrc anywhere in the tag, so a data-src="file:///tmp/decoy" before a remote src won the match and kept the whole tag while Qt, which ignores data-src, fetched the remote image. Scan every src assignment in the tag and keep it only if all of them are local. Also set cache: false on the balloon preview, since the daemon overwrites the same path on a replace and QQuickPixmapCache keys on URL, so a replaced notification could show the previous image.
6 hoursfix(notifications): allowlist local inline image sourcesDanilo M.1-5/+22
The http(s)-only regex let protocol-relative //host, ftp, data and entity-encoded schemes through, and the comment overstated what it removed. Replace it with a deny-by-default allowlist: an <img> is removed unless its src, entities decoded first, is a file: URL or a single leading slash. Protocol-relative //host is rejected while a /absolute/path is kept.
6 hoursfeat(notifications): strip remote inline image sourcesDanilo M.3-2/+10
A notification is untrusted input. Inline <img> now renders only for local sources; an http(s) source is removed before the RichText body is shown, so a remote sender cannot make the shell fetch a URL. The row and the balloon share the one sanitizer in the Notify singleton.
6 hoursfeat(notifications): draw the content image in the balloonDanilo M.1-1/+25
The daemon now publishes an image path; the balloon shows it below the text, scaled to the balloon width with a 240px cap. A daemon without the field leaves it hidden, so the renderer and the daemon can ship in either order.
6 hoursdocs: add the notification images implementation plansDanilo M.2-0/+1218
Two plans, one per subsystem: the notifyd daemon (hint parsing, PNG encode, theme-name resolution, the image field and its cleanup) and the quickshell renderers (the balloon preview and remote inline source stripping). Either can ship first; the renderer tolerates a daemon without the field.
6 hoursdocs: add the notification images designDanilo M.1-0/+201
The daemon drops every image hint today, so a screenshot and an application image never arrive. libnotify 0.8.8 sends the content image via -i/--icon (the image-path hint) while -n/--app-icon is app_icon. The spec adds the image contract field, daemon hint handling and theme-name resolution, the balloon preview, and a local-only inline image policy.
7 hoursfeat(desktop): make the drawer notifications readableDanilo M.4-16/+49
Each row shows when the notification appeared ("Sep 15 14:32", from created), sits on its own rounded card with space between rows instead of reading as one list, and uses the balloon's 16/18/16 sizing rather than 13/11. The close target grows to a 32px hit area in the row and the balloon, since a 20px box was hard to hit.
8 hourschore: ignore the superpowers SDD workspaceDanilo M.1-0/+1
The per-plan ledger, briefs and reports under .superpowers/ are scratch, not source. Committing the ignore rule keeps the tree clean for every clone and worktree instead of only this one, where it lived in the local info/exclude.
9 hoursfix(notifications): apply final review fixesDanilo M.5-4/+26
Mirror the balloon's right-click close-all on the drawer row: the spec says the gestures are identical in both forms, and a right-click on a row previously did nothing. The right-button branch precedes the live guard so it behaves the same on history rows. Stop importing the Status singleton into the notification shell. Referencing it instantiated it, and its onPresentationChanged writes status.dnd and runs breaktimer.sh, so a read-only consumer was writing state and shelling out on every presentation toggle, and doubled the dndBeforePresentation race. Notify now reads "/run/user/<uid>/status.dnd" directly through a FileView, the same convention as the notifyd files; a missing file means off. AGENTS.md named Drawer.qml as the drawer's reserved space; it is desktop/NotificationList.qml.
9 hoursdocs(notifications): document the renderers and record the trapsDanilo M.2-0/+23
The daemon is a separate process and the files are the interface; suppression lives in the balloon shell so the drawer can list what DND held back; and the drawer flag is what stops a notification appearing as both a balloon and a row.
9 hoursfeat(status): add the snooze rowDanilo M.2-0/+124
A switch and a free-text minutes field driving notify-snooze.sh. Snooze is a file the balloon shell reads, so the row only writes it; the switch follows the file, including a snooze that ends while the page is open.
9 hoursfix(desktop): leave history when deep-linking to a pageDanilo M.1-0/+3
show() is the IPC deep-link path and set only page/open, so if the history view was open, historyLoader stayed active alongside pageLoader and painted over the requested module page. Clear history in show(), as close() already does.
10 hoursfeat(desktop): add the notification history pageDanilo M.2-4/+92
Reachable only from the reserved space's History button, so it is a drawer view, not a module and not a tile. Rows are inert; clear-all empties the ring, since the daemon has no per-id history deletion.
10 hoursfix(desktop): publish the drawer flag at startupDanilo M.1-0/+5
onOpenChanged does not fire for the initial value, so a restart while the drawer was open left notifyd/drawer at "1" and every balloon stayed suppressed until a drawer open/close cycle. Write the starting state from Component.onCompleted so the file always reflects the drawer.
10 hoursfeat(desktop): fill the drawer's notification spaceDanilo M.3-6/+199
The reserved Item becomes the live queue with a History button, scrollable because the queue can hold 20. The drawer writes notifyd/drawer so the balloon shell stands down while this space is showing, which is what stops a notification appearing twice.
10 hoursfeat(notifications): add the rofi action pickerDanilo M.1-0/+40
The renderer passes the action labels and keys as arguments, so the picker parses no JSON; a chosen label maps to its key and runs notifyctl action.
10 hoursfix(notifications): map the balloon window from the queue, not layoutDanilo M.1-3/+8
Deriving the window's visible from column.implicitHeight deadlocked it: a hidden window stops polishing, Column computes implicitHeight during polish, so once the column was empty the window never re-mapped and no later balloon could show, including at startup where the queue arrives asynchronously after the window is already hidden. The binding now reads Notify.queue membership, which a property binding re-evaluates whether or not the window is mapped, so a later notification re-maps it. Expired entries left in the queue keep a zero-height window mapped, which is harmless.
10 hoursfix(notifications): keep the balloon model stable across ticksDanilo M.2-14/+31
The Repeater model was a fresh filter of Notify.queue on every 250ms tick because the filter read now, so Qt Quick recreated every balloon delegate four times a second, reloading icons and resetting hover. Model is now Notify.queue itself, and each balloon drops itself at expiry through its own visible binding, driven by the shared tick. Removing required from the balloon's properties is part of the same fix: a required property makes QML create the delegate in its own context, where modelData and index are undefined, so notification: modelData silently arrived undefined once a live queue item was actually drawn.
10 hoursfeat(notifications): add the balloon shellDanilo M.7-0/+244
Reads the daemon's queue through the Notify singleton and draws a balloon per live notification, bottom-right of DP-1 over conky. Suppression is here, not in the daemon: dnd withholds low and normal, snooze withholds all, and the drawer still lists them.
10 hoursfeat(desktop): add the Notify singletonDanilo M.2-0/+124
The daemon publishes its queue, history, drawer flag and snooze as files; this reads them for both renderers, the same files-are-the-interface convention the status registry set. Mutations and the rofi action picker run notifyctl through a Process, which is the one path back to the daemon.
10 hoursdocs: add the notification renderers implementation planDanilo M.1-0/+1380
Plan two of two: the quickshell side of the notification daemon. The Notify singleton reads the daemon's published files, a new notifications/ component draws balloons and owns suppression, the drawer's reserved Item becomes the live queue with a history page, and the Status page gains a snooze row. Suppression lives in the balloon shell so the drawer can list what DND held back, and the drawer flag is the single no-double-show mechanism. Per-id history removal is a tracked TODO in the notifyd repo rather than an untracked spec gap.
10 hoursdocs: fix the expire_timeout direction in the spec and planDanilo M.2-20/+27
The freedesktop spec says -1 means the server decides and 0 means never; the spec and plan had them reversed, which the shipped code inherited and the live handover test caught. Corrected to match the spec, libnotify's -1 default and dunst.
10 hoursdocs: correct the snooze check count to fiveDanilo M.1-1/+1
The check has five assertions, not four.
10 hoursdocs: fix the runtime dir collision in the check scriptDanilo M.1-1/+2
The daemon binary was built at $tmp/notifyd, the same name RuntimeDir() gives under XDG_RUNTIME_DIR, so the two collided. The runtime dir moves to $tmp/run, which is what the shipped script does.
10 hoursdocs: give the plan's test files the GPLv2 headerDanilo M.1-0/+44
The test snippets omitted the header the global constraints require on every .go file, which a task review flagged against the first one shipped. The snippets now carry it, so later tasks do not repeat the gap.
11 hoursdocs: record the notifyd repo as createdDanilo M.1-4/+4
Public under the Linux cgit section, cloned to ~/Programming/GIT/notifyd, so Task 1 Step 1 is done and the executor starts at Step 2.
11 hoursdocs: add the notifyd implementation planDanilo M.1-0/+1780
Plan one of two for the notification daemon: the Go daemon and notifyctl in their own repo. The renderer plan is separate because the daemon is working, testable software on its own, exercised through notifyctl. The policy is pure and tested without a bus; the store's expiry, dismissal and eviction are separate lifetimes, which the store tests pin; and test-notifyctl.sh runs the daemon and CLI on a private session bus against a temporary runtime directory.
11 hoursdocs: separate a notification's balloon and drawer lifetimesDanilo M.1-24/+57
The freedesktop expire_timeout is the notification's lifetime, and --wait and -b clients block until the daemon closes it. So the daemon emits NotificationClosed at expiry and frees the client, but keeps the entry in the live queue: the drawer lists it until it is dismissed or evicted, inert because its client is gone. History holds only what was dismissed or evicted. The reserved space is therefore scrollable, since the live queue can hold 20 at once, and a row past its expiry is read-only.
11 hoursdocs: add the notification daemon designDanilo M.1-0/+276
The second of the two specs the status registry named. A Go daemon owns org.freedesktop.Notifications and holds the state; quickshell renders it. The daemon and notifyctl live in a separate repo, the balloon shell and the drawer's notification centre live here. Rendering is split because quickshell has no generic D-Bus module, so something outside QML has to own the bus name. Files are the interface in the direction the renderers read, the same convention the registry set, and notifyctl is the one surface back. DND suppression lives in the balloon shell, not the daemon: the drawer's reserved space lists every live notification including the suppressed ones, because a list the user opened is not an interruption. This supersedes the registry spec's assumption that the daemon would read dnd.
12 hoursfix(status): address final review findingsDanilo M.6-10/+23
F1: after a click the shared Switch writes checked directly, dropping StatusRow's declarative binding, so an external mode change from waybar or statusctl no longer moved the switch. Resync via onValueChanged. F2: the spec claimed hyprctl clients counts idle inhibitors, which is false for a layer-surface inhibitor; replace with the behavioral hypridle marker check. F3: README said the check covers both watch states; it exercises the activated report and the absent-file down report. F4: give breakProc an onExited check so a failed breaktimer.sh verb is visible instead of silent. F5: drop StatusTile's unused required st property and its injection. F6: comment the startup-order limit on dndBeforePresentation.
12 hoursdocs(status): document the module and record its trapsDanilo M.3-5/+111
A FileView fires its own fileChanged on setText, so a handler that writes in response to a change loops unless it compares first. IdleInhibitor has no window of its own and does nothing without one, so the singleton is handed the keepalive window by shell.qml. Both were read from the documentation while designing and confirmed while implementing.
12 hoursfix(desktop): keep statusctl quiet when the mode file is absentDanilo M.2-12/+12
read_mode() failed on a missing mode file with a shell redirection error, 'statusctl: line 56: .../status.dnd: No such file or directory', printed to stderr on every get/toggle before the file existed and polluting waybar's exec stderr. The '2>/dev/null' sat on the 'tr' command, but the error is the parent shell's input redirection failure, which that redirect cannot suppress. Guard on '[[ -e "$file" ]]' before reading; a missing file still reads as 0, which was always the contract. Mirror the fix into the plan so plan and script agree.
12 hoursfeat(desktop): add the status page and register the moduleDanilo M.2-0/+45
One row per mode. Adding a mode is one file in the singleton and one row here, which is what a registry buys over two separate toggles.
12 hoursfeat(desktop): add the status module, tile and rowDanilo M.3-0/+122
The module is thin because the singleton owns the modes: it is a tile and a page over Status, always active so the effects hold whether or not the drawer has been opened. Injected as st rather than mod, since a component property named the same as the enclosing object's id binds to itself and arrives undefined.
12 hoursfeat(desktop): add presentation mode and its effectsDanilo M.2-7/+60
Presentation mode sets DND, asserts a Wayland idle inhibitor and pauses breaktimer. The effects hang off the mode property rather than the setter, so a mode set with statusctl while the drawer is closed asserts them too. DND has two writers once presentation mode exists, so turning presentation off restores the value DND had before rather than clearing it, or an afternoon of hand-set DND would vanish when a talk ends. That prior value lives in the singleton, not in a file: it means nothing once presentation mode is off, and presentation mode does not survive a reboot. breaktimer owns its own state file and is driven only through its verbs. Two writers on that file would race with its daemon loop.
12 hoursrefactor(desktop): name the keepalive windowDanilo M.1-0/+5
IdleInhibitor needs a non-null window and this is the only one that lives for the whole session, so presentation mode attaches to it. Naming it is a prerequisite for that and changes nothing else.
13 hoursdocs: correct the check count and mirror the statusctl hardeningDanilo M.1-4/+13
Two corrections to the plan, both found by running it rather than reading it. The test script the plan specifies makes nine assertions; the prose around it claimed eight, in both the task step and the final check. An implementer running it would see a passing suite that disagrees with its own expected output, which reads as a failure of either the test or the plan. My arithmetic. The plan's statusctl also swallowed a failed rename and read the mode file through cat piped into tr, so a write that did not land still reported success and an unreadable file fell back to off by accident of pipeline semantics rather than by intent. Review of the implemented script caught both. The plan now carries the same fixes, so a future run of it does not reintroduce them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
13 hoursfeat(desktop): add the statusctl CLI and its checkDanilo M.2-0/+219
statusctl reads and writes the mode files directly rather than going through the shell, so it works while quickshell is down. Setting a mode that way records the state without firing its effects; the shell sees the change through its own watch and reasserts them. The watch listens on the directory, not the file: an atomic write replaces the file, so a watch held on the old inode dies with it. Same trap the mail watcher hit with Xapian, and the reason moved_to is in the event list. An unknown mode exits non-zero rather than reading as off, so a typo cannot masquerade as a mode that happens to be disabled. The watch loop runs inotifywait through process substitution, held open on fd 3, with the PID captured and trapped on EXIT/TERM/INT. Piped straight into the while loop, inotifywait would be a pipeline sibling rather than a child, so a plain kill on statusctl leaves it running, watching a directory nobody reads anymore. Task 7 wires this CLI into waybar's exec, and waybar kills and respawns exec children on every reload the same plain way, so every reload would otherwise leak another watcher for the rest of the session. Process substitution makes inotifywait a real child whose PID the trap can hold and kill. A SIGKILL still orphans it, since KILL cannot be trapped, but nothing in the described path sends one. A failed rename exits non-zero instead of being swallowed. Reporting success on a write that did not land would leave the caller and the shell disagreeing about the mode, with an orphan temp file as the only trace, and this script is the interface the whole registry is read and written through. Verified against an unwritable directory: exit 1, no orphan left, prior value intact. Reading uses a redirect rather than cat piped into tr, so that an unreadable file falling back to off is explicit rather than a side effect of a pipeline discarding cat's exit status. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
13 hoursdocs: set atomicWrites explicitly in the plan's ModeFileDanilo M.1-1/+11
Review of the first task caught that the commit message credited atomicWrites while the code never set it. The property is already the documented default in 0.3.1, so the behaviour was right, but statusctl watches close_write,moved_to specifically because an atomic write arrives as a rename, and leaning on a default for something the CLI depends on is weaker than declaring it. Both ModeFile blocks in the plan now set it, so task four does not reintroduce the gap when it adds the second mode. Worth recording: the review asserted atomicWrites defaults to false in 0.3.1 and concluded the shipped writes were not atomic. The v0.3.1 and v0.3.0 documentation both read "If true (default)", so that conclusion was wrong, though the underlying observation about the unset property was right. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
13 hoursfeat(desktop): add the status singleton with the dnd modeDanilo M.2-0/+81
Modes live as files under XDG_RUNTIME_DIR, one per mode, holding 0 or 1, with a missing file meaning off. That directory is tmpfs, so a reboot resets every mode and no cleanup code is needed. FileView covers both directions: atomicWrites for the write, watchChanges for the watch, so an external writer repaints the drawer with no polling. Both are already the documented defaults in 0.3.1 and are set explicitly anyway, because statusctl watches close_write,moved_to precisely because an atomic write lands as a rename. A future release flipping either default would break the watcher with no error, and a declaration is a stronger guarantee than a default. The documented behaviour is that a FileView fires its own fileChanged on setText, so the reparse compares before assigning and a self-write is a no-op rather than a loop. That has not been observed running yet: nothing consumes the singleton until the status module exists, so the guard stays unproven until then. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
13 hoursdocs: add the status registry implementation planDanilo M.1-0/+1216
Eight tasks against the registry spec: the singleton with the dnd mode, the statusctl CLI and its check, naming the keepalive window, presentation mode and its three effects, the module and tile, the page and registration, installing the CLI and swapping the waybar module, then the README and traps. The plan carries the facts probed while designing so the implementer does not re-probe them: the runtime directory is tmpfs, elogind runs here with the pam caveat that stops it guaranteeing logout cleanup, the compositor advertises zwp_idle_inhibit_manager_v1, inotifywait is present, and Inconsolata Nerd Font lives under ~/.fonts rather than either share directory. That last one cost a failed cmap search that read as a missing glyph when it was a bad search path. Two claims stay marked unverified because they come from documentation and have not been observed running: that a FileView fires its own fileChanged on setText, and that IdleInhibitor does nothing without a non-null window. The plan handles both and tells the implementer to confirm them and record what actually happens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
13 hoursdocs: record the scan, logind and idle-inhibit trapsDanilo M.1-0/+31
Four findings from designing the status registry, all runtime-verified. The scan one shipped as a fix in f43f31a: neither BluetoothAdapter.discovering nor WifiDevice.scannerEnabled self-terminates, and closing the drawer destroys the page without stopping them, so a forgotten scan held the radio for the session. The logind one corrects an assumption that reached a committed spec before it was caught. elogind does run here, with pam_elogind in the PAM stack and a tracked session on seat0. The narrower true caveat is that pam_elogind no-ops when the system was not booted with elogind as init, so its documented removal of XDG_RUNTIME_DIR at last logout is not guaranteed on Slackware; a reboot clears it anyway because /run/user/1000 is tmpfs. The two idle-inhibit entries record that the compositor advertises zwp_idle_inhibit_manager_v1 and hypridle honours it, so an inhibitor goes over Wayland rather than D-Bus, and that waybar's built-in idle_inhibitor owns its own inhibitor object and therefore cannot be demoted to an indicator for a mode owned elsewhere. Held back deliberately: FileView firing its own onFileChanged on setText, and IdleInhibitor requiring a non-null window. Both are read from the documentation and neither has been observed running, so they wait for the registry implementation to confirm them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
13 hoursdocs: add the status registry designDanilo M.1-0/+229
Desktop modes as state: do not disturb and presentation mode, with one place that owns them. First of two specs; the notification daemon that replaces dunst is the second and consumes the registry's dnd mode rather than owning it. The registry comes first because DND belongs to the desktop rather than to the notification daemon, and because the daemon is the larger and riskier component. The design that needed deciding was not the toggle but the coupling. Presentation mode sets DND, so DND has two writers, and turning presentation mode off has to restore what the user had rather than clearing it. That rule is why this is a registry and not two booleans. State is one file per mode in XDG_RUNTIME_DIR, matching the convention breaktimer already writes on this machine. That directory is /run/user/1000, a tmpfs, so a reboot clears every mode with no cleanup code. Logout without reboot is less certain and the design does not lean on it: elogind runs here and its pam module documents removing the runtime directory at last logout, but the same manual says the module does nothing when the system was not booted with elogind as init, which on Slackware it is not. If removal does not happen, a mode survives a logout, which is the same behaviour as surviving a shell restart. FileView covers both directions: atomicWrites for the write, watchChanges for the watch, no shell-out. External watchers need close_write,moved_to because an atomic write arrives as a rename, the same trap the mail watcher hit with Xapian. Confirmed while designing: the compositor advertises zwp_idle_inhibit_manager_v1 and waybar's built-in idle_inhibitor already drives it here, so the inhibitor is asserted over Wayland rather than D-Bus. That module is replaced rather than demoted, because it owns its inhibitor object and cannot display state owned by anything else; left alongside the registry it would assert a second inhibitor and idle would resume only when both released. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
14 hoursfix(desktop): stop bluetooth and wifi scans after 60sDanilo M.2-0/+20
Neither scan property self-terminates. BlueZ discovery runs until StartDiscovery is stopped, and the NetworkManager scanner repeats until disabled; the docs for both say as much and give no timeout or interval property. The scan button was the only writer of either property, and closing the drawer destroys the page without touching them, so a scan started and forgotten kept the radio busy for the rest of the session. Continuous wifi scanning also costs throughput on the connected link. Each page gets a Timer bound to the scan property rather than started by the button. Binding it that way means a manual stop cancels the clock, and reopening the page on a scan still running from an earlier visit re-arms a fresh 60s instead of leaving it running forever. Verified both files parse: the desktop shell hot-reloaded without dropping its process. The timeout firing at 60s is not verified here, since it needs a real scan on the radio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7ThHHh5iTYbVfp3rNAkw2
28 hoursfeat(desktop): show the module glyph in every page titleDanilo M.2-4/+27
Page.qml gains an icon rendered before the title, and the drawer binds it to the loaded module's icon so a module whose glyph changes (network follows the active link) keeps the header in step.
29 hoursfix(desktop): queue KDE Connect actions instead of interruptingDanilo M.3-8/+26
A second action while one was in flight killed the first process, whose onExited then fired under the new label and raised a false failure notification. Actions are now queued and launched on exit, so each exit reports under its own label. Also correct two comments that contradicted the code: the pairing banner is only noticed on the on-open poll, and the laptop glyph keys off the daemon's desktop type.
29 hoursfix(desktop): name the kdeconnect injected property kc, not modDanilo M.5-29/+36
The tile, page and row each took a required property named mod while the module object's id is also mod. In a Component scope the id resolves to the property's own unset self, so every page.mod read was undefined and the device list never rendered. Only a runtime TypeError showed it; the smoke check was clean because the drawer was closed. kc matches the net and bt convention and cannot collide with the id.
29 hoursdocs: record the KDE Connect trapsDanilo M.2-0/+20
No D-Bus module means shell-outs and no push; the daemon and the indicator are separate; the battery plugin is not on every device; and qdbus reads properties and methods through one call shape.
29 hoursdocs(desktop): document the kdeconnect moduleDanilo M.2-7/+53
Records that the phone-to-PC clipboard is the daemon's plugin and not the indicator's, so removing the indicator does not break it, and that polling stops when the drawer closes.
29 hoursfeat(desktop): share a file to a KDE Connect deviceDanilo M.2-0/+30
A QtQuick.Dialogs FileDialog held on the module, not the page, so a closed drawer does not destroy it mid-pick. The path is decoded from the file:// URL before it reaches kdeconnect-cli.