doing 2.1.39 → 2.1.40

Sign up to get free protection for your applications and to get access to all the features.
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