doing 1.0.93 → 2.0.6.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 +616 -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 +1055 -494
  12. data/doing.gemspec +34 -0
  13. data/doing.rdoc +1839 -0
  14. data/example_plugin.rb +209 -0
  15. data/generate_completions.sh +5 -0
  16. data/img/doing-colors.jpg +0 -0
  17. data/img/doing-printf-wrap-800.jpg +0 -0
  18. data/img/doing-show-note-formatting-800.jpg +0 -0
  19. data/lib/completion/_doing.zsh +203 -0
  20. data/lib/completion/doing.bash +449 -0
  21. data/lib/completion/doing.fish +329 -0
  22. data/lib/doing/array.rb +8 -0
  23. data/lib/doing/cli_status.rb +70 -0
  24. data/lib/doing/colors.rb +136 -0
  25. data/lib/doing/configuration.rb +312 -0
  26. data/lib/doing/errors.rb +109 -0
  27. data/lib/doing/hash.rb +31 -0
  28. data/lib/doing/hooks.rb +59 -0
  29. data/lib/doing/item.rb +155 -0
  30. data/lib/doing/log_adapter.rb +344 -0
  31. data/lib/doing/markdown_document_listener.rb +174 -0
  32. data/lib/doing/note.rb +59 -0
  33. data/lib/doing/pager.rb +95 -0
  34. data/lib/doing/plugin_manager.rb +208 -0
  35. data/lib/doing/plugins/export/csv_export.rb +48 -0
  36. data/lib/doing/plugins/export/html_export.rb +83 -0
  37. data/lib/doing/plugins/export/json_export.rb +140 -0
  38. data/lib/doing/plugins/export/markdown_export.rb +85 -0
  39. data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
  40. data/lib/doing/plugins/export/template_export.rb +141 -0
  41. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  42. data/lib/doing/plugins/import/calendar_import.rb +76 -0
  43. data/lib/doing/plugins/import/doing_import.rb +144 -0
  44. data/lib/doing/plugins/import/timing_import.rb +78 -0
  45. data/lib/doing/string.rb +348 -0
  46. data/lib/doing/symbol.rb +16 -0
  47. data/lib/doing/time.rb +18 -0
  48. data/lib/doing/util.rb +186 -0
  49. data/lib/doing/version.rb +1 -1
  50. data/lib/doing/wwid.rb +1868 -2349
  51. data/lib/doing/wwidfile.rb +117 -0
  52. data/lib/doing.rb +43 -3
  53. data/lib/examples/commands/autotag.rb +63 -0
  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 +211 -0
  69. data/scripts/generate_fish_completions.rb +204 -0
  70. data/scripts/generate_zsh_completions.rb +168 -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 settings.dig('plugins', 'command_path')
61
+ commands_from File.expand_path(settings.dig('plugins', 'command_path'))
62
+ end
32
63
 
33
64
  program_desc 'A CLI for a What Was I Doing system'
65
+ program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text record of what you've been doing, complete with tag-based time tracking. The command line tool allows you to add entries, annotate with tags and notes, and view your entries with myriad options, with a focus on a "natural" language syntax.)
34
66
 
35
67
  default_command :recent
36
68
  # sort_help :manually
@@ -41,23 +73,49 @@ switch [:notes], default_value: true, negatable: true
41
73
  desc 'Send results report to STDOUT instead of STDERR'
42
74
  switch [:stdout], default_value: false, negatable: false
43
75
 
76
+ desc 'Use a pager when output is longer than screen'
77
+ switch %i[p pager], default_value: settings['paginate']
78
+
79
+ desc 'Answer yes/no menus with default option'
80
+ switch [:default], default_value: false
81
+
44
82
  desc 'Exclude auto tags and default tags'
45
- switch %i[x noauto], default_value: false
83
+ switch %i[x noauto], default_value: false, negatable: false
84
+
85
+ desc 'Colored output'
86
+ switch %i[color], default_value: true
46
87
 
47
- desc 'Use a specific configuration file'
48
- flag [:config_file], default_value: wwid.config_file
88
+ desc 'Silence info messages'
89
+ switch %i[q quiet], default_value: false, negatable: false
90
+
91
+ desc 'Verbose output'
92
+ switch %i[debug], default_value: false, negatable: false
93
+
94
+ desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead.'
95
+ flag [:config_file], default_value: config.config_file
49
96
 
50
97
  desc 'Specify a different doing_file'
51
98
  flag %i[f doing_file]
52
99
 
53
100
  desc 'Add an entry'
101
+ long_desc %(Record what you're starting now, or backdate the start time using natural language.
102
+
103
+ A parenthetical at the end of the entry will be converted to a note.
104
+
105
+ Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
54
106
  arg_name 'ENTRY'
55
107
  command %i[now next] do |c|
108
+ c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
109
+ c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
110
+ c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
111
+ c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
112
+ c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
113
+
56
114
  c.desc 'Section'
57
115
  c.arg_name 'NAME'
58
116
  c.flag %i[s section]
59
117
 
60
- c.desc "Edit entry with #{ENV['EDITOR']}"
118
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
61
119
  c.switch %i[e editor], negatable: false, default_value: false
62
120
 
63
121
  c.desc 'Backdate start time [4pm|20m|2h|yesterday noon]'
@@ -67,7 +125,7 @@ command %i[now next] do |c|
67
125
  c.desc 'Timed entry, marks last entry in section as @done'
68
126
  c.switch %i[f finish_last], negatable: false, default_value: false
69
127
 
70
- c.desc 'Note'
128
+ c.desc 'Include a note'
71
129
  c.arg_name 'TEXT'
72
130
  c.flag %i[n note]
73
131
 
@@ -77,9 +135,9 @@ command %i[now next] do |c|
77
135
 
78
136
  c.action do |_global_options, options, args|
79
137
  if options[:back]
80
- date = wwid.chronify(options[:back])
138
+ date = wwid.chronify(options[:back], guess: :begin)
81
139
 
82
- exit_now! 'Unable to parse date string' if date.nil?
140
+ raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') 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 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 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 EmptyInput, 'You must provide content when creating a new entry'
119
177
  end
120
178
  end
121
179
  end
122
180
 
181
+ desc 'Reset the start time of an entry'
182
+ command %i[reset begin] do |c|
183
+ c.desc 'Set the start date of an item to now'
184
+ c.arg_name 'NAME'
185
+ c.flag %i[s section], default_value: 'All'
186
+
187
+ c.desc 'Resume entry (remove @done)'
188
+ c.switch %i[r resume], default_value: true
189
+
190
+ c.desc 'Reset last entry matching tag'
191
+ c.arg_name 'TAG'
192
+ c.flag [:tag]
193
+
194
+ c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
195
+ c.arg_name 'QUERY'
196
+ c.flag [:search]
197
+
198
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
199
+ c.arg_name 'BOOLEAN'
200
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
201
+
202
+ c.desc 'Select from a menu of matching entries'
203
+ c.switch %i[i interactive], 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
225
+ end
226
+
227
+ unless last_entry
228
+ Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
229
+ return
230
+ end
231
+
232
+ wwid.reset_item(last_entry, resume: options[:resume])
233
+
234
+ # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
235
+
236
+ wwid.write(wwid.doing_file)
237
+ end
238
+ end
239
+
240
+
123
241
  desc 'Add a note to the last entry'
124
242
  long_desc %(
125
- If -r is provided with no other arguments, the last note is removed. If new content is specified through arguments or STDIN, any previous note will be replaced with the new one.
243
+ If -r is provided with no other arguments, the last note is removed.
244
+ If new content is specified through arguments or STDIN, any previous
245
+ note will be replaced with the new one.
126
246
 
127
247
  Use -e to load the last entry in a text editor where you can append a note.
128
248
  )
@@ -132,47 +252,77 @@ command :note do |c|
132
252
  c.arg_name 'NAME'
133
253
  c.flag %i[s section], default_value: 'All'
134
254
 
135
- c.desc "Edit entry with #{ENV['EDITOR']}"
255
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
136
256
  c.switch %i[e editor], negatable: false, default_value: false
137
257
 
138
258
  c.desc "Replace/Remove last entry's note (default append)"
139
259
  c.switch %i[r remove], negatable: false, default_value: false
140
260
 
261
+ c.desc 'Add/remove note from last entry matching tag'
262
+ c.arg_name 'TAG'
263
+ c.flag [:tag]
264
+
265
+ c.desc 'Add/remove note from last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
266
+ c.arg_name 'QUERY'
267
+ c.flag [:search]
268
+
269
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
270
+ c.arg_name 'BOOLEAN'
271
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
272
+
273
+ c.desc 'Select item for new note from a menu of matching entries'
274
+ c.switch %i[i interactive], 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 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 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,11 +334,11 @@ 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'
191
- c.switch %i[a archive], default_value: false
341
+ c.switch %i[a archive], negatable: false, default_value: false
192
342
 
193
343
  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
194
344
  c.arg_name 'DATE_STRING'
@@ -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 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 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], negatable: false
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 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], negatable: false, default_value: false
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 DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
510
+
511
+ raise 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 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 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 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 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 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 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 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 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 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 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 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 InvalidArgument, 'Only one argument allowed' if args.length > 1
793
+
794
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
554
795
 
