doing 1.0.92 → 2.0.5.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +596 -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 +15 -699
  9. data/Rakefile +79 -0
  10. data/_config.yml +1 -0
  11. data/bin/doing +1012 -486
  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 +312 -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 +347 -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 +1868 -2356
  52. data/lib/doing/wwidfile.rb +117 -0
  53. data/lib/doing.rb +44 -4
  54. data/lib/examples/commands/wiki.rb +81 -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 -7
  72. data/lib/doing/helpers.rb +0 -191
  73. data/lib/doing/markdown_export.rb +0 -16
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
87
+
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
46
93
 
47
- desc 'Use a specific configuration file'
48
- flag [:config_file], default_value: wwid.config_file
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'
177
+ end
178
+ end
179
+ end
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], negatable: false, default_value: false
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
119
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)
120
237
  end
121
238
  end
122
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], negatable: false, default_value: false
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
279
+ end
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
144
288
  end
145
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
@@ -248,30 +398,55 @@ 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|markdown|md|erb)/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|MARKDOWN]`' if args.empty?
257
-
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
263
- when /markdown|md|erb/i
264
- $stdout.puts wwid.markdown_template
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
420
+
421
+ if args.empty?
422
+ type = wwid.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
265
423
  else
266
- exit_now! 'Invalid type specified, must be HAML or CSS'
424
+ type = args[0]
267
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
268
441
  end
269
442
  end
270
443
 
271
- desc 'Display an interactive menu to perform operations (requires fzf)'
444
+ desc 'Display an interactive menu to perform operations'
272
445
  long_desc 'List all entries and select with typeahead fuzzy matching.
273
446
 
274
- 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.'
275
450
  command :select do |c|
276
451
  c.desc 'Select from a specific section'
277
452
  c.arg_name 'SECTION'
@@ -296,7 +471,7 @@ command :select do |c|
296
471
 
297
472
  c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
298
473
  c.arg_name 'QUERY'
299
- c.flag %i[q query]
474
+ c.flag %i[q query search]
300
475
 
301
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.'
302
477
  c.switch %i[menu], negatable: true, default_value: true
@@ -323,12 +498,17 @@ command :select do |c|
323
498
  c.arg_name 'FILE'
324
499
  c.flag %i[save_to]
325
500
 
326
- c.desc 'Output entries to format (doing|taskpaper|csv|html|json|template|timeline|taskpaper|markdown)'
501
+ c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})"
327
502
  c.arg_name 'FORMAT'
328
- c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline|taskpaper|markdown|md)$/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]
329
507
 
330
508
  c.action do |_global_options, options, args|
331
- 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]
332
512
 
333
513
  wwid.interactive(options)
334
514
  end
@@ -337,7 +517,7 @@ end
337
517
  desc 'Add an item to the Later section'
338
518
  arg_name 'ENTRY'
339
519
  command :later do |c|
340
- c.desc "Edit entry with #{ENV['EDITOR']}"
520
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
341
521
  c.switch %i[e editor], negatable: false, default_value: false
342
522
 
343
523
  c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
@@ -350,18 +530,18 @@ command :later do |c|
350
530
 
351
531
  c.action do |_global_options, options, args|
352
532
  if options[:back]
353
- date = wwid.chronify(options[:back])
354
- 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?
355
535
  else
356
536
  date = Time.now
357
537
  end
358
538
 
359
539
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
360
- 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?
361
541
 
362
542
  input = args.empty? ? '' : args.join(' ')
363
543
  input = wwid.fork_editor(input).strip
364
- exit_now! 'No content' unless input && !input.empty?
544
+ raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
365
545
 
366
546
  title, note = wwid.format_input(input)
367
547
  note.push(options[:n]) if options[:n]
@@ -378,7 +558,7 @@ command :later do |c|
378
558
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
379
559
  wwid.write(wwid.doing_file)
380
560
  else
381
- 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'
382
562
  end
383
563
  end
384
564
  end
@@ -414,31 +594,39 @@ command %i[done did] do |c|
414
594
  c.arg_name 'NAME'
415
595
  c.flag %i[s section]
416
596
 
417
- c.desc "Edit entry with #{ENV['EDITOR']}"
597
+ c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
418
598
  c.switch %i[e editor], negatable: false, default_value: false
419
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
+
420
607
  # c.desc "Edit entry with specified app"
421
608
  # c.arg_name 'editor_app'
422
609
  # # c.flag [:a, :app]
423
610
 
424
611
  c.action do |_global_options, options, args|
425
612
  took = 0
613
+ donedate = nil
426
614
 
427
615
  if options[:took]
428
616
  took = wwid.chronify_qty(options[:took])
429
- 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?
430
618
  end
431
619
 
432
620
  if options[:back]
433
- date = wwid.chronify(options[:back])
434
- 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?
435
623
  else
436
624
  date = options[:took] ? Time.now - took : Time.now
437
625
  end
438
626
 
439
627
  if options[:at]
440
- finish_date = wwid.chronify(options[:at])
441
- 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?
442
630
 
443
631
  date = options[:took] ? finish_date - took : finish_date
444
632
  elsif options[:took]
@@ -449,58 +637,116 @@ command %i[done did] do |c|
449
637
  finish_date = Time.now
450
638
  end
451
639
 
452
- if finish_date
453
- donedate = options[:date] ? "(#{finish_date.strftime('%F %R')})" : ''
640
+ if options[:date]
641
+ donedate = finish_date.strftime('%F %R')
454
642
  end
455
643
 
456
644
  if options[:section]
457
645
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
458
646
  else
459
- section = wwid.config['current_section']
647
+ section = settings['current_section']
460
648
  end
461
649
 
650
+ note = Doing::Note.new
651
+ note.add(options[:note]) if options[:note]
652
+
462
653
  if options[:editor]
463
- 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
464
672
 
465
- input = ''
466
- input += args.join(' ') unless args.empty?
467
673
  input = wwid.fork_editor(input).strip
468
- exit_now! 'No content' unless input && !input.empty?
674
+ raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
469
675
 
470
676
  title, note = wwid.format_input(input)
471
- title += " @done#{donedate}"
472
- section = 'Archive' if options[:a]
473
- 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
+
474
696
  wwid.write(wwid.doing_file)
475
697
  elsif args.empty? && $stdin.stat.size.zero?
476
698
  if options[:r]
477
699
  wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
478
700
  else
479
- options = { tags: ['done'],
701
+ note = options[:note] ? Doing::Note.new(options[:note]) : nil
702
+ opt = {
480
703
  archive: options[:a],
481
704
  back: finish_date,
482
705
  count: 1,
483
706
  date: options[:date],
707
+ note: note,
484
708
  section: section,
485
- took: took == 0 ? nil : took
709
+ tags: ['done'],
710
+ took: took == 0 ? nil : took,
711
+ unfinished: options[:unfinished]
486
712
  }
487
- wwid.tag_last(options)
713
+ wwid.tag_last(opt)
488
714
  end
489
715
  elsif !args.empty?
490
- 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"))
491
718
  title.chomp!
492
- title += " @done#{donedate}"
493
719
  section = 'Archive' if options[:a]
494
- 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)
495
729
  wwid.write(wwid.doing_file)
730
+ Doing.logger.info('Entry Added:', new_entry.title)
496
731
  elsif $stdin.stat.size.positive?
497
732
  title, note = wwid.format_input($stdin.read)
498
- title += " @done#{donedate}"
733
+ note.add(options[:note]) if options[:note]
499
734
  section = options[:a] ? 'Archive' : section
500
- 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)
501
746
  wwid.write(wwid.doing_file)
747
+ Doing.logger.info('Entry Added:', new_entry.title)
502
748
  else
503
- 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'
504
750
  end
505
751
  end
506
752
  end
@@ -522,39 +768,37 @@ command :cancel do |c|
522
768
 
523
769
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
524
770
  c.arg_name 'BOOLEAN'
525
- 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'
526
772
 
527
773
  c.desc 'Cancel last entry (or entries) not already marked @done'
528
774
  c.switch %i[u unfinished], negatable: false, default_value: false
529
775
 
776
+ c.desc 'Select item(s) to cancel from a menu of matching entries'
777
+ c.switch %i[i interactive], negatable: false, default_value: false
778
+
530
779
  c.action do |_global_options, options, args|
531
780
  if options[:section]
532
781
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
533
782
  else
534
- section = wwid.config['current_section']
783
+ section = settings['current_section']
535
784
  end
536
785
 
537
786
  if options[:tag].nil?
538
787
  tags = []
539
788
  else
540
- tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
541
- options[:bool] = case options[:bool]
542
- when /(and|all)/i
543
- 'AND'
544
- when /(any|or)/i
545
- 'OR'
546
- when /(not|none)/i
547
- 'NOT'
548
- else
549
- 'AND'
550
- end
789
+ tags = options[:tag].to_tags
551
790
  end
552
791
 
553
- exit_now! 'Only one argument allowed' if args.length > 1
792
+ raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
554
793
 
555
- 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
556
801
 
557
- count = args[0] ? args[0].to_i : 1
558
802
  opts = {
559
803
  archive: options[:a],
560
804
  count: count,
@@ -562,10 +806,12 @@ command :cancel do |c|
562
806
  section: section,
563
807
  sequential: false,
564
808
  tag: tags,
565
- tag_bool: options[:bool],
809
+ tag_bool: options[:bool].normalize_bool,
566
810
  tags: ['done'],
567
- unfinished: options[:unfinished]
811
+ unfinished: options[:unfinished],
812
+ interactive: options[:interactive]
568
813
  }
814
+
569
815
  wwid.tag_last(opts)
570
816
  end
571
817
  end
@@ -594,13 +840,16 @@ command :finish do |c|
594
840
  c.arg_name 'TAG'
595
841
  c.flag [:tag]
596
842
 
597
- 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")'
598
844
  c.arg_name 'QUERY'
599
845
  c.flag [:search]
600
846
 
601
847
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
602
848
  c.arg_name 'BOOLEAN'
603
- 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
604
853
 
605
854
  c.desc 'Finish last entry (or entries) not already marked @done'
606
855
  c.switch %i[u unfinished], negatable: false, default_value: false
@@ -617,32 +866,29 @@ command :finish do |c|
617
866
  c.arg_name 'NAME'
618
867
  c.flag %i[s section]
619
868
 
620
- c.action do |_global_options, options, args|
621
- if options[:section]
622
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
623
- else
624
- section = wwid.config['current_section']
625
- end
869
+ c.desc 'Select item(s) to finish from a menu of matching entries'
870
+ c.switch %i[i interactive], negatable: false, default_value: false
626
871
 
872
+ c.action do |_global_options, options, args|
627
873
  unless options[:auto]
628
874
  if options[:took]
629
875
  took = wwid.chronify_qty(options[:took])
630
- 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?
631
877
  end
632
878
 
633
- 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]
634
880
 
635
- 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]
636
882
 
637
883
  if options[:at]
638
- finish_date = wwid.chronify(options[:at])
639
- 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?
640
886
 
641
887
  date = options[:took] ? finish_date - took : finish_date
642
888
  elsif options[:back]
643
889
  date = wwid.chronify(options[:back])
644
890
 
645
- exit_now! 'Unable to parse date string' if date.nil?
891
+ raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
646
892
  elsif options[:took]
647
893
  date = wwid.chronify_qty(options[:took])
648
894
  else
@@ -653,44 +899,42 @@ command :finish do |c|
653
899
  if options[:tag].nil?
654
900
  tags = []
655
901
  else
656
- tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
657
- options[:bool] = case options[:bool]
658
- when /(and|all)/i
659
- 'AND'
660
- when /(any|or)/i
661
- 'OR'
662
- when /(not|none)/i
663
- 'NOT'
664
- else
665
- 'AND'
666
- end
902
+ tags = options[:tag].to_tags
667
903
  end
668
904
 
669
- exit_now! 'Only one argument allowed' if args.length > 1
905
+ raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
906
+
907
+ raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
670
908
 
671
- exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
909
+ if options[:interactive]
910
+ count = 0
911
+ else
912
+ count = args[0] ? args[0].to_i : 1
913
+ end
672
914
 
673
- count = args[0] ? args[0].to_i : 1
674
915
  opts = {
675
- archive: options[:a],
916
+ archive: options[:archive],
676
917
  back: date,
677
918
  count: count,
678
919
  date: options[:date],
679
920
  search: options[:search],
680
- section: section,
921
+ section: options[:section],
681
922
  sequential: options[:auto],
682
923
  tag: tags,
683
- tag_bool: options[:bool],
924
+ tag_bool: options[:bool].normalize_bool,
684
925
  tags: ['done'],
685
- unfinished: options[:unfinished]
926
+ unfinished: options[:unfinished],
927
+ remove: options[:remove],
928
+ interactive: options[:interactive]
686
929
  }
930
+
687
931
  wwid.tag_last(opts)
688
932
  end
689
933
  end
690
934
 
691
935
  desc 'Repeat last entry as new entry'
692
- command [:again, :resume] do |c|
693
- c.desc 'Section'
936
+ command %i[again resume] do |c|
937
+ c.desc 'Get last entry from a specific section'
694
938
  c.arg_name 'NAME'
695
939
  c.flag %i[s section], default_value: 'All'
696
940
 
@@ -703,45 +947,38 @@ command [:again, :resume] do |c|
703
947
  c.flag [:tag]
704
948
 
705
949
  c.desc 'Repeat last entry matching search. Surround with
706
- slashes for regex (e.g. "/query/").'
950
+ slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
707
951
  c.arg_name 'QUERY'
708
952
  c.flag [:search]
709
953
 
710
954
  c.desc 'Boolean used to combine multiple tags'
711
955
  c.arg_name 'BOOLEAN'
712
- 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
713
960
 
714
961
  c.desc 'Note'
715
962
  c.arg_name 'TEXT'
716
963
  c.flag %i[n note]
717
964
 
965
+ c.desc 'Select item to resume from a menu of matching entries'
966
+ c.switch %i[i interactive], negatable: false, default_value: false
967
+
718
968
  c.action do |_global_options, options, _args|
719
- tags = options[:tag].nil? ? [] : options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }
720
- options[:bool] = case options[:bool]
721
- when /(and|all)/i
722
- 'AND'
723
- when /(any|or)/i
724
- 'OR'
725
- when /(not|none)/i
726
- 'NOT'
727
- else
728
- 'AND'
729
- end
730
- opts = {
731
- in: options[:in],
732
- note: options[:n],
733
- search: options[:search],
734
- section: options[:s],
735
- tag: tags,
736
- tag_bool: options[:bool]
737
- }
738
- 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)
739
976
  end
740
977
  end
741
978
 
742
979
  desc 'Add tag(s) to last entry'
743
980
  long_desc 'Add (or remove) tags from the last entry, or from multiple entries
744
- (with `--count`), entries matching a search (with `--search), or entries
981
+ (with `--count`), entries matching a search (with `--search`), or entries
745
982
  containing another tag (with `--tag`).
