hiiro 0.1.51 → 0.1.53

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: 6181ad8d712bc544591aaa46f8c5c555b33f76cf6ed572cb70369f1f56b91bd9
4
- data.tar.gz: 1a7490eb2e795ffab86b7b4ad1f24fe46fc6d0ba508e76bc074a88470b1eb437
3
+ metadata.gz: b5b124b6980d905a9d41a47e5f81de7aa5920ba8047168a952fb826a7bf98df2
4
+ data.tar.gz: 2fc08d8b0de4bb03678bcf15f07bde9b4e74745260cb2b5c83f1d6601cb3dce1
5
5
  SHA512:
6
- metadata.gz: 2ad79ec0a1e709d3cc7ce65e3142c23612b34f4b63e15d2811671cdca96858744f9dc57d2541b297df6e3abddaf61714ffe06f9491d694622ce129e65a3d5841
7
- data.tar.gz: 7e9e81804965cb3d58a7baa8a2d2ed1597ff474ec0cf56998a86328fe91014286ff7604d7faf6940b6e09e91453afd357285f2be3d2160fb22455dea65275ec6
6
+ metadata.gz: 053f53b20484aa74d612d2948a7c4439ed6b901d2bd7a4d600cabd84c8fd012c7e9903898a69243c3aca7de753ec37a66192d3915337c503c16eb2152075f0a0
7
+ data.tar.gz: ae6342874f5bcda420849a100a7830fd910802d76b7302a8d6e41707c5184f5f7029933e54600e1f314c04d37c593c3edfe2a6ab9ac125b5a061fc41e140fbb5
data/bin/h-app CHANGED
@@ -11,6 +11,10 @@ def load_apps
11
11
  YAML.safe_load_file(APPS_FILE) || {}
12
12
  end
13
13
 
14
+ def save_apps(apps)
15
+ File.write(APPS_FILE, apps.to_yaml)
16
+ end
17
+
14
18
  def task_root
15
19
  # Fall back to current task's tree path if not in a git repo
16
20
  env = Environment.current rescue nil
@@ -135,4 +139,70 @@ Hiiro.run(*ARGV, plugins: [:Tasks]) {
135
139
  puts "App '#{app_name}' not found"
136
140
  end
137
141
  }
142
+
143
+ add_subcmd(:add) { |app_name=nil, relative_path=nil|
144
+ if app_name.nil? || app_name.empty?
145
+ puts "Usage: h app add <app_name> <relative_path_from_root>"
146
+ next
147
+ end
148
+
149
+ if relative_path.nil? || relative_path.empty?
150
+ puts "Usage: h app add <app_name> <relative_path_from_root>"
151
+ next
152
+ end
153
+
154
+ apps = load_apps
155
+
156
+ if apps.key?(app_name)
157
+ puts "App '#{app_name}' already exists with path: #{apps[app_name]}"
158
+ puts "Remove it first with: h app rm #{app_name}"
159
+ next
160
+ end
161
+
162
+ apps[app_name] = relative_path
163
+ save_apps(apps)
164
+ puts "Added app '#{app_name}' => #{relative_path}"
165
+ }
166
+
167
+ add_subcmd(:rm) { |app_name=nil|
168
+ if app_name.nil? || app_name.empty?
169
+ puts "Usage: h app rm <app_name>"
170
+ next
171
+ end
172
+
173
+ apps = load_apps
174
+
175
+ unless apps.key?(app_name)
176
+ puts "App '#{app_name}' not found"
177
+ puts
178
+ puts "Available apps:"
179
+ apps.each { |name, path| puts format(" %-20s => %s", name, path) }
180
+ next
181
+ end
182
+
183
+ removed_path = apps.delete(app_name)
184
+ save_apps(apps)
185
+ puts "Removed app '#{app_name}' (was: #{removed_path})"
186
+ }
187
+
188
+ add_subcmd(:remove) { |app_name=nil|
189
+ if app_name.nil? || app_name.empty?
190
+ puts "Usage: h app remove <app_name>"
191
+ next
192
+ end
193
+
194
+ apps = load_apps
195
+
196
+ unless apps.key?(app_name)
197
+ puts "App '#{app_name}' not found"
198
+ puts
199
+ puts "Available apps:"
200
+ apps.each { |name, path| puts format(" %-20s => %s", name, path) }
201
+ next
202
+ end
203
+
204
+ removed_path = apps.delete(app_name)
205
+ save_apps(apps)
206
+ puts "Removed app '#{app_name}' (was: #{removed_path})"
207
+ }
138
208
  }
