hiiro 0.1.81 → 0.1.82

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.
data/bin/h-remind DELETED
@@ -1,614 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'yaml'
4
- require 'fileutils'
5
- require 'time'
6
- require 'tempfile'
7
- require 'hiiro'
8
-
9
- class ReminderManager
10
- REMINDERS_FILE = File.join(Dir.home, '.config/hiiro/reminders.yml')
11
- REMINDER_TEMPLATE = {
12
- 'message' => '',
13
- 'interval' => '1h',
14
- 'sound' => nil,
15
- 'enabled' => true,
16
- 'once' => false,
17
- 'trigger_at' => nil,
18
- 'last_triggered' => nil,
19
- 'created_at' => Time.now.iso8601,
20
- }
21
-
22
- class Reminder
23
- attr_accessor :message, :interval, :sound, :enabled, :once, :trigger_at, :last_triggered, :created_at
24
-
25
- def initialize(message:, interval: '1h', sound: nil, enabled: true, once: false, trigger_at: nil, last_triggered: nil, created_at: Time.now.iso8601)
26
- @message = message
27
- @interval = interval
28
- @sound = sound
29
- @enabled = enabled
30
- @once = once
31
- @trigger_at = trigger_at
32
- @last_triggered = last_triggered
33
- @created_at = created_at
34
- end
35
-
36
- def self.from_hash(hash)
37
- new(
38
- message: hash['message'],
39
- interval: hash['interval'] || '1h',
40
- sound: hash['sound'],
41
- enabled: hash['enabled'] != false,
42
- once: hash['once'] == true,
43
- trigger_at: hash['trigger_at'],
44
- last_triggered: hash['last_triggered'],
45
- created_at: hash['created_at'] || Time.now.iso8601
46
- )
47
- end
48
-
49
- def to_h
50
- {
51
- 'message' => @message,
52
- 'interval' => @interval,
53
- 'sound' => @sound,
54
- 'enabled' => @enabled,
55
- 'once' => @once,
56
- 'trigger_at' => @trigger_at,
57
- 'last_triggered' => @last_triggered,
58
- 'created_at' => @created_at
59
- }
60
- end
61
-
62
- def to_yaml
63
- to_h.to_yaml
64
- end
65
-
66
- def interval_seconds
67
- parse_interval(@interval)
68
- end
69
-
70
- def parse_interval(str)
71
- return 3600 unless str # default 1 hour
72
-
73
- match = str.to_s.match(/^(\d+)([smhdw])$/i)
74
- return 3600 unless match
75
-
76
- num = match[1].to_i
77
- unit = match[2].downcase
78
-
79
- case unit
80
- when 's' then num
81
- when 'm' then num * 60
82
- when 'h' then num * 3600
83
- when 'd' then num * 86400
84
- when 'w' then num * 604800
85
- else 3600
86
- end
87
- end
88
-
89
- def one_time?
90
- @once == true
91
- end
92
-
93
- def should_trigger?
94
- return false unless @enabled
95
-
96
- if one_time?
97
- # One-time reminder: check if trigger_at time has passed
98
- return false if @last_triggered # already triggered
99
- return false unless @trigger_at
100
-
101
- trigger_time = Time.parse(@trigger_at) rescue nil
102
- return false unless trigger_time
103
-
104
- Time.now >= trigger_time
105
- else
106
- # Recurring reminder: check interval since last trigger
107
- return true if @last_triggered.nil?
108
-
109
- last_time = Time.parse(@last_triggered) rescue Time.at(0)
110
- Time.now - last_time >= interval_seconds
111
- end
112
- end
113
-
114
- def trigger!
115
- @last_triggered = Time.now.iso8601
116
- # For one-time reminders, disable after triggering
117
- @enabled = false if one_time?
118
- end
119
-
120
- def display_string(index = nil)
121
- num = index ? "#{(index + 1).to_s.rjust(3)}." : ""
122
- status = @enabled ? "[ON]" : "[OFF]"
123
- sound_str = @sound ? " (#{@sound})" : ""
124
-
125
- if one_time?
126
- if @trigger_at
127
- trigger_time = Time.parse(@trigger_at) rescue nil
128
- time_str = trigger_time ? trigger_time.strftime('%Y-%m-%d %H:%M') : @trigger_at
129
- "#{num} #{status} [ONCE @ #{time_str}] #{@message}#{sound_str}".strip
130
- else
131
- "#{num} #{status} [ONCE] #{@message}#{sound_str}".strip
132
- end
133
- else
134
- "#{num} #{status} [#{@interval}] #{@message}#{sound_str}".strip
135
- end
136
- end
137
- end
138
-
139
- # Parse time argument - returns [is_once, trigger_at, interval]
140
- def self.parse_time_arg(time_str)
141
- return [false, nil, '1h'] if time_str.nil?
142
-
143
- # Check for absolute time format: YYYYmmddHHMM
144
- if time_str =~ /^(\d{12})$/
145
- year = $1[0..3].to_i
146
- month = $1[4..5].to_i
147
- day = $1[6..7].to_i
148
- hour = $1[8..9].to_i
149
- min = $1[10..11].to_i
150
- trigger_at = Time.new(year, month, day, hour, min).iso8601
151
- return [true, trigger_at, nil]
152
- end
153
-
154
- # Check for relative interval format: 15m, 2h, etc.
155
- if time_str =~ /^(\d+)([smhdw])$/i
156
- num = $1.to_i
157
- unit = $2.downcase
158
- seconds = case unit
159
- when 's' then num
160
- when 'm' then num * 60
161
- when 'h' then num * 3600
162
- when 'd' then num * 86400
163
- when 'w' then num * 604800
164
- else num * 60
165
- end
166
-
167
- # This is a one-time reminder at a relative time
168
- trigger_at = (Time.now + seconds).iso8601
169
- return [true, trigger_at, time_str]
170
- end
171
-
172
- # Default: treat as recurring interval
173
- [false, nil, time_str]
174
- end
175
-
176
- attr_reader :reminders_file
177
-
178
- def initialize(reminders_file = REMINDERS_FILE)
179
- @reminders_file = reminders_file
180
- end
181
-
182
- def ensure_reminders_file
183
- dir = File.dirname(@reminders_file)
184
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
185
- File.write(@reminders_file, [].to_yaml) unless File.exist?(@reminders_file)
186
- end
187
-
188
- def load_reminders
189
- ensure_reminders_file
190
- data = YAML.load_file(@reminders_file) || []
191
- data.map { |hash| Reminder.from_hash(hash) }
192
- end
193
-
194
- def save_reminders(reminders)
195
- ensure_reminders_file
196
- data = reminders.map(&:to_h)
197
- File.write(@reminders_file, data.to_yaml)
198
- end
199
-
200
- def load_reminder_hash(reminders = nil)
201
- reminders ||= load_reminders
202
- reminders.each_with_index.each_with_object({}) do |(reminder, idx), h|
203
- h[reminder.display_string(idx)] = reminder
204
- end
205
- end
206
-
207
- def edit_reminders(reminders = nil)
208
- reminders_array = if reminders.nil?
209
- [REMINDER_TEMPLATE]
210
- elsif reminders.is_a?(Array)
211
- reminders.map { |r| r.is_a?(Reminder) ? r.to_h : r }
212
- else
213
- [reminders.is_a?(Reminder) ? reminders.to_h : reminders]
214
- end
215
-
216
- tmpfile = Tempfile.new(['reminder-edit-', '.yml'])
217
- tmpfile.write(reminders_array.to_yaml)
218
- tmpfile.close
219
-
220
- system(ENV['EDITOR'] || 'vim', tmpfile.path)
221
-
222
- updated_data = YAML.load_file(tmpfile.path)
223
- tmpfile.unlink
224
-
225
- updated_array = updated_data.is_a?(Array) ? updated_data : [updated_data]
226
- updated_array.map { |hash| Reminder.from_hash(hash) }
227
- end
228
-
229
- def find_reminder_by_ref(ref, reminders)
230
- if ref =~ /^\d+$/
231
- idx = ref.to_i - 1
232
- return [idx, reminders[idx]] if idx >= 0 && idx < reminders.length
233
- end
234
- [nil, nil]
235
- end
236
- end
237
-
238
- rm = ReminderManager.new
239
- o = Hiiro.init(*ARGV, plugins: [Pins], reminders_file: rm.reminders_file)
240
-
241
- o.add_subcmd(:add) do |message = nil, time_arg = nil, sound = nil, *args|
242
- reminders = rm.load_reminders
243
-
244
- if message.nil?
245
- new_reminders = rm.edit_reminders
246
-
247
- reminders.concat(new_reminders)
248
- rm.save_reminders(reminders)
249
-
250
- if new_reminders.length == 1
251
- puts "Saved reminder ##{reminders.length}: #{new_reminders.first.message}"
252
- else
253
- puts "Saved #{new_reminders.length} reminders (#{reminders.length - new_reminders.length + 1}-#{reminders.length})"
254
- end
255
- next
256
- end
257
-
258
- is_once, trigger_at, interval = ReminderManager.parse_time_arg(time_arg)
259
-
260
- new_reminder = ReminderManager::Reminder.new(
261
- message: message,
262
- interval: interval,
263
- sound: sound,
264
- once: is_once,
265
- trigger_at: trigger_at,
266
- created_at: Time.now.iso8601
267
- )
268
-
269
- reminders << new_reminder
270
- rm.save_reminders(reminders)
271
-
272
- if is_once
273
- trigger_time = Time.parse(trigger_at)
274
- puts "Saved one-time reminder ##{reminders.length}: #{message} (at #{trigger_time.strftime('%Y-%m-%d %H:%M')})"
275
- else
276
- puts "Saved recurring reminder ##{reminders.length}: #{message} (every #{interval || '1h'})"
277
- end
278
- end
279
-
280
- o.add_subcmd(:once) do |message = nil, time_arg = nil, sound = nil, *args|
281
- reminders = rm.load_reminders
282
-
283
- if message.nil?
284
- puts "Usage: h remind once 'message' <time> [sound]"
285
- puts " time: 15m, 2h, 1d OR YYYYmmddHHMM"
286
- next
287
- end
288
-
289
- time_arg ||= '1h'
290
- is_once, trigger_at, interval = ReminderManager.parse_time_arg(time_arg)
291
-
292
- # Force one-time even if parse didn't detect it
293
- unless trigger_at
294
- seconds = ReminderManager::Reminder.new(message: '').parse_interval(time_arg)
295
- trigger_at = (Time.now + seconds).iso8601
296
- end
297
-
298
- new_reminder = ReminderManager::Reminder.new(
299
- message: message,
300
- interval: interval,
301
- sound: sound,
302
- once: true,
303
- trigger_at: trigger_at,
304
- created_at: Time.now.iso8601
305
- )
306
-
307
- reminders << new_reminder
308
- rm.save_reminders(reminders)
309
-
310
- trigger_time = Time.parse(trigger_at)
311
- puts "Saved one-time reminder ##{reminders.length}: #{message} (at #{trigger_time.strftime('%Y-%m-%d %H:%M')})"
312
- end
313
-
314
- o.add_subcmd(:every) do |message = nil, interval = nil, sound = nil, *args|
315
- reminders = rm.load_reminders
316
-
317
- if message.nil?
318
- puts "Usage: h remind every 'message' <interval> [sound]"
319
- puts " interval: 15m, 2h, 1d, 1w"
320
- next
321
- end
322
-
323
- new_reminder = ReminderManager::Reminder.new(
324
- message: message,
325
- interval: interval || '1h',
326
- sound: sound,
327
- once: false,
328
- created_at: Time.now.iso8601
329
- )
330
-
331
- reminders << new_reminder
332
- rm.save_reminders(reminders)
333
-
334
- puts "Saved recurring reminder ##{reminders.length}: #{message} (every #{interval || '1h'})"
335
- end
336
-
337
- o.add_subcmd(:ls) do |*args|
338
- reminders = rm.load_reminders
339
- if reminders.empty?
340
- puts "No reminders saved."
341
- else
342
- reminders.each_with_index do |reminder, idx|
343
- puts reminder.display_string(idx)
344
- end
345
- end
346
- end
347
-
348
- o.add_subcmd(:list) do |*args|
349
- o.run_subcmd(:ls, *args)
350
- end
351
-
352
- o.add_subcmd(:remove) do |ref = nil, *args|
353
- reminders = rm.load_reminders
354
-
355
- if reminders.empty?
356
- puts "No reminders to remove."
357
- next
358
- end
359
-
360
- idx = nil
361
- if ref.nil?
362
- lines = rm.load_reminder_hash(reminders)
363
-
364
- require 'open3'
365
- selected, status = Open3.capture2('sk', stdin_data: lines.keys.join("\n"))
366
-
367
- if status.success? && !selected.strip.empty?
368
- reminder = lines[selected.strip]
369
- idx = reminders.index(reminder)
370
- end
371
- else
372
- idx, _ = rm.find_reminder_by_ref(ref, reminders)
373
- end
374
-
375
- if idx.nil?
376
- puts "Reminder not found"
377
- next
378
- end
379
-
380
- removed = reminders.delete_at(idx)
381
- rm.save_reminders(reminders)
382
- puts "Removed reminder: #{removed.message}"
383
- end
384
-
385
- o.add_subcmd(:pause) do |ref = nil, *args|
386
- reminders = rm.load_reminders
387
-
388
- if reminders.empty?
389
- puts "No reminders to pause."
390
- next
391
- end
392
-
393
- idx = nil
394
- if ref.nil?
395
- enabled_reminders = reminders.select(&:enabled)
396
- if enabled_reminders.empty?
397
- puts "No enabled reminders to pause."
398
- next
399
- end
400
-
401
- lines = enabled_reminders.each_with_index.each_with_object({}) do |(r, i), h|
402
- actual_idx = reminders.index(r)
403
- h[r.display_string(actual_idx)] = actual_idx
404
- end
405
-
406
- require 'open3'
407
- selected, status = Open3.capture2('sk', stdin_data: lines.keys.join("\n"))
408
-
409
- if status.success? && !selected.strip.empty?
410
- idx = lines[selected.strip]
411
- end
412
- else
413
- idx, _ = rm.find_reminder_by_ref(ref, reminders)
414
- end
415
-
416
- if idx.nil?
417
- puts "Reminder not found"
418
- next
419
- end
420
-
421
- reminders[idx].enabled = false
422
- rm.save_reminders(reminders)
423
- puts "Paused reminder: #{reminders[idx].message}"
424
- end
425
-
426
- o.add_subcmd(:resume) do |ref = nil, *args|
427
- reminders = rm.load_reminders
428
-
429
- if reminders.empty?
430
- puts "No reminders to resume."
431
- next
432
- end
433
-
434
- idx = nil
435
- if ref.nil?
436
- disabled_reminders = reminders.reject(&:enabled)
437
- if disabled_reminders.empty?
438
- puts "No paused reminders to resume."
439
- next
440
- end
441
-
442
- lines = disabled_reminders.each_with_index.each_with_object({}) do |(r, i), h|
443
- actual_idx = reminders.index(r)
444
- h[r.display_string(actual_idx)] = actual_idx
445
- end
446
-
447
- require 'open3'
448
- selected, status = Open3.capture2('sk', stdin_data: lines.keys.join("\n"))
449
-
450
- if status.success? && !selected.strip.empty?
451
- idx = lines[selected.strip]
452
- end
453
- else
454
- idx, _ = rm.find_reminder_by_ref(ref, reminders)
455
- end
456
-
457
- if idx.nil?
458
- puts "Reminder not found"
459
- next
460
- end
461
-
462
- reminders[idx].enabled = true
463
- rm.save_reminders(reminders)
464
- puts "Resumed reminder: #{reminders[idx].message}"
465
- end
466
-
467
- o.add_subcmd(:check) do |*args|
468
- reminders = rm.load_reminders
469
- triggered_count = 0
470
- verbose = args.include?('-v') || args.include?('--verbose')
471
-
472
- reminders.each do |reminder|
473
- next unless reminder.should_trigger?
474
-
475
- # Build notification options
476
- notification_args = ['-m', reminder.message, '-t', 'Reminder']
477
- notification_args += ['-s', reminder.sound] if reminder.sound
478
-
479
- # Use Hiiro::Notification to show the notification
480
- notif_hiiro = Hiiro.init(*notification_args)
481
- Hiiro::Notification.show(notif_hiiro)
482
-
483
- reminder.trigger!
484
- triggered_count += 1
485
-
486
- if verbose
487
- if reminder.one_time?
488
- puts "Triggered (one-time): #{reminder.message}"
489
- else
490
- puts "Triggered: #{reminder.message}"
491
- end
492
- end
493
- end
494
-
495
- rm.save_reminders(reminders)
496
-
497
- if verbose
498
- enabled_count = reminders.count(&:enabled)
499
- one_time_done = reminders.count { |r| r.one_time? && !r.enabled }
500
- puts "Checked #{enabled_count} enabled reminders, triggered #{triggered_count}"
501
- puts "#{one_time_done} completed one-time reminder(s) (use 'h remind clean' to remove)" if one_time_done > 0
502
- end
503
- end
504
-
505
- o.add_subcmd(:clean) do |*args|
506
- reminders = rm.load_reminders
507
- before_count = reminders.length
508
-
509
- # Remove disabled one-time reminders (already triggered)
510
- reminders.reject! { |r| r.one_time? && !r.enabled }
511
-
512
- after_count = reminders.length
513
- removed = before_count - after_count
514
-
515
- rm.save_reminders(reminders)
516
-
517
- if removed > 0
518
- puts "Removed #{removed} completed one-time reminder(s)"
519
- else
520
- puts "No completed one-time reminders to clean up"
521
- end
522
- end
523
-
524
- o.add_subcmd(:howto) do |*args|
525
- puts <<~HOWTO
526
- h remind - Reminder notification system
527
-
528
- SETUP FOR CRON:
529
- ===============
530
- Add the following line to your crontab (run 'crontab -e'):
531
-
532
- * * * * * #{File.expand_path('~/.rbenv/shims/h')} remind check
533
-
534
- Or if you want verbose logging:
535
-
536
- * * * * * #{File.expand_path('~/.rbenv/shims/h')} remind check -v >> ~/.config/hiiro/remind.log 2>&1
537
-
538
- USAGE:
539
- ======
540
- # One-time reminders (triggers once, then disables)
541
- h remind add 'Read the news' 15m # Remind in 15 minutes
542
- h remind add 'Meeting' 202602131430 # Remind at specific time (YYYYmmddHHMM)
543
- h remind once 'Call mom' 2h # Explicit one-time in 2 hours
544
-
545
- # Recurring reminders
546
- h remind every 'Take a break' 30m # Remind every 30 minutes
547
- h remind every 'Stand up' 1h basso # Remind hourly with 'basso' sound
548
-
549
- # Editor mode
550
- h remind add # Open editor to add reminder(s)
551
-
552
- # Management
553
- h remind ls # List all reminders
554
- h remind remove # Select and remove a reminder
555
- h remind remove 2 # Remove reminder #2
556
- h remind clean # Remove completed one-time reminders
557
-
558
- h remind pause # Select and pause a reminder
559
- h remind pause 1 # Pause reminder #1
560
- h remind resume # Select and resume a reminder
561
- h remind resume 1 # Resume reminder #1
562
-
563
- h remind check # Check and trigger due reminders
564
- h remind check -v # Verbose mode
565
-
566
- TIME FORMATS:
567
- =============
568
- Relative (one-time): 15m, 2h, 1d - triggers once after delay
569
- Absolute (one-time): YYYYmmddHHMM - triggers at specific date/time
570
- e.g., 202602131430 = Feb 13, 2026 at 14:30
571
-
572
- Use 'h remind every' to create recurring reminders with intervals.
573
-
574
- INTERVAL FORMAT (for recurring):
575
- =================================
576
- s = seconds (e.g., 30s)
577
- m = minutes (e.g., 10m)
578
- h = hours (e.g., 2h)
579
- d = days (e.g., 1d)
580
- w = weeks (e.g., 1w)
581
-
582
- SOUNDS:
583
- =======
584
- Custom sounds can be placed in: ~/.config/hiiro/sounds/
585
- Use the filename (without extension) as the sound name.
586
-
587
- CONFIG FILE:
588
- ============
589
- Reminders are stored in: #{ReminderManager::REMINDERS_FILE}
590
- HOWTO
591
- end
592
-
593
- o.add_subcmd(:path) do |*args|
594
- print rm.reminders_file
595
- end
596
-
597
- o.add_subcmd(:editall) do |*args|
598
- reminders_before = rm.load_reminders
599
- system(ENV['EDITOR'] || 'vim', rm.reminders_file)
600
-
601
- begin
602
- rm.load_reminders
603
- rescue => e
604
- puts "ERROR: Unable to read updated file...reverting."
605
- rm.save_reminders(reminders_before)
606
- end
607
- end
608
-
609
- begin
610
- o.run
611
- rescue => e
612
- require 'pry'
613
- binding.pry
614
- end
data/bin/h-serve DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'hiiro'
4
-
5
- system('miniserve', '-p', '1111')
6
-