555
- exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
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 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 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 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 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 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 InvalidArgument, 'Only one argument allowed' if args.length > 1
670
906
 
671
- exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
907
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
908
+
909
+ if options[:interactive]
910
+ count = 0
911
+ else
912
+ count = args[0] ? args[0].to_i : 1
913
+ end
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 MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
803
1049
 
804
- exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
1050
+ raise 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,108 @@ 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 'Mark last entry as flagged'
892
1121
  command [:mark, :flag] do |c|
1122
+ c.example 'doing flag', desc: 'Add @flagged to the last entry created'
1123
+ c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
1124
+ 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'
1125
+
893
1126
  c.desc 'Section'
894
- c.arg_name 'NAME'
895
- c.flag %i[s section]
1127
+ c.arg_name 'SECTION_NAME'
1128
+ c.flag %i[s section], default_value: 'All'
896
1129
 
897
- c.desc 'Remove mark'
1130
+ c.desc 'How many recent entries to tag (0 for all)'
1131
+ c.arg_name 'COUNT'
1132
+ c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
1133
+
1134
+ c.desc 'Don\'t ask permission to flag all entries when count is 0'
1135
+ c.switch %i[force], negatable: false, default_value: false
1136
+
1137
+ c.desc 'Include current date/time with tag'
1138
+ c.switch %i[d date], negatable: false, default_value: false
1139
+
1140
+ c.desc 'Remove flag'
898
1141
  c.switch %i[r remove], negatable: false, default_value: false
899
1142
 
900
- c.desc 'Mark last entry not marked @done'
1143
+ c.desc 'Flag last entry (or entries) not marked @done'
901
1144
  c.switch %i[u unfinished], negatable: false, default_value: false
902
1145
 
1146
+ c.desc 'Flag the last entry containing TAG.
1147
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
1148
+ c.arg_name 'TAG'
1149
+ c.flag [:tag]
1150
+
1151
+ 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")'
1152
+ c.arg_name 'QUERY'
1153
+ c.flag [:search]
1154
+
1155
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1156
+ c.arg_name 'BOOLEAN'
1157
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1158
+
1159
+ c.desc 'Select item(s) to flag from a menu of matching entries'
1160
+ c.switch %i[i interactive], negatable: false, default_value: false
1161
+
903
1162
  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
- })
1163
+ mark = settings['marker_tag'] || 'flagged'
1164
+
1165
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1166
+
1167
+ section = 'All'
1168
+
1169
+ if options[:section]
1170
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1171
+ end
1172
+
1173
+ if options[:tag].nil?
1174
+ search_tags = []
1175
+ else
1176
+ search_tags = options[:tag].to_tags
1177
+ end
1178
+
1179
+ if options[:interactive]
1180
+ count = 0
1181
+ options[:force] = true
1182
+ else
1183
+ count = options[:count].to_i
1184
+ end
1185
+
1186
+ if count.zero? && !options[:force]
1187
+ if options[:search]
1188
+ section_q = ' matching your search terms'
1189
+ elsif options[:tag]
1190
+ section_q = ' matching your tag search'
1191
+ elsif section == 'All'
1192
+ section_q = ''
1193
+ else
1194
+ section_q = " in section #{section}"
1195
+ end
1196
+
1197
+
1198
+ question = if options[:remove]
1199
+ "Are you sure you want to unflag all entries#{section_q}"
1200
+ else
1201
+ "Are you sure you want to flag all records#{section_q}"
1202
+ end
1203
+
1204
+ res = wwid.yn(question, default_response: false)
1205
+
1206
+ exit_now! 'Cancelled' unless res
1207
+ end
1208
+
1209
+ options[:count] = count
1210
+ options[:section] = section
1211
+ options[:tag] = search_tags
1212
+ options[:tags] = [mark]
1213
+ options[:tag_bool] = options[:bool].normalize_bool
1214
+
1215
+ wwid.tag_last(options)
911
1216
  end
912
1217
  end
913
1218
 
@@ -918,13 +1223,19 @@ long_desc %(
918
1223
  )
919
1224
  arg_name '[SECTION|@TAGS]'
920
1225
  command :show do |c|
1226
+ c.example 'doing show Currently', desc: 'Show entries in the Currently section'
1227
+ c.example 'doing show @project1', desc: 'Show entries tagged @project1'
1228
+ c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
1229
+ 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.'
1230
+ c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1231
+
921
1232
  c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
922
1233
  c.arg_name 'TAG'
923
1234
  c.flag [:tag]
924
1235
 
925
1236
  c.desc 'Tag boolean (AND,OR,NOT)'
926
1237
  c.arg_name 'BOOLEAN'
