doing 2.1.16 → 2.1.21

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +13 -12
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +136 -53
  6. data/Gemfile.lock +11 -11
  7. data/README.md +1 -1
  8. data/Rakefile +10 -4
  9. data/bin/doing +146 -169
  10. data/docs/doc/Array.html +3 -3
  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 +8 -4
  18. data/docs/doc/Doing/Completion.html +3 -3
  19. data/docs/doc/Doing/Configuration.html +7 -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 +121 -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 +3 -3
  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 +4 -4
  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 +3 -3
  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 +230 -17
  57. data/docs/doc/Symbol.html +3 -3
  58. data/docs/doc/Time.html +3 -3
  59. data/docs/doc/_index.html +4 -4
  60. data/docs/doc/file.README.html +4 -4
  61. data/docs/doc/frames.html +1 -1
  62. data/docs/doc/index.html +4 -4
  63. data/docs/doc/method_list.html +311 -239
  64. data/docs/doc/top-level-namespace.html +94 -3
  65. data/doing.gemspec +1 -1
  66. data/doing.rdoc +35 -6
  67. data/lib/completion/_doing.zsh +10 -10
  68. data/lib/completion/doing.bash +16 -16
  69. data/lib/completion/doing.fish +97 -15
  70. data/lib/doing/colors.rb +4 -0
  71. data/lib/doing/completion/fish_completion.rb +80 -11
  72. data/lib/doing/configuration.rb +3 -1
  73. data/lib/doing/hash.rb +1 -1
  74. data/lib/doing/item.rb +51 -0
  75. data/lib/doing/items.rb +3 -1
  76. data/lib/doing/log_adapter.rb +2 -2
  77. data/lib/doing/pager.rb +1 -1
  78. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  79. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  80. data/lib/doing/plugins/export/template_export.rb +2 -0
  81. data/lib/doing/prompt.rb +16 -5
  82. data/lib/doing/string.rb +54 -0
  83. data/lib/doing/string_chronify.rb +55 -17
  84. data/lib/doing/types.rb +19 -0
  85. data/lib/doing/version.rb +1 -1
  86. data/lib/doing/wwid.rb +80 -52
  87. data/lib/examples/commands/later.rb +32 -0
  88. data/lib/helpers/threaded_tests.rb +250 -0
  89. metadata +9 -6
@@ -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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i
4
+ REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i
5
+ REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/
6
+ REGEX_CLOCK = '(?:\d{1,2}+(?::\d{1,2}+)?(?: *(?:am|pm))?|midnight|noon)'
7
+ REGEX_TIME = /^#{REGEX_CLOCK}$/i
8
+ REGEX_DAY = /^(mon|tue|wed|thur?|fri|sat|sun)(\w+(day)?)?$/i
9
+ REGEX_RANGE_INDICATOR = ' +(?:to|through|thru|(?:un)?til|-+) +'
10
+ REGEX_RANGE = /^\S+#{REGEX_RANGE_INDICATOR}+\S+/i
11
+ REGEX_TIME_RANGE = /^#{REGEX_CLOCK}#{REGEX_RANGE_INDICATOR}#{REGEX_CLOCK}$/i
12
+
13
+ InvalidExportType = Class.new(RuntimeError)
14
+ MissingConfigFile = Class.new(RuntimeError)
15
+ TagArray = Class.new(Array)
16
+ DateBeginString = Class.new(DateTime)
17
+ DateEndString = Class.new(DateTime)
18
+ DateRangeString = Class.new(Array)
19
+ DateIntervalString = Class.new(DateTime)
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.16'
2
+ VERSION = '2.1.21'
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,25 @@ 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
659
+ if opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx
660
+ time_start, time_end = opt[:from]
661
+ elsif opt[:from].is_a?(Time)
662
+ start, finish = opt[:from]
653
663
  end
654
664
 
655
665
  if time_start
656
666
  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
667
  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'}")
668
+ opt[:date_filter] = opt[:from]
663
669
  end
664
670
  end
665
671
 
666
- if opt[:before] =~ time_rx
672
+ if opt[:before].is_a?(String) && opt[:before] =~ time_rx
667
673
  opt[:time_filter][1] = opt[:before]
668
674
  opt[:before] = nil
669
675
  end
670
676
 
671
- if opt[:after] =~ time_rx
677
+ if opt[:after].is_a?(String) && opt[:after] =~ time_rx
672
678
  opt[:time_filter][0] = opt[:after]
673
679
  opt[:after] = nil
674
680
  end
@@ -734,7 +740,7 @@ module Doing
734
740
  start_time = start_string.chronify(guess: :begin)
735
741
 
