This is PowerBar - The last progressbar-library you'll ever need.
# File lib/powerbar.rb, line 34 def initialize(opts={}) @@exit_hooked = false @state = Hashie::Mash.new( { :time_last_show => Time.at(0), # <- don't mess with us :time_last_update => Time.at(0), # <- unless you know :time_start => nil, # <- what you're doing! :time_now => nil, # <- :msg => 'PowerBar!', :done => 0, :total => :unknown, :settings => { :rate_sample_max_interval => 10, # See PowerBar::Rate :rate_sample_window => 6, # See PowerBar::Rate :force_mode => nil, # set to :tty or :notty to force either mode :kilo => 1024, # Change this to 1000 when measuring network traffic or such. :tty => { # <== Settings when stdout is a tty :finite => { # <== Settings for a finite progress bar (when total != :unknown) # The :output Proc is called to draw on the screen --------------------. :output => Proc.new{ |s| $stderr.print s[0..terminal_width()-1] }, # <-' :interval => 0.1, # Minimum interval between screen refreshes (in seconds) :show_eta => true, # Set to false if you want to hide the ETA without changing the template :template => { # <== template for a finite progress bar on a tty :pre => "\e[1000D\e[?25l", # printed before the progress-bar # # :main is the progressbar template # # The following tokens are available: # msg, bar, rate, percent, elapsed, eta, done, total # # Tokens may be used like so: # ${<foo>} # OR: # ${surrounding <foo> text} # # The surrounding text is only rendered when <foo> # evaluates to something other than nil. :main => '${<msg>}: ${[<bar>] }${<rate>/s }${<percent>% }${<elapsed>}${, ETA: <eta>}', :post => '', # printed after the progressbar :wipe => "\e[0m\e[1000D\e[K", # printed when 'wipe' is called :close => "\e[?25h\n", # printed when 'close' is called :exit => "\e[?25h", # printed if the process exits unexpectedly :barchar => RUBY18 ? '#' : "\u2588", # fill-char for the progress-bar :padchar => RUBY18 ? '.' : "\u2022" # padding-char for the progress-bar }, }, :infinite => { # <== Settings for an infinite progress "bar" (when total is :unknown) :output => Proc.new{ |s| $stderr.print s[0..terminal_width()-1] }, :interval => 0.1, :show_eta => false, :template => { :pre => "\e[1000D\e[?25l", :main => "${<msg>}: ${<done> }${<rate>/s }${<elapsed>}", :post => "\e[K", :wipe => "\e[0m\e[1000D\e[K", :close => "\e[?25h\n", :exit => "\e[?25h", :barchar => RUBY18 ? '#' : "\u2588", :padchar => RUBY18 ? '.' : "\u2022" }, } }, :notty => { # <== Settings when stdout is not a tty :finite => { # You may want to hook in your favorite Logger-Library here. ---. :output => Proc.new{ |s| $stderr.print s }, # <----------------' :interval => 1, :show_eta => true, :line_width => 78, # Maximum output line width :template => { :pre => '', :main => "${<msg>}: ${<done>}/${<total>}, ${<percent>%}${, <rate>/s}${, elapsed: <elapsed>}${, ETA: <eta>}\n", :post => '', :wipe => '', :close => nil, :exit => nil, :barchar => "#", :padchar => "." }, }, :infinite => { :output => Proc.new{ |s| $stderr.print s }, :interval => 1, :show_eta => false, :line_width => 78, :template => { :pre => "", :main => "${<msg>}: ${<done> }${<rate>/s }${<elapsed>}\n", :post => "", :wipe => "", :close => nil, :exit => nil, :barchar => "#", :padchar => "." }, } } } }.merge(opts) ) end
Render the actual bar-portion of the PowerBar. The length of the bar is determined from the template. Returns nil if the bar-length would be == 0.
# File lib/powerbar.rb, line 238 def bar return nil if state.total.is_a? Symbol skel = render_template(:main, [:bar]) lwid = state.scope_at[0] == :tty ? terminal_width() : scope.line_width barlen = [lwid - skel.gsub(STRIP_ANSI, '').length, 0].max fill = [0,[(state.done.to_f/state.total*barlen).to_i,barlen].min].max thebar = scope.template.barchar * fill + scope.template.padchar * [barlen - fill,0].max thebar.length == 0 ? nil : thebar end
Print the close-template and defuse the exit-hook. Be a good citizen, always close your PowerBars!
# File lib/powerbar.rb, line 173 def close(fill=false) show( { :done => fill && !state.total.is_a?(Symbol) ? state.total : state.done, :tty => { :finite => { :show_eta => false }, :infinite => { :show_eta => false }, }, :notty => { :finite => { :show_eta => false }, :infinite => { :show_eta => false }, }, }, true) scope.output.call(scope.template.close) unless scope.template.close.nil? state.closed = true end
# File lib/powerbar.rb, line 303 def done state.done end
# File lib/powerbar.rb, line 270 def elapsed (state.time_now - state.time_start).to_f end
# File lib/powerbar.rb, line 260 def eta (state.total - state.done) / rate end
# File lib/powerbar.rb, line 248 def h_bar bar end
# File lib/powerbar.rb, line 307 def h_done humanize_quantity(state.done) end
# File lib/powerbar.rb, line 274 def h_elapsed humanize_interval(elapsed) end
returns nil when eta is < 1 second
# File lib/powerbar.rb, line 265 def h_eta return nil unless scope.show_eta 1 < eta ? humanize_interval(eta) : nil end
# File lib/powerbar.rb, line 256 def h_msg msg end
# File lib/powerbar.rb, line 283 def h_percent sprintf "%d", percent end
# File lib/powerbar.rb, line 291 def h_rate humanize_quantity(round(rate, 1)) end
# File lib/powerbar.rb, line 299 def h_total humanize_quantity(state.total) end
Hook at_exit to ensure cleanup if we get interrupted
# File lib/powerbar.rb, line 161 def hook_exit return if @@exit_hooked if scope.template.exit at_exit do exit! end end @@exit_hooked = true end
# File lib/powerbar.rb, line 252 def msg state.msg end
# File lib/powerbar.rb, line 278 def percent return 0.0 if state.total.is_a? Symbol state.done.to_f/state.total*100 end
Remove progress-bar, print a message
# File lib/powerbar.rb, line 191 def print(s) wipe scope.output.call(s) end
# File lib/powerbar.rb, line 287 def rate @rate.avg end
Render the PowerBar and return as a string.
# File lib/powerbar.rb, line 225 def render(opts={}) update(opts) render_template end
settings under current scope (e.g. tty.infinite)
# File lib/powerbar.rb, line 140 def scope scope_hash = [settings.force_mode,state.total].hash return @state.scope unless @state.scope.nil? or scope_hash != @state.scope_hash state.scope_at = [ settings.force_mode || ($stdout.isatty ? :tty : :notty), :unknown == state.total ? :infinite : :finite ] state.scope = state.settings state.scope_at.each do |s| begin state.scope = state.scope[s] rescue NoMethodError raise StandardError, "Invalid configuration: #{state.scope_at.join('.')} "+ "(Can't resolve: #{state.scope_at[state.scope_at.index(s)-1]})" end end state.scope_hash = scope_hash state.scope end
settings-hash
# File lib/powerbar.rb, line 135 def settings @state.settings end
Output the PowerBar. Returns true if bar was shown, false otherwise.
# File lib/powerbar.rb, line 210 def show(opts={}, force=false) return false if scope.interval > Time.now - state.time_last_show and force == false update(opts) hook_exit state.time_last_show = Time.now state.closed = false scope.output.call(scope.template.pre) scope.output.call(render) scope.output.call(scope.template.post) true end
# File lib/powerbar.rb, line 311 def terminal_width ANSI::Terminal.terminal_width - 1 end
# File lib/powerbar.rb, line 295 def total state.total end
Update state (and settings) without printing anything.
# File lib/powerbar.rb, line 197 def update(opts={}) state.merge!(opts) state.time_start ||= Time.now state.time_now = Time.now @rate ||= PowerBar::Rate.new(state.time_now, state.settings.rate_sample_window, state.settings.rate_sample_max_interval) @rate.append(state.time_now, state.done) end
Remove the PowerBar from the screen.
# File lib/powerbar.rb, line 231 def wipe scope.output.call(scope.template.wipe) end
Cap'n Hook
# File lib/powerbar.rb, line 330 def exit! return if state.closed scope.output.call(scope.template.exit) unless scope.template.exit.nil? end
# File lib/powerbar.rb, line 371 def humanize_interval(s) return nil if s.nil? or s.infinite? sprintf("%02d:%02d:%02d", s / 3600, s / 60 % 60, s % 60) end
# File lib/powerbar.rb, line 355 def humanize_quantity(number, format='%n%u') return nil if number.nil? return nil if number.is_a? Float and (number.nan? or number.infinite?) kilo = settings.kilo return number if number.to_i < kilo max_exp = HQ_UNITS.size - 1 number = Float(number) exponent = (Math.log(number) / Math.log(kilo)).to_i exponent = max_exp if exponent > max_exp number /= kilo ** exponent unit = HQ_UNITS[exponent] return format.gsub(/%n/, round(number, 1).to_s).gsub(/%u/, unit) end
# File lib/powerbar.rb, line 335 def render_template(tplid=:main, skip=[]) tpl = scope.template[tplid] skip.each do |s| tpl = tpl.gsub(/\$\{([^<]*)<#{s}>([^}]*)\}/, '\1\2') end tpl.gsub(/\$\{[^\}]+\}/) do |var| sub = nil r = var.gsub(/<[^>]+>/) do |t| t = t[1..-2] begin sub = send "h_#{t}" rescue NoMethodError raise NameError, "Invalid token '#{t}' in template '#{tplid}'" end end sub.nil? ? '' : r[2..-2] end end
# File lib/powerbar.rb, line 320 def round(number, digit) if number.zero? or number.nan? or number.infinite? number else factor = 10.0 ** digit (number * factor).round / factor end end
# File lib/powerbar.rb, line 316 def state @state end