hiiro 0.1.279 → 0.1.280

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: dac54cf95b87a2dd963a261922d9d269597970236714ace84586538ee38b643c
4
- data.tar.gz: '0408a01c35a5888a2ff78d880085278b719007aa47ed40257749c65410225592'
3
+ metadata.gz: c86b1990635355b7028465d785137a29092594900b220e679107138cdedf5821
4
+ data.tar.gz: fdd08045ed76b8ca7d288fa1f49358eb65649c35f043ce35745114c63af920f0
5
5
  SHA512:
6
- metadata.gz: 74324fbd4fcfdb860270388df1a19f1affff4030d1d0a2c88362c9c3d26f1a1af7170a9eae2303b8467b2a60f6492166cdb2a0f69ba79adbe58dfcc8378ea005
7
- data.tar.gz: b6d351fd166a8b694c067d83ffba0f63681f37c08565ebd3d24eaa3c2bad7d2a598327b54a47eb49f3ccc04d658b0be12cfd52fae4574a2fcfc712f24742cc8b
6
+ metadata.gz: e4a7443498c3902dc536b39df7d128782f5cd3ad8be6b3acb75b50a93b431e27aff7b68e87b3531d860b7cf7bf82c0f658fad17663a5d2f42098bb7cbf605f2b
7
+ data.tar.gz: 1acd39aadcadd0dca8abdccfbe3e91d286cffbeb9e40cc5a2d3a139022c9c324de6c030573848ca0571d5d6dcb3a67785e19c0b2d677fd10d01d1e9c733ca25d
data/CHANGELOG.md CHANGED
@@ -1 +1 @@
1
- Done. CHANGELOG.md has been updated with v0.1.279 for 2026-03-25. The new section documents the refactoring of `Hiiro::PinnedPRManager` extraction to `lib/hiiro/pinned_pr_manager.rb`, which is the only meaningful code change in the recent commits (the others were script updates and documentation restoration).
1
+ Done. CHANGELOG.md has been updated with v0.1.280 for 2026-03-25. The new section documents all the features, changes, and refactorings from the recent commits, organized by Added and Changed categories.
data/bin/h-buffer CHANGED
@@ -4,20 +4,7 @@ require "hiiro"
4
4
 
5
5
  Hiiro.run(*ARGV, plugins: [Pins]) do
6
6
  tmux = tmux_client
7
-
8
- select_buffer_proc = ->(partial = nil) {
9
- buffers = tmux.buffers
10
- return nil if buffers.empty?
11
-
12
- if partial
13
- buffers = buffers.matching(partial)
14
- end
15
-
16
- return nil if buffers.empty?
17
- return buffers.first.name if buffers.size == 1
18
-
19
- fuzzyfind_from_map(buffers.name_map)
20
- }
7
+ Hiiro::Tmux.add_resolvers(self)
21
8
 
22
9
  add_subcmd(:ls) { |*args|
23
10
  if args.empty?
@@ -28,7 +15,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
28
15
  }
29
16
 
30
17
  add_subcmd(:show) { |name = nil, *args|
31
- buffer_name = select_buffer_proc.call(name)
18
+ buffer_name = resolve(:buffer, name)
32
19
 
33
20
  unless buffer_name
34
21
  puts "No buffer selected or found"
@@ -44,7 +31,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
44
31
  }
45
32
 
