diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-13 10:42:56 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-13 10:42:56 +0200 |
| commit | 2d451e35e95a580d0e81c2e1bf7220d879490864 (patch) | |
| tree | 39e2a03c7757db4eacab5582dff94762c04e8126 /docs/superpowers/plans | |
| parent | c396853a9290314ab657ac4414a21f26dc1fa6d5 (diff) | |
| download | qtmaildir-2d451e35e95a580d0e81c2e1bf7220d879490864.tar.gz qtmaildir-2d451e35e95a580d0e81c2e1bf7220d879490864.zip | |
docs: record items 75-78 and design the rule builder
The standing backlog reconciliation found four unrecorded entries in the
user's notes, all fallout from item 44's rules dialog now that it is in
daily use: the window forgets its geometry and column widths (75), every
field is free text (76), a rule cannot be previewed against the thread
list (77), and there is no way to build a rule from something visible in
a message (78). Each cause is verified in the code rather than copied
from the note.
Item 76 then went through a brainstorming pass and has a design. The
shape is Thunderbird's filter window, which the user supplied as the
reference: field and operator dropdowns, +/- buttons per row, an all/any
radio, and a separate "but not" block.
The structural point is that Thunderbird owns its filter format and this
project does not. The storage is a notmuch query string shared with
mailctl and executed by the post-new hook, so the builder is a view over
a string rather than a store. That decides the rest: the stored format
is untouched and this stays a single-repo change; a query the builder
cannot represent still opens, saves and runs, in a text mode every rule
carries; and the string is rewritten only when the rows actually
changed, compared against the parsed value rather than tracked with a
dirty flag, which Qt sets during programmatic population.
Measured against the seventeen rules in the live store, sixteen are flat
and one nests an or group inside an and chain, which is what the
exclusion block exists for. The parser is strict by design: it
recognises a query whole or rejects it whole, because a lenient parser
that salvages what it understands is how a not clause gets dropped and a
filter silently widens.
Diffstat (limited to 'docs/superpowers/plans')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 26464ba..1216542 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -131,6 +131,10 @@ taking that too literally. | 72 | No khard/khal integration | workflow | ? | open, unspecified; the user places it after send, so v2 at the earliest | | 73 | This backlog is past four thousand lines | maintenance | S | open | | 74 | "Searching..." keeps claiming a query is running while rows are already arriving | feedback | XS | open; cause measured 2026-08-11, the delay itself is the cold page cache and is not fixable here | +| 75 | The tagging rules window forgets its size and its column widths | persistence | S | open; follows item 44 | +| 76 | Every field in the rules dialog is free text, so a rule is easy to get wrong | workflow | M | open; design approved 2026-08-13, see `specs/2026-08-13-rule-builder-design.md` | +| 77 | No way to see what a rule would collect, in the thread list | workflow | S | open; the dialog counts matches, it cannot show them | +| 78 | No way to build a rule from something visible in a message | workflow | M | open; wants 76 first, so the created rule lands in a form that can hold it | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -4722,6 +4726,146 @@ inaccurate, not because anything is expected to happen. here and should not be attempted: prefaulting 1.1 GB at startup to make one query look fast is a worse trade than the wait. +## 75. The tagging rules window forgets its size and its column widths + +**Observed.** The rules window opens at the same size every time, whatever +size it was left at, and the rule table's columns reset to their computed +widths on every open. The user also asks whether it should present as a +primary window rather than a popup. + +**Cause.** `TagRulesDialog::TagRulesDialog` calls `resize(760, 520)` +unconditionally (`src/tagrulesdialog.cpp:59`) and never reads or writes a +saved geometry; there is no `saveGeometry`/`restoreGeometry` pair anywhere in +the file, and `MainWindow` is the only class that touches `uiStatePath()` +(`src/mainwindow.cpp:141,183`). The columns reset because `reloadList()` +calls `resizeColumnToContents` for the enabled and stage columns on every +repopulate (`src/tagrulesdialog.cpp:213-214`), and `onCountsReady` does the +same for the count column (`:326`); a width the user dragged is discarded by +the next reload, not only by a close. + +**Approach.** Save `saveGeometry()` and `m_list->header()->saveState()` into +the machine-written UI state file under keys of their own, and restore both +in the constructor, keeping the current `resize` as the fallback for a first +run. Drop the unconditional `resizeColumnToContents` calls once a saved +header state exists, or the restore is undone on the first reload. + +The "popup or primary window" question is a separate decision and not a +defect: the class is a `QDialog` (`src/tagrulesdialog.h:41`), which is what +makes it modal to the main window and what puts it above it. Changing it to a +top-level window means it can be left open beside the main window and can go +behind it, and the rule edits would then need to survive that. Ask before +changing it. + +**Constraints.** UI state goes to `~/.local/state/qtmaildir/uistate.conf` via +`MainWindow::uiStatePath()`, never into the hand-edited config. A geometry +restore under the offscreen platform is what item 46 already tripped over, so +the test asserts on the saved value rather than on the resulting frame. + +**Size: S.** + +## 76. Every field in the rules dialog is free text, so a rule is easy to get wrong + +**Observed.** A rule is written by typing into four line edits, and the user +would rather choose from buttons, radios and completion, with typing reduced +to the parts that genuinely have to be typed. + +**Cause.** Not a defect, this is what shipped. The form is four bare +`QLineEdit`s for id, add, remove and query plus one `QCheckBox` +(`src/tagrulesdialog.cpp:88-96`), and none of them is attached to a +completer. The tag completion machinery already exists as `QueryCompleter` +(`src/querycompleter.h:90`) and is not used here. + +**Approach.** Designed 2026-08-13. **Read +`specs/2026-08-13-rule-builder-design.md` instead of planning from this +entry.** A `RuleQuery` value type parses and compiles the query string, and a +row builder in the dialog edits it, in the shape the user asked for after +showing Thunderbird's filter window: field and operator dropdowns, `+`/`-` +buttons, an all/any radio, and a separate "but not" block. + +Three constraints decide whether to open the spec at all. **The stored format +does not change**, so this is a single-repo change and mailctl needs no edit. +**The query string stays authoritative**, so a rule the builder cannot +represent still opens, saves and runs, in a text mode that every rule +carries. And **the string is rewritten only when the rows actually changed**, +compared against the parsed value rather than tracked with a dirty flag, +which Qt would set during programmatic population. + +Measured against the seventeen real rules: sixteen are flat, one nests an +`or` group inside an `and` chain, which is what the exclusion block exists +for. + +**Completion is the other half of this item** and is independent of the +builder; it can land before or after. It reuses `QueryCompleter` +(`src/querycompleter.h:90`) for tag names on the add and remove fields. + +**Constraints.** A multi-value field must not use `QLineEdit::setCompleter`. +CLAUDE.md records this trap twice over: the line edit overwrites the +completer's prefix with the widget's whole text, so the first tag completes +and nothing after it does. Attach with `QCompleter::setWidget` and drive the +prefix by hand. And a test that uses `setText()` passes against the bug, +because `setText` never drives a completer at all, so the keys have to be +typed. + +The hook refuses to remove `unread` or `inbox`, so a builder that offers +those as removable tags produces a rule that silently does nothing. Say so in +the UI rather than letting the hook decline it invisibly. + +**Size: M.** + +## 77. No way to see what a rule would collect, in the thread list + +**Observed.** The user wants a button that runs the rule's query in the main +window, to look at what it would collect rather than at how many. + +**Cause.** Not a defect. The dialog already counts matches, over +`requestMessageCounts` and `messageCountsReady` +(`src/notmuchworker.h:172`, `src/mainwindow.cpp:1322-1335`), which answers +"how many" and cannot answer "which". Nothing carries a query from the dialog +back to the query bar. + +**Approach.** One signal from the dialog carrying the rule's query string, +and a slot on `MainWindow` that puts it in the query bar and runs it. The +dialog stays open, since the point is to compare the two. + +**Constraints.** The stored query carries no scope on purpose: the hook +supplies `tag:new` and wraps the rule's query in parentheses. A preview must +therefore run the query WITHOUT `tag:new`, or it shows nothing at all outside +a sync window, and it must not add the parentheses silently either, since +what the user is checking is the query as stored. + +A count request must never bump `m_generation`; item 44 already had to add +`m_ruleCountGeneration` for exactly this reason (`src/mainwindow.h:657-664`). +A preview is a real query and does bump it, which is correct, but it also +means the preview discards whatever thread load was in flight. + +**Size: S.** + +## 78. No way to build a rule from something visible in a message + +**Observed.** The user would like to select an address or another piece of a +message in the main window, right-click, and be offered a rule built from it. + +**Cause.** Not a defect, unbuilt. The thread list has a context menu (item +24) and the message pane is a `QWebEngineView` whose selection is inside the +render process. + +**Approach.** Start from the thread list's own context menu, where the +sender is already a value the model holds, rather than from a text selection +in the web view. A "Create rule from sender" entry that opens the rules +dialog with the query prefilled covers the case the user described and needs +no new plumbing. + +**Constraints.** JavaScript is disabled in the profile and must stay +disabled, so reading a selection out of the web view means +`QWebEnginePage::selectedText()` and nothing that injects script. Do that +part only if the sender case turns out not to be enough. + +The rules file is shared with mailctl, so a rule created here must go through +`TagRules` and preserve unknown fields; see "Changing the shared rule format" +in CLAUDE.md. + +**Size: M.** + ## Deferred, unsized, or split out Items noted while triaging but not part of the original list. Same numbering |
