doing 2.1.39 → 2.1.40

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/commands/config.rb +43 -34
  6. data/bin/commands/done.rb +1 -18
  7. data/bin/commands/finish.rb +30 -25
  8. data/bin/commands/grep.rb +3 -14
  9. data/bin/commands/last.rb +2 -8
  10. data/bin/commands/meanwhile.rb +13 -6
  11. data/bin/commands/on.rb +3 -16
  12. data/bin/commands/recent.rb +2 -8
  13. data/bin/commands/reset.rb +24 -1
  14. data/bin/commands/select.rb +1 -1
  15. data/bin/commands/show.rb +6 -17
  16. data/bin/commands/since.rb +1 -12
  17. data/bin/commands/today.rb +2 -13
  18. data/bin/commands/view.rb +1 -1
  19. data/bin/commands/yesterday.rb +2 -13
  20. data/bin/doing +15 -8
  21. data/docs/doc/Array.html +1 -1
  22. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  23. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  24. data/docs/doc/BooleanTermParser/Query.html +1 -1
  25. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  26. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  27. data/docs/doc/BooleanTermParser.html +1 -1
  28. data/docs/doc/Doing/Color.html +166 -20
  29. data/docs/doc/Doing/Completion.html +1 -1
  30. data/docs/doc/Doing/Configuration.html +1 -1
  31. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  32. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  33. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  34. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  35. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  36. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  37. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  38. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  39. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  40. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  41. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  42. data/docs/doc/Doing/Errors.html +9 -9
  43. data/docs/doc/Doing/Hooks.html +1 -1
  44. data/docs/doc/Doing/Item.html +90 -1615
  45. data/docs/doc/Doing/Items.html +121 -5
  46. data/docs/doc/Doing/Logger.html +1 -1
  47. data/docs/doc/Doing/Note.html +1 -1
  48. data/docs/doc/Doing/Pager.html +1 -1
  49. data/docs/doc/Doing/Plugins.html +1 -1
  50. data/docs/doc/Doing/Prompt.html +2 -2
  51. data/docs/doc/Doing/Section.html +1 -1
  52. data/docs/doc/Doing/TemplateString.html +2 -2
  53. data/docs/doc/Doing/Types.html +1 -1
  54. data/docs/doc/Doing/Util/Backup.html +5 -5
  55. data/docs/doc/Doing/Util.html +1 -1
  56. data/docs/doc/Doing/WWID.html +197 -4033
  57. data/docs/doc/Doing.html +2 -2
  58. data/docs/doc/FalseClass.html +1 -1
  59. data/docs/doc/GLI/Commands/Help.html +1 -1
  60. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  61. data/docs/doc/GLI/Commands.html +1 -1
  62. data/docs/doc/GLI.html +1 -1
  63. data/docs/doc/Hash.html +1 -1
  64. data/docs/doc/Object.html +1 -1
  65. data/docs/doc/PhraseParser/Operator.html +1 -1
  66. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  67. data/docs/doc/PhraseParser/Query.html +1 -1
  68. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  69. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  70. data/docs/doc/PhraseParser/TermClause.html +1 -1
  71. data/docs/doc/PhraseParser.html +1 -1
  72. data/docs/doc/Status.html +1 -1
  73. data/docs/doc/String.html +1 -1
  74. data/docs/doc/Symbol.html +1 -1
  75. data/docs/doc/Time.html +1 -1
  76. data/docs/doc/TrueClass.html +1 -1
  77. data/docs/doc/_index.html +26 -5
  78. data/docs/doc/class_list.html +1 -1
  79. data/docs/doc/file.README.html +2 -2
  80. data/docs/doc/index.html +2 -2
  81. data/docs/doc/method_list.html +293 -773
  82. data/docs/doc/top-level-namespace.html +3 -3
  83. data/docs/index.md +1 -1
  84. data/doing.rdoc +49 -7
  85. data/lib/completion/_doing.zsh +5 -5
  86. data/lib/completion/doing.bash +8 -8
  87. data/lib/completion/doing.fish +7 -2
  88. data/lib/doing/add_options.rb +31 -1
  89. data/lib/doing/chronify/array.rb +64 -22
  90. data/lib/doing/colors.rb +77 -30
  91. data/lib/doing/completion.rb +4 -5
  92. data/lib/doing/errors.rb +51 -35
  93. data/lib/doing/hooks.rb +3 -3
  94. data/lib/doing/item/dates.rb +112 -0
  95. data/lib/doing/item/query.rb +433 -0
  96. data/lib/doing/item/state.rb +59 -0
  97. data/lib/doing/item/tags.rb +87 -0
  98. data/lib/doing/item.rb +6 -667
  99. data/lib/doing/items.rb +38 -13
  100. data/lib/doing/plugin_manager.rb +3 -3
  101. data/lib/doing/plugins/export/template_export.rb +4 -4
  102. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  103. data/lib/doing/util_backup.rb +6 -8
  104. data/lib/doing/version.rb +1 -1
  105. data/lib/doing/wwid/display.rb +399 -0
  106. data/lib/doing/wwid/editor.rb +214 -0
  107. data/lib/doing/wwid/filetools.rb +186 -0
  108. data/lib/doing/wwid/filter.rb +218 -0
  109. data/lib/doing/wwid/guess.rb +87 -0
  110. data/lib/doing/wwid/interactive.rb +385 -0
  111. data/lib/doing/wwid/modify.rb +618 -0
  112. data/lib/doing/wwid/tags.rb +54 -0
  113. data/lib/doing/wwid/timers.rb +345 -0
  114. data/lib/doing/wwid/wwidutil.rb +104 -0
  115. data/lib/doing/wwid.rb +31 -2317
  116. metadata +19 -2