927
- c.flag %i[b bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'OR'
1238
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
928
1239
 
929
1240
  c.desc 'Max count to show'
930
1241
  c.arg_name 'MAX'
@@ -942,13 +1253,13 @@ command :show do |c|
942
1253
  c.arg_name 'DATE_STRING'
943
1254
  c.flag [:after]
944
1255
 
945
- c.desc 'Search filter, surround with slashes for regex (/query/)'
1256
+ c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
946
1257
  c.arg_name 'QUERY'
947
1258
  c.flag [:search]
948
1259
 
949
1260
  c.desc 'Sort order (asc/desc)'
950
1261
  c.arg_name 'ORDER'
951
- c.flag %i[s sort], must_match: /^[ad].*/i, default_value: 'ASC'
1262
+ c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
952
1263
 
953
1264
  c.desc %(
954
1265
  Date range to show, or a single day to filter date on.
@@ -966,21 +1277,26 @@ command :show do |c|
966
1277
 
967
1278
  c.desc 'Sort tags by (name|time)'
968
1279
  default = 'time'
969
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1280
+ default = settings['tag_sort'] || 'name'
970
1281
  c.arg_name 'KEY'
971
1282
  c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
972
1283
 
973
1284
  c.desc 'Tag sort direction (asc|desc)'
974
1285
  c.arg_name 'DIRECTION'
975
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1286
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
976
1287
 
977
1288
  c.desc 'Only show items with recorded time intervals'
978
1289
  c.switch [:only_timed], default_value: false, negatable: false
979
1290
 
980
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1291
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1292
+ c.switch %i[i interactive], negatable: false, default_value: false
1293
+
1294
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
981
1295
  c.arg_name 'FORMAT'
982
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
983
- c.action do |_global_options, options, args|
1296
+ c.flag %i[o output]
1297
+ c.action do |global_options, options, args|
1298
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1299
+
984
1300
  tag_filter = false
985
1301
  tags = []
986
1302
  if args.length.positive?
@@ -994,8 +1310,15 @@ command :show do |c|
994
1310
  when /^@/
995
1311
  section = 'All'
996
1312
  else
997
- section = wwid.guess_section(args[0])
998
- exit_now! "No such section: #{args[0]}" unless section
1313
+ begin
1314
+ section = wwid.guess_section(args[0])
1315
+ rescue WrongCommand => exception
1316
+ cmd = commands[:view]
1317
+ action = cmd.send(:get_action, nil)
1318
+ return action.call(global_options, options, args)
1319
+ end
1320
+
1321
+ raise InvalidSection, "No such section: #{args[0]}" unless section
999
1322
 
1000
1323
  args.shift
1001
1324
  end
@@ -1007,72 +1330,51 @@ command :show do |c|
1007
1330
  end
1008
1331
  end
1009
1332
  else
1010
- section = wwid.current_section
1333
+ section = settings['current_section']
1011
1334
  end
1012
1335
 
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
1336
+ tags.concat(options[:tag].to_tags) if options[:tag]
1024
1337
 
1025
1338
  unless tags.empty?
1026
1339
  tag_filter = {
1027
1340
  'tags' => tags,
1028
- 'bool' => options[:bool]
1341
+ 'bool' => options[:bool].normalize_bool
1029
1342
  }
1030
1343
  end
1031
1344
 
1032
1345
  if options[:from]
1346
+
1033
1347
  date_string = options[:from]
1034
1348
  if date_string =~ / (to|through|thru|(un)?til|-+) /
1035
1349
  dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
1036
- start = wwid.chronify(dates[0])
1037
- finish = wwid.chronify(dates[2])
1350
+ start = wwid.chronify(dates[0], guess: :begin)
1351
+ finish = wwid.chronify(dates[2], guess: :end)
1038
1352
  else
1039
- start = wwid.chronify(date_string)
1353
+ start = wwid.chronify(date_string, guess: :begin)
1040
1354
  finish = false
1041
1355
  end
1042
- exit_now! 'Unrecognized date string' unless start
1356
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1043
1357
  dates = [start, finish]
1044
1358
  end
1045
1359
 
1046
1360
  options[:times] = true if options[:totals]
1047
1361
 
1048
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1362
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1049
1363
 
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)
1364
+ opt = options.dup
1365
+
1366
+ opt[:sort_tags] = options[:tag_sort] =~ /^n/i
1367
+ opt[:count] = options[:count].to_i
1368
+ opt[:date_filter] = dates
1369
+ opt[:highlight] = true
1370
+ opt[:order] = options[:sort].normalize_order
1371
+ opt[:section] = section
1372
+ opt[:tag] = nil
1373
+ opt[:tag_filter] = tag_filter
1374
+ opt[:tag_order] = options[:tag_order].normalize_order
1375
+ opt[:tags_color] = tags_color
1376
+
1377
+ Doing::Pager.page wwid.list_section(opt)
1076
1378
  end
1077
1379
  end
1078
1380
 
@@ -1084,7 +1386,12 @@ long_desc <<~'EODESC'
1084
1386
  EODESC
1085
1387
 
1086
1388
  arg_name 'SEARCH_PATTERN'
1087
- command [:grep, :search] do |c|
1389
+ command %i[grep search] do |c|
1390
+ c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
1391
+ c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
1392
+ c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
1393
+ 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'
1394
+
1088
1395
  c.desc 'Section'
1089
1396
  c.arg_name 'NAME'
1090
1397
  c.flag %i[s section], default_value: 'All'
@@ -1097,9 +1404,9 @@ command [:grep, :search] do |c|
1097
1404
  c.arg_name 'DATE_STRING'
1098
1405
  c.flag [:after]
1099
1406
 
1100
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1407
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1101
1408
  c.arg_name 'FORMAT'
1102
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1409
+ c.flag %i[o output]
1103
1410
 
1104
1411
  c.desc 'Show time intervals on @done tasks'
1105
1412
  c.switch %i[t times], default_value: true, negatable: true
@@ -1109,36 +1416,31 @@ command [:grep, :search] do |c|
1109
1416
 
1110
1417
  c.desc 'Sort tags by (name|time)'
1111
1418
  default = 'time'
1112
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1419
+ default = settings['tag_sort'] || 'name'
1113
1420
  c.arg_name 'KEY'
1114
1421
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1115
1422
 
1116
1423
  c.desc 'Only show items with recorded time intervals'
1117
1424
  c.switch [:only_timed], default_value: false, negatable: false
1118
1425
 
1426
+ c.desc 'Display an interactive menu of results to perform further operations'
1427
+ c.switch %i[i interactive], default_value: false, negatable: false
1428
+
1119
1429
  c.action do |_global_options, options, args|
1120
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1430
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1121
1431
 
1122
- section = wwid.guess_section(options[:s]) if options[:s]
1432
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1433
+
1434
+ section = wwid.guess_section(options[:section]) if options[:section]
1123
1435
 
1124
1436
  options[:times] = true if options[:totals]
1125
1437
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1438
+ options[:highlight] = true
1439
+ options[:search] = args.join(' ')
1440
+ options[:section] = section
1441
+ options[:tags_color] = tags_color
1126
1442
 
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)
1443
+ Doing::Pager.page wwid.list_section(options)
1142
1444
  end
1143
1445
  end
1144
1446
 
@@ -1146,6 +1448,11 @@ desc 'List recent entries'
1146
1448
  default_value 10
1147
1449
  arg_name 'COUNT'
1148
1450
  command :recent do |c|
1451
+ c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections'
1452
+ c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections'
1453
+ c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section'
1454
+ c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on'
1455
+
1149
1456
  c.desc 'Section'
1150
1457
  c.arg_name 'NAME'
1151
1458
  c.flag %i[s section], default_value: 'All'
@@ -1158,32 +1465,42 @@ command :recent do |c|
1158
1465
 
1159
1466
  c.desc 'Sort tags by (name|time)'
1160
1467
  default = 'time'
1161
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1468
+ default = settings['tag_sort'] || 'name'
1162
1469
  c.arg_name 'KEY'
1163
1470
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1164
1471
 
1472
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1473
+ c.switch %i[i interactive], negatable: false, default_value: false
1474
+
1165
1475
  c.action do |global_options, options, args|
1166
1476
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
1167
1477
 
1168
1478
  unless global_options[:version]
1169
- if wwid.config['templates']['recent'].key?('count')
1170
- config_count = wwid.config['templates']['recent']['count'].to_i
1479
+ if settings['templates']['recent'].key?('count')
1480
+ config_count = settings['templates']['recent']['count'].to_i
1171
1481
  else
1172
1482
  config_count = 10
1173
1483
  end
1174
- count = args.empty? ? config_count : args[0].to_i
1484
+
1485
+ if options[:interactive]
1486
+ count = 0
1487
+ else
1488
+ count = args.empty? ? config_count : args[0].to_i
1489
+ end
1490
+
1175
1491
  options[:t] = true if options[:totals]
1176
1492
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1177
- tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
1493
+ tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1178
1494
 
1179
1495
  opts = {
1180
1496
  sort_tags: options[:sort_tags],
1181
1497
  tags_color: tags_color,
1182
1498
  times: options[:t],
1183
- totals: options[:totals]
1499
+ totals: options[:totals],
1500
+ interactive: options[:interactive]
1184
1501
  }
