taski 0.8.3 → 0.9.1
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 +52 -0
- data/README.md +108 -50
- data/docs/GUIDE.md +79 -55
- 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 +167 -359
- data/lib/taski/execution/fiber_protocol.rb +27 -0
- 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 +279 -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/start_dep_analyzer.rb +400 -0
- data/lib/taski/static_analysis/visitor.rb +1 -39
- data/lib/taski/task.rb +49 -58
- data/lib/taski/task_proxy.rb +59 -0
- data/lib/taski/test_helper/errors.rb +1 -1
- data/lib/taski/test_helper.rb +22 -36
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +62 -61
- data/sig/taski.rbs +194 -203
- metadata +34 -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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../theme/detail"
|
|
5
|
+
|
|
6
|
+
module Taski
|
|
7
|
+
module Progress
|
|
8
|
+
module Layout
|
|
9
|
+
# Tree layout for hierarchical task display.
|
|
10
|
+
# Renders tasks in a tree structure with visual connectors (├──, └──, │).
|
|
11
|
+
#
|
|
12
|
+
# Operates in two modes:
|
|
13
|
+
# - TTY mode: Periodic full-tree refresh with spinner animation
|
|
14
|
+
# - Non-TTY mode: Incremental output with tree prefixes (for logs/tests)
|
|
15
|
+
#
|
|
16
|
+
# Output format (live updating):
|
|
17
|
+
# BuildApplication
|
|
18
|
+
# ├── ⠹ SetupDatabase
|
|
19
|
+
# │ ├── ✓ CreateSchema (50ms)
|
|
20
|
+
# │ └── ⠹ SeedData
|
|
21
|
+
# ├── ○ ExtractLayers
|
|
22
|
+
# │ ├── ✓ DownloadLayer1 (100ms)
|
|
23
|
+
# │ └── ○ DownloadLayer2
|
|
24
|
+
# └── ✓ RunSystemCommand (200ms)
|
|
25
|
+
#
|
|
26
|
+
# The tree structure (prefixes) is added by this Layout.
|
|
27
|
+
# The task content (icons, names, duration) comes from the Theme.
|
|
28
|
+
#
|
|
29
|
+
# This demonstrates the Theme/Layout separation:
|
|
30
|
+
# - Theme defines "what one line looks like" (icons, colors, formatting)
|
|
31
|
+
# - Layout defines "how lines are arranged" (tree structure, prefixes)
|
|
32
|
+
#
|
|
33
|
+
# @example Using with Theme::Default
|
|
34
|
+
# layout = Taski::Progress::Layout::Tree.new(theme: Taski::Progress::Theme::Default.new)
|
|
35
|
+
class Tree < Base
|
|
36
|
+
# Tree connector characters
|
|
37
|
+
BRANCH = "├── "
|
|
38
|
+
LAST_BRANCH = "└── "
|
|
39
|
+
VERTICAL = "│ "
|
|
40
|
+
SPACE = " "
|
|
41
|
+
|
|
42
|
+
def initialize(output: $stderr, theme: nil)
|
|
43
|
+
theme ||= Theme::Detail.new
|
|
44
|
+
super
|
|
45
|
+
@tree_nodes = {}
|
|
46
|
+
@node_depths = {}
|
|
47
|
+
@node_is_last = {}
|
|
48
|
+
@renderer_thread = nil
|
|
49
|
+
@running = false
|
|
50
|
+
@running_mutex = Mutex.new
|
|
51
|
+
@last_line_count = 0
|
|
52
|
+
@non_tty_started = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the tree structure as a string.
|
|
56
|
+
# Uses the current theme to render task content for each node.
|
|
57
|
+
def render_tree
|
|
58
|
+
build_tree_lines.join("\n") + "\n"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Override on_start to handle non-TTY mode
|
|
62
|
+
def on_start
|
|
63
|
+
@monitor.synchronize do
|
|
64
|
+
@nest_level += 1
|
|
65
|
+
return if @nest_level > 1
|
|
66
|
+
|
|
67
|
+
@start_time = Time.now
|
|
68
|
+
|
|
69
|
+
if should_activate?
|
|
70
|
+
@active = true
|
|
71
|
+
else
|
|
72
|
+
# Non-TTY mode: output execution start message
|
|
73
|
+
@non_tty_started = true
|
|
74
|
+
output_line(render_execution_started(@root_task_class)) if @root_task_class
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
handle_start if @active
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Override on_stop to handle non-TTY mode
|
|
82
|
+
def on_stop
|
|
83
|
+
was_active = false
|
|
84
|
+
non_tty_was_started = false
|
|
85
|
+
@monitor.synchronize do
|
|
86
|
+
@nest_level -= 1 if @nest_level > 0
|
|
87
|
+
return unless @nest_level == 0
|
|
88
|
+
was_active = @active
|
|
89
|
+
non_tty_was_started = @non_tty_started
|
|
90
|
+
@active = false
|
|
91
|
+
@non_tty_started = false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if was_active
|
|
95
|
+
handle_stop
|
|
96
|
+
elsif non_tty_was_started
|
|
97
|
+
# Non-TTY mode: output execution summary
|
|
98
|
+
output_execution_summary
|
|
99
|
+
end
|
|
100
|
+
flush_queued_messages
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
protected
|
|
104
|
+
|
|
105
|
+
def handle_ready
|
|
106
|
+
graph = context&.dependency_graph
|
|
107
|
+
root = context&.root_task_class
|
|
108
|
+
return unless graph && root
|
|
109
|
+
|
|
110
|
+
tree = build_tree_from_graph(root, graph)
|
|
111
|
+
register_tree_nodes(tree, depth: 0, is_last: true, ancestors_last: [])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# In TTY mode, tree is updated by render_live periodically.
|
|
115
|
+
# In non-TTY mode, output lines immediately with tree prefix.
|
|
116
|
+
def handle_task_update(task_class, current_state, phase)
|
|
117
|
+
return if @active # TTY mode: skip per-event output
|
|
118
|
+
|
|
119
|
+
# Non-TTY mode: output with tree prefix
|
|
120
|
+
progress = @tasks[task_class]
|
|
121
|
+
duration = compute_duration(progress, phase)
|
|
122
|
+
text = render_for_task_event(task_class, current_state, duration, nil, phase)
|
|
123
|
+
output_with_prefix(task_class, text) if text
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_group_started(task_class, group_name, phase)
|
|
127
|
+
return if @active # TTY mode: skip per-event output
|
|
128
|
+
|
|
129
|
+
# Non-TTY mode: output with tree prefix
|
|
130
|
+
text = render_group_started(task_class, group_name: group_name)
|
|
131
|
+
output_with_prefix(task_class, text) if text
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def handle_group_completed(task_class, group_name, phase, duration)
|
|
135
|
+
return if @active # TTY mode: skip per-event output
|
|
136
|
+
|
|
137
|
+
# Non-TTY mode: output with tree prefix
|
|
138
|
+
text = render_group_succeeded(task_class, group_name: group_name, task_duration: duration)
|
|
139
|
+
output_with_prefix(task_class, text) if text
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def should_activate?
|
|
143
|
+
tty?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def handle_start
|
|
147
|
+
@running_mutex.synchronize { @running = true }
|
|
148
|
+
start_spinner_timer
|
|
149
|
+
@output.print "\e[?25l" # Hide cursor
|
|
150
|
+
@renderer_thread = Thread.new do
|
|
151
|
+
loop do
|
|
152
|
+
break unless @running_mutex.synchronize { @running }
|
|
153
|
+
render_live
|
|
154
|
+
sleep @theme.render_interval
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def handle_stop
|
|
160
|
+
@running_mutex.synchronize { @running = false }
|
|
161
|
+
@renderer_thread&.join
|
|
162
|
+
stop_spinner_timer
|
|
163
|
+
@output.print "\e[?25h" # Show cursor
|
|
164
|
+
render_final
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# Output text with tree prefix for the given task
|
|
170
|
+
def output_with_prefix(task_class, text)
|
|
171
|
+
prefix = build_tree_prefix(task_class)
|
|
172
|
+
output_line("#{prefix}#{text}")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Output execution summary for non-TTY mode
|
|
176
|
+
def output_execution_summary
|
|
177
|
+
output_line(render_execution_summary)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_tree_from_graph(task_class, graph, ancestors = Set.new)
|
|
181
|
+
is_circular = ancestors.include?(task_class)
|
|
182
|
+
node = {task_class: task_class, is_circular: is_circular, children: []}
|
|
183
|
+
return node if is_circular
|
|
184
|
+
|
|
185
|
+
new_ancestors = ancestors + [task_class]
|
|
186
|
+
deps = graph.dependencies_for(task_class)
|
|
187
|
+
deps.each do |dep|
|
|
188
|
+
child_node = build_tree_from_graph(dep, graph, new_ancestors)
|
|
189
|
+
node[:children] << child_node
|
|
190
|
+
end
|
|
191
|
+
node
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def register_tree_nodes(node, depth:, is_last:, ancestors_last:)
|
|
195
|
+
return unless node
|
|
196
|
+
|
|
197
|
+
task_class = node[:task_class]
|
|
198
|
+
@tasks[task_class] ||= new_task_progress
|
|
199
|
+
@tree_nodes[task_class] = node
|
|
200
|
+
@node_depths[task_class] = depth
|
|
201
|
+
@node_is_last[task_class] = {is_last: is_last, ancestors_last: ancestors_last.dup}
|
|
202
|
+
|
|
203
|
+
children = node[:children]
|
|
204
|
+
children.each_with_index do |child, index|
|
|
205
|
+
child_is_last = (index == children.size - 1)
|
|
206
|
+
new_ancestors_last = ancestors_last + [is_last]
|
|
207
|
+
register_tree_nodes(child, depth: depth + 1, is_last: child_is_last, ancestors_last: new_ancestors_last)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def render_live
|
|
212
|
+
@monitor.synchronize do
|
|
213
|
+
lines = build_tree_lines
|
|
214
|
+
clear_previous_output
|
|
215
|
+
lines.each { |line| @output.puts line }
|
|
216
|
+
@output.flush
|
|
217
|
+
@last_line_count = lines.size
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def render_final
|
|
222
|
+
@monitor.synchronize do
|
|
223
|
+
lines = build_tree_lines
|
|
224
|
+
clear_previous_output
|
|
225
|
+
|
|
226
|
+
lines.each { |line| @output.puts line }
|
|
227
|
+
@output.puts render_execution_summary
|
|
228
|
+
@output.flush
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def clear_previous_output
|
|
233
|
+
return if @last_line_count == 0
|
|
234
|
+
# Move cursor up and clear lines
|
|
235
|
+
@output.print "\e[#{@last_line_count}A\e[J"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def build_tree_lines
|
|
239
|
+
return [] unless @root_task_class
|
|
240
|
+
|
|
241
|
+
lines = []
|
|
242
|
+
root_node = @tree_nodes[@root_task_class]
|
|
243
|
+
build_node_lines(root_node, lines)
|
|
244
|
+
lines
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def build_node_lines(node, lines)
|
|
248
|
+
return unless node
|
|
249
|
+
|
|
250
|
+
task_class = node[:task_class]
|
|
251
|
+
prefix = build_tree_prefix(task_class)
|
|
252
|
+
content = build_task_content(task_class)
|
|
253
|
+
lines << "#{prefix}#{content}"
|
|
254
|
+
|
|
255
|
+
node[:children].each do |child|
|
|
256
|
+
build_node_lines(child, lines)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def build_task_content(task_class)
|
|
261
|
+
progress = @tasks[task_class]
|
|
262
|
+
|
|
263
|
+
case progress&.dig(:run_state)
|
|
264
|
+
when :running
|
|
265
|
+
render_task_started(task_class)
|
|
266
|
+
when :completed
|
|
267
|
+
render_task_succeeded(task_class, task_duration: compute_duration(progress, :run))
|
|
268
|
+
when :failed
|
|
269
|
+
render_task_failed(task_class, error: nil)
|
|
270
|
+
when :skipped
|
|
271
|
+
render_task_skipped(task_class)
|
|
272
|
+
else
|
|
273
|
+
task = TaskDrop.new(name: task_class_name(task_class), state: :pending)
|
|
274
|
+
render_task_template(:task_pending, task:, execution: execution_drop)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def build_tree_prefix(task_class)
|
|
279
|
+
depth = @node_depths[task_class]
|
|
280
|
+
return "" if depth.nil? || depth == 0
|
|
281
|
+
|
|
282
|
+
last_info = @node_is_last[task_class]
|
|
283
|
+
return "" unless last_info
|
|
284
|
+
|
|
285
|
+
ancestors_last = last_info[:ancestors_last]
|
|
286
|
+
is_last = last_info[:is_last]
|
|
287
|
+
|
|
288
|
+
prefix = ""
|
|
289
|
+
# Skip the first ancestor (root) since root has no visual prefix
|
|
290
|
+
ancestors_last[1..].each do |ancestor_is_last|
|
|
291
|
+
prefix += ancestor_is_last ? SPACE : VERTICAL
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
prefix += is_last ? LAST_BRANCH : BRANCH
|
|
295
|
+
prefix
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taski
|
|
4
|
+
module Progress
|
|
5
|
+
module Theme
|
|
6
|
+
# Base class for theme definitions.
|
|
7
|
+
# Theme classes are thin layers that only return Liquid template strings.
|
|
8
|
+
# Rendering (Liquid parsing) is handled by Layout classes.
|
|
9
|
+
#
|
|
10
|
+
# Themes have access to two Drop objects:
|
|
11
|
+
# task: Task-specific info (name, state, duration, error_message, group_name, stdout)
|
|
12
|
+
# execution: Execution-level info (state, pending_count, done_count, completed_count,
|
|
13
|
+
# failed_count, total_count, total_duration, root_task_name, task_names)
|
|
14
|
+
#
|
|
15
|
+
# Use {% if variable %} to conditionally render when a value is present.
|
|
16
|
+
#
|
|
17
|
+
# @example Custom theme
|
|
18
|
+
# class MyTheme < Taski::Progress::Theme::Base
|
|
19
|
+
# def task_start
|
|
20
|
+
# "Starting {{ task.name }}..."
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# layout = Taski::Progress::Layout::Log.new(theme: MyTheme.new)
|
|
25
|
+
class Base
|
|
26
|
+
# === Task lifecycle templates ===
|
|
27
|
+
|
|
28
|
+
def task_pending
|
|
29
|
+
"[PENDING] {{ task.name | short_name }}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def task_start
|
|
33
|
+
"[START] {{ task.name | short_name }}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def task_success
|
|
37
|
+
"[DONE] {{ task.name | short_name }}{% if task.duration %} ({{ task.duration | format_duration }}){% endif %}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def task_fail
|
|
41
|
+
"[FAIL] {{ task.name | short_name }}{% if task.error_message %}: {{ task.error_message }}{% endif %}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def task_skip
|
|
45
|
+
"[SKIP] {{ task.name | short_name }}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# === Clean lifecycle templates ===
|
|
49
|
+
|
|
50
|
+
def clean_start
|
|
51
|
+
"[CLEAN] {{ task.name | short_name }}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clean_success
|
|
55
|
+
"[CLEAN DONE] {{ task.name | short_name }}{% if task.duration %} ({{ task.duration | format_duration }}){% endif %}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def clean_fail
|
|
59
|
+
"[CLEAN FAIL] {{ task.name | short_name }}{% if task.error_message %}: {{ task.error_message }}{% endif %}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# === Group lifecycle templates ===
|
|
63
|
+
|
|
64
|
+
def group_start
|
|
65
|
+
'[GROUP] {{ task.name | short_name }}#{{ task.group_name }}'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def group_success
|
|
69
|
+
'[GROUP DONE] {{ task.name | short_name }}#{{ task.group_name }}{% if task.duration %} ({{ task.duration | format_duration }}){% endif %}'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def group_fail
|
|
73
|
+
'[GROUP FAIL] {{ task.name | short_name }}#{{ task.group_name }}{% if task.error_message %}: {{ task.error_message }}{% endif %}'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# === Execution lifecycle templates ===
|
|
77
|
+
|
|
78
|
+
def execution_start
|
|
79
|
+
"[TASKI] Starting {{ execution.root_task_name | short_name }}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def execution_running
|
|
83
|
+
"[TASKI] Running: {{ execution.done_count }}/{{ execution.total_count }} tasks"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def execution_complete
|
|
87
|
+
"[TASKI] Completed: {{ execution.completed_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def execution_fail
|
|
91
|
+
"[TASKI] Failed: {{ execution.failed_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# === Spinner configuration ===
|
|
95
|
+
|
|
96
|
+
# Spinner animation frames
|
|
97
|
+
# @return [Array<String>] Array of spinner frame characters
|
|
98
|
+
def spinner_frames
|
|
99
|
+
%w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Spinner frame update interval in seconds
|
|
103
|
+
# @return [Float] Interval between spinner frame updates
|
|
104
|
+
def spinner_interval
|
|
105
|
+
0.08
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Screen render interval in seconds
|
|
109
|
+
# @return [Float] Interval between screen updates
|
|
110
|
+
def render_interval
|
|
111
|
+
0.1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# === Icon configuration ===
|
|
115
|
+
|
|
116
|
+
# Icon for successful completion
|
|
117
|
+
# @return [String] Success icon
|
|
118
|
+
def icon_success
|
|
119
|
+
"✓"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Icon for failure
|
|
123
|
+
# @return [String] Failure icon
|
|
124
|
+
def icon_failure
|
|
125
|
+
"✗"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Icon for pending state
|
|
129
|
+
# @return [String] Pending icon
|
|
130
|
+
def icon_pending
|
|
131
|
+
"○"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Icon for skipped state
|
|
135
|
+
# @return [String] Skipped icon
|
|
136
|
+
def icon_skip
|
|
137
|
+
"⊘"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# === Color configuration (ANSI codes) ===
|
|
141
|
+
|
|
142
|
+
# Green color ANSI escape code
|
|
143
|
+
# @return [String] ANSI code for green
|
|
144
|
+
def color_green
|
|
145
|
+
"\e[32m"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Red color ANSI escape code
|
|
149
|
+
# @return [String] ANSI code for red
|
|
150
|
+
def color_red
|
|
151
|
+
"\e[31m"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Yellow color ANSI escape code
|
|
155
|
+
# @return [String] ANSI code for yellow
|
|
156
|
+
def color_yellow
|
|
157
|
+
"\e[33m"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Dim color ANSI escape code
|
|
161
|
+
# @return [String] ANSI code for dim
|
|
162
|
+
def color_dim
|
|
163
|
+
"\e[2m"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Reset color ANSI escape code
|
|
167
|
+
# @return [String] ANSI code to reset color
|
|
168
|
+
def color_reset
|
|
169
|
+
"\e[0m"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# === Formatting methods (used by filters) ===
|
|
173
|
+
|
|
174
|
+
# Format a count value for display.
|
|
175
|
+
# Override in subclasses to customize count formatting.
|
|
176
|
+
#
|
|
177
|
+
# @param count [Integer] The count value
|
|
178
|
+
# @return [String] Formatted count
|
|
179
|
+
# @example
|
|
180
|
+
# def format_count(count)
|
|
181
|
+
# "#{count}件"
|
|
182
|
+
# end
|
|
183
|
+
def format_count(count)
|
|
184
|
+
count.to_s
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Format a duration value for display.
|
|
188
|
+
# Override in subclasses to customize duration formatting.
|
|
189
|
+
#
|
|
190
|
+
# @param ms [Integer, Float] Duration in milliseconds
|
|
191
|
+
# @return [String] Formatted duration
|
|
192
|
+
# @example
|
|
193
|
+
# def format_duration(ms)
|
|
194
|
+
# "#{ms}ミリ秒"
|
|
195
|
+
# end
|
|
196
|
+
def format_duration(ms)
|
|
197
|
+
if ms >= 1000
|
|
198
|
+
"#{(ms / 1000.0).round(1)}s"
|
|
199
|
+
else
|
|
200
|
+
"#{ms}ms"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Separator for truncate_list filter.
|
|
205
|
+
# @return [String] Separator between list items
|
|
206
|
+
def truncate_list_separator
|
|
207
|
+
", "
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Suffix for truncate_list filter when list is truncated.
|
|
211
|
+
# @return [String] Suffix to append when items are omitted
|
|
212
|
+
def truncate_list_suffix
|
|
213
|
+
"..."
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Suffix for truncate_text filter when text is truncated.
|
|
217
|
+
# @return [String] Suffix to append when text is truncated
|
|
218
|
+
def truncate_text_suffix
|
|
219
|
+
"..."
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "default"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Progress
|
|
7
|
+
module Theme
|
|
8
|
+
# Compact theme for TTY environments with single-line progress display.
|
|
9
|
+
# Provides spinner animation, colored icons, and status formatting.
|
|
10
|
+
#
|
|
11
|
+
# Output format:
|
|
12
|
+
# ⠹ [3/5] DeployTask | Uploading files...
|
|
13
|
+
# ✓ [5/5] All tasks completed (1.2s)
|
|
14
|
+
#
|
|
15
|
+
# @example Usage
|
|
16
|
+
# layout = Taski::Progress::Layout::Simple.new(
|
|
17
|
+
# theme: Taski::Progress::Theme::Compact.new
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @example Custom spinner frames
|
|
21
|
+
# class MoonTheme < Taski::Progress::Theme::Compact
|
|
22
|
+
# def spinner_frames
|
|
23
|
+
# %w[🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘]
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @example Custom status theme
|
|
28
|
+
# class JapaneseTheme < Taski::Progress::Theme::Compact
|
|
29
|
+
# def format_count(count)
|
|
30
|
+
# "#{count}件"
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# def status_complete
|
|
34
|
+
# '{% icon %} {{ execution.done_count | format_count }}完了 ({{ execution.total_duration | format_duration }})'
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
class Compact < Default
|
|
38
|
+
# Inherits ANSI colors from Base via Default.
|
|
39
|
+
# Adds spinner and icons for TTY environments.
|
|
40
|
+
|
|
41
|
+
# Execution running with spinner
|
|
42
|
+
def execution_running
|
|
43
|
+
"{% spinner %} [{{ execution.done_count }}/{{ execution.total_count }}]{% if execution.task_names %} {% for name in execution.task_names limit: 3 %}{{ name | short_name }}{% unless forloop.last %}, {% endunless %}{% endfor %}{% if execution.task_names.size > 3 %}...{% endif %}{% endif %}{% if task.stdout %} | {{ task.stdout | truncate_text: 40 }}{% endif %}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Execution complete with icon
|
|
47
|
+
def execution_complete
|
|
48
|
+
"{% icon %} [TASKI] Completed: {{ execution.completed_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Execution fail with icon
|
|
52
|
+
def execution_fail
|
|
53
|
+
"{% icon %} [TASKI] Failed: {{ execution.failed_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Progress
|
|
7
|
+
module Theme
|
|
8
|
+
# Default theme inheriting from Theme::Base.
|
|
9
|
+
#
|
|
10
|
+
# Note: Theme::Base provides ANSI color helper methods (color_red,
|
|
11
|
+
# color_green, etc.) which return escape codes by default. If Liquid
|
|
12
|
+
# templates or filters use these methods, output may contain escape codes.
|
|
13
|
+
#
|
|
14
|
+
# For guaranteed plain text output without any terminal escape codes,
|
|
15
|
+
# use Theme::Plain instead, which overrides all color methods to
|
|
16
|
+
# return empty strings.
|
|
17
|
+
#
|
|
18
|
+
# @see Taski::Progress::Theme::Base Base class with color helpers
|
|
19
|
+
# @see Taski::Progress::Theme::Plain Plain output without escape codes
|
|
20
|
+
class Default < Base
|
|
21
|
+
# Inherits all methods from Base.
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "default"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Progress
|
|
7
|
+
module Theme
|
|
8
|
+
# Detail theme for rich progress display with spinner and icons.
|
|
9
|
+
# Provides spinner animation, colored icons for task states.
|
|
10
|
+
#
|
|
11
|
+
# Output format:
|
|
12
|
+
# ├── ⠹ DeployTask
|
|
13
|
+
# │ └── ✓ UploadFiles (1.2s)
|
|
14
|
+
# └── ✗ MigrateDB: Connection refused
|
|
15
|
+
#
|
|
16
|
+
# @example Usage
|
|
17
|
+
# layout = Taski::Progress::Layout::Tree.new(
|
|
18
|
+
# theme: Taski::Progress::Theme::Detail.new
|
|
19
|
+
# )
|
|
20
|
+
class Detail < Default
|
|
21
|
+
# Task pending with icon
|
|
22
|
+
def task_pending
|
|
23
|
+
"{% icon %} {{ task.name | short_name }}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Task start with spinner
|
|
27
|
+
def task_start
|
|
28
|
+
"{% spinner %} {{ task.name | short_name }}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Task success with colored icon
|
|
32
|
+
def task_success
|
|
33
|
+
"{% icon %} {{ task.name | short_name }}{% if task.duration %} ({{ task.duration | format_duration }}){% endif %}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Task fail with colored icon
|
|
37
|
+
def task_fail
|
|
38
|
+
"{% icon %} {{ task.name | short_name }}{% if task.error_message %}: {{ task.error_message }}{% endif %}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Task skip with colored icon
|
|
42
|
+
def task_skip
|
|
43
|
+
"{% icon %} {{ task.name | short_name }}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "default"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Progress
|
|
7
|
+
module Theme
|
|
8
|
+
# Plain theme for non-TTY environments (CI, log files, piped output).
|
|
9
|
+
# Outputs plain text without terminal escape codes or colors.
|
|
10
|
+
#
|
|
11
|
+
# @example Usage
|
|
12
|
+
# layout = Taski::Progress::Layout::Log.new(
|
|
13
|
+
# theme: Taski::Progress::Theme::Plain.new
|
|
14
|
+
# )
|
|
15
|
+
class Plain < Default
|
|
16
|
+
# === Color configuration (disabled for plain output) ===
|
|
17
|
+
|
|
18
|
+
def color_green
|
|
19
|
+
""
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def color_red
|
|
23
|
+
""
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def color_yellow
|
|
27
|
+
""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def color_dim
|
|
31
|
+
""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def color_reset
|
|
35
|
+
""
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|