taski 0.2.3 → 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 +52 -4
- 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 +65 -38
- 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 +18 -1
data/lib/taski/logger.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "logging/formatter_factory"
|
4
|
+
|
3
5
|
module Taski
|
4
6
|
# Enhanced logging functionality for Taski framework
|
5
7
|
# Provides structured logging with multiple levels and context information
|
@@ -13,7 +15,7 @@ module Taski
|
|
13
15
|
def initialize(level: :info, output: $stdout, format: :structured)
|
14
16
|
@level = level
|
15
17
|
@output = output
|
16
|
-
@
|
18
|
+
@formatter = Logging::FormatterFactory.create(format)
|
17
19
|
@start_time = Time.now
|
18
20
|
end
|
19
21
|
|
@@ -103,21 +105,15 @@ module Taski
|
|
103
105
|
|
104
106
|
private
|
105
107
|
|
106
|
-
# Core logging method
|
108
|
+
# Core logging method using Strategy Pattern
|
107
109
|
# @param level [Symbol] Log level
|
108
110
|
# @param message [String] Log message
|
109
111
|
# @param context [Hash] Additional context
|
110
112
|
def log(level, message, context)
|
111
113
|
return unless should_log?(level)
|
112
114
|
|
113
|
-
|
114
|
-
|
115
|
-
log_simple(level, message, context)
|
116
|
-
when :structured
|
117
|
-
log_structured(level, message, context)
|
118
|
-
when :json
|
119
|
-
log_json(level, message, context)
|
120
|
-
end
|
115
|
+
formatted_line = @formatter.format(level, message, context, @start_time)
|
116
|
+
@output.puts formatted_line
|
121
117
|
end
|
122
118
|
|
123
119
|
# Check if message should be logged based on current level
|
@@ -126,57 +122,6 @@ module Taski
|
|
126
122
|
def should_log?(level)
|
127
123
|
LEVELS[@level] <= LEVELS[level]
|
128
124
|
end
|
129
|
-
|
130
|
-
# Simple log format: [LEVEL] message
|
131
|
-
def log_simple(level, message, context)
|
132
|
-
@output.puts "[#{level.upcase}] #{message}"
|
133
|
-
end
|
134
|
-
|
135
|
-
# Structured log format with timestamp and context
|
136
|
-
def log_structured(level, message, context)
|
137
|
-
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
138
|
-
elapsed = ((Time.now - @start_time) * 1000).round(1)
|
139
|
-
|
140
|
-
line = "[#{timestamp}] [#{elapsed}ms] #{level.to_s.upcase.ljust(5)} Taski: #{message}"
|
141
|
-
|
142
|
-
unless context.empty?
|
143
|
-
context_parts = context.map do |key, value|
|
144
|
-
"#{key}=#{format_value(value)}"
|
145
|
-
end
|
146
|
-
line += " (#{context_parts.join(", ")})"
|
147
|
-
end
|
148
|
-
|
149
|
-
@output.puts line
|
150
|
-
end
|
151
|
-
|
152
|
-
# JSON log format for structured logging systems
|
153
|
-
def log_json(level, message, context)
|
154
|
-
require "json"
|
155
|
-
|
156
|
-
log_entry = {
|
157
|
-
timestamp: Time.now.iso8601(3),
|
158
|
-
level: level.to_s,
|
159
|
-
logger: "taski",
|
160
|
-
message: message,
|
161
|
-
elapsed_ms: ((Time.now - @start_time) * 1000).round(1)
|
162
|
-
}.merge(context)
|
163
|
-
|
164
|
-
@output.puts JSON.generate(log_entry)
|
165
|
-
end
|
166
|
-
|
167
|
-
# Format values for structured logging
|
168
|
-
def format_value(value)
|
169
|
-
case value
|
170
|
-
when String
|
171
|
-
(value.length > 50) ? "#{value[0..47]}..." : value
|
172
|
-
when Array
|
173
|
-
(value.size > 5) ? "[#{value[0..4].join(", ")}, ...]" : value.inspect
|
174
|
-
when Hash
|
175
|
-
(value.size > 3) ? "{#{value.keys[0..2].join(", ")}, ...}" : value.inspect
|
176
|
-
else
|
177
|
-
value.inspect
|
178
|
-
end
|
179
|
-
end
|
180
125
|
end
|
181
126
|
|
182
127
|
class << self
|
@@ -189,7 +134,7 @@ module Taski
|
|
189
134
|
# Get the current progress display instance (always enabled)
|
190
135
|
# @return [ProgressDisplay] Current progress display instance
|
191
136
|
def progress_display
|
192
|
-
@progress_display ||= ProgressDisplay.new
|
137
|
+
@progress_display ||= ProgressDisplay.new
|
193
138
|
end
|
194
139
|
|
195
140
|
# Configure the logger with new settings
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "simple_formatter"
|
4
|
+
require_relative "structured_formatter"
|
5
|
+
require_relative "json_formatter"
|
6
|
+
|
7
|
+
module Taski
|
8
|
+
module Logging
|
9
|
+
# Factory for creating log formatters
|
10
|
+
class FormatterFactory
|
11
|
+
# Create a formatter instance based on format symbol
|
12
|
+
# @param format [Symbol] Format type (:simple, :structured, :json)
|
13
|
+
# @return [FormatterInterface] Formatter instance
|
14
|
+
def self.create(format)
|
15
|
+
case format
|
16
|
+
when :simple
|
17
|
+
SimpleFormatter.new
|
18
|
+
when :structured
|
19
|
+
StructuredFormatter.new
|
20
|
+
when :json
|
21
|
+
JsonFormatter.new
|
22
|
+
else
|
23
|
+
raise ArgumentError, "Unknown format: #{format}. Valid formats: :simple, :structured, :json"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get list of available formats
|
28
|
+
# @return [Array<Symbol>] Available format symbols
|
29
|
+
def self.available_formats
|
30
|
+
[:simple, :structured, :json]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Logging
|
5
|
+
# Interface for log formatters
|
6
|
+
# All formatters must implement the format method
|
7
|
+
module FormatterInterface
|
8
|
+
# Format a log entry
|
9
|
+
# @param level [Symbol] Log level (:debug, :info, :warn, :error)
|
10
|
+
# @param message [String] Log message
|
11
|
+
# @param context [Hash] Additional context information
|
12
|
+
# @param start_time [Time] Logger start time for elapsed calculation
|
13
|
+
# @return [String] Formatted log line
|
14
|
+
def format(level, message, context, start_time)
|
15
|
+
raise NotImplementedError, "Subclass must implement format method"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "formatter_interface"
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
module Logging
|
7
|
+
# JSON log formatter for structured logging systems
|
8
|
+
class JsonFormatter
|
9
|
+
include FormatterInterface
|
10
|
+
|
11
|
+
def format(level, message, context, start_time)
|
12
|
+
require "json"
|
13
|
+
|
14
|
+
log_entry = {
|
15
|
+
timestamp: Time.now.iso8601(3),
|
16
|
+
level: level.to_s,
|
17
|
+
logger: "taski",
|
18
|
+
message: message,
|
19
|
+
elapsed_ms: ((Time.now - start_time) * 1000).round(1)
|
20
|
+
}.merge(context)
|
21
|
+
|
22
|
+
JSON.generate(log_entry)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "formatter_interface"
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
module Logging
|
7
|
+
# Simple log formatter: [LEVEL] message
|
8
|
+
class SimpleFormatter
|
9
|
+
include FormatterInterface
|
10
|
+
|
11
|
+
def format(level, message, context, start_time)
|
12
|
+
"[#{level.upcase}] #{message}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "formatter_interface"
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
module Logging
|
7
|
+
# Structured log formatter with timestamp and context
|
8
|
+
class StructuredFormatter
|
9
|
+
include FormatterInterface
|
10
|
+
|
11
|
+
def format(level, message, context, start_time)
|
12
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
13
|
+
elapsed = ((Time.now - start_time) * 1000).round(1)
|
14
|
+
|
15
|
+
line = "[#{timestamp}] [#{elapsed}ms] #{level.to_s.upcase.ljust(5)} Taski: #{message}"
|
16
|
+
|
17
|
+
unless context.empty?
|
18
|
+
context_parts = context.map do |key, value|
|
19
|
+
"#{key}=#{format_value(value)}"
|
20
|
+
end
|
21
|
+
line += " (#{context_parts.join(", ")})"
|
22
|
+
end
|
23
|
+
|
24
|
+
line
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Format values for structured logging
|
30
|
+
def format_value(value)
|
31
|
+
case value
|
32
|
+
when String
|
33
|
+
(value.length > 50) ? "#{value[0..47]}..." : value
|
34
|
+
when Array
|
35
|
+
(value.size > 5) ? "[#{value[0..4].join(", ")}, ...]" : value.inspect
|
36
|
+
when Hash
|
37
|
+
(value.size > 3) ? "{#{value.keys[0..2].join(", ")}, ...}" : value.inspect
|
38
|
+
else
|
39
|
+
value.inspect
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Progress
|
5
|
+
# Color constants for progress display
|
6
|
+
module DisplayColors
|
7
|
+
COLORS = {
|
8
|
+
reset: "\033[0m",
|
9
|
+
bold: "\033[1m",
|
10
|
+
dim: "\033[2m",
|
11
|
+
cyan: "\033[36m",
|
12
|
+
green: "\033[32m",
|
13
|
+
red: "\033[31m"
|
14
|
+
}.freeze
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "task_status"
|
4
|
+
require_relative "task_formatter"
|
5
|
+
|
6
|
+
module Taski
|
7
|
+
module Progress
|
8
|
+
# Manages the display state and coordinates display updates
|
9
|
+
class DisplayManager
|
10
|
+
def initialize(terminal, spinner, output_capture, include_captured_output: false)
|
11
|
+
@terminal = terminal
|
12
|
+
@spinner = spinner
|
13
|
+
@output_capture = output_capture
|
14
|
+
@include_captured_output = include_captured_output
|
15
|
+
@formatter = TaskFormatter.new
|
16
|
+
@completed_tasks = []
|
17
|
+
@current_display_lines = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def start_task_display(task_name)
|
21
|
+
clear_current_display
|
22
|
+
@output_capture.start
|
23
|
+
start_spinner_display(task_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def complete_task_display(task_name, duration:)
|
27
|
+
status = TaskStatus.new(name: task_name, duration: duration)
|
28
|
+
finish_task_display(status)
|
29
|
+
end
|
30
|
+
|
31
|
+
def fail_task_display(task_name, error:, duration:)
|
32
|
+
status = TaskStatus.new(name: task_name, duration: duration, error: error)
|
33
|
+
finish_task_display(status)
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear_all_displays
|
37
|
+
@spinner.stop
|
38
|
+
@output_capture.stop
|
39
|
+
clear_current_display
|
40
|
+
|
41
|
+
# Display final summary of all completed tasks
|
42
|
+
if @completed_tasks.any?
|
43
|
+
@completed_tasks.each do |status|
|
44
|
+
@terminal.puts @formatter.format_completed_task(status)
|
45
|
+
end
|
46
|
+
@terminal.flush
|
47
|
+
end
|
48
|
+
|
49
|
+
@completed_tasks.clear
|
50
|
+
@current_display_lines = 0
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def start_spinner_display(task_name)
|
56
|
+
@spinner.start(@terminal, task_name) do |spinner_char, name|
|
57
|
+
display_current_state(spinner_char, name)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def display_current_state(spinner_char, task_name)
|
62
|
+
clear_current_display
|
63
|
+
|
64
|
+
lines_count = 0
|
65
|
+
|
66
|
+
# Only display current task with spinner (no past completed tasks during execution)
|
67
|
+
@terminal.puts @formatter.format_current_task(spinner_char, task_name)
|
68
|
+
lines_count += 1
|
69
|
+
|
70
|
+
# Display output lines
|
71
|
+
@output_capture.last_lines.each do |line|
|
72
|
+
@terminal.puts @formatter.format_output_line(line)
|
73
|
+
lines_count += 1
|
74
|
+
end
|
75
|
+
|
76
|
+
@current_display_lines = lines_count
|
77
|
+
@terminal.flush
|
78
|
+
end
|
79
|
+
|
80
|
+
def finish_task_display(status)
|
81
|
+
@spinner.stop
|
82
|
+
|
83
|
+
# Capture output before stopping
|
84
|
+
captured_output = @output_capture.last_lines
|
85
|
+
@output_capture.stop
|
86
|
+
clear_current_display
|
87
|
+
|
88
|
+
# Include captured output if requested (typically for test environments)
|
89
|
+
if @include_captured_output && captured_output.any?
|
90
|
+
captured_output.each do |line|
|
91
|
+
@terminal.puts line.chomp
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
@completed_tasks << status
|
96
|
+
display_final_state
|
97
|
+
end
|
98
|
+
|
99
|
+
def display_final_state
|
100
|
+
# Only display the newly completed task (last one)
|
101
|
+
if @completed_tasks.any?
|
102
|
+
latest_task = @completed_tasks.last
|
103
|
+
@terminal.puts @formatter.format_completed_task(latest_task)
|
104
|
+
end
|
105
|
+
@terminal.flush
|
106
|
+
@current_display_lines = 1 # Only one line for the latest task
|
107
|
+
end
|
108
|
+
|
109
|
+
def clear_current_display
|
110
|
+
@terminal.clear_lines(@current_display_lines)
|
111
|
+
@current_display_lines = 0
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Progress
|
5
|
+
# Captures stdout and maintains last N lines like tail -f
|
6
|
+
class OutputCapture
|
7
|
+
MAX_LINES = 10
|
8
|
+
DISPLAY_LINES = 5
|
9
|
+
|
10
|
+
def initialize(main_output)
|
11
|
+
@main_output = main_output
|
12
|
+
@buffer = []
|
13
|
+
@capturing = false
|
14
|
+
@original_stdout = nil
|
15
|
+
@pipe_reader = nil
|
16
|
+
@pipe_writer = nil
|
17
|
+
@capture_thread = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
return if @capturing
|
22
|
+
|
23
|
+
@buffer.clear
|
24
|
+
setup_stdout_redirection
|
25
|
+
@capturing = true
|
26
|
+
|
27
|
+
start_capture_thread
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
return unless @capturing
|
32
|
+
|
33
|
+
@capturing = false
|
34
|
+
|
35
|
+
# Restore stdout
|
36
|
+
restore_stdout
|
37
|
+
|
38
|
+
# Clean up pipes and thread
|
39
|
+
cleanup_capture_thread
|
40
|
+
cleanup_pipes
|
41
|
+
end
|
42
|
+
|
43
|
+
def last_lines
|
44
|
+
@buffer.last(DISPLAY_LINES)
|
45
|
+
end
|
46
|
+
|
47
|
+
def capturing?
|
48
|
+
@capturing
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def setup_stdout_redirection
|
54
|
+
@original_stdout = $stdout
|
55
|
+
@pipe_reader, @pipe_writer = IO.pipe
|
56
|
+
$stdout = @pipe_writer
|
57
|
+
end
|
58
|
+
|
59
|
+
def restore_stdout
|
60
|
+
return unless @original_stdout
|
61
|
+
|
62
|
+
$stdout = @original_stdout
|
63
|
+
@original_stdout = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def start_capture_thread
|
67
|
+
@capture_thread = Thread.new do
|
68
|
+
while (line = @pipe_reader.gets)
|
69
|
+
line = line.chomp
|
70
|
+
next if line.empty?
|
71
|
+
next if skip_line?(line)
|
72
|
+
|
73
|
+
add_line_to_buffer(line)
|
74
|
+
end
|
75
|
+
rescue IOError
|
76
|
+
# Pipe closed, normal termination
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def skip_line?(line)
|
81
|
+
# Skip logger lines (they appear separately)
|
82
|
+
line.match?(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]/)
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_line_to_buffer(line)
|
86
|
+
@buffer << line
|
87
|
+
@buffer.shift while @buffer.length > MAX_LINES
|
88
|
+
end
|
89
|
+
|
90
|
+
def cleanup_capture_thread
|
91
|
+
@capture_thread&.join(0.1)
|
92
|
+
@capture_thread = nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def cleanup_pipes
|
96
|
+
[@pipe_writer, @pipe_reader].each do |pipe|
|
97
|
+
pipe&.close
|
98
|
+
rescue IOError
|
99
|
+
# Already closed, ignore
|
100
|
+
end
|
101
|
+
@pipe_writer = @pipe_reader = nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -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
|