Pratt 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. data/.gitignore +2 -0
  2. data/Pratt.gemspec +38 -12
  3. data/README.html +85 -0
  4. data/README.txt +6 -3
  5. data/TODO +6 -3
  6. data/VERSION +1 -1
  7. data/config.rb +3 -17
  8. data/db/zips.csv.zip +0 -0
  9. data/lib/models.rb +8 -0
  10. data/lib/pratt.rb +58 -293
  11. data/lib/pratt.rb.orig +626 -0
  12. data/lib/pratt/core_ext.rb +6 -0
  13. data/lib/pratt/{array.rb → core_ext/array.rb} +5 -0
  14. data/lib/pratt/core_ext/float.rb +28 -0
  15. data/lib/pratt/core_ext/nil.rb +5 -0
  16. data/lib/pratt/core_ext/numeric.rb +9 -0
  17. data/lib/pratt/{string.rb → core_ext/string.rb} +13 -0
  18. data/lib/pratt/core_ext/time.rb +20 -0
  19. data/lib/pratt/dialogs.rb +81 -0
  20. data/lib/pratt/formatting.rb +24 -0
  21. data/lib/pratt/project_actions.rb +25 -0
  22. data/lib/pratt/reports.rb +127 -0
  23. data/models/app.rb +1 -2
  24. data/models/customer.rb +22 -0
  25. data/models/invoice.rb +28 -0
  26. data/models/invoice_whence.rb +18 -0
  27. data/models/payment.rb +1 -1
  28. data/models/pratt.rb +0 -9
  29. data/models/project.rb +16 -22
  30. data/models/whence.rb +10 -1
  31. data/models/zip.rb +27 -0
  32. data/spec/fixtures/graph.expectation +0 -5
  33. data/spec/fixtures/proportions.expectation +4 -0
  34. data/spec/float_spec.rb +24 -0
  35. data/spec/numeric_spec.rb +30 -0
  36. data/spec/pratt_spec.rb +5 -4
  37. data/spec/project_spec.rb +8 -2
  38. data/spec/spec.opts +1 -1
  39. data/spec/spec_helper.rb +31 -1
  40. data/spec/string_ext_spec.rb +33 -0
  41. data/views/current.eruby +5 -0
  42. data/views/general-invoice.eruby +538 -0
  43. data/views/graph.eruby +2 -7
  44. data/views/invoice.eruby +3 -3
  45. data/views/main.rb +8 -6
  46. data/views/proportions.eruby +4 -0
  47. data/views/raw.eruby +1 -1
  48. metadata +86 -33
  49. data/lib/pratt/time.rb +0 -12
  50. data/views/env.rb +0 -22