1185
1502
 
1186
- puts wwid.recent(count, section.cap_first, opts)
1503
+ Doing::Pager::page wwid.recent(count, section.cap_first, opts)
1187
1504
 
1188
1505
  end
1189
1506
  end
@@ -1191,6 +1508,11 @@ end
1191
1508
 
1192
1509
  desc 'List entries from today'
1193
1510
  command :today do |c|
1511
+ c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
1512
+ c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
1513
+ c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
1514
+ c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
1515
+
1194
1516
  c.desc 'Specify a section'
1195
1517
  c.arg_name 'NAME'
1196
1518
  c.flag %i[s section], default_value: 'All'
@@ -1203,13 +1525,13 @@ command :today do |c|
1203
1525
 
1204
1526
  c.desc 'Sort tags by (name|time)'
1205
1527
  default = 'time'
1206
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1528
+ default = settings['tag_sort'] || 'name'
1207
1529
  c.arg_name 'KEY'
1208
1530
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1209
1531
 
1210
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1532
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1211
1533
  c.arg_name 'FORMAT'
1212
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1534
+ c.flag %i[o output]
1213
1535
 
1214
1536
  c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
1215
1537
  c.arg_name 'TIME_STRING'
@@ -1220,6 +1542,8 @@ command :today do |c|
1220
1542
  c.flag [:after]
1221
1543
 
1222
1544
  c.action do |_global_options, options, _args|
1545
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1546
+
1223
1547
  options[:t] = true if options[:totals]
1224
1548
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1225
1549
  opt = {
@@ -1227,9 +1551,10 @@ command :today do |c|
1227
1551
  before: options[:before],
1228
1552
  section: options[:section],
1229
1553
  sort_tags: options[:sort_tags],
1230
- totals: options[:totals]
1554
+ totals: options[:totals],
1555
+ order: settings.dig('templates', 'today', 'order')
1231
1556
  }
1232
- puts wwid.today(options[:times], options[:output], opt).chomp
1557
+ Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
1233
1558
  end
1234
1559
  end
1235
1560
 
@@ -1239,6 +1564,10 @@ and "2d" would be interpreted as "two days ago." If you use "to" or "through" be
1239
1564
  it will create a range.)
1240
1565
  arg_name 'DATE_STRING'
1241
1566
  command :on do |c|
1567
+ c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
1568
+ c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
1569
+ c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
1570
+
1242
1571
  c.desc 'Section'
1243
1572
  c.arg_name 'NAME'
1244
1573
  c.flag %i[s section], default_value: 'All'
@@ -1251,38 +1580,40 @@ command :on do |c|
1251
1580
 
1252
1581
  c.desc 'Sort tags by (name|time)'
1253
1582
  default = 'time'
1254
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1583
+ default = settings['tag_sort'] || 'name'
1255
1584
  c.arg_name 'KEY'
1256
1585
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1257
1586
 
1258
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1587
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1259
1588
  c.arg_name 'FORMAT'
1260
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1589
+ c.flag %i[o output]
1261
1590
 
1262
1591
  c.action do |_global_options, options, args|
1263
- exit_now! 'Missing date argument' if args.empty?
1592
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1593
+
1594
+ raise MissingArgument, 'Missing date argument' if args.empty?
1264
1595
 
1265
1596
  date_string = args.join(' ')
1266
1597
 
1267
1598
  if date_string =~ / (to|through|thru) /
1268
1599
  dates = date_string.split(/ (to|through|thru) /)
1269
- start = wwid.chronify(dates[0])
1270
- finish = wwid.chronify(dates[2])
1600
+ start = wwid.chronify(dates[0], guess: :begin)
1601
+ finish = wwid.chronify(dates[2], guess: :end)
1271
1602
  else
1272
- start = wwid.chronify(date_string)
1603
+ start = wwid.chronify(date_string, guess: :begin)
1273
1604
  finish = false
1274
1605
  end
1275
1606
 
1276
- exit_now! 'Unrecognized date string' unless start
1607
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1277
1608
 
1278
1609
  message = "Date interpreted as #{start}"
1279
1610
  message += " to #{finish}" if finish
1280
- wwid.results.push(message)
1611
+ Doing.logger.debug(message)
1281
1612
 
1282
1613
  options[:t] = true if options[:totals]
1283
1614
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1284
1615
 
1285
- puts wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1616
+ Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1286
1617
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1287
1618
  end
1288
1619
  end
@@ -1292,6 +1623,9 @@ long_desc %(Date argument can be natural language and are always interpreted as
1292
1623
  and "2d" would be interpreted as "two days ago.")
1293
1624
  arg_name 'DATE_STRING'
1294
1625
  command :since do |c|
1626
+ c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
1627
+ c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
1628
+
1295
1629
  c.desc 'Section'
1296
1630
  c.arg_name 'NAME'
1297
1631
  c.flag %i[s section], default_value: 'All'
@@ -1304,47 +1638,52 @@ command :since do |c|
1304
1638
 
1305
1639
  c.desc 'Sort tags by (name|time)'
1306
1640
  default = 'time'
1307
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1641
+ default = settings['tag_sort'] || 'name'
1308
1642
  c.arg_name 'KEY'
1309
1643
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1310
1644
 
1311
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1645
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1312
1646
  c.arg_name 'FORMAT'
1313
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1647
+ c.flag %i[o output]
1314
1648
 
1315
1649
  c.action do |_global_options, options, args|
1316
- exit_now! 'Missing date argument' if args.empty?
1650
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1651
+
1652
+ raise MissingArgument, 'Missing date argument' if args.empty?
1317
1653
 
1318
1654
  date_string = args.join(' ')
1319
1655
 
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/
1656
+ date_string.sub!(/(day) (\d)/, '\1 at \2')
1657
+ date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
1322
1658
 
1323
- start = wwid.chronify(date_string)
1659
+ start = wwid.chronify(date_string, guess: :begin)
1324
1660
  finish = Time.now
1325
1661
 
1326
- exit_now! 'Unrecognized date string' unless start
1662
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1327
1663
 
1328
- message = "Date interpreted as #{start} through the current time"
1329
- wwid.results.push(message)
1664
+ Doing.logger.debug("Date interpreted as #{start} through the current time")
1330
1665
 
1331
1666
  options[:t] = true if options[:totals]
1332
1667
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1333
1668
 
