heathrow 0.7.7 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7c8e6773f02f56ba4ac097d508bafaf526e2aed3bb327220614640d5c1bc3bd
4
- data.tar.gz: 78db591713e7d944436a7c797c7099f337e5050bd4bce8689b38637330665056
3
+ metadata.gz: 8023c45996b5eba44dfbb1e9718fe11d149ec7f14f35dd5569fe5172b53a018e
4
+ data.tar.gz: d1f94f8964b220fb7f831ca60298a3ca68f1dc4bdc4b9949282bb7add4c8d65e
5
5
  SHA512:
6
- metadata.gz: c7b866bfaaf9c2f2edd272be1950fd1730d0e40df9500779dbc51fa8f166e9a149472edd7b1099eea0ece4b713b62201e010cd165161c1a8a0f15725400a00ec
7
- data.tar.gz: b1bde82078786c429268cf54cce8bef68de0ef03368612e90414c9b484e76181fe3aba59e1157daa7808c968128c750e565d273ea4c30044fa19a847314d76fb
6
+ metadata.gz: 5b438204942e8ad23d166b36be5acc3dc9a0fa1c2456f9946f258c02d1597176bf0dafde75081e21c469839487938bc8181bf3151cff4fca30d7bd617ae53d18
7
+ data.tar.gz: 1944ede7efac79dac8ace2640dee9c2260754898f8dc24ed1b7793898ed2b3eb57b47cef9fc7102ab8b1da09b20be898aa23312872704301b5aa3fb96a501caf
data/README.md CHANGED
@@ -121,8 +121,9 @@ Press `?` for full help. Here are the essentials:
121
121
  | `<` | Purge deleted |
122
122
  | `x` | Open in browser |
123
123
  | `v` | View attachments |
124
- | `Y` | Copy right pane to clipboard |
124
+ | `Y`/`Ctrl-y` | Copy right pane to clipboard |
125
125
  | `y` | Copy message ID to clipboard |
126
+ | `@` | Address book (a=add sender, e=edit) |
126
127
 
127
128
  ### Compose Prompt
128
129
  | Key | Action |
@@ -139,11 +140,24 @@ Press `?` for full help. Here are the essentials:
139
140
  | `w` | Cycle pane width |
140
141
  | `Ctrl-b` | Cycle border style |
141
142
  | `D` | Cycle date format |
