taski 0.9.0 → 0.9.2

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.
@@ -1,299 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
- require_relative "../theme/detail"
3
+ require_relative "tree/structure"
4
+ require_relative "tree/live"
5
+ require_relative "tree/event"
5
6
 
6
7
  module Taski
7
8
  module Progress
8
9
  module Layout
9
- # Tree layout for hierarchical task display.
10
- # Renders tasks in a tree structure with visual connectors (├──, └──, │).
10
+ # Tree layout module for hierarchical task display.
11
+ # Renders tasks in a tree structure with visual connectors.
11
12
  #
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)
13
+ # Contains two implementations:
14
+ # - Tree::Live — TTY periodic-update with spinner animation
15
+ # - Tree::Event — Non-TTY event-driven incremental output
15
16
  #
16
- # Output format (live updating):
17
- # BuildApplication
18
- # ├── ⠹ SetupDatabase
19
- # │ ├── ✓ CreateSchema (50ms)
20
- # │ └── ⠹ SeedData
21
- # ├── ○ ExtractLayers
22
- # │ ├── ✓ DownloadLayer1 (100ms)
23
- # │ └── ○ DownloadLayer2
24
- # └── ✓ RunSystemCommand (200ms)
17
+ # Use Tree.for to automatically select the appropriate implementation.
25
18
  #
26
- # The tree structure (prefixes) is added by this Layout.
27
- # The task content (icons, names, duration) comes from the Theme.
19
+ # @example Auto-select based on output TTY
20
+ # layout = Taski::Progress::Layout::Tree.for
28
21
  #
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)
22
+ # @example Explicit selection
23
+ # layout = Taski::Progress::Layout::Tree::Live.new # TTY
24
+ # layout = Taski::Progress::Layout::Tree::Event.new # non-TTY
25
+ module Tree
26
+ # Factory method to create the appropriate tree layout.
27
+ # Returns Tree::Live for TTY outputs, Tree::Event otherwise.
28
+ #
29
+ # @param output [IO] Output stream (default: $stderr)
30
+ # @param theme [Theme::Base, nil] Theme instance
31
+ # @return [Tree::Live, Tree::Event]
32
+ def self.for(output: $stderr, theme: nil)
33
+ if output.respond_to?(:tty?) && output.tty?
34
+ Live.new(output: output, theme: theme)
272
35
  else
273
- task = TaskDrop.new(name: task_class_name(task_class), state: :pending)
274
- render_task_template(:task_pending, task:, execution: execution_drop)
36
+ Event.new(output: output, theme: theme)
275
37
  end
276
38
  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
39
  end
298
40
  end
299
41
  end
@@ -84,7 +84,7 @@ module Taski
84
84
  end
85
85
 
86
86
  def execution_complete
87
- "[TASKI] Completed: {{ execution.completed_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
87
+ "[TASKI] Completed: {{ execution.done_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
88
88
  end
89
89
 
90
90
  def execution_fail
@@ -45,7 +45,7 @@ module Taski
45
45
 
46
46
  # Execution complete with icon
47
47
  def execution_complete
48
- "{% icon %} [TASKI] Completed: {{ execution.completed_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
48
+ "{% icon %} [TASKI] Completed: {{ execution.done_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
49
49
  end
50
50
 
51
51
  # Execution fail with icon