46
33
  add_subcmd(:copy) { |name = nil|
47
- buffer_name = select_buffer_proc.call(name)
34
+ buffer_name = resolve(:buffer, name)
48
35
  unless buffer_name
49
36
  puts "No buffer selected or found"
50
37
  next
@@ -66,7 +53,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
66
53
  if name
67
54
  tmux.save_buffer(path, name: name)
68
55
  elsif args.empty?
69
- buffer_name = select_buffer_proc.call(nil)
56
+ buffer_name = resolve(:buffer, nil)
70
57
  if buffer_name
71
58
  tmux.save_buffer(path, name: buffer_name)
72
59
  end
@@ -104,7 +91,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
104
91
 
105
92
  add_subcmd(:delete) { |name = nil, *args|
106
93
  if name.nil?
107
- name = select_buffer_proc.call(nil)
94
+ name = resolve(:buffer, nil)
108
95
  end
109
96
 
110
97
  if name && args.empty?
@@ -130,13 +117,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
130
117
  }
131
118
 
132
119
  add_subcmd(:select) do
133
- buffers = tmux.buffers
134
- if buffers.empty?
135
- STDERR.puts "No buffers found"
136
- next
137
- end
138
-
139
- selected = fuzzyfind_from_map(buffers.name_map)
120
+ selected = resolve(:buffer)
140
121
  print selected if selected
141
122
  end
142
123
  end
data/bin/h-jumplist CHANGED
@@ -174,14 +174,6 @@ Hiiro.run(*ARGV) do
174
174
  entries = read_entries
175
175
  pos = get_pos
176
176
 
177
- # Deduplicate: skip if current position entry matches pane/window/session
178
- current_entry = entries[pos]
179
- if current_entry
180
- current_parts = current_entry.split('|')
181
- current_key = current_parts[0..2]
182
- next if current_key == [pane_id, window_id, session_name]
183
- end
184
-
185
177
  # If position is not at head, truncate forward history
186
178
  if pos > 0 && entries.any?
187
179
  entries = entries[pos..]
@@ -190,13 +182,48 @@ Hiiro.run(*ARGV) do
190
182
  set_pos(0)
191
183
  end
192
184
 
193
- # Prepend new entry (newest first)
194
185
  new_entry = [pane_id, window_id, session_name, timestamp, pane_cmd].join('|')
186
+
187
+ # Skip if the head already matches this pane (no-op navigation)
188
+ head_pane = entries.first&.split('|', 2)&.first
189
+ next if head_pane == pane_id
190
+
191
+ # Remove all earlier occurrences of this pane so it appears only once
192
+ entries.reject! { |e| e.split('|', 2).first == pane_id }
193
+
194
+ # Prepend new entry (newest first)
195
195
  entries.unshift(new_entry)
196
196
  entries = entries.first(MAX_SIZE)
197
197
  write_entries(entries)
198
198
  end
199
199
 
200
+ add_subcmd(:to) do |index = nil|
201
+ if index.nil?
202
+ puts "Usage: h jumplist to <index>"
203
+ next
204
+ end
205
+
206
+ entries, pos = prune_dead_entries(read_entries, get_pos)
207
+
208
+ if entries.empty?
209
+ system('tmux', 'display-message', 'Jumplist: empty')
210
+ next
211
+ end
212
+
213
+ idx = index.to_i
214
+ unless (0...entries.length).include?(idx)
215
+ system('tmux', 'display-message', "Jumplist: no entry at index #{idx}")
216
+ next
217
+ end
218
+
219
+ parts = entries[idx].split('|')
220
+ system('tmux', 'set-environment', 'TMUX_JUMPLIST_SUPPRESS', '1')
221
+ system('tmux', 'switch-client', '-t', parts[2])
222
+ system('tmux', 'select-window', '-t', parts[1])
223
+ system('tmux', 'select-pane', '-t', parts[0])
224
+ set_pos(idx)
225
+ end
226
+
200
227
  add_subcmd(:back) do
201
228
  entries, pos = prune_dead_entries(read_entries, get_pos)
202
229
 
data/bin/h-pane CHANGED
@@ -5,6 +5,7 @@ require 'yaml'
5
5
 
6
6
  Hiiro.run(*ARGV, plugins: [Pins]) do
7
7
  tmux = tmux_client
8
+ Hiiro::Tmux.add_resolvers(self)
8
9
 
9
10
  home_config_path = Hiiro::Config.path('pane_homes.yml')
10
11
 
@@ -54,9 +55,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
54
55
  }
55
56
 
56
57
  add_subcmd(:kill) { |target = nil, *args|
57
- if target.nil?
58
- target = fuzzyfind_from_map(tmux.panes(all: true).name_map)
59
- end
58
+ target = resolve(:pane, target)
60
59
 
61
60
  if target && args.empty?
62
61
  tmux.kill_pane(target)
@@ -87,9 +86,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
87
86
  }
88
87
 
89
88
  add_subcmd(:select) { |target = nil, *args|
90
- if target.nil?
91
- target = fuzzyfind_from_map(tmux.panes(all: true).name_map)
92
- end
89
+ target = resolve(:pane, target)
93
90
 
94
91
  if target
95
92
  print target
@@ -97,9 +94,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
97
94
  }
98
95
 
99
96
  add_subcmd(:copy) { |target = nil, *args|
100
- if target.nil?
101
- target = fuzzyfind_from_map(tmux.panes(all: true).name_map)
102
- end
97
+ target = resolve(:pane, target)
103
98
 
104
99
  if target
105
100
  Hiiro::Shell.pipe(target, 'pbcopy')
@@ -108,7 +103,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
108
103
  }
109
104
 
110
105
  add_subcmd(:sw, :switch) { |target = nil, *args|
111
- target ||= fuzzyfind_from_map(tmux.panes(all: true).name_map)
106
+ target = resolve(:pane, target)
112
107
  tmux.open_session(target) if target
113
108
  }
114
109
 
data/bin/h-project CHANGED
@@ -19,30 +19,16 @@ def start_tmux_session(session_name)
19
19
  end
20
20
  end
21
21
 
22
- # Get project directories from ~/proj/
23
- def project_dirs
24
- Dir.glob(File.join(Dir.home, 'proj', '*/')).map { |path|
25
- [File.basename(path), path]
26
- }.to_h
27
- end
28
-
29
- # Get projects from config file
30
- def projects_from_config
31
- projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
32
-
33
- return {} unless File.exist?(projects_file)
34
-
35
- require 'yaml'
36
- YAML.safe_load_file(projects_file)
37
- end
38
-
39
22
  Hiiro.run(*ARGV) do
23
+ projects = Hiiro::Projects.new
24
+ Hiiro::Projects.add_resolvers(self)
25
+
40
26
  # === OPEN PROJECT (default) ===
