aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/specs/2026-09-13-mail-arrival-notifications-design.md15
-rw-r--r--mail-overview/README.md2
-rwxr-xr-xmail-overview/mail-notify.sh12
-rwxr-xr-xmail-overview/test-mail-notify.sh4
4 files changed, 22 insertions, 11 deletions
diff --git a/docs/superpowers/specs/2026-09-13-mail-arrival-notifications-design.md b/docs/superpowers/specs/2026-09-13-mail-arrival-notifications-design.md
index 93dc13c..1423fe9 100644
--- a/docs/superpowers/specs/2026-09-13-mail-arrival-notifications-design.md
+++ b/docs/superpowers/specs/2026-09-13-mail-arrival-notifications-design.md
@@ -59,12 +59,14 @@ revision is field 3. Verified on notmuch 0.39.
Per tick, if the revision has not advanced, there is nothing to do. Otherwise,
per account:
- notmuch count "tag:unread and tag:inbox and tag:account-<key> and lastmod:<prev>..<cur>"
+ notmuch count "tag:unread and tag:inbox and tag:account-<key> and lastmod:<prev+1>..<cur>"
notmuch search --format=json --limit=3 --sort=newest-first "<same query>"
Two calls, the same count-plus-preview pair `Accounts.qml` already makes, and
only for accounts whose query is non-empty. The count gives the true N for
-"+N more"; the search gives the rows.
+"+N more"; the search gives the rows. The lower bound is exclusive (`prev+1`)
+so the revision just written, which is inclusive at the top end, is not
+re-matched on the next tick.
`search --format=json` returns `authors` and `subject` directly, which is all
the body needs.
@@ -95,9 +97,12 @@ was down is never notified. This is the right trade: the waybar count is still
correct and the drawer still shows the mail, so nothing is lost except a
popup that would have been stale anyway.
-The new revision is written **after** every account has been processed, so a
-failure mid-loop leaves `prev` unchanged and the next tick retries rather than
-dropping a batch silently.
+The new revision is written **after** every account has been processed. A
+single account whose count does not validate is skipped for that tick, and the
+revision still advances: holding it back would make every later tick re-notify
+the successful accounts' whole range. Only a failure to write the state file
+itself is non-fatal and leaves the old revision, so the next tick re-notifies
+rather than dropping mail.
## Accounts
diff --git a/mail-overview/README.md b/mail-overview/README.md
index 6e34140..7465ac1 100644
--- a/mail-overview/README.md
+++ b/mail-overview/README.md
@@ -123,7 +123,7 @@ local index.
## Notifications on arrival
`mail-notify.sh` sends one notification per account when mail lands, with the
-newest three senders and subjects and a `+N more` line when the batch is
+newest three threads' sender and subject and a `+N more` line when the batch is
bigger. Each message is a bullet on its own line with a blank line between,
under a bold `New Mail` heading with the account and count as the second line.
Started from `autostart.lua`, it runs for the whole session.
diff --git a/mail-overview/mail-notify.sh b/mail-overview/mail-notify.sh
index e6c9868..b132775 100755
--- a/mail-overview/mail-notify.sh
+++ b/mail-overview/mail-notify.sh
@@ -25,7 +25,7 @@
#
# Usage:
# mail-notify.sh watch forever (what autostart runs)
-# mail-notify.sh --once process one tick and exit (what the test drives)
+# mail-notify.sh --once process one tick and exit (for manual verification, one tick)
set -u
@@ -104,6 +104,7 @@ build_body() {
rowtext="$(printf '%s' "$json" | jq -r '
.[] | "• " + ((.authors // "(unknown)") + " — " + (.subject // "(no subject)"))
+ | gsub("[\r\n]+"; " ")
| gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;")
' 2>/dev/null)" || return 0
[[ -n "$rowtext" ]] || return 0
@@ -231,7 +232,7 @@ tick() {
while IFS=$'\t' read -r key label; do
[[ -n "$key" ]] || continue
- query="$SCOPE and tag:account-$key and lastmod:$prev..$cur"
+ query="$SCOPE and tag:account-$key and lastmod:$((prev + 1))..$cur"
count="$(notmuch count "$query" 2>/dev/null)"
# Same validation, same reason: an empty string must not become a
@@ -245,9 +246,10 @@ tick() {
notify_account "$label" "$key" "$count" "$(build_body "$rows" "$count")"
done < <(parse_accounts < "$CONFIG")
- # Written only after every account is done, so a failure part way through
- # leaves prev unchanged and the next tick retries rather than dropping a
- # batch silently.
+ # Written only after every account is done. A single account whose count
+ # fails to validate is skipped above but does not hold the revision back;
+ # leaving it behind would re-notify every successful account's range on
+ # each later tick. Only a failed state write itself leaves prev unchanged.
write_state "$uuid" "$cur"
}
diff --git a/mail-overview/test-mail-notify.sh b/mail-overview/test-mail-notify.sh
index 9fc21a9..1c3e01a 100755
--- a/mail-overview/test-mail-notify.sh
+++ b/mail-overview/test-mail-notify.sh
@@ -115,6 +115,10 @@ check "malformed json yields an empty body rather than an error" \
"" \
"$(build_body 'not json at all' 1 2>/dev/null)"
+check "a newline in a subject stays on one row" \
+ "• Alice Example — line one line two" \
+ "$(build_body '[{"authors":"Alice Example","subject":"line one\nline two"}]' 1)"
+
# A scratch state file, removed at exit. Never the real one: that is the
# user's live notification position, and a test must not move it.
tmpstate="$(mktemp)"