summaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-24 19:22:02 +0200
committerDanilo M. <danix@danix.xyz>2026-08-24 19:22:02 +0200
commite972e9c721d15c13d2be76da9f5dbac7412f547e (patch)
tree3a76d2d5daabe3f3aa9fd4b5db058345203df224 /docs/superpowers
parent94462ae2cc68d563f883b29f1812f93d6b5a6c06 (diff)
downloadqtmaildir-e972e9c721d15c13d2be76da9f5dbac7412f547e.tar.gz
qtmaildir-e972e9c721d15c13d2be76da9f5dbac7412f547e.zip
docs(specs): design signatures, item 152
One markdown file per signature under ~/.config/qtmaildir/signatures/, spliced into the composer's buffer and switched from a control on the editor bar. MessageBuilder needs no change: it already builds text/plain from markdownBody verbatim and text/html from MarkdownRenderer over the same string, so one markdown signature in the buffer yields both forms correctly. That is the "transparent to the user" requirement the note asked for, and it is why a two-file text/HTML variant was dropped after being chosen: it buys designed HTML signatures at the cost of the signature no longer being visible while composing. The switch stays stateless. seedBody() deliberately refuses to track "my text" and "the quote" as separate pieces, and a toggle cannot duck that question the way the quote did; it answers it by scanning for the last "-- " block not followed by quoted lines, so nothing can desync from the undo stack. That same scan is what lets signature_position offer both end (the default) and above_quote over one implementation. The per-account key does not reopen the note's constraint: an account seeds the choice, the switch keeps every signature reachable, and the automatic follow stops once the user picks one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEcn3u19xPqv6ggD15PG4c
Diffstat (limited to 'docs/superpowers')
-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.