hiiro 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a553e1c19ac099d93b9232da28a1953a83b7c2a379c8ddb05475a43aa58e31fb
4
- data.tar.gz: 33d06064609e82b1d1da5e7ec83ecfcd90d49cac93cc6a7b8cd3930ab4e48a57
3
+ metadata.gz: da34d25bbe0de638c8339cbaa555791e47db42d4f601bca3f012afda6581eb23
4
+ data.tar.gz: 67021d56522d24752d79285a30ee358b701bd21ec2f090bcb83cfbacd765810d
5
5
  SHA512:
6
- metadata.gz: 4418cdbaa9056006099e9ec9e82eccc85c625ff380d4e92f93f1db6e4bc1b48a0b5617566c3c8d36c308b7db1c6f9caac4e9f596ac56663d5499be05bac1ce4b
7
- data.tar.gz: bf153421fbf79ad299203ba871b9907521dd2674f554efb34619a90c84dfb4ab2b0c56c2d3b32cdab2820cd35efa94e5e5b0baa2d24dbf7619cd7181e98323bd
6
+ metadata.gz: 9e899408d73a42a03cfaa320963e64f698f0954745989d8bc5f20b3d2c7316b93830b9c38172a86b5746e1f6e171e2f5a80edf789eec0b4d21cbe6721b405776
7
+ data.tar.gz: b63b6c5643710b7217fa472480cc036df5eb2034c270c049fd6c0736aa49f9382b858a69f5b53f12ffa3b50702a07ce5a6655cce7203ae085bb7e81336aca076
data/README.md CHANGED
@@ -13,6 +13,23 @@ See [docs/](docs/) for detailed documentation on all subcommands.
13
13
 
14
14
  ## Installation
15
15
 
16
+ ### Via RubyGems
17
+
18
+ ```sh
19
+ gem install hiiro
20
+
21
+ # Install plugins and subcommands
22
+ hiiro setup
23
+ ```
24
+
25
+ This installs:
26
+ - Plugins to `~/.config/hiiro/plugins/`
27
+ - Subcommands (`h-video`, `h-buffer`, etc.) to `~/bin/`
28
+
29
+ Ensure `~/bin` is in your `$PATH`.
30
+
31
+ ### Manual Installation
32
+
16
33
  ```sh
17
34
  # Copy the main script
18
35
  cp bin/h ~/bin/h
@@ -26,12 +43,9 @@ mkdir -p ~/.config/hiiro/plugins
26
43
  cp plugins/*.rb ~/.config/hiiro/plugins/
27
44
  ```
28
45
 
29
- Ensure `~/bin` is in your `$PATH`.
30
-
31
46
  ### Dependencies
32
47
 
33
48
  ```sh
34
- gem install pry
35
49
  # For notify plugin (macOS)
36
50
  brew install terminal-notifier
37
51
  ```
data/bin/h-session CHANGED
@@ -13,11 +13,11 @@ o.add_subcmd(:new) { |*args|
13
13
  }
14
14
 
15
15
  o.add_subcmd(:kill) { |*args|
16
- system('tmux', 'kill-session', *args)
16
+ system('tmux', 'kill-session', '-t', *args)
17
17
  }
18
18
 
19
19
  o.add_subcmd(:attach) { |*args|
20
- system('tmux', 'attach-session', *args)
20
+ system('tmux', 'attach-session', '-t', *args)
21
21
  }
22
22
 
