Class: CLI::UI::Spinner::SpinGroup

Inherits:
Object
  • Object
show all
Defined in:
lib/cli/ui/spinner/spin_group.rb

Defined Under Namespace

Classes: Task

Constant Summary collapse

DEFAULT_FINAL_GLYPH =
->(success) { success ? CLI::UI::Glyph::CHECK : CLI::UI::Glyph::X }

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(auto_debrief: true, interrupt_debrief: false, max_concurrent: 0, work_queue: nil, to: $stdout) ⇒ SpinGroup

Initializes a new spin group This lets you add Task objects to the group to multi-thread work

Options

  • :auto_debrief - Automatically debrief exceptions or through success_debrief? Default to true

  • :interrupt_debrief - Automatically debrief on interrupt. Default to false

  • :max_concurrent - Maximum number of concurrent tasks. Default is 0 (effectively unlimited)

  • :work_queue - Custom WorkQueue instance. If not provided, a new one will be created

  • :to - Target stream, like $stdout or $stderr. Can be anything with print and puts methods, or under Sorbet, IO or StringIO. Defaults to $stdout

Example Usage

CLI::UI::SpinGroup.new do |spin_group|
  spin_group.add('Title')   { |spinner| sleep 3.0 }
  spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
end

Output:

: (?auto_debrief: bool, ?interrupt_debrief: bool, ?max_concurrent: Integer, ?work_queue: WorkQueue?, ?to: io_like) -> void



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/cli/ui/spinner/spin_group.rb', line 63

def initialize(auto_debrief: true, interrupt_debrief: false, max_concurrent: 0, work_queue: nil, to: $stdout)
  @m = Mutex.new
  @tasks = []
  @puts_above = []
  @auto_debrief = auto_debrief
  @interrupt_debrief = interrupt_debrief
  @start = Time.new
  @stopped = false
  @internal_work_queue = work_queue.nil?
  @work_queue = work_queue || WorkQueue.new(max_concurrent.zero? ? 1024 : max_concurrent) #: WorkQueue
  if block_given?
    yield self
    wait(to: to)
  end
end

Class Attribute Details

.pause_mutexObject (readonly)

: Mutex



14
15
16
# File 'lib/cli/ui/spinner/spin_group.rb', line 14

def pause_mutex
  @pause_mutex
end

Class Method Details

.pause_spinners(&block) ⇒ Object

: [T] { -> T } -> T



22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/cli/ui/spinner/spin_group.rb', line 22

def pause_spinners(&block)
  previous_paused = nil #: bool?
  @pause_mutex.synchronize do
    previous_paused = @paused
    @paused = true
  end
  block.call
ensure
  @pause_mutex.synchronize do
    @paused = previous_paused
  end
end

.paused?Boolean

: -> bool

Returns:

  • (Boolean)


17
18
19
# File 'lib/cli/ui/spinner/spin_group.rb', line 17

def paused?
  @paused
end

Instance Method Details

#add(title, final_glyph: DEFAULT_FINAL_GLYPH, merged_output: false, duplicate_output_to: File.new(File::NULL, 'w'), &block) ⇒ Object

Add a new task

Attributes

  • title - Title of the task

  • block - Block for the task, will be provided with an instance of the spinner

Example Usage:

spin_group = CLI::UI::SpinGroup.new
spin_group.add('Title') { |spinner| sleep 1.0 }
spin_group.wait

: (String title, ?final_glyph: ^(bool success) -> (Glyph | String), ?merged_output: bool, ?duplicate_output_to: IO) { (Task task) -> void } -> void



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/cli/ui/spinner/spin_group.rb', line 304

def add(
  title,
  final_glyph: DEFAULT_FINAL_GLYPH,
  merged_output: false,
  duplicate_output_to: File.new(File::NULL, 'w'),
  &block
)
  @m.synchronize do
    @tasks << Task.new(
      title,
      final_glyph: final_glyph,
      merged_output: merged_output,
      duplicate_output_to: duplicate_output_to,
      work_queue: @work_queue,
      &block
    )
  end
end

#all_succeeded?Boolean

: -> bool

Returns:

  • (Boolean)


