taski 0.8.2 → 0.8.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ecbd73a50d1f39207625cda94e9f90a90ce212d270a70ed49cd94705bb30a5c
4
- data.tar.gz: d5efd7fcf8c5c0d5bdfacf147e302375e63d41acd00f428924e87ba3a4e54ac0
3
+ metadata.gz: cddae184fb1f55c69b42995e51c3bf5711d31f978ec383a00a73d2ce3531ca15
4
+ data.tar.gz: 3e79ca2bf3a6cd163272bbff98ff3b980f3a3b69293607dd45879501d50904c4
5
5
  SHA512:
6
- metadata.gz: e263e9c974f17ff46f545adc3cf90cddf66cbfe273c783d07f036a2d3fc4c7f8ec4759c1a94ff72ab0b302d8c4fae50a4654cda5b4b17d9f218ae897ba67572a
7
- data.tar.gz: 9bf8d6b053ee781ef9ba2535926ef663be9ed45d88249d59a5325d7cbd600468ae5bacc69e506ae5bc7f2819707b9e4451a0faf0aa377dbbfb33fe04a7d9199b
6
+ metadata.gz: 9310e8b52be98d9087de88b4b7f5d105896edd85137c2f43e6c968c8daaf00dc6b4843ff9d1f228a089dcd7ceb379715c2b640d3c547ce173659dc7130cd1768
7
+ data.tar.gz: 65bade2024691c9c7527f6249294d01714563ff35c211472e6d984e82ba5acd0767251d33e05d8ade4208eabc8b1326eb47fc4f932b4699bf0b986e208fffcea
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.3] - 2026-01-26
11
+
12
+ ### Fixed
13
+ - Improve progress display accuracy for section candidates ([#136](https://github.com/ahogappa/taski/pull/136))
14
+
15
+ ### Changed
16
+ - Extract helper methods for improved readability ([#136](https://github.com/ahogappa/taski/pull/136))
17
+
10
18
  ## [0.8.2] - 2026-01-26
11
19
 
12
20
  ### Fixed
@@ -290,8 +290,30 @@ module Taski
290
290
  @output.tty?
291
291
  end
292
292
 
293
+ # Collect all dependencies of a task class recursively
294
+ # Useful for determining which tasks are needed by a selected implementation
295
+ # @param task_class [Class] The task class to collect dependencies for
296
+ # @return [Set<Class>] Set of all dependency task classes (including the task itself)
297
+ def collect_all_dependencies(task_class)
298
+ deps = Set.new
299
+ collect_dependencies_recursive(task_class, deps)
300
+ deps
301
+ end
302
+
293
303
  private
294
304
 
305
+ # Recursively collect dependencies into the given set
306
+ # @param task_class [Class] The task class
307
+ # @param collected [Set<Class>] Accumulated dependencies
308
+ def collect_dependencies_recursive(task_class, collected)
309
+ return if collected.include?(task_class)
310
+ collected.add(task_class)
311
+
312
+ task_class.cached_dependencies.each do |dep|
313
+ collect_dependencies_recursive(dep, collected)
314
+ end
315
+ end
316
+
295
317
  # Flush all queued messages to output.
296
318
  # Called when progress display stops.
297
319
  def flush_queued_messages
@@ -300,12 +322,15 @@ module Taski
300
322
  end
301
323
 
302
324
  # Apply state transition to TaskProgress
325
+ # Note: Once a task reaches :completed or :failed, it cannot go back to :running.
326
+ # This prevents progress count from decreasing when nested executors re-execute tasks.
303
327
  def apply_state_transition(progress, state, duration, error)
304
328
  case state
305
329
  # Run lifecycle states
306
330
  when :pending
307
331
  progress.run_state = :pending
308
332
  when :running
333
+ return if run_state_finalized?(progress)
309
334
  progress.run_state = :running
310
335
  progress.run_start_time = Time.now
311
336
  when :completed
@@ -331,6 +356,10 @@ module Taski
331
356
  end
332
357
  end
333
358
 
359
+ def run_state_finalized?(progress)
360
+ progress.run_state == :completed || progress.run_state == :failed
361
+ end
362
+
334
363
  # Apply state transition to GroupProgress
335
364
  def apply_group_state_transition(progress, group_name, state, duration, error)
336
365
  case state
@@ -35,6 +35,7 @@ module Taski
35
35
  @renderer_thread = nil
36
36
  @running = false
37
37
  @section_candidates = {} # section_class => [candidate_classes]
38
+ @section_candidate_subtrees = {} # section_class => { candidate_class => subtree_node }
38
39
  end
39
40
 
40
41
  protected
@@ -50,18 +51,9 @@ module Taski
50
51
  @tasks[impl_class].is_impl_candidate = false
51
52
 
52
53
  # Mark the section itself as completed (it's represented by its impl)
53
- if @tasks[section_class]
54
- @tasks[section_class].run_state = :completed
55
- end
54
+ @tasks[section_class]&.run_state = :completed
56
55
 
57
- # Mark unselected candidates as completed (skipped)
58
- candidates = @section_candidates[section_class] || []
59
- candidates.each do |candidate|
60
- next if candidate == impl_class
61
- progress = @tasks[candidate]
62
- next unless progress
63
- progress.run_state = :completed
64
- end
56
+ mark_unselected_candidates_completed(section_class, impl_class)
65
57
  end
66
58
 
67
59
  # Template method: Determine if display should activate
@@ -106,17 +98,50 @@ module Taski
106
98
 
107
99
  task_class = node[:task_class]
108
100
 
109
- # If this is a section, collect its implementation candidates
101
+ # If this is a section, collect its implementation candidates and their subtrees
110
102
  if node[:is_section]
111
- candidates = node[:children]
112
- .select { |c| c[:is_impl_candidate] }
113
- .map { |c| c[:task_class] }
103
+ candidate_nodes = node[:children].select { |c| c[:is_impl_candidate] }
104
+ candidates = candidate_nodes.map { |c| c[:task_class] }
114
105
  @section_candidates[task_class] = candidates unless candidates.empty?
106
+
107
+ # Store subtrees for each candidate to mark descendants as completed when not selected
108
+ subtrees = {}
109
+ candidate_nodes.each { |c| subtrees[c[:task_class]] = c }
110
+ @section_candidate_subtrees[task_class] = subtrees unless subtrees.empty?
115
111
  end
116
112
 
117
113
  node[:children].each { |child| collect_section_candidates(child) }
118
114
  end
119
115
 
116
+ # Mark unselected candidates and their exclusive subtrees as completed (skipped)
117
+ def mark_unselected_candidates_completed(section_class, impl_class)
118
+ selected_deps = collect_all_dependencies(impl_class)
119
+ candidates = @section_candidates[section_class] || []
120
+ subtrees = @section_candidate_subtrees[section_class] || {}
121
+
122
+ candidates.each do |candidate|
123
+ next if candidate == impl_class
124
+ mark_subtree_completed(subtrees[candidate], exclude: selected_deps)
125
+ end
126
+ end
127
+
128
+ # Recursively mark all pending tasks in a subtree as completed (skipped)
129
+ # Only marks :pending tasks to avoid overwriting :running or :completed states
130
+ # @param node [Hash] The subtree node
131
+ # @param exclude [Set<Class>] Set of task classes to exclude (dependencies of selected impl)
132
+ def mark_subtree_completed(node, exclude: Set.new)
133
+ return unless node
134
+
135
+ task_class = node[:task_class]
136
+ mark_task_as_skipped(task_class) unless exclude.include?(task_class)
137
+ node[:children].each { |child| mark_subtree_completed(child, exclude: exclude) }
138
+ end
139
+
140
+ def mark_task_as_skipped(task_class)
141
+ progress = @tasks[task_class]
142
+ progress.run_state = :completed if progress&.run_state == :pending
143
+ end
144
+
120
145
  def render_live
121
146
  @monitor.synchronize do
122
147
  @spinner_index = (@spinner_index + 1) % SPINNER_FRAMES.size
@@ -153,37 +178,53 @@ module Taski
153
178
  def build_status_line
154
179
  running_tasks = @tasks.select { |_, p| p.run_state == :running }
155
180
  cleaning_tasks = @tasks.select { |_, p| p.clean_state == :cleaning }
156
- # Count both completed and failed tasks as "done"
157
- done = @tasks.values.count { |p| p.run_state == :completed || p.run_state == :failed }
158
- failed = @tasks.values.count { |p| p.run_state == :failed }
159
- total = @tasks.size
181
+ pending_tasks = @tasks.select { |_, p| p.run_state == :pending }
182
+ failed_count = @tasks.values.count { |p| p.run_state == :failed }
183
+ done_count = @tasks.values.count { |p| p.run_state == :completed || p.run_state == :failed }
184
+
185
+ status_icon = determine_status_icon(failed_count, running_tasks, cleaning_tasks, pending_tasks)
186
+ task_names = format_current_task_names(cleaning_tasks, running_tasks, pending_tasks)
160
187
 
161
- spinner = SPINNER_FRAMES[@spinner_index]
162
- status_icon = if failed > 0
188
+ primary_task = running_tasks.keys.first || cleaning_tasks.keys.first
189
+ output_suffix = build_output_suffix(primary_task)
190
+
191
+ build_status_parts(status_icon, done_count, @tasks.size, task_names, output_suffix)
192
+ end
193
+
194
+ def determine_status_icon(failed_count, running_tasks, cleaning_tasks, pending_tasks)
195
+ # Show success only when no failed, running, cleaning, or pending tasks
196
+ # This prevents showing checkmark briefly when mark_subtree_completed marks tasks
197
+ if failed_count > 0
163
198
  "#{COLORS[:red]}#{ICONS[:failure]}#{COLORS[:reset]}"
164
- elsif running_tasks.any? || cleaning_tasks.any?
199
+ elsif running_tasks.any? || cleaning_tasks.any? || pending_tasks.any?
200
+ spinner = SPINNER_FRAMES[@spinner_index]
165
201
  "#{COLORS[:yellow]}#{spinner}#{COLORS[:reset]}"
166
202
  else
167
203
  "#{COLORS[:green]}#{ICONS[:success]}#{COLORS[:reset]}"
168
204
  end
205
+ end
169
206
 
170
- # Get current task names
207
+ def format_current_task_names(cleaning_tasks, running_tasks, pending_tasks)
208
+ # Prioritize: cleaning > running > pending
171
209
  current_tasks = if cleaning_tasks.any?
172
- cleaning_tasks.keys.map { |t| short_name(t) }
210
+ cleaning_tasks.keys
211
+ elsif running_tasks.any?
212
+ running_tasks.keys
213
+ elsif pending_tasks.any?
214
+ pending_tasks.keys.first(3)
173
215
  else
174
- running_tasks.keys.map { |t| short_name(t) }
216
+ []
175
217
  end
176
218
 
177
- task_names = current_tasks.first(3).join(", ")
178
- task_names += "..." if current_tasks.size > 3
179
-
180
- # Get last output message if available
181
- output_suffix = build_output_suffix(running_tasks.keys.first || cleaning_tasks.keys.first)
219
+ names = current_tasks.first(3).map { |t| short_name(t) }.join(", ")
220
+ names += "..." if current_tasks.size > 3
221
+ names
222
+ end
182
223
 
183
- parts = ["#{status_icon} [#{done}/#{total}]"]
184
- parts << task_names if task_names && !task_names.empty?
224
+ def build_status_parts(status_icon, done_count, total, task_names, output_suffix)
225
+ parts = ["#{status_icon} [#{done_count}/#{total}]"]
226
+ parts << task_names unless task_names.empty?
185
227
  parts << "|" << output_suffix if output_suffix
186
-
187
228
  parts.join(" ")
188
229
  end
189
230
 
data/lib/taski/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taski
4
- VERSION = "0.8.2"
4
+ VERSION = "0.8.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taski
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ahogappa