lib/dev/ui/spinner.rb in dev-ui-0.1.0 vs lib/dev/ui/spinner.rb in dev-ui-0.1.1

- old
+ new

@@ -1,168 +1,48 @@ require 'dev/ui' module Dev module UI module Spinner + autoload :Async, 'dev/ui/spinner/async' + autoload :SpinGroup, 'dev/ui/spinner/spin_group' + PERIOD = 0.1 # seconds + TASK_FAILED = :task_failed begin runes = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] colors = [Dev::UI::Color::CYAN.code] * 5 + [Dev::UI::Color::MAGENTA.code] * 5 raise unless runes.size == colors.size GLYPHS = colors.zip(runes).map(&:join) end - def self.spin(title, &block) - sg = SpinGroup.new + # Adds a single spinner + # Uses an interactive session to allow the user to pick an answer + # Can use arrows, y/n, numbers (1/2), and vim bindings to control + # + # https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif + # + # ==== Attributes + # + # * +title+ - Title of the spinner to use + # + # ==== Options + # + # * +:auto_debrief+ - Automatically debrief exceptions? Default to true + # + # ==== Block + # + # * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes + # + # ==== Example Usage: + # + # Dev::UI::Spinner.spin('Title') { sleep 1.0 } + # + def self.spin(title, auto_debrief: true, &block) + sg = SpinGroup.new(auto_debrief: auto_debrief) sg.add(title, &block) sg.wait - end - - class SpinGroup - def initialize - @m = Mutex.new - @consumed_lines = 0 - @tasks = [] - end - - class Task - attr_reader :title, :exception, :success, :stdout, :stderr - - def initialize(title, &block) - @title = title - @thread = Thread.new do - cap = Dev::UI::StdoutRouter::Capture.new(with_frame_inset: false, &block) - begin - cap.run - ensure - @stdout = cap.stdout - @stderr = cap.stderr - end - end - - @done = false - @exception = nil - @success = false - end - - def check - return true if @done - return false if @thread.alive? - - @done = true - begin - status = @thread.join.status - @success = (status == false) - rescue => exc - @exception = exc - @success = false - end - - @done - end - - def render(index, force = true) - return full_render(index) if force - partial_render(index) - end - - private - - def full_render(index) - inset + glyph(index) + Dev::UI::Color::RESET.code + ' ' + Dev::UI.resolve_text(title) - end - - def partial_render(index) - Dev::UI::ANSI.cursor_forward(inset_width) + glyph(index) + Dev::UI::Color::RESET.code - end - - def glyph(index) - if @done - @success ? Dev::UI::Glyph::CHECK.to_s : Dev::UI::Glyph::X.to_s - else - GLYPHS[index] - end - end - - def inset - @inset ||= Dev::UI::Frame.prefix - end - - def inset_width - @inset_width ||= Dev::UI::ANSI.printing_width(inset) - end - end - - def add(title, &block) - @m.synchronize do - @tasks << Task.new(title, &block) - end - end - - def wait - idx = 0 - - loop do - all_done = true - - @m.synchronize do - Dev::UI.raw do - @tasks.each.with_index do |task, int_index| - nat_index = int_index + 1 - task_done = task.check - all_done = false unless task_done - - if nat_index > @consumed_lines - print(task.render(idx, true) + "\n") - @consumed_lines += 1 - else - offset = @consumed_lines - int_index - move_to = Dev::UI::ANSI.cursor_up(offset) + "\r" - move_from = "\r" + Dev::UI::ANSI.cursor_down(offset) - - print(move_to + task.render(idx, idx.zero?) + move_from) - end - end - end - end - - break if all_done - - idx = (idx + 1) % GLYPHS.size - sleep(PERIOD) - end - - debrief - end - - def debrief - @m.synchronize do - @tasks.each do |task| - next if task.success - - e = task.exception - out = task.stdout - err = task.stderr - - Dev::UI::Frame.open('Task Failed: ' + task.title, color: :red) do - if e - puts"#{e.class}: #{e.message}" - puts "\tfrom #{e.backtrace.join("\n\tfrom ")}" - end - - Dev::UI::Frame.divider('STDOUT') - out = "(empty)" if out.nil? || out.strip.empty? - puts out - - Dev::UI::Frame.divider('STDERR') - err = "(empty)" if err.nil? || err.strip.empty? - puts err - end - end - @tasks.all?(&:success) - end - end end end end end