taski 0.5.0 → 0.7.0
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/CHANGELOG.md +50 -0
- data/README.md +40 -21
- data/docs/GUIDE.md +340 -0
- data/examples/README.md +65 -17
- data/examples/{context_demo.rb → args_demo.rb} +27 -27
- data/examples/clean_demo.rb +204 -0
- data/examples/group_demo.rb +113 -0
- data/examples/reexecution_demo.rb +93 -80
- data/examples/system_call_demo.rb +56 -0
- data/lib/taski/{context.rb → args.rb} +3 -3
- data/lib/taski/execution/execution_context.rb +379 -0
- data/lib/taski/execution/executor.rb +408 -117
- data/lib/taski/execution/registry.rb +17 -1
- data/lib/taski/execution/scheduler.rb +308 -0
- data/lib/taski/execution/task_output_pipe.rb +42 -0
- data/lib/taski/execution/task_output_router.rb +216 -0
- data/lib/taski/execution/task_wrapper.rb +210 -40
- data/lib/taski/execution/tree_progress_display.rb +385 -98
- data/lib/taski/execution/worker_pool.rb +104 -0
- data/lib/taski/section.rb +16 -3
- data/lib/taski/task.rb +222 -36
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +138 -23
- data/sig/taski.rbs +207 -27
- metadata +13 -7
- data/docs/advanced-features.md +0 -625
- data/docs/api-guide.md +0 -509
- data/docs/error-handling.md +0 -684
- data/examples/section_progress_demo.rb +0 -78
|
@@ -27,49 +27,105 @@ module Taski
|
|
|
27
27
|
|
|
28
28
|
# Status icons
|
|
29
29
|
ICONS = {
|
|
30
|
+
# Run lifecycle states
|
|
30
31
|
pending: "⏸", # Pause for waiting
|
|
31
32
|
running_prefix: "", # Will use spinner
|
|
32
33
|
completed: "✓",
|
|
33
34
|
failed: "✗",
|
|
34
|
-
skipped: "⊘"
|
|
35
|
+
skipped: "⊘", # Prohibition sign for unselected impl candidates
|
|
36
|
+
# Clean lifecycle states
|
|
37
|
+
cleaning_prefix: "", # Will use spinner
|
|
38
|
+
clean_completed: "♻",
|
|
39
|
+
clean_failed: "✗"
|
|
35
40
|
}.freeze
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
##
|
|
43
|
+
# Checks if a class is a Taski::Section subclass.
|
|
44
|
+
# @param klass [Class] The class to check.
|
|
45
|
+
# @return [Boolean] true if the class is a Section.
|
|
38
46
|
def self.section_class?(klass)
|
|
39
47
|
defined?(Taski::Section) && klass < Taski::Section
|
|
40
48
|
end
|
|
41
49
|
|
|
50
|
+
##
|
|
51
|
+
# Checks if a class is nested within another class by name prefix.
|
|
52
|
+
# @param child_class [Class] The potential nested class.
|
|
53
|
+
# @param parent_class [Class] The potential parent class.
|
|
54
|
+
# @return [Boolean] true if child_class name starts with parent_class name and "::".
|
|
42
55
|
def self.nested_class?(child_class, parent_class)
|
|
43
56
|
child_name = child_class.name.to_s
|
|
44
57
|
parent_name = parent_class.name.to_s
|
|
45
58
|
child_name.start_with?("#{parent_name}::")
|
|
46
59
|
end
|
|
47
60
|
|
|
61
|
+
# Build a tree structure from a root task class.
|
|
62
|
+
# This is the shared tree building logic used by both static and progress display.
|
|
63
|
+
#
|
|
64
|
+
# @param task_class [Class] The task class to build tree for
|
|
65
|
+
# @param ancestors [Set] Set of ancestor task classes for circular detection
|
|
66
|
+
# @return [Hash, nil] Tree node hash or nil if circular
|
|
67
|
+
#
|
|
68
|
+
# Tree node structure:
|
|
69
|
+
# {
|
|
70
|
+
# task_class: Class, # The task class
|
|
71
|
+
# is_section: Boolean, # Whether this is a Section
|
|
72
|
+
# is_circular: Boolean, # Whether this is a circular reference
|
|
73
|
+
# is_impl_candidate: Boolean, # Whether this is an impl candidate
|
|
74
|
+
# children: Array<Hash> # Child nodes
|
|
75
|
+
# }
|
|
76
|
+
def self.build_tree_node(task_class, ancestors = Set.new)
|
|
77
|
+
is_circular = ancestors.include?(task_class)
|
|
78
|
+
|
|
79
|
+
node = {
|
|
80
|
+
task_class: task_class,
|
|
81
|
+
is_section: section_class?(task_class),
|
|
82
|
+
is_circular: is_circular,
|
|
83
|
+
is_impl_candidate: false,
|
|
84
|
+
children: []
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Don't traverse children for circular references
|
|
88
|
+
return node if is_circular
|
|
89
|
+
|
|
90
|
+
new_ancestors = ancestors + [task_class]
|
|
91
|
+
dependencies = StaticAnalysis::Analyzer.analyze(task_class).to_a
|
|
92
|
+
is_section = section_class?(task_class)
|
|
93
|
+
|
|
94
|
+
dependencies.each do |dep|
|
|
95
|
+
child_node = build_tree_node(dep, new_ancestors)
|
|
96
|
+
child_node[:is_impl_candidate] = is_section && nested_class?(dep, task_class)
|
|
97
|
+
node[:children] << child_node
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
node
|
|
101
|
+
end
|
|
102
|
+
|
|
48
103
|
# Render a static tree structure for a task class (used by Task.tree)
|
|
49
104
|
# @param root_task_class [Class] The root task class
|
|
50
105
|
# @return [String] The rendered tree string
|
|
51
106
|
def self.render_static_tree(root_task_class)
|
|
52
|
-
|
|
53
|
-
|
|
107
|
+
tree = build_tree_node(root_task_class)
|
|
108
|
+
formatter = StaticTreeFormatter.new
|
|
109
|
+
formatter.format(tree)
|
|
54
110
|
end
|
|
55
111
|
|
|
56
|
-
#
|
|
57
|
-
class
|
|
58
|
-
def
|
|
112
|
+
# Formatter for static tree display (no progress tracking, uses task numbers)
|
|
113
|
+
class StaticTreeFormatter
|
|
114
|
+
def format(tree)
|
|
59
115
|
@task_index_map = {}
|
|
60
|
-
|
|
116
|
+
format_node(tree, "", false)
|
|
61
117
|
end
|
|
62
118
|
|
|
63
119
|
private
|
|
64
120
|
|
|
65
|
-
def
|
|
121
|
+
def format_node(node, prefix, is_impl)
|
|
122
|
+
task_class = node[:task_class]
|
|
66
123
|
type_label = colored_type_label(task_class)
|
|
67
124
|
impl_prefix = is_impl ? "#{COLORS[:impl]}[impl]#{COLORS[:reset]} " : ""
|
|
68
125
|
task_number = get_task_number(task_class)
|
|
69
126
|
name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
|
|
70
127
|
|
|
71
|
-
|
|
72
|
-
if ancestors.include?(task_class)
|
|
128
|
+
if node[:is_circular]
|
|
73
129
|
circular_marker = "#{COLORS[:impl]}(circular)#{COLORS[:reset]}"
|
|
74
130
|
return "#{impl_prefix}#{task_number} #{name} #{type_label} #{circular_marker}\n"
|
|
75
131
|
end
|
|
@@ -79,26 +135,21 @@ module Taski
|
|
|
79
135
|
# Register task number if not already registered
|
|
80
136
|
@task_index_map[task_class] = @task_index_map.size + 1 unless @task_index_map.key?(task_class)
|
|
81
137
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
dependencies.each_with_index do |dep, index|
|
|
87
|
-
is_last = (index == dependencies.size - 1)
|
|
88
|
-
is_impl_candidate = is_section && TreeProgressDisplay.nested_class?(dep, task_class)
|
|
89
|
-
result += render_dependency_branch(dep, prefix, is_last, is_impl_candidate, new_ancestors)
|
|
138
|
+
node[:children].each_with_index do |child, index|
|
|
139
|
+
is_last = (index == node[:children].size - 1)
|
|
140
|
+
result += format_child_branch(child, prefix, is_last)
|
|
90
141
|
end
|
|
91
142
|
|
|
92
143
|
result
|
|
93
144
|
end
|
|
94
145
|
|
|
95
|
-
def
|
|
146
|
+
def format_child_branch(child, prefix, is_last)
|
|
96
147
|
connector = is_last ? "└── " : "├── "
|
|
97
148
|
extension = is_last ? " " : "│ "
|
|
98
|
-
|
|
149
|
+
child_tree = format_node(child, "#{prefix}#{extension}", child[:is_impl_candidate])
|
|
99
150
|
|
|
100
151
|
result = "#{prefix}#{COLORS[:tree]}#{connector}#{COLORS[:reset]}"
|
|
101
|
-
lines =
|
|
152
|
+
lines = child_tree.lines
|
|
102
153
|
result += lines.first
|
|
103
154
|
lines.drop(1).each { |line| result += line }
|
|
104
155
|
result
|
|
@@ -118,17 +169,71 @@ module Taski
|
|
|
118
169
|
end
|
|
119
170
|
end
|
|
120
171
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
attr_accessor :
|
|
172
|
+
# Tracks the progress of a group within a task
|
|
173
|
+
class GroupProgress
|
|
174
|
+
attr_accessor :name, :state, :start_time, :end_time, :duration, :error, :last_message
|
|
124
175
|
|
|
125
|
-
def initialize
|
|
176
|
+
def initialize(name)
|
|
177
|
+
@name = name
|
|
126
178
|
@state = :pending
|
|
127
179
|
@start_time = nil
|
|
128
180
|
@end_time = nil
|
|
129
|
-
@error = nil
|
|
130
181
|
@duration = nil
|
|
182
|
+
@error = nil
|
|
183
|
+
@last_message = nil
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class TaskProgress
|
|
188
|
+
# Run lifecycle tracking
|
|
189
|
+
attr_accessor :run_state, :run_start_time, :run_end_time, :run_error, :run_duration
|
|
190
|
+
# Clean lifecycle tracking
|
|
191
|
+
attr_accessor :clean_state, :clean_start_time, :clean_end_time, :clean_error, :clean_duration
|
|
192
|
+
# Display properties
|
|
193
|
+
attr_accessor :is_impl_candidate
|
|
194
|
+
# Group tracking
|
|
195
|
+
attr_accessor :groups, :current_group_index
|
|
196
|
+
|
|
197
|
+
def initialize
|
|
198
|
+
# Run lifecycle
|
|
199
|
+
@run_state = :pending
|
|
200
|
+
@run_start_time = nil
|
|
201
|
+
@run_end_time = nil
|
|
202
|
+
@run_error = nil
|
|
203
|
+
@run_duration = nil
|
|
204
|
+
# Clean lifecycle
|
|
205
|
+
@clean_state = nil # nil means clean hasn't started
|
|
206
|
+
@clean_start_time = nil
|
|
207
|
+
@clean_end_time = nil
|
|
208
|
+
@clean_error = nil
|
|
209
|
+
@clean_duration = nil
|
|
210
|
+
# Display
|
|
131
211
|
@is_impl_candidate = false
|
|
212
|
+
# Groups
|
|
213
|
+
@groups = []
|
|
214
|
+
@current_group_index = nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# For backward compatibility - returns the most relevant state for display
|
|
218
|
+
def state
|
|
219
|
+
@clean_state || @run_state
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Legacy accessors for backward compatibility
|
|
223
|
+
def start_time
|
|
224
|
+
@clean_start_time || @run_start_time
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def end_time
|
|
228
|
+
@clean_end_time || @run_end_time
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def error
|
|
232
|
+
@clean_error || @run_error
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def duration
|
|
236
|
+
@clean_duration || @run_duration
|
|
132
237
|
end
|
|
133
238
|
end
|
|
134
239
|
|
|
@@ -143,7 +248,15 @@ module Taski
|
|
|
143
248
|
@root_task_class = nil
|
|
144
249
|
@tree_structure = nil
|
|
145
250
|
@section_impl_map = {} # Section -> selected impl class
|
|
146
|
-
@
|
|
251
|
+
@output_capture = nil # ThreadOutputCapture for getting task output
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Set the output capture for getting task output
|
|
255
|
+
# @param capture [ThreadOutputCapture] The output capture instance
|
|
256
|
+
def set_output_capture(capture)
|
|
257
|
+
@monitor.synchronize do
|
|
258
|
+
@output_capture = capture
|
|
259
|
+
end
|
|
147
260
|
end
|
|
148
261
|
|
|
149
262
|
# Set the root task to build tree structure
|
|
@@ -183,7 +296,7 @@ module Taski
|
|
|
183
296
|
end
|
|
184
297
|
|
|
185
298
|
# @param task_class [Class] The task class to update
|
|
186
|
-
# @param state [Symbol] The new state (:pending, :running, :completed, :failed)
|
|
299
|
+
# @param state [Symbol] The new state (:pending, :running, :completed, :failed, :cleaning, :clean_completed, :clean_failed)
|
|
187
300
|
# @param duration [Float] Duration in milliseconds (for completed tasks)
|
|
188
301
|
# @param error [Exception] Error object (for failed tasks)
|
|
189
302
|
def update_task(task_class, state:, duration: nil, error: nil)
|
|
@@ -191,15 +304,33 @@ module Taski
|
|
|
191
304
|
progress = @tasks[task_class]
|
|
192
305
|
return unless progress
|
|
193
306
|
|
|
194
|
-
progress.state = state
|
|
195
|
-
progress.duration = duration if duration
|
|
196
|
-
progress.error = error if error
|
|
197
|
-
|
|
198
307
|
case state
|
|
308
|
+
# Run lifecycle states
|
|
309
|
+
when :pending
|
|
310
|
+
progress.run_state = :pending
|
|
199
311
|
when :running
|
|
200
|
-
progress.
|
|
201
|
-
|
|
202
|
-
|
|
312
|
+
progress.run_state = :running
|
|
313
|
+
progress.run_start_time = Time.now
|
|
314
|
+
when :completed
|
|
315
|
+
progress.run_state = :completed
|
|
316
|
+
progress.run_end_time = Time.now
|
|
317
|
+
progress.run_duration = duration if duration
|
|
318
|
+
when :failed
|
|
319
|
+
progress.run_state = :failed
|
|
320
|
+
progress.run_end_time = Time.now
|
|
321
|
+
progress.run_error = error if error
|
|
322
|
+
# Clean lifecycle states
|
|
323
|
+
when :cleaning
|
|
324
|
+
progress.clean_state = :cleaning
|
|
325
|
+
progress.clean_start_time = Time.now
|
|
326
|
+
when :clean_completed
|
|
327
|
+
progress.clean_state = :clean_completed
|
|
328
|
+
progress.clean_end_time = Time.now
|
|
329
|
+
progress.clean_duration = duration if duration
|
|
330
|
+
when :clean_failed
|
|
331
|
+
progress.clean_state = :clean_failed
|
|
332
|
+
progress.clean_end_time = Time.now
|
|
333
|
+
progress.clean_error = error if error
|
|
203
334
|
end
|
|
204
335
|
end
|
|
205
336
|
end
|
|
@@ -212,6 +343,50 @@ module Taski
|
|
|
212
343
|
end
|
|
213
344
|
end
|
|
214
345
|
|
|
346
|
+
# Update group state for a task.
|
|
347
|
+
# Called by ExecutionContext when group lifecycle events occur.
|
|
348
|
+
#
|
|
349
|
+
# @param task_class [Class] The task class containing the group
|
|
350
|
+
# @param group_name [String] The name of the group
|
|
351
|
+
# @param state [Symbol] The new state (:running, :completed, :failed)
|
|
352
|
+
# @param duration [Float, nil] Duration in milliseconds (for completed groups)
|
|
353
|
+
# @param error [Exception, nil] Error object (for failed groups)
|
|
354
|
+
def update_group(task_class, group_name, state:, duration: nil, error: nil)
|
|
355
|
+
@monitor.synchronize do
|
|
356
|
+
progress = @tasks[task_class]
|
|
357
|
+
return unless progress
|
|
358
|
+
|
|
359
|
+
case state
|
|
360
|
+
when :running
|
|
361
|
+
# Create new group and set as current
|
|
362
|
+
group = GroupProgress.new(group_name)
|
|
363
|
+
group.state = :running
|
|
364
|
+
group.start_time = Time.now
|
|
365
|
+
progress.groups << group
|
|
366
|
+
progress.current_group_index = progress.groups.size - 1
|
|
367
|
+
when :completed
|
|
368
|
+
# Find the group by name and mark completed
|
|
369
|
+
group = progress.groups.find { |g| g.name == group_name && g.state == :running }
|
|
370
|
+
if group
|
|
371
|
+
group.state = :completed
|
|
372
|
+
group.end_time = Time.now
|
|
373
|
+
group.duration = duration
|
|
374
|
+
end
|
|
375
|
+
progress.current_group_index = nil
|
|
376
|
+
when :failed
|
|
377
|
+
# Find the group by name and mark failed
|
|
378
|
+
group = progress.groups.find { |g| g.name == group_name && g.state == :running }
|
|
379
|
+
if group
|
|
380
|
+
group.state = :failed
|
|
381
|
+
group.end_time = Time.now
|
|
382
|
+
group.duration = duration
|
|
383
|
+
group.error = error
|
|
384
|
+
end
|
|
385
|
+
progress.current_group_index = nil
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
215
390
|
def start
|
|
216
391
|
should_start = false
|
|
217
392
|
@monitor.synchronize do
|
|
@@ -226,8 +401,8 @@ module Taski
|
|
|
226
401
|
|
|
227
402
|
return unless should_start
|
|
228
403
|
|
|
229
|
-
# Hide cursor
|
|
230
|
-
@output.print "\
|
|
404
|
+
@output.print "\e[?25l" # Hide cursor
|
|
405
|
+
@output.print "\e7" # Save cursor position (before any tree output)
|
|
231
406
|
@renderer_thread = Thread.new do
|
|
232
407
|
loop do
|
|
233
408
|
break unless @running
|
|
@@ -251,8 +426,7 @@ module Taski
|
|
|
251
426
|
return unless should_stop
|
|
252
427
|
|
|
253
428
|
@renderer_thread&.join
|
|
254
|
-
# Show cursor
|
|
255
|
-
@output.print "\e[?25h"
|
|
429
|
+
@output.print "\e[?25h" # Show cursor
|
|
256
430
|
render_final
|
|
257
431
|
end
|
|
258
432
|
|
|
@@ -262,38 +436,10 @@ module Taski
|
|
|
262
436
|
def build_tree_structure
|
|
263
437
|
return unless @root_task_class
|
|
264
438
|
|
|
265
|
-
@tree_structure = build_tree_node(@root_task_class
|
|
439
|
+
@tree_structure = self.class.build_tree_node(@root_task_class)
|
|
266
440
|
register_tasks_from_tree(@tree_structure)
|
|
267
441
|
end
|
|
268
442
|
|
|
269
|
-
# Build a single tree node
|
|
270
|
-
def build_tree_node(task_class, ancestors)
|
|
271
|
-
return nil if ancestors.include?(task_class)
|
|
272
|
-
|
|
273
|
-
node = {
|
|
274
|
-
task_class: task_class,
|
|
275
|
-
is_section: section_class?(task_class),
|
|
276
|
-
children: [],
|
|
277
|
-
is_impl_candidate: false
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
new_ancestors = ancestors + [task_class]
|
|
281
|
-
dependencies = StaticAnalysis::Analyzer.analyze(task_class).to_a
|
|
282
|
-
is_section = section_class?(task_class)
|
|
283
|
-
|
|
284
|
-
dependencies.each do |dep|
|
|
285
|
-
child_node = build_tree_node(dep, new_ancestors)
|
|
286
|
-
if child_node
|
|
287
|
-
# Only mark as impl candidate if parent is Section AND
|
|
288
|
-
# the dependency is a nested class of that Section
|
|
289
|
-
child_node[:is_impl_candidate] = is_section && nested_class?(dep, task_class)
|
|
290
|
-
node[:children] << child_node
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
node
|
|
295
|
-
end
|
|
296
|
-
|
|
297
443
|
# Register all tasks from tree structure
|
|
298
444
|
def register_tasks_from_tree(node)
|
|
299
445
|
return unless node
|
|
@@ -310,29 +456,25 @@ module Taski
|
|
|
310
456
|
end
|
|
311
457
|
|
|
312
458
|
def render_live
|
|
459
|
+
# Poll for new output from task pipes
|
|
460
|
+
@output_capture&.poll
|
|
461
|
+
|
|
313
462
|
lines = nil
|
|
314
|
-
line_count = nil
|
|
315
463
|
|
|
316
464
|
@monitor.synchronize do
|
|
317
465
|
@spinner_index += 1
|
|
318
466
|
lines = build_tree_display
|
|
319
|
-
line_count = @last_line_count
|
|
320
467
|
end
|
|
321
468
|
|
|
322
469
|
return if lines.nil? || lines.empty?
|
|
323
470
|
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
end
|
|
471
|
+
# Restore cursor to saved position (from start) and clear
|
|
472
|
+
@output.print "\e8" # Restore cursor position
|
|
473
|
+
@output.print "\e[J" # Clear from cursor to end of screen
|
|
328
474
|
|
|
329
475
|
# Redraw all lines
|
|
330
476
|
lines.each do |line|
|
|
331
|
-
@output.print "
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
@monitor.synchronize do
|
|
335
|
-
@last_line_count = lines.length
|
|
477
|
+
@output.print "#{line}\n"
|
|
336
478
|
end
|
|
337
479
|
|
|
338
480
|
@output.flush
|
|
@@ -343,12 +485,9 @@ module Taski
|
|
|
343
485
|
lines = build_tree_display
|
|
344
486
|
return if lines.empty?
|
|
345
487
|
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
@output.print "\e[1A\e[K"
|
|
350
|
-
end
|
|
351
|
-
end
|
|
488
|
+
# Restore cursor to saved position (from start) and clear
|
|
489
|
+
@output.print "\e8" # Restore cursor position
|
|
490
|
+
@output.print "\e[J" # Clear from cursor to end of screen
|
|
352
491
|
|
|
353
492
|
# Print final state
|
|
354
493
|
lines.each { |line| @output.puts line }
|
|
@@ -431,11 +570,12 @@ module Taski
|
|
|
431
570
|
return "#{COLORS[:dim]}#{ICONS[:skipped]}#{COLORS[:reset]} #{impl_prefix}#{name} #{type_label}#{suffix}"
|
|
432
571
|
end
|
|
433
572
|
|
|
434
|
-
|
|
573
|
+
status_icons = combined_status_icons(progress)
|
|
435
574
|
name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
|
|
436
|
-
details =
|
|
575
|
+
details = combined_task_details(progress)
|
|
576
|
+
output_suffix = task_output_suffix(task_class, progress.state)
|
|
437
577
|
|
|
438
|
-
"#{
|
|
578
|
+
"#{status_icons} #{impl_prefix}#{name} #{type_label}#{details}#{output_suffix}"
|
|
439
579
|
end
|
|
440
580
|
|
|
441
581
|
def format_unknown_task(task_class, is_selected = true)
|
|
@@ -450,12 +590,26 @@ module Taski
|
|
|
450
590
|
end
|
|
451
591
|
end
|
|
452
592
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
593
|
+
##
|
|
594
|
+
# Returns combined status icons for both run and clean phases.
|
|
595
|
+
# Shows run icon first, then clean icon if clean phase has started.
|
|
596
|
+
# @param [TaskProgress] progress - The task progress object with run_state and clean_state.
|
|
597
|
+
# @return [String] The combined ANSI-colored icons.
|
|
598
|
+
def combined_status_icons(progress)
|
|
599
|
+
run_icon = run_status_icon(progress.run_state)
|
|
600
|
+
|
|
601
|
+
# If clean phase hasn't started, only show run icon
|
|
602
|
+
return run_icon unless progress.clean_state
|
|
458
603
|
|
|
604
|
+
clean_icon = clean_status_icon(progress.clean_state)
|
|
605
|
+
"#{run_icon} #{clean_icon}"
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
##
|
|
609
|
+
# Returns the status icon for run phase.
|
|
610
|
+
# @param [Symbol] state - The run state (:pending, :running, :completed, :failed).
|
|
611
|
+
# @return [String] The ANSI-colored icon.
|
|
612
|
+
def run_status_icon(state)
|
|
459
613
|
case state
|
|
460
614
|
when :completed
|
|
461
615
|
"#{COLORS[:success]}#{ICONS[:completed]}#{COLORS[:reset]}"
|
|
@@ -468,10 +622,36 @@ module Taski
|
|
|
468
622
|
end
|
|
469
623
|
end
|
|
470
624
|
|
|
625
|
+
##
|
|
626
|
+
# Returns the status icon for clean phase.
|
|
627
|
+
# @param [Symbol] state - The clean state (:cleaning, :clean_completed, :clean_failed).
|
|
628
|
+
# @return [String] The ANSI-colored icon.
|
|
629
|
+
def clean_status_icon(state)
|
|
630
|
+
case state
|
|
631
|
+
when :cleaning
|
|
632
|
+
"#{COLORS[:running]}#{spinner_char}#{COLORS[:reset]}"
|
|
633
|
+
when :clean_completed
|
|
634
|
+
"#{COLORS[:success]}#{ICONS[:clean_completed]}#{COLORS[:reset]}"
|
|
635
|
+
when :clean_failed
|
|
636
|
+
"#{COLORS[:error]}#{ICONS[:clean_failed]}#{COLORS[:reset]}"
|
|
637
|
+
else
|
|
638
|
+
""
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
##
|
|
643
|
+
# Returns the current spinner character for animation.
|
|
644
|
+
# Cycles through SPINNER_FRAMES based on the current spinner index.
|
|
645
|
+
# @return [String] The current spinner frame character.
|
|
471
646
|
def spinner_char
|
|
472
647
|
SPINNER_FRAMES[@spinner_index % SPINNER_FRAMES.length]
|
|
473
648
|
end
|
|
474
649
|
|
|
650
|
+
##
|
|
651
|
+
# Returns a colored type label for the task class.
|
|
652
|
+
# @param task_class [Class] The task class to get the label for.
|
|
653
|
+
# @param is_selected [Boolean] Whether the task is selected (affects color).
|
|
654
|
+
# @return [String] The colored type label (Section or Task).
|
|
475
655
|
def type_label_for(task_class, is_selected = true)
|
|
476
656
|
if section_class?(task_class)
|
|
477
657
|
is_selected ? "#{COLORS[:section]}(Section)#{COLORS[:reset]}" : "#{COLORS[:dim]}(Section)#{COLORS[:reset]}"
|
|
@@ -480,24 +660,131 @@ module Taski
|
|
|
480
660
|
end
|
|
481
661
|
end
|
|
482
662
|
|
|
483
|
-
|
|
484
|
-
|
|
663
|
+
##
|
|
664
|
+
# Returns combined details for both run and clean phases.
|
|
665
|
+
# @param [TaskProgress] progress - Progress object with run_state, clean_state, etc.
|
|
666
|
+
# @return [String] Combined details for both phases.
|
|
667
|
+
def combined_task_details(progress)
|
|
668
|
+
run_detail = run_phase_details(progress)
|
|
669
|
+
clean_detail = clean_phase_details(progress)
|
|
670
|
+
|
|
671
|
+
if clean_detail.empty?
|
|
672
|
+
run_detail
|
|
673
|
+
else
|
|
674
|
+
"#{run_detail}#{clean_detail}"
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
##
|
|
679
|
+
# Returns details for the run phase only.
|
|
680
|
+
# @param [TaskProgress] progress - Progress object.
|
|
681
|
+
# @return [String] Run phase details.
|
|
682
|
+
def run_phase_details(progress)
|
|
683
|
+
case progress.run_state
|
|
485
684
|
when :completed
|
|
486
|
-
"
|
|
685
|
+
return "" unless progress.run_duration
|
|
686
|
+
" #{COLORS[:success]}(#{progress.run_duration}ms)#{COLORS[:reset]}"
|
|
487
687
|
when :failed
|
|
488
688
|
" #{COLORS[:error]}(failed)#{COLORS[:reset]}"
|
|
489
689
|
when :running
|
|
490
|
-
|
|
690
|
+
return "" unless progress.run_start_time
|
|
691
|
+
elapsed = ((Time.now - progress.run_start_time) * 1000).round(0)
|
|
491
692
|
" #{COLORS[:running]}(#{elapsed}ms)#{COLORS[:reset]}"
|
|
492
693
|
else
|
|
493
694
|
""
|
|
494
695
|
end
|
|
495
696
|
end
|
|
496
697
|
|
|
698
|
+
##
|
|
699
|
+
# Returns details for the clean phase only.
|
|
700
|
+
# @param [TaskProgress] progress - Progress object.
|
|
701
|
+
# @return [String] Clean phase details.
|
|
702
|
+
def clean_phase_details(progress)
|
|
703
|
+
case progress.clean_state
|
|
704
|
+
when :cleaning
|
|
705
|
+
return "" unless progress.clean_start_time
|
|
706
|
+
elapsed = ((Time.now - progress.clean_start_time) * 1000).round(0)
|
|
707
|
+
" #{COLORS[:running]}(cleaning #{elapsed}ms)#{COLORS[:reset]}"
|
|
708
|
+
when :clean_completed
|
|
709
|
+
return "" unless progress.clean_duration
|
|
710
|
+
" #{COLORS[:success]}(cleaned #{progress.clean_duration}ms)#{COLORS[:reset]}"
|
|
711
|
+
when :clean_failed
|
|
712
|
+
" #{COLORS[:error]}(clean failed)#{COLORS[:reset]}"
|
|
713
|
+
else
|
|
714
|
+
""
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Get task output suffix to display next to task
|
|
719
|
+
##
|
|
720
|
+
# Produces a trailing output suffix for a task when it is actively producing output.
|
|
721
|
+
#
|
|
722
|
+
# Fetches the last captured stdout/stderr line for the given task and returns a
|
|
723
|
+
# formatted, dimmed suffix containing that line only when the task `state` is
|
|
724
|
+
# `:running` or `:cleaning` and an output capture is available. The returned
|
|
725
|
+
# string is truncated to fit the terminal width (with a minimum visible length)
|
|
726
|
+
# and includes surrounding dim/reset color codes.
|
|
727
|
+
# @param [Class] task_class - The task class whose output to query.
|
|
728
|
+
# @param [Symbol] state - The task lifecycle state (only `:running` and `:cleaning` produce output).
|
|
729
|
+
# @return [String] A formatted, possibly truncated output suffix prefixed with a dim pipe, or an empty string when no output should be shown.
|
|
730
|
+
def task_output_suffix(task_class, state)
|
|
731
|
+
return "" unless state == :running || state == :cleaning
|
|
732
|
+
return "" unless @output_capture
|
|
733
|
+
|
|
734
|
+
last_line = @output_capture.last_line_for(task_class)
|
|
735
|
+
return "" unless last_line && !last_line.empty?
|
|
736
|
+
|
|
737
|
+
# Get current group name if any
|
|
738
|
+
progress = @tasks[task_class]
|
|
739
|
+
group_prefix = ""
|
|
740
|
+
if progress&.current_group_index
|
|
741
|
+
current_group = progress.groups[progress.current_group_index]
|
|
742
|
+
group_prefix = "#{current_group.name}: " if current_group
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Truncate if too long (leave space for tree structure)
|
|
746
|
+
terminal_cols = terminal_width
|
|
747
|
+
max_output_length = terminal_cols - 50
|
|
748
|
+
max_output_length = 20 if max_output_length < 20
|
|
749
|
+
|
|
750
|
+
full_output = "#{group_prefix}#{last_line}"
|
|
751
|
+
truncated = if full_output.length > max_output_length
|
|
752
|
+
full_output[0, max_output_length - 3] + "..."
|
|
753
|
+
else
|
|
754
|
+
full_output
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
" #{COLORS[:dim]}| #{truncated}#{COLORS[:reset]}"
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
##
|
|
761
|
+
# Returns the terminal width in columns.
|
|
762
|
+
# Defaults to 80 if the output IO doesn't support winsize.
|
|
763
|
+
# @return [Integer] The terminal width in columns.
|
|
764
|
+
def terminal_width
|
|
765
|
+
if @output.respond_to?(:winsize)
|
|
766
|
+
_, cols = @output.winsize
|
|
767
|
+
cols || 80
|
|
768
|
+
else
|
|
769
|
+
80
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
##
|
|
774
|
+
# Checks if a class is a Taski::Section subclass.
|
|
775
|
+
# Delegates to the class method.
|
|
776
|
+
# @param klass [Class] The class to check.
|
|
777
|
+
# @return [Boolean] true if the class is a Section.
|
|
497
778
|
def section_class?(klass)
|
|
498
779
|
self.class.section_class?(klass)
|
|
499
780
|
end
|
|
500
781
|
|
|
782
|
+
##
|
|
783
|
+
# Checks if a class is nested within another class.
|
|
784
|
+
# Delegates to the class method.
|
|
785
|
+
# @param child_class [Class] The potential nested class.
|
|
786
|
+
# @param parent_class [Class] The potential parent class.
|
|
787
|
+
# @return [Boolean] true if child_class is nested within parent_class.
|
|
501
788
|
def nested_class?(child_class, parent_class)
|
|
502
789
|
self.class.nested_class?(child_class, parent_class)
|
|
503
790
|
end
|