norcal 1.0.0 → 1.1.0
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 +4 -1
- data/bin/norcal +169 -49
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01d60640ac4be6724791dd1a510ca21f17748c5cebb705c2acc33864353ee3f2
|
|
4
|
+
data.tar.gz: 6f393645e5b43e5671d71acbfdb132a32aa9add25524e725ae8f25a878879b7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c181d09cbb95b52c24e220c9dc3293fe738d2614cff48d1e65766b4ef18aa2a48e33c8998fb1a420a4d2b12633fd1f2abb881b6501cb597beca45c5ff3ac7cfb
|
|
7
|
+
data.tar.gz: 331b2f5093a691edb71a32b36c906b0c76471baf0cfc38555bc540935ede4d8c88bb73381f815110f59bb7f31bb9bd29e119097c998c41d1de62de8a3ef8898b
|
data/README.md
CHANGED
|
@@ -31,7 +31,7 @@ gem install norcal
|
|
|
31
31
|
git clone https://github.com/baosen/norcal.git
|
|
32
32
|
cd norcal
|
|
33
33
|
gem build norcal.gemspec
|
|
34
|
-
gem install norcal-1.
|
|
34
|
+
gem install norcal-1.1.0.gem
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## Usage
|
|
@@ -52,6 +52,9 @@ norcal --light 2026 # light mode, specific year
|
|
|
52
52
|
- Notable dates: royal birthdays, Samefolkets dag, Morsdag, Farsdag, solverv, sommertid, and more
|
|
53
53
|
- Today highlighted with yellow background
|
|
54
54
|
- Dark mode (default) and light mode, with toggle button
|
|
55
|
+
- Zoom in/out (`+`/`–` buttons, Ctrl+scroll, Ctrl++/Ctrl+-)
|
|
56
|
+
- Auto-fit to screen height on startup, with `fit` button
|
|
57
|
+
- Scrollbar for zoomed-in views
|
|
55
58
|
|
|
56
59
|
## License
|
|
57
60
|
|
data/bin/norcal
CHANGED
|
@@ -66,7 +66,7 @@ def red_days(year)
|
|
|
66
66
|
e - 2 => "Langfredag",
|
|
67
67
|
e => "1. påskedag",
|
|
68
68
|
e + 1 => "2. påskedag",
|
|
69
|
-
Date.new(year, 5, 1) => "
|
|
69
|
+
Date.new(year, 5, 1) => "Arbeidernes dag",
|
|
70
70
|
e + 39 => "Kr. Himmelfartsdag",
|
|
71
71
|
Date.new(year, 5, 17) => "Grunnlovsdag",
|
|
72
72
|
e + 49 => "1. pinsedag",
|
|
@@ -102,7 +102,7 @@ def notable_dates(year)
|
|
|
102
102
|
[e - 2, "Langfredag", true],
|
|
103
103
|
[e, "1. påskedag", true],
|
|
104
104
|
[e + 1, "2. påskedag", true],
|
|
105
|
-
[Date.new(year, 5, 1), "
|
|
105
|
+
[Date.new(year, 5, 1), "Arbeidernes dag", true],
|
|
106
106
|
[Date.new(year, 5, 8), "Frigjøringsdag 1945", false],
|
|
107
107
|
[e + 39, "Kr. Himmelfartsdag", true],
|
|
108
108
|
[Date.new(year, 5, 17), "Grunnlovsdag 1814", true],
|
|
@@ -148,29 +148,35 @@ THEME = {
|
|
|
148
148
|
# Parse arguments: optional --light flag and optional year (dark is default)
|
|
149
149
|
$dark = ARGV.delete('--light') ? false : true
|
|
150
150
|
$year = (ARGV[0] || Date.today.year).to_i
|
|
151
|
+
$zoom = 0
|
|
151
152
|
|
|
152
153
|
def theme
|
|
153
154
|
THEME[$dark ? :dark : :light]
|
|
154
155
|
end
|
|
155
156
|
|
|
157
|
+
FONT = 'Noto Serif'
|
|
158
|
+
|
|
159
|
+
def make_fonts
|
|
160
|
+
z = $zoom
|
|
161
|
+
{
|
|
162
|
+
title: TkFont.new(family: FONT, size: 22 + z, weight: 'bold'),
|
|
163
|
+
mheader: TkFont.new(family: FONT, size: 13 + z, weight: 'bold'),
|
|
164
|
+
cal: TkFont.new(family: FONT, size: 12 + z),
|
|
165
|
+
cal_bold: TkFont.new(family: FONT, size: 12 + z, weight: 'bold'),
|
|
166
|
+
note: TkFont.new(family: FONT, size: 11 + z),
|
|
167
|
+
note_b: TkFont.new(family: FONT, size: 11 + z, weight: 'bold'),
|
|
168
|
+
note_d: TkFont.new(family: FONT, size: 11 + z),
|
|
169
|
+
btn: TkFont.new(family: FONT, size: 9 + z),
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
156
173
|
root = TkRoot.new do
|
|
157
174
|
title "norcal"
|
|
158
175
|
end
|
|
159
176
|
|
|
160
|
-
# Fonts
|
|
161
|
-
ft_title = TkFont.new(family: 'Noto Serif', size: 22, weight: 'bold')
|
|
162
|
-
ft_mheader = TkFont.new(family: 'Noto Serif', size: 13, weight: 'bold')
|
|
163
|
-
ft_cal = TkFont.new(family: 'Noto Serif', size: 12)
|
|
164
|
-
ft_cal_bold = TkFont.new(family: 'Noto Serif', size: 12, weight: 'bold')
|
|
165
|
-
ft_note = TkFont.new(family: 'Noto Serif', size: 11)
|
|
166
|
-
ft_note_b = TkFont.new(family: 'Noto Serif', size: 11, weight: 'bold')
|
|
167
|
-
ft_note_d = TkFont.new(family: 'Noto Serif', size: 11)
|
|
168
|
-
ft_toggle = TkFont.new(family: 'Noto Serif', size: 9)
|
|
169
|
-
|
|
170
177
|
# -- Calendar content ----------------------------------------------------------
|
|
171
178
|
|
|
172
|
-
def render_month(parent, year, month, holidays,
|
|
173
|
-
ft_mheader, ft_cal, ft_cal_bold = fonts
|
|
179
|
+
def render_month(parent, year, month, holidays, f)
|
|
174
180
|
t = theme
|
|
175
181
|
|
|
176
182
|
first = Date.new(year, month, 1)
|
|
@@ -188,12 +194,12 @@ def render_month(parent, year, month, holidays, fonts)
|
|
|
188
194
|
|
|
189
195
|
# Month name header
|
|
190
196
|
TkLabel.new(grid) {
|
|
191
|
-
text MONTHS_NO[month - 1]; font
|
|
197
|
+
text MONTHS_NO[month - 1]; font f[:mheader]; foreground t[:fg]; background t[:bg]
|
|
192
198
|
}.grid(row: 0, column: 0, columnspan: 8, pady: [0, 2])
|
|
193
199
|
|
|
194
200
|
# Day-name headers
|
|
195
201
|
TkLabel.new(grid) {
|
|
196
|
-
text 'Uke'; font
|
|
202
|
+
text 'Uke'; font f[:cal]; foreground t[:dkgray]; background t[:bg]
|
|
197
203
|
}.grid(row: 1, column: 0, sticky: 'e', padx: [0, 4])
|
|
198
204
|
|
|
199
205
|
DAYS_NO.each_with_index do |d, i|
|
|
@@ -203,7 +209,7 @@ def render_month(parent, year, month, holidays, fonts)
|
|
|
203
209
|
else t[:fg]
|
|
204
210
|
end
|
|
205
211
|
TkLabel.new(grid) {
|
|
206
|
-
text d; font
|
|
212
|
+
text d; font f[:cal_bold]; foreground fg; background t[:bg]
|
|
207
213
|
}.grid(row: 1, column: i + 1, sticky: 'e')
|
|
208
214
|
end
|
|
209
215
|
|
|
@@ -212,7 +218,7 @@ def render_month(parent, year, month, holidays, fonts)
|
|
|
212
218
|
cur = monday
|
|
213
219
|
while cur <= last_day
|
|
214
220
|
TkLabel.new(grid) {
|
|
215
|
-
text cur.cweek.to_s; font
|
|
221
|
+
text cur.cweek.to_s; font f[:cal]; foreground t[:dkgray]; background t[:bg]
|
|
216
222
|
}.grid(row: row, column: 0, sticky: 'e', padx: [0, 4])
|
|
217
223
|
|
|
218
224
|
7.times do |i|
|
|
@@ -227,7 +233,7 @@ def render_month(parent, year, month, holidays, fonts)
|
|
|
227
233
|
end
|
|
228
234
|
TkLabel.new(grid) {
|
|
229
235
|
text day.day.to_s
|
|
230
|
-
font(is_today ?
|
|
236
|
+
font(is_today ? f[:cal_bold] : f[:cal])
|
|
231
237
|
foreground fg
|
|
232
238
|
background(is_today ? t[:today_bg] : t[:bg])
|
|
233
239
|
}.grid(row: row, column: i + 1, sticky: 'e')
|
|
@@ -239,7 +245,7 @@ def render_month(parent, year, month, holidays, fonts)
|
|
|
239
245
|
|
|
240
246
|
# Pad empty rows so all months have the same height (max 6 week rows)
|
|
241
247
|
while row < 8
|
|
242
|
-
TkLabel.new(grid) { text ' '; font
|
|
248
|
+
TkLabel.new(grid) { text ' '; font f[:cal]; background t[:bg] }
|
|
243
249
|
.grid(row: row, column: 0)
|
|
244
250
|
row += 1
|
|
245
251
|
end
|
|
@@ -250,8 +256,7 @@ def render_month(parent, year, month, holidays, fonts)
|
|
|
250
256
|
outer
|
|
251
257
|
end
|
|
252
258
|
|
|
253
|
-
def render_notable(parent, year,
|
|
254
|
-
ft_note, ft_note_b, ft_note_d = fonts
|
|
259
|
+
def render_notable(parent, year, f)
|
|
255
260
|
t = theme
|
|
256
261
|
dates = notable_dates(year)
|
|
257
262
|
|
|
@@ -270,14 +275,14 @@ def render_notable(parent, year, fonts)
|
|
|
270
275
|
cols.each_with_index do |entries, ci|
|
|
271
276
|
tw = TkText.new(inner) {
|
|
272
277
|
width 1; height(entries.size + 4)
|
|
273
|
-
font
|
|
278
|
+
font f[:note]; borderwidth 0; highlightthickness 0
|
|
274
279
|
wrap 'word'; background t[:bg]; padx 3; pady 2
|
|
275
280
|
cursor ''; insertwidth 0
|
|
276
281
|
tabs '65'
|
|
277
282
|
}
|
|
278
|
-
tw.tag_configure('dt', foreground: t[:fg], font:
|
|
279
|
-
tw.tag_configure('dtr', foreground: t[:red], font:
|
|
280
|
-
tw.tag_configure('desc', foreground: t[:desc_fg], font:
|
|
283
|
+
tw.tag_configure('dt', foreground: t[:fg], font: f[:note_b])
|
|
284
|
+
tw.tag_configure('dtr', foreground: t[:red], font: f[:note_b])
|
|
285
|
+
tw.tag_configure('desc', foreground: t[:desc_fg], font: f[:note_d])
|
|
281
286
|
|
|
282
287
|
entries.each_with_index do |(date, desc, is_red), i|
|
|
283
288
|
dtag = is_red ? 'dtr' : 'dt'
|
|
@@ -303,8 +308,7 @@ def render_notable(parent, year, fonts)
|
|
|
303
308
|
outer
|
|
304
309
|
end
|
|
305
310
|
|
|
306
|
-
def build_calendar(parent, year,
|
|
307
|
-
ft_mheader, ft_cal, ft_cal_bold, ft_note, ft_note_b, ft_note_d = fonts
|
|
311
|
+
def build_calendar(parent, year, f)
|
|
308
312
|
t = theme
|
|
309
313
|
holidays = red_days(year)
|
|
310
314
|
|
|
@@ -315,55 +319,171 @@ def build_calendar(parent, year, fonts)
|
|
|
315
319
|
grid_f.pack(padx: 10, pady: [2, 8])
|
|
316
320
|
|
|
317
321
|
12.times do |idx|
|
|
318
|
-
m_frame = render_month(grid_f, year, idx + 1, holidays,
|
|
319
|
-
[ft_mheader, ft_cal, ft_cal_bold])
|
|
322
|
+
m_frame = render_month(grid_f, year, idx + 1, holidays, f)
|
|
320
323
|
m_frame.grid(row: idx / 3, column: idx % 3, padx: 3, pady: 3, sticky: 'nsew')
|
|
321
324
|
end
|
|
322
325
|
|
|
323
326
|
# Notable dates in row 4, spanning all 3 month columns
|
|
324
|
-
note_f = render_notable(grid_f, year,
|
|
327
|
+
note_f = render_notable(grid_f, year, f)
|
|
325
328
|
note_f.grid(row: 4, column: 0, columnspan: 3, padx: 3, pady: [4, 0], sticky: 'ew')
|
|
326
329
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
# Button bar: toggle + zoom
|
|
331
|
+
btn_bar = TkFrame.new(grid_f) { background t[:bg] }
|
|
332
|
+
btn_bar.grid(row: 5, column: 0, columnspan: 3, pady: [4, 0])
|
|
333
|
+
|
|
334
|
+
TkButton.new(btn_bar) {
|
|
335
|
+
text t[:toggle_label]; font f[:btn]
|
|
330
336
|
relief 'flat'; borderwidth 0
|
|
331
337
|
foreground t[:dkgray]; background t[:bg]
|
|
332
338
|
activebackground t[:bg]; activeforeground t[:fg]
|
|
333
339
|
command { $dark = !$dark; $rebuild&.call }
|
|
334
|
-
}.
|
|
340
|
+
}.pack(side: 'left', padx: 4)
|
|
341
|
+
|
|
342
|
+
TkButton.new(btn_bar) {
|
|
343
|
+
text '+'; font f[:btn]
|
|
344
|
+
relief 'flat'; borderwidth 0
|
|
345
|
+
foreground t[:dkgray]; background t[:bg]
|
|
346
|
+
activebackground t[:bg]; activeforeground t[:fg]
|
|
347
|
+
command { $zoom += 1; $rebuild&.call }
|
|
348
|
+
}.pack(side: 'left', padx: 2)
|
|
349
|
+
|
|
350
|
+
TkButton.new(btn_bar) {
|
|
351
|
+
text "\u2013"; font f[:btn] # en-dash for minus
|
|
352
|
+
relief 'flat'; borderwidth 0
|
|
353
|
+
foreground t[:dkgray]; background t[:bg]
|
|
354
|
+
activebackground t[:bg]; activeforeground t[:fg]
|
|
355
|
+
command { $zoom -= 1 if $zoom > -8; $rebuild&.call }
|
|
356
|
+
}.pack(side: 'left', padx: 2)
|
|
357
|
+
|
|
358
|
+
TkButton.new(btn_bar) {
|
|
359
|
+
text 'fit'; font f[:btn]
|
|
360
|
+
relief 'flat'; borderwidth 0
|
|
361
|
+
foreground t[:dkgray]; background t[:bg]
|
|
362
|
+
activebackground t[:bg]; activeforeground t[:fg]
|
|
363
|
+
command { $fit&.call }
|
|
364
|
+
}.pack(side: 'left', padx: 2)
|
|
335
365
|
|
|
336
366
|
3.times { |c| grid_f.grid_columnconfigure(c, weight: 1) }
|
|
337
367
|
|
|
338
368
|
frame
|
|
339
369
|
end
|
|
340
370
|
|
|
341
|
-
# --
|
|
371
|
+
# -- Scrollable wrapper --------------------------------------------------------
|
|
342
372
|
|
|
343
|
-
$
|
|
344
|
-
$
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
373
|
+
$scrollbar = TkScrollbar.new(root)
|
|
374
|
+
$canvas = TkCanvas.new(root) {
|
|
375
|
+
highlightthickness 0; borderwidth 0
|
|
376
|
+
}
|
|
377
|
+
$canvas.configure(yscrollcommand: proc { |*a| $scrollbar.set(*a) })
|
|
378
|
+
$scrollbar.command(proc { |*a| $canvas.yview(*a) })
|
|
379
|
+
|
|
380
|
+
$scrollbar.pack(side: 'right', fill: 'y')
|
|
381
|
+
$canvas.pack(side: 'left', fill: 'both', expand: true)
|
|
382
|
+
|
|
383
|
+
# Mouse wheel scrolling
|
|
384
|
+
root.bind('MouseWheel') { |e| $canvas.yview_scroll(-e.delta / 120, 'units') }
|
|
385
|
+
root.bind('Button-4') { $canvas.yview_scroll(-3, 'units') }
|
|
386
|
+
root.bind('Button-5') { $canvas.yview_scroll(3, 'units') }
|
|
387
|
+
|
|
388
|
+
# Keyboard zoom: Ctrl++ and Ctrl+-
|
|
389
|
+
root.bind('Control-plus') { $zoom += 1; $rebuild&.call }
|
|
390
|
+
root.bind('Control-equal') { $zoom += 1; $rebuild&.call } # Ctrl+= (unshifted +)
|
|
391
|
+
root.bind('Control-minus') { $zoom -= 1 if $zoom > -8; $rebuild&.call }
|
|
392
|
+
|
|
393
|
+
# Ctrl+mouse wheel zoom
|
|
394
|
+
root.bind('Control-MouseWheel') { |e|
|
|
395
|
+
if e.delta > 0 then $zoom += 1 else $zoom -= 1 if $zoom > -8 end
|
|
396
|
+
$rebuild&.call
|
|
348
397
|
}
|
|
349
|
-
$
|
|
398
|
+
root.bind('Control-Button-4') { $zoom += 1; $rebuild&.call }
|
|
399
|
+
root.bind('Control-Button-5') { $zoom -= 1 if $zoom > -8; $rebuild&.call }
|
|
350
400
|
|
|
351
401
|
# -- Build & rebuild -----------------------------------------------------------
|
|
352
402
|
|
|
353
|
-
|
|
354
|
-
$content = nil
|
|
403
|
+
$inner_frame = nil
|
|
355
404
|
$rebuild = nil
|
|
356
405
|
|
|
357
406
|
$rebuild = proc {
|
|
358
407
|
t = theme
|
|
359
|
-
|
|
408
|
+
f = make_fonts
|
|
409
|
+
|
|
410
|
+
$inner_frame&.destroy
|
|
360
411
|
root.configure(background: t[:bg])
|
|
361
|
-
$
|
|
362
|
-
$
|
|
412
|
+
$canvas.configure(background: t[:bg])
|
|
413
|
+
$scrollbar.configure(background: t[:bg], troughcolor: t[:bg])
|
|
363
414
|
root.title = "norcal - #{$year}"
|
|
364
|
-
|
|
365
|
-
$
|
|
415
|
+
|
|
416
|
+
$inner_frame = TkFrame.new($canvas) { background t[:bg] }
|
|
417
|
+
|
|
418
|
+
# Title
|
|
419
|
+
TkLabel.new($inner_frame) {
|
|
420
|
+
text $year.to_s; font f[:title]; foreground t[:title_fg]; background t[:bg]
|
|
421
|
+
}.pack(pady: [8, 2])
|
|
422
|
+
|
|
423
|
+
# Calendar content
|
|
424
|
+
build_calendar($inner_frame, $year, f).pack(fill: 'both', expand: true)
|
|
425
|
+
|
|
426
|
+
# Embed frame in canvas, centered
|
|
427
|
+
$canvas.delete('all')
|
|
428
|
+
$canvas_win = $canvas.create(:window, 0, 0, window: $inner_frame, anchor: 'nw')
|
|
429
|
+
|
|
430
|
+
# Center content and update scroll region on resize
|
|
431
|
+
center_content = proc {
|
|
432
|
+
cw = $canvas.winfo_width
|
|
433
|
+
ch = $canvas.winfo_height
|
|
434
|
+
fw = $inner_frame.winfo_reqwidth
|
|
435
|
+
fh = $inner_frame.winfo_reqheight
|
|
436
|
+
x = [(cw - fw) / 2, 0].max
|
|
437
|
+
y = [(ch - fh) / 2, 0].max
|
|
438
|
+
$canvas.coords($canvas_win, x, y)
|
|
439
|
+
$canvas.configure(scrollregion: "#{0} #{0} #{[cw, fw].max} #{[ch, fh].max}")
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
$inner_frame.bind('Configure', center_content)
|
|
443
|
+
$canvas.bind('Configure', center_content)
|
|
366
444
|
}
|
|
367
445
|
|
|
446
|
+
# Fit-to-window: zoom so content fills available height, then resize window
|
|
447
|
+
$fit = proc {
|
|
448
|
+
# Measure current content height and extrapolate target zoom
|
|
449
|
+
cur_h = $inner_frame.winfo_reqheight.to_f
|
|
450
|
+
screen_h = root.winfo_screenheight - 80
|
|
451
|
+
base_avg = 13.0 # average base font size at zoom 0
|
|
452
|
+
|
|
453
|
+
if cur_h > 0 && screen_h > 0
|
|
454
|
+
# Current effective font size, and what it needs to be
|
|
455
|
+
cur_font = base_avg + $zoom
|
|
456
|
+
target_font = cur_font * (screen_h / cur_h)
|
|
457
|
+
$zoom = (target_font - base_avg).floor
|
|
458
|
+
$zoom = [$zoom, -8].max
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
$rebuild.call
|
|
462
|
+
Tk.update_idletasks
|
|
463
|
+
|
|
464
|
+
# If overshot, back off by one
|
|
465
|
+
if $inner_frame.winfo_reqheight > screen_h
|
|
466
|
+
$zoom -= 1
|
|
467
|
+
$rebuild.call
|
|
468
|
+
Tk.update_idletasks
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Size window to actual content, capped at screen
|
|
472
|
+
content_w = $inner_frame.winfo_reqwidth + 20
|
|
473
|
+
content_h = $inner_frame.winfo_reqheight
|
|
474
|
+
screen_w = root.winfo_screenwidth
|
|
475
|
+
max_h = root.winfo_screenheight - 50
|
|
476
|
+
w = [content_w, screen_w].min
|
|
477
|
+
h = [content_h, max_h].min
|
|
478
|
+
x = (screen_w - w) / 2
|
|
479
|
+
root.geometry("#{w}x#{h}+#{x}+0")
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Initial build, then fit to window (withdraw during startup to avoid flicker)
|
|
483
|
+
root.withdraw
|
|
368
484
|
$rebuild.call
|
|
485
|
+
Tk.update_idletasks
|
|
486
|
+
$fit.call
|
|
487
|
+
root.deiconify
|
|
488
|
+
|
|
369
489
|
Tk.mainloop
|