hiiro 0.1.133 → 0.1.134

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: 1fad92bb1b8ae6893d1b27fe0c9f077c938fbdafb440cedeabb8dedf50f09f31
4
- data.tar.gz: 6c702fe77535f178498b5bf561edcde9f247dca6b816b2d1ee68bb9985fb0a25
3
+ metadata.gz: e5c49242912889d365b5eabdfdc3adeb72ea6d55dd7311280dcc9b3f6ac40013
4
+ data.tar.gz: 219b6710d528233190c7bfb30465d4f3b293cea21f24c8a01561768566ce83ff
5
5
  SHA512:
6
- metadata.gz: 599336e66a32564d50eda3c945b7a01be369aa751a0e738f8083e793c35e9a427a976ce508028ca9ef6ff0b75d2bcc888d19d10668bd93b65355358ad2bac6c8
7
- data.tar.gz: da2877f35766f04272a6cb8b4a0c62a4d8d5a2b427fb8366dcba7246ec295cf6060915afb94ca335eb055785c543bf0b656a22d99bf7d37dc3f62568325181b5
6
+ metadata.gz: 8ae90dc0c81c21b6d580ccfa3b2ba3475633f7686efdc5de97147cc3030538632e01c436a244e682d6ee7cc361cd3ed9945652c2fd1c4792da8ad98d6a160913
7
+ data.tar.gz: ebca695230cc51d34b926726c5514af9b6c535eb368920fca89a0dd3dbec0e37d16748612612032485819ac01419b6c1f7b87ca44fc4d5281885610dec8e3900
data/README.md CHANGED
@@ -82,6 +82,7 @@ h ping
82
82
  | `h plugin` | Manage hiiro plugins (list, edit, search) |
83
83
  | `h pr` | GitHub PR management via gh CLI |
84
84
  | `h project` | Project navigation with tmux session management |
85
+ | `h queue` | Claude prompt queue with tmux-based task execution |
85
86
  | `h session` | Tmux session management |
86
87
  | `h sha` | Extract short SHA from git log |
87
88
  | `h todo` | Todo list management with tags and task association |
@@ -212,6 +213,7 @@ All configuration lives in `~/.config/hiiro/`:
212
213
  ~/.config/hiiro/
213
214
  plugins/ # Plugin files (auto-loaded)
214
215
  pins/ # Pin storage (per command)
216
+ queue/ # Prompt queue (wip, pending, running, done, failed)
215
217
  tasks/ # Task metadata
216
218
  projects.yml # Project aliases
217
219
  apps.yml # App directory mappings
