aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md57
-rw-r--r--docs/superpowers/specs/2026-08-24-signatures-design.md260
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.