diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-15 11:13:19 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-15 11:13:19 +0200 |
| commit | 8ea004e67427693139ddd55384c7963d856650f0 (patch) | |
| tree | 80a0366d74cb98b5499abeb9209a689d8b834292 | |
| parent | b1a7339120385f3fd8f1f5251ec20e3f0ff94b22 (diff) | |
| download | qtmaildir-8ea004e67427693139ddd55384c7963d856650f0.tar.gz qtmaildir-8ea004e67427693139ddd55384c7963d856650f0.zip | |
docs: close items 93 and 95, record their traps
Item 93 ships four built-in filters composing with the account dropdown, and
absorbs item 90. Item 95 is the overflow-menu defect found while hand testing
it: pre-existing, not caused by 93, and recorded as its own item rather than
folded in.
Two traps go to CLAUDE.md because they are still true of the code. Qt emits no
triggered for a QAction owning a menu, which is why every entry in the saved
query menu had always been inert. And a generator must be asked for one
account's query rather than having its all-accounts query wrapped, since the
wrap returns the right rows by accident of path: being hierarchical and a
row-count test cannot tell the two apart.
A third goes beside the existing rendering-probe warnings: visualRect reports a
real height for a row scrolled out of the viewport, so a non-empty check passes
while the pixel loop measures nothing and blames the wrong defect.
The Upgrading note covers what a user sees: the row starts with four shipped
buttons, a duplicate name means one of theirs is now beside a built-in, and
their migrated Sent entry is unpinned for them rather than deleted.
| -rw-r--r-- | CHANGELOG.md | 42 | ||||
| -rw-r--r-- | CLAUDE.md | 28 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 95 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 41 |
4 files changed, 167 insertions, 39 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 798e8f8..331ce3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,20 @@ point at which they are stable. ### Added +- **Four built-in filters on the query row: Unread, Inbox, Flagged and Sent.** + They are part of the application rather than saved queries you happened to + pin, and they **compose with the account dropdown**: select an account, hit + Unread, and you get that account's unread mail instead of everyone's. With + "All accounts" selected they span every account, as before. + + Changing the account still runs nothing on its own. The dropdown chooses the + scope and the button is what queries, so you can pick an account and then + decide what to look at. + + Your own saved queries are unchanged and keep the behaviour they had: one + that names no account still clears the selection, because a saved query says + exactly what it shows. + - A third right-click search action, **Exclude from search**, wherever the other two are already offered: the message pane's header and body, a tag chip, and every row of the details dialog. It narrows the current query by @@ -34,6 +48,15 @@ point at which they are stable. the thread had been expanded once, after which the identical click rendered a single message. Read a thread by expanding it and walking the reply rows. +### Fixed + +- **A saved query in the "More queries" menu could not be run.** Clicking one + only opened its submenu of Edit and Delete actions. The entry now offers + **Run** at the top of that submenu, above the editing actions. + + This was not new, but it was easy to miss while most queries lived on the row + as buttons rather than in the menu. + ### Changed - The status bar counts threads as they arrive instead of saying "Searching..." @@ -50,6 +73,25 @@ point at which they are stable. ### Upgrading +**The query row now starts with four buttons the application ships**, so your +own pinned queries sit after them. If you had saved queries named Unread, +Inbox, Flagged or Sent, you will see two buttons with the same name: yours and +the built-in one. Right-click yours and choose **Move to menu** to keep the row +readable. + +Your **Sent** button is a special case and is moved for you. Version 0.19.0 +turned the old hardcoded Sent button into a saved query in +`~/.config/qtmaildir/queries.json`; that entry now duplicates the built-in +filter, so it is unpinned automatically on first launch. It keeps its name, it +keeps working, and it is in the **More queries** menu. Nothing is deleted, and +pinning it again restores it if you prefer it there. + +The difference worth knowing: the built-in filters **use** the account +dropdown, while a saved query **sets** it. Selecting an account and clicking +Unread shows that account's unread mail; selecting an account and clicking a +saved query that names no account clears the selection first, as it always +has. + Reading a thread now means expanding it and clicking down its replies. Nothing in your config changes, and no habit built on the reply rows is affected, but the pane will show one message where it used to show a conversation. @@ -265,6 +265,28 @@ a failure or a `-1` count fails against correct code. This was recorded in building the rules. Assert on the positional contract, never on a provoked failure. +**Qt emits no `triggered` for a `QAction` that owns a menu.** Setting a submenu +on an action makes clicking it open that submenu and nothing else, so any +`connect(action, &QAction::triggered, ...)` on the same action is dead code that +compiles, links and never runs. The saved-query overflow menu shipped this way: +every entry carried both a run connection and a submenu of edit actions, and no +entry in that menu had ever been runnable. It went unnoticed because the menu was +the rarely-used half while the user's queries were pinned buttons, and surfaced +only when item 93 moved every query into it. An action that must both run +something and offer actions needs the run as an item INSIDE its submenu. + +**A generator must be asked for one account's query, never handed its +all-accounts query to wrap.** `Config::resolvedQuery(query, accountKey)` exists +for this. Wrapping produces `path:"a/**" and (path:"a/Sent/**" or +path:"b/Sent/**")`, which returns exactly the right rows, because `path:` is +hierarchical and the other account's half cannot match inside `a`. That is why +it is dangerous: a row-count assertion passes against it, so the tests assert on +the generated STRING. The mutation putting the wrap back fails two of them. +Related: an EMPTY query means "match everything" to notmuch, so a generator with +nothing to match returns `Config::matchNothingQuery()` rather than an empty +string. An account that configures no sent folder would otherwise give a button +labelled Sent that shows the whole Maildir. + **Every query this application builds goes through `SearchTerm` (`src/searchterm.h`), and that is what stops five surfaces growing five quoting rules.** It holds no widget, so the grammar is tested without a painter or a web @@ -468,6 +490,12 @@ a defect that did not exist because of these; each was believed until it was con columns, and sometimes with no discernible cause. A probe that reports "no ink anywhere" is far more likely broken than the code it is testing. Check that it finds the thing it expects to find *before* trusting it to report the thing it expects to miss. +- **A row scrolled out of the viewport reports a `visualRect` with a real height**, so a + guard asserting `rect.height() > 0` passes while the pixel loop below it walks zero + rows and reports "0 pixels, the row was painted over". Item 93 hit this by adding four + buttons to the query row: the thread list shrank, and a test sizing its window to 300px + started failing with a message naming a defect that did not exist. Assert the rect is + INSIDE the viewport, not merely non-empty. - **A "saturated pixel" threshold catches antialiased edges of the selection highlight**, hundreds of distinct near-background colours, and will pass whatever the code does. Match the exact colours the model supplies instead. Two versions of one test passed under diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index ba8b99e..e6c7840 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -5101,3 +5101,98 @@ keep resetting the account. Kept in full because the cause, the comment defending the reset, and the rules preview that motivated it are all still true of the code. + +## 93. The query buttons are whatever the user pinned, not a designed set of filters + +**Observed (user, 2026-08-15),** reached by explaining item 90 rather than from +the notes: + +> The queries that show as buttons shouldn't be in the same league as the ones I +> write and store in the "more queries" menu. If I see "Unread" as a button, I +> read it as "filter all my mails and show me only what is not yet read", but +> being a button, in my head it should cooperate with other UI elements. So if a +> dropdown offers to select an account, that same button should work with that +> selection transparently, not fight it. + +**Cause.** Nothing ships as a default. `Config::startupSavedQuery()` falls back +to `m_savedQueries.first()` and a fresh install has an empty query row, so every +button the user has is one they wrote into `[queries]` and that the queries.json +migration pinned (`src/config.cpp:455`). The buttons became "whatever is pinned" +by migration, never by design. The user's own words: "that's the direction I +wanted from the start, we drifted to what is today". + +**Approach.** Four built-in filters, Unread, Inbox, Flagged and Sent, as +generated entries in the closed set `kQueryGenerators` that already exists for +Sent. They compose with the account dropdown; saved queries keep setting the +account from what they stored. The user's own pinned queries are UNPINNED once +the buttons are confirmed working, never deleted, so they fold into the menu and +stay recoverable. + +**Absorbs item 90.** The button that clears the account selection stops being a +saved query at all, so there is nothing left to fix in `runSavedQuery()`. + +**Specified in `specs/2026-08-15-builtin-filters-design.md`.** Three constraints +worth knowing before opening it: a generator must answer PER ACCOUNT rather than +having its all-accounts query wrapped in a scope, or Sent double-scopes and works +only by accident of `path:` being hierarchical; Sent stays `flat` and the other +three do not, so the four match in scope and not in view mode; and changing the +account deliberately runs nothing, since the button is the verb. + +**Size: M.** + +**Closed 2026-08-15, unreleased.** Four built-in filters ship as generated +entries in `kQueryGenerators`, resolved through a new +`Config::resolvedQuery(query, accountKey)` that asks a generator for the +ACCOUNT'S OWN query rather than wrapping its all-accounts one. `runFilter()` +reads the account box and never writes it, which is the whole of item 90; +`runSavedQuery()` still sets the account from what the entry stored, because a +saved query is a destination. + +Three things worth keeping. + +The Sent trap the spec predicted is real and the tests assert on the query +STRING because of it: wrapping gives `path:"a/**" and (path:"a/Sent/**" or +path:"b/Sent/**")`, which returns the right rows because `path:` is +hierarchical, so a row-count assertion passes against the wrong query. The +mutation putting the wrap back fails two tests. + +Migration unpins and never deletes, in both directions: Sent is no longer +migrated out of the INI at all, and a stored entry naming a known generator is +unpinned on load, which is what every install upgraded through 0.19.0 carries. +The user unpinned their own queries by hand, as they had asked to. + +A rendering probe had to be fixed rather than adapted, and it is the documented +failure mode. `replyRowsKeepTheirTextUnderTheThreadLine` sized the window to +300px; four more buttons pushed the reply row below the viewport, the pixel loop +ran zero times, and it reported "0 pixels, the row was painted over", which is a +different defect from the one it exists to catch. It has 600px and a guard +asserting the row is inside the viewport, verified by putting 300 back: it now +names the row at 83..165 in an 82px viewport. + +## 95. A query in the overflow menu cannot be run + +**Observed (user, 2026-08-15),** hand testing item 93: + +> now no entry in that menu is runnable, they all look like submenus and offer +> all options to edit, show as buttons, etc. + +**Cause, and it is NOT item 93's.** `buildSavedQueryRow` gave each menu entry +both a `triggered` connection and a submenu of edit actions. **Qt emits no +`triggered` for an action that owns a menu**: clicking the entry opens the +submenu and does nothing else, so that connection had never fired, in any +release that had an overflow menu. + +It went unnoticed because the menu was the rarely-used half while the user's +queries were pinned buttons. Item 93 moved every query into the menu, which is +how it surfaced, and item 94 makes the menu their only home, so this is now the +path that has to work. + +**Closed 2026-08-15, unreleased.** Running is an item INSIDE the submenu, first +and above a separator, with Edit, the pin toggle and Delete below it. The entry +keeps its submenu, because an unpinned query must not become the one thing that +cannot be edited or deleted. + +The test asserts the Run item exists, that it is first, that triggering it +reaches the query, and that the edit actions survived beside it. Restoring the +old wiring fails it on the first of those and names the Qt behaviour rather than +reporting a wrong query string. 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 884c56b..c02de31 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 @@ -157,7 +157,8 @@ taking that too literally. | 90 | A saved-query button clears the account selection | workflow | S | **folded into 93** 2026-08-15. Not fixed in place: the button that misbehaves stops being a saved query at all. See `specs/2026-08-15-builtin-filters-design.md` | | 91 | Double-clicking a thread could open it in its own window | workflow | ? | open, unspecified; the user marked it "(?) UX not sure" | | 92 | Nothing distinguishes a tag written by a rule from one the user applied | information | ? | open, unspecified; the user asked it as a question, and the answer decides whether it is a display item or a format change across two repos | -| 93 | The query buttons are whatever the user pinned, not a designed set of filters | workflow | M | open, specified 2026-08-15; see `specs/2026-08-15-builtin-filters-design.md`. Absorbs item 90. Four built-in filters that compose with the account dropdown | +| 93 | The query buttons are whatever the user pinned, not a designed set of filters | workflow | M | **done** 2026-08-15, unreleased; see `specs/2026-08-15-builtin-filters-design.md`. Absorbs item 90. Four built-in filters composing with the account dropdown; the user's own queries unpinned, never deleted | +| 95 | A query in the overflow menu cannot be run | defect | XS | **done** 2026-08-15, unreleased. Pre-existing and not caused by 93: the entry's action owned a submenu, and Qt emits no `triggered` for those, so the connection had never fired. Surfaced because 93 moved every query into the menu | | 94 | `pinned` has nothing left to decide once the buttons are built-in | maintenance | S | open; **blocked on 93**, and deliberately not part of it. A user-visible removal: the row becomes built-ins only and every saved query lives in the menu | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -547,44 +548,6 @@ work. - Do not restore the pre-0.16.0 behaviour by making the delay negative for the user. `auto_sync_delay_ms` is theirs to set. -## 93. The query buttons are whatever the user pinned, not a designed set of filters - -**Observed (user, 2026-08-15),** reached by explaining item 90 rather than from -the notes: - -> The queries that show as buttons shouldn't be in the same league as the ones I -> write and store in the "more queries" menu. If I see "Unread" as a button, I -> read it as "filter all my mails and show me only what is not yet read", but -> being a button, in my head it should cooperate with other UI elements. So if a -> dropdown offers to select an account, that same button should work with that -> selection transparently, not fight it. - -**Cause.** Nothing ships as a default. `Config::startupSavedQuery()` falls back -to `m_savedQueries.first()` and a fresh install has an empty query row, so every -button the user has is one they wrote into `[queries]` and that the queries.json -migration pinned (`src/config.cpp:455`). The buttons became "whatever is pinned" -by migration, never by design. The user's own words: "that's the direction I -wanted from the start, we drifted to what is today". - -**Approach.** Four built-in filters, Unread, Inbox, Flagged and Sent, as -generated entries in the closed set `kQueryGenerators` that already exists for -Sent. They compose with the account dropdown; saved queries keep setting the -account from what they stored. The user's own pinned queries are UNPINNED once -the buttons are confirmed working, never deleted, so they fold into the menu and -stay recoverable. - -**Absorbs item 90.** The button that clears the account selection stops being a -saved query at all, so there is nothing left to fix in `runSavedQuery()`. - -**Specified in `specs/2026-08-15-builtin-filters-design.md`.** Three constraints -worth knowing before opening it: a generator must answer PER ACCOUNT rather than -having its all-accounts query wrapped in a scope, or Sent double-scopes and works -only by accident of `path:` being hierarchical; Sent stays `flat` and the other -three do not, so the four match in scope and not in view mode; and changing the -account deliberately runs nothing, since the button is the verb. - -**Size: M.** - ## 94. `pinned` has nothing left to decide once the buttons are built-in **Observed (user, 2026-08-15),** thinking past item 93 rather than from the |