data/bin/h-jumplist ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "hiiro"
4
+ require "fileutils"
5
+
6
+ JUMPLIST_DIR = File.join(Dir.home, '.config', 'hiiro', 'jumplist')
7
+ JUMPLIST_FILE = File.join(JUMPLIST_DIR, 'entries')
8
+ POS_FILE = File.join(JUMPLIST_DIR, 'position')
9
+ MAX_SIZE = 50
10
+
11
+ FileUtils.mkdir_p(JUMPLIST_DIR)
12
+
13
+ def read_entries
14
+ return [] unless File.exist?(JUMPLIST_FILE)
15
+ File.readlines(JUMPLIST_FILE, chomp: true).reject(&:empty?)
16
+ end
17
+
18
+ def write_entries(entries)
19
+ File.write(JUMPLIST_FILE, entries.join("\n") + "\n")
20
+ end
21
+
22
+ def get_pos
23
+ return 0 unless File.exist?(POS_FILE)
24
+ File.read(POS_FILE).strip.to_i
25
+ end
26
+
27
+ def set_pos(n)
28
+ File.write(POS_FILE, n.to_s)
29
+ end
30
+
31
+ def pane_exists?(pane_id)
32
+ panes = `tmux list-panes -a -F '\#{pane_id}' 2>/dev/null`.strip.split("\n")
33
+ panes.include?(pane_id)
34
+ end
35
+
36
+ def tmux_display(fmt)
37
+ `tmux display-message -p '#{fmt}'`.strip
38
+ end
39
+
40
+ Hiiro.run(*ARGV) do
41
+ add_subcmd(:setup) do
42
+ puts <<~SETUP
43
+ # Add the following to your tmux.conf:
44
+
45
+ # --- Jumplist hooks (record pane/window navigation) ---
46
+ set-hook -g after-select-pane "run-shell -b 'h jumplist record'"
47
+ set-hook -g after-select-window "run-shell -b 'h jumplist record'"
48
+
49
+ # --- Jumplist keybindings ---
50
+ bind-key -r C-b run-shell "h jumplist back"
51
+ bind-key -r C-f run-shell "h jumplist forward"
52
+ SETUP
53
+ end
54
+
55
+ add_subcmd(:record) do
56
+ # Check suppression flag
57
+ suppress = `tmux show-environment TMUX_JUMPLIST_SUPPRESS 2>/dev/null`.strip
58
+ if suppress == "TMUX_JUMPLIST_SUPPRESS=1"
59
+ system('tmux', 'set-environment', '-u', 'TMUX_JUMPLIST_SUPPRESS')
60
+ next
61
+ end
62
+
63
+ pane_id = tmux_display('#\{pane_id\}')
64
+ window_id = tmux_display('#\{window_id\}')
65
+ session_name = tmux_display('#\{session_name\}')
66
+ pane_cmd = tmux_display('#\{pane_current_command\}')
67
+ timestamp = Time.now.to_i.to_s
68
+
69
+ entries = read_entries
70
+ pos = get_pos
71
+
72
+ # If position is not at head, truncate forward history
73
+ if pos > 0 && entries.any?
74
+ entries = entries[pos..]
75
+ set_pos(0)
76
+ elsif pos > 0
77
+ set_pos(0)
78
+ end
79
+
80
+ # Deduplicate: skip if most recent entry is same pane
81
+ if entries.any?
82
+ last_pane = entries[0].split('|', 2).first
83
+ next if last_pane == pane_id
84
+ end
85
+
86
+ # Prepend new entry (newest first)
87
+ new_entry = [pane_id, window_id, session_name, timestamp, pane_cmd].join('|')
88
+ entries.unshift(new_entry)
89
+ entries = entries.first(MAX_SIZE)
90
+ write_entries(entries)
91
+ end
92
+
93
+ add_subcmd(:back) do
94
+ entries = read_entries
95
+ pos = get_pos
96
+
97
+ if entries.empty?
98
+ system('tmux', 'display-message', 'Jumplist: empty')
99
+ next
100
+ end
101
+
102
+ # If at position 0, record current pane first so forward works
103
+ if pos == 0
104
+ pane_id = tmux_display('#\{pane_id\}')
105
+ window_id = tmux_display('#\{window_id\}')
106
+ session_name = tmux_display('#\{session_name\}')
107
+ pane_cmd = tmux_display('#\{pane_current_command\}')
108
+ timestamp = Time.now.to_i.to_s
109
+
110
+ new_entry = [pane_id, window_id, session_name, timestamp, pane_cmd].join('|')
111
+
112
+ # Only prepend if different from current head
113
+ if entries.empty? || entries[0].split('|', 2).first != pane_id
114
+ entries.unshift(new_entry)
115
+ entries = entries.first(MAX_SIZE)
116
+ write_entries(entries)
117
+ end
118
+ end
119
+
120
+ # Find next valid entry going backward
121
+ new_pos = pos + 1
122
+ while new_pos < entries.length
123
+ entry = entries[new_pos]
124
+ parts = entry.split('|')
125
+ target_pane = parts[0]
126
+ target_window = parts[1]
127
+ target_session = parts[2]
128
+
129
+ if pane_exists?(target_pane)
130
+ system('tmux', 'set-environment', 'TMUX_JUMPLIST_SUPPRESS', '1')
131
+ system('tmux', 'switch-client', '-t', target_session)
132
+ system('tmux', 'select-window', '-t', target_window)
133
+ system('tmux', 'select-pane', '-t', target_pane)
134
+ set_pos(new_pos)
135
+ next # exit the subcmd block
136
+ end
137
+
138
+ new_pos += 1
139
+ end
140
+
141
+ system('tmux', 'display-message', 'Jumplist: at oldest entry')
142
+ end
143
+
144
+ add_subcmd(:forward) do
145
+ pos = get_pos
146
+
147
+ if pos <= 0
148
+ system('tmux', 'display-message', 'Jumplist: at newest entry')
149
+ next
150
+ end
151
+
152
+ entries = read_entries
153
+
154
+ if entries.empty?
155
+ system('tmux', 'display-message', 'Jumplist: empty')
156
+ next
157
+ end
158
+
159
+ # Find next valid entry going forward (toward index 0)
160
+ new_pos = pos - 1
161
+ while new_pos >= 0
162
+ entry = entries[new_pos]
163
+ parts = entry.split('|')
164
+ target_pane = parts[0]
165
+ target_window = parts[1]
166
+ target_session = parts[2]
167
+
168
+ if pane_exists?(target_pane)
169
+ system('tmux', 'set-environment', 'TMUX_JUMPLIST_SUPPRESS', '1')
170
+ system('tmux', 'switch-client', '-t', target_session)
171
+ system('tmux', 'select-window', '-t', target_window)
172
+ system('tmux', 'select-pane', '-t', target_pane)
173
+ set_pos(new_pos)
174
+ next # exit the subcmd block
175
+ end
176
+
177
+ new_pos -= 1
178
+ end
179
+
180
+ system('tmux', 'display-message', 'Jumplist: at newest entry')
181
+ set_pos(0)
182
+ end
183
+ end
data/bin/h-notify ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "hiiro"
4
+ require "fileutils"
5
+
6
+ TYPE_PRESETS = {
7
+ 'success' => { prefix: '[OK]', sound: 'Glass', title: 'Success' },
8
+ 'error' => { prefix: '[ERR]', sound: 'Basso', title: 'Error' },
9
+ 'info' => { prefix: '[INFO]', sound: 'Pop', title: 'Info' },
10
+ 'warning' => { prefix: '[WARN]', sound: 'Purr', title: 'Warning' },
11
+ }
12
+
13
+ def session_name
14
+ `tmux display-message -p '\#{session_name}'`.strip
15
+ end
16
+
17
+ def notify_file(session = nil)
18
+ session ||= session_name
19
+ "/tmp/tmux-notifications-#{session}"
20
+ end
21
+
22
+ def read_notifications(session = nil)
23
+ file = notify_file(session)
24
+ return [] unless File.exist?(file)
25
+ File.readlines(file, chomp: true).reject(&:empty?)
26
+ end
27
+
28
+ def write_notifications(entries, session = nil)
29
+ file = notify_file(session)
30
+ if entries.empty?
31
+ File.delete(file) if File.exist?(file)
32
+ else
33
+ File.write(file, entries.join("\n") + "\n")
34
+ end
35
+ end
36
+
37
+ def pane_exists?(pane_id)
38
+ panes = `tmux list-panes -a -F '\#{pane_id}' 2>/dev/null`.strip.split("\n")
39
+ panes.include?(pane_id)
40
+ end
41
+
42
+ Hiiro.run(*ARGV) do
43
+ opts = Hiiro::Options.parse(args) do
44
+ option(:type, short: 't', desc: 'Notification type (success, error, info, warning)', default: 'info')
45
+ end
46
+
47
+ preset = TYPE_PRESETS[opts.type] || TYPE_PRESETS['info']
48
+
49
+ add_subcmd(:push) do |*push_args|
50
+ message = push_args.join(' ')
51
+ if message.empty?
52
+ puts "Usage: h notify push [-t TYPE] \"message\""
53
+ next
54
+ end
55
+
56
+ pane_id = `tmux display-message -p '\#{pane_id}'`.strip
57
+ window_id = `tmux display-message -p '\#{window_id}'`.strip
58
+ session = session_name
59
+ pane_cmd = `tmux display-message -p '\#{pane_current_command}'`.strip
60
+ timestamp = Time.now.to_i.to_s
61
+
62
+ entry = [pane_id, window_id, session, timestamp, pane_cmd, message].join('|')
63
+
64
+ entries = read_notifications(session)
65
+ entries.unshift(entry)
66
+ write_notifications(entries, session)
67
+
68
+ # Show tmux display-message with type prefix
69
+ system('tmux', 'display-message', "#{preset[:prefix]} #{message}")
70
+
71
+ # Also send terminal-notifier
72
+ system('terminal-notifier',
73
+ '-title', preset[:title],
74
+ '-message', message,
75
+ '-sound', preset[:sound])
76
+ end
77
+
78
+ add_subcmd(:menu) do
79
+ session = session_name
80
+ entries = read_notifications(session)
81
+
82
+ if entries.empty?
83
+ system('tmux', 'display-message', 'No notifications')
84
+ next
85
+ end
86
+
87
+ # Build display-menu arguments
88
+ menu_args = ['-T', 'Notifications']
89
+
90
+ entries.first(10).each_with_index do |line, idx|
91
+ parts = line.split('|')
92
+ pane_id = parts[0]
93
+ cmd = parts[4]
94
+ msg = parts[5..].join('|')
95
+ msg = msg[0..46] + '...' if msg.length > 50
96
+
97
+ label = "#{idx}: [#{pane_id}] #{cmd}: #{msg}"
98
+ menu_args.push(label, idx.to_s, "run-shell 'h notify jump #{idx}'")
99
+ end
100
+
101
+ # Separator and clear option
102
+ menu_args.push('', '', 'Clear all', 'c', "run-shell 'h notify clear'")
103
+
104
+ system('tmux', 'display-menu', *menu_args)
105
+ end
106
+
107
+ add_subcmd(:jump) do |index_str = nil|
108
+ if index_str.nil?
109
+ puts "Usage: h notify jump INDEX"
110
+ next
111
+ end
112
+
113
+ index = index_str.to_i
114
+ session = session_name
115
+ entries = read_notifications(session)
116
+
117
+ if index >= entries.length
118
+ system('tmux', 'display-message', 'Notification not found')
119
+ next
120
+ end
121
+
122
+ entry = entries[index]
123
+ parts = entry.split('|')
124
+ target_pane = parts[0]
125
+ target_window = parts[1]
126
+
127
+ unless pane_exists?(target_pane)
128
+ system('tmux', 'display-message', "Pane #{target_pane} no longer exists")
129
+ entries.delete_at(index)
130
+ write_notifications(entries, session)
131
+ next
132
+ end
133
+
134
+ system('tmux', 'select-window', '-t', target_window)
135
+ system('tmux', 'select-pane', '-t', target_pane)
136
+
137
+ # Remove the notification
138
+ entries.delete_at(index)
139
+ write_notifications(entries, session)
140
+ end
141
+
142
+ add_subcmd(:clear) do
143
+ session = session_name
144
+ file = notify_file(session)
145
+ File.delete(file) if File.exist?(file)
146
+ system('tmux', 'display-message', 'Notifications cleared')
147
+ end
148
+ end
data/bin/h-pr CHANGED
@@ -268,87 +268,7 @@ class PinnedPRManager
268
268
  end
