hiiro 0.1.228 → 0.1.229

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: 354d3ef34e0e04716eb2f66882c970c9f46b20b14b9347140829578551f1b19f
4
- data.tar.gz: faaeedd7571edee922c3545882f11c1d9889be5f09fa4df625ee4ec3c9623b30
3
+ metadata.gz: aba86cbd556dd278f47a67b4ca0fbc31b24babdab3986d07e1fca7b48a1401bf
4
+ data.tar.gz: 4162f95dbf01a01c0013c21ddda78a20a40c0a1933d66b3fd808fc21aac64b3c
5
5
  SHA512:
6
- metadata.gz: d2583bef9b597c20a7a786623c78d55f46c8eb9d37f96d2d27a15f7e460c6785ce3cc5da04ef7bcba42c96f2104edb8f68876dfcfacbb5a97794bc2aa2d940bc
7
- data.tar.gz: ed9202b8ab05c8586a4e1bf7e68a7176502000e8f355c14c792d6a22ee7f193acfdf7a2f7a21aa29ea7aeaf8a67d9d6d06c65c642e6ca653227cd90302852ad0
6
+ metadata.gz: 91f6d01eec5b29c6cefc09535549fcf8faf2b392e8598746d6491782ea5a88cf346b35c1b0ca216ec6971217108294dba8b5d56ebed79f112811fb6eb6e09d77
7
+ data.tar.gz: 51d183885dc6000e77ea159a23b5698d4f5af74b74643976d6eaf82e9d42a6257adfdaffc3f7c2d73d8ad4ec22e85749d2687282bfcb9eddd126d50af78a367c
data/bin/h-jumplist CHANGED
@@ -76,10 +76,12 @@ Hiiro.run(*ARGV) do
76
76
  jumplist_conf = <<~CONF
77
77
  # --- Jumplist hooks (record pane/window/session navigation) ---
78
78
  set-hook -g after-select-pane "run-shell -b 'h jumplist record'"
79
+ set-hook -g pane-focus-in "run-shell -b 'h jumplist record'"
79
80
  set-hook -g after-select-window "run-shell -b 'h jumplist record'"
80
81
  set-hook -g after-new-window "run-shell -b 'h jumplist record'"
81
82
  set-hook -g after-split-window "run-shell -b 'h jumplist record'"
82
83
  set-hook -g client-session-changed "run-shell -b 'h jumplist record'"
84
+ set-hook -g session-window-changed "run-shell -b 'h jumplist record'"
83
85
 
84
86
  # --- Jumplist keybindings ---
85
87
  bind-key -r C-b run-shell "h jumplist back"
data/bin/h-notify CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "hiiro"
4
4
  require "fileutils"
5
+ require "yaml"
5
6
 