1334
- puts wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1669
+ Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1335
1670
  { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1336
1671
  end
1337
1672
  end
1338
1673
 
1339
1674
  desc 'List entries from yesterday'
1340
1675
  command :yesterday do |c|
1676
+ c.example 'doing yesterday', desc: 'List all entries from the previous day'
1677
+ c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
1678
+ c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers'
1679
+
1341
1680
  c.desc 'Specify a section'
1342
1681
  c.arg_name 'NAME'
1343
1682
  c.flag %i[s section], default_value: 'All'
1344
1683
 
1345
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1684
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1346
1685
  c.arg_name 'FORMAT'
1347
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1686
+ c.flag %i[o output]
1348
1687
 
1349
1688
  c.desc 'Show time intervals on @done tasks'
1350
1689
  c.switch %i[t times], default_value: true, negatable: true
@@ -1353,8 +1692,7 @@ command :yesterday do |c|
1353
1692
  c.switch [:totals], default_value: false, negatable: false
1354
1693
 
1355
1694
  c.desc 'Sort tags by (name|time)'
1356
- default = 'time'
1357
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1695
+ default = settings['tag_sort'] || 'name'
1358
1696
  c.arg_name 'KEY'
1359
1697
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1360
1698
 
@@ -1368,33 +1706,40 @@ command :yesterday do |c|
1368
1706
 
1369
1707
  c.desc 'Tag sort direction (asc|desc)'
1370
1708
  c.arg_name 'DIRECTION'
1371
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1709
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1372
1710
 
1373
1711
  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
1712
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1713
+
1379
1714
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1715
+
1380
1716
  opt = {
1381
1717
  after: options[:after],
1382
1718
  before: options[:before],
1383
1719
  sort_tags: options[:sort_tags],
1384
- tag_order: options[:tag_order],
1385
- totals: options[:totals]
1720
+ tag_order: options[:tag_order].normalize_order,
1721
+ totals: options[:totals],
1722
+ order: settings.dig('templates', 'today', 'order')
1386
1723
  }
1387
- puts wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
1724
+ Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
1388
1725
  end
1389
1726
  end
1390
1727
 
1391
1728
  desc 'Show the last entry, optionally edit'
1392
1729
  command :last do |c|
1730
+ c.example 'doing last', desc: 'Show the most recent entry in all sections'
1731
+ c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
1732
+ c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
1733
+ c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
1734
+ c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
1735
+ c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
1736
+ c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
1737
+
1393
1738
  c.desc 'Specify a section'
1394
1739
  c.arg_name 'NAME'
1395
1740
  c.flag %i[s section], default_value: 'All'
1396
1741
 
1397
- c.desc "Edit entry with #{ENV['EDITOR']}"
1742
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
1398
1743
  c.switch %i[e editor], negatable: false, default_value: false
1399
1744
 
1400
1745
  c.desc 'Tag filter, combine multiple tags with a comma.'
@@ -1403,19 +1748,19 @@ command :last do |c|
1403
1748
 
1404
1749
  c.desc 'Tag boolean'
1405
1750
  c.arg_name 'BOOLEAN'
1406
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
1751
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1407
1752
 
1408
- c.desc 'Search filter, surround with slashes for regex (/query/)'
1753
+ c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1409
1754
  c.arg_name 'QUERY'
1410
1755
  c.flag [:search]
1411
1756
 
1412
- c.action do |_global_options, options, _args|
1413
- exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search]
1757
+ c.action do |global_options, options, _args|
1758
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1414
1759
 
1415
1760
  if options[:tag].nil?
1416
1761
  tags = []
1417
1762
  else
1418
- tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
1763
+ tags = options[:tag].to_tags
1419
1764
  options[:bool] = case options[:bool]
1420
1765
  when /(any|or)/i
1421
1766
  :or
@@ -1430,7 +1775,7 @@ command :last do |c|
1430
1775
  if options[:editor]
1431
1776
  wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
1432
1777
  else
