doing 1.0.90 → 2.0.2.pre

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +590 -0
  4. data/COMMANDS.md +1181 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +110 -0
  7. data/LICENSE +23 -0
  8. data/README.md +14 -697
  9. data/Rakefile +79 -0
  10. data/_config.yml +1 -0
  11. data/bin/doing +1037 -481
  12. data/doing.fish +278 -0
  13. data/doing.gemspec +34 -0
  14. data/doing.rdoc +1759 -0
  15. data/example_plugin.rb +209 -0
  16. data/generate_completions.sh +4 -0
  17. data/img/doing-colors.jpg +0 -0
  18. data/img/doing-printf-wrap-800.jpg +0 -0
  19. data/img/doing-show-note-formatting-800.jpg +0 -0
  20. data/lib/completion/_doing.zsh +151 -0
  21. data/lib/completion/doing.bash +416 -0
  22. data/lib/completion/doing.fish +278 -0
  23. data/lib/doing/array.rb +8 -0
  24. data/lib/doing/cli_status.rb +66 -0
  25. data/lib/doing/colors.rb +136 -0
  26. data/lib/doing/configuration.rb +310 -0
  27. data/lib/doing/errors.rb +102 -0
  28. data/lib/doing/hash.rb +31 -0
  29. data/lib/doing/hooks.rb +59 -0
  30. data/lib/doing/item.rb +155 -0
  31. data/lib/doing/log_adapter.rb +342 -0
  32. data/lib/doing/markdown_document_listener.rb +174 -0
  33. data/lib/doing/note.rb +59 -0
  34. data/lib/doing/pager.rb +95 -0
  35. data/lib/doing/plugin_manager.rb +208 -0
  36. data/lib/doing/plugins/export/csv_export.rb +48 -0
  37. data/lib/doing/plugins/export/html_export.rb +83 -0
  38. data/lib/doing/plugins/export/json_export.rb +140 -0
  39. data/lib/doing/plugins/export/markdown_export.rb +85 -0
  40. data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
  41. data/lib/doing/plugins/export/template_export.rb +141 -0
  42. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  43. data/lib/doing/plugins/import/calendar_import.rb +76 -0
  44. data/lib/doing/plugins/import/doing_import.rb +144 -0
  45. data/lib/doing/plugins/import/timing_import.rb +78 -0
  46. data/lib/doing/string.rb +346 -0
  47. data/lib/doing/symbol.rb +16 -0
  48. data/lib/doing/time.rb +18 -0
  49. data/lib/doing/util.rb +186 -0
  50. data/lib/doing/version.rb +1 -1
  51. data/lib/doing/wwid.rb +1838 -2266
  52. data/lib/doing/wwidfile.rb +117 -0
  53. data/lib/doing.rb +43 -2
  54. data/lib/examples/commands/wiki.rb +80 -0
  55. data/lib/examples/plugins/hooks.rb +22 -0
  56. data/lib/examples/plugins/say_export.rb +202 -0
  57. data/lib/examples/plugins/templates/wiki.css +169 -0
  58. data/lib/examples/plugins/templates/wiki.haml +27 -0
  59. data/lib/examples/plugins/templates/wiki_index.haml +18 -0
  60. data/lib/examples/plugins/wiki_export.rb +87 -0
  61. data/lib/templates/doing-markdown.erb +5 -0
  62. data/man/doing.1 +964 -0
  63. data/man/doing.1.html +711 -0
  64. data/man/doing.1.ronn +600 -0
  65. data/package-lock.json +3 -0
  66. data/rdoc_to_mmd.rb +42 -0
  67. data/rdocfixer.rb +13 -0
  68. data/scripts/generate_bash_completions.rb +210 -0
  69. data/scripts/generate_fish_completions.rb +201 -0
  70. data/scripts/generate_zsh_completions.rb +164 -0
  71. metadata +82 -6
  72. data/lib/doing/helpers.rb +0 -121
data/bin/doing CHANGED
@@ -20,17 +20,49 @@ if class_exists? 'Encoding'
20
20
  end
21
21
 
22
22
  include GLI::App
23
+ include Doing::Errors
23
24
  version Doing::VERSION
25
+ hide_commands_without_desc true
26
+ autocomplete_commands true
27
+
28
+ REGEX_BOOL = /^(?:and|all|any|or|not|none)$/i
29
+ REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i
30
+
31
+ InvalidExportType = Class.new(RuntimeError)
32
+ MissingConfigFile = Class.new(RuntimeError)
33
+
34
+ colors = Doing::Color
35
+ wwid = Doing::WWID.new
36
+
37
+ Doing.logger.log_level = :info
38
+
39
+ if ENV['DOING_LOG_LEVEL'] || ENV['DOING_DEBUG'] || ENV['DOING_QUIET'] || ENV['DOING_VERBOSE'] || ENV['DOING_PLUGIN_DEBUG']
40
+ # Quiet always wins
41
+ if ENV['DOING_QUIET'] && ENV['DOING_QUIET'].truthy?
42
+ Doing.logger.log_level = :error
43
+ elsif (ENV['DOING_PLUGIN_DEBUG'] && ENV['DOING_PLUGIN_DEBUG'].truthy?)
44
+ Doing.logger.log_level = :debug
45
+ elsif (ENV['DOING_DEBUG'] && ENV['DOING_DEBUG'].truthy?)
46
+ Doing.logger.log_level = :debug
47
+ elsif ENV['DOING_LOG_LEVEL']
48
+ Doing.logger.log_level = ENV['DOING_LOG_LEVEL']
49
+ end
50
+ end
24
51
 
25
- wwid = WWID.new
26
- wwid.user_home = if Dir.respond_to?('home')
27
- Dir.home
28
- else
29
- File.expand_path('~')
30
- end
31
- wwid.configure
52
+ if ENV['DOING_CONFIG']
53
+ Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true })
54
+ end
55
+
56
+ config = Doing.config
57
+ settings = config.settings
58
+ wwid.config = settings
59
+
60
+ if config.settings.dig('plugins', 'command_path')
61
+ commands_from File.expand_path(config.settings.dig('plugins', 'command_path'))
62
+ end
32
63
 
33
64
  program_desc 'A CLI for a What Was I Doing system'
65
+ program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text record of what you've been doing, complete with tag-based time tracking. The command line tool allows you to add entries, annotate with tags and notes, and view your entries with myriad options, with a focus on a "natural" language syntax.)
34
66
 
35
67
  default_command :recent
36
68
  # sort_help :manually
@@ -41,23 +73,49 @@ switch [:notes], default_value: true, negatable: true
41
73
  desc 'Send results report to STDOUT instead of STDERR'
42
74
  switch [:stdout], default_value: false, negatable: false
43
75
 
76
+ desc 'Use a pager when output is longer than screen'
77
+ switch %i[p pager], default_value: settings['paginate']
78
+
79
+ desc 'Answer yes/no menus with default option'
80
+ switch [:default], default_value: false
81
+
44
82
  desc 'Exclude auto tags and default tags'
45
- switch %i[x noauto], default_value: false
83
+ switch %i[x noauto], default_value: false, negatable: false
84
+
85
+ desc 'Colored output'
86
+ switch %i[color], default_value: true
46
87
 
47
- desc 'Use a specific configuration file'
48
- flag [:config_file], default_value: wwid.config_file
88
+ desc 'Silence info messages'
89
+ switch %i[q quiet], default_value: false, negatable: false
90
+
91
+ desc 'Verbose output'
92
+ switch %i[debug], default_value: false, negatable: false
93
+
94
+ desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead.'
95
+ flag [:config_file], default_value: config.config_file
49
96
 
50
97
  desc 'Specify a different doing_file'
51
98
  flag %i[f doing_file]
52
99
 
53
100
  desc 'Add an entry'
