timeless 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +3 -1
- data/LICENSE.txt +17 -18
- data/README.md +152 -0
- data/Rakefile +1 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/elisp/forms.el +22 -0
- data/exe/timeless +5 -0
- data/lib/timeless.rb +6 -5
- data/lib/timeless/cli/command_semantics.rb +352 -0
- data/lib/timeless/cli/command_syntax.rb +411 -0
- data/lib/timeless/pomodoro.rb +5 -8
- data/lib/timeless/storage.rb +27 -3
- data/lib/timeless/version.rb +1 -1
- data/timeless.gemspec +13 -7
- metadata +52 -26
- data/README.textile +0 -99
- data/bin/timeless +0 -386
@@ -0,0 +1,411 @@
|
|
1
|
+
require 'slop'
|
2
|
+
|
3
|
+
module Timeless
|
4
|
+
module CommandSyntax
|
5
|
+
APP_NAME="timeless"
|
6
|
+
|
7
|
+
# return a hash with all the commands and their options
|
8
|
+
def self.commands
|
9
|
+
h = Hash.new
|
10
|
+
self.methods.each do |method|
|
11
|
+
if method.to_s.include?("_opts") then
|
12
|
+
h = h.merge(eval(method.to_s))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
return h
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def self.version_opts
|
21
|
+
opts = Slop::Options.new
|
22
|
+
opts.banner = "version -- print version information"
|
23
|
+
help = <<EOS
|
24
|
+
NAME
|
25
|
+
#{opts.banner}
|
26
|
+
|
27
|
+
SYNOPSYS
|
28
|
+
#{opts.to_s}
|
29
|
+
|
30
|
+
DESCRIPTION
|
31
|
+
return version information
|
32
|
+
|
33
|
+
EXAMPLES
|
34
|
+
#{APP_NAME} version
|
35
|
+
#{APP_NAME} version #{VERSION}
|
36
|
+
EOS
|
37
|
+
return { :version => [opts, :version, help] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.console_opts
|
41
|
+
opts = Slop::Options.new
|
42
|
+
opts.banner = "console [options] -- Enter the console"
|
43
|
+
help = <<EOS
|
44
|
+
NAME
|
45
|
+
#{opts.banner}
|
46
|
+
|
47
|
+
SYNOPSYS
|
48
|
+
#{opts.to_s}
|
49
|
+
|
50
|
+
DESCRIPTION
|
51
|
+
Invoke a console, from which you can more easily run
|
52
|
+
#{APP_NAME} commands.
|
53
|
+
|
54
|
+
EXAMPLES
|
55
|
+
#{APP_NAME} console
|
56
|
+
#{APP_NAME}:000> command
|
57
|
+
....
|
58
|
+
#{APP_NAME}:001> another_command
|
59
|
+
...
|
60
|
+
#{APP_NAME}:002>
|
61
|
+
EOS
|
62
|
+
return { :console => [opts, :console, help] }
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.man_opts
|
66
|
+
opts = Slop::Options.new
|
67
|
+
opts.banner = "man -- print a manual page"
|
68
|
+
help = <<EOS
|
69
|
+
NAME
|
70
|
+
#{opts.banner}
|
71
|
+
|
72
|
+
SYNOPSYS
|
73
|
+
#{opts.to_s}
|
74
|
+
|
75
|
+
DESCRIPTION
|
76
|
+
Print the README file of this gem
|
77
|
+
|
78
|
+
EXAMPLES
|
79
|
+
#{APP_NAME} man
|
80
|
+
EOS
|
81
|
+
return { :man => [opts, :man, help] }
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.help_opts
|
85
|
+
opts = Slop::Options.new
|
86
|
+
opts.banner = "help [command] -- print usage string"
|
87
|
+
help = <<EOS
|
88
|
+
NAME
|
89
|
+
#{opts.banner}
|
90
|
+
|
91
|
+
SYNOPSYS
|
92
|
+
#{opts.to_s}
|
93
|
+
|
94
|
+
DESCRIPTION
|
95
|
+
Print help about a command
|
96
|
+
|
97
|
+
EXAMPLES
|
98
|
+
#{APP_NAME} help
|
99
|
+
#{APP_NAME} help process
|
100
|
+
EOS
|
101
|
+
return { :help => [opts, :help, help] }
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
#
|
106
|
+
# APP SPECIFIC COMMANDS START HERE
|
107
|
+
#
|
108
|
+
|
109
|
+
def self.pom_opts
|
110
|
+
opts = Slop::Options.new
|
111
|
+
opts.banner = "pom [options] [notes] -- run a pomodoro timer"
|
112
|
+
|
113
|
+
opts.boolean "--long", "Use a long timer (50 minutes)"
|
114
|
+
opts.integer "--duration", "Duration of the pomodoro timer, in minutes"
|
115
|
+
|
116
|
+
help = <<EOS
|
117
|
+
NAME
|
118
|
+
#{opts.banner}
|
119
|
+
|
120
|
+
SYNOPSYS
|
121
|
+
#{opts.to_s}
|
122
|
+
|
123
|
+
DESCRIPTION
|
124
|
+
Generate a sidecar file for the track(s) passed as input.
|
125
|
+
|
126
|
+
Once created a sidecar file usually does not need to be regenerated. You can
|
127
|
+
use the `--force` option to cause the sidecar file to be rewritten.
|
128
|
+
|
129
|
+
EXAMPLES
|
130
|
+
timeless pom
|
131
|
+
timeless pom --long
|
132
|
+
EOS
|
133
|
+
return { pom: [opts, :pom, help] }
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.start_opts
|
137
|
+
opts = Slop::Options.new
|
138
|
+
opts.banner = "start [--at chronic expression] [notes] -- start the timer"
|
139
|
+
|
140
|
+
opts.string "--at", "Manually set the start time"
|
141
|
+
|
142
|
+
help = <<EOS
|
143
|
+
NAME
|
144
|
+
#{opts.banner}
|
145
|
+
|
146
|
+
SYNOPSYS
|
147
|
+
#{opts.to_s}
|
148
|
+
|
149
|
+
DESCRIPTION
|
150
|
+
Start the timer: invoke this command when you want to record the actual
|
151
|
+
start of a given activity.
|
152
|
+
|
153
|
+
Use --at, if the start time is not "now". If you specify notes, they
|
154
|
+
will be used when you stop the activity.
|
155
|
+
|
156
|
+
Notes are written in free text, but you can use the following tags to mark certain
|
157
|
+
strings:
|
158
|
+
|
159
|
+
p: to specify a project
|
160
|
+
c: to specify a client
|
161
|
+
a: to specify the activity
|
162
|
+
|
163
|
+
So, for instance, you can write something like: "working on p:next_big_thing trying
|
164
|
+
to a:test function asked by c:john_smith"
|
165
|
+
|
166
|
+
Tags are used when reporting and exporting.
|
167
|
+
|
168
|
+
EXAMPLES
|
169
|
+
|
170
|
+
timeless start p:mobile_app c:john_smith a:testing
|
171
|
+
timeless start --at 'thirty minutes ago'
|
172
|
+
EOS
|
173
|
+
return { start: [opts, :start, help] }
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.forget_opts
|
177
|
+
opts = Slop::Options.new
|
178
|
+
opts.banner = "forget -- forget the last start command"
|
179
|
+
|
180
|
+
help = <<EOS
|
181
|
+
NAME
|
182
|
+
#{opts.banner}
|
183
|
+
|
184
|
+
SYNOPSYS
|
185
|
+
#{opts.to_s}
|
186
|
+
|
187
|
+
DESCRIPTION
|
188
|
+
Forget the active timer (if any).
|
189
|
+
|
190
|
+
Timeless protects your active timer (that is, the timer you start with "timeless start"
|
191
|
+
command) and will complain if you try to overwrite it (e.g., by using another start
|
192
|
+
command or if you specity the start with 'clock' or 'stop').
|
193
|
+
|
194
|
+
EXAMPLES
|
195
|
+
timeless forget
|
196
|
+
EOS
|
197
|
+
return { forget: [opts, :forget, help] }
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.stop_opts
|
201
|
+
opts = Slop::Options.new
|
202
|
+
opts.banner = "stop [--start 'chronic expression'] [--stop|end|at 'chronic expression'] [--last] -- stop the active timer"
|
203
|
+
|
204
|
+
opts.string "--start", "Start time, using chronic expression"
|
205
|
+
|
206
|
+
opts.string "--stop", "Stop time, using chronic expression"
|
207
|
+
opts.string "--at", "A synonym of stop"
|
208
|
+
opts.string "--end", "A synonym of stop"
|
209
|
+
|
210
|
+
opts.boolean "--last", "Reuse notes of last clock"
|
211
|
+
|
212
|
+
help = <<EOS
|
213
|
+
NAME
|
214
|
+
#{opts.banner}
|
215
|
+
|
216
|
+
SYNOPSYS
|
217
|
+
#{opts.to_s}
|
218
|
+
|
219
|
+
DESCRIPTION
|
220
|
+
Stop clocking, using notes, if specified. Require a start time if there is no active timer.
|
221
|
+
|
222
|
+
EXAMPLES
|
223
|
+
timeless stop
|
224
|
+
timeless stop working on p:project
|
225
|
+
timeless stop --at 'one hour ago' working on p:project
|
226
|
+
timeless stop --start 'three hours ago' forgot to start the timer when working on p:project
|
227
|
+
EOS
|
228
|
+
return { stop: [opts, :stop, help] }
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.clock_opts
|
232
|
+
opts = Slop::Options.new
|
233
|
+
opts.banner = "clock [--start 'chronic expression'] [--stop|end 'chronic expression'] [--last] -- stop the active timer"
|
234
|
+
|
235
|
+
opts.string "--start", "Start time, using chronic expression (use last stop time if not specified)"
|
236
|
+
opts.string "--stop", "Stop time, using chronic expression (use now, if not specified)"
|
237
|
+
opts.string "--end", "A synonym of stop"
|
238
|
+
|
239
|
+
opts.boolean "--last", "Reuse notes of last clock"
|
240
|
+
|
241
|
+
help = <<EOS
|
242
|
+
NAME
|
243
|
+
#{opts.banner}
|
244
|
+
|
245
|
+
SYNOPSYS
|
246
|
+
#{opts.to_s}
|
247
|
+
|
248
|
+
DESCRIPTION
|
249
|
+
Clock an entry. Use this command when you want to catchup with activities you have not clocked.
|
250
|
+
|
251
|
+
With no options, it will add an entry ending now and starting from the end of the last clocked
|
252
|
+
entry. You can change the start usin the --start option and the stop time with the --stop option.
|
253
|
+
|
254
|
+
EXAMPLES
|
255
|
+
timeless clock
|
256
|
+
timeless clock --start 'five hours ago' --stop 'three hours ago' quick break
|
257
|
+
timeless clock --end 'one hour ago' working on p:project
|
258
|
+
EOS
|
259
|
+
return { clock: [opts, :clock, help] }
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
def self.current_opts
|
264
|
+
opts = Slop::Options.new
|
265
|
+
opts.banner = "current -- show time elapsed since last start command"
|
266
|
+
help = <<EOS
|
267
|
+
NAME
|
268
|
+
#{opts.banner}
|
269
|
+
|
270
|
+
SYNOPSYS
|
271
|
+
#{opts.to_s}
|
272
|
+
|
273
|
+
DESCRIPTION
|
274
|
+
Show time elapsed since last start command.
|
275
|
+
|
276
|
+
EXAMPLES
|
277
|
+
timeless current
|
278
|
+
EOS
|
279
|
+
return { current: [opts, :current, help] }
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.last_opts
|
283
|
+
opts = Slop::Options.new
|
284
|
+
opts.banner = "last -- show last clocked entry"
|
285
|
+
help = <<EOS
|
286
|
+
NAME
|
287
|
+
#{opts.banner}
|
288
|
+
|
289
|
+
SYNOPSYS
|
290
|
+
#{opts.to_s}
|
291
|
+
|
292
|
+
DESCRIPTION
|
293
|
+
Show last clocked entry.
|
294
|
+
|
295
|
+
EXAMPLES
|
296
|
+
timeless last
|
297
|
+
EOS
|
298
|
+
return { last: [opts, :last, help] }
|
299
|
+
end
|
300
|
+
|
301
|
+
def self.list_opts
|
302
|
+
opts = Slop::Options.new
|
303
|
+
opts.banner = "list key -- list all values assigned to a given tag (e.g., list all projects)"
|
304
|
+
help = <<EOS
|
305
|
+
NAME
|
306
|
+
#{opts.banner}
|
307
|
+
|
308
|
+
SYNOPSYS
|
309
|
+
#{opts.to_s}
|
310
|
+
|
311
|
+
DESCRIPTION
|
312
|
+
List all values recorded for a given tag (e.g., list all projects).
|
313
|
+
|
314
|
+
EXAMPLES
|
315
|
+
timeless list p
|
316
|
+
EOS
|
317
|
+
return { list: [opts, :list, help] }
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.gaps_opts
|
321
|
+
opts = Slop::Options.new
|
322
|
+
opts.banner = "gaps -- report all gaps in you timesheets"
|
323
|
+
|
324
|
+
opts.string "--from", "Start date for listing gaps"
|
325
|
+
opts.string "--to", "End date for listing gaps"
|
326
|
+
opts.integer "--interval", "Minimum interval to report (in minutes)"
|
327
|
+
|
328
|
+
|
329
|
+
help = <<EOS
|
330
|
+
NAME
|
331
|
+
#{opts.banner}
|
332
|
+
|
333
|
+
SYNOPSYS
|
334
|
+
#{opts.to_s}
|
335
|
+
|
336
|
+
DESCRIPTION
|
337
|
+
List all gaps in the timesheets (periods you have not clocked).
|
338
|
+
|
339
|
+
Use --from and --to to limit your search.
|
340
|
+
|
341
|
+
Use --interval to report only gaps longer than a given interval.
|
342
|
+
|
343
|
+
EXAMPLES
|
344
|
+
timeless gaps --interval 10
|
345
|
+
EOS
|
346
|
+
return { gaps: [opts, :gaps, help] }
|
347
|
+
end
|
348
|
+
|
349
|
+
def self.statement_opts
|
350
|
+
opts = Slop::Options.new
|
351
|
+
opts.banner = "statement -- produce a list of clocked activities"
|
352
|
+
|
353
|
+
opts.string "--from", "Start date for listing gaps"
|
354
|
+
opts.string "--to", "End date for listing gaps"
|
355
|
+
opts.string "--filter", "Consider only entries whose notes contain this string"
|
356
|
+
|
357
|
+
|
358
|
+
help = <<EOS
|
359
|
+
NAME
|
360
|
+
#{opts.banner}
|
361
|
+
|
362
|
+
SYNOPSYS
|
363
|
+
#{opts.to_s}
|
364
|
+
|
365
|
+
DESCRIPTION
|
366
|
+
Produce a statement of clocked activities.
|
367
|
+
|
368
|
+
Use --from and --to to restrict to a given time period.
|
369
|
+
|
370
|
+
Use --filter to restrict entries whose notes match the filter criteria.
|
371
|
+
You can use --filter, for instance, to extract all entries related to
|
372
|
+
a project "project_1", using "p:project_1"
|
373
|
+
|
374
|
+
EXAMPLES
|
375
|
+
timeless statement --filter p:project_1
|
376
|
+
EOS
|
377
|
+
return { statement: [opts, :statement, help] }
|
378
|
+
end
|
379
|
+
|
380
|
+
def self.balance_opts
|
381
|
+
opts = Slop::Options.new
|
382
|
+
opts.banner = "balance -- produce a report of clocked activities"
|
383
|
+
|
384
|
+
opts.string "--from", "Start date for listing gaps"
|
385
|
+
opts.string "--to", "End date for listing gaps"
|
386
|
+
opts.string "--filter", "Consider only entries whose notes contain this string"
|
387
|
+
|
388
|
+
|
389
|
+
help = <<EOS
|
390
|
+
NAME
|
391
|
+
#{opts.banner}
|
392
|
+
|
393
|
+
SYNOPSYS
|
394
|
+
#{opts.to_s}
|
395
|
+
|
396
|
+
DESCRIPTION
|
397
|
+
Produce a report organized by keys of clocked activities.
|
398
|
+
|
399
|
+
Use --from and --to to restrict to a given time period.
|
400
|
+
|
401
|
+
Use --filter to restrict entries whose notes match the filter criteria.
|
402
|
+
You can use --filter, for instance, to extract all entries related to
|
403
|
+
a project "project_1", using "p:project_1"
|
404
|
+
|
405
|
+
EXAMPLES
|
406
|
+
timeless balance --filter p:project_1
|
407
|
+
EOS
|
408
|
+
return { balance: [opts, :balance, help] }
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
data/lib/timeless/pomodoro.rb
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
module Timeless
|
2
2
|
module Pomodoro
|
3
|
-
require '
|
3
|
+
require 'notiffany'
|
4
4
|
require 'date'
|
5
5
|
|
6
|
-
TITLE = 'Pomodoro'
|
7
|
-
MESSAGE = 'Finish'
|
8
|
-
SOUND = 'Glass'
|
9
|
-
|
10
6
|
WORKING = 25
|
11
7
|
WORKING_LONG = 50
|
12
8
|
|
@@ -29,9 +25,10 @@ module Timeless
|
|
29
25
|
sleep INTERVAL
|
30
26
|
end
|
31
27
|
print "\n"
|
32
|
-
|
33
|
-
|
34
|
-
|
28
|
+
|
29
|
+
notifier = Notiffany.connect(title: "Pomodoro Complete!")
|
30
|
+
notifier.notify("You clocked: #{total_mins} minute#{"s" if total_mins != 1}.\nYou deserve a break, now", image: :success)
|
31
|
+
notifier.disconnect # some plugins like TMux and TerminalTitle rely on this
|
35
32
|
|
36
33
|
[start, Time.now, notes]
|
37
34
|
end
|
data/lib/timeless/storage.rb
CHANGED
@@ -11,7 +11,7 @@ module Timeless
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.last
|
14
|
-
CSV.read(TIMELESS_FILE).last
|
14
|
+
CSV.read(TIMELESS_FILE).sort { |x, y| x[1] <=> y[1] }.last
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.get from_date=nil, to_date=nil, filter=nil
|
@@ -29,10 +29,33 @@ module Timeless
|
|
29
29
|
entries.map { |x| extract_kpv key, x[2] }.uniq.sort
|
30
30
|
end
|
31
31
|
|
32
|
+
def self.balance from, to, filter
|
33
|
+
entries = Timeless::Storage.get(from, to, filter)
|
34
|
+
|
35
|
+
hash = {"p" => {}, "c" => {}, "a" => {}}
|
36
|
+
|
37
|
+
entries.each do |entry|
|
38
|
+
start = Time.parse(entry[0])
|
39
|
+
stop = Time.parse(entry[1])
|
40
|
+
duration = (stop - start) / 60
|
41
|
+
|
42
|
+
project = extract_kpv "p", entry[2]
|
43
|
+
client = extract_kpv "c", entry[2]
|
44
|
+
activity = extract_kpv "a", entry[2]
|
45
|
+
|
46
|
+
hash["p"][project] = (hash["p"][project] || 0) + duration if project != ""
|
47
|
+
hash["c"][client] = (hash["c"][client] || 0) + duration if client != ""
|
48
|
+
hash["a"][activity] = (hash["a"][activity] || 0) + duration if activity != ""
|
49
|
+
|
50
|
+
end
|
51
|
+
hash
|
52
|
+
end
|
53
|
+
|
54
|
+
|
32
55
|
def self.export
|
33
56
|
entries = CSV.read(TIMELESS_FILE)
|
34
57
|
|
35
|
-
CSV { |csvout| csvout << ["Start Date", "Start Time", "End Date", "End Time", "Duration (s)", "Project", "Notes"] }
|
58
|
+
CSV { |csvout| csvout << ["Start Date", "Start Time", "End Date", "End Time", "Duration (s)", "Project", "Client", "Activity", "Notes"] }
|
36
59
|
CSV do |csvout|
|
37
60
|
entries.each do |entry|
|
38
61
|
start = Time.parse(entry[0])
|
@@ -49,10 +72,11 @@ module Timeless
|
|
49
72
|
# extract project and client, if present
|
50
73
|
project = extract_kpv "p", entry[2]
|
51
74
|
client = extract_kpv "c", entry[2]
|
75
|
+
activity = extract_kpv "a", entry[2]
|
52
76
|
|
53
77
|
notes = entry[2]
|
54
78
|
|
55
|
-
csvout << [start_date, start_time, stop_date, stop_time, duration, project, client, notes]
|
79
|
+
csvout << [start_date, start_time, stop_date, stop_time, duration, project, client, activity, notes]
|
56
80
|
end
|
57
81
|
end
|
58
82
|
end
|