746
983
 
747
984
  When removing tags with `-r`, wildcards are allowed (`*` to match
@@ -755,13 +992,19 @@ long_desc 'Add (or remove) tags from the last entry, or from multiple entries
755
992
  Tag name arguments do not need to be prefixed with @.'
756
993
  arg_name 'TAG', :multiple
757
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
+
758
1001
  c.desc 'Section'
759
1002
  c.arg_name 'SECTION_NAME'
760
1003
  c.flag %i[s section], default_value: 'All'
761
1004
 
762
1005
  c.desc 'How many recent entries to tag (0 for all)'
763
1006
  c.arg_name 'COUNT'
764
- c.flag %i[c count], default_value: 1
1007
+ c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
765
1008
 
766
1009
  c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
767
1010
  c.arg_name 'ORIG_TAG'
@@ -790,18 +1033,21 @@ command :tag do |c|
790
1033
  c.arg_name 'TAG'
791
1034
  c.flag [:tag]
792
1035
 
793
- 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")'
794
1037
  c.arg_name 'QUERY'
795
1038
  c.flag [:search]
796
1039
 
797
1040
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
798
1041
  c.arg_name 'BOOLEAN'
799
- 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], negatable: false, default_value: false
800
1046
 
801
1047
  c.action do |_global_options, options, args|
