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