269
269
 
270
270
 
271
- def batch_fetch_pr_info(pr_numbers)
272
- return {} if pr_numbers.empty?
273
-
274
- # Build GraphQL query to fetch multiple PRs at once
275
- pr_queries = pr_numbers.map.with_index do |num, idx|
276
- <<~GRAPHQL.strip
277
- pr#{idx}: pullRequest(number: #{num}) {
278
- number
279
- title
280
- url
281
- headRefName
282
- state
283
- isDraft
284
- mergeable
285
- reviewDecision
286
- statusCheckRollup {
287
- contexts {
288
- ... on CheckRun {
289
- conclusion
290
- status
291
- }
292
- ... on StatusContext {
293
- state
294
- }
295
- }
296
- }
297
- reviews(last: 50) {
298
- nodes {
299
- author {
300
- login
301
- }
302
- state
303
- }
304
- }
305
- }
306
- GRAPHQL
307
- end
308
-
309
- query = <<~GRAPHQL
310
- query {
311
- repository(owner: "instacart", name: "carrot") {
312
- #{pr_queries.join("\n")}
313
- }
314
- }
315
- GRAPHQL
316
-
317
- result = `gh api graphql -f query='#{query.gsub("'", "'\\''")}' 2>/dev/null`
318
- return {} if result.empty?
319
-
320
- data = JSON.parse(result)
321
- repo_data = data.dig('data', 'repository')
322
- return {} unless repo_data
323
-
324
- # Convert GraphQL response to hash keyed by PR number
325
- pr_info_by_number = {}
326
- pr_numbers.each_with_index do |num, idx|
327
- pr_data = repo_data["pr#{idx}"]
328
- next unless pr_data
329
-
330
- # Transform GraphQL response to match gh pr view format
331
- pr_info_by_number[num] = {
332
- 'number' => pr_data['number'],
333
- 'title' => pr_data['title'],
334
- 'url' => pr_data['url'],
335
- 'headRefName' => pr_data['headRefName'],
336
- 'state' => pr_data['state'],
337
- 'isDraft' => pr_data['isDraft'],
338
- 'mergeable' => pr_data['mergeable'],
339
- 'reviewDecision' => pr_data['reviewDecision'],
340
- 'statusCheckRollup' => pr_data['statusCheckRollup'],
341
- 'reviews' => pr_data.dig('reviews', 'nodes') || []
342
- }
343
- end
344
-
345
- pr_info_by_number
346
- rescue JSON::ParserError, StandardError
347
- {}
348
- end
349
-
350
271
  def refresh_all_status(prs, force: false)
