timeless 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.textile +99 -0
- data/Rakefile +2 -0
- data/bin/timeless +386 -0
- data/lib/timeless.rb +7 -0
- data/lib/timeless/pomodoro.rb +40 -0
- data/lib/timeless/stopwatch.rb +62 -0
- data/lib/timeless/storage.rb +69 -0
- data/lib/timeless/version.rb +3 -0
- data/timeless.gemspec +26 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a45d84d4290e39bba5b6fe3a99fd101cefd7f97c
|
4
|
+
data.tar.gz: 761ab4cf1594beb69534198f556a11f936806801
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5b97c16e7cc2fc1b1b919c93518a11c7ae6a115e11ff1138251bf26bd699da7d18b11f59b0a222c4ca242105d596528b52e1bb6149abd4f1df8f4c37adeffd3f
|
7
|
+
data.tar.gz: 72160f36426d2659db29cb1cfc57deb81948e1a24d483639197f881c0068c65b7b06455fc684d018793e091d7d85b0bd5e22d3c96d9b485d116d175affe9fddd
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Adolfo Villafiorita
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
h1. Timeless
|
2
|
+
|
3
|
+
Timeless is a simple command line time tracker.
|
4
|
+
|
5
|
+
h2. Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
bc.. ruby
|
10
|
+
gem 'timeless'
|
11
|
+
|
12
|
+
p. And then execute:
|
13
|
+
|
14
|
+
bc. $ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
bc. $ gem install timeless
|
19
|
+
|
20
|
+
h2. Usage
|
21
|
+
|
22
|
+
timeless is a simple command line time tracker. Entries store the following information:
|
23
|
+
|
24
|
+
# start time
|
25
|
+
# end time
|
26
|
+
# notes
|
27
|
+
|
28
|
+
timeless allows one to
|
29
|
+
|
30
|
+
* run a pomodoro timer
|
31
|
+
* clock time using a "stopwatch" (with start and stop commands)
|
32
|
+
* manually entering data
|
33
|
+
* print a text report, possibly filtered by date ranges of notes' content
|
34
|
+
* export to csv
|
35
|
+
|
36
|
+
Timeless has also some basic support for key-pair values in the notes form. They can be used to assign a special meaning to some strings and improve filtering and exporting. Two special keys are @p@ for projects and @c@ for clients. These two keys are exported in dedicated columns when exporting data to csv.
|
37
|
+
|
38
|
+
Some non nominal conditions are also handled (e.g., starting a clock twice, trying to stop when no clock was started).
|
39
|
+
|
40
|
+
h2. Examples
|
41
|
+
|
42
|
+
Get information about command syntax:
|
43
|
+
|
44
|
+
bc. timeless -h
|
45
|
+
|
46
|
+
Run a pomodoro timer:
|
47
|
+
|
48
|
+
bc.. timeless pom p:project meeting with c:john
|
49
|
+
timeless pom --duration 60 p:prj2
|
50
|
+
timeless pom --long # default to 50 minutes
|
51
|
+
|
52
|
+
p. Start clocking using a stopwatch:
|
53
|
+
|
54
|
+
bc.. timeless start requirements doc
|
55
|
+
timeless start --at 'thirty minutes ago' fixing bug 182 for c:tim
|
56
|
+
timeless start --force --at 'five minutes ago' requirements document for p:prj1
|
57
|
+
|
58
|
+
Stop clocking:
|
59
|
+
|
60
|
+
bc.. timeless stop
|
61
|
+
timeless stop forgot notes on start
|
62
|
+
timeless stop --last # reuse the notes of last entry
|
63
|
+
timeless stop --at 'five minutes ago'
|
64
|
+
timeless stop --start '1 hour ago' --at 'now' clocked a full entry
|
65
|
+
|
66
|
+
p. Enter a full entry:
|
67
|
+
|
68
|
+
bc.. timeless clock activity on p:project # from end of last entry to now
|
69
|
+
timeless clock --start 'three hours ago' --stop 'five minutes ago'
|
70
|
+
|
71
|
+
p. What was i doing?
|
72
|
+
|
73
|
+
bc.. timeless forget # forget pending clock
|
74
|
+
timeless last # print last entry
|
75
|
+
timeless current # print current entry
|
76
|
+
|
77
|
+
p. Reporting and exporting
|
78
|
+
|
79
|
+
bc.. timeless report
|
80
|
+
timeless report --from yesterday --filter p:project
|
81
|
+
timeless export
|
82
|
+
|
83
|
+
Notice that even though data is stored in csv, the export command exports data in a format which is more easily parsed by a Spreadsheet such as LibreOffice.
|
84
|
+
|
85
|
+
h2. If something goes wrong
|
86
|
+
|
87
|
+
Data is stored in the CSV file @~/.timeless.csv@. You can edit the file to manually fix entries if you make some mistake.
|
88
|
+
|
89
|
+
h2. License
|
90
|
+
|
91
|
+
Licensed under the terms of the MIT License.
|
92
|
+
|
93
|
+
h2. Contributing
|
94
|
+
|
95
|
+
# Fork it ( https://github.com/[my-github-username]/timeless/fork )
|
96
|
+
# Create your feature branch (`git checkout -b my-new-feature`)
|
97
|
+
# Commit your changes (`git commit -am 'Add some feature'`)
|
98
|
+
# Push to the branch (`git push origin my-new-feature`)
|
99
|
+
# Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/timeless
ADDED
@@ -0,0 +1,386 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'slop'
|
4
|
+
require 'date'
|
5
|
+
require 'chronic'
|
6
|
+
|
7
|
+
require "timeless"
|
8
|
+
|
9
|
+
version = Timeless::VERSION
|
10
|
+
app_name = "timeless"
|
11
|
+
app_one_liner = "A command line time tracker"
|
12
|
+
|
13
|
+
man = <<EOS
|
14
|
+
NAME
|
15
|
+
#{app_name} -- #{app_one_liner}
|
16
|
+
|
17
|
+
SYNOPSYS
|
18
|
+
#{app_name} [-h|-v]
|
19
|
+
#{app_name} command [options] [args]
|
20
|
+
|
21
|
+
DESCRIPTION
|
22
|
+
timeless is a simple command line time tracker
|
23
|
+
|
24
|
+
It allows one to:
|
25
|
+
|
26
|
+
* run a pomodoro timer of standard or custom duration
|
27
|
+
* clock time using a \"stopwatch\" (with start and stop commands)
|
28
|
+
* manually entering data
|
29
|
+
* print a text report, possibly filtered by date ranges or strings
|
30
|
+
* export to csv
|
31
|
+
|
32
|
+
Some non nominal conditions are handled (e.g. starting twice, trying to stop
|
33
|
+
when no clock was started).
|
34
|
+
|
35
|
+
Timeless assigns a special meaning to some keypairs in notes. In particular,
|
36
|
+
you can use:
|
37
|
+
|
38
|
+
p:PROJECT
|
39
|
+
c:NAME
|
40
|
+
|
41
|
+
to specify the project and the client a specific entry refers to. The information
|
42
|
+
can be used for filtering and when exporting data.
|
43
|
+
|
44
|
+
EXAMPLES
|
45
|
+
|
46
|
+
timeless help
|
47
|
+
timeless man
|
48
|
+
|
49
|
+
timeless pom
|
50
|
+
|
51
|
+
timeless start doing something for c:a on p:b
|
52
|
+
timeless stop --at '5 minutes ago'
|
53
|
+
|
54
|
+
timeless report
|
55
|
+
timeless export
|
56
|
+
|
57
|
+
VERSION
|
58
|
+
This is version #{version}
|
59
|
+
|
60
|
+
LICENSE
|
61
|
+
MIT
|
62
|
+
|
63
|
+
SEE ALSO
|
64
|
+
#{app_name} -h
|
65
|
+
https://github.com/avillafiorita/#{app_name}
|
66
|
+
EOS
|
67
|
+
|
68
|
+
def last_entry
|
69
|
+
start, stop, notes = Timeless::Storage.last
|
70
|
+
|
71
|
+
interval = Time.parse(stop) - Time.parse(start)
|
72
|
+
seconds = interval % 60
|
73
|
+
minutes = (interval / 60) % 60
|
74
|
+
hours = interval / 3600
|
75
|
+
|
76
|
+
sprintf "Timeless: you clocked %02d:%02d:%02d on %s\nYou stopped clocking at: %s", hours, minutes, seconds, notes, stop
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# code shared by stop and clock (which accept slightly different options,
|
81
|
+
# but behave in the same way
|
82
|
+
#
|
83
|
+
def manage_stop start, stop, notes, reuse_last
|
84
|
+
# syntax check:
|
85
|
+
# - --start is illegal if there is a running clock
|
86
|
+
# - --last is illegal if there are notes
|
87
|
+
#
|
88
|
+
# however:
|
89
|
+
# - --last and notes prevail over notes stored when starting the clock
|
90
|
+
if Timeless::Stopwatch.clocking? and start
|
91
|
+
puts "Timeless error: you specified --start with a running clock. Use 'timeless forget' or drop --start"
|
92
|
+
end
|
93
|
+
if reuse_last and notes != "" then
|
94
|
+
puts "Timeless error: you specified both --last and notes. Choose one or the other"
|
95
|
+
end
|
96
|
+
|
97
|
+
if reuse_last then
|
98
|
+
_, _, notes = Timeless::Storage.last
|
99
|
+
end
|
100
|
+
|
101
|
+
if Timeless::Stopwatch.clocking?
|
102
|
+
start, stop, notes = Timeless::Stopwatch.stop(start, stop, notes) # merge passed with data in running clock
|
103
|
+
Timeless::Storage.store(start, stop, notes)
|
104
|
+
|
105
|
+
puts "Clock stopped at #{stop}."
|
106
|
+
puts last_entry
|
107
|
+
else
|
108
|
+
start = start ? start : Timeless::Storage.last[1]
|
109
|
+
stop = stop ? stop : Time.now
|
110
|
+
Timeless::Storage.store(start, stop, notes)
|
111
|
+
|
112
|
+
puts "Clock stopped at #{stop}."
|
113
|
+
puts last_entry
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Main App Starts Here!
|
119
|
+
#
|
120
|
+
if ARGV == [] then
|
121
|
+
puts "type 'timeless -h' for help."
|
122
|
+
exit
|
123
|
+
end
|
124
|
+
|
125
|
+
#begin
|
126
|
+
opts = Slop.parse :help => true, :strict => true do
|
127
|
+
|
128
|
+
banner "#{app_name} [-h|-v]\n#{app_name} command [options] [args]"
|
129
|
+
|
130
|
+
##############################################################################
|
131
|
+
on "-v", "--version", 'Print version information' do
|
132
|
+
puts "#{app_name} version #{version}"
|
133
|
+
end
|
134
|
+
|
135
|
+
##############################################################################
|
136
|
+
command :man do
|
137
|
+
banner "#{app_name} man"
|
138
|
+
description "Print usage instruction for #{app_name}"
|
139
|
+
|
140
|
+
run do |_, _|
|
141
|
+
puts man
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
##############################################################################
|
146
|
+
command :pom do
|
147
|
+
banner "#{app_name} pom [options] [notes]"
|
148
|
+
description "Start a pomodoro timer."
|
149
|
+
|
150
|
+
on :long, "Use a long timer (50 minutes)"
|
151
|
+
on :duration=, "Duration of the pomodoro timer, in minutes", as: Integer
|
152
|
+
|
153
|
+
run do |opts, args|
|
154
|
+
duration = opts.to_hash[:duration] ||
|
155
|
+
(opts.to_hash[:long] ? Timeless::Pomodoro::WORKING_LONG : Timeless::Pomodoro::WORKING)
|
156
|
+
|
157
|
+
start, stop, notes = Timeless::Pomodoro.run_pomodoro_timer(duration, args.join(" "))
|
158
|
+
Timeless::Storage.store(start, stop, notes)
|
159
|
+
puts last_entry
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
##############################################################################
|
164
|
+
command :start do
|
165
|
+
banner "#{app_name} start [--at chronic expression] [notes]"
|
166
|
+
description "Start clocking."
|
167
|
+
|
168
|
+
on :at=, "Actual start time, using chronic expression"
|
169
|
+
on :force, "Forget existing running timer"
|
170
|
+
|
171
|
+
run do |opts, args|
|
172
|
+
start = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
|
173
|
+
notes = args.join(" ") # empty string is no args specified
|
174
|
+
force = opts.to_hash[:force]
|
175
|
+
|
176
|
+
if Timeless::Stopwatch.clocking? and not force
|
177
|
+
start, notes = Timeless::Stopwatch.get_start # for information purposes only
|
178
|
+
puts "There is a clock started at #{start} (notes: \"#{notes}\"). Use --force to override."
|
179
|
+
else
|
180
|
+
Timeless::Stopwatch.start(start, notes)
|
181
|
+
puts "Clock started at #{start ? start : Time.now}."
|
182
|
+
puts "You may want to specify notes for the entry when you stop clocking." if notes == ""
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
##############################################################################
|
188
|
+
command :forget do
|
189
|
+
banner "#{app_name} forget"
|
190
|
+
description "Forget the timer you started, if any"
|
191
|
+
|
192
|
+
run do |opts, args|
|
193
|
+
Timeless::Stopwatch.forget
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
##############################################################################
|
198
|
+
command :stop do
|
199
|
+
banner "#{app_name} stop [--at chronic expression] [--start chronic expression] [--last|notes]"
|
200
|
+
description "Stop clocking, using notes, if specified. Require a start time if there is no pending clock."
|
201
|
+
|
202
|
+
on :at=, "Stop time, using chronic expression"
|
203
|
+
on :start=, "Start time, using chronic expression. Required if there is no timer started."
|
204
|
+
on :last, "Reuse notes of last clocked entry"
|
205
|
+
|
206
|
+
|
207
|
+
run do |opts, args|
|
208
|
+
start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
|
209
|
+
stop = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
|
210
|
+
notes = args.join(" ") # empty string if no args specified
|
211
|
+
|
212
|
+
manage_stop start, stop, notes, opts[:last]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
##############################################################################
|
217
|
+
command :clock do
|
218
|
+
banner "#{app_name} clock [--start chronic expression] [--stop|--end chronic expression] [--last|notes]"
|
219
|
+
description "Clock an entry specifying start and end time. Leave any running clock."
|
220
|
+
|
221
|
+
on :start=, "Start time, using chronic expression (start from previous entry if not specified)."
|
222
|
+
on :stop=, "End time, using chronic expression (stop now, if not specified)."
|
223
|
+
on :end=, "End time, an alias for stop."
|
224
|
+
on :last, "Reuse notes of last entry"
|
225
|
+
|
226
|
+
run do |opts, args|
|
227
|
+
start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
|
228
|
+
stop_opt = Chronic.parse(opts.to_hash[:stop]) # nil if no option specified
|
229
|
+
end_opt = Chronic.parse(opts.to_hash[:end]) # nil if no option specified
|
230
|
+
stop = stop_opt ? stop_opt : end_opt # stop_opt if --stop, end_opt if --end, nil otherwise
|
231
|
+
notes = args.join(" ") # empty string if no args specified
|
232
|
+
|
233
|
+
if stop_opt and end_opt then
|
234
|
+
puts "Timeless error: specify end time with either --end or --stop (not both)"
|
235
|
+
exit
|
236
|
+
end
|
237
|
+
|
238
|
+
manage_stop start, stop, notes, opts[:last]
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
##############################################################################
|
244
|
+
command :current do
|
245
|
+
banner "#{app_name} current"
|
246
|
+
description "Show time elapsed since last start command."
|
247
|
+
|
248
|
+
run do |opts, args|
|
249
|
+
if Timeless::Stopwatch.clocking? then
|
250
|
+
start, notes = Timeless::Stopwatch.get_start
|
251
|
+
puts "Timeless: you have been clocking #{"%.0d" % ((Time.now- Time.parse(start)) / 60)} minutes"
|
252
|
+
puts "on: #{notes}" if notes
|
253
|
+
else
|
254
|
+
puts "There is no clock started."
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
##############################################################################
|
261
|
+
command :last do
|
262
|
+
banner "#{app_name} last"
|
263
|
+
description "Show last entry."
|
264
|
+
|
265
|
+
run do |_, _|
|
266
|
+
puts last_entry
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
##############################################################################
|
271
|
+
command :list do
|
272
|
+
banner "#{app_name} list key"
|
273
|
+
description "List all values assigned to a given key (e.g., list all projects)"
|
274
|
+
|
275
|
+
run do |_, args|
|
276
|
+
values = Timeless::Storage.get_key args[0]
|
277
|
+
puts "Timeless: list of keys #{args[0]} found in timesheets:"
|
278
|
+
values.each do |value|
|
279
|
+
puts value
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
##############################################################################
|
285
|
+
command :export do
|
286
|
+
banner "#{app_name} export"
|
287
|
+
description "Export to csv"
|
288
|
+
|
289
|
+
run do |_, _|
|
290
|
+
Timeless::Storage.export
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
##############################################################################
|
296
|
+
command :gaps do
|
297
|
+
banner "#{app_name} gaps"
|
298
|
+
description "List all gaps in the timesheets (periods you have not clocked)"
|
299
|
+
|
300
|
+
on :from=, "From date"
|
301
|
+
on :to=, "To date"
|
302
|
+
on :interval=, "Minimum interval to report (in minutes)", :as => Integer
|
303
|
+
|
304
|
+
run do |opts, _|
|
305
|
+
from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
|
306
|
+
to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
|
307
|
+
interval = opts.to_hash[:interval] || 1
|
308
|
+
|
309
|
+
entries = Timeless::Storage.get(from, to)
|
310
|
+
previous_stop = from || Time.parse(entries[0][0])
|
311
|
+
day = ""
|
312
|
+
|
313
|
+
printf "%-18s %-5s - %-5s %-6s %-s\n", "Day", "Start", "End", "Gap", "Command to fix"
|
314
|
+
entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
|
315
|
+
current_start = Time.parse(entry[0])
|
316
|
+
|
317
|
+
current_day = current_start.strftime("%a %b %d, %Y")
|
318
|
+
|
319
|
+
if (current_start.day == previous_stop.day and current_start - previous_stop > interval * 60) then
|
320
|
+
|
321
|
+
if current_day != day then
|
322
|
+
day = current_day
|
323
|
+
else
|
324
|
+
current_day = "" # avoid printing the day if same as before
|
325
|
+
end
|
326
|
+
|
327
|
+
duration = (current_start - previous_stop) / 60
|
328
|
+
printf "%-18s %5s - %5s %02i:%02i %s\n",
|
329
|
+
current_day,
|
330
|
+
previous_stop.strftime("%H:%M"),
|
331
|
+
current_start.strftime("%H:%M"),
|
332
|
+
duration / 60, duration % 60,
|
333
|
+
"timeless clock --start '#{previous_stop}' --end '#{current_start}'"
|
334
|
+
|
335
|
+
end
|
336
|
+
previous_stop = Time.parse(entry[1])
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
##############################################################################
|
343
|
+
command :report do
|
344
|
+
banner "#{app_name} report"
|
345
|
+
description "Display a report of entries."
|
346
|
+
|
347
|
+
on :from=, "From date"
|
348
|
+
on :to=, "To date"
|
349
|
+
on :filter=, "With notes containing string"
|
350
|
+
|
351
|
+
run do |opts, args|
|
352
|
+
from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
|
353
|
+
to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
|
354
|
+
|
355
|
+
entries = Timeless::Storage.get(from, to, opts.to_hash[:filter])
|
356
|
+
# it could become a function of a reporting module
|
357
|
+
printf "%-18s %-5s - %-5s %-8s %-s\n", "Day", "Start", "End", "Duration", "Notes"
|
358
|
+
total = 0
|
359
|
+
day = ""
|
360
|
+
entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
|
361
|
+
current_day = Time.parse(entry[0]).strftime("%a %b %d, %Y")
|
362
|
+
if current_day != day then
|
363
|
+
day = current_day
|
364
|
+
else
|
365
|
+
current_day = "" # avoid printing the day if same as before
|
366
|
+
end
|
367
|
+
|
368
|
+
duration = (Time.parse(entry[1]) - Time.parse(entry[0])) / 60
|
369
|
+
total = total + duration
|
370
|
+
|
371
|
+
printf "%-18s %5s - %5s %02i:%02i %s\n",
|
372
|
+
current_day,
|
373
|
+
Time.parse(entry[0]).strftime("%H:%M"),
|
374
|
+
Time.parse(entry[1]).strftime("%H:%M"),
|
375
|
+
duration / 60, duration % 60,
|
376
|
+
entry[2]
|
377
|
+
end
|
378
|
+
puts "----------------------------------------------------------------------"
|
379
|
+
printf "Total %02i:%02i\n", total / 60, total % 60
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
# rescue
|
384
|
+
# puts "error: unknown option"
|
385
|
+
# puts "type '#{app_name} -h' for help"
|
386
|
+
# end
|
data/lib/timeless.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Timeless
|
2
|
+
module Pomodoro
|
3
|
+
require 'terminal-notifier'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
TITLE = 'Pomodoro'
|
7
|
+
MESSAGE = 'Finish'
|
8
|
+
SOUND = 'Glass'
|
9
|
+
|
10
|
+
WORKING = 25
|
11
|
+
WORKING_LONG = 50
|
12
|
+
|
13
|
+
BREAK = 5
|
14
|
+
BREAK_LONG = 15
|
15
|
+
|
16
|
+
INTERVAL = 0.3
|
17
|
+
|
18
|
+
def self.run_pomodoro_timer total_mins, notes=nil
|
19
|
+
start = Time.now
|
20
|
+
total_secs = total_mins * 60
|
21
|
+
s = 0
|
22
|
+
_s = nil
|
23
|
+
while true
|
24
|
+
s = (Time.now - start).to_i
|
25
|
+
r = total_secs - s
|
26
|
+
printf("\r%i:%02i", r / 60, r % 60) if s != _s
|
27
|
+
break if s >= total_secs
|
28
|
+
_s = s
|
29
|
+
sleep INTERVAL
|
30
|
+
end
|
31
|
+
print "\n"
|
32
|
+
|
33
|
+
TerminalNotifier.notify(MESSAGE, title: TITLE, sound: SOUND)
|
34
|
+
system "say 'Pomodoro complete. Take a break, now'"
|
35
|
+
|
36
|
+
[start, Time.now, notes]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#
|
2
|
+
# implement a stopwatch, which can be started and stopped
|
3
|
+
#
|
4
|
+
module Timeless
|
5
|
+
module Stopwatch
|
6
|
+
require 'date'
|
7
|
+
|
8
|
+
def self.start start=nil, notes=nil
|
9
|
+
store_start (start ? start : Time.now), (notes ? notes : "")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.stop start=nil, stop=nil, notes=nil
|
13
|
+
if not start
|
14
|
+
start, _ = get_start
|
15
|
+
end
|
16
|
+
|
17
|
+
if notes == nil or notes == ""
|
18
|
+
_, notes = get_start
|
19
|
+
end
|
20
|
+
|
21
|
+
if not stop
|
22
|
+
stop = Time.now
|
23
|
+
end
|
24
|
+
|
25
|
+
forget # forget started clock, if any
|
26
|
+
|
27
|
+
[start, stop, notes]
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Storing and retrieving start time and notes
|
32
|
+
#
|
33
|
+
# file containing the start time
|
34
|
+
|
35
|
+
START_FILENAME = File.expand_path("~/.timeless-tmp.csv")
|
36
|
+
|
37
|
+
def self.clocking?
|
38
|
+
File.exists?(START_FILENAME)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.get_start
|
42
|
+
if clocking?
|
43
|
+
array = CSV.read(START_FILENAME, "r")
|
44
|
+
[array[0][0], array[0][1]]
|
45
|
+
else
|
46
|
+
[nil, nil]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.store_start start, notes
|
51
|
+
CSV.open(START_FILENAME, "w") do |csv|
|
52
|
+
csv << [start, notes]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# forget the start clock
|
57
|
+
def self.forget
|
58
|
+
File.delete(START_FILENAME) if File.exists?(START_FILENAME)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Timeless
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
module Storage
|
5
|
+
TIMELESS_FILE=File.expand_path("~/.timeless.csv")
|
6
|
+
|
7
|
+
def self.store start, stop, notes
|
8
|
+
CSV.open(TIMELESS_FILE, "a") do |csv|
|
9
|
+
csv << [start, stop, notes]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.last
|
14
|
+
CSV.read(TIMELESS_FILE).last
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.get from_date=nil, to_date=nil, filter=nil
|
18
|
+
entries = CSV.read(TIMELESS_FILE)
|
19
|
+
entries.select do |x|
|
20
|
+
(from_date ? Time.parse(x[0]) >= from_date : true) and
|
21
|
+
(to_date ? Time.parse(x[1]) <= to_date : true) and
|
22
|
+
(filter ? x[2].include?(filter) : true)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# get all unique keys of a given type in file
|
27
|
+
def self.get_key key
|
28
|
+
entries = CSV.read(TIMELESS_FILE)
|
29
|
+
entries.map { |x| extract_kpv key, x[2] }.uniq.sort
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.export
|
33
|
+
entries = CSV.read(TIMELESS_FILE)
|
34
|
+
|
35
|
+
CSV { |csvout| csvout << ["Start Date", "Start Time", "End Date", "End Time", "Duration (s)", "Project", "Notes"] }
|
36
|
+
CSV do |csvout|
|
37
|
+
entries.each do |entry|
|
38
|
+
start = Time.parse(entry[0])
|
39
|
+
stop = Time.parse(entry[1])
|
40
|
+
|
41
|
+
start_date = start.strftime("%Y-%m-%d")
|
42
|
+
start_time = start.strftime("%H:%M:%S")
|
43
|
+
|
44
|
+
stop_date = stop.strftime("%Y-%m-%d")
|
45
|
+
stop_time = stop.strftime("%H:%M:%S")
|
46
|
+
|
47
|
+
duration = stop - start
|
48
|
+
|
49
|
+
# extract project and client, if present
|
50
|
+
project = extract_kpv "p", entry[2]
|
51
|
+
client = extract_kpv "c", entry[2]
|
52
|
+
|
53
|
+
notes = entry[2]
|
54
|
+
|
55
|
+
csvout << [start_date, start_time, stop_date, stop_time, duration, project, client, notes]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# return the value of a key-pair value in string or "" if not present
|
63
|
+
def self.extract_kpv key, string
|
64
|
+
match = string.match(/#{key}:([^ ]+)/)
|
65
|
+
match ? match[1] : ""
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
data/timeless.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'timeless/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "timeless"
|
8
|
+
spec.version = Timeless::VERSION
|
9
|
+
spec.authors = ["Adolfo Villafiorita"]
|
10
|
+
spec.email = ["adolfo.villafiorita@me.com"]
|
11
|
+
spec.summary = %q{Timeless is a simple command line time tracker}
|
12
|
+
spec.description = %q{Timeless comes with a pomodoro timer, with a standard stopwatch and a simple reporting command. Data is stored in csv and it can be easily imported into a spreadsheet.}
|
13
|
+
spec.homepage = "http://github.com/avillafiorita/timeless"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'slop', '~> 3.6.0', '>= 3.6.0'
|
25
|
+
spec.add_runtime_dependency 'chronic'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: timeless
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adolfo Villafiorita
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: slop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.6.0
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 3.6.0
|
51
|
+
type: :runtime
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 3.6.0
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 3.6.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: chronic
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
description: Timeless comes with a pomodoro timer, with a standard stopwatch and a
|
76
|
+
simple reporting command. Data is stored in csv and it can be easily imported into
|
77
|
+
a spreadsheet.
|
78
|
+
email:
|
79
|
+
- adolfo.villafiorita@me.com
|
80
|
+
executables:
|
81
|
+
- timeless
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- ".gitignore"
|
86
|
+
- Gemfile
|
87
|
+
- LICENSE.txt
|
88
|
+
- README.textile
|
89
|
+
- Rakefile
|
90
|
+
- bin/timeless
|
91
|
+
- lib/timeless.rb
|
92
|
+
- lib/timeless/pomodoro.rb
|
93
|
+
- lib/timeless/stopwatch.rb
|
94
|
+
- lib/timeless/storage.rb
|
95
|
+
- lib/timeless/version.rb
|
96
|
+
- timeless.gemspec
|
97
|
+
homepage: http://github.com/avillafiorita/timeless
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
metadata: {}
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project:
|
117
|
+
rubygems_version: 2.2.2
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: Timeless is a simple command line time tracker
|
121
|
+
test_files: []
|
122
|
+
has_rdoc:
|