data/bin/h-link CHANGED
@@ -10,7 +10,7 @@ require "hiiro"
10
10
  class LinkManager
11
11
  LINKS_FILE = File.join(Dir.home, '.config/hiiro/links.yml')
12
12
  LINK_TEMPLATE = {
13
- 'url' => 'https://',
13
+ 'url' => '',
14
14
  'description' => '',
15
15
  'shorthand' => nil,
16
16
  'created_at' => Time.now.iso8601,
data/bin/h-pane CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- load File.join(Dir.home, 'bin', 'h')
3
+ require 'hiiro'
4
4
 
5
5
  o = Hiiro.init(*ARGV, plugins: [Pins])
6
6
 
@@ -60,4 +60,15 @@ o.add_subcmd(:resize) { |*args|
60
60
  system('tmux', 'resize-pane', *args)
61
61
  }
62
62
 
63
+ o.add_subcmd(:history) { |pane_id=nil|
64
+ entries = Hiiro::History.by_pane(pane_id)
65
+ if entries.empty?
66
+ puts "No history for this pane"
67
+ else
68
+ puts "History for pane: #{pane_id || ENV['TMUX_PANE'] || 'current'}"
69
+ puts
70
+ entries.last(20).each_with_index { |e, i| puts e.oneline(i + 1) }
71
+ end
72
+ }
73
+
63
74
  o.run
data/bin/h-session CHANGED
@@ -60,4 +60,16 @@ o.add_subcmd(:info) { |*args|
60
60
  system('tmux', 'display-message', '-p', '#{session_name}: #{session_windows} windows, #{session_attached} attached', *args)
61
61
  }
62
62
 
63
+ o.add_subcmd(:history) { |session_name=nil|
64
+ session_name ||= `tmux display-message -p '#S'`.strip if ENV['TMUX']
65
+ entries = Hiiro::History.by_session(session_name)
66
+ if entries.empty?
67
+ puts "No history for this session"
68
+ else
69
+ puts "History for session: #{session_name || 'current'}"
70
+ puts
71
+ entries.last(20).each_with_index { |e, i| puts e.oneline(i + 1) }
72
+ end
73
+ }
74
+
63
75
  o.run
data/bin/h-window CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- load File.join(Dir.home, 'bin', 'h')
3
+ require 'hiiro'
4
4
 
5
5
  o = Hiiro.init(*ARGV, plugins: [Pins])
6
6
 
@@ -56,4 +56,16 @@ o.add_subcmd(:unlink) { |*args|
56
56
  system('tmux', 'unlink-window', *args)
57
57
  }
58
58
 
59
+ o.add_subcmd(:history) { |window_id=nil|
60
+ window_id ||= `tmux display-message -p '#I'`.strip if ENV['TMUX']
61
+ entries = Hiiro::History.by_window(window_id)
62
+ if entries.empty?
63
+ puts "No history for this window"
64
+ else
65
+ puts "History for window: #{window_id || 'current'}"
66
+ puts
67
+ entries.last(20).each_with_index { |e, i| puts e.oneline(i + 1) }
68
+ end
69
+ }
70
+
59
71
  o.run
@@ -0,0 +1,135 @@
1
+ class Hiiro
2
+ class History
3
+ class Entry
4
+ FIELDS = %i[
5
+ id timestamp source cmd description pwd
6
+ tmux_session tmux_window tmux_pane
7
+ git_branch git_sha git_origin_sha git_worktree
8
+ task subtask app
9
+ ].freeze
10
+
11
+ attr_reader(*FIELDS)
12
+
13
+ def initialize(data)
14
+ data ||= {}
15
+ @id = data['id']
16
+ @timestamp = data['timestamp']
17
+ @source = data['source']
18
+ @cmd = data['cmd']
19
+ @description = data['description']
20
+ @pwd = data['pwd']
21
+ @tmux_session = data['tmux_session']
22
+ @tmux_window = data['tmux_window']
23
+ @tmux_pane = data['tmux_pane']
24
+ @git_branch = data['git_branch']
25
+ @git_sha = data['git_sha']
26
+ @git_origin_sha = data['git_origin_sha']
27
+ @git_worktree = data['git_worktree']
28
+ @task = data['task']
29
+ @subtask = data['subtask']
30
+ @app = data['app']
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ 'id' => id,
36
+ 'timestamp' => timestamp,
37
+ 'source' => source,
38
+ 'cmd' => cmd,
39
+ 'description' => description,
40
+ 'pwd' => pwd,
41
+ 'tmux_session' => tmux_session,
42
+ 'tmux_window' => tmux_window,
43
+ 'tmux_pane' => tmux_pane,
44
+ 'git_branch' => git_branch,
45
+ 'git_sha' => git_sha,
46
+ 'git_origin_sha' => git_origin_sha,
47
+ 'git_worktree' => git_worktree,
48
+ 'task' => task,
49
+ 'subtask' => subtask,
50
+ 'app' => app,
51
+ }.compact
52
+ end
53
+
54
+ # Data used for change detection (excludes timestamp, id, cmd)
55
+ def state_key
56
+ {
57
+ 'pwd' => pwd,
58
+ 'tmux_session' => tmux_session,
59
+ 'tmux_window' => tmux_window,
60
+ 'tmux_pane' => tmux_pane,
61
+ 'git_branch' => git_branch,
62
+ 'git_sha' => git_sha,
63
+ 'git_origin_sha' => git_origin_sha,
64
+ 'git_worktree' => git_worktree,
65
+ 'task' => task,
66
+ 'subtask' => subtask,
67
+ 'app' => app,
68
+ }.compact
69
+ end
70
+
71
+ def state_fingerprint
72
+ Digest::SHA256.hexdigest(state_key.to_yaml)[0..15]
73
+ end
74
+
75
+ def short_line(index)
76
+ time_str = timestamp ? Time.parse(timestamp).strftime('%Y-%m-%d %H:%M') : Time.now.strftime('%Y-%m-%d %H:%M')
77
+ desc = description || cmd || '(no description)'
78
+ desc = desc[0..50] + '...' if desc.length > 53
79
+ [
80
+ format('%3d %s %s', index, time_str, desc),
81
+ format(' %s @ %s', git_branch || '(no branch)', task || '(no task)'),
82
+ ].join("\n")
83
+ end
84
+
85
+ def oneline(index = nil)
86
+ time_str = timestamp ? Time.parse(timestamp).strftime('%m/%d %H:%M') : ''
87
+ prefix = index ? format('%3d ', index) : ''
88
+ branch_str = git_branch ? "[#{git_branch}]" : ''
89
+ task_str = task ? "(#{task})" : ''
90
+ cmd_str = cmd || description || ''
91
+ cmd_str = cmd_str[0..40] + '...' if cmd_str.length > 43
92
+
93
+ "#{prefix}#{time_str} #{branch_str.ljust(20)} #{task_str.ljust(15)} #{cmd_str}"
94
+ end
95
+
96
+ def full_display
97
+ lines = []
98
+ lines << "ID: #{id}"
99
+ lines << "Timestamp: #{timestamp}"
100
+ lines << "Description: #{description}" if description
101
+ lines << "Command: #{cmd}" if cmd
102
+ lines << "PWD: #{pwd}" if pwd
103
+ lines << "Source: #{source}" if source
104
+ lines << ""
105
+ lines << "Tmux:"
106
+ lines << " Session: #{tmux_session}" if tmux_session
107
+ lines << " Window: #{tmux_window}" if tmux_window
108
+ lines << " Pane: #{tmux_pane}" if tmux_pane
109
+ lines << ""
110
+ lines << "Git:"
111
+ lines << " Worktree: #{git_worktree}" if git_worktree
112
+ lines << " Branch: #{git_branch}" if git_branch
113
+ lines << " SHA: #{git_sha}" if git_sha
114
+ lines << " Origin: #{git_origin_sha}" if git_origin_sha
115
+ lines << ""
116
+ lines << "Task:"
117
+ lines << " Task: #{task}" if task
118
+ lines << " Subtask: #{subtask}" if subtask
119
+ lines << " App: #{app}" if app
120
+ lines.join("\n")
121
+ end
122
+
123
+ # Filter matching
124
+ def matches?(filters)
125
+ filters.all? do |key, value|
126
+ entry_value = send(key) rescue nil
127
+ next true if value.nil?
128
+ next entry_value == value if value.is_a?(String)
129
+ next value.include?(entry_value) if value.is_a?(Array)
130
+ true
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
data/lib/hiiro/history.rb CHANGED
@@ -1,84 +1,14 @@
1
1
  require 'yaml'
