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,45 @@
1
+
2
+ module TheFox
3
+ module Timr
4
+ module Helper
5
+
6
+ class TranslationHelper
7
+
8
+ # All methods in this block are static.
9
+ class << self
10
+
11
+ # Based on the number `n` return singular or plural of a given word.
12
+ #
13
+ # For example:
14
+ #
15
+ # ```
16
+ # pluralize(1, 'track', 'tracks')
17
+ # => "1 track"
18
+ # ```
19
+ #
20
+ # ```
21
+ # pluralize(2, 'track', 'tracks')
22
+ # => "2 tracks"
23
+ # ```
24
+ #
25
+ # ```
26
+ # pluralize(0, 'track', 'tracks')
27
+ # => "0 tracks"
28
+ # ```
29
+ def pluralize(n, singular, plural=nil)
30
+ if n == 1
31
+ "1 #{singular}"
32
+ elsif plural
33
+ "#{n} #{plural}"
34
+ else
35
+ "#{n} #{singular}s"
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end # class TranslationHelper
42
+
43
+ end # module Helper
44
+ end # module Timr
45
+ end # module TheFox
@@ -0,0 +1,287 @@
1
+
2
+ require 'time'
3
+ require 'yaml/store'
4
+ require 'uuid'
5
+ require 'digest/sha1'
6
+ require 'pathname'
7
+
8
+ module TheFox
9
+ module Timr
10
+
11
+ # See BasicModel for more details.
12
+ module Model
13
+
14
+ # Basic Class
15
+ #
16
+ # Models hold data and can be stored to YAML files. Except for [Tracks](rdoc-ref:Track). Tracks are stored to a Task file.
17
+ class BasicModel
18
+
19
+ include TheFox::Timr::Error
20
+
21
+ # When calling `save_to_file`, it will only write the file if `@has_changed` is `true`.
22
+ attr_accessor :has_changed
23
+
24
+ # Path to file.
25
+ attr_accessor :file_path
26
+
27
+ def initialize
28
+ # id 40 chars long.
29
+ id = Digest::SHA1.hexdigest(UUID.new.generate)
30
+
31
+ @meta = {
32
+ 'id' => id,
33
+ 'short_id' => id[0, 6],
34
+ 'created' => Time.now.utc.strftime(MODEL_DATETIME_FORMAT),
35
+ 'modified' => Time.now.utc.strftime(MODEL_DATETIME_FORMAT),
36
+ }
37
+ @data = nil
38
+ @has_changed = false
39
+ @file_path = nil
40
+ end
41
+
42
+ # Set ID.
43
+ def id=(id)
44
+ @meta['id'] = id
45
+
46
+ changed
47
+ end
48
+
49
+ # Get ID.
50
+ def id
51
+ @meta['id']
52
+ end
53
+
54
+ # Get Short ID. Only 6 chars long.
55
+ def short_id
56
+ @meta['id'][0, 6]
57
+ end
58
+
59
+ # Set created Time String.
60
+ def created=(created)
61
+ @meta['created'] = created
62
+ end
63
+
64
+ # Set modified Time String.
65
+ def modified=(modified)
66
+ @meta['modified'] = modified
67
+ end
68
+
69
+ # Mark an object as changed. Only changed objects are stored to files on save_to_file().
70
+ def changed
71
+ @meta['modified'] = Time.now.utc.strftime(MODEL_DATETIME_FORMAT)
72
+ @has_changed = true
73
+ end
74
+
75
+ # Load an entity into the current instance.
76
+ #
77
+ # If `path` is not given `@file_path` will be taken. If `@file_path` is also not given throw ModelError exception.
78
+ def load_from_file(path = nil)
79
+ load = pre_load_from_file
80
+
81
+ if path.nil?
82
+ path = @file_path
83
+ if path.nil?
84
+ raise ModelError, 'Path cannot be nil.'
85
+ end
86
+ else
87
+ @file_path = path
88
+ end
89
+
90
+ if load
91
+ content = YAML::load_file(path)
92
+ @meta = content['meta']
93
+ @data = content['data']
94
+ @has_changed = false
95
+ end
96
+
97
+ post_load_from_file
98
+ end
99
+
100
+ # Hook function for subclass called before `load_from_file` payload will be executed.
101
+ def pre_load_from_file
102
+ true
103
+ end
104
+
105
+ # Hook function for subclass called after `load_from_file` payload was executed.
106
+ #
107
+ # Subclasses can access `@meta` and `@data` to write values into instance variables, or to convert data to other formats.
108
+ #
109
+ # See `pre_save_to_file`.
110
+ def post_load_from_file
111
+ end
112
+
113
+ # Save an entity to a YAML file.
114
+ def save_to_file(path = nil, force = false)
115
+ store = pre_save_to_file
116
+
117
+ if path.nil?
118
+ path = @file_path
119
+ if path.nil?
120
+ raise ModelError, 'Path cannot be nil.'
121
+ end
122
+ else
123
+ @file_path = path
124
+ end
125
+
126
+ if force || (store && @has_changed)
127
+ @meta['modified'] = Time.now.utc.strftime(MODEL_DATETIME_FORMAT)
128
+
129
+ # Create underlying directories.
130
+ unless path.dirname.exist?
131
+ path.dirname.mkpath
132
+ end
133
+
134
+ store = YAML::Store.new(path)
135
+ store.transaction do
136
+ store['meta'] = @meta
137
+ store['data'] = @data
138
+ end
139
+
140
+ @has_changed = false
141
+ end
142
+
143
+ post_save_to_file
144
+ end
145
+
146
+ # Hook function for subclass called before `save_to_file` payload will be executed.
147
+ #
148
+ # Subclasses can modify `@meta` and `@data` in this method to store more informations to the meta Hash, or to convert data to other formats that can be better written to file.
149
+ #
150
+ # For example, it's probably better to convert a floating point number to a `%.2f` formatted String and convert it back to float on `post_load_from_file`.
151
+ # See Floating Point Math <http://0.30000000000000004.com>.
152
+ def pre_save_to_file
153
+ true
154
+ end
155
+
156
+ # Hook function for subclass called after `save_to_file` payload was executed.
157
+ def post_save_to_file
158
+ end
159
+
160
+ # Delete the file.
161
+ def delete_file(path = nil)
162
+ path ||= @file_path
163
+ if path.nil?
164
+ raise ModelError, 'Path cannot be nil.'
165
+ end
166
+
167
+ path.delete
168
+ end
169
+
170
+ # All methods in this block are static.
171
+ class << self
172
+
173
+ include TheFox::Timr::Error
174
+
175
+ # Converts an [SHA1](http://ruby-doc.org/stdlib-2.4.1/libdoc/digest/rdoc/Digest/SHA1.html) Hash into a path.
176
+ #
177
+ # Function IO:
178
+ #
179
+ # ```
180
+ # 3dd50a2b50eabc84022a23ad2c06d9bb6396f978 <- input
181
+ # 3d/d50a2b50eabc84022a23ad2c06d9bb6396f978
182
+ # 3d/d50a2b50eabc84022a23ad2c06d9bb6396f978
183
+ # 3d/d5/0a2b50eabc84022a23ad2c06d9bb6396f978
184
+ # 3d/d5/0a/2b50eabc84022a23ad2c06d9bb6396f978
185
+ # 3d/d5/0a/2b50eabc84022a23ad2c06d9bb6396f978.yml <- output
186
+ # ```
187
+ def create_path_by_id(base_path, id)
188
+ if base_path.is_a?(String)
189
+ base_path = Pathname.new(base_path)
190
+ end
191
+ unless id.is_a?(String)
192
+ raise IdError, "ID is not a String. #{id.class} given."
193
+ end
194
+ if id.length <= 6
195
+ raise IdError, "ID is too short for creating a path. Minimum length: 7"
196
+ end
197
+
198
+ path_s = '%s/%s/%s/%s.yml' % [id[0, 2], id[2, 2], id[4, 2], id[6..-1]]
199
+ Pathname.new(path_s).expand_path(base_path)
200
+ end
201
+
202
+ # Opposite of `find_file_by_id`.
203
+ #
204
+ # Function IO:
205
+ #
206
+ # ```
207
+ # 3d/d5/0a/2b50eabc84022a23ad2c06d9bb6396f978.yml <- input
208
+ # 3dd50a2b50eabc84022a23ad2c06d9bb6396f978 <- output
209
+ # ```
210
+ def get_id_from_path(base_path, path)
211
+ path.relative_path_from(base_path).to_s.gsub('/', '')[0..-5]
212
+ end
213
+
214
+ # Opposite of `get_id_from_path`.
215
+ def find_file_by_id(base_path, id)
216
+ if base_path.is_a?(String)
217
+ base_path = Pathname.new(base_path)
218
+ end
219
+ unless id.is_a?(String)
220
+ raise IdError, "ID '#{id}' is not a String. #{id.class} given."
221
+ end
222
+ if id.length < 4
223
+ raise IdError, "ID '#{id}' is too short for find. Minimum length: 4"
224
+ end
225
+
226
+ if id.length == 40
227
+ path = create_path_by_id(base_path, id)
228
+ else
229
+ # 12/34
230
+ search_path = '%s/%s' % [id[0, 2], id[2, 2]]
231
+ if id.length >= 5
232
+ # 12/34/5
233
+ search_path << '/' << id[4]
234
+
235
+ if id.length >= 6
236
+ # 12/34/56
237
+ search_path << id[5]
238
+
239
+ if id.length >= 7
240
+ # 12/34/56/xxxxxx
241
+ search_path << '/' << id[6..-1]
242
+ end
243
+ end
244
+ end
245
+ search_path << '*'
246
+
247
+ path = nil
248
+ base_path.find.each do |file|
249
+ # Filter all directories.
250
+ unless file.file?
251
+ next
252
+ end
253
+
254
+ # Filter all non-yaml files.
255
+ unless file.basename.fnmatch('*.yml')
256
+ next
257
+ end
258
+
259
+ rel_path = file.relative_path_from(base_path)
260
+ unless rel_path.fnmatch(search_path)
261
+ next
262
+ end
263
+
264
+ if path
265
+ raise ModelError.new(id), "ID '#{id}' is not a unique identifier."
266
+ else
267
+ path = file
268
+
269
+ # Do not break the loop here.
270
+ # Iterate all keys to make sure the ID is unique.
271
+ end
272
+ end
273
+ end
274
+
275
+ if path && path.exist?
276
+ return path
277
+ end
278
+ raise ModelError, "Could not find a file for ID '#{id}' at #{base_path}."
279
+ end
280
+
281
+ end
282
+
283
+ end # class Model
284
+
285
+ end # module Model
286
+ end # module Timr
287
+ end #module TheFox
@@ -0,0 +1,48 @@
1
+
2
+ module TheFox
3
+ module Timr
4
+ module Model
5
+
6
+ class Config < BasicModel
7
+
8
+ # The version String which the file was created with.
9
+ attr_accessor :inital_version
10
+
11
+ # The version of the previous Timr run.
12
+ attr_accessor :last_used_version
13
+
14
+ def initialize
15
+ super()
16
+
17
+ @inital_version = nil
18
+ @last_used_version = nil
19
+ end
20
+
21
+ private
22
+
23
+ # BasicModel Hook
24
+ def pre_save_to_file
25
+ @data = {
26
+ 'inital_version' => @inital_version || VERSION,
27
+ 'last_used_version' => VERSION,
28
+ }
29
+ end
30
+
31
+ # BasicModel Hook
32
+ def post_load_from_file
33
+ @inital_version = @data.fetch('inital_version', VERSION)
34
+ @last_used_version = @data.fetch('last_used_version', VERSION)
35
+
36
+ if @last_used_version != VERSION
37
+ @last_used_version = VERSION
38
+
39
+ # Mark Config as changed.
40
+ changed
41
+ end
42
+ end
43
+
44
+ end # class Config
45
+
46
+ end # module Model
47
+ end # module Timr
48
+ end #module TheFox
@@ -0,0 +1,84 @@
1
+
2
+ module TheFox
3
+ module Timr
4
+ module Model
5
+
6
+ # Holds the Task IDs for each Foreign Task ID.
7
+ #
8
+ # Basically it's a Hash with structure:
9
+ #
10
+ # ```
11
+ # foreign_id => task_id
12
+ # ```
13
+ class ForeignIdDb < BasicModel
14
+
15
+ include TheFox::Timr::Error
16
+
17
+ # Holds all Foreign Task IDs.
18
+ #
19
+ # ```
20
+ # @foreign_ids[foreign_id] = task.id
21
+ # ```
22
+ attr_reader :foreign_ids
23
+
24
+ def initialize
25
+ super()
26
+
27
+ @foreign_ids = Hash.new
28
+ end
29
+
30
+ def add_task(task, foreign_id)
31
+ task_id = task.id
32
+ foreign_id = foreign_id.strip # needs clone
33
+
34
+ if @foreign_ids[foreign_id]
35
+ if @foreign_ids[foreign_id] == task_id
36
+ # Foreign ID has already a match.
37
+ false
38
+ else
39
+ raise ForeignIdError, "Want to add Foreign ID '#{foreign_id}' for Task '#{task.short_id}', but Foreign ID '#{foreign_id}' is already used by Task '#{@foreign_ids[foreign_id]}'."
40
+ end
41
+ else
42
+ @foreign_ids[foreign_id] = task_id
43
+
44
+ task.foreign_id = foreign_id
45
+
46
+ # Mark ForeignIdDb as changed.
47
+ changed
48
+
49
+ true
50
+ end
51
+ end
52
+
53
+ def get_task_id(foreign_id)
54
+ foreign_id = foreign_id.strip # needs clone
55
+
56
+ @foreign_ids[foreign_id]
57
+ end
58
+
59
+ def remove_task(task)
60
+ @foreign_ids.delete(task.foreign_id)
61
+
62
+ task.foreign_id = nil
63
+
64
+ # Mark ForeignIdDb as changed.
65
+ changed
66
+ end
67
+
68
+ private
69
+
70
+ # BasicModel Hook
71
+ def pre_save_to_file
72
+ @data = @foreign_ids
73
+ end
74
+
75
+ # BasicModel Hook
76
+ def post_load_from_file
77
+ @foreign_ids = @data
78
+ end
79
+
80
+ end # class ForeignIdDb
81
+
82
+ end # module Model
83
+ end # module Timr
84
+ end #module TheFox