101
+ long_desc %(Record what you're starting now, or backdate the start time using natural language.
102
+
103
+ A parenthetical at the end of the entry will be converted to a note.
104
+
105
+ Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
54
106
  arg_name 'ENTRY'
55
107
  command %i[now next] do |c|
108
+ c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
109
+ c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
110
+ c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
111
+ c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
112
+ c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
113
+
56
114
  c.desc 'Section'
57
115
  c.arg_name 'NAME'
58
116
  c.flag %i[s section]
59
117
 
60
- c.desc "Edit entry with #{ENV['EDITOR']}"
118
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
61
119
  c.switch %i[e editor], negatable: false, default_value: false
62
120
 
63
121
  c.desc 'Backdate start time [4pm|20m|2h|yesterday noon]'
@@ -67,7 +125,7 @@ command %i[now next] do |c|
67
125
  c.desc 'Timed entry, marks last entry in section as @done'
68
126
  c.switch %i[f finish_last], negatable: false, default_value: false
69
127
 
70
- c.desc 'Note'
128
+ c.desc 'Include a note'
71
129
  c.arg_name 'TEXT'
72
130
  c.flag %i[n note]
73
131
 
@@ -77,9 +135,9 @@ command %i[now next] do |c|
77
135
 
78
136
  c.action do |_global_options, options, args|
79
137
  if options[:back]
80
- date = wwid.chronify(options[:back])
138
+ date = wwid.chronify(options[:back], guess: :begin)
81
139
 
82
- exit_now! 'Unable to parse date string' if date.nil?
140
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
83
141
  else
84
142
  date = Time.now
85
143
  end
@@ -87,17 +145,17 @@ command %i[now next] do |c|
87
145
  if options[:section]
88
146
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
89
147
  else
90
- options[:section] = wwid.config['current_section']
148
+ options[:section] = settings['current_section']
91
149
  end
92
150
 
93
151
  if options[:e] || (args.empty? && $stdin.stat.size.zero?)
94
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
152
+ raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
95
153
 
96
154
  input = ''
97
155
  input += args.join(' ') unless args.empty?
98
156
  input = wwid.fork_editor(input).strip
99
157
 
100
- exit_now! 'No content' if input.empty?
158
+ raise Doing::Errors::EmptyInput, 'No content' if input.empty?
101
159
 
102
160
  title, note = wwid.format_input(input)
103
161
  note.push(options[:n]) if options[:n]
@@ -115,14 +173,76 @@ command %i[now next] do |c|
115
173
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
116
174
  wwid.write(wwid.doing_file)
117
175
  else
118
- exit_now! 'You must provide content when creating a new entry'
176
+ raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
119
177
  end
120
178
  end
121
179
  end
122
180
 
181
+ desc 'Reset the start time of an entry'
182
+ command %i[reset begin] do |c|
183
+ c.desc 'Set the start date of an item to now'
184
+ c.arg_name 'NAME'
185
+ c.flag %i[s section], default_value: 'All'
186
+
187
+ c.desc 'Resume entry (remove @done)'
188
+ c.switch %i[r resume], default_value: true
189
+
190
+ c.desc 'Reset last entry matching tag'
191
+ c.arg_name 'TAG'
192
+ c.flag [:tag]
193
+
194
+ c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
195
+ c.arg_name 'QUERY'
196
+ c.flag [:search]
197
+
198
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
199
+ c.arg_name 'BOOLEAN'
200
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
201
+
202
+ c.desc 'Select from a menu of matching entries'
203
+ c.switch %i[i interactive]
204
+
205
+ c.action do |global_options, options, args|
206
+ if options[:section]
207
+ options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
208
+ end
209
+
210
+ options[:tag_bool] = options[:bool].normalize_bool
211
+
212
+ items = wwid.filter_items([], opt: options)
213
+
214
+ if options[:interactive]
215
+ last_entry = wwid.choose_from_items(items, {
216
+ menu: true,
217
+ header: '',
218
+ prompt: 'Select an entry to start/reset > ',
219
+ multiple: false,
220
+ sort: false,
221
+ show_if_single: true
222
+ }, include_section: options[:section].nil? )
223
+ else
224
+ last_entry = items.last
225
+ end
226
+
227
+ unless last_entry
228
+ Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
229
+ return
230
+ end
231
+
232
+ wwid.reset_item(last_entry, resume: options[:resume])
233
+
234
+ # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
235
+
236
+ wwid.write(wwid.doing_file)
237
+ end
238
+ end
239
+
240
+
123
241
  desc 'Add a note to the last entry'
124
242
  long_desc %(
125
- If -r is provided with no other arguments, the last note is removed. If new content is specified through arguments or STDIN, any previous note will be replaced with the new one.
243
+ If -r is provided with no other arguments, the last note is removed.
244
+ If new content is specified through arguments or STDIN, any previous
245
+ note will be replaced with the new one.
126
246
 
127
247
  Use -e to load the last entry in a text editor where you can append a note.
128
248
  )
@@ -132,47 +252,77 @@ command :note do |c|
132
252
  c.arg_name 'NAME'
133
253
  c.flag %i[s section], default_value: 'All'
134
254
 
135
- c.desc "Edit entry with #{ENV['EDITOR']}"
255
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
136
256
  c.switch %i[e editor], negatable: false, default_value: false
137
257
 
138
258
  c.desc "Replace/Remove last entry's note (default append)"
139
259
  c.switch %i[r remove], negatable: false, default_value: false
140
260
 
261
+ c.desc 'Add/remove note from last entry matching tag'
262
+ c.arg_name 'TAG'
263
+ c.flag [:tag]
264
+
265
+ c.desc 'Add/remove note from last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
266
+ c.arg_name 'QUERY'
267
+ c.flag [:search]
268
+
269
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
270
+ c.arg_name 'BOOLEAN'
271
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
272
+
273
+ c.desc 'Select item for new note from a menu of matching entries'
274
+ c.switch %i[i interactive]
275
+
141
276
  c.action do |_global_options, options, args|
142
277
  if options[:section]
143
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
278
+ options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
144
279
  end
145
280
 
281
+ options[:tag_bool] = options[:bool].normalize_bool
282
+
283
+ last_entry = wwid.last_entry(options)
284
+
285
+ unless last_entry
286
+ Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
287
+ return
288
+ end
289
+
290
+ last_note = last_entry.note || Doing::Note.new
291
+ new_note = Doing::Note.new
292
+
146
293
  if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
147
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
294
+ raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
148
295
 
149
296
  input = !args.empty? ? args.join(' ') : ''
150
297
 
151
- prev_input = wwid.last_note(section) || ''
152
- prev_input = prev_input.join("\n") if prev_input.instance_of?(Array)
153
- input = prev_input + input
298
+ if options[:remove]
299
+ prev_input = Doing::Note.new
300
+ else
301
+ prev_input = last_entry.note || Doing::Note.new
302
+ end
154
303
 
155
- input = wwid.fork_editor(input).strip
156
- exit_now! 'No content, cancelled' unless input
304
+ input = prev_input.add(input)
157
305
 
306
+ input = wwid.fork_editor([last_entry.title, '### Edit below this line', input.to_s].join("\n")).strip
158
307
  _title, note = wwid.format_input(input)
159
-
160
- exit_now! 'No note content' unless note
161
-
162
- wwid.note_last(section, note, replace: true)
308
+ options[:remove] = true
309
+ new_note.add(note)
163
310
  elsif !args.empty?
164
- title, note = wwid.format_input(args.join(' '))
165
- note.insert(0, title)
166
- wwid.note_last(section, note, replace: options[:r])
311
+ new_note.add(args.join(' '))
167
312
  elsif $stdin.stat.size.positive?
168
- title, note = wwid.format_input($stdin.read)
169
- note.insert(0, title)
170
- wwid.note_last(section, note, replace: options[:r])
171
- elsif options[:r]
172
- wwid.note_last(section, [], replace: true)
313
+ new_note.add($stdin.read)
173
314
  else
174
- exit_now! 'You must provide content when adding a note'
315
+ raise Doing::Errors::EmptyInput, 'You must provide content when adding a note' unless options[:remove]
175
316
  end
317
+
318
+ if last_note.equal?(new_note)
319
+ Doing.logger.debug('Skipped:', 'No note change')
320
+ else
321
+ last_note.add(new_note, replace: options[:remove])
322
+ Doing.logger.info('Entry updated:', last_entry.title)
323
+ end
324
+ # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
325
+
176
326
  wwid.write(wwid.doing_file)
177
327
  end
178
328
  end
@@ -184,7 +334,7 @@ command :meanwhile do |c|
184
334
  c.arg_name 'NAME'
185
335
  c.flag %i[s section]
186
336
 
187
- c.desc "Edit entry with #{ENV['EDITOR']}"
337
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
188
338
  c.switch %i[e editor], negatable: false, default_value: false
189
339
 
190
340
  c.desc 'Archive previous @meanwhile entry'
@@ -200,9 +350,9 @@ command :meanwhile do |c|
200
350
 
201
351
  c.action do |_global_options, options, args|
202
352
  if options[:back]
203
- date = wwid.chronify(options[:back])
353
+ date = wwid.chronify(options[:back], guess: :begin)
204
354
 
205
- exit_now! 'Unable to parse date string' if date.nil?
355
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
206
356
  else
207
357
  date = Time.now
208
358
  end
@@ -210,12 +360,12 @@ command :meanwhile do |c|
210
360
  if options[:section]
211
361
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
212
362
  else
213
- section = wwid.config['current_section']
363
+ section = settings['current_section']
214
364
  end
215
365
  input = ''
216
366
 
217
367
  if options[:e]
218
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
368
+ raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
219
369
 
220
370
  input += args.join(' ') unless args.empty?
221
371
  input = wwid.fork_editor(input).strip
@@ -243,33 +393,60 @@ command :meanwhile do |c|
243
393
  end
244
394
  end
245
395
 
246
- desc 'Output HTML and CSS templates for customization'
396
+ desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
247
397
  long_desc %(
248
398
  Templates are printed to STDOUT for piping to a file.
249
399
  Save them and use them in the configuration file under html_template.
250
400
 
251
- Example `doing template HAML > ~/styles/my_doing.haml`
401
+ Example `doing template haml > ~/styles/my_doing.haml`
252
402
  )
253
- arg_name 'TYPE', must_match: /^(?:html|haml|css)/i
403
+ arg_name 'TYPE', must_match: Doing::Plugins.template_regex
254
404
  command :template do |c|
405
+ c.desc 'List all available templates'
406
+ c.switch %i[l list]
407
+
408
+ c.desc 'List in single column for completion'
409
+ c.switch %i[c]
410
+
255
411
  c.action do |_global_options, options, args|
256
- exit_now! 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
412
+ if options[:list] || options[:c]
413
+ if options[:c]
414
+ $stdout.print Doing::Plugins.plugin_templates.join("\n")
415
+ else
416
+ $stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
417
+ end
418
+ return
419
+ end
257
420
 
258
- case args[0]
259
- when /html|haml/i
260
- $stdout.puts wwid.haml_template
261
- when /css/i
262
- $stdout.puts wwid.css_template
421
+ if args.empty?
422
+ type = wwid.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
263
423
  else
264
- exit_now! 'Invalid type specified, must be HAML or CSS'
424
+ type = args[0]
265
425
  end
426
+
427
+ raise Doing::Errors::InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
428
+
429
+ $stdout.puts Doing::Plugins.template_for_trigger(type)
430
+
431
+ # case args[0]
432
+ # when /html|haml/i
433
+ # $stdout.puts wwid.haml_template
434
+ # when /css/i
435
+ # $stdout.puts wwid.css_template
436
+ # when /markdown|md|erb/i
437
+ # $stdout.puts wwid.markdown_template
438
+ # else
439
+ # exit_now! 'Invalid type specified, must be HAML or CSS'
440
+ # end
266
441
  end
267
442
  end
268
443
 
269
- desc 'Display an interactive menu to perform operations (requires fzf)'
444
+ desc 'Display an interactive menu to perform operations'
270
445
  long_desc 'List all entries and select with typeahead fuzzy matching.
271
446
 
272
- Multiple selections are allowed, hit tab to add the highlighted entry to the selection. Return processes the selected entries.'
447
+ Multiple selections are allowed, hit tab to add the highlighted entry to the
448
+ selection, and use ctrl-a to select all visible items. Return processes the
449
+ selected entries.'
273
450
  command :select do |c|
274
451
  c.desc 'Select from a specific section'
275
452
  c.arg_name 'SECTION'
@@ -294,7 +471,7 @@ command :select do |c|
294
471
 
295
472
  c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
296
473
  c.arg_name 'QUERY'
297
- c.flag %i[q query]
474
+ c.flag %i[q query search]
298
475
 
299
476
  c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
300
477
  c.switch %i[menu], negatable: true, default_value: true
@@ -321,12 +498,17 @@ command :select do |c|
321
498
  c.arg_name 'FILE'
322
499
  c.flag %i[save_to]
323
500
 
324
- c.desc 'Output entries to format (doing|taskpaper|csv|html|json|template|timeline)'
501
+ c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})"
325
502
  c.arg_name 'FORMAT'
326
- c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i
503
+ c.flag %i[o output]
504
+
505
+ c.desc "Copy selection as a new entry with current time and no @done tag. Only works with single selections. Can be combined with --editor."
506
+ c.switch %i[again resume]
327
507
 
328
508
  c.action do |_global_options, options, args|
329
- exit_now! "--no-menu requires --query" if !options[:menu] && !options[:query]
509
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
510
+
511
+ raise Doing::Errors::InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
330
512
 
331
513
  wwid.interactive(options)
332
514
  end
@@ -335,7 +517,7 @@ end
335
517
  desc 'Add an item to the Later section'
336
518
  arg_name 'ENTRY'
337
519
  command :later do |c|
338
- c.desc "Edit entry with #{ENV['EDITOR']}"
520
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
339
521
  c.switch %i[e editor], negatable: false, default_value: false
340
522
 
341
523
  c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
@@ -348,18 +530,18 @@ command :later do |c|
348
530
 
349
531
  c.action do |_global_options, options, args|
350
532
  if options[:back]
351
- date = wwid.chronify(options[:back])
352
- exit_now! 'Unable to parse date string' if date.nil?
533
+ date = wwid.chronify(options[:back], guess: :begin)
534
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
353
535
  else
354
536
  date = Time.now
355
537
  end
356
538
 
357
539
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
358
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
540
+ raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
359
541
 
360
542
  input = args.empty? ? '' : args.join(' ')
361
543
  input = wwid.fork_editor(input).strip
362
- exit_now! 'No content' unless input && !input.empty?
544
+ raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
363
545
 
364
546
  title, note = wwid.format_input(input)
365
547
  note.push(options[:n]) if options[:n]
@@ -376,7 +558,7 @@ command :later do |c|
376
558
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
377
559
  wwid.write(wwid.doing_file)
378
560
  else
379
- exit_now! 'You must provide content when creating a new entry'
561
+ raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
380
562
  end
381
563
  end
382
564
  end
@@ -412,31 +594,39 @@ command %i[done did] do |c|
412
594
  c.arg_name 'NAME'
413
595
  c.flag %i[s section]
414
596
 
415
- c.desc "Edit entry with #{ENV['EDITOR']}"
597
+ c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
416
598
  c.switch %i[e editor], negatable: false, default_value: false
417
599
 
600
+ c.desc 'Include a note'
601
+ c.arg_name 'TEXT'
602
+ c.flag %i[n note]
603
+
604
+ c.desc 'Finish last entry not already marked @done'
605
+ c.switch %i[u unfinished], negatable: false, default_value: false
606
+
418
607
  # c.desc "Edit entry with specified app"
419
608
  # c.arg_name 'editor_app'
420
609
  # # c.flag [:a, :app]
421
610
 
422
611
  c.action do |_global_options, options, args|
423
612
  took = 0
613
+ donedate = nil
424
614
 
425
615
  if options[:took]
426
616
  took = wwid.chronify_qty(options[:took])
427
- exit_now! 'Unable to parse date string for --took' if took.nil?
617
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
428
618
  end
429
619
 
430
620
  if options[:back]
431
- date = wwid.chronify(options[:back])
432
- exit_now! 'Unable to parse date string for --back' if date.nil?
621
+ date = wwid.chronify(options[:back], guess: :begin)
622
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
433
623
  else
434
624
  date = options[:took] ? Time.now - took : Time.now
435
625
  end
436
626
 
437
627
  if options[:at]
438
- finish_date = wwid.chronify(options[:at])
439
- exit_now! 'Unable to parse date string for --at' if finish_date.nil?
628
+ finish_date = wwid.chronify(options[:at], guess: :begin)
629
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
440
630
 
441
631
  date = options[:took] ? finish_date - took : finish_date
442
632
  elsif options[:took]
