aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-13 18:26:47 +0200
committerDanilo M. <danix@danix.xyz>2026-09-13 18:26:47 +0200
commit1a01efb088df07cae3f517e69940392606d00d94 (patch)
treeeff67bf588b6bd479cf10127bde760fadd551a7b
parent2eb373726a0abe07741138616a9e2bc70a2f753b (diff)
downloadquickshell-1a01efb088df07cae3f517e69940392606d00d94.tar.gz
quickshell-1a01efb088df07cae3f517e69940392606d00d94.zip
fix(mail-overview): don't re-notify the prior batch, flatten newlines
The lastmod range is inclusive at both ends and the stored revision was reused as the next start, so the batch just notified matched again. Start one revision later. A subject can contain a newline, which broke the bullet rows and the +N more count; flatten CR/LF to a space before escaping. Also reconciles the state-write comment and spec with the actual skip-and-advance failure behavior, and corrects two doc wordings.
-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)"