samg-timetrap 0.0.1

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 (6) hide show
  1. data/README +148 -0
  2. data/Rakefile +16 -0
  3. data/bin/t +3 -0
  4. data/lib/timetrap.rb +317 -0
  5. data/spec/timetrap_spec.rb +373 -0
  6. metadata +107 -0
data/README ADDED
@@ -0,0 +1,148 @@
1
+ Timetrap
2
+ ========
3
+
4
+ Timetrap is a ruby port of Trevor Caira's Timebook, a small utility which aims
5
+ to be a low-overhead way of tracking what you spend time on. Timetrap
6
+ maintains its state in a sqlite3 database.
7
+
8
+ To install:
9
+ $ gem sources -a http://gems.github.com (you only have to do this once)
10
+ $ sudo gem install samg-timetrap
11
+
12
+ This will place a ``t`` executable in your path.
13
+
14
+ Original Timebook available at:
15
+ http://bitbucket.org/trevor/timebook/src/
16
+
17
+
18
+ Concepts
19
+ ~~~~~~~~
20
+
21
+ Timetrap maintains a list of *timesheets* -- distinct lists of timed *periods*.
22
+ Each period has a start and end time, with the exception of the most recent
23
+ period, which may have no end time set. This indicates that this period is
24
+ still running. Timesheets containing such periods are considered *active*. It
25
+ is possible to have multiple timesheets active simultaneously, though a single
26
+ time sheet may only have one period running at once.
27
+
28
+ Interactions with timetrap are performed through the ``t`` command on the
29
+ command line. ``t`` is followed by one of timetrap's subcommands. Often used
30
+ subcommands include ``in``, ``out``, ``switch``, ``now``, ``list`` and
31
+ ``display``. Commands may be abbreviated as long as they are unambiguous: thus
32
+ ``t switch foo`` and ``t s foo`` are identical. With the default command set,
33
+ no two commands share the first same letter, thus it is only necessary to type
34
+ the first letter of a command. Likewise, commands which display timesheets
35
+ accept abbreviated timesheet names. ``t display f`` is thus equivalent to ``t
36
+ display foo`` if ``foo`` is the only timesheet which begins with "f". Note that
37
+ this does not apply to ``t switch``, since this command also creates
38
+ timesheets. (Using the earlier example, if ``t switch f`` is entered, it would
39
+ thus be ambiguous whether a new timesheet ``f`` or switching to the existing
40
+ timesheet ``foo`` was desired).
41
+
42
+ Usage
43
+ ~~~~~
44
+
45
+ The basic usage is as follows::
46
+
47
+ $ t switch writing
48
+ $ t in document timetrap --at "10 minutes ago"
49
+ $ t out
50
+
51
+ The first command, ``t switch writing``, switches to the timesheet "writing"
52
+ (or creates it if it does not exist). ``t in document timetrap --at "10 minutes
53
+ ago"`` creates a new period in the current timesheet, and annotates it with the
54
+ description "document timetrap". The optional --at flag can be passed to start
55
+ the entry at a time other than the present. Any Chronic or database parsable
56
+ strings are accepted Note that this command would be in error if the
57
+ ``writing`` timesheet was already active. Finally, ``t out`` records the
58
+ current time as the end time for the most recent period in the ``writing``
59
+ timesheet.
60
+
61
+ To display the current timesheet, invoke the ``t display`` command::
62
+
63
+ $ t display
64
+ Timesheet writing:
65
+ Day Start End Duration Notes
66
+ Mar 14, 2009 19:53:30 - 20:06:15 0:12:45 document timetrap
67
+ 20:07:02 - 0:00:01 write home about timetrap
68
+ 0:12:46
69
+ Total 0:12:46
70
+
71
+ Each period in the timesheet is listed on a row. If the timesheet is active,
72
+ the final period in the timesheet will have no end time. After each day, the
73
+ total time tracked in the timesheet for that day is listed. Note that this is
74
+ computed by summing the durations of the periods beginning in the day. In the
75
+ last row, the total time tracked in the timesheet is shown.
76
+
77
+ Commands
78
+ ~~~~~~~~
79
+
80
+ **alter**
81
+ Inserts a note associated with the currently active period in the timesheet.
82
+
83
+ usage: ``t alter NOTES...``
84
+
85
+ **backend**
86
+ Run an interactive database session on the timetrap database. Requires the
87
+ sqlite3 command.
88
+
89
+ usage: ``t backend``
90
+
91
+ **display**
92
+ Display a given timesheet. If no timesheet is specified, show the current
93
+ timesheet.
94
+
95
+ usage: ``t display [TIMESHEET]``
96
+
97
+ **format**
98
+ Export the current sheet as a comma-separated value format spreadsheet. If
99
+ the final entry is active, it is ignored.
100
+
101
+ If a specific timesheet is given, display the same information for that
102
+ timesheet instead.
103
+
104
+ usage: ``t format [--start DATE] [--end DATE] [TIMESHEET]``
105
+
106
+ **in**
107
+ Start the timer for the current timesheet. Must be called before out. Notes
108
+ may be specified for this period. This is exactly equivalent to
109
+ ``t in; t alter NOTES``
110
+
111
+ usage: ``t in [--at TIME] [NOTES...]``
112
+
113
+ **kill**
114
+ Delete a timesheet. If no timesheet is specified, delete the current
115
+ timesheet and switch to the default timesheet.
116
+
117
+ usage: ``t kill [TIMESHEET]``
118
+
119
+ **list**
120
+ List the available timesheets.
121
+
122
+ usage: ``t list``
123
+
124
+ **now**
125
+ Print the current sheet, whether it's active, and if so, how long it has been
126
+ active and what notes are associated with the current period.
127
+
128
+ If a specific timesheet is given, display the same information for that
129
+ timesheet instead.
130
+
131
+ usage: ``t now``
132
+
133
+ **out**
134
+ Stop the timer for the current timesheet. Must be called after in.
135
+
136
+ usage: ``t in [--at TIME]``
137
+
138
+ **running**
139
+ Print all active sheets and any messages associated with them.
140
+
141
+ usage: ``t running``
142
+
143
+ **switch**
144
+ Switch to a new timesheet. this causes all future operation (except switch)
145
+ to operate on that timesheet. The default timesheet is called "default".
146
+
147
+ usage: ``t switch TIMESHEET``
148
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'spec/rake/spectask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+
5
+ task :default => :spec
6
+
7
+ desc "Run all specs in spec directory"
8
+ Spec::Rake::SpecTask.new(:spec) do |t|
9
+ t.spec_files = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ Rake::RDocTask.new do |rd|
13
+ rd.main = "README"
14
+ rd.rdoc_dir = 'doc'
15
+ rd.rdoc_files.include("README", "**/*.rb")
16
+ end
data/bin/t ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'timetrap'
3
+ Timetrap::CLI.invoke
data/lib/timetrap.rb ADDED
@@ -0,0 +1,317 @@
1
+ require 'rubygems'
2
+ require 'chronic'
3
+ require 'sequel'
4
+ require 'Getopt/Declare'
5
+ # connect to database. This will create one if it doesn't exist
6
+ DB_NAME = defined?(TEST_MODE) ? nil : "#{ENV['HOME']}/.timetrap.db"
7
+ DB = Sequel.sqlite DB_NAME
8
+
9
+ module Timetrap
10
+ extend self
11
+
12
+ module CLI
13
+ attr_accessor :args
14
+ extend self
15
+
16
+ COMMANDS = {
17
+ "alter" => "alter the description of the active period",
18
+ "backend" => "open an the backend's interactive shell",
19
+ "display" => "display the current timesheet",
20
+ "format" => "export a sheet to csv format",
21
+ "in" => "start the timer for the current timesheet",
22
+ "kill" => "delete a timesheet",
23
+ "list" => "show the available timesheets",
24
+ "now" => "show the status of the current timesheet",
25
+ "out" => "stop the timer for the current timesheet",
26
+ "running" => "show all running timesheets",
27
+ "switch" => "switch to a new timesheet"
28
+ }
29
+
30
+ def parse arguments
31
+ args.parse arguments
32
+ end
33
+
34
+ def invoke
35
+ invoke_command_if_valid
36
+ end
37
+
38
+ def invoke_command_if_valid
39
+ command = args.unused.shift
40
+ case (valid = COMMANDS.keys.select{|name| name =~ %r|^#{command}|}).size
41
+ when 0 then say "Invalid command: #{command}"
42
+ when 1 then send valid[0]
43
+ else; say "Ambigous command: #{command}"; end
44
+ end
45
+
46
+ def alter
47
+ Timetrap.active_entry.update :note => args.unused.join(' ')
48
+ end
49
+
50
+ def backend
51
+ exec "sqlite3 #{DB_NAME}"
52
+ end
53
+
54
+ def in
55
+ Timetrap.start args.unused.join(' '), args['--at']
56
+ end
57
+
58
+ def out
59
+ Timetrap.stop args['--at']
60
+ end
61
+
62
+ def kill
63
+ sheet = args.unused.join(' ')
64
+ unless (sheets = Entry.map{|e| e.sheet }.uniq).include?(sheet)
65
+ say "ain't no sheet #{sheet.inspect}", 'sheets:', *sheets
66
+ return
67
+ end
68
+ victims = Entry.filter(:sheet => sheet).count
69
+ print "are you sure you want to delete #{victims} entries on sheet #{sheet.inspect}? "
70
+ if $stdin.gets =~ /\Aye?s?\Z/i
71
+ Timetrap.kill sheet
72
+ say "killed #{victims} entries"
73
+ else
74
+ say "will not kill"
75
+ end
76
+ end
77
+
78
+ def display
79
+ sheet = sheet_name_from_string(args.unused.join(' '))
80
+ sheet = (sheet =~ /.+/ ? sheet : Timetrap.current_sheet)
81
+ say "Timesheet: #{sheet}"
82
+ say " Day Start End Duration Notes"
83
+ last_start = nil
84
+ from_current_day = []
85
+ (ee = Timetrap.entries(sheet)).each_with_index do |e, i|
86
+
87
+
88
+ from_current_day << e
89
+ e_end = e.end || Time.now
90
+ say "%27s%11s -%9s%10s %s" % [
91
+ format_date_if_new(e.start, last_start),
92
+ format_time(e.start),
93
+ format_time(e.end),
94
+ format_duration(e.start, e_end),
95
+ e.note
96
+ ]
97
+
98
+ nxt = Timetrap.entries(sheet).map[i+1]
99
+ if nxt == nil or !same_day?(e.start, nxt.start)
100
+ say "%59s" % format_total(from_current_day)
101
+ from_current_day = []
102
+ else
103
+ end
104
+ last_start = e.start
105
+ end
106
+ say <<-OUT
107
+ ---------------------------------------------------------
108
+ OUT
109
+ say " Total%43s" % format_total(ee)
110
+ end
111
+
112
+ def format
113
+ say "Sorry not implemented yet :-("
114
+ end
115
+
116
+ def switch
117
+ sheet = args.unused.join(' ')
118
+ if not sheet then say "No sheet specified"; return end
119
+ say "Switching to sheet " + Timetrap.switch(sheet)
120
+ end
121
+
122
+ def list
123
+ sheets = Entry.map{|e|e.sheet}.uniq.sort.map do |sheet|
124
+ sheet_atts = {:total => 0, :running => 0, :today => 0}
125
+ DB[:entries].filter(:sheet => sheet).inject(sheet_atts) do |m, e|
126
+ e_end = e[:end] || Time.now
127
+ m[:name] ||= sheet
128
+ m[:total] += (e_end.to_i - e[:start].to_i)
129
+ m[:running] += (e_end.to_i - e[:start].to_i) unless e[:end]
130
+ m[:today] += (e_end.to_i - e[:start].to_i) if same_day?(Time.now, e[:start])
131
+ m
132
+ end
133
+ end
134
+ width = sheets.sort_by{|h|h[:name].length }.last[:name].length + 4
135
+ say " %-#{width}s%-12s%-12s%s" % ["Timesheet", "Running", "Today", "Total Time"]
136
+ sheets.each do |sheet|
137
+ star = sheet[:name] == Timetrap.current_sheet ? '*' : ' '
138
+ say "#{star}%-#{width}s%-12s%-12s%s" % [
139
+ sheet[:running],
140
+ sheet[:today],
141
+ sheet[:total]
142
+ ].map(&method(:format_seconds)).unshift(sheet[:name])
143
+ end
144
+ end
145
+
146
+ def now
147
+ if Timetrap.running?
148
+ out = "#{Timetrap.current_sheet}: #{format_duration(Timetrap.active_entry.start, Time.now)}".gsub(/ /, ' ')
149
+ out << " (#{Timetrap.active_entry.note})" if Timetrap.active_entry.note =~ /.+/
150
+ say out
151
+ else
152
+ say "#{Timetrap.current_sheet}: not running"
153
+ end
154
+ end
155
+
156
+
157
+ private
158
+
159
+ def format_time time
160
+ return '' unless time.respond_to?(:strftime)
161
+ time.strftime('%H:%M:%S')
162
+ end
163
+
164
+ def format_date time
165
+ return '' unless time.respond_to?(:strftime)
166
+ time.strftime('%a %b %d, %Y')
167
+ end
168
+
169
+ def format_date_if_new time, last_time
170
+ return '' unless time.respond_to?(:strftime)
171
+ same_day?(time, last_time) ? '' : format_date(time)
172
+ end
173
+
174
+ def same_day? time, other_time
175
+ format_date(time) == format_date(other_time)
176
+ end
177
+
178
+ def format_duration stime, etime
179
+ return '' unless stime and etime
180
+ secs = etime.to_i - stime.to_i
181
+ format_seconds secs
182
+ end
183
+
184
+ def format_seconds secs
185
+ "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
186
+ end
187
+
188
+ def format_total entries
189
+ 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}
190
+ "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
191
+ end
192
+
193
+ def sheet_name_from_string string
194
+ return "" unless string =~ /.+/
195
+ DB[:entries].filter(:sheet.like("#{string}%")).first[:sheet]
196
+ rescue
197
+ ""
198
+ end
199
+
200
+ public
201
+ def say *something
202
+ puts *something
203
+ end
204
+ end
205
+
206
+ def current_sheet= sheet
207
+ m = Meta.find_or_create(:key => 'current_sheet')
208
+ m.value = sheet
209
+ m.save
210
+ end
211
+
212
+ def current_sheet
213
+ unless Meta.find(:key => 'current_sheet')
214
+ Meta.create(:key => 'current_sheet', :value => 'default')
215
+ end
216
+ Meta.find(:key => 'current_sheet').value
217
+ end
218
+
219
+ def invoked_as_executable?
220
+ $0 == __FILE__
221
+ end
222
+
223
+ def entries sheet = nil
224
+ Entry.filter(:sheet => sheet).order_by(:start)
225
+ end
226
+
227
+ def running?
228
+ !!active_entry
229
+ end
230
+
231
+ def active_entry
232
+ Entry.find(:sheet => Timetrap.current_sheet, :end => nil)
233
+ end
234
+
235
+ def stop time = nil
236
+ while a = active_entry
237
+ time ||= Time.now
238
+ a.end = time
239
+ a.save
240
+ end
241
+ end
242
+
243
+ def start note, time = nil
244
+ raise AlreadyRunning if running?
245
+ time ||= Time.now
246
+ Entry.create(:sheet => Timetrap.current_sheet, :note => note, :start => time).save
247
+ rescue => e
248
+ CLI.say e.message
249
+ end
250
+
251
+ def switch sheet
252
+ self.current_sheet = sheet
253
+ end
254
+
255
+ def kill sheet
256
+ Entry.filter(:sheet => sheet).destroy
257
+ end
258
+
259
+ class AlreadyRunning < StandardError
260
+ def message
261
+ "Timetrap is already running"
262
+ end
263
+ end
264
+
265
+
266
+ class Entry < Sequel::Model
267
+ plugin :schema
268
+
269
+ def start= time
270
+ self[:start]= Chronic.parse(time) || time
271
+ end
272
+
273
+ def end= time
274
+ self[:end]= Chronic.parse(time) || time
275
+ end
276
+
277
+ # do a quick pseudo migration. This should only get executed on the first run
278
+ set_schema do
279
+ primary_key :id
280
+ column :note, :string
281
+ column :start, :timestamp
282
+ column :end, :timestamp
283
+ column :sheet, :string
284
+ end
285
+ create_table unless table_exists?
286
+ end
287
+
288
+ class Meta < Sequel::Model(:meta)
289
+ plugin :schema
290
+
291
+ set_schema do
292
+ primary_key :id
293
+ column :key, :string
294
+ column :value, :string
295
+ end
296
+ create_table unless table_exists?
297
+ end
298
+ CLI.args = Getopt::Declare.new(<<-EOF)
299
+ Usage: #{File.basename $0} COMMAND [OPTIONS] [ARGS...]
300
+
301
+ where COMMAND is one of:
302
+ alter - alter the description of the active period
303
+ backend - open an the backend's interactive shell
304
+ display - display the current timesheet
305
+ format - export a sheet to csv format
306
+ in - start the timer for the current timesheet
307
+ kill - delete a timesheet
308
+ list - show the available timesheets
309
+ now - show the status of the current timesheet
310
+ out - stop the timer for the current timesheet
311
+ running - show all running timesheets
312
+ switch - switch to a new timesheet
313
+
314
+ COMMAND OPTIONS
315
+ -a, --at <time:qs> Use this time instead of now
316
+ EOF
317
+ end
@@ -0,0 +1,373 @@
1
+ TEST_MODE = true
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'timetrap')
3
+ require 'spec'
4
+
5
+ describe Timetrap do
6
+ def create_entry atts = {}
7
+ Timetrap::Entry.create({
8
+ :sheet => 's1',
9
+ :start => Time.now,
10
+ :end => Time.now,
11
+ :note => 'note'}.merge(atts))
12
+ end
13
+
14
+ before :each do
15
+ Timetrap::Entry.create_table!
16
+ Timetrap::Meta.create_table!
17
+ $stdout = StringIO.new
18
+ $stdin = StringIO.new
19
+ end
20
+
21
+ describe 'CLI' do
22
+ describe "COMMANDS" do
23
+ def invoke command
24
+ Timetrap::CLI.parse command
25
+ Timetrap::CLI.invoke
26
+ end
27
+
28
+ describe 'alter' do
29
+ before do
30
+ Timetrap.start "running entry", nil
31
+ end
32
+ it "should alter the description of the active period" do
33
+ Timetrap.active_entry.note.should == 'running entry'
34
+ invoke 'alter new description'
35
+ Timetrap.active_entry.note.should == 'new description'
36
+ end
37
+ end
38
+
39
+ describe "backend" do
40
+ it "should open an sqlite console to the db" do
41
+ Timetrap::CLI.should_receive(:exec).with("sqlite3 #{DB_NAME}")
42
+ invoke 'backend'
43
+ end
44
+ end
45
+
46
+ describe "display" do
47
+ before do
48
+ Timetrap::Entry.create( :sheet => 'another',
49
+ :note => 'entry 4', :start => '2008-10-05 18:00:00'
50
+ )
51
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
52
+ :note => 'entry 2', :start => '2008-10-03 16:00:00', :end => '2008-10-03 18:00:00'
53
+ )
54
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
55
+ :note => 'entry 1', :start => '2008-10-03 12:00:00', :end => '2008-10-03 14:00:00'
56
+ )
57
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
58
+ :note => 'entry 3', :start => '2008-10-05 16:00:00', :end => '2008-10-05 18:00:00'
59
+ )
60
+ Timetrap::Entry.create( :sheet => 'SpecSheet',
61
+ :note => 'entry 4', :start => '2008-10-05 18:00:00'
62
+ )
63
+
64
+ Time.stub!(:now).and_return Time.at(1223254800 + (60*60*2))
65
+ @desired_output = <<-OUTPUT
66
+ Timesheet: SpecSheet
67
+ Day Start End Duration Notes
68
+ Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
69
+ 16:00:00 - 18:00:00 2:00:00 entry 2
70
+ 4:00:00
71
+ Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
72
+ 18:00:00 - 2:00:00 entry 4
73
+ 4:00:00
74
+ ---------------------------------------------------------
75
+ Total 8:00:00
76
+ OUTPUT
77
+ end
78
+
79
+ it "should display the current timesheet" do
80
+ Timetrap.current_sheet = 'SpecSheet'
81
+ invoke 'display'
82
+ $stdout.string.should == @desired_output
83
+ end
84
+
85
+ it "should display a non current timesheet" do
86
+ Timetrap.current_sheet = 'another'
87
+ invoke 'display SpecSheet'
88
+ $stdout.string.should == @desired_output
89
+ end
90
+
91
+ it "should display a non current timesheet based on a partial name match" do
92
+ Timetrap.current_sheet = 'another'
93
+ invoke 'display S'
94
+ $stdout.string.should == @desired_output
95
+ end
96
+ end
97
+
98
+ describe "format" do
99
+ it "should export a sheet to a csv format" do
100
+ pending
101
+ end
102
+ end
103
+
104
+ describe "in" do
105
+ it "should start the time for the current timesheet" do
106
+ lambda do
107
+ invoke 'in'
108
+ end.should change(Timetrap::Entry, :count).by(1)
109
+ end
110
+
111
+ it "should set the note when starting a new entry" do
112
+ invoke 'in working on something'
113
+ Timetrap::Entry.order_by(:id).last.note.should == 'working on something'
114
+ end
115
+
116
+ it "should set the start when starting a new entry" do
117
+ @time = Time.now
118
+ Time.stub!(:now).and_return @time
119
+ invoke 'in working on something'
120
+ Timetrap::Entry.order_by(:id).last.start.to_i.should == @time.to_i
121
+ end
122
+
123
+ it "should not start the time if the timetrap is running" do
124
+ Timetrap.stub!(:running?).and_return true
125
+ lambda do
126
+ invoke 'in'
127
+ end.should_not change(Timetrap::Entry, :count)
128
+ end
129
+
130
+ it "should allow the sheet to be started at a certain time" do
131
+ invoke 'in work --at "10am 2008-10-03"'
132
+ Timetrap::Entry.order_by(:id).last.start.should == Time.parse('2008-10-03 10:00')
133
+ end
134
+ end
135
+
136
+ describe "kill" do
137
+ it "should give me a chance not to fuck up" do
138
+ entry = create_entry
139
+ lambda do
140
+ $stdin.string = ""
141
+ invoke "kill #{entry.sheet}"
142
+ end.should_not change(Timetrap::Entry, :count).by(-1)
143
+ end
144
+
145
+ it "should delete a timesheet" do
146
+ entry = create_entry
147
+ lambda do
148
+ $stdin.string = "yes\n"
149
+ invoke "kill #{entry.sheet}"
150
+ end.should change(Timetrap::Entry, :count).by(-1)
151
+ end
152
+ end
153
+
154
+ describe "list" do
155
+ before do
156
+ Time.stub!(:now).and_return Time.parse("Oct 5 18:00:00 -0700 2008")
157
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-03 12:00:00',
158
+ :end => '2008-10-03 14:00:00')
159
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-03 12:00:00',
160
+ :end => '2008-10-03 14:00:00')
161
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-05 12:00:00',
162
+ :end => '2008-10-05 14:00:00')
163
+ create_entry( :sheet => 'A Longly Named Sheet 2', :start => '2008-10-05 14:00:00',
164
+ :end => nil)
165
+ create_entry( :sheet => 'Sheet 1', :start => '2008-10-03 16:00:00',
166
+ :end => '2008-10-03 18:00:00')
167
+ Timetrap.current_sheet = 'A Longly Named Sheet 2'
168
+ end
169
+ it "should list available timesheets" do
170
+ invoke 'list'
171
+ $stdout.string.should == <<-OUTPUT
172
+ Timesheet Running Today Total Time
173
+ *A Longly Named Sheet 2 4:00:00 6:00:00 10:00:00
174
+ Sheet 1 0:00:00 0:00:00 2:00:00
175
+ OUTPUT
176
+ end
177
+ end
178
+
179
+ describe "now" do
180
+ before do
181
+ Timetrap.current_sheet = 'current sheet'
182
+ end
183
+
184
+ describe "when the current timesheet isn't running" do
185
+ it "should show that it isn't running" do
186
+ invoke 'now'
187
+ $stdout.string.should == <<-OUTPUT
188
+ current sheet: not running
189
+ OUTPUT
190
+ end
191
+ end
192
+
193
+ describe "when the current timesheet is running" do
194
+ before do
195
+ invoke 'in a timesheet that is running'
196
+ @entry = Timetrap.active_entry
197
+ @entry.stub!(:start).and_return(Time.at(0))
198
+ Time.stub!(:now).and_return Time.at(60)
199
+ Timetrap.stub!(:active_entry).and_return @entry
200
+ end
201
+
202
+ it "should show how long the current item is running for" do
203
+ invoke 'now'
204
+ $stdout.string.should == <<-OUTPUT
205
+ current sheet: 0:01:00 (a timesheet that is running)
206
+ OUTPUT
207
+ end
208
+ end
209
+ end
210
+
211
+ describe "out" do
212
+ before :each do
213
+ invoke 'in'
214
+ @active = Timetrap.active_entry
215
+ @now = Time.now
216
+ Time.stub!(:now).and_return @now
217
+ end
218
+ it "should set the stop for the running entry" do
219
+ @active.refresh.end.should == nil
220
+ invoke 'out'
221
+ @active.refresh.end.to_i.should == @now.to_i
222
+ end
223
+
224
+ it "should not do anything if nothing is running" do
225
+ lambda do
226
+ invoke 'out'
227
+ invoke 'out'
228
+ end.should_not raise_error
229
+ end
230
+
231
+ it "should allow the sheet to be stopped at a certain time" do
232
+ invoke 'out --at "10am 2008-10-03"'
233
+ Timetrap::Entry.order_by(:id).last.end.should == Time.parse('2008-10-03 10:00')
234
+ end
235
+ end
236
+
237
+ describe "running" do
238
+ before do
239
+ create_entry :sheet => 'one', :end => nil
240
+ create_entry :sheet => 'two', :end => nil
241
+ end
242
+ it "should show all running timesheets" do
243
+
244
+
245
+ end
246
+ end
247
+
248
+ describe "switch" do
249
+ it "should switch to a new timesheet" do
250
+ invoke 'switch sheet 1'
251
+ Timetrap.current_sheet.should == 'sheet 1'
252
+ invoke 'switch sheet 2'
253
+ Timetrap.current_sheet.should == 'sheet 2'
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ describe "entries" do
260
+ it "should give the entires for a sheet" do
261
+ e = create_entry :sheet => 'sheet'
262
+ Timetrap.entries('sheet').all.should include(e)
263
+ end
264
+
265
+ end
266
+
267
+ describe "start" do
268
+ it "should start an new entry" do
269
+ @time = Time.now
270
+ Timetrap.current_sheet = 'sheet1'
271
+ lambda do
272
+ Timetrap.start 'some work', @time
273
+ end.should change(Timetrap::Entry, :count).by(1)
274
+ Timetrap::Entry.order(:id).last.sheet.should == 'sheet1'
275
+ Timetrap::Entry.order(:id).last.note.should == 'some work'
276
+ Timetrap::Entry.order(:id).last.start.to_i.should == @time.to_i
277
+ Timetrap::Entry.order(:id).last.end.should be_nil
278
+ end
279
+
280
+ it "should be running if it is started" do
281
+ Timetrap.should_not be_running
282
+ Timetrap.start 'some work', @time
283
+ Timetrap.should be_running
284
+ end
285
+
286
+ it "should raise and error if it is already running" do
287
+ lambda do
288
+ Timetrap.start 'some work', @time
289
+ Timetrap.start 'some work', @time
290
+ end.should change(Timetrap::Entry, :count).by(1)
291
+ end
292
+ end
293
+
294
+ describe "stop" do
295
+ it "should stop a new entry" do
296
+ @time = Time.now
297
+ Timetrap.start 'some work', @time
298
+ entry = Timetrap.active_entry
299
+ entry.end.should be_nil
300
+ Timetrap.stop @time
301
+ entry.refresh.end.to_i.should == @time.to_i
302
+ end
303
+
304
+ it "should not be running if it is stopped" do
305
+ Timetrap.should_not be_running
306
+ Timetrap.start 'some work', @time
307
+ Timetrap.stop
308
+ Timetrap.should_not be_running
309
+ end
310
+
311
+ it "should not stop it twice" do
312
+ Timetrap.start 'some work'
313
+ e = Timetrap.active_entry
314
+ Timetrap.stop
315
+ time = e.refresh.end
316
+ Timetrap.stop
317
+ time.to_i.should == e.refresh.end.to_i
318
+ end
319
+
320
+ end
321
+
322
+ describe 'switch' do
323
+ it "should switch to a new sheet" do
324
+ Timetrap.switch 'sheet1'
325
+ Timetrap.current_sheet.should == 'sheet1'
326
+ Timetrap.switch 'sheet2'
327
+ Timetrap.current_sheet.should == 'sheet2'
328
+ end
329
+ end
330
+ end
331
+
332
+ describe Timetrap::Entry do
333
+ before do
334
+ @time = Time.now
335
+ @entry = Timetrap::Entry.new
336
+ end
337
+
338
+ describe 'attributes' do
339
+ it "should have a note" do
340
+ @entry.note = "world takeover"
341
+ @entry.note.should == "world takeover"
342
+ end
343
+
344
+ it "should have a start" do
345
+ @entry.start = @time
346
+ @entry.start.should == @time
347
+ end
348
+
349
+ it "should have a end" do
350
+ @entry.end = @time
351
+ @entry.end.should == @time
352
+ end
353
+
354
+ it "should have a sheet" do
355
+ @entry.sheet= 'name'
356
+ @entry.sheet.should == 'name'
357
+ end
358
+ end
359
+
360
+ describe "parsing natural language times" do
361
+ it "should set start time using english" do
362
+ @entry.start = "yesterday 10am"
363
+ @entry.start.should_not be_nil
364
+ @entry.start.should == Chronic.parse("yesterday 10am")
365
+ end
366
+
367
+ it "should set end time using english" do
368
+ @entry.end = "tomorrow 1pm"
369
+ @entry.end.should_not be_nil
370
+ @entry.end.should == Chronic.parse("tomorrow 1pm")
371
+ end
372
+ end
373
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: samg-timetrap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sam Goldstein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-14 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: sequel
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.12.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: chronic
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.3
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: getopt-declare
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "1.28"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: mime-types
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "1.15"
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: diff-lcs
57
+ type: :runtime
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 1.1.2
64
+ version:
65
+ description: Command line time tracker
66
+ email: sgrock@gmail.com
67
+ executables:
68
+ - t
69
+ extensions: []
70
+
71
+ extra_rdoc_files: []
72
+
73
+ files:
74
+ - README
75
+ - Rakefile
76
+ - lib/timetrap.rb
77
+ - bin/t
78
+ - spec/timetrap_spec.rb
79
+ has_rdoc: true
80
+ homepage: http://github.com/samg/timetrap/tree/master
81
+ post_install_message:
82
+ rdoc_options:
83
+ - --inline-source
84
+ - --charset=UTF-8
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ version:
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: "0"
98
+ version:
99
+ requirements: []
100
+
101
+ rubyforge_project:
102
+ rubygems_version: 1.2.0
103
+ signing_key:
104
+ specification_version: 2
105
+ summary: Command line time tracker
106
+ test_files: []
107
+