@@ -447,58 +637,116 @@ command %i[done did] do |c|
447
637
  finish_date = Time.now
448
638
  end
449
639
 
450
- if finish_date
451
- donedate = options[:date] ? "(#{finish_date.strftime('%F %R')})" : ''
640
+ if options[:date]
641
+ donedate = finish_date.strftime('%F %R')
452
642
  end
453
643
 
454
644
  if options[:section]
455
645
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
456
646
  else
457
- section = wwid.config['current_section']
647
+ section = settings['current_section']
458
648
  end
459
649
 
650
+ note = Doing::Note.new
651
+ note.add(options[:note]) if options[:note]
652
+
460
653
  if options[:editor]
461
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
654
+ raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
655
+ is_new = false
656
+
657
+ if args.empty?
658
+ last_entry = wwid.filter_items([], opt: {unfinished: options[:unfinished], section: section, count: 1, age: 'new'}).max_by { |item| item.date }
659
+
660
+ unless last_entry
661
+ Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
662
+ raise Doing::Errors::NoResults, 'No results'
663
+ end
664
+
665
+ old_entry = last_entry.dup
666
+ last_entry.note.add(note)
667
+ input = [last_entry.title, last_entry.note.to_s].join("\n")
668
+ else
669
+ is_new = true
670
+ input = [args.join(' '), note.to_s].join("\n")
671
+ end
462
672
 
463
- input = ''
464
- input += args.join(' ') unless args.empty?
465
673
  input = wwid.fork_editor(input).strip
466
- exit_now! 'No content' unless input && !input.empty?
674
+ raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
467
675
 
468
676
  title, note = wwid.format_input(input)
469
- title += " @done#{donedate}"
470
- section = 'Archive' if options[:a]
471
- wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date })
677
+ new_entry = Doing::Item.new(date, title, section, note)
678
+ if new_entry.should_finish?
679
+ if new_entry.should_time?
680
+ new_entry.tag('done', value: donedate)
681
+ else
682
+ new_entry.tag('done')
683
+ end
684
+ end
685
+
686
+ if (is_new)
687
+ wwid.content[section][:items].push(new_entry)
688
+ else
689
+ wwid.update_item(old_entry, new_entry)
690
+ end
691
+
692
+ if options[:a]
693
+ wwid.move_item(new_entry, 'Archive', label: true)
694
+ end
695
+
472
696
  wwid.write(wwid.doing_file)
473
697
  elsif args.empty? && $stdin.stat.size.zero?
474
698
  if options[:r]
475
699
  wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
476
700
  else
477
- options = { tags: ['done'],
701
+ note = options[:note] ? Doing::Note.new(options[:note]) : nil
702
+ opt = {
478
703
  archive: options[:a],
479
704
  back: finish_date,
480
705
  count: 1,
481
706
  date: options[:date],
707
+ note: note,
482
708
  section: section,
483
- took: took == 0 ? nil : took
709
+ tags: ['done'],
710
+ took: took == 0 ? nil : took,
711
+ unfinished: options[:unfinished]
484
712
  }
485
- wwid.tag_last(options)
713
+ wwid.tag_last(opt)
486
714
  end
487
715
  elsif !args.empty?
488
- title, note = wwid.format_input(args.join(' '))
716
+ note = Doing::Note.new(options[:note])
717
+ title, new_note = wwid.format_input([args.join(' '), note.to_s].join("\n"))
489
718
  title.chomp!
490
- title += " @done#{donedate}"
491
719
  section = 'Archive' if options[:a]
492
- wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date })
720
+ new_entry = Doing::Item.new(date, title, section, new_note)
721
+ if new_entry.should_finish?
722
+ if new_entry.should_time?
723
+ new_entry.tag('done', value: donedate)
724
+ else
725
+ new_entry.tag('done')
726
+ end
727
+ end
728
+ wwid.content[section][:items].push(new_entry)
493
729
  wwid.write(wwid.doing_file)
730
+ Doing.logger.info('Entry Added:', new_entry.title)
494
731
  elsif $stdin.stat.size.positive?
495
732
  title, note = wwid.format_input($stdin.read)
496
- title += " @done#{donedate}"
733
+ note.add(options[:note]) if options[:note]
497
734
  section = options[:a] ? 'Archive' : section
498
- wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date })
735
+ new_entry = Doing::Item.new(date, title, section, note)
736
+
737
+ if new_entry.should_finish?
738
+ if new_entry.should_time?
739
+ new_entry.tag('done', value: donedate)
740
+ else
741
+ new_entry.tag('done')
742
+ end
743
+ end
744
+
745
+ wwid.content[section][:items].push(new_entry)
499
746
  wwid.write(wwid.doing_file)
747
+ Doing.logger.info('Entry Added:', new_entry.title)
500
748
  else
501
- exit_now! 'You must provide content when creating a new entry'
749
+ raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
502
750
  end
503
751
  end
504
752
  end
@@ -520,39 +768,37 @@ command :cancel do |c|
520
768
 
521
769
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
522
770
  c.arg_name 'BOOLEAN'
523
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
771
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
524
772
 
525
773
  c.desc 'Cancel last entry (or entries) not already marked @done'
526
774
  c.switch %i[u unfinished], negatable: false, default_value: false
527
775
 
776
+ c.desc 'Select item(s) to cancel from a menu of matching entries'
777
+ c.switch %i[i interactive]
778
+
528
779
  c.action do |_global_options, options, args|
529
780
  if options[:section]
530
781
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
531
782
  else
532
- section = wwid.config['current_section']
783
+ section = settings['current_section']
533
784
  end
534
785
 
535
786
  if options[:tag].nil?
536
787
  tags = []
537
788
  else
538
- tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
539
- options[:bool] = case options[:bool]
540
- when /(and|all)/i
541
- 'AND'
542
- when /(any|or)/i
543
- 'OR'
544
- when /(not|none)/i
545
- 'NOT'
546
- else
547
- 'AND'
548
- end
789
+ tags = options[:tag].to_tags
549
790
  end
550
791
 
551
- exit_now! 'Only one argument allowed' if args.length > 1
792
+ raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
552
793
 
553
- exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
794
+ raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
795
+
796
+ if options[:interactive]
797
+ count = 0
798
+ else
799
+ count = args[0] ? args[0].to_i : 1
800
+ end
554
801
 
555
- count = args[0] ? args[0].to_i : 1
556
802
  opts = {
557
803
  archive: options[:a],
558
804
  count: count,
@@ -560,10 +806,12 @@ command :cancel do |c|
560
806
  section: section,
561
807
  sequential: false,
562
808
  tag: tags,
563
- tag_bool: options[:bool],
809
+ tag_bool: options[:bool].normalize_bool,
564
810
  tags: ['done'],
565
- unfinished: options[:unfinished]
811
+ unfinished: options[:unfinished],
812
+ interactive: options[:interactive]
566
813
  }
814
+
567
815
  wwid.tag_last(opts)
568
816
  end
569
817
  end
@@ -592,13 +840,16 @@ command :finish do |c|
592
840
  c.arg_name 'TAG'
593
841
  c.flag [:tag]
594
842
 
595
- c.desc 'Finish the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/")'
843
+ c.desc 'Finish the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
596
844
  c.arg_name 'QUERY'
597
845
  c.flag [:search]
598
846
 
599
847
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
600
848
  c.arg_name 'BOOLEAN'
601
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
849
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
850
+
851
+ c.desc 'Remove done tag'
852
+ c.switch %i[r remove], negatable: false, default_value: false
602
853
 
603
854
  c.desc 'Finish last entry (or entries) not already marked @done'
604
855
  c.switch %i[u unfinished], negatable: false, default_value: false
@@ -615,32 +866,29 @@ command :finish do |c|
615
866
  c.arg_name 'NAME'
616
867
  c.flag %i[s section]
617
868
 
618
- c.action do |_global_options, options, args|
619
- if options[:section]
620
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
621
- else
622
- section = wwid.config['current_section']
623
- end
869
+ c.desc 'Select item(s) to finish from a menu of matching entries'
870
+ c.switch %i[i interactive]
624
871
 
872
+ c.action do |_global_options, options, args|
625
873
  unless options[:auto]
626
874
  if options[:took]
627
875
  took = wwid.chronify_qty(options[:took])
628
- exit_now! 'Unable to parse date string for --took' if took.nil?
876
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
629
877
  end
630
878
 
631
- exit_now! '--back and --took cannot be used together' if options[:back] && options[:took]
879
+ raise Doing::Errors::InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
632
880
 
633
- exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
881
+ raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
634
882
 
635
883
  if options[:at]
636
- finish_date = wwid.chronify(options[:at])
637
- exit_now! 'Unable to parse date string for --at' if finish_date.nil?
884
+ finish_date = wwid.chronify(options[:at], guess: :begin)
885
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
638
886
 
639
887
  date = options[:took] ? finish_date - took : finish_date
640
888
  elsif options[:back]
641
889
  date = wwid.chronify(options[:back])
642
890
 
643
- exit_now! 'Unable to parse date string' if date.nil?
891
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
644
892
  elsif options[:took]
645
893
  date = wwid.chronify_qty(options[:took])
646
894
  else
@@ -651,44 +899,42 @@ command :finish do |c|
651
899
  if options[:tag].nil?
652
900
  tags = []
653
901
  else
654
- tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
655
- options[:bool] = case options[:bool]
656
- when /(and|all)/i
657
- 'AND'
658
- when /(any|or)/i
659
- 'OR'
660
- when /(not|none)/i
661
- 'NOT'
662
- else
663
- 'AND'
664
- end
902
+ tags = options[:tag].to_tags
665
903
  end
666
904
 
667
- exit_now! 'Only one argument allowed' if args.length > 1
905
+ raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
668
906
 
669
- exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
907
+ raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
908
+
909
+ if options[:interactive]
910
+ count = 0
911
+ else
912
+ count = args[0] ? args[0].to_i : 1
913
+ end
670
914
 
671
- count = args[0] ? args[0].to_i : 1
672
915
  opts = {
673
- archive: options[:a],
916
+ archive: options[:archive],
674
917
  back: date,
675
918
  count: count,
676
919
  date: options[:date],
677
920
  search: options[:search],
678
- section: section,
921
+ section: options[:section],
679
922
  sequential: options[:auto],
680
923
  tag: tags,
681
- tag_bool: options[:bool],
924
+ tag_bool: options[:bool].normalize_bool,
682
925
  tags: ['done'],
683
- unfinished: options[:unfinished]
926
+ unfinished: options[:unfinished],
927
+ remove: options[:remove],
928
+ interactive: options[:interactive]
684
929
  }
930
+
685
931
  wwid.tag_last(opts)
686
932
  end
687
933
  end
688
934
 
689
935
  desc 'Repeat last entry as new entry'
690
- command [:again, :resume] do |c|
691
- c.desc 'Section'
936
+ command %i[again resume] do |c|
937
+ c.desc 'Get last entry from a specific section'
692
938
  c.arg_name 'NAME'
693
939
  c.flag %i[s section], default_value: 'All'
694
940
 
@@ -701,52 +947,68 @@ command [:again, :resume] do |c|
701
947
  c.flag [:tag]
702
948
 
703
949
  c.desc 'Repeat last entry matching search. Surround with
704
- slashes for regex (e.g. "/query/").'
950
+ slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
705
951
  c.arg_name 'QUERY'
706
952
  c.flag [:search]
707
953
 
708
954
  c.desc 'Boolean used to combine multiple tags'
709
955
  c.arg_name 'BOOLEAN'
710
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
956
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
957
+
958
+ c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
959
+ c.switch %i[e editor], negatable: false, default_value: false
711
960
 
712
961
  c.desc 'Note'
713
962
  c.arg_name 'TEXT'
714
963
  c.flag %i[n note]
715
964
 
965
+ c.desc 'Select item to resume from a menu of matching entries'
966
+ c.switch %i[i interactive]
967
+
716
968
  c.action do |_global_options, options, _args|