1433
- puts wwid.last(times: true, section: options[:s],
1778
+ Doing::Pager::page wwid.last(times: true, section: options[:s],
1434
1779
  options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
1435
1780
  end
1436
1781
  end
@@ -1439,7 +1784,7 @@ end
1439
1784
  desc 'List sections'
1440
1785
  command :sections do |c|
1441
1786
  c.desc 'List in single column'
1442
- c.switch %i[c column], default_value: false
1787
+ c.switch %i[c column], negatable: false, default_value: false
1443
1788
 
1444
1789
  c.action do |_global_options, options, _args|
1445
1790
  joiner = options[:c] ? "\n" : "\t"
@@ -1452,17 +1797,19 @@ command :choose do |c|
1452
1797
  c.action do |_global_options, _options, _args|
1453
1798
  section = wwid.choose_section
1454
1799
 
1455
- puts wwid.list_section({ section: section.cap_first, count: 0 }) if section
1800
+ Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
1456
1801
  end
1457
1802
  end
1458
1803
 
1459
1804
  desc 'Add a new section to the "doing" file'
1460
1805
  arg_name 'SECTION_NAME'
1461
1806
  command :add_section do |c|
1807
+ c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
1808
+
1462
1809
  c.action do |_global_options, _options, args|
1463
- exit_now! "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1810
+ raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1464
1811
 
1465
- wwid.add_section(args[0].cap_first)
1812
+ wwid.add_section(args.join(' ').cap_first)
1466
1813
  wwid.write(wwid.doing_file)
1467
1814
  end
1468
1815
  end
@@ -1470,18 +1817,80 @@ end
1470
1817
  desc 'List available color variables for configuration templates and views'
1471
1818
  command :colors do |c|
1472
1819
  c.action do |_global_options, _options, _args|
1473
- clrs = wwid.colors
1474
1820
  bgs = []
1475
1821
  fgs = []
1476
- clrs.each do |k, v|
1477
- if k =~ /bg/
1478
- bgs.push("#{v} #{clrs['default']} <-- #{k}")
1822
+ colors::attributes.each do |color|
1823
+ if color.to_s =~ /bg/
1824
+ bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}")
1479
1825
  else
1480
- fgs.push("#{v}XXXX#{clrs['default']} <-- #{k}")
1826
+ fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
1827
+ end
1828
+ end
1829
+ out = []
1830
+ out << fgs.join("\n")
1831
+ out << bgs.join("\n")
1832
+ Doing::Pager.page out.join("\n")
1833
+ end
1834
+ end
1835
+
1836
+ desc 'List installed plugins'
1837
+ long_desc %(Lists available plugins, including user-installed plugins.
1838
+
1839
+ Export plugins are available with the `--output` flag on commands that support it.
1840
+
1841
+ Import plugins are available using `doing import --type PLUGIN`.
1842
+ )
1843
+ command :plugins do |c|
1844
+ c.example 'doing plugins', desc: 'List all plugins'
1845
+ c.example 'doing plugins -t import', desc: 'List all import plugins'
1846
+
1847
+ c.desc 'List plugins of type (import, export)'
1848
+ c.arg_name 'TYPE'
1849
+ c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
1850
+
1851
+ c.desc 'List in single column for completion'
1852
+ c.switch %i[c column], negatable: false, default_value: false
1853
+
1854
+ c.action do |_global_options, options, _args|
1855
+ Doing::Plugins.list_plugins(options)
1856
+ end
1857
+ end
1858
+
1859
+ desc 'Generate shell completion scripts'
1860
+ command :completion do |c|
1861
+ c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
1862
+ c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
1863
+ c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
1864
+ c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
1865
+
1866
+ c.desc 'Shell to generate for (bash, zsh, fish)'
1867
+ c.arg_name 'SHELL'
1868
+ c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'
1869
+
1870
+ c.desc 'File to write output to'
1871
+ c.arg_name 'PATH'
1872
+ c.flag %i[f file], default_value: 'stdout'
1873
+
1874
+ c.action do |_global_options, options, _args|
1875
+ script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
1876
+
1877
+ case options[:type]
1878
+ when /^b/
1879
+ result = `ruby #{File.join(script_dir, 'generate_bash_completions.rb')}`
1880
+ when /^z/
1881
+ result = `ruby #{File.join(script_dir, 'generate_zsh_completions.rb')}`
1882
+ when /^f/
1883
+ result = `ruby #{File.join(script_dir, 'generate_fish_completions.rb')}`
1884
+ end
1885
+
1886
+ if options[:file] =~ /^stdout$/i
1887
+ $stdout.puts result
1888
+ else
1889
+ File.open(File.expand_path(options[:file]), 'w') do |f|
1890
+ f.puts result
1481
1891
  end
1892
+ Doing.logger.warn('File written:', "#{options[:type]} completions written to #{options[:file]}")
1482
1893
  end
1483
- puts fgs.join("\n")
1484
- puts bgs.join("\n")
1485
1894
  end
1486
1895
  end
1487
1896
 
@@ -1489,6 +1898,9 @@ desc 'Display a user-created view'
1489
1898
  long_desc 'Command line options override view configuration'
1490
1899
  arg_name 'VIEW_NAME'
1491
1900
  command :view do |c|
1901
+ c.example 'doing view color', desc: 'Display entries according to config for view "color"'
1902
+ c.example 'doing view color --section Archive --count 10', desc: 'Display view "color", overriding some configured settings'
1903
+
1492
1904
  c.desc 'Section'
1493
1905
  c.arg_name 'NAME'
1494
1906
  c.flag %i[s section]
@@ -1497,9 +1909,9 @@ command :view do |c|
1497
1909
  c.arg_name 'COUNT'
1498
1910
  c.flag %i[c count], must_match: /^\d+$/, type: Integer
1499
1911
 
1500
- c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1912
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1501
1913
  c.arg_name 'FORMAT'
1502
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1914
+ c.flag %i[o output]
1503
1915
 
1504
1916
  c.desc 'Show time intervals on @done tasks'
1505
1917
  c.switch %i[t times], default_value: true, negatable: true
@@ -1516,9 +1928,9 @@ command :view do |c|
1516
1928
 
1517
1929
  c.desc 'Tag boolean (AND,OR,NOT)'
1518
1930
  c.arg_name 'BOOLEAN'
1519
- c.flag %i[b bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'OR'
1931
+ c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
1520
1932
 
1521
- c.desc 'Search filter, surround with slashes for regex (/query/)'
1933
+ c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1522
1934
  c.arg_name 'QUERY'
1523
1935
  c.flag [:search]
1524
1936
 
@@ -1528,7 +1940,7 @@ command :view do |c|
1528
1940
 
1529
1941
  c.desc 'Tag sort direction (asc|desc)'
1530
1942
  c.arg_name 'DIRECTION'
1531
- c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1943
+ c.flag [:tag_order], must_match: REGEX_SORT_ORDER
1532
1944
 
1533
1945
  c.desc 'View entries older than date'
1534
1946
  c.arg_name 'DATE_STRING'
@@ -1541,23 +1953,36 @@ command :view do |c|
1541
1953
  c.desc 'Only show items with recorded time intervals (override view settings)'
1542
1954
  c.switch [:only_timed], default_value: false, negatable: false
1543
1955
 
1544
- c.action do |_global_options, options, args|
1545
- exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search]
1956
+ c.desc 'Select from a menu of matching entries to perform additional operations'
1957
+ c.switch %i[i interactive], negatable: false, default_value: false
1958
+
1959
+ c.action do |global_options, options, args|
1960
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1961
+
1962
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1546
1963
 
1547
1964
  title = if args.empty?
1548
1965
  wwid.choose_view
1549
1966
  else
1550
- wwid.guess_view(args[0])
1967
+ begin
1968
+ wwid.guess_view(args[0])
1969
+ rescue WrongCommand => exception
1970
+ cmd = commands[:show]
1971
+ options[:sort] = 'asc'
1972
+ action = cmd.send(:get_action, nil)
1973
+ return action.call(global_options, options, args)
1974
+ end
1551
1975
  end
1552
1976
 
1553
1977
  if options[:section]
1554
1978
  section = wwid.guess_section(options[:section]) || options[:section].cap_first
1555
1979
  else
1556
- section = wwid.config['current_section']
1980
+ section = settings['current_section']
1557
1981
  end
1558
1982
 
1559
1983
  view = wwid.get_view(title)
1560
1984
  if view
1985
+ page_title = view.key?('title') ? view['title'] : title.cap_first
1561
1986
  only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
1562
1987
  true
1563
1988
  else
@@ -1565,7 +1990,7 @@ command :view do |c|
1565
1990
  end
1566
1991
 
1567
1992
  template = view.key?('template') ? view['template'] : nil
1568
- format = view.key?('date_format') ? view['date_format'] : nil
1993
+ date_format = view.key?('date_format') ? view['date_format'] : nil
1569
1994
  tags_color = view.key?('tags_color') ? view['tags_color'] : nil
1570
1995
  tag_filter = false
1571
1996
  if options[:tag]
@@ -1583,7 +2008,7 @@ command :view do |c|
1583
2008
  end
1584
2009
 
1585
2010
  # 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'
2011
+ options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
1587
2012
 
1588
2013
  count = if options[:c]
1589
2014
  options[:c]
@@ -1593,18 +2018,23 @@ command :view do |c|
1593
2018
  section = if options[:s]
1594
2019
  section
1595
2020
  else
1596
- view.key?('section') ? view['section'] : wwid.current_section
2021
+ view.key?('section') ? view['section'] : settings['current_section']
1597
2022
  end
1598
- order = view.key?('order') ? view['order'] : 'asc'
2023
+ order = view.key?('order') ? view['order'].normalize_order : 'asc'
1599
2024
 
1600
2025
  totals = if options[:totals]
1601
2026
  true
1602
2027
  else
1603
2028
  view.key?('totals') ? view['totals'] : false
1604
2029
  end
2030
+ tag_order = if options[:tag_order]
2031
+ options[:tag_order].normalize_order
2032
+ else
2033
+ view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
2034
+ end
1605
2035
 
1606
2036
  options[:t] = true if totals
1607
- options[:output]&.downcase!
2037
+ output_format = options[:output]&.downcase || 'template'
1608
2038
 
1609
2039
  options[:sort_tags] = if options[:tag_sort]
1610
2040
  options[:tag_sort] =~ /^n/i ? true : false
@@ -1613,40 +2043,50 @@ command :view do |c|
1613
2043
  else
1614
2044
  false
1615
2045
  end
2046
+ if view.key?('after') && !options[:after]
2047
+ options[:after] = view['after']
2048
+ end
1616
2049
 
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
2050
+ if view.key?('before') && !options[:before]
2051
+ options[:before] = view['before']
2052
+ end
1624
2053
 
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
- }
2054
+ if view.key?('from')
2055
+ date_string = view['from']
2056
+ if date_string =~ / (to|through|thru|(un)?til|-+) /
2057
+ dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2058
+ start = wwid.chronify(dates[0], guess: :begin)
2059
+ finish = wwid.chronify(dates[2], guess: :end)
2060
+ else
2061
+ start = wwid.chronify(date_string, guess: :begin)
2062
+ finish = false
2063
+ end
2064
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2065
+ dates = [start, finish]
2066
+ end
1644
2067
 
1645
- puts wwid.list_section(opts)
2068
+ opts = options
2069
+ opts[:output] = output_format
2070
+ opts[:count] = count
2071
+ opts[:format] = date_format
2072
+ opts[:highlight] = options[:color]
2073
+ opts[:only_timed] = only_timed
2074
+ opts[:order] = order
2075
+ opts[:section] = section
2076
+ opts[:tag_filter] = tag_filter
2077
+ opts[:tag_order] = tag_order
2078
+ opts[:tags_color] = tags_color
2079
+ opts[:template] = template
2080
+ opts[:totals] = totals
2081
+ opts[:page_title] = page_title
2082
+ opts[:date_filter] = dates
2083
+ opts[:output] = options[:interactive] ? nil : options[:output]
2084
+
2085
+ Doing::Pager.page wwid.list_section(opts)
1646
2086
  elsif title.instance_of?(FalseClass)
1647
- exit_now! 'Cancelled'
2087
+ raise UserCancelled, 'Cancelled' unless res
1648
2088
  else
1649
- exit_now! "View #{title} not found in config"
2089
+ raise InvalidView, "View #{title} not found in config"
1650
2090
  end
1651
2091
  end
1652
2092
  end
@@ -1663,9 +2103,18 @@ command :views do |c|
1663
2103
  end
1664
2104
 
1665
2105
  desc 'Move entries between sections'