736
742
  end_string = if opt[:time_filter][1].nil?
737
- "#{item.date.next_day.strftime('%Y-%m-%d')} 12am"
743
+ "#{item.date.to_datetime.next_day.strftime('%Y-%m-%d')} 12am"
738
744
  else
739
745
  "#{item.date.strftime('%Y-%m-%d')} #{opt[:time_filter][1]}"
740
746
  end
@@ -753,22 +759,26 @@ module Doing
753
759
  end
754
760
 
755
761
  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)
762
+ before = opt[:before]
763
+ if before =~ time_rx
764
+ cutoff = "#{item.date.strftime('%Y-%m-%d')} #{before}".chronify(guess: :begin)
765
+ elsif before.is_a?(String)
766
+ cutoff = before.chronify(guess: :begin)
759
767
  else
760
- cutoff = time_string.chronify(guess: :begin)
768
+ cutoff = before
761
769
  end
762
770
  keep = cutoff && item.date <= cutoff
763
771
  keep = opt[:not] ? !keep : keep
764
772
  end
765
773
 
766
774
  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)
775
+ after = opt[:after]
776
+ if after =~ time_rx
777
+ cutoff = "#{item.date.strftime('%Y-%m-%d')} #{after}".chronify(guess: :end)
778
+ elsif after.is_a?(String)
779
+ cutoff = after.chronify(guess: :end)
770
780
  else
771
- cutoff = time_string.chronify(guess: :end)
781
+ cutoff = after
772
782
  end
773
783
  keep = cutoff && item.date >= cutoff
774
784
  keep = opt[:not] ? !keep : keep
@@ -798,14 +808,14 @@ module Doing
798
808
  end
799
809
 
800
810
  def delete_items(items, force: false)
801
- res = force ? true : Prompt.yn("Delete #{items.size} #{items.size == 1 ? 'item' : 'items'}?", default_response: 'y')
802
- if res
803
- items.each do |i|
804
- deleted = @content.delete_item(i, single: items.count == 1)
805
- Hooks.trigger :post_entry_removed, self, deleted
806
- end
807
- write(@doing_file)
808
- end
811
+ items.slice(0, 5).each { |i| puts i.to_pretty } unless force
812
+ puts softpurple("+ #{items.size - 5} additional #{'item'.to_p(items.size - 5)}") if items.size > 5 && !force
813
+
814
+ res = force ? true : Prompt.yn("Delete #{items.size} #{'item'.to_p(items.size)}?", default_response: 'y')
815
+ return unless res
816
+
817
+ items.each { |i| Hooks.trigger :post_entry_removed, self, @content.delete_item(i, single: items.count == 1) }
818
+ write(@doing_file)
809
819
  end
810
820
 
811
821
  def edit_items(items)
@@ -934,6 +944,7 @@ module Doing
934
944
  actions = [
935
945
  'add tag',
936
946
  'remove tag',
947
+ 'autotag',
937
948
  'cancel',
938
949
  'delete',
939
950
  'finish',
@@ -960,6 +971,8 @@ module Doing
960
971
  opt[:resume] = true
961
972
  when /reset/
962
973
  opt[:reset] = true
974
+ when /autotag/
975
+ opt[:autotag] = true
963
976
  when /(add|remove) tag/
964
977
  type = action =~ /^add/ ? 'add' : 'remove'
965
978
  raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
@@ -1071,6 +1084,21 @@ module Doing
1071
1084
  end
1072
1085
  end
1073
1086
 
1087
+ if opt[:autotag]
1088
+ items.map! do |i|
1089
+ new_title = autotag(i.title)
1090
+ if new_title == i.title
1091
+ logger.count(:skipped, level: :debug, message: '%count unchaged %items')
1092
+ # logger.debug('Autotag:', 'No changes')
1093
+ else
1094
+ logger.count(:added_tags)
1095
+ logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
1096
+ i.title = new_title
1097
+ Hooks.trigger :post_entry_updated, self, i
1098
+ end
1099
+ end
1100
+ end
1101
+
1074
1102
  if opt[:tag]
1075
1103
  tag = opt[:tag]
1076
1104
  items.map! do |i|
@@ -1098,10 +1126,7 @@ module Doing
1098
1126
 
1099
1127
  return unless opt[:output]
1100
1128
 
1101
- items.map! do |i|
1102
- i.title = "#{i.title} @project(#{i.section})"
1103
- i
1104
- end
1129
+ items.each { |i| i.title = "#{i.title} @section(#{i.section})" }
1105
1130
 
1106
1131
  export_items = Items.new
1107
1132
  export_items.concat(items)
@@ -1138,6 +1163,8 @@ module Doing
1138
1163
  def verify_duration(date, finish_date, title: nil)
1139
1164
  max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
1140
1165
  max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
1166
+ date = date.chronify(guess: :end, context: :today) if finish_date.is_a?(String)
1167
+
1141
1168
  elapsed = finish_date - date
1142
1169
 
1143
1170
  if max_elapsed.positive? && (elapsed > max_elapsed)
@@ -1316,7 +1343,7 @@ module Doing
1316
1343
  ##
1317
1344
  ## @return [Item] the next chronological item in the index
1318
1345
  ##
1319
- def next_item(item, options)
1346
+ def next_item(item, options = {})
1320
1347
  options ||= {}
1321
1348
  items = filter_items(Items.new, opt: options)
1322
1349
 
@@ -1640,9 +1667,9 @@ module Doing
1640
1667
  opt[:menu] = !opt[:force]
1641
1668
  opt[:query] = '' # opt[:search]
1642
1669
  opt[:multiple] = true
1643
- selected = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, **opt)
1670
+ selected = Prompt.choose_from_items(items.reverse, include_section: opt[:section] =~ /^all$/i, **opt)
1644
1671
 