2
2
  require 'time'
3
3
  require 'fileutils'
4
+ require 'digest'
5
+ require_relative 'history/entry'
4
6
 
5
7
  class Hiiro
6
8
  class History
7
- HISTORY_FILE = File.join(Dir.home, '.config/hiiro/history.yml')
8
-
9
- class Entry
10
- attr_reader :id, :timestamp, :source, :cmd, :description
11
- attr_reader :tmux_session, :tmux_window, :tmux_pane
12
- attr_reader :git_branch, :git_worktree
13
- attr_reader :task, :subtask
14
-
15
- def initialize(data)
16
- data ||= {}
17
- @id = data['id']
18
- @timestamp = data['timestamp']
19
- @source = data['source']
20
- @cmd = data['cmd']
21
- @description = data['description']
22
- @tmux_session = data['tmux_session']
23
- @tmux_window = data['tmux_window']
24
- @tmux_pane = data['tmux_pane']
25
- @git_branch = data['git_branch']
26
- @git_worktree = data['git_worktree']
27
- @task = data['task']
28
- @subtask = data['subtask']
29
- end
30
-
31
- def to_h
32
- {
33
- 'id' => id,
34
- 'timestamp' => timestamp,
35
- 'source' => source,
36
- 'cmd' => cmd,
37
- 'description' => description,
38
- 'tmux_session' => tmux_session,
39
- 'tmux_window' => tmux_window,
40
- 'tmux_pane' => tmux_pane,
41
- 'git_branch' => git_branch,
42
- 'git_worktree' => git_worktree,
43
- 'task' => task,
44
- 'subtask' => subtask,
45
- }.compact
46
- end
47
-
48
- def short_line(index)
49
- time_str = timestamp ? Time.parse(timestamp).strftime('%Y-%m-%d %H:%M') : Time.now.iso8601
50
- desc = description || cmd || '(no description)'
51
- desc = desc[0..60] + '...' if desc.length > 63
52
- [
53
- format('%3d %s %s', index, time_str, desc),
54
- format(' cmd: %s', cmd),
55
- ].join("\n")
56
- end
57
-
58
- def full_display
59
- lines = []
60
- lines << "ID: #{id}"
61
- lines << "Timestamp: #{timestamp}"
62
- lines << "Description: #{description}" if description
63
- lines << "Command: #{cmd}" if cmd
64
- lines << "PWD: #{pwd}" if pwd
65
- lines << "Source: #{source}" if source
66
- lines << ""
67
- lines << "Tmux:"
68
- lines << " Session: #{tmux_session}" if tmux_session
69
- lines << " Window: #{tmux_window}" if tmux_window
70
- lines << " Pane: #{tmux_pane}" if tmux_pane
71
- lines << ""
72
- lines << "Git:"
73
- lines << " Branch: #{git_branch}" if git_branch
74
- lines << " Worktree: #{git_worktree}" if git_worktree
75
- lines << ""
76
- lines << "Task:"
77
- lines << " Task: #{task}" if task
78
- lines << " Subtask: #{subtask}" if subtask
79
- lines.join("\n")
80
- end
81
- end
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')
82
12
 
