summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-04 11:36:16 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:54:54 +0200
commit0a160651cfb9a0f580bcc446941058a339e55643 (patch)
tree911d60baf92426f5f32a662d5e9ebf6e72ae0cf2 /README.md
parent4574a6e8d1253c0a70972f0c6e4d13d528ea6420 (diff)
downloadqtmaildir-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 'README.md')
-rw-r--r--README.md22
1 files changed, 22 insertions, 0 deletions
diff --git a/README.md b/README.md
index a52a211..ec4c22a 100644
--- a/README.md
+++ b/README.md
@@ -248,6 +248,27 @@ an attachment, so it is visible without opening the thread. It comes from the
`attachment` tag notmuch applies while indexing, not from parsing the message,
and costs no extra query.
+## Tagging
+
+Archive, delete, spam, flag and toggle-unread write fixed tags. For anything
+else, **Ctrl+T** opens a dialog over the selected threads: type tags to add or
+remove, separated by commas, or clear a checkbox to drop a tag already present.
+
+Both fields complete against every tag in your database, which is a guard
+against typing `shoppping` beside `shopping`, not a restriction: a tag that does
+not exist yet is exactly what the dialog is for, so any valid name is accepted.
+
+With several threads selected, a tag on only some of them shows a partially
+checked box saying how many. **Leaving it alone changes nothing.** Check it to
+apply it to all, clear it to remove it from all.
+
+Tag names are rejected if empty, if they start with `-` (notmuch reads that as
+"remove this tag"), or if they contain spaces or unprintable characters. The
+dialog says which name was refused and why, and applies nothing until the whole
+set is valid.
+
+Every change goes on the undo stack, so `Ctrl+Z` reverses a mistyped tag.
+
## Unsynced changes
Tagging changes the notmuch index at once, but the mail store only learns about
@@ -311,6 +332,7 @@ Defaults, all rebindable through `[keys]`:
| `Ctrl+Space` | `complete_query` | Focus the query bar and offer completions |
| `Ctrl+H` | `toggle_html` | Switch the thread between HTML and plain text |
| `Ctrl+M` | `load_remote` | Load remote images for the current thread |
+| `Ctrl+T` | `edit_tags` | Add or remove any tag on the selected threads |
| `Ctrl+Shift+D` | `message_details` | Show the full headers of every message in the thread |
| `Ctrl+Z` | `undo` | Undo the last tag change |
| `Ctrl+G` | `sync` | Run the configured sync command |