23
23
  o.add_subcmd(:rename) { |*args|
@@ -25,7 +25,7 @@ o.add_subcmd(:rename) { |*args|
25
25
  }
26
26
 
27
27
  o.add_subcmd(:switch) { |*args|
28
- system('tmux', 'switch-client', *args)
28
+ system('tmux', 'switch-client', '-t', *args)
29
29
  }
30
30
 
31
31
  o.add_subcmd(:detach) { |*args|
data/bin/h-task ADDED
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "hiiro"
4
+ require "fileutils"
5
+ require "yaml"
6
+
7
+ Hiiro.load_env
8
+ hiiro = Hiiro.init(*ARGV, plugins: [Tmux])
9
+
10
+ class TaskManager
11
+ attr_reader :hiiro
12
+
13
+ def initialize(hiiro)
14
+ @hiiro = hiiro
15
+ end
16
+
17
+ def help
18
+ puts "Usage: h task <subcommand> [args]"
19
+ puts
20
+ puts "Subcommands:"
21
+ puts " list, ls List all worktrees and their active tasks"
22
+ puts " start TASK [APP] Start a task (reuses available worktree or creates new)"
23
+ puts " switch TASK Switch to an existing task"
24
+ puts " app APP_NAME Open a tmux window for an app in current worktree"
25
+ puts " apps List configured apps from apps.yml"
26
+ puts " save Save current tmux session info for this task"
27
+ puts " status, st Show current task status"
28
+ puts " stop Stop working on current task (worktree becomes available)"
29
+ end
30
+
31
+ # List all worktrees and their active tasks
32
+ def list_trees
33
+ puts "Git worktrees:"
34
+ puts
35
+
36
+ if trees.empty?
37
+ puts " (no worktrees found)"
38
+ puts
39
+ puts " Start a task with 'h task start TASK_NAME' to create one."
40
+ return
41
+ end
42
+
43
+ current = current_task
44
+ active, available = trees.partition { |tree_name| task_for_tree(tree_name) }
45
+
46
+ active.each do |tree_name|
47
+ task = task_for_tree(tree_name)
48
+ marker = (current && current[:tree] == tree_name) ? "*" : " "
49
+ puts format("%s %-20s => %s", marker, tree_name, task)
50
+ end
51
+
52
+ puts if active.any? && available.any?
53
+
54
+ available.each do |tree_name|
55
+ puts format(" %-20s (available)", tree_name)
56
+ end
57
+ end
58
+
59
+ # Start working on a task
60
+ def start_task(task_name, app: nil)
61
+ # Check if task already exists as a worktree
62
+ existing_tree = tree_for_task(task_name)
63
+ if existing_tree
64
+ puts "Task '#{task_name}' already active in tree '#{existing_tree}'"
65
+ puts "Switching to existing session..."
66
+ switch_to_task(task_name, existing_tree, app: app)
67
+ return true
68
+ end
69
+
70
+ # Find an available worktree to reuse, or create a new one
71
+ available_tree = find_available_tree
72
+
73
+ if available_tree
74
+ # Rename the available worktree to the task name
75
+ old_path = tree_path(available_tree)
76
+ new_path = File.join(File.dirname(old_path), task_name)
77
+
78
+ if available_tree != task_name
79
+ puts "Renaming worktree '#{available_tree}' to '#{task_name}'..."
80
+ result = system('git', '-C', main_repo_path, 'worktree', 'move', old_path, new_path)
81
+ unless result
82
+ puts "ERROR: Failed to rename worktree"
83
+ return false
84
+ end
85
+ clear_worktree_cache
86
+ end
87
+
88
+ final_tree_name = task_name
89
+ final_tree_path = new_path
90
+ else
91
+ # No available worktree, create a new one
92
+ puts "Creating new worktree for '#{task_name}'..."
93
+ new_path = File.join(Dir.home, 'work', task_name)
94
+
95
+ # Create worktree from main branch (detached to avoid branch conflicts)
96
+ result = system('git', '-C', main_repo_path, 'worktree', 'add', '--detach', new_path)
97
+ unless result
98
+ puts "ERROR: Failed to create worktree"
99
+ return false
100
+ end
101
+ clear_worktree_cache
102
+
103
+ final_tree_name = task_name
104
+ final_tree_path = new_path
105
+ end
106
+
107
+ # Associate task with tree
108
+ assign_task_to_tree(task_name, final_tree_name)
109
+
110
+ # Create/switch to tmux session
111
+ session_name = session_name_for(task_name)
112
+
113
+ # Determine base path (app path if specified, otherwise tree root)
114
+ base_path = final_tree_path
115
+ if app
116
+ result = find_app_path(final_tree_name, app)
117
+ case result
118
+ in nil
119
+ puts "WARNING: App '#{app}' not found, using tree root"
120
+ in [:ambiguous, matches]
121
+ puts "WARNING: '#{app}' matches multiple apps: #{matches.join(', ')}"
122
+ puts "Using tree root instead"
123
+ in [resolved_name, app_path]
124
+ base_path = app_path
125
+ puts "Using app '#{resolved_name}' as base path"
126
+ end
127
+ end
128
+
129
+ Dir.chdir(base_path)
130
+ hiiro.start_tmux_session(session_name)
131
+
132
+ save_task_metadata(task_name, tree: final_tree_name, session: session_name, app: app)
133
+
134
+ puts "Started task '#{task_name}' in worktree '#{final_tree_name}'"
135
+ true
136
+ end
137
+
138
+ # Start working on a task
139
+ def switch_task(task_name)
140
+ tree, task = assignments.find { |tree, task| task.start_with?(task_name) } || []
141
+
142
+ unless task
143
+ puts "No task matching #{task_name} found."
144
+ return false
145
+ end
146
+
147
+ switch_to_task(task, tree)
148
+
149
+ puts "Started task '#{task}' in tree '#{tree}'"
150
+ true
151
+ end
152
+
153
+ # Open an app window within the current tree
154
+ def open_app(app_name)
155
+ current = current_task
156
+ unless current
157
+ puts "ERROR: Not currently in a task session"
158
+ puts "Use 'h task start TASK_NAME' first"
159
+ return false
160
+ end
161
+
162
+ tree = current[:tree]
163
+ result = find_app_path(tree, app_name)
164
+
165
+ case result
166
+ in nil
167
+ puts "ERROR: App '#{app_name}' not found"
168
+ puts
169
+ list_apps(tree)
170
+ return false
171
+ in [:ambiguous, matches]
172
+ puts "ERROR: '#{app_name}' matches multiple apps:"
173
+ matches.each { |m| puts " #{m}" }
174
+ puts
175
+ puts "Be more specific."
176
+ return false
177
+ in [resolved_name, app_path]
178
+ # Create new tmux window with app directory as base
179
+ system('tmux', 'new-window', '-n', resolved_name, '-c', app_path)
180
+ puts "Opened '#{resolved_name}' in new window (#{app_path})"
181
+ true
182
+ end
183
+ end
184
+
185
+ # Open an app window within the current tree
186
+ def cd_app(app_name=nil)
187
+ current = current_task
188
+ unless current
189
+ puts "ERROR: Not currently in a task session"
190
+ puts "Use 'h task start TASK_NAME' first"
191
+ return false
192
+ end
193
+
194
+ tree = current[:tree]
195
+
196
+ result = []
197
+ if app_name.to_s == ''
198
+ result = ['root', tree_path(tree)]
199
+ else
200
+ result = find_app_path(tree, app_name)
201
+ end
202
+
203
+ case result
204
+ in nil
205
+ puts "ERROR: App '#{app_name}' not found"
206
+ puts
207
+ list_apps(tree)
208
+ return false
209
+ in [:ambiguous, matches]
210
+ puts "ERROR: '#{app_name}' matches multiple apps:"
211
+ matches.each { |m| puts " #{m}" }
212
+ puts
213
+ puts "Be more specific."
214
+ return false
215
+ in [resolved_name, app_path]
216
+ # Create new tmux window with app directory as base
217
+ pane = ENV['TMUX_PANE']
218
+ if pane
219
+ puts "PANE: #{pane}"
220
+ puts command: ['tmux', 'send-keys', '-t', pane, "cd #{app_path}\n"].join(' ')
221
+ system('tmux', 'send-keys', '-t', pane, "cd #{app_path}\n")
222
+ else
223
+ puts command: ['tmux', 'send-keys', "cd #{app_path}\n"].join(' ')
224
+ system('tmux', 'send-keys', "cd #{app_path}\n")
225
+ end
226
+ puts "Opened '#{resolved_name}' in new window (#{app_path})"
227
+ true
228
+ end
229
+ end
230
+
231
+ # Save current tmux session state
232
+ def save_current
233
+ current = current_task
234
+ unless current
235
+ puts "ERROR: Not currently in a task session"
236
+ return false
237
+ end
238
+
239
+ task_name = current[:task]
240
+ tree = current[:tree]
241
+ session = current[:session]
242
+
243
+ # Capture tmux window info
244
+ windows = capture_tmux_windows(session)
245
+
246
+ save_task_metadata(task_name,
247
+ tree: tree,
248
+ session: session,
249
+ windows: windows,
250
+ saved_at: Time.now.iso8601
251
+ )
252
+
253
+ puts "Saved task '#{task_name}' state (#{windows.count} windows)"
254
+ true
255
+ end
256
+
257
+ # Show current task status
258
+ def status
259
+ current = current_task
260
+ unless current
261
+ puts "Not currently in a task session"
262
+ return
263
+ end
264
+
265
+ puts "Current task: #{current[:task]}"
266
+ puts "Worktree: #{current[:tree]}"
267
+ puts "Path: #{tree_path(current[:tree])}"
268
+ puts "Session: #{current[:session]}"
269
+
270
+ meta = task_metadata(current[:task])
271
+ if meta && meta['saved_at']
272
+ puts "Last saved: #{meta['saved_at']}"
273
+ end
274
+ end
275
+
276
+ # Stop working on current task (disassociate from worktree)
277
+ def stop_current
278
+ current = current_task
279
+ unless current
280
+ puts "Not currently in a task session"
281
+ return false
282
+ end
283
+
284
+ stop_task(current[:task])
285
+ end
286
+
287
+ # Stop working on a task (disassociate from worktree)
288
+ def stop_task(task_name)
289
+ tree = tree_for_task(task_name)
290
+ task_name = task_for_tree(tree)
291
+
292
+ if RESERVED_WORKTREES.key?(tree)
293
+ puts "Cannot stop reserved task '#{task_name}'"
294
+ return false
295
+ end
296
+
297
+ unassign_task_from_tree(tree)
298
+ puts "Stopped task '#{task_name}' (worktree '#{tree}' now available for reuse)"
299
+ true
300
+ end
301
+
302
+ def list_configured_apps
303
+ if apps_config.any?
304
+ puts "Configured apps (#{apps_config_file}):"
305
+ puts
306
+ apps_config.each do |name, path|
307
+ puts format(" %-20s => %s", name, path)
308
+ end
309
+ else
310
+ puts "No apps configured."
311
+ puts
312
+ puts "Create #{apps_config_file} with format:"
313
+ puts " app_name: relative/path/from/repo"
314
+ puts
315
+ puts "Example:"
316
+ puts " partners: partners/partners"
317
+ puts " admin: admin_portal/admin"
318
+ end
319
+ end
320
+
321
+ def app_path(app_name, task: nil)
322
+ tree_root = task ? tree_path(tree_for_task(task)) : `git rev-parse --show-toplevel`.strip
323
+
324
+ if app_name.nil?
325
+ print tree_root
326
+ exit 0
327
+ end
328
+
329
+ matching_apps = find_all_apps(app_name)
330
+ longest_app_name = printable_apps.keys.max_by(&:length).length + 2
331
+
332
+ case matching_apps.count
333
+ when 0
334
+ puts "ERROR: No matches found"
335
+ puts
336
+ puts "Possible Apps:"
337
+ puts printable_apps.keys.sort.map{|k| format("%#{longest_app_name}s => %s", k, printable_apps[k]) }
338
+ exit 1
339
+ when 1
340
+ print File.join(tree_root, apps_config[matching_apps.first])
341
+ exit 0
342
+ else
343
+ puts "Multiple matches found:"
344
+ puts matching_apps.sort.map{|k| format("%#{longest_app_name}s => %s", k, printable_apps[k]) }
345
+ exit 1
346
+ end
347
+ end
348
+
349
+ private
350
+
351
+ def printable_apps
352
+ apps_config.transform_keys(&:to_s)
353
+ end
354
+
355
+ # Find worktrees using git worktree list
356
+ def trees
357
+ worktree_info.keys.sort
358
+ end
359
+
360
+ # Parse git worktree list output into { name => path } hash
361
+ def worktree_info
362
+ @worktree_info ||= begin
363
+ output = `git -C #{main_repo_path} worktree list --porcelain 2>/dev/null`
364
+ info = {}
365
+ current_path = nil
366
+
367
+ output.lines.each do |line|
368
+ line = line.strip
369
+ if line.start_with?('worktree ')
370
+ current_path = line.sub('worktree ', '')
371
+ elsif line == 'bare'
372
+ # Skip bare repo
373
+ current_path = nil
374
+ elsif line.start_with?('branch ') || line == 'detached'
375
+ # Capture worktree (both named branches and detached HEAD)
376
+ if current_path && current_path != main_repo_path
377
+ name = File.basename(current_path)
378
+ info[name] = current_path
379
+ end
380
+ current_path = nil
381
+ end
382
+ end
383
+
384
+ info
385
+ end
386
+ end
387
+
388
+ def clear_worktree_cache
389
+ @worktree_info = nil
390
+ end
391
+
392
+ # Get the main repo path (where we run git worktree commands from)
393
+ def main_repo_path
394
+ File.join(Dir.home, 'work', '.bare')
395
+ end
396
+
397
+ def tree_path(tree_name)
398
+ worktree_info[tree_name] || File.join(Dir.home, 'work', tree_name)
399
+ end
400
+
401
+ # Worktrees with permanent task assignments (worktree => task)
402
+ RESERVED_WORKTREES = { 'carrot' => 'master' }.freeze
403
+
404
+ # Find an available tree (one without an active task)
405
+ def find_available_tree
406
+ trees.find { |tree| task_for_tree(tree).nil? && !RESERVED_WORKTREES.key?(tree) }
407
+ end
408
+
409
+ # Get the task currently assigned to a tree
410
+ def task_for_tree(tree_name)
411
+ assignments[tree_name]
412
+ end
413
+
414
+ # Get the tree a task is assigned to
415
+ def tree_for_task(task_name)
416
+ assignment_for_task(task_name)&.first
417
+ end
418
+
419
+ def assignment_for_task(partial)
420
+ assignments.find { |tree, task| task.start_with?(partial) }
421
+ end
422
+
423
+ def find_task(partial)
424
+ assignments.values.find { |task| task.start_with?(partial) }
425
+ end
426
+
427
+ # Assign a task to a tree
428
+ def assign_task_to_tree(task_name, tree_name)
429
+ data = assignments
430
+ data[tree_name] = task_name
431
+ save_assignments(data)
432
+ end
433
+
434
+ # Unassign task from tree
435
+ def unassign_task_from_tree(tree_name)
436
+ return if RESERVED_WORKTREES.key?(tree_name)
437
+ data = assignments.dup
438
+ data.delete(tree_name)
439
+ save_assignments(data)
440
+ end
441
+
442
+ # Tree -> Task assignments
443
+ def assignments
444
+ @assignments ||= load_assignments
445
+ end
446
+
447
+ def load_assignments
448
+ data = if File.exist?(assignments_file)
449
+ YAML.safe_load_file(assignments_file) || {}
450
+ else
451
+ {}
452
+ end
453
+ # Always include reserved worktree assignments
454
+ RESERVED_WORKTREES.merge(data)
455
+ end
456
+
457
+ def save_assignments(data)
458
+ FileUtils.mkdir_p(task_dir)
459
+ File.write(assignments_file, YAML.dump(data))
460
+ @assignments = data
461
+ end
462
+
463
+ def assignments_file
464
+ File.join(task_dir, 'assignments.yml')
465
+ end
466
+
467
+ # Task metadata
468
+ def task_metadata(task_name)
469
+ file = task_metadata_file(task_name)
470
+ return nil unless File.exist?(file)
471
+ YAML.safe_load_file(file)
472
+ end
473
+
474
+ def save_task_metadata(task_name, **data)
475
+ FileUtils.mkdir_p(task_dir)
476
+ existing = task_metadata(task_name) || {}
477
+ merged = existing.merge(data.transform_keys(&:to_s))
478
+ File.write(task_metadata_file(task_name), YAML.dump(merged))
479
+ end
480
+
481
+ def task_metadata_file(task_name)
482
+ safe_name = task_name.gsub(/[^a-zA-Z0-9_-]/, '_')
483
+ File.join(task_dir, "task_#{safe_name}.yml")
484
+ end
485
+
486
+ def task_dir
487
+ File.join(Dir.home, '.config', 'hiiro', 'tasks')
488
+ end
489
+
490
+ # Session name for a task
491
+ def session_name_for(task_name)
492
+ task_name
493
+ end
494
+
495
+ # Detect current task from tmux session name
496
+ def current_task
497
+ return nil unless ENV['TMUX']
498
+
499
+ session = `tmux display-message -p '#S'`.strip
500
+
501
+ task_name = session
502
+ tree = tree_for_task(task_name)
503
+
504
+ return nil unless tree
505
+
506
+ { task: task_name, tree: tree, session: session }
507
+ end
508
+
509
+ def switch_to_task(task_name, tree, app: nil)
510
+ session = session_name_for(task_name)
511
+ base_path = tree_path(tree)
512
+
513
+ if app
514
+ result = find_app_path(tree, app)
515
+ case result
516
+ in [resolved_name, app_path]
517
+ base_path = app_path
518
+ puts "Using app '#{resolved_name}' as base path"
519
+ else
520
+ # Ignore errors on switch, just use tree root
521
+ end
522
+ end
523
+
524
+ Dir.chdir(base_path)
525
+ hiiro.start_tmux_session(session)
526
+ end
527
+
528
+ # Apps config from ~/.config/hiiro/apps.yml
529
+ # Format: { "app_name" => "relative/path/from/repo/root" }
530
+ def apps_config
531
+ @apps_config ||= load_apps_config
532
+ end
533
+
534
+ def load_apps_config
535
+ return {} unless File.exist?(apps_config_file)
536
+ YAML.safe_load_file(apps_config_file) || {}
537
+ end
538
+
539
+ def apps_config_file
540
+ File.join(Dir.home, '.config', 'hiiro', 'apps.yml')
541
+ end
542
+
543
+ # Find app by partial match (must be unique)
544
+ def find_app(partial)
545
+ matches = find_all_apps(partial)
546
+
547
+ case matches.count
548
+ when 0
549
+ nil
550
+ when 1
551
+ matches.first
552
+ else
553
+ # Check for exact match among multiple partial matches
554
+ exact = matches.find { |name| name == partial }
555
+ exact ? [exact] : matches
556
+ end
557
+ end
558
+
559
+ def find_all_apps(partial)
560
+ apps_config.keys.select { |name| name.start_with?(partial) }
561
+ end
562
+
563
+ # App discovery within a tree
564
+ def find_app_path(tree, app_name)
565
+ tree_root = tree_path(tree)
566
+
567
+ # First, check apps.yml config
568
+ result = find_app(app_name)
569
+
570
+ case result
571
+ when String
572
+ # Single match - use configured path
573
+ return [result, File.join(tree_root, apps_config[result])]
574
+ when Array
575
+ # Multiple matches - return them for error reporting
576
+ return [:ambiguous, result]
577
+ end
578
+
579
+ # Fallback: directory discovery if not in config
580
+ # Look for exact match first
581
+ exact = File.join(tree_root, app_name)
582
+ return [app_name, exact] if Dir.exist?(exact)
583
+
584
+ # Look for nested app dirs (monorepo pattern: app/app)
585
+ nested = File.join(tree_root, app_name, app_name)
586
+ return [app_name, nested] if Dir.exist?(nested)
587
+
588
+ # Fuzzy match on directories
589
+ pattern = File.join(tree_root, '*')
590
+ match = Dir.glob(pattern).find { |path|
591
+ File.basename(path).start_with?(app_name) && File.directory?(path)
592
+ }
593
+ return [File.basename(match), match] if match
594
+
595
+ nil
596
+ end
597
+
598
+ def list_apps(tree)
599
+ if apps_config.any?
600
+ puts "Configured apps (from apps.yml):"
601
+ apps_config.each do |name, path|
602
+ puts format(" %-20s => %s", name, path)
603
+ end
604
+ else
605
+ puts "No apps configured. Create ~/.config/hiiro/apps.yml"
606
+ puts "Format:"
607
+ puts " app_name: relative/path/from/repo"
608
+ puts
609
+ puts "Directories in tree:"
610
+ tree_root = tree_path(tree)
611
+ pattern = File.join(tree_root, '*')
612
+ Dir.glob(pattern).select { |p| File.directory?(p) }.each do |path|
613
+ puts " #{File.basename(path)}"
614
+ end
615
+ end
616
+ end
617
+
618
+ # Capture tmux window state
619
+ def capture_tmux_windows(session)
620
+ output = `tmux list-windows -t #{session} -F '\#{window_index}:\#{window_name}:\#{pane_current_path}'`
621
+ output.lines.map(&:strip).map { |line|
622
+ idx, name, path = line.split(':')
623
+ { 'index' => idx, 'name' => name, 'path' => path }
624
+ }
625
+ end
626
+ end
627
+
628
+ # Create task manager instance
629
+ tasks = TaskManager.new(hiiro)
630
+
631
+ # Subcommands
632
+ hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
633
+ hiiro.add_subcmd(:list) { tasks.list_trees }
634
+ hiiro.add_subcmd(:ls) { tasks.list_trees }
635
+ hiiro.add_subcmd(:start) { |task_name, app=nil| tasks.start_task(task_name, app: app) }
636
+ hiiro.add_subcmd(:switch) { |task_name| tasks.switch_task(task_name) }
637
+ hiiro.add_subcmd(:app) { |app_name| tasks.open_app(app_name) }
638
+ hiiro.add_subcmd(:path) { |app_name=nil, task=nil| tasks.app_path(app_name, task: task) }
639
+ hiiro.add_subcmd(:cd) { |*args| tasks.cd_app(*args) }
640
+ hiiro.add_subcmd(:apps) { tasks.list_configured_apps }
641
+ hiiro.add_subcmd(:status) { tasks.status }
642
+ hiiro.add_subcmd(:st) { tasks.status }
643
+ hiiro.add_subcmd(:save) { tasks.save_current }
644
+ hiiro.add_subcmd(:stop) { |task_name=nil| task_name ? tasks.stop_task(task_name) : tasks.stop_current }
645
+
646
+ hiiro.add_default { tasks.help }
647
+
648
+ hiiro.run
data/exe/h ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "hiiro"
4
+ require "fileutils"
5
+
6
+ Hiiro.load_env
7
+
8
+ hiiro = Hiiro.init(*ARGV, cwd: Dir.pwd)
9
+
10
+ hiiro.add_subcommand(:ping) { |*args|
11
+ puts "pong"
12
+ }
13
+
14
+ hiiro.add_subcommand(:setup) do |*args|
15
+ gem_root = File.expand_path("../..", __FILE__)
16
+ source_plugins = File.join(gem_root, "plugins")
17
+ source_bins = File.join(gem_root, "bin")
18
+ dest_plugins = File.expand_path("~/.config/hiiro/plugins")
19
+ dest_bin = File.expand_path("~/bin")
20
+
21
+ FileUtils.mkdir_p(dest_plugins)
22
+ FileUtils.mkdir_p(dest_bin)
23
+
24
+ # Copy plugins
25
+ plugin_files = Dir["#{source_plugins}/*.rb"]
26
+ if plugin_files.any?
27
+ FileUtils.cp(plugin_files, dest_plugins)
28
+ puts "Installed #{plugin_files.size} plugins to #{dest_plugins}"
29
+ plugin_files.each { |f| puts " - #{File.basename(f)}" }
30
+ else
31
+ puts "No plugins found in #{source_plugins}"
32
+ end
33
+
34
+ puts
35
+
36
+ # Copy bin scripts
37
+ bin_files = Dir["#{source_bins}/h-*"]
38
+ if bin_files.any?
39
+ FileUtils.cp(bin_files, dest_bin)
40
+ bin_files.each { |f| FileUtils.chmod(0755, File.join(dest_bin, File.basename(f))) }
41
+ puts "Installed #{bin_files.size} subcommands to #{dest_bin}"
42
+ bin_files.each { |f| puts " - #{File.basename(f)}" }
43
+ else
44
+ puts "No subcommands found in #{source_bins}"
45
+ end
46
+
47
+ puts
48
+ puts "Setup complete! Make sure ~/bin is in your PATH."
49
+ end
50
+
51
+ hiiro.run
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
data/script/publish ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "hiiro"
4
+
5
+ major, minor, patch = Hiiro::VERSION.split(?.)
6
+
7
+ new_version = [major, minor, patch.to_i + 1].join(?.)
8
+
9
+ File.open('lib/hiiro/version.rb', 'w+') do |f|
10
+ f.puts 'class Hiiro'
11
+ f.puts " VERSION = #{new_version.inspect}"
12
+ f.puts 'end'
13
+ end
14
+
15
+ built = system('gem', 'build', 'hiiro.gemspec')
16
+
17
+ puts "\nERROR: unable to build gem\n" unless built
18
+
19
+ pushed = system('gem', 'push', "hiiro-#{new_version}.gem")
20
+
21
+ puts "\nERROR: unable to push\n" unless pushed
22
+
23
+ system 'git', 'add', '--all'
24
+ system 'git', 'commit', '-m', "publishing v#{new_version}"
25
+
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.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -29,7 +29,7 @@ description: Build multi-command CLI tools with subcommand dispatch, abbreviatio
29
29
  email:
30
30
  - jearsh+rubygems@gmail.com
31
31
  executables:
32
- - hiiro
32
+ - h
33
33
  extensions: []
34
34
  extra_rdoc_files: []
35
35
  files:
@@ -43,6 +43,7 @@ files:
43
43
  - bin/h-pane
44
44
  - bin/h-plugin
45
45
  - bin/h-session
46
+ - bin/h-task
46
47
  - bin/h-video
47
48
  - bin/h-window
48
49
  - docs/README.md
@@ -52,7 +53,7 @@ files:
52
53
  - docs/h-session.md
53
54
  - docs/h-video.md
54
55
  - docs/h-window.md
55
- - exe/hiiro
56
+ - exe/h
56
57
  - hiiro.gemspec
57
58
  - lib/hiiro.rb
58
59
  - lib/hiiro/version.rb
@@ -63,6 +64,7 @@ files:
63
64
  - plugins/tmux.rb
64
65
  - script/compare
65
66
  - script/install
67
+ - script/publish
66
68
  homepage: https://github.com/unixsuperhero/hiiro
67
69
  licenses:
68
70
  - MIT
data/exe/hiiro DELETED
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "hiiro"
4
-
5
- Hiiro.load_env
6
-
7
- hiiro = Hiiro.init(*ARGV, cwd: Dir.pwd)
8
-
9
- hiiro.add_subcommand(:ping) { |*args|
10
- puts "pong"
11
- }
12
-
13
- hiiro.run