hiiro 0.1.105.pre.1 → 0.1.105.pre.2

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hiiro/version.rb +1 -1
  3. metadata +1 -2
  4. data/plugins/tasks.rb +0 -1070
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02a9dfd205fe167b06ddca8e9166eef9fb0455e579e0f30aea8ebf2beebced8a
4
- data.tar.gz: f6a2b07f167e9d9ce5e1e57b008b2860cb7888dbb56d4f8602a0150a3aae243c
3
+ metadata.gz: ec412364c10d96baca34abfe06b466364e10bad8dd5ab79163282d8996631fee
4
+ data.tar.gz: 23961906f1965a27bdbb7250e7fe9c870bdf457e594fce98d7abbe81628442b1
5
5
  SHA512:
6
- metadata.gz: 8575faa796545313d705f3c850d94b922b6987b063ce8a645942a8ec94f19039719800f80cb360a1e2436cc4dc506f92f519eb73f7d70b1b9890593bf83f7270
7
- data.tar.gz: 827a12ffa83d83c837d46daf38d0be7ec46f2b5f92d6d6ab5fea62e45e9b6f9d854347bf516ddef2e98f2414c3706158693e4939895f607d753ea39c9c1e174a
6
+ metadata.gz: '08961fbea3c8a8a19e9c213bdc09779cf8781a47f772a2baefdb255b4173a483981404a931ac546df1ae0739e73efa89cddf981d9d6873091996d977a8b13846'
7
+ data.tar.gz: 789aa50c54f4839567610a56c34879938d581d51834e913070254931b82bd24c8b8edccb776d464e3bd963b7288fa7f3e945b9bc0e6a89878cc7afa4e981bcfb
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.105.pre.1"
2
+ VERSION = "0.1.105.pre.2"
3
3
  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.105.pre.1
4
+ version: 0.1.105.pre.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -141,7 +141,6 @@ files:
141
141
  - plugins/notify.rb
142
142
  - plugins/pins.rb
143
143
  - plugins/project.rb
144
- - plugins/tasks.rb
145
144
  - scripts/compare
146
145
  - scripts/install
147
146
  - scripts/publish
