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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +15 -14
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +117 -53
  6. data/Gemfile.lock +11 -11
  7. data/README.md +1 -1
  8. data/Rakefile +12 -4
  9. data/bin/doing +161 -205
  10. data/docs/doc/Array.html +9 -37
  11. data/docs/doc/BooleanTermParser/Clause.html +3 -3
  12. data/docs/doc/BooleanTermParser/Operator.html +3 -3
  13. data/docs/doc/BooleanTermParser/Query.html +3 -3
  14. data/docs/doc/BooleanTermParser/QueryParser.html +3 -3
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +3 -3
  16. data/docs/doc/BooleanTermParser.html +3 -3
  17. data/docs/doc/Doing/Color.html +3 -3
  18. data/docs/doc/Doing/Completion.html +3 -3
  19. data/docs/doc/Doing/Configuration.html +4 -3
  20. data/docs/doc/Doing/Errors/DoingNoTraceError.html +3 -3
  21. data/docs/doc/Doing/Errors/DoingRuntimeError.html +3 -3
  22. data/docs/doc/Doing/Errors/DoingStandardError.html +3 -3
  23. data/docs/doc/Doing/Errors/EmptyInput.html +3 -3
  24. data/docs/doc/Doing/Errors/NoResults.html +3 -3
  25. data/docs/doc/Doing/Errors/PluginException.html +3 -3
  26. data/docs/doc/Doing/Errors/UserCancelled.html +3 -3
  27. data/docs/doc/Doing/Errors/WrongCommand.html +3 -3
  28. data/docs/doc/Doing/Errors.html +3 -3
  29. data/docs/doc/Doing/Hooks.html +3 -3
  30. data/docs/doc/Doing/Item.html +3 -3
  31. data/docs/doc/Doing/Items.html +3 -3
  32. data/docs/doc/Doing/LogAdapter.html +3 -3
  33. data/docs/doc/Doing/Note.html +3 -3
  34. data/docs/doc/Doing/Pager.html +3 -3
  35. data/docs/doc/Doing/Plugins.html +3 -3
  36. data/docs/doc/Doing/Prompt.html +7 -7
  37. data/docs/doc/Doing/Section.html +3 -3
  38. data/docs/doc/Doing/TemplateString.html +4 -4
  39. data/docs/doc/Doing/Util/Backup.html +3 -3
  40. data/docs/doc/Doing/Util.html +3 -3
  41. data/docs/doc/Doing/WWID.html +66 -8
  42. data/docs/doc/Doing.html +6 -6
  43. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
  44. data/docs/doc/GLI/Commands.html +3 -3
  45. data/docs/doc/GLI.html +3 -3
  46. data/docs/doc/Hash.html +78 -6
  47. data/docs/doc/Numeric.html +3 -3
  48. data/docs/doc/PhraseParser/Operator.html +3 -3
  49. data/docs/doc/PhraseParser/PhraseClause.html +3 -3
  50. data/docs/doc/PhraseParser/Query.html +3 -3
  51. data/docs/doc/PhraseParser/QueryParser.html +3 -3
  52. data/docs/doc/PhraseParser/QueryTransformer.html +3 -3
  53. data/docs/doc/PhraseParser/TermClause.html +3 -3
  54. data/docs/doc/PhraseParser.html +3 -3
  55. data/docs/doc/Status.html +3 -3
  56. data/docs/doc/String.html +156 -17
  57. data/docs/doc/Symbol.html +3 -3
  58. data/docs/doc/Time.html +3 -3
  59. data/docs/doc/_index.html +23 -16
  60. data/docs/doc/class_list.html +1 -1
  61. data/docs/doc/file.README.html +4 -4
  62. data/docs/doc/frames.html +1 -1
  63. data/docs/doc/index.html +4 -4
  64. data/docs/doc/method_list.html +331 -283
  65. data/docs/doc/top-level-namespace.html +3 -3
  66. data/doing.gemspec +1 -1
  67. data/doing.rdoc +26 -12
  68. data/lib/completion/_doing.zsh +5 -5
  69. data/lib/completion/doing.bash +8 -8
  70. data/lib/completion/doing.fish +93 -15
  71. data/lib/doing/array.rb +5 -4
  72. data/lib/doing/array_chronify.rb +4 -3
  73. data/lib/doing/completion/fish_completion.rb +80 -11
  74. data/lib/doing/configuration.rb +2 -1
  75. data/lib/doing/hash.rb +22 -4
  76. data/lib/doing/item.rb +2 -2
  77. data/lib/doing/items.rb +3 -1
  78. data/lib/doing/log_adapter.rb +1 -1
  79. data/lib/doing/pager.rb +2 -2
  80. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  81. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  82. data/lib/doing/plugins/export/template_export.rb +8 -2
  83. data/lib/doing/prompt.rb +4 -2
  84. data/lib/doing/string.rb +25 -2
  85. data/lib/doing/string_chronify.rb +55 -17
  86. data/lib/doing/template_string.rb +7 -0
  87. data/lib/doing/types.rb +23 -0
  88. data/lib/doing/version.rb +1 -1
  89. data/lib/doing/wwid.rb +71 -50
  90. data/lib/doing.rb +1 -0
  91. data/lib/examples/commands/later.rb +32 -0
  92. data/lib/helpers/threaded_tests.rb +273 -0
  93. 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 { description_of_the_return_value }
