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 +4 -4
- data/README.md +2 -0
- data/bin/h-jumplist +183 -0
- data/bin/h-notify +148 -0
- data/bin/h-pr +29 -137
- data/bin/h-project +1 -1
- data/demo.sh +116 -0
- data/demo.typescript +0 -0
- data/h-tmux-plugins.tmux +30 -0
- data/lib/hiiro/tmux.rb +26 -10
- data/lib/hiiro/version.rb +1 -1
- data/record-demo.sh +176 -0
- data/slides.md +211 -0
- metadata +9 -3
- data/bin/testlink +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5c49242912889d365b5eabdfdc3adeb72ea6d55dd7311280dcc9b3f6ac40013
|
|
4
|
+
data.tar.gz: 219b6710d528233190c7bfb30465d4f3b293cea21f24c8a01561768566ce83ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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(:
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
|
|
781
|
-
|
|
688
|
+
pinned.each_with_index do |pr, idx|
|
|
689
|
+
puts pinned_manager.display_pinned(pr, idx)
|
|
690
|
+
end
|
|
782
691
|
|
|
783
|
-
|
|
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
|
-
|
|
716
|
+
last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
|
|
795
717
|
puts "---"
|
|
796
|
-
puts "
|
|
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
|
data/h-tmux-plugins.tmux
ADDED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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.
|
|
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-
|
|
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
|
-
-
|
|
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
|