83
13
  class << self
84
14
  def load(hiiro)
@@ -88,21 +18,35 @@ class Hiiro
88
18
  subcmd, *rest = args
89
19
  case subcmd
90
20
  when 'ls', 'list', nil
91
- history.list
21
+ history.list(limit: 20)
22
+ when 'all'
23
+ history.list(limit: nil)
92
24
  when 'show'
93
25
  history.show(rest.first)
94
26
  when 'goto'
95
27
  history.goto(rest.first)
96
28
  when 'add'
97
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."
98
37
  else
99
38
  puts "Unknown history subcommand: #{subcmd}"
100
- puts "Available: ls, show <id>, goto <id>, add <description>"
39
+ puts "Available: ls, all, show <id>, goto <id>, add <description>, by <dimension> [value], clear"
101
40
  false
102
41
  end
103
42
  end
104
43
  end
105
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
+
106
50
  def log(description: nil, pwd: Dir.pwd, cmd: nil, source: nil, task: nil, subtask: nil)
107
51
  new.add(
108
52
  description: description,
@@ -113,10 +57,59 @@ class Hiiro
113
57
  subtask: subtask
114
58
  )
115
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
116
109
  end
117
110
 
118
111
  def initialize
119
- ensure_history_file
112
+ ensure_history_dir
120
113
  end
