timeless 0.1.0 → 0.4.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 +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
|