timr 0.3.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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.ackrc +9 -0
  3. data/.editorconfig +1 -0
  4. data/.env.example +7 -0
  5. data/.github/CONTRIBUTING.md +32 -0
  6. data/.github/ISSUE_TEMPLATE.md +13 -0
  7. data/.gitignore +8 -2
  8. data/.rdoc_options +21 -0
  9. data/.travis.yml +10 -7
  10. data/Gemfile +8 -0
  11. data/README.md +216 -3
  12. data/bin/.gitignore +2 -0
  13. data/bin/README.md +17 -0
  14. data/bin/build.sh +14 -0
  15. data/bin/build_api.sh +14 -0
  16. data/bin/build_coverage.sh +23 -0
  17. data/bin/build_info.sh +27 -0
  18. data/bin/build_man.sh +41 -0
  19. data/bin/clean.sh +14 -0
  20. data/bin/dev_setup.sh +19 -0
  21. data/bin/install.sh +49 -0
  22. data/bin/publish +38 -0
  23. data/bin/release.sh +35 -0
  24. data/bin/test.sh +19 -0
  25. data/bin/timr +20 -40
  26. data/bin/timr_bash_completion.sh +337 -0
  27. data/bin/uninstall.sh +24 -0
  28. data/lib/timr.rb +36 -8
  29. data/lib/timr/command/basic_command.rb +170 -0
  30. data/lib/timr/command/continue_command.rb +86 -0
  31. data/lib/timr/command/help_command.rb +137 -0
  32. data/lib/timr/command/log_command.rb +297 -0
  33. data/lib/timr/command/pause_command.rb +89 -0
  34. data/lib/timr/command/pop_command.rb +176 -0
  35. data/lib/timr/command/push_command.rb +141 -0
  36. data/lib/timr/command/report_command.rb +689 -0
  37. data/lib/timr/command/start_command.rb +172 -0
  38. data/lib/timr/command/status_command.rb +198 -0
  39. data/lib/timr/command/stop_command.rb +127 -0
  40. data/lib/timr/command/task_command.rb +318 -0
  41. data/lib/timr/command/track_command.rb +381 -0
  42. data/lib/timr/command/version_command.rb +18 -0
  43. data/lib/timr/duration.rb +159 -0
  44. data/lib/timr/exception/timr_error.rb +113 -0
  45. data/lib/timr/ext/time.rb +12 -0
  46. data/lib/timr/helper/datetime_helper.rb +128 -0
  47. data/lib/timr/helper/terminal_helper.rb +58 -0
  48. data/lib/timr/helper/translation_helper.rb +45 -0
  49. data/lib/timr/model/basic_model.rb +287 -0
  50. data/lib/timr/model/config.rb +48 -0
  51. data/lib/timr/model/foreign_id_db.rb +84 -0
  52. data/lib/timr/model/stack.rb +161 -0
  53. data/lib/timr/model/task.rb +1039 -0
  54. data/lib/timr/model/track.rb +589 -0
  55. data/lib/timr/progressbar.rb +41 -0
  56. data/lib/timr/simple_opt_parser.rb +230 -0
  57. data/lib/timr/status.rb +70 -0
  58. data/lib/timr/table.rb +88 -0
  59. data/lib/timr/timr.rb +500 -558
  60. data/lib/timr/version.rb +4 -15
  61. data/man/.gitignore +2 -0
  62. data/man/_footer +3 -0
  63. data/man/timr-continue.1 +48 -0
  64. data/man/timr-continue.1.ronn +39 -0
  65. data/man/timr-ftime.7 +77 -0
  66. data/man/timr-ftime.7.ronn +57 -0
  67. data/man/timr-log.1 +109 -0
  68. data/man/timr-log.1.ronn +87 -0
  69. data/man/timr-pause.1 +56 -0
  70. data/man/timr-pause.1.ronn +45 -0
  71. data/man/timr-pop.1 +66 -0
  72. data/man/timr-pop.1.ronn +53 -0
  73. data/man/timr-push.1 +25 -0
  74. data/man/timr-push.1.ronn +20 -0
  75. data/man/timr-report.1 +228 -0
  76. data/man/timr-report.1.ronn +193 -0
  77. data/man/timr-start.1 +100 -0
  78. data/man/timr-start.1.ronn +82 -0
  79. data/man/timr-status.1 +53 -0
  80. data/man/timr-status.1.ronn +42 -0
  81. data/man/timr-stop.1 +75 -0
  82. data/man/timr-stop.1.ronn +60 -0
  83. data/man/timr-task.1 +147 -0
  84. data/man/timr-task.1.ronn +115 -0
  85. data/man/timr-track.1 +109 -0
  86. data/man/timr-track.1.ronn +89 -0
  87. data/man/timr.1 +119 -0
  88. data/man/timr.1.ronn +68 -0
  89. data/timr.gemspec +18 -3
  90. data/timr.sublime-project +20 -1
  91. metadata +142 -23
  92. data/Makefile +0 -12
  93. data/Makefile.common +0 -56
  94. data/lib/timr/stack.rb +0 -81
  95. data/lib/timr/task.rb +0 -258
  96. data/lib/timr/track.rb +0 -167
  97. data/lib/timr/window.rb +0 -259
  98. data/lib/timr/window_help.rb +0 -41
  99. data/lib/timr/window_tasks.rb +0 -30
  100. data/lib/timr/window_test.rb +0 -20
  101. data/lib/timr/window_timeline.rb +0 -33
  102. data/tests/tc_stack.rb +0 -121
  103. data/tests/tc_task.rb +0 -190
  104. data/tests/tc_track.rb +0 -144
  105. data/tests/tc_window.rb +0 -428
  106. data/tests/ts_all.rb +0 -6