351
- # Determine which PRs need refreshing
352
272
  prs_to_refresh = prs.select { |pr| needs_refresh?(pr, force: force) }
353
273
 
354
274
  if prs_to_refresh.empty?
@@ -356,25 +276,7 @@ class PinnedPRManager
356
276
  return prs
357
277
  end
358
278
 
359
- # Batch fetch all PR info at once
360
- pr_numbers = prs_to_refresh.map { |pr| pr['number'] }
361
- fetched_info = batch_fetch_pr_info(pr_numbers)
362
-
363
- # Update each PR with fetched info
364
- prs_to_refresh.each do |pr|
365
- info = fetched_info[pr['number']]
366
- next unless info
367
-
368
- pr['state'] = info['state']
369
- pr['title'] = info['title']
370
- pr['checks'] = summarize_checks(info['statusCheckRollup'])
371
- pr['reviews'] = summarize_reviews(info['reviews'])
372
- pr['review_decision'] = info['reviewDecision']
373
- pr['is_draft'] = info['isDraft']
374
- pr['mergeable'] = info['mergeable']
375
- pr['last_checked'] = Time.now.iso8601
376
- end
377
-
279
+ prs_to_refresh.each { |pr| refresh_status(pr, force: true) }
378
280
  prs
379
281
  end
380
282
 
@@ -766,7 +668,9 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
766
668
  end
767
669
  end
768
670
 
769
- add_subcmd(:status) do |*status_args|
671
+ add_subcmd(:ls) do |*ls_args|
672
+ update = ls_args.include?('-u') || ls_args.include?('--update')
673
+
770
674
  pinned = pinned_manager.load_pinned
771
675
 
772
676
  if pinned.empty?
@@ -774,13 +678,31 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
774
678
  next
775
679
  end
776
680
 
777
- compact = status_args.include?('-c') || status_args.include?('--compact')
778
- force = status_args.include?('-U') || status_args.include?('--force-update')
681
+ if update
682
+ puts "Updating status for #{pinned.length} PR(s)..."
683
+ pinned_manager.refresh_all_status(pinned, force: true)
684
+ pinned_manager.save_pinned(pinned)
685
+ puts
686
+ end
779
687
 
780
- puts "Refreshing status for #{pinned.length} pinned PR(s)..."
781
- puts
688
+ pinned.each_with_index do |pr, idx|
689
+ puts pinned_manager.display_pinned(pr, idx)
690
+ end
782
691
 
783
- pinned_manager.refresh_all_status(pinned, force: force)
692
+ last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
693
+ puts "---"
694
+ puts "Last updated: #{last_checked || 'never'}"
695
+ end
696
+
697
+ add_subcmd(:status) do |*status_args|
698
+ pinned = pinned_manager.load_pinned
699
+
700
+ if pinned.empty?
701
+ puts "No pinned PRs"
702
+ next
703
+ end
704
+
705
+ compact = status_args.include?('-c') || status_args.include?('--compact')
784
706
 
785
707
  pinned.each_with_index do |pr, idx|
786
708
  if compact
@@ -791,9 +713,9 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
791
713
  end
792
714
  end
793
715
 
794
- pinned_manager.save_pinned(pinned)
716
+ last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
795
717
  puts "---"
796
- puts "Status updated at #{Time.now.strftime('%H:%M:%S')}"
718
+ puts "Last updated: #{last_checked || 'never'}"
797
719
  end
798
720
 
799
721
  add_subcmd(:update) do |*update_args|
@@ -814,10 +736,6 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
814
736
 
815
737
  add_subcmd(:green) do |*green_args|
816
738
  pinned = pinned_manager.load_pinned
817
- force = green_args.include?('-U') || green_args.include?('--force-update')
818
-
819
- pinned_manager.refresh_all_status(pinned, force: force)
820
- pinned_manager.save_pinned(pinned)
821
739
 