9
+ ## @return Hash with all values frozen
10
10
  ##
11
11
  def deep_freeze
12
- map { |k, v| v.is_a?(Hash) ? v.deep_freeze : v.freeze }.freeze
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
- if value
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
@@ -633,8 +633,8 @@ module Doing
633
633
  end
634
634
 
635
635
  def split_tags(tags)
636
- tags = tags.split(/ *, */) if tags.is_a? String
637
- tags.map { |t| t.strip.add_at }
636
+ tags = tags.to_tags if tags.is_a? String
637
+ tags.map(&:remove_at)
638
638
  end
639
639
  end
640
640
  end
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).each { |item| out.push(item.to_s)}
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")
@@ -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| "@#{t}".cyan }.join(', ') : 'tags'
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} @project(#{i.section})" unless variables[:is_single]
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} @project(#{i.section})" unless variables[:is_single]
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
- else
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
- else
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
- query[:must].concat(query[:should]).each do |s|
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(/^([+-]*)@/, '\1')
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
- Chronic.parse(self, {
45
- guess: options.fetch(:guess, :begin),
46
- context: options.fetch(:future, false) ? :future : :past,
47
- ambiguous_time_range: 8
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
- 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)
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
- start = date_string.chronify(guess: :begin)
174
- finish = nil
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
- Doing.logger.debug('Parser:', "date range interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
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
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.17'
2
+ VERSION = '2.1.22'
3
3
  end
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.title.tag!('done', value: Time.now.strftime('%F %R'))
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: true })
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
- date_string = opt[:from]
637
- case date_string
638
- when / (to|through|thru|(un)?til|-+) /
639
- dates = date_string.split(/ (?:to|through|thru|(?:un)?til|-+) /)
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
- time_string = opt[:before]
757
- if time_string =~ time_rx
758
- cutoff = "#{item.date.strftime('%Y-%m-%d')} #{time_string}".chronify(guess: :begin)
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 = time_string.chronify(guess: :begin)
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
- time_string = opt[:after]
768
- if time_string =~ time_rx
769
- cutoff = "#{item.date.strftime('%Y-%m-%d')} #{time_string}".chronify(guess: :end)
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 = time_string.chronify(guess: :end)
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.map! do |i|
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 = [dates, dates] if dates.instance_of?(String)
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
@@ -24,6 +24,7 @@ require 'tty-reader'
24
24
  require 'tty-screen'
25
25
 
26
26
  require_relative 'doing/hash'
27
+ require_relative 'doing/types'
27
28
  require_relative 'doing/colors'
28
29
  require_relative 'doing/template_string'
29
30
  require_relative 'doing/string'
@@ -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