802
- 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]
803
1049
 
804
- 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]
805
1051
 
806
1052
  section = 'All'
807
1053
 
@@ -813,17 +1059,7 @@ command :tag do |c|
813
1059
  if options[:tag].nil?
814
1060
  search_tags = []
815
1061
  else
816
- search_tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
817
- options[:bool] = case options[:bool]
818
- when /(and|all)/i
819
- 'AND'
820
- when /(any|or)/i
821
- 'OR'
822
- when /(not|none)/i
823
- 'NOT'
824
- else
825
- 'AND'
826
- end
1062
+ search_tags = options[:tag].to_tags
827
1063
  end
828
1064
 
829
1065
  if options[:autotag]
@@ -838,7 +1074,13 @@ command :tag do |c|
838
1074
  tags.map! { |tag| tag.sub(/^@/, '').strip }
839
1075
  end
840
1076
 
841
- 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
+
842
1084
 
843
1085
  if count.zero? && !options[:force]
844
1086
  if options[:search]
@@ -869,45 +1111,121 @@ command :tag do |c|
869
1111
  options[:section] = section
870
1112
  options[:tag] = search_tags
871
1113
  options[:tags] = tags
872
- options[:tag_bool] = options[:bool]
873
-
874
- # opts = {
875
- # autotag: options[:a],
876
- # count: count,
877
- # date: options[:date],
878
- # iregex: options[:iregex]
879
- # remove: options[:r],
880
- # search: options[:search],
881
- # section: section,
882
- # tag: search_tags,
883
- # tag_bool: options[:bool],
884
- # tags: tags,
885
- # unfinished: options[:unfinished]
886
- # }
1114
+ options[:tag_bool] = options[:bool].normalize_bool
1115
+
887
1116
  wwid.tag_last(options)
888
1117
  end
889
1118
  end
890
1119
 
891
- 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'
892
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
+
893
1139
  c.desc 'Section'
894
- c.arg_name 'NAME'
895
- 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
1146
+
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
896
1149
 
897
- c.desc 'Remove mark'
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'
898
1154
  c.switch %i[r remove], negatable: false, default_value: false
899
1155
 
900
- c.desc 'Mark last entry not marked @done'
1156
+ c.desc 'Flag last entry (or entries) not marked @done'
901
1157
  c.switch %i[u unfinished], negatable: false, default_value: false
902
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], negatable: false, default_value: false
1174
+
903
1175
  c.action do |_global_options, options, _args|
904
- mark = wwid.config['marker_tag'] || 'flagged'
905
- wwid.tag_last({
906
- remove: options[:r],
907
- section: options[:s],
908
- tags: [mark],
909
- unfinished: options[:unfinished]
910
- })
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)
911
1229
  end
912
1230
  end
913
1231
 
@@ -918,13 +1236,19 @@ long_desc %(
918
1236
  )
919
1237
  arg_name '[SECTION|@TAGS]'
920
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
+
921
1245
  c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
922
1246
  c.arg_name 'TAG'
923
1247
  c.flag [:tag]
924
1248
 
925
1249
  c.desc 'Tag boolean (AND,OR,NOT)'
926
1250
  c.arg_name 'BOOLEAN'
927
- 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'
928
1252
 
929
1253
  c.desc 'Max count to show'
930
1254
  c.arg_name 'MAX'
@@ -942,13 +1266,13 @@ command :show do |c|
942
1266
  c.arg_name 'DATE_STRING'
943
1267
  c.flag [:after]
944
1268
 
945
- 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")'
946
1270
  c.arg_name 'QUERY'
947
1271
  c.flag [:search]
948
1272
 
949
1273
  c.desc 'Sort order (asc/desc)'
950
1274
  c.arg_name 'ORDER'
951
- 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'
952
1276
 