121
114
 
122
115
  def entries
@@ -128,14 +121,63 @@ class Hiiro
128
121
  entries
129
122
  end
130
123
 
131
- def list
132
- if entries.empty?
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
133
  puts "No history entries."
134
134
  return true
135
135
  end
136
136
 
137
- entries.each_with_index do |entry, idx|
138
- puts entry.short_line(idx + 1)
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)
139
181
  end
140
182
  true
141
183
  end
@@ -174,26 +216,49 @@ class Hiiro
174
216
  add(
175
217
  description: description,
176
218
  source: 'manual',
177
- cmd: hiiro.full_command,
219
+ cmd: hiiro&.full_command,
178
220
  )
179
221
  true
180
222
  end
181
223
 
182
- def add(description: nil, cmd: nil, source: nil, task: nil, subtask: nil, pwd: Dir.pwd)
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(description: nil, cmd: nil, source: nil, task: nil, subtask: nil, pwd: nil, app: nil)
243
+ context = gather_context
244
+
183
245
  entry_data = {
184
246
  'id' => generate_id,
185
247
  'timestamp' => Time.now.iso8601,
186
248
  'description' => description,
187
249
  'cmd' => cmd,
188
- 'pwd' => pwd,
250
+ 'pwd' => pwd || context[:pwd],
189
251
  'source' => source,
190
- 'tmux_session' => current_tmux_session,
191
- 'tmux_window' => current_tmux_window,
192
- 'tmux_pane' => current_tmux_pane,
193
- 'git_branch' => current_git_branch,
194
- 'git_worktree' => current_git_worktree,
195
- 'task' => task,
196
- 'subtask' => subtask,
252
+ 'tmux_session' => context[:tmux_session],
253
+ 'tmux_window' => context[:tmux_window],
254
+ 'tmux_pane' => context[:tmux_pane],
255
+ 'git_branch' => context[:git_branch],
256
+ 'git_sha' => context[:git_sha],
257
+ 'git_origin_sha' => context[:git_origin_sha],
258
+ 'git_worktree' => context[:git_worktree],
259
+ 'task' => task || context[:task],
260
+ 'subtask' => subtask || context[:subtask],
261
+ 'app' => app || context[:app],
197
262
  }.compact
