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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -1
  3. data/bin/norcal +169 -49
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be5696e7434a165a2ef025c8152ddd359795d5cf6e1a42d6dae6a32c2f636e90
4
- data.tar.gz: fd6398b5697251b1c28c1c0b20c8fb3d99bc6b08980004612d37c94ea00e3469
3
+ metadata.gz: 01d60640ac4be6724791dd1a510ca21f17748c5cebb705c2acc33864353ee3f2
4
+ data.tar.gz: 6f393645e5b43e5671d71acbfdb132a32aa9add25524e725ae8f25a878879b7d
5
5
  SHA512:
6
- metadata.gz: f8e7e3cf2c3e977ba03769fa6c215c18e2ff5fe13f7439b36ed18108f4db5d7ce6d12ef0cb9076ff88b70f923a30e2815b422d79a4da0de80a7de0d4a8192abb
7
- data.tar.gz: d2f1258004cd2b3b3a0fbc0a501fd94ef6bd0b5df63145747c899d68916718ffa9fcdb181d3ea46b7355b4672df1c8056f053b5988d2c7632f81a71093c2e42a
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.0.0.gem
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) => "Off. høgtidsdag",
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), "Off. høgtidsdag", true],
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, fonts)
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 ft_mheader; foreground t[:fg]; background t[:bg]
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 ft_cal; foreground t[:dkgray]; background t[:bg]
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 ft_cal_bold; foreground fg; background t[:bg]
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 ft_cal; foreground t[:dkgray]; background t[:bg]
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 ? ft_cal_bold : ft_cal)
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 ft_cal; background t[:bg] }
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, fonts)
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 ft_note; borderwidth 0; highlightthickness 0
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: ft_note_b)
279
- tw.tag_configure('dtr', foreground: t[:red], font: ft_note_b)
280
- tw.tag_configure('desc', foreground: t[:desc_fg], font: ft_note_d)
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, fonts)
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, [ft_note, ft_note_b, ft_note_d])
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
- # Toggle button below notable dates
328
- TkButton.new(grid_f) {
329
- text t[:toggle_label]; font fonts[5] # ft_note_d
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
- }.grid(row: 5, column: 0, columnspan: 3, pady: [4, 0])
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
- # -- Title ---------------------------------------------------------------------
371
+ # -- Scrollable wrapper --------------------------------------------------------
342
372
 
343
- $top = TkFrame.new(root) { background theme[:bg] }
344
- $top.pack(fill: 'x', pady: [8, 2])
345
-
346
- $title_lbl = TkLabel.new($top) {
347
- text $year.to_s; font ft_title; foreground theme[:title_fg]; background theme[:bg]
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
- $title_lbl.pack
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
- all_fonts = [ft_mheader, ft_cal, ft_cal_bold, ft_note, ft_note_b, ft_note_d]
354
- $content = nil
403
+ $inner_frame = nil
355
404
  $rebuild = nil
356
405
 
357
406
  $rebuild = proc {
358
407
  t = theme
359
- $content&.destroy
408
+ f = make_fonts
409
+
410
+ $inner_frame&.destroy
360
411
  root.configure(background: t[:bg])
361
- $top.configure(background: t[:bg])
362
- $title_lbl.configure(foreground: t[:title_fg], background: t[:bg])
412
+ $canvas.configure(background: t[:bg])
413
+ $scrollbar.configure(background: t[:bg], troughcolor: t[:bg])
363
414
  root.title = "norcal - #{$year}"
364
- $content = build_calendar(root, $year, all_fonts)
365
- $content.pack(fill: 'both', expand: true)
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: norcal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - baosen