41
27
  add_subcmd(:open) { |project_name|
42
28
  re = /#{project_name}/i
43
29
 
44
- conf_matches = (projects_from_config || {}).select { |k, v| k.match?(re) }
45
- dir_matches = (project_dirs || {}).select { |proj, path| proj.match?(re) }
30
+ conf_matches = projects.from_config.select { |k, v| k.match?(re) }
31
+ dir_matches = projects.dirs.select { |proj, path| proj.match?(re) }
46
32
 
47
33
  matches = dir_matches.merge(conf_matches)
48
34
  if matches.count > 1
@@ -82,15 +68,14 @@ Hiiro.run(*ARGV) do
82
68
 
83
69
  # === LIST PROJECTS ===
84
70
  add_subcmd(:list) { |*list_args|
85
- dirs = project_dirs
86
- conf = projects_from_config
87
-
88
- all_projects = dirs.merge(conf)
71
+ all = projects.all
72
+ conf = projects.from_config
89
73
 
90
74
  puts "Projects:"
91
75
  puts
92
- all_projects.keys.sort.each do |name|
93
- path = all_projects[name]
76
+
77
+ all.keys.sort.each do |name|
78
+ path = all[name]
94
79
  source = conf.key?(name) ? '[config]' : '[dir]'
95
80
  puts format(" %-12s %-8s %s", name, source, path)
96
81
  end
@@ -102,12 +87,10 @@ Hiiro.run(*ARGV) do
102
87
 
103
88
  # === SHOW CONFIG FILE ===
104
89
  add_subcmd(:config) { |*config_args|
105
- projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
106
-
107
- if File.exist?(projects_file)
108
- puts File.read(projects_file)
90
+ if File.exist?(Hiiro::Projects::CONFIG_FILE)
91
+ puts File.read(Hiiro::Projects::CONFIG_FILE)
109
92
  else
110
- puts "No config file found at: #{projects_file}"
93
+ puts "No config file found at: #{Hiiro::Projects::CONFIG_FILE}"
111
94
  puts
112
95
  puts "Create it with YAML format:"
113
96
  puts " project_name: /path/to/project"
@@ -116,70 +99,88 @@ Hiiro.run(*ARGV) do
116
99
 
117
100
  # === EDIT CONFIG FILE ===
118
101
  add_subcmd(:edit) { |*edit_args|
119
- projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
120
- # Create config dir if needed
121
- config_dir = File.dirname(projects_file)
102
+ config_dir = File.dirname(Hiiro::Projects::CONFIG_FILE)
122
103
  Dir.mkdir(config_dir) unless Dir.exist?(config_dir)
123
104
 
124
- # Create empty file if it doesn't exist
125
- unless File.exist?(projects_file)
126
- File.write(projects_file, "# Project aliases\n# project_name: /path/to/project\n")
105
+ unless File.exist?(Hiiro::Projects::CONFIG_FILE)
106
+ File.write(Hiiro::Projects::CONFIG_FILE, "# Project aliases\n# project_name: /path/to/project\n")
127
107
  end
128
108
 
129
- edit_files(projects_file)
109
+ edit_files(Hiiro::Projects::CONFIG_FILE)
130
110
  }
131
111
 
132
112
  # === SELECT PROJECT ===
133
113
  add_subcmd(:select) { |*select_args|
134
- dirs = project_dirs
135
- conf = projects_from_config
136
-
137
- all_projects = dirs.merge(conf)
114
+ all = projects.all
138
115
 
139
- if all_projects.empty?
116
+ if all.empty?
140
117
  STDERR.puts "No projects found"
141
118
  next
142
119
  end
143
120
 
144
- lines = all_projects.each_with_object({}) do |(name, path), h|
121
+ conf = projects.from_config
122
+ lines = all.each_with_object({}) do |(name, path), h|
145
123
  source = conf.key?(name) ? '[config]' : '[dir]'
146
124
  display = format("%-15s %s %s", name, source, path)
147
125
  h[display] = path
148
126
  end
149
127
 
150
128
  selected = fuzzyfind_from_map(lines)
151
-
152
- if selected
153
- print selected
154
- end
129
+ print selected if selected
155
130
  }
156
131
 
157
132
  # === COPY PROJECT PATH ===
158
133
  add_subcmd(:copy) { |*copy_args|
159
- dirs = project_dirs
160
- conf = projects_from_config
161
-
162
- all_projects = dirs.merge(conf)
134
+ all = projects.all
163
135
 
164
- if all_projects.empty?
136
+ if all.empty?
165
137
  STDERR.puts "No projects found"
166
138
  next
167
139
  end
168
140
 
169
- lines = all_projects.each_with_object({}) do |(name, path), h|
141
+ conf = projects.from_config
142
+ lines = all.each_with_object({}) do |(name, path), h|
170
143
  source = conf.key?(name) ? '[config]' : '[dir]'
171
144
  display = format("%-15s %s %s", name, source, path)
172
145
  h[display] = path
173
146
  end
174
147
 
175
148
  selected = fuzzyfind_from_map(lines)
176
-
177
149
  if selected
178
150
  Hiiro::Shell.pipe(selected, 'pbcopy')
179
151
  puts "Copied project path '#{selected}' to clipboard"
180
152
  end
181
153
  }
182
154
 
155
+ # === SHELL IN PROJECT ===
156
+ add_subcmd(:sh) { |project_name=nil, *args|
157
+ if project_name.nil? || project_name.empty?
158
+ puts "Usage: h project sh <project_name> [command...]"
159
+ next
160
+ end
161
+
162
+ re = /#{project_name}/i
163
+ conf_matches = projects.from_config.select { |k, v| k.match?(re) }
164
+ dir_matches = projects.dirs.select { |proj, path| proj.match?(re) }
165
+ matches = dir_matches.merge(conf_matches)
166
+ matches = matches.select { |name, path| name == project_name } if matches.count > 1
167
+
168
+ case matches.count
169
+ when 0
170
+ puts "Project '#{project_name}' not found"
171
+ puts
172
+ puts "Available projects:"
173
+ projects.all.keys.sort.each { |k| puts " #{k}" }
174
+ when 1
175
+ _, path = matches.first
176
+ Dir.chdir(path)
177
+ args.empty? ? exec(ENV['SHELL'] || 'zsh') : exec(*args)
178
+ else
179
+ puts "Ambiguous project '#{project_name}' — multiple matches:"
180
+ matches.each { |name, path| puts format(" %-15s %s", name, path) }
181
+ end
182
+ }
183
+
183
184
  # === HELP ===
184
185
  add_subcmd(:help) { |*help_args|
185
186
  puts <<~HELP
@@ -192,6 +193,7 @@ Hiiro.run(*ARGV) do
192
193
  h-project ls Alias for list
193
194
  h-project config Show config file contents
194
195
  h-project edit Edit config file
196
+ h-project sh <project_name> Open shell in project directory
195
197
 
196
198
  SOURCES:
197
199
  Projects are discovered from two sources:
@@ -209,35 +211,6 @@ Hiiro.run(*ARGV) do
209
211
  HELP
210
212
  }
211
213
 
212
- # === SHELL IN PROJECT ===
213
- add_subcmd(:sh) { |project_name=nil, *args|
214
- if project_name.nil? || project_name.empty?
215
- puts "Usage: h project sh <project_name> [command...]"
216
- next
217
- end
218
-
219
- re = /#{project_name}/i
220
- conf_matches = (projects_from_config || {}).select { |k, v| k.match?(re) }
221
- dir_matches = (project_dirs || {}).select { |proj, path| proj.match?(re) }
222
- matches = dir_matches.merge(conf_matches)
223
- matches = matches.select { |name, path| name == project_name } if matches.count > 1
224
-
225
- case matches.count
226
- when 0
227
- puts "Project '#{project_name}' not found"
228
- puts
229
- puts "Available projects:"
230
- project_dirs.merge(projects_from_config).keys.sort.each { |k| puts " #{k}" }
231
- when 1
232
- _, path = matches.first
233
- Dir.chdir(path)
234
- args.empty? ? exec(ENV['SHELL'] || 'zsh') : exec(*args)
235
- else
236
- puts "Ambiguous project '#{project_name}' — multiple matches:"
237
- matches.each { |name, path| puts format(" %-15s %s", name, path) }
238
- end
239
- }
240
-
241
214
  add_default {
242
215
  run_subcmd(:help)
243
216
  }
data/bin/h-session CHANGED
@@ -6,6 +6,7 @@ require 'yaml'
6
6
 
7
7
  Hiiro.run(*ARGV, tasks: true, plugins: [Pins]) do
8
8
  tmux = tmux_client
9
+ Hiiro::Tmux.add_resolvers(self)
9
10
 
10
11
  add_subcmd(:ls, :list) { |*args|
11
12
  if args.empty?
@@ -24,9 +25,7 @@ Hiiro.run(*ARGV, tasks: true, plugins: [Pins]) do
24
25
  }
25
26
 
26
27
  add_subcmd(:kill) { |name = nil, *args|
27
- if name.nil?
28
- name = fuzzyfind_from_map(tmux.sessions.name_map)
29
- end
28
+ name = resolve(:session, name)
30
29
 
31
30
  if name && args.empty?
32
31
  tmux.kill_session(name)
@@ -36,9 +35,7 @@ Hiiro.run(*ARGV, tasks: true, plugins: [Pins]) do
36
35
  }
37
36
 
38
37
  add_subcmd(:attach) { |name = nil, *args|
39
- if name.nil?
40
- name = fuzzyfind_from_map(tmux.sessions.name_map)
41
- end
38
+ name = resolve(:session, name)
42
39
 
43
40
  if name && args.empty?
44
41
  tmux.attach_session(name)
@@ -48,9 +45,7 @@ Hiiro.run(*ARGV, tasks: true, plugins: [Pins]) do
48
45
  }
49
46
 
50
47
  add_subcmd(:rename) { |old_name = nil, new_name = nil, *args|
51
- if old_name.nil?
52
- old_name = fuzzyfind_from_map(tmux.sessions.name_map)
53
- end
48
+ old_name = resolve(:session, old_name)
54
49
 
55
50
  if old_name && new_name && args.empty?
56
51
  tmux.rename_session(old_name, new_name)
@@ -60,9 +55,7 @@ Hiiro.run(*ARGV, tasks: true, plugins: [Pins]) do
60
55
  }
61
56
 
62
57
  add_subcmd(:switch) { |name = nil, *args|
63
- if name.nil?
64
- name = fuzzyfind_from_map(tmux.sessions.name_map)
65
- end
58
+ name = resolve(:session, name)
66
59
 
67
60
  if name
68
61
  tmux.open_session(name)
@@ -105,24 +98,12 @@ Hiiro.run(*ARGV, tasks: true, plugins: [Pins]) do
105
98
  }
106
99
 
107
100
  add_subcmd(:select) do
108
- sessions = tmux.sessions
109
- if sessions.empty?
110
- STDERR.puts "No sessions found"
111
- next
112
- end
113
-
114
- selected = fuzzyfind_from_map(sessions.name_map)
101
+ selected = resolve(:session)
115
102
  print selected if selected
116
103
  end
117
104
 
118
105
  add_subcmd(:copy) do
119
- sessions = tmux.sessions
120
- if sessions.empty?
121
- STDERR.puts "No sessions found"
122
- next
123
- end
124
-
125
- selected = fuzzyfind_from_map(sessions.name_map)
106
+ selected = resolve(:session)
126
107
  if selected
127
108
  Hiiro::Shell.pipe(selected, 'pbcopy')
128
109
  puts "Copied session '#{selected}' to clipboard"
data/bin/h-window CHANGED
@@ -4,6 +4,7 @@ require 'hiiro'
4
4
 
5
5
  Hiiro.run(*ARGV, plugins: [Pins]) do
6
6
  tmux = tmux_client
7
+ Hiiro::Tmux.add_resolvers(self)
7
8
 
8
9
  add_subcmd(:ls) { |*args|
9
10
  if args.empty?
@@ -30,9 +31,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
30
31
  }
31
32
 
32
33
  add_subcmd(:kill) { |target = nil, *args|
33
- if target.nil?
34
- target = fuzzyfind_from_map(tmux.windows(all: true).name_map)
35
- end
34
+ target = resolve(:window, target)
36
35
 
37
36
  if target && args.empty?
38
37
  tmux.kill_window(target)
@@ -54,9 +53,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
54
53
  }
55
54
 
56
55
  add_subcmd(:select) { |target = nil, *args|
57
- if target.nil?
58
- target = fuzzyfind_from_map(tmux.windows(all: true).name_map)
59
- end
56
+ target = resolve(:window, target)
60
57
 
61
58
  if target
62
59
  print target
@@ -64,9 +61,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
64
61
  }
65
62
 
66
63
  add_subcmd(:copy) { |target = nil, *args|
67
- if target.nil?
68
- target = fuzzyfind_from_map(tmux.windows(all: true).name_map)
69
- end
64
+ target = resolve(:window, target)
70
65
 
71
66
  if target
72
67
  Hiiro::Shell.pipe(target, 'pbcopy')
@@ -75,9 +70,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
75
70
  }
76
71
 
77
72
  add_subcmd(:sw, :switch) { |target = nil, *args|
78
- if target.nil?
79
- target = fuzzyfind_from_map(tmux.windows(all: true).name_map)
80
- end
73
+ target = resolve(:window, target)
81
74
 
82
75
  if target
83
76
  tmux.open_session(target)
data/lib/hiiro/options.rb CHANGED
@@ -51,16 +51,27 @@ class Hiiro
51
51
 
52
52
  def flag(name, long: nil, short: nil, default: false, desc: nil)
53
53
  defn = Definition.new(name, long: long, short: short, type: :flag, default: default, desc: desc)
54
+ deconflict_short(short) if short
54
55
  @definitions[name.to_sym] = defn
55
56
  self
56
57
  end
57
58
 
58
- def option(name, long: nil, short: nil, type: :string, default: nil, desc: nil, multi: false)
59
- defn = Definition.new(name, long: long, short: short, type: type, default: default, desc: desc, multi: multi)
59
+ def option(name, long: nil, short: nil, type: :string, default: nil, desc: nil, multi: false, flag_ifs: [])
60
+ defn = Definition.new(name, long: long, short: short, type: type, default: default, desc: desc, multi: multi, flag_ifs: Array(flag_ifs))
61
+ deconflict_short(short) if short
60
62
  @definitions[name.to_sym] = defn
61
63
  self
62
64
  end
63
65
 
66
+ private
67
+
68
+ def deconflict_short(short)
69
+ short_s = short.to_s
70
+ @definitions.each_value { |d| d.short = nil if d.short == short_s }
71
+ end
72
+
73
+ public
74
+
64
75
  def select(names)
65
76
  subset = self.class.setup {}
66
77
  names.each do |name|
@@ -170,7 +181,7 @@ class Hiiro
170
181
  defn = @definitions.values.find { |d| d.long_form == flag_part }
171
182
  return unless defn
172
183
 
173
- if defn.flag?
184
+ if defn.flag? || defn.flag_active?(@values)
174
185
  @values[defn.name] = !defn.default
175
186
  else
176
187
  value ||= args.shift
@@ -185,7 +196,7 @@ class Hiiro
185
196
  defn = @definitions.values.find { |d| d.short == char }
186
197
  next unless defn
187
198
 
188
- if defn.flag?
199
+ if defn.flag? || defn.flag_active?(@values)
189
200
  @values[defn.name] = !defn.default
190
201
  elsif idx == chars.length - 1
191
202
  store_value(defn, args.shift)
@@ -207,9 +218,10 @@ class Hiiro
207
218
  end
208
219
 
209
220
  class Definition
210
- attr_reader :name, :short, :long, :type, :default, :desc, :multi
221
+ attr_reader :name, :long, :type, :default, :desc, :multi, :flag_ifs
222
+ attr_accessor :short
211
223
 
212
- def initialize(name, short: nil, long: nil, type: :string, default: nil, desc: nil, multi: false)
224
+ def initialize(name, short: nil, long: nil, type: :string, default: nil, desc: nil, multi: false, flag_ifs: [])
213
225
  @name = name.to_sym
214
226
  @short = short&.to_s
215
227
  @long = long&.to_sym
@@ -217,6 +229,11 @@ class Hiiro
217
229
  @default = default
218
230
  @desc = desc
219
231
  @multi = multi
232
+ @flag_ifs = flag_ifs.map(&:to_sym)
233
+ end
234
+
235
+ def flag_active?(values)
236
+ @flag_ifs.any? { |f| values[f] }
220
237
  end
221
238
 
222
239
  def flag?
@@ -0,0 +1,54 @@
1
+ require 'yaml'
2
+
3
+ class Hiiro
4
+ class Projects
5
+ CONFIG_FILE = File.join(Dir.home, '.config', 'hiiro', 'projects.yml')
6
+
7
+ def self.add_resolvers(hiiro)
8
+ pm = new
9
+ hiiro.add_resolver(:project,
10
+ -> {
11
+ all = pm.all
12
+ return nil if all.empty?
13
+ conf = pm.from_config
14
+ lines = all.each_with_object({}) do |(name, path), h|
15
+ source = conf.key?(name) ? '[config]' : '[dir]'
16
+ h[format("%-15s %-8s %s", name, source, path)] = path
17
+ end
18
+ hiiro.fuzzyfind_from_map(lines)
19
+ }
20
+ ) do |name|
21
+ pm.find(name)
22
+ end
23
+ end
24
+
25
+ def dirs
26
+ Dir.glob(File.join(Dir.home, 'proj', '*/')).map { |path|
27
+ [File.basename(path), path]
28
+ }.to_h
29
+ end
30
+
31
+ def from_config
32
+ return {} unless File.exist?(CONFIG_FILE)
33
+ YAML.safe_load_file(CONFIG_FILE) || {}
34
+ end
35
+
36
+ def from_config?(name)
37
+ from_config.key?(name)
38
+ end
39
+
40
+ def all
41
+ dirs.merge(from_config)
42
+ end
43
+
44
+ # Find a project path by name (regex match, exact-match tiebreak).
45
+ # Returns the path string, or nil if 0 or >1 matches.
46
+ def find(name)
47
+ re = /#{name}/i
48
+ conf = from_config
49
+ matches = dirs.select { |k, _| k.match?(re) }.merge(conf.select { |k, _| k.match?(re) })
50
+ matches = matches.select { |k, _| k == name } if matches.count > 1
51
+ matches.count == 1 ? matches.values.first : nil
52
+ end
53
+ end
54
+ end
data/lib/hiiro/queue.rb CHANGED
@@ -304,7 +304,7 @@ class Hiiro
304
304
  end
305
305
 
306
306
  def resolve_task_info(opts, hiiro, default_task_info)
307
- if opts.choose
307
+ if opts.find
308
308
  selection = select_task_or_session(hiiro)
309
309
  if selection
310
310
  case selection[:type]
@@ -314,7 +314,7 @@ class Hiiro
314
314
  else
315
315
  default_task_info
316
316
  end
317
- elsif opts.task
317
+ elsif opts.task.is_a?(String)
318
318
  task_info_for(opts.task)
319
319
  else
320
320
  default_task_info
@@ -523,11 +523,13 @@ class Hiiro
523
523
  do_add = lambda do |args, split: nil|
524
524
  q.queue_dirs
525
525
  opts = Hiiro::Options.parse(args) do
526
- option(:task, short: :t, desc: 'Task name')
527
- option(:name, short: :n, desc: 'Base filename for the queue task')
528
- flag(:choose, short: :T, desc: 'Choose task interactively')
529
- flag(:session, short: :s, desc: 'Use current tmux session')
530
- flag(:ignore, short: :i, desc: 'Background task close window when done, no shell')
526
+ option(:task, short: :t, desc: 'Task name', flag_ifs: [:find])
527
+ option(:name, short: :n, desc: 'Base filename for the queue task')
528
+ flag(:find, short: :f, desc: 'Choose task/session interactively (fuzzyfind)')
529
+ flag(:horizontal, short: :h, desc: 'Split horizontally in the current tmux window')
530
+ flag(:vertical, short: :v, desc: 'Split vertically in the current tmux window')
531
+ flag(:session, short: :s, desc: 'Use current tmux session')
532
+ flag(:ignore, short: :i, desc: 'Background task — close window when done, no shell')
531
533
  end
532
534
 
533
535
  if opts.help?
@@ -535,6 +537,9 @@ class Hiiro
535
537
  exit 1
536
538
  end
537
539
 
540
+ split ||= :hsplit if opts.horizontal
541
+ split ||= :vsplit if opts.vertical
542
+
538
543
  args = opts.args
539
544
  ti = q.resolve_task_info(opts, h, task_info)
540
545
 
@@ -543,6 +548,47 @@ class Hiiro
543
548
  ti = (ti || {}).merge(session_name: session_name) if session_name
544
549
  end
545
550
 
551
+ # Split+interactive: open editor AND run claude in a new tmux pane
552
+ if split && args.empty? && $stdin.tty?
553
+ fm_lines = ["---"]
554
+ fm_lines << "task_name: #{ti[:task_name]}" if ti&.dig(:task_name)
555
+ fm_lines << "tree_name: #{ti[:tree_name]}" if ti&.dig(:tree_name)
556
+ fm_lines << "session_name: #{ti[:session_name]}" if ti&.dig(:session_name)
557
+ fm_lines << "ignore: true" if opts.ignore
558
+ fm_lines << "# app: <partial-app-name> (run claude from this app's directory)"
559
+ fm_lines << "# dir: <relative-path> (subdir within app or tree root)"
560
+ fm_lines << "---"
561
+ fm_lines << ""
562
+
563
+ tmp_dir = File.join(Dir.home, '.config/hiiro/tmp')
564
+ FileUtils.mkdir_p(tmp_dir)
565
+ base = File.join(tmp_dir, "hq-#{Time.now.strftime('%Y%m%d%H%M%S%L')}")
566
+ prompt_path = "#{base}.md"
567
+ script_path = "#{base}.sh"
568
+ File.write(prompt_path, fm_lines.join("\n"))
569
+
570
+ orig_pane = `tmux display-message -p '\#{pane_id}'`.strip
571
+ split_flag = split == :hsplit ? '-v' : '-h'
572
+ claude_cmd = opts.ignore ? 'claude -p' : 'claude'
573
+ shell_line = opts.ignore ? '' : "exec ${SHELL:-zsh}"
574
+
575
+ File.write(script_path, <<~SH)
576
+ #!/usr/bin/env bash
577
+ ${EDITOR:-vim} #{Shellwords.shellescape(prompt_path)}
578
+ tmux select-pane -t #{Shellwords.shellescape(orig_pane)}
579
+ if [ -s #{Shellwords.shellescape(prompt_path)} ]; then
580
+ cat #{Shellwords.shellescape(prompt_path)} | #{claude_cmd}
581
+ fi
582
+ rm -f #{Shellwords.shellescape(prompt_path)} #{Shellwords.shellescape(script_path)}
583
+ #{shell_line}
584
+ SH
585
+ FileUtils.chmod(0755, script_path)
586
+
587
+ new_pane = `tmux split-window #{split_flag} -P -F '\#{pane_id}' #{Shellwords.shellescape(script_path)} 2>/dev/null`.strip
588
+ system('tmux', 'select-pane', '-t', new_pane) unless new_pane.empty?
589
+ next
590
+ end
591
+
546
592
  if args.empty? && !$stdin.tty?
547
593
  content = $stdin.read.strip
548
594
  elsif args.any?
@@ -590,9 +636,9 @@ class Hiiro
590
636
  h.add_subcmd(:wip) { |*args|
591
637
  q.queue_dirs
592
638
  opts = Hiiro::Options.parse(args) do
593
- option(:task, short: :t, desc: 'Task name')
594
- flag(:choose, short: :T, desc: 'Choose task interactively')
595
- flag(:session, short: :s, desc: 'Use current tmux session')
639
+ option(:task, short: :t, desc: 'Task name', flag_ifs: [:find])
640
+ flag(:find, short: :f, desc: 'Choose task/session interactively (fuzzyfind)')
641
+ flag(:session, short: :s, desc: 'Use current tmux session')
596
642
  end
597
643
  args = opts.args
598
644
  ti = q.resolve_task_info(opts, h, task_info)
data/lib/hiiro/tasks.rb CHANGED
@@ -6,6 +6,16 @@ class Hiiro
6
6
  TASKS_DIR = File.join(Dir.home, '.config', 'hiiro', 'tasks')
7
7
  APPS_FILE = File.join(Dir.home, '.config', 'hiiro', 'apps.yml')
8
8
 
9
+ def self.add_resolvers(hiiro, scope: :task)
10
+ tm = new(hiiro, scope: scope)
11
+ hiiro.add_resolver(:task,
12
+ -> { tm.select_task_interactive }
13
+ ) { |name|
14
+ task = tm.task_by_name(name)
15
+ task&.name || name
16
+ }
17
+ end
18
+
9
19
  attr_reader :hiiro, :scope, :environment
10
20
 
11
21
  def initialize(hiiro, scope: :task, environment: nil)
@@ -937,6 +947,18 @@ class Hiiro
937
947
  print task.session_name if task.session_name
938
948
  end
939
949
 
950
+ h.add_subcmd(:sh) do |task_name=nil, *args|
951
+ task = task_name ? tm.task_by_name(task_name) : tm.current_task
952
+ unless task
953
+ puts task_name ? "Task '#{task_name}' not found" : "Not in a task session"
954
+ next
955
+ end
956
+ tree = tm.environment.find_tree(task.tree_name)
957
+ path = tree ? tree.path : File.join(Hiiro::WORK_DIR, task.tree_name)
958
+ Dir.chdir(path)
959
+ args.empty? ? exec(ENV['SHELL'] || 'zsh') : exec(*args)
960
+ end
961
+
940
962
  h.add_subcmd(:status) { tm.status }
941
963
  h.add_subcmd(:st) { tm.status }
942
964
 
data/lib/hiiro/tmux.rb CHANGED
@@ -27,6 +27,32 @@ class Hiiro
27
27
  client.open_session(name, **opts)
28
28
  end
29
29
 
30
+ def self.add_resolvers(hiiro)
31
+ hiiro.add_resolver(:pane,
32
+ -> { hiiro.fuzzyfind_from_map(hiiro.tmux_client.panes(all: true).name_map) }
33
+ ) { |ref| ref }
34
+
35
+ hiiro.add_resolver(:window,
36
+ -> { hiiro.fuzzyfind_from_map(hiiro.tmux_client.windows(all: true).name_map) }
37
+ ) { |ref| ref }
38
+
39
+ hiiro.add_resolver(:session,
40
+ -> { hiiro.fuzzyfind_from_map(hiiro.tmux_client.sessions.name_map) }
41
+ ) { |ref| ref }
42
+
43
+ hiiro.add_resolver(:buffer,
44
+ -> {
45
+ buffers = hiiro.tmux_client.buffers
46
+ return nil if buffers.empty?
47
+ hiiro.fuzzyfind_from_map(buffers.name_map)
48
+ }
49
+ ) { |ref|
50
+ buffers = hiiro.tmux_client.buffers
51
+ matching = buffers.matching(ref)
52
+ matching.size == 1 ? matching.first.name : ref
53
+ }
54
+ end
55
+
30
56
  attr_reader :hiiro
31
57
 
32
58
  def initialize(hiiro = nil)
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.279"
2
+ VERSION = "0.1.280"
3
3
  end
data/lib/hiiro.rb CHANGED
@@ -28,6 +28,7 @@ require_relative "hiiro/app_files"
28
28
  require_relative "hiiro/rbenv"
29
29
  require_relative "hiiro/any_struct"
30
30
  require_relative "hiiro/pinned_pr_manager"
31
+ require_relative "hiiro/projects"
31
32
 
32
33
  class String
33
34
  def underscore(camel_cased_word=self)
data/plugins/project.rb CHANGED
@@ -2,72 +2,32 @@
2
2
 
3
3
  module Project
4
4
  def self.load(hiiro)
5
- # hiiro.load_plugin(Tmux)
6
- attach_methods(hiiro)
7
5
  add_subcommands(hiiro)
8
6
  end
9
7
 
10
8
  def self.add_subcommands(hiiro)
11
9
  hiiro.add_subcmd(:project) do |project_name|
12
- re = /#{project_name}/i
10
+ projects = Hiiro::Projects.new
11
+ path = projects.find(project_name.to_s)
13
12
 
14
- conf_matches = hiiro.projects_from_config.select{|k,v| k.match?(re) }
15
- dir_matches = hiiro.project_dirs.select{|proj, path| proj.match?(re) }
16
-
17
- puts(conf_matches:,dir_matches:)
18
- matches = dir_matches.merge(conf_matches)
19
- if matches.count > 1
20
- matches = matches.select{|name, path| name == project_name }
21
- end
22
-
23
- puts matches_two: matches
24
- case matches.count
25
- when 0
13
+ if path.nil?
14
+ # Fall back to ~/proj root
26
15
  name = 'proj'
27
16
  path = File.join(Dir.home, 'proj')
28
-
29
17
  unless Dir.exist?(path)
30
18
  puts "Error: #{path.inspect} does not exist"
31
19
  exit 1
32
20
  end
33
-
34
- puts "changing dir: #{path}"
35
- Dir.chdir(path)
36
-
37
- hiiro.start_tmux_session(name)
38
- when 1
39
- name, path = matches.first
40
-
41
21
  puts "changing dir: #{path}"
42
22
  Dir.chdir(path)
43
-
44
23
  hiiro.start_tmux_session(name)
45
- when (2..)
46
- puts "ERROR: Multiple matches found"
47
- puts
48
- puts "Matches:"
49
- matches.each { |name, path|
50
- puts format(" %s: %s", name, path)
51
- }
24
+ next
52
25
  end
53
- end
54
- end
55
26
 
56
- def self.attach_methods(hiiro)
57
- hiiro.instance_eval do
58
- def project_dirs
59
- Dir.glob(File.join(Dir.home, 'proj', '*/')).map { |path|
60
- [File.basename(path), path]
61
- }.to_h
62
- end
63
-
64
- def projects_from_config
65
- projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
66
-
67
- return {} unless File.exist?(projects_file)
68
-
69
- YAML.safe_load_file(projects_file)
70
- end
27
+ name = project_name.to_s
28
+ puts "changing dir: #{path}"
29
+ Dir.chdir(path)
30
+ hiiro.start_tmux_session(name)
71
31
  end
72
32
  end
73
33
  end
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.279
4
+ version: 0.1.280
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -271,6 +271,7 @@ files:
271
271
  - lib/hiiro/options.rb
272
272
  - lib/hiiro/paths.rb
273
273
  - lib/hiiro/pinned_pr_manager.rb
274
+ - lib/hiiro/projects.rb
274
275
  - lib/hiiro/queue.rb
275
276
  - lib/hiiro/rbenv.rb
276
277
  - lib/hiiro/runner_tool.rb