lib/support/rake/task_with_spinner.rb (101 lines of code) (raw):

# frozen_string_literal: true begin require 'tty-spinner' require 'tty-screen' rescue LoadError end module Support module Rake module TaskWithSpinner class << self attr_accessor :spinner_manager, :screen_cols end TaskSkippedError = Class.new(StandardError) def self.set_screen_cols self.screen_cols = TTY::Screen.cols end def self.set_screen_cols_on_window_size_change Signal.trap('WINCH') { set_screen_cols } end def enable_spinner! return unless GDK::Output.interactive? return unless defined?(TTY::Spinner) Support::Rake::TaskWithSpinner.set_screen_cols Support::Rake::TaskWithSpinner.set_screen_cols_on_window_size_change @enable_spinner = true end def invoke(...) if @enable_spinner TaskWithSpinner.spinner_manager&.stop TaskWithSpinner.spinner_manager = ::TTY::Spinner::Multi.new( spinner_name, success_mark: "\e[32m#{TTY::Spinner::TICK}\e[0m", error_mark: "\e[31m#{TTY::Spinner::CROSS}\e[0m", format: :dots, # $stderr is overwritten in TaskWithLogger output: STDERR # rubocop:disable Style/GlobalStdStream ) end super ensure TaskWithSpinner.spinner_manager&.stop if @enable_spinner end def execute(...) @gdk_execute_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) spinner = nil if TaskWithSpinner.spinner_manager && show_spinner? logger = Support::Rake::TaskLogger.current thread = Thread.new do Support::Rake::TaskLogger.set_current!(logger) sleep 0.001 next if @skipped || TaskWithSpinner.spinner_manager.top_spinner.message == spinner_name spinner = TaskWithSpinner.spinner_manager.register spinner_name(':recent_line') spinner.update(recent_line: '') spinner.on(:spin, &on_spin(logger, spinner)) spinner.auto_spin end end super rescue StandardError => e spinner&.update(recent_line: '') spinner&.error(execution_duration_message) raise e unless e.instance_of?(TaskSkippedError) else spinner&.update(recent_line: '') spinner&.success(execution_duration_message) ensure thread&.join end def skip! @skipped = true raise TaskSkippedError end private # rubocop:disable Style/AsciiComments -- Real-world example # "└── ⠏ Run " # rubocop:enable Style/AsciiComments -- Real-world example LABEL_PREFIX_LENGTH = 10 def on_spin(logger, spinner) proc do line_limit = Support::Rake::TaskWithSpinner.screen_cols - LABEL_PREFIX_LENGTH - name.length recent_line = logger.recent_line&.slice(0, line_limit) if recent_line && @previous_recent_line != recent_line @previous_recent_line = recent_line recent_line = GDK::Output.wrap_in_color(recent_line, GDK::Output::COLOR_CODE_BRIGHT_BLACK) spinner.update(recent_line: recent_line.prepend(' ')) end end end # A task without action (i.e. do ... end block) will finish # instantly after all dependencies have finished, so we don't want # to show a spinner for it. def show_spinner? !actions.empty? end def spinner_name(appendix = '') ":spinner #{comment || name}#{appendix}" end def execution_duration_message duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - (@gdk_execute_start || 0) "[#{format_duration(duration)}]" end def format_duration(seconds) return "#{(seconds * 1000).floor}ms" if seconds < 1 return "#{seconds.round}s" if seconds < 60 "#{(seconds / 60).floor}m #{seconds.round % 60}s" end end end end