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.
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