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 +4 -4
- data/bin/h-app +70 -0
- data/bin/h-link +1 -1
- data/bin/h-pane +12 -1
- data/bin/h-session +12 -0
- data/bin/h-window +13 -1
- data/lib/hiiro/history/entry.rb +135 -0
- data/lib/hiiro/history.rb +273 -97
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +3 -5
- data/script/update +7 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5b124b6980d905a9d41a47e5f81de7aa5920ba8047168a952fb826a7bf98df2
|
|
4
|
+
data.tar.gz: 2fc08d8b0de4bb03678bcf15f07bde9b4e74745260cb2b5c83f1d6601cb3dce1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/bin/h-pane
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
puts entry.
|
|
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
|
|
219
|
+
cmd: hiiro&.full_command,
|
|
178
220
|
)
|
|
179
221
|
true
|
|
180
222
|
end
|
|
181
223
|
|
|
182
|
-
|
|
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' =>
|
|
191
|
-
'tmux_window' =>
|
|
192
|
-
'tmux_pane' =>
|
|
193
|
-
'git_branch' =>
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
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
|
-
|
|
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
|
|
224
|
-
|
|
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
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.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
|