@@ -0,0 +1,589 @@
1
+
2
+ require 'time'
3
+
4
+ module TheFox
5
+ module Timr
6
+ module Model
7
+
8
+ class Track < BasicModel
9
+
10
+ include TheFox::Timr::Helper
11
+ include TheFox::Timr::Error
12
+
13
+ # Parent Task instance
14
+ attr_accessor :task
15
+
16
+ # Track Message. What have you done?
17
+ attr_reader :message
18
+
19
+ # Is this even in use? ;D
20
+ attr_accessor :paused
21
+
22
+ attr_reader :is_billed
23
+
24
+ def initialize
25
+ super()
26
+
27
+ @task = nil
28
+
29
+ @begin_datetime = nil
30
+ @end_datetime = nil
31
+ @is_billed = false
32
+ @message = nil
33
+ @paused = false
34
+ end
35
+
36
+ # Set begin_datetime.
37
+ def begin_datetime=(begin_datetime)
38
+ case begin_datetime
39
+ when String
40
+ begin_datetime = Time.parse(begin_datetime)
41
+ when Time
42
+ # OK
43
+ else
44
+ raise TrackError, "begin_datetime needs to be a String or Time, #{begin_datetime.class} given."
45
+ end
46
+
47
+ if @end_datetime && begin_datetime >= @end_datetime
48
+ raise TrackError, 'begin_datetime must be lesser than end_datetime.'
49
+ end
50
+
51
+ @begin_datetime = begin_datetime
52
+
53
+ # Mark Track as changed.
54
+ changed
55
+ end
56
+
57
+ # Get begin_datetime.
58
+ #
59
+ # Options:
60
+ #
61
+ # - `:from` (Time)
62
+ # See documentation about `:to` on `end_datetime()`.
63
+ def begin_datetime(options = Hash.new)
64
+ from_opt = options.fetch(:from, nil)
65
+
66
+ if @begin_datetime
67
+ if from_opt && from_opt > @begin_datetime
68
+ bdt = from_opt
69
+ else
70
+ bdt = @begin_datetime
71
+ end
72
+ bdt.localtime
73
+ end
74
+ end
75
+
76
+ # Get begin_datetime String.
77
+ #
78
+ # Options:
79
+ #
80
+ # - `:format` (String)
81
+ def begin_datetime_s(options = Hash.new)
82
+ format_opt = options.fetch(:format, HUMAN_DATETIME_FOMRAT)
83
+
84
+ bdt = begin_datetime(options)
85
+ if bdt
86
+ bdt.strftime(format_opt)
87
+ else
88
+ '---'
89
+ end
90
+ end
91
+
92
+ # Set end_datetime.
93
+ def end_datetime=(end_datetime)
94
+ if !@begin_datetime
95
+ raise TrackError, 'end_datetime cannot be set until begin_datetime is set.'
96
+ end
97
+
98
+ case end_datetime
99
+ when String
100
+ end_datetime = Time.parse(end_datetime)
101
+ when Time
102
+ # OK
103
+ else
104
+ raise TrackError, "end_datetime needs to be a String or Time, #{end_datetime.class} given."
105
+ end
106
+
107
+ if end_datetime <= @begin_datetime
108
+ raise TrackError, 'end_datetime must be greater than begin_datetime.'
109
+ end
110
+
111
+ @end_datetime = end_datetime
112
+
113
+ # Mark Track as changed.
114
+ changed
115
+ end
116
+
117
+ # Get end_datetime.
118
+ #
119
+ # Options:
120
+ #
121
+ # - `:to` (Time)
122
+ # This limits `@end_datetime`. If `:to` > `@end_datetime` it returns the
123
+ # original `@end_datetime`. Otherwise it will return `:to`. The same applies for
124
+ # `:from` on `begin_datetime()` but just the other way round.
125
+ def end_datetime(options = Hash.new)
126
+ to_opt = options.fetch(:to, nil)
127
+
128
+ if @end_datetime
129
+ if to_opt && to_opt < @end_datetime
130
+ edt = to_opt
131
+ else
132
+ edt = @end_datetime
133
+ end
134
+ edt.localtime
135
+ end
136
+ end
137
+
138
+ # Get end_datetime String.
139
+ #
140
+ # Options:
141
+ #
142
+ # - `:format` (String)
143
+ def end_datetime_s(options = Hash.new)
144
+ format_opt = options.fetch(:format, HUMAN_DATETIME_FOMRAT)
145
+
146
+ edt = end_datetime(options)
147
+ if edt
148
+ edt.strftime(format_opt)
149
+ else
150
+ '---'
151
+ end
152
+ end
153
+
154
+ # Set message.
155
+ def message=(message)
156
+ @message = message
157
+
158
+ # Mark Track as changed.
159
+ changed
160
+ end
161
+
162
+ # Start this Track. A Track cannot be restarted because it's the smallest time unit.
163
+ def start(options = Hash.new)
164
+ message_opt = options.fetch(:message, nil)
165
+
166
+ if @begin_datetime
167
+ raise TrackError, 'Cannot restart Track. Use dup() on this instance or create a new instance by using Track.new().'
168
+ end
169
+
170
+ @begin_datetime = DateTimeHelper.get_datetime_from_options(options)
171
+
172
+ if message_opt
173
+ @message = message_opt
174
+ end
175
+ end
176
+
177
+ # Stop this Track.
178
+ #
179
+ # Options:
180
+ #
181
+ # - `:start_date`
182
+ # - `:start_time`
183
+ # - `:end_date`, `:date`
184
+ # - `:end_time`, `:time`
185
+ # - `:message` (String)
186
+ def stop(options = Hash.new)
187
+ start_date_opt = options.fetch(:start_date, nil)
188
+ start_time_opt = options.fetch(:start_time, nil)
189
+ end_date_opt = options.fetch(:end_date, options.fetch(:date, nil))
190
+ end_time_opt = options.fetch(:end_time, options.fetch(:time, nil))
191
+ message_opt = options.fetch(:message, nil)
192
+ # paused_opt = options.fetch(:paused, false)
193
+
194
+ # Set Start DateTime
195
+ if start_date_opt || start_time_opt
196
+ begin_options = {
197
+ :date => start_date_opt,
198
+ :time => start_time_opt,
199
+ }
200
+ @begin_datetime = DateTimeHelper.get_datetime_from_options(begin_options)
201
+ end
202
+
203
+ # Set End DateTime
204
+ end_options = {
205
+ :date => end_date_opt,
206
+ :time => end_time_opt,
207
+ }
208
+ @end_datetime = DateTimeHelper.get_datetime_from_options(end_options)
209
+
210
+ if message_opt
211
+ @message = message_opt
212
+ end
213
+
214
+ # @paused = paused_opt
215
+
216
+ # Mark Track as changed.
217
+ changed
218
+ end
219
+
220
+ # Cacluates the secondes between begin and end datetime and returns a new Duration instance.
221
+ #
222
+ # Options:
223
+ #
224
+ # - `:from` (Time), `:to` (Time)
225
+ # Limit the begin and end datetimes to a specific range.
226
+ def duration(options = Hash.new)
227
+ from_opt = options.fetch(:from, nil)
228
+ to_opt = options.fetch(:to, nil)
229
+
230
+ if @begin_datetime
231
+ bdt = @begin_datetime.utc
232
+ end
233
+ if @end_datetime
234
+ edt = @end_datetime.utc
235
+ else
236
+ edt = Time.now.utc
237
+ end
238
+
239
+ # Cut Start
240
+ if from_opt && bdt && from_opt > bdt
241
+ bdt = from_opt.utc
242
+ end
243
+
244
+ # Cut End
245
+ if to_opt && edt && to_opt < edt
246
+ edt = to_opt.utc
247
+ end
248
+
249
+ seconds = 0
250
+ if bdt && edt
251
+ if bdt < edt
252
+ seconds = (edt - bdt).to_i
253
+ end
254
+ end
255
+
256
+ Duration.new(seconds)
257
+ end
258
+
259
+ # Alias method.
260
+ def billed_duration(options = Hash.new)
261
+ if self.is_billed
262
+ duration(options)
263
+ else
264
+ Duration.new(0)
265
+ end
266
+ end
267
+
268
+ # Alias method.
269
+ def unbilled_duration(options = Hash.new)
270
+ if !self.is_billed
271
+ duration(options)
272
+ else
273
+ Duration.new(0)
274
+ end
275
+ end
276
+
277
+ # When begin_datetime is `2017-01-01 01:15`
278
+ # and end_datetime is `2017-01-03 02:17`
279
+ # this function returns
280
+ #
281
+ # ```
282
+ # [
283
+ # Date.new(2017, 1, 1),
284
+ # Date.new(2017, 1, 2),
285
+ # Date.new(2017, 1, 3),
286
+ # ]
287
+ # ```
288
+ def days
289
+ begin_date = @begin_datetime.to_date
290
+ end_date = @end_datetime.to_date
291
+
292
+ begin_date.upto(end_date)
293
+ end
294
+
295
+ # Evaluates the Short Status and returns a new Status instance.
296
+ def status
297
+ if @begin_datetime.nil?
298
+ short_status = '-' # not started
299
+ elsif @end_datetime.nil?
300
+ short_status = 'R' # running
301
+ elsif @end_datetime
302
+ if @paused
303
+ # It's actually stopped but with an additional flag.
304
+ short_status = 'P' # paused
305
+ else
306
+ short_status = 'S' # stopped
307
+ end
308
+ else
309
+ short_status = 'U' # unknown
310
+ end
311
+
312
+ Status.new(short_status)
313
+ end
314
+
315
+ # Is the Track stopped?
316
+ def stopped?
317
+ status.short_status == 'S' # stopped
318
+ end
319
+
320
+ # Title generated from message. If the message has multiple lines only the first
321
+ # line will be taken to create the title.
322
+ #
323
+ # `max_length` can be used to define a maximum length. Three dots `...` will be appended
324
+ # at the end if the title is longer than `max_length`.
325
+ def title(max_length = nil)
326
+ unless @message
327
+ return
328
+ end
329
+
330
+ msg = @message.split("\n\n").first.split("\n").first
331
+
332
+ if max_length && msg.length > max_length + 2
333
+ msg = msg[0, max_length] << '...'
334
+ end
335
+ msg
336
+ end
337
+
338
+ # Title Alias
339
+ def name(max_length = nil)
340
+ title(max_length)
341
+ end
342
+
343
+ # Set is_billed.
344
+ def is_billed=(is_billed)
345
+ @is_billed = is_billed
346
+
347
+ # Mark Track as changed.
348
+ changed
349
+ end
350
+
351
+ # When the Track is marked as changed it needs to mark the Task as changed.
352
+ #
353
+ # A single Track cannot be stored to a file. Tracks are assiged to a Task and are stored to the Task file.
354
+ def changed
355
+ super()
356
+
357
+ if @task
358
+ @task.changed
359
+ end
360
+ end
361
+
362
+ # Alias for Task. A Track cannot saved to a file. Only the whole Task.
363
+ def save_to_file(path = nil, force = false)
364
+ if @task
365
+ @task.save_to_file(path, force)
366
+ end
367
+ end
368
+
369
+ # Duplicate this Track using the same Message. This is used almost by every Command.
370
+ # Start, Continue, Push, etc.
371
+ def dup
372
+ track = Track.new
373
+ track.task = @task
374
+ track.message = @message.clone
375
+ track
376
+ end
377
+
378
+ # Removes itself from parent Task.
379
+ def remove
380
+ if @task
381
+ @task.remove_track(self)
382
+ else
383
+ false
384
+ end
385
+ end
386
+
387
+ # To String
388
+ def to_s
389
+ "Track_#{short_id}"
390
+ end
391
+
392
+ # To Hash
393
+ def to_h
394
+ h = {
395
+ 'id' => @meta['id'],
396
+ 'short_id' => short_id, # Not used.
397
+ 'created' => @meta['created'],
398
+ 'modified' => @meta['modified'],
399
+ 'is_billed' => @is_billed,
400
+ 'message' => @message,
401
+ }
402
+ if @begin_datetime
403
+ h['begin_datetime'] = @begin_datetime.utc.strftime(MODEL_DATETIME_FORMAT)
404
+ end
405
+ if @end_datetime
406
+ h['end_datetime'] = @end_datetime.utc.strftime(MODEL_DATETIME_FORMAT)
407
+ end
408
+ h
409
+ end
410
+
411
+ # Used to print informations to STDOUT.
412
+ def to_compact_str
413
+ to_compact_array.join("\n")
414
+ end
415
+
416
+ # Used to print informations to STDOUT.
417
+ def to_compact_array
418
+ to_ax = Array.new
419
+
420
+ if @task
421
+ to_ax.concat(@task.to_track_array)
422
+ end
423
+
424
+ to_ax << 'Track: %s %s' % [self.short_id, self.title]
425
+
426
+ # if self.title
427
+ # to_ax << 'Title: %s' % [self.title]
428
+ # end
429
+ if self.begin_datetime
430
+ to_ax << 'Start: %s' % [self.begin_datetime_s]
431
+ end
432
+ if self.end_datetime
433
+ to_ax << 'End: %s' % [self.end_datetime_s]
434
+ end
435
+
436
+ if self.duration && self.duration.to_i > 0
437
+ to_ax << 'Duration: %s' % [self.duration.to_human]
438
+ end
439
+
440
+ to_ax << 'Status: %s' % [self.status.colorized]
441
+
442
+ # if self.message
443
+ # to_ax << 'Message: %s' % [self.message]
444
+ # end
445
+
446
+ to_ax
447
+ end
448
+
449
+ # Used to print informations to STDOUT.
450
+ def to_detailed_str(options = Hash.new)
451
+ to_detailed_array(options).join("\n")
452
+ end
453
+
454
+ # Used to print informations to STDOUT.
455
+ #
456
+ # Options:
457
+ #
458
+ # - `:full_id` (Boolean) Show full Task and Track IDs.
459
+ def to_detailed_array(options = Hash.new)
460
+ full_id_opt = options.fetch(:full_id, false) # @TODO full_id unit test
461
+
462
+ to_ax = Array.new
463
+
464
+ if @task
465
+ to_ax.concat(@task.to_track_array(options))
466
+ end
467
+
468
+ if full_id_opt
469
+ to_ax << 'Track: %s' % [self.id]
470
+ else
471
+ to_ax << 'Track: %s' % [self.short_id]
472
+ end
473
+
474
+ if self.begin_datetime
475
+ to_ax << 'Start: %s' % [self.begin_datetime_s]
476
+ end
477
+ if self.end_datetime
478
+ to_ax << 'End: %s' % [self.end_datetime_s]
479
+ end
480
+
481
+ if self.duration && self.duration.to_i > 0
482
+ duration_human = self.duration.to_human
483
+ to_ax << 'Duration: %s' % [duration_human]
484
+
485
+ duration_man_days = self.duration.to_man_days
486
+ if duration_human != duration_man_days
487
+ to_ax << 'Man Unit: %s' % [duration_man_days]
488
+ end
489
+ end
490
+
491
+ to_ax << 'Billed: %s' % [self.is_billed ? 'Yes' : 'No']
492
+ to_ax << 'Status: %s' % [self.status.colorized]
493
+
494
+ if self.message
495
+ to_ax << 'Message: %s' % [self.message]
496
+ end
497
+
498
+ to_ax
499
+ end
500
+
501
+ # Are two Tracks equal?
502
+ #
503
+ # Uses ID for comparision.
504
+ def eql?(track)
505
+ unless track.is_a?(Track)
506
+ raise TrackError, "track variable must be a Track instance. #{track.class} given."
507
+ end
508
+
509
+ self.id == track.id
510
+ end
511
+
512
+ def inspect
513
+ "#<Track #{short_id}>"
514
+ end
515
+
516
+ # All methods in this block are static.
517
+ class << self
518
+
519
+ # Create a new Track instance from a Hash.
520
+ def create_track_from_hash(hash)
521
+ unless hash.is_a?(Hash)
522
+ raise TrackError, "hash variable must be a Hash instance. #{hash.class} given."
523
+ end
524
+
525
+ track = Track.new
526
+ if hash['id']
527
+ track.id = hash['id']
528
+ end
529
+ if hash['created']
530
+ track.created = hash['created']
531
+ end
532
+ if hash['modified']
533
+ track.modified = hash['modified']
534
+ end
535
+ if hash['is_billed']
536
+ track.is_billed = hash['is_billed']
537
+ end
538
+ if hash['message']
539
+ track.message = hash['message']
540
+ end
541
+ if hash['begin_datetime']
542
+ track.begin_datetime = hash['begin_datetime']
543
+ end
544
+ if hash['end_datetime']
545
+ track.end_datetime = hash['end_datetime']
546
+ end
547
+ track.has_changed = false
548
+ track
549
+ end
550
+
551
+ # This is really bad. Do not use this.
552
+ def find_track_by_id(base_path, track_id)
553
+ found_track = nil
554
+
555
+ # Iterate all files.
556
+ base_path.find.each do |file|
557
+ # Filter all directories.
558
+ unless file.file?
559
+ next
560
+ end
561
+
562
+ # Filter all non-yaml files.
563
+ unless file.basename.fnmatch('*.yml')
564
+ next
565
+ end
566
+
567
+ task = Task.load_task_from_file(file)
568
+ tmp_track = task.find_track_by_id(track_id)
569
+ if tmp_track
570
+ if found_track
571
+ raise TrackError, "Track ID '#{track_id}' is not a unique identifier."
572
+ else
573
+ found_track = tmp_track
574
+
575
+ # Do not break the loop here.
576
+ end
577
+ end
578
+ end
579
+
580
+ found_track
581
+ end
582
+
583
+ end
584
+
585
+ end # class Track
586
+
587
+ end # module Model
588
+ end # module Timr
589
+ end #module TheFox