1645
- raise NoResults, 'no items selected' if selected.empty?
1672
+ raise NoResults, 'no items selected' if selected.nil? || selected.empty?
1646
1673
 
1647
1674
  act_on(selected, opt)
1648
1675
  return
@@ -1650,6 +1677,7 @@ module Doing
1650
1677
 
1651
1678
  opt[:output] ||= 'template'
1652
1679
  opt[:wrap_width] ||= @config['templates']['default']['wrap_width'] || 0
1680
+
1653
1681
  output(items, title, is_single, opt)
1654
1682
  end
1655
1683
 
@@ -1746,7 +1774,7 @@ module Doing
1746
1774
  opt[:sort_tags] ||= false
1747
1775
  section = guess_section(section)
1748
1776
  # :date_filter expects an array with start and end date
1749
- dates = [dates, dates] if dates.instance_of?(String)
1777
+ dates = dates.split_date_range if dates.instance_of?(String)
1750
1778
 
1751
1779
  list_section({
1752
1780
  section: section,
@@ -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
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tty-spinner'
4
+ require 'tty-progressbar'
5
+ require './lib/doing'
6
+ require 'open3'
7
+ require 'shellwords'
8
+
9
+ class ::String
10
+ include Doing::Color
11
+
12
+ def highlight_errors
13
+ cols = `tput cols`.strip.to_i
14
+
15
+ string = dup
16
+
17
+ errs = string.scan(/(?<==\n)(?:Failure|Error):.*?(?=\n=+)/m)
18
+
19
+ errs.map! do |error|
20
+ err = error.dup
21
+
22
+ err.gsub!(%r{^(/.*?/)([^/:]+):(\d+):in (.*?)$}) do
23
+ m = Regexp.last_match
24
+ "#{m[1].white}#{m[2].bold.white}:#{m[3].yellow}:in #{m[4].cyan}"
25
+ end
26
+ err.gsub!(/(Failure|Error): (.*?)\((.*?)\):\n (.*?)(?=\n)/m) do
27
+ m = Regexp.last_match
28
+ [
29
+ m[1].bold.boldbgred.white,
30
+ m[3].bold.boldbgcyan.white,
31
+ m[2].bold.boldbgyellow.black,
32
+ " #{m[4]} ".bold.boldbgwhite.black.reset
33
+ ].join(':'.boldblack.boldbgblack.reset)
34
+ end
35
+ err.gsub!(/(<.*?>) (was expected to) (.*?)\n( *<.*?>)./m) do
36
+ m = Regexp.last_match
37
+ "#{m[1].bold.green} #{m[2].white} #{m[3].boldwhite.boldbgred.reset}\n#{m[4].bold.white}"
38
+ end
39
+ err.gsub!(/(Finished in) ([\d.]+) (seconds)/) do
40
+ m = Regexp.last_match
41
+ "#{m[1].green} #{m[2].bold.white} #{m[3].green}"
42
+ end
43
+ err.gsub!(/(\d+) (failures)/) do
44
+ m = Regexp.last_match
45
+ "#{m[1].bold.red} #{m[2].red}"
46
+ end
47
+ err.gsub!(/100% passed/) do |m|
48
+ m.bold.green
49
+ end
50
+
51
+ err
52
+ end
53
+
54
+ errs.join("\n#{('=' * cols).blue}\n")
55
+ end
56
+ end
57
+
58
+ class ThreadedTests
59
+ include Doing::Color
60
+
61
+ def run(pattern: '*', max_threads: 24, max_tests: 0)
62
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
63
+
64
+ max_threads = 24 if max_threads == 0
65
+
66
+ c = Doing::Color
67
+ c.coloring = true
68
+
69
+ pattern = "test/doing_*#{pattern}*_test.rb"
70
+
71
+ tests = Dir.glob(pattern)
72
+
73
+ if max_tests > 0
74
+ tests = tests.slice(0, max_tests - 1)
75
+ end
76
+
77
+ puts "#{tests.count} test files".boldcyan
78
+
79
+ banner = [
80
+ 'Running tests '.bold.white,
81
+ '['.black,
82
+ ':bar'.boldcyan,
83
+ '] '.black,
84
+ 'T'.green,
85
+ '/'.white,
86
+ 'A'.cyan,
87
+ ' ('.white,
88
+ max_threads.to_s.bold.magenta,
89
+ ' threads)'.white
90
+ ].join('')
91
+ progress = TTY::ProgressBar::Multi.new(banner,
92
+ width: 12,
93
+ hide_cursor: true)
94
+ children = []
95
+ tests.each do |t|
96
+ test_name = File.basename(t, '.rb').sub(/doing_(.*?)_test/, '\1')
97
+ new_sp = progress.register("[#{':bar'.cyan}] #{test_name.bold.white}:status", total: 2, width: 1, head: '.', hide_cursor: true)
98
+ children.push([test_name, new_sp, nil])
99
+ end
100
+
101
+ @elapsed = 0.0
102
+ @test_total = 0
103
+ @assrt_total = 0
104
+ @error_out = []
105
+ # progress.start
106
+
107
+ begin
108
+ while children.count.positive?
109
+ threads = []
110
+ slices = children.slice!(0, max_threads)
111
+ slices.each { |c| c[1].start }
112
+ slices.each do |s|
113
+ bar = s[1]
114
+ bar.advance(status: ": #{'running'.green}")
115
+
116
+ threads << Thread.new do
117
+ out, _err, status = Open3.capture3(ENV, 'rake', "test:#{s[0]}", stdin_data: nil)
118
+ unless status.success?
119
+ m = out.match(/(?<fail>\d+) failures, (?<err>\d+) errors/)
120
+ status = ": #{m['fail'].bold.red} #{'failures'.red}, #{m['err'].bold.red} #{'errors'.red}"
121
+ bar.update(head: '✖'.boldred)
122
+ bar.advance(head: '✖'.boldred, status: status)
123
+
124
+ # errs = out.scan(/(?:Failure|Error): [\w_]+\((?:.*?)\):(?:.*?)(?=\n=======)/m)
125
+ @error_out.push(out.highlight_errors)
126
+ bar.finish
127
+
128
+ Thread.exit
129
+ end
130
+
131
+ time = out.match(/^Finished in (?<time>\d+\.\d+) seconds\./)
132
+ count = out.match(/^(?<tests>\d+) tests, (?<assrt>\d+) assertions, (?<fails>\d+) failures, (?<errs>\d+) errors/)
133
+ status = [
134
+ ': ',
135
+ count['tests'].green,
136
+ '/',
137
+ count['assrt'].cyan,
138
+ # ' (',
139
+ # count['fails'].to_i == 0 ? '-'.dark.white.reset : count['fails'].bold.red,
140
+ # '/',
141
+ # count['errs'].to_i == 0 ? '-'.dark.white.reset : count['errs'].bold.red,
142
+ # ') ',
143
+ ' ',
144
+ time['time'].to_f.round(3).to_s.yellow,
145
+ 's'
146
+ ].join('')
147
+ bar.update(head: '✔'.boldgreen)
148
+ bar.advance(head: '✔'.boldgreen, status: status)
149
+ @test_total += count['tests'].to_i
150
+ @assrt_total += count['assrt'].to_i
151
+ @elapsed += time['time'].to_f
152
+
153
+ bar.finish
154
+ end
155
+ end
156
+ threads.each { |t| t.join }
157
+ end
158
+
159
+ finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
160
+
161
+ progress.finish
162
+
163
+ output = []
164
+ if @error_out.count.positive?
165
+ output << c.boldred("#{@error_out.count} Issues")
166
+ else
167
+ output << c.green('Success')
168
+ end
169
+ output << c.green("#{@test_total} tests")
170
+ output << c.cyan("#{@assrt_total} assertions")
171
+ output << c.yellow("#{(finish_time - start_time).round(3)}s")
172
+ puts output.join(', ')
173
+
174
+ puts @error_out.join("\n----\n".boldwhite) if @error_out.count.positive?
175
+ rescue
176
+ progress.stop
177
+ end
178
+ end
179
+ end
180
+
181
+
182
+ # require 'pastel'
183
+ ### Individual tests, multiple spinners
184
+ # pastel = Pastel.new
185
+ # format = "[#{pastel.yellow(':spinner')}] #{pastel.white("Running tests")} (#{pastel.green('tests')}/#{pastel.cyan('assertions')} #{pastel.yellow('time')})"
186
+ # spinners = TTY::Spinner::Multi.new(format, format: :dots, success_mark: pastel.green('✔'), error_mark: pastel.red('✖'))
187
+ # children = []
188
+ # tests = Dir.glob('test/doing_*_test.rb').each do |t|
189
+ # test_name = File.basename(t, '.rb').sub(/doing_(.*?)_test/, '\1')
190
+ # new_sp = spinners.register "[#{pastel.cyan(':spinner')}] #{test_name}:msg"
191
+ # new_sp.update(msg: '')
192
+ # children.push([test_name, new_sp])
193
+ # end
194
+
195
+ # @elapsed = 0.0
196
+ # @test_total = 0
197
+ # @assrt_total = 0
198
+ # spinners.auto_spin
199
+
200
+ # children.each do |spinner|
201
+ # spinner[1].run do |s|
202
+ # out, _err, status = Open3.capture3(ENV, 'rake', "test:#{spinner[0]}", stdin_data: nil)
203
+ # unless status.success?
204
+ # s.update(msg: "#{pastel.red('- FAILURE:')} #{pastel.bold.white(func)} in #{pastel.bold.yellow(tst)}")
205
+ # s.error
206
+ # s.stop
207
+ # puts `echo #{Shellwords.escape(out)} | colout '^(/.*?/)([^/:]+):(\d+):in (.*?)$' white,yellow,green,magenta | colout 'Failure: (.*?)\\((.*?)\\)' red,green | colout '(.*?) (was expected to be)' green,red | colout '(Finished in) ([\d.]+) (seconds)' green,white,green | colout '(\d+ failures)' red | colout '(100% passed)' green`
208
+ # Process.exit
209
+ # end
210
+
211
+ # time = out.match(/^Finished in (?<time>\d+\.\d+) seconds\./)
212
+ # count = out.match(/^(?<tests>\d+) tests, (?<assrt>\d+) assertions/)
213
+ # s.update(msg: ": #{pastel.green(count['tests'])}/#{pastel.cyan(count['assrt'])} #{pastel.yellow(time['time'].to_f.round(3))}s")
214
+ # @test_total += count['tests'].to_i
215
+ # @assrt_total += count['assrt'].to_i
216
+ # @elapsed += time['time'].to_f
217
+ # s.success
218
+ # end
219
+ # end
220
+
221
+ # output = []
222
+ # output << pastel.green('Success')
223
+ # output << pastel.green("#{@test_total} tests")
224
+ # output << pastel.cyan("#{@assrt_total} assertions")
225
+ # output << pastel.yellow("#{@elapsed.round(4)}s")
226
+ # puts output.join(', ')
227
+
228
+ ### Parallel test single spinner
229
+ # pastel = Pastel.new
230
+ # format = "[#{pastel.yellow(':spinner')}] #{pastel.white('Running parallel tests')} :msg"
231
+ # spinner = TTY::Spinner.new(format, format: :dots, success_mark: pastel.green('✔'), error_mark: pastel.red('✖'))
232
+
233
+ # spinner.run do |sp|
234
+ # sp.update(msg: '')
235
+ # out, err, status = Open3.capture3(ENV, 'rake', 'parallel:test', stdin_data: nil)
236
+
237
+ # unless status.success?
238
+ # failure = out.match(/^Failure: (.*?)\(([A-Z].*?)\)/)
239
+ # func = failure[1]
240
+ # tst = failure[2]
241
+ # sp.update(msg: "#{pastel.red('- FAILURE:')} #{pastel.bold.white(func)} in #{pastel.bold.yellow(tst)}")
242
+ # sp.error
243
+ # sp.stop
244
+ # puts `echo #{Shellwords.escape(out)} | colout '^(/.*?/)([^/:]+):(\d+):in (.*?)$' white,yellow,green,magenta | colout 'Failure: (.*?)\\((.*?)\\)' red,green | colout '(.*?) (was expected to be)' green,red | colout '(Finished in) ([\d.]+) (seconds)' green,white,green | colout '(\d+ failures)' red | colout '(100% passed)' green`
245
+ # Process.exit
246
+ # end
247
+
248
+ # sp.update(msg: pastel.green('- All tests passed'))
249
+ # sp.success
250
+ # end