464
465
466
467
468
# File 'lib/cli/ui/spinner/spin_group.rb', line 464

def all_succeeded?
  @m.synchronize do
    @tasks.all?(&:success)
  end
end

#failure_debrief(&block) ⇒ Object

Provide an alternative debriefing for failed tasks : { (String title, Exception? exception, String out, String err) -> void } -> void



453
454
455
# File 'lib/cli/ui/spinner/spin_group.rb', line 453

def failure_debrief(&block)
  @failure_debrief = block
end

#puts_above(message) ⇒ Object

: (String message) -> void



445
446
447
448
449
# File 'lib/cli/ui/spinner/spin_group.rb', line 445

def puts_above(message)
  @m.synchronize do
    @puts_above << message
  end
end

#stopObject

: -> void



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/cli/ui/spinner/spin_group.rb', line 324

def stop
  # If we already own the mutex (called from within another synchronized block),
  # set stopped directly to avoid deadlock
  if @m.owned?
    return if @stopped

    @stopped = true
  else
    @m.synchronize do
      return if @stopped

      @stopped = true
    end
  end
  # Interrupt is thread-safe on its own, so we can call it outside the mutex
  @work_queue.interrupt
end

#stopped?Boolean

: -> bool

Returns:

  • (Boolean)


343
344
345
346
347
348
349
# File 'lib/cli/ui/spinner/spin_group.rb', line 343

def stopped?
  if @m.owned?
    @stopped
  else
    @m.synchronize { @stopped }
  end
end

#success_debrief(&block) ⇒ Object

Provide a debriefing for successful tasks : { (String title, String out, String err) -> void } -> void



459
460
461
# File 'lib/cli/ui/spinner/spin_group.rb', line 459

def success_debrief(&block)
  @success_debrief = block
end

#wait(to: $stdout) ⇒ Object

Tells the group you’re done adding tasks and to wait for all of them to finish

Options

  • :to - Target stream, like $stdout or $stderr. Can be anything with print and puts methods, or under Sorbet, IO or StringIO. Defaults to $stdout

Example Usage:

spin_group = CLI::UI::SpinGroup.new
spin_group.add('Title') { |spinner| sleep 1.0 }
spin_group.wait

: (?to: io_like) -> bool



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/cli/ui/spinner/spin_group.rb', line 364

def wait(to: $stdout)
  result = false #: bool

  CLI::UI::ProgressReporter.with_progress(mode: :indeterminate, to: to, delay_start: true) do |reporter|
    idx = 0
    consumed_lines = 0

    @work_queue.close if @internal_work_queue

    tasks_seen = @tasks.map { false }
    tasks_seen_done = @tasks.map { false }

    current_mode = :indeterminate #: Symbol
    first_render = true #: bool

    loop do
      break if stopped?

      done_count = 0
      width = CLI::UI::Terminal.width

      # Update progress mode based on task states
      current_mode = update_progress_mode(reporter, current_mode, first_render)

      self.class.pause_mutex.synchronize do
        next if self.class.paused?

        @m.synchronize do
          CLI::UI.raw do
            # Render any messages above the spinner
            force_full_render = render_puts_above(to, consumed_lines)

            # Render all tasks
            done_count, consumed_lines = render_tasks(
              to: to,
              tasks_seen: tasks_seen,
              tasks_seen_done: tasks_seen_done,
              consumed_lines: consumed_lines,
              idx: idx,
              force_full_render: force_full_render,
              width: width,
            )
          end
        end
      end

      break if done_count == @tasks.size

      # After first render, start the progress reporter in indeterminate mode
      if first_render
        reporter.force_set_indeterminate
        first_render = false
      end

      idx = (idx + 1) % GLYPHS.size
      Spinner.index = idx
      sleep(PERIOD)
    end

    # Show error state briefly if tasks failed
    success = all_succeeded?
    unless success
      reporter.set_error
      sleep(0.5)
    end

    result = if @auto_debrief
      debrief(to: to)
    else
      all_succeeded?
    end
  end

  result
rescue Interrupt
  @work_queue.interrupt
  debrief(to: to) if @interrupt_debrief
  stopped? ? false : raise
end