doing 2.1.17 → 2.1.22
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 +15 -14
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +117 -53
- data/Gemfile.lock +11 -11
- data/README.md +1 -1
- data/Rakefile +12 -4
- data/bin/doing +161 -205
- data/docs/doc/Array.html +9 -37
- 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 +4 -3
- 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/Util/Backup.html +3 -3
- data/docs/doc/Doing/Util.html +3 -3
- data/docs/doc/Doing/WWID.html +66 -8
- data/docs/doc/Doing.html +6 -6
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
- data/docs/doc/GLI/Commands.html +3 -3
- data/docs/doc/GLI.html +3 -3
- data/docs/doc/Hash.html +78 -6
- 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 +156 -17
- data/docs/doc/Symbol.html +3 -3
- data/docs/doc/Time.html +3 -3
- data/docs/doc/_index.html +23 -16
- 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 +331 -283
- data/docs/doc/top-level-namespace.html +3 -3
- data/doing.gemspec +1 -1
- data/doing.rdoc +26 -12
- data/lib/completion/_doing.zsh +5 -5
- data/lib/completion/doing.bash +8 -8
- data/lib/completion/doing.fish +93 -15
- data/lib/doing/array.rb +5 -4
- data/lib/doing/array_chronify.rb +4 -3
- data/lib/doing/completion/fish_completion.rb +80 -11
- data/lib/doing/configuration.rb +2 -1
- data/lib/doing/hash.rb +22 -4
- data/lib/doing/item.rb +2 -2
- 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 +8 -2
- data/lib/doing/prompt.rb +4 -2
- data/lib/doing/string.rb +25 -2
- data/lib/doing/string_chronify.rb +55 -17
- data/lib/doing/template_string.rb +7 -0
- data/lib/doing/types.rb +23 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +71 -50
- data/lib/doing.rb +1 -0
- data/lib/examples/commands/later.rb +32 -0
- data/lib/helpers/threaded_tests.rb +273 -0
- metadata +9 -6
data/lib/doing/hash.rb
CHANGED
|
@@ -6,14 +6,32 @@ module Doing
|
|
|
6
6
|
##
|
|
7
7
|
## Freeze all values in a hash
|
|
8
8
|
##
|
|
9
|
-
## @return
|
|
9
|
+
## @return Hash with all values frozen
|
|
10
10
|
##
|
|
11
11
|
def deep_freeze
|
|
12
|
-
|
|
12
|
+
chilled = {}
|
|
13
|
+
each do |k, v|
|
|
14
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_freeze : v.freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
chilled.freeze
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
def deep_freeze!
|
|
16
|
-
replace deep_freeze
|
|
21
|
+
replace deep_thaw.deep_freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def deep_thaw
|
|
25
|
+
chilled = {}
|
|
26
|
+
each do |k, v|
|
|
27
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_thaw : v.dup
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
chilled.dup
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def deep_thaw!
|
|
34
|
+
replace deep_thaw
|
|
17
35
|
end
|
|
18
36
|
|
|
19
37
|
# Turn all keys into string
|
|
@@ -38,7 +56,7 @@ module Doing
|
|
|
38
56
|
#
|
|
39
57
|
def deep_set(path, value)
|
|
40
58
|
if path.count == 1
|
|
41
|
-
|
|
59
|
+
unless value.nil? || value =~ /^ *$/
|
|
42
60
|
self[path[0]] = value
|
|
43
61
|
else
|
|
44
62
|
delete(path[0])
|
data/lib/doing/item.rb
CHANGED
data/lib/doing/items.rb
CHANGED
|
@@ -131,7 +131,9 @@ module Doing
|
|
|
131
131
|
out = []
|
|
132
132
|
@sections.each do |section|
|
|
133
133
|
out.push(section.original)
|
|
134
|
-
in_section(section.title).
|
|
134
|
+
items = in_section(section.title).sort_by { |i| i.date }
|
|
135
|
+
items.reverse! if Doing.config.settings['doing_file_sort'].normalize_order == 'desc'
|
|
136
|
+
items.each { |item| out.push(item.to_s)}
|
|
135
137
|
end
|
|
136
138
|
|
|
137
139
|
out.join("\n")
|
data/lib/doing/log_adapter.rb
CHANGED
|
@@ -356,7 +356,7 @@ module Doing
|
|
|
356
356
|
next if data[:count].zero?
|
|
357
357
|
|
|
358
358
|
count = data[:count]
|
|
359
|
-
tags = data[:tag] ? data[:tag].uniq.map { |t|
|
|
359
|
+
tags = data[:tag] ? data[:tag].uniq.map { |t| t.add_at.cyan }.join(', ') : 'tags'
|
|
360
360
|
topic, m = format_counter(key, data)
|
|
361
361
|
message = m.dup
|
|
362
362
|
message.sub!(/%count/, count.to_s)
|
data/lib/doing/pager.rb
CHANGED
|
@@ -54,8 +54,8 @@ module Doing
|
|
|
54
54
|
read_io.close
|
|
55
55
|
write_io.write(text)
|
|
56
56
|
write_io.close
|
|
57
|
-
rescue SystemCallError => e
|
|
58
|
-
raise Errors::DoingStandardError, "Pager error, #{e}"
|
|
57
|
+
rescue SystemCallError # => e
|
|
58
|
+
# raise Errors::DoingStandardError, "Pager error, #{e}"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
_, status = Process.waitpid2(pid)
|
|
@@ -75,7 +75,7 @@ module Doing
|
|
|
75
75
|
note = i.note.map { |line| line.strip.link_urls(format: :markdown) } if i.note
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
title = "#{title} @
|
|
78
|
+
title = "#{title} @section(#{i.section})" unless variables[:is_single]
|
|
79
79
|
|
|
80
80
|
tags.concat(i.tag_array).sort!.uniq!
|
|
81
81
|
flagged = day_flagged = true if i.tags?(wwid.config['marker_tag'])
|
|
@@ -48,7 +48,7 @@ module Doing
|
|
|
48
48
|
note = i.note.map { |line| line.strip.link_urls(format: :markdown) } if i.note
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
title = "#{title} @
|
|
51
|
+
title = "#{title} @section(#{i.section})" unless variables[:is_single]
|
|
52
52
|
|
|
53
53
|
interval = wwid.get_interval(i, record: true) if i.title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
|
|
54
54
|
interval ||= false
|
|
@@ -49,6 +49,8 @@ module Doing
|
|
|
49
49
|
note = []
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
placeholders['tags'] = item.tags
|
|
53
|
+
|
|
52
54
|
placeholders['date'] = item.date.strftime(opt[:format])
|
|
53
55
|
|
|
54
56
|
interval = wwid.get_interval(item, record: true, formatted: false) if opt[:times]
|
|
@@ -56,8 +58,10 @@ module Doing
|
|
|
56
58
|
interval = case opt[:interval_format].to_sym
|
|
57
59
|
when :human
|
|
58
60
|
interval.time_string(format: :hm)
|
|
59
|
-
|
|
61
|
+
when :text
|
|
60
62
|
interval.time_string(format: :clock)
|
|
63
|
+
else
|
|
64
|
+
interval.time_string(format: opt[:interval_format].to_sym)
|
|
61
65
|
end
|
|
62
66
|
end
|
|
63
67
|
|
|
@@ -69,8 +73,10 @@ module Doing
|
|
|
69
73
|
duration = case opt[:interval_format].to_sym
|
|
70
74
|
when :human
|
|
71
75
|
duration.time_string(format: :hm)
|
|
72
|
-
|
|
76
|
+
when :text
|
|
73
77
|
duration.time_string(format: :clock)
|
|
78
|
+
else
|
|
79
|
+
duration.time_string(format: opt[:interval_format].to_sym)
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
duration ||= ''
|
data/lib/doing/prompt.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Doing
|
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def read_lines(prompt: 'Enter text', completions: [])
|
|
45
|
+
def read_lines(prompt: 'Enter text', completions: [], default_response: '')
|
|
46
46
|
$stdin.reopen('/dev/tty')
|
|
47
47
|
return default_response if @default_answer
|
|
48
48
|
|
|
@@ -72,8 +72,10 @@ module Doing
|
|
|
72
72
|
res.join("\n").strip
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def request_lines(prompt: 'Enter text')
|
|
75
|
+
def request_lines(prompt: 'Enter text', default_response: '')
|
|
76
76
|
$stdin.reopen('/dev/tty')
|
|
77
|
+
return default_response if @default_answer
|
|
78
|
+
|
|
77
79
|
ask_note = []
|
|
78
80
|
reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
|
|
79
81
|
puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
|
data/lib/doing/string.rb
CHANGED
|
@@ -133,7 +133,10 @@ module Doing
|
|
|
133
133
|
query[:must] = query[:should]
|
|
134
134
|
query[:should] = []
|
|
135
135
|
end
|
|
136
|
-
|
|
136
|
+
qs = []
|
|
137
|
+
qs.concat(query[:must]) if query[:must]
|
|
138
|
+
qs.concat(query[:should]) if query[:should]
|
|
139
|
+
qs.each do |s|
|
|
137
140
|
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
138
141
|
out.gsub!(rx) { |m| m.bgyellow.black }
|
|
139
142
|
end
|
|
@@ -455,7 +458,16 @@ module Doing
|
|
|
455
458
|
## @return [String] @string
|
|
456
459
|
##
|
|
457
460
|
def add_at
|
|
458
|
-
strip.sub(/^([+-]*)
|
|
461
|
+
strip.sub(/^([+-]*)@?/, '\1@')
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
##
|
|
465
|
+
## Removes @ prefix if needed, maintains +/- prefix
|
|
466
|
+
##
|
|
467
|
+
## @return [String] string without @ prefix
|
|
468
|
+
##
|
|
469
|
+
def remove_at
|
|
470
|
+
strip.sub(/^([+-]*)@?/, '\1')
|
|
459
471
|
end
|
|
460
472
|
|
|
461
473
|
##
|
|
@@ -682,6 +694,15 @@ module Doing
|
|
|
682
694
|
end
|
|
683
695
|
end
|
|
684
696
|
|
|
697
|
+
def to_bool
|
|
698
|
+
case self
|
|
699
|
+
when /^[yt1]/i
|
|
700
|
+
true
|
|
701
|
+
else
|
|
702
|
+
false
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
685
706
|
##
|
|
686
707
|
## Convert a string value to an appropriate type. If
|
|
687
708
|
## kind is not specified, '[one, two]' becomes an Array,
|
|
@@ -701,6 +722,8 @@ module Doing
|
|
|
701
722
|
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
|
702
723
|
when /^i/i
|
|
703
724
|
to_i
|
|
725
|
+
when /^(fa|tr)/i
|
|
726
|
+
to_bool
|
|
704
727
|
when /^f/i
|
|
705
728
|
to_f
|
|
706
729
|
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
|
|
@@ -163,20 +176,45 @@ module Doing
|
|
|
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,23 @@
|
|
|
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
|
+
TagArray = Class.new(Array)
|
|
18
|
+
DateBeginString = Class.new(DateTime)
|
|
19
|
+
DateEndString = Class.new(DateTime)
|
|
20
|
+
DateRangeString = Class.new(Array)
|
|
21
|
+
DateIntervalString = Class.new(DateTime)
|
|
22
|
+
end
|
|
23
|
+
end
|
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)
|
|
@@ -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
|
|
@@ -1747,7 +1769,7 @@ module Doing
|
|
|
1747
1769
|
opt[:sort_tags] ||= false
|
|
1748
1770
|
section = guess_section(section)
|
|
1749
1771
|
# :date_filter expects an array with start and end date
|
|
1750
|
-
dates =
|
|
1772
|
+
dates = dates.split_date_range if dates.instance_of?(String)
|
|
1751
1773
|
|
|
1752
1774
|
list_section({
|
|
1753
1775
|
section: section,
|
|
@@ -2280,7 +2302,6 @@ EOS
|
|
|
2280
2302
|
|
|
2281
2303
|
section_items = @content.in_section(section)
|
|
2282
2304
|
max = section_items.count - count.to_i
|
|
2283
|
-
moved_items = []
|
|
2284
2305
|
|
|
2285
2306
|
counter = 0
|
|
2286
2307
|
|
data/lib/doing.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example command that calls an existing command (tag) with
|
|
4
|
+
# preset options
|
|
5
|
+
desc 'Add an item to the Later section'
|
|
6
|
+
arg_name 'ENTRY'
|
|
7
|
+
command :later do |c|
|
|
8
|
+
c.example 'doing later "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Later section'
|
|
9
|
+
c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note'
|
|
10
|
+
|
|
11
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
|
12
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
13
|
+
|
|
14
|
+
c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
|
|
15
|
+
c.arg_name 'DATE_STRING'
|
|
16
|
+
c.flag %i[b back started], type: DateBeginString
|
|
17
|
+
|
|
18
|
+
c.desc 'Note'
|
|
19
|
+
c.arg_name 'TEXT'
|
|
20
|
+
c.flag %i[n note]
|
|
21
|
+
|
|
22
|
+
c.desc 'Prompt for note via multi-line input'
|
|
23
|
+
c.switch %i[ask], negatable: false, default_value: false
|
|
24
|
+
|
|
25
|
+
c.action do |global_options, options, args|
|
|
26
|
+
cmd = commands[:now]
|
|
27
|
+
options[:section] = 'Later'
|
|
28
|
+
options[:finish_last] = false
|
|
29
|
+
action = cmd.send(:get_action, nil)
|
|
30
|
+
action.call(global_options, options, args)
|
|
31
|
+
end
|
|
32
|
+
end
|