doing 2.1.14 → 2.1.15
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/.irbrc +1 -0
- data/.yardoc/checksums +11 -9
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +3 -2
- data/README.md +56 -19
- data/bin/doing +4 -3
- data/docs/doc/Array.html +117 -3
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +1 -1
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +5 -2
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +106 -2
- data/docs/doc/Doing/Items.html +2 -2
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +2 -2
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +1 -1
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +35 -65
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Numeric.html +279 -0
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +767 -115
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/_index.html +14 -9
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +41 -15
- data/docs/doc/index.html +41 -15
- data/docs/doc/method_list.html +357 -293
- data/docs/doc/top-level-namespace.html +2 -2
- data/docs/index.md +56 -19
- data/doing.gemspec +1 -0
- data/doing.rdoc +3 -3
- data/example_plugin.rb +2 -4
- data/lib/completion/_doing.zsh +3 -3
- data/lib/completion/doing.bash +4 -4
- data/lib/completion/doing.fish +2 -2
- data/lib/doing/array_chronify.rb +57 -0
- data/lib/doing/configuration.rb +4 -1
- data/lib/doing/item.rb +32 -0
- data/lib/doing/log_adapter.rb +1 -1
- data/lib/doing/numeric_chronify.rb +40 -0
- data/lib/doing/plugins/export/dayone_export.rb +1 -1
- data/lib/doing/plugins/export/json_export.rb +2 -2
- data/lib/doing/plugins/export/template_export.rb +47 -90
- data/lib/doing/string.rb +97 -33
- data/lib/doing/string_chronify.rb +83 -13
- data/lib/doing/time.rb +4 -4
- data/lib/doing/util_backup.rb +1 -1
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +58 -83
- data/lib/doing.rb +30 -27
- data/lib/examples/plugins/say_export.rb +1 -4
- metadata +26 -2
data/lib/doing/string.rb
CHANGED
@@ -259,7 +259,13 @@ module Doing
|
|
259
259
|
end
|
260
260
|
end
|
261
261
|
|
262
|
-
|
262
|
+
##
|
263
|
+
## Pluralize a string based on quantity
|
264
|
+
##
|
265
|
+
## @param number [Integer] the quantity of the
|
266
|
+
## object the string represents
|
267
|
+
##
|
268
|
+
def to_p(number)
|
263
269
|
number == 1 ? self : "#{self}s"
|
264
270
|
end
|
265
271
|
|
@@ -268,10 +274,6 @@ module Doing
|
|
268
274
|
##
|
269
275
|
## @return [Symbol] :oldest or :newest
|
270
276
|
##
|
271
|
-
def normalize_age!(default = :newest)
|
272
|
-
replace normalize_age(default)
|
273
|
-
end
|
274
|
-
|
275
277
|
def normalize_age(default = :newest)
|
276
278
|
case self
|
277
279
|
when /^o/i
|
@@ -283,6 +285,11 @@ module Doing
|
|
283
285
|
end
|
284
286
|
end
|
285
287
|
|
288
|
+
## @see #normalize_age
|
289
|
+
def normalize_age!(default = :newest)
|
290
|
+
replace normalize_age(default)
|
291
|
+
end
|
292
|
+
|
286
293
|
##
|
287
294
|
## Convert a sort order string to a qualified type
|
288
295
|
##
|
@@ -308,10 +315,6 @@ module Doing
|
|
308
315
|
##
|
309
316
|
## @return Symbol :smart, :sensitive, :ignore
|
310
317
|
##
|
311
|
-
def normalize_case!
|
312
|
-
replace normalize_case
|
313
|
-
end
|
314
|
-
|
315
318
|
def normalize_case(default = :smart)
|
316
319
|
case self
|
317
320
|
when /^(c|sens)/i
|
@@ -325,15 +328,16 @@ module Doing
|
|
325
328
|
end
|
326
329
|
end
|
327
330
|
|
331
|
+
## @see #normalize_case
|
332
|
+
def normalize_case!
|
333
|
+
replace normalize_case
|
334
|
+
end
|
335
|
+
|
328
336
|
##
|
329
337
|
## Convert a boolean string to a symbol
|
330
338
|
##
|
331
339
|
## @return Symbol :and, :or, or :not
|
332
340
|
##
|
333
|
-
def normalize_bool!(default = :and)
|
334
|
-
replace normalize_bool(default)
|
335
|
-
end
|
336
|
-
|
337
341
|
def normalize_bool(default = :and)
|
338
342
|
case self
|
339
343
|
when /(and|all)/i
|
@@ -349,15 +353,19 @@ module Doing
|
|
349
353
|
end
|
350
354
|
end
|
351
355
|
|
356
|
+
## @see #normalize_bool
|
357
|
+
def normalize_bool!(default = :and)
|
358
|
+
replace normalize_bool(default)
|
359
|
+
end
|
360
|
+
|
352
361
|
##
|
353
362
|
## Convert a matching configuration string to a symbol
|
354
363
|
##
|
364
|
+
## @param default [Symbol] the default matching
|
365
|
+
## type to return if the string
|
366
|
+
## doesn't match a known symbol
|
355
367
|
## @return Symbol :fuzzy, :pattern, :exact
|
356
368
|
##
|
357
|
-
def normalize_matching!(default = :pattern)
|
358
|
-
replace normalize_bool(default)
|
359
|
-
end
|
360
|
-
|
361
369
|
def normalize_matching(default = :pattern)
|
362
370
|
case self
|
363
371
|
when /^f/i
|
@@ -371,30 +379,64 @@ module Doing
|
|
371
379
|
end
|
372
380
|
end
|
373
381
|
|
374
|
-
|
375
|
-
|
382
|
+
## @see #normalize_matching
|
383
|
+
def normalize_matching!(default = :pattern)
|
384
|
+
replace normalize_bool(default)
|
376
385
|
end
|
377
386
|
|
387
|
+
##
|
388
|
+
## Adds ?: to any parentheticals in a regular expression
|
389
|
+
## to avoid match groups
|
390
|
+
##
|
391
|
+
## @return [String] modified regular expression
|
392
|
+
##
|
378
393
|
def normalize_trigger
|
379
394
|
gsub(/\((?!\?:)/, '(?:').downcase
|
380
395
|
end
|
381
396
|
|
397
|
+
## @see #normalize_trigger
|
398
|
+
def normalize_trigger!
|
399
|
+
replace normalize_trigger
|
400
|
+
end
|
401
|
+
|
402
|
+
##
|
403
|
+
## Convert ? and * wildcards to regular expressions.
|
404
|
+
## Uses \S (non-whitespace) instead of . (any character)
|
405
|
+
##
|
406
|
+
## @return [String] Regular expression string
|
407
|
+
##
|
382
408
|
def wildcard_to_rx
|
383
409
|
gsub(/\?/, '\S').gsub(/\*/, '\S*?')
|
384
410
|
end
|
385
411
|
|
412
|
+
##
|
413
|
+
## Add @ prefix to string if needed, maintains +/- prefix
|
414
|
+
##
|
415
|
+
## @return [String] @string
|
416
|
+
##
|
386
417
|
def add_at
|
387
418
|
strip.sub(/^([+-]*)@/, '\1')
|
388
419
|
end
|
389
420
|
|
421
|
+
##
|
422
|
+
## Convert a list of tags to an array. Tags can be with
|
423
|
+
## or without @ symbols, separated by any character, and
|
424
|
+
## can include parenthetical values (with spaces)
|
425
|
+
##
|
426
|
+
## @return [Array] array of tags including @ symbols
|
427
|
+
##
|
390
428
|
def to_tags
|
391
|
-
gsub(/ *, */, ' ').
|
392
|
-
end
|
393
|
-
|
394
|
-
def add_tags!(tags, remove: false)
|
395
|
-
replace add_tags(tags, remove: remove)
|
429
|
+
gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
|
396
430
|
end
|
397
431
|
|
432
|
+
##
|
433
|
+
## @brief Adds tags to a string
|
434
|
+
##
|
435
|
+
## @param tags [String or Array] List of tags to add. @ symbol optional
|
436
|
+
## @param remove [Boolean] remove tags instead of adding
|
437
|
+
##
|
438
|
+
## @return [String] the tagged string
|
439
|
+
##
|
398
440
|
def add_tags(tags, remove: false)
|
399
441
|
title = self.dup
|
400
442
|
tags = tags.to_tags
|
@@ -402,6 +444,11 @@ module Doing
|
|
402
444
|
title
|
403
445
|
end
|
404
446
|
|
447
|
+
## @see #add_tags
|
448
|
+
def add_tags!(tags, remove: false)
|
449
|
+
replace add_tags(tags, remove: remove)
|
450
|
+
end
|
451
|
+
|
405
452
|
##
|
406
453
|
## Add, rename, or remove a tag in place
|
407
454
|
##
|
@@ -484,10 +531,6 @@ module Doing
|
|
484
531
|
##
|
485
532
|
## @return Deduplicated string
|
486
533
|
##
|
487
|
-
def dedup_tags!
|
488
|
-
replace dedup_tags
|
489
|
-
end
|
490
|
-
|
491
534
|
def dedup_tags
|
492
535
|
title = dup
|
493
536
|
tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
|
@@ -505,6 +548,11 @@ module Doing
|
|
505
548
|
title
|
506
549
|
end
|
507
550
|
|
551
|
+
## @see #dedup_tags
|
552
|
+
def dedup_tags!
|
553
|
+
replace dedup_tags
|
554
|
+
end
|
555
|
+
|
508
556
|
# Returns the last escape sequence from a string.
|
509
557
|
#
|
510
558
|
# Actually returns all escape codes, with the assumption
|
@@ -525,11 +573,9 @@ module Doing
|
|
525
573
|
##
|
526
574
|
## @param opt [Hash] Additional Options
|
527
575
|
##
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
end
|
532
|
-
|
576
|
+
## @option opt [Symbol] :format can be :markdown or
|
577
|
+
## :html (default)
|
578
|
+
##
|
533
579
|
def link_urls(**opt)
|
534
580
|
fmt = opt.fetch(:format, :html)
|
535
581
|
return self unless fmt
|
@@ -541,6 +587,12 @@ module Doing
|
|
541
587
|
str.replace_qualified_urls(format: fmt).clean_unlinked_urls
|
542
588
|
end
|
543
589
|
|
590
|
+
## @see #link_urls
|
591
|
+
def link_urls!(**opt)
|
592
|
+
fmt = opt.fetch(:format, :html)
|
593
|
+
replace link_urls(format: fmt)
|
594
|
+
end
|
595
|
+
|
544
596
|
# Remove <self-linked> formatting
|
545
597
|
def remove_self_links
|
546
598
|
gsub(/<(.*?)>/) do |match|
|
@@ -590,6 +642,18 @@ module Doing
|
|
590
642
|
end
|
591
643
|
end
|
592
644
|
|
645
|
+
##
|
646
|
+
## Convert a string value to an appropriate type. If
|
647
|
+
## kind is not specified, '[one, two]' becomes an Array,
|
648
|
+
## '1' becomes Integer, '1.5' becomes Float, 'true' or
|
649
|
+
## 'yes' becomes TrueClass, 'false' or 'no' becomes
|
650
|
+
## FalseClass.
|
651
|
+
##
|
652
|
+
## @param kind [String] specify string, array,
|
653
|
+
## integer, float, symbol, or boolean
|
654
|
+
## (falls back to string if value is
|
655
|
+
## not recognized)
|
656
|
+
## @return Converted object type
|
593
657
|
def set_type(kind = nil)
|
594
658
|
if kind
|
595
659
|
case kind.to_s
|
@@ -64,22 +64,92 @@ module Doing
|
|
64
64
|
when /^(\d+):(\d\d)$/
|
65
65
|
minutes += Regexp.last_match(1).to_i * 60
|
66
66
|
minutes += Regexp.last_match(2).to_i
|
67
|
-
when /^(\d+(?:\.\d+)?)([hmd])
|
68
|
-
|
69
|
-
|
67
|
+
when /^(\d+(?:\.\d+)?)([hmd])?/
|
68
|
+
scan(/(\d+(?:\.\d+)?)([hmd])?/).each do |m|
|
69
|
+
amt = m[0]
|
70
|
+
type = m[1].nil? ? 'm' : m[1]
|
70
71
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
72
|
+
minutes += case type.downcase
|
73
|
+
when 'm'
|
74
|
+
amt.to_i
|
75
|
+
when 'h'
|
76
|
+
(amt.to_f * 60).round
|
77
|
+
when 'd'
|
78
|
+
(amt.to_f * 60 * 24).round
|
79
|
+
else
|
80
|
+
0
|
81
|
+
end
|
82
|
+
end
|
81
83
|
end
|
82
84
|
minutes * 60
|
83
85
|
end
|
86
|
+
|
87
|
+
##
|
88
|
+
## Convert DD:HH:MM to seconds
|
89
|
+
##
|
90
|
+
## @return [Integer] rounded number of seconds
|
91
|
+
##
|
92
|
+
def to_seconds
|
93
|
+
mtch = match(/(\d+):(\d+):(\d+)/)
|
94
|
+
|
95
|
+
raise Errors::DoingRuntimeError, "Invalid time string: #{self}" unless mtch
|
96
|
+
|
97
|
+
h = mtch[1]
|
98
|
+
m = mtch[2]
|
99
|
+
s = mtch[3]
|
100
|
+
(h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
## Convert DD:HH:MM to a natural language string
|
105
|
+
##
|
106
|
+
## @param format [Symbol] The format to output (:dhm, :hm, :m, :clock, :natural)
|
107
|
+
##
|
108
|
+
def time_string(format: :dhm)
|
109
|
+
to_seconds.time_string(format: format)
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
## Convert (chronify) natural language dates
|
114
|
+
## within configured date tags (tags whose value is
|
115
|
+
## expected to be a date). Modifies string in place.
|
116
|
+
##
|
117
|
+
## @param additional_tags [Array] An array of
|
118
|
+
## additional tags to
|
119
|
+
## consider date_tags
|
120
|
+
##
|
121
|
+
def expand_date_tags(additional_tags = nil)
|
122
|
+
iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
|
123
|
+
|
124
|
+
watch_tags = [
|
125
|
+
'start(?:ed)?',
|
126
|
+
'beg[ia]n',
|
127
|
+
'done',
|
128
|
+
'finished',
|
129
|
+
'completed?',
|
130
|
+
'waiting',
|
131
|
+
'defer(?:red)?'
|
132
|
+
]
|
133
|
+
|
134
|
+
if additional_tags
|
135
|
+
date_tags = additional_tags
|
136
|
+
date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
|
137
|
+
date_tags.map! do |tag|
|
138
|
+
tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
|
139
|
+
end
|
140
|
+
watch_tags.concat(date_tags).uniq!
|
141
|
+
end
|
142
|
+
|
143
|
+
done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i
|
144
|
+
|
145
|
+
gsub!(done_rx) do
|
146
|
+
m = Regexp.last_match
|
147
|
+
t = m['tag']
|
148
|
+
d = m['date']
|
149
|
+
future = t =~ /^(done|complete)/ ? false : true
|
150
|
+
parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
|
151
|
+
parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
|
152
|
+
end
|
153
|
+
end
|
84
154
|
end
|
85
155
|
end
|
data/lib/doing/time.rb
CHANGED
@@ -25,10 +25,10 @@ module Doing
|
|
25
25
|
h = h % 24
|
26
26
|
|
27
27
|
output = []
|
28
|
-
output.push("#{d} #{'day'.
|
29
|
-
output.push("#{h} #{'hour'.
|
30
|
-
output.push("#{m} #{'minute'.
|
31
|
-
output.push("#{s} #{'second'.
|
28
|
+
output.push("#{d} #{'day'.to_p(d)}") if d.positive?
|
29
|
+
output.push("#{h} #{'hour'.to_p(h)}") if h.positive?
|
30
|
+
output.push("#{m} #{'minute'.to_p(m)}") if m.positive?
|
31
|
+
output.push("#{s} #{'second'.to_p(s)}") if s.positive?
|
32
32
|
output.join(', ')
|
33
33
|
end
|
34
34
|
|
data/lib/doing/util_backup.rb
CHANGED
@@ -79,7 +79,7 @@ module Doing
|
|
79
79
|
def redo_backup(filename = nil, count: 1)
|
80
80
|
filename ||= Doing.config.settings['doing_file']
|
81
81
|
# redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
|
82
|
-
undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
|
82
|
+
undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort.reverse
|
83
83
|
total = undones.count
|
84
84
|
count = total if count > total
|
85
85
|
|
data/lib/doing/version.rb
CHANGED
data/lib/doing/wwid.rb
CHANGED
@@ -185,34 +185,9 @@ module Doing
|
|
185
185
|
|
186
186
|
date = nil
|
187
187
|
iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
|
188
|
-
watch_tags = [
|
189
|
-
'start(?:ed)?',
|
190
|
-
'beg[ia]n',
|
191
|
-
'done',
|
192
|
-
'finished',
|
193
|
-
'completed?',
|
194
|
-
'waiting',
|
195
|
-
'defer(?:red)?'
|
196
|
-
]
|
197
|
-
if @config['date_tags']
|
198
|
-
date_tags = @config['date_tags']
|
199
|
-
date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
|
200
|
-
date_tags.map! do |tag|
|
201
|
-
tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
|
202
|
-
end
|
203
|
-
watch_tags.concat(date_tags).uniq!
|
204
|
-
end
|
205
|
-
|
206
|
-
done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i
|
207
188
|
date_rx = /^(?:\s*- )?(?<date>.*?) \| (?=\S)/
|
208
189
|
|
209
|
-
title.
|
210
|
-
m = Regexp.last_match
|
211
|
-
t = m['tag']
|
212
|
-
d = m['date']
|
213
|
-
parsed_date = d =~ date_rx ? Time.parse(d) : d.chronify(guess: :begin)
|
214
|
-
parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
|
215
|
-
end
|
190
|
+
title.expand_date_tags(@config['date_tags'])
|
216
191
|
|
217
192
|
if title =~ date_rx
|
218
193
|
m = title.match(date_rx)
|
@@ -369,7 +344,8 @@ module Doing
|
|
369
344
|
items.each_with_index do |i, x|
|
370
345
|
next if i.title =~ / @done/
|
371
346
|
|
372
|
-
|
347
|
+
finish_date = verify_duration(i.date, opt[:back], title: i.title)
|
348
|
+
items[x].tag('done', value: finish_date.strftime('%F %R'))
|
373
349
|
break
|
374
350
|
end
|
375
351
|
end
|
@@ -1001,7 +977,7 @@ module Doing
|
|
1001
977
|
'--no-sort',
|
1002
978
|
'--info=hidden'
|
1003
979
|
])
|
1004
|
-
next if
|
980
|
+
next if output_format =~ /^ *$/
|
1005
981
|
|
1006
982
|
raise UserCancelled unless output_format
|
1007
983
|
|
@@ -1089,6 +1065,7 @@ module Doing
|
|
1089
1065
|
tag = opt[:tag]
|
1090
1066
|
items.map! do |i|
|
1091
1067
|
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1068
|
+
i.expand_date_tags(@config['date_tags'])
|
1092
1069
|
Hooks.trigger :post_entry_updated, self, i
|
1093
1070
|
end
|
1094
1071
|
end
|
@@ -1148,6 +1125,26 @@ module Doing
|
|
1148
1125
|
end
|
1149
1126
|
end
|
1150
1127
|
|
1128
|
+
def verify_duration(date, finish_date, title: nil)
|
1129
|
+
max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
|
1130
|
+
max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
|
1131
|
+
elapsed = finish_date - date
|
1132
|
+
|
1133
|
+
if max_elapsed.positive? && (elapsed > max_elapsed)
|
1134
|
+
puts boldwhite(title) if title
|
1135
|
+
human = elapsed.time_string(format: :natural)
|
1136
|
+
res = Prompt.yn(yellow("Did this entry actually take #{human}"), default_response: true)
|
1137
|
+
unless res
|
1138
|
+
new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
|
1139
|
+
raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed.positive?
|
1140
|
+
|
1141
|
+
finish_date = date + new_elapsed if new_elapsed
|
1142
|
+
end
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
finish_date
|
1146
|
+
end
|
1147
|
+
|
1151
1148
|
##
|
1152
1149
|
## Tag the last entry or X entries
|
1153
1150
|
##
|
@@ -1209,21 +1206,8 @@ module Doing
|
|
1209
1206
|
else
|
1210
1207
|
next_entry.date - 60
|
1211
1208
|
end
|
1212
|
-
elsif opt[:took]
|
1213
|
-
if item.date + opt[:took] > Time.now
|
1214
|
-
item.date = Time.now - opt[:took]
|
1215
|
-
done_date = Time.now
|
1216
|
-
else
|
1217
|
-
done_date = item.date + opt[:took]
|
1218
|
-
end
|
1219
|
-
elsif opt[:back]
|
1220
|
-
done_date = if opt[:back].is_a? Integer
|
1221
|
-
item.date + opt[:back]
|
1222
|
-
else
|
1223
|
-
item.date + (opt[:back] - item.date)
|
1224
|
-
end
|
1225
1209
|
else
|
1226
|
-
done_date =
|
1210
|
+
done_date = item.calculate_end_date(opt)
|
1227
1211
|
end
|
1228
1212
|
|
1229
1213
|
opt[:tags].each do |tag|
|
@@ -1234,7 +1218,28 @@ module Doing
|
|
1234
1218
|
next
|
1235
1219
|
end
|
1236
1220
|
|
1221
|
+
|
1237
1222
|
tag = tag.strip
|
1223
|
+
|
1224
|
+
if tag =~ /^done$/
|
1225
|
+
max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
|
1226
|
+
max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
|
1227
|
+
elapsed = done_date - item.date
|
1228
|
+
|
1229
|
+
if max_elapsed.positive? && (elapsed > max_elapsed) && !opt[:took]
|
1230
|
+
puts boldwhite(item.title)
|
1231
|
+
human = elapsed.time_string(format: :natural)
|
1232
|
+
res = Prompt.yn(yellow("Did this actually take #{human}"), default_response: true)
|
1233
|
+
unless res
|
1234
|
+
new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
|
1235
|
+
raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed > 0
|
1236
|
+
|
1237
|
+
opt[:took] = new_elapsed
|
1238
|
+
done_date = item.calculate_end_date(opt) if opt[:took]
|
1239
|
+
end
|
1240
|
+
end
|
1241
|
+
end
|
1242
|
+
|
1238
1243
|
if opt[:remove] || opt[:rename] || opt[:value]
|
1239
1244
|
rename_to = nil
|
1240
1245
|
if opt[:value]
|
@@ -1272,6 +1277,7 @@ module Doing
|
|
1272
1277
|
logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
|
1273
1278
|
end
|
1274
1279
|
|
1280
|
+
item.expand_date_tags(@config['date_tags'])
|
1275
1281
|
Hooks.trigger :post_entry_updated, self, item
|
1276
1282
|
end
|
1277
1283
|
|
@@ -2005,7 +2011,7 @@ module Doing
|
|
2005
2011
|
EOS
|
2006
2012
|
sorted_tags_data.reverse.each do |k, v|
|
2007
2013
|
if v > 0
|
2008
|
-
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{
|
2014
|
+
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}</td></tr>\n"
|
2009
2015
|
end
|
2010
2016
|
end
|
2011
2017
|
tail = <<EOS
|
@@ -2016,7 +2022,7 @@ EOS
|
|
2016
2022
|
<tfoot>
|
2017
2023
|
<tr>
|
2018
2024
|
<td style="text-align:left;"><strong>Total</strong></td>
|
2019
|
-
<td style="text-align:left;">#{
|
2025
|
+
<td style="text-align:left;">#{total.time_string(format: :clock)}</td>
|
2020
2026
|
</tr>
|
2021
2027
|
</tfoot>
|
2022
2028
|
</table>
|
@@ -2031,7 +2037,7 @@ EOS
|
|
2031
2037
|
EOS
|
2032
2038
|
sorted_tags_data.reverse.each do |k, v|
|
2033
2039
|
if v > 0
|
2034
|
-
output += "| #{' ' * (pad - k.length)}#{k} | #{
|
2040
|
+
output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)} |\n"
|
2035
2041
|
end
|
2036
2042
|
end
|
2037
2043
|
tail = "[Tag Totals]"
|
@@ -2039,11 +2045,10 @@ EOS
|
|
2039
2045
|
when :json
|
2040
2046
|
output = []
|
2041
2047
|
sorted_tags_data.reverse.each do |k, v|
|
2042
|
-
d, h, m = format_time(v)
|
2043
2048
|
output << {
|
2044
2049
|
'tag' => k,
|
2045
2050
|
'seconds' => v,
|
2046
|
-
'formatted' => format
|
2051
|
+
'formatted' => v.time_string(format: :clock)
|
2047
2052
|
}
|
2048
2053
|
end
|
2049
2054
|
output
|
@@ -2054,8 +2059,7 @@ EOS
|
|
2054
2059
|
(max - k.length).times do
|
2055
2060
|
spacer += ' '
|
2056
2061
|
end
|
2057
|
-
|
2058
|
-
output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
|
2062
|
+
output.push("┃ #{spacer}#{k}:#{v.time_string(format: :hm)} ┃")
|
2059
2063
|
end
|
2060
2064
|
|
2061
2065
|
header = '┏━━ Tag Totals '
|
@@ -2068,14 +2072,14 @@ EOS
|
|
2068
2072
|
(max + 12).times { divider += '━' }
|
2069
2073
|
divider += '┫'
|
2070
2074
|
output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
|
2071
|
-
d, h, m = format_time(total, human: true)
|
2072
2075
|
output += "\n#{divider}"
|
2073
2076
|
spacer = ''
|
2074
2077
|
(max - 6).times do
|
2075
2078
|
spacer += ' '
|
2076
2079
|
end
|
2080
|
+
total_time = total.time_string(format: :hm)
|
2077
2081
|
total = "┃ #{spacer}total: "
|
2078
|
-
total +=
|
2082
|
+
total += total_time
|
2079
2083
|
total += ' ┃'
|
2080
2084
|
output += "\n#{total}"
|
2081
2085
|
output += "\n#{footer}"
|
@@ -2087,13 +2091,11 @@ EOS
|
|
2087
2091
|
(max - k.length).times do
|
2088
2092
|
spacer += ' '
|
2089
2093
|
end
|
2090
|
-
|
2091
|
-
output.push("#{k}:#{spacer}#{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}")
|
2094
|
+
output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
|
2092
2095
|
end
|
2093
2096
|
|
2094
2097
|
output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
|
2095
|
-
|
2096
|
-
output += "\n\nTotal tracked: #{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}\n"
|
2098
|
+
output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
|
2097
2099
|
output
|
2098
2100
|
end
|
2099
2101
|
end
|
@@ -2118,39 +2120,12 @@ EOS
|
|
2118
2120
|
record_tag_times(item, seconds) if record
|
2119
2121
|
return seconds.positive? ? seconds : false unless formatted
|
2120
2122
|
|
2121
|
-
return seconds.positive? ? format
|
2123
|
+
return seconds.positive? ? seconds.time_string(format: :clock) : false
|
2122
2124
|
end
|
2123
2125
|
|
2124
2126
|
false
|
2125
2127
|
end
|
2126
2128
|
|
2127
|
-
##
|
2128
|
-
## Format human readable time from seconds
|
2129
|
-
##
|
2130
|
-
## @param seconds [Integer] Seconds
|
2131
|
-
##
|
2132
|
-
def format_time(seconds, human: false)
|
2133
|
-
return [0, 0, 0] if seconds.nil?
|
2134
|
-
|
2135
|
-
if seconds.instance_of?(String) && seconds =~ /(\d+):(\d+):(\d+)/
|
2136
|
-
h = Regexp.last_match(1)
|
2137
|
-
m = Regexp.last_match(2)
|
2138
|
-
s = Regexp.last_match(3)
|
2139
|
-
seconds = (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
|
2140
|
-
end
|
2141
|
-
minutes = (seconds / 60).to_i
|
2142
|
-
hours = (minutes / 60).to_i
|
2143
|
-
if human
|
2144
|
-
minutes = (minutes % 60).to_i
|
2145
|
-
[0, hours, minutes]
|
2146
|
-
else
|
2147
|
-
days = (hours / 24).to_i
|
2148
|
-
hours = (hours % 24).to_i
|
2149
|
-
minutes = (minutes % 60).to_i
|
2150
|
-
[days, hours, minutes]
|
2151
|
-
end
|
2152
|
-
end
|
2153
|
-
|
2154
2129
|
def configure(filename = nil)
|
2155
2130
|
if filename
|
2156
2131
|
Doing.config_with(filename, { ignore_local: true })
|