| Age | Commit message (Collapse) | Author | Files | Lines |
|
Quickshell has no BlueZ agent and no generic D-Bus module, so a pairing
that needs a passkey confirmed cannot be done from QML. bluetoothctl
registers its own agent, so pairing is the one shell-out; adapter state,
scanning, connecting and forgetting all stay native.
The timeout is the ceiling: a device bluetoothctl cannot auto-confirm
fails visibly rather than hanging the page.
|
|
|
|
Native Quickshell.Bluetooth for adapter power, discoverable, scan,
connect, disconnect, trust and forget. Pairing is separate, because
quickshell ships no BlueZ agent.
The adapter is null for the first ~2s, so every access is navigated
safely rather than assuming a first paint.
|
|
|
|
|
|
Radio switch, scan, and a list ordered connected, known, strongest. A
secured unknown network reveals an inline password field rather than
opening a popup, since a QtQuick.Controls popup is a separate window that
positions badly on this layer surface, the same reason the sound page
expands in place.
Connection failures come through the Network.connectionFailed signal and
show inline; the same failure also notifies, for the case where the
drawer has closed by the time it lands.
|
|
Wired first because it is the simpler half and proves the module wiring.
Native Quickshell.Networking, no polling: the singletons push, and
alwaysActive is true with no Service.qml because there is nothing to gate.
The tile shows one line per active link rather than picking a primary,
since the API exposes no route metric and guessing wrong would be worse
than showing both.
|
|
Eight tasks, each leaving the tree working: network wired page, wifi
section, README, bluetooth module and page, pairing, README, then the
waybar removal and the trap notes.
The spec's failure section is corrected in the same commit: it claimed
forget and disconnect failures would surface inline, but neither backend
exposes a failure signal for them, so that is a ceiling rather than a
feature.
Verified by probe before writing any task: the singletons start empty for
about two seconds, the device lists are ObjectModels, signalStrength is
0..1, scanner and discovering are writable, and NMSettings is not in QML
scope, which drops the hidden-network join the spec had left conditional.
|
|
Two drawer modules covering wired, wifi and bluetooth, using the native
Quickshell.Networking and Quickshell.Bluetooth backends rather than nmcli
or bluetoothctl. The one exception is pairing: quickshell ships no BlueZ
agent and no generic D-Bus module, so a device needing passkey
confirmation is paired through bluetoothctl, whose own agent handles it.
The wifi module became Network so a connected eth0 does not mask the wifi
state; the tile shows both links when both are up because the API exposes
no route metric.
Replaces waybar's network and bluetooth indicators, per the drawer being
the single place they are managed.
|
|
The sound page showed the default sink and source but gave no way to
choose another device and the meter was inert. Each section now lists
the real audio devices, streams and monitor nodes filtered out, behind
an in-place dropdown that sets Pipewire.preferredDefaultAudioSink or
preferredDefaultAudioSource. The meter takes the wheel, click and drag,
and the state glyph beside the percentage toggles mute.
The list expands in place rather than in a popup because a
QtQuick.Controls popup is a separate window and does not position
reliably on a layer surface. The meter accepts the wheel and prevents
stealing so the page's ScrollView neither scrolls nor flicks instead.
|
|
|
|
The README claimed a click outside returns to the grid; it closes the
drawer, and only Escape-with-a-page or the back arrow return to the grid.
The three moved module READMEs' run and IPC commands still named the
deleted volume-osd, mail-overview and vm-manager paths, so point them at
qs -p desktop and ipc call drawer open sound|mail|vm.
|
|
Five components become three, and the Theme symlink count with them. Two
new notes: a pragma Singleton in a subdirectory resolves through a plain
directory import with no qmldir, and Virsh.sampling gates only the stats
poll, not the lifecycle stream that keeps the tile correct while the page
is closed.
|
|
Virsh.qml is unchanged; VmPanel's body becomes the page and the stat row
becomes a Flow, because four stats with 150px bars do not fit a 600px
drawer as one row. The page starts and stops Virsh.sampling, so the 2s
poll runs only while something is looking at it; the lifecycle event
stream still runs always, which is what keeps the tile's dots current.
|
|
The singleton was referenced only inside lazy Components, so its
qtmaildir.conf watcher and unread-count refresh did not start until the
drawer first opened, contradicting alwaysActive and the old MailPanel,
which shell.qml instantiated directly. Naming Accounts at the Module top
level creates it at shell load; the page keeps its own refresh on open.
|
|
Accounts.qml is unchanged and MailPanel's body becomes the page. The
three scripts move with it, which changes the absolute paths in autostart
and in the waybar module; both are outside this repo and listed in the
plan's final task. Button.qml, byte-identical here and in vm-manager,
lands at the shell root as the single copy.
|
|
VolumeOsd.qml did three jobs in one file: PipeWire tracking, the OSD
surface and the player transport. They become Service, Osd and the page.
The OSD keeps its namespace so the existing Hyprland blur rule still
matches, and the service stays always-active because the OSD has to
answer a keypress with no drawer open.
|
|
A tile with no page: appearance stays its own process because a wallpaper
picker needs more room than a 600px drawer, so the tile only fires its
existing IPC. This is the case the contract's activate() exists for.
|
|
Quickshell 0.3.1 IPC requires every declared argument, so the plan's
`ipc call drawer open` with no page fails; the page-less grid entry is
the zero-argument `toggle`. Also records that `... | head` races
timeout and kills the shell early, so verification uses a file redirect.
|
|
The page Loader is anchored to its parent, so the x binding on it was
overridden and the slide never rendered. Drive the motion with a
Translate transform instead, which the anchors do not touch. Opacity
and the back-arrow wiring are unchanged.
|
|
Left of DP-1 because conky holds the right; ExclusionMode.Normal so the
bar the drawer is reached from stays visible and clickable. The top is an
empty Item claiming the space the notification engine will fill, so the
grid already sits where it will sit once that lands.
|
|
The page body scrolls because mail with several accounts and vm with
several VMs both exceed the drawer height; the tile grid deliberately
does not, being fixed at a 3x3 ceiling.
|
|
A module provides a tile, a page, both or neither, plus alwaysActive,
which governs the background service rather than the page: the drawer is
closed most of the time and three of four modules have background work.
|
|
The plan claimed a pragma Singleton outside the config root is invisible
until a qmldir names it, and had Task 1 create one plus Tasks 6, 7 and 8
append a line each. That premise was wrong.
Tested: a pragma Singleton in modules/sub/, reached by a plain
import "modules/sub", resolves with no qmldir anywhere. The control was a
reference to a genuinely undefined type, which warns "ReferenceError:
<name> is not defined"; the singleton case produced no such warning and
the binding evaluated. This matches what AGENTS.md already documented
about Theme.qml, which the code review pointed out.
Four registration steps removed and the remaining steps renumbered. The
AGENTS.md note Task 9 adds now records the tested behaviour rather than
the invented one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
|
|
The window draws nothing and catches nothing; it exists because a
quickshell config with no visible window exits silently, and this
shell's drawer is closed most of the time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
|
|
The paths are written with a tilde because this file is committed and the
repo forbids home paths in committed files. But neither Hyprland's
exec_cmd nor waybar's exec goes through a shell, so a tilde there never
expands and fails silently, which reads as a broken keybind rather than a
bad path. The two rules genuinely conflict here, so the plan now says
which form goes where instead of shipping lines that would not run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
|
|
Ten tasks: skeleton, contract, chrome, drawer, then the four modules in
increasing order of risk, then documentation, then the six config edits
outside this repo.
Appearance goes first among the modules despite being last in the grid,
because it is nine lines of substance and puts something in the grid
before the three migrations start. Sound, mail and vm follow in that
order: sound splits one 302-line file into three, mail is the gentlest
move and carries the only automated oracle in the project, and vm is the
largest and the one whose polling behaviour actually changes.
Each task leaves the repo working, because several delete a component and
a half-finished deletion is painful to unpick.
Paths in the config task are written with a tilde: the gitleaks hook
rejected the absolute form, correctly, since this file is committed and
the live Hyprland config it describes is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
|
|
The vm module's service is lazy, so the tile had no state to show before its
page had been visited. One `virsh list` when the drawer opens fixes that for
the cost of a single call; the per-VM polling for guest-agent stats and
managed-save detection still waits for the page, which is where the expense
actually is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
|
|
Five components mean five processes, five autostart lines, five blur rules,
and functionality scattered across surfaces with nothing in common. Nothing
is discoverable without remembering it exists.
The design is one left-side drawer on DP-1, reached from a launcher at the
left end of waybar, hosting modules as tiles that may open full-height
pages. Modules declare a tile, a page, both or neither, plus whether their
background service runs while the drawer is closed: sound and mail must
(the OSD reacts to keypresses, the unread count outlives the drawer), vm
must not, since polling virsh only paints a page nobody is looking at.
Three components merge in (volume-osd, mail-overview, vm-manager), two stay
separate because a 600px drawer is the wrong shape for them: window-switcher
is a fullscreen ALT+TAB grid, appearance needs room for a wallpaper picker.
Appearance still earns a tile that fires its existing IPC, which is the case
that proves a tile needs no page of its own.
The top of the drawer is reserved and empty. The notification engine is a
later project, but the space has to be claimed now: build the grid anywhere
else and it moves the day notifications land, having been tuned against a
position that does not survive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We9zcHEH8ZWHPcQwMaJRNv
|
|
The query starts one revision after the stored one, so the lower
bound is exclusive.
|
|
The lastmod range is inclusive at both ends and the stored revision
was reused as the next start, so the batch just notified matched
again. Start one revision later.
A subject can contain a newline, which broke the bullet rows and the
+N more count; flatten CR/LF to a space before escaping.
Also reconciles the state-write comment and spec with the actual
skip-and-advance failure behavior, and corrects two doc wordings.
|
|
Why a revision counter rather than a count delta or a date
watermark, why the state file carries a UUID, and why an unseeded
run stays quiet. Also records the final notification presentation:
the split heading, the absolute-path icon, and the show_indicators
setting that keeps the action indicator off.
|
|
dunst here resolves a themed icon name only through icon_path,
which holds no mail icon, so the name rendered nothing and dunst
stored an empty icon_path. An absolute path resolves, which is
what this machine's other notifiers already do.
|
|
The user's dunst format renders %a bold on the top line and %s
italic below it, so the app name now carries "New Mail" and the
summary carries "<account> (<count>)". mail-unread is not in the
active icon theme; mail-unread-multiple is.
|
|
A live test showed the old title ("<label> · N new") read poorly
and the body packed the rows into one block. Title is now
"New Mail - <label> (N)" with the mail-unread icon, and each
message is a bullet on its own line with a blank line between.
+N more counts rows, not physical lines, so the blank lines do
not skew it.
|
|
Asks notmuch what changed since the revision last seen, then sends
one notification per account with mail in that window. Counts are
validated as integers because a rejected notmuch query prints nothing
and exits 1, and an empty string must not read as zero.
dunstify is backgrounded because -b blocks until the notification is
dismissed, which would otherwise stall the loop for the full timeout
on every account. The watch is on the xapian directory rather than a
file inside it, because a commit replaces files and a watch held on a
filename dies with it.
|
|
|
|
Stores the database UUID beside the revision, because notmuch
revisions only compare within one database and a rebuild restarts
the counter. Missing, corrupt or mismatched state reports nothing,
which the caller treats as seed-silently: with no floor, lastmod:0..
matches every unread message and startup becomes a wall of popups.
|
|
Three rows of author and subject, then +N more, which bounds the
height so a mailing list burst is not a wall. Markup characters are
escaped because dunst renders body markup and a subject is untrusted
text off the internet.
|
|
Same source the drawer parses, so adding an account in qtmaildir
notifies with no edit here. Keys run to the closing bracket because
real ones contain dots, and parsing walks lines because a folder
named [Gmail]/Bozze ends a section body early.
|
|
A library guard so the test can source the script for its pure
functions without starting a watch loop, and the check itself,
which is one bash script rather than a framework.
|
|
Six tasks, TDD against one bash check rather than a framework: the
script is sourced as a library so the pure functions, the ones that
only move text around, are asserted without notmuch, dunstify or
inotify in the picture.
The two parsing traps that already cost this component a debugging
session each get a fixture apiece: a key containing dots, and a
folder value containing a bracket.
Verification does not wait for mail. Backdating the stored revision
by 2000 replays a real window on demand, and the seed run before it
proves the anti-storm rule with 101 unread messages on disk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZwWrCEbdKzmisfg9bb1nS
|
|
One notification per account per arriving batch, from a new
mail-notify.sh sibling to waybar-mail.sh.
Arrival is detected with notmuch's lastmod revision counter rather
than a count delta or a date watermark: a commit is not the same as
new mail, since reading and tagging also commit, and date: is the
message's own Date header, so backdated mail would never notify and
future-dated mail would notify forever.
A separate process rather than extending waybar-mail.sh, which
already has the arrival edge: waybar owns that process, so a bar
restart would stop notifications with nothing reporting it.
State carries the database UUID beside the revision, because notmuch
revisions are only comparable within one database. Missing, corrupt
or mismatched state seeds silently: with no floor, lastmod:0..
matches all 101 unread messages here and startup becomes a wall of
popups.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZwWrCEbdKzmisfg9bb1nS
|
|
The watcher already writes ~/.local/state/mail-watcher.heartbeat every
60s, so the drawer reads it and shows a dot: green when idling, yellow
when a folder gave up, red when the heartbeat is stale or missing.
Backoff stays green because it is normal recovery and the watcher's own
health check treats it that way. The staleness rule is reimplemented in
a few lines rather than shelling out to --status; the heartbeat file is
the documented cheap-to-poll surface.
Read once per open rather than watched: the heartbeat is written by
atomic replace, so an inotify watch held on the old inode dies with it.
"Sync now" is removed with its Process. The watcher triggers syncs, the
cron tick is the backstop, and waybar's right-click still runs
mailsync.sh.
|
|
A VM saved rather than shut down restores its memory image on the next
start. When that image cannot be restored the start fails every time with
"unable to execute QEMU command 'migrate-incoming'", and the panel showed
an ordinary "shut off" with a Start button that could never work.
Detection is virsh dominfo grepped for "Managed save: yes", polled per VM
on every list refresh the way the agent rows are. domstats does not report
it: the shut-off reason reads "failed", from the failed start, not from
the save. virsh list --all --managed-save does print "saved" in the state
column, but not together with --name, which is the form the panel lists
with.
The button appears only when a saved image exists, since managedsave-remove
fails without one and would be noise on every other VM. It takes a
confirmation click but not a typed name: it deletes the memory image and
leaves the disk alone, so the cost of a misclick is a cold boot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lo1FG4qTr1inavmhqhfobe
|
|
The component is finished and on the user's ALT+TAB, so what is left is the
part that would otherwise have to be rediscovered. Most of the time this
build took went to three things that report success and then do nothing:
Hyprland 0.56.2 evaluating dispatch arguments as Lua, so the
documented-looking `focuswindow address:0x...` is a silent syntax error; a
focus dispatched while the overlay still holds an exclusive keyboard grab
being accepted and ignored; and a sort on a `focusHistoryID` property that
does not exist, comparing NaN and leaving an arbitrary order that looks
plausible. None of them logs anything.
Those six, plus the missing `0x` prefix, the empty `toplevels` before
`refreshToplevels()` and `ScreencopyView` reporting (-1, -1) rather than
(0, 0), generalise past this component, so they go in AGENTS.md next to the
other notes that exist because a commit message explained itself once. The
card geometry and the blank icon square are specific to the grid and stay in
the component README.
Also corrects the counts that four components made true and five do not: the
component lists, "the other three use ExclusionMode.Ignore", and the Theme
symlink note.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
|
|
The layer surface announced itself as window-switcher, while the other
components all use a quickshell- prefix and the blur rules in
decorations.lua match on it. A rule for the odd one out would have been
the only one in that file matching a bare name, so the component matches
the convention instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
|
|
Picking a window focused nothing, and focus returned to whatever had it
before. The overlay holds keyboard focus exclusively, and the compositor
will not move window focus out from under that grab: the dispatch is
accepted, reports ok, and is then ignored. Nothing in the log says so,
which is why this looked like a dispatch that had worked.
Closing first in the same turn is not enough either, because close() only
clears a property and the layer surface survives until the frame after,
so the dispatch still lands inside the grab. The target is now remembered
and focused once the surface is actually gone.
Tested in both directions, since the case that exposed it was
cross-monitor: DP-1 to a window on DP-3, and back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
|
|
Only the width was clamped, so rows were free to run off the top and
bottom of the screen. A centred Grid has no way to scroll to a card that
lands there, so the cards were unreachable rather than merely cramped: at
19 windows on a 1080-tall screen the fourth row was clipped, and it grew
worse from there.
Cards now shrink to fit the available height as well, which leaves every
count up to 18 exactly as it was, since the width term still wins there.
Thirty windows fit in 995px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
|
|
Columns are the window count capped at six and the grid centres, so one
card sits dead centre and two straddle the middle. Cards grow to fill
their row up to 640px: without a ceiling a lone preview becomes a
full-screen mirror of the window it stands for.
The close button leaves the overlay open, because closing is a side
errand and a dismissal would mean reopening to close a second window.
That makes an empty overlay reachable by closing the last window, so
there is an empty state rather than a full-screen dim with nothing in it,
which reads as a hang.
The selection needs no reset. The LazyLoader destroys the overlay on
close and constructs a new one on the next opening, verified with a build
counter: two openings, two constructions. selectedIndex is therefore 0 on
every showing by initialisation, and an explicit assignment in
Component.onCompleted would be a no-op.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
|