doing 2.1.13 → 2.1.17
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 +14 -12
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +76 -0
- data/Gemfile.lock +9 -2
- data/README.md +56 -19
- data/bin/doing +218 -68
- 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 +6 -2
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +8 -4
- 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 +340 -14
- 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 +103 -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 +77 -71
- 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 +997 -118
- 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 +449 -305
- data/docs/doc/top-level-namespace.html +2 -2
- data/docs/index.md +56 -19
- data/doing.gemspec +2 -0
- data/doing.rdoc +76 -9
- data/example_plugin.rb +2 -4
- data/lib/completion/_doing.zsh +17 -17
- data/lib/completion/doing.bash +25 -25
- data/lib/completion/doing.fish +18 -6
- data/lib/doing/array_chronify.rb +57 -0
- data/lib/doing/colors.rb +4 -0
- data/lib/doing/configuration.rb +6 -2
- data/lib/doing/item.rb +108 -0
- data/lib/doing/log_adapter.rb +3 -3
- 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 +49 -90
- data/lib/doing/plugins/import/calendar_import.rb +13 -1
- data/lib/doing/plugins/import/doing_import.rb +12 -1
- data/lib/doing/plugins/import/timing_import.rb +13 -1
- data/lib/doing/prompt.rb +65 -1
- data/lib/doing/string.rb +137 -33
- data/lib/doing/string_chronify.rb +112 -14
- data/lib/doing/template_string.rb +1 -1
- data/lib/doing/time.rb +6 -6
- data/lib/doing/util_backup.rb +1 -1
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +117 -106
- data/lib/doing.rb +36 -31
- data/lib/examples/plugins/say_export.rb +1 -4
- metadata +46 -2
@@ -64,22 +64,120 @@ 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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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]
|
71
|
+
|
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
|
154
|
+
|
155
|
+
##
|
156
|
+
## Splits a range string and returns an array of
|
157
|
+
## DateTime objects as [start, end]. If only one date is
|
158
|
+
## given, end time is nil.
|
159
|
+
##
|
160
|
+
## @return [Array<DateTime>] Start and end dates as
|
161
|
+
## array
|
162
|
+
## @example Process a natural language date range
|
163
|
+
## "mon 3pm to mon 5pm".split_date_range
|
164
|
+
##
|
165
|
+
def split_date_range
|
166
|
+
date_string = dup
|
167
|
+
case date_string
|
168
|
+
when / (to|through|thru|(un)?til|-+) /
|
169
|
+
dates = date_string.split(/ (?:to|through|thru|(?:un)?til|-+) /)
|
170
|
+
start = dates[0].chronify(guess: :begin)
|
171
|
+
finish = dates[-1].chronify(guess: :end)
|
172
|
+
else
|
173
|
+
start = date_string.chronify(guess: :begin)
|
174
|
+
finish = nil
|
175
|
+
end
|
176
|
+
|
177
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
178
|
+
|
179
|
+
Doing.logger.debug('Parser:', "date range interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
|
180
|
+
[start, finish]
|
181
|
+
end
|
84
182
|
end
|
85
183
|
end
|
@@ -176,7 +176,7 @@ module Doing
|
|
176
176
|
' '
|
177
177
|
else
|
178
178
|
line = l.gsub(/%/, '\%').strip.wrap(width, pad: pad, indent: indent, offset: 0, prefix: prefix, color: last_color, after: after, reset: reset, pad_first: true)
|
179
|
-
line.highlight_tags!(tags_color, last_color: last_color) unless tags_color.nil? || tags_color.empty?
|
179
|
+
line.highlight_tags!(tags_color, last_color: last_color) unless !tags_color || tags_color.nil? || tags_color.empty?
|
180
180
|
"#{line} "
|
181
181
|
end
|
182
182
|
end.join("\n")
|
data/lib/doing/time.rb
CHANGED
@@ -8,10 +8,10 @@ module Doing
|
|
8
8
|
strftime('%_I:%M%P')
|
9
9
|
elsif self > (Date.today - 6).to_time
|
10
10
|
strftime('%a %_I:%M%P')
|
11
|
-
elsif self.year == Date.today.year
|
11
|
+
elsif self.year == Date.today.year || (self.year + 1 == Date.today.year && self.month > Date.today.month)
|
12
12
|
strftime('%m/%d %_I:%M%P')
|
13
13
|
else
|
14
|
-
strftime('%m/%d/%
|
14
|
+
strftime('%m/%d/%y %_I:%M%P')
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
@@ -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,11 @@ 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
|
-
|
211
|
-
|
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
|
+
raise EmptyInput, 'No content' if title.sub(/^.*?\| */, '').strip.empty?
|
191
|
+
|
192
|
+
title.expand_date_tags(@config['date_tags'])
|
216
193
|
|
217
194
|
if title =~ date_rx
|
218
195
|
m = title.match(date_rx)
|
@@ -369,7 +346,8 @@ module Doing
|
|
369
346
|
items.each_with_index do |i, x|
|
370
347
|
next if i.title =~ / @done/
|
371
348
|
|
372
|
-
|
349
|
+
finish_date = verify_duration(i.date, opt[:back], title: i.title)
|
350
|
+
items[x].tag('done', value: finish_date.strftime('%F %R'))
|
373
351
|
break
|
374
352
|
end
|
375
353
|
end
|
@@ -384,10 +362,13 @@ module Doing
|
|
384
362
|
end
|
385
363
|
|
386
364
|
##
|
387
|
-
## Remove items from
|
365
|
+
## Remove items from an array that already exist in
|
366
|
+
## @content based on start and end times
|
388
367
|
##
|
389
|
-
## @param items [Array] The items to
|
390
|
-
##
|
368
|
+
## @param items [Array] The items to
|
369
|
+
## deduplicate
|
370
|
+
## @param no_overlap [Boolean] Remove items with
|
371
|
+
## overlapping time spans
|
391
372
|
##
|
392
373
|
def dedup(items, no_overlap: false)
|
393
374
|
items.delete_if do |item|
|
@@ -817,17 +798,18 @@ module Doing
|
|
817
798
|
end
|
818
799
|
|
819
800
|
def delete_items(items, force: false)
|
820
|
-
|
821
|
-
if
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
801
|
+
items.slice(0, 5).each { |i| puts i.to_pretty } unless force
|
802
|
+
puts softpurple("+ #{items.size - 5} additional #{'item'.to_p(items.size - 5)}") if items.size > 5 && !force
|
803
|
+
|
804
|
+
res = force ? true : Prompt.yn("Delete #{items.size} #{'item'.to_p(items.size)}?", default_response: 'y')
|
805
|
+
return unless res
|
806
|
+
|
807
|
+
items.each { |i| Hooks.trigger :post_entry_removed, self, @content.delete_item(i, single: items.count == 1) }
|
808
|
+
write(@doing_file)
|
828
809
|
end
|
829
810
|
|
830
811
|
def edit_items(items)
|
812
|
+
items.sort_by! { |i| i.date }
|
831
813
|
editable_items = []
|
832
814
|
|
833
815
|
items.each do |i|
|
@@ -836,16 +818,16 @@ module Doing
|
|
836
818
|
editable += "\n#{old_note}" unless old_note.nil?
|
837
819
|
editable_items << editable
|
838
820
|
end
|
839
|
-
divider = "
|
821
|
+
divider = "-----------"
|
840
822
|
notice =<<~EONOTICE
|
841
823
|
# - You may delete entries, but leave all divider lines (---) in place.
|
842
824
|
# - Start and @done dates replaced with a time string (yesterday 3pm) will
|
843
825
|
# be parsed automatically. Do not delete the pipe (|) between start date
|
844
826
|
# and entry title.
|
845
827
|
EONOTICE
|
846
|
-
input = "#{editable_items.map(&:strip).join(divider)}\n\n#{notice}"
|
828
|
+
input = "#{editable_items.map(&:strip).join("\n#{divider}\n")}\n\n#{notice}"
|
847
829
|
|
848
|
-
new_items = fork_editor(input).split(
|
830
|
+
new_items = fork_editor(input).split(/^#{divider}/).map(&:strip)
|
849
831
|
|
850
832
|
new_items.each_with_index do |new_item, i|
|
851
833
|
input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
|
@@ -854,7 +836,7 @@ module Doing
|
|
854
836
|
if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
|
855
837
|
deleted = @content.delete_item(items[i], single: new_items.count == 1)
|
856
838
|
Hooks.trigger :post_entry_removed, self, deleted
|
857
|
-
Doing.logger.
|
839
|
+
Doing.logger.info('Deleted:', deleted.title)
|
858
840
|
else
|
859
841
|
date, title, note = format_input(new_item)
|
860
842
|
|
@@ -894,8 +876,8 @@ module Doing
|
|
894
876
|
opt[:search] = search
|
895
877
|
end
|
896
878
|
|
897
|
-
opt[:query] = opt[:search] if opt[:search] && !opt[:query]
|
898
|
-
opt[:query] = "!#{opt[:query]}" if opt[:not]
|
879
|
+
# opt[:query] = opt[:search] if opt[:search] && !opt[:query]
|
880
|
+
opt[:query] = "!#{opt[:query]}" if opt[:query] && opt[:not]
|
899
881
|
opt[:multiple] = true
|
900
882
|
opt[:show_if_single] = true
|
901
883
|
filter_options = %i[after before case date_filter from fuzzy not search section val].each_with_object({}) {
|
@@ -982,8 +964,14 @@ module Doing
|
|
982
964
|
type = action =~ /^add/ ? 'add' : 'remove'
|
983
965
|
raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
|
984
966
|
|
985
|
-
|
986
|
-
|
967
|
+
tags = type == 'add' ? all_tags(@content) : all_tags(items)
|
968
|
+
|
969
|
+
puts "#{yellow}Separate multiple tags with spaces, hit tab to complete known tags#{type == 'add' ? ', include values with tag(value)' : ''}"
|
970
|
+
puts "#{boldgreen}Available tags: #{boldwhite}#{tags.sort.map(&:add_at).join(', ')}" if type == 'remove'
|
971
|
+
tag = Prompt.read_line(prompt: "Tags to #{type}", completions: tags)
|
972
|
+
|
973
|
+
# print "#{yellow("Tag to #{type}: ")}#{reset}"
|
974
|
+
# tag = $stdin.gets
|
987
975
|
next if tag =~ /^ *$/
|
988
976
|
|
989
977
|
opt[:tag] = tag.strip.sub(/^@/, '')
|
@@ -998,15 +986,16 @@ module Doing
|
|
998
986
|
'--no-sort',
|
999
987
|
'--info=hidden'
|
1000
988
|
])
|
1001
|
-
next if
|
989
|
+
next if output_format =~ /^ *$/
|
1002
990
|
|
1003
991
|
raise UserCancelled unless output_format
|
1004
992
|
|
1005
993
|
opt[:output] = output_format.strip
|
1006
994
|
res = opt[:force] ? false : Prompt.yn('Save to file?', default_response: 'n')
|
1007
995
|
if res
|
1008
|
-
print "#{yellow('File path/name: ')}#{reset}"
|
1009
|
-
filename = $stdin.gets.strip
|
996
|
+
# print "#{yellow('File path/name: ')}#{reset}"
|
997
|
+
# filename = $stdin.gets.strip
|
998
|
+
filename = Prompt.read_line(prompt: 'File path/name')
|
1010
999
|
next if filename.empty?
|
1011
1000
|
|
1012
1001
|
opt[:save_to] = filename
|
@@ -1086,6 +1075,7 @@ module Doing
|
|
1086
1075
|
tag = opt[:tag]
|
1087
1076
|
items.map! do |i|
|
1088
1077
|
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1078
|
+
i.expand_date_tags(@config['date_tags'])
|
1089
1079
|
Hooks.trigger :post_entry_updated, self, i
|
1090
1080
|
end
|
1091
1081
|
end
|
@@ -1113,10 +1103,10 @@ module Doing
|
|
1113
1103
|
i
|
1114
1104
|
end
|
1115
1105
|
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
options = { section: '
|
1106
|
+
export_items = Items.new
|
1107
|
+
export_items.concat(items)
|
1108
|
+
export_items.add_section(Section.new('Export'), log: false)
|
1109
|
+
options = { section: 'All' }
|
1120
1110
|
|
1121
1111
|
if opt[:output] =~ /doing/
|
1122
1112
|
options[:output] = 'template'
|
@@ -1126,7 +1116,7 @@ module Doing
|
|
1126
1116
|
options[:template] = opt[:template] || nil
|
1127
1117
|
end
|
1128
1118
|
|
1129
|
-
output = list_section(options) # hooked
|
1119
|
+
output = list_section(options, items: export_items) # hooked
|
1130
1120
|
|
1131
1121
|
if opt[:save_to]
|
1132
1122
|
file = File.expand_path(opt[:save_to])
|
@@ -1145,6 +1135,26 @@ module Doing
|
|
1145
1135
|
end
|
1146
1136
|
end
|
1147
1137
|
|
1138
|
+
def verify_duration(date, finish_date, title: nil)
|
1139
|
+
max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
|
1140
|
+
max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
|
1141
|
+
elapsed = finish_date - date
|
1142
|
+
|
1143
|
+
if max_elapsed.positive? && (elapsed > max_elapsed)
|
1144
|
+
puts boldwhite(title) if title
|
1145
|
+
human = elapsed.time_string(format: :natural)
|
1146
|
+
res = Prompt.yn(yellow("Did this entry actually take #{human}"), default_response: true)
|
1147
|
+
unless res
|
1148
|
+
new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
|
1149
|
+
raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed.positive?
|
1150
|
+
|
1151
|
+
finish_date = date + new_elapsed if new_elapsed
|
1152
|
+
end
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
finish_date
|
1156
|
+
end
|
1157
|
+
|
1148
1158
|
##
|
1149
1159
|
## Tag the last entry or X entries
|
1150
1160
|
##
|
@@ -1183,6 +1193,19 @@ module Doing
|
|
1183
1193
|
|
1184
1194
|
raise NoResults, 'no items matched your search' if items.empty?
|
1185
1195
|
|
1196
|
+
if opt[:tags].empty? && !opt[:autotag]
|
1197
|
+
completions = opt[:remove] ? all_tags(items) : all_tags(@content)
|
1198
|
+
if opt[:remove]
|
1199
|
+
puts "#{yellow}Available tags: #{boldwhite}#{completions.map(&:add_at).join(', ')}"
|
1200
|
+
else
|
1201
|
+
puts "#{yellow}Use tab to complete known tags"
|
1202
|
+
end
|
1203
|
+
opt[:tags] = Doing::Prompt.read_line(prompt: "Enter tag(s) to #{opt[:remove] ? 'remove' : 'add'}",
|
1204
|
+
completions: completions,
|
1205
|
+
default_response: '').to_tags
|
1206
|
+
raise UserCancelled, 'No tags provided' if opt[:tags].empty?
|
1207
|
+
end
|
1208
|
+
|
1186
1209
|
items.each do |item|
|
1187
1210
|
added = []
|
1188
1211
|
removed = []
|
@@ -1206,21 +1229,8 @@ module Doing
|
|
1206
1229
|
else
|
1207
1230
|
next_entry.date - 60
|
1208
1231
|
end
|
1209
|
-
elsif opt[:took]
|
1210
|
-
if item.date + opt[:took] > Time.now
|
1211
|
-
item.date = Time.now - opt[:took]
|
1212
|
-
done_date = Time.now
|
1213
|
-
else
|
1214
|
-
done_date = item.date + opt[:took]
|
1215
|
-
end
|
1216
|
-
elsif opt[:back]
|
1217
|
-
done_date = if opt[:back].is_a? Integer
|
1218
|
-
item.date + opt[:back]
|
1219
|
-
else
|
1220
|
-
item.date + (opt[:back] - item.date)
|
1221
|
-
end
|
1222
1232
|
else
|
1223
|
-
done_date =
|
1233
|
+
done_date = item.calculate_end_date(opt)
|
1224
1234
|
end
|
1225
1235
|
|
1226
1236
|
opt[:tags].each do |tag|
|
@@ -1231,7 +1241,28 @@ module Doing
|
|
1231
1241
|
next
|
1232
1242
|
end
|
1233
1243
|
|
1244
|
+
|
1234
1245
|
tag = tag.strip
|
1246
|
+
|
1247
|
+
if tag =~ /^done$/
|
1248
|
+
max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
|
1249
|
+
max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
|
1250
|
+
elapsed = done_date - item.date
|
1251
|
+
|
1252
|
+
if max_elapsed.positive? && (elapsed > max_elapsed) && !opt[:took]
|
1253
|
+
puts boldwhite(item.title)
|
1254
|
+
human = elapsed.time_string(format: :natural)
|
1255
|
+
res = Prompt.yn(yellow("Did this actually take #{human}"), default_response: true)
|
1256
|
+
unless res
|
1257
|
+
new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
|
1258
|
+
raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed > 0
|
1259
|
+
|
1260
|
+
opt[:took] = new_elapsed
|
1261
|
+
done_date = item.calculate_end_date(opt) if opt[:took]
|
1262
|
+
end
|
1263
|
+
end
|
1264
|
+
end
|
1265
|
+
|
1235
1266
|
if opt[:remove] || opt[:rename] || opt[:value]
|
1236
1267
|
rename_to = nil
|
1237
1268
|
if opt[:value]
|
@@ -1269,6 +1300,7 @@ module Doing
|
|
1269
1300
|
logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
|
1270
1301
|
end
|
1271
1302
|
|
1303
|
+
item.expand_date_tags(@config['date_tags'])
|
1272
1304
|
Hooks.trigger :post_entry_updated, self, item
|
1273
1305
|
end
|
1274
1306
|
|
@@ -1618,6 +1650,7 @@ module Doing
|
|
1618
1650
|
|
1619
1651
|
opt[:output] ||= 'template'
|
1620
1652
|
opt[:wrap_width] ||= @config['templates']['default']['wrap_width'] || 0
|
1653
|
+
|
1621
1654
|
output(items, title, is_single, opt)
|
1622
1655
|
end
|
1623
1656
|
|
@@ -2002,7 +2035,7 @@ module Doing
|
|
2002
2035
|
EOS
|
2003
2036
|
sorted_tags_data.reverse.each do |k, v|
|
2004
2037
|
if v > 0
|
2005
|
-
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{
|
2038
|
+
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}</td></tr>\n"
|
2006
2039
|
end
|
2007
2040
|
end
|
2008
2041
|
tail = <<EOS
|
@@ -2013,7 +2046,7 @@ EOS
|
|
2013
2046
|
<tfoot>
|
2014
2047
|
<tr>
|
2015
2048
|
<td style="text-align:left;"><strong>Total</strong></td>
|
2016
|
-
<td style="text-align:left;">#{
|
2049
|
+
<td style="text-align:left;">#{total.time_string(format: :clock)}</td>
|
2017
2050
|
</tr>
|
2018
2051
|
</tfoot>
|
2019
2052
|
</table>
|
@@ -2028,7 +2061,7 @@ EOS
|
|
2028
2061
|
EOS
|
2029
2062
|
sorted_tags_data.reverse.each do |k, v|
|
2030
2063
|
if v > 0
|
2031
|
-
output += "| #{' ' * (pad - k.length)}#{k} | #{
|
2064
|
+
output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)} |\n"
|
2032
2065
|
end
|
2033
2066
|
end
|
2034
2067
|
tail = "[Tag Totals]"
|
@@ -2036,11 +2069,10 @@ EOS
|
|
2036
2069
|
when :json
|
2037
2070
|
output = []
|
2038
2071
|
sorted_tags_data.reverse.each do |k, v|
|
2039
|
-
d, h, m = format_time(v)
|
2040
2072
|
output << {
|
2041
2073
|
'tag' => k,
|
2042
2074
|
'seconds' => v,
|
2043
|
-
'formatted' => format
|
2075
|
+
'formatted' => v.time_string(format: :clock)
|
2044
2076
|
}
|
2045
2077
|
end
|
2046
2078
|
output
|
@@ -2051,8 +2083,7 @@ EOS
|
|
2051
2083
|
(max - k.length).times do
|
2052
2084
|
spacer += ' '
|
2053
2085
|
end
|
2054
|
-
|
2055
|
-
output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
|
2086
|
+
output.push("┃ #{spacer}#{k}:#{v.time_string(format: :hm)} ┃")
|
2056
2087
|
end
|
2057
2088
|
|
2058
2089
|
header = '┏━━ Tag Totals '
|
@@ -2065,14 +2096,14 @@ EOS
|
|
2065
2096
|
(max + 12).times { divider += '━' }
|
2066
2097
|
divider += '┫'
|
2067
2098
|
output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
|
2068
|
-
d, h, m = format_time(total, human: true)
|
2069
2099
|
output += "\n#{divider}"
|
2070
2100
|
spacer = ''
|
2071
2101
|
(max - 6).times do
|
2072
2102
|
spacer += ' '
|
2073
2103
|
end
|
2104
|
+
total_time = total.time_string(format: :hm)
|
2074
2105
|
total = "┃ #{spacer}total: "
|
2075
|
-
total +=
|
2106
|
+
total += total_time
|
2076
2107
|
total += ' ┃'
|
2077
2108
|
output += "\n#{total}"
|
2078
2109
|
output += "\n#{footer}"
|
@@ -2084,13 +2115,11 @@ EOS
|
|
2084
2115
|
(max - k.length).times do
|
2085
2116
|
spacer += ' '
|
2086
2117
|
end
|
2087
|
-
|
2088
|
-
output.push("#{k}:#{spacer}#{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}")
|
2118
|
+
output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
|
2089
2119
|
end
|
2090
2120
|
|
2091
2121
|
output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
|
2092
|
-
|
2093
|
-
output += "\n\nTotal tracked: #{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}\n"
|
2122
|
+
output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
|
2094
2123
|
output
|
2095
2124
|
end
|
2096
2125
|
end
|
@@ -2115,39 +2144,19 @@ EOS
|
|
2115
2144
|
record_tag_times(item, seconds) if record
|
2116
2145
|
return seconds.positive? ? seconds : false unless formatted
|
2117
2146
|
|
2118
|
-
return seconds.positive? ? format
|
2147
|
+
return seconds.positive? ? seconds.time_string(format: :clock) : false
|
2119
2148
|
end
|
2120
2149
|
|
2121
2150
|
false
|
2122
2151
|
end
|
2123
2152
|
|
2124
2153
|
##
|
2125
|
-
##
|
2154
|
+
## Load configuration files and updated the @config
|
2155
|
+
## attribute with a Doing::Configuration object
|
2126
2156
|
##
|
2127
|
-
## @param
|
2157
|
+
## @param filename [String] (optional) path to
|
2158
|
+
## alternative config file
|
2128
2159
|
##
|
2129
|
-
def format_time(seconds, human: false)
|
2130
|
-
return [0, 0, 0] if seconds.nil?
|
2131
|
-
|
2132
|
-
if seconds.instance_of?(String) && seconds =~ /(\d+):(\d+):(\d+)/
|
2133
|
-
h = Regexp.last_match(1)
|
2134
|
-
m = Regexp.last_match(2)
|
2135
|
-
s = Regexp.last_match(3)
|
2136
|
-
seconds = (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
|
2137
|
-
end
|
2138
|
-
minutes = (seconds / 60).to_i
|
2139
|
-
hours = (minutes / 60).to_i
|
2140
|
-
if human
|
2141
|
-
minutes = (minutes % 60).to_i
|
2142
|
-
[0, hours, minutes]
|
2143
|
-
else
|
2144
|
-
days = (hours / 24).to_i
|
2145
|
-
hours = (hours % 24).to_i
|
2146
|
-
minutes = (minutes % 60).to_i
|
2147
|
-
[days, hours, minutes]
|
2148
|
-
end
|
2149
|
-
end
|
2150
|
-
|
2151
2160
|
def configure(filename = nil)
|
2152
2161
|
if filename
|
2153
2162
|
Doing.config_with(filename, { ignore_local: true })
|
@@ -2227,6 +2236,8 @@ EOS
|
|
2227
2236
|
break
|
2228
2237
|
end
|
2229
2238
|
|
2239
|
+
logger.debug('Output:', "#{items.count} #{items.count == 1 ? 'item' : 'items'} shown")
|
2240
|
+
|
2230
2241
|
out
|
2231
2242
|
end
|
2232
2243
|
|