doing 2.1.18 → 2.1.23
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/.yardoc/checksums +19 -16
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +121 -53
- data/Gemfile.lock +11 -11
- data/README.md +1 -1
- data/Rakefile +12 -4
- data/bin/doing +297 -234
- data/docs/doc/Array.html +7 -30
- data/docs/doc/BooleanTermParser/Clause.html +3 -3
- data/docs/doc/BooleanTermParser/Operator.html +3 -3
- data/docs/doc/BooleanTermParser/Query.html +3 -3
- data/docs/doc/BooleanTermParser/QueryParser.html +3 -3
- data/docs/doc/BooleanTermParser/QueryTransformer.html +3 -3
- data/docs/doc/BooleanTermParser.html +3 -3
- data/docs/doc/Doing/Color.html +3 -3
- data/docs/doc/Doing/Completion.html +3 -3
- data/docs/doc/Doing/Configuration.html +6 -5
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +3 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +3 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +3 -3
- data/docs/doc/Doing/Errors/EmptyInput.html +3 -3
- data/docs/doc/Doing/Errors/NoResults.html +3 -3
- data/docs/doc/Doing/Errors/PluginException.html +3 -3
- data/docs/doc/Doing/Errors/UserCancelled.html +3 -3
- data/docs/doc/Doing/Errors/WrongCommand.html +3 -3
- data/docs/doc/Doing/Errors.html +3 -3
- data/docs/doc/Doing/Hooks.html +3 -3
- data/docs/doc/Doing/Item.html +3 -3
- data/docs/doc/Doing/Items.html +3 -3
- data/docs/doc/Doing/LogAdapter.html +3 -3
- data/docs/doc/Doing/Note.html +3 -3
- data/docs/doc/Doing/Pager.html +3 -3
- data/docs/doc/Doing/Plugins.html +3 -3
- data/docs/doc/Doing/Prompt.html +7 -7
- data/docs/doc/Doing/Section.html +3 -3
- data/docs/doc/Doing/TemplateString.html +4 -4
- data/docs/doc/Doing/Types.html +201 -0
- data/docs/doc/Doing/Util/Backup.html +3 -3
- data/docs/doc/Doing/Util.html +4 -7
- data/docs/doc/Doing/WWID.html +66 -8
- data/docs/doc/Doing.html +6 -6
- data/docs/doc/GLI/Commands/Help.html +185 -0
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
- data/docs/doc/GLI/Commands.html +7 -5
- data/docs/doc/GLI.html +6 -4
- data/docs/doc/Hash.html +80 -16
- data/docs/doc/Numeric.html +3 -3
- data/docs/doc/PhraseParser/Operator.html +3 -3
- data/docs/doc/PhraseParser/PhraseClause.html +3 -3
- data/docs/doc/PhraseParser/Query.html +3 -3
- data/docs/doc/PhraseParser/QueryParser.html +3 -3
- data/docs/doc/PhraseParser/QueryTransformer.html +3 -3
- data/docs/doc/PhraseParser/TermClause.html +3 -3
- data/docs/doc/PhraseParser.html +3 -3
- data/docs/doc/Status.html +3 -3
- data/docs/doc/String.html +195 -26
- data/docs/doc/Symbol.html +3 -3
- data/docs/doc/Time.html +3 -3
- data/docs/doc/_index.html +22 -8
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +4 -4
- data/docs/doc/frames.html +1 -1
- data/docs/doc/index.html +4 -4
- data/docs/doc/method_list.html +334 -270
- data/docs/doc/top-level-namespace.html +3 -3
- data/docs/index.md +1 -1
- data/doing.gemspec +1 -1
- data/doing.rdoc +173 -15
- data/lib/completion/_doing.zsh +20 -20
- data/lib/completion/doing.bash +37 -26
- data/lib/completion/doing.fish +114 -16
- data/lib/doing/array.rb +5 -4
- data/lib/doing/array_chronify.rb +4 -3
- data/lib/doing/changelog/change.rb +115 -0
- data/lib/doing/changelog/changes.rb +73 -0
- data/lib/doing/changelog/entry.rb +21 -0
- data/lib/doing/changelog/version.rb +97 -0
- data/lib/doing/changelog.rb +6 -0
- data/lib/doing/completion/fish_completion.rb +80 -11
- data/lib/doing/configuration.rb +17 -8
- data/lib/doing/hash.rb +25 -6
- data/lib/doing/help_monkey_patch.rb +31 -0
- data/lib/doing/hooks.rb +5 -1
- data/lib/doing/item.rb +10 -25
- data/lib/doing/items.rb +3 -1
- data/lib/doing/log_adapter.rb +1 -1
- data/lib/doing/pager.rb +2 -2
- data/lib/doing/plugins/export/dayone_export.rb +1 -1
- data/lib/doing/plugins/export/markdown_export.rb +1 -1
- data/lib/doing/plugins/export/template_export.rb +9 -3
- data/lib/doing/prompt.rb +4 -2
- data/lib/doing/string.rb +44 -12
- data/lib/doing/string_chronify.rb +56 -18
- data/lib/doing/template_string.rb +7 -0
- data/lib/doing/types.rb +25 -0
- data/lib/doing/util.rb +2 -1
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +93 -69
- data/lib/doing.rb +2 -0
- data/lib/examples/commands/later.rb +32 -0
- data/lib/helpers/threaded_tests.rb +286 -0
- metadata +17 -6
data/lib/doing/string.rb
CHANGED
|
@@ -20,7 +20,9 @@ module Doing
|
|
|
20
20
|
## can be separated by up to *distance* characters in
|
|
21
21
|
## haystack, spaces indicate unlimited distance.
|
|
22
22
|
##
|
|
23
|
-
## @example
|
|
23
|
+
## @example
|
|
24
|
+
## "this word".to_rx(3)
|
|
25
|
+
## # => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/
|
|
24
26
|
##
|
|
25
27
|
## @param distance [Integer] Allowed distance
|
|
26
28
|
## between characters
|
|
@@ -53,6 +55,20 @@ module Doing
|
|
|
53
55
|
Regexp.new(pattern, !case_sensitive)
|
|
54
56
|
end
|
|
55
57
|
|
|
58
|
+
def to_phrase_query
|
|
59
|
+
parser = PhraseParser::QueryParser.new
|
|
60
|
+
transformer = PhraseParser::QueryTransformer.new
|
|
61
|
+
parse_tree = parser.parse(self)
|
|
62
|
+
transformer.apply(parse_tree).to_elasticsearch
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_query
|
|
66
|
+
parser = BooleanTermParser::QueryParser.new
|
|
67
|
+
transformer = BooleanTermParser::QueryTransformer.new
|
|
68
|
+
parse_tree = parser.parse(self)
|
|
69
|
+
transformer.apply(parse_tree).to_elasticsearch
|
|
70
|
+
end
|
|
71
|
+
|
|
56
72
|
##
|
|
57
73
|
## Test string for truthiness (0, "f", "false", "n", "no" all return false, case insensitive, otherwise true)
|
|
58
74
|
##
|
|
@@ -101,13 +117,6 @@ module Doing
|
|
|
101
117
|
gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
|
|
102
118
|
end
|
|
103
119
|
|
|
104
|
-
def to_phrase_query(query)
|
|
105
|
-
parser = PhraseParser::QueryParser.new
|
|
106
|
-
transformer = PhraseParser::QueryTransformer.new
|
|
107
|
-
parse_tree = parser.parse(query)
|
|
108
|
-
transformer.apply(parse_tree).to_elasticsearch
|
|
109
|
-
end
|
|
110
|
-
|
|
111
120
|
def ignore_case(search, case_type)
|
|
112
121
|
(case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
|
|
113
122
|
end
|
|
@@ -127,13 +136,16 @@ module Doing
|
|
|
127
136
|
rx = search.to_rx(distance: distance, case_type: case_type)
|
|
128
137
|
out.gsub!(rx) { |m| m.bgyellow.black }
|
|
129
138
|
else
|
|
130
|
-
query =
|
|
139
|
+
query = search.strip.to_phrase_query
|
|
131
140
|
|
|
132
141
|
if query[:must].nil? && query[:must_not].nil?
|
|
133
142
|
query[:must] = query[:should]
|
|
134
143
|
query[:should] = []
|
|
135
144
|
end
|
|
136
|
-
|
|
145
|
+
qs = []
|
|
146
|
+
qs.concat(query[:must]) if query[:must]
|
|
147
|
+
qs.concat(query[:should]) if query[:should]
|
|
148
|
+
qs.each do |s|
|
|
137
149
|
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
138
150
|
out.gsub!(rx) { |m| m.bgyellow.black }
|
|
139
151
|
end
|
|
@@ -446,7 +458,7 @@ module Doing
|
|
|
446
458
|
## @return [String] Regular expression string
|
|
447
459
|
##
|
|
448
460
|
def wildcard_to_rx
|
|
449
|
-
gsub(/\?/, '\S').gsub(/\*/, '\S*?')
|
|
461
|
+
gsub(/\?/, '\S').gsub(/\*/, '\S*?').gsub(/\]\]/, '--')
|
|
450
462
|
end
|
|
451
463
|
|
|
452
464
|
##
|
|
@@ -455,7 +467,16 @@ module Doing
|
|
|
455
467
|
## @return [String] @string
|
|
456
468
|
##
|
|
457
469
|
def add_at
|
|
458
|
-
strip.sub(/^([+-]*)
|
|
470
|
+
strip.sub(/^([+-]*)@?/, '\1@')
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
##
|
|
474
|
+
## Removes @ prefix if needed, maintains +/- prefix
|
|
475
|
+
##
|
|
476
|
+
## @return [String] string without @ prefix
|
|
477
|
+
##
|
|
478
|
+
def remove_at
|
|
479
|
+
strip.sub(/^([+-]*)@?/, '\1')
|
|
459
480
|
end
|
|
460
481
|
|
|
461
482
|
##
|
|
@@ -682,6 +703,15 @@ module Doing
|
|
|
682
703
|
end
|
|
683
704
|
end
|
|
684
705
|
|
|
706
|
+
def to_bool
|
|
707
|
+
case self
|
|
708
|
+
when /^[yt1]/i
|
|
709
|
+
true
|
|
710
|
+
else
|
|
711
|
+
false
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
685
715
|
##
|
|
686
716
|
## Convert a string value to an appropriate type. If
|
|
687
717
|
## kind is not specified, '[one, two]' becomes an Array,
|
|
@@ -701,6 +731,8 @@ module Doing
|
|
|
701
731
|
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
|
702
732
|
when /^i/i
|
|
703
733
|
to_i
|
|
734
|
+
when /^(fa|tr)/i
|
|
735
|
+
to_bool
|
|
704
736
|
when /^f/i
|
|
705
737
|
to_f
|
|
706
738
|
when /^sy/i
|
|
@@ -26,12 +26,12 @@ module Doing
|
|
|
26
26
|
##
|
|
27
27
|
def chronify(**options)
|
|
28
28
|
now = Time.now
|
|
29
|
-
raise InvalidTimeExpression, "Invalid time expression #{inspect}" if to_s.strip == ''
|
|
29
|
+
raise Errors::InvalidTimeExpression, "Invalid time expression #{inspect}" if to_s.strip == ''
|
|
30
30
|
|
|
31
31
|
secs_ago = if match(/^(\d+)$/)
|
|
32
32
|
# plain number, assume minutes
|
|
33
33
|
Regexp.last_match(1).to_i * 60
|
|
34
|
-
elsif (m = match(/^(?:(?<day>\d+)d)?(?:(?<hour>\d+)h)?(?:(?<min>\d+)m)?$/i))
|
|
34
|
+
elsif (m = match(/^(?:(?<day>\d+)d)? *(?:(?<hour>\d+)h)? *(?:(?<min>\d+)m)?$/i))
|
|
35
35
|
# day/hour/minute format e.g. 1d2h30m
|
|
36
36
|
[[m['day'], 24 * 3600],
|
|
37
37
|
[m['hour'], 3600],
|
|
@@ -39,14 +39,23 @@ module Doing
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
if secs_ago
|
|
42
|
-
now - secs_ago
|
|
42
|
+
res = now - secs_ago
|
|
43
|
+
Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res} (#{secs_ago} seconds ago)))
|
|
43
44
|
else
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
date_string = dup
|
|
46
|
+
date_string = 'today' if date_string.match(REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
|
|
47
|
+
date_string = "#{options[:context].to_s} #{date_string}" if date_string =~ REGEX_TIME && options[:context]
|
|
48
|
+
|
|
49
|
+
res = Chronic.parse(date_string, {
|
|
50
|
+
guess: options.fetch(:guess, :begin),
|
|
51
|
+
context: options.fetch(:future, false) ? :future : :past,
|
|
52
|
+
ambiguous_time_range: 8
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res}))
|
|
49
56
|
end
|
|
57
|
+
|
|
58
|
+
res
|
|
50
59
|
end
|
|
51
60
|
|
|
52
61
|
##
|
|
@@ -152,6 +161,10 @@ module Doing
|
|
|
152
161
|
end
|
|
153
162
|
end
|
|
154
163
|
|
|
164
|
+
def is_range?
|
|
165
|
+
self =~ / (to|through|thru|(un)?til|-+) /
|
|
166
|
+
end
|
|
167
|
+
|
|
155
168
|
##
|
|
156
169
|
## Splits a range string and returns an array of
|
|
157
170
|
## DateTime objects as [start, end]. If only one date is
|
|
@@ -160,23 +173,48 @@ module Doing
|
|
|
160
173
|
## @return [Array<DateTime>] Start and end dates as
|
|
161
174
|
## array
|
|
162
175
|
## @example Process a natural language date range
|
|
163
|
-
##
|
|
176
|
+
## "mon 3pm to mon 5pm".split_date_range
|
|
164
177
|
##
|
|
165
178
|
def split_date_range
|
|
179
|
+
time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
|
|
180
|
+
range_rx = / (to|through|thru|(?:un)?til|-+) /
|
|
181
|
+
|
|
166
182
|
date_string = dup
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
|
|
184
|
+
if date_string.is_range?
|
|
185
|
+
# Do we want to differentiate between "to" and "through"?
|
|
186
|
+
# inclusive = date_string =~ / (through|thru|-+) / ? true : false
|
|
187
|
+
inclusive = true
|
|
188
|
+
|
|
189
|
+
dates = date_string.split(range_rx)
|
|
190
|
+
if dates[0].strip =~ time_rx && dates[-1].strip =~ time_rx
|
|
191
|
+
start = dates[0].strip
|
|
192
|
+
finish = dates[-1].strip
|
|
193
|
+
else
|
|
194
|
+
start = dates[0].chronify(guess: :begin, future: false)
|
|
195
|
+
finish = dates[-1].chronify(guess: inclusive ? :end : :begin, future: false)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
raise Errors::InvalidTimeExpression, 'Unrecognized date string' if start.nil? || finish.nil?
|
|
199
|
+
|
|
172
200
|
else
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
if date_string.strip =~ time_rx
|
|
202
|
+
start = date_string.strip
|
|
203
|
+
finish = nil
|
|
204
|
+
else
|
|
205
|
+
start = date_string.strip.chronify(guess: :begin, future: false)
|
|
206
|
+
finish = date_string.strip.chronify(guess: :end)
|
|
207
|
+
end
|
|
208
|
+
raise Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
|
209
|
+
|
|
175
210
|
end
|
|
176
211
|
|
|
177
|
-
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
|
178
212
|
|
|
179
|
-
|
|
213
|
+
if start.is_a? String
|
|
214
|
+
Doing.logger.debug('Parser:', "--from string interpreted as time span, from #{start || '12am'} to #{finish || '11:59pm'}")
|
|
215
|
+
else
|
|
216
|
+
Doing.logger.debug('Parser:', "date range interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
|
|
217
|
+
end
|
|
180
218
|
[start, finish]
|
|
181
219
|
end
|
|
182
220
|
end
|
|
@@ -146,6 +146,13 @@ module Doing
|
|
|
146
146
|
end
|
|
147
147
|
indent ||= placeholder =~ /^title/ ? '' : "\t"
|
|
148
148
|
prefix = m['prefix']
|
|
149
|
+
|
|
150
|
+
if placeholder =~ /^tags/
|
|
151
|
+
prefix ||= ''
|
|
152
|
+
value = value.map { |t| "#{prefix}#{t.sub(/^#{prefix}?/, '')}" }.join(' ')
|
|
153
|
+
prefix = ''
|
|
154
|
+
end
|
|
155
|
+
|
|
149
156
|
if placeholder =~ /^title/
|
|
150
157
|
color = last_color + color
|
|
151
158
|
|
data/lib/doing/types.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
module Types
|
|
5
|
+
REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i.freeze
|
|
6
|
+
REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i.freeze
|
|
7
|
+
REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/.freeze
|
|
8
|
+
REGEX_CLOCK = '(?:\d{1,2}+(?::\d{1,2}+)?(?: *(?:am|pm))?|midnight|noon)'
|
|
9
|
+
REGEX_TIME = /^#{REGEX_CLOCK}$/i.freeze
|
|
10
|
+
REGEX_DAY = /^(mon|tue|wed|thur?|fri|sat|sun)(\w+(day)?)?$/i.freeze
|
|
11
|
+
REGEX_RANGE_INDICATOR = ' +(?:to|through|thru|(?:un)?til|-+) +'
|
|
12
|
+
REGEX_RANGE = /^\S+#{REGEX_RANGE_INDICATOR}+\S+/i.freeze
|
|
13
|
+
REGEX_TIME_RANGE = /^#{REGEX_CLOCK}#{REGEX_RANGE_INDICATOR}#{REGEX_CLOCK}$/i.freeze
|
|
14
|
+
|
|
15
|
+
InvalidExportType = Class.new(RuntimeError)
|
|
16
|
+
MissingConfigFile = Class.new(RuntimeError)
|
|
17
|
+
|
|
18
|
+
TagArray = Class.new(Array)
|
|
19
|
+
TemplateName = Class.new(String)
|
|
20
|
+
DateBeginString = Class.new(DateTime)
|
|
21
|
+
DateEndString = Class.new(DateTime)
|
|
22
|
+
DateRangeString = Class.new(Array)
|
|
23
|
+
DateIntervalString = Class.new(DateTime)
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/doing/util.rb
CHANGED
|
@@ -27,7 +27,8 @@ module Doing
|
|
|
27
27
|
##
|
|
28
28
|
## Return the first valid executable from a list of commands
|
|
29
29
|
##
|
|
30
|
-
## @example
|
|
30
|
+
## @example
|
|
31
|
+
## Doing::Util.first_available_exec('bat', 'less -Xr', 'more -r', 'cat')
|
|
31
32
|
##
|
|
32
33
|
def first_available_exec(*commands)
|
|
33
34
|
commands.compact.map(&:strip).reject(&:empty?).uniq
|
data/lib/doing/version.rb
CHANGED
data/lib/doing/wwid.rb
CHANGED
|
@@ -305,6 +305,28 @@ module Doing
|
|
|
305
305
|
view
|
|
306
306
|
end
|
|
307
307
|
|
|
308
|
+
def add_with_editor(**options)
|
|
309
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
|
|
310
|
+
|
|
311
|
+
input = options[:date].strftime('%F %R | ')
|
|
312
|
+
input += options[:title]
|
|
313
|
+
input += "\n#{options[:note]}" if options[:note]
|
|
314
|
+
input = fork_editor(input).strip
|
|
315
|
+
|
|
316
|
+
d, title, note = format_input(input)
|
|
317
|
+
raise EmptyInput, 'No content' if title.empty?
|
|
318
|
+
|
|
319
|
+
if options[:ask]
|
|
320
|
+
ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
|
|
321
|
+
note.add(ask_note) unless ask_note.empty?
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
date = d.nil? ? options[:date] : d
|
|
325
|
+
finish = options[:finish_last] || false
|
|
326
|
+
add_item(title.cap_first, options[:section], { note: note, back: date, timed: finish })
|
|
327
|
+
write(@doing_file)
|
|
328
|
+
end
|
|
329
|
+
|
|
308
330
|
##
|
|
309
331
|
## Adds an entry
|
|
310
332
|
##
|
|
@@ -356,7 +378,7 @@ module Doing
|
|
|
356
378
|
|
|
357
379
|
@content.push(entry)
|
|
358
380
|
# logger.count(:added, level: :debug)
|
|
359
|
-
logger.info('New entry:', %(added "#{entry.title}" to #{section}))
|
|
381
|
+
logger.info('New entry:', %(added "#{entry.date.relative_date}: #{entry.title}" to #{section}))
|
|
360
382
|
|
|
361
383
|
Hooks.trigger :post_entry_added, self, entry.dup
|
|
362
384
|
end
|
|
@@ -454,7 +476,8 @@ module Doing
|
|
|
454
476
|
opt ||= {}
|
|
455
477
|
if item.should_finish?
|
|
456
478
|
if item.should_time?
|
|
457
|
-
item.
|
|
479
|
+
finish_date = verify_duration(item.date, Time.now, title: item.title)
|
|
480
|
+
item.title.tag!('done', value: finish_date.strftime('%F %R'))
|
|
458
481
|
else
|
|
459
482
|
item.title.tag!('done')
|
|
460
483
|
end
|
|
@@ -484,7 +507,7 @@ module Doing
|
|
|
484
507
|
end
|
|
485
508
|
|
|
486
509
|
# @content.update_item(original, item)
|
|
487
|
-
add_item(title, section, { note: note, back: opt[:date], timed:
|
|
510
|
+
add_item(title, section, { note: note, back: opt[:date], timed: false })
|
|
488
511
|
end
|
|
489
512
|
|
|
490
513
|
##
|
|
@@ -633,42 +656,19 @@ module Doing
|
|
|
633
656
|
|
|
634
657
|
opt[:time_filter] = [nil, nil]
|
|
635
658
|
if opt[:from] && !opt[:date_filter]
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if dates[0].strip =~ time_rx && dates[-1].strip =~ time_rx
|
|
641
|
-
time_start = dates[0].strip
|
|
642
|
-
time_end = dates[-1].strip
|
|
643
|
-
else
|
|
644
|
-
start = dates[0].chronify(guess: :begin)
|
|
645
|
-
finish = dates[-1].chronify(guess: :end)
|
|
646
|
-
end
|
|
647
|
-
when time_rx
|
|
648
|
-
time_start = date_string
|
|
649
|
-
time_end = nil
|
|
650
|
-
else
|
|
651
|
-
start = date_string.chronify(guess: :begin)
|
|
652
|
-
finish = false
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
if time_start
|
|
656
|
-
opt[:time_filter] = [time_start, time_end]
|
|
657
|
-
Doing.logger.debug('Parser:', "--from string interpreted as time span, from #{time_start ? time_start : '12am'} to #{time_end ? time_end : '11:59pm'}")
|
|
658
|
-
else
|
|
659
|
-
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
|
660
|
-
|
|
661
|
-
opt[:date_filter] = [start, finish]
|
|
662
|
-
Doing.logger.debug('Parser:', "--from string interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
|
|
659
|
+
if opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx
|
|
660
|
+
opt[:time_filter] = opt[:from]
|
|
661
|
+
elsif opt[:from][0].is_a?(Time)
|
|
662
|
+
opt[:date_filter] = opt[:from]
|
|
663
663
|
end
|
|
664
664
|
end
|
|
665
665
|
|
|
666
|
-
if opt[:before] =~ time_rx
|
|
666
|
+
if opt[:before].is_a?(String) && opt[:before] =~ time_rx
|
|
667
667
|
opt[:time_filter][1] = opt[:before]
|
|
668
668
|
opt[:before] = nil
|
|
669
669
|
end
|
|
670
670
|
|
|
671
|
-
if opt[:after] =~ time_rx
|
|
671
|
+
if opt[:after].is_a?(String) && opt[:after] =~ time_rx
|
|
672
672
|
opt[:time_filter][0] = opt[:after]
|
|
673
673
|
opt[:after] = nil
|
|
674
674
|
end
|
|
@@ -734,7 +734,7 @@ module Doing
|
|
|
734
734
|
start_time = start_string.chronify(guess: :begin)
|
|
735
735
|
|
|
736
736
|
end_string = if opt[:time_filter][1].nil?
|
|
737
|
-
"#{item.date.next_day.strftime('%Y-%m-%d')} 12am"
|
|
737
|
+
"#{item.date.to_datetime.next_day.strftime('%Y-%m-%d')} 12am"
|
|
738
738
|
else
|
|
739
739
|
"#{item.date.strftime('%Y-%m-%d')} #{opt[:time_filter][1]}"
|
|
740
740
|
end
|
|
@@ -753,22 +753,26 @@ module Doing
|
|
|
753
753
|
end
|
|
754
754
|
|
|
755
755
|
if keep && opt[:before]
|
|
756
|
-
|
|
757
|
-
if
|
|
758
|
-
cutoff = "#{item.date.strftime('%Y-%m-%d')} #{
|
|
756
|
+
before = opt[:before]
|
|
757
|
+
if before =~ time_rx
|
|
758
|
+
cutoff = "#{item.date.strftime('%Y-%m-%d')} #{before}".chronify(guess: :begin)
|
|
759
|
+
elsif before.is_a?(String)
|
|
760
|
+
cutoff = before.chronify(guess: :begin)
|
|
759
761
|
else
|
|
760
|
-
cutoff =
|
|
762
|
+
cutoff = before
|
|
761
763
|
end
|
|
762
764
|
keep = cutoff && item.date <= cutoff
|
|
763
765
|
keep = opt[:not] ? !keep : keep
|
|
764
766
|
end
|
|
765
767
|
|
|
766
768
|
if keep && opt[:after]
|
|
767
|
-
|
|
768
|
-
if
|
|
769
|
-
cutoff = "#{item.date.strftime('%Y-%m-%d')} #{
|
|
769
|
+
after = opt[:after]
|
|
770
|
+
if after =~ time_rx
|
|
771
|
+
cutoff = "#{item.date.strftime('%Y-%m-%d')} #{after}".chronify(guess: :end)
|
|
772
|
+
elsif after.is_a?(String)
|
|
773
|
+
cutoff = after.chronify(guess: :end)
|
|
770
774
|
else
|
|
771
|
-
cutoff =
|
|
775
|
+
cutoff = after
|
|
772
776
|
end
|
|
773
777
|
keep = cutoff && item.date >= cutoff
|
|
774
778
|
keep = opt[:not] ? !keep : keep
|
|
@@ -934,6 +938,7 @@ module Doing
|
|
|
934
938
|
actions = [
|
|
935
939
|
'add tag',
|
|
936
940
|
'remove tag',
|
|
941
|
+
'autotag',
|
|
937
942
|
'cancel',
|
|
938
943
|
'delete',
|
|
939
944
|
'finish',
|
|
@@ -960,6 +965,8 @@ module Doing
|
|
|
960
965
|
opt[:resume] = true
|
|
961
966
|
when /reset/
|
|
962
967
|
opt[:reset] = true
|
|
968
|
+
when /autotag/
|
|
969
|
+
opt[:autotag] = true
|
|
963
970
|
when /(add|remove) tag/
|
|
964
971
|
type = action =~ /^add/ ? 'add' : 'remove'
|
|
965
972
|
raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
|
|
@@ -1071,6 +1078,21 @@ module Doing
|
|
|
1071
1078
|
end
|
|
1072
1079
|
end
|
|
1073
1080
|
|
|
1081
|
+
if opt[:autotag]
|
|
1082
|
+
items.map! do |i|
|
|
1083
|
+
new_title = autotag(i.title)
|
|
1084
|
+
if new_title == i.title
|
|
1085
|
+
logger.count(:skipped, level: :debug, message: '%count unchaged %items')
|
|
1086
|
+
# logger.debug('Autotag:', 'No changes')
|
|
1087
|
+
else
|
|
1088
|
+
logger.count(:added_tags)
|
|
1089
|
+
logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
|
|
1090
|
+
i.title = new_title
|
|
1091
|
+
Hooks.trigger :post_entry_updated, self, i
|
|
1092
|
+
end
|
|
1093
|
+
end
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1074
1096
|
if opt[:tag]
|
|
1075
1097
|
tag = opt[:tag]
|
|
1076
1098
|
items.map! do |i|
|
|
@@ -1098,10 +1120,7 @@ module Doing
|
|
|
1098
1120
|
|
|
1099
1121
|
return unless opt[:output]
|
|
1100
1122
|
|
|
1101
|
-
items.
|
|
1102
|
-
i.title = "#{i.title} @project(#{i.section})"
|
|
1103
|
-
i
|
|
1104
|
-
end
|
|
1123
|
+
items.each { |i| i.title = "#{i.title} @section(#{i.section})" }
|
|
1105
1124
|
|
|
1106
1125
|
export_items = Items.new
|
|
1107
1126
|
export_items.concat(items)
|
|
@@ -1138,6 +1157,8 @@ module Doing
|
|
|
1138
1157
|
def verify_duration(date, finish_date, title: nil)
|
|
1139
1158
|
max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
|
|
1140
1159
|
max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
|
|
1160
|
+
date = date.chronify(guess: :end, context: :today) if finish_date.is_a?(String)
|
|
1161
|
+
|
|
1141
1162
|
elapsed = finish_date - date
|
|
1142
1163
|
|
|
1143
1164
|
if max_elapsed.positive? && (elapsed > max_elapsed)
|
|
@@ -1244,7 +1265,7 @@ module Doing
|
|
|
1244
1265
|
|
|
1245
1266
|
tag = tag.strip
|
|
1246
1267
|
|
|
1247
|
-
if tag =~ /^done$/
|
|
1268
|
+
if tag =~ /^done$/ && opt[:date] && item.should_time?
|
|
1248
1269
|
max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
|
|
1249
1270
|
max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
|
|
1250
1271
|
elapsed = done_date - item.date
|
|
@@ -1316,7 +1337,7 @@ module Doing
|
|
|
1316
1337
|
##
|
|
1317
1338
|
## @return [Item] the next chronological item in the index
|
|
1318
1339
|
##
|
|
1319
|
-
def next_item(item, options)
|
|
1340
|
+
def next_item(item, options = {})
|
|
1320
1341
|
options ||= {}
|
|
1321
1342
|
items = filter_items(Items.new, opt: options)
|
|
1322
1343
|
|
|
@@ -1595,6 +1616,7 @@ module Doing
|
|
|
1595
1616
|
'duration' => @config['duration'],
|
|
1596
1617
|
'interval_format' => @config['interval_format']
|
|
1597
1618
|
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
|
1619
|
+
|
|
1598
1620
|
opt[:duration] ||= cfg['duration'] || false
|
|
1599
1621
|
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
|
1600
1622
|
opt[:count] ||= 0
|
|
@@ -1640,9 +1662,9 @@ module Doing
|
|
|
1640
1662
|
opt[:menu] = !opt[:force]
|
|
1641
1663
|
opt[:query] = '' # opt[:search]
|
|
1642
1664
|
opt[:multiple] = true
|
|
1643
|
-
selected = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, **opt)
|
|
1665
|
+
selected = Prompt.choose_from_items(items.reverse, include_section: opt[:section] =~ /^all$/i, **opt)
|
|
1644
1666
|
|
|
1645
|
-
raise NoResults, 'no items selected' if selected.empty?
|
|
1667
|
+
raise NoResults, 'no items selected' if selected.nil? || selected.empty?
|
|
1646
1668
|
|
|
1647
1669
|
act_on(selected, opt)
|
|
1648
1670
|
return
|
|
@@ -1697,7 +1719,7 @@ module Doing
|
|
|
1697
1719
|
opt[:totals] ||= false
|
|
1698
1720
|
opt[:sort_tags] ||= false
|
|
1699
1721
|
|
|
1700
|
-
cfg = @config['templates'][
|
|
1722
|
+
cfg = @config['templates'][opt[:config_template]].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
|
1701
1723
|
'wrap_width' => @config['wrap_width'] || 0,
|
|
1702
1724
|
'date_format' => @config['default_date_format'],
|
|
1703
1725
|
'order' => @config['order'] || 'asc',
|
|
@@ -1706,6 +1728,8 @@ module Doing
|
|
|
1706
1728
|
'interval_format' => @config['interval_format']
|
|
1707
1729
|
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
|
1708
1730
|
|
|
1731
|
+
template = opt[:template] || cfg['template']
|
|
1732
|
+
|
|
1709
1733
|
opt[:duration] ||= cfg['duration'] || false
|
|
1710
1734
|
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
|
1711
1735
|
|
|
@@ -1721,13 +1745,13 @@ module Doing
|
|
|
1721
1745
|
output: output,
|
|
1722
1746
|
section: opt[:section],
|
|
1723
1747
|
sort_tags: opt[:sort_tags],
|
|
1724
|
-
template:
|
|
1748
|
+
template: template,
|
|
1725
1749
|
times: times,
|
|
1726
1750
|
today: true,
|
|
1727
1751
|
totals: opt[:totals],
|
|
1728
1752
|
wrap_width: cfg['wrap_width'],
|
|
1729
1753
|
tags_color: cfg['tags_color'],
|
|
1730
|
-
config_template:
|
|
1754
|
+
config_template: opt[:config_template]
|
|
1731
1755
|
}
|
|
1732
1756
|
list_section(options)
|
|
1733
1757
|
end
|
|
@@ -1747,7 +1771,7 @@ module Doing
|
|
|
1747
1771
|
opt[:sort_tags] ||= false
|
|
1748
1772
|
section = guess_section(section)
|
|
1749
1773
|
# :date_filter expects an array with start and end date
|
|
1750
|
-
dates =
|
|
1774
|
+
dates = dates.split_date_range if dates.instance_of?(String)
|
|
1751
1775
|
|
|
1752
1776
|
list_section({
|
|
1753
1777
|
section: section,
|
|
@@ -1759,7 +1783,8 @@ module Doing
|
|
|
1759
1783
|
totals: opt[:totals],
|
|
1760
1784
|
duration: opt[:duration],
|
|
1761
1785
|
sort_tags: opt[:sort_tags],
|
|
1762
|
-
|
|
1786
|
+
template: opt[:template],
|
|
1787
|
+
config_template: opt[:config_template]
|
|
1763
1788
|
})
|
|
1764
1789
|
end
|
|
1765
1790
|
|
|
@@ -1794,7 +1819,8 @@ module Doing
|
|
|
1794
1819
|
times: times,
|
|
1795
1820
|
totals: opt[:totals],
|
|
1796
1821
|
yesterday: true,
|
|
1797
|
-
config_template: 'today'
|
|
1822
|
+
config_template: opt[:config_template] || 'today',
|
|
1823
|
+
template: opt[:template]
|
|
1798
1824
|
}
|
|
1799
1825
|
|
|
1800
1826
|
list_section(options)
|
|
@@ -1813,7 +1839,7 @@ module Doing
|
|
|
1813
1839
|
opt[:totals] ||= false
|
|
1814
1840
|
opt[:sort_tags] ||= false
|
|
1815
1841
|
|
|
1816
|
-
cfg = @config['templates'][
|
|
1842
|
+
cfg = @config['templates'][opt[:config_template]].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
|
1817
1843
|
'wrap_width' => @config['wrap_width'] || 0,
|
|
1818
1844
|
'date_format' => @config['default_date_format'],
|
|
1819
1845
|
'order' => @config['order'] || 'asc',
|
|
@@ -1831,10 +1857,9 @@ module Doing
|
|
|
1831
1857
|
opt[:wrap_width] = cfg['wrap_width']
|
|
1832
1858
|
opt[:count] = count
|
|
1833
1859
|
opt[:format] = cfg['date_format']
|
|
1834
|
-
opt[:template] = cfg['template']
|
|
1860
|
+
opt[:template] = opt[:template] || cfg['template']
|
|
1835
1861
|
opt[:order] = 'asc'
|
|
1836
1862
|
opt[:times] = times
|
|
1837
|
-
opt[:config_template] = 'recent'
|
|
1838
1863
|
|
|
1839
1864
|
list_section(opt)
|
|
1840
1865
|
end
|
|
@@ -1847,7 +1872,7 @@ module Doing
|
|
|
1847
1872
|
##
|
|
1848
1873
|
def last(times: true, section: nil, options: {})
|
|
1849
1874
|
section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
|
|
1850
|
-
cfg = @config['templates'][
|
|
1875
|
+
cfg = @config['templates'][options[:config_template]].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
|
1851
1876
|
'wrap_width' => @config['wrap_width'] || 0,
|
|
1852
1877
|
'date_format' => @config['default_date_format'],
|
|
1853
1878
|
'order' => @config['order'] || 'asc',
|
|
@@ -1859,19 +1884,19 @@ module Doing
|
|
|
1859
1884
|
options[:interval_format] ||= cfg['interval_format'] || 'text'
|
|
1860
1885
|
|
|
1861
1886
|
opts = {
|
|
1862
|
-
|
|
1863
|
-
|
|
1887
|
+
case: options[:case],
|
|
1888
|
+
config_template: 'last',
|
|
1864
1889
|
count: 1,
|
|
1865
|
-
|
|
1866
|
-
template: cfg['template'],
|
|
1867
|
-
times: times,
|
|
1890
|
+
delete: options[:delete],
|
|
1868
1891
|
duration: options[:duration],
|
|
1892
|
+
format: cfg['date_format'],
|
|
1869
1893
|
interval_format: options[:interval_format],
|
|
1870
|
-
case: options[:case],
|
|
1871
1894
|
not: options[:negate],
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1895
|
+
section: section,
|
|
1896
|
+
template: options[:template] || cfg['template'],
|
|
1897
|
+
times: times,
|
|
1898
|
+
val: options[:val],
|
|
1899
|
+
wrap_width: cfg['wrap_width']
|
|
1875
1900
|
}
|
|
1876
1901
|
|
|
1877
1902
|
if options[:tag]
|
|
@@ -2280,7 +2305,6 @@ EOS
|
|
|
2280
2305
|
|
|
2281
2306
|
section_items = @content.in_section(section)
|
|
2282
2307
|
max = section_items.count - count.to_i
|
|
2283
|
-
moved_items = []
|
|
2284
2308
|
|
|
2285
2309
|
counter = 0
|
|
2286
2310
|
|
data/lib/doing.rb
CHANGED
|
@@ -23,7 +23,9 @@ require 'tty-markdown'
|
|
|
23
23
|
require 'tty-reader'
|
|
24
24
|
require 'tty-screen'
|
|
25
25
|
|
|
26
|
+
require_relative 'doing/changelog'
|
|
26
27
|
require_relative 'doing/hash'
|
|
28
|
+
require_relative 'doing/types'
|
|
27
29
|
require_relative 'doing/colors'
|
|
28
30
|
require_relative 'doing/template_string'
|
|
29
31
|
require_relative 'doing/string'
|