samg-timetrap 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +148 -0
- data/Rakefile +16 -0
- data/bin/t +3 -0
- data/lib/timetrap.rb +317 -0
- data/spec/timetrap_spec.rb +373 -0
- 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
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
|
+
|