aboutsummaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
AgeCommit message (Collapse)AuthorFilesLines
2026-08-06fix(ui): one Sync control, and a query bar that looks finishedDanilo M.1-3/+13
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.
2026-08-04docs: print the open backlog as a table at session startDanilo M.1-0/+32
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>
2026-08-04docs: record items 28-35 from the user's notes, and how to keep them in syncDanilo M.1-0/+20
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>
2026-08-04docs: record the two selection-model traps found in 987a9e7Danilo M.1-0/+21
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>
2026-08-04feat(sync): bring mailsync.sh here, print to stdout, exit truthfullyDanilo M.1-0/+10
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>
2026-08-04fix(tags): complete every tag in the list, not only the firstDanilo M.1-0/+15
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>
2026-08-04docs: spec query bar completion, require translatable stringsDanilo M.1-0/+6
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.
2026-08-04feat: own the message-pane zoom and persist itDanilo M.1-1/+16
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>
2026-08-04docs: add CLAUDE.md with build commands and architectureDanilo M.1-0/+124
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>