| Age | Commit message (Collapse) | Author | Files | Lines |
|
The architecture section described ThreadListView as existing to paint a strip
across columns. That was true until this change and is now the opposite of true:
it survives only for the expander hit-test. Kept as one paragraph of history,
since it explains the file's shape, but no longer stated as current behaviour.
Two traps are recorded inverted rather than deleted, because the rule survived
its own reason changing. The reply indent is still asserted on where the TEXT
lands, but where visualRect lies has flipped: it used to report an indent the
text did not have, and now reports none while the text is indented. And Q_ENUM
is documented as insufficient for a queued Q_ARG, which cost a silently dropped
sort argument.
Item 60's recorded cause was wrong and is corrected in place. It was read off
master, where the row arithmetic really is current.row() + 1; the branch had
already fixed it a commit earlier with threadRowOf(). The entry stays, with the
correction, because the reasoning was sound and the tests it demanded now exist.
Items 20, 51 and 53 are marked built on the branch rather than done. Nothing is
merged and the user has not seen it, which is the whole point of Task 10.
|
|
CLAUDE.md described a QTableView over a table model, which has not been true
since the view port. Updated with the traps the port produced, each of which
shipped a plausible-looking broken build before being caught:
- A tree numbers rows per parent, so nothing may be keyed on a row NUMBER.
- drawBranches runs before the row's cells, so an expander on a content
column is painted over by the delegate's background.
- setRootIsDecorated(false) removes the style's HIT AREA along with its
indicator, leaving a glyph that renders and does nothing.
- isExpanded and setExpanded are keyed on column 0.
- A reply's indent must beat the account chip's width, and visualRect
reports the indent correctly even when nothing is visibly indented.
- paintEvent runs after the cells, so a full-row fill erases their text.
Also the notmuch ownership rule, which is a double-free if undone: messages
reached through a thread are freed with it, so walkReplies holds them raw
against this file's own RAII convention.
Item 20 is marked built, not done, and item 53 records why. The user's verdict
on the finished result was that the table view does not fit the use, said with
every cue in and working. That is a design finding rather than a defect: the
item shipped exactly what its four decisions specified, and all four were the
user's own choices. Recording it as a defect would misattribute the cause;
recording nothing would leave the next session building on a rejected design.
Item 53 carries the cause verified in code rather than guessed. A message row
fills the same five columns as a thread row (threadlistmodel.cpp:275-283
mirroring :428-431), so replies land on the same rigid column boundaries as the
threads around them, and the eye reads columns before indentation or tint. The
reference the user gave has no column rules through its reply rows at all, and
that absence is the one thing three added cues cannot supply.
|
|
The repo reached v0.12.0 with fourteen tags and zero GitHub Releases,
because every release had stopped after commit, tag and push. The user
noticed and asked for the backfill, then asked that it not happen again.
Records the whole procedure rather than the missing step alone, since
nothing had written it down: changelog section, version bump, signed
tag, push to both remotes, then `gh release create` with the body taken
from that version's changelog section rather than composed fresh.
Also records that the version choice follows semver on the user-visible
surface pre-1.0, which is why 0.12.0 was minor rather than patch, and
that the stale SlackBuild version is deliberately not part of this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
printf does, and the habit transfers silently. In generated CSS it is
quietly destructive: a percentage written %% reaches the browser malformed,
and a browser drops that one declaration and renders the rest, so the pane
still paints and nothing looks broken. The 0.11.0 placeholder lost its
mask, its glow and both radial gradients this way.
Records the review lesson alongside it, which generalizes further than the
rule: a geometry probe endorsed that layout because it measured only
properties carrying no percentage. A probe that cannot see the thing that
breaks reports success forever.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A session was spent chasing a defect that did not exist, because three
separate rendering probes returned confidently wrong results and each
was believed until contradicted. The specific failure modes are worth
recording, since the next attempt will otherwise rediscover them:
counting lit pixels cannot tell bold from regular in either direction,
viewport()->render() returns blank images in several ordinary
situations, and a "saturated pixel" threshold catches the antialiased
edge of the selection highlight and passes whatever the code does.
The bug that started it was not in the code at all: the desktop's Qt
font was configured Bold in qt6ct, so every row rendered bold and
setBold() changed nothing.
Also records ThreadListView in the architecture diagram, with the three
consequences of painting a row-wide strip in a per-cell view that are
easiest to undo by accident.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Sync had two controls that behaved differently. The QPushButton beside the
query bar cleared the log, opened the pane and disabled itself; the QAction
behind the toolbar, menu and shortcut called start() and did nothing else,
discarding its return value so a rejected start was silent. Worse, item 29's
"disable Sync during a background sync" set the button only, so the toolbar
entry stayed clickable through a cron sync and could only produce the
script's EX_TEMPFAIL skip.
startSync() is now the single handler behind every route in, and the enabled
state lives on the QAction, which reaches the toolbar, the menu and the
shortcut at once. It also reports when no sync command is configured rather
than doing nothing.
The QPushButton is gone. It read as a Search button given it sat beside a
text field, which is the user's own observation and the reason the toolbar
one survives instead. Its unavailable-command tooltip moved to the action,
since that is the only thing that says why the control is dead.
Removing it left the query field running flush to the window edge, so the
saved-query buttons move from their own row onto the query row. The bar is
now framed by the account dropdown on the left and the saved queries on the
right, the empty row is gone, and the thread list gains the space. The field
also gains setClearButtonEnabled, which is Qt's own themed clear icon rather
than a hand-rolled button. A "Search" button was considered and rejected:
Return already runs the query.
No overflow handling for [queries], which is unbounded. Three entries fit;
item 23 already specifies buttons-plus-menu and is where that belongs.
CLAUDE.md's architecture diagram named four widget classes that have never
existed, QueryBar, SavedQueryBar, HeaderWidget and AttachmentBar. The query
row and the message header are built inline. Corrected, and the components
that do exist but were missing from it added.
Tests: the new action test was verified red first and load-bearing by
mutation. The old button test is deleted rather than repointed, being an
exact duplicate of it, and the unobservable-lock test now drives the action.
The clear button and the row layout were confirmed by hand; no test clicks
the icon, which is a mouse path.
Backlog: 45 done and reclassified as a defect rather than a cosmetic
redundancy, 47 added for the bar.
|
|
Extends the reconciliation step rather than adding a second one: after
diffing the user's notes against the backlog, render what is open as a
table with a short description, the size, and one note saying what
decides whether the item can be picked up now.
Then stop. The user picks. Recommending a single item reads as though
the choice were already made.
Defects are flagged apart from enhancements, because they read alike in a
numbered list and do not deserve equal billing: item 28 sat as "a counter
is wrong" while the indicator was quietly lying about whether the user's
work was safe to quit on.
Records two gotchas found by running the grep: item 12 lives in the
deferred table, which has different columns and no size, and a status
cell is prose rather than a keyword, so "open, on demand" and "open,
unspecified" both mean open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The backlog is downstream of the user's own notes, which they add to while
using the app. Comparing the two turned up nine entries with no item here,
two of them defects rather than enhancements:
- Re-adding `unread` after the automatic mark-read counts 2 unsynced
changes when the mail store is back where it started. The counter
counts writes and never decrements, so any add-then-remove of the
same tag inflates it (item 28).
- The Sync button stays enabled while a background sync holds the lock.
That was a written constraint of item 27 and shipped unbuilt, which
is the strongest argument for doing this comparison at all (item 29).
The rest are enhancements: a useful blank right pane, for which the user
has produced the two logo images added here; a default button on the quit
prompt, recorded as needing a repro because the code does set one; Esc to
blank the pane; expiring status messages; a Maildir overview; and an
automatic refresh after a sync.
Item 35 is the one 0.8.0 deliberately did not build. A background sync
reports rather than refreshes, because runCurrentQuery() clears the undo
stack, the selection and the message pane. A non-destructive refresh is
real work, so it is an item rather than a flag.
Item 27 gains its outcome, including that its own proposed approach was
the wrong one: flock -n acquires in order to test, so polling with it
would have caused the exact skips the sync script reports.
CLAUDE.md now carries the reconciliation step, since a note saying "X is
broken" is a bug report that will sit in a personal file indefinitely
unless someone goes looking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
currentRowChanged fires before the selection model updates, and
selectAll() emits it not at all. Both cost real debugging, and the first
caused two distinct faults in one change, so they belong beside the
setCompleter trap rather than only in commit history.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The reference sync script lived in the companion mailctl project, which
documents that it never calls it: sync and organization are separate
there on purpose. qtmaildir is the only thing that runs it
programmatically, and two of its behaviours were live bugs here, so it
belongs in this repo where the constraints on it are.
It printed nothing a caller could see. Everything was redirected to
$LOGFILE, so MailSync captured an empty stream and the sync log pane
stayed blank, a known limitation carried since 0.1.0. It now writes to
both through tee: cron keeps its log, and the pane has something to show.
It also ended in an unconditional exit 0, discarding statuses it had
already computed. qtmaildir believes that exit code: a failed mbsync
reported success, cleared the unsynced-changes count, and would have
allowed a sync-on-exit to quit over a sync that never happened, which is
exactly the case the exit prompt exists to prevent. It now exits with
mbsync's status, or notmuch's when mbsync succeeded.
The statuses move through files rather than shell variables because the
run block is piped into tee, which puts it in a subshell where an
assignment does not survive.
Verified against stub mbsync and notmuch binaries in a fake HOME, so no
mail or database was touched: output reaches stdout, the log still gets
every line, and a failing mbsync or notmuch produces its own exit code
rather than zero.
Both properties are now recorded in CLAUDE.md, since they exist for this
application's sake and an edit that looks like a cleanup would remove
them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Reported by the user: in the Edit tags fields, the first tag suggested
existing tags and the second did not. Typing a comma, a space and a
letter offered nothing.
QLineEdit::setCompleter hands completion to the line edit, which
overwrites the completer's prefix with the widget's ENTIRE text on every
keystroke. These fields hold a comma-separated list, so once one reads
"unread, fl" that whole string is matched against the tag names, nothing
matches, and completion silently stops after the first tag. Confirmed
with a probe: the prefix really is "unread, fl" and the completion count
really is zero.
Attach with setWidget instead, which keeps the popup anchored without
ceding control of the prefix, and drive it from the token under the
cursor on every edit. Setting the prefix from a textEdited handler while
leaving setCompleter in place does NOT work, which was the first attempt:
the line edit sets it again afterwards.
Accepting a candidate needed the same treatment, and is the other half of
the fix. QCompleter's own insertion replaces the whole field, so taking
"flagged" from the popup would have discarded every tag already typed.
replaceCurrentToken() overwrites only the token under the cursor and
keeps the separator's spacing, so the result is "unread, flagged" rather
than "unread,flagged".
This is the same defect QueryCompleter hit in c98b179. Having now cost
two debugging rounds, it is written into CLAUDE.md as a Qt trap rather
than a property of either class, together with the reason a test using
setText() passes against it: setText does not drive a completer at all,
so the keys have to be typed.
Both new tests were confirmed to fail against setCompleter before the fix
was kept.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Design for backlog item 17. Completion covers query prefixes, tag values,
date values, path values and mimetype values, each carrying a description
so the bar documents the query language while it is typed.
A new QueryCompleter class owns it, with the cursor-context tokenizer as a
pure function so the parsing rules are testable without a widget or a
database. NotmuchWorker gains an all-tags call, which did not exist.
Addresses for from:/to: are out of scope: libnotmuch exposes no all-addresses
call. Both prefixes still appear so the vocabulary reads complete.
Account maildirs belong to path:, not folder:. Account::scopedQuery builds
path:"<maildir>/**", and folder: is a different matcher in notmuch, against
the folder name rather than the directory path.
Also records the translatable-strings rule in CLAUDE.md. Existing code is
not yet audited against it.
|
|
Zoom was Chromium's, not the application's: the web view handled the
keys natively and never told anyone, so there was no value to save.
qtmaildir now owns it. Zoom in, out and reset are real actions, in the
View menu and rebindable through [keys], and the factor is persisted to
the UI state file. Ctrl+wheel over the body zooms and Ctrl+middle-click
resets, both filtered by ancestry from an application-level filter: the
events are delivered to an internal QQuickWidget the web view creates
lazily, so a filter on the view itself never sees them.
The factor is clamped to 0.5 - 3.0, and NaN, infinity, zero and negative
values fall back to 1.0, since a corrupt state file must not be able to
leave the pane unreadable with no visible way back.
Both risks the plan flagged turned out not to exist, verified by probe
rather than assumed. The application QAction wins over the web view's
native zoom key, so the tracked factor cannot diverge from what is on
screen. And the factor survives setHtml(), so the web view is the single
source of truth and needs no reapply per render.
A third finding is worth recording because it produced a wrong fix
first. A probe using QTest::keyClick() reported Ctrl++ as a dead
binding, and a test was written asserting that. Both were wrong: Ctrl++
is exactly what the '+' key emits on an Italian layout, confirmed
against the real keyboard, and it is the shipped default. Whether a
symbol needs Shift is a property of the layout, not of Qt, and
keyClick() reproduces neither. The test now only checks that every
default parses, and the comment in defaultBindings() says not to
re-derive this from synthetic input.
Ctrl+= is a second binding for reset, skipped when [keys] gives it to
something else.
Also fixes a pre-existing bug the new config key exposed. [general]
entries were read as "general/<key>", which matches nothing: QSettings'
INI backend treats a section literally named [general] as its own
fallback section and strips the prefix. notmuch_config had therefore
never worked. Both keys are now read without it; the file format is
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Records the non-obvious constraints that require reading several files to
reconstruct: the two-thread split with no notmuch pointer crossing it,
generation counters instead of cancellation, the close-before-reopen
ordering the exclusive write lock forces, and the web view's fail-closed
interceptor contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|