heathrow 0.7.6 → 0.7.8
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 +4 -4
- data/README.md +24 -2
- data/img/heathrow-kb.svg +321 -0
- data/lib/heathrow/database.rb +2 -2
- data/lib/heathrow/ui/application.rb +179 -12
- data/lib/heathrow/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c08562dbe55671975842e77588946f4e89bf5b94810c7c6396ace1becdd8994
|
|
4
|
+
data.tar.gz: 0e77bfadabbfbd37b56c93cba7cc42fb7ede639133e58c64a64c4c78564b1df7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4abff9476dbeb379c387ea9105d541f1a2f0ca599a763f118c04defa8fd76047161a077b9ab990fb5428722a58f1cbcdfbb15864ef3340ab8f825d8da044e291
|
|
7
|
+
data.tar.gz: 7f2ca4bdb2685d8496eb716124bc32422b4910ea113fc4e0de61b72aa6f789a337d43c61a5213658249c8639f50d62f5721e64be4f1f17fa190864fda58f4a9f
|
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.
|
data/img/heathrow-kb.svg
ADDED
|
@@ -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">, <</text>
|
|
229
|
+
<text x="603" y="325" class="sp">< purge</text>
|
|
230
|
+
|
|
231
|
+
<rect x="638" y="286" width="62" height="55" class="key"/>
|
|
232
|
+
<text x="669" y="315" class="kl">. ></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>
|
data/lib/heathrow/database.rb
CHANGED
|
@@ -375,9 +375,9 @@ module Heathrow
|
|
|
375
375
|
# Handle sender pattern (supports regex via pipe separation)
|
|
376
376
|
if filters[:sender_pattern]
|
|
377
377
|
patterns = filters[:sender_pattern].split('|')
|
|
378
|
-
conditions = patterns.map { "sender LIKE ?" }.join(' OR ')
|
|
378
|
+
conditions = patterns.map { "(sender LIKE ? OR sender_name LIKE ?)" }.join(' OR ')
|
|
379
379
|
query += " AND (#{conditions})"
|
|
380
|
-
|
|
380
|
+
patterns.each { |p| params += ["%#{p}%", "%#{p}%"] }
|
|
381
381
|
end
|
|
382
382
|
|
|
383
383
|
# Handle subject pattern
|
|
@@ -748,7 +748,7 @@ module Heathrow
|
|
|
748
748
|
load_more_messages
|
|
749
749
|
when 'w'
|
|
750
750
|
change_width
|
|
751
|
-
when 'Y'
|
|
751
|
+
when 'Y', 'C-Y'
|
|
752
752
|
copy_right_pane_to_clipboard
|
|
753
753
|
when 'c'
|
|
754
754
|
set_view_top_bg
|
|
@@ -835,10 +835,14 @@ module Heathrow
|
|
|
835
835
|
label_message
|
|
836
836
|
when 'v'
|
|
837
837
|
view_attachments
|
|
838
|
+
when 'Z'
|
|
839
|
+
open_in_timely
|
|
838
840
|
when 'I'
|
|
839
841
|
ai_assistant
|
|
840
842
|
when 'V'
|
|
841
843
|
toggle_inline_image
|
|
844
|
+
when '@'
|
|
845
|
+
address_book_menu
|
|
842
846
|
when 'ESC', "\e"
|
|
843
847
|
if @showing_image
|
|
844
848
|
clear_inline_image
|
|
@@ -1395,6 +1399,7 @@ module Heathrow
|
|
|
1395
1399
|
when 'latest' then 'Latest'
|
|
1396
1400
|
when 'alphabetical' then 'A-Z'
|
|
1397
1401
|
when 'sender' then 'Sender'
|
|
1402
|
+
when 'from' then 'From'
|
|
1398
1403
|
when 'unread' then 'Unread'
|
|
1399
1404
|
when 'source' then 'Source'
|
|
1400
1405
|
else @sort_order.capitalize
|
|
@@ -2040,10 +2045,10 @@ module Heathrow
|
|
|
2040
2045
|
end
|
|
2041
2046
|
end
|
|
2042
2047
|
|
|
2043
|
-
header << ("─" * 60).fg(238)
|
|
2044
|
-
|
|
2045
|
-
# Format message body — prefer HTML rendered via w3m
|
|
2046
2048
|
pane_w = @panes[:right].w rescue 80
|
|
2049
|
+
header << ("─" * (pane_w - 2)).fg(238)
|
|
2050
|
+
|
|
2051
|
+
# Format message body — prefer HTML rendered via w3m
|
|
2047
2052
|
render_width = [pane_w - 2, 40].max
|
|
2048
2053
|
content = nil
|
|
2049
2054
|
if msg['html_content'] && !msg['html_content'].to_s.strip.empty?
|
|
@@ -2997,6 +3002,83 @@ module Heathrow
|
|
|
2997
3002
|
(msg['content'] && msg['content'] =~ /\A\s*<(!DOCTYPE|html|head|body)\b/i)
|
|
2998
3003
|
end
|
|
2999
3004
|
|
|
3005
|
+
def open_in_timely
|
|
3006
|
+
msg = current_message
|
|
3007
|
+
return unless msg
|
|
3008
|
+
msg = ensure_full_message(msg)
|
|
3009
|
+
|
|
3010
|
+
# Try to find a date from calendar data or message timestamp
|
|
3011
|
+
timely_home = File.expand_path('~/.timely')
|
|
3012
|
+
return set_feedback("Timely not configured (~/.timely missing)", 196, 3) unless File.directory?(timely_home)
|
|
3013
|
+
|
|
3014
|
+
# Check for ICS attachment or inline calendar data
|
|
3015
|
+
date_str = nil
|
|
3016
|
+
meta = msg['metadata']
|
|
3017
|
+
meta = JSON.parse(meta) if meta.is_a?(String) rescue nil
|
|
3018
|
+
file = meta['maildir_file'] if meta.is_a?(Hash)
|
|
3019
|
+
|
|
3020
|
+
if file && File.exist?(file)
|
|
3021
|
+
begin
|
|
3022
|
+
require 'mail'
|
|
3023
|
+
mail = Mail.read(file)
|
|
3024
|
+
if mail.multipart?
|
|
3025
|
+
mail.parts.each do |part|
|
|
3026
|
+
ct = (part.content_type || '').downcase
|
|
3027
|
+
if ct.include?('calendar') || ct.include?('ics')
|
|
3028
|
+
ics = part.decoded
|
|
3029
|
+
vevent = ics[/BEGIN:VEVENT(.*?)END:VEVENT/m, 1]
|
|
3030
|
+
if vevent
|
|
3031
|
+
vevent = vevent.gsub(/\r?\n[ \t]/, '')
|
|
3032
|
+
if vevent =~ /^DTSTART;TZID=[^:]*:(\d{8})/i ||
|
|
3033
|
+
vevent =~ /^DTSTART:(\d{8})/i ||
|
|
3034
|
+
vevent =~ /^DTSTART;VALUE=DATE:(\d{8})/i
|
|
3035
|
+
d = $1
|
|
3036
|
+
date_str = "#{d[0,4]}-#{d[4,2]}-#{d[6,2]}"
|
|
3037
|
+
end
|
|
3038
|
+
end
|
|
3039
|
+
break
|
|
3040
|
+
end
|
|
3041
|
+
end
|
|
3042
|
+
end
|
|
3043
|
+
rescue => e
|
|
3044
|
+
# Fall through to timestamp
|
|
3045
|
+
end
|
|
3046
|
+
end
|
|
3047
|
+
|
|
3048
|
+
# Fallback: use message timestamp
|
|
3049
|
+
unless date_str
|
|
3050
|
+
ts = msg['timestamp'].to_i
|
|
3051
|
+
date_str = Time.at(ts).strftime('%Y-%m-%d') if ts > 0
|
|
3052
|
+
end
|
|
3053
|
+
|
|
3054
|
+
return set_feedback("Could not determine date for Timely", 245, 2) unless date_str
|
|
3055
|
+
|
|
3056
|
+
# Write goto file for Timely
|
|
3057
|
+
File.write(File.join(timely_home, 'goto'), date_str)
|
|
3058
|
+
|
|
3059
|
+
# Also copy ICS to incoming if it has calendar data
|
|
3060
|
+
if file && File.exist?(file)
|
|
3061
|
+
begin
|
|
3062
|
+
incoming = File.join(timely_home, 'incoming')
|
|
3063
|
+
FileUtils.mkdir_p(incoming)
|
|
3064
|
+
require 'mail'
|
|
3065
|
+
mail = Mail.read(file)
|
|
3066
|
+
mail.parts.each do |part|
|
|
3067
|
+
ct = (part.content_type || '').downcase
|
|
3068
|
+
if ct.include?('calendar') || ct.include?('ics')
|
|
3069
|
+
ics_file = File.join(incoming, "heathrow_#{msg['id']}.ics")
|
|
3070
|
+
File.write(ics_file, part.decoded) unless File.exist?(ics_file)
|
|
3071
|
+
break
|
|
3072
|
+
end
|
|
3073
|
+
end
|
|
3074
|
+
rescue => e
|
|
3075
|
+
# Non-fatal
|
|
3076
|
+
end
|
|
3077
|
+
end
|
|
3078
|
+
|
|
3079
|
+
set_feedback("Sent to Timely: #{date_str}", 156, 0)
|
|
3080
|
+
end
|
|
3081
|
+
|
|
3000
3082
|
def open_message_external
|
|
3001
3083
|
msg = current_message
|
|
3002
3084
|
return unless msg
|
|
@@ -4677,6 +4759,7 @@ module Heathrow
|
|
|
4677
4759
|
ai_reply_with_draft(msg, response.strip)
|
|
4678
4760
|
when 'y'
|
|
4679
4761
|
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(response.strip) } rescue nil
|
|
4762
|
+
IO.popen('xclip -selection primary', 'w') { |io| io.write(response.strip) } rescue nil
|
|
4680
4763
|
set_feedback("Draft copied to clipboard", 156, 2)
|
|
4681
4764
|
end
|
|
4682
4765
|
@panes[:right].content_update = true
|
|
@@ -4772,6 +4855,7 @@ module Heathrow
|
|
|
4772
4855
|
chr = getchr
|
|
4773
4856
|
if chr == 'y'
|
|
4774
4857
|
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(response.strip) } rescue nil
|
|
4858
|
+
IO.popen('xclip -selection primary', 'w') { |io| io.write(response.strip) } rescue nil
|
|
4775
4859
|
set_feedback("Corrected text copied to clipboard", 156, 2)
|
|
4776
4860
|
end
|
|
4777
4861
|
@panes[:right].content_update = true
|
|
@@ -5819,7 +5903,7 @@ module Heathrow
|
|
|
5819
5903
|
['%d %b %H:%M', 'DD Mon HH:MM'],
|
|
5820
5904
|
['%b %d %H:%M', 'Mon DD HH:MM']
|
|
5821
5905
|
]
|
|
5822
|
-
sort_orders = ['latest', 'alphabetical', 'sender', 'conversation', 'unread', 'source']
|
|
5906
|
+
sort_orders = ['latest', 'alphabetical', 'sender', 'from', 'conversation', 'unread', 'source']
|
|
5823
5907
|
border_labels = ['none', 'right', 'both', 'left']
|
|
5824
5908
|
# Build view choices: A, N, plus any user-defined views
|
|
5825
5909
|
view_choices = [['A', 'All'], ['N', 'New/Unread']]
|
|
@@ -6249,11 +6333,12 @@ module Heathrow
|
|
|
6249
6333
|
end
|
|
6250
6334
|
|
|
6251
6335
|
def cycle_sort_order
|
|
6252
|
-
# Cycle through: latest -> alphabetical -> sender -> conversation -> unread -> source -> latest
|
|
6336
|
+
# Cycle through: latest -> alphabetical -> sender -> from -> conversation -> unread -> source -> latest
|
|
6253
6337
|
@sort_order = case @sort_order
|
|
6254
6338
|
when 'latest' then 'alphabetical'
|
|
6255
6339
|
when 'alphabetical' then 'sender'
|
|
6256
|
-
when 'sender' then '
|
|
6340
|
+
when 'sender' then 'from'
|
|
6341
|
+
when 'from' then 'conversation'
|
|
6257
6342
|
when 'conversation' then 'unread'
|
|
6258
6343
|
when 'unread' then 'source'
|
|
6259
6344
|
else 'latest' # This handles 'source' and any other value
|
|
@@ -6391,6 +6476,24 @@ module Heathrow
|
|
|
6391
6476
|
end
|
|
6392
6477
|
end
|
|
6393
6478
|
end
|
|
6479
|
+
when 'from'
|
|
6480
|
+
# Group by sender, most recently active sender first
|
|
6481
|
+
# Within each sender group, newest message first
|
|
6482
|
+
latest_per_sender = {}
|
|
6483
|
+
@filtered_messages.each do |m|
|
|
6484
|
+
s = display_sender(m).downcase
|
|
6485
|
+
t = ts_cache[m.object_id] || Time.at(0)
|
|
6486
|
+
latest_per_sender[s] = t if !latest_per_sender[s] || t > latest_per_sender[s]
|
|
6487
|
+
end
|
|
6488
|
+
@filtered_messages.sort! do |a, b|
|
|
6489
|
+
sa = display_sender(a).downcase
|
|
6490
|
+
sb = display_sender(b).downcase
|
|
6491
|
+
if sa == sb
|
|
6492
|
+
(ts_cache[b.object_id] || Time.at(0)) <=> (ts_cache[a.object_id] || Time.at(0))
|
|
6493
|
+
else
|
|
6494
|
+
(latest_per_sender[sb] || Time.at(0)) <=> (latest_per_sender[sa] || Time.at(0))
|
|
6495
|
+
end
|
|
6496
|
+
end
|
|
6394
6497
|
when 'unread'
|
|
6395
6498
|
# Sort by unread first, then by timestamp
|
|
6396
6499
|
@filtered_messages.sort! do |a, b|
|
|
@@ -6799,8 +6902,9 @@ Required: URL, optional CSS selector
|
|
|
6799
6902
|
#{"D".fg(10)} = Cycle date/time format
|
|
6800
6903
|
#{"o".fg(10)} = Cycle sort order (newest/oldest/unread first)
|
|
6801
6904
|
#{"i".fg(10)} = Invert sort order (toggle reverse)
|
|
6802
|
-
#{"Y".fg(10)}
|
|
6905
|
+
#{"Y/C-y".fg(10)} = Copy right pane content to clipboard
|
|
6803
6906
|
#{"P".fg(10)} = Settings popup (theme, format, etc.)
|
|
6907
|
+
#{"@".fg(10)} = Address book (a=add sender, e=edit file)
|
|
6804
6908
|
#{custom_bindings_help}
|
|
6805
6909
|
Press #{"?".fg(10)} again for extended help • Any other key to continue
|
|
6806
6910
|
HELP
|
|
@@ -7022,7 +7126,8 @@ Required: URL, optional CSS selector
|
|
|
7022
7126
|
msg = current_message
|
|
7023
7127
|
return unless msg && msg['id']
|
|
7024
7128
|
id_str = "heathrow:#{msg['id']}"
|
|
7025
|
-
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(id_str) }
|
|
7129
|
+
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(id_str) } rescue nil
|
|
7130
|
+
IO.popen('xclip -selection primary', 'w') { |io| io.write(id_str) } rescue nil
|
|
7026
7131
|
set_feedback("Message ID #{msg['id']} copied", 156, 2)
|
|
7027
7132
|
rescue => e
|
|
7028
7133
|
set_feedback("Copy failed: #{e.message}", 196, 2)
|
|
@@ -7033,7 +7138,8 @@ Required: URL, optional CSS selector
|
|
|
7033
7138
|
if text && !text.strip.empty?
|
|
7034
7139
|
# Strip ANSI codes for clean clipboard content
|
|
7035
7140
|
clean = text.gsub(/\e\[[0-9;]*m/, '')
|
|
7036
|
-
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(clean) }
|
|
7141
|
+
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(clean) } rescue nil
|
|
7142
|
+
IO.popen('xclip -selection primary', 'w') { |io| io.write(clean) } rescue nil
|
|
7037
7143
|
set_feedback("Copied to clipboard", 156, 2)
|
|
7038
7144
|
else
|
|
7039
7145
|
set_feedback("Nothing to copy", 196, 2)
|
|
@@ -7042,6 +7148,69 @@ Required: URL, optional CSS selector
|
|
|
7042
7148
|
set_feedback("Copy failed: #{e.message}", 196, 2)
|
|
7043
7149
|
end
|
|
7044
7150
|
|
|
7151
|
+
# Address book: add sender or edit file
|
|
7152
|
+
def address_book_menu
|
|
7153
|
+
@panes[:bottom].text = " @: a=Add sender to address book, e=Edit address book".fg(245)
|
|
7154
|
+
@panes[:bottom].refresh
|
|
7155
|
+
chr = getchr
|
|
7156
|
+
case chr
|
|
7157
|
+
when 'a'
|
|
7158
|
+
add_sender_to_address_book
|
|
7159
|
+
when 'e'
|
|
7160
|
+
edit_address_book
|
|
7161
|
+
else
|
|
7162
|
+
set_feedback("Cancelled", 245, 1)
|
|
7163
|
+
end
|
|
7164
|
+
end
|
|
7165
|
+
|
|
7166
|
+
def add_sender_to_address_book
|
|
7167
|
+
msg = current_message
|
|
7168
|
+
return set_feedback("No message selected", 196, 2) unless msg
|
|
7169
|
+
|
|
7170
|
+
sender = msg['sender'].to_s.strip
|
|
7171
|
+
sender_name = msg['sender_name'].to_s.strip
|
|
7172
|
+
return set_feedback("No sender", 196, 2) if sender.empty?
|
|
7173
|
+
|
|
7174
|
+
# Build full address: "Name <email>" or just "email"
|
|
7175
|
+
full = if sender_name.empty? || sender_name == sender
|
|
7176
|
+
sender
|
|
7177
|
+
else
|
|
7178
|
+
"#{sender_name} <#{sender}>"
|
|
7179
|
+
end
|
|
7180
|
+
|
|
7181
|
+
# Suggest alias from name or email local part
|
|
7182
|
+
suggested = if !sender_name.empty? && sender_name != sender
|
|
7183
|
+
sender_name.split.first.downcase
|
|
7184
|
+
else
|
|
7185
|
+
sender.split('@').first.downcase
|
|
7186
|
+
end
|
|
7187
|
+
|
|
7188
|
+
@panes[:bottom].text = " Add: #{full}".fg(156)
|
|
7189
|
+
@panes[:bottom].refresh
|
|
7190
|
+
alias_name = @panes[:bottom].ask(" Alias: ", suggested)
|
|
7191
|
+
return set_feedback("Cancelled", 245, 1) if alias_name.nil? || alias_name.strip.empty?
|
|
7192
|
+
alias_name = alias_name.strip.gsub(/\s+/, '.')
|
|
7193
|
+
|
|
7194
|
+
ab_path = File.expand_path('~/setup/addressbook')
|
|
7195
|
+
File.open(ab_path, 'a') { |f| f.puts "alias #{alias_name} #{full}" }
|
|
7196
|
+
@address_book = AddressBook.new(ab_path)
|
|
7197
|
+
set_feedback("Added: #{alias_name} → #{full}", 156, 3)
|
|
7198
|
+
rescue => e
|
|
7199
|
+
set_feedback("Failed: #{e.message}", 196, 2)
|
|
7200
|
+
end
|
|
7201
|
+
|
|
7202
|
+
def edit_address_book
|
|
7203
|
+
ab_path = File.expand_path('~/setup/addressbook')
|
|
7204
|
+
editor = ENV['EDITOR'] || 'vim'
|
|
7205
|
+
Rcurses.clear_screen
|
|
7206
|
+
system("#{editor} #{ab_path}")
|
|
7207
|
+
@address_book = AddressBook.new(ab_path)
|
|
7208
|
+
setup_display
|
|
7209
|
+
create_panes
|
|
7210
|
+
render_all
|
|
7211
|
+
set_feedback("Address book reloaded", 156, 2)
|
|
7212
|
+
end
|
|
7213
|
+
|
|
7045
7214
|
def set_view_top_bg
|
|
7046
7215
|
view = @views[@current_view]
|
|
7047
7216
|
unless view
|
|
@@ -8072,7 +8241,6 @@ Required: URL, optional CSS selector
|
|
|
8072
8241
|
|
|
8073
8242
|
# Format the event for display
|
|
8074
8243
|
lines = []
|
|
8075
|
-
lines << ("─" * 50).fg(238)
|
|
8076
8244
|
lines << "Calendar Event".b.fg(226)
|
|
8077
8245
|
lines << ""
|
|
8078
8246
|
lines << "WHAT: #{event[:summary]}".fg(156) if event[:summary]
|
|
@@ -8093,7 +8261,6 @@ Required: URL, optional CSS selector
|
|
|
8093
8261
|
end
|
|
8094
8262
|
# Skip description (email body already shows it, and ICS descriptions
|
|
8095
8263
|
# often contain raw URLs that can overflow the pane)
|
|
8096
|
-
lines << ("─" * 50).fg(238)
|
|
8097
8264
|
lines.join("\n")
|
|
8098
8265
|
rescue => e
|
|
8099
8266
|
nil # Don't crash on calendar parse errors
|
data/lib/heathrow/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.7.8
|
|
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-
|
|
12
|
+
date: 2026-03-25 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
|