data/lib/doing/items.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Doing
4
- # Items Array
4
+ # A collection of Item objects
5
5
  class Items < Array
6
6
  attr_accessor :sections
7
7
 
@@ -27,13 +27,29 @@ module Doing
27
27
  def section?(section)
28
28
  has_section = false
29
29
  section = section.is_a?(Section) ? section.title.downcase : section.downcase
30
- @sections.each do |s|
31
- if s.title.downcase == section
32
- has_section = true
33
- break
34
- end
30
+ @sections.map { |i| i.title.downcase }.include?(section)
31
+ end
32
+
33
+ ##
34
+ ## Return the best section match for a search query
35
+ ##
36
+ ## @param frag The search query
37
+ ## @param distance The distance apart characters can be (fuzziness)
38
+ ##
39
+ ## @return [Section] (first) matching section object
40
+ ##
41
+ def guess_section(frag, distance: 2)
42
+ section = nil
43
+ re = frag.to_rx(distance: distance, case_type: :ignore)
44
+ @sections.each do |sect|
45
+ next unless sect.title =~ /#{re}/i
46
+
47
+ Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}"))
48
+ section = sect
49
+ break
35
50
  end
36
- has_section
51
+
52
+ section
37
53
  end
38
54
 
39
55
  # Add a new section to the sections array. Accepts
@@ -131,17 +147,26 @@ module Doing
131
147
  end
132
148
 
133
149
  ##
134
- ## Return Items containing items that don't exist in receiver
150
+ ## Return Items containing items that don't exist in
151
+ ## receiver
135
152
  ##
136
153
  ## @param items [Items] Receiver
137
154
  ##
155
+ ## @return [Hash] Hash of added and deleted items
156
+ ##
138
157
  def diff(items)
139
- diff = Items.new
140
- each do |item|
141
- res = items.select { |i| i.equal?(item) }
142
- diff.push(item) unless res.count.positive?
158
+ a = clone
159
+ b = items.clone
160
+
161
+ a.delete_if do |item|
162
+ if b.index(item)
163
+ b.delete(item)
164
+ true
165
+ else
166
+ false
167
+ end
143
168
  end
