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.
- 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
|
+
|