doing 1.0.93 → 2.0.6.pre

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