diff options
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 57 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-08-24-signatures-design.md | 260 |
2 files changed, 316 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 49b9613..c91884e 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 @@ -221,7 +221,7 @@ taking that too literally. | 149 | A reply's cursor lands on the attribution line, not on blank space | defect | XS | **done** 2026-08-24, unreleased, in TWO passes. The first fixed the cursor within each branch (`End` under Above, `Start` under Below) and the user still saw the old layout, because the branches were already right and the DEFAULT was wrong: `above` shipped, and the layout asked for is what `below` produces. Default flipped, and the composer now focuses the body whenever To: is already filled, which a Reply and a Forward always are. Both halves were invisible to the existing `theQuotePositionDecidesWhereTheQuoteLands`, which asserts the quote's position and never the cursor's | | 150 | The receive-only ribbon stays up after the message that raised it is gone | defect | S | **done** 2026-08-24, unreleased. One line in `MessageView::clear()`, beside the blocked-content bar, the stale notice and the attachment bar it already reset by hand. Only `setReceiveOnlyAccount()` hid the ribbon, which every SELECTION change reaches, so a row-to-row move was never the reproducer: it survived the FOUR routes that blank the pane without one (`clear_pane`, `clear_selection`, a new query, a multi-row selection). The first test written for it passed against the defect for exactly that reason | | 151 | The message-pane bars blend into the UI and carry no severity | presentation | S | **done** 2026-08-24, unreleased. Two severities as the user asked: yellow for a warning that only explains (the receive-only ribbon), blue for one offering an action (remote content blocked, stale thread), each with its own light and dark set read off `QPalette::Base` as `HtmlBuilder` does. The blocked row had to become a WIDGET first: it was a bare `QHBoxLayout`, which has nothing to paint a ground on, and its six `hide()` sites then had to move to the wrapper or a painted empty strip would show. Both action bars put the button right of a stretch | -| 152 | Signatures are not managed at all | v2 | ? | open, 2026-08-24, from the notes, and the user added a constraint the same day: a signature is **not tied to an account**, and is switched from a control in the composer's editor bar. That rules out the obvious `[account.*] signature` key as the whole answer. Still unspecified in the rest: where the text is stored, how it interacts with the quote, and whether the HTML part gets its own form | +| 152 | Signatures are not managed at all | v2 | S | **specified** 2026-08-24, unbuilt. Design in `specs/2026-08-24-signatures-design.md`; read that, not this row. Sized `?` until the brainstorm; it is an S. One markdown file per signature under `~/.config/qtmaildir/signatures/`, spliced into the composer buffer, so `MessageBuilder` needs NO change at all: it already derives both parts from one string, which is the transparency the user asked for. The constraint holds, and the per-account key the user then asked for does not break it: an account SEEDS the choice, the editor-bar switch keeps every signature reachable | | 153 | A draft cannot be opened for editing, so it is write-only | defect | M | **done** 2026-08-24, unreleased. `ComposeContextBuilder::forDraft()` reads a draft back into a context; a new `Kind::Draft` seeds the fields verbatim, takes the body with no quote framing, and carries `draftPath` so the autosave REPLACES the file instead of leaving a second copy. `MimeParser` gained `bcc`, which nothing read before: `MessageBuilder` writes Bcc into the draft deliberately, so a resumed draft that ignored it would silently drop every blind recipient. Reachable by double-click and by an `edit_draft` action, gated on the file being in a configured drafts folder because opening ordinary mail this way would make the first autosave DELETE a received message. Found a live defect on the way, see the section | | 154 | No read confirmation | v2 | ? | open, 2026-08-24, from the notes. `Disposition-Notification-To`, which is a header `MessageBuilder` would add and a request the message pane would have to honour or ignore on the receiving side. Unspecified: whether this is send-side only, and what the reader is asked | | 155 | No urgency switch on an outgoing message | v2 | S | open, 2026-08-24, from the notes: low, regular, high. `X-Priority` and `Importance`, headers `MessageBuilder` adds; regular writes neither. A control in the composer, and the same question item 144 answered for the HTML toggle applies to where it sits | @@ -1306,3 +1306,58 @@ production move rather than the test, this stops being a test-hygiene item and becomes a mail-safety one. --- + +## 152. Signatures are not managed at all + +**Observed (user, 2026-08-24, from the notes):** listed under "some basic +functionalities not brainstormed which didn't enter the first Send +implementation", as: + +> signatures: +> - not tied to an account, with a switch in the editor bar UI. + +**Specified 2026-08-24.** The design is in +`docs/superpowers/specs/2026-08-24-signatures-design.md`. Read that rather +than this section, which records only what the brainstorm settled and why. + +**The constraint is the shape of the item, and it survived a second key.** +Not tied to an account rules out `[account.*] signature` as the whole answer. +The user then asked for that key anyway, as a convenience, and it does not +reopen the constraint: the account supplies a STARTING value, the editor-bar +switch keeps every signature reachable under any account, and changing From: +stops re-seeding the moment the user touches the switch. Seeding is not +binding. + +**One choice must serve both forms, and that costs nothing.** +`MessageBuilder` already derives `text/plain` from `markdownBody` verbatim and +`text/html` from `MarkdownRenderer::toHtml()` over the same string +(`messagebuilder.cpp:321-324`). A markdown signature in the buffer therefore +gets both, correctly, with no change to `MessageBuilder` and no second code +path. The user's "transparent to the user" requirement is a property the +pipeline already has. + +A two-file variant (`work.md` plus an optional `work.html` overriding the +rendered form) was chosen and then dropped by the user the same session: it +buys designed HTML signatures at the cost of the signature no longer being +visible in the editor, since the two parts diverge and the buffer can hold +only one of them. + +**The switch is stateless, by using the delimiter rather than tracking a +range.** `seedBody()` deliberately refuses to track "my text" and "the quote" +as separate pieces (`composewindow.cpp:640-644`), and a signature switch is a +toggle by definition, so it cannot duck that question the way the quote did. +It answers it without state: the signature is the last `-- ` block not +followed by quoted lines, found by scanning. Nothing to desync from the undo +stack, and it survives editing above it. + +**`signature_position` covers both placements over one scan.** The user's own +habit is `end`, which is the default; `above_quote` exists because other +clients offer it. The scan needed the quote-aware clause for `above_quote` +anyway, so the key is roughly ten lines rather than one, and a naive tail rule +would have eaten the quote under the other placement. + +**Two things the design refuses.** No signature editor: the directory is +edited with the user's own editor, and a text editor inside a mail client is +not this project's to build. And a resumed draft seeds nothing, because the +saved body already carries whatever signature it was written with, and +seeding again would put a second one on a message written once. diff --git a/docs/superpowers/specs/2026-08-24-signatures-design.md b/docs/superpowers/specs/2026-08-24-signatures-design.md new file mode 100644 index 0000000..3af88a4 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-signatures-design.md @@ -0,0 +1,260 @@ +# Signatures + +Design for backlog item 152. Brainstormed with the user on 2026-08-24. + +**Status:** specified, unbuilt. + +## The problem + +The composer has no signature support at all. Every message is typed from +nothing, and a user who signs their mail retypes the signature or pastes it in +by hand. + +The user's own note (`notes on qtmaildir.md`) states the requirement in one +line and one constraint: + +> signatures: not tied to an account, with a switch in the editor bar UI. + +The constraint is the whole shape of this item. It rules out the obvious +`[account.*] signature` key as the entire answer, because a signature that +belongs to an account cannot be switched per message. + +A second requirement came out of the brainstorm: one choice must serve both +the plain and the HTML form of the message, with the format transparent to the +user. Picking "work" must not also mean picking whether it is text or HTML. + +## What makes this small + +`MessageBuilder` already derives both parts from one string +(`messagebuilder.cpp:321-324`): `text/plain` is `markdownBody` verbatim, and +`text/html` is `MarkdownRenderer::toHtml()` over that same string. + +A signature written in markdown and placed in the composer's buffer therefore +gets both forms for free, correctly, with **no change to `MessageBuilder`**. +The transparency the user asked for is a property the pipeline already has. + +This is why the design stores markdown and splices into the buffer rather than +appending at build time. Two alternatives were considered and dropped: + +- **Separate text and HTML files per signature** (`work.md` + `work.html`, + the second overriding the rendered form). Considered and chosen briefly, + then dropped by the user: "I'm overthinking it, let's drop the html part." + It buys designed HTML signatures (coloured text, table layouts, inline + logos) at the cost of the signature no longer being visible in the editor, + because the two parts diverge and the buffer can only hold one of them. + Nothing the user signs with needs it. +- **Appending at build time** from a key on `OutgoingMessage`. Necessary + under the two-file design, pointless without it, and it makes the signature + invisible while composing. + +## Storage + +``` +~/.config/qtmaildir/signatures/ +├── work.md +├── personal.md +└── short.md +``` + +One file per signature. The filename stem is the name shown in the switch, +listed alphabetically. Content is markdown, like the body. + +- A missing directory is not a misconfiguration. It means no signatures, and + the switch offers only "None". +- A file that cannot be read is skipped with a warning through + `Config::addProblem()`, the same route every other config problem takes. +- No management UI. The directory is edited with the user's own editor. This + is a deliberate stop on the ladder: a signature editor would be a text + editor inside a mail client, and the user already has one. + +The path is fixed rather than configurable, beside `qtmaildir.conf` in +`QStandardPaths::ConfigLocation`. Nothing yet suggests a second location, and +a key can be added later without breaking a file already on disk. + +## Config + +Three keys, all optional. + +| Key | Default | Meaning | +|---|---|---| +| `[compose] signature` | empty | Name seeded when no account override applies | +| `[account.<key>] signature` | unset | Overrides the above, for that account | +| `[compose] signature_position` | `end` | `end` or `above_quote` | + +### The account seeds, it does not bind + +`[account.<key>] signature` does not contradict the user's constraint. The +constraint is that a signature is not *owned by* an account and is switchable +per message; this key supplies only a **starting value**. Every signature stays +reachable from the switch under any account, and switching the From: account +does not restrict the choice. + +The user asked for this key explicitly after the constraint was restated +("D, one more config but it's a nice thing to have"). + +### `signature_position` + +`end` by default, which is the user's own habit ("My usual placement is always +B"). `above_quote` exists because other clients offer the choice, and it is one +enum key over machinery the splice needs anyway (see below). + +It follows `quote_position`'s exact shape (`config.cpp:529-545`): an absent key +is silent and the struct default holds, but a **present and malformed** value +is reported through `addProblem()` and falls back. `value(key, default)` alone +would accept `signature_position = abov` as `above_quote` silently, and this +file already refuses to be the one place that does that. + +### A name matching no file + +Reported as a config problem and seeds nothing. It is not a fatal error: the +composer opens, with no signature, and the switch still works. + +## The switch + +A `QToolButton` with a checkable menu on the composer's editor bar, at the +right end with Attach and Send as HTML. That end is where item 142 put the +controls *of the editor*, as against the formatting buttons on the left, and a +signature choice is one of those. + +Entries are "None" plus one per file, the current one checked. + +A menu rather than a `QComboBox` because the bar's other controls are tool +buttons and a combo would read as a different class of thing. The count is +small and static. + +**Not a `KeyMap` action.** It is parented to the composer, exactly as the +formatting actions are (`composewindow.cpp:424-429`), so its scope is the +composer window, the main window's namespace is untouched, and item 132's +menu-reachability rule does not apply to it. + +## The splice + +One free function, in a new `Signatures` namespace +(`src/signatures.h` / `src/signatures.cpp`): + +```cpp +QString replace(const QString &buffer, const QString &signature, + Position position); +``` + +Stateless. No stored ranges, no tracked insertion point, nothing that can +desync from the undo stack. This is the property that makes the switch safe to +use repeatedly, and it is why the delimiter does the work. + +`ComposeWindow` gets a companion for the directory: + +```cpp +QStringList names(const QString &dir); // stems, sorted +QString text(const QString &dir, const QString &name); +``` + +A namespace of free functions over values, matching `MarkdownFormat`, +`MessageBuilder`, `ComposeContextBuilder` and `DraftStore`. The splice is then +testable with no widget, which is the point of that convention. + +### Finding an existing signature + +Scan for the **last** line equal to `-- ` (dash, dash, space: the RFC 3676 +delimiter) that is **not** followed by a run of quoted lines. From that +delimiter to the end of its block is the existing signature. + +The "not followed by quoted lines" clause is what makes one scan serve both +positions. Under `end` the signature is the buffer's tail and a naive tail rule +would do; under `above_quote` it sits before the quote, and a tail rule would +select the quote and destroy it. The user chose to cover both. + +The quote is recognised as a contiguous run of lines beginning with `>`. That +is a scan of the buffer, not stored state, so it survives editing and undo. + +### Inserting + +- `end`: append to the buffer. +- `above_quote`: insert before the first quoted line; with no quote in the + buffer, this is identical to `end`. A New message under `above_quote` is + therefore not a special case. + +Selecting "None" removes the found block and inserts nothing. + +### Accepted limit + +A buffer in which the user has typed a literal `-- ` line is indistinguishable +from one holding a signature, and the switch will replace from there. This is +the correct reading rather than a defect: that string is the signature +delimiter, and typing it means what it means. + +## Seeding a new composer + +On open, after `seedBody()`, `ComposeWindow` resolves a name: + +``` +account override → [compose] signature → none +``` + +and splices it in. + +### A resumed draft seeds nothing + +The draft branch of `seedBody()` puts the saved body in verbatim, "no +attribution, no quote markers, no blank lines added" +(`composewindow.cpp:627-634`), because a draft is the message itself. That body +already contains whatever signature it was saved with. Seeding again would +append a second one, and the user would find two signatures on a message they +had written once. + +### The seeded signature is not an undo step + +`clearUndoRedoStacks()` already covers the seeded quote for this reason +(`composewindow.cpp:661-665`): one Ctrl+Z on a fresh composer must not wipe +content the user never typed. The signature is seeded before that call and is +covered by it. + +### Changing the From: account + +The signature re-splices when the account changes, **but only while the user +has not touched the switch**. A bool on the window records the first use of the +menu and stops the automatic follow from then on. + +This matches how `send_html` behaves: seeded from context, then left alone +(`composewindow.cpp:606-610`). The rejected alternative was re-seeding +unconditionally, which can silently discard a signature the user picked +deliberately a moment earlier. + +## Testing + +`test_signatures`, no widget, over the namespace: + +- names and text read back from a directory; a missing directory yields empty +- insert into an empty buffer, both positions +- replace an existing signature, both positions +- remove for "None" +- `above_quote` with no quote in the buffer behaves as `end` +- **the delimiter scan against a quoted reply**, which is the case a naive + tail rule gets wrong: assert the quote survives + +`test_composewindow`, for the wiring: + +- seeded from the account override +- seeded from `[compose] signature` when the account has no key +- a resumed draft seeds nothing +- From: follows the account until the switch is used, and stops after +- an unknown name seeds nothing and does not block the composer + +`test_config`: + +- all three keys read +- a malformed `signature_position` is reported and falls back to `end` +- an unknown signature name is reported + +### Not covered, stated rather than faked + +The editor bar's own layout, like item 141's, is a look-at-it property. The +switch's position on the bar is verified by hand. + +## What this does not do + +- **No signature editor.** The directory is edited externally. +- **No per-signature HTML.** Dropped by the user; see "What makes this small". +- **No automatic signature on a draft resume.** By design; the draft carries + its own. +- **No `From:`-address-derived signature** (e.g. one per identity within an + account). Accounts are the only identity this application models. |
