doing 1.0.90 → 2.0.2.pre

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