| Age | Commit message (Collapse) | Author | Files | Lines |
|
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.
|
|
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.
|
|
The check has five assertions, not four.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
Outgoing pair and unpair go through kdeconnect-cli; accepting and
rejecting an incoming request go straight to the daemon, since the CLI
has no verb for them. The verification key is shown on the row while a
pairing is in flight so the two ends can be compared.
An incoming request is not noticed while the drawer is closed, because
the poll only runs when it is open; the daemon keeps the request, so it
is there on the next open.
|
|
Ring, clipboard, mount and unpair, all shell-outs since there is no D-Bus
module. Actions share one serialized Process and a failure notifies, for
the case where the drawer has closed by the time the command returns.
Mount runs --mount and then --get-mount-point in one shell, because
--mount prints nothing and the path is a second call.
|
|
State is polled from kdeconnect-state.sh because there is no D-Bus module
to subscribe with. The poll runs only while the drawer is open: the tile
is created when the panel loads and destroyed when it unloads, and it
flips the module's polling flag.
The laptop reports no battery plugin, so an absent charge is an empty
field and renders as nothing, never as a zero.
|
|
Quickshell 0.3.1 has no generic D-Bus module, so device state comes from
qdbus6 shell-outs. The script emits a tab line protocol for the QML side
and is checked by a stub-daemon test, since the live daemon is not an
oracle an agent can rely on.
A failed daemon query exits non-zero with no output so the module keeps
its last list, which is not the same as a valid empty device list.
|
|
Absorbs the kdeconnect-indicator into the drawer: status, battery,
pairing, ping, clipboard send, file share, refresh and mount. State is
polled from a qdbus6 helper script while the drawer is open, since
quickshell 0.3.1 has no generic D-Bus module.
The spec notes the phone-to-PC clipboard is the daemon's plugin, not the
indicator's, so removing the indicator does not touch it.
|
|
The pairing flow needs isPairRequested, isPairRequestedByPeer and the
verification key, so the polled line carries them; the key is only queried
with a pairing in flight. A failed daemon query is a non-zero exit with no
output, which is how the module tells it apart from a valid empty list.
Pairing failure comes from the kdeconnect-cli exit code, not the
pairingFailed signal, since there is no generic D-Bus module to watch it.
|
|
Status plus basic actions, absorbed from the indicator. State comes from
qdbus6 over kdeconnectd, since quickshell 0.3.1 has no generic DBus module.
Polled while the drawer is open, driven by the tile lifecycle.
Notes the indicator is not what serves the phone-to-PC clipboard, and the
FileDialog focus risk under an Exclusive layer surface.
|
|
Adding desktop/Switch.qml while only editing files under modules/ left it
out of the running shell's generated qmldir, so the two pages that use
Switch could not resolve it and their tiles dropped from the grid. The
code was correct; a fresh process rendered it. The directory's qmldir is
regenerated only when a file that imports the directory reloads.
|
|
|
|
|
|
|
|
Fontconfig falls back per codepoint, so the same codepoint can be a
different glyph in a different Nerd Font: the ethernet codepoint rendered
as a gamepad. The theme now pins the family and the note says to check the
font cmap for a new glyph.
|
|
Glyphs are Nerd Font private-use codepoints, and relying on per-codepoint fontconfig fallback from Noto Sans let the same codepoint resolve to a different glyph in a different Nerd Font.
A Theme.iconFamily now names the render family once, and every glyph Text uses it, while fontFamily stays Noto Sans for words and labels. The ethernet glyph moves from \uf6ff to \uef44, the fa-ethernet codepoint present in Inconsolata.
|
|
|
|
The tile no longer swaps a wired/wifi icon, and the local IP comes from
ip -j because Quickshell.Networking exposes only the MAC. Recorded in the
spec so it matches the code.
|
|
|
|
|
|
The native backends start empty for about two seconds, device lists are
ObjectModels, NMSettings is not in QML scope, and quickshell has no BlueZ
agent. Each one cost a probe to learn; writing them down is cheaper than
re-learning them.
|
|
The Connections block needs a typed property on a QtObject, the bluetooth
README lines belong to Task 6 so every commit stays self-consistent, and
Pairing has no status property. Each was found by a task review and fixed
in the plan as well as the code.
|
|
|