Class: PowerBar

Inherits:
Object
  • Object
show all
Defined in:
lib/powerbar.rb

Overview

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

Defined Under Namespace

Classes: Rate

Constant Summary collapse

STRIP_ANSI =
Regexp.compile '\e\[(\d+)(;\d+)?(;\d+)?[m|K]', nil
RUBY18 =
RUBY_VERSION[0..2] == "1.8"

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ PowerBar

Returns a new instance of PowerBar.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 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

Instance Method Details

#barObject

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.



237
238
239
240
241
242
243
244
245
# 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) ⇒ Object

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



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# 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

#doneObject



302
303
304
# File 'lib/powerbar.rb', line 302

def done
  state.done
end

#elapsedObject



269
270
271
# File 'lib/powerbar.rb', line 269

def elapsed
  (state.time_now - state.time_start).to_f
end

#etaObject



259
260
261
# File 'lib/powerbar.rb', line 259

def eta
  (state.total - state.done) / rate
end

#h_barObject



247
248
249
# File 'lib/powerbar.rb', line 247

def h_bar
  bar
end

#h_doneObject



306
307
308
# File 'lib/powerbar.rb', line 306

def h_done
  humanize_quantity(state.done)
end

#h_elapsedObject



273
274
275
# File 'lib/powerbar.rb', line 273

def h_elapsed
  humanize_interval(elapsed)
end

#h_etaObject

returns nil when eta is < 1 second



264
265
266
267
# File 'lib/powerbar.rb', line 264

def h_eta
  return nil unless scope.show_eta
  1 < eta ? humanize_interval(eta) : nil
end

#h_msgObject



255
256
257
# File 'lib/powerbar.rb', line 255

def h_msg
  msg
end

#h_percentObject



282
283
284
# File 'lib/powerbar.rb', line 282

def h_percent
  sprintf "%d", percent
end

#h_rateObject



290
291
292
# File 'lib/powerbar.rb', line 290

def h_rate
  humanize_quantity(round(rate, 1))
end

#h_totalObject



298
299
300
# File 'lib/powerbar.rb', line 298

def h_total
  humanize_quantity(state.total)
end

#hook_exitObject

Hook at_exit to ensure cleanup if we get interrupted



160
161
162
163
164
165
166
167
168
# 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

#msgObject



251
252
253
# File 'lib/powerbar.rb', line 251

def msg
  state.msg
end

#percentObject



277
278
279
280
# 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

Remove progress-bar, print a message



190
191
192
193
# File 'lib/powerbar.rb', line 190

def print(s)
  wipe
  scope.output.call(s)
end

#rateObject



286
287
288
# File 'lib/powerbar.rb', line 286

def rate
  @rate.avg
end

#render(opts = {}) ⇒ Object

Render the PowerBar and return as a string.



224
225
226
227
# File 'lib/powerbar.rb', line 224

def render(opts={})
  update(opts)
  render_template
end

#scopeObject

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



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# 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

#settingsObject

settings-hash



134
135
136
# File 'lib/powerbar.rb', line 134

def settings
  @state.settings
end

#show(opts = {}, force = false) ⇒ Object

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



209
210
211
212
213
214
215
216
217
218
219
220
221
# 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_widthObject



310
311
312
313
314
315
316
317
318
319
# File 'lib/powerbar.rb', line 310

def terminal_width
  if /solaris/ =~ RUBY_PLATFORM && (`stty` =~ /\brows = (\d+).*\bcolumns = (\d+)/)
    w, r = [$2, $1]
  else
    w, r = `stty size 2>/dev/null`.split.reverse
  end
  w = `tput cols` unless w
  w = w.to_i if w
  w
end

#totalObject



294
295
296
# File 'lib/powerbar.rb', line 294

def total
  state.total
end

#update(opts = {}) ⇒ Object

Update state (and settings) without printing anything.



196
197
198
199
200
201
202
203
204
205
# 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

#wipeObject

Remove the PowerBar from the screen.



230
231
232
# File 'lib/powerbar.rb', line 230

def wipe
  scope.output.call(scope.template.wipe)
end