717
- tags = options[:tag].nil? ? [] : options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }
718
- options[:bool] = case options[:bool]
719
- when /(and|all)/i
720
- 'AND'
721
- when /(any|or)/i
722
- 'OR'
723
- when /(not|none)/i
724
- 'NOT'
725
- else
726
- 'AND'
727
- end
728
- opts = {
729
- in: options[:in],
730
- note: options[:n],
731
- search: options[:search],
732
- section: options[:s],
733
- tag: tags,
734
- tag_bool: options[:bool]
735
- }
736
- wwid.restart_last(opts)
969
+ tags = options[:tag].nil? ? [] : options[:tag].to_tags
970
+ opts = options
971
+ opts[:tag] = tags
972
+ opts[:tag_bool] = options[:bool].normalize_bool
973
+ opts[:interactive] = options[:interactive]
974
+
975
+ wwid.repeat_last(opts)
737
976
  end
738
977
  end
739
978
 
740
979
  desc 'Add tag(s) to last entry'
980
+ long_desc 'Add (or remove) tags from the last entry, or from multiple entries
981
+ (with `--count`), entries matching a search (with `--search`), or entries
982
+ containing another tag (with `--tag`).
983
+
984
+ When removing tags with `-r`, wildcards are allowed (`*` to match
985
+ multiple characters, `?` to match a single character). With `--regex`,
986
+ regular expressions will be interpreted instead of wildcards.
987
+
988
+ For all tag removals the match is case insensitive by default, but if
989
+ the tag search string contains any uppercase letters, the match will
990
+ become case sensitive automatically.
991
+
992
+ Tag name arguments do not need to be prefixed with @.'
741
993
  arg_name 'TAG', :multiple
742
994
  command :tag do |c|
995
+ c.example 'doing tag mytag', desc: 'Add @mytag to the last entry created'
996
+ c.example 'doing tag --remove mytag', desc: 'Remove @mytag from the last entry created'
997
+ c.example 'doing tag --rename "other*" --count 10 newtag', desc: 'Rename tags beginning with "other" (wildcard) to @newtag on the last 10 entries'
998
+ c.example 'doing tag --search "developing" coding', desc: 'Add @coding to the last entry containing string "developing" (fuzzy matching)'
999
+ c.example 'doing tag --interactive --tag project1 coding', desc: 'Create an interactive menu from entries tagged @project1, selection(s) will be tagged with @coding'
1000
+
743
1001
  c.desc 'Section'
744
1002
  c.arg_name 'SECTION_NAME'
745
1003
  c.flag %i[s section], default_value: 'All'
746
1004
 
747
1005
  c.desc 'How many recent entries to tag (0 for all)'
748
1006
  c.arg_name 'COUNT'
749
- c.flag %i[c count], default_value: 1
1007
+ c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
1008
+
1009
+ c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
1010
+ c.arg_name 'ORIG_TAG'
1011
+ c.flag %i[rename]
750
1012
 
751
1013
  c.desc 'Don\'t ask permission to tag all entries when count is 0'
752
1014
  c.switch %i[force], negatable: false, default_value: false
@@ -757,6 +1019,9 @@ command :tag do |c|
757
1019
  c.desc 'Remove given tag(s)'
758
1020
  c.switch %i[r remove], negatable: false, default_value: false
759
1021
 
1022
+ c.desc 'Interpret tag string as regular expression (with --remove)'
1023
+ c.switch %i[regex], negatable: false, default_value: false
1024
+
760
1025
  c.desc 'Tag last entry (or entries) not marked @done'
761
1026
  c.switch %i[u unfinished], negatable: false, default_value: false
762
1027
 
@@ -768,18 +1033,21 @@ command :tag do |c|
768
1033
  c.arg_name 'TAG'
769
1034
  c.flag [:tag]
770
1035
 
771
- c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/")'
1036
+ c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
772
1037
  c.arg_name 'QUERY'
773
1038
  c.flag [:search]
774
1039
 
775
1040
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
776
1041
  c.arg_name 'BOOLEAN'
777
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
1042
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1043
+
1044
+ c.desc 'Select item(s) to tag from a menu of matching entries'
1045
+ c.switch %i[i interactive]
778
1046
 
779
1047
  c.action do |_global_options, options, args|
780
- exit_now! 'You must specify at least one tag' if args.empty? && !options[:a]
1048
+ raise Doing::Errors::MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:a]
781
1049
 
782
- exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
1050
+ raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
783
1051
 
784
1052
  section = 'All'
785
1053
 
@@ -791,17 +1059,7 @@ command :tag do |c|
791
1059
  if options[:tag].nil?
792
1060
  search_tags = []
793
1061
  else
794
- search_tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
795
- options[:bool] = case options[:bool]
796
- when /(and|all)/i
797
- 'AND'
798
- when /(any|or)/i
799
- 'OR'
800
- when /(not|none)/i
801
- 'NOT'
802
- else
803
- 'AND'
804
- end
1062
+ search_tags = options[:tag].to_tags
805
1063
  end
806
1064
 
807
1065
  if options[:autotag]
@@ -816,7 +1074,13 @@ command :tag do |c|
816
1074
  tags.map! { |tag| tag.sub(/^@/, '').strip }
817
1075
  end
818
1076
 
819
- count = options[:count].to_i
1077
+ if options[:interactive]
1078
+ count = 0
1079
+ options[:force] = true
1080
+ else
1081
+ count = options[:count].to_i
1082
+ end
1083
+
820
1084
 
821
1085
  if count.zero? && !options[:force]
822
1086
  if options[:search]
@@ -843,42 +1107,125 @@ command :tag do |c|
843
1107
  exit_now! 'Cancelled' unless res
844
1108
  end
845
1109
 
846
- opts = {
847
- autotag: options[:a],
848
- count: count,
849
- date: options[:date],
850
- remove: options[:r],
851
- search: options[:search],
852
- section: section,
853
- tag: search_tags,
854
- tag_bool: options[:bool],
855
- tags: tags,
856
- unfinished: options[:unfinished]
857
- }
858
- wwid.tag_last(opts)
1110
+ options[:count] = count
1111
+ options[:section] = section
1112
+ options[:tag] = search_tags
1113
+ options[:tags] = tags
1114
+ options[:tag_bool] = options[:bool].normalize_bool
1115
+
1116
+ wwid.tag_last(options)
859
1117
  end
860
1118
  end
861
1119
 
862
- desc 'Mark last entry as highlighted'
1120
+ # desc 'Autotag last X entries'
1121
+ # arg_name 'COUNT'
1122
+ # command :autotag do |c|
1123
+ # c.action do |global_options, options, args|
1124
+ # options = {
1125
+ # autotag: true,
1126
+ # count: args[0].to_i
1127
+ # }
1128
+ # cmd = commands[:tag]
1129
+ # cmd.action.(global_options, options, [])
1130
+ # end
1131
+ # end
1132
+
1133
+ desc 'Mark last entry as flagged'
863
1134
  command [:mark, :flag] do |c|
1135
+ c.example 'doing flag', desc: 'Add @flagged to the last entry created'
1136
+ c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
1137
+ c.example 'doing flag --interactive --search "/(develop|cod)ing/"', desc: 'Find entries matching regular expression and create a menu allowing multiple selections, selected items will be @flagged'
1138
+
864
1139
  c.desc 'Section'
865
- c.arg_name 'NAME'
866
- c.flag %i[s section]
1140
+ c.arg_name 'SECTION_NAME'
1141
+ c.flag %i[s section], default_value: 'All'
1142
+
1143
+ c.desc 'How many recent entries to tag (0 for all)'
1144
+ c.arg_name 'COUNT'
1145
+ c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
867
1146
 
868
- c.desc 'Remove mark'
1147
+ c.desc 'Don\'t ask permission to flag all entries when count is 0'
1148
+ c.switch %i[force], negatable: false, default_value: false
1149
+
1150
+ c.desc 'Include current date/time with tag'
1151
+ c.switch %i[d date], negatable: false, default_value: false
1152
+
1153
+ c.desc 'Remove flag'
869
1154
  c.switch %i[r remove], negatable: false, default_value: false
870
1155
 
871
- c.desc 'Mark last entry not marked @done'
1156
+ c.desc 'Flag last entry (or entries) not marked @done'
872
1157
  c.switch %i[u unfinished], negatable: false, default_value: false
873
1158
 
1159
+ c.desc 'Flag the last entry containing TAG.
1160
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1161
+ c.arg_name 'TAG'
1162
+ c.flag [:tag]
1163
+
1164
+ c.desc 'Flag the last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1165
+ c.arg_name 'QUERY'
1166
+ c.flag [:search]
1167
+
1168
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1169
+ c.arg_name 'BOOLEAN'
1170
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1171
+
1172
+ c.desc 'Select item(s) to flag from a menu of matching entries'
1173
+ c.switch %i[i interactive]
1174
+
874
1175
  c.action do |_global_options, options, _args|
875
- mark = wwid.config['marker_tag'] || 'flagged'
876
- wwid.tag_last({
877
- remove: options[:r],
878
- section: options[:s],
879
- tags: [mark],
880
- unfinished: options[:unfinished]
881
- })
1176
+ mark = settings['marker_tag'] || 'flagged'
1177
+
1178
+ raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1179
+
1180
+ section = 'All'
1181
+
1182
+ if options[:section]
1183
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1184
+ end
1185
+
1186
+ if options[:tag].nil?
1187
+ search_tags = []
1188
+ else
1189
+ search_tags = options[:tag].to_tags
1190
+ end
1191
+
1192
+ if options[:interactive]
1193
+ count = 0
1194
+ options[:force] = true
1195
+ else
1196
+ count = options[:count].to_i
1197
+ end
1198
+
1199
+ if count.zero? && !options[:force]
1200
+ if options[:search]
1201
+ section_q = ' matching your search terms'
1202
+ elsif options[:tag]
1203
+ section_q = ' matching your tag search'
1204
+ elsif section == 'All'
1205
+ section_q = ''
1206
+ else
1207
+ section_q = " in section #{section}"
1208
+ end
1209
+
1210
+
1211
+ question = if options[:remove]
1212
+ "Are you sure you want to unflag all entries#{section_q}"
1213
+ else
1214
+ "Are you sure you want to flag all records#{section_q}"
1215
+ end
1216
+
1217
+ res = wwid.yn(question, default_response: false)
1218
+
1219
+ exit_now! 'Cancelled' unless res
1220
+ end
1221
+
1222
+ options[:count] = count
1223
+ options[:section] = section
1224
+ options[:tag] = search_tags
1225
+ options[:tags] = [mark]
1226
+ options[:tag_bool] = options[:bool].normalize_bool
1227
+
1228
+ wwid.tag_last(options)
882
1229
  end
883
1230
  end
884
1231
 
@@ -889,13 +1236,19 @@ long_desc %(
889
1236
  )
890
1237
  arg_name '[SECTION|@TAGS]'
891
1238
  command :show do |c|
1239
+ c.example 'doing show Currently', desc: 'Show entries in the Currently section'
1240
+ c.example 'doing show @project1', desc: 'Show entries tagged @project1'
1241
+ c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
1242
+ c.example 'doing show Ideas --from "mon to fri" --tag doing', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.'
1243
+ c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1244
+
892
1245
  c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
893
1246
  c.arg_name 'TAG'
894
1247
  c.flag [:tag]
895
1248
 
896
1249
  c.desc 'Tag boolean (AND,OR,NOT)'
897
1250
  c.arg_name 'BOOLEAN'
