hiiro 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/plugins/tasks.rb ADDED
@@ -0,0 +1,813 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'open3'
4
+
5
+ WORK_DIR = File.join(Dir.home, 'work')
6
+ REPO_PATH = File.join(WORK_DIR, '.bare')
7
+
8
+ class TmuxSession
9
+ attr_reader :name
10
+
11
+ def self.current
12
+ return nil unless ENV['TMUX']
13
+
14
+ name = `tmux display-message -p '#S'`.chomp
15
+ new(name)
16
+ end
17
+
18
+ def self.all
19
+ output = `tmux list-sessions -F '#S' 2>/dev/null`
20
+ output.lines(chomp: true).map { |name| new(name) }
21
+ end
22
+
23
+ def initialize(name)
24
+ @name = name
25
+ end
26
+
27
+ def ==(other)
28
+ other.is_a?(TmuxSession) && name == other.name
29
+ end
30
+
31
+ def to_s
32
+ name
33
+ end
34
+ end
35
+
36
+ class Tree
37
+ attr_reader :path, :head, :branch
38
+
39
+ def self.all(repo_path: REPO_PATH)
40
+ output = `git -C #{repo_path} worktree list --porcelain 2>/dev/null`
41
+
42
+ trees = []
43
+ current = nil
44
+
45
+ output.lines(chomp: true).each do |line|
46
+ case line
47
+ when /^worktree (.*)/
48
+ trees << new(**current) if current
49
+ current = { path: $1 }
50
+ when /^HEAD (.*)/
51
+ current[:head] = $1 if current
52
+ when /^branch refs\/heads\/(.*)/
53
+ current[:branch] = $1 if current
54
+ when 'bare'
55
+ current = nil
56
+ end
57
+ end
58
+
59
+ trees << new(**current) if current
60
+ trees
61
+ end
62
+
63
+ def initialize(path:, head: nil, branch: nil)
64
+ @path = path
65
+ @head = head
66
+ @branch = branch
67
+ end
68
+
69
+ def name
70
+ @name ||= if path.start_with?(WORK_DIR + '/')
71
+ path.sub(WORK_DIR + '/', '')
72
+ else
73
+ File.basename(path)
74
+ end
75
+ end
76
+
77
+ def match?(pwd = Dir.pwd)
78
+ pwd == path || pwd.start_with?(path + '/')
79
+ end
80
+
81
+ def detached?
82
+ branch.nil?
83
+ end
84
+
85
+ def ==(other)
86
+ other.is_a?(Tree) && path == other.path
87
+ end
88
+
89
+ def to_s
90
+ name
91
+ end
92
+ end
93
+
94
+ class Task
95
+ attr_reader :name, :tree_name, :session_name
96
+
97
+ def initialize(name:, tree: nil, session: nil, **_)
98
+ @name = name
99
+ @tree_name = tree
100
+ @session_name = session || name
101
+ end
102
+
103
+ def parent_name
104
+ return nil unless subtask?
105
+ name.split('/').first
106
+ end
107
+
108
+ def short_name
109
+ subtask? ? name.split('/', 2).last : name
110
+ end
111
+
112
+ def subtask?
113
+ name.include?('/')
114
+ end
115
+
116
+ def top_level?
117
+ !subtask?
118
+ end
119
+
120
+ def ==(other)
121
+ other.is_a?(Task) && name == other.name
122
+ end
123
+
124
+ def to_s
125
+ name
126
+ end
127
+
128
+ def to_h
129
+ h = { 'name' => name }
130
+ h['tree'] = tree_name if tree_name
131
+ h['session'] = session_name if session_name != name
132
+ h
133
+ end
134
+ end
135
+
136
+ class App
137
+ attr_reader :name, :relative_path
138
+
139
+ def initialize(name:, path:)
140
+ @name = name
141
+ @relative_path = path
142
+ end
143
+
144
+ def resolve(tree_root)
145
+ File.join(tree_root, relative_path)
146
+ end
147
+
148
+ def ==(other)
149
+ other.is_a?(App) && name == other.name
150
+ end
151
+
152
+ def to_s
153
+ name
154
+ end
155
+ end
156
+
157
+ class Environment
158
+ attr_reader :path
159
+
160
+ def self.current
161
+ new(path: Dir.pwd)
162
+ end
163
+
164
+ def initialize(path: Dir.pwd, config: nil)
165
+ @path = path
166
+ @config = config
167
+ end
168
+
169
+ def config
170
+ @config ||= TaskManager::Config.new
171
+ end
172
+
173
+ def all_tasks
174
+ @all_tasks ||= config.tasks
175
+ end
176
+
177
+ def all_sessions
178
+ @all_sessions ||= TmuxSession.all
179
+ end
180
+
181
+ def all_trees
182
+ @all_trees ||= Tree.all
183
+ end
184
+
185
+ def all_apps
186
+ @all_apps ||= config.apps
187
+ end
188
+
189
+ def task
190
+ @task ||= begin
191
+ s = session
192
+ t = tree
193
+ all_tasks.find { |task|
194
+ (s && task.session_name == s.name) ||
195
+ (t && task.tree_name == t.name)
196
+ }
197
+ end
198
+ end
199
+
200
+ def session
201
+ @session ||= TmuxSession.current
202
+ end
203
+
204
+ def tree
205
+ @tree ||= all_trees.find { |t| t.match?(path) }
206
+ end
207
+
208
+ def find_task(abbreviated)
209
+ return nil if abbreviated.nil?
210
+
211
+ if abbreviated.include?('/')
212
+ parent_prefix, child_prefix = abbreviated.split('/', 2)
213
+ parent = all_tasks.select(&:top_level?).find { |t| t.name.start_with?(parent_prefix) }
214
+ return nil unless parent
215
+
216
+ subtask = all_tasks.select { |t| t.parent_name == parent.name }.find { |t| t.short_name.start_with?(child_prefix) }
217
+ return subtask if subtask
218
+
219
+ # "main" refers to the parent task itself
220
+ return parent if 'main'.start_with?(child_prefix)
221
+
222
+ nil
223
+ else
224
+ all_tasks.find { |t| t.name.start_with?(abbreviated) }
225
+ end
226
+ end
227
+
228
+ def find_tree(abbreviated)
229
+ return nil if abbreviated.nil?
230
+ all_trees.find { |t| t.name.start_with?(abbreviated) }
231
+ end
232
+
233
+ def find_session(abbreviated)
234
+ return nil if abbreviated.nil?
235
+ all_sessions.find { |s| s.name.start_with?(abbreviated) }
236
+ end
237
+
238
+ def find_app(abbreviated)
239
+ return nil if abbreviated.nil?
240
+ all_apps.find { |a| a.name.start_with?(abbreviated) }
241
+ end
242
+ end
243
+
244
+ class TaskManager
245
+ TASKS_DIR = File.join(Dir.home, '.config', 'hiiro', 'tasks')
246
+ APPS_FILE = File.join(Dir.home, '.config', 'hiiro', 'apps.yml')
247
+
248
+ class Config
249
+ attr_reader :tasks_file, :apps_file
250
+
251
+ def initialize(tasks_file: nil, apps_file: nil)
252
+ @tasks_file = tasks_file || File.join(TASKS_DIR, 'tasks.yml')
253
+ @apps_file = apps_file || APPS_FILE
254
+ end
255
+
256
+ def tasks
257
+ data = load_tasks
258
+ (data['tasks'] || []).map { |h| Task.new(**h.transform_keys(&:to_sym)) }
259
+ end
260
+
261
+ def apps
262
+ return [] unless File.exist?(apps_file)
263
+ data = YAML.safe_load_file(apps_file) || {}
264
+ data.map { |name, path| App.new(name: name, path: path) }
265
+ end
266
+
267
+ def save_task(task)
268
+ data = load_tasks
269
+ data['tasks'] ||= []
270
+ data['tasks'].reject! { |t| t['name'] == task.name }
271
+ data['tasks'] << task.to_h
272
+ save_tasks(data)
273
+ end
274
+
275
+ def remove_task(name)
276
+ data = load_tasks
277
+ data['tasks'] ||= []
278
+ data['tasks'].reject! { |t| t['name'] == name }
279
+ save_tasks(data)
280
+ end
281
+
282
+ private
283
+
284
+ def load_tasks
285
+ if File.exist?(tasks_file)
286
+ return YAML.safe_load_file(tasks_file) || { 'tasks' => [] }
287
+ end
288
+
289
+ assignments_file = File.join(File.dirname(tasks_file), 'assignments.yml')
290
+ if File.exist?(assignments_file)
291
+ raw = YAML.safe_load_file(assignments_file) || {}
292
+ data = { 'tasks' => raw.map { |path, name| { 'name' => name, 'tree' => path } } }
293
+ save_tasks(data)
294
+ return data
295
+ end
296
+
297
+ { 'tasks' => [] }
298
+ end
299
+
300
+ def save_tasks(data)
301
+ FileUtils.mkdir_p(File.dirname(tasks_file))
302
+ File.write(tasks_file, YAML.dump(data))
303
+ end
304
+ end
305
+
306
+ attr_reader :hiiro, :scope, :environment
307
+
308
+ def initialize(hiiro, scope: :task, environment: nil)
309
+ @hiiro = hiiro
310
+ @scope = scope
311
+ @environment = environment || Environment.current
312
+ end
313
+
314
+ def config
315
+ environment.config
316
+ end
317
+
318
+ # --- Scope-aware queries ---
319
+
320
+ def tasks
321
+ if scope == :subtask
322
+ parent = current_parent_task
323
+ return [] unless parent
324
+ main_task = Task.new(name: "#{parent.name}/main", tree: parent.tree_name, session: parent.session_name)
325
+ subtask_list = environment.all_tasks.select { |t| t.parent_name == parent.name }
326
+ [main_task, *subtask_list]
327
+ else
328
+ environment.all_tasks.select(&:top_level?)
329
+ end
330
+ end
331
+
332
+ def subtasks(task)
333
+ environment.all_tasks.select { |t| t.parent_name == task.name }
334
+ end
335
+
336
+ def task_by_name(name)
337
+ return slash_lookup(name) if name.include?('/')
338
+
339
+ tasks.find { |t|
340
+ match_name = (scope == :subtask) ? t.short_name : t.name
341
+ match_name.start_with?(name)
342
+ }
343
+ end
344
+
345
+ def task_by_tree(tree_name)
346
+ tasks.find { |t| t.tree_name == tree_name }
347
+ end
348
+
349
+ def task_by_session(session_name)
350
+ tasks.find { |t| t.session_name == session_name }
351
+ end
352
+
353
+ def current_task
354
+ environment.task
355
+ end
356
+
357
+ def current_session
358
+ environment.session
359
+ end
360
+
361
+ def current_tree
362
+ environment.tree
363
+ end
364
+
365
+ # --- Actions ---
366
+
367
+ def start_task(name, app_name: nil)
368
+ existing = task_by_name(name)
369
+ if existing
370
+ puts "Task '#{existing.name}' already exists. Switching..."
371
+ switch_to_task(existing, app_name: app_name)
372
+ return
373
+ end
374
+
375
+ task_name = scope == :subtask ? "#{current_parent_task.name}/#{name}" : name
376
+ subtree_name = scope == :subtask ? "#{current_parent_task.name}/#{name}" : "#{name}/main"
377
+
378
+ target_path = File.join(WORK_DIR, subtree_name)
379
+
380
+ available = find_available_tree
381
+ if available
382
+ puts "Renaming worktree '#{available.name}' to '#{subtree_name}'..."
383
+ FileUtils.mkdir_p(File.dirname(target_path))
384
+ unless system('git', '-C', REPO_PATH, 'worktree', 'move', available.path, target_path)
385
+ puts "ERROR: Failed to rename worktree"
386
+ return
387
+ end
388
+ else
389
+ puts "Creating new worktree '#{subtree_name}'..."
390
+ FileUtils.mkdir_p(File.dirname(target_path))
391
+ unless system('git', '-C', REPO_PATH, 'worktree', 'add', '--detach', target_path)
392
+ puts "ERROR: Failed to create worktree"
393
+ return
394
+ end
395
+ end
396
+
397
+ session_name = task_name
398
+ task = Task.new(name: task_name, tree: subtree_name, session: session_name)
399
+ config.save_task(task)
400
+
401
+ base_dir = target_path
402
+ if app_name
403
+ app = environment.find_app(app_name)
404
+ base_dir = app.resolve(target_path) if app
405
+ end
406
+
407
+ Dir.chdir(base_dir)
408
+ hiiro.start_tmux_session(session_name)
409
+
410
+ puts "Started task '#{task_name}' in worktree '#{subtree_name}'"
411
+ end
412
+
413
+ def switch_to_task(task, app_name: nil)
414
+ unless task
415
+ puts "Task not found"
416
+ return
417
+ end
418
+
419
+ tree = environment.find_tree(task.tree_name)
420
+ tree_path = tree ? tree.path : File.join(WORK_DIR, task.tree_name)
421
+
422
+ session_name = task.session_name
423
+ session_exists = system('tmux', 'has-session', '-t', session_name, err: File::NULL)
424
+
425
+ if session_exists
426
+ hiiro.start_tmux_session(session_name)
427
+ else
428
+ base_dir = tree_path
429
+ if app_name
430
+ app = environment.find_app(app_name)
431
+ base_dir = app.resolve(tree_path) if app
432
+ end
433
+
434
+ if Dir.exist?(base_dir)
435
+ Dir.chdir(base_dir)
436
+ hiiro.start_tmux_session(session_name)
437
+ else
438
+ puts "ERROR: Path '#{base_dir}' does not exist"
439
+ return
440
+ end
441
+ end
442
+
443
+ puts "Switched to '#{task.name}'"
444
+ end
445
+
446
+ def stop_task(task)
447
+ unless task
448
+ puts "Task not found"
449
+ return
450
+ end
451
+
452
+ config.remove_task(task.name)
453
+ # Also remove any subtasks
454
+ subtasks(task).each { |st| config.remove_task(st.name) }
455
+
456
+ puts "Stopped task '#{task.name}' (worktree available for reuse)"
457
+ end
458
+
459
+ def list
460
+ items = tasks
461
+ if items.empty?
462
+ puts scope == :subtask ? "No subtasks found" : "No tasks found"
463
+ puts "Use 'h #{scope} start NAME' to create one."
464
+ return
465
+ end
466
+
467
+ current = current_task
468
+ label = scope == :subtask ? "Subtasks" : "Tasks"
469
+ if scope == :subtask && current
470
+ parent = current_parent_task
471
+ label = "Subtasks of '#{parent&.name}'" if parent
472
+ end
473
+
474
+ puts "#{label}:"
475
+ puts
476
+
477
+ items.each do |task|
478
+ marker = (current && current.name == task.name) ? "*" : " "
479
+ tree = environment.find_tree(task.tree_name)
480
+ branch = tree&.branch || (tree&.detached? ? '(detached)' : nil)
481
+ branch_str = branch ? " [#{branch}]" : ""
482
+
483
+ display_name = scope == :subtask ? task.short_name : task.name
484
+ puts format("%s %-25s tree: %-20s%s", marker, display_name, task.tree_name || '(none)', branch_str)
485
+
486
+ # Show subtask count for top-level tasks
487
+ if scope == :task
488
+ subs = subtasks(task)
489
+ subs.each do |st|
490
+ sub_marker = (current && current.name == st.name) ? "*" : " "
491
+ sub_tree = environment.find_tree(st.tree_name)
492
+ sub_branch = sub_tree&.branch || (sub_tree&.detached? ? '(detached)' : nil)
493
+ sub_branch_str = sub_branch ? " [#{sub_branch}]" : ""
494
+ padding = " " * task.name.length
495
+ puts format("%s %s/%-*s tree: %-20s%s", sub_marker, padding, 25 - task.name.length - 1, st.short_name, st.tree_name || '(none)', sub_branch_str)
496
+ end
497
+ end
498
+ end
499
+
500
+ available = environment.all_trees.reject { |t|
501
+ environment.all_tasks.any? { |task| task.tree_name == t.name }
502
+ }
503
+
504
+ if available.any?
505
+ puts
506
+ available.each do |tree|
507
+ branch_str = tree.branch ? " [#{tree.branch}]" : tree.detached? ? " [(detached)]" : ""
508
+ puts format(" %-25s (available)%s", tree.name, branch_str)
509
+ end
510
+ end
511
+ end
512
+
513
+ def status
514
+ task = current_task
515
+ unless task
516
+ puts "Not currently in a task session"
517
+ return
518
+ end
519
+
520
+ puts "Task: #{task.name}"
521
+ puts "Worktree: #{task.tree_name}"
522
+ tree = environment.find_tree(task.tree_name)
523
+ puts "Path: #{tree&.path || '(unknown)'}"
524
+ puts "Session: #{task.session_name}"
525
+ puts "Parent: #{task.parent_name}" if task.subtask?
526
+ end
527
+
528
+ def save
529
+ task = current_task
530
+ unless task
531
+ puts "ERROR: Not currently in a task session"
532
+ return
533
+ end
534
+
535
+ windows = capture_tmux_windows(task.session_name)
536
+ puts "Saved task '#{task.name}' state (#{windows.count} windows)"
537
+ end
538
+
539
+ def open_app(app_name)
540
+ task = current_task
541
+ unless task
542
+ puts "ERROR: Not currently in a task session"
543
+ return
544
+ end
545
+
546
+ result = resolve_app(app_name, task)
547
+ return unless result
548
+
549
+ resolved_name, app_path = result
550
+ system('tmux', 'new-window', '-n', resolved_name, '-c', app_path)
551
+ puts "Opened '#{resolved_name}' in new window (#{app_path})"
552
+ end
553
+
554
+ def list_apps
555
+ apps = environment.all_apps
556
+ if apps.any?
557
+ puts "Configured apps:"
558
+ puts
559
+ apps.each do |app|
560
+ puts format(" %-20s => %s", app.name, app.relative_path)
561
+ end
562
+ else
563
+ puts "No apps configured."
564
+ puts "Create #{APPS_FILE} with format:"
565
+ puts " app_name: relative/path/from/repo"
566
+ end
567
+ end
568
+
569
+ def cd_to_task(task)
570
+ unless task
571
+ puts "Task not found"
572
+ return
573
+ end
574
+
575
+ tree = environment.find_tree(task.tree_name)
576
+ path = tree ? tree.path : File.join(WORK_DIR, task.tree_name)
577
+ send_cd(path)
578
+ end
579
+
580
+ def cd_to_app(app_name = nil)
581
+ task = current_task
582
+ unless task
583
+ puts "ERROR: Not currently in a task session"
584
+ return
585
+ end
586
+
587
+ if app_name.nil? || app_name.empty?
588
+ tree = environment.find_tree(task.tree_name)
589
+ send_cd(tree&.path || File.join(WORK_DIR, task.tree_name))
590
+ return
591
+ end
592
+
593
+ result = resolve_app(app_name, task)
594
+ return unless result
595
+
596
+ _resolved_name, app_path = result
597
+ send_cd(app_path)
598
+ end
599
+
600
+ def app_path(app_name = nil)
601
+ task = current_task
602
+ tree_root = if task
603
+ tree = environment.find_tree(task.tree_name)
604
+ tree&.path || File.join(WORK_DIR, task.tree_name)
605
+ else
606
+ `git rev-parse --show-toplevel`.strip
607
+ end
608
+
609
+ if app_name.nil?
610
+ print tree_root
611
+ return
612
+ end
613
+
614
+ matches = environment.all_apps.select { |a| a.name.start_with?(app_name) }
615
+
616
+ case matches.count
617
+ when 0
618
+ puts "ERROR: No matches found"
619
+ puts
620
+ puts "Possible Apps:"
621
+ environment.all_apps.each { |a| puts format(" %-20s => %s", a.name, a.relative_path) }
622
+ when 1
623
+ print matches.first.resolve(tree_root)
624
+ else
625
+ puts "Multiple matches found:"
626
+ matches.each { |a| puts format(" %-20s => %s", a.name, a.relative_path) }
627
+ end
628
+ end
629
+
630
+ def help
631
+ scope_name = scope.to_s
632
+ puts "Usage: h #{scope_name} <subcommand> [args]"
633
+ puts
634
+ puts "Subcommands:"
635
+ puts " list, ls List #{scope_name}s"
636
+ puts " start NAME [APP] Start a new #{scope_name}"
637
+ puts " switch [NAME] Switch to a #{scope_name} (interactive if no name)"
638
+ puts " app [APP_NAME] Open app in new tmux window (interactive if no name)"
639
+ puts " apps List configured apps"
640
+ puts " cd [APP_NAME] Change directory to app"
641
+ puts " path [APP_NAME] Print app path"
642
+ puts " status, st Show current #{scope_name} status"
643
+ puts " save Save current session state"
644
+ puts " stop [NAME] Stop a #{scope_name} (interactive if no name)"
645
+ end
646
+
647
+ # --- Interactive selection with sk ---
648
+
649
+ def select_task_interactive(prompt = nil)
650
+ names = tasks.map { |t| scope == :subtask ? t.short_name : t.name }
651
+ return nil if names.empty?
652
+
653
+ sk_select(names)
654
+ end
655
+
656
+ # --- Private helpers ---
657
+
658
+ private
659
+
660
+ def slash_lookup(input)
661
+ environment.find_task(input)
662
+ end
663
+
664
+ def current_parent_task
665
+ task = current_task
666
+ return nil unless task
667
+
668
+ if task.subtask?
669
+ environment.find_task(task.parent_name)
670
+ else
671
+ task
672
+ end
673
+ end
674
+
675
+ def find_available_tree
676
+ assigned_tree_names = environment.all_tasks.map(&:tree_name)
677
+ environment.all_trees.find { |tree| !assigned_tree_names.include?(tree.name) }
678
+ end
679
+
680
+ def resolve_app(app_name, task)
681
+ tree = environment.find_tree(task.tree_name)
682
+ tree_root = tree ? tree.path : File.join(WORK_DIR, task.tree_name)
683
+
684
+ matches = environment.all_apps.select { |a| a.name.start_with?(app_name) }
685
+
686
+ case matches.count
687
+ when 0
688
+ # Fallback: directory discovery
689
+ exact = File.join(tree_root, app_name)
690
+ return [app_name, exact] if Dir.exist?(exact)
691
+
692
+ nested = File.join(tree_root, app_name, app_name)
693
+ return [app_name, nested] if Dir.exist?(nested)
694
+
695
+ puts "ERROR: App '#{app_name}' not found"
696
+ list_apps
697
+ nil
698
+ when 1
699
+ app = matches.first
700
+ [app.name, app.resolve(tree_root)]
701
+ else
702
+ exact = matches.find { |a| a.name == app_name }
703
+ if exact
704
+ [exact.name, exact.resolve(tree_root)]
705
+ else
706
+ puts "ERROR: '#{app_name}' matches multiple apps:"
707
+ matches.each { |a| puts " #{a.name}" }
708
+ nil
709
+ end
710
+ end
711
+ end
712
+
713
+ def send_cd(path)
714
+ pane = ENV['TMUX_PANE']
715
+ if pane
716
+ system('tmux', 'send-keys', '-t', pane, "cd #{path}\n")
717
+ else
718
+ system('tmux', 'send-keys', "cd #{path}\n")
719
+ end
720
+ end
721
+
722
+ def capture_tmux_windows(session)
723
+ output = `tmux list-windows -t #{session} -F '\#{window_index}:\#{window_name}:\#{pane_current_path}' 2>/dev/null`
724
+ output.lines.map(&:strip).map { |line|
725
+ idx, name, path = line.split(':')
726
+ { 'index' => idx, 'name' => name, 'path' => path }
727
+ }
728
+ end
729
+
730
+ def sk_select(items)
731
+ selected, status = Open3.capture2('sk', stdin_data: items.join("\n"))
732
+ return selected.strip if status.success? && !selected.strip.empty?
733
+ nil
734
+ end
735
+ end
736
+
737
+ module Tasks
738
+ def self.load(hiiro)
739
+ hiiro.load_plugin(Tmux)
740
+ add_subcommands(hiiro)
741
+ end
742
+
743
+ def self.add_subcommands(hiiro)
744
+ hiiro.add_subcmd(:task) do |*args|
745
+ mgr = TaskManager.new(hiiro, scope: :task)
746
+ task_hiiro = Tasks.build_hiiro(hiiro, mgr)
747
+ task_hiiro.run
748
+ end
749
+
750
+ hiiro.add_subcmd(:subtask) do |*args|
751
+ mgr = TaskManager.new(hiiro, scope: :subtask)
752
+ task_hiiro = Tasks.build_hiiro(hiiro, mgr)
753
+ task_hiiro.run
754
+ end
755
+ end
756
+
757
+ def self.build_hiiro(parent_hiiro, mgr)
758
+ Hiiro.init(*parent_hiiro.args, mgr: mgr) do |h|
759
+ h.add_subcmd(:list) { mgr.list }
760
+ h.add_subcmd(:ls) { mgr.list }
761
+
762
+ h.add_subcmd(:start) do |task_name, app_name=nil|
763
+ mgr.start_task(task_name, app_name: app_name)
764
+ end
765
+
766
+ h.add_subcmd(:switch) do |task_name=nil, app_name=nil|
767
+ if task_name.nil?
768
+ task_name = mgr.select_task_interactive
769
+ return unless task_name
770
+ end
771
+ task = mgr.task_by_name(task_name)
772
+ mgr.switch_to_task(task, app_name: app_name)
773
+ end
774
+
775
+ h.add_subcmd(:app) do |app_name=nil|
776
+ if app_name.nil?
777
+ names = mgr.environment.all_apps.map(&:name)
778
+ app_name = mgr.send(:sk_select, names)
779
+ return unless app_name
780
+ end
781
+ mgr.open_app(app_name)
782
+ end
783
+
784
+ h.add_subcmd(:apps) { mgr.list_apps }
785
+
786
+ h.add_subcmd(:cd) do |app_name=nil|
787
+ mgr.cd_to_app(app_name)
788
+ end
789
+
790
+ h.add_subcmd(:path) do |app_name=nil|
791
+ mgr.app_path(app_name)
792
+ end
793
+
794
+ h.add_subcmd(:status) { mgr.status }
795
+ h.add_subcmd(:st) { mgr.status }
796
+
797
+ h.add_subcmd(:save) { mgr.save }
798
+
799
+ h.add_subcmd(:stop) do |task_name=nil|
800
+ if task_name.nil?
801
+ task_name = mgr.select_task_interactive
802
+ return unless task_name
803
+ end
804
+ task = mgr.task_by_name(task_name)
805
+ mgr.stop_task(task)
806
+ end
807
+
808
+ h.add_subcmd(:edit) do
809
+ system(ENV['EDITOR'] || 'nvim', __FILE__)
810
+ end
811
+ end
812
+ end
813
+ end