144
- diff
169
+ { deleted: b, added: a }
145
170
  end
146
171
 
147
172
  ##
@@ -84,11 +84,11 @@ module Doing
84
84
  def validate_plugin(title, type, klass)
85
85
  type = valid_type(type)
86
86
  if type == :import && !klass.respond_to?(:import)
87
- raise Errors::PluginUncallable.new('Import plugins must respond to :import', type: type, plugin: title)
87
+ raise Errors::PluginUncallable.new('Import plugins must respond to :import', type, title)
88
88
  end
89
89
 
90
90
  if type == :export && !klass.respond_to?(:render)
91
- raise Errors::PluginUncallable.new('Export plugins must respond to :render', type: type, plugin: title)
91
+ raise Errors::PluginUncallable.new('Export plugins must respond to :render', type, title)
92
92
  end
93
93
 
94
94
  type
@@ -113,7 +113,7 @@ module Doing
113
113
  when /^e(x(p(o(r(t)?)?)?)?)?$/
114
114
  :export
115
115
  else
116
- raise Errors::InvalidPluginType, 'Invalid plugin type'
116
+ raise Errors::InvalidPluginType.new('Invalid plugin type', 'unrecognized')
117
117
  end
118
118
 
119
119
  type.to_sym
@@ -17,7 +17,7 @@ module Doing
17
17
  end
18
18
 
19
19
  def self.render(wwid, items, variables: {})
20
- # Doing.logger.benchmark(:template_render, :start)
20
+ Doing.logger.benchmark(:template_render, :start)
21
21
  return if items.nil?
22
22
 
23
23
  opt = variables[:options]
@@ -126,18 +126,18 @@ module Doing
126
126
 
127
127
  output.gsub!(/\\%/, '%')
128
128
 
129
- output.highlight_search!(opt[:search]) if opt[:template] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
129
+ output.highlight_search!(opt[:search]) if opt[:output] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
130
130
 
131
131
  out += "#{output}\n"
132
132
  end
133
133
 
134
- # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
134
+ # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:output]}")
135
135
  if opt[:totals]
136
136
  out += wwid.tag_times(format: Doing.setting('timer_format').to_sym,
137
137
  sort_by: opt[:sort_tags],
138
138
  sort_order: opt[:tag_order])
139
139
  end
140
- # Doing.logger.benchmark(:template_render, :finish)
140
+ Doing.logger.benchmark(:template_render, :finish)
141
141
  out
142
142
  end
143
143
 
@@ -62,7 +62,7 @@ module Doing
62
62
  filename ||= Doing.setting('doing_file')
63
63
 
64
64
  backup_file = last_backup(filename, count: count)
65
- raise DoingRuntimeError, 'End of undo history' if backup_file.nil?
65
+ raise HistoryLimitError, 'End of undo history' if backup_file.nil?
66
66
 
67
67
  save_undone(filename)
68
68
  FileUtils.mv(backup_file, filename)
@@ -86,7 +86,7 @@ module Doing
86
86
  skipped = undones.slice!(0, count)
87
87
  undone = skipped.pop
88
88
 
89
- raise DoingRuntimeError, 'End of redo history' if undone.nil?
89
+ raise HistoryLimitError, 'End of redo history' if undone.nil?
90
90
 
91
91
  redo_file = File.join(backup_dir, undone)
92
92
 
@@ -118,8 +118,7 @@ module Doing
118
118
  filename ||= Doing.setting('doing_file')
119
119
 
120
120
  undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
121
-
122
- raise DoingRuntimeError, 'End of redo history' if undones.empty?
121
+ raise HistoryLimitError, 'End of redo history' if undones.empty?
123
122
 
124
123
  total = undones.count
125
124
  options = undones.each_with_object([]) do |file, arr|
@@ -128,8 +127,7 @@ module Doing
128
127
 
129
128
  arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
130
129
  end