953
1277
  c.desc %(
954
1278
  Date range to show, or a single day to filter date on.
@@ -966,21 +1290,26 @@ command :show do |c|
966
1290
 
967
1291
  c.desc 'Sort tags by (name|time)'
968
1292
  default = 'time'
969
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1293
+ default = settings['tag_sort'] || 'name'
970
1294
  c.arg_name 'KEY'
971
1295
  c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
972
1296
 
973
1297
  c.desc 'Tag sort direction (asc|desc)'
974
1298
  c.arg_name 'DIRECTION'
975
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1299
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
976
1300
 
977
1301
  c.desc 'Only show items with recorded time intervals'
978
1302
  c.switch [:only_timed], default_value: false, negatable: false
979
1303
 
980
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1304
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1305
+ c.switch %i[i interactive], negatable: false, default_value: false
1306
+
1307
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
981
1308
  c.arg_name 'FORMAT'
982
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1309
+ c.flag %i[o output]
983
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
+
984
1313
  tag_filter = false
985
1314
  tags = []
986
1315
  if args.length.positive?
@@ -995,7 +1324,7 @@ command :show do |c|
995
1324
  section = 'All'
996
1325
  else
997
1326
  section = wwid.guess_section(args[0])
998
- exit_now! "No such section: #{args[0]}" unless section
1327
+ raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section
999
1328
 
1000
1329
  args.shift
1001
1330
  end
@@ -1007,72 +1336,51 @@ command :show do |c|
1007
1336
  end
1008
1337
  end
1009
1338
  else
1010
- section = wwid.current_section
1339
+ section = settings['current_section']
1011
1340
  end
1012
1341
 
1013
- tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
1014
- options[:bool] = case options[:bool]
1015
- when /(and|all)/i
1016
- 'AND'
1017
- when /(any|or)/i
1018
- 'OR'
1019
- when /(not|none)/i
1020
- 'NOT'
1021
- else
1022
- 'AND'
1023
- end
1342
+ tags.concat(options[:tag].to_tags) if options[:tag]
1024
1343
 
1025
1344
  unless tags.empty?
1026
1345
  tag_filter = {
1027
1346
  'tags' => tags,
1028
- 'bool' => options[:bool]
1347
+ 'bool' => options[:bool].normalize_bool
1029
1348
  }
1030
1349
  end
1031
1350
 
1032
1351
  if options[:from]
1352
+
1033
1353
  date_string = options[:from]
1034
1354
  if date_string =~ / (to|through|thru|(un)?til|-+) /
1035
1355
  dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
1036
- start = wwid.chronify(dates[0])
1037
- finish = wwid.chronify(dates[2])
1356
+ start = wwid.chronify(dates[0], guess: :begin)
1357
+ finish = wwid.chronify(dates[2], guess: :end)
1038
1358
  else
1039
- start = wwid.chronify(date_string)
1359
+ start = wwid.chronify(date_string, guess: :begin)
1040
1360
  finish = false
1041
1361
  end
1042
- exit_now! 'Unrecognized date string' unless start
1362
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1043
1363
  dates = [start, finish]
1044
1364
  end
1045
1365
 
1046
1366
  options[:times] = true if options[:totals]
1047
1367
 
1048
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1368
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1049
1369
 
1050
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
1051
- tag_order = if options[:tag_order]
1052
- options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1053
- else
1054
- 'asc'
1055
- end
1056
- opts = {
1057
- after: options[:after],
1058
- age: options[:age],
1059
- before: options[:before],
1060
- count: options[:c].to_i,
1061
- date_filter: dates,
1062
- highlight: true,
1063
- only_timed: options[:only_timed],
1064
- order: options[:s],
1065
- output: options[:output],
1066
- search: options[:search],
1067
- section: section,
1068
- sort_tags: options[:sort_tags],
1069
- tag_filter: tag_filter,
1070
- tag_order: tag_order,
1071
- tags_color: tags_color,
1072
- times: options[:t],
1073
- totals: options[:totals]
1074
- }
1075
- 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)
1076
1384
  end
1077
1385
  end
1078
1386
 
@@ -1084,7 +1392,12 @@ long_desc <<~'EODESC'
1084
1392
  EODESC
1085
1393
 
1086
1394
  arg_name 'SEARCH_PATTERN'
1087
- 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
+
1088
1401
  c.desc 'Section'
1089
1402
  c.arg_name 'NAME'
1090
1403
  c.flag %i[s section], default_value: 'All'
@@ -1097,9 +1410,9 @@ command [:grep, :search] do |c|
1097
1410
  c.arg_name 'DATE_STRING'
1098
1411
  c.flag [:after]
1099
1412
 
1100
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1413
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1101
1414
  c.arg_name 'FORMAT'
1102
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1415
+ c.flag %i[o output]
1103
1416
 
1104
1417
  c.desc 'Show time intervals on @done tasks'
1105
1418
  c.switch %i[t times], default_value: true, negatable: true
@@ -1109,36 +1422,31 @@ command [:grep, :search] do |c|
1109
1422
 
1110
1423
  c.desc 'Sort tags by (name|time)'
1111
1424
  default = 'time'
1112
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1425
+ default = settings['tag_sort'] || 'name'
1113
1426
  c.arg_name 'KEY'
1114
1427
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1115
1428
 
1116
1429
  c.desc 'Only show items with recorded time intervals'
1117
1430
  c.switch [:only_timed], default_value: false, negatable: false
1118
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
+
1119
1435
  c.action do |_global_options, options, args|
1120
- 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)
1437
+
1438
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1121
1439
 
1122
- section = wwid.guess_section(options[:s]) if options[:s]
1440
+ section = wwid.guess_section(options[:section]) if options[:section]
1123
1441
 
1124
1442
  options[:times] = true if options[:totals]
1125
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
1126
1448
 
1127
- opts = {
1128
- after: options[:after],
1129
- before: options[:before],
1130
- highlight: true,
1131
- only_timed: options[:only_timed],
1132
- output: options[:output],
1133
- search: args.join(' '),
1134
- section: section,
1135
- sort_tags: options[:sort_tags],
1136
- tags_color: tags_color,
1137
- times: options[:times],
1138
- totals: options[:totals]
1139
- }
1140
-
1141
- puts wwid.list_section(opts)
1449
+ Doing::Pager.page wwid.list_section(options)
1142
1450
  end
1143
1451
  end
1144
1452
 
@@ -1146,6 +1454,11 @@ desc 'List recent entries'
1146
1454
  default_value 10
1147
1455
  arg_name 'COUNT'
1148
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
+
1149
1462
  c.desc 'Section'
1150
1463
  c.arg_name 'NAME'
1151
1464
  c.flag %i[s section], default_value: 'All'
@@ -1158,32 +1471,42 @@ command :recent do |c|
1158
1471
 
1159
1472
  c.desc 'Sort tags by (name|time)'
1160
1473
  default = 'time'
1161
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1474
+ default = settings['tag_sort'] || 'name'
1162
1475
  c.arg_name 'KEY'
1163
1476
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1164
1477
 
