samg-timetrap 0.0.7 → 0.0.8
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.
- data/bin/dev_t +4 -0
- data/lib/timetrap/cli.rb +227 -0
- data/lib/timetrap/helpers.rb +44 -0
- data/lib/timetrap/models.rb +34 -0
- data/lib/timetrap.rb +12 -283
- data/spec/timetrap_spec.rb +37 -3
- metadata +9 -3
    
        data/bin/dev_t
    ADDED
    
    
    
        data/lib/timetrap/cli.rb
    ADDED
    
    | @@ -0,0 +1,227 @@ | |
| 1 | 
            +
            module Timetrap
         | 
| 2 | 
            +
              module CLI
         | 
| 3 | 
            +
                extend Helpers
         | 
| 4 | 
            +
                attr_accessor :args
         | 
| 5 | 
            +
                extend self
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                USAGE = <<-EOF
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Timetrap - Simple Time Tracking
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            Usage: #{File.basename $0} COMMAND [OPTIONS] [ARGS...]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            where COMMAND is one of:
         | 
| 14 | 
            +
              * alter - alter an entry's note, start, or end time. Defaults to the active entry
         | 
| 15 | 
            +
                usage: t alter [--id ID] [--start TIME] [--end TIME] [NOTES]
         | 
| 16 | 
            +
                -i, --id <id:i>           Alter entry with id <id> instead of the running entry
         | 
| 17 | 
            +
                -s, --start <time:qs>     Change the start time to <time>
         | 
| 18 | 
            +
                -e, --end <time:qs>       Change the end time to <time>
         | 
| 19 | 
            +
              * backend - open an sqlite shell to the database
         | 
| 20 | 
            +
                usage: t backend
         | 
| 21 | 
            +
              * display - display the current timesheet
         | 
| 22 | 
            +
                usage: t display [--ids] [--start DATE] [--end DATE] [TIMESHEET]
         | 
| 23 | 
            +
                -v, --ids                 Print database ids (for use with alter)
         | 
| 24 | 
            +
                -s, --start <date:qs>     Include entries that start on this date or later
         | 
| 25 | 
            +
                -e, --end <date:qs>       Include entries that start on this date or earlier
         | 
| 26 | 
            +
              * format - export a sheet to csv format
         | 
| 27 | 
            +
                usage: t format [--ids] [--start DATE] [--end DATE] FORMATTER
         | 
| 28 | 
            +
                FORMATTER                 Currently only supports 'ical'
         | 
| 29 | 
            +
                -s, --start <date:qs>     Include entries that start on this date or later
         | 
| 30 | 
            +
                -e, --end <date:qs>       Include entries that start on this date or earlier
         | 
| 31 | 
            +
              * in - start the timer for the current timesheet
         | 
| 32 | 
            +
                usage: t in [--at TIME] [NOTES]
         | 
| 33 | 
            +
                -a, --at <time:qs>        Use this time instead of now
         | 
| 34 | 
            +
              * kill - delete a timesheet
         | 
| 35 | 
            +
                usage: t kill [--id ID] [TIMESHEET]
         | 
| 36 | 
            +
                -i, --id <id:i>           Alter entry with id <id> instead of the running entry
         | 
| 37 | 
            +
              * list - show the available timesheets
         | 
| 38 | 
            +
                usage: t list
         | 
| 39 | 
            +
              * now - show the status of the current timesheet
         | 
| 40 | 
            +
                usage: t now
         | 
| 41 | 
            +
              * out - stop the timer for the current timesheet
         | 
| 42 | 
            +
                usage: t out [--at TIME]
         | 
| 43 | 
            +
                -a, --at <time:qs>        Use this time instead of now
         | 
| 44 | 
            +
              * running - show all running timesheets
         | 
| 45 | 
            +
                NOT IMPLEMENTED
         | 
| 46 | 
            +
              * switch - switch to a new timesheet
         | 
| 47 | 
            +
                usage: t switch TIMESHEET
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                OTHER OPTIONS
         | 
| 50 | 
            +
                -h, --help     Display this help
         | 
| 51 | 
            +
                EOF
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def parse arguments
         | 
| 54 | 
            +
                  args.parse arguments
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def invoke
         | 
| 58 | 
            +
                  args['-h'] ? say(USAGE) : invoke_command_if_valid
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def commands
         | 
| 62 | 
            +
                  Timetrap::CLI::USAGE.scan(/\* \w+/).map{|s| s.gsub(/\* /, '')}
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def say *something
         | 
| 66 | 
            +
                  puts *something
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def invoke_command_if_valid
         | 
| 70 | 
            +
                  command = args.unused.shift
         | 
