timely-calendar 1.0.1 → 1.0.3

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: db1c94b2b016e7ce47aaa79da922a03e65956b992251ab0b263f8d2f36cf965e
4
- data.tar.gz: 5d620757c14c5ffeca1a9e9b1fba5686357cf3ad573780dfa091e2a62f45450a
3
+ metadata.gz: afd143174ee0c9300fbce699d4400b3bd2dabb7761571dcbe1196857c81c46cc
4
+ data.tar.gz: '0938df470ceedda38b7ce563df8b9ece0c296bc11ba52d4e0a3fef16a19bb0f7'
5
5
  SHA512:
6
- metadata.gz: c86cfd413b6f262f4247473b982b554b395e4d554c185260ebae8c764f26a0399e94503a0ea632406a6f1fca851c1a9fc7eef30c811fb5a91a93fff97e58b1c0
7
- data.tar.gz: c8d4fb8be5e6aa6b80a2a991e93caaa8bc828cf8085621ccd17c70ad3c5c520b8f203dc4c51b9a6197e8d2b48750d480365de8f25cb9ba8f7d7c9b47c3b8c9ad
6
+ metadata.gz: f7f46d4646e632f9a9770556252eb83d377cd7b4b4b5dbc89f3bf6b0bba6ee3cf2f44bd915ef1562b8daaeb7acef139c9f9dd874bff22e5f84eac5a0f117a2f4
7
+ data.tar.gz: 6db50a84acd413e8bda0d6cdbdaff8752d643b43c1bfdeba7d6b233116a759b2f7ca82061e39ec691e09598ca5e0177e89d5cdb6276a1bf7555eca566d9970b6
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">, &lt;</text>
204
+
205
+ <rect x="638" y="286" width="62" height="55" class="key"/>
206
+ <text x="669" y="315" class="kl">. &gt;</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>
@@ -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
 
@@ -313,8 +340,14 @@ module Timely
313
340
  desc = desc.gsub(/<[^>]+>/, ' ').gsub(/&nbsp;/i, ' ').gsub(/&amp;/i, '&').gsub(/&lt;/i, '<').gsub(/&gt;/i, '>')
314
341
  end
315
342
  end
316
- # Remove common garbage from Google/Outlook descriptions
317
- desc.gsub(/BC\d+-Color:\s*-?\d+\s*/, '').gsub(/-::~:~::~:~.*$/m, '').gsub(/\s+/, ' ').strip
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(/[━═─]{3,}/, '') # Heavy/double/light box-drawing lines
349
+ .gsub(/\n{3,}/, "\n\n") # Collapse excessive blank lines
350
+ .strip
318
351
  end
319
352
 
320
353
  # Find the event at the currently selected time slot
@@ -323,11 +356,18 @@ module Timely
323
356
  events = events_on_selected_day
324
357
 
325
358
  if @selected_slot < 0
326
- # Negative slot: -n = top (first allday), -1 = bottom (last allday)
359
+ # Negative slot uses max_allday across the visible week (same as render_mid_pane)
360
+ week_start = @selected_date - (@selected_date.cwday - 1)
361
+ max_allday = 0
362
+ 7.times do |i|
363
+ day = week_start + i
364
+ n = (@events_by_date[day] || []).count { |e| e['all_day'].to_i == 1 }
365
+ max_allday = n if n > max_allday
366
+ end
327
367
  allday = events.select { |e| e['all_day'].to_i == 1 }
328
- n = allday.size
329
- idx = n - @selected_slot.abs # -n -> 0 (first), -1 -> n-1 (last)
330
- return allday[idx]
368
+ idx = max_allday - @selected_slot.abs # -max -> 0 (first), -1 -> max-1 (last)
369
+ return allday[idx] if idx >= 0 && idx < allday.size
370
+ return nil
331
371
  end
332
372
 
333
373
  hour = @selected_slot / 2
@@ -363,6 +403,7 @@ module Timely
363
403
  # If there are more events on the current day after the selected one, go to next
364
404
  if events.length > 0 && @selected_event_index < events.length - 1
365
405
  @selected_event_index += 1
406
+ move_slot_to_event(events[@selected_event_index], events)
366
407
  render_mid_pane
367
408
  render_bottom_pane
368
409
  return
@@ -375,7 +416,10 @@ module Timely
375
416
  if day_events && !day_events.empty?
376
417
  @selected_date = check_date
377
418
  @selected_event_index = 0
378
- date_changed
419
+ load_events_for_range
420
+ events = events_on_selected_day
421
+ move_slot_to_event(events.first, events) if events.any?
422
+ render_all
379
423
  return
380
424
  end
381
425
  end
@@ -388,6 +432,7 @@ module Timely
388
432
  # If there are more events on the current day before the selected one, go to prev
389
433
  if events.length > 0 && @selected_event_index > 0
390
434
  @selected_event_index -= 1
435
+ move_slot_to_event(events[@selected_event_index], events)
391
436
  render_mid_pane
392
437
  render_bottom_pane
393
438
  return
@@ -399,8 +444,11 @@ module Timely
399
444
  day_events = @db.get_events_for_date(check_date)