898
- c.flag %i[b bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'OR'
1251
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
899
1252
 
900
1253
  c.desc 'Max count to show'
901
1254
  c.arg_name 'MAX'
@@ -913,13 +1266,13 @@ command :show do |c|
913
1266
  c.arg_name 'DATE_STRING'
914
1267
  c.flag [:after]
915
1268
 
916
- c.desc 'Search filter, surround with slashes for regex (/query/)'
1269
+ c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
917
1270
  c.arg_name 'QUERY'
918
1271
  c.flag [:search]
919
1272
 
920
1273
  c.desc 'Sort order (asc/desc)'
921
1274
  c.arg_name 'ORDER'
922
- c.flag %i[s sort], must_match: /^[ad].*/i, default_value: 'ASC'
1275
+ c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
923
1276
 
924
1277
  c.desc %(
925
1278
  Date range to show, or a single day to filter date on.
@@ -937,21 +1290,26 @@ command :show do |c|
937
1290
 
938
1291
  c.desc 'Sort tags by (name|time)'
939
1292
  default = 'time'
940
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1293
+ default = settings['tag_sort'] || 'name'
941
1294
  c.arg_name 'KEY'
942
1295
  c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
943
1296
 
944
1297
  c.desc 'Tag sort direction (asc|desc)'
945
1298
  c.arg_name 'DIRECTION'
946
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1299
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
947
1300
 
948
1301
  c.desc 'Only show items with recorded time intervals'
949
1302
  c.switch [:only_timed], default_value: false, negatable: false
950
1303
 
951
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1304
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1305
+ c.switch %i[i interactive]
1306
+
1307
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
952
1308
  c.arg_name 'FORMAT'
953
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1309
+ c.flag %i[o output]
954
1310
  c.action do |_global_options, options, args|
1311
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1312
+
955
1313
  tag_filter = false
956
1314
  tags = []
957
1315
  if args.length.positive?
@@ -966,7 +1324,7 @@ command :show do |c|
966
1324
  section = 'All'
967
1325
  else
968
1326
  section = wwid.guess_section(args[0])
969
- exit_now! "No such section: #{args[0]}" unless section
1327
+ raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section
970
1328
 
971
1329
  args.shift
972
1330
  end
@@ -978,72 +1336,51 @@ command :show do |c|
978
1336
  end
979
1337
  end
980
1338
  else
981
- section = wwid.current_section
1339
+ section = settings['current_section']
982
1340
  end
983
1341
 
984
- tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
985
- options[:bool] = case options[:bool]
986
- when /(and|all)/i
987
- 'AND'
988
- when /(any|or)/i
989
- 'OR'
990
- when /(not|none)/i
991
- 'NOT'
992
- else
993
- 'AND'
994
- end
1342
+ tags.concat(options[:tag].to_tags) if options[:tag]
995
1343
 
996
1344
  unless tags.empty?
997
1345
  tag_filter = {
998
1346
  'tags' => tags,
999
- 'bool' => options[:bool]
1347
+ 'bool' => options[:bool].normalize_bool
1000
1348
  }
1001
1349
  end
1002
1350
 
1003
1351
  if options[:from]
1352
+
1004
1353
  date_string = options[:from]
1005
1354
  if date_string =~ / (to|through|thru|(un)?til|-+) /
1006
1355
  dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
1007
- start = wwid.chronify(dates[0])
1008
- finish = wwid.chronify(dates[2])
1356
+ start = wwid.chronify(dates[0], guess: :begin)
1357
+ finish = wwid.chronify(dates[2], guess: :end)
1009
1358
  else
1010
- start = wwid.chronify(date_string)
1359
+ start = wwid.chronify(date_string, guess: :begin)
1011
1360
  finish = false
1012
1361
  end
1013
- exit_now! 'Unrecognized date string' unless start
1362
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1014
1363
  dates = [start, finish]
1015
1364
  end
1016
1365
 
1017
1366
  options[:times] = true if options[:totals]
1018
1367
 
1019
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1368
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1020
1369
 
1021
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
1022
- tag_order = if options[:tag_order]
1023
- options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1024
- else
1025
- 'asc'
1026
- end
1027
- opts = {
1028
- after: options[:after],
1029
- age: options[:age],
1030
- before: options[:before],
1031
- count: options[:c].to_i,
1032
- date_filter: dates,
1033
- highlight: true,
1034
- only_timed: options[:only_timed],
1035
- order: options[:s],
1036
- output: options[:output],
1037
- search: options[:search],
1038
- section: section,
1039
- sort_tags: options[:sort_tags],
1040
- tag_filter: tag_filter,
1041
- tag_order: tag_order,
1042
- tags_color: tags_color,
1043
- times: options[:t],
1044
- totals: options[:totals]
1045
- }
1046
- puts wwid.list_section(opts)
1370
+ opt = options.dup
1371
+
1372
+ opt[:sort_tags] = options[:tag_sort] =~ /^n/i
1373
+ opt[:count] = options[:count].to_i
1374
+ opt[:date_filter] = dates
1375
+ opt[:highlight] = true
1376
+ opt[:order] = options[:sort].normalize_order
1377
+ opt[:section] = section
1378
+ opt[:tag] = nil
1379
+ opt[:tag_filter] = tag_filter
1380
+ opt[:tag_order] = options[:tag_order].normalize_order
1381
+ opt[:tags_color] = tags_color
1382
+
1383
+ Doing::Pager.page wwid.list_section(opt)
1047
1384
  end
1048
1385
  end
1049
1386
 
@@ -1055,7 +1392,12 @@ long_desc <<~'EODESC'
1055
1392
  EODESC
1056
1393
 
1057
1394
  arg_name 'SEARCH_PATTERN'
1058
- command [:grep, :search] do |c|
1395
+ command %i[grep search] do |c|
1396
+ c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
1397
+ c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
1398
+ c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
1399
+ c.example 'doing search --before 12/21 "doing wiki"', desc: 'Find entries containing "doing wiki" with entry dates before 12/21 of the current year'
1400
+
1059
1401
  c.desc 'Section'
1060
1402
  c.arg_name 'NAME'
1061
1403
  c.flag %i[s section], default_value: 'All'
@@ -1068,9 +1410,9 @@ command [:grep, :search] do |c|
1068
1410
  c.arg_name 'DATE_STRING'
1069
1411
  c.flag [:after]
1070
1412
 
1071
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1413
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1072
1414
  c.arg_name 'FORMAT'
1073
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1415
+ c.flag %i[o output]
1074
1416
 
1075
1417
  c.desc 'Show time intervals on @done tasks'
1076
1418
  c.switch %i[t times], default_value: true, negatable: true
@@ -1080,36 +1422,31 @@ command [:grep, :search] do |c|
1080
1422
 
1081
1423
  c.desc 'Sort tags by (name|time)'
1082
1424
  default = 'time'
1083
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1425
+ default = settings['tag_sort'] || 'name'
1084
1426
  c.arg_name 'KEY'
1085
1427
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1086
1428
 
1087
1429
  c.desc 'Only show items with recorded time intervals'
1088
1430
  c.switch [:only_timed], default_value: false, negatable: false
1089
1431
 
1432
+ c.desc 'Display an interactive menu of results to perform further operations'
1433
+ c.switch %i[i interactive], default_value: false, negatable: false
1434
+
1090
1435
  c.action do |_global_options, options, args|
1091
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1436
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1092
1437
 
1093
- section = wwid.guess_section(options[:s]) if options[:s]
1438
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1439
+
1440
+ section = wwid.guess_section(options[:section]) if options[:section]
1094
1441
 
1095
1442
  options[:times] = true if options[:totals]
1096
1443
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1444
+ options[:highlight] = true
1445
+ options[:search] = args.join(' ')
1446
+ options[:section] = section
1447
+ options[:tags_color] = tags_color
1097
1448
 
1098
- opts = {
1099
- after: options[:after],
1100
- before: options[:before],
1101
- highlight: true,
1102
- only_timed: options[:only_timed],
1103
- output: options[:output],
1104
- search: args.join(' '),
1105
- section: section,
1106
- sort_tags: options[:sort_tags],
1107
- tags_color: tags_color,
1108
- times: options[:times],
1109
- totals: options[:totals]
1110
- }
1111
-
1112
- puts wwid.list_section(opts)
1449
+ Doing::Pager.page wwid.list_section(options)
1113
1450
  end
1114
1451
  end
1115
1452
 
@@ -1117,6 +1454,11 @@ desc 'List recent entries'
1117
1454
  default_value 10
1118
1455
  arg_name 'COUNT'
1119
1456
  command :recent do |c|
1457
+ c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections'
1458
+ c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections'
1459
+ c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section'
1460
+ c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on'
1461
+
1120
1462
  c.desc 'Section'
1121
1463
  c.arg_name 'NAME'
1122
1464
  c.flag %i[s section], default_value: 'All'
@@ -1129,32 +1471,42 @@ command :recent do |c|
1129
1471
 
1130
1472
  c.desc 'Sort tags by (name|time)'
1131
1473
  default = 'time'
1132
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1474
+ default = settings['tag_sort'] || 'name'
1133
1475
  c.arg_name 'KEY'
1134
1476
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1135
1477
 
1478
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1479
+ c.switch %i[i interactive]
1480
+
1136
1481
  c.action do |global_options, options, args|
1137
1482
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
1138
1483
 
1139
1484
  unless global_options[:version]
1140
- if wwid.config['templates']['recent'].key?('count')
1141
- config_count = wwid.config['templates']['recent']['count'].to_i
1485
+ if settings['templates']['recent'].key?('count')
1486
+ config_count = settings['templates']['recent']['count'].to_i
1142
1487
  else
1143
1488
  config_count = 10
1144
1489
  end
1145
- count = args.empty? ? config_count : args[0].to_i
1490
+
1491
+ if options[:interactive]
1492
+ count = 0
1493
+ else
1494
+ count = args.empty? ? config_count : args[0].to_i
1495
+ end
1496
+
1146
1497
  options[:t] = true if options[:totals]
1147
1498
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1148
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1499
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1149
1500
 
1150
1501
  opts = {
1151
1502
  sort_tags: options[:sort_tags],
1152
1503
  tags_color: tags_color,
1153
1504
  times: options[:t],
1154
- totals: options[:totals]
1505
+ totals: options[:totals],
1506
+ interactive: options[:interactive]
1155
1507
  }
1156
1508
 
1157
- puts wwid.recent(count, section.cap_first, opts)
1509
+ Doing::Pager::page wwid.recent(count, section.cap_first, opts)
1158
1510
 
1159
1511
  end
1160
1512
  end
@@ -1162,6 +1514,11 @@ end
1162
1514
 
1163
1515
  desc 'List entries from today'
1164
1516
  command :today do |c|
1517
+ c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
1518
+ c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
1519
+ c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
1520
+ c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
1521
+
1165
1522
  c.desc 'Specify a section'
1166
1523
  c.arg_name 'NAME'
1167
1524
  c.flag %i[s section], default_value: 'All'
@@ -1174,13 +1531,13 @@ command :today do |c|
1174
1531
 
1175
1532
  c.desc 'Sort tags by (name|time)'
1176
1533
  default = 'time'
1177
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1534
+ default = settings['tag_sort'] || 'name'
1178
1535
  c.arg_name 'KEY'
1179
1536
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1180
1537
 
1181
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1538
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1182
1539
  c.arg_name 'FORMAT'
1183
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1540
+ c.flag %i[o output]
1184
1541
 
1185
1542
  c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
1186
1543
  c.arg_name 'TIME_STRING'
@@ -1191,6 +1548,8 @@ command :today do |c|
1191
1548
  c.flag [:after]
1192
1549
 
1193
1550
  c.action do |_global_options, options, _args|
1551
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1552
+
1194
1553
  options[:t] = true if options[:totals]
1195
1554
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1196
1555
  opt = {
@@ -1198,9 +1557,10 @@ command :today do |c|
1198
1557
  before: options[:before],
1199
1558
  section: options[:section],
1200
1559
  sort_tags: options[:sort_tags],
1201
- totals: options[:totals]
1560
+ totals: options[:totals],
1561
+ order: settings.dig('templates', 'today', 'order')
1202
1562
  }
1203
- puts wwid.today(options[:times], options[:output], opt).chomp
1563
+ Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
1204
1564
  end
1205
1565
  end
1206
1566
 
@@ -1210,6 +1570,10 @@ and "2d" would be interpreted as "two days ago." If you use "to" or "through" be
1210
1570
  it will create a range.)
1211
1571
  arg_name 'DATE_STRING'
1212
1572
  command :on do |c|
1573
+ c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
1574
+ c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
1575
+ c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
1576
+
1213
1577
  c.desc 'Section'
1214
1578
  c.arg_name 'NAME'
1215
1579
  c.flag %i[s section], default_value: 'All'
@@ -1222,38 +1586,40 @@ command :on do |c|
1222
1586
 
1223
1587
  c.desc 'Sort tags by (name|time)'
1224
1588
  default = 'time'
1225
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1589
+ default = settings['tag_sort'] || 'name'
1226
1590
  c.arg_name 'KEY'
1227
1591
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1228
1592
 
1229
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1593
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1230
1594
  c.arg_name 'FORMAT'
1231
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1595
+ c.flag %i[o output]
1232
1596
 
1233
1597
  c.action do |_global_options, options, args|
1234
- exit_now! 'Missing date argument' if args.empty?
1598
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1599
+
1600
+ raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1235
1601
 
1236
1602
  date_string = args.join(' ')
1237
1603
 
1238
1604
  if date_string =~ / (to|through|thru) /
1239
1605
  dates = date_string.split(/ (to|through|thru) /)
1240
- start = wwid.chronify(dates[0])
1241
- finish = wwid.chronify(dates[2])
1606
+ start = wwid.chronify(dates[0], guess: :begin)
1607
+ finish = wwid.chronify(dates[2], guess: :end)
1242
1608
  else
1243
- start = wwid.chronify(date_string)
1609
+ start = wwid.chronify(date_string, guess: :begin)
1244
1610
  finish = false
1245
1611
  end
1246
1612
 
1247
- exit_now! 'Unrecognized date string' unless start
1613
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1248
1614
 
1249
1615
  message = "Date interpreted as #{start}"
1250
1616
  message += " to #{finish}" if finish
1251
- wwid.results.push(message)
1617
+ Doing.logger.debug(message)
1252
1618
 
1253
1619
  options[:t] = true if options[:totals]
1254
1620
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1255
1621
 
1256
- puts wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1622
+ Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1257
1623
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1258
1624
  end
1259
1625
  end
@@ -1263,6 +1629,9 @@ long_desc %(Date argument can be natural language and are always interpreted as
1263
1629
  and "2d" would be interpreted as "two days ago.")
1264
1630
  arg_name 'DATE_STRING'
1265
1631
  command :since do |c|
1632
+ c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
1633
+ c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
1634
+
1266
1635
  c.desc 'Section'
1267
1636
  c.arg_name 'NAME'
1268
1637
  c.flag %i[s section], default_value: 'All'
@@ -1275,47 +1644,52 @@ command :since do |c|
1275
1644
 
1276
1645
  c.desc 'Sort tags by (name|time)'
1277
1646
  default = 'time'
1278
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1647
+ default = settings['tag_sort'] || 'name'
1279
1648
  c.arg_name 'KEY'
1280
1649
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1281
1650
 
1282
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1651
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1283
1652
  c.arg_name 'FORMAT'
1284
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1653
+ c.flag %i[o output]
1285
1654
 
1286
1655
  c.action do |_global_options, options, args|
1287
- exit_now! 'Missing date argument' if args.empty?
1656
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1657
+
1658
+ raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1288
1659
 
1289
1660
  date_string = args.join(' ')
1290
1661
 
1291
- date_string += ' at midnight' unless date_string =~ /(\d:|\d *[ap]m?|midnight|noon)/i
1292
- date_string.sub!(/(day) (\d)/, '\1 at \2') if date_string =~ /day \d/
1662
+ date_string.sub!(/(day) (\d)/, '\1 at \2')
1663
+ date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
1293
1664
 
1294
- start = wwid.chronify(date_string)
1665
+ start = wwid.chronify(date_string, guess: :begin)
1295
1666
  finish = Time.now
1296
1667
 
1297
- exit_now! 'Unrecognized date string' unless start
1668
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1298
1669
 
1299
- message = "Date interpreted as #{start} through the current time"
1300
- wwid.results.push(message)
1670
+ Doing.logger.debug("Date interpreted as #{start} through the current time")
1301
1671
 
1302
1672
  options[:t] = true if options[:totals]
1303
1673
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1304
1674
 
1305
- puts wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1675
+ Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1306
1676
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1307
1677
  end
1308
1678
  end
1309
1679
 
1310
1680
  desc 'List entries from yesterday'
1311
1681
  command :yesterday do |c|
1682
+ c.example 'doing yesterday', desc: 'List all entries from the previous day'
1683
+ c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
1684
+ c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers'
1685
+
1312
1686
  c.desc 'Specify a section'
1313
1687
  c.arg_name 'NAME'
1314
1688
  c.flag %i[s section], default_value: 'All'
1315
1689
 
1316
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1690
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1317
1691
  c.arg_name 'FORMAT'
1318
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1692
+ c.flag %i[o output]
1319
1693
 
1320
1694
  c.desc 'Show time intervals on @done tasks'
1321
1695
  c.switch %i[t times], default_value: true, negatable: true
@@ -1325,7 +1699,7 @@ command :yesterday do |c|
1325
1699
 
1326
1700
  c.desc 'Sort tags by (name|time)'
1327
1701
  default = 'time'
1328
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1702
+ default = settings['tag_sort'] || 'name'
1329
1703
  c.arg_name 'KEY'
1330
1704
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1331
1705
 
@@ -1339,33 +1713,40 @@ command :yesterday do |c|
1339
1713
 
1340
1714
  c.desc 'Tag sort direction (asc|desc)'
1341
1715
  c.arg_name 'DIRECTION'
1342
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1716
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1343
1717
 
1344
1718
  c.action do |_global_options, options, _args|
1345
- tag_order = if options[:tag_order]
1346
- options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1347
- else
1348
- 'asc'
1349
- end
1719
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1720
+
1350
1721
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1722
+
1351
1723
  opt = {
1352
1724
  after: options[:after],
1353
1725
  before: options[:before],
1354
1726
  sort_tags: options[:sort_tags],
1355
- tag_order: options[:tag_order],
1356
- totals: options[:totals]
1727
+ tag_order: options[:tag_order].normalize_order,
1728
+ totals: options[:totals],
1729
+ order: settings.dig('templates', 'today', 'order')
1357
1730
  }
1358
- puts wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
1731
+ Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
1359
1732
  end
1360
1733
  end
1361
1734
 
1362
1735
  desc 'Show the last entry, optionally edit'
1363
1736
  command :last do |c|
1737
+ c.example 'doing last', desc: 'Show the most recent entry in all sections'
1738
+ c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
1739
+ c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
1740
+ c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
1741
+ c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
1742
+ c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
1743
+ c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
1744
+
1364
1745
  c.desc 'Specify a section'
1365
1746
  c.arg_name 'NAME'
1366
1747
  c.flag %i[s section], default_value: 'All'
1367
1748
 
1368
- c.desc "Edit entry with #{ENV['EDITOR']}"
1749
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
1369
1750
  c.switch %i[e editor], negatable: false, default_value: false
1370
1751
 
1371
1752
  c.desc 'Tag filter, combine multiple tags with a comma.'
@@ -1374,19 +1755,19 @@ command :last do |c|
1374
1755
 
1375
1756
  c.desc 'Tag boolean'
1376
1757
  c.arg_name 'BOOLEAN'
1377
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
1758
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1378
1759
 
1379
- c.desc 'Search filter, surround with slashes for regex (/query/)'
1760
+ c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1380
1761
  c.arg_name 'QUERY'
1381
1762
  c.flag [:search]
1382
1763
 
1383
- c.action do |_global_options, options, _args|
1384
- exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search]
1764
+ c.action do |global_options, options, _args|
1765
+ raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1385
1766
 