1478
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1479
+ c.switch %i[i interactive], negatable: false, default_value: false
1480
+
1165
1481
  c.action do |global_options, options, args|
1166
1482
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
1167
1483
 
1168
1484
  unless global_options[:version]
1169
- if wwid.config['templates']['recent'].key?('count')
1170
- 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
1171
1487
  else
1172
1488
  config_count = 10
1173
1489
  end
1174
- 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
+
1175
1497
  options[:t] = true if options[:totals]
1176
1498
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1177
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1499
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1178
1500
 
1179
1501
  opts = {
1180
1502
  sort_tags: options[:sort_tags],
1181
1503
  tags_color: tags_color,
1182
1504
  times: options[:t],
1183
- totals: options[:totals]
1505
+ totals: options[:totals],
1506
+ interactive: options[:interactive]
1184
1507
  }
1185
1508
 
1186
- puts wwid.recent(count, section.cap_first, opts)
1509
+ Doing::Pager::page wwid.recent(count, section.cap_first, opts)
1187
1510
 
1188
1511
  end
1189
1512
  end
@@ -1191,6 +1514,11 @@ end
1191
1514
 
1192
1515
  desc 'List entries from today'
1193
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
+
1194
1522
  c.desc 'Specify a section'
1195
1523
  c.arg_name 'NAME'
1196
1524
  c.flag %i[s section], default_value: 'All'
@@ -1203,13 +1531,13 @@ command :today do |c|
1203
1531
 
1204
1532
  c.desc 'Sort tags by (name|time)'
1205
1533
  default = 'time'
1206
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1534
+ default = settings['tag_sort'] || 'name'
1207
1535
  c.arg_name 'KEY'
1208
1536
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1209
1537
 
1210
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1538
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1211
1539
  c.arg_name 'FORMAT'
1212
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1540
+ c.flag %i[o output]
1213
1541
 
1214
1542
  c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
1215
1543
  c.arg_name 'TIME_STRING'
@@ -1220,6 +1548,8 @@ command :today do |c|
1220
1548
  c.flag [:after]
1221
1549
 
1222
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
+
1223
1553
  options[:t] = true if options[:totals]
1224
1554
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1225
1555
  opt = {
@@ -1227,9 +1557,10 @@ command :today do |c|
1227
1557
  before: options[:before],
1228
1558
  section: options[:section],
1229
1559
  sort_tags: options[:sort_tags],
1230
- totals: options[:totals]
1560
+ totals: options[:totals],
1561
+ order: settings.dig('templates', 'today', 'order')
1231
1562
  }
1232
- puts wwid.today(options[:times], options[:output], opt).chomp
1563
+ Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
1233
1564
  end
1234
1565
  end
1235
1566
 
@@ -1239,6 +1570,10 @@ and "2d" would be interpreted as "two days ago." If you use "to" or "through" be
1239
1570
  it will create a range.)
1240
1571
  arg_name 'DATE_STRING'
1241
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
+
1242
1577
  c.desc 'Section'
1243
1578
  c.arg_name 'NAME'
1244
1579
  c.flag %i[s section], default_value: 'All'
@@ -1251,38 +1586,40 @@ command :on do |c|
1251
1586
 
1252
1587
  c.desc 'Sort tags by (name|time)'
1253
1588
  default = 'time'
1254
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1589
+ default = settings['tag_sort'] || 'name'
1255
1590
  c.arg_name 'KEY'
1256
1591
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1257
1592
 
1258
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1593
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1259
1594
  c.arg_name 'FORMAT'
1260
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1595
+ c.flag %i[o output]
1261
1596
 
1262
1597
  c.action do |_global_options, options, args|
1263
- 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?
1264
1601
 
1265
1602
  date_string = args.join(' ')
1266
1603
 
1267
1604
  if date_string =~ / (to|through|thru) /
1268
1605
  dates = date_string.split(/ (to|through|thru) /)
1269
- start = wwid.chronify(dates[0])
1270
- finish = wwid.chronify(dates[2])
1606
+ start = wwid.chronify(dates[0], guess: :begin)
1607
+ finish = wwid.chronify(dates[2], guess: :end)
1271
1608
  else
1272
- start = wwid.chronify(date_string)
1609
+ start = wwid.chronify(date_string, guess: :begin)
1273
1610
  finish = false
1274
1611
  end
1275
1612
 
1276
- exit_now! 'Unrecognized date string' unless start
1613
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1277
1614
 
1278
1615
  message = "Date interpreted as #{start}"
1279
1616
  message += " to #{finish}" if finish
1280
- wwid.results.push(message)
1617
+ Doing.logger.debug(message)
1281
1618
 
1282
1619
  options[:t] = true if options[:totals]
1283
1620
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1284
1621
 
1285
- 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],
1286
1623
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1287
1624
  end
1288
1625
  end
@@ -1292,6 +1629,9 @@ long_desc %(Date argument can be natural language and are always interpreted as
1292
1629
  and "2d" would be interpreted as "two days ago.")
1293
1630
  arg_name 'DATE_STRING'
1294
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
+
1295
1635
  c.desc 'Section'
1296
1636
  c.arg_name 'NAME'
1297
1637
  c.flag %i[s section], default_value: 'All'
@@ -1304,47 +1644,52 @@ command :since do |c|
1304
1644
 
1305
1645
  c.desc 'Sort tags by (name|time)'
1306
1646
  default = 'time'
1307
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1647
+ default = settings['tag_sort'] || 'name'
1308
1648
  c.arg_name 'KEY'
1309
1649
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1310
1650
 
1311
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1651
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1312
1652
  c.arg_name 'FORMAT'
1313
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1653
+ c.flag %i[o output]
1314
1654
 
1315
1655
  c.action do |_global_options, options, args|
1316
- 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?
1317
1659
 
1318
1660
  date_string = args.join(' ')
1319
1661
 
1320
- date_string += ' at midnight' unless date_string =~ /(\d:|\d *[ap]m?|midnight|noon)/i
1321
- 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')
1322
1664
 
1323
- start = wwid.chronify(date_string)
1665
+ start = wwid.chronify(date_string, guess: :begin)
1324
1666
  finish = Time.now
1325
1667
 
1326
- exit_now! 'Unrecognized date string' unless start
1668
+ raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1327
1669
 
1328
- message = "Date interpreted as #{start} through the current time"
1329
- wwid.results.push(message)
1670
+ Doing.logger.debug("Date interpreted as #{start} through the current time")
1330
1671
 
1331
1672
  options[:t] = true if options[:totals]
1332
1673
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1333
1674
 
1334
- 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],
1335
1676
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1336
1677
  end
1337
1678
  end
1338
1679
 
1339
1680
  desc 'List entries from yesterday'
1340
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
+
1341
1686
  c.desc 'Specify a section'
1342
1687
  c.arg_name 'NAME'
