Class: Clack::Prompts::Spinner

Inherits:
Object
  • Object
show all
Defined in:
lib/clack/prompts/spinner.rb

Overview

Animated spinner for async operations.

Runs animation in a background thread. Call #start to begin, #stop/#error/#cancel to finish. Thread-safe message updates.

Indicator modes:

  • :dots - animating dots after message (default)
  • :timer - elapsed time display [Xs] or [Xm Ys]

Exit safety: a spinner still running when the process exits, whether by exit (including exit 0), Ctrl+C, or an uncaught exception, prints its cancel or error line first and stops the animation thread, instead of leaving a half-drawn frame. Clack.spin does the same when its block exits early. Exit and signals count as cancelled; any other exception counts as an error.

Examples:

Basic usage

s = Clack.spinner
s.start("Installing...")
# ... do work ...
s.stop("Done!")

With timer

s = Clack.spinner(indicator: :timer)
s.start("Building")
build_project
s.stop("Build complete")  # => "Build complete [12s]"

Updating message mid-spin

s = Clack.spinner
s.start("Step 1...")
do_step_1
s.message("Step 2...")
do_step_2
s.stop("All done!")

Cancel and error messages

s = Clack.spinner(cancel_message: "Deploy aborted", error_message: "Deploy failed",
                  on_cancel: -> { release_lock })
s.start("Deploying")
s.cancel   # => "■  Deploy aborted", then on_cancel runs

Defined Under Namespace

Modules: Registry

Instance Method Summary collapse

Constructor Details

#initialize(indicator: :dots, frames: nil, delay: nil, style_frame: nil, cancel_message: nil, error_message: nil, on_cancel: nil, with_guide: nil, output: $stdout) ⇒ Spinner

Returns a new instance of Spinner.

Parameters:

  • indicator (:dots, :timer) (defaults to: :dots)

    animation style (default: :dots)

  • frames (Array<String>, nil) (defaults to: nil)

    custom spinner frames

  • delay (Float, nil) (defaults to: nil)

    delay between frames in seconds

  • style_frame (Proc, nil) (defaults to: nil)

    proc to style each frame character

  • cancel_message (String, nil) (defaults to: nil)

    text for #cancel with no argument and for Ctrl+C / exit while running (default: the global messages, "Cancelled")

  • error_message (String, nil) (defaults to: nil)

    text for #error with no argument and for an uncaught exception while running (default: the global messages, "Something went wrong")

  • on_cancel (#call, nil) (defaults to: nil)

    called with no arguments after the spinner is cancelled; a StandardError raised by it is reported with Kernel#warn and swallowed

  • with_guide (Boolean, nil) (defaults to: nil)

    print the guide rail line above the spinner (default: Clack.settings)

  • output (IO) (defaults to: $stdout)

    output stream (default: $stdout)

Raises:

  • (ArgumentError)

    if cancel_message/error_message are not Strings or on_cancel does not respond to #call



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/clack/prompts/spinner.rb', line 152

def initialize(
  indicator: :dots,
  frames: nil,
  delay: nil,
  style_frame: nil,
  cancel_message: nil,
  error_message: nil,
  on_cancel: nil,
  with_guide: nil,
  output: $stdout
)
  validate_options(cancel_message, error_message, on_cancel)
  @messages = {cancel: cancel_message, error: error_message}.freeze
  @on_cancel = on_cancel
  @with_guide = with_guide
  @output = output
  @indicator = indicator
  @frames = frames || Symbols::SPINNER_FRAMES
  @delay = delay || Symbols::SPINNER_DELAY
  @style_frame = style_frame || ->(frame) { Colors.magenta(frame) }
  @state = :idle
  @message = ""
  @thread = nil
  @frame_idx = 0
  @prev_frame = nil
  # The pid that started the spinner and the monotonic start time; nil
  # while idle. One hash so the class stays within reek's ivar limit.
  @run = nil
  @mutex = Mutex.new
end

Instance Method Details

#abandon(exception = nil) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Finish a spinner whose caller is not coming back. Exit and signals end cancelled, any other exception ends in the error state, nil (break, throw, or a forgotten stop at process exit) ends cancelled. No-op unless running.

Parameters:

  • exception (Exception, nil) (defaults to: nil)


263
264
265
266
267
# File 'lib/clack/prompts/spinner.rb', line 263

def abandon(exception = nil)
  return unless running?

  crash?(exception) ? error : cancel
end

#cancel(message = nil) ⇒ Object

Stop with cancelled state and run on_cancel.

Parameters:

  • message (String, nil) (defaults to: nil)

    cancellation message (default: cancel_message:, then the global messages)



227
228
229
# File 'lib/clack/prompts/spinner.rb', line 227

def cancel(message = nil)
  finish(:cancelled, message)
end

#cancelled?Boolean

Returns true once #cancel has finished the spinner.

Returns:

  • (Boolean)

    true once #cancel has finished the spinner



252
# File 'lib/clack/prompts/spinner.rb', line 252

def cancelled? = @mutex.synchronize { @state == :cancelled }

#clearObject

Clear the spinner without showing a final message.



240
241
242
243
244
245
246
247
248
249
# File 'lib/clack/prompts/spinner.rb', line 240

def clear
  @mutex.synchronize do
    @state = :idle
    Registry.unregister(self)
  end
  @thread&.join
  restore_cursor
  @output.print Core::Cursor.clear_down
  @output.print Core::Cursor.show
end

#error(message = nil) ⇒ Object

Stop with error state.

Parameters:

  • message (String, nil) (defaults to: nil)

    error message (default: error_message:, then the global messages)



219
220
221
# File 'lib/clack/prompts/spinner.rb', line 219

def error(message = nil)
  finish(:error, message)
end

#message(msg) ⇒ Object

Update the spinner message while running.

Parameters:

  • msg (String)

    new message to display



234
235
236
237
# File 'lib/clack/prompts/spinner.rb', line 234

def message(msg)
  @mutex.synchronize { @message = remove_trailing_dots(msg) }
  self
end

#running?Boolean

Returns true between start and the first of stop/error/cancel/clear.

Returns:

  • (Boolean)

    true between start and the first of stop/error/cancel/clear



255
# File 'lib/clack/prompts/spinner.rb', line 255

def running? = @mutex.synchronize { @state == :running }

#start(message = nil) ⇒ self

Start the spinner animation.

Parameters:

  • message (String, nil) (defaults to: nil)

    initial message to display

Returns:

  • (self)

    for method chaining



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/clack/prompts/spinner.rb', line 187

def start(message = nil)
  @mutex.synchronize do
    return unless @state == :idle

    @message = remove_trailing_dots(message || "")
    @state = :running
    @prev_frame = nil
    @frame_idx = 0
    @run = {pid: Process.pid, started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC)}.freeze
    # Under the mutex so a concurrent finish cannot slip between the
    # state flip and registration and leave a finished spinner tracked.
    Registry.register(self)
  end

  @output.print Core::Cursor.hide
  @output.print "#{Colors.gray(Symbols::S_BAR)}\n" if Core::Settings.with_guide?(@with_guide)

  @thread = Thread.new { spin_loop }
  self
end

#stop(message = nil) ⇒ Object

Stop with success state.

Parameters:

  • message (String, nil) (defaults to: nil)

    final message (uses current if nil)



211
212
213
# File 'lib/clack/prompts/spinner.rb', line 211

def stop(message = nil)
  finish(:success, message)
end