131
-
132
- raise DoingRuntimeError, 'No backup files to load' if options.empty?
130
+ raise MissingBackupFile, 'No backup files to load' if options.empty?
133
131
 
134
132
  backup_file = show_menu(options, filename)
135
133
  idx = undones.index(File.basename(backup_file))
@@ -163,7 +161,7 @@ module Doing
163
161
  arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
164
162
  end
165
163
 
166
- raise DoingRuntimeError, 'No backup files to load' if options.empty?
164
+ raise MissingBackupFile, 'No backup files to load' if options.empty?
167
165
 
168
166
  backup_file = show_menu(options, filename)
169
167
  Util.write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
@@ -281,7 +279,7 @@ module Doing
281
279
  def create_backup_dir
282
280
  dir = File.expand_path(Doing.setting('backup_dir')) || File.join(user_home, '.doing_backup')
283
281
  if File.exist?(dir) && !File.directory?(dir)
284
- raise DoingRuntimeError, "Backup error: #{dir} is not a directory"
282
+ raise DoingNoTraceError.new("#{dir} is not a directory", topic: 'History:', exit_code: 27)
285
283
 
286
284
  end
287
285
 
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.39'
2
+ VERSION = '2.1.40'
3
3
  end
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class WWID
5
+ # Display methods for WWID class
6
+ module Display
7
+ ##
8
+ ## Display contents of a section based on options
9
+ ##
10
+ ## @param opt [Hash] Additional Options
11
+ ##
12
+ def list_section(opt, items: Items.new)
13
+ logger.benchmark(:list_section, :start)
14
+ opt[:config_template] ||= 'default'
15
+
16
+ tpl_cfg = Doing.setting(['templates', opt[:config_template]])
17
+
18
+ cfg = if opt[:view_template]
19
+ Doing.setting(['views', opt[:view_template]]).deep_merge(tpl_cfg, { extend_existing_arrays: true, sort_merged_arrays: true })
20
+ else
21
+ tpl_cfg
22
+ end
23
+
24
+ cfg.deep_merge({
25
+ 'wrap_width' => Doing.setting('wrap_width') || 0,
26
+ 'date_format' => Doing.setting('default_date_format'),
27
+ 'order' => Doing.setting('order') || :asc,
28
+ 'tags_color' => Doing.setting('tags_color'),
29
+ 'duration' => Doing.setting('duration'),
30
+ 'interval_format' => Doing.setting('interval_format')
31
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
32
+
33
+ opt[:duration] ||= cfg['duration'] || false
34
+ opt[:interval_format] ||= cfg['interval_format'] || 'text'
35
+ opt[:count] ||= 0
36
+ opt[:age] ||= :newest
37
+ opt[:age] = opt[:age].normalize_age
38
+ opt[:format] ||= cfg['date_format']
39
+ opt[:order] ||= cfg['order'] || :asc
40
+ opt[:tag_order] ||= :asc
41
+ opt[:tags_color] = cfg['tags_color'] || false if opt[:tags_color].nil?
42
+ opt[:template] ||= cfg['template']
43
+ opt[:sort_tags] ||= opt[:tag_sort]
44
+
45
+ # opt[:highlight] ||= true
46
+ title = ''
47
+ is_single = true
48
+ if opt[:section].nil?
49
+ opt[:section] = choose_section
50
+ title = opt[:section]
51
+ elsif opt[:section].instance_of?(String)
52
+ title = if opt[:section] =~ /^all$/i
53
+ if opt[:page_title]
54
+ opt[:page_title]
55
+ elsif opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
56
+ opt[:tag_filter]['tags'].map { |tag| "@#{tag}" }.join(' + ')
57
+ else
58
+ 'doing'
59
+ end
60
+ else
61
+ guess_section(opt[:section])
62
+ end
63
+ end
64
+
65
+ items = filter_items(items, opt: opt)
66
+
67
+ items.reverse! unless opt[:order].normalize_order == :desc
68
+
69
+ if opt[:delete]
70
+ delete_items(items, force: opt[:force])
71
+
72
+ write(@doing_file)
73
+ return
74
+ elsif opt[:editor]
75
+ edit_items(items)
76
+
77
+ write(@doing_file)
78
+ return
79
+ elsif opt[:interactive]
80
+ opt[:menu] = !opt[:force]
81
+ opt[:query] = '' # opt[:search]
82
+ opt[:multiple] = true
83
+ selected = Prompt.choose_from_items(items.reverse, include_section: opt[:section] =~ /^all$/i, **opt)
84
+
85
+ raise NoResults, 'no items selected' if selected.nil? || selected.empty?
86
+
87
+ act_on(selected, opt)
88
+ return
89
+ end
90
+
91
+ opt[:output] ||= 'template'
92
+ opt[:wrap_width] ||= Doing.setting('templates.default.wrap_width', 0)
93
+
94
+ logger.benchmark(:list_section, :finish)
95
+ output(items, title, is_single, opt)
96
+ end
97
+
98
+ ##
99
+ ## Display entries within a date range
100
+ ##
101
+ ## @param dates [Array] [start, end]
102
+ ## @param section [String] The section
103
+ ## @param times (Bool) Show times
104
+ ## @param output [String] Output format
105
+ ## @param opt [Hash] Additional Options
106
+ ##
107
+ def list_date(dates, section, times = nil, output = nil, opt)
108
+ opt ||= {}
109
+ opt[:totals] ||= false
110
+ opt[:sort_tags] ||= false
111
+ section = guess_section(section)
112
+ # :date_filter expects an array with start and end date
113
+ dates = dates.split_date_range if dates.instance_of?(String)
114
+
115
+ opt[:section] = section
116
+ opt[:count] = 0
117
+ opt[:order] = :asc
118
+ opt[:date_filter] = dates
119
+ opt[:times] = times
120
+ opt[:output] = output
121
+
122
+ time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
123
+ if opt[:from] && opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx
124
+ opt[:time_filter] = opt[:from]
125
+ end
126
+
127
+ list_section(opt)
128
+ end
129
+
130
+ ##
131
+ ## Show all entries from the current day
132
+ ##
133
+ ## @param times [Boolean] show times
134
+ ## @param output [String] output format
135
+ ## @param opt [Hash] Options
136
+ ##
137
+ def today(times = true, output = nil, opt)
138
+ opt ||= {}
139
+ opt[:totals] ||= false
140
+ opt[:sort_tags] ||= false
141
+
142
+ cfg = Doing.setting('templates').deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
143
+ 'wrap_width' => Doing.setting('wrap_width') || 0,
144
+ 'date_format' => Doing.setting('default_date_format'),
145
+ 'order' => Doing.setting('order') || :asc,
146
+ 'tags_color' => Doing.setting('tags_color'),
147
+ 'duration' => Doing.setting('duration'),
148
+ 'interval_format' => Doing.setting('interval_format')
149
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
150
+
151
+ template = opt[:template] || cfg['template']
152
+
153
+ opt[:duration] ||= cfg['duration'] || false
154
+ opt[:interval_format] ||= cfg['interval_format'] || 'text'
155
+
156
+ options = {
157
+ after: opt[:after],
158
+ before: opt[:before],
159
+ count: 0,
160
+ duration: opt[:duration],
161
+ from: opt[:from],
162
+ format: cfg['date_format'],
163
+ interval_format: opt[:interval_format],
164
+ only_timed: opt[:only_timed],
165
+ order: cfg['order'] || :asc,
166
+ output: output,
167
+ section: opt[:section],
168
+ sort_tags: opt[:sort_tags],
169
+ template: template,
170
+ times: times,
171
+ today: true,
172
+ totals: opt[:totals],
173
+ wrap_width: cfg['wrap_width'],
174
+ tags_color: cfg['tags_color'],
175
+ config_template: opt[:config_template]
176
+ }
177
+ list_section(options)
178
+ end
179
+
180
+ ##
181
+ ## Show entries from the previous day
182
+ ##
183
+ ## @param section [String] The section
184
+ ## @param times (Bool) Show times
185
+ ## @param output [String] Output format
186
+ ## @param opt [Hash] Additional Options
187
+ ##
188
+ def yesterday(section, times = nil, output = nil, opt)
189
+ opt ||= {}
190
+ opt[:totals] ||= false
191
+ opt[:sort_tags] ||= false
192
+ opt[:config_template] ||= 'today'
193
+ opt[:yesterday] = true
194
+
195
+ section = guess_section(section)
196
+ y = (Time.now - (60 * 60 * 24)).strftime('%Y-%m-%d')
197
+ opt[:after] = "#{y} #{opt[:after]}" if opt[:after]
198
+ opt[:before] = "#{y} #{opt[:before]}" if opt[:before]
199
+
200
+ opt[:output] = output
201
+ opt[:section] = section
202
+ opt[:times] = times
203
+ opt[:count] = 0
204
+
205
+ list_section(opt)
206
+ end
207
+
208
+ ##
209
+ ## Show recent entries
210
+ ##
211
+ ## @param count [Integer] The number to show
212
+ ## @param section [String] The section to show from, default Currently
213
+ ## @param opt [Hash] Additional Options
214
+ ##
215
+ def recent(count = 10, section = nil, opt)
216
+ opt ||= {}
217
+ times = opt[:t] || true
218
+ opt[:totals] ||= false
219
+ opt[:sort_tags] ||= false
220
+
221
+ cfg = Doing.setting('templates.recent').deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
222
+ 'wrap_width' => Doing.setting('wrap_width') || 0,
223
+ 'date_format' => Doing.setting('default_date_format'),
224
+ 'order' => Doing.setting('order') || :asc,
225
+ 'tags_color' => Doing.setting('tags_color'),
226
+ 'duration' => Doing.setting('duration'),
227
+ 'interval_format' => Doing.setting('interval_format')
228
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
229
+ opt[:duration] ||= cfg['duration'] || false
230
+ opt[:interval_format] ||= cfg['interval_format'] || 'text'
231
+
232
+ section ||= Doing.setting('current_section')
233
+ section = guess_section(section)
234
+
235
+ opt[:section] = section
236
+ opt[:wrap_width] = cfg['wrap_width']
237
+ opt[:count] = count
238
+ opt[:format] = cfg['date_format']
239
+ opt[:template] = opt[:template] || cfg['template']
240
+ opt[:order] = :asc
241
+ opt[:times] = times
242
+
243
+ list_section(opt)
244
+ end
245
+
246
+ ##
247
+ ## Show the last entry
248
+ ##
249
+ ## @param times (Bool) Show times
250
+ ## @param section [String] Section to pull from, default Currently
251
+ ##
252
+ def last(times: true, section: nil, options: {})
253
+ section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
254
+ cfg = Doing.setting(['templates', options[:config_template]]).deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
255
+ 'wrap_width' => Doing.setting('wrap_width', 0),
256
+ 'date_format' => Doing.setting('default_date_format'),
257
+ 'order' => Doing.setting('order', :asc),
258
+ 'tags_color' => Doing.setting('tags_color'),
259
+ 'duration' => Doing.setting('duration'),
260
+ 'interval_format' => Doing.setting('interval_format')
261
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
262
+ options[:duration] ||= cfg['duration'] || false
263
+ options[:interval_format] ||= cfg['interval_format'] || 'text'
264
+
265
+ opts = {
266
+ case: options[:case],
267
+ config_template: options[:config_template] || 'last',
268
+ count: 1,
269
+ delete: options[:delete],
270
+ duration: options[:duration],
271
+ format: cfg['date_format'],
272
+ interval_format: options[:interval_format],
273
+ not: options[:negate],
274
+ output: options[:output],
275
+ section: section,
276
+ template: options[:template] || cfg['template'],
277
+ times: times,
278
+ val: options[:val],
279
+ wrap_width: cfg['wrap_width']
280
+ }
281
+
282
+ if options[:tag]
283
+ opts[:tag_filter] = {
284
+ 'tags' => options[:tag],
285
+ 'bool' => options[:tag_bool]
286
+ }
287
+ end
288
+
289
+ opts[:search] = options[:search] if options[:search]
290
+
291
+ list_section(opts)
292
+ end
293
+
294
+ ##
295
+ ## Return the content of the last note for a given section
296
+ ##
297
+ ## @param section [String] The section to retrieve from, default
298
+ ## All
299
+ ##
300
+ def last_note(section = 'All')
301
+ section = guess_section(section)
302
+
303
+ last_item = last_entry({ section: section })
304
+
305
+ raise NoEntryError, 'No entry found' unless last_item
306
+
307
+ logger.log_now(:info, 'Edit note:', last_item.title)
308
+
309
+ note = last_item.note&.to_s || ''
310
+ "#{last_item.title}\n# EDIT BELOW THIS LINE ------------\n#{note}"
311
+ end
312
+
313
+ ##
314
+ ## Get the last entry
315
+ ##
316
+ ## @param opt [Hash] Additional Options
317
+ ##
318
+ def last_entry(opt)
319
+ opt ||= {}
320
+ opt[:tag_bool] ||= :and
321
+ opt[:section] ||= Doing.setting('current_section')
322
+
323
+ items = filter_items(Items.new, opt: opt)
324
+
325
+ logger.debug('Filtered:', "Parameters matched #{items.count} entries")
326
+
327
+ if opt[:interactive]
328
+ last_entry = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i,
329
+ menu: true,
330
+ header: '',
331
+ prompt: 'Select an entry > ',
332
+ multiple: false,
333
+ sort: false,
334
+ show_if_single: true
335
+ )
336
+ else
337
+ last_entry = items.max_by { |item| item.date }
338
+ end
339
+
340
+ last_entry
341
+ end
342
+
343
+ private
344
+
345
+ ##
346
+ ## Generate output using available export plugins
347
+ ##
348
+ ## @param items [Array] The items
349
+ ## @param title [String] Page title
350
+ ## @param is_single [Boolean] Indicates if single
351
+ ## section
352
+ ## @param opt [Hash] Additional options
353
+ ##
354
+ ## @return [String] formatted output based on opt[:output]
355
+ ## template trigger
356
+ ## @api private
357
+ def output(items, title, is_single, opt)
358
+ logger.benchmark(:output, :start)
359
+ opt ||= {}
360
+ out = nil
361
+
362
+ raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
363
+
364
+ export_options = { page_title: title, is_single: is_single, options: opt }
365
+
366
+ Hooks.trigger :pre_export, self, opt[:output], items
367
+
368
+ Plugins.plugins[:export].each do |_, options|
369
+ next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
370
+
371
+ out = options[:class].render(self, items, variables: export_options)
372
+ break
373
+ end
374
+
375
+ logger.debug('Output:', "#{items.count} #{items.count == 1 ? 'item' : 'items'} shown")
376
+ logger.benchmark(:output, :finish)
377
+ out
378
+ end
379
+
380
+ ##
381
+ ## Get next item in the index
382
+ ##
383
+ ## @param item [Item] target item
384
+ ## @param options [Hash] additional options
385
+ ## @see #filter_items
386
+ ##
387
+ ## @return [Item] the next chronological item in the index
388
+ ##
389
+ def next_item(item, options = {})
390
+ options ||= {}
391
+ items = filter_items(Items.new, opt: options)
392
+
393
+ idx = items.index(item)
394
+
395
+ idx.positive? ? items[idx - 1] : nil
396
+ end
397
+ end
398
+ end
399
+ end