hiiro 0.1.132 → 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: a94b559f926e6063309e12ab18a22be9c452994768647e0af1252980909fa330
4
- data.tar.gz: 9c88e2f283302dd57b96814a707c355a96f6506ca8cd05a5e4e0278ce4612af3
3
+ metadata.gz: e5c49242912889d365b5eabdfdc3adeb72ea6d55dd7311280dcc9b3f6ac40013
4
+ data.tar.gz: 219b6710d528233190c7bfb30465d4f3b293cea21f24c8a01561768566ce83ff
5
5
  SHA512:
6
- metadata.gz: 3cfcd22992e39f48c4293f5a839e57831353e626d5930ca07f5ee093a782e36d43a241e45d0437a4b1fbd450537ae397abffdb37904af98288314c3809c6149b
7
- data.tar.gz: a7f92cc931a2fee9cb6c10e8b7e7e3bbe3a695385309c1973bb07ab22b00e2f9d0dcf374b3e320ad3179fc16d4267ac59a6fb0340ad2713e3e1046dfd6d4337b
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-claude CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'hiiro'
4
4
  require 'shellwords'
5
+ require 'tempfile'
5
6
 
6
7
  opts = Hiiro::Options.setup {
7
8
  flag(:danger, short: :d, default: false)
@@ -55,4 +56,100 @@ Hiiro.run(*ARGV, plugins: [Pins]) {
55
56
 
56
57
  system('tmux', 'split-window', '-v', '-l', size, start_command(opts))
57
58
  }
59
+
60
+ add_subcmd(:env) { |*args|
61
+ ap ENV
62
+ }
63
+
64
+ add_subcmd(:inline) { |*args|
65
+ if args.empty? && !$stdin.tty?
66
+ prompt = $stdin.read.strip
67
+ elsif args.any?
68
+ prompt = args.join(' ')
69
+ else
70
+ tmpfile = Tempfile.new(['claude-inline-', '.md'])
71
+ tmpfile.close
72
+ editor = ENV['EDITOR'] || 'vim'
73
+ system(editor, tmpfile.path)
74
+ prompt = File.read(tmpfile.path).strip
75
+ tmpfile.unlink
76
+ if prompt.empty?
77
+ puts "Aborted (empty file)"
78
+ next
79
+ end
80
+ end
81
+
82
+ IO.popen(['claude', '-p'], 'w') { |io| io.write(prompt) }
83
+ }
84
+
85
+ add_subcmd(:loop) { |*args|
86
+ editor = ENV['EDITOR'] || 'vim'
87
+ separator = "-" * 72
88
+ history = []
89
+
90
+ loop do
91
+ tmpfile = Tempfile.new(['claude-loop-', '.md'])
92
+
93
+ if history.any?
94
+ lines = []
95
+ history.each_with_index do |(prev_prompt, response), i|
96
+ lines << "# Prompt #{i + 1} (old - do not edit)"
97
+ prev_prompt.lines.each { |l| lines << "> #{l.chomp}" }
98
+ lines << ""
99
+ lines << "## Response #{i + 1}"
100
+ lines << response
101
+ lines << ""
102
+ end
103
+ lines << separator
104
+ lines << "# Put your new prompt below this line"
105
+ lines << ""
106
+ tmpfile.write(lines.join("\n"))
107
+ end
108
+
109
+ tmpfile.close
110
+ system(editor, tmpfile.path)
111
+ content = File.read(tmpfile.path).strip
112
+ tmpfile.unlink
113
+
114
+ if content.empty?
115
+ puts "Done."
116
+ break
117
+ end
118
+
119
+ # Extract only the new prompt (below the separator)
120
+ if content.include?(separator)
121
+ new_prompt = content.split(separator, 2).last.strip
122
+ # Strip the instruction line if present
123
+ new_prompt = new_prompt.sub(/\A# Put your new prompt below this line\n*/, '').strip
124
+ else
125
+ new_prompt = content.strip
126
+ end
127
+
128
+ if new_prompt.empty?
129
+ puts "Done."
130
+ break
131
+ end
132
+
133
+ # Build the full prompt with context for claude
134
+ full_prompt = ""
135
+ if history.any?
136
+ history.each_with_index do |(prev_prompt, response), i|
137
+ full_prompt << "--- Previous prompt #{i + 1} ---\n#{prev_prompt}\n\n"
138
+ full_prompt << "--- Response #{i + 1} ---\n#{response}\n\n"
139
+ end
140
+ full_prompt << "--- New prompt ---\n"
141
+ end
142
+ full_prompt << new_prompt
143
+
144
+ puts "\nSending to claude...\n\n"
145
+ response = IO.popen(['claude', '-p'], 'r+') { |io|
146
+ io.write(full_prompt)
147
+ io.close_write
148
+ io.read
149
+ }
150
+
151
+ puts response
152
+ history << [new_prompt, response]
153
+ end
154
+ }
58
155
  }
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