1386
1767
  if options[:tag].nil?
1387
1768
  tags = []
1388
1769
  else
1389
- tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
1770
+ tags = options[:tag].to_tags
1390
1771
  options[:bool] = case options[:bool]
1391
1772
  when /(any|or)/i
1392
1773
  :or
@@ -1401,7 +1782,7 @@ command :last do |c|
1401
1782
  if options[:editor]
1402
1783
  wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
1403
1784
  else
1404
- puts wwid.last(times: true, section: options[:s],
1785
+ Doing::Pager::page wwid.last(times: true, section: options[:s],
1405
1786
  options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
1406
1787
  end
1407
1788
  end
@@ -1423,17 +1804,19 @@ command :choose do |c|
1423
1804
  c.action do |_global_options, _options, _args|
1424
1805
  section = wwid.choose_section
1425
1806
 
1426
- puts wwid.list_section({ section: section.cap_first, count: 0 }) if section
1807
+ Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
1427
1808
  end
1428
1809
  end
1429
1810
 
1430
1811
  desc 'Add a new section to the "doing" file'
1431
1812
  arg_name 'SECTION_NAME'
1432
1813
  command :add_section do |c|
1814
+ c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
1815
+
1433
1816
  c.action do |_global_options, _options, args|
1434
- exit_now! "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1817
+ raise Doing::Errors::InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1435
1818
 
1436
- wwid.add_section(args[0].cap_first)
1819
+ wwid.add_section(args.join(' ').cap_first)
1437
1820
  wwid.write(wwid.doing_file)
1438
1821
  end
1439
1822
  end
@@ -1441,18 +1824,42 @@ end
1441
1824
  desc 'List available color variables for configuration templates and views'
1442
1825
  command :colors do |c|
1443
1826
  c.action do |_global_options, _options, _args|
1444
- clrs = wwid.colors
1445
1827
  bgs = []
1446
1828
  fgs = []
1447
- clrs.each do |k, v|
1448
- if k =~ /bg/
1449
- bgs.push("#{v} #{clrs['default']} <-- #{k}")
1829
+ colors::attributes.each do |color|
1830
+ if color.to_s =~ /bg/
1831
+ bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}")
1450
1832
  else
1451
- fgs.push("#{v}XXXX#{clrs['default']} <-- #{k}")
1833
+ fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
1452
1834
  end
1453
1835
  end
1454
- puts fgs.join("\n")
1455
- puts bgs.join("\n")
1836
+ out = []
1837
+ out << fgs.join("\n")
1838
+ out << bgs.join("\n")
1839
+ Doing::Pager.page out.join("\n")
1840
+ end
1841
+ end
1842
+
1843
+ desc 'List installed plugins'
1844
+ long_desc %(Lists available plugins, including user-installed plugins.
1845
+
1846
+ Export plugins are available with the `--output` flag on commands that support it.
1847
+
1848
+ Import plugins are available using `doing import --type PLUGIN`.
1849
+ )
1850
+ command :plugins do |c|
1851
+ c.example 'doing plugins', desc: 'List all plugins'
1852
+ c.example 'doing plugins -t import', desc: 'List all import plugins'
1853
+
1854
+ c.desc 'List plugins of type (import, export)'
1855
+ c.arg_name 'TYPE'
1856
+ c.flag %i[t type], must_match: /^[iea].*$/i, default_value: 'all'
1857
+
1858
+ c.desc 'List in single column for completion'
1859
+ c.switch %i[c column], default_value: false
1860
+
1861
+ c.action do |_global_options, options, _args|
1862
+ Doing::Plugins.list_plugins(options)
1456
1863
  end
1457
1864
  end
1458
1865
 
@@ -1460,6 +1867,9 @@ desc 'Display a user-created view'
1460
1867
  long_desc 'Command line options override view configuration'
1461
1868
  arg_name 'VIEW_NAME'
1462
1869
  command :view do |c|
1870
+ c.example 'doing view color', desc: 'Display entries according to config for view "color"'
1871
+ c.example 'doing view color --section Archive --count 10', desc: 'Display view "color", overriding some configured settings'
1872
+
1463
1873
  c.desc 'Section'
1464
1874
  c.arg_name 'NAME'
1465
1875
  c.flag %i[s section]
@@ -1468,9 +1878,9 @@ command :view do |c|
1468
1878
  c.arg_name 'COUNT'
1469
1879
  c.flag %i[c count], must_match: /^\d+$/, type: Integer
1470
1880
 
1471
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1881
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1472
1882
  c.arg_name 'FORMAT'
1473
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1883
+ c.flag %i[o output]
1474
1884
 
1475
1885
  c.desc 'Show time intervals on @done tasks'
1476
1886
  c.switch %i[t times], default_value: true, negatable: true
@@ -1487,9 +1897,9 @@ command :view do |c|
1487
1897
 
1488
1898
  c.desc 'Tag boolean (AND,OR,NOT)'
1489
1899
  c.arg_name 'BOOLEAN'
1490
- c.flag %i[b bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'OR'
1900
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
1491
1901
 
1492
- c.desc 'Search filter, surround with slashes for regex (/query/)'
1902
+ c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1493
1903
  c.arg_name 'QUERY'
1494
1904
  c.flag [:search]
1495
1905
 
@@ -1499,7 +1909,7 @@ command :view do |c|
1499
1909
 
1500
1910
  c.desc 'Tag sort direction (asc|desc)'
1501
1911
  c.arg_name 'DIRECTION'
1502
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1912
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER
1503
1913
 
1504
1914
  c.desc 'View entries older than date'
1505
1915
  c.arg_name 'DATE_STRING'
@@ -1512,8 +1922,13 @@ command :view do |c|
1512
1922
  c.desc 'Only show items with recorded time intervals (override view settings)'
1513
1923
  c.switch [:only_timed], default_value: false, negatable: false
1514
1924
 
1925
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1926
+ c.switch %i[i interactive]
1927
+
1515
1928
  c.action do |_global_options, options, args|
1516
- exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search]
1929
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1930
+
1931
+ raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1517
1932
 
1518
1933
  title = if args.empty?
1519
1934
  wwid.choose_view
@@ -1524,11 +1939,12 @@ command :view do |c|
1524
1939
  if options[:section]
1525
1940
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
1526
1941
  else
1527
- section = wwid.config['current_section']
1942
+ section = settings['current_section']
1528
1943
  end
1529
1944
 
1530
1945
  view = wwid.get_view(title)
1531
1946
  if view
1947
+ page_title = view.key?('title') ? view['title'] : title.cap_first
1532
1948
  only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
1533
1949
  true
1534
1950
  else
@@ -1536,7 +1952,7 @@ command :view do |c|
1536
1952
  end
1537
1953
 
1538
1954
  template = view.key?('template') ? view['template'] : nil
1539
- format = view.key?('date_format') ? view['date_format'] : nil
1955
+ date_format = view.key?('date_format') ? view['date_format'] : nil
1540
1956
  tags_color = view.key?('tags_color') ? view['tags_color'] : nil
1541
1957
  tag_filter = false
1542
1958
  if options[:tag]
@@ -1554,7 +1970,7 @@ command :view do |c|
1554
1970
  end
1555
1971
 
1556
1972
  # If the -o/--output flag was specified, override any default in the view template
1557
- options[:o] ||= view.key?('output_format') ? view['output_format'] : 'template'
1973
+ options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
1558
1974
 
1559
1975
  count = if options[:c]
1560
1976
  options[:c]
@@ -1564,18 +1980,23 @@ command :view do |c|
1564
1980
  section = if options[:s]
1565
1981
  section
1566
1982
  else
1567
- view.key?('section') ? view['section'] : wwid.current_section
1983
+ view.key?('section') ? view['section'] : settings['current_section']
1568
1984
  end
1569
- order = view.key?('order') ? view['order'] : 'asc'
1985
+ order = view.key?('order') ? view['order'].normalize_order : 'asc'
1570
1986
 
1571
1987
  totals = if options[:totals]
1572
1988
  true
1573
1989
  else
1574
1990
  view.key?('totals') ? view['totals'] : false
1575
1991
  end
1992
+ tag_order = if options[:tag_order]
1993
+ options[:tag_order].normalize_order
1994
+ else
1995
+ view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
1996
+ end
1576
1997
 
1577
1998
  options[:t] = true if totals
1578
- options[:output]&.downcase!
1999
+ output_format = options[:output]&.downcase || 'template'
1579
2000
 
1580
2001
  options[:sort_tags] = if options[:tag_sort]
1581
2002
  options[:tag_sort] =~ /^n/i ? true : false
@@ -1584,40 +2005,50 @@ command :view do |c|
1584
2005
  else
1585
2006
  false
1586
2007
  end
2008
+ if view.key?('after') && !options[:after]
2009
+ options[:after] = view['after']
2010
+ end
1587
2011
 
1588
- tag_order = if options[:tag_order]
1589
- options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1590
- elsif view.key?('tag_order')
1591
- view['tag_order'] =~ /^d/i ? 'desc' : 'asc'
1592
- else
1593
- 'asc'
1594
- end
2012
+ if view.key?('before') && !options[:before]
2013
+ options[:before] = view['before']
2014
+ end
1595
2015
 
1596
- opts = {
1597
- after: options[:after],
1598
- before: options[:before],
1599
- count: count,
1600
- format: format,
1601
- highlight: options[:color],
1602
- only_timed: only_timed,
1603
- order: order,
1604
- output: options[:output],
1605
- search: options[:search],
1606
- section: section,
1607
- sort_tags: options[:sort_tags],
1608
- tag_filter: tag_filter,
1609
- tag_order: tag_order,
1610
- tags_color: tags_color,
1611
- template: template,
1612
- times: options[:t],
1613
- totals: totals
1614
- }
2016
+ if view.key?('from')
2017
+ date_string = view['from']
2018
+ if date_string =~ / (to|through|thru|(un)?til|-+) /
2019
+ dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2020
+ start = wwid.chronify(dates[0], guess: :begin)
2021
+ finish = wwid.chronify(dates[2], guess: :end)
2022
+ else
2023
+ start = wwid.chronify(date_string, guess: :begin)
2024
+ finish = false
2025
+ end
2026
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2027
+ dates = [start, finish]
2028
+ end
1615
2029
 
1616
- puts wwid.list_section(opts)
2030
+ opts = options
2031
+ opts[:output] = output_format
2032
+ opts[:count] = count
2033
+ opts[:format] = date_format
2034
+ opts[:highlight] = options[:color]
2035
+ opts[:only_timed] = only_timed
2036
+ opts[:order] = order
2037
+ opts[:section] = section
2038
+ opts[:tag_filter] = tag_filter
2039
+ opts[:tag_order] = tag_order
2040
+ opts[:tags_color] = tags_color
2041
+ opts[:template] = template
2042
+ opts[:totals] = totals
2043
+ opts[:page_title] = page_title
2044
+ opts[:date_filter] = dates
2045
+ opts[:output] = options[:interactive] ? nil : options[:output]
2046
+
2047
+ Doing::Pager.page wwid.list_section(opts)
1617
2048
  elsif title.instance_of?(FalseClass)
1618
2049
  exit_now! 'Cancelled'
1619
2050
  else
1620
- exit_now! "View #{title} not found in config"
2051
+ raise Doing::Errors::InvalidView, "View #{title} not found in config"
1621
2052
  end
1622
2053
  end
1623
2054
  end
@@ -1634,9 +2065,18 @@ command :views do |c|
1634
2065
  end
1635
2066
 
1636
2067
  desc 'Move entries between sections'
1637
- arg_name 'SECTION_NAME'
1638
- default_value wwid.current_section
1639
- command :archive do |c|
2068
+ long_desc %(Argument can be a section name to move all entries from a section,
2069
+ or start with an "@" to move entries matching a tag.
2070
+
2071
+ Default with no argument moves items from the "#{settings['current_section']}" section to Archive.)
2072
+ arg_name 'SECTION_OR_TAG'
2073
+ default_value settings['current_section']
2074
+ command %i[archive move] do |c|
2075
+ c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section'
2076
+ c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive'
2077
+ c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section'
2078
+ c.example 'doing move Later --tag project1 --to Currently', desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)'
2079
+
1640
2080
  c.desc 'How many items to keep (ignored if archiving by tag or search)'
