doing 2.1.7 → 2.1.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +9 -9
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +39 -1
  6. data/Dockerfile +9 -0
  7. data/Dockerfile-2.6 +9 -0
  8. data/Dockerfile-2.7 +8 -0
  9. data/Dockerfile-3.0 +8 -0
  10. data/Gemfile.lock +1 -1
  11. data/README.md +1 -1
  12. data/Rakefile +51 -6
  13. data/bin/doing +2098 -1944
  14. data/docs/doc/Array.html +1 -1
  15. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  16. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  17. data/docs/doc/BooleanTermParser/Query.html +1 -1
  18. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  19. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  20. data/docs/doc/BooleanTermParser.html +1 -1
  21. data/docs/doc/Doing/Color.html +1 -1
  22. data/docs/doc/Doing/Completion.html +1 -1
  23. data/docs/doc/Doing/Configuration.html +2 -2
  24. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  25. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  26. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  27. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  28. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  29. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  30. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  31. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  32. data/docs/doc/Doing/Errors.html +1 -1
  33. data/docs/doc/Doing/Hooks.html +1 -1
  34. data/docs/doc/Doing/Item.html +1 -1
  35. data/docs/doc/Doing/Items.html +1 -1
  36. data/docs/doc/Doing/LogAdapter.html +1 -1
  37. data/docs/doc/Doing/Note.html +1 -1
  38. data/docs/doc/Doing/Pager.html +1 -1
  39. data/docs/doc/Doing/Plugins.html +1 -1
  40. data/docs/doc/Doing/Prompt.html +132 -18
  41. data/docs/doc/Doing/Section.html +1 -1
  42. data/docs/doc/Doing/TemplateString.html +2 -2
  43. data/docs/doc/Doing/Util/Backup.html +79 -2
  44. data/docs/doc/Doing/Util.html +1 -1
  45. data/docs/doc/Doing/WWID.html +90 -77
  46. data/docs/doc/Doing.html +2 -2
  47. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  48. data/docs/doc/GLI/Commands.html +1 -1
  49. data/docs/doc/GLI.html +1 -1
  50. data/docs/doc/Hash.html +1 -1
  51. data/docs/doc/PhraseParser/Operator.html +1 -1
  52. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  53. data/docs/doc/PhraseParser/Query.html +1 -1
  54. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  55. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  56. data/docs/doc/PhraseParser/TermClause.html +1 -1
  57. data/docs/doc/PhraseParser.html +1 -1
  58. data/docs/doc/Status.html +1 -1
  59. data/docs/doc/String.html +97 -1
  60. data/docs/doc/Symbol.html +36 -2
  61. data/docs/doc/Time.html +1 -1
  62. data/docs/doc/_index.html +1 -1
  63. data/docs/doc/file.README.html +2 -2
  64. data/docs/doc/index.html +2 -2
  65. data/docs/doc/method_list.html +299 -235
  66. data/docs/doc/top-level-namespace.html +1 -1
  67. data/docs/index.md +1 -1
  68. data/doing.rdoc +9 -2
  69. data/generate_completions.sh +1 -3
  70. data/lib/completion/_doing.zsh +1 -1
  71. data/lib/completion/doing.bash +2 -2
  72. data/lib/completion/doing.fish +2 -1
  73. data/lib/doing/completion/bash_completion.rb +2 -2
  74. data/lib/doing/completion/fish_completion.rb +2 -2
  75. data/lib/doing/completion/zsh_completion.rb +2 -2
  76. data/lib/doing/completion.rb +12 -2
  77. data/lib/doing/configuration.rb +19 -9
  78. data/lib/doing/hooks.rb +10 -5
  79. data/lib/doing/items.rb +16 -1
  80. data/lib/doing/log_adapter.rb +1 -0
  81. data/lib/doing/pager.rb +2 -20
  82. data/lib/doing/plugins/import/calendar_import.rb +5 -0
  83. data/lib/doing/plugins/import/doing_import.rb +2 -0
  84. data/lib/doing/plugins/import/timing_import.rb +5 -0
  85. data/lib/doing/prompt.rb +47 -8
  86. data/lib/doing/string.rb +20 -0
  87. data/lib/doing/symbol.rb +4 -0
  88. data/lib/doing/util_backup.rb +38 -8
  89. data/lib/doing/version.rb +1 -1
  90. data/lib/doing/wwid.rb +211 -106
  91. data/lib/doing.rb +1 -0
  92. data/lib/examples/plugins/hooks.rb +31 -0
  93. data/scripts/generate_bash_completions.rb +2 -2
  94. data/scripts/sort_commands.rb +59 -0
  95. metadata +7 -3
  96. 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 (Array or comma-separated string)
621
- ## @option opt [Symbol] :tag_bool (:and, :or, :not)
622
- ## @option opt [String] :search (string, optional regex with //)
623
- ## @option opt [Array] :date_filter [[Time]start, [Time]end]
624
- ## @option opt [Boolean] :only_timed
625
- ## @option opt [String] :before (Date/Time string, unparsed)
626
- ## @option opt [String] :after (Date/Time string, unparsed)
627
- ## @option opt [Boolean] :today
628
- ## @option opt [Boolean] :yesterday
629
- ## @option opt [Number] :count (Number to return)
630
- ## @option opt [String] :age ('old' or 'new')
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] =~ /^o/i
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
- selection = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, **opt)
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
- @content.update_item(item, reset_item(item, date: date, resume: res))
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
- res = opt[:force] ? true : Prompt.yn("Delete #{items.size} items?", default_response: 'y')
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! { |i| i.move_to(section, label: true) }
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
- @content.each do |item|
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)
@@ -1482,13 +1530,13 @@ module Doing
1482
1530
  ##
1483
1531
  ## @param opt [Hash] Additional Options
1484
1532
  ##
1485
- def list_section(opt = {}, items: Items.new)
1533
+ def list_section(opt, items: Items.new)
1486
1534
  opt[:config_template] ||= 'default'
1487
1535
 
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] ||= 'newest'
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[:interactive]
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 text [String] The text to tag
1846
+ ## @param string [String] The text to tag
1786
1847
  ##
1787
- def autotag(text)
1788
- return unless text
1789
- return text unless @auto_tag
1848
+ def autotag(string)
1849
+ return unless string
1850
+ return string unless @auto_tag
1790
1851
 
1791
- original = text.dup
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
- rx, r = tag.split(/:/)
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! do |item|
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