400
445
  if day_events && !day_events.empty?
401
446
  @selected_date = check_date
402
- @selected_event_index = day_events.length - 1
403
- date_changed
447
+ load_events_for_range
448
+ events = events_on_selected_day
449
+ @selected_event_index = [events.length - 1, 0].max
450
+ move_slot_to_event(events.last, events) if events.any?
451
+ render_all
404
452
  return
405
453
  end
406
454
  end
@@ -408,6 +456,27 @@ module Timely
408
456
  show_feedback("No earlier events found within the past year", 245)
409
457
  end
410
458
 
459
+ def move_slot_to_event(evt, day_events)
460
+ return unless evt
461
+ if evt['all_day'].to_i == 1
462
+ # Compute max_allday across the visible week (same as render_mid_pane)
463
+ week_start = @selected_date - (@selected_date.cwday - 1)
464
+ max_allday = 0
465
+ 7.times do |i|
466
+ day = week_start + i
467
+ n = (@events_by_date[day] || []).count { |e| e['all_day'].to_i == 1 }
468
+ max_allday = n if n > max_allday
469
+ end
470
+ allday = day_events.select { |e| e['all_day'].to_i == 1 }
471
+ idx = allday.index { |e| e['id'] == evt['id'] } || 0
472
+ @selected_slot = -(max_allday - idx)
473
+ else
474
+ t = Time.at(evt['start_time'].to_i)
475
+ @selected_slot = t.hour * 2 + (t.min >= 30 ? 1 : 0)
476
+ @slot_offset = [@selected_slot - 5, 0].max
477
+ end
478
+ end
479
+
411
480
  # --- Data loading ---
412
481
 
413
482
  def load_events_for_range
@@ -839,7 +908,16 @@ module Timely
839
908
  astro.each { |evt| lines << " #{evt}".fg(180) } if astro.any?
840
909
 
841
910
  lines << ""
842
- lines << " No events scheduled".fg(240)
911
+ if events.any?
912
+ allday = events.count { |e| e['all_day'].to_i == 1 }
913
+ timed = events.size - allday
914
+ parts = []
915
+ parts << "#{timed} timed" if timed > 0
916
+ parts << "#{allday} all-day" if allday > 0
917
+ lines << " #{parts.join(', ')} event#{'s' if events.size > 1} today".fg(240)
918
+ else
919
+ lines << " No events scheduled".fg(240)
920
+ end
843
921
  end
844
922
 
845
923
  # Pad to fill pane
@@ -854,6 +932,7 @@ module Timely
854
932
  # --- Actions ---
855
933
 
856
934
  def go_to_date
935
+ blank_bottom("")
857
936
  input = bottom_ask("Go to: ", "")
858
937
  return if input.nil? || input.strip.empty?
859
938
 
@@ -1112,8 +1191,8 @@ module Timely
1112
1191
  end
1113
1192
 
1114
1193
  text = lines.join("\n")
1115
- IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) } rescue
1116
- IO.popen('xsel --clipboard --input', 'w') { |io| io.write(text) } rescue nil
1194
+ IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) } rescue nil
1195
+ IO.popen('xclip -selection primary', 'w') { |io| io.write(text) } rescue nil
1117
1196
  show_feedback("Event copied to clipboard", 156)
1118
1197
  end
1119
1198
 
@@ -1192,7 +1271,7 @@ module Timely
1192
1271
  end
1193
1272
 
1194
1273
  lines << ""
1195
- lines << " " + "UP/DOWN:scroll ESC/q:close".fg(245)
1274
+ lines << " " + "UP/DOWN:scroll C-Y:copy ESC/q:close".fg(245)
1196
1275
 
1197
1276
  popup.text = lines.join("\n")
1198
1277
  popup.refresh
@@ -1210,6 +1289,13 @@ module Timely
1210
1289
  popup.pagedown
1211
1290
  when 'PgUP'
1212
1291
  popup.pageup
1292
+ when 'C-Y'
1293
+ clean = lines.map { |l| l.respond_to?(:pure) ? l.pure : l.gsub(/\e\[[0-9;]*m/, '') }.join("\n")
1294
+ IO.popen('xclip -selection clipboard', 'w') { |io| io.write(clean) } rescue nil
1295
+ IO.popen('xclip -selection primary', 'w') { |io| io.write(clean) } rescue nil
1296
+ lines[-1] = " " + "Copied to clipboard".fg(156)
1297
+ popup.text = lines.join("\n")
1298
+ popup.refresh
1213
1299
  end
1214
1300
  end
1215
1301
 
@@ -14,9 +14,9 @@ module Timely
14
14
  def create_panes
15
15
  @panes = {}
16
16
 
17
- # Layout: info(1) + top(months, fixed 9) + mid(week, flexible) + bottom(details) + status(1)
18
- # Top pane: 1 blank row + 8 month rows = 9
19
- top_h = 9
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
@@ -1,3 +1,3 @@
1
1
  module Timely
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.3'
3
3
  end
@@ -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.1
4
+ version: 1.0.3
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
@@ -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