timely-calendar 1.0.0 → 1.0.2
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/img/timely-kb.svg +282 -0
- data/lib/timely/sources/google.rb +14 -6
- data/lib/timely/sources/outlook.rb +4 -0
- data/lib/timely/ui/application.rb +113 -13
- data/lib/timely/ui/panes.rb +3 -3
- data/lib/timely/version.rb +1 -1
- data/lib/timely/weather.rb +2 -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: a336a4e6f30f78ab53e5da5005c8a95114d0dc6657d2c5323101615d3cfa4015
|
|
4
|
+
data.tar.gz: fc5783786223c1d421832cb0cf46599192fb0f06386fae3c6a7cfc0050aeedf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a62d684e33d3f597748b142e18e0d0c5dc579d4bdb11c3d143efe4fec76129d982d5b47a4944b89e7466a72be92a680a8d10feac54539b1220a23048cc34cf4
|
|
7
|
+
data.tar.gz: f2613ecb9a86d8febc5cd8de611c42b371b54440ee6a6c5b7c2c4bffabaf29854be4795d7ba9aa601f982fee6179962956096e567e0a27ba200d67254e3be1cf
|
data/img/timely-kb.svg
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
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">Timely 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="sp">close popup</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="127" class="kl">1</text>
|
|
28
|
+
|
|
29
|
+
<rect x="138" y="97" width="62" height="55" class="key"/>
|
|
30
|
+
<text x="169" y="127" class="kl">2</text>
|
|
31
|
+
|
|
32
|
+
<rect x="204" y="97" width="62" height="55" class="key"/>
|
|
33
|
+
<text x="235" y="127" class="kl">3</text>
|
|
34
|
+
|
|
35
|
+
<rect x="270" y="97" width="62" height="55" class="key"/>
|
|
36
|
+
<text x="301" y="127" class="kl">4</text>
|
|
37
|
+
|
|
38
|
+
<rect x="336" y="97" width="62" height="55" class="key"/>
|
|
39
|
+
<text x="367" y="127" class="kl">5</text>
|
|
40
|
+
|
|
41
|
+
<rect x="402" y="97" width="62" height="55" class="key"/>
|
|
42
|
+
<text x="433" y="127" class="kl">6</text>
|
|
43
|
+
|
|
44
|
+
<rect x="468" y="97" width="62" height="55" class="key"/>
|
|
45
|
+
<text x="499" y="127" class="kl">7</text>
|
|
46
|
+
|
|
47
|
+
<rect x="534" y="97" width="62" height="55" class="key"/>
|
|
48
|
+
<text x="565" y="127" class="kl">8</text>
|
|
49
|
+
|
|
50
|
+
<rect x="600" y="97" width="62" height="55" class="key"/>
|
|
51
|
+
<text x="631" y="127" class="kl">9</text>
|
|
52
|
+
|
|
53
|
+
<rect x="666" y="97" width="62" height="55" class="key"/>
|
|
54
|
+
<text x="697" y="127" class="kl">0</text>
|
|
55
|
+
|
|
56
|
+
<rect x="732" y="97" width="62" height="55" class="key"/>
|
|
57
|
+
<text x="763" y="127" class="kl">- _</text>
|
|
58
|
+
|
|
59
|
+
<rect x="798" y="97" width="62" height="55" class="key"/>
|
|
60
|
+
<text x="829" y="127" class="kl">= +</text>
|
|
61
|
+
|
|
62
|
+
<rect x="864" y="97" width="76" height="55" class="kw"/>
|
|
63
|
+
<text x="902" y="130" class="nv">Bksp</text>
|
|
64
|
+
|
|
65
|
+
<!-- Row 2: QWERTY start x=10+80=90 offset, kw=62 -->
|
|
66
|
+
<rect x="10" y="160" width="76" height="55" class="kw"/>
|
|
67
|
+
<text x="48" y="193" class="nv">Tab</text>
|
|
68
|
+
|
|
69
|
+
<rect x="90" y="160" width="62" height="55" class="key"/>
|
|
70
|
+
<text x="121" y="176" class="kl">Q</text>
|
|
71
|
+
<text x="121" y="207" class="lo">q quit</text>
|
|
72
|
+
|
|
73
|
+
<rect x="156" y="160" width="62" height="55" class="key"/>
|
|
74
|
+
<text x="187" y="176" class="kl">W</text>
|
|
75
|
+
<text x="187" y="194" class="up">W Week-1</text>
|
|
76
|
+
<text x="187" y="207" class="lo">w Week+1</text>
|
|
77
|
+
|
|
78
|
+
<rect x="222" y="160" width="62" height="55" class="key"/>
|
|
79
|
+
<text x="253" y="176" class="kl">E</text>
|
|
80
|
+
<text x="253" y="194" class="up">E Prev evt</text>
|
|
81
|
+
<text x="253" y="207" class="lo">e Next evt</text>
|
|
82
|
+
|
|
83
|
+
<rect x="288" y="160" width="62" height="55" class="key"/>
|
|
84
|
+
<text x="319" y="176" class="kl">R</text>
|
|
85
|
+
<text x="319" y="207" class="lo">r reply</text>
|
|
86
|
+
|
|
87
|
+
<rect x="354" y="160" width="62" height="55" class="key"/>
|
|
88
|
+
<text x="385" y="176" class="kl">T</text>
|
|
89
|
+
<text x="385" y="207" class="lo">t today</text>
|
|
90
|
+
|
|
91
|
+
<rect x="420" y="160" width="62" height="55" class="key"/>
|
|
92
|
+
<text x="451" y="176" class="kl">Y</text>
|
|
93
|
+
<text x="451" y="194" class="up">Y Year-1</text>
|
|
94
|
+
<text x="451" y="207" class="lo">y Year+1</text>
|
|
95
|
+
|
|
96
|
+
<rect x="486" y="160" width="62" height="55" class="key"/>
|
|
97
|
+
<text x="517" y="193" class="kl">U</text>
|
|
98
|
+
|
|
99
|
+
<rect x="552" y="160" width="62" height="55" class="key"/>
|
|
100
|
+
<text x="583" y="176" class="kl">I</text>
|
|
101
|
+
<text x="583" y="207" class="lo">i import ICS</text>
|
|
102
|
+
|
|
103
|
+
<rect x="618" y="160" width="62" height="55" class="key"/>
|
|
104
|
+
<text x="649" y="176" class="kl">O</text>
|
|
105
|
+
<text x="649" y="194" class="up">O Outlook</text>
|
|
106
|
+
|
|
107
|
+
<rect x="684" y="160" width="62" height="55" class="key"/>
|
|
108
|
+
<text x="715" y="176" class="kl">P</text>
|
|
109
|
+
<text x="715" y="194" class="up">P Prefs</text>
|
|
110
|
+
|
|
111
|
+
<rect x="750" y="160" width="62" height="55" class="key"/>
|
|
112
|
+
<text x="781" y="193" class="kl">Å</text>
|
|
113
|
+
|
|
114
|
+
<rect x="816" y="160" width="62" height="55" class="key"/>
|
|
115
|
+
<text x="847" y="193" class="kl">^</text>
|
|
116
|
+
|
|
117
|
+
<rect x="882" y="160" width="58" height="55" class="key"/>
|
|
118
|
+
<text x="911" y="193" class="kl">\</text>
|
|
119
|
+
|
|
120
|
+
<!-- Row 3: ASDF start x=10+90=100 offset -->
|
|
121
|
+
<rect x="10" y="223" width="86" height="55" class="kw"/>
|
|
122
|
+
<text x="53" y="255" class="nv">Ctrl</text>
|
|
123
|
+
|
|
124
|
+
<rect x="100" y="223" width="62" height="55" class="key"/>
|
|
125
|
+
<text x="131" y="239" class="kl">A</text>
|
|
126
|
+
<text x="131" y="270" class="lo">a accept</text>
|
|
127
|
+
|
|
128
|
+
<rect x="166" y="223" width="62" height="55" class="key"/>
|
|
129
|
+
<text x="197" y="239" class="kl">S</text>
|
|
130
|
+
<text x="197" y="257" class="up">S Sync</text>
|
|
131
|
+
|
|
132
|
+
<rect x="232" y="223" width="62" height="55" class="key"/>
|
|
133
|
+
<text x="263" y="239" class="kl">D</text>
|
|
134
|
+
<text x="263" y="257" class="up">D Day-1</text>
|
|
135
|
+
<text x="263" y="270" class="lo">d Day+1</text>
|
|
136
|
+
|
|
137
|
+
<rect x="298" y="223" width="62" height="55" class="key"/>
|
|
138
|
+
<text x="329" y="255" class="kl">F</text>
|
|
139
|
+
|
|
140
|
+
<rect x="364" y="223" width="62" height="55" class="key"/>
|
|
141
|
+
<text x="395" y="239" class="kl">G</text>
|
|
142
|
+
<text x="395" y="257" class="up">G Google</text>
|
|
143
|
+
<text x="395" y="270" class="lo">g go to date</text>
|
|
144
|
+
|
|
145
|
+
<rect x="430" y="223" width="62" height="55" class="key"/>
|
|
146
|
+
<text x="461" y="239" class="kl">H</text>
|
|
147
|
+
<text x="461" y="270" class="nv">h = Day-1</text>
|
|
148
|
+
|
|
149
|
+
<rect x="496" y="223" width="62" height="55" class="key"/>
|
|
150
|
+
<text x="527" y="239" class="kl">J</text>
|
|
151
|
+
<text x="527" y="270" class="lo">j next e/d</text>
|
|
152
|
+
|
|
153
|
+
<rect x="562" y="223" width="62" height="55" class="key"/>
|
|
154
|
+
<text x="593" y="239" class="kl">K</text>
|
|
155
|
+
<text x="593" y="270" class="lo">k prev e/d</text>
|
|
156
|
+
|
|
157
|
+
<rect x="628" y="223" width="62" height="55" class="key"/>
|
|
158
|
+
<text x="659" y="239" class="kl">L</text>
|
|
159
|
+
<text x="659" y="270" class="nv">l = Day+1</text>
|
|
160
|
+
|
|
161
|
+
<rect x="694" y="223" width="62" height="55" class="key"/>
|
|
162
|
+
<text x="725" y="255" class="kl">Ø</text>
|
|
163
|
+
|
|
164
|
+
<rect x="760" y="223" width="62" height="55" class="key"/>
|
|
165
|
+
<text x="791" y="255" class="kl">Æ</text>
|
|
166
|
+
|
|
167
|
+
<rect x="826" y="223" width="114" height="55" class="kw"/>
|
|
168
|
+
<text x="883" y="248" class="nv">Enter</text>
|
|
169
|
+
<text x="883" y="268" class="sp">edit event</text>
|
|
170
|
+
|
|
171
|
+
<!-- Row 4: ZXCV start x=10+100=110 offset -->
|
|
172
|
+
<rect x="10" y="286" width="96" height="55" class="kw"/>
|
|
173
|
+
<text x="58" y="318" class="nv">Shift</text>
|
|
174
|
+
|
|
175
|
+
<rect x="110" y="286" width="62" height="55" class="key"/>
|
|
176
|
+
<text x="141" y="315" class="kl">Z</text>
|
|
177
|
+
|
|
178
|
+
<rect x="176" y="286" width="62" height="55" class="key"/>
|
|
179
|
+
<text x="207" y="302" class="kl">X</text>
|
|
180
|
+
<text x="207" y="333" class="sp">x del event</text>
|
|
181
|
+
|
|
182
|
+
<rect x="242" y="286" width="62" height="55" class="key"/>
|
|
183
|
+
<text x="273" y="302" class="kl">C</text>
|
|
184
|
+
<text x="273" y="320" class="up">C Calendars</text>
|
|
185
|
+
|
|
186
|
+
<rect x="308" y="286" width="62" height="55" class="key"/>
|
|
187
|
+
<text x="339" y="302" class="kl">V</text>
|
|
188
|
+
<text x="339" y="333" class="lo">v view popup</text>
|
|
189
|
+
|
|
190
|
+
<rect x="374" y="286" width="62" height="55" class="key"/>
|
|
191
|
+
<text x="405" y="315" class="kl">B</text>
|
|
192
|
+
|
|
193
|
+
<rect x="440" y="286" width="62" height="55" class="key"/>
|
|
194
|
+
<text x="471" y="302" class="kl">N</text>
|
|
195
|
+
<text x="471" y="333" class="lo">n new event</text>
|
|
196
|
+
|
|
197
|
+
<rect x="506" y="286" width="62" height="55" class="key"/>
|
|
198
|
+
<text x="537" y="302" class="kl">M</text>
|
|
199
|
+
<text x="537" y="320" class="up">M Month-1</text>
|
|
200
|
+
<text x="537" y="333" class="lo">m Month+1</text>
|
|
201
|
+
|
|
202
|
+
<rect x="572" y="286" width="62" height="55" class="key"/>
|
|
203
|
+
<text x="603" y="315" class="kl">, <</text>
|
|
204
|
+
|
|
205
|
+
<rect x="638" y="286" width="62" height="55" class="key"/>
|
|
206
|
+
<text x="669" y="315" class="kl">. ></text>
|
|
207
|
+
|
|
208
|
+
<rect x="704" y="286" width="62" height="55" class="key"/>
|
|
209
|
+
<text x="735" y="302" class="kl">/ ?</text>
|
|
210
|
+
<text x="735" y="333" class="sp">? help</text>
|
|
211
|
+
|
|
212
|
+
<rect x="770" y="286" width="170" height="55" class="kw"/>
|
|
213
|
+
<text x="855" y="318" class="nv">Shift</text>
|
|
214
|
+
|
|
215
|
+
<!-- Row 5: Bottom -->
|
|
216
|
+
<rect x="10" y="349" width="70" height="50" class="kw"/>
|
|
217
|
+
<text x="45" y="378" class="nv">Ctrl</text>
|
|
218
|
+
|
|
219
|
+
<rect x="84" y="349" width="58" height="50" class="kw"/>
|
|
220
|
+
<text x="113" y="378" class="nv">Alt</text>
|
|
221
|
+
|
|
222
|
+
<rect x="146" y="349" width="330" height="50" class="key"/>
|
|
223
|
+
<text x="311" y="378" class="nv">Space</text>
|
|
224
|
+
|
|
225
|
+
<rect x="480" y="349" width="62" height="50" class="key"/>
|
|
226
|
+
<text x="511" y="378" class="kl"># @</text>
|
|
227
|
+
|
|
228
|
+
<!-- Nav cluster -->
|
|
229
|
+
<rect x="990" y="97" width="64" height="55" class="key"/>
|
|
230
|
+
<text x="1022" y="120" class="nv">Home</text>
|
|
231
|
+
<text x="1022" y="143" class="nv">top slot</text>
|
|
232
|
+
|
|
233
|
+
<rect x="1058" y="97" width="64" height="55" class="key"/>
|
|
234
|
+
<text x="1090" y="120" class="nv">End</text>
|
|
235
|
+
<text x="1090" y="143" class="nv">btm slot</text>
|
|
236
|
+
|
|
237
|
+
<rect x="990" y="160" width="64" height="55" class="key"/>
|
|
238
|
+
<text x="1022" y="183" class="nv">PgUp</text>
|
|
239
|
+
<text x="1022" y="206" class="nv">page slots</text>
|
|
240
|
+
|
|
241
|
+
<rect x="1058" y="160" width="64" height="55" class="key"/>
|
|
242
|
+
<text x="1090" y="183" class="nv">PgDn</text>
|
|
243
|
+
<text x="1090" y="206" class="nv">page slots</text>
|
|
244
|
+
|
|
245
|
+
<rect x="990" y="223" width="64" height="55" class="key"/>
|
|
246
|
+
<text x="1022" y="248" class="nv">Del</text>
|
|
247
|
+
<text x="1022" y="268" class="sp">del event</text>
|
|
248
|
+
|
|
249
|
+
<rect x="1022" y="286" width="60" height="55" class="key"/>
|
|
250
|
+
<text x="1052" y="310" class="nv">UP</text>
|
|
251
|
+
<text x="1052" y="333" class="nv">prev slot</text>
|
|
252
|
+
|
|
253
|
+
<rect x="958" y="349" width="60" height="50" class="key"/>
|
|
254
|
+
<text x="988" y="370" class="nv">LEFT</text>
|
|
255
|
+
<text x="988" y="390" class="nv">prev day</text>
|
|
256
|
+
|
|
257
|
+
<rect x="1022" y="349" width="60" height="50" class="key"/>
|
|
258
|
+
<text x="1052" y="370" class="nv">DOWN</text>
|
|
259
|
+
<text x="1052" y="390" class="nv">next slot</text>
|
|
260
|
+
|
|
261
|
+
<rect x="1086" y="349" width="60" height="50" class="key"/>
|
|
262
|
+
<text x="1116" y="370" class="nv">RIGHT</text>
|
|
263
|
+
<text x="1116" y="390" class="nv">next day</text>
|
|
264
|
+
|
|
265
|
+
<!-- Right panel: Ctrl combos -->
|
|
266
|
+
<rect x="1180" y="34" width="300" height="200" rx="6" fill="#1a2a1a" stroke="#4a4" stroke-width="1"/>
|
|
267
|
+
<text x="1330" y="56" text-anchor="middle" font-size="13" font-weight="bold" fill="#66FF66">Ctrl Combos</text>
|
|
268
|
+
<text x="1194" y="80" class="ct">C-R Refresh all calendars</text>
|
|
269
|
+
<text x="1194" y="105" class="ct">C-L Redraw screen</text>
|
|
270
|
+
<text x="1194" y="130" class="ct">C-Y Copy event to clipboard</text>
|
|
271
|
+
|
|
272
|
+
<!-- Legend -->
|
|
273
|
+
<rect x="20" y="470" width="14" height="14" fill="#3a2222" rx="2" stroke="#FF6666" stroke-width="0.5"/>
|
|
274
|
+
<text x="40" y="482" font-size="10" fill="#FF6666">lowercase</text>
|
|
275
|
+
<rect x="120" y="470" width="14" height="14" fill="#22223a" rx="2" stroke="#6699FF" stroke-width="0.5"/>
|
|
276
|
+
<text x="140" y="482" font-size="10" fill="#6699FF">UPPERCASE</text>
|
|
277
|
+
<rect x="240" y="470" width="14" height="14" fill="#223a22" rx="2" stroke="#66FF66" stroke-width="0.5"/>
|
|
278
|
+
<text x="260" y="482" font-size="10" fill="#66FF66">Ctrl+key</text>
|
|
279
|
+
<rect x="350" y="470" width="14" height="14" fill="#2a223a" rx="2" stroke="#CC99FF" stroke-width="0.5"/>
|
|
280
|
+
<text x="370" y="482" font-size="10" fill="#CC99FF">special</text>
|
|
281
|
+
<text x="460" y="482" font-size="10" fill="#888">gray = navigation</text>
|
|
282
|
+
</svg>
|
|
@@ -211,13 +211,15 @@ module Timely
|
|
|
211
211
|
body
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
-
def api_get(path)
|
|
214
|
+
def api_get(path, retries: 2)
|
|
215
215
|
token = get_access_token
|
|
216
216
|
return nil unless token
|
|
217
|
-
|
|
217
|
+
full_url = path.start_with?('http') ? path : "#{API_BASE}#{path}"
|
|
218
|
+
uri = URI(full_url)
|
|
218
219
|
req = Net::HTTP::Get.new(uri)
|
|
219
220
|
req['Authorization'] = "Bearer #{token}"
|
|
220
|
-
|
|
221
|
+
req['Accept-Encoding'] = 'identity' # Disable chunked/gzip (rcurses raw mode breaks chunked reads)
|
|
222
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60, open_timeout: 15) { |http| http.request(req) }
|
|
221
223
|
if res.is_a?(Net::HTTPSuccess)
|
|
222
224
|
JSON.parse(res.body)
|
|
223
225
|
else
|
|
@@ -225,6 +227,10 @@ module Timely
|
|
|
225
227
|
nil
|
|
226
228
|
end
|
|
227
229
|
rescue Timeout::Error, Net::OpenTimeout, SocketError, Errno::ECONNREFUSED => e
|
|
230
|
+
if retries > 0
|
|
231
|
+
sleep 1
|
|
232
|
+
retry if (retries -= 1) >= 0
|
|
233
|
+
end
|
|
228
234
|
@last_error = "Network error: #{e.message}"
|
|
229
235
|
nil
|
|
230
236
|
rescue => e
|
|
@@ -239,8 +245,9 @@ module Timely
|
|
|
239
245
|
req = Net::HTTP::Post.new(uri)
|
|
240
246
|
req['Authorization'] = "Bearer #{token}"
|
|
241
247
|
req['Content-Type'] = 'application/json'
|
|
248
|
+
req['Accept-Encoding'] = 'identity'
|
|
242
249
|
req.body = JSON.generate(body)
|
|
243
|
-
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout:
|
|
250
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60, open_timeout: 15) { |http| http.request(req) }
|
|
244
251
|
res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body) : nil
|
|
245
252
|
rescue => e
|
|
246
253
|
nil
|
|
@@ -253,8 +260,9 @@ module Timely
|
|
|
253
260
|
req = Net::HTTP::Put.new(uri)
|
|
254
261
|
req['Authorization'] = "Bearer #{token}"
|
|
255
262
|
req['Content-Type'] = 'application/json'
|
|
263
|
+
req['Accept-Encoding'] = 'identity'
|
|
256
264
|
req.body = JSON.generate(body)
|
|
257
|
-
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout:
|
|
265
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60, open_timeout: 15) { |http| http.request(req) }
|
|
258
266
|
res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body) : nil
|
|
259
267
|
rescue => e
|
|
260
268
|
nil
|
|
@@ -266,7 +274,7 @@ module Timely
|
|
|
266
274
|
uri = URI("#{API_BASE}#{path}")
|
|
267
275
|
req = Net::HTTP::Delete.new(uri)
|
|
268
276
|
req['Authorization'] = "Bearer #{token}"
|
|
269
|
-
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout:
|
|
277
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60, open_timeout: 15) { |http| http.request(req) }
|
|
270
278
|
res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
271
279
|
rescue => e
|
|
272
280
|
false
|
|
@@ -227,6 +227,7 @@ module Timely
|
|
|
227
227
|
uri = URI(full_url)
|
|
228
228
|
req = Net::HTTP::Get.new(uri)
|
|
229
229
|
req['Authorization'] = "Bearer #{token}"
|
|
230
|
+
req['Accept-Encoding'] = 'identity'
|
|
230
231
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 30, open_timeout: 10) { |http| http.request(req) }
|
|
231
232
|
if res.is_a?(Net::HTTPSuccess)
|
|
232
233
|
JSON.parse(res.body)
|
|
@@ -248,6 +249,7 @@ module Timely
|
|
|
248
249
|
uri = URI("#{GRAPH_BASE}#{path}")
|
|
249
250
|
req = Net::HTTP::Post.new(uri)
|
|
250
251
|
req['Authorization'] = "Bearer #{token}"
|
|
252
|
+
req['Accept-Encoding'] = 'identity'
|
|
251
253
|
req['Content-Type'] = 'application/json'
|
|
252
254
|
req.body = JSON.generate(body)
|
|
253
255
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 30, open_timeout: 10) { |http| http.request(req) }
|
|
@@ -262,6 +264,7 @@ module Timely
|
|
|
262
264
|
uri = URI("#{GRAPH_BASE}#{path}")
|
|
263
265
|
req = Net::HTTP::Patch.new(uri)
|
|
264
266
|
req['Authorization'] = "Bearer #{token}"
|
|
267
|
+
req['Accept-Encoding'] = 'identity'
|
|
265
268
|
req['Content-Type'] = 'application/json'
|
|
266
269
|
req.body = JSON.generate(body)
|
|
267
270
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 30, open_timeout: 10) { |http| http.request(req) }
|
|
@@ -276,6 +279,7 @@ module Timely
|
|
|
276
279
|
uri = URI("#{GRAPH_BASE}#{path}")
|
|
277
280
|
req = Net::HTTP::Delete.new(uri)
|
|
278
281
|
req['Authorization'] = "Bearer #{token}"
|
|
282
|
+
req['Accept-Encoding'] = 'identity'
|
|
279
283
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 30, open_timeout: 10) { |http| http.request(req) }
|
|
280
284
|
res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
281
285
|
rescue => e
|
|
@@ -123,6 +123,9 @@ module Timely
|
|
|
123
123
|
when 't'
|
|
124
124
|
@selected_date = Date.today
|
|
125
125
|
@selected_event_index = 0
|
|
126
|
+
now = Time.now
|
|
127
|
+
@selected_slot = now.hour * 2 + (now.min >= 30 ? 1 : 0)
|
|
128
|
+
@slot_offset = [@selected_slot - 5, 0].max
|
|
126
129
|
date_changed
|
|
127
130
|
when 'g'
|
|
128
131
|
go_to_date
|
|
@@ -150,6 +153,17 @@ module Timely
|
|
|
150
153
|
manual_sync
|
|
151
154
|
when 'C'
|
|
152
155
|
show_calendars
|
|
156
|
+
when 'C-R'
|
|
157
|
+
@_cached_planets_date = nil # Force planet recalculation
|
|
158
|
+
@weather_forecast = nil # Force weather refresh
|
|
159
|
+
@_weather_fetched_at = nil
|
|
160
|
+
@db.execute("DELETE FROM weather_cache") rescue nil # Clear DB cache too
|
|
161
|
+
load_events_for_range
|
|
162
|
+
render_all
|
|
163
|
+
when 'C-L'
|
|
164
|
+
Rcurses.clear_screen
|
|
165
|
+
create_panes
|
|
166
|
+
render_all
|
|
153
167
|
when 'P'
|
|
154
168
|
show_preferences
|
|
155
169
|
when '?'
|
|
@@ -267,6 +281,19 @@ module Timely
|
|
|
267
281
|
def date_changed
|
|
268
282
|
@selected_event_index = 0
|
|
269
283
|
load_events_for_range
|
|
284
|
+
# If slot is in all-day area but no event there, jump to first timed event or current time
|
|
285
|
+
if @selected_slot && @selected_slot < 0 && event_at_selected_slot.nil?
|
|
286
|
+
events = events_on_selected_day
|
|
287
|
+
first_timed = events.find { |e| e['all_day'].to_i != 1 }
|
|
288
|
+
if first_timed
|
|
289
|
+
t = Time.at(first_timed['start_time'].to_i)
|
|
290
|
+
@selected_slot = t.hour * 2 + (t.min >= 30 ? 1 : 0)
|
|
291
|
+
else
|
|
292
|
+
now = Time.now
|
|
293
|
+
@selected_slot = now.hour * 2 + (now.min >= 30 ? 1 : 0)
|
|
294
|
+
end
|
|
295
|
+
@slot_offset = [@selected_slot - 5, 0].max
|
|
296
|
+
end
|
|
270
297
|
render_all
|
|
271
298
|
end
|
|
272
299
|
|
|
@@ -298,8 +325,28 @@ module Timely
|
|
|
298
325
|
|
|
299
326
|
def clean_description(desc)
|
|
300
327
|
return nil unless desc
|
|
301
|
-
|
|
302
|
-
|
|
328
|
+
desc = desc.to_s
|
|
329
|
+
# Convert HTML to plain text via w3m if it looks like HTML
|
|
330
|
+
if desc =~ /\A\s*<(!DOCTYPE|html|head|body|div|p\b)/i
|
|
331
|
+
begin
|
|
332
|
+
text = IO.popen(['w3m', '-T', 'text/html', '-dump', '-cols', '200'], 'r+') do |io|
|
|
333
|
+
io.write(desc)
|
|
334
|
+
io.close_write
|
|
335
|
+
io.read
|
|
336
|
+
end
|
|
337
|
+
desc = text if text && !text.strip.empty?
|
|
338
|
+
rescue Errno::ENOENT
|
|
339
|
+
# w3m not installed, strip tags manually
|
|
340
|
+
desc = desc.gsub(/<[^>]+>/, ' ').gsub(/ /i, ' ').gsub(/&/i, '&').gsub(/</i, '<').gsub(/>/i, '>')
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
# Remove common garbage from Google/Outlook/Teams descriptions
|
|
344
|
+
desc.gsub(/BC\d+-Color:\s*-?\d+\s*/, '') # Google color metadata
|
|
345
|
+
.gsub(/-::~:~::~:~.*$/m, '') # Google Meet block
|
|
346
|
+
.gsub(/_{3,}/, '') # Long underscore lines (Teams HR)
|
|
347
|
+
.gsub(/-{5,}/, '') # Long dash lines
|
|
348
|
+
.gsub(/\n{3,}/, "\n\n") # Collapse excessive blank lines
|
|
349
|
+
.strip
|
|
303
350
|
end
|
|
304
351
|
|
|
305
352
|
# Find the event at the currently selected time slot
|
|
@@ -308,11 +355,18 @@ module Timely
|
|
|
308
355
|
events = events_on_selected_day
|
|
309
356
|
|
|
310
357
|
if @selected_slot < 0
|
|
311
|
-
# Negative slot
|
|
358
|
+
# Negative slot uses max_allday across the visible week (same as render_mid_pane)
|
|
359
|
+
week_start = @selected_date - (@selected_date.cwday - 1)
|
|
360
|
+
max_allday = 0
|
|
361
|
+
7.times do |i|
|
|
362
|
+
day = week_start + i
|
|
363
|
+
n = (@events_by_date[day] || []).count { |e| e['all_day'].to_i == 1 }
|
|
364
|
+
max_allday = n if n > max_allday
|
|
365
|
+
end
|
|
312
366
|
allday = events.select { |e| e['all_day'].to_i == 1 }
|
|
313
|
-
|
|
314
|
-
idx
|
|
315
|
-
return
|
|
367
|
+
idx = max_allday - @selected_slot.abs # -max -> 0 (first), -1 -> max-1 (last)
|
|
368
|
+
return allday[idx] if idx >= 0 && idx < allday.size
|
|
369
|
+
return nil
|
|
316
370
|
end
|
|
317
371
|
|
|
318
372
|
hour = @selected_slot / 2
|
|
@@ -348,6 +402,7 @@ module Timely
|
|
|
348
402
|
# If there are more events on the current day after the selected one, go to next
|
|
349
403
|
if events.length > 0 && @selected_event_index < events.length - 1
|
|
350
404
|
@selected_event_index += 1
|
|
405
|
+
move_slot_to_event(events[@selected_event_index], events)
|
|
351
406
|
render_mid_pane
|
|
352
407
|
render_bottom_pane
|
|
353
408
|
return
|
|
@@ -360,7 +415,10 @@ module Timely
|
|
|
360
415
|
if day_events && !day_events.empty?
|
|
361
416
|
@selected_date = check_date
|
|
362
417
|
@selected_event_index = 0
|
|
363
|
-
|
|
418
|
+
load_events_for_range
|
|
419
|
+
events = events_on_selected_day
|
|
420
|
+
move_slot_to_event(events.first, events) if events.any?
|
|
421
|
+
render_all
|
|
364
422
|
return
|
|
365
423
|
end
|
|
366
424
|
end
|
|
@@ -373,6 +431,7 @@ module Timely
|
|
|
373
431
|
# If there are more events on the current day before the selected one, go to prev
|
|
374
432
|
if events.length > 0 && @selected_event_index > 0
|
|
375
433
|
@selected_event_index -= 1
|
|
434
|
+
move_slot_to_event(events[@selected_event_index], events)
|
|
376
435
|
render_mid_pane
|
|
377
436
|
render_bottom_pane
|
|
378
437
|
return
|
|
@@ -384,8 +443,11 @@ module Timely
|
|
|
384
443
|
day_events = @db.get_events_for_date(check_date)
|
|
385
444
|
if day_events && !day_events.empty?
|
|
386
445
|
@selected_date = check_date
|
|
387
|
-
|
|
388
|
-
|
|
446
|
+
load_events_for_range
|
|
447
|
+
events = events_on_selected_day
|
|
448
|
+
@selected_event_index = [events.length - 1, 0].max
|
|
449
|
+
move_slot_to_event(events.last, events) if events.any?
|
|
450
|
+
render_all
|
|
389
451
|
return
|
|
390
452
|
end
|
|
391
453
|
end
|
|
@@ -393,6 +455,27 @@ module Timely
|
|
|
393
455
|
show_feedback("No earlier events found within the past year", 245)
|
|
394
456
|
end
|
|
395
457
|
|
|
458
|
+
def move_slot_to_event(evt, day_events)
|
|
459
|
+
return unless evt
|
|
460
|
+
if evt['all_day'].to_i == 1
|
|
461
|
+
# Compute max_allday across the visible week (same as render_mid_pane)
|
|
462
|
+
week_start = @selected_date - (@selected_date.cwday - 1)
|
|
463
|
+
max_allday = 0
|
|
464
|
+
7.times do |i|
|
|
465
|
+
day = week_start + i
|
|
466
|
+
n = (@events_by_date[day] || []).count { |e| e['all_day'].to_i == 1 }
|
|
467
|
+
max_allday = n if n > max_allday
|
|
468
|
+
end
|
|
469
|
+
allday = day_events.select { |e| e['all_day'].to_i == 1 }
|
|
470
|
+
idx = allday.index { |e| e['id'] == evt['id'] } || 0
|
|
471
|
+
@selected_slot = -(max_allday - idx)
|
|
472
|
+
else
|
|
473
|
+
t = Time.at(evt['start_time'].to_i)
|
|
474
|
+
@selected_slot = t.hour * 2 + (t.min >= 30 ? 1 : 0)
|
|
475
|
+
@slot_offset = [@selected_slot - 5, 0].max
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
396
479
|
# --- Data loading ---
|
|
397
480
|
|
|
398
481
|
def load_events_for_range
|
|
@@ -824,7 +907,16 @@ module Timely
|
|
|
824
907
|
astro.each { |evt| lines << " #{evt}".fg(180) } if astro.any?
|
|
825
908
|
|
|
826
909
|
lines << ""
|
|
827
|
-
|
|
910
|
+
if events.any?
|
|
911
|
+
allday = events.count { |e| e['all_day'].to_i == 1 }
|
|
912
|
+
timed = events.size - allday
|
|
913
|
+
parts = []
|
|
914
|
+
parts << "#{timed} timed" if timed > 0
|
|
915
|
+
parts << "#{allday} all-day" if allday > 0
|
|
916
|
+
lines << " #{parts.join(', ')} event#{'s' if events.size > 1} today".fg(240)
|
|
917
|
+
else
|
|
918
|
+
lines << " No events scheduled".fg(240)
|
|
919
|
+
end
|
|
828
920
|
end
|
|
829
921
|
|
|
830
922
|
# Pad to fill pane
|
|
@@ -839,6 +931,7 @@ module Timely
|
|
|
839
931
|
# --- Actions ---
|
|
840
932
|
|
|
841
933
|
def go_to_date
|
|
934
|
+
blank_bottom("")
|
|
842
935
|
input = bottom_ask("Go to: ", "")
|
|
843
936
|
return if input.nil? || input.strip.empty?
|
|
844
937
|
|
|
@@ -1097,8 +1190,8 @@ module Timely
|
|
|
1097
1190
|
end
|
|
1098
1191
|
|
|
1099
1192
|
text = lines.join("\n")
|
|
1100
|
-
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) } rescue
|
|
1101
|
-
|
|
1193
|
+
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) } rescue nil
|
|
1194
|
+
IO.popen('xclip -selection primary', 'w') { |io| io.write(text) } rescue nil
|
|
1102
1195
|
show_feedback("Event copied to clipboard", 156)
|
|
1103
1196
|
end
|
|
1104
1197
|
|
|
@@ -1177,7 +1270,7 @@ module Timely
|
|
|
1177
1270
|
end
|
|
1178
1271
|
|
|
1179
1272
|
lines << ""
|
|
1180
|
-
lines << " " + "UP/DOWN:scroll ESC/q:close".fg(245)
|
|
1273
|
+
lines << " " + "UP/DOWN:scroll C-Y:copy ESC/q:close".fg(245)
|
|
1181
1274
|
|
|
1182
1275
|
popup.text = lines.join("\n")
|
|
1183
1276
|
popup.refresh
|
|
@@ -1195,6 +1288,13 @@ module Timely
|
|
|
1195
1288
|
popup.pagedown
|
|
1196
1289
|
when 'PgUP'
|
|
1197
1290
|
popup.pageup
|
|
1291
|
+
when 'C-Y'
|
|
1292
|
+
clean = lines.map { |l| l.respond_to?(:pure) ? l.pure : l.gsub(/\e\[[0-9;]*m/, '') }.join("\n")
|
|
1293
|
+
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(clean) } rescue nil
|
|
1294
|
+
IO.popen('xclip -selection primary', 'w') { |io| io.write(clean) } rescue nil
|
|
1295
|
+
lines[-1] = " " + "Copied to clipboard".fg(156)
|
|
1296
|
+
popup.text = lines.join("\n")
|
|
1297
|
+
popup.refresh
|
|
1198
1298
|
end
|
|
1199
1299
|
end
|
|
1200
1300
|
|
data/lib/timely/ui/panes.rb
CHANGED
|
@@ -14,9 +14,9 @@ module Timely
|
|
|
14
14
|
def create_panes
|
|
15
15
|
@panes = {}
|
|
16
16
|
|
|
17
|
-
# Layout: info(1) + top(months, fixed
|
|
18
|
-
# Top pane: 1 blank row + 8 month rows =
|
|
19
|
-
top_h =
|
|
17
|
+
# Layout: info(1) + top(months, fixed 10) + mid(week, flexible) + bottom(details) + status(1)
|
|
18
|
+
# Top pane: 1 blank row + 8 month rows + 1 padding row = 10
|
|
19
|
+
top_h = 10
|
|
20
20
|
bottom_h = (@h * 0.2).to_i
|
|
21
21
|
bottom_h = [bottom_h, 5].max
|
|
22
22
|
mid_h = @h - 2 - top_h - bottom_h # 2 = info + status
|
data/lib/timely/version.rb
CHANGED
data/lib/timely/weather.rb
CHANGED
|
@@ -31,6 +31,7 @@ module Timely
|
|
|
31
31
|
uri = URI("https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=#{lat}&lon=#{lon}")
|
|
32
32
|
req = Net::HTTP::Get.new(uri)
|
|
33
33
|
req['User-Agent'] = 'timely-calendar/0.1 g@isene.com'
|
|
34
|
+
req['Accept-Encoding'] = 'identity'
|
|
34
35
|
|
|
35
36
|
res = Net::HTTP.start(uri.hostname, uri.port,
|
|
36
37
|
use_ssl: true,
|
|
@@ -91,7 +92,7 @@ module Timely
|
|
|
91
92
|
if db
|
|
92
93
|
db.execute(
|
|
93
94
|
"INSERT OR REPLACE INTO weather_cache (date, hour, data, fetched_at) VALUES (?, ?, ?, ?)",
|
|
94
|
-
'forecast', '00', JSON.generate(forecast), Time.now.to_i
|
|
95
|
+
['forecast', '00', JSON.generate(forecast), Time.now.to_i]
|
|
95
96
|
)
|
|
96
97
|
end
|
|
97
98
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: timely-calendar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
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
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- CLAUDE.md
|
|
54
54
|
- README.md
|
|
55
55
|
- bin/timely
|
|
56
|
+
- img/timely-kb.svg
|
|
56
57
|
- img/timely.svg
|
|
57
58
|
- lib/timely.rb
|
|
58
59
|
- lib/timely/astronomy.rb
|