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.
- checksums.yaml +4 -4
- data/.ackrc +9 -0
- data/.editorconfig +1 -0
- data/.env.example +7 -0
- data/.github/CONTRIBUTING.md +32 -0
- data/.github/ISSUE_TEMPLATE.md +13 -0
- data/.gitignore +8 -2
- data/.rdoc_options +21 -0
- data/.travis.yml +10 -7
- data/Gemfile +8 -0
- data/README.md +216 -3
- data/bin/.gitignore +2 -0
- data/bin/README.md +17 -0
- data/bin/build.sh +14 -0
- data/bin/build_api.sh +14 -0
- data/bin/build_coverage.sh +23 -0
- data/bin/build_info.sh +27 -0
- data/bin/build_man.sh +41 -0
- data/bin/clean.sh +14 -0
- data/bin/dev_setup.sh +19 -0
- data/bin/install.sh +49 -0
- data/bin/publish +38 -0
- data/bin/release.sh +35 -0
- data/bin/test.sh +19 -0
- data/bin/timr +20 -40
- data/bin/timr_bash_completion.sh +337 -0
- data/bin/uninstall.sh +24 -0
- data/lib/timr.rb +36 -8
- data/lib/timr/command/basic_command.rb +170 -0
- data/lib/timr/command/continue_command.rb +86 -0
- data/lib/timr/command/help_command.rb +137 -0
- data/lib/timr/command/log_command.rb +297 -0
- data/lib/timr/command/pause_command.rb +89 -0
- data/lib/timr/command/pop_command.rb +176 -0
- data/lib/timr/command/push_command.rb +141 -0
- data/lib/timr/command/report_command.rb +689 -0
- data/lib/timr/command/start_command.rb +172 -0
- data/lib/timr/command/status_command.rb +198 -0
- data/lib/timr/command/stop_command.rb +127 -0
- data/lib/timr/command/task_command.rb +318 -0
- data/lib/timr/command/track_command.rb +381 -0
- data/lib/timr/command/version_command.rb +18 -0
- data/lib/timr/duration.rb +159 -0
- data/lib/timr/exception/timr_error.rb +113 -0
- data/lib/timr/ext/time.rb +12 -0
- data/lib/timr/helper/datetime_helper.rb +128 -0
- data/lib/timr/helper/terminal_helper.rb +58 -0
- data/lib/timr/helper/translation_helper.rb +45 -0
- data/lib/timr/model/basic_model.rb +287 -0
- data/lib/timr/model/config.rb +48 -0
- data/lib/timr/model/foreign_id_db.rb +84 -0
- data/lib/timr/model/stack.rb +161 -0
- data/lib/timr/model/task.rb +1039 -0
- data/lib/timr/model/track.rb +589 -0
- data/lib/timr/progressbar.rb +41 -0
- data/lib/timr/simple_opt_parser.rb +230 -0
- data/lib/timr/status.rb +70 -0
- data/lib/timr/table.rb +88 -0
- data/lib/timr/timr.rb +500 -558
- data/lib/timr/version.rb +4 -15
- data/man/.gitignore +2 -0
- data/man/_footer +3 -0
- data/man/timr-continue.1 +48 -0
- data/man/timr-continue.1.ronn +39 -0
- data/man/timr-ftime.7 +77 -0
- data/man/timr-ftime.7.ronn +57 -0
- data/man/timr-log.1 +109 -0
- data/man/timr-log.1.ronn +87 -0
- data/man/timr-pause.1 +56 -0
- data/man/timr-pause.1.ronn +45 -0
- data/man/timr-pop.1 +66 -0
- data/man/timr-pop.1.ronn +53 -0
- data/man/timr-push.1 +25 -0
- data/man/timr-push.1.ronn +20 -0
- data/man/timr-report.1 +228 -0
- data/man/timr-report.1.ronn +193 -0
- data/man/timr-start.1 +100 -0
- data/man/timr-start.1.ronn +82 -0
- data/man/timr-status.1 +53 -0
- data/man/timr-status.1.ronn +42 -0
- data/man/timr-stop.1 +75 -0
- data/man/timr-stop.1.ronn +60 -0
- data/man/timr-task.1 +147 -0
- data/man/timr-task.1.ronn +115 -0
- data/man/timr-track.1 +109 -0
- data/man/timr-track.1.ronn +89 -0
- data/man/timr.1 +119 -0
- data/man/timr.1.ronn +68 -0
- data/timr.gemspec +18 -3
- data/timr.sublime-project +20 -1
- metadata +142 -23
- data/Makefile +0 -12
- data/Makefile.common +0 -56
- data/lib/timr/stack.rb +0 -81
- data/lib/timr/task.rb +0 -258
- data/lib/timr/track.rb +0 -167
- data/lib/timr/window.rb +0 -259
- data/lib/timr/window_help.rb +0 -41
- data/lib/timr/window_tasks.rb +0 -30
- data/lib/timr/window_test.rb +0 -20
- data/lib/timr/window_timeline.rb +0 -33
- data/tests/tc_stack.rb +0 -121
- data/tests/tc_task.rb +0 -190
- data/tests/tc_track.rb +0 -144
- data/tests/tc_window.rb +0 -428
- 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
|