| 71 | 
            +
                  case (valid = commands.select{|name| name =~ %r|^#{command}|}).size
         | 
| 72 | 
            +
                  when 0 then say "Invalid command: #{command}"
         | 
| 73 | 
            +
                  when 1 then send valid[0]
         | 
| 74 | 
            +
                  else; say "Ambigous command: #{command}"; end
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def alter
         | 
| 78 | 
            +
                  entry = args['-i'] ? Entry[args['-i']] : Timetrap.active_entry
         | 
| 79 | 
            +
                  say "can't find entry" && return unless entry
         | 
| 80 | 
            +
                  entry.update :start => args['-s'] if args['-s'] =~ /.+/
         | 
| 81 | 
            +
                  entry.update :end => args['-e'] if args['-e'] =~ /.+/
         | 
| 82 | 
            +
                  entry.update :note => unused_args if unused_args =~ /.+/
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def backend
         | 
| 86 | 
            +
                  exec "sqlite3 #{DB_NAME}"
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def in
         | 
| 90 | 
            +
                  Timetrap.start unused_args, args['-a']
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def out
         | 
| 94 | 
            +
                  Timetrap.stop args['-a']
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def kill
         | 
| 98 | 
            +
                  if e = Entry[args['-i']]
         | 
| 99 | 
            +
                    out = "are you sure you want to delete entry #{e.id}? "
         | 
| 100 | 
            +
                    out << "(#{e.note}) " if e.note.to_s =~ /.+/
         | 
| 101 | 
            +
                    print out
         | 
| 102 | 
            +
                    if $stdin.gets =~ /\Aye?s?\Z/i
         | 
| 103 | 
            +
                      e.destroy
         | 
| 104 | 
            +
                      say "it's dead"
         | 
| 105 | 
            +
                    else
         | 
| 106 | 
            +
                      say "will not kill"
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
                  elsif (sheets = Entry.map{|e| e.sheet }.uniq).include?(sheet = unused_args)
         | 
| 109 | 
            +
                    victims = Entry.filter(:sheet => sheet).count
         | 
| 110 | 
            +
                    print "are you sure you want to delete #{victims} entries on sheet #{sheet.inspect}? "
         | 
| 111 | 
            +
                    if $stdin.gets =~ /\Aye?s?\Z/i
         | 
| 112 | 
            +
                      Timetrap.kill_sheet sheet
         | 
| 113 | 
            +
                      say "killed #{victims} entries"
         | 
| 114 | 
            +
                    else
         | 
| 115 | 
            +
                      say "will not kill"
         | 
| 116 | 
            +
                    end
         | 
| 117 | 
            +
                  else
         | 
| 118 | 
            +
                    victim = args['-i'] ? args['-i'].to_s.inspect : sheet.inspect
         | 
| 119 | 
            +
                    say "can't find #{victim} to kill", 'sheets:', *sheets
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def display
         | 
| 124 | 
            +
                  sheet = sheet_name_from_string(unused_args)
         | 
| 125 | 
            +
                  sheet = (sheet =~ /.+/ ? sheet : Timetrap.current_sheet)
         | 
| 126 | 
            +
                  say "Timesheet: #{sheet}"
         | 
| 127 | 
            +
                  id_heading = args['-v'] ? 'Id' : '  '
         | 
| 128 | 
            +
                  say "#{id_heading}  Day                Start      End        Duration   Notes"
         | 
| 129 | 
            +
                  last_start = nil
         | 
| 130 | 
            +
                  from_current_day = []
         | 
| 131 | 
            +
                  ee = Timetrap::Entry.filter(:sheet => sheet).order(:start)
         | 
| 132 | 
            +
                  ee = ee.filter(:end >= Date.parse(args['-s'])) if args['-s']
         | 
| 133 | 
            +
                  ee = ee.filter(:start <= Date.parse(args['-e']) + 1) if args['-e']
         | 
| 134 | 
            +
                  ee.each_with_index do |e, i|
         | 
| 135 | 
            +
             | 
| 136 | 
            +
             | 
| 137 | 
            +
                    from_current_day << e
         | 
| 138 | 
            +
                    e_end = e.end || Time.now
         | 
| 139 | 
            +
                    say "%-4s%16s%11s -%9s%10s    %s" % [
         | 
| 140 | 
            +
                      (args['-v'] ? e.id : ''),
         | 
| 141 | 
            +
                      format_date_if_new(e.start, last_start),
         | 
| 142 | 
            +
                      format_time(e.start),
         | 
| 143 | 
            +
                      format_time(e.end),
         | 
| 144 | 
            +
                      format_duration(e.start, e_end),
         | 
| 145 | 
            +
                      e.note
         | 
| 146 | 
            +
                    ]
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                    nxt = ee.map[i+1]
         | 
| 149 | 
            +
                    if nxt == nil or !same_day?(e.start, nxt.start)
         | 
| 150 | 
            +
                      say "%52s" % format_total(from_current_day)
         | 
| 151 | 
            +
                      from_current_day = []
         | 
| 152 | 
            +
                    else
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
                    last_start = e.start
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                  say <<-OUT
         | 
| 157 | 
            +
                ---------------------------------------------------------
         | 
| 158 | 
            +
                  OUT
         | 
| 159 | 
            +
                  say "    Total%43s" % format_total(ee)
         | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                # TODO: Consolidate display and format
         | 
| 163 | 
            +
                def format
         | 
| 164 | 
            +
                  begin
         | 
| 165 | 
            +
                    fmt_klass = Timetrap::Formatters.const_get("#{unused_args.classify}")
         | 
| 166 | 
            +
                  rescue
         | 
| 167 | 
            +
                    say "Invalid format specified `#{unused_args}'"
         | 
| 168 | 
            +
                    return
         | 
| 169 | 
            +
                  end
         | 
| 170 | 
            +
                  ee = Timetrap::Entry.filter(:sheet => Timetrap.current_sheet)
         | 
| 171 | 
            +
                  ee = ee.filter(:end >= Date.parse(args['-s'])) if args['-s']
         | 
| 172 | 
            +
                  ee = ee.filter(:start <= Date.parse(args['-e']) + 1) if args['-e']
         | 
| 173 | 
            +
                  say Timetrap.format(fmt_klass,ee.all)
         | 
| 174 | 
            +
                end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                def switch
         | 
| 177 | 
            +
                  sheet = unused_args
         | 
| 178 | 
            +
                  if not sheet =~ /.+/ then say "No sheet specified"; return end
         | 
| 179 | 
            +
                  say "Switching to sheet " + Timetrap.switch(sheet)
         | 
| 180 | 
            +
                end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                def list
         | 
| 183 | 
            +
                  sheets = Entry.map{|e|e.sheet}.uniq.sort.map do |sheet|
         | 
| 184 | 
            +
                    sheet_atts = {:total => 0, :running => 0, :today => 0}
         | 
| 185 | 
            +
                    DB[:entries].filter(:sheet => sheet).inject(sheet_atts) do |m, e|
         | 
| 186 | 
            +
                      e_end = e[:end] || Time.now
         | 
| 187 | 
            +
                      m[:name] ||= sheet
         | 
| 188 | 
            +
                      m[:total] += (e_end.to_i - e[:start].to_i)
         | 
| 189 | 
            +
                      m[:running] += (e_end.to_i - e[:start].to_i) unless e[:end]
         | 
| 190 | 
            +
                      m[:today] += (e_end.to_i - e[:start].to_i) if same_day?(Time.now, e[:start])
         | 
| 191 | 
            +
                      m
         | 
| 192 | 
            +
                    end
         | 
| 193 | 
            +
                  end
         | 
| 194 | 
            +
                  width = sheets.sort_by{|h|h[:name].length }.last[:name].length + 4
         | 
| 195 | 
            +
                  say " %-#{width}s%-12s%-12s%s" % ["Timesheet", "Running", "Today", "Total Time"]
         | 
| 196 | 
            +
                  sheets.each do |sheet|
         | 
| 197 | 
            +
                    star = sheet[:name] == Timetrap.current_sheet ? '*' : ' '
         | 
| 198 | 
            +
                    say "#{star}%-#{width}s%-12s%-12s%s" % [
         | 
| 199 | 
            +
                      sheet[:running],
         | 
| 200 | 
            +
                      sheet[:today],
         | 
| 201 | 
            +
                      sheet[:total]
         | 
| 202 | 
            +
                    ].map(&method(:format_seconds)).unshift(sheet[:name])
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                def now
         | 
| 207 | 
            +
                  if Timetrap.running?
         | 
| 208 | 
            +
                    out = "#{Timetrap.current_sheet}: #{format_duration(Timetrap.active_entry.start, Time.now)}".gsub(/  /, ' ')
         | 
| 209 | 
            +
                    out << " (#{Timetrap.active_entry.note})" if Timetrap.active_entry.note =~ /.+/
         | 
| 210 | 
            +
                    say out
         | 
| 211 | 
            +
                  else
         | 
| 212 | 
            +
                    say "#{Timetrap.current_sheet}: not running"
         | 
| 213 | 
            +
                  end
         | 
| 214 | 
            +
                end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                def running
         | 
| 217 | 
            +
                  say "Sorry not implemented yet :-("
         | 
| 218 | 
            +
                end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                private
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                def unused_args
         | 
| 223 | 
            +
                  args.unused.join(' ')
         | 
| 224 | 
            +
                end
         | 
| 225 | 
            +
             | 
| 226 | 
            +
              end
         | 
| 227 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            module Timetrap
         | 
| 2 | 
            +
              module Helpers
         | 
| 3 | 
            +
                def format_time time
         | 
| 4 | 
            +
                  return '' unless time.respond_to?(:strftime)
         | 
| 5 | 
            +
                  time.strftime('%H:%M:%S')
         | 
| 6 | 
            +
                end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def format_date time
         | 
| 9 | 
            +
                  return '' unless time.respond_to?(:strftime)
         | 
| 10 | 
            +
                  time.strftime('%a %b %d, %Y')
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def format_date_if_new time, last_time
         | 
| 14 | 
            +
                  return '' unless time.respond_to?(:strftime)
         | 
| 15 | 
            +
                  same_day?(time, last_time) ? '' : format_date(time)
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def same_day? time, other_time
         | 
| 19 | 
            +
                  format_date(time) == format_date(other_time)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def format_duration stime, etime
         | 
| 23 | 
            +
                  return '' unless stime and etime
         | 
| 24 | 
            +
                  secs = etime.to_i - stime.to_i
         | 
| 25 | 
            +
                  format_seconds secs
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def format_seconds secs
         | 
| 29 | 
            +
                  "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def format_total entries
         | 
| 33 | 
            +
                  secs = entries.inject(0){|m, e|e_end = e.end || Time.now; m += e_end.to_i - e.start.to_i if e_end && e.start;m}
         | 
| 34 | 
            +
                  "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def sheet_name_from_string string
         | 
| 38 | 
            +
                  return "" unless string =~ /.+/
         | 
| 39 | 
            +
                  DB[:entries].filter(:sheet.like("#{string}%")).first[:sheet]
         | 
| 40 | 
            +
                rescue
         | 
| 41 | 
            +
                  ""
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            module Timetrap
         | 
| 2 | 
            +
              class Entry < Sequel::Model
         | 
| 3 | 
            +
                plugin :schema
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def start= time
         | 
| 6 | 
            +
                  self[:start]= Chronic.parse(time) || time
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def end= time
         | 
| 10 | 
            +
                  self[:end]= Chronic.parse(time) || time
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                # do a quick pseudo migration.  This should only get executed on the first run
         | 
| 14 | 
            +
                set_schema do
         | 
| 15 | 
            +
                  primary_key :id
         | 
| 16 | 
            +
                  column :note, :string
         | 
| 17 | 
            +
                  column :start, :timestamp
         | 
| 18 | 
            +
                  column :end, :timestamp
         | 
| 19 | 
            +
                  column :sheet, :string
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
                create_table unless table_exists?
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              class Meta < Sequel::Model(:meta)
         | 
| 25 | 
            +
                plugin :schema
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                set_schema do
         | 
| 28 | 
            +
                  primary_key :id
         | 
| 29 | 
            +
                  column :key, :string
         | 
| 30 | 
            +
                  column :value, :string
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
                create_table unless table_exists?
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
            end
         | 
    
        data/lib/timetrap.rb
    CHANGED
    
    | @@ -1,262 +1,20 @@ | |
| 1 1 | 
             
            require 'rubygems'
         | 
| 2 2 | 
             
            require 'chronic'
         | 
| 3 3 | 
             
            require 'sequel'
         | 
| 4 | 
            +
            require 'sequel/extensions/inflector'
         | 
| 4 5 | 
             
            require 'Getopt/Declare'
         | 
| 5 | 
            -
             | 
| 6 | 
            +
            require File.join(File.dirname(__FILE__), 'timetrap', 'helpers')
         | 
| 7 | 
            +
            require File.join(File.dirname(__FILE__), 'timetrap', 'cli')
         | 
| 6 8 | 
             
            DB_NAME = defined?(TEST_MODE) ? nil : "#{ENV['HOME']}/.timetrap.db"
         | 
| 9 | 
            +
            # connect to database.  This will create one if it doesn't exist
         | 
| 7 10 | 
             
            DB = Sequel.sqlite DB_NAME
         | 
| 8 | 
            -
             | 
| 11 | 
            +
            require File.join(File.dirname(__FILE__), 'timetrap', 'models')
         | 
| 12 | 
            +
            Dir["#{File.dirname(__FILE__)}/timetrap/formatters/*.rb"].each do |path|
         | 
| 13 | 
            +
              require path
         | 
| 14 | 
            +
            end
         | 
| 9 15 | 
             
            module Timetrap
         | 
| 10 16 | 
             
              extend self
         | 
| 11 17 |  | 
| 12 | 
            -
              module CLI
         | 
| 13 | 
            -
                attr_accessor :args
         | 
| 14 | 
            -
                extend self
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                USAGE = <<-EOF
         | 
| 17 | 
            -
             | 
| 18 | 
            -
            Timetrap - Simple Time Tracking
         | 
| 19 | 
            -
             | 
| 20 | 
            -
            Usage: #{File.basename $0} COMMAND [OPTIONS] [ARGS...]
         | 
| 21 | 
            -
             | 
| 22 | 
            -
            where COMMAND is one of:
         | 
| 23 | 
            -
              * alter - alter an entry's note, start, or end time. Defaults to the active entry
         | 
| 24 | 
            -
                usage: t alter [--id ID] [--start TIME] [--end TIME] [NOTES]
         | 
| 25 | 
            -
                -i, --id <id:i>           Alter entry with id <id> instead of the running entry
         | 
| 26 | 
            -
                -s, --start <time:qs>     Change the start time to <time>
         | 
| 27 | 
            -
                -e, --end <time:qs>       Change the end time to <time>
         | 
| 28 | 
            -
              * backend - open an sqlite shell to the database
         | 
| 29 | 
            -
                usage: t backend
         | 
| 30 | 
            -
              * display - display the current timesheet
         | 
| 31 | 
            -
                usage: t display [--ids] [TIMESHEET]
         | 
| 32 | 
            -
                -v, --ids                 Print database ids (for use with alter)
         | 
| 33 | 
            -
              * format - export a sheet to csv format
         | 
| 34 | 
            -
                NOT IMPLEMENTED
         | 
| 35 | 
            -
              * in - start the timer for the current timesheet
         | 
| 36 | 
            -
                usage: t in [--at TIME] [NOTES]
         | 
| 37 | 
            -
                -a, --at <time:qs>        Use this time instead of now
         | 
| 38 | 
            -
              * kill - delete a timesheet
         | 
| 39 | 
            -
                usage: t kill [--id ID] [TIMESHEET]
         | 
| 40 | 
            -
                -i, --id <id:i>           Alter entry with id <id> instead of the running entry
         | 
| 41 | 
            -
              * list - show the available timesheets
         | 
| 42 | 
            -
                usage: t list
         | 
| 43 | 
            -
              * now - show the status of the current timesheet
         | 
| 44 | 
            -
                usage: t now
         | 
| 45 | 
            -
              * out - stop the timer for the current timesheet
         | 
| 46 | 
            -
                usage: t out [--at TIME]
         | 
| 47 | 
            -
                -a, --at <time:qs>        Use this time instead of now
         | 
| 48 | 
            -
              * running - show all running timesheets
         | 
| 49 | 
            -
                NOT IMPLEMENTED
         | 
| 50 | 
            -
              * switch - switch to a new timesheet
         | 
| 51 | 
            -
                usage: t switch TIMESHEET
         | 
| 52 | 
            -
             | 
| 53 | 
            -
                OTHER OPTIONS
         | 
| 54 | 
            -
                -h, --help     Display this help
         | 
| 55 | 
            -
                EOF
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                def parse arguments
         | 
| 58 | 
            -
                  args.parse arguments
         | 
| 59 | 
            -
                end
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                def invoke
         | 
| 62 | 
            -
                  args['-h'] ? say(USAGE) : invoke_command_if_valid
         | 
| 63 | 
            -
                end
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                def commands
         | 
| 66 | 
            -
                  Timetrap::CLI::USAGE.scan(/\* \w+/).map{|s| s.gsub(/\* /, '')}
         | 
| 67 | 
            -
                end
         | 
| 68 | 
            -
             | 
| 69 | 
            -
                def invoke_command_if_valid
         | 
| 70 | 
            -
                  command = args.unused.shift
         | 
| 71 | 
            -
                  case (valid = commands.select{|name| name =~ %r|^#{command}|}).size
         | 
| 72 | 
            -
                  when 0 then say "Invalid command: #{command}"
         | 
| 73 | 
            -
                  when 1 then send valid[0]
         | 
| 74 | 
            -
                  else; say "Ambigous command: #{command}"; end
         | 
| 75 | 
            -
                end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                def alter
         | 
| 78 | 
            -
                  entry = args['-i'] ? Entry[args['-i']] : Timetrap.active_entry
         | 
| 79 | 
            -
                  say "can't find entry" && return unless entry
         | 
| 80 | 
            -
                  entry.update :start => args['-s'] if args['-s'] =~ /.+/
         | 
| 81 | 
            -
                  entry.update :end => args['-e'] if args['-e'] =~ /.+/
         | 
| 82 | 
            -
                  entry.update :note => unused_args if unused_args =~ /.+/
         | 
| 83 | 
            -
                end
         | 
| 84 | 
            -
             | 
| 85 | 
            -
                def backend
         | 
| 86 | 
            -
                  exec "sqlite3 #{DB_NAME}"
         | 
| 87 | 
            -
                end
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                def in
         | 
| 90 | 
            -
                  Timetrap.start unused_args, args['-a']
         | 
| 91 | 
            -
                end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                def out
         | 
| 94 | 
            -
                  Timetrap.stop args['-a']
         | 
| 95 | 
            -
                end
         | 
| 96 | 
            -
             | 
| 97 | 
            -
                def kill
         | 
| 98 | 
            -
                  if e = Entry[args['-i']]
         | 
| 99 | 
            -
                    out = "are you sure you want to delete entry #{e.id}? "
         | 
| 100 | 
            -
                    out << "(#{e.note}) " if e.note.to_s =~ /.+/
         | 
| 101 | 
            -
                    print out
         | 
| 102 | 
            -
                    if $stdin.gets =~ /\Aye?s?\Z/i
         | 
| 103 | 
            -
                      e.destroy
         | 
| 104 | 
            -
                      say "it's dead"
         | 
| 105 | 
            -
                    else
         | 
| 106 | 
            -
                      say "will not kill"
         | 
| 107 | 
            -
                    end
         | 
| 108 | 
            -
                  elsif (sheets = Entry.map{|e| e.sheet }.uniq).include?(sheet = unused_args)
         | 
| 109 | 
            -
                    victims = Entry.filter(:sheet => sheet).count
         | 
| 110 | 
            -
                    print "are you sure you want to delete #{victims} entries on sheet #{sheet.inspect}? "
         | 
| 111 | 
            -
                    if $stdin.gets =~ /\Aye?s?\Z/i
         | 
| 112 | 
            -
                      Timetrap.kill_sheet sheet
         | 
| 113 | 
            -
                      say "killed #{victims} entries"
         | 
| 114 | 
            -
                    else
         | 
| 115 | 
            -
                      say "will not kill"
         | 
| 116 | 
            -
                    end
         | 
| 117 | 
            -
                  else
         | 
| 118 | 
            -
                    victim = args['-i'] ? args['-i'].to_s.inspect : sheet.inspect
         | 
| 119 | 
            -
                    say "can't find #{victim} to kill", 'sheets:', *sheets
         | 
| 120 | 
            -
                  end
         | 
| 121 | 
            -
                end
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                def display
         | 
| 124 | 
            -
                  sheet = sheet_name_from_string(unused_args)
         | 
| 125 | 
            -
                  sheet = (sheet =~ /.+/ ? sheet : Timetrap.current_sheet)
         | 
| 126 | 
            -
                  say "Timesheet: #{sheet}"
         | 
| 127 | 
            -
                  id_heading = args['-v'] ? 'Id' : '  '
         | 
| 128 | 
            -
                  say "#{id_heading}  Day                Start      End        Duration   Notes"
         | 
| 129 | 
            -
                  last_start = nil
         | 
| 130 | 
            -
                  from_current_day = []
         | 
| 131 | 
            -
                  (ee = Timetrap.entries(sheet)).each_with_index do |e, i|
         | 
| 132 | 
            -
             | 
| 133 | 
            -
             | 
| 134 | 
            -
                    from_current_day << e
         | 
| 135 | 
            -
                    e_end = e.end || Time.now
         | 
| 136 | 
            -
                    say "%-4s%16s%11s -%9s%10s    %s" % [
         | 
| 137 | 
            -
                      (args['-v'] ? e.id : ''),
         | 
| 138 | 
            -
                      format_date_if_new(e.start, last_start),
         | 
| 139 | 
            -
                      format_time(e.start),
         | 
| 140 | 
            -
                      format_time(e.end),
         | 
| 141 | 
            -
                      format_duration(e.start, e_end),
         | 
| 142 | 
            -
                      e.note
         | 
| 143 | 
            -
                    ]
         | 
| 144 | 
            -
             | 
| 145 | 
            -
                    nxt = Timetrap.entries(sheet).map[i+1]
         | 
| 146 | 
            -
                    if nxt == nil or !same_day?(e.start, nxt.start)
         | 
| 147 | 
            -
                      say "%52s" % format_total(from_current_day)
         | 
| 148 | 
            -
                      from_current_day = []
         | 
| 149 | 
            -
                    else
         | 
| 150 | 
            -
                    end
         | 
| 151 | 
            -
                    last_start = e.start
         | 
| 152 | 
            -
                  end
         | 
| 153 | 
            -
                  say <<-OUT
         | 
| 154 | 
            -
                ---------------------------------------------------------
         | 
| 155 | 
            -
                  OUT
         | 
| 156 | 
            -
                  say "    Total%43s" % format_total(ee)
         | 
| 157 | 
            -
                end
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                def format
         | 
| 160 | 
            -
                  say "Sorry not implemented yet :-("
         | 
| 161 | 
            -
                end
         | 
| 162 | 
            -
             | 
| 163 | 
            -
                def switch
         | 
| 164 | 
            -
                  sheet = args.unused.join(' ')
         | 
| 165 | 
            -
                  if not sheet =~ /.+/ then say "No sheet specified"; return end
         | 
| 166 | 
            -
                  say "Switching to sheet " + Timetrap.switch(sheet)
         | 
| 167 | 
            -
                end
         | 
| 168 | 
            -
             | 
| 169 | 
            -
                def list
         | 
| 170 | 
            -
                  sheets = Entry.map{|e|e.sheet}.uniq.sort.map do |sheet|
         | 
| 171 | 
            -
                    sheet_atts = {:total => 0, :running => 0, :today => 0}
         | 
| 172 | 
            -
                    DB[:entries].filter(:sheet => sheet).inject(sheet_atts) do |m, e|
         | 
| 173 | 
            -
                      e_end = e[:end] || Time.now
         | 
| 174 | 
            -
                      m[:name] ||= sheet
         | 
| 175 | 
            -
                      m[:total] += (e_end.to_i - e[:start].to_i)
         | 
| 176 | 
            -
                      m[:running] += (e_end.to_i - e[:start].to_i) unless e[:end]
         | 
| 177 | 
            -
                      m[:today] += (e_end.to_i - e[:start].to_i) if same_day?(Time.now, e[:start])
         | 
| 178 | 
            -
                      m
         | 
| 179 | 
            -
                    end
         | 
| 180 | 
            -
                  end
         | 
| 181 | 
            -
                  width = sheets.sort_by{|h|h[:name].length }.last[:name].length + 4
         | 
| 182 | 
            -
                  say " %-#{width}s%-12s%-12s%s" % ["Timesheet", "Running", "Today", "Total Time"]
         | 
| 183 | 
            -
                  sheets.each do |sheet|
         | 
| 184 | 
            -
                    star = sheet[:name] == Timetrap.current_sheet ? '*' : ' '
         | 
| 185 | 
            -
                    say "#{star}%-#{width}s%-12s%-12s%s" % [
         | 
| 186 | 
            -
                      sheet[:running],
         | 
| 187 | 
            -
                      sheet[:today],
         | 
| 188 | 
            -
                      sheet[:total]
         | 
| 189 | 
            -
                    ].map(&method(:format_seconds)).unshift(sheet[:name])
         | 
| 190 | 
            -
                  end
         | 
| 191 | 
            -
                end
         | 
| 192 | 
            -
             | 
| 193 | 
            -
                def now
         | 
| 194 | 
            -
                  if Timetrap.running?
         | 
| 195 | 
            -
                    out = "#{Timetrap.current_sheet}: #{format_duration(Timetrap.active_entry.start, Time.now)}".gsub(/  /, ' ')
         | 
| 196 | 
            -
                    out << " (#{Timetrap.active_entry.note})" if Timetrap.active_entry.note =~ /.+/
         | 
| 197 | 
            -
                    say out
         | 
| 198 | 
            -
                  else
         | 
| 199 | 
            -
                    say "#{Timetrap.current_sheet}: not running"
         | 
| 200 | 
            -
                  end
         | 
| 201 | 
            -
                end
         | 
| 202 | 
            -
             | 
| 203 | 
            -
                def running
         | 
| 204 | 
            -
                  say "Sorry not implemented yet :-("
         | 
| 205 | 
            -
                end
         | 
| 206 | 
            -
             | 
| 207 | 
            -
                private
         | 
| 208 | 
            -
             | 
| 209 | 
            -
                def format_time time
         | 
| 210 | 
            -
                  return '' unless time.respond_to?(:strftime)
         | 
| 211 | 
            -
                  time.strftime('%H:%M:%S')
         | 
| 212 | 
            -
                end
         | 
| 213 | 
            -
             | 
| 214 | 
            -
                def format_date time
         | 
| 215 | 
            -
                  return '' unless time.respond_to?(:strftime)
         | 
| 216 | 
            -
                  time.strftime('%a %b %d, %Y')
         | 
| 217 | 
            -
                end
         | 
| 218 | 
            -
             | 
| 219 | 
            -
                def format_date_if_new time, last_time
         | 
| 220 | 
            -
                  return '' unless time.respond_to?(:strftime)
         | 
| 221 | 
            -
                  same_day?(time, last_time) ? '' : format_date(time)
         | 
| 222 | 
            -
                end
         | 
| 223 | 
            -
             | 
| 224 | 
            -
                def same_day? time, other_time
         | 
| 225 | 
            -
                  format_date(time) == format_date(other_time)
         | 
| 226 | 
            -
                end
         | 
| 227 | 
            -
             | 
| 228 | 
            -
                def format_duration stime, etime
         | 
| 229 | 
            -
                  return '' unless stime and etime
         | 
| 230 | 
            -
                  secs = etime.to_i - stime.to_i
         | 
| 231 | 
            -
                  format_seconds secs
         | 
| 232 | 
            -
                end
         | 
| 233 | 
            -
             | 
| 234 | 
            -
                def format_seconds secs
         | 
| 235 | 
            -
                  "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
         | 
| 236 | 
            -
                end
         | 
| 237 | 
            -
             | 
| 238 | 
            -
                def format_total entries
         | 
| 239 | 
            -
                  secs = entries.inject(0){|m, e|e_end = e.end || Time.now; m += e_end.to_i - e.start.to_i if e_end && e.start;m}
         | 
| 240 | 
            -
                  "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
         | 
| 241 | 
            -
                end
         | 
| 242 | 
            -
             | 
| 243 | 
            -
                def sheet_name_from_string string
         | 
| 244 | 
            -
                  return "" unless string =~ /.+/
         | 
| 245 | 
            -
                  DB[:entries].filter(:sheet.like("#{string}%")).first[:sheet]
         | 
| 246 | 
            -
                rescue
         | 
| 247 | 
            -
                  ""
         | 
| 248 | 
            -
                end
         | 
| 249 | 
            -
             | 
| 250 | 
            -
                def unused_args
         | 
| 251 | 
            -
                  args.unused.join(' ')
         | 
| 252 | 
            -
                end
         | 
| 253 | 
            -
             | 
| 254 | 
            -
                public
         | 
| 255 | 
            -
                def say *something
         | 
| 256 | 
            -
                  puts *something
         | 
| 257 | 
            -
                end
         | 
| 258 | 
            -
              end
         | 
| 259 | 
            -
             | 
| 260 18 | 
             
              def current_sheet= sheet
         | 
| 261 19 | 
             
                m = Meta.find_or_create(:key => 'current_sheet')
         | 
| 262 20 | 
             
                m.value = sheet
         | 
| @@ -306,45 +64,16 @@ where COMMAND is one of: | |
| 306 64 | 
             
                Entry.filter(:sheet => sheet).destroy
         | 
| 307 65 | 
             
              end
         | 
| 308 66 |  | 
| 67 | 
            +
              def format format_klass, entries
         | 
| 68 | 
            +
                format_klass.new(entries).output
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
             | 
| 309 71 | 
             
              class AlreadyRunning < StandardError
         | 
| 310 72 | 
             
                def message
         | 
| 311 73 | 
             
                  "Timetrap is already running"
         | 
| 312 74 | 
             
                end
         | 
| 313 75 | 
             
              end
         | 
| 314 76 |  | 
| 315 | 
            -
             | 
| 316 | 
            -
              class Entry < Sequel::Model
         | 
| 317 | 
            -
                plugin :schema
         | 
| 318 | 
            -
             | 
| 319 | 
            -
                def start= time
         | 
| 320 | 
            -
                  self[:start]= Chronic.parse(time) || time
         | 
| 321 | 
            -
                end
         | 
| 322 | 
            -
             | 
| 323 | 
            -
                def end= time
         | 
| 324 | 
            -
                  self[:end]= Chronic.parse(time) || time
         | 
| 325 | 
            -
                end
         | 
| 326 | 
            -
             | 
| 327 | 
            -
                # do a quick pseudo migration.  This should only get executed on the first run
         | 
| 328 | 
            -
                set_schema do
         | 
| 329 | 
            -
                  primary_key :id
         | 
| 330 | 
            -
                  column :note, :string
         | 
| 331 | 
            -
                  column :start, :timestamp
         | 
| 332 | 
            -
                  column :end, :timestamp
         | 
| 333 | 
            -
                  column :sheet, :string
         | 
| 334 | 
            -
                end
         | 
| 335 | 
            -
                create_table unless table_exists?
         | 
| 336 | 
            -
              end
         | 
| 337 | 
            -
             | 
| 338 | 
            -
              class Meta < Sequel::Model(:meta)
         | 
| 339 | 
            -
                plugin :schema
         | 
| 340 | 
            -
             | 
| 341 | 
            -
                set_schema do
         | 
| 342 | 
            -
                  primary_key :id
         | 
| 343 | 
            -
                  column :key, :string
         | 
| 344 | 
            -
                  column :value, :string
         | 
| 345 | 
            -
                end
         | 
| 346 | 
            -
                create_table unless table_exists?
         | 
| 347 | 
            -
              end
         | 
| 348 77 | 
             
              CLI.args = Getopt::Declare.new(<<-EOF)
         | 
| 349 78 | 
             
                #{CLI::USAGE}
         | 
| 350 79 | 
             
              EOF
         | 
    
        data/spec/timetrap_spec.rb
    CHANGED
    
    | @@ -5,7 +5,7 @@ require 'spec' | |
| 5 5 | 
             
            describe Timetrap do
         | 
| 6 6 | 
             
              def create_entry atts = {}
         | 
| 7 7 | 
             
                Timetrap::Entry.create({
         | 
| 8 | 
            -
                  :sheet => ' | 
| 8 | 
            +
                  :sheet => 'default',
         | 
| 9 9 | 
             
                  :start => Time.now,
         | 
| 10 10 | 
             
                  :end => Time.now,
         | 
| 11 11 | 
             
                  :note => 'note'}.merge(atts))
         | 
| @@ -136,8 +136,42 @@ Id  Day                Start      End        Duration   Notes | |
| 136 136 | 
             
                  end
         | 
| 137 137 |  | 
| 138 138 | 
             
                  describe "format" do
         | 
| 139 | 
            -
                     | 
| 140 | 
            -
                       | 
| 139 | 
            +
                    describe 'ical' do
         | 
| 140 | 
            +
                      before do
         | 
| 141 | 
            +
                        create_entry(:start => '2008-10-03 12:00:00', :end => '2008-10-03 14:00:00')
         | 
| 142 | 
            +
                        create_entry(:start => '2008-10-05 12:00:00', :end => '2008-10-05 14:00:00')
         | 
| 143 | 
            +
                      end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                      it "should filter events by the passed dates" do
         | 
| 146 | 
            +
                        invoke 'format ical --start 2008-10-03 --end 2008-10-03'
         | 
| 147 | 
            +
                        $stdout.string.scan(/BEGIN:VEVENT/).should have(1).item
         | 
| 148 | 
            +
                      end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                      it "should not filter events by date when none are passed" do
         | 
| 151 | 
            +
                        invoke 'format ical'
         | 
| 152 | 
            +
                        $stdout.string.scan(/BEGIN:VEVENT/).should have(2).item
         | 
| 153 | 
            +
                      end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                      it "should export a sheet to an ical format" do
         | 
| 156 | 
            +
                        invoke 'format ical --start 2008-10-03 --end 2008-10-03'
         | 
| 157 | 
            +
                        desired = <<-EOF
         | 
| 158 | 
            +
            BEGIN:VCALENDAR
         | 
| 159 | 
            +
            VERSION:2.0
         | 
| 160 | 
            +
            CALSCALE:GREGORIAN
         | 
| 161 | 
            +
            METHOD:PUBLISH
         | 
| 162 | 
            +
            PRODID:iCalendar-Ruby
         | 
| 163 | 
            +
            BEGIN:VEVENT
         | 
| 164 | 
            +
            SEQUENCE:0
         | 
| 165 | 
            +
            DTEND:20081003T140000
         | 
| 166 | 
            +
            SUMMARY:note
         | 
| 167 | 
            +
            DTSTART:20081003T120000
         | 
| 168 | 
            +
            END:VEVENT
         | 
| 169 | 
            +
            END:VCALENDAR
         | 
| 170 | 
            +
                        EOF
         | 
| 171 | 
            +
                        desired.each_line do |line|
         | 
| 172 | 
            +
                          $stdout.string.should =~ /#{line.chomp}/
         | 
| 173 | 
            +
                        end
         | 
| 174 | 
            +
                      end
         | 
| 141 175 | 
             
                    end
         | 
| 142 176 | 
             
                  end
         | 
| 143 177 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification 
         | 
| 2 2 | 
             
            name: samg-timetrap
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version 
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.8
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors: 
         | 
| 7 7 | 
             
            - Sam Goldstein
         | 
| @@ -71,12 +71,18 @@ extensions: [] | |
| 71 71 | 
             
            extra_rdoc_files: []
         | 
| 72 72 |  | 
| 73 73 | 
             
            files: 
         | 
| 74 | 
            -
            - README.md
         | 
| 75 74 | 
             
            - LICENSE.txt
         | 
| 76 75 | 
             
            - Rakefile
         | 
| 77 | 
            -
            -  | 
| 76 | 
            +
            - README.md
         | 
| 77 | 
            +
            - bin/dev_t
         | 
| 78 78 | 
             
            - bin/t
         | 
| 79 | 
            +
            - lib/timetrap
         | 
| 80 | 
            +
            - lib/timetrap.rb
         | 
| 79 81 | 
             
            - spec/timetrap_spec.rb
         | 
| 82 | 
            +
            - lib/timetrap/cli.rb
         | 
| 83 | 
            +
            - lib/timetrap/formatters
         | 
| 84 | 
            +
            - lib/timetrap/helpers.rb
         | 
| 85 | 
            +
            - lib/timetrap/models.rb
         | 
| 80 86 | 
             
            has_rdoc: true
         | 
| 81 87 | 
             
            homepage: http://github.com/samg/timetrap/tree/master
         | 
| 82 88 | 
             
            post_install_message: 
         |