198
263
 
199
264
  all = load_raw_entries
@@ -203,14 +268,60 @@ class Hiiro
203
268
  Entry.new(entry_data)
204
269
  end
205
270
 
271
+ def clear!
272
+ save_entries([])
273
+ File.delete(LAST_STATE_FILE) if File.exist?(LAST_STATE_FILE)
274
+ @entries = nil
275
+ end
276
+
206
277
  private
207
278
 
279
+ def gather_context
280
+ {
281
+ pwd: Dir.pwd,
282
+ tmux_session: current_tmux_session,
283
+ tmux_window: current_tmux_window,
284
+ tmux_pane: current_tmux_pane,
285
+ git_branch: current_git_branch,
286
+ git_sha: current_git_sha,
287
+ git_origin_sha: current_git_origin_sha,
288
+ git_worktree: current_git_worktree,
289
+ task: current_task_name,
290
+ subtask: current_subtask_name,
291
+ app: current_app_name,
292
+ }
293
+ end
294
+
295
+ def build_state_key(context)
296
+ context.slice(
297
+ :pwd, :tmux_session, :tmux_window, :tmux_pane,
298
+ :git_branch, :git_sha, :git_origin_sha, :git_worktree,
299
+ :task, :subtask, :app
300
+ ).compact
301
+ end
302
+
303
+ def state_unchanged?(new_state)
304
+ return false unless File.exist?(LAST_STATE_FILE)
305
+ last_state = YAML.load_file(LAST_STATE_FILE) rescue {}
306
+ last_state == new_state.transform_keys(&:to_s)
307
+ end
308
+
309
+ def save_last_state(state)
310
+ File.write(LAST_STATE_FILE, state.transform_keys(&:to_s).to_yaml)
311
+ end
312
+
313
+ def current_value_for(field)
314
+ context = gather_context
315
+ context[field]
316
+ end
317
+
208
318
  def find_entry(ref)
209
319
  return nil unless ref
210
320
 
211
321
  if ref.to_s.match?(/^\d+$/)
212
322
  idx = ref.to_i - 1
213
- return entries[idx] if idx >= 0 && idx < entries.length
323
+ reversed = entries.reverse
324
+ return reversed[idx] if idx >= 0 && idx < reversed.length
214
325
  end
215
326
 
216
327
  entries.find { |e| e.id == ref.to_s }
@@ -220,9 +331,8 @@ class Hiiro
220
331
  Time.now.strftime('%Y%m%d%H%M%S') + '-' + rand(10000).to_s.rjust(4, '0')
221
332
  end
222
333
 
223
- def ensure_history_file
224
- dir = File.dirname(HISTORY_FILE)
225
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
334
+ def ensure_history_dir
335
+ FileUtils.mkdir_p(HISTORY_DIR) unless Dir.exist?(HISTORY_DIR)
226
336
 
227
337
  unless File.exist?(HISTORY_FILE)
228
338
  File.write(HISTORY_FILE, [].to_yaml)
@@ -240,9 +350,12 @@ class Hiiro
240
350
  end
241
351
 
242
352
  def save_entries(entries_data)
353
+ # Keep only last 1000 entries to prevent unbounded growth
354
+ entries_data = entries_data.last(1000) if entries_data.length > 1000
243
355
  File.write(HISTORY_FILE, entries_data.to_yaml)
244
356
  end
245
357
 
358
+ # Tmux context
246
359
  def current_tmux_session
247
360
  return nil unless ENV['TMUX']
248
361
  `tmux display-message -p '#S'`.strip rescue nil
@@ -255,13 +368,24 @@ class Hiiro
255
368
 
256
369
  def current_tmux_pane
