taski 0.8.3 → 0.9.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 +39 -0
- data/README.md +65 -50
- data/docs/GUIDE.md +41 -56
- data/examples/README.md +10 -29
- data/examples/clean_demo.rb +25 -65
- data/examples/large_tree_demo.rb +356 -0
- data/examples/message_demo.rb +0 -1
- data/examples/progress_demo.rb +13 -24
- data/examples/reexecution_demo.rb +8 -44
- data/lib/taski/execution/execution_facade.rb +150 -0
- data/lib/taski/execution/executor.rb +156 -357
- data/lib/taski/execution/registry.rb +15 -19
- data/lib/taski/execution/scheduler.rb +161 -140
- data/lib/taski/execution/task_observer.rb +41 -0
- data/lib/taski/execution/task_output_router.rb +41 -58
- data/lib/taski/execution/task_wrapper.rb +123 -219
- data/lib/taski/execution/worker_pool.rb +238 -64
- data/lib/taski/logging.rb +105 -0
- data/lib/taski/progress/layout/base.rb +600 -0
- data/lib/taski/progress/layout/filters.rb +126 -0
- data/lib/taski/progress/layout/log.rb +27 -0
- data/lib/taski/progress/layout/simple.rb +166 -0
- data/lib/taski/progress/layout/tags.rb +76 -0
- data/lib/taski/progress/layout/theme_drop.rb +84 -0
- data/lib/taski/progress/layout/tree.rb +300 -0
- data/lib/taski/progress/theme/base.rb +224 -0
- data/lib/taski/progress/theme/compact.rb +58 -0
- data/lib/taski/progress/theme/default.rb +25 -0
- data/lib/taski/progress/theme/detail.rb +48 -0
- data/lib/taski/progress/theme/plain.rb +40 -0
- data/lib/taski/static_analysis/analyzer.rb +5 -17
- data/lib/taski/static_analysis/dependency_graph.rb +19 -1
- data/lib/taski/static_analysis/visitor.rb +1 -39
- data/lib/taski/task.rb +44 -58
- data/lib/taski/test_helper/errors.rb +1 -1
- data/lib/taski/test_helper.rb +21 -35
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +60 -61
- data/sig/taski.rbs +194 -203
- metadata +31 -8
- data/examples/section_demo.rb +0 -195
- data/lib/taski/execution/base_progress_display.rb +0 -393
- data/lib/taski/execution/execution_context.rb +0 -390
- data/lib/taski/execution/plain_progress_display.rb +0 -76
- data/lib/taski/execution/simple_progress_display.rb +0 -247
- data/lib/taski/execution/tree_progress_display.rb +0 -643
- data/lib/taski/section.rb +0 -74
|
@@ -1,643 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "stringio"
|
|
4
|
-
require_relative "base_progress_display"
|
|
5
|
-
|
|
6
|
-
module Taski
|
|
7
|
-
module Execution
|
|
8
|
-
# Tree-based progress display that shows task execution in a tree structure
|
|
9
|
-
# similar to Task.tree, with real-time status updates and stdout capture.
|
|
10
|
-
class TreeProgressDisplay < BaseProgressDisplay
|
|
11
|
-
SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
12
|
-
|
|
13
|
-
# Output display settings
|
|
14
|
-
OUTPUT_RESERVED_WIDTH = 30 # Characters reserved for tree structure
|
|
15
|
-
OUTPUT_MIN_LENGTH = 70 # Minimum visible output length
|
|
16
|
-
OUTPUT_SEPARATOR = " > " # Separator before task output
|
|
17
|
-
GROUP_SEPARATOR = " | " # Separator between group name and task name
|
|
18
|
-
TRUNCATION_ELLIPSIS = "..." # Ellipsis for truncated output
|
|
19
|
-
|
|
20
|
-
# Display settings
|
|
21
|
-
RENDER_INTERVAL = 0.1 # Seconds between display updates
|
|
22
|
-
DEFAULT_TERMINAL_WIDTH = 80 # Default terminal width when unknown
|
|
23
|
-
DEFAULT_TERMINAL_HEIGHT = 24 # Default terminal height when unknown
|
|
24
|
-
|
|
25
|
-
# ANSI color codes (matching Task.tree)
|
|
26
|
-
COLORS = {
|
|
27
|
-
reset: "\e[0m",
|
|
28
|
-
task: "\e[32m", # green
|
|
29
|
-
section: "\e[34m", # blue
|
|
30
|
-
impl: "\e[33m", # yellow
|
|
31
|
-
tree: "\e[90m", # gray
|
|
32
|
-
name: "\e[1m", # bold
|
|
33
|
-
success: "\e[32m", # green
|
|
34
|
-
error: "\e[31m", # red
|
|
35
|
-
running: "\e[36m", # cyan
|
|
36
|
-
pending: "\e[90m", # gray
|
|
37
|
-
dim: "\e[2m" # dim
|
|
38
|
-
}.freeze
|
|
39
|
-
|
|
40
|
-
# Status icons
|
|
41
|
-
ICONS = {
|
|
42
|
-
# Run lifecycle states
|
|
43
|
-
pending: "⏸", # Pause for waiting
|
|
44
|
-
running_prefix: "", # Will use spinner
|
|
45
|
-
completed: "✓",
|
|
46
|
-
failed: "✗",
|
|
47
|
-
skipped: "⊘", # Prohibition sign for unselected impl candidates
|
|
48
|
-
# Clean lifecycle states
|
|
49
|
-
cleaning_prefix: "", # Will use spinner
|
|
50
|
-
clean_completed: "♻",
|
|
51
|
-
clean_failed: "✗"
|
|
52
|
-
}.freeze
|
|
53
|
-
|
|
54
|
-
##
|
|
55
|
-
# Checks if a class is a Taski::Section subclass.
|
|
56
|
-
# @param klass [Class] The class to check.
|
|
57
|
-
# @return [Boolean] true if the class is a Section.
|
|
58
|
-
def self.section_class?(klass)
|
|
59
|
-
defined?(Taski::Section) && klass < Taski::Section
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
##
|
|
63
|
-
# Checks if a class is nested within another class by name prefix.
|
|
64
|
-
# @param child_class [Class] The potential nested class.
|
|
65
|
-
# @param parent_class [Class] The potential parent class.
|
|
66
|
-
# @return [Boolean] true if child_class name starts with parent_class name and "::".
|
|
67
|
-
def self.nested_class?(child_class, parent_class)
|
|
68
|
-
child_name = child_class.name.to_s
|
|
69
|
-
parent_name = parent_class.name.to_s
|
|
70
|
-
child_name.start_with?("#{parent_name}::")
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Build a tree structure from a root task class.
|
|
74
|
-
# This is the shared tree building logic used by both static and progress display.
|
|
75
|
-
#
|
|
76
|
-
# @param task_class [Class] The task class to build tree for
|
|
77
|
-
# @param ancestors [Set] Set of ancestor task classes for circular detection
|
|
78
|
-
# @return [Hash, nil] Tree node hash or nil if circular
|
|
79
|
-
#
|
|
80
|
-
# Tree node structure:
|
|
81
|
-
# {
|
|
82
|
-
# task_class: Class, # The task class
|
|
83
|
-
# is_section: Boolean, # Whether this is a Section
|
|
84
|
-
# is_circular: Boolean, # Whether this is a circular reference
|
|
85
|
-
# is_impl_candidate: Boolean, # Whether this is an impl candidate
|
|
86
|
-
# children: Array<Hash> # Child nodes
|
|
87
|
-
# }
|
|
88
|
-
def self.build_tree_node(task_class, ancestors = Set.new)
|
|
89
|
-
is_circular = ancestors.include?(task_class)
|
|
90
|
-
|
|
91
|
-
node = {
|
|
92
|
-
task_class: task_class,
|
|
93
|
-
is_section: section_class?(task_class),
|
|
94
|
-
is_circular: is_circular,
|
|
95
|
-
is_impl_candidate: false,
|
|
96
|
-
children: []
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
# Don't traverse children for circular references
|
|
100
|
-
return node if is_circular
|
|
101
|
-
|
|
102
|
-
new_ancestors = ancestors + [task_class]
|
|
103
|
-
dependencies = StaticAnalysis::Analyzer.analyze(task_class).to_a
|
|
104
|
-
is_section = section_class?(task_class)
|
|
105
|
-
|
|
106
|
-
dependencies.each do |dep|
|
|
107
|
-
child_node = build_tree_node(dep, new_ancestors)
|
|
108
|
-
child_node[:is_impl_candidate] = is_section && nested_class?(dep, task_class)
|
|
109
|
-
node[:children] << child_node
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
node
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Render a static tree structure for a task class (used by Task.tree)
|
|
116
|
-
# @param root_task_class [Class] The root task class
|
|
117
|
-
# @return [String] The rendered tree string
|
|
118
|
-
def self.render_static_tree(root_task_class)
|
|
119
|
-
tree = build_tree_node(root_task_class)
|
|
120
|
-
formatter = StaticTreeFormatter.new
|
|
121
|
-
formatter.format(tree)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Formatter for static tree display (no progress tracking, uses task numbers)
|
|
125
|
-
class StaticTreeFormatter
|
|
126
|
-
def format(tree)
|
|
127
|
-
@task_index_map = {}
|
|
128
|
-
format_node(tree, "", false)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
private
|
|
132
|
-
|
|
133
|
-
def format_node(node, prefix, is_impl)
|
|
134
|
-
task_class = node[:task_class]
|
|
135
|
-
type_label = colored_type_label(task_class)
|
|
136
|
-
impl_prefix = is_impl ? "#{COLORS[:impl]}[impl]#{COLORS[:reset]} " : ""
|
|
137
|
-
task_number = get_task_number(task_class)
|
|
138
|
-
name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
|
|
139
|
-
|
|
140
|
-
if node[:is_circular]
|
|
141
|
-
circular_marker = "#{COLORS[:impl]}(circular)#{COLORS[:reset]}"
|
|
142
|
-
return "#{impl_prefix}#{task_number} #{name} #{type_label} #{circular_marker}\n"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
result = "#{impl_prefix}#{task_number} #{name} #{type_label}\n"
|
|
146
|
-
|
|
147
|
-
# Register task number if not already registered
|
|
148
|
-
@task_index_map[task_class] = @task_index_map.size + 1 unless @task_index_map.key?(task_class)
|
|
149
|
-
|
|
150
|
-
node[:children].each_with_index do |child, index|
|
|
151
|
-
is_last = (index == node[:children].size - 1)
|
|
152
|
-
result += format_child_branch(child, prefix, is_last)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
result
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def format_child_branch(child, prefix, is_last)
|
|
159
|
-
connector = is_last ? "└── " : "├── "
|
|
160
|
-
extension = is_last ? " " : "│ "
|
|
161
|
-
child_tree = format_node(child, "#{prefix}#{extension}", child[:is_impl_candidate])
|
|
162
|
-
|
|
163
|
-
result = "#{prefix}#{COLORS[:tree]}#{connector}#{COLORS[:reset]}"
|
|
164
|
-
lines = child_tree.lines
|
|
165
|
-
result += lines.first
|
|
166
|
-
lines.drop(1).each { |line| result += line }
|
|
167
|
-
result
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def get_task_number(task_class)
|
|
171
|
-
number = @task_index_map[task_class] || (@task_index_map.size + 1)
|
|
172
|
-
"#{COLORS[:tree]}[#{number}]#{COLORS[:reset]}"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def colored_type_label(klass)
|
|
176
|
-
if TreeProgressDisplay.section_class?(klass)
|
|
177
|
-
"#{COLORS[:section]}(Section)#{COLORS[:reset]}"
|
|
178
|
-
else
|
|
179
|
-
"#{COLORS[:task]}(Task)#{COLORS[:reset]}"
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def initialize(output: $stdout)
|
|
185
|
-
super
|
|
186
|
-
@spinner_index = 0
|
|
187
|
-
@renderer_thread = nil
|
|
188
|
-
@running = false
|
|
189
|
-
@tree_structure = nil
|
|
190
|
-
@section_impl_map = {} # Section -> selected impl class
|
|
191
|
-
@last_line_count = 0 # Track number of lines drawn for cursor movement
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
protected
|
|
195
|
-
|
|
196
|
-
# Template method: Called when root task is set
|
|
197
|
-
def on_root_task_set
|
|
198
|
-
build_tree_structure
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# Template method: Called when a section impl is registered
|
|
202
|
-
def on_section_impl_registered(section_class, impl_class)
|
|
203
|
-
@section_impl_map[section_class] = impl_class
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Template method: Determine if display should activate
|
|
207
|
-
def should_activate?
|
|
208
|
-
tty?
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Template method: Called when display starts
|
|
212
|
-
def on_start
|
|
213
|
-
@running = true
|
|
214
|
-
@output.print "\e[?1049h" # Switch to alternate screen buffer
|
|
215
|
-
@output.print "\e[H" # Move cursor to home (top-left)
|
|
216
|
-
@output.print "\e[?25l" # Hide cursor
|
|
217
|
-
@renderer_thread = Thread.new do
|
|
218
|
-
loop do
|
|
219
|
-
break unless @running
|
|
220
|
-
render_live
|
|
221
|
-
sleep RENDER_INTERVAL
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Template method: Called when display stops
|
|
227
|
-
def on_stop
|
|
228
|
-
@running = false
|
|
229
|
-
@renderer_thread&.join
|
|
230
|
-
@output.print "\e[?25h" # Show cursor
|
|
231
|
-
@output.print "\e[?1049l" # Switch back to main screen buffer
|
|
232
|
-
render_final
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
private
|
|
236
|
-
|
|
237
|
-
# Build tree structure from root task for display
|
|
238
|
-
def build_tree_structure
|
|
239
|
-
return unless @root_task_class
|
|
240
|
-
|
|
241
|
-
@tree_structure = self.class.build_tree_node(@root_task_class)
|
|
242
|
-
register_tasks_from_tree(@tree_structure)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def render_live
|
|
246
|
-
# Poll for new output from task pipes
|
|
247
|
-
@output_capture&.poll
|
|
248
|
-
|
|
249
|
-
lines = nil
|
|
250
|
-
|
|
251
|
-
@monitor.synchronize do
|
|
252
|
-
@spinner_index += 1
|
|
253
|
-
lines = build_tree_display
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
return if lines.nil? || lines.empty?
|
|
257
|
-
|
|
258
|
-
# Build complete frame in buffer for single write (flicker-free)
|
|
259
|
-
buffer = build_frame_buffer(lines)
|
|
260
|
-
|
|
261
|
-
# Write entire frame in single operation
|
|
262
|
-
@output.print buffer
|
|
263
|
-
@output.flush
|
|
264
|
-
|
|
265
|
-
@last_line_count = lines.size
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
##
|
|
269
|
-
# Builds a complete frame buffer for flicker-free rendering.
|
|
270
|
-
# Uses cursor home positioning and line-by-line overwrite instead of clear.
|
|
271
|
-
# @param lines [Array<String>] The lines to render.
|
|
272
|
-
# @return [String] The complete frame buffer ready for single write.
|
|
273
|
-
def build_frame_buffer(lines)
|
|
274
|
-
buffer = +""
|
|
275
|
-
|
|
276
|
-
# Move cursor to home position (top-left) for overwrite
|
|
277
|
-
buffer << "\e[H"
|
|
278
|
-
|
|
279
|
-
# Build each line with clear-to-end-of-line for clean overwrite
|
|
280
|
-
lines.each do |line|
|
|
281
|
-
buffer << line
|
|
282
|
-
buffer << "\e[K" # Clear from cursor to end of line (removes old content)
|
|
283
|
-
buffer << "\n"
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# Clear any extra lines from previous render if current has fewer lines
|
|
287
|
-
if @last_line_count > lines.size
|
|
288
|
-
(@last_line_count - lines.size).times do
|
|
289
|
-
buffer << "\e[K\n" # Clear line and move to next
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
buffer
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def render_final
|
|
297
|
-
@monitor.synchronize do
|
|
298
|
-
return unless @root_task_class
|
|
299
|
-
|
|
300
|
-
root_progress = @tasks[@root_task_class]
|
|
301
|
-
return unless root_progress
|
|
302
|
-
|
|
303
|
-
# Print single summary line instead of full tree
|
|
304
|
-
@output.puts build_summary_line(@root_task_class, root_progress)
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def build_summary_line(task_class, progress)
|
|
309
|
-
# Determine overall status and icon
|
|
310
|
-
if progress.run_state == :failed || progress.clean_state == :clean_failed
|
|
311
|
-
icon = "#{COLORS[:error]}#{ICONS[:failed]}#{COLORS[:reset]}"
|
|
312
|
-
status = "#{COLORS[:error]}failed#{COLORS[:reset]}"
|
|
313
|
-
else
|
|
314
|
-
icon = "#{COLORS[:success]}#{ICONS[:completed]}#{COLORS[:reset]}"
|
|
315
|
-
status = "#{COLORS[:success]}completed#{COLORS[:reset]}"
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
|
|
319
|
-
|
|
320
|
-
# Calculate total duration
|
|
321
|
-
duration_str = ""
|
|
322
|
-
if progress.run_duration
|
|
323
|
-
duration_str = " (#{progress.run_duration}ms)"
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# Count completed tasks
|
|
327
|
-
completed_count = @tasks.values.count { |p| p.run_state == :completed }
|
|
328
|
-
total_count = @tasks.values.count { |p| p.run_state != :pending || p == progress }
|
|
329
|
-
task_count_str = " [#{completed_count}/#{total_count} tasks]"
|
|
330
|
-
|
|
331
|
-
"#{icon} #{name} #{status}#{duration_str}#{task_count_str}"
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
# Build display lines from tree structure
|
|
335
|
-
def build_tree_display
|
|
336
|
-
return [] unless @tree_structure
|
|
337
|
-
|
|
338
|
-
lines = []
|
|
339
|
-
build_root_tree_lines(@tree_structure, "", lines)
|
|
340
|
-
lines
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# Build tree lines starting from root node
|
|
344
|
-
# @param node [Hash] Tree node (root)
|
|
345
|
-
# @param prefix [String] Line prefix for tree drawing
|
|
346
|
-
# @param lines [Array<String>] Accumulated output lines
|
|
347
|
-
def build_root_tree_lines(node, prefix, lines)
|
|
348
|
-
task_class = node[:task_class]
|
|
349
|
-
progress = @tasks[task_class]
|
|
350
|
-
|
|
351
|
-
# Root node is never an impl candidate and is always selected
|
|
352
|
-
line = format_tree_line(task_class, progress, false, true)
|
|
353
|
-
lines << "#{prefix}#{line}"
|
|
354
|
-
|
|
355
|
-
render_children(node, prefix, lines, task_class, true)
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# Render all children of a node recursively
|
|
359
|
-
# @param node [Hash] Tree node
|
|
360
|
-
# @param prefix [String] Line prefix for tree drawing
|
|
361
|
-
# @param lines [Array<String>] Accumulated output lines
|
|
362
|
-
# @param parent_task_class [Class] Parent task class (for impl selection lookup)
|
|
363
|
-
# @param ancestor_selected [Boolean] Whether all ancestor impl candidates were selected
|
|
364
|
-
def render_children(node, prefix, lines, parent_task_class, ancestor_selected)
|
|
365
|
-
children = node[:children]
|
|
366
|
-
children.each_with_index do |child, index|
|
|
367
|
-
is_last = (index == children.size - 1)
|
|
368
|
-
connector = is_last ? "└── " : "├── "
|
|
369
|
-
extension = is_last ? " " : "│ "
|
|
370
|
-
|
|
371
|
-
child_progress = @tasks[child[:task_class]]
|
|
372
|
-
|
|
373
|
-
# Determine child's selection status
|
|
374
|
-
child_is_selected = true
|
|
375
|
-
if child[:is_impl_candidate]
|
|
376
|
-
selected_impl = @section_impl_map[parent_task_class]
|
|
377
|
-
child_is_selected = (selected_impl == child[:task_class])
|
|
378
|
-
end
|
|
379
|
-
# Propagate ancestor selection state
|
|
380
|
-
child_effective_selected = ancestor_selected && child_is_selected
|
|
381
|
-
|
|
382
|
-
child_line = format_tree_line(
|
|
383
|
-
child[:task_class],
|
|
384
|
-
child_progress,
|
|
385
|
-
child[:is_impl_candidate],
|
|
386
|
-
child_effective_selected
|
|
387
|
-
)
|
|
388
|
-
lines << "#{prefix}#{COLORS[:tree]}#{connector}#{COLORS[:reset]}#{child_line}"
|
|
389
|
-
|
|
390
|
-
if child[:children].any?
|
|
391
|
-
render_children(child, "#{prefix}#{COLORS[:tree]}#{extension}#{COLORS[:reset]}", lines, child[:task_class], child_effective_selected)
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
def format_tree_line(task_class, progress, is_impl, is_selected)
|
|
397
|
-
return format_unknown_task(task_class, is_selected) unless progress
|
|
398
|
-
|
|
399
|
-
type_label = type_label_for(task_class, is_selected)
|
|
400
|
-
impl_prefix = is_impl ? "#{COLORS[:impl]}[impl]#{COLORS[:reset]} " : ""
|
|
401
|
-
|
|
402
|
-
# Handle unselected nodes (either impl candidates or children of unselected impl)
|
|
403
|
-
# Show dimmed regardless of task state since they belong to unselected branch
|
|
404
|
-
unless is_selected
|
|
405
|
-
name = "#{COLORS[:dim]}#{task_class.name}#{COLORS[:reset]}"
|
|
406
|
-
suffix = is_impl ? " #{COLORS[:dim]}(not selected)#{COLORS[:reset]}" : ""
|
|
407
|
-
return "#{COLORS[:dim]}#{ICONS[:skipped]}#{COLORS[:reset]} #{impl_prefix}#{name} #{type_label}#{suffix}"
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
status_icons = combined_status_icons(progress)
|
|
411
|
-
name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
|
|
412
|
-
details = combined_task_details(progress)
|
|
413
|
-
output_suffix = task_output_suffix(task_class, progress.state)
|
|
414
|
-
|
|
415
|
-
"#{status_icons} #{impl_prefix}#{name} #{type_label}#{details}#{output_suffix}"
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def format_unknown_task(task_class, is_selected = true)
|
|
419
|
-
if is_selected
|
|
420
|
-
name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
|
|
421
|
-
type_label = type_label_for(task_class, true)
|
|
422
|
-
"#{COLORS[:pending]}#{ICONS[:pending]}#{COLORS[:reset]} #{name} #{type_label}"
|
|
423
|
-
else
|
|
424
|
-
name = "#{COLORS[:dim]}#{task_class.name}#{COLORS[:reset]}"
|
|
425
|
-
type_label = type_label_for(task_class, false)
|
|
426
|
-
"#{COLORS[:dim]}#{ICONS[:skipped]}#{COLORS[:reset]} #{name} #{type_label}"
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
##
|
|
431
|
-
# Returns combined status icons for both run and clean phases.
|
|
432
|
-
# Shows run icon first, then clean icon if clean phase has started.
|
|
433
|
-
# @param [TaskProgress] progress - The task progress object with run_state and clean_state.
|
|
434
|
-
# @return [String] The combined ANSI-colored icons.
|
|
435
|
-
def combined_status_icons(progress)
|
|
436
|
-
run_icon = run_status_icon(progress.run_state)
|
|
437
|
-
|
|
438
|
-
# If clean phase hasn't started, only show run icon
|
|
439
|
-
return run_icon unless progress.clean_state
|
|
440
|
-
|
|
441
|
-
clean_icon = clean_status_icon(progress.clean_state)
|
|
442
|
-
"#{run_icon} #{clean_icon}"
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
##
|
|
446
|
-
# Returns the status icon for run phase.
|
|
447
|
-
# @param [Symbol] state - The run state (:pending, :running, :completed, :failed).
|
|
448
|
-
# @return [String] The ANSI-colored icon.
|
|
449
|
-
def run_status_icon(state)
|
|
450
|
-
case state
|
|
451
|
-
when :completed
|
|
452
|
-
"#{COLORS[:success]}#{ICONS[:completed]}#{COLORS[:reset]}"
|
|
453
|
-
when :failed
|
|
454
|
-
"#{COLORS[:error]}#{ICONS[:failed]}#{COLORS[:reset]}"
|
|
455
|
-
when :running
|
|
456
|
-
"#{COLORS[:running]}#{spinner_char}#{COLORS[:reset]}"
|
|
457
|
-
else
|
|
458
|
-
"#{COLORS[:pending]}#{ICONS[:pending]}#{COLORS[:reset]}"
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
##
|
|
463
|
-
# Returns the status icon for clean phase.
|
|
464
|
-
# @param [Symbol] state - The clean state (:cleaning, :clean_completed, :clean_failed).
|
|
465
|
-
# @return [String] The ANSI-colored icon.
|
|
466
|
-
def clean_status_icon(state)
|
|
467
|
-
case state
|
|
468
|
-
when :cleaning
|
|
469
|
-
"#{COLORS[:running]}#{spinner_char}#{COLORS[:reset]}"
|
|
470
|
-
when :clean_completed
|
|
471
|
-
"#{COLORS[:success]}#{ICONS[:clean_completed]}#{COLORS[:reset]}"
|
|
472
|
-
when :clean_failed
|
|
473
|
-
"#{COLORS[:error]}#{ICONS[:clean_failed]}#{COLORS[:reset]}"
|
|
474
|
-
else
|
|
475
|
-
""
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
##
|
|
480
|
-
# Returns the current spinner character for animation.
|
|
481
|
-
# Cycles through SPINNER_FRAMES based on the current spinner index.
|
|
482
|
-
# @return [String] The current spinner frame character.
|
|
483
|
-
def spinner_char
|
|
484
|
-
SPINNER_FRAMES[@spinner_index % SPINNER_FRAMES.length]
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
##
|
|
488
|
-
# Returns a colored type label for the task class.
|
|
489
|
-
# @param task_class [Class] The task class to get the label for.
|
|
490
|
-
# @param is_selected [Boolean] Whether the task is selected (affects color).
|
|
491
|
-
# @return [String] The colored type label (Section or Task).
|
|
492
|
-
def type_label_for(task_class, is_selected = true)
|
|
493
|
-
if section_class?(task_class)
|
|
494
|
-
is_selected ? "#{COLORS[:section]}(Section)#{COLORS[:reset]}" : "#{COLORS[:dim]}(Section)#{COLORS[:reset]}"
|
|
495
|
-
else
|
|
496
|
-
is_selected ? "#{COLORS[:task]}(Task)#{COLORS[:reset]}" : "#{COLORS[:dim]}(Task)#{COLORS[:reset]}"
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
##
|
|
501
|
-
# Returns combined details for both run and clean phases.
|
|
502
|
-
# @param [TaskProgress] progress - Progress object with run_state, clean_state, etc.
|
|
503
|
-
# @return [String] Combined details for both phases.
|
|
504
|
-
def combined_task_details(progress)
|
|
505
|
-
run_detail = run_phase_details(progress)
|
|
506
|
-
clean_detail = clean_phase_details(progress)
|
|
507
|
-
|
|
508
|
-
if clean_detail.empty?
|
|
509
|
-
run_detail
|
|
510
|
-
else
|
|
511
|
-
"#{run_detail}#{clean_detail}"
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
##
|
|
516
|
-
# Returns details for the run phase only.
|
|
517
|
-
# @param [TaskProgress] progress - Progress object.
|
|
518
|
-
# @return [String] Run phase details.
|
|
519
|
-
def run_phase_details(progress)
|
|
520
|
-
case progress.run_state
|
|
521
|
-
when :completed
|
|
522
|
-
return "" unless progress.run_duration
|
|
523
|
-
" #{COLORS[:success]}(#{progress.run_duration}ms)#{COLORS[:reset]}"
|
|
524
|
-
when :failed
|
|
525
|
-
" #{COLORS[:error]}(failed)#{COLORS[:reset]}"
|
|
526
|
-
when :running
|
|
527
|
-
return "" unless progress.run_start_time
|
|
528
|
-
elapsed = ((Time.now - progress.run_start_time) * 1000).round(0)
|
|
529
|
-
" #{COLORS[:running]}(#{elapsed}ms)#{COLORS[:reset]}"
|
|
530
|
-
else
|
|
531
|
-
""
|
|
532
|
-
end
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
##
|
|
536
|
-
# Returns details for the clean phase only.
|
|
537
|
-
# @param [TaskProgress] progress - Progress object.
|
|
538
|
-
# @return [String] Clean phase details.
|
|
539
|
-
def clean_phase_details(progress)
|
|
540
|
-
case progress.clean_state
|
|
541
|
-
when :cleaning
|
|
542
|
-
return "" unless progress.clean_start_time
|
|
543
|
-
elapsed = ((Time.now - progress.clean_start_time) * 1000).round(0)
|
|
544
|
-
" #{COLORS[:running]}(cleaning #{elapsed}ms)#{COLORS[:reset]}"
|
|
545
|
-
when :clean_completed
|
|
546
|
-
return "" unless progress.clean_duration
|
|
547
|
-
" #{COLORS[:success]}(cleaned #{progress.clean_duration}ms)#{COLORS[:reset]}"
|
|
548
|
-
when :clean_failed
|
|
549
|
-
" #{COLORS[:error]}(clean failed)#{COLORS[:reset]}"
|
|
550
|
-
else
|
|
551
|
-
""
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
# Get task output suffix to display next to task
|
|
556
|
-
##
|
|
557
|
-
# Produces a trailing output suffix for a task when it is actively producing output.
|
|
558
|
-
#
|
|
559
|
-
# Fetches the last captured stdout/stderr line for the given task and returns a
|
|
560
|
-
# formatted, dimmed suffix containing that line only when the task `state` is
|
|
561
|
-
# `:running` or `:cleaning` and an output capture is available. The returned
|
|
562
|
-
# string is truncated to fit the terminal width (with a minimum visible length)
|
|
563
|
-
# and includes surrounding dim/reset color codes.
|
|
564
|
-
# @param [Class] task_class - The task class whose output to query.
|
|
565
|
-
# @param [Symbol] state - The task lifecycle state (only `:running` and `:cleaning` produce output).
|
|
566
|
-
# @return [String] A formatted, possibly truncated output suffix prefixed with a dim pipe, or an empty string when no output should be shown.
|
|
567
|
-
def task_output_suffix(task_class, state)
|
|
568
|
-
return "" unless state == :running || state == :cleaning
|
|
569
|
-
return "" unless @output_capture
|
|
570
|
-
|
|
571
|
-
last_line = @output_capture.last_line_for(task_class)
|
|
572
|
-
return "" unless last_line && !last_line.empty?
|
|
573
|
-
|
|
574
|
-
# Get current group name if any
|
|
575
|
-
progress = @tasks[task_class]
|
|
576
|
-
group_prefix = ""
|
|
577
|
-
if progress&.current_group_index
|
|
578
|
-
current_group = progress.groups[progress.current_group_index]
|
|
579
|
-
group_prefix = "#{current_group.name}#{GROUP_SEPARATOR}" if current_group
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
# Truncate if too long (leave space for tree structure)
|
|
583
|
-
terminal_cols = terminal_width
|
|
584
|
-
max_output_length = terminal_cols - OUTPUT_RESERVED_WIDTH
|
|
585
|
-
max_output_length = OUTPUT_MIN_LENGTH if max_output_length < OUTPUT_MIN_LENGTH
|
|
586
|
-
|
|
587
|
-
full_output = "#{group_prefix}#{last_line}"
|
|
588
|
-
truncated = if full_output.length > max_output_length
|
|
589
|
-
full_output[0, max_output_length - TRUNCATION_ELLIPSIS.length] + TRUNCATION_ELLIPSIS
|
|
590
|
-
else
|
|
591
|
-
full_output
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
"#{COLORS[:dim]}#{OUTPUT_SEPARATOR}#{truncated}#{COLORS[:reset]}"
|
|
595
|
-
end
|
|
596
|
-
|
|
597
|
-
##
|
|
598
|
-
# Returns the terminal width in columns.
|
|
599
|
-
# Defaults to 80 if the output IO doesn't support winsize.
|
|
600
|
-
# @return [Integer] The terminal width in columns.
|
|
601
|
-
def terminal_width
|
|
602
|
-
if @output.respond_to?(:winsize)
|
|
603
|
-
_, cols = @output.winsize
|
|
604
|
-
cols || DEFAULT_TERMINAL_WIDTH
|
|
605
|
-
else
|
|
606
|
-
DEFAULT_TERMINAL_WIDTH
|
|
607
|
-
end
|
|
608
|
-
end
|
|
609
|
-
|
|
610
|
-
##
|
|
611
|
-
# Returns the terminal height in rows.
|
|
612
|
-
# Defaults to 24 if the output IO doesn't support winsize.
|
|
613
|
-
# @return [Integer] The terminal height in rows.
|
|
614
|
-
def terminal_height
|
|
615
|
-
if @output.respond_to?(:winsize)
|
|
616
|
-
rows, _ = @output.winsize
|
|
617
|
-
rows || DEFAULT_TERMINAL_HEIGHT
|
|
618
|
-
else
|
|
619
|
-
DEFAULT_TERMINAL_HEIGHT
|
|
620
|
-
end
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
##
|
|
624
|
-
# Checks if a class is a Taski::Section subclass.
|
|
625
|
-
# Delegates to the class method.
|
|
626
|
-
# @param klass [Class] The class to check.
|
|
627
|
-
# @return [Boolean] true if the class is a Section.
|
|
628
|
-
def section_class?(klass)
|
|
629
|
-
self.class.section_class?(klass)
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
##
|
|
633
|
-
# Checks if a class is nested within another class.
|
|
634
|
-
# Delegates to the class method.
|
|
635
|
-
# @param child_class [Class] The potential nested class.
|
|
636
|
-
# @param parent_class [Class] The potential parent class.
|
|
637
|
-
# @return [Boolean] true if child_class is nested within parent_class.
|
|
638
|
-
def nested_class?(child_class, parent_class)
|
|
639
|
-
self.class.nested_class?(child_class, parent_class)
|
|
640
|
-
end
|
|
641
|
-
end
|
|
642
|
-
end
|
|
643
|
-
end
|