6
7
  TYPE_PRESETS = {
7
8
  'success' => { prefix: '[OK]', sound: 'Glass', title: 'Success' },
@@ -10,100 +11,117 @@ TYPE_PRESETS = {
10
11
  'warning' => { prefix: '[WARN]', sound: 'Purr', title: 'Warning' },
11
12
  }
12
13
 
13
- NOTIFY_DIR = File.join(Dir.home, '.config', 'hiiro', 'notify')
14
- FileUtils.mkdir_p(NOTIFY_DIR)
14
+ LOG_FILE = Hiiro::Config.data_path('notify_log.yml')
15
15
 
16
- def session_name
17
- `tmux display-message -p '\#{session_name}'`.strip
16
+ def read_log
17
+ return {} unless File.exist?(LOG_FILE)
18
+ YAML.safe_load(File.read(LOG_FILE)) || {}
19
+ rescue
20
+ {}
18
21
  end
19
22
 
20
- def notify_file(session = nil)
21
- session ||= session_name
22
- File.join(NOTIFY_DIR, "#{session}.log")
23
- end
24
-
25
- def read_notifications(session = nil)
26
- file = notify_file(session)
27
- return [] unless File.exist?(file)
28
- File.readlines(file, chomp: true).reject(&:empty?)
29
- end
30
-
31
- def write_notifications(entries, session = nil)
32
- file = notify_file(session)
33
- if entries.empty?
34
- File.delete(file) if File.exist?(file)
35
- else
36
- File.write(file, entries.join("\n") + "\n")
37
- end
23
+ def write_log(data)
24
+ FileUtils.mkdir_p(File.dirname(LOG_FILE))
25
+ data.reject! { |_, entries| entries.nil? || entries.empty? }
26
+ File.write(LOG_FILE, data.to_yaml)
38
27
  end
39
28
 
40
29
  def pane_exists?(pane_id)
41
- panes = `tmux list-panes -a -F '\#{pane_id}' 2>/dev/null`.strip.split("\n")
42
- panes.include?(pane_id)
30
+ Hiiro::Tmux::Panes.fetch(all: true).find_by_id(pane_id)
43
31
  end
44
32
 
45
- Hiiro.run(*ARGV) do
33
+ Hiiro.run(*ARGV, tasks: true) do
46
34
  @opts = Hiiro::Options.parse(args) do
47
35
  option(:type, short: 't', desc: 'Notification type (success, error, info, warning)', default: 'info')
48
36
  end
49
37
 
50
38
  @preset = TYPE_PRESETS[@opts.type] || TYPE_PRESETS['info']
51
39
 
52
- add_subcmd(:push) do |*push_args|
53
- message = push_args.join(' ')
40
+ add_subcmd(:push) do
41
+ message = @opts.args.join(' ')
54
42
  if message.empty?
55
43
  puts "Usage: h notify push [-t TYPE] \"message\""
56
44
  next
57
45
  end
58
46
 
59
- pane_id = `tmux display-message -p '\#{pane_id}'`.strip
60
- window_id = `tmux display-message -p '\#{window_id}'`.strip
61
- session = session_name
62
- pane_cmd = `tmux display-message -p '\#{pane_current_command}'`.strip
63
- timestamp = Time.now.to_i.to_s
47
+ pane = Hiiro::Tmux::Pane.current
48
+ session = pane&.session_name || Hiiro::Tmux::Session.current&.name
64
49
 
65
- entry = [pane_id, window_id, session, timestamp, pane_cmd, message].join('|')
50
+ unless pane && session
51
+ $stderr.puts "h notify push: not in a tmux session"
52
+ next
53
+ end
66
54
 
67
- entries = read_notifications(session)
68
- entries.unshift(entry)
69
- write_notifications(entries, session)
55
+ entry = {
56
+ 'pane_id' => pane.id,
57
+ 'window_id' => pane.window_id,
58
+ 'session' => session,
59
+ 'timestamp' => Time.now.to_i,
60
+ 'pane_cmd' => pane.command,
61
+ 'message' => message,
62
+ 'type' => @opts.type,
63
+ }
70
64
 
71
- # Show tmux display-message with type prefix
72
- system('tmux', 'display-message', "#{@preset[:prefix]} #{message}")
65
+ data = read_log
66
+ data[session] ||= []
67
+ # One entry per pane — replace existing entry for this pane, newest first
68
+ data[session].reject! { |e| e['pane_id'] == pane.id }
69
+ data[session].unshift(entry)
70
+ write_log(data)
73
71
 
74
- # Also send terminal-notifier
72
+ tmux_client.display_message("#{@preset[:prefix]} #{message}")
75
73
  system('terminal-notifier',
76
74
  '-title', @preset[:title],
77
75
  '-message', message,
78
76
  '-sound', @preset[:sound])
79
77
  end
80
78
 
79
+ add_subcmd(:ls) do
80
+ ls_opts = Hiiro::Options.parse(@opts.args) do
81
+ flag(:all, short: 'a', desc: 'Show all sessions')
82
+ end
83
+
84
+ data = read_log
85
+ sessions = ls_opts.all ? data.keys : [Hiiro::Tmux::Session.current&.name].compact
86
+
87
+ any = false
88
+ sessions.each do |sess|
89
+ entries = data[sess] || []
90
+ next if entries.empty?
91
+
92
+ puts "=== #{sess} ===" if ls_opts.all
93
+ entries.each_with_index do |e, i|
94
+ time_str = Time.at(e['timestamp']).strftime('%H:%M:%S')
95
+ prefix = TYPE_PRESETS[e['type']]&.dig(:prefix) || '[INFO]'
96
+ puts "%3d) %s [%s] %s: %s (%s)" % [i, prefix, e['pane_id'], e['pane_cmd'], e['message'], time_str]
97
+ end
98
+ any = true
99
+ end
100
+
101
+ puts "No notifications." unless any
102
+ end
103
+
81
104
  add_subcmd(:menu) do
82
- session = session_name
83
- entries = read_notifications(session)
105
+ session = Hiiro::Tmux::Session.current&.name
106
+ data = read_log
107
+ entries = data[session] || []
84
108
 
85
109
  if entries.empty?
86
- system('tmux', 'display-message', 'No notifications')
110
+ tmux_client.display_message('No notifications')
87
111
  next
88
112
  end
89
113
 
90
- # Build display-menu arguments
91
114
  menu_args = ['-T', 'Notifications']
92
115
 
93
- entries.first(10).each_with_index do |line, idx|
94
- parts = line.split('|')
95
- pane_id = parts[0]
96
- cmd = parts[4]
97
- msg = parts[5..].join('|')
98
- msg = msg[0..46] + '...' if msg.length > 50
99
-
100
- label = "#{idx}: [#{pane_id}] #{cmd}: #{msg}"
116
+ entries.first(10).each_with_index do |e, idx|
117
+ msg = e['message']
118
+ msg = msg[0..46] + '...' if msg.length > 50
119
+ prefix = TYPE_PRESETS[e['type']]&.dig(:prefix) || '[INFO]'
120
+ label = "#{idx}: #{prefix} [#{e['pane_id']}] #{e['pane_cmd']}: #{msg}"
101
121
  menu_args.push(label, idx.to_s, "run-shell 'h notify jump #{idx}'")
102
122
  end
103
123
 
104
- # Separator and clear option
105
124
  menu_args.push('', '', 'Clear all', 'c', "run-shell 'h notify clear'")
106
-
107
125
  system('tmux', 'display-menu', *menu_args)
108
126
  end
109
127
 
@@ -113,39 +131,140 @@ Hiiro.run(*ARGV) do
113
131
  next
114
132
  end
115
133
 
116
- index = index_str.to_i
117
- session = session_name
118
- entries = read_notifications(session)
134
+ index = index_str.to_i
135
+ session = Hiiro::Tmux::Session.current&.name
136
+ data = read_log
137
+ entries = data[session] || []
119
138
 
120
139
  if index >= entries.length
121
- system('tmux', 'display-message', 'Notification not found')
140
+ tmux_client.display_message('Notification not found')
122
141
  next
123
142
  end
124
143
 
125
144
  entry = entries[index]
126
- parts = entry.split('|')
127
- target_pane = parts[0]
128
- target_window = parts[1]
129
145
 
130
- unless pane_exists?(target_pane)
131
- system('tmux', 'display-message', "Pane #{target_pane} no longer exists")
146
+ unless pane_exists?(entry['pane_id'])
147
+ tmux_client.display_message("Pane #{entry['pane_id']} no longer exists")
132
148
  entries.delete_at(index)
133
- write_notifications(entries, session)
149
+ data[session] = entries
150
+ write_log(data)
134
151
  next
135
152
  end
136
153
 
137
- system('tmux', 'select-window', '-t', target_window)
138
- system('tmux', 'select-pane', '-t', target_pane)
154
+ tmux_client.select_window(entry['window_id'])
155
+ tmux_client.select_pane(entry['pane_id'])
139
156
 
140
- # Remove the notification
141
157
  entries.delete_at(index)
142
- write_notifications(entries, session)
158
+ data[session] = entries
159
+ write_log(data)
143
160
  end
144
161
 
145
162
  add_subcmd(:clear) do
146
- session = session_name
147
- file = notify_file(session)
148
- File.delete(file) if File.exist?(file)
149
- system('tmux', 'display-message', 'Notifications cleared')
163
+ session = Hiiro::Tmux::Session.current&.name
164
+ data = read_log
165
+ data.delete(session)
166
+ write_log(data)
167
+ tmux_client.display_message('Notifications cleared')
168
+ end
169
+
170
+ # Called by tmux hook: after-kill-pane / pane-died
171
+ add_subcmd(:remove_pane) do |pane_id = nil|
172
+ next unless pane_id
173
+ data = read_log
174
+ data.each_value { |entries| entries.reject! { |e| e['pane_id'] == pane_id } }
175
+ write_log(data)
176
+ end
177
+
178
+ # Called by tmux hook: window-closed
179
+ add_subcmd(:remove_window) do |window_id = nil|
180
+ next unless window_id
181
+ data = read_log
182
+ data.each_value { |entries| entries.reject! { |e| e['window_id'] == window_id } }
183
+ write_log(data)
184
+ end
185
+
186
+ # Called by tmux hook: session-closed
187
+ add_subcmd(:remove_session) do |sess = nil|
188
+ next unless sess
189
+ data = read_log
190
+ data.delete(sess)
191
+ write_log(data)
192
+ end
193
+
194
+ add_subcmd(:setup) do
195
+ conf_dir = File.join(Dir.home, '.config', 'tmux')
196
+ conf_file = File.join(conf_dir, 'h-notify.tmux.conf')
197
+ tmux_conf = File.join(Dir.home, '.tmux.conf')
198
+ source_line = "source-file #{conf_file}"
199
+
200
+ FileUtils.mkdir_p(conf_dir)
201
+
202
+ notify_conf = <<~'CONF'
203
+ # --- Notify hooks (clean up notifications when panes/windows/sessions close) ---
204
+ set-hook -g after-kill-pane "run-shell -b 'h notify remove_pane \#{hook_pane_id}'"
205
+ set-hook -g pane-died "run-shell -b 'h notify remove_pane \#{pane_id}'"
206
+ set-hook -g pane-exited "run-shell -b 'h notify remove_pane \#{pane_id}'"
207
+ set-hook -g window-unlinked "run-shell -b 'h notify remove_window \#{hook_window_id}'"
208
+ set-hook -g session-closed "run-shell -b 'h notify remove_session \#{hook_session_name}'"
209
+
210
+ # --- Notify keybinding (prefix + N to open notification menu) ---
211
+ bind-key N run-shell "h notify menu"
212
+ CONF
213
+
214
+ File.write(conf_file, notify_conf)
215
+ puts "Wrote #{conf_file}"
216
+
217
+ if File.exist?(tmux_conf)
218
+ existing = File.read(tmux_conf)
219
+ if existing.include?(source_line)
220
+ puts "#{tmux_conf} already sources #{conf_file}"
221
+ else
222
+ File.open(tmux_conf, 'a') { |f| f.puts; f.puts source_line }
223
+ puts "Added '#{source_line}' to #{tmux_conf}"
224
+ end
225
+ else
226
+ File.write(tmux_conf, source_line + "\n")
227
+ puts "Created #{tmux_conf} with source line"
228
+ end
229
+
230
+ puts
231
+ puts "Reload tmux config:"
232
+ puts " tmux source-file ~/.tmux.conf"
233
+ puts
234
+ puts "─────────────────────────────────────────────────"
235
+ puts "Claude Code hook setup"
236
+ puts "─────────────────────────────────────────────────"
237
+ puts
238
+ puts "Run this to auto-update ~/.claude/settings.json:"
239
+ puts " h notify update_hooks"
240
+ puts
241
+ puts "Or manually update hooks to:"
242
+ puts
243
+ puts ' Notification: h alert ... ; h notify push -t info "$MSG"'
244
+ puts ' Stop: h alert ... ; h notify push -t success "Work completed"'
245
+ end
246
+
247
+ add_subcmd(:update_hooks) do
248
+ settings_file = File.join(Dir.home, '.claude', 'settings.json')
249
+
250
+ unless File.exist?(settings_file)
251
+ puts "#{settings_file} not found"
252
+ next
253
+ end
254
+
255
+ require 'json'
256
+ settings = JSON.parse(File.read(settings_file))
257
+ settings['hooks'] ||= {}
258
+
259
+ notification_cmd = %q(MSG=$(cat | jq -r '.message // "Needs input"'); h alert -t 'Claude Code' -m "$MSG" -s k -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t info "$MSG")
260
+ stop_cmd = %q(h alert -t 'Claude Code' -m 'Work completed' -s b -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t success 'Work completed')
261
+
262
+ settings['hooks']['Notification'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => notification_cmd }] }]
263
+ settings['hooks']['Stop'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => stop_cmd }] }]
264
+
265
+ File.write(settings_file, JSON.pretty_generate(settings))
266
+ puts "Updated #{settings_file}"
267
+ puts " Notification -> h alert + h notify push -t info"
268
+ puts " Stop -> h alert + h notify push -t success"
150
269
  end
151
270
  end
data/bin/h-pr CHANGED
@@ -366,8 +366,8 @@ class PinnedPRManager
366
366
  as = r['approved'].to_i
367
367
  crs = r['changes_requested'].to_i
368
368
 
369
- as_str = as > 0 ? "\e[1;92m#{as}\e[0m" : '-'
370
- crs_str = crs > 0 ? "\e[1;33m#{crs}\e[0m" : '-'
369
+ as_str = as > 0 ? "\e[30;102m#{as}\e[0m" : '-'
370
+ crs_str = crs > 0 ? "\e[30;103m#{crs}\e[0m" : '-'
371
371
  reviews_str = "#{as_str}a/#{crs_str}cr"
372
372
 
373
373
  repo = pr_repo(pr)
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.228"
2
+ VERSION = "0.1.229"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.228
4
+ version: 0.1.229
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota