hiiro 0.1.86 → 0.1.87
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/CLAUDE.md +6 -6
- data/README.md +1 -1
- data/bin/h-branch +141 -87
- data/bin/h-buffer +2 -3
- data/bin/h-link +2 -4
- data/bin/h-pane +1 -12
- data/bin/h-pr +3 -67
- data/bin/h-project +1 -1
- data/bin/h-session +0 -12
- data/bin/h-sha +2 -7
- data/bin/h-window +1 -13
- data/bin/h-wtree +1 -1
- data/lib/hiiro/fuzzyfind.rb +32 -0
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +7 -12
- data/plugins/tasks.rb +6 -6
- metadata +7 -6
- data/lib/hiiro/history/entry.rb +0 -136
- data/lib/hiiro/history.rb +0 -454
- data/lib/hiiro/sk.rb +0 -18
data/lib/hiiro/history.rb
DELETED
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
require 'yaml'
|
|
2
|
-
require 'time'
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
require 'digest'
|
|
5
|
-
require_relative 'history/entry'
|
|
6
|
-
|
|
7
|
-
class Hiiro
|
|
8
|
-
class History
|
|
9
|
-
HISTORY_DIR = File.join(Dir.home, '.config/hiiro/history')
|
|
10
|
-
HISTORY_FILE = File.join(HISTORY_DIR, 'entries.yml')
|
|
11
|
-
LAST_STATE_FILE = File.join(HISTORY_DIR, 'last_state.yml')
|
|
12
|
-
|
|
13
|
-
class << self
|
|
14
|
-
def load(hiiro)
|
|
15
|
-
history = new
|
|
16
|
-
|
|
17
|
-
hiiro.add_subcmd(:history) do |*args|
|
|
18
|
-
subcmd, *rest = args
|
|
19
|
-
case subcmd
|
|
20
|
-
when 'ls', 'list', nil
|
|
21
|
-
history.list(limit: 20)
|
|
22
|
-
when 'all'
|
|
23
|
-
history.list(limit: nil)
|
|
24
|
-
when 'show'
|
|
25
|
-
history.show(rest.first)
|
|
26
|
-
when 'goto'
|
|
27
|
-
history.goto(rest.first)
|
|
28
|
-
when 'add'
|
|
29
|
-
history.add_manual(rest.join(' '), hiiro: hiiro)
|
|
30
|
-
when 'by'
|
|
31
|
-
# h history by pane|window|session|task|branch [value]
|
|
32
|
-
dimension, value = rest
|
|
33
|
-
history.list_by(dimension, value)
|
|
34
|
-
when 'clear'
|
|
35
|
-
history.clear!
|
|
36
|
-
puts "History cleared."
|
|
37
|
-
else
|
|
38
|
-
puts "Unknown history subcommand: #{subcmd}"
|
|
39
|
-
puts "Available: ls, all, show <id>, goto <id>, add <description>, by <dimension> [value], clear"
|
|
40
|
-
false
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Called automatically when a command runs (if in a task context)
|
|
46
|
-
def track(cmd:, hiiro: nil)
|
|
47
|
-
new.track_command(cmd: cmd, hiiro: hiiro)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def log(description: nil, pwd: Dir.pwd, cmd: nil, source: nil, task: nil, subtask: nil)
|
|
51
|
-
new.add(
|
|
52
|
-
description: description,
|
|
53
|
-
cmd: cmd,
|
|
54
|
-
pwd: pwd,
|
|
55
|
-
source: source,
|
|
56
|
-
task: task,
|
|
57
|
-
subtask: subtask
|
|
58
|
-
)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Query helpers for other commands (e.g., h pane history)
|
|
62
|
-
def by_pane(pane_id = nil)
|
|
63
|
-
pane_id ||= current_tmux_pane
|
|
64
|
-
new.filter(tmux_pane: pane_id)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def by_window(window_id = nil)
|
|
68
|
-
window_id ||= current_tmux_window
|
|
69
|
-
new.filter(tmux_window: window_id)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def by_session(session_name = nil)
|
|
73
|
-
session_name ||= current_tmux_session
|
|
74
|
-
new.filter(tmux_session: session_name)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def by_task(task_name)
|
|
78
|
-
new.filter(task: task_name)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def by_branch(branch_name)
|
|
82
|
-
new.filter(git_branch: branch_name)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def by_worktree(worktree_path)
|
|
86
|
-
new.filter(git_worktree: worktree_path)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def by_app(app_name)
|
|
90
|
-
new.filter(app: app_name)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
def current_tmux_pane
|
|
96
|
-
return nil unless ENV['TMUX']
|
|
97
|
-
`tmux display-message -p '#D'`.strip rescue nil
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def current_tmux_window
|
|
101
|
-
return nil unless ENV['TMUX']
|
|
102
|
-
`tmux display-message -p '#S:#I'`.strip rescue nil
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def current_tmux_session
|
|
106
|
-
return nil unless ENV['TMUX']
|
|
107
|
-
`tmux display-message -p '#S'`.strip rescue nil
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def initialize
|
|
112
|
-
ensure_history_dir
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def entries
|
|
116
|
-
@entries ||= load_entries
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def reload
|
|
120
|
-
@entries = nil
|
|
121
|
-
entries
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def filter(filters = {})
|
|
125
|
-
entries.select { |e| e.matches?(filters) }
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def list(limit: 20)
|
|
129
|
-
items = entries.reverse
|
|
130
|
-
items = items.first(limit) if limit
|
|
131
|
-
|
|
132
|
-
if items.empty?
|
|
133
|
-
puts "No history entries."
|
|
134
|
-
return true
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
items.reverse.each_with_index do |entry, idx|
|
|
138
|
-
puts entry.oneline(idx + 1)
|
|
139
|
-
end
|
|
140
|
-
true
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def list_by(dimension, value = nil)
|
|
144
|
-
dimension_map = {
|
|
145
|
-
'pane' => :tmux_pane,
|
|
146
|
-
'window' => :tmux_window,
|
|
147
|
-
'session' => :tmux_session,
|
|
148
|
-
'task' => :task,
|
|
149
|
-
'subtask' => :subtask,
|
|
150
|
-
'branch' => :git_branch,
|
|
151
|
-
'worktree' => :git_worktree,
|
|
152
|
-
'app' => :app,
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
field = dimension_map[dimension]
|
|
156
|
-
unless field
|
|
157
|
-
puts "Unknown dimension: #{dimension}"
|
|
158
|
-
puts "Available: #{dimension_map.keys.join(', ')}"
|
|
159
|
-
return false
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# If no value specified, use current context
|
|
163
|
-
value ||= current_value_for(field)
|
|
164
|
-
|
|
165
|
-
if value.nil?
|
|
166
|
-
puts "No current #{dimension} detected. Please specify a value."
|
|
167
|
-
return false
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
filtered = filter(field => value)
|
|
171
|
-
|
|
172
|
-
if filtered.empty?
|
|
173
|
-
puts "No history for #{dimension}=#{value}"
|
|
174
|
-
return true
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
puts "History for #{dimension}: #{value}"
|
|
178
|
-
puts
|
|
179
|
-
filtered.last(20).each_with_index do |entry, idx|
|
|
180
|
-
puts entry.oneline(idx + 1)
|
|
181
|
-
end
|
|
182
|
-
true
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def show(ref)
|
|
186
|
-
entry = find_entry(ref)
|
|
187
|
-
unless entry
|
|
188
|
-
puts "Entry not found: #{ref}"
|
|
189
|
-
return false
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
puts entry.full_display
|
|
193
|
-
true
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def goto(ref)
|
|
197
|
-
entry = find_entry(ref)
|
|
198
|
-
unless entry
|
|
199
|
-
puts "Entry not found: #{ref}"
|
|
200
|
-
return false
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
unless entry.tmux_session
|
|
204
|
-
puts "No tmux session recorded for this entry"
|
|
205
|
-
return false
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
target = entry.tmux_session
|
|
209
|
-
target += ":#{entry.tmux_window}" if entry.tmux_window
|
|
210
|
-
target += ".#{entry.tmux_pane}" if entry.tmux_pane
|
|
211
|
-
|
|
212
|
-
system('tmux', 'switch-client', '-t', target)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def add_manual(description, hiiro: nil)
|
|
216
|
-
add(
|
|
217
|
-
description: description,
|
|
218
|
-
source: 'manual',
|
|
219
|
-
cmd: hiiro&.full_command,
|
|
220
|
-
)
|
|
221
|
-
true
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Main tracking method - gathers all context and saves if changed
|
|
225
|
-
def track_command(cmd:, hiiro: nil, force: false)
|
|
226
|
-
context = gather_context
|
|
227
|
-
return nil unless context[:task] || force # Only track if in a task context (unless forced)
|
|
228
|
-
|
|
229
|
-
context[:cmd] = cmd
|
|
230
|
-
context[:source] = 'auto'
|
|
231
|
-
|
|
232
|
-
# Check if state changed
|
|
233
|
-
unless force
|
|
234
|
-
new_state = build_state_key(context)
|
|
235
|
-
return nil if state_unchanged?(new_state)
|
|
236
|
-
save_last_state(new_state)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
add(**context)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def add(
|
|
243
|
-
description: nil, cmd: nil, source: nil, pwd: nil,
|
|
244
|
-
tmux_session: nil, tmux_window: nil, tmux_pane: nil,
|
|
245
|
-
git_branch: nil, git_sha: nil, git_origin_sha: nil, git_worktree: nil,
|
|
246
|
-
task: nil, subtask: nil, app: nil
|
|
247
|
-
)
|
|
248
|
-
context = gather_context
|
|
249
|
-
|
|
250
|
-
entry_data = {
|
|
251
|
-
'id' => generate_id,
|
|
252
|
-
'timestamp' => Time.now.iso8601,
|
|
253
|
-
'description' => description,
|
|
254
|
-
'cmd' => cmd,
|
|
255
|
-
'pwd' => pwd || context[:pwd],
|
|
256
|
-
'source' => source,
|
|
257
|
-
'tmux_session' => tmux_session || context[:tmux_session],
|
|
258
|
-
'tmux_window' => tmux_window || context[:tmux_window],
|
|
259
|
-
'tmux_pane' => tmux_pane || context[:tmux_pane],
|
|
260
|
-
'git_branch' => git_branch || context[:git_branch],
|
|
261
|
-
'git_sha' => git_sha || context[:git_sha],
|
|
262
|
-
'git_origin_sha' => git_origin_sha || context[:git_origin_sha],
|
|
263
|
-
'git_worktree' => git_worktree || context[:git_worktree],
|
|
264
|
-
'task' => task || context[:task],
|
|
265
|
-
'subtask' => subtask || context[:subtask],
|
|
266
|
-
'app' => app || context[:app],
|
|
267
|
-
}.compact
|
|
268
|
-
|
|
269
|
-
all = load_raw_entries
|
|
270
|
-
all << entry_data
|
|
271
|
-
save_entries(all)
|
|
272
|
-
|
|
273
|
-
Entry.new(entry_data)
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def clear!
|
|
277
|
-
save_entries([])
|
|
278
|
-
File.delete(LAST_STATE_FILE) if File.exist?(LAST_STATE_FILE)
|
|
279
|
-
@entries = nil
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
private
|
|
283
|
-
|
|
284
|
-
def gather_context
|
|
285
|
-
{
|
|
286
|
-
pwd: Dir.pwd,
|
|
287
|
-
tmux_session: current_tmux_session,
|
|
288
|
-
tmux_window: current_tmux_window,
|
|
289
|
-
tmux_pane: current_tmux_pane,
|
|
290
|
-
git_branch: current_git_branch,
|
|
291
|
-
git_sha: current_git_sha,
|
|
292
|
-
git_origin_sha: current_git_origin_sha,
|
|
293
|
-
git_worktree: current_git_worktree,
|
|
294
|
-
task: current_task_name,
|
|
295
|
-
subtask: current_subtask_name,
|
|
296
|
-
app: current_app_name,
|
|
297
|
-
}
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def build_state_key(context)
|
|
301
|
-
context.slice(
|
|
302
|
-
:pwd, :tmux_session, :tmux_window, :tmux_pane,
|
|
303
|
-
:git_branch, :git_sha, :git_origin_sha, :git_worktree,
|
|
304
|
-
:task, :subtask, :app
|
|
305
|
-
).compact
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def state_unchanged?(new_state)
|
|
309
|
-
return false unless File.exist?(LAST_STATE_FILE)
|
|
310
|
-
last_state = YAML.load_file(LAST_STATE_FILE) rescue {}
|
|
311
|
-
last_state == new_state.transform_keys(&:to_s)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def save_last_state(state)
|
|
315
|
-
File.write(LAST_STATE_FILE, state.transform_keys(&:to_s).to_yaml)
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def current_value_for(field)
|
|
319
|
-
context = gather_context
|
|
320
|
-
context[field]
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def find_entry(ref)
|
|
324
|
-
return nil unless ref
|
|
325
|
-
|
|
326
|
-
if ref.to_s.match?(/^\d+$/)
|
|
327
|
-
idx = ref.to_i - 1
|
|
328
|
-
reversed = entries.reverse
|
|
329
|
-
return reversed[idx] if idx >= 0 && idx < reversed.length
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
entries.find { |e| e.id == ref.to_s }
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def generate_id
|
|
336
|
-
Time.now.strftime('%Y%m%d%H%M%S') + '-' + rand(10000).to_s.rjust(4, '0')
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def ensure_history_dir
|
|
340
|
-
FileUtils.mkdir_p(HISTORY_DIR) unless Dir.exist?(HISTORY_DIR)
|
|
341
|
-
|
|
342
|
-
unless File.exist?(HISTORY_FILE)
|
|
343
|
-
File.write(HISTORY_FILE, [].to_yaml)
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
def load_raw_entries
|
|
348
|
-
YAML.load_file(HISTORY_FILE) || []
|
|
349
|
-
rescue => e
|
|
350
|
-
[]
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def load_entries
|
|
354
|
-
load_raw_entries.map { |data| Entry.new(data) }
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def save_entries(entries_data)
|
|
358
|
-
# Keep only last 1000 entries to prevent unbounded growth
|
|
359
|
-
entries_data = entries_data.last(1000) if entries_data.length > 1000
|
|
360
|
-
File.write(HISTORY_FILE, entries_data.to_yaml)
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
# Tmux context
|
|
364
|
-
def current_tmux_session
|
|
365
|
-
return nil unless ENV['TMUX']
|
|
366
|
-
`tmux display-message -p '#S'`.strip rescue nil
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def current_tmux_window
|
|
370
|
-
return nil unless ENV['TMUX']
|
|
371
|
-
`tmux display-message -p '#I'`.strip rescue nil
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def current_tmux_pane
|
|
375
|
-
return nil unless ENV['TMUX']
|
|
376
|
-
ENV['TMUX_PANE'] || `tmux display-message -p '#P'`.strip rescue nil
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
# Git context
|
|
380
|
-
def current_git_branch
|
|
381
|
-
git_helper.branch_current
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
def current_git_sha
|
|
385
|
-
git_helper.commit('HEAD')
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
def current_git_origin_sha
|
|
389
|
-
branch = current_git_branch
|
|
390
|
-
return nil unless branch
|
|
391
|
-
git_helper.commit("origin/#{branch}")
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
def current_git_worktree
|
|
395
|
-
git_helper.root
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def git_helper
|
|
399
|
-
@git_helper ||= Git.new(nil, Dir.pwd)
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
# Task context
|
|
403
|
-
def current_task_name
|
|
404
|
-
env = environment
|
|
405
|
-
return nil unless env
|
|
406
|
-
|
|
407
|
-
task = env.task
|
|
408
|
-
return nil unless task
|
|
409
|
-
|
|
410
|
-
task.subtask? ? task.parent_name : task.name
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
def current_subtask_name
|
|
414
|
-
env = environment
|
|
415
|
-
return nil unless env
|
|
416
|
-
|
|
417
|
-
task = env.task
|
|
418
|
-
return nil unless task
|
|
419
|
-
return nil unless task.subtask?
|
|
420
|
-
|
|
421
|
-
task.short_name
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
def current_app_name
|
|
425
|
-
env = environment
|
|
426
|
-
return nil unless env
|
|
427
|
-
|
|
428
|
-
pwd = Dir.pwd
|
|
429
|
-
task = env.task
|
|
430
|
-
return nil unless task
|
|
431
|
-
|
|
432
|
-
tree = env.find_tree(task.tree_name)
|
|
433
|
-
return nil unless tree
|
|
434
|
-
|
|
435
|
-
# Check if pwd is in an app directory
|
|
436
|
-
env.all_apps.each do |app|
|
|
437
|
-
app_path = app.resolve(tree.path)
|
|
438
|
-
if pwd == app_path || pwd.start_with?(app_path + '/')
|
|
439
|
-
return app.name
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
nil
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def environment
|
|
447
|
-
@environment ||= begin
|
|
448
|
-
Environment.current
|
|
449
|
-
rescue NameError
|
|
450
|
-
nil
|
|
451
|
-
end
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
end
|
data/lib/hiiro/sk.rb
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
require 'open3'
|
|
2
|
-
require_relative 'shell'
|
|
3
|
-
|
|
4
|
-
class Hiiro
|
|
5
|
-
class Sk
|
|
6
|
-
def self.select(lines)
|
|
7
|
-
Shell.pipe_lines(lines, 'sk')
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def self.map_select(mapping)
|
|
11
|
-
keys = mapping.keys
|
|
12
|
-
|
|
13
|
-
key = select(keys)
|
|
14
|
-
|
|
15
|
-
mapping[key.to_s.chomp]
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|