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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -16
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +121 -53
  6. data/Gemfile.lock +11 -11
  7. data/README.md +1 -1
  8. data/Rakefile +12 -4
  9. data/bin/doing +297 -234
  10. data/docs/doc/Array.html +7 -30
  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 +6 -5
  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/Types.html +201 -0
  40. data/docs/doc/Doing/Util/Backup.html +3 -3
  41. data/docs/doc/Doing/Util.html +4 -7
  42. data/docs/doc/Doing/WWID.html +66 -8
  43. data/docs/doc/Doing.html +6 -6
  44. data/docs/doc/GLI/Commands/Help.html +185 -0
  45. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
  46. data/docs/doc/GLI/Commands.html +7 -5
  47. data/docs/doc/GLI.html +6 -4
  48. data/docs/doc/Hash.html +80 -16
  49. data/docs/doc/Numeric.html +3 -3
  50. data/docs/doc/PhraseParser/Operator.html +3 -3
  51. data/docs/doc/PhraseParser/PhraseClause.html +3 -3
  52. data/docs/doc/PhraseParser/Query.html +3 -3
  53. data/docs/doc/PhraseParser/QueryParser.html +3 -3
  54. data/docs/doc/PhraseParser/QueryTransformer.html +3 -3
  55. data/docs/doc/PhraseParser/TermClause.html +3 -3
  56. data/docs/doc/PhraseParser.html +3 -3
  57. data/docs/doc/Status.html +3 -3
  58. data/docs/doc/String.html +195 -26
  59. data/docs/doc/Symbol.html +3 -3
  60. data/docs/doc/Time.html +3 -3
  61. data/docs/doc/_index.html +22 -8
  62. data/docs/doc/class_list.html +1 -1
  63. data/docs/doc/file.README.html +4 -4
  64. data/docs/doc/frames.html +1 -1
  65. data/docs/doc/index.html +4 -4
  66. data/docs/doc/method_list.html +334 -270
  67. data/docs/doc/top-level-namespace.html +3 -3
  68. data/docs/index.md +1 -1
  69. data/doing.gemspec +1 -1
  70. data/doing.rdoc +173 -15
  71. data/lib/completion/_doing.zsh +20 -20
  72. data/lib/completion/doing.bash +37 -26
  73. data/lib/completion/doing.fish +114 -16
  74. data/lib/doing/array.rb +5 -4
  75. data/lib/doing/array_chronify.rb +4 -3
  76. data/lib/doing/changelog/change.rb +115 -0
  77. data/lib/doing/changelog/changes.rb +73 -0
  78. data/lib/doing/changelog/entry.rb +21 -0
  79. data/lib/doing/changelog/version.rb +97 -0
  80. data/lib/doing/changelog.rb +6 -0
  81. data/lib/doing/completion/fish_completion.rb +80 -11
  82. data/lib/doing/configuration.rb +17 -8
  83. data/lib/doing/hash.rb +25 -6
  84. data/lib/doing/help_monkey_patch.rb +31 -0
  85. data/lib/doing/hooks.rb +5 -1
  86. data/lib/doing/item.rb +10 -25
  87. data/lib/doing/items.rb +3 -1
  88. data/lib/doing/log_adapter.rb +1 -1
  89. data/lib/doing/pager.rb +2 -2
  90. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  91. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  92. data/lib/doing/plugins/export/template_export.rb +9 -3
  93. data/lib/doing/prompt.rb +4 -2
  94. data/lib/doing/string.rb +44 -12
  95. data/lib/doing/string_chronify.rb +56 -18
  96. data/lib/doing/template_string.rb +7 -0
  97. data/lib/doing/types.rb +25 -0
  98. data/lib/doing/util.rb +2 -1
  99. data/lib/doing/version.rb +1 -1
  100. data/lib/doing/wwid.rb +93 -69
  101. data/lib/doing.rb +2 -0
  102. data/lib/examples/commands/later.rb +32 -0
  103. data/lib/helpers/threaded_tests.rb +286 -0
  104. 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 `"this word".to_rx(2) => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/`
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 = to_phrase_query(search.strip)
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
- query[:must].concat(query[:should]).each do |s|
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(/^([+-]*)@/, '\1')
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
- 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
@@ -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
- ## "mon 3pm to mon 5pm".split_date_range
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,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 `Doing::Util.first_available_exec('bat', 'less -Xr', 'more -r', 'cat')`
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
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.18'
2
+ VERSION = '2.1.23'
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)
@@ -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']['today'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
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: cfg['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: 'today'
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 = [dates, dates] if dates.instance_of?(String)
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
- config_template: 'default'
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']['recent'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
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']['last'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
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
- section: section,
1863
- wrap_width: cfg['wrap_width'],
1887
+ case: options[:case],
1888
+ config_template: 'last',
1864
1889
  count: 1,
1865
- format: cfg['date_format'],
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
- config_template: 'last',
1873
- delete: options[:delete],
1874
- val: options[:val]
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'