@@ -0,0 +1,626 @@
1
+ # coding: utf-8
2
+ require 'fileutils'
3
+ require 'optparse'
4
+ require 'pathname'
5
+
6
+ require 'rubygems'
7
+ require 'colored'
8
+ require 'chronic'
9
+ require 'active_record'
10
+ require 'erubis'
11
+ require 'hoe'
12
+ require 'mocha'
13
+ require 'config'
14
+ require 'shifty_week'
15
+ require 'shifty_week/time'
16
+ require 'shifty_week/date'
17
+
18
+ require 'lib/pratt/array'
19
+ require 'lib/pratt/string'
20
+ require 'lib/pratt/time'
21
+
22
+ require 'models/app'
23
+ require 'models/customer'
24
+ require 'models/whence'
25
+ require 'models/project'
26
+ require 'models/payment'
27
+
28
+ class Pratt
29
+
30
+ NAME = 'Pratt'
31
+ URL = 'http://www.frogstarr78.com/projects/pratt'
32
+ AUTHORS = ['Scott Noel-Hemming']
33
+ SUMMARY = "Pro/Re-Active Time Tracker. Track time based on what you expect to be working on, with frequent prompts to ensure accuracy."
34
+ DESCRIPTION = %q|
35
+ Need a way to keep track of your time, but get caught up in work? Or constant interruptions?
36
+ Yeah you know who I'm talking about. Those people from the [abc] department always "NEEDING xyz FEATURE NOW!!!".
37
+ Seriously though. I'm usually just loose track of time. I wanted an app that I could start with a task I think
38
+ I'll be working on, but that get's in my face constantly to ensure I'm still working on it. And if I'm not any longer,
39
+ provides an easy way of changing to another task, or if I have changed tasks and not already updated the app, would
40
+ provide an easy way of changing the task of the previously recorded interval. That's what this is intended to do.
41
+
42
+ Time Tracking.
43
+ Proactively set what you expect to work on.
44
+ Reactively modify what you are no longer working on.
45
+ |
46
+ DEPENDENCIES = ["activerecord >=2.1.1", "sqlite3-ruby >=1.2.4", "rspec >=1.2.6", "mocha >=0.9.5"]
47
+ VERSION = File.open( File.join(Dir.pwd, 'VERSION') ).read.strip
48
+
49
+ PID_FILE='pratt.pid'
50
+ FMT = "%a %X %b %d %Y"
51
+ INVOICE_FMT = "%x"
52
+ @@color = true
53
+
54
+ attr_accessor :when_to, :scale, :color, :show_all, :env, :raw_conditions, :template, :week_day_start
55
+ attr_reader :project, :todo
56
+ def initialize proj = nil
57
+ @when_to = DateTime.now
58
+ @week = false
59
+ @day = false
60
+ @todo = []
61
+ @scale = nil
62
+ @show_all = false
63
+ @template = nil
64
+ @env = :development
65
+ @raw_conditions = ''
66
+ self.project = proj unless proj.nil?
67
+ end
68
+
69
+ # Set the project to something (Project/String)
70
+ # Conditionally creating a new project if the project
71
+ # named by the parameter isn't found
72
+ def project= proj
73
+ if proj.is_a?(Project)
74
+ @project = proj
75
+ else
76
+ @project = Project.find_or_create_by_name( { :name => proj } )
77
+ end
78
+ end
79
+
80
+ # We should act like an array
81
+ def << what
82
+ @todo << what
83
+ end
84
+
85
+ # Singleton Accessor for @app
86
+ def app
87
+ @app ||= App.last
88
+ @app
89
+ end
90
+
91
+ # TODO Rename
92
+ def graph
93
+ @primary = @off_total = @rest_total = 0.0
94
+ self.template = 'graph'
95
+
96
+ if project?
97
+ @projects = [project]
98
+
99
+ @primary = project.time_spent(scale, when_to)
100
+ @scaled_total = project.whences.time_spent(scale, when_to)
101
+ else
102
+ @projects = Project.all
103
+
104
+ @projects.each do |proj|
105
+ @primary = proj.time_spent(scale, when_to) if proj.name == Project.primary.name
106
+ @off_total = proj.time_spent(scale, when_to) if proj.name == Project.off.name
107
+ @rest_total += proj.time_spent(scale, when_to) if Project.rest.collect(&:name).include?(proj.name)
108
+ end
109
+ @scaled_total = Whence.time_spent(scale, when_to)-@off_total
110
+ end
111
+
112
+ if @primary + @off_total + @rest_total > 0.0
113
+ process_template!
114
+ else
115
+ "No data to report"
116
+ end
117
+ end
118
+
119
+ # Generate an invoice for a given time period
120
+ def invoice
121
+ self.template = 'invoice'
122
+
123
+ if project?
124
+ @projects = [project]
125
+
126
+ @total = project.time_spent(scale, when_to)
127
+ else
128
+ @projects = (Project.all - [Project.primary, Project.off])
129
+ @projects.select! {|proj| show_all or ( !show_all and proj.time_spent(scale, when_to) != 0.0 ) }
130
+
131
+ @total = @projects.inject 0.0 do |total, proj|
132
+ total += proj.time_spent(scale, when_to)
133
+ total
134
+ end
135
+ end
136
+
137
+ @projects.each do |project|
138
+ puts "#{project.name} #{project.payment.inspect}"
139
+ end
140
+ if @total > 0.0
141
+ puts process_template!
142
+ else
143
+ puts "No data to report"
144
+ end
145
+ end
146
+
147
+ def console options = []
148
+ options << %w(-r\ irb/completion -r\ lib/pratt --simple-prompt)
149
+ exec "irb #{options.join ' '}"
150
+ end
151
+
152
+ def current
153
+ project_names = ([Project.primary, Project.off] | Project.rest).collect(&:name)
154
+
155
+ if last_whence = Whence.last_unended || Whence.last
156
+ puts " projects: " << (
157
+ project_names.collect {|project_name| "'#{project_name.send(last_whence.end_at.nil? && last_whence.project.name == project_name ? :green : :magenta)}'" }
158
+ ) * ' '
159
+ if last_whence.end_at.nil?
160
+ puts " started: #{last_whence.start_at.strftime(FMT).send(:blue)}"
161
+ time_til = ( app.interval - ( Time.now - last_whence.start_at ) )
162
+ puts "next prompt: %s %s"% [Pratt.fmt_i( time_til / 60.0, 'min', :yellow ), Pratt.fmt_i( time_til % 60, 'sec', :yellow ) ], ''
163
+ end
164
+ else
165
+ puts " projects: " << (
166
+ project_names.collect do |project_name|
167
+ "'#{project_name.magenta}'"
168
+ end
169
+ ) * ' '
170
+ end
171
+ end
172
+
173
+ def begin
174
+ self.project.start! when_to
175
+ end
176
+ def restart
177
+ if project?
178
+ project.restart! when_to
179
+ else
180
+ Whence.last_unended.project.restart! when_to
181
+ end
182
+ end
183
+ def end
184
+ if project?
185
+ project.stop! when_to
186
+ else
187
+ Whence.last_unended.stop! when_to
188
+ end
189
+ end
190
+ def change
191
+ Whence.last.change! project.name
192
+ end
193
+ def destroy
194
+ project.destroy
195
+ end
196
+
197
+ def cpid
198
+ `pgrep -fl 'bin/pratt'`.chomp.split(' ').first
199
+ end
200
+
201
+ def pid
202
+ self.template = 'pid'
203
+ puts process_template!
204
+ end
205
+
206
+ def raw
207
+ self.template = 'raw'
208
+
209
+ if project?
210
+ @whences = project.whences.all
211
+ else
212
+ case raw_conditions
213
+ when 'all'
214
+ @whences = Whence.find raw_conditions.to_sym
215
+ when /^last$/, 'first'
216
+ @whences = [Whence.find raw_conditions.to_sym]
217
+ when /last[\(\s]?(\d+)[\)\s]?/
218
+ @whences = Whence.all.last($1.to_i)
219
+ else
220
+ @whences = Whence.all :conditions => ["start_at > ?", Chronic.parse("today 00:00")]
221
+ end
222
+ end
223
+ @whences.sort_by(&:id)
224
+ puts process_template!
225
+ end
226
+
227
+ def quit
228
+ project.stop! if project? and project.whences.last_unended
229
+ Whence.last_unended.stop! if Whence.last_unended
230
+ begin
231
+ Process.kill("KILL", app.pid.to_i)
232
+ rescue Errno::ESRCH
233
+ end
234
+ app.pid = ''
235
+ app.gui = ''
236
+ app.save!
237
+ end
238
+
239
+ def gui
240
+ if Whence.last_unended
241
+ pop
242
+ else
243
+ main
244
+ end
245
+ end
246
+
247
+ def show_env
248
+ defork { system("ruby views/env.rb ") }
249
+ end
250
+
251
+ def detect
252
+ if self.daemonized?
253
+ gui
254
+ else
255
+ daemonize!
256
+ end
257
+ end
258
+
259
+ <<<<<<< HEAD
260
+ def unlock
261
+ self.app.unlock
262
+ end
263
+ =======
264
+ def run
265
+ # Pratt.connect self.env
266
+
267
+ self.begin if i_should?(:begin)
268
+ self.change if i_should?(:change)
269
+ self.restart if i_should?(:restart)
270
+ self.end if i_should?(:end)
271
+
272
+ self.destroy if i_should?(:destroy)
273
+
274
+ self.pid if i_should?(:pid)
275
+ self.raw if i_should?(:raw)
276
+ self.current if i_should?(:current)
277
+ self.graph if i_should?(:graph)
278
+ self.gui if i_should?(:gui)
279
+ self.detect if i_should?(:detect)
280
+ self.tray_menu if i_should?(:tray_menu)
281
+ self.app.unlock if i_should?(:unlock)
282
+ >>>>>>> 08f31db4a4500d2f6950d31ca6646c6b08ac7432
283
+
284
+ def run
285
+ self.when_to.week_day_start = self.week_day_start
286
+ puts self.when_to.inspect, self.when_to.week_day_start
287
+ # must happen before any actions but after all cli argument parsing
288
+
289
+ self.begin if i_should? :begin
290
+ self.change if i_should? :change
291
+ self.restart if i_should? :restart
292
+ self.end if i_should? :end
293
+
294
+ self.destroy if i_should? :destroy
295
+
296
+ self.pid if i_should? :pid
297
+ self.raw if i_should? :raw
298
+ self.current if i_should? :current
299
+ puts self.graph if i_should? :graph
300
+ self.invoice if i_should? :invoice
301
+ self.console if i_should? :console
302
+ self.gui if i_should? :gui
303
+ self.show_env if i_should? :env
304
+ self.detect if i_should? :detect
305
+ self.unlock if i_should? :unlock
306
+
307
+ self.quit if i_should? :quit
308
+ self.daemonize! if i_should? :daemonize and not self.daemonized?
309
+ end
310
+
311
+ def daemonized?
312
+ !app.pid.blank? and ( cpid.to_i == app.pid )
313
+ end
314
+ def daemonize!
315
+ <<<<<<< HEAD
316
+ defork {
317
+ puts "pratt (#{Process.pid.to_s.yellow})"
318
+ app.pid = Process.pid
319
+ app.save!
320
+
321
+ gui
322
+ while(daemonized?)
323
+ sleep(app.interval)
324
+ gui
325
+ end
326
+ quit
327
+ }
328
+ =======
329
+ # self.class.connect :development
330
+ puts "pratt (#{Process.pid.to_s.yellow})"
331
+ app.pid = Process.pid
332
+ app.save!
333
+
334
+ tray_icon
335
+ gui
336
+ while(daemonized?)
337
+ sleep(app.interval)
338
+ gui
339
+ end
340
+ quit
341
+ end
342
+
343
+ def gui
344
+ if Whence.last_unended
345
+ pop
346
+ else
347
+ main
348
+ end
349
+ end
350
+
351
+ def tray_icon
352
+ Process.detach(
353
+ fork { system("ruby views/tray_icon.rb") }
354
+ )
355
+ >>>>>>> 08f31db4a4500d2f6950d31ca6646c6b08ac7432
356
+ end
357
+
358
+ def tray_menu
359
+ self.app.reload
360
+ return if self.app.gui?('tray_menu', true)
361
+ self.app.log('tray_menu')
362
+ Process.detach(
363
+ fork { system("ruby views/tray_menu.rb '#{Whence.last_unended.project.name}' 1197 2") }
364
+ )
365
+ end
366
+
367
+ private
368
+ def main
369
+ self.app.reload
370
+ return if self.app.gui?('main', true)
371
+ self.app.log('main')
372
+ projects = ([Project.primary, Project.off] | Project.rest).collect(&:name)
373
+ if Whence.count == 0
374
+ # first run
375
+ project = Whence.new(:project => Project.primary)
376
+ else
377
+ project = Whence.last_unended || Whence.last
378
+ end
379
+ defork { system("ruby views/main.rb --projects '#{projects*"','"}' --current '#{project.project.name}'") }
380
+ end
381
+ def pop
382
+ self.app.reload
383
+ return if self.app.gui?('pop', true)
384
+ self.app.log('pop')
385
+ self.project = Whence.last_unended.project
386
+ defork { system("ruby views/pop.rb --project '#{project.name}' --start '#{project.whences.last_unended.start_at}' --project_time '#{Pratt.totals(project.time_spent)}'") }
387
+ end
388
+
389
+ def i_should? what
390
+ @todo.include?(what)
391
+ end
392
+
393
+ def project?
394
+ !@project.nil? and @project.name?
395
+ end
396
+
397
+ def process_template!
398
+ input = File.open(Pratt.root("views", "#{template}.eruby").first).read
399
+ erubis = Erubis::Eruby.new(input)
400
+ erubis.evaluate(self)
401
+ end
402
+
403
+ def padded_to_max string
404
+ self.class.padded_to_max string
405
+ end
406
+
407
+ def defork &block
408
+ Process.detach(
409
+ fork &block
410
+ )
411
+ end
412
+
413
+ class << self
414
+
415
+
416
+ def max
417
+ # TODO Fix me
418
+ @max ||= Project.all.inject(0) {|x,p| x = p.name.length if p.name.length > x; x }
419
+ end
420
+
421
+ def color
422
+ @@color
423
+ end
424
+
425
+ def color= c
426
+ @@color = c
427
+ end
428
+
429
+ def color?
430
+ @@color == true
431
+ end
432
+
433
+
434
+ def parse args
435
+ me = Pratt.new
436
+
437
+ # There are aa few things we're parsing here
438
+ # Pratt config arguments (Ideally that should be all we do)
439
+ # Pratt actions. These may require ordering or not. They also may require an argument value.
440
+ #
441
+ # TODO: Redo the cli parsing...
442
+ # In some cases we require arguments to be run in a certain order, but we don't want some to be run concurrently w/ others.
443
+ # Sometimes it may be unexpected but helpful to allow that behavior.
444
+ opt = OptionParser.new do |opt|
445
+ Pratt.connect! ENV['PRATT_ENV'] || 'development' unless Pratt.connected?
446
+
447
+ # Actionable options
448
+ opt.on('-b', "--begin", String, "Begin project tracking.") do
449
+ me << :begin
450
+ end
451
+ opt.on('-r', "--restart", String, "Restart project log (stop last log and start a new one).
452
+ Applies to last un-ended project, unless a specific project is provided.") do
453
+ me << :restart
454
+ end
455
+ opt.on('-e', "--end", String, "Stop tracking interval for last project or supplied project if provided.") do
456
+ me << :end
457
+ end
458
+ opt.on('-c', "--change", String, "Change last time interval to this project.") do
459
+ me << :change
460
+ end
461
+ opt.on('-g', "--graph", String, "Display time spent on supplied project or all projects without argument value.") do
462
+ me << :graph
463
+ end
464
+ opt.on('-I', "--invoice", "Create an invoice.") do
465
+ me << :invoice
466
+ end
467
+ opt.on('-p', '--pid', "Process id display. (Is it still running)") do
468
+ me << :pid
469
+ end
470
+ opt.on('-R', '--raw [CONDITIONS]', "Dump logs (semi-)raw") do |conditions|
471
+ me << :raw
472
+ me.raw_conditions = conditions
473
+ end
474
+ opt.on('-C', "--current", "Show available projects and current project (if there is one)") do
475
+ me << :current
476
+ end
477
+ opt.on("-d", "--daemonize", "Start daemon.") do
478
+ me << :daemonize
479
+ end
480
+ opt.on('-D', '--detect', 'Detect appropriate behavior. (Daemonize or Graphical).') do
481
+ me << :detect
482
+ end
483
+ opt.on('-G', '--gui', 'Show "smart" gui.') do
484
+ me << :gui
485
+ end
486
+ opt.on('-q', "--quit", "Stop daemon.") do
487
+ me << :quit
488
+ end
489
+ opt.on('-U', '--unlock', "Manually unlock a gui that has died but left it's lock around.") do
490
+ me << :unlock
491
+ end
492
+ opt.on '-V', '--version' do
493
+ puts "Pro-Reactive Time Tracker [Pratt] (#{VERSION})"
494
+ end
495
+ opt.on('--destroy', String, "Remove a project.") do
496
+ me << :destroy
497
+ end
498
+
499
+ # Strictly configuration options
500
+ opt.on('-P', "--project PROJECT_NAME", String, "Set project.") do |proj|
501
+ me.project = proj
502
+ end
503
+
504
+ templates = []
505
+ Pratt.root("views", "*.eruby") {|view| templates << File.basename(view, '.eruby') }
506
+ opt.on('-t', "--template TEMPLATE", templates, "Template to use for displaying work done.
507
+ Available templates are #{templates.to_sentence('or')}.") do |template|
508
+ me.template = template
509
+ end
510
+ opt.on '-w', '--when_to TIME', String, 'When to do something.
511
+ (e.g. log time start|stop, or what time interval to graph)
512
+ If graphing, silently ignored w/out scale argument.' do |when_to|
513
+ me.when_to = Chronic.parse(when_to).to_datetime
514
+ end
515
+ scales = %w(day week month quarter year)
516
+ opt.on('-l', '--scale SCALE', scales, "Granularity of time argument
517
+ Available scales are #{scales.to_sentence('or')}.
518
+ Only applies to graphing.") do |scale|
519
+ me.scale = scale
520
+ end
521
+ opt.on('-L', '--log', "Redirect errors") do
522
+ FileUtils.mkdir 'log' unless File.exists? 'log'
523
+ $stderr.reopen('log/pratt.log', 'a')
524
+ $stderr.sync = true
525
+ end
526
+ opt.on('-N', '--no-color', "Display output without color or special characters.") do
527
+ Pratt.color = false
528
+ end
529
+ opt.on('-A', '--all', "Display all project regardless of other options.") do
530
+ me.show_all = true
531
+ end
532
+ opt.on '-s', '--start-day WEEK_DAY_START', String, "" do |wday_start|
533
+ me.week_day_start = wday_start
534
+ end
535
+ opt.on('-i', "--interval INTERVAL", Float, "Set the remind interval/min (Only applies to daemonized process).") do |interval|
536
+ me.app.interval = interval
537
+ me.app.save
538
+ end
539
+ <<<<<<< HEAD
540
+ =======
541
+ opt.on('-D', '--detect', 'Detect appropriate behavior. (Daemonize or Graphical).') do
542
+ me << :detect
543
+ end
544
+ opt.on('-G', '--gui', 'Show "smart" gui.') do
545
+ me << :gui
546
+ end
547
+ opt.on("-d", "--daemonize", "Start daemon.") do
548
+ me << :daemonize
549
+ end
550
+ opt.on('-q', "--quit", "Stop daemon.") do
551
+ me << :quit
552
+ end
553
+ opt.on('-G', '--gui', 'Show "smart" gui.') do
554
+ me << :gui
555
+ end
556
+ opt.on('-y', '--tray', 'Show tray') do
557
+ me << :tray_menu
558
+ end
559
+ opt.on('--last MODEL', %w(app project whence log), 'Show the last entry for supplied model') do |model|
560
+ puts send(model.classify, last).inspect
561
+ end
562
+ opt.on('-U', '--unlock', "Manually unlock a gui that has died but left it's lock around.") do
563
+ me.app.unlock
564
+ # me << :unlock
565
+ end
566
+ >>>>>>> 08f31db4a4500d2f6950d31ca6646c6b08ac7432
567
+
568
+ opt.parse!
569
+ end
570
+
571
+ me << :env if args.include? 'env'
572
+ me << :console if args.include? 'console'
573
+
574
+ me.run
575
+ end
576
+
577
+ # Calculate totals. I think this should be an instance method on Projects/?/Whences
578
+ def totals hr, fmt = false
579
+ "#{fmt_i(hr / 24, 'day', :cyan)} #{fmt_i(hr % 24, 'hour', :yellow)} #{fmt_i((60*(hr -= hr.to_i)), 'min', :green)}"
580
+ end
581
+
582
+ def percent label, off, total, color
583
+ percent = "%0.2f"% ((off/total)*100)
584
+ padded_to_max(label) << " #{percent}%".send(color)
585
+ end
586
+
587
+ # Where is Pratt installed.
588
+ # We've already chdir'd to it's base dir in the bin file
589
+ def root *globs, &block
590
+ root = Dir.pwd
591
+ if globs.empty?
592
+ subdir = root
593
+ else
594
+ subdir = File.join(root, *globs)
595
+ end
596
+
597
+ if block_given?
598
+ Pathname.glob(subdir) {|dir_files| yield dir_files }
599
+ else
600
+ Pathname.glob(subdir)
601
+ end
602
+ end
603
+
604
+ # Pad the output string to the maximum Project name
605
+ def padded_to_max string
606
+ "%#{max}.#{max}s"% string
607
+ end
608
+
609
+ # Migrate schema.
610
+ def migrate
611
+ Pratt.root( 'models', '*.rb' ) do |model_file|
612
+ klass = File.basename( model_file, '.rb' ).capitalize.constantize
613
+ begin
614
+ ActiveRecord::Base.connection.table_structure(model_file)
615
+ rescue ActiveRecord::StatementInvalid
616
+ klass.migrate :up if klass.superclass == ActiveRecord::Base
617
+ end
618
+ end
619
+ end
620
+
621
+ def fmt_i int, label, color
622
+ "%s #{label}"% [("%02i"% int).send(color), label]
623
+ end
624
+
625
+ end
626
+ end