1666
- arg_name 'SECTION_NAME'
1667
- default_value wwid.current_section
1668
- command :archive do |c|
2106
+ long_desc %(Argument can be a section name to move all entries from a section,
2107
+ or start with an "@" to move entries matching a tag.
2108
+
2109
+ Default with no argument moves items from the "#{settings['current_section']}" section to Archive.)
2110
+ arg_name 'SECTION_OR_TAG'
2111
+ default_value settings['current_section']
2112
+ command %i[archive move] do |c|
2113
+ c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section'
2114
+ c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive'
2115
+ c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section'
2116
+ c.example 'doing move Later --tag project1 --to Currently', desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)'
2117
+
1669
2118
  c.desc 'How many items to keep (ignored if archiving by tag or search)'
1670
2119
  c.arg_name 'X'
1671
2120
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer
@@ -1683,7 +2132,7 @@ command :archive do |c|
1683
2132
 
1684
2133
  c.desc 'Tag boolean (AND|OR|NOT)'
1685
2134
  c.arg_name 'BOOLEAN'
1686
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
2135
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1687
2136
 
1688
2137
  c.desc 'Search filter'
1689
2138
  c.arg_name 'QUERY'
@@ -1696,7 +2145,7 @@ command :archive do |c|
1696
2145
 
1697
2146
  c.action do |_global_options, options, args|
1698
2147
  if args.empty?
1699
- section = wwid.current_section
2148
+ section = settings['current_section']
1700
2149
  tags = []
1701
2150
  elsif args[0] =~ /^all/i
1702
2151
  section = 'all'
@@ -1708,34 +2157,25 @@ command :archive do |c|
1708
2157
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
1709
2158
  end
1710
2159
 
1711
- exit_now! '--keep and --count can\'t be used together' if options[:keep] && options[:count]
2160
+ raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
1712
2161
 
1713
- tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
2162
+ tags.concat(options[:tag].to_tags) if options[:tag]
2163
+
2164
+ opts = options
2165
+ opts[:bool] = options[:bool].normalize_bool
2166
+ opts[:destination] = options[:to]
2167
+ opts[:tags] = tags
1714
2168
 
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
2169
  wwid.archive(section, opts)
1734
2170
  end
1735
2171
  end
1736
2172
 
1737
2173
  desc 'Move entries to archive file'
1738
2174
  command :rotate do |c|
2175
+ c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
2176
+ 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'
2177
+ c.example 'doing rotate --tag project1,done --bool AND', desc: 'Move entries tagged @project1 and @done to a secondary file'
2178
+
1739
2179
  c.desc 'How many items to keep in each section (most recent)'
1740
2180
  c.arg_name 'X'
1741
2181
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer
@@ -1750,7 +2190,7 @@ command :rotate do |c|
1750
2190
 
1751
2191
  c.desc 'Tag boolean (AND|OR|NOT)'
1752
2192
  c.arg_name 'BOOLEAN'
1753
- c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
2193
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1754
2194
 
1755
2195
  c.desc 'Search filter'
1756
2196
  c.arg_name 'QUERY'
@@ -1766,23 +2206,14 @@ command :rotate do |c|
1766
2206
  options[:section] = wwid.guess_section(options[:section])
1767
2207
  end
1768
2208
 
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
2209
+ options[:bool] = options[:bool].normalize_bool
1779
2210
 
1780
2211
  wwid.rotate(options)
1781
2212
  end
1782
2213
  end
1783
2214
 
1784
2215
  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'})"
2216
+ 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
2217
  command :open do |c|
1787
2218
  if `uname` =~ /Darwin/
1788
2219
  c.desc 'Open with app name'
@@ -1793,8 +2224,6 @@ command :open do |c|
1793
2224
  c.arg_name 'BUNDLE_ID'
1794
2225
  c.flag %i[b bundle_id]
1795
2226
  end
1796
- c.desc "Open with $EDITOR (#{ENV['EDITOR']})"
1797
- c.switch %i[e editor], negatable: false, default_value: false
1798
2227
 
1799
2228
  c.action do |_global_options, options, _args|
1800
2229
  params = options.dup
@@ -1806,30 +2235,54 @@ command :open do |c|
1806
2235
  system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
1807
2236
  elsif options[:bundle_id]
1808
2237
  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)}")
2238
+ elsif Doing::Util.find_default_editor('doing_file')
2239
+ editor = Doing::Util.find_default_editor('doing_file')
2240
+ if Doing::Util.exec_available(editor)
2241
+ system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
2242
+ else
2243
+ system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
2244
+ end
1815
2245
  else
1816
2246
  system %(open "#{File.expand_path(wwid.doing_file)}")
1817
2247
  end
1818
-
1819
2248
  else
1820
- exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
2249
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1821
2250
 
1822
- system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
2251
+ system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
1823
2252
  end
1824
2253
  end
1825
2254
  end
1826
2255
 
1827
- desc 'Edit the configuration file'
2256
+ desc 'Edit the configuration file or output a value from it'
2257
+ long_desc %(Run without arguments, `doing config` opens your `.doingrc` in an editor.
2258
+ If local configurations are found in the path between the current directory
2259
+ and `~/.doingrc`, a menu will allow you to select which to open in the editor.
2260
+
2261
+ It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
2262
+
2263
+ Use `doing config -d` to output the configuration to the terminal, and
2264
+ provide a dot-separated key path to get a specific value. Shows the current value
2265
+ including keys/overrides set by local configs.)
2266
+ arg_name 'KEY_PATH'
1828
2267
  command :config do |c|
2268
+ c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
2269
+ c.example 'doing config -d doing_file', desc: 'Output the value of a config key as YAML'
2270
+ c.example 'doing config -d plugins.say.say_voice -o json', desc: 'Output the value of a key path as JSON'
2271
+
1829
2272
  c.desc 'Editor to use'
1830
2273
  c.arg_name 'EDITOR'
1831
2274
  c.flag %i[e editor], default_value: nil
1832
2275
 
2276
+ 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.'
2277
+ c.switch %i[d dump], negatable: false
2278
+
2279
+ c.desc 'Format for --dump (json|yaml|raw)'
2280
+ c.arg_name 'FORMAT'
2281
+ c.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
2282
+
2283
+ c.desc 'Update config file with missing configuration options'
2284
+ c.switch %i[u update], default_value: false, negatable: false
2285
+
1833
2286
  if `uname` =~ /Darwin/
1834
2287
  c.desc 'Application to use'
1835
2288
  c.arg_name 'APP_NAME'
@@ -1839,31 +2292,82 @@ command :config do |c|
1839
2292
  c.arg_name 'BUNDLE_ID'
1840
2293
  c.flag [:b]
1841
2294
 
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'})"
2295
+ 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
2296
  c.switch [:x]
1844
2297
  end
1845
2298
 
1846
- c.action do |_global_options, options, _args|
2299
+ c.action do |_global_options, options, args|
2300
+ if options[:update]
2301
+ config.configure({rewrite: true, ignore_local: true})
2302
+ return
2303
+ end
2304
+
2305
+ if options[:dump]
2306
+ keypath = args.join('.')
2307
+ cfg = config.value_for_key(keypath)
2308
+
2309
+ if cfg
2310
+ $stdout.puts case options[:output]
2311
+ when /^j/
2312
+ JSON.pretty_generate(cfg)
2313
+ when /^r/
2314
+ cfg
2315
+ else
2316
+ YAML.dump(cfg)
2317
+ end
2318
+ else
2319
+ Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
2320
+ end
2321
+ Doing.logger.output_results
2322
+ return
2323
+ end
2324
+
2325
+ if config.additional_configs.count.positive?
2326
+ choices = [config.config_file]
2327
+ choices.concat(config.additional_configs)
2328
+ res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
2329
+
2330
+ raise UserCancelled, 'Cancelled' unless res
2331
+
2332
+ config_file = res.strip || config.config_file
2333
+ else
2334
+ config_file = config.config_file
2335
+ end
2336
+
1847
2337
  if `uname` =~ /Darwin/
