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