1343
1688
  c.flag %i[s section], default_value: 'All'
1344
1689
 
1345
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1690
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1346
1691
  c.arg_name 'FORMAT'
1347
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1692
+ c.flag %i[o output]
1348
1693
 
1349
1694
  c.desc 'Show time intervals on @done tasks'
1350
1695
  c.switch %i[t times], default_value: true, negatable: true
@@ -1354,7 +1699,7 @@ command :yesterday do |c|
1354
1699
 
1355
1700
  c.desc 'Sort tags by (name|time)'
1356
1701
  default = 'time'
1357
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1702
+ default = settings['tag_sort'] || 'name'
1358
1703
  c.arg_name 'KEY'
1359
1704
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1360
1705
 
@@ -1368,33 +1713,40 @@ command :yesterday do |c|
1368
1713
 
1369
1714
  c.desc 'Tag sort direction (asc|desc)'
1370
1715
  c.arg_name 'DIRECTION'
1371
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1716
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1372
1717
 
1373
1718
  c.action do |_global_options, options, _args|
1374
- tag_order = if options[:tag_order]
1375
- options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1376
- else
1377
- 'asc'
1378
- end
1719
+ raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1720
+
1379
1721
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1722
+
1380
1723
  opt = {
1381
1724
  after: options[:after],
1382
1725
  before: options[:before],
1383
1726
  sort_tags: options[:sort_tags],
1384
- tag_order: options[:tag_order],
1385
- totals: options[:totals]
1727
+ tag_order: options[:tag_order].normalize_order,
1728
+ totals: options[:totals],
1729
+ order: settings.dig('templates', 'today', 'order')
1386
1730
  }
1387
- 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
1388
1732
  end
1389
1733
  end
1390
1734
 
1391
1735
  desc 'Show the last entry, optionally edit'
1392
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
+
1393
1745
  c.desc 'Specify a section'
1394
1746
  c.arg_name 'NAME'
1395
1747
  c.flag %i[s section], default_value: 'All'
1396
1748
 
1397
- c.desc "Edit entry with #{ENV['EDITOR']}"
1749
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
1398
1750
  c.switch %i[e editor], negatable: false, default_value: false
1399
1751
 
1400
1752
  c.desc 'Tag filter, combine multiple tags with a comma.'
@@ -1403,19 +1755,19 @@ command :last do |c|
1403
1755
 
1404
1756
  c.desc 'Tag boolean'
1405
1757
  c.arg_name 'BOOLEAN'
1406
- 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'
1407
1759
 
1408
- 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")'
1409
1761
  c.arg_name 'QUERY'
1410
1762
  c.flag [:search]
1411
1763
 
1412
- c.action do |_global_options, options, _args|
1413
- 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]
1414
1766
 
1415
1767
  if options[:tag].nil?
1416
1768
  tags = []
1417
1769
  else
1418
- tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
1770
+ tags = options[:tag].to_tags
1419
1771
  options[:bool] = case options[:bool]
1420
1772
  when /(any|or)/i
1421
1773
  :or
@@ -1430,7 +1782,7 @@ command :last do |c|
1430
1782
  if options[:editor]
1431
1783
  wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
1432
1784
  else
1433
- puts wwid.last(times: true, section: options[:s],
1785
+ Doing::Pager::page wwid.last(times: true, section: options[:s],
1434
1786
  options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
1435
1787
  end
1436
1788
  end
@@ -1452,17 +1804,19 @@ command :choose do |c|
1452
1804
  c.action do |_global_options, _options, _args|
1453
1805
  section = wwid.choose_section
1454
1806
 
1455
- 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
1456
1808
  end
1457
1809
  end
1458
1810
 
1459
1811
  desc 'Add a new section to the "doing" file'
1460
1812
  arg_name 'SECTION_NAME'
1461
1813
  command :add_section do |c|
1814
+ c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
1815
+
1462
1816
  c.action do |_global_options, _options, args|
1463
- 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])
1464
1818
 
1465
- wwid.add_section(args[0].cap_first)
1819
+ wwid.add_section(args.join(' ').cap_first)
1466
1820
  wwid.write(wwid.doing_file)
1467
1821
  end
1468
1822
  end
@@ -1470,18 +1824,42 @@ end
1470
1824
  desc 'List available color variables for configuration templates and views'
1471
1825
  command :colors do |c|
1472
1826
  c.action do |_global_options, _options, _args|
1473
- clrs = wwid.colors
1474
1827
  bgs = []
1475
1828
  fgs = []
1476
- clrs.each do |k, v|
1477
- if k =~ /bg/
1478
- 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}")
1479
1832
  else
1480
- fgs.push("#{v}XXXX#{clrs['default']} <-- #{k}")
1833
+ fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
1481
1834
  end
1482
1835
  end
1483
- puts fgs.join("\n")
1484
- 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)
1485
1863
  end
1486
1864
  end
1487
1865
 
@@ -1489,6 +1867,9 @@ desc 'Display a user-created view'
1489
1867
  long_desc 'Command line options override view configuration'
1490
1868
  arg_name 'VIEW_NAME'
1491
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
+
1492
1873
  c.desc 'Section'
1493
1874
  c.arg_name 'NAME'
1494
1875
  c.flag %i[s section]
@@ -1497,9 +1878,9 @@ command :view do |c|
1497
1878
  c.arg_name 'COUNT'
1498
1879
  c.flag %i[c count], must_match: /^\d+$/, type: Integer
1499
1880
 
1500
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1881
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1501
1882
  c.arg_name 'FORMAT'
1502
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1883
+ c.flag %i[o output]
1503
1884
 
1504
1885
  c.desc 'Show time intervals on @done tasks'
1505
1886
  c.switch %i[t times], default_value: true, negatable: true
@@ -1516,9 +1897,9 @@ command :view do |c|
1516
1897
 
1517
1898
  c.desc 'Tag boolean (AND,OR,NOT)'
1518
1899
  c.arg_name 'BOOLEAN'
1519
- 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'
1520
1901
 
1521
- 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")'
1522
1903
  c.arg_name 'QUERY'
1523
1904
  c.flag [:search]
1524
1905
 
@@ -1528,7 +1909,7 @@ command :view do |c|
1528
1909
 
1529
1910
  c.desc 'Tag sort direction (asc|desc)'
1530
1911
  c.arg_name 'DIRECTION'
1531
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1912
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER
1532
1913
 
1533
1914
  c.desc 'View entries older than date'
1534
1915
  c.arg_name 'DATE_STRING'
@@ -1541,8 +1922,13 @@ command :view do |c|
1541
1922
  c.desc 'Only show items with recorded time intervals (override view settings)'
1542
1923
  c.switch [:only_timed], default_value: false, negatable: false
1543
1924
 
