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.
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 +38 -0
  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 +88 -73
  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 +210 -105
  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)
@@ -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] ||= '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
 
data/lib/doing.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'doing/version'
3
4
  require 'time'
4
5
  require 'date'