1848
2338
  if options[:x]
1849
- `open -a "#{wwid.config['config_editor_app']}" "#{wwid.config_file}"`
2339
+ editor = Doing::Util.find_default_editor('config')
2340
+ if editor
2341
+ if Doing::Util.exec_available(editor)
2342
+ system %(#{editor} "#{config_file}")
2343
+ else
2344
+ `open -a "#{editor}" "#{config_file}"`
2345
+ end
2346
+ else
2347
+ raise InvalidArgument, 'No viable editor found in config or environment.'
2348
+ end
1850
2349
  elsif options[:a] || options[:b]
1851
2350
  if options[:a]
1852
- `open -a "#{options[:a]}" "#{wwid.config_file}"`
2351
+ `open -a "#{options[:a]}" "#{config_file}"`
1853
2352
  elsif options[:b]
1854
- `open -b #{options[:b]} "#{wwid.config_file}"`
2353
+ `open -b #{options[:b]} "#{config_file}"`
1855
2354
  end
1856
2355
  else
1857
- exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
2356
+ editor = options[:e] || Doing::Util.find_default_editor('config')
1858
2357
 
1859
- editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1860
- system %(#{editor} "#{wwid.config_file}")
2358
+ raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2359
+
2360
+ if Doing::Util.exec_available(editor)
2361
+ system %(#{editor} "#{config_file}")
2362
+ else
2363
+ `open -a "#{editor}" "#{config_file}"`
2364
+ end
1861
2365
  end
1862
2366
  else
1863
- exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
2367
+ editor = options[:e] || Doing::Util.default_editor
2368
+ raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
1864
2369
 
1865
- editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1866
- system %(#{editor} "#{wwid.config_file}")
2370
+ system %(#{editor} "#{config_file}")
1867
2371
  end
1868
2372
  end
1869
2373
  end
@@ -1881,12 +2385,19 @@ command :undo do |c|
1881
2385
  end
1882
2386
 
1883
2387
  desc 'Import entries from an external source'
1884
- long_desc 'Imports entries from other sources. Currently only handles JSON reports exported from Timing.app.'
2388
+ long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
1885
2389
  arg_name 'PATH'
1886
2390
  command :import do |c|
1887
- c.desc 'Import type'
2391
+ c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
1888
2392
  c.arg_name 'TYPE'
1889
- c.flag :type, default_value: 'timing'
2393
+ c.flag :type, default_value: 'doing'
2394
+
2395
+ c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2396
+ c.arg_name 'QUERY'
2397
+ c.flag [:search]
2398
+
2399
+ c.desc 'Only import items with recorded time intervals'
2400
+ c.switch [:only_timed], default_value: false, negatable: false
1890
2401
 
1891
2402
  c.desc 'Target section'
1892
2403
  c.arg_name 'NAME'
@@ -1903,53 +2414,67 @@ command :import do |c|
1903
2414
  c.arg_name 'PREFIX'
1904
2415
  c.flag :prefix
1905
2416
 
2417
+ c.desc 'Import entries older than date'
2418
+ c.arg_name 'DATE_STRING'
2419
+ c.flag [:before]
2420
+
2421
+ c.desc 'Import entries newer than date'
2422
+ c.arg_name 'DATE_STRING'
2423
+ c.flag [:after]
2424
+
2425
+ c.desc %(
2426
+ Date range to import. Date range argument should be quoted. Date specifications can be natural language.
2427
+ To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
2428
+ Has no effect unless the import plugin has implemented date range filtering.
2429
+ )
2430
+ c.arg_name 'DATE_OR_RANGE'
2431
+ c.flag %i[f from]
2432
+
1906
2433
  c.desc 'Allow entries that overlap existing times'
1907
2434
  c.switch [:overlap], negatable: true
1908
2435
 
1909
2436
  c.action do |_global_options, options, args|
1910
2437
 
1911
2438
  if options[:section]
1912
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
1913
- else
1914
- section = wwid.config['current_section']
2439
+ options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
1915
2440
  end
1916
2441
 
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)
2442
+ if options[:from]
2443
+ date_string = options[:from]
2444
+ if date_string =~ / (to|through|thru|(un)?til|-+) /
2445
+ dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
2446
+ start = wwid.chronify(dates[0], guess: :begin)
2447
+ finish = wwid.chronify(dates[2], guess: :end)
2448
+ else
2449
+ start = wwid.chronify(date_string, guess: :begin)
2450
+ finish = false
1928
2451
  end
2452
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2453
+ dates = [start, finish]
2454
+ end
2455
+
2456
+ if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
2457
+ options[:no_overlap] = !options[:overlap]
2458
+ options[:date_filter] = dates
2459
+ wwid.import(args, options)
2460
+ wwid.write(wwid.doing_file)
1929
2461
  else
1930
- exit_now! 'Invalid import type'
2462
+ raise InvalidPluginType, "Invalid import type: #{options[:type]}"
1931
2463
  end
1932
2464
  end
1933
2465
  end
1934
2466
 
1935
2467
  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
1941
-
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]
2468
+ # global[:pager] ||= settings['paginate']
1949
2469
 
1950
- wwid.config[:include_notes] = false unless global[:notes]
2470
+ Doing::Pager.paginate = global[:pager]
1951
2471
 
1952
2472
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
2473
+ unless STDOUT.isatty
2474
+ Doing::Color::coloring = global[:pager] ? global[:color] : false
2475
+ else
2476
+ Doing::Color::coloring = global[:color]
2477
+ end
1953
2478
 
1954
2479
  # Return true to proceed; false to abort and not call the
1955
2480
  # chosen command
@@ -1958,20 +2483,56 @@ pre do |global, _command, _options, _args|
1958
2483
  true
1959
2484
  end
1960
2485
 
2486
+ on_error do |exception|
2487
+ if exception.kind_of?(SystemExit)
2488
+ false
2489
+ else
2490
+ # Doing.logger.error('Fatal:', exception)
2491
+ Doing.logger.output_results
2492
+ true
2493
+ end
2494
+ end
2495
+
1961
2496
  post do |global, _command, _options, _args|
1962
2497
  # Use skips_post before a command to skip this
1963
2498
  # block on that command only
2499
+ Doing.logger.output_results
2500
+ end
2501
+
2502
+ around do |global, command, options, arguments, code|
2503
+ # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
2504
+ Doing.logger.adjust_verbosity(global)
1964
2505
 
1965
2506
  if global[:stdout]
1966
- $stdout.print wwid.results.join("\n")
2507
+ Doing.logger.logdev = $stdout
2508
+ end
2509
+
2510
+ wwid.default_option = global[:default]
2511
+
2512
+ if global[:config_file] && global[:config_file] != config.config_file
2513
+ 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))
2514
+ 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))
2515
+
2516
+ cf = File.expand_path(global[:config_file])
2517
+ raise MissingConfigFile, "Config file not found (#{global[:config_file]})" unless File.exist?(cf)
2518
+
2519
+ config.config_file = cf
2520
+ settings = config.configure({ ignore_local: true })
2521
+ end
2522
+
2523
+ if global[:doing_file]
2524
+ wwid.init_doing_file(global[:doing_file])
1967
2525
  else
1968
- warn wwid.results.join("\n")
2526
+ wwid.init_doing_file
1969
2527
  end
1970
- end
1971
2528
 
1972
- on_error do |_exception|
1973
- # puts exception.message
1974
- true
2529
+ wwid.auto_tag = !global[:noauto]
2530
+
2531
+ settings[:include_notes] = false unless global[:notes]
2532
+
2533
+ global[:wwid] = wwid
2534
+
2535
+ code.call
1975
2536
  end
1976
2537
 
1977
2538
  exit run(ARGV)