doing 2.1.12 → 2.1.16
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 +16 -14
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +67 -0
- data/Gemfile.lock +9 -2
- data/README.md +56 -19
- data/bin/doing +317 -113
- 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 +7 -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 +337 -14
- data/docs/doc/Doing/Items.html +66 -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 +84 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +214 -35
- 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 +881 -138
- 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 +408 -256
- data/docs/doc/top-level-namespace.html +2 -2
- data/docs/index.md +56 -19
- data/doing.gemspec +2 -0
- data/doing.rdoc +257 -48
- data/example_plugin.rb +2 -4
- data/lib/completion/_doing.zsh +31 -27
- data/lib/completion/doing.bash +50 -39
- data/lib/completion/doing.fish +37 -7
- data/lib/doing/array_chronify.rb +57 -0
- data/lib/doing/configuration.rb +4 -1
- data/lib/doing/item.rb +176 -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/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 +54 -1
- data/lib/doing/string.rb +97 -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 +128 -103
- 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|
|
@@ -640,6 +621,7 @@ module Doing
|
|
640
621
|
## @option opt [Boolean] :yesterday (false) limit to entries from yesterday
|
641
622
|
## @option opt [Number] :count (0) max entries to return
|
642
623
|
## @option opt [String] :age (new) 'old' or 'new'
|
624
|
+
## @option opt [Array] :val (nil) Array of tag value queries
|
643
625
|
##
|
644
626
|
def filter_items(items = Items.new, opt: {})
|
645
627
|
time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
|
@@ -701,6 +683,16 @@ module Doing
|
|
701
683
|
keep = false if finished
|
702
684
|
end
|
703
685
|
|
686
|
+
if keep && opt[:val]&.count&.positive?
|
687
|
+
bool = opt[:bool].normalize_bool if opt[:bool]
|
688
|
+
bool ||= :and
|
689
|
+
bool = :and if bool == :pattern
|
690
|
+
|
691
|
+
val_match = opt[:val].nil? || opt[:val].empty? ? true : item.tag_values?(opt[:val], bool)
|
692
|
+
keep = false unless val_match
|
693
|
+
keep = opt[:not] ? !keep : keep
|
694
|
+
end
|
695
|
+
|
704
696
|
if keep && opt[:tag]
|
705
697
|
opt[:tag_bool] = opt[:bool].normalize_bool if opt[:bool]
|
706
698
|
opt[:tag_bool] ||= :and
|
@@ -817,6 +809,7 @@ module Doing
|
|
817
809
|
end
|
818
810
|
|
819
811
|
def edit_items(items)
|
812
|
+
items.sort_by! { |i| i.date }
|
820
813
|
editable_items = []
|
821
814
|
|
822
815
|
items.each do |i|
|
@@ -825,16 +818,16 @@ module Doing
|
|
825
818
|
editable += "\n#{old_note}" unless old_note.nil?
|
826
819
|
editable_items << editable
|
827
820
|
end
|
828
|
-
divider = "
|
821
|
+
divider = "-----------"
|
829
822
|
notice =<<~EONOTICE
|
830
823
|
# - You may delete entries, but leave all divider lines (---) in place.
|
831
824
|
# - Start and @done dates replaced with a time string (yesterday 3pm) will
|
832
825
|
# be parsed automatically. Do not delete the pipe (|) between start date
|
833
826
|
# and entry title.
|
834
827
|
EONOTICE
|
835
|
-
input = "#{editable_items.map(&:strip).join(divider)}\n\n#{notice}"
|
828
|
+
input = "#{editable_items.map(&:strip).join("\n#{divider}\n")}\n\n#{notice}"
|
836
829
|
|
837
|
-
new_items = fork_editor(input).split(
|
830
|
+
new_items = fork_editor(input).split(/^#{divider}/).map(&:strip)
|
838
831
|
|
839
832
|
new_items.each_with_index do |new_item, i|
|
840
833
|
input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
|
@@ -843,7 +836,7 @@ module Doing
|
|
843
836
|
if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
|
844
837
|
deleted = @content.delete_item(items[i], single: new_items.count == 1)
|
845
838
|
Hooks.trigger :post_entry_removed, self, deleted
|
846
|
-
Doing.logger.
|
839
|
+
Doing.logger.info('Deleted:', deleted.title)
|
847
840
|
else
|
848
841
|
date, title, note = format_input(new_item)
|
849
842
|
|
@@ -883,11 +876,11 @@ module Doing
|
|
883
876
|
opt[:search] = search
|
884
877
|
end
|
885
878
|
|
886
|
-
opt[:query] = opt[:search] if opt[:search] && !opt[:query]
|
887
|
-
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]
|
888
881
|
opt[:multiple] = true
|
889
882
|
opt[:show_if_single] = true
|
890
|
-
filter_options = %i[after before case date_filter from fuzzy not search section].each_with_object({}) {
|
883
|
+
filter_options = %i[after before case date_filter from fuzzy not search section val].each_with_object({}) {
|
891
884
|
|k, hsh| hsh[k] = opt[k]
|
892
885
|
}
|
893
886
|
items = filter_items(Items.new, opt: filter_options)
|
@@ -971,8 +964,14 @@ module Doing
|
|
971
964
|
type = action =~ /^add/ ? 'add' : 'remove'
|
972
965
|
raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
|
973
966
|
|
974
|
-
|
975
|
-
|
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
|
976
975
|
next if tag =~ /^ *$/
|
977
976
|
|
978
977
|
opt[:tag] = tag.strip.sub(/^@/, '')
|
@@ -987,15 +986,16 @@ module Doing
|
|
987
986
|
'--no-sort',
|
988
987
|
'--info=hidden'
|
989
988
|
])
|
990
|
-
next if
|
989
|
+
next if output_format =~ /^ *$/
|
991
990
|
|
992
991
|
raise UserCancelled unless output_format
|
993
992
|
|
994
993
|
opt[:output] = output_format.strip
|
995
994
|
res = opt[:force] ? false : Prompt.yn('Save to file?', default_response: 'n')
|
996
995
|
if res
|
997
|
-
print "#{yellow('File path/name: ')}#{reset}"
|
998
|
-
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')
|
999
999
|
next if filename.empty?
|
1000
1000
|
|
1001
1001
|
opt[:save_to] = filename
|
@@ -1075,6 +1075,7 @@ module Doing
|
|
1075
1075
|
tag = opt[:tag]
|
1076
1076
|
items.map! do |i|
|
1077
1077
|
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1078
|
+
i.expand_date_tags(@config['date_tags'])
|
1078
1079
|
Hooks.trigger :post_entry_updated, self, i
|
1079
1080
|
end
|
1080
1081
|
end
|
@@ -1102,10 +1103,10 @@ module Doing
|
|
1102
1103
|
i
|
1103
1104
|
end
|
1104
1105
|
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
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' }
|
1109
1110
|
|
1110
1111
|
if opt[:output] =~ /doing/
|
1111
1112
|
options[:output] = 'template'
|
@@ -1115,7 +1116,7 @@ module Doing
|
|
1115
1116
|
options[:template] = opt[:template] || nil
|
1116
1117
|
end
|
1117
1118
|
|
1118
|
-
output = list_section(options) # hooked
|
1119
|
+
output = list_section(options, items: export_items) # hooked
|
1119
1120
|
|
1120
1121
|
if opt[:save_to]
|
1121
1122
|
file = File.expand_path(opt[:save_to])
|
@@ -1134,6 +1135,26 @@ module Doing
|
|
1134
1135
|
end
|
1135
1136
|
end
|
1136
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
|
+
|
1137
1158
|
##
|
1138
1159
|
## Tag the last entry or X entries
|
1139
1160
|
##
|
@@ -1172,6 +1193,19 @@ module Doing
|
|
1172
1193
|
|
1173
1194
|
raise NoResults, 'no items matched your search' if items.empty?
|
1174
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
|
+
|
1175
1209
|
items.each do |item|
|
1176
1210
|
added = []
|
1177
1211
|
removed = []
|
@@ -1195,21 +1229,8 @@ module Doing
|
|
1195
1229
|
else
|
1196
1230
|
next_entry.date - 60
|
1197
1231
|
end
|
1198
|
-
elsif opt[:took]
|
1199
|
-
if item.date + opt[:took] > Time.now
|
1200
|
-
item.date = Time.now - opt[:took]
|
1201
|
-
done_date = Time.now
|
1202
|
-
else
|
1203
|
-
done_date = item.date + opt[:took]
|
1204
|
-
end
|
1205
|
-
elsif opt[:back]
|
1206
|
-
done_date = if opt[:back].is_a? Integer
|
1207
|
-
item.date + opt[:back]
|
1208
|
-
else
|
1209
|
-
item.date + (opt[:back] - item.date)
|
1210
|
-
end
|
1211
1232
|
else
|
1212
|
-
done_date =
|
1233
|
+
done_date = item.calculate_end_date(opt)
|
1213
1234
|
end
|
1214
1235
|
|
1215
1236
|
opt[:tags].each do |tag|
|
@@ -1220,15 +1241,39 @@ module Doing
|
|
1220
1241
|
next
|
1221
1242
|
end
|
1222
1243
|
|
1244
|
+
|
1223
1245
|
tag = tag.strip
|
1224
|
-
|
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
|
+
|
1266
|
+
if opt[:remove] || opt[:rename] || opt[:value]
|
1225
1267
|
rename_to = nil
|
1226
|
-
if opt[:
|
1268
|
+
if opt[:value]
|
1269
|
+
rename_to = tag
|
1270
|
+
elsif opt[:rename]
|
1227
1271
|
rename_to = tag
|
1228
1272
|
tag = opt[:rename]
|
1229
1273
|
end
|
1230
1274
|
old_title = item.title.dup
|
1231
|
-
|
1275
|
+
force = opt[:value].nil? ? false : true
|
1276
|
+
item.title.tag!(tag, remove: opt[:remove], rename_to: rename_to, regex: opt[:regex], value: opt[:value], force: force)
|
1232
1277
|
if old_title != item.title
|
1233
1278
|
removed << tag
|
1234
1279
|
added << rename_to if rename_to
|
@@ -1255,6 +1300,7 @@ module Doing
|
|
1255
1300
|
logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
|
1256
1301
|
end
|
1257
1302
|
|
1303
|
+
item.expand_date_tags(@config['date_tags'])
|
1258
1304
|
Hooks.trigger :post_entry_updated, self, item
|
1259
1305
|
end
|
1260
1306
|
|
@@ -1823,7 +1869,8 @@ module Doing
|
|
1823
1869
|
case: options[:case],
|
1824
1870
|
not: options[:negate],
|
1825
1871
|
config_template: 'last',
|
1826
|
-
delete: options[:delete]
|
1872
|
+
delete: options[:delete],
|
1873
|
+
val: options[:val]
|
1827
1874
|
}
|
1828
1875
|
|
1829
1876
|
if options[:tag]
|
@@ -1987,7 +2034,7 @@ module Doing
|
|
1987
2034
|
EOS
|
1988
2035
|
sorted_tags_data.reverse.each do |k, v|
|
1989
2036
|
if v > 0
|
1990
|
-
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{
|
2037
|
+
output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}</td></tr>\n"
|
1991
2038
|
end
|
1992
2039
|
end
|
1993
2040
|
tail = <<EOS
|
@@ -1998,7 +2045,7 @@ EOS
|
|
1998
2045
|
<tfoot>
|
1999
2046
|
<tr>
|
2000
2047
|
<td style="text-align:left;"><strong>Total</strong></td>
|
2001
|
-
<td style="text-align:left;">#{
|
2048
|
+
<td style="text-align:left;">#{total.time_string(format: :clock)}</td>
|
2002
2049
|
</tr>
|
2003
2050
|
</tfoot>
|
2004
2051
|
</table>
|
@@ -2013,7 +2060,7 @@ EOS
|
|
2013
2060
|
EOS
|
2014
2061
|
sorted_tags_data.reverse.each do |k, v|
|
2015
2062
|
if v > 0
|
2016
|
-
output += "| #{' ' * (pad - k.length)}#{k} | #{
|
2063
|
+
output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)} |\n"
|
2017
2064
|
end
|
2018
2065
|
end
|
2019
2066
|
tail = "[Tag Totals]"
|
@@ -2021,11 +2068,10 @@ EOS
|
|
2021
2068
|
when :json
|
2022
2069
|
output = []
|
2023
2070
|
sorted_tags_data.reverse.each do |k, v|
|
2024
|
-
d, h, m = format_time(v)
|
2025
2071
|
output << {
|
2026
2072
|
'tag' => k,
|
2027
2073
|
'seconds' => v,
|
2028
|
-
'formatted' => format
|
2074
|
+
'formatted' => v.time_string(format: :clock)
|
2029
2075
|
}
|
2030
2076
|
end
|
2031
2077
|
output
|
@@ -2036,8 +2082,7 @@ EOS
|
|
2036
2082
|
(max - k.length).times do
|
2037
2083
|
spacer += ' '
|
2038
2084
|
end
|
2039
|
-
|
2040
|
-
output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
|
2085
|
+
output.push("┃ #{spacer}#{k}:#{v.time_string(format: :hm)} ┃")
|
2041
2086
|
end
|
2042
2087
|
|
2043
2088
|
header = '┏━━ Tag Totals '
|
@@ -2050,14 +2095,14 @@ EOS
|
|
2050
2095
|
(max + 12).times { divider += '━' }
|
2051
2096
|
divider += '┫'
|
2052
2097
|
output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
|
2053
|
-
d, h, m = format_time(total, human: true)
|
2054
2098
|
output += "\n#{divider}"
|
2055
2099
|
spacer = ''
|
2056
2100
|
(max - 6).times do
|
2057
2101
|
spacer += ' '
|
2058
2102
|
end
|
2103
|
+
total_time = total.time_string(format: :hm)
|
2059
2104
|
total = "┃ #{spacer}total: "
|
2060
|
-
total +=
|
2105
|
+
total += total_time
|
2061
2106
|
total += ' ┃'
|
2062
2107
|
output += "\n#{total}"
|
2063
2108
|
output += "\n#{footer}"
|
@@ -2069,13 +2114,11 @@ EOS
|
|
2069
2114
|
(max - k.length).times do
|
2070
2115
|
spacer += ' '
|
2071
2116
|
end
|
2072
|
-
|
2073
|
-
output.push("#{k}:#{spacer}#{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}")
|
2117
|
+
output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
|
2074
2118
|
end
|
2075
2119
|
|
2076
2120
|
output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
|
2077
|
-
|
2078
|
-
output += "\n\nTotal tracked: #{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}\n"
|
2121
|
+
output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
|
2079
2122
|
output
|
2080
2123
|
end
|
2081
2124
|
end
|
@@ -2100,39 +2143,19 @@ EOS
|
|
2100
2143
|
record_tag_times(item, seconds) if record
|
2101
2144
|
return seconds.positive? ? seconds : false unless formatted
|
2102
2145
|
|
2103
|
-
return seconds.positive? ? format
|
2146
|
+
return seconds.positive? ? seconds.time_string(format: :clock) : false
|
2104
2147
|
end
|
2105
2148
|
|
2106
2149
|
false
|
2107
2150
|
end
|
2108
2151
|
|
2109
2152
|
##
|
2110
|
-
##
|
2153
|
+
## Load configuration files and updated the @config
|
2154
|
+
## attribute with a Doing::Configuration object
|
2111
2155
|
##
|
2112
|
-
## @param
|
2156
|
+
## @param filename [String] (optional) path to
|
2157
|
+
## alternative config file
|
2113
2158
|
##
|
2114
|
-
def format_time(seconds, human: false)
|
2115
|
-
return [0, 0, 0] if seconds.nil?
|
2116
|
-
|
2117
|
-
if seconds.instance_of?(String) && seconds =~ /(\d+):(\d+):(\d+)/
|
2118
|
-
h = Regexp.last_match(1)
|
2119
|
-
m = Regexp.last_match(2)
|
2120
|
-
s = Regexp.last_match(3)
|
2121
|
-
seconds = (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
|
2122
|
-
end
|
2123
|
-
minutes = (seconds / 60).to_i
|
2124
|
-
hours = (minutes / 60).to_i
|
2125
|
-
if human
|
2126
|
-
minutes = (minutes % 60).to_i
|
2127
|
-
[0, hours, minutes]
|
2128
|
-
else
|
2129
|
-
days = (hours / 24).to_i
|
2130
|
-
hours = (hours % 24).to_i
|
2131
|
-
minutes = (minutes % 60).to_i
|
2132
|
-
[days, hours, minutes]
|
2133
|
-
end
|
2134
|
-
end
|
2135
|
-
|
2136
2159
|
def configure(filename = nil)
|
2137
2160
|
if filename
|
2138
2161
|
Doing.config_with(filename, { ignore_local: true })
|
@@ -2212,6 +2235,8 @@ EOS
|
|
2212
2235
|
break
|
2213
2236
|
end
|
2214
2237
|
|
2238
|
+
logger.debug('Output:', "#{items.count} #{items.count == 1 ? 'item' : 'items'} shown")
|
2239
|
+
|
2215
2240
|
out
|
2216
2241
|
end
|
2217
2242
|
|