hiiro 0.1.31 → 0.1.33.pre.1

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,818 @@
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
+ tasks = raw.map do |tree_path, task_name|
293
+ h = { 'name' => task_name, 'tree' => tree_path }
294
+ h['session'] = task_name if task_name.include?('/')
295
+ h
296
+ end
297
+ data = { 'tasks' => tasks }
298
+ save_tasks(data)
299
+ return data
300
+ end
301
+
302
+ { 'tasks' => [] }
303
+ end
304
+
305
+ def save_tasks(data)
306
+ FileUtils.mkdir_p(File.dirname(tasks_file))
307
+ File.write(tasks_file, YAML.dump(data))
308
+ end
309
+ end
310
+
311
+ attr_reader :hiiro, :scope, :environment
312
+
313
+ def initialize(hiiro, scope: :task, environment: nil)
314
+ @hiiro = hiiro
315
+ @scope = scope
316
+ @environment = environment || Environment.current
317
+ end
318
+
319
+ def config
320
+ environment.config
321
+ end
322
+
323
+ # --- Scope-aware queries ---
324
+
325
+ def tasks
326
+ if scope == :subtask
327
+ parent = current_parent_task
328
+ return [] unless parent
329
+ main_task = Task.new(name: "#{parent.name}/main", tree: parent.tree_name, session: parent.session_name)
330
+ subtask_list = environment.all_tasks.select { |t| t.parent_name == parent.name }
331
+ [main_task, *subtask_list]
332
+ else
333
+ environment.all_tasks.select(&:top_level?)
334
+ end
335
+ end
336
+
337
+ def subtasks(task)
338
+ environment.all_tasks.select { |t| t.parent_name == task.name }
339
+ end
340
+
341
+ def task_by_name(name)
342
+ return slash_lookup(name) if name.include?('/')
343
+
344
+ tasks.find { |t|
345
+ match_name = (scope == :subtask) ? t.short_name : t.name
346
+ match_name.start_with?(name)
347
+ }
348
+ end
349
+
350
+ def task_by_tree(tree_name)
351
+ tasks.find { |t| t.tree_name == tree_name }
352
+ end
353
+
354
+ def task_by_session(session_name)
355
+ tasks.find { |t| t.session_name == session_name }
356
+ end
357
+
358
+ def current_task
359
+ environment.task
360
+ end
361
+
362
+ def current_session
363
+ environment.session
364
+ end
365
+
366
+ def current_tree
367
+ environment.tree
368
+ end
369
+
370
+ # --- Actions ---
371
+
372
+ def start_task(name, app_name: nil)
373
+ existing = task_by_name(name)
374
+ if existing
375
+ puts "Task '#{existing.name}' already exists. Switching..."
376
+ switch_to_task(existing, app_name: app_name)
377
+ return
378
+ end
379
+
380
+ task_name = scope == :subtask ? "#{current_parent_task.name}/#{name}" : name
381
+ subtree_name = scope == :subtask ? "#{current_parent_task.name}/#{name}" : "#{name}/main"
382
+
383
+ target_path = File.join(WORK_DIR, subtree_name)
384
+
385
+ available = find_available_tree
386
+ if available
387
+ puts "Renaming worktree '#{available.name}' to '#{subtree_name}'..."
388
+ FileUtils.mkdir_p(File.dirname(target_path))
389
+ unless system('git', '-C', REPO_PATH, 'worktree', 'move', available.path, target_path)
390
+ puts "ERROR: Failed to rename worktree"
391
+ return
392
+ end
393
+ else
394
+ puts "Creating new worktree '#{subtree_name}'..."
395
+ FileUtils.mkdir_p(File.dirname(target_path))
396
+ unless system('git', '-C', REPO_PATH, 'worktree', 'add', '--detach', target_path)
397
+ puts "ERROR: Failed to create worktree"
398
+ return
399
+ end
400
+ end
401
+
402
+ session_name = task_name
403
+ task = Task.new(name: task_name, tree: subtree_name, session: session_name)
404
+ config.save_task(task)
405
+
406
+ base_dir = target_path
407
+ if app_name
408
+ app = environment.find_app(app_name)
409
+ base_dir = app.resolve(target_path) if app
410
+ end
411
+
412
+ Dir.chdir(base_dir)
413
+ hiiro.start_tmux_session(session_name)
414
+
415
+ puts "Started task '#{task_name}' in worktree '#{subtree_name}'"
416
+ end
417
+
418
+ def switch_to_task(task, app_name: nil)
419
+ unless task
420
+ puts "Task not found"
421
+ return
422
+ end
423
+
424
+ tree = environment.find_tree(task.tree_name)
425
+ tree_path = tree ? tree.path : File.join(WORK_DIR, task.tree_name)
426
+
427
+ session_name = task.session_name
428
+ session_exists = system('tmux', 'has-session', '-t', session_name, err: File::NULL)
429
+
430
+ if session_exists
431
+ hiiro.start_tmux_session(session_name)
432
+ else
433
+ base_dir = tree_path
434
+ if app_name
435
+ app = environment.find_app(app_name)
436
+ base_dir = app.resolve(tree_path) if app
437
+ end
438
+
439
+ if Dir.exist?(base_dir)
440
+ Dir.chdir(base_dir)
441
+ hiiro.start_tmux_session(session_name)
442
+ else
443
+ puts "ERROR: Path '#{base_dir}' does not exist"
444
+ return
445
+ end
446
+ end
447
+
448
+ puts "Switched to '#{task.name}'"
449
+ end
450
+
451
+ def stop_task(task)
452
+ unless task
453
+ puts "Task not found"
454
+ return
455
+ end
456
+
457
+ config.remove_task(task.name)
458
+ # Also remove any subtasks
459
+ subtasks(task).each { |st| config.remove_task(st.name) }
460
+
461
+ puts "Stopped task '#{task.name}' (worktree available for reuse)"
462
+ end
463
+
464
+ def list
465
+ items = tasks
466
+ if items.empty?
467
+ puts scope == :subtask ? "No subtasks found" : "No tasks found"
468
+ puts "Use 'h #{scope} start NAME' to create one."
469
+ return
470
+ end
471
+
472
+ current = current_task
473
+ label = scope == :subtask ? "Subtasks" : "Tasks"
474
+ if scope == :subtask && current
475
+ parent = current_parent_task
476
+ label = "Subtasks of '#{parent&.name}'" if parent
477
+ end
478
+
479
+ puts "#{label}:"
480
+ puts
481
+
482
+ items.each do |task|
483
+ marker = (current && current.name == task.name) ? "*" : " "
484
+ tree = environment.find_tree(task.tree_name)
485
+ branch = tree&.branch || (tree&.detached? ? '(detached)' : nil)
486
+ branch_str = branch ? " [#{branch}]" : ""
487
+
488
+ display_name = scope == :subtask ? task.short_name : task.name
489
+ puts format("%s %-25s tree: %-20s%s", marker, display_name, task.tree_name || '(none)', branch_str)
490
+
491
+ # Show subtask count for top-level tasks
492
+ if scope == :task
493
+ subs = subtasks(task)
494
+ subs.each do |st|
495
+ sub_marker = (current && current.name == st.name) ? "*" : " "
496
+ sub_tree = environment.find_tree(st.tree_name)
497
+ sub_branch = sub_tree&.branch || (sub_tree&.detached? ? '(detached)' : nil)
498
+ sub_branch_str = sub_branch ? " [#{sub_branch}]" : ""
499
+ padding = " " * task.name.length
500
+ 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)
501
+ end
502
+ end
503
+ end
504
+
505
+ available = environment.all_trees.reject { |t|
506
+ environment.all_tasks.any? { |task| task.tree_name == t.name }
507
+ }
508
+
509
+ if available.any?
510
+ puts
511
+ available.each do |tree|
512
+ branch_str = tree.branch ? " [#{tree.branch}]" : tree.detached? ? " [(detached)]" : ""
513
+ puts format(" %-25s (available)%s", tree.name, branch_str)
514
+ end
515
+ end
516
+ end
517
+
518
+ def status
519
+ task = current_task
520
+ unless task
521
+ puts "Not currently in a task session"
522
+ return
523
+ end
524
+
525
+ puts "Task: #{task.name}"
526
+ puts "Worktree: #{task.tree_name}"
527
+ tree = environment.find_tree(task.tree_name)
528
+ puts "Path: #{tree&.path || '(unknown)'}"
529
+ puts "Session: #{task.session_name}"
530
+ puts "Parent: #{task.parent_name}" if task.subtask?
531
+ end
532
+
533
+ def save
534
+ task = current_task
535
+ unless task
536
+ puts "ERROR: Not currently in a task session"
537
+ return
538
+ end
539
+
540
+ windows = capture_tmux_windows(task.session_name)
541
+ puts "Saved task '#{task.name}' state (#{windows.count} windows)"
542
+ end
543
+
544
+ def open_app(app_name)
545
+ task = current_task
546
+ unless task
547
+ puts "ERROR: Not currently in a task session"
548
+ return
549
+ end
550
+
551
+ result = resolve_app(app_name, task)
552
+ return unless result
553
+
554
+ resolved_name, app_path = result
555
+ system('tmux', 'new-window', '-n', resolved_name, '-c', app_path)
556
+ puts "Opened '#{resolved_name}' in new window (#{app_path})"
557
+ end
558
+
559
+ def list_apps
560
+ apps = environment.all_apps
561
+ if apps.any?
562
+ puts "Configured apps:"
563
+ puts
564
+ apps.each do |app|
565
+ puts format(" %-20s => %s", app.name, app.relative_path)
566
+ end
567
+ else
568
+ puts "No apps configured."
569
+ puts "Create #{APPS_FILE} with format:"
570
+ puts " app_name: relative/path/from/repo"
571
+ end
572
+ end
573
+
574
+ def cd_to_task(task)
575
+ unless task
576
+ puts "Task not found"
577
+ return
578
+ end
579
+
580
+ tree = environment.find_tree(task.tree_name)
581
+ path = tree ? tree.path : File.join(WORK_DIR, task.tree_name)
582
+ send_cd(path)
583
+ end
584
+
585
+ def cd_to_app(app_name = nil)
586
+ task = current_task
587
+ unless task
588
+ puts "ERROR: Not currently in a task session"
589
+ return
590
+ end
591
+
592
+ if app_name.nil? || app_name.empty?
593
+ tree = environment.find_tree(task.tree_name)
594
+ send_cd(tree&.path || File.join(WORK_DIR, task.tree_name))
595
+ return
596
+ end
597
+
598
+ result = resolve_app(app_name, task)
599
+ return unless result
600
+
601
+ _resolved_name, app_path = result
602
+ send_cd(app_path)
603
+ end
604
+
605
+ def app_path(app_name = nil)
606
+ task = current_task
607
+ tree_root = if task
608
+ tree = environment.find_tree(task.tree_name)
609
+ tree&.path || File.join(WORK_DIR, task.tree_name)
610
+ else
611
+ `git rev-parse --show-toplevel`.strip
612
+ end
613
+
614
+ if app_name.nil?
615
+ print tree_root
616
+ return
617
+ end
618
+
619
+ matches = environment.all_apps.select { |a| a.name.start_with?(app_name) }
620
+
621
+ case matches.count
622
+ when 0
623
+ puts "ERROR: No matches found"
624
+ puts
625
+ puts "Possible Apps:"
626
+ environment.all_apps.each { |a| puts format(" %-20s => %s", a.name, a.relative_path) }
627
+ when 1
628
+ print matches.first.resolve(tree_root)
629
+ else
630
+ puts "Multiple matches found:"
631
+ matches.each { |a| puts format(" %-20s => %s", a.name, a.relative_path) }
632
+ end
633
+ end
634
+
635
+ def help
636
+ scope_name = scope.to_s
637
+ puts "Usage: h #{scope_name} <subcommand> [args]"
638
+ puts
639
+ puts "Subcommands:"
640
+ puts " list, ls List #{scope_name}s"
641
+ puts " start NAME [APP] Start a new #{scope_name}"
642
+ puts " switch [NAME] Switch to a #{scope_name} (interactive if no name)"
643
+ puts " app [APP_NAME] Open app in new tmux window (interactive if no name)"
644
+ puts " apps List configured apps"
645
+ puts " cd [APP_NAME] Change directory to app"
646
+ puts " path [APP_NAME] Print app path"
647
+ puts " status, st Show current #{scope_name} status"
648
+ puts " save Save current session state"
649
+ puts " stop [NAME] Stop a #{scope_name} (interactive if no name)"
650
+ end
651
+
652
+ # --- Interactive selection with sk ---
653
+
654
+ def select_task_interactive(prompt = nil)
655
+ names = tasks.map { |t| scope == :subtask ? t.short_name : t.name }
656
+ return nil if names.empty?
657
+
658
+ sk_select(names)
659
+ end
660
+
661
+ # --- Private helpers ---
662
+
663
+ private
664
+
665
+ def slash_lookup(input)
666
+ environment.find_task(input)
667
+ end
668
+
669
+ def current_parent_task
670
+ task = current_task
671
+ return nil unless task
672
+
673
+ if task.subtask?
674
+ environment.find_task(task.parent_name)
675
+ else
676
+ task
677
+ end
678
+ end
679
+
680
+ def find_available_tree
681
+ assigned_tree_names = environment.all_tasks.map(&:tree_name)
682
+ environment.all_trees.find { |tree| !assigned_tree_names.include?(tree.name) }
683
+ end
684
+
685
+ def resolve_app(app_name, task)
686
+ tree = environment.find_tree(task.tree_name)
687
+ tree_root = tree ? tree.path : File.join(WORK_DIR, task.tree_name)
688
+
689
+ matches = environment.all_apps.select { |a| a.name.start_with?(app_name) }
690
+
691
+ case matches.count
692
+ when 0
693
+ # Fallback: directory discovery
694
+ exact = File.join(tree_root, app_name)
695
+ return [app_name, exact] if Dir.exist?(exact)
696
+
697
+ nested = File.join(tree_root, app_name, app_name)
698
+ return [app_name, nested] if Dir.exist?(nested)
699
+
700
+ puts "ERROR: App '#{app_name}' not found"
701
+ list_apps
702
+ nil
703
+ when 1
704
+ app = matches.first
705
+ [app.name, app.resolve(tree_root)]
706
+ else
707
+ exact = matches.find { |a| a.name == app_name }
708
+ if exact
709
+ [exact.name, exact.resolve(tree_root)]
710
+ else
711
+ puts "ERROR: '#{app_name}' matches multiple apps:"
712
+ matches.each { |a| puts " #{a.name}" }
713
+ nil
714
+ end
715
+ end
716
+ end
717
+
718
+ def send_cd(path)
719
+ pane = ENV['TMUX_PANE']
720
+ if pane
721
+ system('tmux', 'send-keys', '-t', pane, "cd #{path}\n")
722
+ else
723
+ system('tmux', 'send-keys', "cd #{path}\n")
724
+ end
725
+ end
726
+
727
+ def capture_tmux_windows(session)
728
+ output = `tmux list-windows -t #{session} -F '\#{window_index}:\#{window_name}:\#{pane_current_path}' 2>/dev/null`
729
+ output.lines.map(&:strip).map { |line|
730
+ idx, name, path = line.split(':')
731
+ { 'index' => idx, 'name' => name, 'path' => path }
732
+ }
733
+ end
734
+
735
+ def sk_select(items)
736
+ selected, status = Open3.capture2('sk', stdin_data: items.join("\n"))
737
+ return selected.strip if status.success? && !selected.strip.empty?
738
+ nil
739
+ end
740
+ end
741
+
742
+ module Tasks
743
+ def self.load(hiiro)
744
+ hiiro.load_plugin(Tmux)
745
+ add_subcommands(hiiro)
746
+ end
747
+
748
+ def self.add_subcommands(hiiro)
749
+ hiiro.add_subcmd(:task) do |*args|
750
+ mgr = TaskManager.new(hiiro, scope: :task)
751
+ task_hiiro = Tasks.build_hiiro(hiiro, mgr)
752
+ task_hiiro.run
753
+ end
754
+
755
+ hiiro.add_subcmd(:subtask) do |*args|
756
+ mgr = TaskManager.new(hiiro, scope: :subtask)
757
+ task_hiiro = Tasks.build_hiiro(hiiro, mgr)
758
+ task_hiiro.run
759
+ end
760
+ end
761
+
762
+ def self.build_hiiro(parent_hiiro, mgr)
763
+ Hiiro.init(*parent_hiiro.args, mgr: mgr) do |h|
764
+ h.add_subcmd(:list) { mgr.list }
765
+ h.add_subcmd(:ls) { mgr.list }
766
+
767
+ h.add_subcmd(:start) do |task_name, app_name=nil|
768
+ mgr.start_task(task_name, app_name: app_name)
769
+ end
770
+
771
+ h.add_subcmd(:switch) do |task_name=nil, app_name=nil|
772
+ if task_name.nil?
773
+ task_name = mgr.select_task_interactive
774
+ return unless task_name
775
+ end
776
+ task = mgr.task_by_name(task_name)
777
+ mgr.switch_to_task(task, app_name: app_name)
778
+ end
779
+
780
+ h.add_subcmd(:app) do |app_name=nil|
781
+ if app_name.nil?
782
+ names = mgr.environment.all_apps.map(&:name)
783
+ app_name = mgr.send(:sk_select, names)
784
+ return unless app_name
785
+ end
786
+ mgr.open_app(app_name)
787
+ end
788
+
789
+ h.add_subcmd(:apps) { mgr.list_apps }
790
+
791
+ h.add_subcmd(:cd) do |app_name=nil|
792
+ mgr.cd_to_app(app_name)
793
+ end
794
+
795
+ h.add_subcmd(:path) do |app_name=nil|
796
+ mgr.app_path(app_name)
797
+ end
798
+
799
+ h.add_subcmd(:status) { mgr.status }
800
+ h.add_subcmd(:st) { mgr.status }
801
+
802
+ h.add_subcmd(:save) { mgr.save }
803
+
804
+ h.add_subcmd(:stop) do |task_name=nil|
805
+ if task_name.nil?
806
+ task_name = mgr.select_task_interactive
807
+ return unless task_name
808
+ end
809
+ task = mgr.task_by_name(task_name)
810
+ mgr.stop_task(task)
811
+ end
812
+
813
+ h.add_subcmd(:edit) do
814
+ system(ENV['EDITOR'] || 'nvim', __FILE__)
815
+ end
816
+ end
817
+ end
818
+ end