257
370
  return nil unless ENV['TMUX']
258
- `tmux display-message -p '#P'`.strip rescue nil
371
+ ENV['TMUX_PANE'] || `tmux display-message -p '#P'`.strip rescue nil
259
372
  end
260
373
 
374
+ # Git context
261
375
  def current_git_branch
262
376
  git_helper.branch_current
263
377
  end
264
378
 
379
+ def current_git_sha
380
+ git_helper.commit('HEAD', short: true)
381
+ end
382
+
383
+ def current_git_origin_sha
384
+ branch = current_git_branch
385
+ return nil unless branch
386
+ git_helper.commit("origin/#{branch}", short: true)
387
+ end
388
+
265
389
  def current_git_worktree
266
390
  git_helper.root
267
391
  end
@@ -269,5 +393,57 @@ class Hiiro
269
393
  def git_helper
270
394
  @git_helper ||= Git.new(nil, Dir.pwd)
271
395
  end
396
+
397
+ # Task context
398
+ def current_task_name
399
+ env = environment
400
+ return nil unless env
401
+
402
+ task = env.task
403
+ return nil unless task
404
+
405
+ task.subtask? ? task.parent_name : task.name
406
+ end
407
+
408
+ def current_subtask_name
409
+ env = environment
410
+ return nil unless env
411
+
412
+ task = env.task
413
+ return nil unless task
414
+ return nil unless task.subtask?
415
+
416
+ task.short_name
417
+ end
418
+
419
+ def current_app_name
420
+ env = environment
421
+ return nil unless env
422
+
423
+ pwd = Dir.pwd
424
+ task = env.task
425
+ return nil unless task
426
+
427
+ tree = env.find_tree(task.tree_name)
428
+ return nil unless tree
429
+
430
+ # Check if pwd is in an app directory
431
+ env.all_apps.each do |app|
432
+ app_path = app.resolve(tree.path)
433
+ if pwd == app_path || pwd.start_with?(app_path + '/')
434
+ return app.name
435
+ end
436
+ end
437
+
438
+ nil
439
+ end
440
+
441
+ def environment
442
+ @environment ||= begin
443
+ Environment.current
444
+ rescue NameError
445
+ nil
446
+ end
447
+ end
272
448
  end
273
449
  end
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.51"
2
+ VERSION = "0.1.53"
3
3
  end
data/lib/hiiro.rb CHANGED
@@ -91,9 +91,11 @@ class Hiiro
91
91
  end
92
92
 
93
93
  def run
94
- log_event('hiiro: ran command')
95
94
  result = runner.run(*args)
96
95
 
96
+ # Track command after running (only saves if state changed and in task context)
97
+ History.track(cmd: full_command, hiiro: self)
98
+
97
99
  handle_result(result)
98
100
 
99
101
  exit 1
@@ -103,10 +105,6 @@ class Hiiro
103
105
  exit 1
104
106
  end
105
107
 
106
- def log_event(message)
107
- history.add_manual(message || 'command ran', hiiro: self)
108
- end
109
-
110
108
  def handle_result(result)
111
109
  exit 0 if result.nil? || result
112
110
 
data/script/update ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+
3
+ gem install hiiro
4
+
5
+ h setup
6
+
7
+
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.51
4
+ version: 0.1.53
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -77,6 +77,7 @@ files:
77
77
  - lib/hiiro/git/worktree.rb
78
78
  - lib/hiiro/git/worktrees.rb
79
79
  - lib/hiiro/history.rb
80
+ - lib/hiiro/history/entry.rb
80
81
  - lib/hiiro/options.rb
81
82
  - lib/hiiro/sk.rb
82
83
  - lib/hiiro/version.rb
@@ -90,6 +91,7 @@ files:
90
91
  - script/install
91
92
  - script/publish
92
93
  - script/sync
94
+ - script/update
93
95
  homepage: https://github.com/unixsuperhero/hiiro
94
96
  licenses:
95
97
  - MIT