class PowerBar

This is PowerBar - The last progressbar-library you'll ever need.

Constants

HQ_UNITS
RUBY18
STRIP_ANSI

Public Class Methods

new(opts={}) click to toggle source
# File lib/powerbar.rb, line 33
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[1G\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[1G\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[1G\e[?25l",
            :main => "${<msg>}: ${<done> }${<rate>/s }${<elapsed>}",
            :post => "\e[K",
            :wipe => "\e[0m\e[1G\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

Public Instance Methods

bar() click to toggle source

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 237
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
close(fill=false) click to toggle source

Print the close-template and defuse the exit-hook. Be a good citizen, always close your PowerBars!

# File lib/powerbar.rb, line 172
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
done() click to toggle source
# File lib/powerbar.rb, line 302
def done
  state.done
end
elapsed() click to toggle source
# File lib/powerbar.rb, line 269
def elapsed
  (state.time_now - state.time_start).to_f
end
eta() click to toggle source
# File lib/powerbar.rb, line 259
def eta
  (state.total - state.done) / rate
end
h_bar() click to toggle source
# File lib/powerbar.rb, line 247
def h_bar
  bar
end
h_done() click to toggle source
# File lib/powerbar.rb, line 306
def h_done
  humanize_quantity(state.done)
end
h_elapsed() click to toggle source
# File lib/powerbar.rb, line 273
def h_elapsed
  humanize_interval(elapsed)
end
h_eta() click to toggle source

returns nil when eta is < 1 second

# File lib/powerbar.rb, line 264
def h_eta
  return nil unless scope.show_eta
  1 < eta ? humanize_interval(eta) : nil
end
h_msg() click to toggle source
# File lib/powerbar.rb, line 255
def h_msg
  msg
end
h_percent() click to toggle source
# File lib/powerbar.rb, line 282
def h_percent
  sprintf "%d", percent
end
h_rate() click to toggle source
# File lib/powerbar.rb, line 290
def h_rate
  humanize_quantity(round(rate, 1))
end
h_total() click to toggle source
# File lib/powerbar.rb, line 298
def h_total
  humanize_quantity(state.total)
end
hook_exit() click to toggle source

Hook at_exit to ensure cleanup if we get interrupted

# File lib/powerbar.rb, line 160
def hook_exit
  return if @@exit_hooked
  if scope.template.exit
    at_exit do
      exit!
    end
  end
  @@exit_hooked = true
end
msg() click to toggle source
# File lib/powerbar.rb, line 251
def msg
  state.msg
end
percent() click to toggle source
# File lib/powerbar.rb, line 277
def percent
  return 0.0 if state.total.is_a? Symbol
  state.done.to_f/state.total*100
end
print(s) click to toggle source

Remove progress-bar, print a message

rate() click to toggle source
# File lib/powerbar.rb, line 286
def rate
  @rate.avg
end
render(opts={}) click to toggle source

Render the PowerBar and return as a string.

# File lib/powerbar.rb, line 224
def render(opts={})
  update(opts)
  render_template
end
scope() click to toggle source

settings under current scope (e.g. tty.infinite)

# File lib/powerbar.rb, line 139
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() click to toggle source

settings-hash

# File lib/powerbar.rb, line 134
def settings
  @state.settings
end
show(opts={}, force=false) click to toggle source

Output the PowerBar. Returns true if bar was shown, false otherwise.

# File lib/powerbar.rb, line 209
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
terminal_width() click to toggle source
# File lib/powerbar.rb, line 310
def terminal_width
  if /solaris/ =~ RUBY_PLATFORM && (%xstty` =~ /\brows = (\d+).*\bcolumns = (\d+)/)
    w, r = [$2, $1]
  else
    w, r = %xstty size 2>/dev/null`.split.reverse
  end
  w = %xtput cols` unless w
  w = w.to_i if w
  w
end
total() click to toggle source
# File lib/powerbar.rb, line 294
def total
  state.total
end
update(opts={}) click to toggle source

Update state (and settings) without printing anything.

# File lib/powerbar.rb, line 196
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
wipe() click to toggle source

Remove the PowerBar from the screen.

# File lib/powerbar.rb, line 230
def wipe
  scope.output.call(scope.template.wipe)
end

Private Instance Methods

exit!() click to toggle source

Cap'n Hook

# File lib/powerbar.rb, line 336
def exit!
  return if state.closed
  scope.output.call(scope.template.exit) unless scope.template.exit.nil?
end
humanize_interval(s) click to toggle source
# File lib/powerbar.rb, line 377
def humanize_interval(s)
  return nil if s.nil? or s.infinite?
  sprintf("%02d:%02d:%02d", s / 3600, s / 60 % 60, s % 60)
end
humanize_quantity(number, format='%n%u') click to toggle source
# File lib/powerbar.rb, line 361
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
render_template(tplid=:main, skip=[]) click to toggle source
# File lib/powerbar.rb, line 341
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
round(number, digit) click to toggle source
# File lib/powerbar.rb, line 326
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
state() click to toggle source
# File lib/powerbar.rb, line 322
def state
  @state
end