doing 2.1.8 → 2.1.12
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 +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
|
|