taski 0.2.2 → 0.3.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/README.md +130 -7
- data/examples/README.md +13 -1
- data/examples/section_configuration.rb +212 -0
- data/examples/tree_demo.rb +125 -0
- data/lib/taski/dependency_analyzer.rb +104 -45
- data/lib/taski/exceptions.rb +3 -0
- data/lib/taski/logger.rb +7 -62
- data/lib/taski/logging/formatter_factory.rb +34 -0
- data/lib/taski/logging/formatter_interface.rb +19 -0
- data/lib/taski/logging/json_formatter.rb +26 -0
- data/lib/taski/logging/simple_formatter.rb +16 -0
- data/lib/taski/logging/structured_formatter.rb +44 -0
- data/lib/taski/progress/display_colors.rb +17 -0
- data/lib/taski/progress/display_manager.rb +115 -0
- data/lib/taski/progress/output_capture.rb +105 -0
- data/lib/taski/progress/spinner_animation.rb +46 -0
- data/lib/taski/progress/task_formatter.rb +25 -0
- data/lib/taski/progress/task_status.rb +38 -0
- data/lib/taski/progress/terminal_controller.rb +35 -0
- data/lib/taski/progress_display.rb +23 -320
- data/lib/taski/section.rb +268 -0
- data/lib/taski/task/base.rb +11 -32
- data/lib/taski/task/dependency_resolver.rb +4 -64
- data/lib/taski/task/instance_management.rb +28 -15
- data/lib/taski/tree_colors.rb +91 -0
- data/lib/taski/utils/dependency_resolver_helper.rb +85 -0
- data/lib/taski/utils/tree_display_helper.rb +71 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +4 -0
- metadata +20 -3
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Progress
|
5
|
+
# Spinner animation with dots-style characters
|
6
|
+
class SpinnerAnimation
|
7
|
+
SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
|
8
|
+
FRAME_DELAY = 0.1
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@frame = 0
|
12
|
+
@running = false
|
13
|
+
@thread = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def start(terminal, task_name, &display_callback)
|
17
|
+
return if @running
|
18
|
+
|
19
|
+
@running = true
|
20
|
+
@frame = 0
|
21
|
+
|
22
|
+
@thread = Thread.new do
|
23
|
+
while @running
|
24
|
+
current_char = SPINNER_CHARS[@frame % SPINNER_CHARS.length]
|
25
|
+
display_callback&.call(current_char, task_name)
|
26
|
+
|
27
|
+
@frame += 1
|
28
|
+
sleep FRAME_DELAY
|
29
|
+
end
|
30
|
+
rescue
|
31
|
+
# Silently handle thread errors
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
@running = false
|
37
|
+
@thread&.join(0.2)
|
38
|
+
@thread = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def running?
|
42
|
+
@running
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "display_colors"
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
module Progress
|
7
|
+
# Handles formatting of task display messages
|
8
|
+
class TaskFormatter
|
9
|
+
include DisplayColors
|
10
|
+
|
11
|
+
def format_completed_task(status)
|
12
|
+
color = status.success? ? COLORS[:green] : COLORS[:red]
|
13
|
+
"#{color}#{COLORS[:bold]}#{status.icon} #{status.name}#{COLORS[:reset]} #{COLORS[:dim]}#{status.format_duration}#{COLORS[:reset]}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def format_current_task(spinner_char, task_name)
|
17
|
+
"#{COLORS[:cyan]}#{spinner_char}#{COLORS[:reset]} #{COLORS[:bold]}#{task_name}#{COLORS[:reset]}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_output_line(line)
|
21
|
+
" #{COLORS[:dim]}#{line}#{COLORS[:reset]}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Progress
|
5
|
+
# Represents task execution status
|
6
|
+
class TaskStatus
|
7
|
+
attr_reader :name, :duration, :error
|
8
|
+
|
9
|
+
def initialize(name:, duration: nil, error: nil)
|
10
|
+
@name = name
|
11
|
+
@duration = duration
|
12
|
+
@error = error
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
@error.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure?
|
20
|
+
!success?
|
21
|
+
end
|
22
|
+
|
23
|
+
def duration_ms
|
24
|
+
return nil unless @duration
|
25
|
+
(@duration * 1000).round(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
def icon
|
29
|
+
success? ? "✅" : "❌"
|
30
|
+
end
|
31
|
+
|
32
|
+
def format_duration
|
33
|
+
return "" unless duration_ms
|
34
|
+
"(#{duration_ms}ms)"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Progress
|
5
|
+
# Terminal control operations with ANSI escape sequences
|
6
|
+
class TerminalController
|
7
|
+
# ANSI escape sequences
|
8
|
+
MOVE_UP = "\033[A"
|
9
|
+
CLEAR_LINE = "\033[K"
|
10
|
+
MOVE_UP_AND_CLEAR = "#{MOVE_UP}#{CLEAR_LINE}"
|
11
|
+
|
12
|
+
def initialize(output)
|
13
|
+
@output = output
|
14
|
+
end
|
15
|
+
|
16
|
+
def clear_lines(count)
|
17
|
+
return if count == 0
|
18
|
+
|
19
|
+
count.times { @output.print MOVE_UP_AND_CLEAR }
|
20
|
+
end
|
21
|
+
|
22
|
+
def puts(text)
|
23
|
+
@output.puts text
|
24
|
+
end
|
25
|
+
|
26
|
+
def print(text)
|
27
|
+
@output.print text
|
28
|
+
end
|
29
|
+
|
30
|
+
def flush
|
31
|
+
@output.flush
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,356 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "progress/terminal_controller"
|
4
|
+
require_relative "progress/spinner_animation"
|
5
|
+
require_relative "progress/output_capture"
|
6
|
+
require_relative "progress/display_manager"
|
4
7
|
|
5
8
|
module Taski
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
MOVE_UP_AND_CLEAR = "#{MOVE_UP}#{CLEAR_LINE}"
|
9
|
+
# Backward compatibility aliases
|
10
|
+
TerminalController = Progress::TerminalController
|
11
|
+
SpinnerAnimation = Progress::SpinnerAnimation
|
12
|
+
OutputCapture = Progress::OutputCapture
|
13
|
+
TaskStatus = Progress::TaskStatus
|
12
14
|
|
13
|
-
|
14
|
-
@output = output
|
15
|
-
end
|
16
|
-
|
17
|
-
def clear_lines(count)
|
18
|
-
return if count == 0
|
19
|
-
|
20
|
-
count.times { @output.print MOVE_UP_AND_CLEAR }
|
21
|
-
end
|
22
|
-
|
23
|
-
def puts(text)
|
24
|
-
@output.puts text
|
25
|
-
end
|
26
|
-
|
27
|
-
def print(text)
|
28
|
-
@output.print text
|
29
|
-
end
|
30
|
-
|
31
|
-
def flush
|
32
|
-
@output.flush
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Spinner animation with dots-style characters
|
37
|
-
class SpinnerAnimation
|
38
|
-
SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
|
39
|
-
FRAME_DELAY = 0.1
|
40
|
-
|
41
|
-
def initialize
|
42
|
-
@frame = 0
|
43
|
-
@running = false
|
44
|
-
@thread = nil
|
45
|
-
end
|
46
|
-
|
47
|
-
def start(terminal, task_name, &display_callback)
|
48
|
-
return if @running
|
49
|
-
|
50
|
-
@running = true
|
51
|
-
@frame = 0
|
52
|
-
|
53
|
-
@thread = Thread.new do
|
54
|
-
while @running
|
55
|
-
current_char = SPINNER_CHARS[@frame % SPINNER_CHARS.length]
|
56
|
-
display_callback&.call(current_char, task_name)
|
57
|
-
|
58
|
-
@frame += 1
|
59
|
-
sleep FRAME_DELAY
|
60
|
-
end
|
61
|
-
rescue
|
62
|
-
# Silently handle thread errors
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def stop
|
67
|
-
@running = false
|
68
|
-
@thread&.join(0.2)
|
69
|
-
@thread = nil
|
70
|
-
end
|
71
|
-
|
72
|
-
def running?
|
73
|
-
@running
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Captures stdout and maintains last N lines like tail -f
|
78
|
-
class OutputCapture
|
79
|
-
MAX_LINES = 10
|
80
|
-
DISPLAY_LINES = 5
|
81
|
-
|
82
|
-
def initialize(main_output)
|
83
|
-
@main_output = main_output
|
84
|
-
@buffer = []
|
85
|
-
@capturing = false
|
86
|
-
@original_stdout = nil
|
87
|
-
@pipe_reader = nil
|
88
|
-
@pipe_writer = nil
|
89
|
-
@capture_thread = nil
|
90
|
-
end
|
91
|
-
|
92
|
-
def start
|
93
|
-
return if @capturing
|
94
|
-
|
95
|
-
@buffer.clear
|
96
|
-
setup_stdout_redirection
|
97
|
-
@capturing = true
|
98
|
-
|
99
|
-
start_capture_thread
|
100
|
-
end
|
101
|
-
|
102
|
-
def stop
|
103
|
-
return unless @capturing
|
104
|
-
|
105
|
-
@capturing = false
|
106
|
-
|
107
|
-
# Restore stdout
|
108
|
-
restore_stdout
|
109
|
-
|
110
|
-
# Clean up pipes and thread
|
111
|
-
cleanup_capture_thread
|
112
|
-
cleanup_pipes
|
113
|
-
end
|
114
|
-
|
115
|
-
def last_lines
|
116
|
-
@buffer.last(DISPLAY_LINES)
|
117
|
-
end
|
118
|
-
|
119
|
-
def capturing?
|
120
|
-
@capturing
|
121
|
-
end
|
122
|
-
|
123
|
-
private
|
124
|
-
|
125
|
-
def setup_stdout_redirection
|
126
|
-
@original_stdout = $stdout
|
127
|
-
@pipe_reader, @pipe_writer = IO.pipe
|
128
|
-
$stdout = @pipe_writer
|
129
|
-
end
|
130
|
-
|
131
|
-
def restore_stdout
|
132
|
-
return unless @original_stdout
|
133
|
-
|
134
|
-
$stdout = @original_stdout
|
135
|
-
@original_stdout = nil
|
136
|
-
end
|
137
|
-
|
138
|
-
def start_capture_thread
|
139
|
-
@capture_thread = Thread.new do
|
140
|
-
while (line = @pipe_reader.gets)
|
141
|
-
line = line.chomp
|
142
|
-
next if line.empty?
|
143
|
-
next if skip_line?(line)
|
144
|
-
|
145
|
-
add_line_to_buffer(line)
|
146
|
-
end
|
147
|
-
rescue IOError
|
148
|
-
# Pipe closed, normal termination
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def skip_line?(line)
|
153
|
-
# Skip logger lines (they appear separately)
|
154
|
-
line.match?(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]/)
|
155
|
-
end
|
156
|
-
|
157
|
-
def add_line_to_buffer(line)
|
158
|
-
@buffer << line
|
159
|
-
@buffer.shift while @buffer.length > MAX_LINES
|
160
|
-
end
|
161
|
-
|
162
|
-
def cleanup_capture_thread
|
163
|
-
@capture_thread&.join(0.1)
|
164
|
-
@capture_thread = nil
|
165
|
-
end
|
166
|
-
|
167
|
-
def cleanup_pipes
|
168
|
-
[@pipe_writer, @pipe_reader].each do |pipe|
|
169
|
-
pipe&.close
|
170
|
-
rescue IOError
|
171
|
-
# Already closed, ignore
|
172
|
-
end
|
173
|
-
@pipe_writer = @pipe_reader = nil
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
# Represents task execution status
|
178
|
-
class TaskStatus
|
179
|
-
attr_reader :name, :duration, :error
|
180
|
-
|
181
|
-
def initialize(name:, duration: nil, error: nil)
|
182
|
-
@name = name
|
183
|
-
@duration = duration
|
184
|
-
@error = error
|
185
|
-
end
|
186
|
-
|
187
|
-
def success?
|
188
|
-
@error.nil?
|
189
|
-
end
|
190
|
-
|
191
|
-
def failure?
|
192
|
-
!success?
|
193
|
-
end
|
194
|
-
|
195
|
-
def duration_ms
|
196
|
-
return nil unless @duration
|
197
|
-
(@duration * 1000).round(1)
|
198
|
-
end
|
199
|
-
|
200
|
-
def icon
|
201
|
-
success? ? "✅" : "❌"
|
202
|
-
end
|
203
|
-
|
204
|
-
def format_duration
|
205
|
-
return "" unless duration_ms
|
206
|
-
"(#{duration_ms}ms)"
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
# Main progress display controller
|
15
|
+
# Main progress display controller - refactored for better separation of concerns
|
211
16
|
class ProgressDisplay
|
212
|
-
|
213
|
-
COLORS = {
|
214
|
-
reset: "\033[0m",
|
215
|
-
bold: "\033[1m",
|
216
|
-
dim: "\033[2m",
|
217
|
-
cyan: "\033[36m",
|
218
|
-
green: "\033[32m",
|
219
|
-
red: "\033[31m"
|
220
|
-
}.freeze
|
221
|
-
|
222
|
-
def initialize(output: $stdout, force_enable: nil)
|
17
|
+
def initialize(output: $stdout, enable: true, include_captured_output: nil)
|
223
18
|
@output = output
|
224
|
-
@terminal = TerminalController.new(output)
|
225
|
-
@spinner = SpinnerAnimation.new
|
226
|
-
@output_capture = OutputCapture.new(output)
|
19
|
+
@terminal = Progress::TerminalController.new(output)
|
20
|
+
@spinner = Progress::SpinnerAnimation.new
|
21
|
+
@output_capture = Progress::OutputCapture.new(output)
|
227
22
|
|
228
|
-
#
|
229
|
-
|
23
|
+
# Default to including captured output in test environments (when output != $stdout)
|
24
|
+
# This ensures test output is visible in the test output stream
|
25
|
+
include_captured_output = include_captured_output.nil? ? (output != $stdout) : include_captured_output
|
26
|
+
@display_manager = Progress::DisplayManager.new(@terminal, @spinner, @output_capture, include_captured_output: include_captured_output)
|
230
27
|
|
231
|
-
@
|
232
|
-
@current_display_lines = 0
|
28
|
+
@enabled = ENV["TASKI_PROGRESS_DISABLE"] != "1" && enable
|
233
29
|
end
|
234
30
|
|
235
31
|
def start_task(task_name, dependencies: [])
|
236
|
-
puts "DEBUG: start_task called for #{task_name}, enabled: #{@enabled}" if ENV["TASKI_DEBUG"]
|
237
32
|
return unless @enabled
|
238
33
|
|
239
|
-
|
240
|
-
@output_capture.start
|
241
|
-
|
242
|
-
start_spinner_display(task_name)
|
34
|
+
@display_manager.start_task_display(task_name)
|
243
35
|
end
|
244
36
|
|
245
37
|
def complete_task(task_name, duration:)
|
246
38
|
return unless @enabled
|
247
39
|
|
248
|
-
|
249
|
-
finish_task(status)
|
40
|
+
@display_manager.complete_task_display(task_name, duration: duration)
|
250
41
|
end
|
251
42
|
|
252
43
|
def fail_task(task_name, error:, duration:)
|
253
44
|
return unless @enabled
|
254
45
|
|
255
|
-
|
256
|
-
finish_task(status)
|
46
|
+
@display_manager.fail_task_display(task_name, error: error, duration: duration)
|
257
47
|
end
|
258
48
|
|
259
49
|
def clear
|
260
50
|
return unless @enabled
|
261
51
|
|
262
|
-
@
|
263
|
-
@output_capture.stop
|
264
|
-
clear_current_display
|
265
|
-
|
266
|
-
# Display final summary of all completed tasks
|
267
|
-
if @completed_tasks.any?
|
268
|
-
@completed_tasks.each do |status|
|
269
|
-
@terminal.puts format_completed_task(status)
|
270
|
-
end
|
271
|
-
@terminal.flush
|
272
|
-
end
|
273
|
-
|
274
|
-
@completed_tasks.clear
|
275
|
-
@current_display_lines = 0
|
52
|
+
@display_manager.clear_all_displays
|
276
53
|
end
|
277
54
|
|
278
55
|
def enabled?
|
279
56
|
@enabled
|
280
57
|
end
|
281
|
-
|
282
|
-
private
|
283
|
-
|
284
|
-
def start_spinner_display(task_name)
|
285
|
-
@spinner.start(@terminal, task_name) do |spinner_char, name|
|
286
|
-
display_current_state(spinner_char, name)
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
def display_current_state(spinner_char, task_name)
|
291
|
-
clear_current_display
|
292
|
-
|
293
|
-
lines_count = 0
|
294
|
-
|
295
|
-
# Only display current task with spinner (no past completed tasks during execution)
|
296
|
-
@terminal.puts format_current_task(spinner_char, task_name)
|
297
|
-
lines_count += 1
|
298
|
-
|
299
|
-
# Display output lines
|
300
|
-
@output_capture.last_lines.each do |line|
|
301
|
-
@terminal.puts format_output_line(line)
|
302
|
-
lines_count += 1
|
303
|
-
end
|
304
|
-
|
305
|
-
@current_display_lines = lines_count
|
306
|
-
@terminal.flush
|
307
|
-
end
|
308
|
-
|
309
|
-
def finish_task(status)
|
310
|
-
@spinner.stop
|
311
|
-
|
312
|
-
# Capture output before stopping
|
313
|
-
captured_output = @output_capture.last_lines
|
314
|
-
@output_capture.stop
|
315
|
-
clear_current_display
|
316
|
-
|
317
|
-
# In test environments (when terminal is StringIO), include captured output
|
318
|
-
if @terminal.is_a?(StringIO) && captured_output.any?
|
319
|
-
captured_output.each do |line|
|
320
|
-
@terminal.puts line.chomp
|
321
|
-
end
|
322
|
-
end
|
323
|
-
|
324
|
-
@completed_tasks << status
|
325
|
-
display_final_state
|
326
|
-
end
|
327
|
-
|
328
|
-
def display_final_state
|
329
|
-
# Only display the newly completed task (last one)
|
330
|
-
if @completed_tasks.any?
|
331
|
-
latest_task = @completed_tasks.last
|
332
|
-
@terminal.puts format_completed_task(latest_task)
|
333
|
-
end
|
334
|
-
@terminal.flush
|
335
|
-
@current_display_lines = 1 # Only one line for the latest task
|
336
|
-
end
|
337
|
-
|
338
|
-
def format_completed_task(status)
|
339
|
-
color = status.success? ? COLORS[:green] : COLORS[:red]
|
340
|
-
"#{color}#{COLORS[:bold]}#{status.icon} #{status.name}#{COLORS[:reset]} #{COLORS[:dim]}#{status.format_duration}#{COLORS[:reset]}"
|
341
|
-
end
|
342
|
-
|
343
|
-
def format_current_task(spinner_char, task_name)
|
344
|
-
"#{COLORS[:cyan]}#{spinner_char}#{COLORS[:reset]} #{COLORS[:bold]}#{task_name}#{COLORS[:reset]}"
|
345
|
-
end
|
346
|
-
|
347
|
-
def format_output_line(line)
|
348
|
-
" #{COLORS[:dim]}#{line}#{COLORS[:reset]}"
|
349
|
-
end
|
350
|
-
|
351
|
-
def clear_current_display
|
352
|
-
@terminal.clear_lines(@current_display_lines)
|
353
|
-
@current_display_lines = 0
|
354
|
-
end
|
355
58
|
end
|
356
59
|
end
|