aboutsummaryrefslogtreecommitdiffstats
path: root/mail-overview/README.md
blob: daf2bf92b4a9e526f380f8b1c161769b3ae45119 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# mail-overview

Unread mail across every account, as one waybar number and a drawer behind it.
Clicking the icon opens the drawer; Escape or a click outside closes it.

    ┌──────────────────────────────────────────────────┐
    │  Mail                                 33 unread  │
    ├──────────────────────────────────────────────────┤
    │  ● Account A                                  2  │
    │      Some Sender              Today 06:18        │
    │      [a-list] a subject line that is elided...   │
    ├──────────────────────────────────────────────────┤
    │  ● Account B                                  0  │
    │  ● Account C                                 31  │
    │      ...three newest threads...                  │
    │  ● Account D                                  0  │
    │  ● Account E                                  0  │
    ├──────────────────────────────────────────────────┤
    │              [Sync now]  [Open qtmaildir]        │
    └──────────────────────────────────────────────────┘

## Running it

    qs -p .

It is started from `autostart.lua` and reached over IPC, so the shell has to be
running for the waybar click to do anything:

    qs -p ~/Programming/GIT/quickshell/mail-overview ipc call mail toggle

Write that path out in full in the real config. Waybar's `exec` and `on-click`
have no shell to expand `~`, and neither do Hyprland's Lua strings.

## The accounts are not listed here

`qtmaildir.conf` already has one `[account.<key>]` section per account, and
`<key>` is exactly the suffix of the notmuch tag `account-<key>`, with a short
`label` and a `color` alongside. The panel parses that file, in file order, so
adding an account to qtmaildir makes it appear here with no edit to any QML.
The file is watched, so that happens without a restart.

The `color` is the account's own, from its config section. It marks the dot
beside each row and nothing else: per-account identity is not a palette, and
`Theme` still owns every other colour.

**Parsing walks lines rather than matching one regex over the file.** The
obvious pattern for a section body, everything up to the next `[`, is wrong
here: several accounts have folders named like `[Gmail]/Bozze`, so the body
ended at that bracket, before the `label` line, and three of five accounts
quietly fell back to displaying their raw key. Nothing errored, because falling
back is a legitimate path for an account with no label.

## Counting

Every count is `tag:unread and tag:inbox`. Plain `tag:unread` also counts
archived-but-unread mail and mailing list traffic that was never in the inbox,
which for one account here was the difference between 32 and 41; the
inbox-scoped number is the one that means new mail worth looking at.

**By the `account-*` tag, never by a `path:` glob.** qtmaildir itself scopes an
account with `path:"<maildir>/**"`, and copying that query would be wrong for
this panel. notmuch deduplicates by message id, so a message that arrived at
two of the configured addresses is one message with two file paths: a glob
counts it under both accounts, and the rows then sum to more than the total the
waybar icon shows. Measured here, five accounts summed to 102 against a global
total of 101. The tag is a property of the message, so it is singular and the
rows always sum to the header. The cost is that such a message appears under
only the account the `post-new` hook attributed it to, which is the right trade
for an overview whose headline figure is a single number.

## notmuch fails two ways, and only one is detectable

A query notmuch rejects prints nothing and exits 1:

    notmuch count 'tag:unread and ('

A query Xapian merely misparses returns a plausible wrong number and exits 0:

    notmuch count 'tag:unread and (('      # 41, exit 0
    notmuch count 'tag:unread and tag:'    # 3, exit 0

So the exit status is not the test, and neither is any check that a wrong
number could pass. Counts are validated as non-negative integers, which catches
the first case, where empty output would otherwise render as an empty inbox.
The second is not catchable; the defence against it is that the queries are
fixed strings and are never built from anything.

An unknown count shows a dash, never a zero, for the same reason the VM panel
refuses to substitute libvirt's host-side figures: a number that is actually a
failure reads as a fact.

## The waybar module

A `custom` module in **continuous mode**: the script never exits, prints one
JSON line per database commit, and waybar redraws on each line. There is no
`interval`, and no daemon to supervise, because waybar owns the process.

    "custom/mail": {
      "format": "<span font='18px'>󰇮</span> {}",
      "return-type": "json",
      "exec": "~/Programming/GIT/quickshell/mail-overview/waybar-mail.sh",
      "on-click": "qs -p ~/Programming/GIT/quickshell/mail-overview ipc call mail toggle",
      "on-click-right": "~/bin/mailsync.sh",
      "tooltip": false
    }

The count drops the moment mail is read in qtmaildir and rises the moment
mbsync commits, because both write the database and the watcher sees either.

Two details in that loop are load-bearing. The watch is on the **xapian
directory**, not on a file inside it: a commit replaces files, and a watch held
on a filename dies with it. And a short sleep coalesces the several writes of
one commit, without which the module redraws three or four times per sync with
intermediate counts.

The old setup this replaces ran three copies of a python script polling the
Gmail API with stored credentials, covered three of the five accounts, and
opened Thunderbird. Everything it fetched over the network was already in the
local index.

## Geometry, and the one property that differs from the other panels

The window is a fullscreen overlay with a dimmed backdrop and the drawer itself
anchored to its top right, which is the idiom `vm-manager` and `appearance` both
use and what gives click-outside-to-close and a focusable item for Escape.

It differs from those two in one property: `exclusionMode` is `Normal`, not
`Ignore`. Waybar claims an exclusive zone at the top of this screen, so
respecting it places the overlay below the bar with no height constant here to
drift. Measured: waybar at `y=-540 h=42`, this overlay at `y=-498 h=1038`,
starting exactly where the bar ends. The backdrop therefore never dims waybar.

**Key events reach a focused item, not a window.** Setting
`WlrLayershell.keyboardFocus` is necessary but not sufficient, and
`Keys.onEscapePressed` on a `PanelWindow` never fires, so the focus sits on an
inner `Item`.

## Thread rows are read-only

Each account shows its three newest unread threads as author, relative date and
subject. Clicking one launches qtmaildir and nothing more, because `qtmaildir`
accepts no command line arguments: there is no way to tell it which thread to
open. `startup_account` in its config is a static setting, not a flag, so
"open on this account" is unavailable for the same reason. Marking read from
here was rejected rather than deferred: it would write the database behind a
possibly running client.

"Sync now" runs `~/bin/mailsync.sh`, which is already lock-protected against a
concurrent cron run, and the watcher picks up whatever it commits.

## Theme and blur

`Theme.qml` is the shared one: the palette comes from
`~/.cache/wal/udt-palette.qml` and is watched. It is a fourth identical copy,
which is a known loose end, not a palette to grow.

Frosting is Hyprland's, matched on this window's namespace:

    hl.layer_rule({
        name         = "blur-mail",
        match        = { namespace = "^(quickshell-mail)$" },
        blur         = true,
        xray         = false,
        ignore_alpha = 0.1,
    })

Without the rule it still works, rendering flat translucent.