1925
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1926
+ c.switch %i[i interactive], negatable: false, default_value: false
1927
+
1544
1928
  c.action do |_global_options, options, args|
1545
- 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]
1546
1932
 
1547
1933
  title = if args.empty?
1548
1934
  wwid.choose_view
@@ -1553,11 +1939,12 @@ command :view do |c|
1553
1939
  if options[:section]
1554
1940
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
1555
1941
  else
1556
- section = wwid.config['current_section']
1942
+ section = settings['current_section']
1557
1943
  end
1558
1944
 
1559
1945
  view = wwid.get_view(title)
1560
1946
  if view
1947
+ page_title = view.key?('title') ? view['title'] : title.cap_first
1561
1948
  only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
1562
1949
  true
1563
1950
  else
@@ -1565,7 +1952,7 @@ command :view do |c|
1565
1952
  end
1566
1953
 
1567
1954
  template = view.key?('template') ? view['template'] : nil
1568
- format = view.key?('date_format') ? view['date_format'] : nil
1955
+ date_format = view.key?('date_format') ? view['date_format'] : nil
1569
1956
  tags_color = view.key?('tags_color') ? view['tags_color'] : nil
1570
1957
  tag_filter = false
1571
1958
  if options[:tag]
@@ -1583,7 +1970,7 @@ command :view do |c|
1583
1970
  end
1584
1971
 
1585
1972
  # If the -o/--output flag was specified, override any default in the view template
1586
- options[:o] ||= view.key?('output_format') ? view['output_format'] : 'template'
1973
+ options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
1587
1974
 
1588
1975
  count = if options[:c]
1589
1976
  options[:c]
@@ -1593,18 +1980,23 @@ command :view do |c|
1593
1980
  section = if options[:s]
1594
1981
  section
1595
1982
  else
1596
- view.key?('section') ? view['section'] : wwid.current_section
1983
+ view.key?('section') ? view['section'] : settings['current_section']
1597
1984
  end
1598
- order = view.key?('order') ? view['order'] : 'asc'
1985
+ order = view.key?('order') ? view['order'].normalize_order : 'asc'
1599
1986
 
1600
1987
  totals = if options[:totals]
1601
1988
  true
1602
1989
  else
1603
1990
  view.key?('totals') ? view['totals'] : false
1604
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
1605
1997
 
1606
1998
  options[:t] = true if totals
1607
- options[:output]&.downcase!
1999
+ output_format = options[:output]&.downcase || 'template'
1608
2000
 
1609
2001
  options[:sort_tags] = if options[:tag_sort]
1610
2002
  options[:tag_sort] =~ /^n/i ? true : false
@@ -1613,40 +2005,50 @@ command :view do |c|
1613
2005
  else
1614
2006
  false
1615
2007
  end
2008
+ if view.key?('after') && !options[:after]
2009
+ options[:after] = view['after']
2010
+ end
1616
2011
 
1617
- tag_order = if options[:tag_order]
1618
- options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1619
- elsif view.key?('tag_order')
1620
- view['tag_order'] =~ /^d/i ? 'desc' : 'asc'
1621
- else
1622
- 'asc'
1623
- end
2012
+ if view.key?('before') && !options[:before]
2013
+ options[:before] = view['before']
2014
+ end
1624
2015
 
1625
- opts = {
1626
- after: options[:after],
1627
- before: options[:before],
1628
- count: count,
1629
- format: format,
1630
- highlight: options[:color],
1631
- only_timed: only_timed,
1632
- order: order,
1633
- output: options[:output],
1634
- search: options[:search],
1635
- section: section,
1636
- sort_tags: options[:sort_tags],
1637
- tag_filter: tag_filter,
1638
- tag_order: tag_order,
1639
- tags_color: tags_color,
1640
- template: template,
1641
- times: options[:t],
1642
- totals: totals
1643
- }
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
1644
2029
 
1645
- 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)
1646
2048
  elsif title.instance_of?(FalseClass)
1647
2049
  exit_now! 'Cancelled'
1648
2050
  else
1649
- exit_now! "View #{title} not found in config"
2051
+ raise Doing::Errors::InvalidView, "View #{title} not found in config"
1650
2052
  end
1651
2053
  end
1652
2054
  end
@@ -1663,9 +2065,18 @@ command :views do |c|
1663
2065
  end
1664
2066
 
1665
2067
  desc 'Move entries between sections'
1666
- arg_name 'SECTION_NAME'
1667
- default_value wwid.current_section
1668
- 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
+
1669
2080
  c.desc 'How many items to keep (ignored if archiving by tag or search)'
1670
2081
  c.arg_name 'X'
1671
2082
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer
@@ -1683,7 +2094,7 @@ command :archive do |c|
1683
2094
 
1684
2095
  c.desc 'Tag boolean (AND|OR|NOT)'
1685
2096
  c.arg_name 'BOOLEAN'
1686
- 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'
1687
2098
 
1688
2099
  c.desc 'Search filter'
1689
2100
  c.arg_name 'QUERY'
@@ -1696,7 +2107,7 @@ command :archive do |c|
1696
2107
 
1697
2108
  c.action do |_global_options, options, args|
1698
2109
  if args.empty?
1699
- section = wwid.current_section
2110
+ section = settings['current_section']
1700
2111
  tags = []
1701
2112
  elsif args[0] =~ /^all/i
1702
2113
  section = 'all'
@@ -1708,34 +2119,25 @@ command :archive do |c|
1708
2119
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
1709
2120
  end
1710
2121
 
1711
- 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]
1712
2123
 
1713
- 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
1714
2130
 
1715
- options[:bool] = case options[:bool]
1716
- when /(and|all)/i
1717
- 'AND'
1718
- when /(any|or)/i
1719
- 'OR'
1720
- when /(not|none)/i
1721
- 'NOT'
1722
- else
1723
- 'AND'
1724
- end
1725
- opts = {
1726
- before: options[:before],
1727
- bool: options[:bool],
1728
- destination: options[:to],
1729
- keep: options[:keep],
1730
- search: options[:search],
1731
- tags: tags
1732
- }
1733
2131
  wwid.archive(section, opts)
1734
2132
  end
1735
2133
  end
1736
2134
 
1737
2135
  desc 'Move entries to archive file'
1738
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
+
1739
2141
  c.desc 'How many items to keep in each section (most recent)'
1740
2142
  c.arg_name 'X'
1741
2143
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer
@@ -1750,7 +2152,7 @@ command :rotate do |c|
1750
2152
 
1751
2153
  c.desc 'Tag boolean (AND|OR|NOT)'
1752
2154
  c.arg_name 'BOOLEAN'
1753
- 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'
1754
2156
 
1755
2157
  c.desc 'Search filter'
1756
2158
  c.arg_name 'QUERY'
@@ -1766,23 +2168,14 @@ command :rotate do |c|
1766
2168
  options[:section] = wwid.guess_section(options[:section])