822
740
  filtered = pinned.select { |pr|
823
741
  c = pr['checks']
@@ -836,10 +754,6 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
836
754
 
837
755
  add_subcmd(:red) do |*red_args|
838
756
  pinned = pinned_manager.load_pinned
839
- force = red_args.include?('-U') || red_args.include?('--force-update')
840
-
841
- pinned_manager.refresh_all_status(pinned, force: force)
842
- pinned_manager.save_pinned(pinned)
843
757
 
844
758
  filtered = pinned.select { |pr|
845
759
  c = pr['checks']
@@ -858,10 +772,6 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
858
772
 
859
773
  add_subcmd(:old) do |*old_args|
860
774
  pinned = pinned_manager.load_pinned
861
- force = old_args.include?('-U') || old_args.include?('--force-update')
862
-
863
- pinned_manager.refresh_all_status(pinned, force: force)
864
- pinned_manager.save_pinned(pinned)
865
775
 
866
776
  filtered = pinned.select { |pr| pr['state'] == 'MERGED' }
867
777
 
@@ -884,14 +794,10 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
884
794
  next
885
795
  end
886
796
 
887
- force = args.include?('-U') || args.include?('--force-update')
888
- pinned_manager.refresh_all_status(pinned, force: force)
889
-
890
797
  merged_or_closed = pinned.select { |pr| pr['state'] == 'MERGED' || pr['state'] == 'CLOSED' }
891
798
 
892
799
  if merged_or_closed.empty?
893
800
  puts "No merged or closed PRs to prune"
894
- pinned_manager.save_pinned(pinned)
895
801
  next
896
802
  end
897
803
 
@@ -909,10 +815,6 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
909
815
 
910
816
  add_subcmd(:draft) do |*draft_args|
911
817
  pinned = pinned_manager.load_pinned
912
- force = draft_args.include?('-U') || draft_args.include?('--force-update')
913
-
914
- pinned_manager.refresh_all_status(pinned, force: force)
915
- pinned_manager.save_pinned(pinned)
916
818
 
917
819
  filtered = pinned.select { |pr| pr['is_draft'] == true }
918
820
 
@@ -1077,11 +979,6 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
1077
979
  next
1078
980
  end
1079
981
 
1080
- # Refresh all statuses first
1081
- force = args.include?('-U') || args.include?('--force-update')
1082
- pinned_manager.refresh_all_status(pinned, force: force)
1083
- pinned_manager.save_pinned(pinned)
1084
-
1085
982
  merged = pinned.select { |pr| pr['state'] == 'MERGED' }
1086
983
  closed = pinned.select { |pr| pr['state'] == 'CLOSED' }
1087
984
 
@@ -1109,15 +1006,10 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
1109
1006
  next
1110
1007
  end
1111
1008
 
1112
- # Refresh all statuses first
1113
- force = args.include?('-U') || args.include?('--force-update')
1114
- pinned_manager.refresh_all_status(pinned, force: force)
1115
-
1116
1009
  merged_or_closed = pinned.select { |pr| pr['state'] == 'MERGED' || pr['state'] == 'CLOSED' }
1117
1010
 
1118
1011
  if merged_or_closed.empty?
1119
1012
  puts "No merged or closed PRs to unpin"
1120
- pinned_manager.save_pinned(pinned)
1121
1013
  next
1122
1014
  end
1123
1015
 
data/bin/h-project CHANGED
@@ -4,7 +4,7 @@ require "hiiro"
4
4
 
5
5
  # Helper to start or attach to a tmux session
6
6
  def start_tmux_session(session_name)
7
- session_name = session_name.to_s
7
+ session_name = session_name.to_s.gsub(?., ?_)
8
8
 
9
9
  unless system('tmux', 'has-session', '-t', session_name)
10
10
  system('tmux', 'new', '-d', '-A', '-s', session_name)
data/demo.sh ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Hiiro Quick Demo
4
+ #
5
+ # Usage: bash demo.sh
6
+ #
7
+ # Walks through key Hiiro features with commentary.
8
+ # Each step pauses so you can see the output.
9
+
10
+ set -e
11
+
12
+ BOLD="\033[1m"
13
+ DIM="\033[2m"
14
+ CYAN="\033[36m"
15
+ GREEN="\033[32m"
16
+ YELLOW="\033[33m"
17
+ RESET="\033[0m"
18
+
19
+ step=0
20
+
21
+ demo() {
22
+ step=$((step + 1))
23
+ echo ""
24
+ echo -e "${BOLD}${CYAN}── Step $step: $1${RESET}"
25
+ echo -e "${DIM}$ $2${RESET}"
26
+ echo ""
27
+ eval "$2"
28
+ echo ""
29
+ echo -e "${DIM}(press enter to continue)${RESET}"
30
+ read -r
31
+ }
32
+
33
+ echo -e "${BOLD}${GREEN}"
34
+ echo "╔══════════════════════════════════════╗"
35
+ echo "║ Hiiro Quick Demo ║"
36
+ echo "╚══════════════════════════════════════╝"
37
+ echo -e "${RESET}"
38
+ echo "Hiiro is a lightweight CLI framework for Ruby."
39
+ echo "It gives you subcommand dispatch, abbreviation"
40
+ echo "matching, and a plugin system."
41
+ echo ""
42
+ echo -e "${DIM}(press enter to start)${RESET}"
43
+ read -r
44
+
45
+ # ─── Abbreviations ───
46
+
47
+ demo "List all top-level commands" \
48
+ "h || true"
49
+
50
+ demo "Abbreviation matching: 'h to' resolves to 'h todo'" \
51
+ "h to || true"
52
+
53
+ demo "Abbreviations work at every level: 'h to ls' = 'h todo ls'" \
54
+ "h to ls || true"
55
+
56
+ demo "Ambiguous abbreviations show candidates: 'h s' matches multiple commands" \
57
+ "h s || true"
58
+
59
+ demo "More specific prefix resolves: 'h se' narrows to 'h serve' (if unambiguous) or shows remaining matches" \
60
+ "h se || true"
61
+
62
+ demo "Abbreviations chain: 'h wi ls' = 'h window ls'" \
63
+ "h wi ls || true"
64
+
65
+ demo "Even shorter: 'h br' = 'h branch'" \
66
+ "h br || true"
67
+
68
+ # ─── h task ───
69
+
70
+ demo "h task - manage worktree-based tasks (via Tasks plugin)" \
71
+ "h task || true"
72
+
73
+ demo "h task ls - list current tasks" \
74
+ "h task ls || true"
75
+
76
+ demo "Abbreviation: 'h ta ls' = 'h task ls'" \
77
+ "h ta ls || true"
78
+
79
+ # ─── h subtask ───
80
+
81
+ demo "h subtask - same interface as task, scoped to subtasks" \
82
+ "h subtask || true"
83
+
84
+ demo "h subtask ls - list current subtasks" \
85
+ "h subtask ls || true"
86
+
87
+ demo "Abbreviation: 'h su ls' = 'h subtask ls'" \
88
+ "h su ls || true"
89
+
90
+ # ─── h queue ───
91
+
92
+ demo "h queue - background job queue management" \
93
+ "h queue || true"
94
+
95
+ demo "h queue ls - list queued jobs" \
96
+ "h queue ls || true"
97
+
98
+ demo "h queue status - show queue status" \
99
+ "h queue status || true"
100
+
101
+ demo "Abbreviation: 'h q ls' = 'h queue ls'" \
102
+ "h q ls || true"
103
+
104
+ # ─── Wrap up ───
105
+
106
+ echo -e "${BOLD}${GREEN}"
107
+ echo "╔══════════════════════════════════════╗"
108
+ echo "║ Demo Complete! ║"
109
+ echo "╚══════════════════════════════════════╝"
110
+ echo -e "${RESET}"
111
+ echo "Key takeaways:"
112
+ echo " - Type just enough letters to be unambiguous"
113
+ echo " - Abbreviations work at every level (command + subcommand)"
114
+ echo " - 'h task' / 'h subtask' manage worktree-based workflows"
115
+ echo " - 'h queue' manages background jobs"
116
+ echo ""
data/demo.typescript ADDED
Binary file
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ # h-tmux-plugins.tmux -- Main entry point (sourced by TPM)
3
+ # Provides: Jumplist navigation + Notification tracker via Hiiro bin commands
4
+
5
+ # Helper to read tmux option with default
6
+ get_option() {
7
+ local option="$1"
8
+ local default="$2"
9
+ local value
10
+ value=$(tmux show-option -gqv "$option" 2>/dev/null || true)
11
+ echo "${value:-$default}"
12
+ }
13
+
14
+ # --- Configuration ---
15
+ JUMPLIST_BACK_KEY=$(get_option "@jumplist-back-key" "C-b")
16
+ JUMPLIST_FORWARD_KEY=$(get_option "@jumplist-forward-key" "C-f")
17
+ NOTIFY_MENU_KEY=$(get_option "@notify-menu-key" "C-n")
18
+ NOTIFY_CLEAR_KEY=$(get_option "@notify-clear-key" "M-n")
19
+
20
+ # --- Jumplist hooks ---
21
+ tmux set-hook -g after-select-pane "run-shell -b 'h jumplist record'"
22
+ tmux set-hook -g after-select-window "run-shell -b 'h jumplist record'"
23
+
24
+ # --- Jumplist keybindings ---
25
+ tmux bind-key -r "$JUMPLIST_BACK_KEY" run-shell "h jumplist back"
26
+ tmux bind-key -r "$JUMPLIST_FORWARD_KEY" run-shell "h jumplist forward"
27
+
28
+ # --- Notification keybindings ---
29
+ tmux bind-key "$NOTIFY_MENU_KEY" run-shell "h notify menu"
30
+ tmux bind-key "$NOTIFY_CLEAR_KEY" run-shell "h notify clear"
data/lib/hiiro/tmux.rb CHANGED
@@ -79,24 +79,33 @@ class Hiiro
79
79
 
80
80
  # Session methods
81
81
 
82
+ def find_session(name)
83
+ normalized = name.to_s.tr('.', '_')
84
+ sessions.find { |s| s.name == normalized } ||
85
+ Matcher.by_prefix(sessions.map(&:name), normalized)&.resolved&.then { |m| sessions.find { |s| s.name == m.item } }
86
+ end
87
+
82
88
  def session_exists?(name)
83
- sessions.any?{|session| session.name == name }
89
+ !find_session(name).nil?
84
90
  end
85
91
 
86
92
  def open_session(name, **opts)
87
- session_name = name.to_s
93
+ session_name = name.to_s.tr('.', '_')
94
+ existing = find_session(session_name)
88
95
 
89
- unless session_exists?(session_name)
96
+ unless existing
90
97
  new_session(session_name, **opts, detached: true)
91
98
  end
92
99
 
100
+ resolved_name = existing&.name || session_name
101
+
93
102
  if in_tmux?
94
- switch_client(session_name)
103
+ switch_client(resolved_name)
95
104
  elsif in_nvim?
96
105
  puts "Can't attach to tmux inside a vim terminal"
97
106
  false
98
107
  else
99
- attach_session(session_name)
108
+ attach_session(resolved_name)
100
109
  end
101
110
  end
102
111
 
@@ -110,25 +119,32 @@ class Hiiro
110
119
  end
111
120
 
112
121
  def kill_session(name)
113
- run_system('kill-session', '-t', name)
122
+ resolved = find_session(name)&.name || name.to_s.tr('.', '_')
123
+ run_system('kill-session', '-t', resolved)
114
124
  end
115
125
 
116
126
  def attach_session(name)
117
- run_system('attach-session', '-t', name)
127
+ resolved = find_session(name)&.name || name.to_s.tr('.', '_')
128
+ run_system('attach-session', '-t', resolved)
118
129
  end
119
130
 
120
131
  def switch_client(name)
121
- run_system('switch-client', '-t', name)
132
+ resolved = find_session(name)&.name || name.to_s.tr('.', '_')
133
+ run_system('switch-client', '-t', resolved)
122
134
  end
123
135
 
124
136
  def detach_client(session: nil)
125
137
  args = ['detach-client']
126
- args += ['-s', session] if session
138
+ if session
139
+ resolved = find_session(session)&.name || session.to_s.tr('.', '_')
140
+ args += ['-s', resolved]
141
+ end
127
142
  run_system(*args)
128
143
  end
129
144
 
130
145
  def rename_session(old_name, new_name)
131
- run_system('rename-session', '-t', old_name, new_name)
146
+ resolved = find_session(old_name)&.name || old_name.to_s.tr('.', '_')
147
+ run_system('rename-session', '-t', resolved, new_name)
132
148
  end
133
149
 
134
150
  # Window methods
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.133"
2
+ VERSION = "0.1.134"
3
3
  end
data/record-demo.sh ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Record a Hiiro demo typescript
4
+ #
5
+ # Usage:
6
+ # bash record-demo.sh
7
+ #
8
+ # This records a terminal session to 'demo.typescript' that can be
9
+ # played back with:
10
+ # script -p demo.typescript
11
+ #
12
+ # The recording simulates typing commands and showing their output.
13
+
14
+ TYPESCRIPT_FILE="demo.typescript"
15
+
16
+ # If called without the __INNER flag, wrap ourselves in `script -r`
17
+ if [[ -z "$__HIIRO_DEMO_INNER" ]]; then
18
+ export __HIIRO_DEMO_INNER=1
19
+ echo "Recording demo to $TYPESCRIPT_FILE ..."
20
+ echo "Play back with: script -p $TYPESCRIPT_FILE"
21
+ echo ""
22
+ script -r "$TYPESCRIPT_FILE" bash "$0"
23
+ exit $?
24
+ fi
25
+
26
+ # ── Helpers ──
27
+
28
+ BOLD="\033[1m"
29
+ DIM="\033[2m"
30
+ CYAN="\033[36m"
31
+ GREEN="\033[32m"
32
+ YELLOW="\033[33m"
33
+ WHITE="\033[37m"
34
+ RESET="\033[0m"
35
+
36
+ # Simulate typing a command character by character
37
+ typecmd() {
38
+ local cmd="$1"
39
+ echo -ne "${GREEN}\$ ${BOLD}${WHITE}"
40
+ for (( i=0; i<${#cmd}; i++ )); do
41
+ echo -n "${cmd:$i:1}"
42
+ sleep 0.08
43
+ done
44
+ echo -e "${RESET}"
45
+ sleep 1.5 # pause after typing so viewer can read the command
46
+ }
47
+
48
+ # Print a section header
49
+ header() {
50
+ echo ""
51
+ echo -e "${BOLD}${CYAN}━━━ $1 ━━━${RESET}"
52
+ echo ""
53
+ sleep 2.5 # time to read the section heading
54
+ }
55
+
56
+ # Pause after output so viewer can read what happened
57
+ pause() {
58
+ sleep "${1:-3}"
59
+ }
60
+
61
+ # Run a command with simulated typing
62
+ run() {
63
+ typecmd "$1"
64
+ eval "$1"
65
+ echo ""
66
+ pause
67
+ }
68
+
69
+ # ── Banner ──
70
+
71
+ clear
72
+ echo ""
73
+ echo -e "${BOLD}${GREEN}╔══════════════════════════════════════╗${RESET}"
74
+ echo -e "${BOLD}${GREEN}║ Hiiro Quick Demo ║${RESET}"
75
+ echo -e "${BOLD}${GREEN}╚══════════════════════════════════════╝${RESET}"
76
+ echo ""
77
+ echo -e "Hiiro is a lightweight CLI framework for Ruby."
78
+ echo -e "It gives you subcommand dispatch, abbreviation"
79
+ echo -e "matching, and a plugin system."
80
+ echo ""
81
+ sleep 4
82
+
83
+ # ── Abbreviations ──
84
+
85
+ header "Top-level commands"
86
+ run "h || true"
87
+
88
+ header "Abbreviation matching"
89
+ echo -e "${DIM}h to → resolves to → h todo${RESET}"
90
+ echo ""
91
+ sleep 2
92
+ run "h to || true"
93
+
94
+ header "Deep abbreviations"
95
+ echo -e "${DIM}h to ls → resolves to → h todo ls${RESET}"
96
+ echo ""
97
+ sleep 2
98
+ run "h to ls || true"
99
+
100
+ header "Ambiguous abbreviations"
101
+ echo -e "${DIM}h s → matches multiple commands${RESET}"
102
+ echo ""
103
+ sleep 2
104
+ run "h s || true"
105
+
106
+ header "Narrowing down"
107
+ echo -e "${DIM}h se → narrows the match${RESET}"
108
+ echo ""
109
+ sleep 2
110
+ run "h se || true"
111
+
112
+ header "Chained abbreviations"
113
+ echo -e "${DIM}h wi ls → resolves to → h window ls${RESET}"
114
+ echo ""
115
+ sleep 2
116
+ run "h wi ls || true"
117
+
118
+ echo -e "${DIM}h br → resolves to → h branch${RESET}"
119
+ echo ""
120
+ sleep 2
121
+ run "h br || true"
122
+
123
+ # ── h task ──
124
+
125
+ header "h task — worktree-based task management"
126
+ run "h task || true"
127
+
128
+ run "h task ls || true"
129
+
130
+ echo -e "${DIM}h ta ls → resolves to → h task ls${RESET}"
131
+ echo ""
132
+ sleep 2
133
+ run "h ta ls || true"
134
+
135
+ # ── h subtask ──
136
+
137
+ header "h subtask — scoped subtask management"
138
+ run "h subtask || true"
139
+
140
+ run "h subtask ls || true"
141
+
142
+ echo -e "${DIM}h su ls → resolves to → h subtask ls${RESET}"
143
+ echo ""
144
+ sleep 2
145
+ run "h su ls || true"
146
+
147
+ # ── h queue ──
148
+
149
+ header "h queue — background job queue"
150
+ run "h queue || true"
151
+
152
+ run "h queue ls || true"
153
+
154
+ run "h queue status || true"
155
+
156
+ echo -e "${DIM}h q ls → resolves to → h queue ls${RESET}"
157
+ echo ""
158
+ sleep 2
159
+ run "h q ls || true"
160
+
161
+ # ── Wrap up ──
162
+
163
+ echo ""
164
+ echo -e "${BOLD}${GREEN}╔══════════════════════════════════════╗${RESET}"
165
+ echo -e "${BOLD}${GREEN}║ Demo Complete! ║${RESET}"
166
+ echo -e "${BOLD}${GREEN}╚══════════════════════════════════════╝${RESET}"
167
+ echo ""
168
+ echo -e "Key takeaways:"
169
+ echo -e " ${CYAN}•${RESET} Type just enough letters to be unambiguous"
170
+ echo -e " ${CYAN}•${RESET} Abbreviations work at every level"
171
+ echo -e " ${CYAN}•${RESET} ${WHITE}h task${RESET} / ${WHITE}h subtask${RESET} manage worktree-based workflows"
172
+ echo -e " ${CYAN}•${RESET} ${WHITE}h queue${RESET} manages background jobs"
173
+ echo ""
174
+ sleep 4
175
+
176
+ exit 0
data/slides.md ADDED
@@ -0,0 +1,211 @@
1
+ ---
2
+ author: Hiiro
3
+ date: 2026-03-01
4
+ paging: "%d / %d"
5
+ ---
6
+
7
+ # Hiiro
8
+
9
+ A lightweight CLI framework for Ruby
10
+
11
+ - Subcommand dispatch
12
+ - Abbreviation matching
13
+ - Plugin system
14
+
15
+ ---
16
+
17
+ # Abbreviation Matching
18
+
19
+ Type just enough letters to be unambiguous.
20
+
21
+ Hiiro resolves prefixes at **every level** of the command tree.
22
+
23
+ ```bash
24
+ h
25
+ ```
26
+
27
+ > Press `Ctrl+E` to run
28
+
29
+ ---
30
+
31
+ # Abbreviations in Action
32
+
33
+ `h to` resolves to `h todo`
34
+
35
+ ```bash
36
+ h to
37
+ ```
38
+
39
+ ---
40
+
41
+ # Deep Abbreviations
42
+
43
+ Abbreviations work at every level.
44
+
45
+ `h to ls` resolves to `h todo ls`
46
+
47
+ ```bash
48
+ h to ls
49
+ ```
50
+
51
+ ---
52
+
53
+ # Ambiguous Abbreviations
54
+
55
+ When a prefix matches multiple commands,
56
+ Hiiro shows the candidates.
57
+
58
+ `h s` matches several commands:
59
+
60
+ ```bash
61
+ h s || true
62
+ ```
63
+
64
+ ---
65
+
66
+ # Narrowing Down
67
+
68
+ A longer prefix narrows the match.
69
+
70
+ `h se` gets closer:
71
+
72
+ ```bash
73
+ h se || true
74
+ ```
75
+
76
+ ---
77
+
78
+ # More Examples
79
+
80
+ `h wi ls` = `h window ls`
81
+
82
+ ```bash
83
+ h wi ls
84
+ ```
85
+
86
+ ---
87
+
88
+ # Even Shorter
89
+
90
+ `h br` = `h branch`
91
+
92
+ ```bash
93
+ h br
94
+ ```
95
+
96
+ ---
97
+
98
+ # h task
99
+
100
+ Manage **worktree-based tasks** via the Tasks plugin.
101
+
102
+ ```bash
103
+ h task
104
+ ```
105
+
106
+ ---
107
+
108
+ # h task ls
109
+
110
+ List current tasks:
111
+
112
+ ```bash
113
+ h task ls || true
114
+ ```
115
+
116
+ ---
117
+
118
+ # Task Abbreviation
119
+
120
+ `h ta ls` = `h task ls`
121
+
122
+ ```bash
123
+ h ta ls || true
124
+ ```
125
+
126
+ ---
127
+
128
+ # h subtask
129
+
130
+ Same interface as task, scoped to **subtasks**.
131
+
132
+ ```bash
133
+ h subtask
134
+ ```
135
+
136
+ ---
137
+
138
+ # h subtask ls
139
+
140
+ List current subtasks:
141
+
142
+ ```bash
143
+ h subtask ls || true
144
+ ```
145
+
146
+ ---
147
+
148
+ # Subtask Abbreviation
149
+
150
+ `h su ls` = `h subtask ls`
151
+
152
+ ```bash
153
+ h su ls || true
154
+ ```
155
+
156
+ ---
157
+
158
+ # h queue
159
+
160
+ Background **job queue** management.
161
+
162
+ ```bash
163
+ h queue
164
+ ```
165
+
166
+ ---
167
+
168
+ # h queue ls
169
+
170
+ List queued jobs:
171
+
172
+ ```bash
173
+ h queue ls || true
174
+ ```
175
+
176
+ ---
177
+
178
+ # h queue status
179
+
180
+ Show queue status:
181
+
182
+ ```bash
183
+ h queue status || true
184
+ ```
185
+
186
+ ---
187
+
188
+ # Queue Abbreviation
189
+
190
+ `h q ls` = `h queue ls`
191
+
192
+ ```bash
193
+ h q ls || true
194
+ ```
195
+
196
+ ---
197
+
198
+ # Key Takeaways
199
+
200
+ - Type just enough letters to be unambiguous
201
+ - Abbreviations work at every level
202
+ - `h task` / `h subtask` - worktree-based workflows
203
+ - `h queue` - background job management
204
+
205
+ ---
206
+
207
+ # Thanks!
208
+
209
+ ```
210
+ github.com/unixsuperhero/hiiro
211
+ ```
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.133
4
+ version: 0.1.134
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-01 00:00:00.000000000 Z
11
+ date: 2026-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -90,8 +90,10 @@ files:
90
90
  - bin/h-claude
91
91
  - bin/h-commit
92
92
  - bin/h-config
93
+ - bin/h-jumplist
93
94
  - bin/h-link
94
95
  - bin/h-misc
96
+ - bin/h-notify
95
97
  - bin/h-pane
96
98
  - bin/h-plugin
97
99
  - bin/h-pr
@@ -101,7 +103,8 @@ files:
101
103
  - bin/h-todo
102
104
  - bin/h-window
103
105
  - bin/h-wtree
104
- - bin/testlink
106
+ - demo.sh
107
+ - demo.typescript
105
108
  - docs/README.md
106
109
  - docs/docker-testing-guide.md
107
110
  - docs/h-buffer.md
@@ -112,6 +115,7 @@ files:
112
115
  - editboth
113
116
  - exe/h
114
117
  - exe/hiiro
118
+ - h-tmux-plugins.tmux
115
119
  - hiiro.gemspec
116
120
  - lib/hiiro.rb
117
121
  - lib/hiiro/bins.rb
@@ -146,12 +150,14 @@ files:
146
150
  - plugins/notify.rb
147
151
  - plugins/pins.rb
148
152
  - plugins/project.rb
153
+ - record-demo.sh
149
154
  - scripts/compare
150
155
  - scripts/install
151
156
  - scripts/publish
152
157
  - scripts/sync
153
158
  - scripts/test
154
159
  - scripts/update
160
+ - slides.md
155
161
  homepage: https://github.com/unixsuperhero/hiiro
156
162
  licenses:
157
163
  - MIT
data/bin/testlink DELETED
@@ -1 +0,0 @@
1
- ../exe/h