doing 2.1.8 → 2.1.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardoc/checksums +9 -9
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +38 -0
- data/Dockerfile +9 -0
- data/Dockerfile-2.6 +9 -0
- data/Dockerfile-2.7 +8 -0
- data/Dockerfile-3.0 +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +51 -6
- data/bin/doing +2098 -1944
- data/docs/doc/Array.html +1 -1
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +1 -1
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +2 -2
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +1 -1
- data/docs/doc/Doing/Items.html +1 -1
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +132 -18
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Util/Backup.html +79 -2
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +88 -73
- data/docs/doc/Doing.html +2 -2
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +97 -1
- data/docs/doc/Symbol.html +36 -2
- data/docs/doc/Time.html +1 -1
- data/docs/doc/_index.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +299 -235
- data/docs/doc/top-level-namespace.html +1 -1
- data/docs/index.md +1 -1
- data/doing.rdoc +9 -2
- data/generate_completions.sh +1 -3
- data/lib/completion/_doing.zsh +1 -1
- data/lib/completion/doing.bash +2 -2
- data/lib/completion/doing.fish +2 -1
- data/lib/doing/completion/bash_completion.rb +2 -2
- data/lib/doing/completion/fish_completion.rb +2 -2
- data/lib/doing/completion/zsh_completion.rb +2 -2
- data/lib/doing/completion.rb +12 -2
- data/lib/doing/configuration.rb +19 -9
- data/lib/doing/hooks.rb +10 -5
- data/lib/doing/items.rb +16 -1
- data/lib/doing/log_adapter.rb +1 -0
- data/lib/doing/pager.rb +2 -20
- data/lib/doing/plugins/import/calendar_import.rb +5 -0
- data/lib/doing/plugins/import/doing_import.rb +2 -0
- data/lib/doing/plugins/import/timing_import.rb +5 -0
- data/lib/doing/prompt.rb +47 -8
- data/lib/doing/string.rb +20 -0
- data/lib/doing/symbol.rb +4 -0
- data/lib/doing/util_backup.rb +38 -8
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +210 -105
- data/lib/doing.rb +1 -0
- data/lib/examples/plugins/hooks.rb +31 -0
- data/scripts/generate_bash_completions.rb +2 -2
- data/scripts/sort_commands.rb +59 -0
- metadata +7 -3
- data/lib/helpers/fuzzyfilefinder +0 -0
data/lib/doing/wwid.rb
CHANGED
@@ -114,6 +114,8 @@ module Doing
|
|
114
114
|
filename = @doing_file if filename.nil?
|
115
115
|
return if File.exist?(filename) && File.stat(filename).size.positive?
|
116
116
|
|
117
|
+
FileUtils.mkdir_p(File.dirname(filename)) unless File.directory?(File.dirname(filename))
|
118
|
+
|
117
119
|
File.open(filename, 'w+') do |f|
|
118
120
|
f.puts "#{@config['current_section']}:"
|
119
121
|
end
|
@@ -338,7 +340,8 @@ module Doing
|
|
338
340
|
## @option opt :back [Date] backdate
|
339
341
|
## @option opt :timed [Boolean] new item is timed entry, marks previous entry as @done
|
340
342
|
##
|
341
|
-
def add_item(title, section = nil, opt
|
343
|
+
def add_item(title, section = nil, opt)
|
344
|
+
opt ||= {}
|
342
345
|
section ||= @config['current_section']
|
343
346
|
@content.add_section(section, log: false)
|
344
347
|
opt[:back] ||= opt[:date] ? opt[:date] : Time.now
|
@@ -371,9 +374,13 @@ module Doing
|
|
371
374
|
end
|
372
375
|
end
|
373
376
|
|
377
|
+
Hooks.trigger :pre_entry_add, self, entry
|
378
|
+
|
374
379
|
@content.push(entry)
|
375
380
|
# logger.count(:added, level: :debug)
|
376
381
|
logger.info('New entry:', %(added "#{entry.title}" to #{section}))
|
382
|
+
|
383
|
+
Hooks.trigger :post_entry_added, self, entry.dup
|
377
384
|
end
|
378
385
|
|
379
386
|
##
|
@@ -401,7 +408,8 @@ module Doing
|
|
401
408
|
## @param paths [String] Path to JSON report file
|
402
409
|
## @param opt [Hash] Additional Options
|
403
410
|
##
|
404
|
-
def import(paths, opt
|
411
|
+
def import(paths, opt)
|
412
|
+
opt ||= {}
|
405
413
|
Plugins.plugins[:import].each do |_, options|
|
406
414
|
next unless opt[:type] =~ /^(#{options[:trigger].normalize_trigger})$/i
|
407
415
|
|
@@ -461,13 +469,15 @@ module Doing
|
|
461
469
|
#
|
462
470
|
# @return nothing
|
463
471
|
#
|
464
|
-
def repeat_item(item, opt
|
472
|
+
def repeat_item(item, opt)
|
473
|
+
opt ||= {}
|
465
474
|
if item.should_finish?
|
466
475
|
if item.should_time?
|
467
476
|
item.title.tag!('done', value: Time.now.strftime('%F %R'))
|
468
477
|
else
|
469
478
|
item.title.tag!('done')
|
470
479
|
end
|
480
|
+
Hooks.trigger :post_entry_updated, self, item
|
471
481
|
end
|
472
482
|
|
473
483
|
# Remove @done tag
|
@@ -501,7 +511,8 @@ module Doing
|
|
501
511
|
##
|
502
512
|
## @param opt [Hash] Additional Options
|
503
513
|
##
|
504
|
-
def repeat_last(opt
|
514
|
+
def repeat_last(opt)
|
515
|
+
opt ||= {}
|
505
516
|
opt[:section] ||= 'all'
|
506
517
|
opt[:section] = guess_section(opt[:section])
|
507
518
|
opt[:note] ||= []
|
@@ -523,7 +534,8 @@ module Doing
|
|
523
534
|
##
|
524
535
|
## @param opt [Hash] Additional Options
|
525
536
|
##
|
526
|
-
def last_entry(opt
|
537
|
+
def last_entry(opt)
|
538
|
+
opt ||= {}
|
527
539
|
opt[:tag_bool] ||= :and
|
528
540
|
opt[:section] ||= @config['current_section']
|
529
541
|
|
@@ -615,19 +627,19 @@ module Doing
|
|
615
627
|
## @param items [Array] The items to filter (if empty, filters all items)
|
616
628
|
## @param opt [Hash] The filter parameters
|
617
629
|
##
|
618
|
-
## @option opt [String] :section
|
619
|
-
## @option opt [Boolean] :unfinished
|
620
|
-
## @option opt [Array or String] :tag
|
621
|
-
## @option opt [Symbol] :tag_bool
|
622
|
-
## @option opt [String] :search
|
623
|
-
## @option opt [Array] :date_filter
|
624
|
-
## @option opt [Boolean] :only_timed
|
625
|
-
## @option opt [String] :before
|
626
|
-
## @option opt [String] :after (Date/Time string, unparsed
|
627
|
-
## @option opt [Boolean] :today
|
628
|
-
## @option opt [Boolean] :yesterday
|
629
|
-
## @option opt [Number] :count
|
630
|
-
## @option opt [String] :age
|
630
|
+
## @option opt [String] :section ('all')
|
631
|
+
## @option opt [Boolean] :unfinished (false)
|
632
|
+
## @option opt [Array or String] :tag ([]) Array or comma-separated string
|
633
|
+
## @option opt [Symbol] :tag_bool (:and) :and, :or, :not
|
634
|
+
## @option opt [String] :search ('') string, optional regex with `/string/`
|
635
|
+
## @option opt [Array] :date_filter (nil) [[Time]start, [Time]end]
|
636
|
+
## @option opt [Boolean] :only_timed (false)
|
637
|
+
## @option opt [String] :before (nil) Date/Time string, unparsed
|
638
|
+
## @option opt [String] :after (nil) Date/Time string, unparsed
|
639
|
+
## @option opt [Boolean] :today (false) limit to entries from today
|
640
|
+
## @option opt [Boolean] :yesterday (false) limit to entries from yesterday
|
641
|
+
## @option opt [Number] :count (0) max entries to return
|
642
|
+
## @option opt [String] :age (new) 'old' or 'new'
|
631
643
|
##
|
632
644
|
def filter_items(items = Items.new, opt: {})
|
633
645
|
time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
|
@@ -784,7 +796,7 @@ module Doing
|
|
784
796
|
|
785
797
|
output = Items.new
|
786
798
|
|
787
|
-
if opt[:age]
|
799
|
+
if opt[:age] && opt[:age].normalize_age == :oldest
|
788
800
|
output.concat(filtered_items.slice(0, count).reverse)
|
789
801
|
else
|
790
802
|
output.concat(filtered_items.reverse.slice(0, count))
|
@@ -793,6 +805,65 @@ module Doing
|
|
793
805
|
output
|
794
806
|
end
|
795
807
|
|
808
|
+
def delete_items(items, force: false)
|
809
|
+
res = force ? true : Prompt.yn("Delete #{items.size} #{items.size == 1 ? 'item' : 'items'}?", default_response: 'y')
|
810
|
+
if res
|
811
|
+
items.each do |i|
|
812
|
+
deleted = @content.delete_item(i, single: items.count == 1)
|
813
|
+
Hooks.trigger :post_entry_removed, self, deleted
|
814
|
+
end
|
815
|
+
write(@doing_file)
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
def edit_items(items)
|
820
|
+
editable_items = []
|
821
|
+
|
822
|
+
items.each do |i|
|
823
|
+
editable = "#{i.date.strftime('%F %R')} | #{i.title}"
|
824
|
+
old_note = i.note ? i.note.strip_lines.join("\n") : nil
|
825
|
+
editable += "\n#{old_note}" unless old_note.nil?
|
826
|
+
editable_items << editable
|
827
|
+
end
|
828
|
+
divider = "\n-----------\n"
|
829
|
+
notice =<<~EONOTICE
|
830
|
+
# - You may delete entries, but leave all divider lines (---) in place.
|
831
|
+
# - Start and @done dates replaced with a time string (yesterday 3pm) will
|
832
|
+
# be parsed automatically. Do not delete the pipe (|) between start date
|
833
|
+
# and entry title.
|
834
|
+
EONOTICE
|
835
|
+
input = "#{editable_items.map(&:strip).join(divider)}\n\n#{notice}"
|
836
|
+
|
837
|
+
new_items = fork_editor(input).split(/#{divider}/)
|
838
|
+
|
839
|
+
new_items.each_with_index do |new_item, i|
|
840
|
+
input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
|
841
|
+
first_line = input_lines[0]&.strip
|
842
|
+
|
843
|
+
if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
|
844
|
+
deleted = @content.delete_item(items[i], single: new_items.count == 1)
|
845
|
+
Hooks.trigger :post_entry_removed, self, deleted
|
846
|
+
Doing.logger.count(:deleted)
|
847
|
+
else
|
848
|
+
date, title, note = format_input(new_item)
|
849
|
+
|
850
|
+
note.map!(&:strip)
|
851
|
+
note.delete_if(&:ignore?)
|
852
|
+
item = items[i]
|
853
|
+
old_item = item.dup
|
854
|
+
item.date = date || items[i].date
|
855
|
+
item.title = title
|
856
|
+
item.note = note
|
857
|
+
if (item.equal?(old_item))
|
858
|
+
Doing.logger.count(:skipped, level: :debug)
|
859
|
+
else
|
860
|
+
Doing.logger.count(:updated)
|
861
|
+
Hooks.trigger :post_entry_updated, self, item
|
862
|
+
end
|
863
|
+
end
|
864
|
+
end
|
865
|
+
end
|
866
|
+
|
796
867
|
##
|
797
868
|
## Display an interactive menu of entries
|
798
869
|
##
|
@@ -800,7 +871,8 @@ module Doing
|
|
800
871
|
##
|
801
872
|
## Options hash is shared with #filter_items and #act_on
|
802
873
|
##
|
803
|
-
def interactive(opt
|
874
|
+
def interactive(opt)
|
875
|
+
opt ||= {}
|
804
876
|
opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
|
805
877
|
|
806
878
|
search = nil
|
@@ -820,7 +892,11 @@ module Doing
|
|
820
892
|
}
|
821
893
|
items = filter_items(Items.new, opt: filter_options)
|
822
894
|
|
823
|
-
|
895
|
+
menu_options = %i[search query exact multiple show_if_single menu sort case].each_with_object({}) {
|
896
|
+
|k, hsh| hsh[k] = opt[k]
|
897
|
+
}
|
898
|
+
|
899
|
+
selection = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, **menu_options)
|
824
900
|
|
825
901
|
raise NoResults, 'no items selected' if selection.nil? || selection.empty?
|
826
902
|
|
@@ -848,7 +924,8 @@ module Doing
|
|
848
924
|
## @option opt [Boolean] :again
|
849
925
|
## @option opt [Boolean] :resume
|
850
926
|
##
|
851
|
-
def act_on(items, opt
|
927
|
+
def act_on(items, opt)
|
928
|
+
opt ||= {}
|
852
929
|
actions = %i[editor delete tag flag finish cancel archive output save_to again resume]
|
853
930
|
has_action = false
|
854
931
|
single = items.count == 1
|
@@ -947,7 +1024,7 @@ module Doing
|
|
947
1024
|
|
948
1025
|
item = items[0]
|
949
1026
|
if opt[:resume] && !opt[:reset]
|
950
|
-
repeat_item(item, { editor: opt[:editor] })
|
1027
|
+
repeat_item(item, { editor: opt[:editor] }) # hooked
|
951
1028
|
elsif opt[:reset]
|
952
1029
|
res = Prompt.enter_text('Start date (blank for current time)', default_response: '')
|
953
1030
|
if res =~ /^ *$/
|
@@ -961,7 +1038,9 @@ module Doing
|
|
961
1038
|
else
|
962
1039
|
opt[:resume]
|
963
1040
|
end
|
964
|
-
|
1041
|
+
new_entry = reset_item(item, date: date, resume: res)
|
1042
|
+
@content.update_item(item, new_entry)
|
1043
|
+
Hooks.trigger :post_entry_updated, self, new_entry
|
965
1044
|
end
|
966
1045
|
write(@doing_file)
|
967
1046
|
|
@@ -969,11 +1048,7 @@ module Doing
|
|
969
1048
|
end
|
970
1049
|
|
971
1050
|
if opt[:delete]
|
972
|
-
|
973
|
-
if res
|
974
|
-
items.each { |i| @content.delete_item(i, single: items.count == 1) }
|
975
|
-
write(@doing_file)
|
976
|
-
end
|
1051
|
+
delete_items(items, force: opt[:force]) # hooked
|
977
1052
|
return
|
978
1053
|
end
|
979
1054
|
|
@@ -981,6 +1056,7 @@ module Doing
|
|
981
1056
|
tag = @config['marker_tag'] || 'flagged'
|
982
1057
|
items.map! do |i|
|
983
1058
|
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1059
|
+
Hooks.trigger :post_entry_updated, self, i
|
984
1060
|
end
|
985
1061
|
end
|
986
1062
|
|
@@ -990,6 +1066,7 @@ module Doing
|
|
990
1066
|
if i.should_finish?
|
991
1067
|
should_date = !opt[:cancel] && i.should_time?
|
992
1068
|
i.tag(tag, date: should_date, remove: opt[:remove], single: single)
|
1069
|
+
Hooks.trigger :post_entry_updated, self, i
|
993
1070
|
end
|
994
1071
|
end
|
995
1072
|
end
|
@@ -998,61 +1075,22 @@ module Doing
|
|
998
1075
|
tag = opt[:tag]
|
999
1076
|
items.map! do |i|
|
1000
1077
|
i.tag(tag, date: false, remove: opt[:remove], single: single)
|
1078
|
+
Hooks.trigger :post_entry_updated, self, i
|
1001
1079
|
end
|
1002
1080
|
end
|
1003
1081
|
|
1004
1082
|
if opt[:archive] || opt[:move]
|
1005
1083
|
section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
|
1006
|
-
items.map!
|
1084
|
+
items.map! do |i|
|
1085
|
+
i.move_to(section, label: true)
|
1086
|
+
Hooks.trigger :post_entry_updated, self, i
|
1087
|
+
end
|
1007
1088
|
end
|
1008
1089
|
|
1009
1090
|
write(@doing_file)
|
1010
1091
|
|
1011
1092
|
if opt[:editor]
|
1012
|
-
|
1013
|
-
editable_items = []
|
1014
|
-
|
1015
|
-
items.each do |i|
|
1016
|
-
editable = "#{i.date.strftime('%F %R')} | #{i.title}"
|
1017
|
-
old_note = i.note ? i.note.strip_lines.join("\n") : nil
|
1018
|
-
editable += "\n#{old_note}" unless old_note.nil?
|
1019
|
-
editable_items << editable
|
1020
|
-
end
|
1021
|
-
divider = "\n-----------\n"
|
1022
|
-
notice =<<~EONOTICE
|
1023
|
-
# - You may delete entries, but leave all divider lines (---) in place.
|
1024
|
-
# - Start and @done dates replaced with a time string (yesterday 3pm) will
|
1025
|
-
# be parsed automatically. Do not delete the pipe (|) between start date
|
1026
|
-
# and entry title.
|
1027
|
-
EONOTICE
|
1028
|
-
input = "#{editable_items.map(&:strip).join(divider)}\n\n#{notice}"
|
1029
|
-
|
1030
|
-
new_items = fork_editor(input).split(/#{divider}/)
|
1031
|
-
|
1032
|
-
new_items.each_with_index do |new_item, i|
|
1033
|
-
input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
|
1034
|
-
first_line = input_lines[0]&.strip
|
1035
|
-
|
1036
|
-
if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
|
1037
|
-
@content.delete_item(items[i], single: new_items.count == 1)
|
1038
|
-
Doing.logger.count(:deleted)
|
1039
|
-
else
|
1040
|
-
date, title, note = format_input(new_item)
|
1041
|
-
|
1042
|
-
note.map!(&:strip)
|
1043
|
-
note.delete_if(&:ignore?)
|
1044
|
-
item = items[i]
|
1045
|
-
old_item = item.dup
|
1046
|
-
item.date = date || items[i].date
|
1047
|
-
item.title = title
|
1048
|
-
item.note = note
|
1049
|
-
if (item.equal?(old_item))
|
1050
|
-
Doing.logger.count(:skipped, level: :debug)
|
1051
|
-
else
|
1052
|
-
Doing.logger.count(:updated)
|
1053
|
-
end
|
1054
|
-
end
|
1055
|
-
end
|
1093
|
+
edit_items(items) # hooked
|
1056
1094
|
|
1057
1095
|
write(@doing_file)
|
1058
1096
|
end
|
@@ -1077,7 +1115,7 @@ module Doing
|
|
1077
1115
|
options[:template] = opt[:template] || nil
|
1078
1116
|
end
|
1079
1117
|
|
1080
|
-
output = list_section(options)
|
1118
|
+
output = list_section(options) # hooked
|
1081
1119
|
|
1082
1120
|
if opt[:save_to]
|
1083
1121
|
file = File.expand_path(opt[:save_to])
|
@@ -1105,7 +1143,8 @@ module Doing
|
|
1105
1143
|
##
|
1106
1144
|
## @see #filter_items
|
1107
1145
|
##
|
1108
|
-
def tag_last(opt
|
1146
|
+
def tag_last(opt) # hooked
|
1147
|
+
opt ||= {}
|
1109
1148
|
opt[:count] ||= 1
|
1110
1149
|
opt[:archive] ||= false
|
1111
1150
|
opt[:tags] ||= ['done']
|
@@ -1215,6 +1254,8 @@ module Doing
|
|
1215
1254
|
elsif opt[:archive] && opt[:count].zero?
|
1216
1255
|
logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
|
1217
1256
|
end
|
1257
|
+
|
1258
|
+
Hooks.trigger :post_entry_updated, self, item
|
1218
1259
|
end
|
1219
1260
|
|
1220
1261
|
write(@doing_file)
|
@@ -1229,7 +1270,8 @@ module Doing
|
|
1229
1270
|
##
|
1230
1271
|
## @return [Item] the next chronological item in the index
|
1231
1272
|
##
|
1232
|
-
def next_item(item, options
|
1273
|
+
def next_item(item, options)
|
1274
|
+
options ||= {}
|
1233
1275
|
items = filter_items(Items.new, opt: options)
|
1234
1276
|
|
1235
1277
|
idx = items.index(item)
|
@@ -1267,6 +1309,7 @@ module Doing
|
|
1267
1309
|
item.title = title
|
1268
1310
|
item.note.add(note, replace: true)
|
1269
1311
|
logger.info('Edited:', item.title)
|
1312
|
+
Hooks.trigger :post_entry_updated, self, item.dup
|
1270
1313
|
|
1271
1314
|
write(@doing_file)
|
1272
1315
|
end
|
@@ -1287,7 +1330,8 @@ module Doing
|
|
1287
1330
|
## @option opt :back [Date] backdate new item
|
1288
1331
|
## @option opt :new_item [String] content to use for new item
|
1289
1332
|
## @option opt :note [Array] note content for new item
|
1290
|
-
def stop_start(target_tag, opt
|
1333
|
+
def stop_start(target_tag, opt)
|
1334
|
+
opt ||= {}
|
1291
1335
|
tag = target_tag.dup
|
1292
1336
|
opt[:section] ||= @config['current_section']
|
1293
1337
|
opt[:archive] ||= false
|
@@ -1320,8 +1364,10 @@ module Doing
|
|
1320
1364
|
logger.count(:completed)
|
1321
1365
|
logger.info('Completed:', item.title)
|
1322
1366
|
end
|
1367
|
+
Hooks.trigger :post_entry_updated, self, item
|
1323
1368
|
end
|
1324
1369
|
|
1370
|
+
|
1325
1371
|
logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
|
1326
1372
|
|
1327
1373
|
if opt[:new_item]
|
@@ -1354,7 +1400,8 @@ module Doing
|
|
1354
1400
|
##
|
1355
1401
|
## Rename doing file with date and start fresh one
|
1356
1402
|
##
|
1357
|
-
def rotate(opt
|
1403
|
+
def rotate(opt)
|
1404
|
+
opt ||= {}
|
1358
1405
|
keep = opt[:keep] || 0
|
1359
1406
|
tags = []
|
1360
1407
|
tags.concat(opt[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if opt[:tag]
|
@@ -1369,7 +1416,7 @@ module Doing
|
|
1369
1416
|
counter = 0
|
1370
1417
|
new_content = Items.new
|
1371
1418
|
|
1372
|
-
|
1419
|
+
section_items.each do |item|
|
1373
1420
|
break if counter >= max
|
1374
1421
|
if opt[:before]
|
1375
1422
|
time_string = opt[:before]
|
@@ -1378,6 +1425,7 @@ module Doing
|
|
1378
1425
|
|
1379
1426
|
unless ((!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s)) || (opt[:before] && item.date >= cutoff))
|
1380
1427
|
new_item = @content.delete(item)
|
1428
|
+
Hooks.trigger :post_entry_removed, self, item.dup
|
1381
1429
|
raise DoingRuntimeError, "Error deleting item: #{item}" if new_item.nil?
|
1382
1430
|
|
1383
1431
|
new_content.add_section(new_item.section, log: false)
|
@@ -1488,7 +1536,7 @@ module Doing
|
|
1488
1536
|
tpl_cfg = @config.dig('templates', opt[:config_template])
|
1489
1537
|
|
1490
1538
|
cfg = if opt[:view_template]
|
1491
|
-
@config.dig('views', opt[:view_template]).deep_merge(tpl_cfg)
|
1539
|
+
@config.dig('views', opt[:view_template]).deep_merge(tpl_cfg, { extend_existing_arrays: true, sort_merged_arrays: true })
|
1492
1540
|
else
|
1493
1541
|
tpl_cfg
|
1494
1542
|
end
|
@@ -1500,11 +1548,12 @@ module Doing
|
|
1500
1548
|
'tags_color' => @config['tags_color'],
|
1501
1549
|
'duration' => @config['duration'],
|
1502
1550
|
'interval_format' => @config['interval_format']
|
1503
|
-
})
|
1551
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
1504
1552
|
opt[:duration] ||= cfg['duration'] || false
|
1505
1553
|
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
1506
1554
|
opt[:count] ||= 0
|
1507
|
-
opt[:age] ||=
|
1555
|
+
opt[:age] ||= :newest
|
1556
|
+
opt[:age] = opt[:age].normalize_age
|
1508
1557
|
opt[:format] ||= cfg['date_format']
|
1509
1558
|
opt[:order] ||= cfg['order'] || 'asc'
|
1510
1559
|
opt[:tag_order] ||= 'asc'
|
@@ -1535,7 +1584,13 @@ module Doing
|
|
1535
1584
|
|
1536
1585
|
items.reverse! unless opt[:order] =~ /^d/i
|
1537
1586
|
|
1538
|
-
if opt[:
|
1587
|
+
if opt[:delete]
|
1588
|
+
delete_items(items, force: opt[:force])
|
1589
|
+
return
|
1590
|
+
elsif opt[:editor]
|
1591
|
+
edit_items(items)
|
1592
|
+
return
|
1593
|
+
elsif opt[:interactive]
|
1539
1594
|
opt[:menu] = !opt[:force]
|
1540
1595
|
opt[:query] = '' # opt[:search]
|
1541
1596
|
opt[:multiple] = true
|
@@ -1559,7 +1614,8 @@ module Doing
|
|
1559
1614
|
## @param section [String] The source section
|
1560
1615
|
## @param options [Hash] Options
|
1561
1616
|
##
|
1562
|
-
def archive(section = @config['current_section'], options
|
1617
|
+
def archive(section = @config['current_section'], options)
|
1618
|
+
options ||= {}
|
1563
1619
|
count = options[:keep] || 0
|
1564
1620
|
destination = options[:destination] || 'Archive'
|
1565
1621
|
tags = options[:tags] || []
|
@@ -1589,18 +1645,19 @@ module Doing
|
|
1589
1645
|
## @param output [String] output format
|
1590
1646
|
## @param opt [Hash] Options
|
1591
1647
|
##
|
1592
|
-
def today(times = true, output = nil, opt
|
1648
|
+
def today(times = true, output = nil, opt)
|
1649
|
+
opt ||= {}
|
1593
1650
|
opt[:totals] ||= false
|
1594
1651
|
opt[:sort_tags] ||= false
|
1595
1652
|
|
1596
|
-
cfg = @config['templates']['today'].deep_merge(@config['templates']['default']).deep_merge({
|
1653
|
+
cfg = @config['templates']['today'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
1597
1654
|
'wrap_width' => @config['wrap_width'] || 0,
|
1598
1655
|
'date_format' => @config['default_date_format'],
|
1599
1656
|
'order' => @config['order'] || 'asc',
|
1600
1657
|
'tags_color' => @config['tags_color'],
|
1601
1658
|
'duration' => @config['duration'],
|
1602
1659
|
'interval_format' => @config['interval_format']
|
1603
|
-
})
|
1660
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
1604
1661
|
|
1605
1662
|
opt[:duration] ||= cfg['duration'] || false
|
1606
1663
|
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
@@ -1637,7 +1694,8 @@ module Doing
|
|
1637
1694
|
## @param output [String] Output format
|
1638
1695
|
## @param opt [Hash] Additional Options
|
1639
1696
|
##
|
1640
|
-
def list_date(dates, section, times = nil, output = nil, opt
|
1697
|
+
def list_date(dates, section, times = nil, output = nil, opt)
|
1698
|
+
opt ||= {}
|
1641
1699
|
opt[:totals] ||= false
|
1642
1700
|
opt[:sort_tags] ||= false
|
1643
1701
|
section = guess_section(section)
|
@@ -1666,7 +1724,8 @@ module Doing
|
|
1666
1724
|
## @param output [String] Output format
|
1667
1725
|
## @param opt [Hash] Additional Options
|
1668
1726
|
##
|
1669
|
-
def yesterday(section, times = nil, output = nil, opt
|
1727
|
+
def yesterday(section, times = nil, output = nil, opt)
|
1728
|
+
opt ||= {}
|
1670
1729
|
opt[:totals] ||= false
|
1671
1730
|
opt[:sort_tags] ||= false
|
1672
1731
|
section = guess_section(section)
|
@@ -1701,19 +1760,20 @@ module Doing
|
|
1701
1760
|
## @param section [String] The section to show from, default Currently
|
1702
1761
|
## @param opt [Hash] Additional Options
|
1703
1762
|
##
|
1704
|
-
def recent(count = 10, section = nil, opt
|
1763
|
+
def recent(count = 10, section = nil, opt)
|
1764
|
+
opt ||= {}
|
1705
1765
|
times = opt[:t] || true
|
1706
1766
|
opt[:totals] ||= false
|
1707
1767
|
opt[:sort_tags] ||= false
|
1708
1768
|
|
1709
|
-
cfg = @config['templates']['recent'].deep_merge(@config['templates']['default']).deep_merge({
|
1769
|
+
cfg = @config['templates']['recent'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
1710
1770
|
'wrap_width' => @config['wrap_width'] || 0,
|
1711
1771
|
'date_format' => @config['default_date_format'],
|
1712
1772
|
'order' => @config['order'] || 'asc',
|
1713
1773
|
'tags_color' => @config['tags_color'],
|
1714
1774
|
'duration' => @config['duration'],
|
1715
1775
|
'interval_format' => @config['interval_format']
|
1716
|
-
})
|
1776
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
1717
1777
|
opt[:duration] ||= cfg['duration'] || false
|
1718
1778
|
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
1719
1779
|
|
@@ -1740,14 +1800,14 @@ module Doing
|
|
1740
1800
|
##
|
1741
1801
|
def last(times: true, section: nil, options: {})
|
1742
1802
|
section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
|
1743
|
-
cfg = @config['templates']['last'].deep_merge(@config['templates']['default']).deep_merge({
|
1803
|
+
cfg = @config['templates']['last'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
1744
1804
|
'wrap_width' => @config['wrap_width'] || 0,
|
1745
1805
|
'date_format' => @config['default_date_format'],
|
1746
1806
|
'order' => @config['order'] || 'asc',
|
1747
1807
|
'tags_color' => @config['tags_color'],
|
1748
1808
|
'duration' => @config['duration'],
|
1749
1809
|
'interval_format' => @config['interval_format']
|
1750
|
-
})
|
1810
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
1751
1811
|
options[:duration] ||= cfg['duration'] || false
|
1752
1812
|
options[:interval_format] ||= cfg['interval_format'] || 'text'
|
1753
1813
|
|
@@ -1762,7 +1822,8 @@ module Doing
|
|
1762
1822
|
interval_format: options[:interval_format],
|
1763
1823
|
case: options[:case],
|
1764
1824
|
not: options[:negate],
|
1765
|
-
config_template: 'last'
|
1825
|
+
config_template: 'last',
|
1826
|
+
delete: options[:delete]
|
1766
1827
|
}
|
1767
1828
|
|
1768
1829
|
if options[:tag]
|
@@ -1782,13 +1843,14 @@ module Doing
|
|
1782
1843
|
## Does not repeat tags in a title, and only converts the first instance of an
|
1783
1844
|
## untagged keyword
|
1784
1845
|
##
|
1785
|
-
## @param
|
1846
|
+
## @param string [String] The text to tag
|
1786
1847
|
##
|
1787
|
-
def autotag(
|
1788
|
-
return unless
|
1789
|
-
return
|
1848
|
+
def autotag(string)
|
1849
|
+
return unless string
|
1850
|
+
return string unless @auto_tag
|
1790
1851
|
|
1791
|
-
original =
|
1852
|
+
original = string.dup
|
1853
|
+
text = string.dup
|
1792
1854
|
|
1793
1855
|
current_tags = text.scan(/@\w+/).map { |t| t.sub(/^@/, '') }
|
1794
1856
|
tagged = {
|
@@ -1810,6 +1872,7 @@ module Doing
|
|
1810
1872
|
|
1811
1873
|
@config['autotag']['synonyms'].each do |tag, v|
|
1812
1874
|
v.each do |word|
|
1875
|
+
word = word.wildcard_to_rx
|
1813
1876
|
next unless text =~ /\b#{word}\b/i
|
1814
1877
|
|
1815
1878
|
unless current_tags.include?(tag) || tagged[:whitelisted].include?(tag)
|
@@ -1823,7 +1886,12 @@ module Doing
|
|
1823
1886
|
@config['autotag']['transform'].each do |tag|
|
1824
1887
|
next unless tag =~ /\S+:\S+/
|
1825
1888
|
|
1826
|
-
|
1889
|
+
if tag =~ /::/
|
1890
|
+
rx, r = tag.split(/::/)
|
1891
|
+
else
|
1892
|
+
rx, r = tag.split(/:/)
|
1893
|
+
end
|
1894
|
+
|
1827
1895
|
flag_rx = %r{/([r]+)$}
|
1828
1896
|
if r =~ flag_rx
|
1829
1897
|
flags = r.match(flag_rx)[1].split(//)
|
@@ -2065,6 +2133,36 @@ EOS
|
|
2065
2133
|
end
|
2066
2134
|
end
|
2067
2135
|
|
2136
|
+
def configure(filename = nil)
|
2137
|
+
if filename
|
2138
|
+
Doing.config_with(filename, { ignore_local: true })
|
2139
|
+
elsif ENV['DOING_CONFIG']
|
2140
|
+
Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true })
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
Doing.logger.benchmark(:configure, :start)
|
2144
|
+
config = Doing.config
|
2145
|
+
Doing.logger.benchmark(:configure, :finish)
|
2146
|
+
|
2147
|
+
config.settings['backup_dir'] = ENV['DOING_BACKUP_DIR'] if ENV['DOING_BACKUP_DIR']
|
2148
|
+
@config = config.settings
|
2149
|
+
end
|
2150
|
+
|
2151
|
+
def get_diff(filename = nil)
|
2152
|
+
configure if @config.nil?
|
2153
|
+
|
2154
|
+
filename ||= @config['doing_file']
|
2155
|
+
init_doing_file(filename)
|
2156
|
+
current_content = @content.dup
|
2157
|
+
backup_file = Util::Backup.last_backup(filename, count: 1)
|
2158
|
+
raise DoingRuntimeError, 'No undo history to diff' if backup_file.nil?
|
2159
|
+
|
2160
|
+
backup = WWID.new
|
2161
|
+
backup.config = @config
|
2162
|
+
backup.init_doing_file(backup_file)
|
2163
|
+
current_content.diff(backup.content)
|
2164
|
+
end
|
2165
|
+
|
2068
2166
|
private
|
2069
2167
|
|
2070
2168
|
##
|
@@ -2097,13 +2195,16 @@ EOS
|
|
2097
2195
|
## @return [String] formatted output based on opt[:output]
|
2098
2196
|
## template trigger
|
2099
2197
|
##
|
2100
|
-
def output(items, title, is_single, opt
|
2198
|
+
def output(items, title, is_single, opt)
|
2199
|
+
opt ||= {}
|
2101
2200
|
out = nil
|
2102
2201
|
|
2103
2202
|
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
2104
2203
|
|
2105
2204
|
export_options = { page_title: title, is_single: is_single, options: opt }
|
2106
2205
|
|
2206
|
+
Hooks.trigger :pre_export, self, opt[:output], items
|
2207
|
+
|
2107
2208
|
Plugins.plugins[:export].each do |_, options|
|
2108
2209
|
next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
|
2109
2210
|
|
@@ -2141,7 +2242,8 @@ EOS
|
|
2141
2242
|
## section
|
2142
2243
|
## @param opt [Hash] Additional Options
|
2143
2244
|
##
|
2144
|
-
def do_archive(section, destination, opt
|
2245
|
+
def do_archive(section, destination, opt)
|
2246
|
+
opt ||= {}
|
2145
2247
|
count = opt[:count] || 0
|
2146
2248
|
tags = opt[:tags] || []
|
2147
2249
|
bool = opt[:bool] || :and
|
@@ -2152,10 +2254,11 @@ EOS
|
|
2152
2254
|
|
2153
2255
|
section_items = @content.in_section(section)
|
2154
2256
|
max = section_items.count - count.to_i
|
2257
|
+
moved_items = []
|
2155
2258
|
|
2156
2259
|
counter = 0
|
2157
2260
|
|
2158
|
-
@content.map
|
2261
|
+
@content.map do |item|
|
2159
2262
|
break if counter >= max
|
2160
2263
|
if opt[:before]
|
2161
2264
|
time_string = opt[:before]
|
@@ -2169,6 +2272,8 @@ EOS
|
|
2169
2272
|
else
|
2170
2273
|
counter += 1
|
2171
2274
|
item.move_to(destination, label: label, log: false)
|
2275
|
+
Hooks.trigger :post_entry_updated, self, item.dup
|
2276
|
+
item
|
2172
2277
|
end
|
2173
2278
|
end
|
2174
2279
|
|