142
- | `o` | Cycle sort order |
143
+ | `o` | Cycle sort order (Latest, A-Z, Sender, From, Conversation, Unread, Source) |
144
+ | `Z` | Open calendar invite in [Timely](https://github.com/isene/timely) |
143
145
  | `P` | Preferences popup |
144
146
  | `?` | Help (press again for extended) |
145
147
  | `q` | Quit |
146
148
 
149
+ ### Sort Orders
150
+
151
+ | Order | Description |
152
+ |-------|-------------|
153
+ | Latest | Newest messages first (default) |
154
+ | A-Z | Alphabetical by subject |
155
+ | Sender | Alphabetical by sender |
156
+ | From | Group by sender, most recently active sender first |
157
+ | Conversation | Group by thread/conversation |
158
+ | Unread | Unread messages first |
159
+ | Source | Group by source type |
160
+
147
161
  ## Configuration
148
162
 
149
163
  All settings live in `~/.heathrow/heathrowrc` (Ruby syntax):
@@ -200,6 +214,14 @@ Each source is a self-contained plugin. Sources crash independently without affe
200
214
  - Built on [rcurses](https://github.com/isene/rcurses) TUI library
201
215
  - Inspired by [RTFM](https://github.com/isene/RTFM) file manager
202
216
 
217
+ ## Timely Integration
218
+
219
+ Heathrow works with [Timely](https://github.com/isene/timely), a terminal calendar app:
220
+
221
+ - **Z key**: On a calendar invite email, press `Z` to open it in Timely (extracts the event date from ICS data and copies the ICS for import)
222
+ - **Wezterm mailto**: Click any email address in wezterm to compose in Heathrow
223
+ - **From sort**: The "From" sort order groups messages by sender with the most recently active person at the top, perfect for family or team views
224
+
203
225
  ## License
204
226
 
205
227
  [Unlicense](https://unlicense.org/) - released into the public domain.
@@ -0,0 +1,321 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1500 560" width="1500" height="560" font-family="monospace, Courier New, Courier">
2
+ <defs>
3
+ <style>
4
+ .key { fill: #333; stroke: #555; stroke-width: 1; rx: 5; ry: 5; }
5
+ .kw { fill: #2a2a2a; stroke: #555; stroke-width: 1; rx: 5; ry: 5; }
6
+ .kl { font-size: 12px; fill: #bbb; font-weight: bold; text-anchor: middle; }
7
+ .lo { font-size: 8.5px; fill: #FF8888; text-anchor: middle; }
8
+ .up { font-size: 8.5px; fill: #88BBFF; text-anchor: middle; }
9
+ .ct { font-size: 10px; fill: #88FF88; }
10
+ .sp { font-size: 8.5px; fill: #DDAAFF; text-anchor: middle; }
11
+ .nv { font-size: 8.5px; fill: #bbb; text-anchor: middle; }
12
+ </style>
13
+ </defs>
14
+ <rect width="1500" height="560" fill="#1a1a1a" rx="8"/>
15
+ <text x="470" y="22" text-anchor="middle" font-size="15" font-weight="bold" fill="#fff">Heathrow Keyboard Reference</text>
16
+
17
+ <!-- Row 0: Esc -->
18
+ <rect x="10" y="34" width="58" height="55" class="key"/>
19
+ <text x="39" y="54" class="kl">Esc</text>
20
+ <text x="39" y="80" class="lo">quit</text>
21
+
22
+ <!-- Row 1: Number row kw=62 gap=4 -->
23
+ <rect x="10" y="97" width="58" height="55" class="key"/>
24
+ <text x="39" y="127" class="kl">` ~</text>
25
+
26
+ <rect x="72" y="97" width="62" height="55" class="key"/>
27
+ <text x="103" y="117" class="kl">1</text>
28
+ <text x="103" y="143" class="lo">view 1</text>
29
+
30
+ <rect x="138" y="97" width="62" height="55" class="key"/>
31
+ <text x="169" y="117" class="kl">2</text>
32
+ <text x="169" y="143" class="lo">view 2</text>
33
+
34
+ <rect x="204" y="97" width="62" height="55" class="key"/>
35
+ <text x="235" y="117" class="kl">3</text>
36
+ <text x="235" y="143" class="lo">view 3</text>
37
+
38
+ <rect x="270" y="97" width="62" height="55" class="key"/>
39
+ <text x="301" y="117" class="kl">4</text>
40
+ <text x="301" y="143" class="lo">view 4</text>
41
+
42
+ <rect x="336" y="97" width="62" height="55" class="key"/>
43
+ <text x="367" y="117" class="kl">5</text>
44
+ <text x="367" y="143" class="lo">view 5</text>
45
+
46
+ <rect x="402" y="97" width="62" height="55" class="key"/>
47
+ <text x="433" y="117" class="kl">6</text>
48
+ <text x="433" y="143" class="lo">view 6</text>
49
+
50
+ <rect x="468" y="97" width="62" height="55" class="key"/>
51
+ <text x="499" y="117" class="kl">7</text>
52
+ <text x="499" y="143" class="lo">view 7</text>
53
+
54
+ <rect x="534" y="97" width="62" height="55" class="key"/>
55
+ <text x="565" y="117" class="kl">8</text>
56
+ <text x="565" y="143" class="lo">view 8</text>
57
+
58
+ <rect x="600" y="97" width="62" height="55" class="key"/>
59
+ <text x="631" y="117" class="kl">9</text>
60
+ <text x="631" y="143" class="lo">view 9</text>
61
+
62
+ <rect x="666" y="97" width="62" height="55" class="key"/>
63
+ <text x="697" y="117" class="kl">0</text>
64
+ <text x="697" y="143" class="lo">view 0</text>
65
+
66
+ <rect x="732" y="97" width="62" height="55" class="key"/>
67
+ <text x="763" y="115" class="kl">- _</text>
68
+
69
+ <rect x="798" y="97" width="62" height="55" class="key"/>
70
+ <text x="829" y="115" class="kl">= +</text>
71
+ <text x="829" y="140" class="sp">+ fav folder</text>
72
+
73
+ <rect x="864" y="97" width="76" height="55" class="kw"/>
74
+ <text x="902" y="130" class="nv">Bksp</text>
75
+
76
+ <!-- Row 2: QWERTY start x=10+80=90 offset, kw=62 -->
77
+ <rect x="10" y="160" width="76" height="55" class="kw"/>
78
+ <text x="48" y="183" class="nv">Tab</text>
79
+ <text x="48" y="206" class="nv">scroll R</text>
80
+
81
+ <rect x="90" y="160" width="62" height="55" class="key"/>
82
+ <text x="121" y="176" class="kl">Q</text>
83
+ <text x="121" y="194" class="up">Force quit</text>
84
+ <text x="121" y="207" class="lo">quit</text>
85
+
86
+ <rect x="156" y="160" width="62" height="55" class="key"/>
87
+ <text x="187" y="176" class="kl">W</text>
88
+ <text x="187" y="207" class="lo">width</text>
89
+
90
+ <rect x="222" y="160" width="62" height="55" class="key"/>
91
+ <text x="253" y="176" class="kl">E</text>
92
+ <text x="253" y="194" class="up">Edit as new</text>
93
+ <text x="253" y="207" class="lo">reply+edit</text>
94
+
95
+ <rect x="288" y="160" width="62" height="55" class="key"/>
96
+ <text x="319" y="176" class="kl">R</text>
97
+ <text x="319" y="194" class="up">Read/Unrd</text>
98
+ <text x="319" y="207" class="lo">reply</text>
99
+
100
+ <rect x="354" y="160" width="62" height="55" class="key"/>
101
+ <text x="385" y="176" class="kl">T</text>
102
+ <text x="385" y="194" class="up">Tag all</text>
103
+ <text x="385" y="207" class="lo">tag</text>
104
+
105
+ <rect x="420" y="160" width="62" height="55" class="key"/>
106
+ <text x="451" y="176" class="kl">Y</text>
107
+ <text x="451" y="194" class="up">Copy pane</text>
108
+ <text x="451" y="207" class="lo">copy ID</text>
109
+
110
+ <rect x="486" y="160" width="62" height="55" class="key"/>
111
+ <text x="517" y="176" class="kl">U</text>
112
+
113
+ <rect x="552" y="160" width="62" height="55" class="key"/>
114
+ <text x="583" y="176" class="kl">I</text>
115
+ <text x="583" y="194" class="up">AI assist</text>
116
+ <text x="583" y="207" class="lo">invert</text>
117
+
118
+ <rect x="618" y="160" width="62" height="55" class="key"/>
119
+ <text x="649" y="176" class="kl">O</text>
120
+ <text x="649" y="207" class="lo">sort</text>
121
+
122
+ <rect x="684" y="160" width="62" height="55" class="key"/>
123
+ <text x="715" y="176" class="kl">P</text>
124
+ <text x="715" y="194" class="up">Prefs</text>
125
+ <text x="715" y="207" class="lo">prev unrd</text>
126
+
127
+ <rect x="750" y="160" width="62" height="55" class="key"/>
128
+ <text x="781" y="176" class="kl">[ {</text>
129
+
130
+ <rect x="816" y="160" width="62" height="55" class="key"/>
131
+ <text x="847" y="176" class="kl">] }</text>
132
+
133
+ <rect x="882" y="160" width="58" height="55" class="key"/>
134
+ <text x="911" y="190" class="kl">\</text>
135
+
136
+ <!-- Row 3: ASDF start x=10+90=100 offset -->
137
+ <rect x="10" y="223" width="86" height="55" class="kw"/>
138
+ <text x="53" y="255" class="nv">Ctrl</text>
139
+
140
+ <rect x="100" y="223" width="62" height="55" class="key"/>
141
+ <text x="131" y="239" class="kl">A</text>
142
+ <text x="131" y="262" class="up">All msgs</text>
143
+
144
+ <rect x="166" y="223" width="62" height="55" class="key"/>
145
+ <text x="197" y="239" class="kl">S</text>
146
+ <text x="197" y="257" class="up">Sources</text>
147
+ <text x="197" y="270" class="lo">save</text>
148
+
149
+ <rect x="232" y="223" width="62" height="55" class="key"/>
150
+ <text x="263" y="239" class="kl">D</text>
151
+ <text x="263" y="257" class="up">Date fmt</text>
152
+ <text x="263" y="270" class="lo">del mark</text>
153
+
154
+ <rect x="298" y="223" width="62" height="55" class="key"/>
155
+ <text x="329" y="239" class="kl">F</text>
156
+ <text x="329" y="257" class="up">Favorites</text>
157
+ <text x="329" y="270" class="lo">forward</text>
158
+
159
+ <rect x="364" y="223" width="62" height="55" class="key"/>
160
+ <text x="395" y="239" class="kl">G</text>
161
+ <text x="395" y="257" class="up">View mode</text>
162
+ <text x="395" y="270" class="lo">reply all</text>
163
+
164
+ <rect x="430" y="223" width="62" height="55" class="key"/>
165
+ <text x="461" y="239" class="kl">H</text>
166
+ <text x="461" y="270" class="nv">h = back</text>
167
+
168
+ <rect x="496" y="223" width="62" height="55" class="key"/>
169
+ <text x="527" y="239" class="kl">J</text>
170
+ <text x="527" y="257" class="up">Jump date</text>
171
+ <text x="527" y="270" class="nv">j = down</text>
172
+
173
+ <rect x="562" y="223" width="62" height="55" class="key"/>
174
+ <text x="593" y="239" class="kl">K</text>
175
+ <text x="593" y="257" class="up">Kill view</text>
176
+ <text x="593" y="270" class="nv">k = up</text>
177
+
178
+ <rect x="628" y="223" width="62" height="55" class="key"/>
179
+ <text x="659" y="239" class="kl">L</text>
180
+ <text x="659" y="257" class="up">Load more</text>
181
+ <text x="659" y="270" class="nv">l = open</text>
182
+
183
+ <rect x="694" y="223" width="62" height="55" class="key"/>
184
+ <text x="725" y="239" class="kl">; :</text>
185
+
186
+ <rect x="760" y="223" width="62" height="55" class="key"/>
187
+ <text x="791" y="255" class="kl">'</text>
188
+
189
+ <rect x="826" y="223" width="114" height="55" class="kw"/>
190
+ <text x="883" y="248" class="nv">Enter</text>
191
+ <text x="883" y="268" class="nv">open msg</text>
192
+
193
+ <!-- Row 4: ZXCV start x=10+100=110 offset -->
194
+ <rect x="10" y="286" width="96" height="55" class="kw"/>
195
+ <text x="58" y="318" class="nv">Shift</text>
196
+
197
+ <rect x="110" y="286" width="62" height="55" class="key"/>
198
+ <text x="141" y="302" class="kl">Z</text>
199
+ <text x="141" y="325" class="up">Timely</text>
200
+
201
+ <rect x="176" y="286" width="62" height="55" class="key"/>
202
+ <text x="207" y="302" class="kl">X</text>
203
+ <text x="207" y="325" class="lo">browser</text>
204
+
205
+ <rect x="242" y="286" width="62" height="55" class="key"/>
206
+ <text x="273" y="302" class="kl">C</text>
207
+
208
+ <rect x="308" y="286" width="62" height="55" class="key"/>
209
+ <text x="339" y="302" class="kl">V</text>
210
+ <text x="339" y="320" class="up">Inline img</text>
211
+ <text x="339" y="333" class="lo">attach</text>
212
+
213
+ <rect x="374" y="286" width="62" height="55" class="key"/>
214
+ <text x="405" y="302" class="kl">B</text>
215
+ <text x="405" y="325" class="up">Browse</text>
216
+
217
+ <rect x="440" y="286" width="62" height="55" class="key"/>
218
+ <text x="471" y="302" class="kl">N</text>
219
+ <text x="471" y="320" class="up">New msgs</text>
220
+ <text x="471" y="333" class="lo">next unrd</text>
221
+
222
+ <rect x="506" y="286" width="62" height="55" class="key"/>
223
+ <text x="537" y="302" class="kl">M</text>
224
+ <text x="537" y="320" class="up">Mark read</text>
225
+ <text x="537" y="333" class="lo">compose</text>
226
+
227
+ <rect x="572" y="286" width="62" height="55" class="key"/>
228
+ <text x="603" y="302" class="kl">, &lt;</text>
229
+ <text x="603" y="325" class="sp">&lt; purge</text>
230
+
231
+ <rect x="638" y="286" width="62" height="55" class="key"/>
232
+ <text x="669" y="315" class="kl">. &gt;</text>
233
+
234
+ <rect x="704" y="286" width="62" height="55" class="key"/>
235
+ <text x="735" y="302" class="kl">/ ?</text>
236
+ <text x="735" y="320" class="sp">? help</text>
237
+ <text x="735" y="333" class="sp">/ search</text>
238
+
239
+ <rect x="770" y="286" width="170" height="55" class="kw"/>
240
+ <text x="855" y="318" class="nv">Shift</text>
241
+
242
+ <!-- Row 5: Bottom -->
243
+ <rect x="10" y="349" width="70" height="50" class="kw"/>
244
+ <text x="45" y="378" class="nv">Ctrl</text>
245
+
246
+ <rect x="84" y="349" width="58" height="50" class="kw"/>
247
+ <text x="113" y="378" class="nv">Alt</text>
248
+
249
+ <rect x="146" y="349" width="330" height="50" class="key"/>
250
+ <text x="311" y="372" class="nv">Space</text>
251
+ <text x="311" y="390" class="sp">collapse</text>
252
+
253
+ <rect x="480" y="349" width="62" height="50" class="key"/>
254
+ <text x="511" y="366" class="kl"># @</text>
255
+ <text x="511" y="382" class="sp">* star</text>
256
+ <text x="511" y="394" class="sp">@ addr book</text>
257
+
258
+ <!-- Nav cluster -->
259
+ <rect x="990" y="97" width="64" height="55" class="key"/>
260
+ <text x="1022" y="120" class="nv">Home</text>
261
+ <text x="1022" y="143" class="nv">first</text>
262
+
263
+ <rect x="1058" y="97" width="64" height="55" class="key"/>
264
+ <text x="1090" y="120" class="nv">End</text>
265
+ <text x="1090" y="143" class="nv">last</text>
266
+
267
+ <rect x="990" y="160" width="64" height="55" class="key"/>
268
+ <text x="1022" y="183" class="nv">PgUp</text>
269
+ <text x="1022" y="206" class="nv">page up</text>
270
+
271
+ <rect x="1058" y="160" width="64" height="55" class="key"/>
272
+ <text x="1090" y="183" class="nv">PgDn</text>
273
+ <text x="1090" y="206" class="nv">page dn</text>
274
+
275
+ <rect x="1022" y="286" width="60" height="55" class="key"/>
276
+ <text x="1052" y="310" class="nv">UP</text>
277
+ <text x="1052" y="333" class="nv">k</text>
278
+
279
+ <rect x="958" y="349" width="60" height="50" class="key"/>
280
+ <text x="988" y="370" class="nv">LEFT</text>
281
+ <text x="988" y="390" class="nv">h/back</text>
282
+
283
+ <rect x="1022" y="349" width="60" height="50" class="key"/>
284
+ <text x="1052" y="370" class="nv">DOWN</text>
285
+ <text x="1052" y="390" class="nv">j</text>
286
+
287
+ <rect x="1086" y="349" width="60" height="50" class="key"/>
288
+ <text x="1116" y="370" class="nv">RIGHT</text>
289
+ <text x="1116" y="390" class="nv">l/open</text>
290
+
291
+ <!-- Right panel: Ctrl combos -->
292
+ <rect x="1180" y="34" width="300" height="370" rx="6" fill="#1a2a1a" stroke="#4a6a4a" stroke-width="1"/>
293
+ <text x="1330" y="56" text-anchor="middle" font-size="13" font-weight="bold" fill="#66FF66">Ctrl Combos</text>
294
+ <text x="1194" y="80" class="ct">C-B Toggle border</text>
295
+ <text x="1194" y="100" class="ct">C-F Edit filter</text>
296
+ <text x="1194" y="120" class="ct">C-L Redraw screen</text>
297
+ <text x="1194" y="140" class="ct">C-R Refresh sources</text>
298
+ <text x="1194" y="160" class="ct">C-T Tag by regex</text>
299
+ <text x="1194" y="180" class="ct">C-Y Copy pane</text>
300
+
301
+ <text x="1330" y="220" text-anchor="middle" font-size="13" font-weight="bold" fill="#888">Navigation</text>
302
+ <text x="1194" y="244" class="ct" style="fill:#888">Enter Open message</text>
303
+ <text x="1194" y="264" class="ct" style="fill:#888">Arrows Navigate</text>
304
+ <text x="1194" y="284" class="ct" style="fill:#888">PgUp/Dn Page scroll</text>
305
+ <text x="1194" y="304" class="ct" style="fill:#888">Home/End First/Last</text>
306
+ <text x="1194" y="324" class="ct" style="fill:#888">Tab Scroll right</text>
307
+ <text x="1194" y="344" class="ct" style="fill:#888">S-Arrows Scroll pane</text>
308
+ <text x="1194" y="364" class="ct" style="fill:#888">Space Collapse thread</text>
309
+ <text x="1194" y="384" class="ct" style="fill:#888">0-9 Switch views</text>
310
+
311
+ <!-- Legend -->
312
+ <rect x="20" y="470" width="14" height="14" fill="#3a2020" rx="2" stroke="#553333" stroke-width="0.5"/>
313
+ <text x="40" y="482" font-size="10" fill="#FF6666">lowercase</text>
314
+ <rect x="120" y="470" width="14" height="14" fill="#20203a" rx="2" stroke="#333355" stroke-width="0.5"/>
315
+ <text x="140" y="482" font-size="10" fill="#6699FF">UPPERCASE</text>
316
+ <rect x="240" y="470" width="14" height="14" fill="#203a20" rx="2" stroke="#335533" stroke-width="0.5"/>
317
+ <text x="260" y="482" font-size="10" fill="#66FF66">Ctrl+key</text>
318
+ <rect x="350" y="470" width="14" height="14" fill="#2a2030" rx="2" stroke="#443355" stroke-width="0.5"/>
319
+ <text x="370" y="482" font-size="10" fill="#CC99FF">special</text>
320
+ <text x="460" y="482" font-size="10" fill="#888">gray = navigation</text>
321
+ </svg>
@@ -736,6 +736,8 @@ module Heathrow
736
736
  jump_to_date
737
737
  when 'x'
738
738
  open_message_external
739
+ when 'X'
740
+ open_in_brrowser
739
741
  when 'HOME'
740
742
  go_first
741
743
  when 'END'
@@ -748,7 +750,7 @@ module Heathrow
748
750
  load_more_messages
749
751
  when 'w'
750
752
  change_width
751
- when 'Y'
753
+ when 'Y', 'C-Y'
752
754
  copy_right_pane_to_clipboard
753
755
  when 'c'
754
756
  set_view_top_bg
@@ -841,6 +843,8 @@ module Heathrow
841
843
  ai_assistant
842
844
  when 'V'
843
845
  toggle_inline_image
846
+ when '@'
847
+ address_book_menu
844
848
  when 'ESC', "\e"
845
849
  if @showing_image
846
850
  clear_inline_image
@@ -1943,6 +1947,9 @@ module Heathrow
1943
1947
  return
1944
1948
  end
1945
1949
 
1950
+ # Auto-mark as read when content is rendered in the right pane
1951
+ mark_current_message_as_read
1952
+
1946
1953
  msg = current_msg
1947
1954
 
1948
1955
  # Lazily load full content if this was a light query result
@@ -2043,10 +2050,10 @@ module Heathrow
2043
2050
  end
2044
2051
  end
2045
2052
 
2046
- header << ("─" * 60).fg(238)
2047
-
2048
- # Format message body — prefer HTML rendered via w3m
2049
2053
  pane_w = @panes[:right].w rescue 80
2054
+ header << ("─" * (pane_w - 2)).fg(238)
2055
+
2056
+ # Format message body — prefer HTML rendered via w3m
2050
2057
  render_width = [pane_w - 2, 40].max
2051
2058
  content = nil
2052
2059
  if msg['html_content'] && !msg['html_content'].to_s.strip.empty?
@@ -2251,8 +2258,14 @@ module Heathrow
2251
2258
  end
2252
2259
 
2253
2260
  def render_bottom_bar
2254
- # Check if there's an active feedback message
2255
- if @feedback_expires_at && Time.now < @feedback_expires_at
2261
+ # Check if there's an active feedback message (timed or sticky)
2262
+ if @feedback_sticky && @feedback_message
2263
+ if @panes[:bottom]
2264
+ @panes[:bottom].text = " #{@feedback_message}".fg(@feedback_color || 156)
2265
+ @panes[:bottom].refresh
2266
+ end
2267
+ return
2268
+ elsif @feedback_expires_at && Time.now < @feedback_expires_at
2256
2269
  if @panes[:bottom]
2257
2270
  @panes[:bottom].text = " #{@feedback_message}".fg(@feedback_color || 156)
2258
2271
  @panes[:bottom].refresh
@@ -3171,6 +3184,47 @@ module Heathrow
3171
3184
  end
3172
3185
  end
3173
3186
 
3187
+ def open_in_brrowser
3188
+ unless system("which brrowser > /dev/null 2>&1")
3189
+ set_feedback("brrowser not installed. See https://github.com/isene/brrowser", 196, 4)
3190
+ return
3191
+ end
3192
+
3193
+ msg = current_message
3194
+ return unless msg
3195
+ return if header_message?(msg)
3196
+ msg = ensure_full_message(msg)
3197
+
3198
+ # Build URL from message (same logic as open_message_external)
3199
+ url = nil
3200
+ if message_has_html?(msg)
3201
+ html = msg['html_content']
3202
+ html = msg['content'] if !html || html.to_s.strip.empty?
3203
+ tmpfile = "/tmp/heathrow-view-#{msg['id']}.html"
3204
+ File.write(tmpfile, html)
3205
+ url = "file://#{tmpfile}"
3206
+ else
3207
+ meta = msg['metadata']
3208
+ if meta
3209
+ parsed = meta.is_a?(Hash) ? meta : (JSON.parse(meta) rescue {})
3210
+ url = parsed['link'] || parsed['url']
3211
+ end
3212
+ url ||= msg['url'] || msg['link'] || msg['permalink']
3213
+ url ||= msg['external_id'] if msg['external_id']&.start_with?('http')
3214
+ end
3215
+
3216
+ unless url
3217
+ set_feedback("No URL or HTML content to open", 226, 3)
3218
+ return
3219
+ end
3220
+
3221
+ Rcurses.clear_screen
3222
+ system("brrowser '#{url}'")
3223
+ setup_display
3224
+ create_panes
3225
+ render_all
3226
+ end
3227
+
3174
3228
  def view_attachments
3175
3229
  msg = current_message
3176
3230
  return unless msg
@@ -3376,7 +3430,8 @@ module Heathrow
3376
3430
  success = @db.mark_as_read(msg['id'])
3377
3431
  if success
3378
3432
  msg['is_read'] = 1
3379
-
3433
+ sync_maildir_flag(msg, 'S', true)
3434
+
3380
3435
  # Re-sort if sorting by unread
3381
3436
  if @sort_order == 'unread'
3382
3437
  sort_messages
@@ -4295,13 +4350,19 @@ module Heathrow
4295
4350
  return if msgs.empty?
4296
4351
 
4297
4352
  count = 0
4353
+ failed = 0
4354
+ filed_ids = Set.new
4298
4355
  msgs.each do |msg|
4299
- file_single_message(msg, dest)
4300
- count += 1
4356
+ begin
4357
+ file_single_message(msg, dest)
4358
+ filed_ids << msg['id'] if msg['id']
4359
+ count += 1
4360
+ rescue => e
4361
+ failed += 1
4362
+ end
4301
4363
  end
4302
4364
 
4303
4365
  # Remove filed messages from current view (by id, works in both flat and threaded mode)
4304
- filed_ids = msgs.map { |m| m['id'] }.compact.to_set
4305
4366
  @filtered_messages.reject! { |m| m['id'] && filed_ids.include?(m['id']) }
4306
4367
  if @show_threaded
4307
4368
  @display_messages&.reject! { |m| m['id'] && filed_ids.include?(m['id']) }
@@ -4312,7 +4373,9 @@ module Heathrow
4312
4373
  @index = [@index, (@filtered_messages.size - 1)].min
4313
4374
  @index = 0 if @index < 0 || @filtered_messages.empty?
4314
4375
 
4315
- set_feedback("Moved #{count} message#{count > 1 ? 's' : ''} to #{dest}", 156, 2)
4376
+ msg_text = "Moved #{count} message#{count > 1 ? 's' : ''} to #{dest}"
4377
+ msg_text += " (#{failed} failed)" if failed > 0
4378
+ set_feedback(msg_text, failed > 0 ? 208 : 156, 2)
4316
4379
  render_all
4317
4380
  end
4318
4381
 
@@ -4757,6 +4820,7 @@ module Heathrow
4757
4820
  ai_reply_with_draft(msg, response.strip)
4758
4821
  when 'y'
4759
4822
  IO.popen('xclip -selection clipboard', 'w') { |io| io.write(response.strip) } rescue nil
4823
+ IO.popen('xclip -selection primary', 'w') { |io| io.write(response.strip) } rescue nil
4760
4824
  set_feedback("Draft copied to clipboard", 156, 2)
4761
4825
  end
4762
4826
  @panes[:right].content_update = true
@@ -4852,6 +4916,7 @@ module Heathrow
4852
4916
  chr = getchr
4853
4917
  if chr == 'y'
4854
4918
  IO.popen('xclip -selection clipboard', 'w') { |io| io.write(response.strip) } rescue nil
4919
+ IO.popen('xclip -selection primary', 'w') { |io| io.write(response.strip) } rescue nil
4855
4920
  set_feedback("Corrected text copied to clipboard", 156, 2)
4856
4921
  end
4857
4922
  @panes[:right].content_update = true
@@ -6898,8 +6963,9 @@ Required: URL, optional CSS selector
6898
6963
  #{"D".fg(10)} = Cycle date/time format
6899
6964
  #{"o".fg(10)} = Cycle sort order (newest/oldest/unread first)
6900
6965
  #{"i".fg(10)} = Invert sort order (toggle reverse)
6901
- #{"Y".fg(10)} = Copy right pane content to clipboard
6966
+ #{"Y/C-y".fg(10)} = Copy right pane content to clipboard
6902
6967
  #{"P".fg(10)} = Settings popup (theme, format, etc.)
6968
+ #{"@".fg(10)} = Address book (a=add sender, e=edit file)
6903
6969
  #{custom_bindings_help}
6904
6970
  Press #{"?".fg(10)} again for extended help • Any other key to continue
6905
6971
  HELP
@@ -7121,7 +7187,8 @@ Required: URL, optional CSS selector
7121
7187
  msg = current_message
7122
7188
  return unless msg && msg['id']
7123
7189
  id_str = "heathrow:#{msg['id']}"
7124
- IO.popen('xclip -selection clipboard', 'w') { |io| io.write(id_str) }
7190
+ IO.popen('xclip -selection clipboard', 'w') { |io| io.write(id_str) } rescue nil
7191
+ IO.popen('xclip -selection primary', 'w') { |io| io.write(id_str) } rescue nil
7125
7192
  set_feedback("Message ID #{msg['id']} copied", 156, 2)
7126
7193
  rescue => e
7127
7194
  set_feedback("Copy failed: #{e.message}", 196, 2)
@@ -7132,7 +7199,8 @@ Required: URL, optional CSS selector
7132
7199
  if text && !text.strip.empty?
7133
7200
  # Strip ANSI codes for clean clipboard content
7134
7201
  clean = text.gsub(/\e\[[0-9;]*m/, '')
7135
- IO.popen('xclip -selection clipboard', 'w') { |io| io.write(clean) }
7202
+ IO.popen('xclip -selection clipboard', 'w') { |io| io.write(clean) } rescue nil
7203
+ IO.popen('xclip -selection primary', 'w') { |io| io.write(clean) } rescue nil
7136
7204
  set_feedback("Copied to clipboard", 156, 2)
7137
7205
  else
7138
7206
  set_feedback("Nothing to copy", 196, 2)
@@ -7141,6 +7209,69 @@ Required: URL, optional CSS selector
7141
7209
  set_feedback("Copy failed: #{e.message}", 196, 2)
7142
7210
  end
7143
7211
 
7212
+ # Address book: add sender or edit file
7213
+ def address_book_menu
7214
+ @panes[:bottom].text = " @: a=Add sender to address book, e=Edit address book".fg(245)
7215
+ @panes[:bottom].refresh
7216
+ chr = getchr
7217
+ case chr
7218
+ when 'a'
7219
+ add_sender_to_address_book
7220
+ when 'e'
7221
+ edit_address_book
7222
+ else
7223
+ set_feedback("Cancelled", 245, 1)
7224
+ end
7225
+ end
7226
+
7227
+ def add_sender_to_address_book
7228
+ msg = current_message
7229
+ return set_feedback("No message selected", 196, 2) unless msg
7230
+
7231
+ sender = msg['sender'].to_s.strip
7232
+ sender_name = msg['sender_name'].to_s.strip
7233
+ return set_feedback("No sender", 196, 2) if sender.empty?
7234
+
7235
+ # Build full address: "Name <email>" or just "email"
7236
+ full = if sender_name.empty? || sender_name == sender
7237
+ sender
7238
+ else
7239
+ "#{sender_name} <#{sender}>"
7240
+ end
7241
+
7242
+ # Suggest alias from name or email local part
7243
+ suggested = if !sender_name.empty? && sender_name != sender
7244
+ sender_name.split.first.downcase
7245
+ else
7246
+ sender.split('@').first.downcase
7247
+ end
7248
+
7249
+ @panes[:bottom].text = " Add: #{full}".fg(156)
7250
+ @panes[:bottom].refresh
7251
+ alias_name = @panes[:bottom].ask(" Alias: ", suggested)
7252
+ return set_feedback("Cancelled", 245, 1) if alias_name.nil? || alias_name.strip.empty?
7253
+ alias_name = alias_name.strip.gsub(/\s+/, '.')
7254
+
7255
+ ab_path = File.expand_path('~/setup/addressbook')
7256
+ File.open(ab_path, 'a') { |f| f.puts "alias #{alias_name} #{full}" }
7257
+ @address_book = AddressBook.new(ab_path)
7258
+ set_feedback("Added: #{alias_name} → #{full}", 156, 3)
7259
+ rescue => e
7260
+ set_feedback("Failed: #{e.message}", 196, 2)
7261
+ end
7262
+
7263
+ def edit_address_book
7264
+ ab_path = File.expand_path('~/setup/addressbook')
7265
+ editor = ENV['EDITOR'] || 'vim'
7266
+ Rcurses.clear_screen
7267
+ system("#{editor} #{ab_path}")
7268
+ @address_book = AddressBook.new(ab_path)
7269
+ setup_display
7270
+ create_panes
7271
+ render_all
7272
+ set_feedback("Address book reloaded", 156, 2)
7273
+ end
7274
+
7144
7275
  def set_view_top_bg
7145
7276
  view = @views[@current_view]
7146
7277
  unless view
@@ -8171,7 +8302,6 @@ Required: URL, optional CSS selector
8171
8302
 
8172
8303
  # Format the event for display
8173
8304
  lines = []
8174
- lines << ("─" * 50).fg(238)
8175
8305
  lines << "Calendar Event".b.fg(226)
8176
8306
  lines << ""
8177
8307
  lines << "WHAT: #{event[:summary]}".fg(156) if event[:summary]
@@ -8192,7 +8322,6 @@ Required: URL, optional CSS selector
8192
8322
  end
8193
8323
  # Skip description (email body already shows it, and ICS descriptions
8194
8324
  # often contain raw URLs that can overflow the pane)
8195
- lines << ("─" * 50).fg(238)
8196
8325
  lines.join("\n")
8197
8326
  rescue => e
8198
8327
  nil # Don't crash on calendar parse errors
@@ -1,3 +1,3 @@
1
1
  module Heathrow
2
- VERSION = '0.7.7'
2
+ VERSION = '0.7.9'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heathrow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.7
4
+ version: 0.7.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-03-24 00:00:00.000000000 Z
12
+ date: 2026-03-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rcurses
@@ -66,6 +66,7 @@ files:
66
66
  - docs/WHATSAPP_TELEGRAM_SETUP.md
67
67
  - heathrow.gemspec
68
68
  - heathrowd.service
69
+ - img/heathrow-kb.svg
69
70
  - img/heathrow.svg
70
71
  - img/rss_threaded.png
71
72
  - img/sources.png