data/plugins/tasks.rb DELETED
@@ -1,1070 +0,0 @@
1
- require 'yaml'
2
- require 'fileutils'
3
-
4
- class TmuxSession
5
- attr_reader :name
6
-
7
- def self.current
8
- return nil unless ENV['TMUX']
9
-
10
- name = `tmux display-message -p '#S'`.chomp
11
- new(name)
12
- end
13
-
14
- def self.all
15
- output = `tmux list-sessions -F '#S' 2>/dev/null`
16
- output.lines(chomp: true).map { |name| new(name) }
17
- end
18
-
19
- def initialize(name)
20
- @name = name
21
- end
22
-
23
- def ==(other)
24
- other.is_a?(TmuxSession) && name == other.name
25
- end
26
-
27
- def to_s
28
- name
29
- end
30
- end
31
-
32
- class Tree
33
- attr_reader :path, :head, :branch
34
-
35
- def self.all(repo_path: Hiiro::REPO_PATH)
36
- git = Hiiro::Git.new(nil, repo_path)
37
- git.worktrees(repo_path: repo_path).filter_map do |wt|
38
- next if wt.bare?
39
- new(path: wt.path, head: wt.head, branch: wt.branch)
40
- end
41
- end
42
-
43
- def initialize(path:, head: nil, branch: nil)
44
- @path = path
45
- @head = head
46
- @branch = branch
47
- end
48
-
49
- def name
50
- @name ||= if path.start_with?(Hiiro::WORK_DIR + '/')
51
- path.sub(Hiiro::WORK_DIR + '/', '')
52
- else
53
- File.basename(path)
54
- end
55
- end
56
-
57
- def match?(pwd = Dir.pwd)
58
- pwd == path || pwd.start_with?(path + '/')
59
- end
60
-
61
- def detached?
62
- branch.nil?
63
- end
64
-
65
- def ==(other)
66
- other.is_a?(Tree) && path == other.path
67
- end
68
-
69
- def to_s
70
- name
71
- end
72
- end
73
-
74
- class Task
75
- attr_reader :name, :tree_name, :session_name
76
-
77
- def initialize(name:, tree: nil, session: nil, **_)
78
- @name = name
79
- @tree_name = tree
80
- @session_name = session || name
81
- end
82
-
83
- def parent_name
84
- return nil unless subtask?
85
- name.split('/').first
86
- end
87
-
88
- def short_name
89
- subtask? ? name.split('/', 2).last : name
90
- end
91
-
92
- def subtask?
93
- name.include?('/')
94
- end
95
-
96
- def top_level?
97
- !subtask?
98
- end
99
-
100
- def tree
101
- @tree ||= Environment.current&.find_tree(tree_name)
102
- end
103
-
104
- def branch
105
- tree&.branch
106
- end
107
-
108
- def ==(other)
109
- other.is_a?(Task) && name == other.name
110
- end
111
-
112
- def to_s
113
- name
114
- end
115
-
116
- def to_h
117
- h = { name: name }
118
- h[:tree] = tree_name if tree_name
119
- h[:session] = session_name if session_name != name
120
- h
121
- end
122
- end
123
-
124
- class App
125
- attr_reader :name, :relative_path
126
-
127
- def initialize(name:, path:)
128
- @name = name
129
- @relative_path = path
130
- end
131
-
132
- def resolve(tree_root)
133
- File.join(tree_root, relative_path)
134
- end
135
-
136
- def ==(other)
137
- other.is_a?(App) && name == other.name
138
- end
139
-
140
- def to_s
141
- name
142
- end
143
- end
144
-
145
- class Environment
146
- attr_reader :path
147
-
148
- def self.current
149
- new(path: Dir.pwd)
150
- end
151
-
152
- def initialize(path: Dir.pwd, config: nil)
153
- @path = path
154
- @config = config
155
- end
156
-
157
- def config
158
- @config ||= TaskManager::Config.new
159
- end
160
-
161
- def all_tasks
162
- @all_tasks ||= config.tasks
163
- end
164
-
165
- def all_sessions
166
- @all_sessions ||= TmuxSession.all
167
- end
168
-
169
- def all_trees
170
- @all_trees ||= Tree.all
171
- end
172
-
173
- def all_apps
174
- @all_apps ||= config.apps
175
- end
176
-
177
- def tree_matcher
178
- @tree_matcher ||= Hiiro::Matcher.new(all_trees, :name)
179
- end
180
-
181
- def session_matcher
182
- @session_matcher ||= Hiiro::Matcher.new(all_sessions, :name)
183
- end
184
-
185
- def app_matcher
186
- @app_matcher ||= Hiiro::Matcher.new(all_apps, :name)
187
- end
188
-
189
- def task_matcher
190
- @task_matcher ||= Hiiro::Matcher.new(all_tasks, :name)
191
- end
192
-
193
- def task
194
- @task ||= begin
195
- s = session
196
- t = tree
197
- all_tasks.find { |task|
198
- (s && task.session_name == s.name) ||
199
- (t && task.tree_name == t.name)
200
- }
201
- end
202
- end
203
-
204
- def session
205
- @session ||= TmuxSession.current
206
- end
207
-
208
- def tree
209
- @tree ||= all_trees.find { |t| t.match?(path) }
210
- end
211
-
212
- def find_task(abbreviated)
213
- return nil if abbreviated.nil?
214
-
215
- # Try path-based matching first (handles "parent/child" patterns)
216
- if abbreviated.include?('/')
217
- result = task_matcher.resolve_path(abbreviated)
218
- return result.resolved&.item if result.match?
219
-
220
- # "main" refers to the parent task itself
221
- parent_prefix, child_prefix = abbreviated.split('/', 2)
222
- if 'main'.start_with?(child_prefix)
223
- return task_matcher.find(parent_prefix).first&.item
224
- end
225
-
226
- nil
227
- else
228
- task_matcher.find(abbreviated).first&.item
229
- end
230
- end
231
-
232
- def find_tree(abbreviated)
233
- return nil if abbreviated.nil?
234
- tree_matcher.find(abbreviated).first&.item
235
- end
236
-
237
- def find_session(abbreviated)
238
- return nil if abbreviated.nil?
239
- session_matcher.find(abbreviated).first&.item
240
- end
241
-
242
- def find_app(abbreviated)
243
- return nil if abbreviated.nil?
244
- app_matcher.find(abbreviated).first&.item
245
- end
246
- end
247
-
248
- class TaskManager
249
- TASKS_DIR = File.join(Dir.home, '.config', 'hiiro', 'tasks')
250
- APPS_FILE = File.join(Dir.home, '.config', 'hiiro', 'apps.yml')
251
-
252
- attr_reader :hiiro, :scope, :environment
253
-
254
- def initialize(hiiro, scope: :task, environment: nil)
255
- @hiiro = hiiro
256
- @scope = scope
257
- @environment = environment || Environment.current
258
- end
259
-
260
- def config
261
- environment.config
262
- end
263
-
264
- # --- Scope-aware queries ---
265
-
266
- def tasks
267
- if scope == :subtask
268
- parent = current_parent_task
269
- return [] unless parent
270
- main_task = Task.new(name: "#{parent.name}/main", tree: parent.tree_name, session: parent.session_name)
271
- subtask_list = environment.all_tasks.select { |t| t.parent_name == parent.name }
272
- [main_task, *subtask_list]
273
- else
274
- environment.all_tasks.select(&:top_level?)
275
- end
276
- end
277
-
278
- def subtasks(task)
279
- environment.all_tasks.select { |t| t.parent_name == task.name }
280
- end
281
-
282
- def task_by_name(name)
283
- return slash_lookup(name) if name.include?('/')
284
-
285
- key = (scope == :subtask) ? :short_name : :name
286
- Hiiro::Matcher.new(tasks, key).by_prefix(name).first&.item
287
- end
288
-
289
- def task_by_tree(tree_name)
290
- environment.task_matcher.resolve(tree_name, :tree_name).resolved&.item
291
- end
292
-
293
- def task_by_session(session_name)
294
- environment.task_matcher.resolve(session_name, :session_name).resolved&.item
295
- end
296
-
297
- def current_task
298
- environment.task
299
- end
300
-
301
- def current_session
302
- environment.session
303
- end
304
-
305
- def current_tree
306
- environment.tree
307
- end
308
-
309
- # --- Actions ---
310
-
311
- def start_task(name, app_name: nil)
312
- existing = task_by_name(name)
313
- if existing
314
- puts "Task '#{existing.name}' already exists. Switching..."
315
- switch_to_task(existing, app_name: app_name)
316
- return
317
- end
318
-
319
- task_name = scope == :subtask ? "#{current_parent_task.name}/#{name}" : name
320
- subtree_name = scope == :subtask ? "#{current_parent_task.name}/#{name}" : "#{name}/main"
321
-
322
- target_path = File.join(Hiiro::WORK_DIR, subtree_name)
323
-
324
- git = Hiiro::Git.new(nil, Hiiro::REPO_PATH)
325
- available = find_available_tree
326
- if available
327
- puts "Renaming worktree '#{available.name}' to '#{subtree_name}'..."
328
- FileUtils.mkdir_p(File.dirname(target_path))
329
- unless git.move_worktree(available.path, target_path, repo_path: Hiiro::REPO_PATH)
330
- puts "ERROR: Failed to rename worktree"
331
- return
332
- end
333
- else
334
- puts "Creating new worktree '#{subtree_name}'..."
335
- FileUtils.mkdir_p(File.dirname(target_path))
336
- unless git.add_worktree_detached(target_path, repo_path: Hiiro::REPO_PATH)
337
- puts "ERROR: Failed to create worktree"
338
- return
339
- end
340
- end
341
-
342
- session_name = task_name
343
- task = Task.new(name: task_name, tree: subtree_name, session: session_name)
344
- config.save_task(task)
345
-
346
- base_dir = target_path
347
- if app_name
348
- app = environment.find_app(app_name)
349
- base_dir = app.resolve(target_path) if app
350
- end
351
-
352
- Dir.chdir(base_dir)
353
- hiiro.start_tmux_session(session_name)
354
-
355
- puts "Started task '#{task_name}' in worktree '#{subtree_name}'"
356
- end
357
-
358
- def switch_to_task(task, app_name: nil)
359
- unless task
360
- puts "Task not found"
361
- return
362
- end
363
-
364
- tree = environment.find_tree(task.tree_name)
365
- tree_path = tree ? tree.path : File.join(Hiiro::WORK_DIR, task.tree_name)
366
-
367
- session_name = task.session_name
368
- session_exists = system('tmux', 'has-session', '-t', session_name, err: File::NULL)
369
-
370
- if session_exists
371
- hiiro.start_tmux_session(session_name)
372
- else
373
- base_dir = tree_path
374
- if app_name
375
- app = environment.find_app(app_name)
376
- base_dir = app.resolve(tree_path) if app
377
- end
378
-
379
- if Dir.exist?(base_dir)
380
- Dir.chdir(base_dir)
381
- hiiro.start_tmux_session(session_name)
382
- else
383
- puts "ERROR: Path '#{base_dir}' does not exist"
384
- return
385
- end
386
- end
387
-
388
- puts "Switched to '#{task.name}'"
389
- end
390
-
391
- def stop_task(task)
392
- unless task
393
- puts "Task not found"
394
- return
395
- end
396
-
397
- config.remove_task(task.name)
398
- # Also remove any subtasks
399
- subtasks(task).each { |st| config.remove_task(st.name) }
400
-
401
- puts "Stopped task '#{task.name}' (worktree available for reuse)"
402
- end
403
-
404
- def list
405
- items = tasks
406
- if items.empty?
407
- puts scope == :subtask ? "No subtasks found" : "No tasks found"
408
- puts "Use 'h #{scope} start NAME' to create one."
409
- return
410
- end
411
-
412
- current = current_task
413
- label = scope == :subtask ? "Subtasks" : "Tasks"
414
- if scope == :subtask && current
415
- parent = current_parent_task
416
- label = "Subtasks of '#{parent&.name}'" if parent
417
- end
418
-
419
- puts "#{label}:"
420
- puts
421
-
422
- items.each do |task|
423
- marker = (current && current.name == task.name) ? "*" : " "
424
- tree = environment.find_tree(task.tree_name)
425
- branch = tree&.branch || (tree&.detached? ? '(detached)' : nil)
426
- branch_str = branch ? " [#{branch}]" : ""
427
-
428
- display_name = scope == :subtask ? task.short_name : task.name
429
- puts format("%s %-25s tree: %-20s%s", marker, display_name, task.tree_name || '(none)', branch_str)
430
-
431
- # Show subtasks under parent
432
- if scope == :task
433
- subs = subtasks(task)
434
- subs.each do |st|
435
- sub_marker = (current && current.name == st.name) ? "*" : " "
436
- sub_tree = environment.find_tree(st.tree_name)
437
- sub_branch = sub_tree&.branch || (sub_tree&.detached? ? '(detached)' : nil)
438
- sub_branch_str = sub_branch ? " [#{sub_branch}]" : ""
439
- puts format("%s - %-23s tree: %-20s%s", sub_marker, st.short_name, st.tree_name || '(none)', sub_branch_str)
440
- end
441
- end
442
- end
443
-
444
- available = environment.all_trees.reject { |t|
445
- environment.all_tasks.any? { |task| task.tree_name == t.name }
446
- }
447
-
448
- if available.any?
449
- puts
450
- available.each do |tree|
451
- branch_str = tree.branch ? " [#{tree.branch}]" : tree.detached? ? " [(detached)]" : ""
452
- puts format(" %-25s (available)%s", tree.name, branch_str)
453
- end
454
- end
455
- end
456
-
457
- def status
458
- task = current_task
459
- unless task
460
- puts "Not currently in a task session"
461
- return
462
- end
463
-
464
- puts "Task: #{task.name}"
465
- puts "Worktree: #{task.tree_name}"
466
- tree = environment.find_tree(task.tree_name)
467
- puts "Path: #{tree&.path || '(unknown)'}"
468
- puts "Session: #{task.session_name}"
469
- puts "Parent: #{task.parent_name}" if task.subtask?
470
- end
471
-
472
- def save
473
- task = current_task
474
- unless task
475
- puts "ERROR: Not currently in a task session"
476
- return
477
- end
478
-
479
- windows = capture_tmux_windows(task.session_name)
480
- puts "Saved task '#{task.name}' state (#{windows.count} windows)"
481
- end
482
-
483
- def open_app(app_name)
484
- task = current_task
485
- unless task
486
- puts "ERROR: Not currently in a task session"
487
- return
488
- end
489
-
490
- result = resolve_app(app_name, task)
491
- return unless result
492
-
493
- resolved_name, app_path = result
494
- system('tmux', 'new-window', '-n', resolved_name, '-c', app_path)
495
- puts "Opened '#{resolved_name}' in new window (#{app_path})"
496
- end
497
-
498
- def list_apps
499
- apps = environment.all_apps
500
- if apps.any?
501
- puts "Configured apps:"
502
- puts
503
- apps.each do |app|
504
- puts format(" %-20s => %s", app.name, app.relative_path)
505
- end
506
- else
507
- puts "No apps configured."
508
- puts "Create #{APPS_FILE} with format:"
509
- puts " app_name: relative/path/from/repo"
510
- end
511
- end
512
-
513
- def branch(task_name = nil)
514
- branch_name = nil
515
-
516
- if task_name.nil?
517
- branch = select_branch_interactive
518
- return unless branch
519
- print branch
520
- return
521
- end
522
-
523
- task = task_by_name(task_name)
524
- unless task
525
- puts "Task not found: #{task_name}"
526
- return
527
- end
528
-
529
- if task.branch
530
- print task.branch
531
- elsif task.tree&.detached?
532
- puts "(detached HEAD)"
533
- else
534
- puts "(no branch)"
535
- end
536
- end
537
-
538
- def cd_to_task(task)
539
- unless task
540
- puts "Task not found"
541
- return
542
- end
543
-
544
- tree = environment.find_tree(task.tree_name)
545
- path = tree ? tree.path : File.join(Hiiro::WORK_DIR, task.tree_name)
546
- send_cd(path)
547
- end
548
-
549
- def cd_to_app(app_name = nil)
550
- task = current_task
551
- unless task
552
- puts "ERROR: Not currently in a task session"
553
- return
554
- end
555
-
556
- if app_name.nil? || app_name.empty?
557
- tree = environment.find_tree(task.tree_name)
558
- send_cd(tree&.path || File.join(Hiiro::WORK_DIR, task.tree_name))
559
- return
560
- end
561
-
562
- result = resolve_app(app_name, task)
563
- return unless result
564
-
565
- _resolved_name, app_path = result
566
- send_cd(app_path)
567
- end
568
-
569
- def app_path(app_name = nil)
570
- task = current_task
571
- tree_root = if task
572
- tree = environment.find_tree(task.tree_name)
573
- tree&.path || File.join(Hiiro::WORK_DIR, task.tree_name)
574
- else
575
- Hiiro::Git.new(nil, Dir.pwd).root
576
- end
577
-
578
- if app_name.nil?
579
- print tree_root
580
- return
581
- end
582
-
583
- result = environment.app_matcher.find_all(app_name)
584
-
585
- case result.count
586
- when 0
587
- puts "ERROR: No matches found"
588
- puts
589
- puts "Possible Apps:"
590
- environment.all_apps.each { |a| puts format(" %-20s => %s", a.name, a.relative_path) }
591
- when 1
592
- print result.first.item.resolve(tree_root)
593
- else
594
- puts "Multiple matches found:"
595
- result.matches.each { |m| puts format(" %-20s => %s", m.item.name, m.item.relative_path) }
596
- end
597
- end
598
-
599
- def DELETEME_help
600
- scope_name = scope.to_s
601
- puts "Usage: h #{scope_name} <subcommand> [args]"
602
- puts
603
- puts "Subcommands:"
604
- puts " list, ls List #{scope_name}s"
605
- puts " start NAME [APP] Start a new #{scope_name}"
606
- puts " switch [NAME] Switch to a #{scope_name} (interactive if no name)"
607
- puts " branch [NAME] Print git branch for a #{scope_name} (interactive if no name)"
608
- puts " app [APP_NAME] Open app in new tmux window (interactive if no name)"
609
- puts " apps List configured apps"
610
- puts " cd [APP_NAME] Change directory to app"
611
- puts " path [APP_NAME] Print app path"
612
- puts " status, st Show current #{scope_name} status"
613
- puts " save Save current session state"
614
- puts " stop [NAME] Stop a #{scope_name} (interactive if no name)"
615
- end
616
-
617
- # --- Interactive selection with sk ---
618
-
619
- def select_task_interactive(prompt = nil)
620
- names = if scope == :subtask
621
- tasks.map(&:short_name)
622
- else
623
- environment.all_tasks.map(&:name)
624
- end
625
- return nil if names.empty?
626
-
627
- hiiro.fuzzyfind(names.sort)
628
- end
629
-
630
- def value_for_task(task_name = nil, &block)
631
- if task_name
632
- task = task_by_name(task_name)
633
- return block.call(task) if task
634
- end
635
-
636
- tasks = scope == :subtask ? tasks.sort_by(&:short_name) : environment.all_tasks.sort_by(&:name)
637
-
638
- mapping = tasks.each_with_object({}) do |task,h|
639
- name = scope == :subtask ? task.short_name : task.name
640
- val = block.call(task)&.to_s
641
-
642
- line = format("%-25s | %s", name, val)
643
- h[line] = val
644
- end
645
-
646
- hiiro.fuzzyfind_from_map(mapping)
647
- end
648
-
649
- def select_branch_interactive(prompt = nil)
650
- name_map = if scope == :subtask
651
- tasks.sort_by(&:short_name).each_with_object({}) { |t,h| h[format('%-25s | %s', t.short_name, t.branch)] = t.branch }
652
- else
653
- environment.all_tasks.sort_by(&:name).each_with_object({}) { |t,h| h[format('%-25s | %s', t.name, t.branch)] = t.branch }
654
- end
655
- return nil if name_map.empty?
656
-
657
- hiiro.fuzzyfind_from_map(name_map)
658
- end
659
-
660
- # --- Private helpers ---
661
-
662
- private
663
-
664
- def slash_lookup(input)
665
- environment.find_task(input)
666
- end
667
-
668
- def current_parent_task
669
- task = current_task
670
- return nil unless task
671
-
672
- if task.subtask?
673
- environment.find_task(task.parent_name)
674
- else
675
- task
676
- end
677
- end
678
-
679
- def find_available_tree
680
- assigned_tree_names = environment.all_tasks.map(&:tree_name)
681
- environment.all_trees.find { |tree| !assigned_tree_names.include?(tree.name) }
682
- end
683
-
684
- def resolve_app(app_name, task)
685
- tree = environment.find_tree(task.tree_name)
686
- tree_root = tree ? tree.path : File.join(Hiiro::WORK_DIR, task.tree_name)
687
-
688
- result = environment.app_matcher.find_all(app_name)
689
-
690
- case result.count
691
- when 0
692
- # Fallback: directory discovery
693
- exact = File.join(tree_root, app_name)
694
- return [app_name, exact] if Dir.exist?(exact)
695
-
696
- nested = File.join(tree_root, app_name, app_name)
697
- return [app_name, nested] if Dir.exist?(nested)
698
-
699
- puts "ERROR: App '#{app_name}' not found"
700
- list_apps
701
- nil
702
- when 1
703
- app = result.first.item
704
- [app.name, app.resolve(tree_root)]
705
- else
706
- exact = result.matches.find { |m| m.item.name == app_name }
707
- if exact
708
- [exact.item.name, exact.item.resolve(tree_root)]
709
- else
710
- puts "ERROR: '#{app_name}' matches multiple apps:"
711
- result.matches.each { |m| puts " #{m.item.name}" }
712
- nil
713
- end
714
- end
715
- end
716
-
717
- def send_cd(path)
718
- pane = ENV['TMUX_PANE']
719
- if pane
720
- system('tmux', 'send-keys', '-t', pane, "cd #{path}\n")
721
- else
722
- system('tmux', 'send-keys', "cd #{path}\n")
723
- end
724
- end
725
-
726
- def capture_tmux_windows(session)
727
- output = `tmux list-windows -t #{session} -F '\#{window_index}:\#{window_name}:\#{pane_current_path}' 2>/dev/null`
728
- output.lines.map(&:strip).map { |line|
729
- idx, name, path = line.split(':')
730
- { 'index' => idx, 'name' => name, 'path' => path }
731
- }
732
- end
733
-
734
- def sk_select(items)
735
- hiiro.fuzzyfind(items) # Hiiro::Fuzzyfind.select(items)
736
- end
737
-
738
- class Config
739
- attr_reader :tasks_file, :apps_file
740
-
741
- def initialize(tasks_file: nil, apps_file: nil)
742
- @tasks_file = tasks_file || File.join(TASKS_DIR, 'tasks.yml')
743
- @apps_file = apps_file || APPS_FILE
744
- end
745
-
746
- def tasks
747
- data = load_tasks
748
- (data['tasks'] || []).map { |h| Task.new(**h.transform_keys(&:to_sym)) }
749
- end
750
-
751
- def apps
752
- return [] unless File.exist?(apps_file)
753
- data = YAML.safe_load_file(apps_file) || {}
754
- data.map { |name, path| App.new(name: name, path: path) }
755
- end
756
-
757
- def save_task(task)
758
- data = load_tasks
759
- data['tasks'] ||= []
760
- data['tasks'].reject! { |t| t['name'] == task.name }
761
- data['tasks'] << task.to_h.transform_keys(&:to_s)
762
- save_tasks(data)
763
- end
764
-
765
- def remove_task(name)
766
- data = load_tasks
767
- data['tasks'] ||= []
768
- data['tasks'].reject! { |t| t['name'] == name }
769
- save_tasks(data)
770
- end
771
-
772
- private
773
-
774
- def load_tasks
775
- if File.exist?(tasks_file)
776
- return YAML.safe_load_file(tasks_file) || { 'tasks' => [] }
777
- end
778
-
779
- # Load from individual task_*.yml files
780
- task_files = Dir.glob(File.join(File.dirname(tasks_file), 'task_*.yml'))
781
- if task_files.any?
782
- tasks = task_files.map do |file|
783
- short_name = File.basename(file, '.yml').sub(/^task_/, '')
784
- data = YAML.safe_load_file(file) || {}
785
- # Support parent key for subtasks, or infer from tree path
786
- parent = data['parent']
787
- if parent.nil? && data['tree']&.include?('/')
788
- parent = data['tree'].split('/').first
789
- end
790
- name = parent ? "#{parent}/#{short_name}" : short_name
791
- h = { 'name' => name }
792
- h['tree'] = data['tree'] if data['tree']
793
- h['session'] = data['session'] if data['session']
794
- h
795
- end
796
- return { 'tasks' => tasks }
797
- end
798
-
799
- assignments_file = File.join(File.dirname(tasks_file), 'assignments.yml')
800
- if File.exist?(assignments_file)
801
- raw = YAML.safe_load_file(assignments_file) || {}
802
- tasks = raw.map do |tree_path, task_name|
803
- h = { 'name' => task_name, 'tree' => tree_path }
804
- h['session'] = task_name if task_name.include?('/')
805
- h
806
- end
807
- data = { 'tasks' => tasks }
808
- save_tasks(data)
809
- return data
810
- end
811
-
812
- { 'tasks' => [] }
813
- end
814
-
815
- def save_tasks(data)
816
- FileUtils.mkdir_p(File.dirname(tasks_file))
817
- File.write(tasks_file, YAML.dump(data))
818
- end
819
- end
820
- end
821
-
822
- module Tasks
823
- def self.load(hiiro)
824
- # hiiro.load_plugin(Tmux)
825
- attach_methods(hiiro)
826
- add_subcommands(hiiro)
827
- end
828
-
829
- def self.attach_methods(hiiro, task_manager=nil)
830
- environment = Environment.current
831
-
832
- hiiro.attach_method(:environment) { environment }
833
- hiiro.attach_method(:env) { environment }
834
- hiiro.attach_method(:task_manager) { task_manager }
835
- hiiro.attach_method(:tm) { task_manager }
836
-
837
- hiiro
838
- end
839
-
840
- def self.add_subcommands(hiiro)
841
- hiiro.add_subcmd(:task) do |*args|
842
- tm = TaskManager.new(hiiro, scope: :task)
843
- build_hiiro(hiiro, tm).run
844
- end
845
-
846
- hiiro.add_subcmd(:subtask) do |*args|
847
- tm = TaskManager.new(hiiro, scope: :subtask)
848
- build_hiiro(hiiro, tm).run
849
- end
850
- end
851
-
852
- def self.build_hiiro(parent_hiiro, tm)
853
- bin_name = [parent_hiiro.bin, parent_hiiro.subcmd || ''].join('-')
854
-
855
- task_hiiro = Hiiro.init(
856
- bin_name:,
857
- args: parent_hiiro.args,
858
- ) do |h|
859
- h.add_subcmd(:list) { tm.list }
860
- h.add_subcmd(:ls) { tm.list }
861
-
862
- h.add_subcmd(:start) do |task_name, app_name=nil|
863
- tm.start_task(task_name, app_name: app_name)
864
- end
865
-
866
- h.add_subcmd(:switch) do |task_name=nil, app_name=nil|
867
- if task_name.nil?
868
- task_name = tm.select_task_interactive
869
- next unless task_name
870
- end
871
- task = tm.task_by_name(task_name)
872
- tm.switch_to_task(task, app_name: app_name)
873
- end
874
-
875
- h.add_subcmd(:app) do |app_name=nil|
876
- if app_name.nil?
877
- names = tm.environment.all_apps.map(&:name)
878
- app_name = h.fuzzyfind(names)
879
- next unless app_name
880
- end
881
- tm.open_app(app_name)
882
- end
883
-
884
- h.add_subcmd(:apps) { tm.list_apps }
885
-
886
- h.add_subcmd(:cd) do |app_name=nil|
887
- tm.cd_to_app(app_name)
888
- end
889
-
890
- h.add_subcmd(:path) do |app_name=nil|
891
- tm.app_path(app_name)
892
- end
893
-
894
- h.add_subcmd(:branch) do |task_name=nil|
895
- print tm.value_for_task(task_name, &:branch)
896
- end
897
-
898
- h.add_subcmd(:tree) do |task_name=nil|
899
- print tm.value_for_task(task_name, &:tree_name)
900
- end
901
-
902
- h.add_subcmd(:session) do |task_name=nil|
903
- print tm.value_for_task(task_name, &:session_name)
904
- end
905
-
906
- h.add_subcmd(:current) do
907
- task = tm.current_task
908
- next STDERR.puts("Not in a task") unless task
909
- print task.name
910
- end
911
-
912
- h.add_subcmd(:cbranch) do
913
- task = tm.current_task
914
- next STDERR.puts("Not in a task") unless task
915
- print task.branch if task.branch
916
- end
917
-
918
- h.add_subcmd(:ctree) do
919
- task = tm.current_task
920
- next STDERR.puts("Not in a task") unless task
921
- print task.tree_name if task.tree_name
922
- end
923
-
924
- h.add_subcmd(:csession) do
925
- task = tm.current_task
926
- next STDERR.puts("Not in a task") unless task
927
- print task.session_name if task.session_name
928
- end
929
-
930
- h.add_subcmd(:status) { tm.status }
931
- h.add_subcmd(:st) { tm.status }
932
-
933
- h.add_subcmd(:save) { tm.save }
934
-
935
- h.add_subcmd(:stop) do |task_name=nil|
936
- if task_name.nil?
937
- task_name = tm.select_task_interactive
938
- next unless task_name
939
- end
940
- task = tm.task_by_name(task_name)
941
- tm.stop_task(task)
942
- end
943
-
944
- h.add_subcmd(:edit) do
945
- system(ENV['EDITOR'] || 'nvim', __FILE__)
946
- end
947
-
948
- h.add_subcmd(:todo) do |*todo_args|
949
- todo_manager = Hiiro::TodoManager.new
950
- task = tm.current_task
951
-
952
- task_info = if task
953
- {
954
- task_name: task.subtask? ? task.parent_name : task.name,
955
- subtask_name: task.subtask? ? task.short_name : nil,
956
- tree: task.tree_name,
957
- branch: task.branch,
958
- session: task.session_name
959
- }
960
- end
961
-
962
- todo_subcmd = todo_args.shift
963
- case todo_subcmd
964
- when 'ls', 'list', nil
965
- show_all = todo_args.delete('-a') || todo_args.delete('--all')
966
- # Default to filtering by current task unless -a or --all is used
967
- items = if show_all
968
- todo_manager.all
969
- elsif task
970
- todo_manager.filter_by_task(task.name).select { |i| %w[not_started started].include?(i.status) }
971
- else
972
- todo_manager.active
973
- end
974
-
975
- if items.empty?
976
- puts task ? "No todo items for task '#{task.name}'." : "No todo items found."
977
- else
978
- puts todo_manager.list(items)
979
- end
980
-
981
- when 'add'
982
- if todo_args.empty?
983
- new_items = todo_manager.edit_items(task_info: task_info)
984
- if new_items.empty?
985
- puts "No items added."
986
- next
987
- end
988
- todo_manager.add_items(new_items)
989
- if new_items.length == 1
990
- puts "Added: #{todo_manager.format_item(new_items.first)}"
991
- else
992
- puts "Added #{new_items.length} items:"
993
- new_items.each { |item| puts " #{todo_manager.format_item(item)}" }
994
- end
995
- next
996
- end
997
-
998
- tags = nil
999
- text_parts = []
1000
- while todo_args.any?
1001
- arg = todo_args.shift
1002
- case arg
1003
- when '-t', '--tags'
1004
- tags = todo_args.shift
1005
- else
1006
- text_parts << arg
1007
- end
1008
- end
1009
- text = text_parts.join(' ')
1010
- item = todo_manager.add(text, tags: tags, task_info: task_info)
1011
- puts "Added: #{todo_manager.format_item(item)}"
1012
-
1013
- when 'rm', 'remove'
1014
- id_or_index = todo_args.shift
1015
- unless id_or_index
1016
- puts "Usage: h #{tm.scope} todo rm <id|index>"
1017
- next
1018
- end
1019
- item = todo_manager.remove(id_or_index)
1020
- puts item ? "Removed: #{item.text}" : "Item not found: #{id_or_index}"
1021
-
1022
- when 'start'
1023
- id_or_index = todo_args.shift
1024
- unless id_or_index
1025
- puts "Usage: h #{tm.scope} todo start <id|index>"
1026
- next
1027
- end
1028
- item = todo_manager.start(id_or_index)
1029
- puts item ? "Started: #{todo_manager.format_item(item)}" : "Item not found: #{id_or_index}"
1030
-
1031
- when 'done'
1032
- id_or_index = todo_args.shift
1033
- unless id_or_index
1034
- puts "Usage: h #{tm.scope} todo done <id|index>"
1035
- next
1036
- end
1037
- item = todo_manager.done(id_or_index)
1038
- puts item ? "Done: #{todo_manager.format_item(item)}" : "Item not found: #{id_or_index}"
1039
-
1040
- when 'skip'
1041
- id_or_index = todo_args.shift
1042
- unless id_or_index
1043
- puts "Usage: h #{tm.scope} todo skip <id|index>"
1044
- next
1045
- end
1046
- item = todo_manager.skip(id_or_index)
1047
- puts item ? "Skipped: #{todo_manager.format_item(item)}" : "Item not found: #{id_or_index}"
1048
-
1049
- when 'search'
1050
- query = todo_args.join(' ')
1051
- if query.empty?
1052
- puts "Usage: h #{tm.scope} todo search <query>"
1053
- next
1054
- end
1055
- items = todo_manager.search(query)
1056
- if items.empty?
1057
- puts "No items matching: #{query}"
1058
- else
1059
- puts todo_manager.list(items)
1060
- end
1061
-
1062
- else
1063
- puts "Usage: h #{tm.scope} todo <ls|add|rm|start|done|skip|search> [args]"
1064
- end
1065
- end
1066
- end
1067
-
1068
- attach_methods task_hiiro, tm
1069
- end
1070
- end