hiiro 0.1.51 → 0.1.52
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/bin/h-app +70 -0
- data/bin/h-link +1 -1
- data/bin/h-pane +11 -0
- data/bin/h-session +12 -0
- data/bin/h-window +12 -0
- data/lib/hiiro/history.rb +338 -31
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +3 -5
- data/script/update +7 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16885dd29890048ddde89a6423f9f369c30960bc77b32a330becf2a4cabe4d9e
|
|
4
|
+
data.tar.gz: a76ae53eced544b5d3ae231718c3423be4a6af4cee32c7fea9a1a9b66b0b2f4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6475ba7d65044917c5511806cd5e51b8a20c5316fa92f3470c025deca0d199abde8972e72357eb9d0cdcd94f323018c1286c984f6c649b12c264da34bd830ca8
|
|
7
|
+
data.tar.gz: dfdc403a2e94bcaba0de0c6bd7d60e94faa5e55fd2a04a087fc3a4dea6ece9bf560e9c97925a3955f65385fe2f24dd601378fad53e905114fca0b289763f0095
|
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
data/bin/h-pane
CHANGED
|
@@ -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
|
@@ -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
|
data/lib/hiiro/history.rb
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
require 'yaml'
|
|
2
2
|
require 'time'
|
|
3
3
|
require 'fileutils'
|
|
4
|
+
require 'digest'
|
|
4
5
|
|
|
5
6
|
class Hiiro
|
|
6
7
|
class History
|
|
7
|
-
|
|
8
|
+
HISTORY_DIR = File.join(Dir.home, '.config/hiiro/history')
|
|
9
|
+
HISTORY_FILE = File.join(HISTORY_DIR, 'entries.yml')
|
|
10
|
+
LAST_STATE_FILE = File.join(HISTORY_DIR, 'last_state.yml')
|
|
8
11
|
|
|
9
12
|
class Entry
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
FIELDS = %i[
|
|
14
|
+
id timestamp source cmd description pwd
|
|
15
|
+
tmux_session tmux_window tmux_pane
|
|
16
|
+
git_branch git_sha git_origin_sha git_worktree
|
|
17
|
+
task subtask app
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader(*FIELDS)
|
|
14
21
|
|
|
15
22
|
def initialize(data)
|
|
16
23
|
data ||= {}
|
|
@@ -19,13 +26,17 @@ class Hiiro
|
|
|
19
26
|
@source = data['source']
|
|
20
27
|
@cmd = data['cmd']
|
|
21
28
|
@description = data['description']
|
|
29
|
+
@pwd = data['pwd']
|
|
22
30
|
@tmux_session = data['tmux_session']
|
|
23
31
|
@tmux_window = data['tmux_window']
|
|
24
32
|
@tmux_pane = data['tmux_pane']
|
|
25
33
|
@git_branch = data['git_branch']
|
|
34
|
+
@git_sha = data['git_sha']
|
|
35
|
+
@git_origin_sha = data['git_origin_sha']
|
|
26
36
|
@git_worktree = data['git_worktree']
|
|
27
37
|
@task = data['task']
|
|
28
38
|
@subtask = data['subtask']
|
|
39
|
+
@app = data['app']
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
def to_h
|
|
@@ -35,26 +46,62 @@ class Hiiro
|
|
|
35
46
|
'source' => source,
|
|
36
47
|
'cmd' => cmd,
|
|
37
48
|
'description' => description,
|
|
49
|
+
'pwd' => pwd,
|
|
50
|
+
'tmux_session' => tmux_session,
|
|
51
|
+
'tmux_window' => tmux_window,
|
|
52
|
+
'tmux_pane' => tmux_pane,
|
|
53
|
+
'git_branch' => git_branch,
|
|
54
|
+
'git_sha' => git_sha,
|
|
55
|
+
'git_origin_sha' => git_origin_sha,
|
|
56
|
+
'git_worktree' => git_worktree,
|
|
57
|
+
'task' => task,
|
|
58
|
+
'subtask' => subtask,
|
|
59
|
+
'app' => app,
|
|
60
|
+
}.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Data used for change detection (excludes timestamp, id, cmd)
|
|
64
|
+
def state_key
|
|
65
|
+
{
|
|
66
|
+
'pwd' => pwd,
|
|
38
67
|
'tmux_session' => tmux_session,
|
|
39
68
|
'tmux_window' => tmux_window,
|
|
40
69
|
'tmux_pane' => tmux_pane,
|
|
41
70
|
'git_branch' => git_branch,
|
|
71
|
+
'git_sha' => git_sha,
|
|
72
|
+
'git_origin_sha' => git_origin_sha,
|
|
42
73
|
'git_worktree' => git_worktree,
|
|
43
74
|
'task' => task,
|
|
44
75
|
'subtask' => subtask,
|
|
76
|
+
'app' => app,
|
|
45
77
|
}.compact
|
|
46
78
|
end
|
|
47
79
|
|
|
80
|
+
def state_fingerprint
|
|
81
|
+
Digest::SHA256.hexdigest(state_key.to_yaml)[0..15]
|
|
82
|
+
end
|
|
83
|
+
|
|
48
84
|
def short_line(index)
|
|
49
|
-
time_str = timestamp ? Time.parse(timestamp).strftime('%Y-%m-%d %H:%M') : Time.now.
|
|
85
|
+
time_str = timestamp ? Time.parse(timestamp).strftime('%Y-%m-%d %H:%M') : Time.now.strftime('%Y-%m-%d %H:%M')
|
|
50
86
|
desc = description || cmd || '(no description)'
|
|
51
|
-
desc = desc[0..
|
|
87
|
+
desc = desc[0..50] + '...' if desc.length > 53
|
|
52
88
|
[
|
|
53
89
|
format('%3d %s %s', index, time_str, desc),
|
|
54
|
-
format('
|
|
90
|
+
format(' %s @ %s', git_branch || '(no branch)', task || '(no task)'),
|
|
55
91
|
].join("\n")
|
|
56
92
|
end
|
|
57
93
|
|
|
94
|
+
def oneline(index = nil)
|
|
95
|
+
time_str = timestamp ? Time.parse(timestamp).strftime('%m/%d %H:%M') : ''
|
|
96
|
+
prefix = index ? format('%3d ', index) : ''
|
|
97
|
+
branch_str = git_branch ? "[#{git_branch}]" : ''
|
|
98
|
+
task_str = task ? "(#{task})" : ''
|
|
99
|
+
cmd_str = cmd || description || ''
|
|
100
|
+
cmd_str = cmd_str[0..40] + '...' if cmd_str.length > 43
|
|
101
|
+
|
|
102
|
+
"#{prefix}#{time_str} #{branch_str.ljust(20)} #{task_str.ljust(15)} #{cmd_str}"
|
|
103
|
+
end
|
|
104
|
+
|
|
58
105
|
def full_display
|
|
59
106
|
lines = []
|
|
60
107
|
lines << "ID: #{id}"
|
|
@@ -70,14 +117,28 @@ class Hiiro
|
|
|
70
117
|
lines << " Pane: #{tmux_pane}" if tmux_pane
|
|
71
118
|
lines << ""
|
|
72
119
|
lines << "Git:"
|
|
73
|
-
lines << " Branch: #{git_branch}" if git_branch
|
|
74
120
|
lines << " Worktree: #{git_worktree}" if git_worktree
|
|
121
|
+
lines << " Branch: #{git_branch}" if git_branch
|
|
122
|
+
lines << " SHA: #{git_sha}" if git_sha
|
|
123
|
+
lines << " Origin: #{git_origin_sha}" if git_origin_sha
|
|
75
124
|
lines << ""
|
|
76
125
|
lines << "Task:"
|
|
77
126
|
lines << " Task: #{task}" if task
|
|
78
127
|
lines << " Subtask: #{subtask}" if subtask
|
|
128
|
+
lines << " App: #{app}" if app
|
|
79
129
|
lines.join("\n")
|
|
80
130
|
end
|
|
131
|
+
|
|
132
|
+
# Filter matching
|
|
133
|
+
def matches?(filters)
|
|
134
|
+
filters.all? do |key, value|
|
|
135
|
+
entry_value = send(key) rescue nil
|
|
136
|
+
next true if value.nil?
|
|
137
|
+
next entry_value == value if value.is_a?(String)
|
|
138
|
+
next value.include?(entry_value) if value.is_a?(Array)
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
end
|
|
81
142
|
end
|
|
82
143
|
|
|
83
144
|
class << self
|
|
@@ -88,21 +149,35 @@ class Hiiro
|
|
|
88
149
|
subcmd, *rest = args
|
|
89
150
|
case subcmd
|
|
90
151
|
when 'ls', 'list', nil
|
|
91
|
-
history.list
|
|
152
|
+
history.list(limit: 20)
|
|
153
|
+
when 'all'
|
|
154
|
+
history.list(limit: nil)
|
|
92
155
|
when 'show'
|
|
93
156
|
history.show(rest.first)
|
|
94
157
|
when 'goto'
|
|
95
158
|
history.goto(rest.first)
|
|
96
159
|
when 'add'
|
|
97
160
|
history.add_manual(rest.join(' '), hiiro: hiiro)
|
|
161
|
+
when 'by'
|
|
162
|
+
# h history by pane|window|session|task|branch [value]
|
|
163
|
+
dimension, value = rest
|
|
164
|
+
history.list_by(dimension, value)
|
|
165
|
+
when 'clear'
|
|
166
|
+
history.clear!
|
|
167
|
+
puts "History cleared."
|
|
98
168
|
else
|
|
99
169
|
puts "Unknown history subcommand: #{subcmd}"
|
|
100
|
-
puts "Available: ls, show <id>, goto <id>, add <description>"
|
|
170
|
+
puts "Available: ls, all, show <id>, goto <id>, add <description>, by <dimension> [value], clear"
|
|
101
171
|
false
|
|
102
172
|
end
|
|
103
173
|
end
|
|
104
174
|
end
|
|
105
175
|
|
|
176
|
+
# Called automatically when a command runs (if in a task context)
|
|
177
|
+
def track(cmd:, hiiro: nil)
|
|
178
|
+
new.track_command(cmd: cmd, hiiro: hiiro)
|
|
179
|
+
end
|
|
180
|
+
|
|
106
181
|
def log(description: nil, pwd: Dir.pwd, cmd: nil, source: nil, task: nil, subtask: nil)
|
|
107
182
|
new.add(
|
|
108
183
|
description: description,
|
|
@@ -113,10 +188,59 @@ class Hiiro
|
|
|
113
188
|
subtask: subtask
|
|
114
189
|
)
|
|
115
190
|
end
|
|
191
|
+
|
|
192
|
+
# Query helpers for other commands (e.g., h pane history)
|
|
193
|
+
def by_pane(pane_id = nil)
|
|
194
|
+
pane_id ||= current_tmux_pane
|
|
195
|
+
new.filter(tmux_pane: pane_id)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def by_window(window_id = nil)
|
|
199
|
+
window_id ||= current_tmux_window
|
|
200
|
+
new.filter(tmux_window: window_id)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def by_session(session_name = nil)
|
|
204
|
+
session_name ||= current_tmux_session
|
|
205
|
+
new.filter(tmux_session: session_name)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def by_task(task_name)
|
|
209
|
+
new.filter(task: task_name)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def by_branch(branch_name)
|
|
213
|
+
new.filter(git_branch: branch_name)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def by_worktree(worktree_path)
|
|
217
|
+
new.filter(git_worktree: worktree_path)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def by_app(app_name)
|
|
221
|
+
new.filter(app: app_name)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def current_tmux_pane
|
|
227
|
+
return nil unless ENV['TMUX']
|
|
228
|
+
`tmux display-message -p '#D'`.strip rescue nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def current_tmux_window
|
|
232
|
+
return nil unless ENV['TMUX']
|
|
233
|
+
`tmux display-message -p '#S:#I'`.strip rescue nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def current_tmux_session
|
|
237
|
+
return nil unless ENV['TMUX']
|
|
238
|
+
`tmux display-message -p '#S'`.strip rescue nil
|
|
239
|
+
end
|
|
116
240
|
end
|
|
117
241
|
|
|
118
242
|
def initialize
|
|
119
|
-
|
|
243
|
+
ensure_history_dir
|
|
120
244
|
end
|
|
121
245
|
|
|
122
246
|
def entries
|
|
@@ -128,14 +252,63 @@ class Hiiro
|
|
|
128
252
|
entries
|
|
129
253
|
end
|
|
130
254
|
|
|
131
|
-
def
|
|
132
|
-
|
|
255
|
+
def filter(filters = {})
|
|
256
|
+
entries.select { |e| e.matches?(filters) }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def list(limit: 20)
|
|
260
|
+
items = entries.reverse
|
|
261
|
+
items = items.first(limit) if limit
|
|
262
|
+
|
|
263
|
+
if items.empty?
|
|
133
264
|
puts "No history entries."
|
|
134
265
|
return true
|
|
135
266
|
end
|
|
136
267
|
|
|
137
|
-
|
|
138
|
-
puts entry.
|
|
268
|
+
items.reverse.each_with_index do |entry, idx|
|
|
269
|
+
puts entry.oneline(idx + 1)
|
|
270
|
+
end
|
|
271
|
+
true
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def list_by(dimension, value = nil)
|
|
275
|
+
dimension_map = {
|
|
276
|
+
'pane' => :tmux_pane,
|
|
277
|
+
'window' => :tmux_window,
|
|
278
|
+
'session' => :tmux_session,
|
|
279
|
+
'task' => :task,
|
|
280
|
+
'subtask' => :subtask,
|
|
281
|
+
'branch' => :git_branch,
|
|
282
|
+
'worktree' => :git_worktree,
|
|
283
|
+
'app' => :app,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
field = dimension_map[dimension]
|
|
287
|
+
unless field
|
|
288
|
+
puts "Unknown dimension: #{dimension}"
|
|
289
|
+
puts "Available: #{dimension_map.keys.join(', ')}"
|
|
290
|
+
return false
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# If no value specified, use current context
|
|
294
|
+
value ||= current_value_for(field)
|
|
295
|
+
|
|
296
|
+
if value.nil?
|
|
297
|
+
puts "No current #{dimension} detected. Please specify a value."
|
|
298
|
+
return false
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
filtered = filter(field => value)
|
|
302
|
+
|
|
303
|
+
if filtered.empty?
|
|
304
|
+
puts "No history for #{dimension}=#{value}"
|
|
305
|
+
return true
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
puts "History for #{dimension}: #{value}"
|
|
309
|
+
puts
|
|
310
|
+
filtered.last(20).each_with_index do |entry, idx|
|
|
311
|
+
puts entry.oneline(idx + 1)
|
|
139
312
|
end
|
|
140
313
|
true
|
|
141
314
|
end
|
|
@@ -174,26 +347,49 @@ class Hiiro
|
|
|
174
347
|
add(
|
|
175
348
|
description: description,
|
|
176
349
|
source: 'manual',
|
|
177
|
-
cmd: hiiro
|
|
350
|
+
cmd: hiiro&.full_command,
|
|
178
351
|
)
|
|
179
352
|
true
|
|
180
353
|
end
|
|
181
354
|
|
|
182
|
-
|
|
355
|
+
# Main tracking method - gathers all context and saves if changed
|
|
356
|
+
def track_command(cmd:, hiiro: nil, force: false)
|
|
357
|
+
context = gather_context
|
|
358
|
+
return nil unless context[:task] || force # Only track if in a task context (unless forced)
|
|
359
|
+
|
|
360
|
+
context[:cmd] = cmd
|
|
361
|
+
context[:source] = 'auto'
|
|
362
|
+
|
|
363
|
+
# Check if state changed
|
|
364
|
+
unless force
|
|
365
|
+
new_state = build_state_key(context)
|
|
366
|
+
return nil if state_unchanged?(new_state)
|
|
367
|
+
save_last_state(new_state)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
add(**context)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def add(description: nil, cmd: nil, source: nil, task: nil, subtask: nil, pwd: nil, app: nil)
|
|
374
|
+
context = gather_context
|
|
375
|
+
|
|
183
376
|
entry_data = {
|
|
184
377
|
'id' => generate_id,
|
|
185
378
|
'timestamp' => Time.now.iso8601,
|
|
186
379
|
'description' => description,
|
|
187
380
|
'cmd' => cmd,
|
|
188
|
-
'pwd' => pwd,
|
|
381
|
+
'pwd' => pwd || context[:pwd],
|
|
189
382
|
'source' => source,
|
|
190
|
-
'tmux_session' =>
|
|
191
|
-
'tmux_window' =>
|
|
192
|
-
'tmux_pane' =>
|
|
193
|
-
'git_branch' =>
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
383
|
+
'tmux_session' => context[:tmux_session],
|
|
384
|
+
'tmux_window' => context[:tmux_window],
|
|
385
|
+
'tmux_pane' => context[:tmux_pane],
|
|
386
|
+
'git_branch' => context[:git_branch],
|
|
387
|
+
'git_sha' => context[:git_sha],
|
|
388
|
+
'git_origin_sha' => context[:git_origin_sha],
|
|
389
|
+
'git_worktree' => context[:git_worktree],
|
|
390
|
+
'task' => task || context[:task],
|
|
391
|
+
'subtask' => subtask || context[:subtask],
|
|
392
|
+
'app' => app || context[:app],
|
|
197
393
|
}.compact
|
|
198
394
|
|
|
199
395
|
all = load_raw_entries
|
|
@@ -203,14 +399,60 @@ class Hiiro
|
|
|
203
399
|
Entry.new(entry_data)
|
|
204
400
|
end
|
|
205
401
|
|
|
402
|
+
def clear!
|
|
403
|
+
save_entries([])
|
|
404
|
+
File.delete(LAST_STATE_FILE) if File.exist?(LAST_STATE_FILE)
|
|
405
|
+
@entries = nil
|
|
406
|
+
end
|
|
407
|
+
|
|
206
408
|
private
|
|
207
409
|
|
|
410
|
+
def gather_context
|
|
411
|
+
{
|
|
412
|
+
pwd: Dir.pwd,
|
|
413
|
+
tmux_session: current_tmux_session,
|
|
414
|
+
tmux_window: current_tmux_window,
|
|
415
|
+
tmux_pane: current_tmux_pane,
|
|
416
|
+
git_branch: current_git_branch,
|
|
417
|
+
git_sha: current_git_sha,
|
|
418
|
+
git_origin_sha: current_git_origin_sha,
|
|
419
|
+
git_worktree: current_git_worktree,
|
|
420
|
+
task: current_task_name,
|
|
421
|
+
subtask: current_subtask_name,
|
|
422
|
+
app: current_app_name,
|
|
423
|
+
}
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def build_state_key(context)
|
|
427
|
+
context.slice(
|
|
428
|
+
:pwd, :tmux_session, :tmux_window, :tmux_pane,
|
|
429
|
+
:git_branch, :git_sha, :git_origin_sha, :git_worktree,
|
|
430
|
+
:task, :subtask, :app
|
|
431
|
+
).compact
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def state_unchanged?(new_state)
|
|
435
|
+
return false unless File.exist?(LAST_STATE_FILE)
|
|
436
|
+
last_state = YAML.load_file(LAST_STATE_FILE) rescue {}
|
|
437
|
+
last_state == new_state.transform_keys(&:to_s)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def save_last_state(state)
|
|
441
|
+
File.write(LAST_STATE_FILE, state.transform_keys(&:to_s).to_yaml)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def current_value_for(field)
|
|
445
|
+
context = gather_context
|
|
446
|
+
context[field]
|
|
447
|
+
end
|
|
448
|
+
|
|
208
449
|
def find_entry(ref)
|
|
209
450
|
return nil unless ref
|
|
210
451
|
|
|
211
452
|
if ref.to_s.match?(/^\d+$/)
|
|
212
453
|
idx = ref.to_i - 1
|
|
213
|
-
|
|
454
|
+
reversed = entries.reverse
|
|
455
|
+
return reversed[idx] if idx >= 0 && idx < reversed.length
|
|
214
456
|
end
|
|
215
457
|
|
|
216
458
|
entries.find { |e| e.id == ref.to_s }
|
|
@@ -220,9 +462,8 @@ class Hiiro
|
|
|
220
462
|
Time.now.strftime('%Y%m%d%H%M%S') + '-' + rand(10000).to_s.rjust(4, '0')
|
|
221
463
|
end
|
|
222
464
|
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
465
|
+
def ensure_history_dir
|
|
466
|
+
FileUtils.mkdir_p(HISTORY_DIR) unless Dir.exist?(HISTORY_DIR)
|
|
226
467
|
|
|
227
468
|
unless File.exist?(HISTORY_FILE)
|
|
228
469
|
File.write(HISTORY_FILE, [].to_yaml)
|
|
@@ -240,9 +481,12 @@ class Hiiro
|
|
|
240
481
|
end
|
|
241
482
|
|
|
242
483
|
def save_entries(entries_data)
|
|
484
|
+
# Keep only last 1000 entries to prevent unbounded growth
|
|
485
|
+
entries_data = entries_data.last(1000) if entries_data.length > 1000
|
|
243
486
|
File.write(HISTORY_FILE, entries_data.to_yaml)
|
|
244
487
|
end
|
|
245
488
|
|
|
489
|
+
# Tmux context
|
|
246
490
|
def current_tmux_session
|
|
247
491
|
return nil unless ENV['TMUX']
|
|
248
492
|
`tmux display-message -p '#S'`.strip rescue nil
|
|
@@ -255,13 +499,24 @@ class Hiiro
|
|
|
255
499
|
|
|
256
500
|
def current_tmux_pane
|
|
257
501
|
return nil unless ENV['TMUX']
|
|
258
|
-
`tmux display-message -p '#P'`.strip rescue nil
|
|
502
|
+
ENV['TMUX_PANE'] || `tmux display-message -p '#P'`.strip rescue nil
|
|
259
503
|
end
|
|
260
504
|
|
|
505
|
+
# Git context
|
|
261
506
|
def current_git_branch
|
|
262
507
|
git_helper.branch_current
|
|
263
508
|
end
|
|
264
509
|
|
|
510
|
+
def current_git_sha
|
|
511
|
+
git_helper.commit('HEAD', short: true)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def current_git_origin_sha
|
|
515
|
+
branch = current_git_branch
|
|
516
|
+
return nil unless branch
|
|
517
|
+
git_helper.commit("origin/#{branch}", short: true)
|
|
518
|
+
end
|
|
519
|
+
|
|
265
520
|
def current_git_worktree
|
|
266
521
|
git_helper.root
|
|
267
522
|
end
|
|
@@ -269,5 +524,57 @@ class Hiiro
|
|
|
269
524
|
def git_helper
|
|
270
525
|
@git_helper ||= Git.new(nil, Dir.pwd)
|
|
271
526
|
end
|
|
527
|
+
|
|
528
|
+
# Task context
|
|
529
|
+
def current_task_name
|
|
530
|
+
env = environment
|
|
531
|
+
return nil unless env
|
|
532
|
+
|
|
533
|
+
task = env.task
|
|
534
|
+
return nil unless task
|
|
535
|
+
|
|
536
|
+
task.subtask? ? task.parent_name : task.name
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def current_subtask_name
|
|
540
|
+
env = environment
|
|
541
|
+
return nil unless env
|
|
542
|
+
|
|
543
|
+
task = env.task
|
|
544
|
+
return nil unless task
|
|
545
|
+
return nil unless task.subtask?
|
|
546
|
+
|
|
547
|
+
task.short_name
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def current_app_name
|
|
551
|
+
env = environment
|
|
552
|
+
return nil unless env
|
|
553
|
+
|
|
554
|
+
pwd = Dir.pwd
|
|
555
|
+
task = env.task
|
|
556
|
+
return nil unless task
|
|
557
|
+
|
|
558
|
+
tree = env.find_tree(task.tree_name)
|
|
559
|
+
return nil unless tree
|
|
560
|
+
|
|
561
|
+
# Check if pwd is in an app directory
|
|
562
|
+
env.all_apps.each do |app|
|
|
563
|
+
app_path = app.resolve(tree.path)
|
|
564
|
+
if pwd == app_path || pwd.start_with?(app_path + '/')
|
|
565
|
+
return app.name
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def environment
|
|
573
|
+
@environment ||= begin
|
|
574
|
+
Environment.current
|
|
575
|
+
rescue NameError
|
|
576
|
+
nil
|
|
577
|
+
end
|
|
578
|
+
end
|
|
272
579
|
end
|
|
273
580
|
end
|
data/lib/hiiro/version.rb
CHANGED
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
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.
|
|
4
|
+
version: 0.1.52
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Toyota
|
|
@@ -90,6 +90,7 @@ files:
|
|
|
90
90
|
- script/install
|
|
91
91
|
- script/publish
|
|
92
92
|
- script/sync
|
|
93
|
+
- script/update
|
|
93
94
|
homepage: https://github.com/unixsuperhero/hiiro
|
|
94
95
|
licenses:
|
|
95
96
|
- MIT
|