1641
2081
  c.arg_name 'X'
1642
2082
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer
@@ -1654,7 +2094,7 @@ command :archive do |c|
1654
2094
 
1655
2095
  c.desc 'Tag boolean (AND|OR|NOT)'
1656
2096
  c.arg_name 'BOOLEAN'
1657
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
2097
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1658
2098
 
1659
2099
  c.desc 'Search filter'
1660
2100
  c.arg_name 'QUERY'
@@ -1667,7 +2107,7 @@ command :archive do |c|
1667
2107
 
1668
2108
  c.action do |_global_options, options, args|
1669
2109
  if args.empty?
1670
- section = wwid.current_section
2110
+ section = settings['current_section']
1671
2111
  tags = []
1672
2112
  elsif args[0] =~ /^all/i
1673
2113
  section = 'all'
@@ -1679,34 +2119,25 @@ command :archive do |c|
1679
2119
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
1680
2120
  end
1681
2121
 
1682
- exit_now! '--keep and --count can\'t be used together' if options[:keep] && options[:count]
2122
+ raise Doing::Errors::InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
1683
2123
 
1684
- tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
2124
+ tags.concat(options[:tag].to_tags) if options[:tag]
2125
+
2126
+ opts = options
2127
+ opts[:bool] = options[:bool].normalize_bool
2128
+ opts[:destination] = options[:to]
2129
+ opts[:tags] = tags
1685
2130
 
1686
- options[:bool] = case options[:bool]
1687
- when /(and|all)/i
1688
- 'AND'
1689
- when /(any|or)/i
1690
- 'OR'
1691
- when /(not|none)/i
1692
- 'NOT'
1693
- else
1694
- 'AND'
1695
- end
1696
- opts = {
1697
- before: options[:before],
1698
- bool: options[:bool],
1699
- destination: options[:to],
1700
- keep: options[:keep],
1701
- search: options[:search],
1702
- tags: tags
1703
- }
1704
2131
  wwid.archive(section, opts)
1705
2132
  end
1706
2133
  end
1707
2134
 
1708
2135
  desc 'Move entries to archive file'
1709
2136
  command :rotate do |c|
2137
+ c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
2138
+ c.example 'doing rotate --section Archive --keep 10', desc: 'Move entries in the Archive section to a secondary file, keeping the most recent 10 entries'
2139
+ c.example 'doing rotate --tag project1,done --bool AND', desc: 'Move entries tagged @project1 and @done to a secondary file'
2140
+
1710
2141
  c.desc 'How many items to keep in each section (most recent)'
1711
2142
  c.arg_name 'X'
1712
2143
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer
@@ -1721,7 +2152,7 @@ command :rotate do |c|
1721
2152
 
1722
2153
  c.desc 'Tag boolean (AND|OR|NOT)'
1723
2154
  c.arg_name 'BOOLEAN'
1724
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
2155
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1725
2156
 
1726
2157
  c.desc 'Search filter'
1727
2158
  c.arg_name 'QUERY'
@@ -1737,23 +2168,14 @@ command :rotate do |c|
1737
2168
  options[:section] = wwid.guess_section(options[:section])
1738
2169
  end
1739
2170
 
1740
- options[:bool] = case options[:bool]
1741
- when /(and|all)/i
1742
- 'AND'
1743
- when /(any|or)/i
1744
- 'OR'
1745
- when /(not|none)/i
1746
- 'NOT'
1747
- else
1748
- 'AND'
1749
- end
2171
+ options[:bool] = options[:bool].normalize_bool
1750
2172
 
1751
2173
  wwid.rotate(options)
1752
2174
  end
1753
2175
  end
1754
2176
 
1755
2177
  desc 'Open the "doing" file in an editor'
1756
- long_desc "`doing open` defaults to using the editor_app setting in #{wwid.config_file} (#{wwid.config.key?('editor_app') ? wwid.config['editor_app'] : 'not set'})"
2178
+ long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})."
1757
2179
  command :open do |c|
1758
2180
  if `uname` =~ /Darwin/
1759
2181
  c.desc 'Open with app name'
@@ -1764,8 +2186,6 @@ command :open do |c|
1764
2186
  c.arg_name 'BUNDLE_ID'
1765
2187
  c.flag %i[b bundle_id]
1766
2188
  end
1767
- c.desc "Open with $EDITOR (#{ENV['EDITOR']})"
1768
- c.switch %i[e editor], negatable: false, default_value: false
1769
2189
 
1770
2190
  c.action do |_global_options, options, _args|
1771
2191
  params = options.dup
@@ -1777,30 +2197,54 @@ command :open do |c|
1777
2197
  system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
1778
2198
  elsif options[:bundle_id]
1779
2199
  system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
1780
- elsif options[:editor]
1781
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1782
-
1783
- system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
1784
- elsif wwid.config.key?('editor_app') && !wwid.config['editor_app'].nil?
1785
- system %(open -a "#{wwid.config['editor_app']}" "#{File.expand_path(wwid.doing_file)}")
2200
+ elsif Doing::Util.find_default_editor('doing_file')
2201
+ editor = Doing::Util.find_default_editor('doing_file')
2202
+ if Doing::Util.exec_available(editor)
2203
+ system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
2204
+ else
2205
+ system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
2206
+ end
1786
2207
  else
1787
2208
  system %(open "#{File.expand_path(wwid.doing_file)}")
1788
2209
  end
1789
-
1790
2210
  else
1791
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
2211
+ raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1792
2212
 
1793
- system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
2213
+ system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
1794
2214
  end
1795
2215
  end
1796
2216
  end
1797
2217
 