1767
2169
  end
1768
2170
 
1769
- options[:bool] = case options[:bool]
1770
- when /(and|all)/i
1771
- 'AND'
1772
- when /(any|or)/i
1773
- 'OR'
1774
- when /(not|none)/i
1775
- 'NOT'
1776
- else
1777
- 'AND'
1778
- end
2171
+ options[:bool] = options[:bool].normalize_bool
1779
2172
 
1780
2173
  wwid.rotate(options)
1781
2174
  end
1782
2175
  end
1783
2176
 
1784
2177
  desc 'Open the "doing" file in an editor'
1785
- 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'})."
1786
2179
  command :open do |c|
1787
2180
  if `uname` =~ /Darwin/
1788
2181
  c.desc 'Open with app name'
@@ -1793,8 +2186,6 @@ command :open do |c|
1793
2186
  c.arg_name 'BUNDLE_ID'
1794
2187
  c.flag %i[b bundle_id]
1795
2188
  end
1796
- c.desc "Open with $EDITOR (#{ENV['EDITOR']})"
1797
- c.switch %i[e editor], negatable: false, default_value: false
1798
2189
 
1799
2190
  c.action do |_global_options, options, _args|
1800
2191
  params = options.dup
@@ -1806,30 +2197,54 @@ command :open do |c|
1806
2197
  system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
1807
2198
  elsif options[:bundle_id]
1808
2199
  system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
1809
- elsif options[:editor]
1810
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1811
-
1812
- system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
1813
- elsif wwid.config.key?('editor_app') && !wwid.config['editor_app'].nil?
1814
- 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
1815
2207
  else
1816
2208
  system %(open "#{File.expand_path(wwid.doing_file)}")
1817
2209
  end
1818
-
1819
2210
  else
1820
- 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?
1821
2212
 
1822
- system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
2213
+ system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
1823
2214
  end
1824
2215
  end
1825
2216
  end
1826
2217
 
1827
- 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'
1828
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
+
1829
2234
  c.desc 'Editor to use'
1830
2235
  c.arg_name 'EDITOR'
1831
2236
  c.flag %i[e editor], default_value: nil
1832
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
+
1833
2248
  if `uname` =~ /Darwin/
1834
2249
  c.desc 'Application to use'
1835
2250
  c.arg_name 'APP_NAME'
@@ -1839,31 +2254,84 @@ command :config do |c|
1839
2254
  c.arg_name 'BUNDLE_ID'
1840
2255
  c.flag [:b]
1841
2256
 
1842
- 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'})"
1843
2258
  c.switch [:x]
1844
2259
  end
1845
2260
 
1846
- 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
+
1847
2301
  if `uname` =~ /Darwin/
1848
2302
  if options[:x]
1849
- `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
1850
2313
  elsif options[:a] || options[:b]
1851
2314
  if options[:a]
1852
- `open -a "#{options[:a]}" "#{wwid.config_file}"`
2315
+ `open -a "#{options[:a]}" "#{config_file}"`
1853
2316
  elsif options[:b]
1854
- `open -b #{options[:b]} "#{wwid.config_file}"`
2317
+ `open -b #{options[:b]} "#{config_file}"`
1855
2318
  end
1856
2319
  else
1857
- 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')
1858
2321
 
1859
- editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1860
- 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
1861
2329
  end
1862
2330
  else
1863
- 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)
1864
2333
 
1865
- editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1866
- system %(#{editor} "#{wwid.config_file}")
2334
+ system %(#{editor} "#{config_file}")
1867
2335
  end
1868
2336
  end
1869
2337
  end
@@ -1881,12 +2349,19 @@ command :undo do |c|
1881
2349
  end
1882
2350
 
1883
2351
  desc 'Import entries from an external source'
1884
- 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: ', ')}"
1885
2353
  arg_name 'PATH'
1886
2354
  command :import do |c|
1887
- c.desc 'Import type'
2355
+ c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
1888
2356
  c.arg_name 'TYPE'
1889
- 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
1890
2365
 
1891
2366
  c.desc 'Target section'
1892
2367
  c.arg_name 'NAME'
@@ -1903,53 +2378,67 @@ command :import do |c|
1903
2378
  c.arg_name 'PREFIX'
1904
2379
  c.flag :prefix
1905
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
+
1906
2397
  c.desc 'Allow entries that overlap existing times'
1907
2398
  c.switch [:overlap], negatable: true
1908
2399
 
1909
2400
  c.action do |_global_options, options, args|
1910
2401
 
1911
2402
  if options[:section]
1912
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
1913
- else
1914
- section = wwid.config['current_section']
2403
+ options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
1915
2404
  end
1916
2405
 
1917
- if options[:type] =~ /^tim/i
1918
- args.each do |path|
1919
- options = {
1920
- autotag: options[:autotag],
1921
- no_overlap: !options[:overlap],
1922
- prefix: options[:prefix],
1923
- section: section,
1924
- tag: options[:tag]
1925
- }
1926
- wwid.import_timing(path, options)
1927
- 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
1928
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)
1929
2425
  else
1930
- exit_now! 'Invalid import type'
2426
+ raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}"
1931
2427
  end
1932
2428
  end
1933
2429
  end
1934
2430
 
1935
2431
  pre do |global, _command, _options, _args|
1936
- if global[:config_file] && global[:config_file] != wwid.config_file
1937
- wwid.config_file = global[:config_file]
1938
- wwid.configure({ ignore_local: true })
1939
- # wwid.results.push("Override config file #{wwid.config_file}")
1940
- end
2432
+ # global[:pager] ||= settings['paginate']
1941
2433
 
1942
- if global[:doing_file]
1943
- wwid.init_doing_file(global[:doing_file])
1944
- else
1945
- wwid.init_doing_file
1946
- end
1947
-
1948
- wwid.auto_tag = !global[:noauto]
1949
-
1950
- wwid.config[:include_notes] = false unless global[:notes]
2434
+ Doing::Pager.paginate = global[:pager]
1951
2435
 
1952
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
1953
2442
 
1954
2443
  # Return true to proceed; false to abort and not call the
1955
2444
  # chosen command
@@ -1958,20 +2447,57 @@ pre do |global, _command, _options, _args|
1958
2447
  true
1959
2448
  end
1960
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
+
1961
2461
  post do |global, _command, _options, _args|
1962
2462
  # Use skips_post before a command to skip this
1963
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)
1964
2470
 
1965
2471
  if global[:stdout]
1966
- $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])
1967
2490
  else
1968
- warn wwid.results.join("\n")
2491
+ wwid.init_doing_file
1969
2492
  end
1970
- end
1971
2493
 
1972
- on_error do |_exception|
1973
- # puts exception.message
1974
- 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
1975
2501
  end
1976
2502
 
1977
2503
  exit run(ARGV)