hiiro 0.1.31 → 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.
- checksums.yaml +4 -4
- data/README.md +4 -23
- data/TODO.md +21 -0
- data/bin/g-pr +1 -1
- data/bin/h-branch +1 -1
- data/bin/{h-subtask → h-osubtask} +1 -1
- data/bin/h-pr +1 -1
- data/exe/h +1 -1
- data/lib/hiiro/version.rb +1 -1
- data/notes +224 -0
- data/plugins/{task.rb → old_task.rb} +2 -2
- data/plugins/tasks.rb +813 -0
- metadata +8 -5
- /data/bin/{h-task → h-otask} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33682128f00683ee714b4bc368dbcc2243544cf3c495063231ec95497ded5bc8
|
|
4
|
+
data.tar.gz: 8bfb329a9ec2f09a96e886c8a3ea3146ccb7a5a728ca5efc7a83509df3d578a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d4ad31db7afe3ca4ee5f7160e5004a086b8a5962908bc346bd30ca9a0c8a4d6948f1c699339d5103a14ea37b101b1beb8a515f17a154ec77e3b7a422cf8211e
|
|
7
|
+
data.tar.gz: b6f2348435f48f1ba9a2ce9b38cd7a6db4d5aefca7c32d8291e96ff6faf589d061f4b2d9838fc9643ff05988dfd9489680dd684e36a478f11203ceb8b40a311d
|
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ See [docs/](docs/) for detailed documentation on all subcommands.
|
|
|
19
19
|
gem install hiiro
|
|
20
20
|
|
|
21
21
|
# Install plugins and subcommands
|
|
22
|
-
|
|
22
|
+
h setup
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
This installs:
|
|
@@ -28,26 +28,15 @@ This installs:
|
|
|
28
28
|
|
|
29
29
|
Ensure `~/bin` is in your `$PATH`.
|
|
30
30
|
|
|
31
|
-
### Manual Installation
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
# Copy the main script
|
|
35
|
-
cp bin/h ~/bin/h
|
|
36
|
-
chmod +x ~/bin/h
|
|
37
|
-
|
|
38
|
-
# Copy subcommands (optional)
|
|
39
|
-
cp bin/h-* ~/bin/
|
|
40
|
-
|
|
41
|
-
# Copy plugins (optional)
|
|
42
|
-
mkdir -p ~/.config/hiiro/plugins
|
|
43
|
-
cp plugins/*.rb ~/.config/hiiro/plugins/
|
|
44
|
-
```
|
|
45
31
|
|
|
46
32
|
### Dependencies
|
|
47
33
|
|
|
48
34
|
```sh
|
|
49
35
|
# For notify plugin (macOS)
|
|
50
36
|
brew install terminal-notifier
|
|
37
|
+
|
|
38
|
+
# For fuzzy-finder
|
|
39
|
+
brew install sk
|
|
51
40
|
```
|
|
52
41
|
|
|
53
42
|
## Quick Start
|
|
@@ -56,14 +45,6 @@ brew install terminal-notifier
|
|
|
56
45
|
# List available subcommands
|
|
57
46
|
h
|
|
58
47
|
|
|
59
|
-
# Edit the main h script
|
|
60
|
-
h edit
|
|
61
|
-
|
|
62
|
-
# Get paths
|
|
63
|
-
h path # Print current directory
|
|
64
|
-
h ppath # Print project root (git repo root + relative dir)
|
|
65
|
-
h rpath # Print relative directory from git root
|
|
66
|
-
|
|
67
48
|
# Simple test
|
|
68
49
|
h ping
|
|
69
50
|
# => pong
|
data/TODO.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
# To do...
|
|
3
|
+
|
|
4
|
+
- [ ] move task/subtask back into a plugin
|
|
5
|
+
- [ ] have it add :task and :subtask subcommands to the instance of Hiiro
|
|
6
|
+
- [ ] they should init another hiiro instance with a `scope` value set
|
|
7
|
+
- [ ] if scope is :task, it looks at the whole world
|
|
8
|
+
- [ ] if the scope is :subtask, it scopes behavior to the current task
|
|
9
|
+
- [ ] redesign the yml structure for managing tasks
|
|
10
|
+
|
|
11
|
+
```yml
|
|
12
|
+
tasks:
|
|
13
|
+
- name: some_name
|
|
14
|
+
parent_task: parent_task_name (if included it's a subtask)
|
|
15
|
+
tree: tree_name
|
|
16
|
+
root: repo_root/tree_name
|
|
17
|
+
```
|
|
18
|
+
- [ ] refactor task plugin to not be procedural AI slop
|
|
19
|
+
- [ ] convert to js and using node
|
|
20
|
+
- [ ] h-config {vim,zsh,tmux}
|
|
21
|
+
- [ ]
|
data/bin/g-pr
CHANGED
data/bin/h-branch
CHANGED
data/bin/h-pr
CHANGED
data/exe/h
CHANGED
data/lib/hiiro/version.rb
CHANGED
data/notes
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
|
|
2
|
+
# Principles to use for refactoring
|
|
3
|
+
|
|
4
|
+
- Separate data from behavior - create domain objects and any relevant value objects
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Goals
|
|
8
|
+
|
|
9
|
+
- same code to manage tasks/subtasks
|
|
10
|
+
- just scoped differently
|
|
11
|
+
- if scope is `:task` then consider all tasks, if scope is :subtask then only consider the tasks associated with the parent task
|
|
12
|
+
- extract data objects
|
|
13
|
+
- so our procedures are human and easy af
|
|
14
|
+
- more composable
|
|
15
|
+
- separate presentation layer from the data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# features we want
|
|
19
|
+
|
|
20
|
+
- list tasks/subtasks
|
|
21
|
+
- start a new one (wtree/tmux session)
|
|
22
|
+
- switch between them
|
|
23
|
+
- cd to an app's directory
|
|
24
|
+
- stop a task
|
|
25
|
+
- use yml files to track things
|
|
26
|
+
|
|
27
|
+
turn @plugins/tasks.rb into a plugin similar to @plugins/task.rb
|
|
28
|
+
|
|
29
|
+
but in add_subcommands(hiiro) i want to define 2 subtasks:
|
|
30
|
+
1. `:task`
|
|
31
|
+
2. `:subtask`
|
|
32
|
+
|
|
33
|
+
both subcommands should initialize `Tasks.new(hiiro_instance, :task)` or
|
|
34
|
+
`Tasks.new(hiiro_instance, :subtask)` if the subtask subcmd was called
|
|
35
|
+
|
|
36
|
+
then uses the instance of Tasks to either do all the things that it needs to
|
|
37
|
+
do... or just use it to get the data it needs, so that it can do the high level
|
|
38
|
+
things like running tmux or git worktree commands.
|
|
39
|
+
if it doesn't put those things inside Tasks, that would be better, so I can use
|
|
40
|
+
these objects in other places like inside @bin/h-link
|
|
41
|
+
|
|
42
|
+
use the following interface as a guide for how to organize things inside the
|
|
43
|
+
new Tasks plugin
|
|
44
|
+
|
|
45
|
+
# Interface
|
|
46
|
+
|
|
47
|
+
class Environment
|
|
48
|
+
def self.current # => Environment - makes new instance
|
|
49
|
+
def all_tasks # => Task[] - call Task.all and memo cache the result
|
|
50
|
+
def all_sessions # => Session[] - call TmuxSession.all and memo cache the result
|
|
51
|
+
def all_trees # => Tree[] - call Tree.all and memo cache the result
|
|
52
|
+
def task # => Task - move Task.current's behavior into this method, and memo cache the result
|
|
53
|
+
def session # => Session - move TmuxSession.current here and memo cache the result
|
|
54
|
+
def tree # => Tree - move Tree.current here and memo cache the result
|
|
55
|
+
|
|
56
|
+
class Tree
|
|
57
|
+
attr_reader :path, :task, :session
|
|
58
|
+
|
|
59
|
+
def self.current # => Tree - move to Environment#tree
|
|
60
|
+
def self.all # => Tree[]
|
|
61
|
+
|
|
62
|
+
class Session
|
|
63
|
+
attr_reader :name, :task
|
|
64
|
+
|
|
65
|
+
def self.current # => Session - move to Environment#session
|
|
66
|
+
def self.all # => Session[]
|
|
67
|
+
|
|
68
|
+
class Task
|
|
69
|
+
attr_reader :name, :tree, :session, :parent, :subtasks
|
|
70
|
+
|
|
71
|
+
def self.current # => Task - move to Environment#task
|
|
72
|
+
def self.all # => Task[]
|
|
73
|
+
|
|
74
|
+
class App
|
|
75
|
+
attr_reader :name, :path
|
|
76
|
+
def self.current # => App[]
|
|
77
|
+
def self.all # => App[]
|
|
78
|
+
|
|
79
|
+
class Tasks
|
|
80
|
+
attr_reader :hiiro, :scope, :environment
|
|
81
|
+
|
|
82
|
+
def tasks # => Task[]
|
|
83
|
+
def subtasks(task) # => Task[]
|
|
84
|
+
def task_by_name(name) # => Task
|
|
85
|
+
def task_by_tree(tree_name) # => Task
|
|
86
|
+
def task_by_session(session_name) # => Task
|
|
87
|
+
def current_task # => Task
|
|
88
|
+
def current_session # => Session
|
|
89
|
+
def current_tree # => Tree
|
|
90
|
+
|
|
91
|
+
def start_task(name, path) # => Task
|
|
92
|
+
def switch_to_task(task) # => nil
|
|
93
|
+
def stop_task(task) # => nil
|
|
94
|
+
def list(tasks) # => nil # print to screen
|
|
95
|
+
def cd_to_task(task) # => nil
|
|
96
|
+
def cd_to_app(app_name) # => nil
|
|
97
|
+
|
|
98
|
+
class Config
|
|
99
|
+
attr_reader :path
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Follow-up prompts
|
|
105
|
+
|
|
106
|
+
## i forget
|
|
107
|
+
|
|
108
|
+
> when initializing an instance of `Task`...it shouldn't have a `parent_task` or `parent_name` kwarg. it derive that context from the name given. if the name has a `/` in it...then it
|
|
109
|
+
is a subtask and the left side is the parent name and the right side is the subtask name.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## lots of updates
|
|
113
|
+
|
|
114
|
+
here are some updates i'd like to make:
|
|
115
|
+
|
|
116
|
+
- all arguments received from the user should looking that object using
|
|
117
|
+
- something like `start_with?` so they can abbreviate the names of things to
|
|
118
|
+
match
|
|
119
|
+
- the only special one is task_name or subtask_name...if there is a `/` the
|
|
120
|
+
left side will do the abbreviation lookup on a parent name and the right
|
|
121
|
+
side will do a lookup on the subtask name
|
|
122
|
+
- you might want to add `#find_tree(abbreviated_name)` method to
|
|
123
|
+
Environment
|
|
124
|
+
- you might want to add `#find_session(abbreviated_name)` method to
|
|
125
|
+
Environment
|
|
126
|
+
- you might want to add `#find_app(abbreviated_name)` method to
|
|
127
|
+
Environment
|
|
128
|
+
- you might want to add `#find_task(abbreviated_name)` method to
|
|
129
|
+
Environment, where it does the left/right abbreviation lookups
|
|
130
|
+
- these new methods should help clean up and simplify all the lookups in
|
|
131
|
+
the other methods that typically use `.find{ ... }`
|
|
132
|
+
|
|
133
|
+
- get rid of methods like `apps_hash` and in the places that used it...
|
|
134
|
+
- just use the array returned from `#apps` and the attr_reader methods
|
|
135
|
+
for things like name and path or other values that there are attr_readers
|
|
136
|
+
for.
|
|
137
|
+
- always favor an instance method or attr_reader over exporting something
|
|
138
|
+
like hash
|
|
139
|
+
|
|
140
|
+
- `#start_task` shouldn't take an optional `tree_path` keyword arg
|
|
141
|
+
- instead it should take an optional `app_name` kwarg and set the tmux
|
|
142
|
+
session's base directory to the matching app's configured directory
|
|
143
|
+
|
|
144
|
+
- `#switch_task` should also take an optional `app_name` kwarg. if a tmux
|
|
145
|
+
session already exists, just update the configs, associating the task
|
|
146
|
+
with that app. if the tmux session doesn't exist, use the app's path as the
|
|
147
|
+
base_directory.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
here's one fix i really want to get right.
|
|
151
|
+
|
|
152
|
+
right now TasksPlugin has a dispatch method that uses a case statement to
|
|
153
|
+
handle the subcommands. what i'd like instead is for tasks plugin to initialize
|
|
154
|
+
a new instance of `Hiiro` but instead of it receiving it's usual `*ARGV` as
|
|
155
|
+
positional args, you should pass-in t `*hiiro.args` using the existing `hiiro`
|
|
156
|
+
instance. then for the new `Hiiro` instance...before you call `#run` on
|
|
157
|
+
it...add each of the subcommands defined in the case statement using the
|
|
158
|
+
`add_subcmd` method and have the block passed in be the handler for those
|
|
159
|
+
subcommands
|
|
160
|
+
|
|
161
|
+
so something like this:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
module TasksPlugin
|
|
165
|
+
# ...
|
|
166
|
+
|
|
167
|
+
def self.add_subcommands(hiiro)
|
|
168
|
+
hiiro.add_subcmd(:task) do |*args|
|
|
169
|
+
mgr = Tasks.new(hiiro, scope: :task)
|
|
170
|
+
new_hiiro = task_hiiro(hiiro, mgr)
|
|
171
|
+
new_hiiro.run
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
hiiro.add_subcmd(:subtask) do |*args|
|
|
175
|
+
mgr = Tasks.new(hiiro, scope: :subtask)
|
|
176
|
+
new_hiiro = task_hiiro(hiiro, mgr)
|
|
177
|
+
new_hiiro.run
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.task_hiiro(parent_hiiro, mgr)
|
|
182
|
+
Hiiro.init(*parent_hiiro.args, mgr: mgr) do |task_hiiro|
|
|
183
|
+
task_hiiro.add_subcmd(:ls) do |*args|
|
|
184
|
+
# TODO: the logic to print out the task list based on mgr's values
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
task_hiiro.add_subcmd(:start) do |task_name, app_name=nil|
|
|
188
|
+
# TODO: the logic to print out the task list based on mgr's values
|
|
189
|
+
# like:
|
|
190
|
+
mgr.start_task(task_name, app_name: app_name)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
task_hiiro.add_subcmd(:switch) do |task_name, app_name=nil|
|
|
194
|
+
# TODO: the logic to print out the task list based on mgr's values
|
|
195
|
+
mgr.switch_to_task(task_name, app_name: app_name)
|
|
196
|
+
|
|
197
|
+
# but if you need to add logic, i would do it here...like:
|
|
198
|
+
task = mgr.find_task(task_name)
|
|
199
|
+
|
|
200
|
+
if task
|
|
201
|
+
# run cmds to switch to task here...
|
|
202
|
+
else
|
|
203
|
+
# print some msg like no task found
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# ...
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# ...
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## third prompt
|
|
216
|
+
|
|
217
|
+
for things like listing and switching subtasks
|
|
218
|
+
always consider the main/parent task as a potential subtask to switch to.
|
|
219
|
+
for lookups, treat the task name as parent name and `main` as the subtask name
|
|
220
|
+
so something like `nameofparent/main`
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module OldTask
|
|
4
4
|
def self.load(hiiro)
|
|
5
5
|
hiiro.load_plugin(Tmux)
|
|
6
6
|
attach_methods(hiiro)
|
|
@@ -79,7 +79,7 @@ module Task
|
|
|
79
79
|
def self.attach_methods(hiiro)
|
|
80
80
|
hiiro.instance_eval do
|
|
81
81
|
def task_manager
|
|
82
|
-
@task_manager ||=
|
|
82
|
+
@task_manager ||= OldTask::TaskManager.new(self)
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
end
|
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
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hiiro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.32
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Toyota
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pry
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- LICENSE
|
|
39
39
|
- README.md
|
|
40
40
|
- Rakefile
|
|
41
|
+
- TODO.md
|
|
41
42
|
- bin/g-pr
|
|
42
43
|
- bin/h
|
|
43
44
|
- bin/h-branch
|
|
@@ -49,6 +50,8 @@ files:
|
|
|
49
50
|
- bin/h-link
|
|
50
51
|
- bin/h-mic
|
|
51
52
|
- bin/h-note
|
|
53
|
+
- bin/h-osubtask
|
|
54
|
+
- bin/h-otask
|
|
52
55
|
- bin/h-pane
|
|
53
56
|
- bin/h-plugin
|
|
54
57
|
- bin/h-pr
|
|
@@ -59,8 +62,6 @@ files:
|
|
|
59
62
|
- bin/h-serve
|
|
60
63
|
- bin/h-session
|
|
61
64
|
- bin/h-sha
|
|
62
|
-
- bin/h-subtask
|
|
63
|
-
- bin/h-task
|
|
64
65
|
- bin/h-video
|
|
65
66
|
- bin/h-vim
|
|
66
67
|
- bin/h-window
|
|
@@ -78,10 +79,12 @@ files:
|
|
|
78
79
|
- lib/hiiro/history.rb
|
|
79
80
|
- lib/hiiro/version.rb
|
|
80
81
|
- links.backup.yml
|
|
82
|
+
- notes
|
|
81
83
|
- plugins/notify.rb
|
|
84
|
+
- plugins/old_task.rb
|
|
82
85
|
- plugins/pins.rb
|
|
83
86
|
- plugins/project.rb
|
|
84
|
-
- plugins/
|
|
87
|
+
- plugins/tasks.rb
|
|
85
88
|
- plugins/tmux.rb
|
|
86
89
|
- script/compare
|
|
87
90
|
- script/install
|
/data/bin/{h-task → h-otask}
RENAMED
|
File without changes
|