1798
- desc 'Edit the configuration file'
2218
+ desc 'Edit the configuration file or output a value from it'
2219
+ long_desc %(Run without arguments, `doing config` opens your `.doingrc` in an editor.
2220
+ If local configurations are found in the path between the current directory
2221
+ and `~/.doingrc`, a menu will allow you to select which to open in the editor.
2222
+
2223
+ It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
2224
+
2225
+ Use `doing config -d` to output the configuration to the terminal, and
2226
+ provide a dot-separated key path to get a specific value. Shows the current value
2227
+ including keys/overrides set by local configs.)
2228
+ arg_name 'KEY_PATH'
1799
2229
  command :config do |c|
2230
+ c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
2231
+ c.example 'doing config -d doing_file', desc: 'Output the value of a config key as YAML'
2232
+ c.example 'doing config -d plugins.say.say_voice -o json', desc: 'Output the value of a key path as JSON'
2233
+
1800
2234
  c.desc 'Editor to use'
1801
2235
  c.arg_name 'EDITOR'
1802
2236
  c.flag %i[e editor], default_value: nil
1803
2237
 
2238
+ c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.'
2239
+ c.switch %i[d dump]
2240
+
2241
+ c.desc 'Format for --dump (json|yaml|raw)'
2242
+ c.arg_name 'FORMAT'
2243
+ c.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
2244
+
2245
+ c.desc 'Update config file with missing configuration options'
2246
+ c.switch %i[u update], default_value: false, negatable: false
2247
+
1804
2248
  if `uname` =~ /Darwin/
1805
2249
  c.desc 'Application to use'
1806
2250
  c.arg_name 'APP_NAME'
@@ -1810,31 +2254,84 @@ command :config do |c|
1810
2254
  c.arg_name 'BUNDLE_ID'
1811
2255
  c.flag [:b]
1812
2256
 
1813
- c.desc "Use the config_editor_app defined in ~/.doingrc (#{wwid.config.key?('config_editor_app') ? wwid.config['config_editor_app'] : 'config_editor_app not set'})"
2257
+ c.desc "Use the config_editor_app defined in ~/.doingrc (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
1814
2258
  c.switch [:x]
1815
2259
  end
1816
2260
 
1817
- c.action do |_global_options, options, _args|
2261
+ c.action do |_global_options, options, args|
2262
+ if options[:update]
2263
+ config.configure({rewrite: true, ignore_local: true})
2264
+ # Doing.logger.warn("Config file rewritten: #{config.config_file}")
2265
+ return
2266
+ end
2267
+
2268
+ if options[:dump]
2269
+ keypath = args.join('.')
2270
+ cfg = config.value_for_key(keypath)
2271
+
2272
+ if cfg
2273
+ $stdout.puts case options[:output]
2274
+ when /^j/
2275
+ JSON.pretty_generate(cfg)
2276
+ when /^r/
2277
+ cfg
2278
+ else
2279
+ # cfg = { last_key => cfg } unless last_key.nil?
2280
+ YAML.dump(cfg)
2281
+ end
2282
+ else
2283
+ Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
2284
+ end
2285
+ Doing.logger.output_results
2286
+ return
2287
+ end
2288
+
2289
+ if config.additional_configs.count.positive?
2290
+ choices = [config.config_file]
2291
+ choices.concat(config.additional_configs)
2292
+ res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
2293
+
2294
+ raise Doing::Errors::UserCancelled, 'Cancelled' unless res
2295
+
2296
+ config_file = res.strip || config.config_file
2297
+ else
2298
+ config_file = config.config_file
2299
+ end
2300
+
1818
2301
  if `uname` =~ /Darwin/
1819
2302
  if options[:x]
1820
- `open -a "#{wwid.config['config_editor_app']}" "#{wwid.config_file}"`
2303
+ editor = Doing::Util.find_default_editor('config')
2304
+ if editor
2305
+ if Doing::Util.exec_available(editor)
2306
+ system %(#{editor} "#{config_file}")
2307
+ else
2308
+ `open -a "#{editor}" "#{config_file}"`
2309
+ end
2310
+ else
2311
+ raise Doing::Errors::InvalidArgument, 'No viable editor found in config or environment.'
2312
+ end
1821
2313
  elsif options[:a] || options[:b]
1822
2314
  if options[:a]
1823
- `open -a "#{options[:a]}" "#{wwid.config_file}"`
2315
+ `open -a "#{options[:a]}" "#{config_file}"`
1824
2316
  elsif options[:b]
1825
- `open -b #{options[:b]} "#{wwid.config_file}"`
2317
+ `open -b #{options[:b]} "#{config_file}"`
1826
2318
  end
1827
2319
  else
1828
- exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
2320
+ editor = options[:e] || Doing::Util.find_default_editor('config')
1829
2321
 
1830
- editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1831
- system %(#{editor} "#{wwid.config_file}")
2322
+ raise Doing::Errors::MissingEditor, 'No viable editor defined in config or environment' unless editor
2323
+
2324
+ if Doing::Util.exec_available(editor)
2325
+ system %(#{editor} "#{config_file}")
2326
+ else
2327
+ `open -a "#{editor}" "#{config_file}"`
2328
+ end
1832
2329
  end
1833
2330
  else
1834
- exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
2331
+ editor = options[:e] || Doing::Util.default_editor
2332
+ raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
1835
2333
 
1836
- editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1837
- system %(#{editor} "#{wwid.config_file}")
2334
+ system %(#{editor} "#{config_file}")
1838
2335
  end
1839
2336
  end
1840
2337
  end
@@ -1852,12 +2349,19 @@ command :undo do |c|
1852
2349
  end
1853
2350
 
1854
2351
  desc 'Import entries from an external source'
1855
- long_desc 'Imports entries from other sources. Currently only handles JSON reports exported from Timing.app.'
2352
+ long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
1856
2353
  arg_name 'PATH'
1857
2354
  command :import do |c|
1858
- c.desc 'Import type'
2355
+ c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
1859
2356
  c.arg_name 'TYPE'
1860
- c.flag :type, default_value: 'timing'
2357
+ c.flag :type, default_value: 'doing'
2358
+
2359
+ c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2360
+ c.arg_name 'QUERY'
2361
+ c.flag [:search]
2362
+
2363
+ c.desc 'Only import items with recorded time intervals'
2364
+ c.switch [:only_timed], default_value: false, negatable: false
1861
2365
 
1862
2366
  c.desc 'Target section'
1863
2367
  c.arg_name 'NAME'
@@ -1874,53 +2378,67 @@ command :import do |c|
1874
2378
  c.arg_name 'PREFIX'
1875
2379
  c.flag :prefix
1876
2380
 
2381
+ c.desc 'Import entries older than date'
2382
+ c.arg_name 'DATE_STRING'
2383
+ c.flag [:before]
2384
+
2385
+ c.desc 'Import entries newer than date'
2386
+ c.arg_name 'DATE_STRING'
2387
+ c.flag [:after]
2388
+
2389
+ c.desc %(
2390
+ Date range to import. Date range argument should be quoted. Date specifications can be natural language.
2391
+ To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
2392
+ Has no effect unless the import plugin has implemented date range filtering.
2393
+ )
2394
+ c.arg_name 'DATE_OR_RANGE'
2395
+ c.flag %i[f from]
2396
+
1877
2397
  c.desc 'Allow entries that overlap existing times'
1878
2398
  c.switch [:overlap], negatable: true
1879
2399
 
1880
2400
  c.action do |_global_options, options, args|
1881
2401
 
1882
2402
  if options[:section]
1883
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
1884
- else
1885
- section = wwid.config['current_section']
2403
+ options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
1886
2404
  end
1887
2405
 
1888
- if options[:type] =~ /^tim/i
1889
- args.each do |path|
1890
- options = {
1891
- autotag: options[:autotag],
1892
- no_overlap: !options[:overlap],
1893
- prefix: options[:prefix],
1894
- section: section,
1895
- tag: options[:tag]
1896
- }
1897
- wwid.import_timing(path, options)
1898
- wwid.write(wwid.doing_file)
2406
+ if options[:from]
2407
+ date_string = options[:from]
2408
+ if date_string =~ / (to|through|thru|(un)?til|-+) /
2409
+ dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2410
+ start = wwid.chronify(dates[0], guess: :begin)
2411
+ finish = wwid.chronify(dates[2], guess: :end)
2412
+ else
2413
+ start = wwid.chronify(date_string, guess: :begin)
2414
+ finish = false
1899
2415
  end
2416
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2417
+ dates = [start, finish]
2418
+ end
2419
+
2420
+ if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
2421
+ options[:no_overlap] = !options[:overlap]
2422
+ options[:date_filter] = dates
2423
+ wwid.import(args, options)
2424
+ wwid.write(wwid.doing_file)
1900
2425
  else
1901
- exit_now! 'Invalid import type'
2426
+ raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}"
1902
2427
  end
1903
2428
  end
1904
2429
  end
1905
2430
 
1906
2431
  pre do |global, _command, _options, _args|
1907
- if global[:config_file] && global[:config_file] != wwid.config_file
1908
- wwid.config_file = global[:config_file]
1909
- wwid.configure({ ignore_local: true })
1910
- # wwid.results.push("Override config file #{wwid.config_file}")
1911
- end
1912
-
1913
- if global[:doing_file]
1914
- wwid.init_doing_file(global[:doing_file])
1915
- else
1916
- wwid.init_doing_file
1917
- end
2432
+ # global[:pager] ||= settings['paginate']
1918
2433
 
1919
- wwid.auto_tag = !global[:noauto]
1920
-
1921
- wwid.config[:include_notes] = false unless global[:notes]
2434
+ Doing::Pager.paginate = global[:pager]
1922
2435
 
1923
2436
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
2437
+ unless STDOUT.isatty
2438
+ Doing::Color::coloring = global[:pager] ? global[:color] : false
2439
+ else
2440
+ Doing::Color::coloring = global[:color]
2441
+ end
1924
2442
 
1925
2443
  # Return true to proceed; false to abort and not call the
1926
2444
  # chosen command
@@ -1929,19 +2447,57 @@ pre do |global, _command, _options, _args|
1929
2447
  true
1930
2448
  end
1931
2449
 
2450
+ on_error do |exception|
2451
+ # if exception.kind_of?(SystemExit)
2452
+ # false
2453
+ # else
2454
+ # p exception.inspect
2455
+ # Doing.logger.output_results
2456
+ # true
2457
+ # end
2458
+ false
2459
+ end
2460
+
1932
2461
  post do |global, _command, _options, _args|
1933
2462
  # Use skips_post before a command to skip this
1934
2463
  # block on that command only
2464
+ Doing.logger.output_results
2465
+ end
2466
+
2467
+ around do |global, command, options, arguments, code|
2468
+ # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
2469
+ Doing.logger.adjust_verbosity(global)
2470
+
1935
2471
  if global[:stdout]
1936
- $stdout.print wwid.results.join("\n")
2472
+ Doing.logger.logdev = $stdout
2473
+ end
2474
+
2475
+ wwid.default_option = global[:default]
2476
+
2477
+ if global[:config_file] && global[:config_file] != config.config_file
2478
+ Doing.logger.warn(format('%sWARNING:%s %sThe use of --config_file is deprecated, please set the environment variable DOING_CONFIG instead.', colors.flamingo, colors.default, colors.boldred))
2479
+ Doing.logger.warn(format('%sTo set it just for the current command, use: %sDOING_CONFIG=/path/to/doingrc doing [command]%s', colors.red, colors.boldwhite, colors.default))
2480
+
2481
+ cf = File.expand_path(global[:config_file])
2482
+ raise MissingConfigFile, "Config file not found (#{global[:config_file]})" unless File.exist?(cf)
2483
+
2484
+ config.config_file = cf
2485
+ settings = config.configure({ ignore_local: true })
2486
+ end
2487
+
2488
+ if global[:doing_file]
2489
+ wwid.init_doing_file(global[:doing_file])
1937
2490
  else
1938
- warn wwid.results.join("\n")
2491
+ wwid.init_doing_file
1939
2492
  end
1940
- end
1941
2493
 
1942
- on_error do |_exception|
1943
- # puts exception.message
1944
- true
2494
+ wwid.auto_tag = !global[:noauto]
2495
+
2496
+ settings[:include_notes] = false unless global[:notes]
2497
+
2498
+ global[:wwid] = wwid
2499
+
2500
+ code.call
1945
2501
  end
1946
2502
 
1947
2503
  exit run(ARGV)