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.
- checksums.yaml +4 -4
- data/.yardoc/checksums +13 -12
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +136 -53
- data/Gemfile.lock +11 -11
- data/README.md +1 -1
- data/Rakefile +10 -4
- data/bin/doing +146 -169
- data/docs/doc/Array.html +3 -3
- 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 +8 -4
- data/docs/doc/Doing/Completion.html +3 -3
- data/docs/doc/Doing/Configuration.html +7 -5
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +3 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +3 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +3 -3
- data/docs/doc/Doing/Errors/EmptyInput.html +3 -3
- data/docs/doc/Doing/Errors/NoResults.html +3 -3
- data/docs/doc/Doing/Errors/PluginException.html +3 -3
- data/docs/doc/Doing/Errors/UserCancelled.html +3 -3
- data/docs/doc/Doing/Errors/WrongCommand.html +3 -3
- data/docs/doc/Doing/Errors.html +3 -3
- data/docs/doc/Doing/Hooks.html +3 -3
- data/docs/doc/Doing/Item.html +121 -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 +3 -3
- 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 +4 -4
- 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 +3 -3
- 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 +230 -17
- data/docs/doc/Symbol.html +3 -3
- data/docs/doc/Time.html +3 -3
- data/docs/doc/_index.html +4 -4
- 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 +311 -239
- data/docs/doc/top-level-namespace.html +94 -3
- data/doing.gemspec +1 -1
- data/doing.rdoc +35 -6
- data/lib/completion/_doing.zsh +10 -10
- data/lib/completion/doing.bash +16 -16
- data/lib/completion/doing.fish +97 -15
- data/lib/doing/colors.rb +4 -0
- data/lib/doing/completion/fish_completion.rb +80 -11
- data/lib/doing/configuration.rb +3 -1
- data/lib/doing/hash.rb +1 -1
- data/lib/doing/item.rb +51 -0
- data/lib/doing/items.rb +3 -1
- data/lib/doing/log_adapter.rb +2 -2
- data/lib/doing/pager.rb +1 -1
- 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 +2 -0
- data/lib/doing/prompt.rb +16 -5
- data/lib/doing/string.rb +54 -0
- data/lib/doing/string_chronify.rb +55 -17
- data/lib/doing/types.rb +19 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +80 -52
- data/lib/examples/commands/later.rb +32 -0
- data/lib/helpers/threaded_tests.rb +250 -0
- 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
|
-
|
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
|
data/lib/doing/types.rb
ADDED
@@ -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
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,25 @@ 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
|
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
|
-
|
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
|
-
|
757
|
-
if
|
758
|
-
cutoff = "#{item.date.strftime('%Y-%m-%d')} #{
|
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 =
|
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
|
-
|
768
|
-
if
|
769
|
-
cutoff = "#{item.date.strftime('%Y-%m-%d')} #{
|
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 =
|
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
|
-
|
802
|
-
if
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
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.
|
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 =
|
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
|