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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +108 -50
  4. data/docs/GUIDE.md +79 -55
  5. data/examples/README.md +10 -29
  6. data/examples/clean_demo.rb +25 -65
  7. data/examples/large_tree_demo.rb +356 -0
  8. data/examples/message_demo.rb +0 -1
  9. data/examples/progress_demo.rb +13 -24
  10. data/examples/reexecution_demo.rb +8 -44
  11. data/lib/taski/execution/execution_facade.rb +150 -0
  12. data/lib/taski/execution/executor.rb +167 -359
  13. data/lib/taski/execution/fiber_protocol.rb +27 -0
  14. data/lib/taski/execution/registry.rb +15 -19
  15. data/lib/taski/execution/scheduler.rb +161 -140
  16. data/lib/taski/execution/task_observer.rb +41 -0
  17. data/lib/taski/execution/task_output_router.rb +41 -58
  18. data/lib/taski/execution/task_wrapper.rb +123 -219
  19. data/lib/taski/execution/worker_pool.rb +279 -64
  20. data/lib/taski/logging.rb +105 -0
  21. data/lib/taski/progress/layout/base.rb +600 -0
  22. data/lib/taski/progress/layout/filters.rb +126 -0
  23. data/lib/taski/progress/layout/log.rb +27 -0
  24. data/lib/taski/progress/layout/simple.rb +166 -0
  25. data/lib/taski/progress/layout/tags.rb +76 -0
  26. data/lib/taski/progress/layout/theme_drop.rb +84 -0
  27. data/lib/taski/progress/layout/tree.rb +300 -0
  28. data/lib/taski/progress/theme/base.rb +224 -0
  29. data/lib/taski/progress/theme/compact.rb +58 -0
  30. data/lib/taski/progress/theme/default.rb +25 -0
  31. data/lib/taski/progress/theme/detail.rb +48 -0
  32. data/lib/taski/progress/theme/plain.rb +40 -0
  33. data/lib/taski/static_analysis/analyzer.rb +5 -17
  34. data/lib/taski/static_analysis/dependency_graph.rb +19 -1
  35. data/lib/taski/static_analysis/start_dep_analyzer.rb +400 -0
  36. data/lib/taski/static_analysis/visitor.rb +1 -39
  37. data/lib/taski/task.rb +49 -58
  38. data/lib/taski/task_proxy.rb +59 -0
  39. data/lib/taski/test_helper/errors.rb +1 -1
  40. data/lib/taski/test_helper.rb +22 -36
  41. data/lib/taski/version.rb +1 -1
  42. data/lib/taski.rb +62 -61
  43. data/sig/taski.rbs +194 -203
  44. metadata +34 -8
  45. data/examples/section_demo.rb +0 -195
  46. data/lib/taski/execution/base_progress_display.rb +0 -393
  47. data/lib/taski/execution/execution_context.rb +0 -390
  48. data/lib/taski/execution/plain_progress_display.rb +0 -76
  49. data/lib/taski/execution/simple_progress_display.rb +0 -247
  50. data/lib/taski/execution/tree_progress_display.rb +0 -643
  51. 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