| Age | Commit message (Collapse) | Author | Files | Lines |
|
The four copies had already drifted, which is the argument for doing this
now rather than when a palette change has to be made four times and lands
in three of them. vm-manager was missing surfaceAlt; volume-osd was missing
green, yellow and surfaceAlt. The shared file is the superset, so the two
thin ones gain properties they never had and nothing loses one.
A symlink rather than a shared import path: a singleton outside the config
directory needs a qmldir beside it, the same friction that already keeps
the generated palette parsed rather than imported. Quickshell follows the
link and resolves the singleton with no qmldir, so no consumer changes and
no component stops running standalone under qs -p.
All four load clean with Configuration Loaded and no QML resolution error.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvHeJEXJzdfAaCCYqURAWp
|
|
Four of the notes generalise beyond this component. Two were measured while
building it: notmuch fails two different ways and only the rejected-query case
is detectable, the misparsed-query case returning a plausible wrong number
with exit 0; and notmuch deduplicates by message id, so one message with two
file paths is counted twice by a path glob and per-account counts then sum
above the total.
Two more were bugs found only by running the thing. Xapian replaces files on
commit, so a watch on a filename dies with it and the watch belongs on the
directory. And an INI section body is not everything up to the next bracket,
because values can contain brackets themselves: folders named [Gmail]/Bozze
ended a section before its label and three of five accounts quietly displayed
their raw key instead.
The Blur section gains the exclusion-zone note with its measured geometry,
since mail-overview is the first panel here that sits below waybar rather than
over it. The "Both" phrasing that assumed two components is now plural.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Records the decisions a later reader would otherwise reverse, each with the
measurement behind it: counting by the account tag rather than the path glob
qtmaildir itself uses, because notmuch deduplicates by message id and a glob
double-counts a message that arrived at two addresses; validating notmuch
output as an integer, because a rejected query prints nothing while a
misparsed one returns a plausible wrong number; and parsing the config by
walking lines, because account folders named like [Gmail]/Bozze end a
bracket-delimited section body before its label.
Also notes the one property that differs from the other two panels,
exclusionMode Normal, with the measured geometry showing the overlay starting
exactly where waybar ends.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Builds the panel body on the house idiom the other two components already
use: a fullscreen transparent PanelWindow holding a dim backdrop, with the
real content as a Rectangle anchored inside it. The idiom is worth the
seemingly oversized window because it hands us two behaviours for free.
Click-outside-to-close is just a MouseArea filling the backdrop, and the
drawer only needs its own MouseArea to swallow clicks so they do not fall
through. Escape works because the window actually covers the screen and can
take exclusive keyboard focus.
exclusionMode is Normal here, unlike the Ignore the other two components set.
Waybar claims an exclusive zone at the top of this screen, and the mail icon
lives in it, so respecting that zone positions the drawer immediately below
the bar without this file hardcoding, or even knowing, the bar's height. It
also means the backdrop begins below waybar, so the bar itself does not dim
while its own drawer is open, which reads correctly: the icon you clicked
stays lit.
Focus sits on an inner Item with focus: true, not on the window. Setting
WlrLayershell.keyboardFocus is necessary but not sufficient, because key
events in QML are delivered to a focused item and never to a window, so
Keys.onEscapePressed on the PanelWindow would never fire. This cost time in
the vm-manager panel and the same shape is used here deliberately.
Thread rows are deliberately read-only, with no click handler. qtmaildir
takes no arguments, so there is no way to ask it to open a particular thread
and a clickable row would promise navigation it cannot deliver. The rows are
a preview; the button opens the client at its own idea of where to start.
show() refreshes before opening rather than relying on a timer. The panel is
opened rarely and the shell is autostarted for the whole session, so the
counts are almost always stale by the time anyone looks at them, and a
refresh on open is both cheaper and fresher than polling.
Also removes the temporary console probe from shell.qml, which existed only
to verify the account model parsed and counted correctly. The drawer now
displays the same data, so the probe has no job left. Dropping it also drops
the QtQuick import, which nothing else in that file needed.
Button.qml is an unmodified copy of vm-manager's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Two bugs, both found by actually running the shell rather than reading the
code.
shell.qml used Connections without importing QtQuick, so the config failed to
load outright: "Connections is not a type". That one was loud, and it only
affected the temporary verification probe.
The parser was the real bug and it was quiet. Section bodies were matched as
"everything up to the next [", which is wrong for this file: accounts whose
folders are named like [Gmail]/Bozze end their body at that bracket, before
the label line is reached. Three of the five accounts therefore fell back to
displaying their raw key, and nothing reported a problem because falling back
is a legitimate path for an account that genuinely has no label.
Replaced with a line walk. It says what it means, its only state is which
section is open, and it was checked against the real config before being
trusted: all five accounts now resolve both label and colour. An attempted
fix with a lazy quantifier and a lookahead made things worse, dropping every
label, which is the argument for the boring version.
Verified in a running shell: five accounts in config order with their labels,
counts summing exactly to the global notmuch count, no unknowns, and the
config stays loaded for the full timeout rather than exiting silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Accounts.qml is the data layer for the mail drawer: it parses accounts
from qtmaildir.conf rather than hardcoding them, so adding an account
there needs no edit here. The key runs to the closing bracket, not the
first dot, because at least one configured key contains a dot of its
own; matching only up to the first dot would silently truncate it and
drop that account's mail from every count.
Counting uses the account-<key> notmuch tag, not a path glob. notmuch
deduplicates by message id, so a message delivered to two of the
configured addresses is one message with two file paths: a path glob
would count it under both accounts and the per-account rows would sum
higher than the global total the waybar icon already shows (measured
elsewhere as 102 vs 101). The tag is a property of the message, so it
is singular and the rows always sum to the header exactly.
An unknown count is -1, never 0. notmuch has one failure mode with no
distinct exit status: a rejected query fragment can still return 0
exit and empty output, and an empty string must not become a zero,
which would render as "no unread mail" instead of "count unavailable".
Output is validated as a non-negative integer before being trusted.
Includes a temporary console-log probe in shell.qml, added
deliberately for runtime verification of this model outside the
drawer UI (Task 4). It stays until the user has run and confirmed it
against real notmuch output, then gets removed in a follow-up commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
The user works at this machine and runs the shell themselves. An agent
executing this plan writes code and runs non-GUI checks, then hands over the
command and the numbers to compare against; the four steps that need a running
qs are now marked as the user's.
Also warns against a bare pkill -x qs, which would take down the three
components the user already has running.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Nothing is drawn yet. This commit exists on its own because the thing most
likely to be wrong at this stage is invisible: a config whose only window is
hidden exits straight after logging Configuration Loaded, reporting no error,
and the symptom is a keybind that appears to do nothing.
Theme.qml is a verbatim copy of the one in appearance. It is a fallback for
before the generated palette is read, not a palette to grow; deduplicating
the four copies is a separate change.
Not yet run: the user runs the shell themselves, so loading and IPC are
verified in their session rather than here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
The previous commit, the spec and the plan all said notmuch exits 0 on a
malformed query while printing something that is not a count, and that
validating the output as an integer therefore catches it. Measured properly,
that is wrong in a way worth recording, because the truth is worse.
notmuch fails two different ways. A rejected query prints nothing and exits 1:
`notmuch count 'tag:unread and ('`. A query Xapian merely misparses returns a
plausible wrong number and exits 0: `notmuch count 'tag:unread and (('` gives
41, and `'tag:unread and tag:'` gives 3. The second is undetectable by any
check on the output, which is why the original claim was not just imprecise
but inverted: the case it described as caught is the case nothing can catch.
The integer validation still earns its place, on the first failure mode, where
empty output would otherwise render as an empty inbox. The real defence
against the second is that QUERY is a fixed string and is never built from
anything, which the comment now says.
The earlier measurement that produced the wrong claim read 40 as mangled
output when it was a successful parse answering a different question.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Prints one JSON line per notmuch commit rather than polling on an interval,
so the count drops the moment mail is read in qtmaildir and rises the moment
mbsync commits, and waybar owns the watcher process: nothing to supervise on
a machine with no systemd.
The watch is on the xapian directory, not on a file inside it, because a
commit replaces files and a watch held on a filename dies with it. The short
sleep coalesces the several writes of one commit into one redraw.
notmuch exits 0 even for a malformed query, printing something that is not a
count, so the output is validated as an integer rather than trusting the exit
status. A failure there renders as an error glyph: a count that silently
reads zero would look exactly like an empty inbox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Seven tasks, ordered so the thing most likely to be wrong is checked first.
The watcher script comes before any QML because it is the only piece with a
real runnable check and needs no compositor. The component skeleton gets its
own commit because the failure it guards against is invisible: a config whose
only window is hidden exits straight after logging Configuration Loaded and
reports no error.
Two decisions changed while reading the existing components. The drawer is a
fullscreen overlay with content anchored top right, which is the idiom both
other panels use and what gives click-outside and Escape for free, rather
than a small anchored window. And its exclusionMode is Normal rather than the
Ignore those two use, so waybar's exclusive zone places it below the bar
without this component carrying the bar's 42px height as a constant to drift.
Also records a trap measured this session: notmuch exits 0 even for a
malformed query, printing something that is not a count, so every count is
validated as an integer rather than trusting the exit status. A failure that
rendered as 0 would look exactly like an empty inbox.
The live-config task needs real absolute paths, because waybar's exec and
Hyprland's Lua strings do not expand a tilde. They are written as <repo> and
<home> placeholders with a note to substitute, since the gitleaks hook blocks
committed home paths and was right to reject the first attempt.
The spec was updated to match both changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
qtmaildir scopes an account with a path glob, so the obvious move was to copy
that query. It is wrong for this panel. notmuch deduplicates by message id, so
a message that arrived at two configured addresses is one message with two
paths, and a path glob counts it under both accounts: the five accounts here
sum to 102 against a global total of 101.
The icon shows one global total and the panel header repeats it, so rows that
sum to a different number would look like a bug. Counting by the account tag,
which is a property of the message and therefore singular, makes the rows sum
to the header. The cost is that a cross posted message appears under only the
account the post-new hook attributed it to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Replaces three waybar modules that each poll the Gmail API through a python
script with its own credentials file, cover three of five accounts, and open
Thunderbird. Everything they fetch over the network is already in the local
notmuch index, tagged per account.
Two decisions carry most of the design. Counts are scoped to
"tag:unread and tag:inbox" rather than plain "tag:unread", which for one
account is the difference between 32 and 41, because the inbox number is the
one that means new mail worth looking at. And the waybar module runs in
continuous mode with an inotifywait loop, so waybar owns the watcher process
and there is no daemon to supervise on a machine with no systemd; the watch is
on the xapian directory rather than on named files because a commit replaces
them, which breaks a watch held on a file.
The account list is not in the component. qtmaildir.conf already has one
[account.<key>] section per account, where <key> is the notmuch tag suffix,
carrying a display label and a colour, so the panel parses that and a new
account appears without touching QML.
Thread level actions are out of scope because they are blocked rather than
deferred: qtmaildir takes no arguments, so nothing can tell it which thread
to open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU
|
|
Both binaries ship and qs is the one that runs, so pkill -x quickshell
matches nothing and exits successfully. Every restart during development
therefore left the old instance running, and 47 accumulated before the
user noticed. The same mistake produced pgrep readings of zero that were
twice taken for the shell having exited, and sent one debugging session
after a bug that did not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
Both scans ran once at startup. This shell is autostarted, so that is
once per session: a scheme or a wallpaper added afterwards never
appeared, and restarting the shell was the only way to see it. Six
schemes added during a session were invisible for exactly this reason.
Opening the panel now refreshes both. Verified by adding a palette pair
to a running shell and watching it appear, with its colours resolved,
after the refresh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
One drawer for how the desktop looks, on SUPER+Return, replacing the
qarma file dialog that key used to open. wallp is untouched and still
does the work: the panel calls it with H= and V= and it still runs from
a terminal.
Clicking a thumbnail stages it rather than setting it, so both screens
can be composed before anything changes and Apply is a single wallp
call. The mock screens show the result: the staged pick where there is
one, what is set where there is not, and the hovered thumbnail on the
targeted screen. They are drawn at the real proportions from hyprctl
monitors, DP-3 upright because it has transform=1, centred against each
other as Hyprland has them.
The scheme tab reads the palette pair unified-desktop-theme keeps for
each scheme, so the swatches are that scheme's real colours and the list
needs no edit when a scheme is added. Applying writes roles.conf and
runs install.sh, and reloads nothing, because install.sh reloads
nothing: the panel says what is still showing the old scheme rather than
pretending the switch is done.
Two QML traps are documented in AGENTS.md because neither announces
itself: there is no String.matchAll, which threw inside a try and left
every swatch empty, and assigning running = true to an already-running
Process does nothing, which stopped the scheme loader after the first
file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
Records what cost time rather than what the code already says: the
no-visible-window exit that broke both components, the two measurement
traps that made it look like a code bug, and the per-component findings
that generalise to a third one.
CLAUDE.md points at it, matching unified-desktop-theme, so there is one
source of truth per repo rather than one per agent tool.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
Two bugs that both looked like the panel ignoring input.
Escape did nothing despite the panel saying it closes. Keys.onEscapePressed
was on the PanelWindow, and key events go to a focused item rather than to
a window, so it never fired. A filled Item with focus: true now catches it
and reclaims focus when a delete confirmation's TextInput releases it.
The shell also exited whenever the drawer was closed, which is most of the
time: with the LazyLoader inactive there is no visible window, and
quickshell quits at that point. Since the keybind reaches the panel over
IPC, that left the key talking to nothing. Same 1x1 transparent keepalive
window as volume-osd, for the same reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
Quickshell exits once no window is visible. The OSD is hidden between
changes, and with no MPRIS player running there is nothing else to hold
it open, so the process loaded cleanly, logged no error and quit. The
symptom was a panel that never appeared: not a blank panel, a dead shell.
A 1x1 transparent window with an empty mask keeps it running. The mask
makes it click-through, so it cannot catch a stray click, and it draws
nothing.
This window existed earlier and was removed after a process check that
was measuring the wrong thing, which made it look unnecessary. The
README now says why it is there, so it does not get removed a third time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
The OSD gains a track row above the volume bar when an MPRIS player is
running: album art, title, artist, and prev/play/next. With no player it
is the volume OSD it was, at the same size. A track change or a
play/pause shows the panel as well, so the row is not something you only
see by happening to touch the volume.
Hovering freezes the fade so the buttons can be clicked and leaving
restarts it, which keeps the passive behaviour intact for anyone not
reaching for the mouse. The panel still takes no keyboard focus.
playerctld proxies whichever player is active and republishes it under
its own bus name, so every player is enumerated twice. Player.qml drops
that name rather than deduplicating by identity, and controls the real
player, which works whether or not playerctld is running.
Album art is loaded with cache off and a per-track suffix on the URL.
Audacious extracts embedded art into one temp file in its cache and
rewrites that same path for each track, so the URL repeats while the
image changes, and a cached Image would keep showing the last cover.
Theme gains overlay, used by the art placeholder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
Both components had their own copy of the Macchiato colours, and the
second copy appeared the moment there was a second component. Editing the
palette meant editing it in as many places as there were shells, and they
would have drifted.
They now read ~/.cache/wal/udt-palette.qml, which udt-accent generates
from unified-desktop-theme's palette.rasi. The file is watched, so a
palette change reaches a running shell in place, the same way the accent
already did.
What is left in Theme.qml is a fallback, used before the file has been
read and on a machine where unified-desktop-theme is not installed, which
is what keeps each directory runnable on its own.
The file is parsed rather than imported: a generated QML singleton cannot
be imported without a qmldir beside it, and the wal cache has no reason
to carry one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
A top drawer on the secondary monitor showing every libvirt machine: its
state, live statistics while it runs, the actions that make sense in that
state, and the snapshot list. SUPER+v toggles it over IPC.
This replaces the menu half of rofi-qemu.sh, which is still in ~/bin but
no longer bound. A dmenu row can only ever be a string, so the script
could not show what a machine is actually doing, and the snapshots it
could create were never listed anywhere.
Everything shells out to virsh, the same commands the script used, so
there is no libvirt binding and no new dependency. State changes arrive
by `virsh event --all --loop` rather than polling, which means a machine
started from virt-manager or the CLI updates the drawer too. Statistics
do need a 2 second timer, but it only runs while the drawer is open.
Memory, filesystem usage and the address come from qemu-guest-agent, so
they are absent for the first seconds after boot and on any machine
without the agent. Those rows show a dash rather than falling back to
libvirt's own figures: balloon.current is memory allocated rather than
used and reads full forever, and block.allocation is qcow2 growth on the
host rather than usage inside the guest. Either would be quietly wrong.
Destructive actions are gated, which the rofi version did not do at all:
Delete VM requires the name typed, since it runs undefine with
--remove-all-storage and erases the disk image, and reset, force stop,
snapshot revert and snapshot delete each take a confirmation click.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|
|
Scaffold the repo that will hold quickshell implementations, each in its
own directory with its own README, and add the first one.
volume-osd shows an on-screen display for output and input volume. It
watches PipeWire through Quickshell's native service rather than polling
wpctl, so it appears for any volume change, not only for the keybinds:
pavucontrol, a per-application slider, or a phone acting as a remote.
One widget serves both directions, showing whichever device moved last.
Colours follow unified-desktop-theme: Catppuccin Macchiato fixed, with
the accent read from the file udt-accent already writes and watched, so
it tracks the wallpaper. Lavender is the fallback, which keeps the
directory runnable where that file does not exist.
The frosting is the compositor's: the panel draws translucent and
Hyprland blurs behind it via a layerrule matched on the window's
namespace. That avoids a MultiEffect and an offscreen blur pass in QML,
and degrades to flat translucent without the rule.
Two details are documented in the component README because both were
wrong before they were right: PwObjectTracker is required or the volume
reads stale, and a node reports its initial volume before `ready` goes
true, so the ready check alone suppresses the startup values. An extra
guard on top of it ate the user's first keypress instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
|