diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-04 11:36:16 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-04 12:54:54 +0200 |
| commit | 0a160651cfb9a0f580bcc446941058a339e55643 (patch) | |
| tree | 911d60baf92426f5f32a662d5e9ebf6e72ae0cf2 /docs/superpowers | |
| parent | 4574a6e8d1253c0a70972f0c6e4d13d528ea6420 (diff) | |
| download | qtmaildir-0a160651cfb9a0f580bcc446941058a339e55643.tar.gz qtmaildir-0a160651cfb9a0f580bcc446941058a339e55643.zip | |
feat(tags): add an Edit tags dialog on Ctrl+T
Five hardcoded tags were the only ones reachable from the UI: archive,
delete, spam, flag and toggle_unread. For an application whose purpose is
organising mail by tag, applying any other one meant leaving for a
terminal. Item 26 of the usability backlog, raised by the user asking how
to add a tag and finding they could not.
One dialog rather than separate add and remove actions, at the user's
choice: filing something under a new tag while dropping inbox is one
thought, not two. Type tags to add or remove, comma separated, or clear a
checkbox to drop a tag already on the selection without retyping its
name.
Both fields complete against the tag list MainWindow already holds for
the query completer. Completion is a guard against typing shoppping
beside shopping, never a whitelist: inventing a tag is the entire point,
so any valid name goes through whether or not it exists yet.
Tri-state checkboxes carry the multi-thread case, and are where the risk
is. A tag on some selected threads shows partially checked, and leaving
it alone changes nothing; the opposite reading would silently tag threads
the user never looked at. A tag already on every thread and left checked
is likewise not a change and is not sent as one.
Tag names are validated before anything is applied, through a free
function so the rules are testable on their own. Empty, a leading dash
(notmuch's CLI reads it as removal, making such a tag a trap), whitespace
and control characters are refused by name and reason. Nothing is applied
until the whole set passes, since the user cannot tell which half of a
partial change landed.
TagDialog is pure UI: handed the vocabulary and the current state,
returning two lists, contacting no worker. That is what lets its fifteen
tests run without a notmuch database. Integration is a single call to the
existing tagSelected(), so undo, the optimistic model update, the
combined multi-row query and the completer refresh for a brand-new tag
all come for free.
One test assumption was wrong and the code was right: a case asserted
that QStringLiteral("null\0byte") truncates at the null and reads as
empty. It does not, so the null is caught as a control character. The
test was corrected rather than the validator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 34 |
1 files changed, 33 insertions, 1 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 6fd40c1..4df48dd 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 @@ -64,7 +64,7 @@ taking that too literally. | 23 | No way to save a search query from the UI | workflow | M | open | | 24 | No right-click actions on the thread list | discoverability | S | open | | 25 | No select-all, and bulk actions are undiscoverable | workflow | S | open | -| 26 | No way to add or remove an arbitrary tag from the UI | workflow | S | open | +| 26 | No way to add or remove an arbitrary tag from the UI | workflow | S | **done** | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -1160,6 +1160,38 @@ dialog and two actions, not new plumbing. refreshes the list when a mutation introduces an unknown tag, so that path is in place and should be relied on rather than duplicated. +### Outcome (done) + +Built as the user chose: one **Edit tags** dialog on `Ctrl+T` rather than +separate add and remove actions, since filing something under a new tag while +dropping `inbox` is one thought. + +`TagDialog` is pure UI in `qtmaildir_lib`. It is handed the vocabulary and the +selection's current tags and returns two lists; it contacts no worker and holds +no database handle, which is what lets fifteen tests run without a notmuch +database. Integration is one call to the existing `tagSelected()`, so undo, the +optimistic model update, the one-query multi-row resolution and the completer +refresh all come for free. + +**Tri-state is the part that needed the tests.** With several threads selected a +tag can be on some, and `PartiallyChecked` means "leave alone" rather than +"apply to all". The opposite reading silently tags threads the user never +looked at. `m_fullyTagged` exists for the neighbouring case: a tag already on +every thread and left checked is not a change and must not be sent as one. + +**Validation is a free function** so the rules are testable directly. It rejects +empty, a leading `-` (notmuch's CLI reads that as removal, so such a tag is a +trap), whitespace, and control characters. Nothing is applied until every name +passes, since a half-applied change is worse than none: the user cannot tell +which half landed. + +**One test assumption was wrong and the code was right.** A case asserted that +`QStringLiteral("null\0byte")` truncates at the null and reads as Empty. It +does not; the literal is kept whole, so the null is caught as a control +character. The test was corrected, not the validator. + +Rendered and inspected rather than only asserted. + --- ## Deferred, unsized, or split out |
