doing 2.1.22 → 2.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +17 -14
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +323 -111
  6. data/Gemfile.lock +1 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -1
  9. data/bin/commands/add_section.rb +13 -0
  10. data/bin/commands/again.rb +99 -0
  11. data/bin/commands/archive.rb +96 -0
  12. data/bin/commands/cancel.rb +102 -0
  13. data/bin/commands/changes.rb +42 -0
  14. data/bin/commands/choose.rb +9 -0
  15. data/bin/commands/colors.rb +19 -0
  16. data/bin/commands/commands.rb +87 -0
  17. data/bin/commands/commands_accepting.rb +25 -0
  18. data/bin/commands/completion.rb +24 -0
  19. data/bin/commands/config.rb +245 -0
  20. data/bin/commands/done.rb +249 -0
  21. data/bin/commands/finish.rb +149 -0
  22. data/bin/commands/flag.rb +126 -0
  23. data/bin/commands/grep.rb +124 -0
  24. data/bin/commands/import.rb +101 -0
  25. data/bin/commands/install_fzf.rb +17 -0
  26. data/bin/commands/last.rb +114 -0
  27. data/bin/commands/meanwhile.rb +86 -0
  28. data/bin/commands/note.rb +130 -0
  29. data/bin/commands/now.rb +151 -0
  30. data/bin/commands/on.rb +66 -0
  31. data/bin/commands/open.rb +53 -0
  32. data/bin/commands/plugins.rb +23 -0
  33. data/bin/commands/recent.rb +78 -0
  34. data/bin/commands/redo.rb +22 -0
  35. data/bin/commands/reset.rb +106 -0
  36. data/bin/commands/rotate.rb +73 -0
  37. data/bin/commands/sections.rb +11 -0
  38. data/bin/commands/select.rb +123 -0
  39. data/bin/commands/show.rb +231 -0
  40. data/bin/commands/since.rb +64 -0
  41. data/bin/commands/tag.rb +179 -0
  42. data/bin/commands/tag_dir.rb +29 -0
  43. data/bin/commands/tags.rb +93 -0
  44. data/bin/commands/template.rb +61 -0
  45. data/bin/commands/today.rb +65 -0
  46. data/bin/commands/undo.rb +49 -0
  47. data/bin/commands/view.rb +238 -0
  48. data/bin/commands/views.rb +11 -0
  49. data/bin/commands/yesterday.rb +73 -0
  50. data/bin/doing +54 -3505
  51. data/docs/doc/Array.html +79 -11
  52. data/docs/doc/BooleanTermParser/Clause.html +5 -5
  53. data/docs/doc/BooleanTermParser/Operator.html +4 -4
  54. data/docs/doc/BooleanTermParser/Query.html +8 -8
  55. data/docs/doc/BooleanTermParser/QueryParser.html +2 -2
  56. data/docs/doc/BooleanTermParser/QueryTransformer.html +2 -2
  57. data/docs/doc/BooleanTermParser.html +1 -1
  58. data/docs/doc/Doing/Color.html +4 -4
  59. data/docs/doc/Doing/Completion.html +2 -2
  60. data/docs/doc/Doing/Configuration.html +17 -18
  61. data/docs/doc/Doing/Errors/DoingNoTraceError.html +2 -2
  62. data/docs/doc/Doing/Errors/DoingRuntimeError.html +2 -2
  63. data/docs/doc/Doing/Errors/DoingStandardError.html +2 -2
  64. data/docs/doc/Doing/Errors/EmptyInput.html +2 -2
  65. data/docs/doc/Doing/Errors/NoResults.html +2 -2
  66. data/docs/doc/Doing/Errors/PluginException.html +3 -3
  67. data/docs/doc/Doing/Errors/UserCancelled.html +2 -2
  68. data/docs/doc/Doing/Errors/WrongCommand.html +2 -2
  69. data/docs/doc/Doing/Errors.html +1 -1
  70. data/docs/doc/Doing/Hooks.html +6 -6
  71. data/docs/doc/Doing/Item.html +50 -16
  72. data/docs/doc/Doing/Items.html +10 -10
  73. data/docs/doc/Doing/LogAdapter.html +24 -24
  74. data/docs/doc/Doing/Note.html +7 -7
  75. data/docs/doc/Doing/Pager.html +4 -4
  76. data/docs/doc/Doing/Plugins.html +7 -7
  77. data/docs/doc/Doing/Prompt.html +59 -14
  78. data/docs/doc/Doing/Section.html +6 -6
  79. data/docs/doc/Doing/TemplateString.html +8 -8
  80. data/docs/doc/Doing/Types.html +206 -0
  81. data/docs/doc/Doing/Util/Backup.html +10 -10
  82. data/docs/doc/Doing/Util.html +16 -19
  83. data/docs/doc/Doing/WWID.html +65 -53
  84. data/docs/doc/Doing.html +3 -3
  85. data/docs/doc/FalseClass.html +201 -0
  86. data/docs/doc/GLI/Commands/Help.html +185 -0
  87. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +17 -17
  88. data/docs/doc/GLI/Commands.html +5 -3
  89. data/docs/doc/GLI.html +4 -2
  90. data/docs/doc/Hash.html +47 -21
  91. data/docs/doc/Numeric.html +5 -5
  92. data/docs/doc/Object.html +203 -0
  93. data/docs/doc/PhraseParser/Operator.html +4 -4
  94. data/docs/doc/PhraseParser/PhraseClause.html +5 -5
  95. data/docs/doc/PhraseParser/Query.html +10 -10
  96. data/docs/doc/PhraseParser/QueryParser.html +2 -2
  97. data/docs/doc/PhraseParser/QueryTransformer.html +2 -2
  98. data/docs/doc/PhraseParser/TermClause.html +5 -5
  99. data/docs/doc/PhraseParser.html +1 -1
  100. data/docs/doc/Status.html +7 -7
  101. data/docs/doc/String.html +144 -51
  102. data/docs/doc/Symbol.html +8 -8
  103. data/docs/doc/Time.html +6 -6
  104. data/docs/doc/TrueClass.html +201 -0
  105. data/docs/doc/_index.html +46 -16
  106. data/docs/doc/class_list.html +1 -1
  107. data/docs/doc/file.README.html +2 -2
  108. data/docs/doc/index.html +2 -2
  109. data/docs/doc/method_list.html +292 -212
  110. data/docs/doc/top-level-namespace.html +2 -2
  111. data/docs/index.md +1 -1
  112. data/doing.rdoc +178 -16
  113. data/example_plugin.rb +2 -2
  114. data/lib/completion/_doing.zsh +27 -27
  115. data/lib/completion/doing.bash +31 -20
  116. data/lib/completion/doing.fish +33 -11
  117. data/lib/doing/array.rb +2 -2
  118. data/lib/doing/changelog/change.rb +115 -0
  119. data/lib/doing/changelog/changes.rb +73 -0
  120. data/lib/doing/changelog/entry.rb +21 -0
  121. data/lib/doing/changelog/version.rb +97 -0
  122. data/lib/doing/changelog.rb +6 -0
  123. data/lib/doing/completion/fish_completion.rb +2 -1
  124. data/lib/doing/configuration.rb +20 -13
  125. data/lib/doing/good.rb +64 -0
  126. data/lib/doing/hash.rb +7 -2
  127. data/lib/doing/help_monkey_patch.rb +31 -0
  128. data/lib/doing/hooks.rb +8 -4
  129. data/lib/doing/item.rb +24 -35
  130. data/lib/doing/pager.rb +1 -0
  131. data/lib/doing/plugins/export/template_export.rb +1 -1
  132. data/lib/doing/plugins/import/calendar_import.rb +1 -1
  133. data/lib/doing/plugins/import/doing_import.rb +1 -1
  134. data/lib/doing/plugins/import/timing_import.rb +1 -1
  135. data/lib/doing/prompt.rb +8 -0
  136. data/lib/doing/string.rb +20 -11
  137. data/lib/doing/string_chronify.rb +1 -1
  138. data/lib/doing/template_string.rb +2 -2
  139. data/lib/doing/types.rb +3 -0
  140. data/lib/doing/util.rb +12 -11
  141. data/lib/doing/version.rb +1 -1
  142. data/lib/doing/wwid.rb +62 -37
  143. data/lib/doing.rb +2 -0
  144. data/lib/examples/commands/wiki.rb +6 -7
  145. data/lib/helpers/threaded_tests.rb +61 -71
  146. data/lib/helpers/threaded_tests_string.rb +50 -0
  147. metadata +56 -2
data/bin/doing CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
5
5
  require 'gli'
6
+ require 'doing/help_monkey_patch'
6
7
  require 'doing'
7
8
  require 'tempfile'
8
9
  require 'pp'
@@ -21,14 +22,15 @@ end
21
22
 
22
23
  include GLI::App
23
24
  include Doing::Errors
25
+
24
26
  version Doing::VERSION
25
27
  hide_commands_without_desc true
26
28
  autocomplete_commands true
27
29
 
28
30
  include Doing::Types
29
31
 
30
- colors = Doing::Color
31
- wwid = Doing::WWID.new
32
+ @colors = Doing::Color
33
+ @wwid = Doing::WWID.new
32
34
 
33
35
  Doing.logger.log_level = :info
34
36
  env_log_level = nil
@@ -54,15 +56,22 @@ if ENV['DOING_CONFIG']
54
56
  end
55
57
 
56
58
  Doing.logger.benchmark(:configure, :start)
57
- config = Doing.config
59
+ @config = Doing.config
58
60
  Doing.logger.benchmark(:configure, :finish)
59
61
 
60
- config.settings['backup_dir'] = ENV['DOING_BACKUP_DIR'] if ENV['DOING_BACKUP_DIR']
61
- settings = config.settings
62
- wwid.config = settings
62
+ @config.settings['backup_dir'] = ENV['DOING_BACKUP_DIR'] if ENV['DOING_BACKUP_DIR']
63
+ @settings = @config.settings
64
+ @wwid.config = @settings
65
+
66
+ if @settings.dig('plugins', 'command_path')
67
+ commands_from File.expand_path(@settings.dig('plugins', 'command_path'))
68
+ end
69
+
70
+ accept TemplateName do |value|
71
+ res = @settings['templates'].keys.select { |k| k =~ value.to_rx(distance: 2) }
72
+ raise InvalidArgument, "Unknown template: #{value}" unless res.good?
63
73
 
64
- if settings.dig('plugins', 'command_path')
65
- commands_from File.expand_path(settings.dig('plugins', 'command_path'))
74
+ res.group_by(&:length).min.last[0]
66
75
  end
67
76
 
68
77
  accept DateBeginString do |value|
@@ -95,6 +104,13 @@ accept DateRangeString do |value|
95
104
  [start, finish]
96
105
  end
97
106
 
107
+ accept DateRangeOptionalString do |value|
108
+ start, finish = value.split_date_range
109
+ raise InvalidTimeExpression, 'Invalid range' unless start
110
+
111
+ [start, finish]
112
+ end
113
+
98
114
  accept DateIntervalString do |value|
99
115
  res = value.chronify_qty
100
116
  raise InvalidTimeExpression, 'Invalid time quantity' unless res
@@ -124,7 +140,7 @@ desc 'Send results report to STDOUT instead of STDERR'
124
140
  switch [:stdout], default_value: false, negatable: false
125
141
 
126
142
  desc 'Use a pager when output is longer than screen'
127
- switch %i[p pager], default_value: settings['paginate']
143
+ switch %i[p pager], default_value: @settings['paginate']
128
144
 
129
145
  desc 'Answer yes/no menus with default option'
130
146
  switch [:default], default_value: false, negatable: false
@@ -148,3504 +164,37 @@ desc 'Verbose output'
148
164
  switch %i[debug], default_value: false, negatable: false
149
165
 
150
166
  desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead'
151
- flag [:config_file], default_value: config.config_file
167
+ flag [:config_file], default_value: @config.config_file
152
168
 
153
169
  desc 'Specify a different doing_file'
154
170
  flag %i[f doing_file]
155
171
 
156
- ## Add/modify commands
157
-
158
- # @@again @@resume
159
- desc 'Repeat last entry as new entry'
160
- long_desc 'This command is designed to allow multiple time intervals to be created for an entry by duplicating it with a new start (and end, eventually) time'
161
- command %i[again resume] do |c|
162
- c.example 'doing resume', desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
163
- c.example 'doing again', desc: 'again is an alias for resume'
164
- c.example 'doing resume --editor', desc: 'Repeat the last entry, opening the new entry in the default editor'
165
- c.example 'doing resume --tag project1 --in Projects', desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
166
- c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
167
-
168
- c.desc 'Get last entry from a specific section'
169
- c.arg_name 'NAME'
170
- c.flag %i[s section], default_value: 'All'
171
-
172
- c.desc 'Add new entry to section (default: same section as repeated entry)'
173
- c.arg_name 'SECTION_NAME'
174
- c.flag [:in]
175
-
176
- c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
177
- c.arg_name 'DATE_STRING'
178
- c.flag %i[b back started], type: DateBeginString
179
-
180
- c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
181
- c.arg_name 'TAG'
182
- c.flag [:tag], type: TagArray
183
-
184
- c.desc 'Repeat last entry matching search. Surround with
185
- slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
186
- c.arg_name 'QUERY'
187
- c.flag [:search]
188
-
189
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
190
- c.arg_name 'QUERY'
191
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
192
-
193
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
194
- # c.switch [:fuzzy], default_value: false, negatable: false
195
-
196
- c.desc 'Force exact search string matching (case sensitive)'
197
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
198
-
199
- c.desc 'Resume items that *don\'t* match search/tag filters'
200
- c.switch [:not], default_value: false, negatable: false
201
-
202
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
203
- c.arg_name 'TYPE'
204
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
205
-
206
- c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans'
207
- c.arg_name 'BOOLEAN'
208
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
209
-
210
- c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
211
- c.switch %i[e editor], negatable: false, default_value: false
212
-
213
- c.desc 'Add a note'
214
- c.arg_name 'TEXT'
215
- c.flag %i[n note]
216
-
217
- c.desc 'Prompt for note via multi-line input'
218
- c.switch %i[ask], negatable: false, default_value: false
219
-
220
- c.desc 'Select item to resume from a menu of matching entries'
221
- c.switch %i[i interactive], negatable: false, default_value: false
222
-
223
- c.action do |_global_options, options, _args|
224
- options[:fuzzy] = false
225
- tags = options[:tag].nil? ? [] : options[:tag]
226
-
227
- options[:case] = options[:case].normalize_case
228
-
229
- if options[:search]
230
- search = options[:search]
231
- search.sub!(/^'?/, "'") if options[:exact]
232
- options[:search] = search
233
- end
234
-
235
- if options[:back]
236
- date = options[:back]
237
- raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
238
- else
239
- date = Time.now
240
- end
241
-
242
- note = Doing::Note.new(options[:note])
243
- note.add(Doing::Prompt.read_lines(prompt: 'Add a note')) if options[:ask]
244
-
245
- options[:note] = note
246
-
247
- opts = options.dup
248
-
249
- opts[:tag] = tags
250
- opts[:tag_bool] = options[:bool].normalize_bool
251
- opts[:interactive] = options[:interactive]
252
- opts[:date] = date
253
-
254
- wwid.repeat_last(opts)
255
- end
256
- end
257
-
258
- # @@cancel
259
- desc 'End last X entries with no time tracked'
260
- long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`'
261
- arg_name 'COUNT'
262
- command :cancel do |c|
263
- c.example 'doing cancel', desc: 'Cancel the last entry'
264
- c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'
265
-
266
- c.desc 'Archive entries'
267
- c.switch %i[a archive], negatable: false, default_value: false
268
-
269
- c.desc 'Section'
270
- c.arg_name 'NAME'
271
- c.flag %i[s section]
272
-
273
- c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?)'
274
- c.arg_name 'TAG'
275
- c.flag [:tag], type: TagArray
276
-
277
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
278
- c.arg_name 'BOOLEAN'
279
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
280
-
281
- c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
282
- c.arg_name 'QUERY'
283
- c.flag [:search]
284
-
285
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
286
- c.arg_name 'QUERY'
287
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
288
-
289
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
290
- # c.switch [:fuzzy], default_value: false, negatable: false
291
-
292
- c.desc 'Force exact search string matching (case sensitive)'
293
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
294
-
295
- c.desc 'Finish items that *don\'t* match search/tag filters'
296
- c.switch [:not], default_value: false, negatable: false
297
-
298
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
299
- c.arg_name 'TYPE'
300
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
301
-
302
- c.desc 'Cancel last entry (or entries) not already marked @done'
303
- c.switch %i[u unfinished], negatable: false, default_value: false
304
-
305
- c.desc 'Select item(s) to cancel from a menu of matching entries'
306
- c.switch %i[i interactive], negatable: false, default_value: false
307
-
308
- c.action do |_global_options, options, args|
309
- options[:fuzzy] = false
310
- if options[:section]
311
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
312
- else
313
- section = settings['current_section']
314
- end
315
-
316
- if options[:tag].nil?
317
- tags = []
318
- else
319
- tags = options[:tag]
320
- end
321
-
322
- raise InvalidArgument, 'Only one argument allowed' if args.length > 1
323
-
324
- raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
325
-
326
- if options[:interactive]
327
- count = 0
328
- else
329
- count = args[0] ? args[0].to_i : 1
330
- end
331
-
332
- search = nil
333
-
334
- if options[:search]
335
- search = options[:search]
336
- search.sub!(/^'?/, "'") if options[:exact]
337
- end
338
-
339
- opts = {
340
- archive: options[:archive],
341
- case: options[:case].normalize_case,
342
- count: count,
343
- date: false,
344
- fuzzy: options[:fuzzy],
345
- interactive: options[:interactive],
346
- not: options[:not],
347
- search: search,
348
- section: section,
349
- sequential: false,
350
- tag: tags,
351
- tag_bool: options[:bool].normalize_bool,
352
- tags: ['done'],
353
- unfinished: options[:unfinished],
354
- val: options[:val]
355
- }
356
-
357
- wwid.tag_last(opts)
358
- end
359
- end
360
-
361
- # @@done @@did
362
- desc 'Add a completed item with @done(date). No argument finishes last entry'
363
- long_desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
364
- You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
365
- way to add entries in post and maintain accurate (albeit manual) time tracking.'
366
- arg_name 'ENTRY', optional: true
367
- command %i[done did] do |c|
368
- c.example 'doing done', desc: 'Tag the last entry @done'
369
- c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
370
- c.example 'doing done --back 30m This took me half an hour', desc: 'Add an entry with a start date 30 minutes ago and a @done date of right now'
371
- c.example 'doing done --at 3pm --took 1h Started and finished this afternoon', desc: 'Add an entry with a @done date of 3pm and a start date of 2pm (3pm - 1h)'
372
-
373
- c.desc 'Remove @done tag'
374
- c.switch %i[r remove], negatable: false, default_value: false
375
-
376
- c.desc 'Include date'
377
- c.switch [:date], negatable: true, default_value: true
378
-
379
- c.desc 'Immediately archive the entry'
380
- c.switch %i[a archive], negatable: false, default_value: false
381
-
382
- c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
383
- Used with --took, backdates start date)
384
- c.arg_name 'DATE_STRING'
385
- c.flag %i[at finished], type: DateEndString
386
-
387
- c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
388
- c.arg_name 'DATE_STRING'
389
- c.flag %i[b back started], type: DateBeginString
390
-
391
- c.desc %(
392
- Start and end times as a date/time range `doing done --from "1am to 8am"`.
393
- Overrides other date flags.
394
- )
395
- c.arg_name 'TIME_RANGE'
396
- c.flag [:from], must_match: REGEX_RANGE
397
-
398
- c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
399
- If used without the --back option, the start date will be moved back to allow
400
- the completion date to be the current time.)
401
- c.arg_name 'INTERVAL'
402
- c.flag %i[t took for], type: DateIntervalString
403
-
404
- c.desc 'Section'
405
- c.arg_name 'NAME'
406
- c.flag %i[s section]
407
-
408
- c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
409
- c.switch %i[e editor], negatable: false, default_value: false
410
-
411
- c.desc 'Include a note'
412
- c.arg_name 'TEXT'
413
- c.flag %i[n note]
414
-
415
- c.desc 'Prompt for note via multi-line input'
416
- c.switch %i[ask], negatable: false, default_value: false
417
-
418
- c.desc 'Finish last entry not already marked @done'
419
- c.switch %i[u unfinished], negatable: false, default_value: false
420
-
421
- # c.desc "Edit entry with specified app"
422
- # c.arg_name 'editor_app'
423
- # # c.flag [:a, :app]
424
-
425
- c.action do |_global_options, options, args|
426
- took = 0
427
- donedate = nil
428
-
429
- if options[:from]
430
- options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
431
- time =~ REGEX_TIME ? "today #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}" : time
432
- end.join(' to ').split_date_range
433
- date, finish_date = options[:from]
434
- finish_date ||= Time.now
435
- else
436
- if options[:took]
437
- took = options[:took]
438
- raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
439
- end
440
-
441
- if options[:back]
442
- date = options[:back]
443
- raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
444
- else
445
- date = options[:took] ? Time.now - took : Time.now
446
- end
447
-
448
- if options[:at]
449
- finish_date = options[:at]
450
- finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
451
- raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
452
-
453
- if options[:took]
454
- date = finish_date - took
455
- else
456
- date ||= finish_date
457
- end
458
- elsif options[:took]
459
- finish_date = date + took
460
- else
461
- finish_date = Time.now
462
- end
463
- end
464
-
465
- if options[:date]
466
- date = date.chronify(guess: :begin, context: :today) if date =~ REGEX_TIME
467
- finish_date = wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
468
-
469
- donedate = finish_date.strftime('%F %R')
470
- end
471
-
472
- if options[:section]
473
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
474
- else
475
- section = settings['current_section']
476
- end
477
-
478
-
479
- note = Doing::Note.new
480
- note.add(options[:note]) if options[:note]
481
-
482
- if options[:ask] && !options[:editor]
483
- note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
484
- end
485
-
486
- if options[:editor]
487
- raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
488
- is_new = false
489
-
490
- if args.empty?
491
- last_entry = wwid.filter_items([], opt: { unfinished: options[:unfinished], section: section, count: 1, age: :newest }).max_by { |item| item.date }
492
-
493
- unless last_entry
494
- Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
495
- raise NoResults, 'No results'
496
- end
497
-
498
- old_entry = last_entry.dup
499
- last_entry.note.add(note)
500
- input = ["#{last_entry.date.strftime('%F %R | ')}#{last_entry.title}", last_entry.note.strip_lines.join("\n")].join("\n")
501
- else
502
- is_new = true
503
- input = ["#{date.strftime('%F %R | ')}#{args.join(' ')}", note.strip_lines.join("\n")].join("\n")
504
- end
505
-
506
- input = wwid.fork_editor(input).strip
507
- raise EmptyInput, 'No content' unless input && !input.empty?
508
-
509
- d, title, note = wwid.format_input(input)
510
-
511
- if options[:ask]
512
- ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
513
- note.add(ask_note) unless ask_note.empty?
514
- end
515
-
516
- date = d.nil? ? date : d
517
- new_entry = Doing::Item.new(date, title, section, note)
518
- if new_entry.should_finish?
519
- if new_entry.should_time?
520
- new_entry.tag('done', value: donedate)
521
- else
522
- new_entry.tag('done')
523
- end
524
- end
525
-
526
- if (is_new)
527
- Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
528
- wwid.content.push(new_entry)
529
- Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
530
- else
531
- wwid.content.update_item(old_entry, new_entry)
532
- Doing::Hooks.trigger :post_entry_updated, wwid, new_entry unless options[:archive]
533
- end
534
-
535
- if options[:archive]
536
- wwid.move_item(new_entry, 'Archive', label: true)
537
- Doing::Hooks.trigger :post_entry_updated, wwid, new_entry
538
- end
539
-
540
- wwid.write(wwid.doing_file)
541
- elsif args.empty? && $stdin.stat.size.zero?
542
- if options[:remove]
543
- wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
544
- else
545
- opt = {
546
- archive: options[:archive],
547
- back: finish_date,
548
- count: 1,
549
- date: options[:date],
550
- note: note,
551
- section: section,
552
- tags: ['done'],
553
- took: took == 0 ? nil : took,
554
- unfinished: options[:unfinished]
555
- }
556
- wwid.tag_last(opt)
557
- end
558
- elsif !args.empty?
559
- d, title, new_note = wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
560
- date = d.nil? ? date : d
561
- new_note.add(options[:note])
562
- title.chomp!
563
- section = 'Archive' if options[:archive]
564
- new_entry = Doing::Item.new(date, title, section, new_note)
565
-
566
- if new_entry.should_finish?
567
- if new_entry.should_time?
568
- new_entry.tag('done', value: donedate)
569
- else
570
- new_entry.tag('done')
571
- end
572
- end
573
-
574
- Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
575
- wwid.content.push(new_entry)
576
- Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
577
- wwid.write(wwid.doing_file)
578
- Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
579
- elsif $stdin.stat.size.positive?
580
- note = Doing::Note.new(options[:note])
581
- d, title, note = wwid.format_input($stdin.read.strip)
582
- unless d.nil?
583
- Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
584
- date = d
585
- end
586
- note.add(options[:note]) if options[:note]
587
- section = options[:archive] ? 'Archive' : section
588
- new_entry = Doing::Item.new(date, title, section, note)
589
-
590
- if new_entry.should_finish?
591
- if new_entry.should_time?
592
- new_entry.tag('done', value: donedate)
593
- else
594
- new_entry.tag('done')
595
- end
596
- end
597
-
598
- Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
599
- wwid.content.push(new_entry)
600
- Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
601
-
602
- wwid.write(wwid.doing_file)
603
- Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
604
- else
605
- raise EmptyInput, 'You must provide content when creating a new entry'
606
- end
607
- end
608
- end
609
-
610
- # @@finish
611
- desc 'Mark last X entries as @done'
612
- long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
613
- arg_name 'COUNT', optional: true
614
- command :finish do |c|
615
- c.example 'doing finish', desc: 'Mark the last entry @done'
616
- c.example 'doing finish --auto --section Later 10', desc: 'Add @done to any unfinished entries in the last 10 in Later, setting the finish time based on the start time of the task after it'
617
- c.example 'doing finish --search "a specific entry" --at "yesterday 3pm"', desc: 'Search for an entry containing string and set its @done time to yesterday at 3pm'
618
-
619
- c.desc 'Include date'
620
- c.switch [:date], negatable: true, default_value: true
621
-
622
- c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
623
- c.arg_name 'DATE_STRING'
624
- c.flag %i[b back started], type: DateBeginString
625
-
626
- c.desc 'Set the completed date to the start date plus XX[hmd]'
627
- c.arg_name 'INTERVAL'
628
- c.flag %i[t took for], type: DateIntervalString
629
-
630
- c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
631
- c.arg_name 'DATE_STRING'
632
- c.flag %i[at finished], type: DateEndString
633
-
634
- c.desc 'Finish the last X entries containing TAG.
635
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
636
- c.arg_name 'TAG'
637
- c.flag [:tag], type: TagArray
638
-
639
- 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")'
640
- c.arg_name 'QUERY'
641
- c.flag [:search]
642
-
643
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
644
- c.arg_name 'QUERY'
645
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
646
-
647
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
648
- # c.switch [:fuzzy], default_value: false, negatable: false
649
-
650
- c.desc 'Force exact search string matching (case sensitive)'
651
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
652
-
653
- c.desc 'Finish items that *don\'t* match search/tag filters'
654
- c.switch [:not], default_value: false, negatable: false
655
-
656
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
657
- c.arg_name 'TYPE'
658
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
659
-
660
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
661
- c.arg_name 'BOOLEAN'
662
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
663
-
664
- c.desc 'Remove done tag'
665
- c.switch %i[r remove], negatable: false, default_value: false
666
-
667
- c.desc 'Finish last entry (or entries) not already marked @done'
668
- c.switch %i[u unfinished], negatable: false, default_value: false
669
-
670
- c.desc %(Auto-generate finish dates from next entry's start time.
671
- Automatically generate completion dates 1 minute before next item (in any section) began.
672
- --auto overrides the --date and --back parameters.)
673
- c.switch [:auto], negatable: false, default_value: false
674
-
675
- c.desc 'Archive entries'
676
- c.switch %i[a archive], negatable: false, default_value: false
677
-
678
- c.desc 'Section'
679
- c.arg_name 'NAME'
680
- c.flag %i[s section]
681
-
682
- c.desc 'Select item(s) to finish from a menu of matching entries'
683
- c.switch %i[i interactive], negatable: false, default_value: false
684
-
685
- c.action do |_global_options, options, args|
686
- options[:fuzzy] = false
687
- unless options[:auto]
688
- if options[:took]
689
- took = options[:took]
690
- raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
691
- end
692
-
693
- raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
694
-
695
- raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
696
-
697
- if options[:at]
698
- finish_date = options[:at]
699
- finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
700
- raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
701
-
702
- date = options[:took] ? finish_date - took : finish_date
703
- elsif options[:back]
704
- date = options[:back]
705
-
706
- raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
707
- else
708
- date = Time.now
709
- end
710
- end
711
-
712
- if options[:tag].nil?
713
- tags = []
714
- else
715
- tags = options[:tag]
716
- end
717
-
718
- raise InvalidArgument, 'Only one argument allowed' if args.length > 1
719
-
720
- raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
721
-
722
- if options[:interactive]
723
- count = 0
724
- else
725
- count = args[0] ? args[0].to_i : 1
726
- end
727
-
728
- search = nil
729
-
730
- if options[:search]
731
- search = options[:search]
732
- search.sub!(/^'?/, "'") if options[:exact]
733
- end
734
-
735
- opts = {
736
- archive: options[:archive],
737
- back: date,
738
- case: options[:case].normalize_case,
739
- count: count,
740
- date: options[:date],
741
- fuzzy: options[:fuzzy],
742
- interactive: options[:interactive],
743
- not: options[:not],
744
- remove: options[:remove],
745
- search: search,
746
- section: options[:section],
747
- sequential: options[:auto],
748
- tag: tags,
749
- tag_bool: options[:bool].normalize_bool,
750
- tags: ['done'],
751
- took: options[:took],
752
- unfinished: options[:unfinished],
753
- val: options[:val]
754
- }
755
-
756
- wwid.tag_last(opts)
757
- end
758
- end
759
-
760
- # @@mark @@flag
761
- desc 'Mark last entry as flagged'
762
- command %i[mark flag] do |c|
763
- c.example 'doing flag', desc: 'Add @flagged to the last entry created'
764
- c.example 'doing mark', desc: 'mark is an alias for flag'
765
- c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
766
- 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'
767
-
768
- c.desc 'Section'
769
- c.arg_name 'SECTION_NAME'
770
- c.flag %i[s section], default_value: 'All'
771
-
772
- c.desc 'How many recent entries to tag (0 for all)'
773
- c.arg_name 'COUNT'
774
- c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
775
-
776
- c.desc 'Don\'t ask permission to flag all entries when count is 0'
777
- c.switch %i[force], negatable: false, default_value: false
778
-
779
- c.desc 'Include current date/time with tag'
780
- c.switch %i[d date], negatable: false, default_value: false
781
-
782
- c.desc 'Remove flag'
783
- c.switch %i[r remove], negatable: false, default_value: false
784
-
785
- c.desc 'Flag last entry (or entries) not marked @done'
786
- c.switch %i[u unfinished], negatable: false, default_value: false
787
-
788
- c.desc 'Flag the last entry containing TAG.
789
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
790
- c.arg_name 'TAG'
791
- c.flag [:tag], type: TagArray
792
-
793
- 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")'
794
- c.arg_name 'QUERY'
795
- c.flag [:search]
796
-
797
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
798
- c.arg_name 'QUERY'
799
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
800
-
801
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
802
- # c.switch [:fuzzy], default_value: false, negatable: false
803
-
804
- c.desc 'Force exact search string matching (case sensitive)'
805
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
806
-
807
- c.desc 'Flag items that *don\'t* match search/tag/date filters'
808
- c.switch [:not], default_value: false, negatable: false
809
-
810
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
811
- c.arg_name 'TYPE'
812
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
813
-
814
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
815
- c.arg_name 'BOOLEAN'
816
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
817
-
818
- c.desc 'Select item(s) to flag from a menu of matching entries'
819
- c.switch %i[i interactive], negatable: false, default_value: false
820
-
821
- c.action do |_global_options, options, _args|
822
- options[:fuzzy] = false
823
- mark = settings['marker_tag'] || 'flagged'
824
-
825
- raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
826
-
827
- section = 'All'
828
-
829
- if options[:section]
830
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
831
- end
832
-
833
- if options[:tag].nil?
834
- search_tags = []
835
- else
836
- search_tags = options[:tag]
837
- end
838
-
839
- if options[:interactive]
840
- count = 0
841
- options[:force] = true
842
- else
843
- count = options[:count].to_i
844
- end
845
-
846
- options[:case] = options[:case].normalize_case
847
-
848
- if options[:search]
849
- search = options[:search]
850
- search.sub!(/^'?/, "'") if options[:exact]
851
- options[:search] = search
852
- end
853
-
854
- if count.zero? && !options[:force]
855
- if options[:search]
856
- section_q = ' matching your search terms'
857
- elsif options[:tag]
858
- section_q = ' matching your tag search'
859
- elsif section == 'All'
860
- section_q = ''
861
- else
862
- section_q = " in section #{section}"
863
- end
864
-
865
-
866
- question = if options[:remove]
867
- "Are you sure you want to unflag all entries#{section_q}"
868
- else
869
- "Are you sure you want to flag all records#{section_q}"
870
- end
871
-
872
- res = Doing::Prompt.yn(question, default_response: false)
873
-
874
- exit_now! 'Cancelled' unless res
875
- end
876
-
877
- options[:count] = count
878
- options[:section] = section
879
- options[:tag] = search_tags
880
- options[:tags] = [mark]
881
- options[:tag_bool] = options[:bool].normalize_bool
882
-
883
- wwid.tag_last(options)
884
- end
885
- end
886
-
887
- # @@meanwhile
888
- desc 'Finish any running @meanwhile tasks and optionally create a new one'
889
- long_desc 'The @meanwhile tag allows you to have long-running entries that encompass smaller entries.
890
- This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
891
- big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
892
- itself to mark the entry as @done.'
893
- arg_name 'ENTRY', optional: true
894
- command :meanwhile do |c|
895
- c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
896
- c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
897
- c.example 'doing meanwhile --archive', desc: 'Finish any open @meanwhile entry and archive it'
898
- c.example 'doing meanwhile --back 2h "Something I\'ve been working on for a while', desc: 'Add a @meanwhile entry with a start date 2 hours ago'
899
-
900
- c.desc 'Section'
901
- c.arg_name 'NAME'
902
- c.flag %i[s section]
903
-
904
- c.desc "Edit entry with #{Doing::Util.default_editor}"
905
- c.switch %i[e editor], negatable: false, default_value: false
906
-
907
- c.desc 'Archive previous @meanwhile entry'
908
- c.switch %i[a archive], negatable: false, default_value: false
909
-
910
- c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
911
- c.arg_name 'DATE_STRING'
912
- c.flag %i[b back started], type: DateBeginString
913
-
914
- c.desc 'Note'
915
- c.arg_name 'TEXT'
916
- c.flag %i[n note]
917
-
918
- c.desc 'Prompt for note via multi-line input'
919
- c.switch %i[ask], negatable: false, default_value: false
920
-
921
- c.action do |_global_options, options, args|
922
- if options[:back]
923
- date = options[:back]
924
-
925
- raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
926
- else
927
- date = Time.now
928
- end
929
-
930
- if options[:section]
931
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
932
- else
933
- section = settings['current_section']
934
- end
935
- input = ''
936
-
937
- ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : []
938
-
939
- if options[:editor]
940
- raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
941
- input += date.strftime('%F %R | ')
942
- input += args.join(' ') unless args.empty?
943
- input += "\n#{options[:note]}" if options[:note]
944
- input += "\n#{ask_note}" unless ask_note.empty?
945
-
946
- input = wwid.fork_editor(input).strip
947
- elsif !args.empty?
948
- input = args.join(' ')
949
- elsif $stdin.stat.size.positive?
950
- input = $stdin.read.strip
951
- end
952
-
953
- if input && !input.empty?
954
- d, input, note = wwid.format_input(input)
955
- unless d.nil?
956
- Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
957
- date = d
958
- end
959
- else
960
- input = nil
961
- note = []
962
- end
963
-
964
- unless options[:editor]
965
- note.add(options[:note]) if options[:note]
966
- note.add(ask_note) unless ask_note.empty?
967
- end
968
-
969
- wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:archive], note: note })
970
- wwid.write(wwid.doing_file)
971
- end
972
- end
973
-
974
- # @@note
975
- desc 'Add a note to the last entry'
976
- long_desc %(
977
- If -r is provided with no other arguments, the last note is removed.
978
- If new content is specified through arguments or STDIN, any previous
979
- note will be replaced with the new one.
980
-
981
- Use -e to load the last entry in a text editor where you can append a note.
982
- )
983
- arg_name 'NOTE_TEXT', optional: true
984
- command :note do |c|
985
- c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
986
- c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
987
- c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
988
- c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'
989
-
990
- c.desc 'Section'
991
- c.arg_name 'NAME'
992
- c.flag %i[s section], default_value: 'All'
993
-
994
- c.desc "Edit entry with #{Doing::Util.default_editor}"
995
- c.switch %i[e editor], negatable: false, default_value: false
996
-
997
- c.desc "Replace/Remove last entry's note (default append)"
998
- c.switch %i[r remove], negatable: false, default_value: false
999
-
1000
- c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?)'
1001
- c.arg_name 'TAG'
1002
- c.flag [:tag], type: TagArray
1003
-
1004
- 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")'
1005
- c.arg_name 'QUERY'
1006
- c.flag [:search]
1007
-
1008
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1009
- c.arg_name 'QUERY'
1010
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1011
-
1012
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1013
- # c.switch [:fuzzy], default_value: false, negatable: false
1014
-
1015
- c.desc 'Force exact search string matching (case sensitive)'
1016
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1017
-
1018
- c.desc 'Add note to item that *doesn\'t* match search/tag filters'
1019
- c.switch [:not], default_value: false, negatable: false
1020
-
1021
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1022
- c.arg_name 'TYPE'
1023
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1024
-
1025
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
1026
- c.arg_name 'BOOLEAN'
1027
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1028
-
1029
- c.desc 'Select item for new note from a menu of matching entries'
1030
- c.switch %i[i interactive], negatable: false, default_value: false
1031
-
1032
- c.desc 'Prompt for note via multi-line input'
1033
- c.switch %i[ask], negatable: false, default_value: false
1034
-
1035
- c.action do |_global_options, options, args|
1036
- options[:fuzzy] = false
1037
- if options[:section]
1038
- options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
1039
- end
1040
-
1041
- options[:tag_bool] = options[:bool].normalize_bool
1042
-
1043
- options[:case] = options[:case].normalize_case
1044
-
1045
- if options[:search]
1046
- search = options[:search]
1047
- search.sub!(/^'?/, "'") if options[:exact]
1048
- options[:search] = search
1049
- end
1050
-
1051
- last_entry = wwid.last_entry(options)
1052
-
1053
- unless last_entry
1054
- Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
1055
- return
1056
- end
1057
-
1058
- last_note = last_entry.note || Doing::Note.new
1059
- new_note = Doing::Note.new
1060
-
1061
- if $stdin.stat.size.positive?
1062
- new_note.add($stdin.read.strip)
1063
- end
1064
-
1065
- unless args.empty?
1066
- new_note.add(args.join(' '))
1067
- end
1068
-
1069
- if options[:editor]
1070
- raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1071
-
1072
- if options[:remove]
1073
- input = Doing::Note.new
1074
- else
1075
- input = last_entry.note || Doing::Note.new
1076
- end
1077
-
1078
- input.add(new_note)
1079
-
1080
- new_note = Doing::Note.new(wwid.fork_editor(input.strip_lines.join("\n"), message: nil).strip)
1081
- options[:remove] = true
1082
- end
1083
-
1084
- if (new_note.empty? && !options[:remove]) || options[:ask]
1085
- $stderr.puts last_note unless last_note.empty?
1086
- $stderr.puts new_note unless new_note.empty?
1087
- new_note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
1088
- end
1089
-
1090
- raise EmptyInput, 'You must provide content when adding a note' unless options[:remove] || !new_note.empty?
1091
-
1092
- if last_note.equal?(new_note)
1093
- Doing.logger.debug('Skipped:', 'No note change')
1094
- else
1095
- last_note.add(new_note, replace: options[:remove])
1096
- Doing.logger.info('Entry updated:', last_entry.title)
1097
- Doing::Hooks.trigger :post_entry_updated, wwid, last_entry
1098
- end
1099
- # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
1100
- wwid.write(wwid.doing_file)
1101
- end
1102
- end
1103
-
1104
- # @@now @@next
1105
- desc 'Add an entry'
1106
- long_desc %(Record what you're starting now, or backdate the start time using natural language.
1107
-
1108
- A parenthetical at the end of the entry will be converted to a note.
1109
-
1110
- Run without arguments to create a new entry interactively.
1111
-
1112
- Run with --editor to create a new entry using #{Doing::Util.default_editor}.)
1113
- arg_name 'ENTRY'
1114
- command %i[now next] do |c|
1115
- c.example 'doing now', desc: 'Create a new entry with interactive prompts'
1116
- c.example 'doing now -e', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note"
1117
- c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
1118
- c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
1119
- c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
1120
- c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
1121
-
1122
- c.desc 'Section'
1123
- c.arg_name 'NAME'
1124
- c.flag %i[s section]
1125
-
1126
- c.desc "Edit entry with #{Doing::Util.default_editor}"
1127
- c.switch %i[e editor], negatable: false, default_value: false
1128
-
1129
- c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
1130
- c.arg_name 'DATE_STRING'
1131
- c.flag %i[b back started], type: DateBeginString
1132
-
1133
- c.desc 'Timed entry, marks last entry in section as @done'
1134
- c.switch %i[f finish_last], negatable: false, default_value: false
1135
-
1136
- c.desc 'Include a note'
1137
- c.arg_name 'TEXT'
1138
- c.flag %i[n note]
1139
-
1140
- c.desc 'Prompt for note via multi-line input'
1141
- c.switch %i[ask], negatable: false, default_value: false
1142
-
1143
- # c.desc "Edit entry with specified app"
1144
- # c.arg_name 'editor_app'
1145
- # # c.flag [:a, :app]
1146
-
1147
- c.action do |_global_options, options, args|
1148
- if options[:back]
1149
- date = options[:back]
1150
-
1151
- raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
1152
- else
1153
- date = Time.now
1154
- end
1155
-
1156
- if options[:section]
1157
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
1158
- else
1159
- options[:section] = settings['current_section']
1160
- end
1161
-
1162
- ask_note = options[:ask] && !options[:editor] && args.count.positive? ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
1163
-
1164
- if options[:editor]
1165
- raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
1166
-
1167
- input = date.strftime('%F %R | ')
1168
- input += args.join(' ') unless args.empty?
1169
- input += "\n#{options[:note]}" if options[:note]
1170
- input += "\n#{ask_note}" unless ask_note.empty?
1171
- input = wwid.fork_editor(input).strip
1172
-
1173
- d, title, note = wwid.format_input(input)
1174
- raise EmptyInput, 'No content' if title.strip.empty?
1175
-
1176
- if ask_note.empty? && options[:ask]
1177
- ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
1178
- note.add(ask_note) unless ask_note.empty?
1179
- end
1180
-
1181
- date = d.nil? ? date : d
1182
- wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1183
- wwid.write(wwid.doing_file)
1184
- elsif args.length.positive?
1185
- d, title, note = wwid.format_input(args.join(' '))
1186
- date = d.nil? ? date : d
1187
- note.add(options[:note]) if options[:note]
1188
- note.add(ask_note) unless ask_note.empty?
1189
- wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1190
- wwid.write(wwid.doing_file)
1191
- elsif $stdin.stat.size.positive?
1192
- input = $stdin.read.strip
1193
- d, title, note = wwid.format_input(input)
1194
- unless d.nil?
1195
- Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
1196
- date = d
1197
- end
1198
- note.add(options[:note]) if options[:note]
1199
- if ask_note.empty? && options[:ask]
1200
- ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
1201
- note.add(ask_note) unless ask_note.empty?
1202
- end
1203
- wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1204
- wwid.write(wwid.doing_file)
1205
- else
1206
- tags = wwid.all_tags(wwid.content)
1207
- $stderr.puts Doing::Color.boldgreen("Add a new entry. Tab will autocomplete known tags. Ctrl-c to cancel.")
1208
- title = Doing::Prompt.read_line(prompt: 'Entry content', completions: tags)
1209
- raise EmptyInput, 'You must provide content when creating a new entry' if title.strip.empty?
1210
-
1211
- note = Doing::Note.new
1212
- note.add(options[:note]) if options[:note]
1213
- res = Doing::Prompt.yn('Add a note', default_response: false)
1214
- ask_note = res ? Doing::Prompt.read_lines(prompt: 'Enter note') : []
1215
- note.add(ask_note)
1216
-
1217
- wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
1218
- wwid.write(wwid.doing_file)
1219
- end
1220
- end
1221
- end
1222
-
1223
- # @@reset @@begin
1224
- desc 'Reset the start time of an entry'
1225
- long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
1226
- If no argument is provided, the start time will be reset to the current time.
1227
- If a date string is provided as an argument, the start time will be set to the parsed result.'
1228
- arg_name 'DATE_STRING', optional: true
1229
- command %i[reset begin] do |c|
1230
- c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
1231
- c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
1232
- c.example 'doing reset 3pm', desc: 'Reset the start time of the last entry to 3pm of the current day'
1233
- c.example 'doing begin --tag todo --resume', desc: 'alias for reset. Updates the last @todo entry to the current time, removing @done tag.'
1234
-
1235
- c.desc 'Limit search to section'
1236
- c.arg_name 'NAME'
1237
- c.flag %i[s section], default_value: 'All'
1238
-
1239
- c.desc 'Resume entry (remove @done)'
1240
- c.switch %i[r resume], default_value: true
1241
-
1242
- c.desc 'Change start date but do not remove @done (shortcut for --no-resume)'
1243
- c.switch [:n]
1244
-
1245
- c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?)'
1246
- c.arg_name 'TAG'
1247
- c.flag [:tag]
1248
-
1249
- c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1250
- c.arg_name 'QUERY'
1251
- c.flag [:search]
1252
-
1253
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1254
- c.arg_name 'QUERY'
1255
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1256
-
1257
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1258
- # c.switch [:fuzzy], default_value: false, negatable: false
1259
-
1260
- c.desc 'Force exact search string matching (case sensitive)'
1261
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1262
-
1263
- c.desc 'Reset items that *don\'t* match search/tag filters'
1264
- c.switch [:not], default_value: false, negatable: false
1265
-
1266
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1267
- c.arg_name 'TYPE'
1268
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1269
-
1270
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
1271
- c.arg_name 'BOOLEAN'
1272
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1273
-
1274
- c.desc 'Select from a menu of matching entries'
1275
- c.switch %i[i interactive], negatable: false, default_value: false
1276
-
1277
- c.action do |global_options, options, args|
1278
- if args.count > 0
1279
- reset_date = args.join(' ').chronify(guess: :begin)
1280
- raise InvalidArgument, 'Invalid date string' unless reset_date
1281
- else
1282
- reset_date = Time.now
1283
- end
1284
-
1285
- options[:fuzzy] = false
1286
- if options[:section]
1287
- options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
1288
- end
1289
-
1290
- options[:bool] = options[:bool].normalize_bool
1291
-
1292
- options[:case] = options[:case].normalize_case
1293
-
1294
- if options[:search]
1295
- search = options[:search]
1296
- search.sub!(/^'?/, "'") if options[:exact]
1297
- options[:search] = search
1298
- end
1299
-
1300
-
1301
- items = wwid.filter_items([], opt: options)
1302
-
1303
- if options[:interactive]
1304
- last_entry = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
1305
- menu: true,
1306
- header: '',
1307
- prompt: 'Select an entry to start/reset > ',
1308
- multiple: false,
1309
- sort: false,
1310
- show_if_single: true)
1311
- else
1312
- last_entry = items.reverse.last
1313
- end
1314
-
1315
- unless last_entry
1316
- Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
1317
- return
1318
- end
1319
-
1320
- wwid.reset_item(last_entry, date: reset_date, resume: options[:resume])
1321
- Doing::Hooks.trigger :post_entry_updated, wwid, last_entry
1322
- # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
1323
-
1324
- wwid.write(wwid.doing_file)
1325
- end
1326
- end
1327
-
1328
- # @@select
1329
- desc 'Display an interactive menu to perform operations'
1330
- long_desc 'List all entries and select with typeahead fuzzy matching.
1331
-
1332
- Multiple selections are allowed, hit tab to add the highlighted entry to the
1333
- selection, and use ctrl-a to select all visible items. Return processes the
1334
- selected entries.
1335
-
1336
- Search in the menu by typing:
1337
-
1338
- sbtrkt fuzzy-match Items that match s*b*t*r*k*t
1339
-
1340
- \'wild exact-match (quoted) Items that include wild
1341
-
1342
- !fire inverse-exact-match Items that do not include fire'
1343
- command :select do |c|
1344
- c.example 'doing select', desc: 'Select from all entries. A menu of available actions will be presented after confirming the selection.'
1345
- c.example 'doing select --editor', desc: 'Select entries from a menu and batch edit them in your default editor'
1346
- c.example 'doing select --after "yesterday 12pm" --tag project1', desc: 'Display a menu of entries created after noon yesterday, add @project1 to selected entries'
1347
- c.desc 'Select from a specific section'
1348
- c.arg_name 'SECTION'
1349
- c.flag %i[s section]
1350
-
1351
- c.desc 'Tag selected entries'
1352
- c.arg_name 'TAG'
1353
- c.flag %i[t tag]
1354
-
1355
- c.desc 'Reverse -c, -f, --flag, and -t (remove instead of adding)'
1356
- c.switch %i[r remove], negatable: false
1357
-
1358
- # c.desc 'Add @done to selected item(s), using start time of next item as the finish time'
1359
- # c.switch %i[a auto], negatable: false, default_value: false
1360
-
1361
- c.desc 'Archive selected items'
1362
- c.switch %i[a archive], negatable: false, default_value: false
1363
-
1364
- c.desc 'Move selected items to section'
1365
- c.arg_name 'SECTION'
1366
- c.flag %i[m move]
1367
-
1368
- c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
1369
- c.arg_name 'QUERY'
1370
- c.flag %i[q query]
1371
-
1372
- c.desc 'Select from entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1373
- c.arg_name 'QUERY'
1374
- c.flag [:search]
1375
-
1376
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1377
- c.arg_name 'QUERY'
1378
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1379
-
1380
- c.desc 'Select from entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1381
- c.arg_name 'DATE_STRING'
1382
- c.flag [:before], type: DateBeginString
1383
-
1384
- c.desc 'Select from entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1385
- c.arg_name 'DATE_STRING'
1386
- c.flag [:after], type: DateEndString
1387
-
1388
- c.desc %(
1389
- Date range to show, or a single day to filter date on.
1390
- Date range argument should be quoted. Date specifications can be natural language.
1391
- To specify a range, use "to" or "through": `doing select --from "monday 8am to friday 5pm"`.
1392
-
1393
- If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
1394
- by time of day.
1395
- )
1396
- c.arg_name 'DATE_OR_RANGE'
1397
- c.flag [:from], type: DateRangeString
1398
-
1399
- c.desc 'Force exact search string matching (case sensitive)'
1400
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1401
-
1402
- c.desc 'Select items that *don\'t* match search/tag filters'
1403
- c.switch [:not], default_value: false, negatable: false
1404
-
1405
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1406
- c.arg_name 'TYPE'
1407
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1408
-
1409
- 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'
1410
- c.switch %i[menu], negatable: true, default_value: true
1411
-
1412
- c.desc 'Cancel selected items (add @done without timestamp)'
1413
- c.switch %i[c cancel], negatable: false, default_value: false
1414
-
1415
- c.desc 'Delete selected items'
1416
- c.switch %i[d delete], negatable: false, default_value: false
1417
-
1418
- c.desc 'Edit selected item(s)'
1419
- c.switch %i[e editor], negatable: false, default_value: false
1420
-
1421
- c.desc 'Add @done with current time to selected item(s)'
1422
- c.switch %i[f finish], negatable: false, default_value: false
1423
-
1424
- c.desc 'Add flag to selected item(s)'
1425
- c.switch %i[flag], negatable: false, default_value: false
1426
-
1427
- c.desc 'Perform action without confirmation'
1428
- c.switch %i[force], negatable: false, default_value: false
1429
-
1430
- c.desc 'Save selected entries to file using --output format'
1431
- c.arg_name 'FILE'
1432
- c.flag %i[save_to]
1433
-
1434
- c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})"
1435
- c.arg_name 'FORMAT'
1436
- c.flag %i[o output]
1437
-
1438
- 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."
1439
- c.switch %i[again resume], negatable: false, default_value: false
1440
-
1441
- c.action do |_global_options, options, args|
1442
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1443
-
1444
- raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
1445
-
1446
- options[:case] = options[:case].normalize_case
1447
-
1448
- wwid.interactive(options) # hooked
1449
- end
1450
- end
1451
-
1452
- # @@tag
1453
- desc 'Add tag(s) to last entry'
1454
- long_desc 'Add (or remove) tags from the last entry, or from multiple entries
1455
- (with `--count`), entries matching a search (with `--search`), or entries
1456
- containing another tag (with `--tag`).
1457
-
1458
- When removing tags with `-r`, wildcards are allowed (`*` to match
1459
- multiple characters, `?` to match a single character). With `--regex`,
1460
- regular expressions will be interpreted instead of wildcards.
1461
-
1462
- For all tag removals the match is case insensitive by default, but if
1463
- the tag search string contains any uppercase letters, the match will
1464
- become case sensitive automatically.
1465
-
1466
- Tag name arguments do not need to be prefixed with @.'
1467
- arg_name 'TAG', :multiple
1468
- command :tag do |c|
1469
- c.example 'doing tag mytag', desc: 'Add @mytag to the last entry created'
1470
- c.example 'doing tag --remove mytag', desc: 'Remove @mytag from the last entry created'
1471
- c.example 'doing tag --rename "other*" --count 10 newtag', desc: 'Rename tags beginning with "other" (wildcard) to @newtag on the last 10 entries'
1472
- c.example 'doing tag --search "developing" coding', desc: 'Add @coding to the last entry containing string "developing" (fuzzy matching)'
1473
- c.example 'doing tag --interactive --tag project1 coding', desc: 'Create an interactive menu from entries tagged @project1, selection(s) will be tagged with @coding'
1474
-
1475
- c.desc 'Section'
1476
- c.arg_name 'SECTION_NAME'
1477
- c.flag %i[s section], default_value: 'All'
1478
-
1479
- c.desc 'How many recent entries to tag (0 for all)'
1480
- c.arg_name 'COUNT'
1481
- c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
1482
-
1483
- c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
1484
- c.arg_name 'ORIG_TAG'
1485
- c.flag %i[rename]
1486
-
1487
- c.desc 'Include a value, e.g. @tag(value)'
1488
- c.arg_name 'VALUE'
1489
- c.flag %i[v value]
1490
-
1491
- c.desc 'Don\'t ask permission to tag all entries when count is 0'
1492
- c.switch %i[force], negatable: false, default_value: false
1493
-
1494
- c.desc 'Include current date/time with tag'
1495
- c.switch %i[d date], negatable: false, default_value: false
1496
-
1497
- c.desc 'Remove given tag(s)'
1498
- c.switch %i[r remove], negatable: false, default_value: false
1499
-
1500
- c.desc 'Interpret tag string as regular expression (with --remove)'
1501
- c.switch %i[regex], negatable: false, default_value: false
1502
-
1503
- c.desc 'Tag last entry (or entries) not marked @done'
1504
- c.switch %i[u unfinished], negatable: false, default_value: false
1505
-
1506
- c.desc 'Autotag entries based on autotag configuration in ~/.config/doing/config.yml'
1507
- c.switch %i[a autotag], negatable: false, default_value: false
1508
-
1509
- c.desc 'Tag the last X entries containing TAG.
1510
- Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
1511
- c.arg_name 'TAG'
1512
- c.flag [:tag], type: TagArray
1513
-
1514
- c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
1515
- c.arg_name 'QUERY'
1516
- c.flag [:search]
1517
-
1518
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1519
- c.arg_name 'QUERY'
1520
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1521
-
1522
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1523
- # c.switch [:fuzzy], default_value: false, negatable: false
1524
-
1525
- c.desc 'Force exact search string matching (case sensitive)'
1526
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1527
-
1528
- c.desc 'Tag items that *don\'t* match search/tag filters'
1529
- c.switch [:not], default_value: false, negatable: false
1530
-
1531
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1532
- c.arg_name 'TYPE'
1533
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1534
-
1535
- c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
1536
- c.arg_name 'BOOLEAN'
1537
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1538
-
1539
- c.desc 'Select item(s) to tag from a menu of matching entries'
1540
- c.switch %i[i interactive], negatable: false, default_value: false
1541
-
1542
- c.action do |_global_options, options, args|
1543
- options[:fuzzy] = false
1544
- # raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1545
-
1546
- raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1547
-
1548
- section = 'All'
1549
-
1550
- if options[:section]
1551
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
1552
- end
1553
-
1554
-
1555
- if options[:tag].nil?
1556
- search_tags = []
1557
- else
1558
- search_tags = options[:tag]
1559
- end
1560
-
1561
- if options[:autotag]
1562
- tags = []
1563
- else
1564
- if args.empty?
1565
- tags = []
1566
- else
1567
- tags = if args.join('') =~ /,/
1568
- args.join('').split(/ *, */)
1569
- else
1570
- args.join(' ').split(' ') # in case tags are quoted as one arg
1571
- end
1572
- end
1573
-
1574
- tags.map! { |tag| tag.sub(/^@/, '').strip }
1575
- end
1576
-
1577
- if options[:interactive]
1578
- count = 0
1579
- options[:force] = true
1580
- else
1581
- count = options[:count].to_i
1582
- end
1583
-
1584
- options[:case] ||= :smart
1585
- options[:case] = options[:case].normalize_case
1586
-
1587
- if options[:search]
1588
- search = options[:search]
1589
- search.sub!(/^'?/, "'") if options[:exact]
1590
- options[:search] = search
1591
- end
1592
-
1593
- options[:count] = count
1594
- options[:section] = section
1595
- options[:tag] = search_tags
1596
- options[:tags] = tags
1597
- options[:tag_bool] = options[:bool].normalize_bool
1598
-
1599
- if count.zero? && !options[:force]
1600
- matches = wwid.filter_items([], opt: options).count
1601
-
1602
- if matches > 5
1603
- if options[:search]
1604
- section_q = ' matching your search terms'
1605
- elsif options[:tag]
1606
- section_q = ' matching your tag search'
1607
- elsif section == 'All'
1608
- section_q = ''
1609
- else
1610
- section_q = " in section #{section}"
1611
- end
1612
-
1613
-
1614
- question = if options[:autotag]
1615
- "Are you sure you want to autotag #{matches} records#{section_q}"
1616
- elsif options[:remove]
1617
- "Are you sure you want to remove #{tags.join(' and ')} from #{matches} records#{section_q}"
1618
- else
1619
- "Are you sure you want to add #{tags.join(' and ')} to #{matches} records#{section_q}"
1620
- end
1621
-
1622
- res = Doing::Prompt.yn(question, default_response: false)
1623
-
1624
- raise UserCancelled unless res
1625
- end
1626
- end
1627
-
1628
- wwid.tag_last(options)
1629
- end
1630
- end
1631
-
1632
- ## View commands
1633
-
1634
- # @@choose
1635
- desc 'Select a section to display from a menu'
1636
- command :choose do |c|
1637
- c.action do |_global_options, _options, _args|
1638
- section = wwid.choose_section
1639
-
1640
- Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
1641
- end
1642
- end
1643
-
1644
- # @@grep @@search
1645
- desc 'Search for entries'
1646
- long_desc %(
1647
- Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
1648
-
1649
- To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
1650
- )
1651
- arg_name 'SEARCH_PATTERN'
1652
- command %i[grep search] do |c|
1653
- c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
1654
- c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
1655
- c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
1656
- 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'
1657
-
1658
- c.desc 'Section'
1659
- c.arg_name 'NAME'
1660
- c.flag %i[s section], default_value: 'All'
1661
-
1662
- c.desc 'Search entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1663
- c.arg_name 'DATE_STRING'
1664
- c.flag [:before], type: DateBeginString
1665
-
1666
- c.desc 'Search entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1667
- c.arg_name 'DATE_STRING'
1668
- c.flag [:after], type: DateEndString
1669
-
1670
- c.desc %(
1671
- Date range to show, or a single day to filter date on.
1672
- Date range argument should be quoted. Date specifications can be natural language.
1673
- To specify a range, use "to" or "through": `doing search --from "monday 8am to friday 5pm"`.
1674
-
1675
- If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
1676
- by time of day.
1677
- )
1678
- c.arg_name 'DATE_OR_RANGE'
1679
- c.flag [:from], type: DateRangeString
1680
-
1681
- c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1682
- c.arg_name 'FORMAT'
1683
- c.flag %i[o output]
1684
-
1685
- c.desc 'Show time intervals on @done tasks'
1686
- c.switch %i[t times], default_value: true, negatable: true
1687
-
1688
- c.desc 'Show elapsed time on entries without @done tag'
1689
- c.switch [:duration]
1690
-
1691
- c.desc 'Show intervals with totals at the end of output'
1692
- c.switch [:totals], default_value: false, negatable: false
1693
-
1694
- c.desc 'Sort tags by (name|time)'
1695
- default = 'time'
1696
- default = settings['tag_sort'] || 'name'
1697
- c.arg_name 'KEY'
1698
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1699
-
1700
- c.desc 'Only show items with recorded time intervals'
1701
- c.switch [:only_timed], default_value: false, negatable: false
1702
-
1703
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1704
- # c.switch [:fuzzy], default_value: false, negatable: false
1705
-
1706
- c.desc 'Force exact string matching (case sensitive)'
1707
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1708
-
1709
- c.desc 'Show items that *don\'t* match search string'
1710
- c.switch [:not], default_value: false, negatable: false
1711
-
1712
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1713
- c.arg_name 'TYPE'
1714
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1715
-
1716
- c.desc "Highlight search matches in output. Only affects command line output"
1717
- c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
1718
-
1719
- c.desc "Edit matching entries with #{Doing::Util.default_editor}"
1720
- c.switch %i[e editor], negatable: false, default_value: false
1721
-
1722
- c.desc "Delete matching entries"
1723
- c.switch %i[d delete], negatable: false, default_value: false
1724
-
1725
- c.desc 'Display an interactive menu of results to perform further operations'
1726
- c.switch %i[i interactive], default_value: false, negatable: false
1727
-
1728
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1729
- c.arg_name 'QUERY'
1730
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1731
-
1732
- c.desc 'Combine multiple tags or value queries using AND, OR, or NOT'
1733
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
1734
-
1735
- c.action do |_global_options, options, args|
1736
- options[:fuzzy] = false
1737
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1738
-
1739
- template = settings['templates']['default'].deep_merge(settings)
1740
- tags_color = template.key?('tags_color') ? template['tags_color'] : nil
1741
-
1742
- section = wwid.guess_section(options[:section]) if options[:section]
1743
-
1744
- options[:case] = options[:case].normalize_case
1745
- options[:bool] = options[:bool].normalize_bool
1746
-
1747
- search = args.join(' ')
1748
- search.sub!(/^'?/, "'") if options[:exact]
1749
-
1750
- options[:times] = true if options[:totals]
1751
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
1752
- options[:highlight] = true
1753
- options[:search] = search
1754
- options[:section] = section
1755
- options[:tags_color] = tags_color
1756
-
1757
- Doing::Pager.page wwid.list_section(options)
1758
- end
1759
- end
1760
-
1761
- # @@last
1762
- desc 'Show the last entry, optionally edit'
1763
- long_desc 'Shows the last entry. Using --search and --tag filters, you can view/edit the last entry matching a filter,
1764
- allowing `doing last` to target historical entries.'
1765
- command :last do |c|
1766
- c.example 'doing last', desc: 'Show the most recent entry in all sections'
1767
- c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
1768
- c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
1769
- c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
1770
- c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
1771
- c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
1772
- c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
1773
-
1774
- c.desc 'Specify a section'
1775
- c.arg_name 'NAME'
1776
- c.flag %i[s section], default_value: 'All'
1777
-
1778
- c.desc "Edit entry with #{Doing::Util.default_editor}"
1779
- c.switch %i[e editor], negatable: false, default_value: false
1780
-
1781
- c.desc "Delete the last entry"
1782
- c.switch %i[d delete], negatable: false, default_value: false
1783
-
1784
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
1785
- c.arg_name 'TAG'
1786
- c.flag [:tag], type: TagArray
1787
-
1788
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
1789
- c.arg_name 'BOOLEAN'
1790
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1791
-
1792
- c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1793
- c.arg_name 'QUERY'
1794
- c.flag [:search]
1795
-
1796
- c.desc "Highlight search matches in output. Only affects command line output"
1797
- c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
1798
-
1799
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1800
- c.arg_name 'QUERY'
1801
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1802
-
1803
- c.desc 'Show elapsed time if entry is not tagged @done'
1804
- c.switch [:duration]
1805
-
1806
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
1807
- # c.switch [:fuzzy], default_value: false, negatable: false
1808
-
1809
- c.desc 'Force exact search string matching (case sensitive)'
1810
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
1811
-
1812
- c.desc 'Show items that *don\'t* match search string or tag filter'
1813
- c.switch [:not], default_value: false, negatable: false
1814
-
1815
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
1816
- c.arg_name 'TYPE'
1817
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
1818
-
1819
- c.action do |global_options, options, _args|
1820
- options[:fuzzy] = false
1821
- raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1822
-
1823
- if options[:tag].nil?
1824
- options[:tag] = []
1825
- else
1826
- options[:tag] = options[:tag]
1827
- options[:bool] = options[:bool].normalize_bool
1828
- end
1829
-
1830
- options[:case] = options[:case].normalize_case
1831
-
1832
- options[:search] = options[:search].sub(/^'?/, "'") if options[:search] && options[:exact]
1833
-
1834
- if options[:editor]
1835
- wwid.edit_last(section: options[:section],
1836
- options: {
1837
- search: options[:search],
1838
- fuzzy: options[:fuzzy],
1839
- case: options[:case],
1840
- tag: options[:tag],
1841
- tag_bool: options[:bool],
1842
- not: options[:not],
1843
- val: options[:val],
1844
- bool: options[:bool]
1845
- })
1846
- else
1847
- last = wwid.last(times: true, section: options[:section],
1848
- options: {
1849
- duration: options[:duration],
1850
- search: options[:search],
1851
- fuzzy: options[:fuzzy],
1852
- case: options[:case],
1853
- hilite: options[:hilite],
1854
- negate: options[:not],
1855
- tag: options[:tag],
1856
- tag_bool: options[:bool],
1857
- delete: options[:delete],
1858
- bool: options[:bool],
1859
- val: options[:val]
1860
- })
1861
- Doing::Pager::page last.strip if last
1862
- end
1863
- end
1864
- end
1865
-
1866
- # @@recent
1867
- desc 'List recent entries'
1868
- default_value 10
1869
- arg_name 'COUNT'
1870
- command :recent do |c|
1871
- c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections'
1872
- c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections'
1873
- c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section'
1874
- c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on'
1875
-
1876
- c.desc 'Section'
1877
- c.arg_name 'NAME'
1878
- c.flag %i[s section], default_value: 'All'
1879
-
1880
- c.desc 'Show time intervals on @done tasks'
1881
- c.switch %i[t times], default_value: true, negatable: true
1882
-
1883
- c.desc 'Show elapsed time on entries without @done tag'
1884
- c.switch [:duration]
1885
-
1886
- c.desc 'Show intervals with totals at the end of output'
1887
- c.switch [:totals], default_value: false, negatable: false
1888
-
1889
- c.desc 'Sort tags by (name|time)'
1890
- default = 'time'
1891
- default = settings['tag_sort'] || 'name'
1892
- c.arg_name 'KEY'
1893
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1894
-
1895
- c.desc 'Select from a menu of matching entries to perform additional operations'
1896
- c.switch %i[i interactive], negatable: false, default_value: false
1897
-
1898
- c.action do |global_options, options, args|
1899
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
1900
-
1901
- unless global_options[:version]
1902
- if settings['templates']['recent'].key?('count')
1903
- config_count = settings['templates']['recent']['count'].to_i
1904
- else
1905
- config_count = 10
1906
- end
1907
-
1908
- if options[:interactive]
1909
- count = 0
1910
- else
1911
- count = args.empty? ? config_count : args[0].to_i
1912
- end
1913
-
1914
- options[:times] = true if options[:totals]
1915
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
1916
-
1917
- template = settings['templates']['recent'].deep_merge(settings['templates']['default'])
1918
- tags_color = template.key?('tags_color') ? template['tags_color'] : nil
1919
-
1920
- opts = {
1921
- sort_tags: options[:sort_tags],
1922
- tags_color: tags_color,
1923
- times: options[:times],
1924
- totals: options[:totals],
1925
- interactive: options[:interactive],
1926
- duration: options[:duration]
1927
- }
1928
-
1929
- Doing::Pager::page wwid.recent(count, section.cap_first, opts)
1930
-
1931
- end
1932
- end
1933
- end
1934
-
1935
- # @@show
1936
- desc 'List all entries'
1937
- long_desc %(
1938
- The argument can be a section name, @tag(s) or both.
1939
- "pick" or "choose" as an argument will offer a section menu. Run with `--menu` to get a menu of available tags.
1940
-
1941
- Show tags by passing @tagname arguments. Multiple tags can be combined, and you can specify the boolean used to
1942
- combine them with `--bool (AND|OR|NOT)`. You can also use @+tagname to require a tag to match, or @-tagname to ignore
1943
- entries containing tagname. +/- operators require `--bool PATTERN` (which is the default).
1944
- )
1945
- arg_name '[SECTION|@TAGS]'
1946
- command :show do |c|
1947
- c.example 'doing show Currently', desc: 'Show entries in the Currently section'
1948
- c.example 'doing show @project1', desc: 'Show entries tagged @project1'
1949
- c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
1950
- c.example 'doing show @oracle @writing --bool and', desc: 'Show entries tagged both @oracle and @writing'
1951
- c.example 'doing show Currently @devo --bool not', desc: 'Show entries in Currently NOT tagged @devo'
1952
- c.example 'doing show Ideas @doing --from "mon to fri"', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.'
1953
- c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
1954
-
1955
- c.desc 'Tag filter, combine multiple tags with a comma. Use `--tag pick` for a menu of available tags. Wildcards allowed (*, ?). Added for compatibility with other commands'
1956
- c.arg_name 'TAG'
1957
- c.flag [:tag], type: TagArray
1958
-
1959
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
1960
- c.arg_name 'QUERY'
1961
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
1962
-
1963
- c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans'
1964
- c.arg_name 'BOOLEAN'
1965
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
1966
-
1967
- c.desc 'Max count to show'
1968
- c.arg_name 'MAX'
1969
- c.flag %i[c count], default_value: 0, must_match: /^\d+$/, type: Integer
1970
-
1971
- c.desc 'Age (oldest|newest)'
1972
- c.arg_name 'AGE'
1973
- c.flag %i[a age], default_value: 'newest'
1974
-
1975
- c.desc 'Show entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1976
- c.arg_name 'DATE_STRING'
1977
- c.flag [:before], type: DateBeginString
1978
-
1979
- c.desc 'Show entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
1980
- c.arg_name 'DATE_STRING'
1981
- c.flag [:after], type: DateEndString
1982
-
1983
- c.desc %(
1984
- Date range to show, or a single day to filter date on.
1985
- Date range argument should be quoted. Date specifications can be natural language.
1986
- To specify a range, use "to" or "through": `doing show --from "monday 8am to friday 5pm"`.
1987
-
1988
- If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
1989
- by time of day.
1990
- )
1991
-
1992
- c.arg_name 'DATE_OR_RANGE'
1993
- c.flag [:from], type: DateRangeString
1994
-
1995
- c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
1996
- c.arg_name 'QUERY'
1997
- c.flag [:search]
1998
-
1999
- c.desc "Highlight search matches in output. Only affects command line output"
2000
- c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
2001
-
2002
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2003
- # c.switch [:fuzzy], default_value: false, negatable: false
2004
-
2005
- c.desc 'Force exact search string matching (case sensitive)'
2006
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2007
-
2008
- c.desc 'Show items that *don\'t* match search/tag/date filters'
2009
- c.switch [:not], default_value: false, negatable: false
2010
-
2011
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2012
- c.arg_name 'TYPE'
2013
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2014
-
2015
- c.desc 'Sort order (asc/desc)'
2016
- c.arg_name 'ORDER'
2017
- c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
2018
-
2019
- c.desc 'Show time intervals on @done tasks'
2020
- c.switch %i[t times], default_value: true, negatable: true
2021
-
2022
- c.desc 'Show elapsed time on entries without @done tag'
2023
- c.switch [:duration]
2024
-
2025
- c.desc 'Show intervals with totals at the end of output'
2026
- c.switch [:totals], default_value: false, negatable: false
2027
-
2028
- c.desc 'Sort tags by (name|time)'
2029
- default = 'time'
2030
- default = settings['tag_sort'] || 'name'
2031
- c.arg_name 'KEY'
2032
- c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
2033
-
2034
- c.desc 'Tag sort direction (asc|desc)'
2035
- c.arg_name 'DIRECTION'
2036
- c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
2037
-
2038
- c.desc 'Only show items with recorded time intervals'
2039
- c.switch [:only_timed], default_value: false, negatable: false
2040
-
2041
- c.desc 'Select section or tag to display from a menu'
2042
- c.switch %i[m menu], negatable: false, default_value: false
2043
-
2044
- c.desc 'Select from a menu of matching entries to perform additional operations'
2045
- c.switch %i[i interactive], negatable: false, default_value: false
2046
-
2047
- c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
2048
- c.arg_name 'FORMAT'
2049
- c.flag %i[o output]
2050
- c.action do |global_options, options, args|
2051
- options[:fuzzy] = false
2052
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2053
-
2054
- tag_filter = false
2055
- tags = []
2056
-
2057
- if args.length.positive?
2058
- case args[0]
2059
- when /^all$/i
2060
- section = 'All'
2061
- args.shift
2062
- when /^(choose|pick)$/i
2063
- section = wwid.choose_section(include_all: true)
2064
-
2065
- args.shift
2066
- when /^[@+-]/
2067
- section = 'All'
2068
- else
2069
- begin
2070
- section = wwid.guess_section(args[0])
2071
- rescue WrongCommand => exception
2072
- cmd = commands[:view]
2073
- action = cmd.send(:get_action, nil)
2074
- return action.call(global_options, options, args)
2075
- end
2076
-
2077
- raise InvalidSection, "No such section: #{args[0]}" unless section
2078
-
2079
- args.shift
2080
- end
2081
- if args.length.positive?
2082
- args.each do |arg|
2083
- arg.split(/,/).each do |tag|
2084
- tags.push(tag.strip.sub(/^@/, ''))
2085
- end
2086
- end
2087
- end
2088
- else
2089
- section = options[:menu] ? wwid.choose_section(include_all: true) : settings['current_section']
2090
- section ||= 'All'
2091
- end
2092
-
2093
- tags.concat(options[:tag]) if options[:tag]
2094
-
2095
- options[:times] = true if options[:totals]
2096
-
2097
- template = settings['templates']['default'].deep_merge({
2098
- 'wrap_width' => settings['wrap_width'] || 0,
2099
- 'date_format' => settings['default_date_format'],
2100
- 'order' => settings['order'] || 'asc',
2101
- 'tags_color' => settings['tags_color']
2102
- })
2103
-
2104
- options[:case] = options[:case].normalize_case
2105
-
2106
- if options[:search]
2107
- search = options[:search]
2108
- search.sub!(/^'?/, "'") if options[:exact]
2109
- options[:search] = search
2110
- end
2111
-
2112
- options[:section] = section
2113
-
2114
- unless tags.empty?
2115
- tag_filter = {
2116
- 'tags' => tags,
2117
- 'bool' => options[:bool].normalize_bool
2118
- }
2119
- end
2120
-
2121
- options[:tag_filter] = tag_filter
2122
- options[:tag] = nil
2123
-
2124
- items = wwid.filter_items([], opt: options)
2125
-
2126
- if options[:menu]
2127
- tag = wwid.choose_tag(section, items: items, include_all: true)
2128
- raise UserCancelled unless tag
2129
-
2130
- # options[:bool] = :and unless tags.empty?
2131
-
2132
- tags = tag.split(/ +/).map { |t| t.strip.sub(/^@?/, '') } if tag =~ /^@/
2133
- unless tags.empty?
2134
- tag_filter = {
2135
- 'tags' => tags,
2136
- 'bool' => options[:bool].normalize_bool
2137
- }
2138
- options[:tag_filter] = tag_filter
2139
- end
2140
- end
2141
-
2142
- options[:age] ||= :newest
2143
-
2144
- opt = options.dup
2145
- opt[:age] = options[:age].normalize_age(:newest) if options[:age]
2146
- opt[:sort_tags] = options[:tag_sort] =~ /^n/i
2147
- opt[:count] = options[:count].to_i
2148
- opt[:highlight] = true
2149
- opt[:hilite] = options[:hilite]
2150
- opt[:order] = options[:sort].normalize_order
2151
- opt[:tag] = nil
2152
- opt[:tag_order] = options[:tag_order].normalize_order
2153
- opt[:tags_color] = template['tags_color']
2154
-
2155
- Doing::Pager.page wwid.list_section(opt, items: items)
2156
- end
2157
- end
2158
-
2159
- # @@tags
2160
- desc 'List all tags in the current Doing file'
2161
- command :tags do |c|
2162
- c.desc 'Section'
2163
- c.arg_name 'SECTION_NAME'
2164
- c.flag %i[s section], default_value: 'All'
2165
-
2166
- c.desc 'Show count of occurrences'
2167
- c.switch %i[c counts]
2168
-
2169
- c.desc 'Sort by name or count'
2170
- c.arg_name 'SORT_ORDER'
2171
- c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
2172
-
2173
- c.desc 'Sort order (asc/desc)'
2174
- c.arg_name 'ORDER'
2175
- c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
2176
-
2177
- c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?)'
2178
- c.arg_name 'TAG'
2179
- c.flag [:tag]
2180
-
2181
- c.desc 'Get tags for items matching search. Surround with
2182
- slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
2183
- c.arg_name 'QUERY'
2184
- c.flag [:search]
2185
-
2186
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
2187
- c.arg_name 'QUERY'
2188
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
2189
-
2190
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2191
- # c.switch [:fuzzy], default_value: false, negatable: false
2192
-
2193
- c.desc 'Force exact search string matching (case sensitive)'
2194
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2195
-
2196
- c.desc 'Get tags from items that *don\'t* match search/tag filters'
2197
- c.switch [:not], default_value: false, negatable: false
2198
-
2199
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2200
- c.arg_name 'TYPE'
2201
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2202
-
2203
- c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans'
2204
- c.arg_name 'BOOLEAN'
2205
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2206
-
2207
- c.desc 'Select items to scan from a menu of matching entries'
2208
- c.switch %i[i interactive], negatable: false, default_value: false
2209
-
2210
- c.action do |_global, options, args|
2211
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
2212
-
2213
- items = wwid.filter_items([], opt: options)
2214
-
2215
- if options[:interactive]
2216
- items = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
2217
- menu: true,
2218
- header: '',
2219
- prompt: 'Select entries to scan > ',
2220
- multiple: true,
2221
- sort: true,
2222
- show_if_single: true)
2223
- end
2224
-
2225
- # items = wwid.content.in_section(section)
2226
- tags = wwid.all_tags(items, counts: true)
2227
-
2228
- if options[:sort] =~ /^n/i
2229
- tags = tags.sort_by { |tag, count| tag }
2230
- else
2231
- tags = tags.sort_by { |tag, count| count }
2232
- end
2233
-
2234
- tags.reverse! if options[:order].normalize_order == 'desc'
2235
-
2236
- if options[:counts]
2237
- tags.each { |t, c| puts "#{t} (#{c})" }
2238
- else
2239
- tags.each { |t, c| puts "#{t}" }
2240
- end
2241
- end
2242
- end
2243
-
2244
- # @@today
2245
- desc 'List entries from today'
2246
- long_desc 'List entries from the current day. Use --before, --after, and
2247
- --from to specify time ranges.'
2248
- command :today do |c|
2249
- c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
2250
- c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
2251
- c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
2252
- c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
2253
-
2254
- c.desc 'Specify a section'
2255
- c.arg_name 'NAME'
2256
- c.flag %i[s section], default_value: 'All'
2257
-
2258
- c.desc 'Show time intervals on @done tasks'
2259
- c.switch %i[t times], default_value: true, negatable: true
2260
-
2261
- c.desc 'Show elapsed time on entries without @done tag'
2262
- c.switch [:duration]
2263
-
2264
- c.desc 'Show time totals at the end of output'
2265
- c.switch [:totals], default_value: false, negatable: false
2266
-
2267
- c.desc 'Sort tags by (name|time)'
2268
- default = 'time'
2269
- default = settings['tag_sort'] || 'name'
2270
- c.arg_name 'KEY'
2271
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
2272
-
2273
- c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
2274
- c.arg_name 'FORMAT'
2275
- c.flag %i[o output]
2276
-
2277
- c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
2278
- c.arg_name 'TIME_STRING'
2279
- c.flag [:before]
2280
-
2281
- c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)'
2282
- c.arg_name 'TIME_STRING'
2283
- c.flag [:after]
2284
-
2285
- c.desc %(
2286
- Time range to show `doing today --from "12pm to 4pm"`
2287
- )
2288
- c.arg_name 'TIME_RANGE'
2289
- c.flag [:from], type: DateRangeString
2290
-
2291
- c.action do |_global_options, options, _args|
2292
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2293
-
2294
- options[:times] = true if options[:totals]
2295
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
2296
- filter_options = %i[after before duration from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
2297
-
2298
- Doing::Pager.page wwid.today(options[:times], options[:output], filter_options).chomp
2299
- end
2300
- end
2301
-
2302
- # @@on
2303
- desc 'List entries for a date'
2304
- long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
2305
- and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
2306
- it will create a range.)
2307
- arg_name 'DATE_STRING'
2308
- command :on do |c|
2309
- c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
2310
- c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
2311
- c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
2312
-
2313
- c.desc 'Section'
2314
- c.arg_name 'NAME'
2315
- c.flag %i[s section], default_value: 'All'
2316
-
2317
- c.desc 'Show time intervals on @done tasks'
2318
- c.switch %i[t times], default_value: true, negatable: true
2319
-
2320
- c.desc 'Show elapsed time on entries without @done tag'
2321
- c.switch [:duration]
2322
-
2323
- c.desc 'Show time totals at the end of output'
2324
- c.switch [:totals], default_value: false, negatable: false
2325
-
2326
- c.desc 'Sort tags by (name|time)'
2327
- default = 'time'
2328
- default = settings['tag_sort'] || 'name'
2329
- c.arg_name 'KEY'
2330
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
2331
-
2332
- c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
2333
- c.arg_name 'FORMAT'
2334
- c.flag %i[o output]
2335
-
2336
- c.action do |_global_options, options, args|
2337
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2338
-
2339
- raise MissingArgument, 'Missing date argument' if args.empty?
2340
-
2341
- date_string = args.join(' ').strip
2342
- if date_string =~ /^tod(?:ay)?/i
2343
- date_string = 'today to tomorrow 12am'
2344
- end
2345
- start, finish = date_string.split_date_range
2346
-
2347
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
2348
-
2349
- message = "date interpreted as #{start}"
2350
- message += " to #{finish}" if finish
2351
- Doing.logger.debug('Interpreter:', message)
2352
-
2353
- options[:times] = true if options[:totals]
2354
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
2355
-
2356
- Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
2357
- { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2358
- end
2359
- end
2360
-
2361
- # @since
2362
- desc 'List entries since a date'
2363
- long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday,"
2364
- and "2d" would be interpreted as "two days ago.")
2365
- arg_name 'DATE_STRING'
2366
- command :since do |c|
2367
- c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
2368
- c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
2369
-
2370
- c.desc 'Section'
2371
- c.arg_name 'NAME'
2372
- c.flag %i[s section], default_value: 'All'
2373
-
2374
- c.desc 'Show time intervals on @done tasks'
2375
- c.switch %i[t times], default_value: true, negatable: true
2376
-
2377
- c.desc 'Show elapsed time on entries without @done tag'
2378
- c.switch [:duration]
2379
-
2380
- c.desc 'Show time totals at the end of output'
2381
- c.switch [:totals], default_value: false, negatable: false
2382
-
2383
- c.desc 'Sort tags by (name|time)'
2384
- default = 'time'
2385
- default = settings['tag_sort'] || 'name'
2386
- c.arg_name 'KEY'
2387
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
2388
-
2389
- c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
2390
- c.arg_name 'FORMAT'
2391
- c.flag %i[o output]
2392
-
2393
- c.action do |_global_options, options, args|
2394
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2395
-
2396
- raise MissingArgument, 'Missing date argument' if args.empty?
2397
-
2398
- date_string = args.join(' ')
2399
-
2400
- date_string.sub!(/(day) (\d)/, '\1 at \2')
2401
- date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
2402
-
2403
- start = date_string.chronify(guess: :begin)
2404
- finish = Time.now
2405
-
2406
- raise InvalidTimeExpression, 'Unrecognized date string' unless start
2407
-
2408
- Doing.logger.debug('Interpreter:', "date interpreted as #{start} through the current time")
2409
-
2410
- options[:times] = true if options[:totals]
2411
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
2412
-
2413
- Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
2414
- { duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
2415
- end
2416
- end
2417
-
2418
- # @@view
2419
- desc 'Display a user-created view'
2420
- long_desc 'Views are defined in your configuration (use `doing config` to edit).
2421
- Command line options override view configuration.'
2422
- arg_name 'VIEW_NAME'
2423
- command :view do |c|
2424
- c.example 'doing view color', desc: 'Display entries according to config for view "color"'
2425
- c.example 'doing view color --section Archive --count 10', desc: 'Display view "color", overriding some configured settings'
2426
-
2427
- c.desc 'Section'
2428
- c.arg_name 'NAME'
2429
- c.flag %i[s section]
2430
-
2431
- c.desc 'Count to display'
2432
- c.arg_name 'COUNT'
2433
- c.flag %i[c count], must_match: /^\d+$/, type: Integer
2434
-
2435
- c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
2436
- c.arg_name 'FORMAT'
2437
- c.flag %i[o output]
2438
-
2439
- c.desc 'Age (oldest|newest)'
2440
- c.arg_name 'AGE'
2441
- c.flag %i[age], default_value: 'newest'
2442
-
2443
- c.desc 'Show time intervals on @done tasks'
2444
- c.switch %i[t times], default_value: true, negatable: true
2445
-
2446
- c.desc 'Show elapsed time on entries without @done tag'
2447
- c.switch [:duration]
2448
-
2449
- c.desc 'Show intervals with totals at the end of output'
2450
- c.switch [:totals], default_value: false, negatable: false
2451
-
2452
- c.desc 'Include colors in output'
2453
- c.switch [:color], default_value: true, negatable: true
2454
-
2455
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
2456
- c.arg_name 'TAG'
2457
- c.flag [:tag]
2458
-
2459
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
2460
- c.arg_name 'QUERY'
2461
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
2462
-
2463
- c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans'
2464
- c.arg_name 'BOOLEAN'
2465
- c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
2466
-
2467
- c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
2468
- c.arg_name 'QUERY'
2469
- c.flag [:search]
2470
-
2471
- c.desc "Highlight search matches in output. Only affects command line output"
2472
- c.switch %i[h hilite], default_value: settings.dig('search', 'highlight')
2473
-
2474
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
2475
- # c.switch [:fuzzy], default_value: false, negatable: false
2476
-
2477
- c.desc 'Force exact search string matching (case sensitive)'
2478
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
2479
-
2480
- c.desc 'Show items that *don\'t* match search string'
2481
- c.switch [:not], default_value: false, negatable: false
2482
-
2483
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
2484
- c.arg_name 'TYPE'
2485
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
2486
-
2487
- c.desc 'Sort tags by (name|time)'
2488
- c.arg_name 'KEY'
2489
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i
2490
-
2491
- c.desc 'Tag sort direction (asc|desc)'
2492
- c.arg_name 'DIRECTION'
2493
- c.flag [:tag_order], must_match: REGEX_SORT_ORDER
2494
-
2495
- c.desc 'View entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
2496
- c.arg_name 'DATE_STRING'
2497
- c.flag [:before], type: DateBeginString
2498
-
2499
- c.desc 'View entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
2500
- c.arg_name 'DATE_STRING'
2501
- c.flag [:after], type: DateEndString
2502
-
2503
- c.desc %(
2504
- Date range to show, or a single day to filter date on.
2505
- Date range argument should be quoted. Date specifications can be natural language.
2506
- To specify a range, use "to" or "through": `doing view --from "monday 8am to friday 5pm" view_name`.
2507
-
2508
- If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
2509
- by time of day.
2510
- )
2511
- c.arg_name 'DATE_OR_RANGE'
2512
- c.flag [:from], type: DateRangeString
2513
-
2514
- c.desc 'Only show items with recorded time intervals (override view settings)'
2515
- c.switch [:only_timed], default_value: false, negatable: false
2516
-
2517
- c.desc 'Select from a menu of matching entries to perform additional operations'
2518
- c.switch %i[i interactive], negatable: false, default_value: false
2519
-
2520
- c.action do |global_options, options, args|
2521
- options[:fuzzy] = false
2522
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2523
-
2524
- raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
2525
-
2526
- title = if args.empty?
2527
- wwid.choose_view
2528
- else
2529
- begin
2530
- wwid.guess_view(args[0])
2531
- rescue WrongCommand => exception
2532
- cmd = commands[:show]
2533
- options[:sort] = 'asc'
2534
- options[:tag_order] = 'asc'
2535
- action = cmd.send(:get_action, nil)
2536
- return action.call(global_options, options, args)
2537
- end
2538
- end
2539
-
2540
- if options[:section]
2541
- section = wwid.guess_section(options[:section]) || options[:section].cap_first
2542
- else
2543
- section = settings['current_section']
2544
- end
2545
-
2546
- view = wwid.get_view(title)
2547
-
2548
- if view
2549
- page_title = view['title'] || title.cap_first
2550
- only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
2551
- true
2552
- else
2553
- false
2554
- end
2555
-
2556
- template = view['template'] || nil
2557
- date_format = view['date_format'] || nil
2558
-
2559
- tags_color = view['tags_color'] || nil
2560
- tag_filter = false
2561
- if options[:tag]
2562
- tag_filter = { 'tags' => [], 'bool' => 'OR' }
2563
- bool = options[:bool].normalize_bool
2564
- tag_filter['bool'] = bool
2565
- tag_filter['tags'] = if bool == :pattern
2566
- options[:tag]
2567
- else
2568
- options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2569
- end
2570
- elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?)
2571
- tag_filter = { 'tags' => [], 'bool' => 'OR' }
2572
- bool = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :pattern
2573
- tag_filter['bool'] = bool
2574
- tag_filter['tags'] = if view['tags'].instance_of?(Array)
2575
- bool == :pattern ? view['tags'].join(' ').strip : view['tags'].map(&:strip)
2576
- else
2577
- bool == :pattern ? view['tags'].strip : view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
2578
- end
2579
- end
2580
-
2581
- # If the -o/--output flag was specified, override any default in the view template
2582
- options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
2583
-
2584
- count = options[:count] ? options[:count] : view.key?('count') ? view['count'] : 10
2585
-
2586
- section = if options[:section]
2587
- section
2588
- else
2589
- view['section'] || settings['current_section']
2590
- end
2591
- order = view['order']&.normalize_order || 'asc'
2592
-
2593
- totals = if options[:totals]
2594
- true
2595
- else
2596
- view['totals'] || false
2597
- end
2598
- tag_order = if options[:tag_order]
2599
- options[:tag_order].normalize_order
2600
- else
2601
- view['tag_order']&.normalize_order || 'asc'
2602
- end
2603
-
2604
- options[:times] = true if totals
2605
- output_format = options[:output]&.downcase || 'template'
2606
-
2607
- options[:sort_tags] = if options[:tag_sort]
2608
- options[:tag_sort] =~ /^n/i ? true : false
2609
- elsif view.key?('tag_sort')
2610
- view['tag_sort'] =~ /^n/i ? true : false
2611
- else
2612
- false
2613
- end
2614
-
2615
- %w[before after from duration].each { |k| options[k.to_sym] = view[k] if view.key?(k) && !options[k.to_sym] }
2616
-
2617
- options[:case] = options[:case].normalize_case
2618
-
2619
- search = nil
2620
-
2621
- if options[:search]
2622
- search = options[:search]
2623
- search.sub!(/^'?/, "'") if options[:exact]
2624
- end
2625
-
2626
- options[:age] ||= :newest
2627
-
2628
- opts = options.dup
2629
- opts[:age] = options[:age].normalize_age(:newest)
2630
- opts[:count] = count
2631
- opts[:format] = date_format
2632
- opts[:highlight] = options[:color]
2633
- opts[:hilite] = options[:hilite]
2634
- opts[:only_timed] = only_timed
2635
- opts[:order] = order
2636
- opts[:output] = options[:interactive] ? nil : options[:output]
2637
- opts[:output] = output_format
2638
- opts[:page_title] = page_title
2639
- opts[:search] = search
2640
- opts[:section] = section
2641
- opts[:tag_filter] = tag_filter
2642
- opts[:tag_order] = tag_order
2643
- opts[:tags_color] = tags_color
2644
- opts[:template] = template
2645
- opts[:totals] = totals
2646
- opts[:view_template] = title
2647
-
2648
- Doing::Pager.page wwid.list_section(opts)
2649
- elsif title.instance_of?(FalseClass)
2650
- raise UserCancelled, 'Cancelled'
2651
- else
2652
- raise InvalidView, "View #{title} not found in config"
2653
- end
2654
- end
2655
- end
2656
-
2657
- # @@yesterday
2658
- desc 'List entries from yesterday'
2659
- long_desc 'Show only entries with start times within the previous 24 hour period. Use --before, --after, and --from to limit to
2660
- time spans within the day.'
2661
- command :yesterday do |c|
2662
- c.example 'doing yesterday', desc: 'List all entries from the previous day'
2663
- c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
2664
- c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers'
2665
-
2666
- c.desc 'Specify a section'
2667
- c.arg_name 'NAME'
2668
- c.flag %i[s section], default_value: 'All'
2669
-
2670
- c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
2671
- c.arg_name 'FORMAT'
2672
- c.flag %i[o output]
2673
-
2674
- c.desc 'Show time intervals on @done tasks'
2675
- c.switch %i[t times], default_value: true, negatable: true
2676
-
2677
- c.desc 'Show elapsed time on entries without @done tag'
2678
- c.switch [:duration]
2679
-
2680
- c.desc 'Show time totals at the end of output'
2681
- c.switch [:totals], default_value: false, negatable: false
2682
-
2683
- c.desc 'Sort tags by (name|time)'
2684
- default = settings['tag_sort'] || 'name'
2685
- c.arg_name 'KEY'
2686
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
2687
-
2688
- c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
2689
- c.arg_name 'TIME_STRING'
2690
- c.flag [:before]
2691
-
2692
- c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)'
2693
- c.arg_name 'TIME_STRING'
2694
- c.flag [:after]
2695
-
2696
- c.desc 'Time range to show, e.g. `doing yesterday --from "1am to 8am"`'
2697
- c.arg_name 'TIME_RANGE'
2698
- c.flag [:from], must_match: REGEX_TIME_RANGE
2699
-
2700
- c.desc 'Tag sort direction (asc|desc)'
2701
- c.arg_name 'DIRECTION'
2702
- c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
2703
-
2704
- c.action do |_global_options, options, _args|
2705
- raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2706
-
2707
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
2708
-
2709
- if options[:from]
2710
- options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
2711
- "yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
2712
- end.join(' to ').split_date_range
2713
- end
2714
-
2715
- opt = {
2716
- after: options[:after],
2717
- before: options[:before],
2718
- duration: options[:duration],
2719
- from: options[:from],
2720
- sort_tags: options[:sort_tags],
2721
- tag_order: options[:tag_order].normalize_order,
2722
- totals: options[:totals],
2723
- order: settings.dig('templates', 'today', 'order')
2724
- }
2725
- Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
2726
- end
172
+ def add_commands(commands)
173
+ commands = [commands] unless commands.is_a?(Array)
174
+ hidden = @settings['disabled_commands']
175
+ hidden = hidden.set_type('array') if hidden.good? && !hidden.is_a?(Array)
176
+ commands.delete_if { |c| hidden.include?(c) }
177
+ commands.each { |cmd| require_relative "commands/#{cmd}" }
2727
178
  end
2728
179
 
180
+ ## Add/modify commands
181
+ add_commands(%w[now done finish note select tag])
182
+ ## View commands
183
+ add_commands(%w[grep last recent show on view])
2729
184
  ## Utility commands
2730
-
2731
- # @@add_section
2732
- desc 'Add a new section to the "doing" file'
2733
- arg_name 'SECTION_NAME'
2734
- command :add_section do |c|
2735
- c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
2736
-
2737
- c.action do |_global_options, _options, args|
2738
- raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
2739
-
2740
- wwid.content.add_section(args.join(' ').cap_first, log: true)
2741
- wwid.write(wwid.doing_file)
2742
- end
2743
- end
2744
-
2745
- # @@config
2746
- desc 'Edit the configuration file or output a value from it'
2747
- long_desc %(Run without arguments, `doing config` opens your `config.yml` in an editor.
2748
- If local configurations are found in the path between the current directory
2749
- and the root (/), a menu will allow you to select which to open in the editor.
2750
-
2751
- It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
2752
-
2753
- Use `doing config get` to output the configuration to the terminal, and
2754
- provide a dot-separated key path to get a specific value. Shows the current value
2755
- including keys/overrides set by local configs.)
2756
- command :config do |c|
2757
- c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
2758
- c.example 'doing config get doing_file', desc: 'Output the value of a config key as YAML'
2759
- c.example 'doing config get plugins.plugin_path -o json', desc: 'Output the value of a key path as JSON'
2760
- c.example 'doing config set plugins.say.say_voice Alex', desc: 'Set the value of a key path and update config file'
2761
- c.example 'doing config set plug.say.voice Zarvox', desc: 'Key paths for get and set are fuzzy matched'
2762
-
2763
- c.default_command :edit
2764
-
2765
- c.desc 'DEPRECATED'
2766
- c.switch %i[d dump]
2767
-
2768
- c.desc 'DEPRECATED'
2769
- c.switch %i[u update]
2770
-
2771
- # @@config.list
2772
- c.desc 'List configuration paths, including .doingrc files in the current and parent directories'
2773
- c.long_desc 'Config files are listed in order of precedence (if there are multiple configs detected).
2774
- Values defined in the top item in the list will override values in configutations below it.'
2775
- c.command :list do |list|
2776
- list.action do |global, options, args|
2777
- puts config.additional_configs.join("\n")
2778
- puts config.config_file
2779
- end
2780
- end
2781
-
2782
- # @@config.edit
2783
- c.desc 'Open config file in editor'
2784
- c.command :edit do |edit|
2785
- edit.example 'doing config edit', desc: 'Open a config file in the default editor'
2786
- edit.example 'doing config edit --editor vim', desc: 'Open config in specific editor'
2787
-
2788
- edit.desc 'Editor to use'
2789
- edit.arg_name 'EDITOR'
2790
- edit.flag %i[e editor], default_value: nil
2791
-
2792
- if `uname` =~ /Darwin/
2793
- edit.desc 'Application to use'
2794
- edit.arg_name 'APP_NAME'
2795
- edit.flag %i[a app]
2796
-
2797
- edit.desc 'Application bundle id to use'
2798
- edit.arg_name 'BUNDLE_ID'
2799
- edit.flag %i[b bundle_id]
2800
-
2801
- edit.desc "Use the config_editor_app defined in ~/.config/doing/config.yml (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
2802
- edit.switch %i[x default]
2803
- end
2804
-
2805
- edit.action do |global, options, args|
2806
- if options[:update] || options[:dump]
2807
- cmd = commands[:config]
2808
- if options[:update]
2809
- cmd = cmd.commands[:update]
2810
- elsif options[:dump]
2811
- cmd = cmd.commands[:get]
2812
- end
2813
- action = cmd.send(:get_action, nil)
2814
- action.call(global, options, args)
2815
- Doing.logger.warn('Deprecated:', '--dump and --update are deprecated,
2816
- use `doing config get` and `doing config update`')
2817
- Doing.logger.output_results
2818
- return
2819
- end
2820
-
2821
- config_file = config.choose_config
2822
-
2823
- if `uname` =~ /Darwin/
2824
- if options[:default]
2825
- editor = Doing::Util.find_default_editor('config')
2826
- if editor
2827
- if Doing::Util.exec_available(editor.split(/ /).first)
2828
- system %(#{editor} "#{config_file}")
2829
- else
2830
- `open -a "#{editor}" "#{config_file}"`
2831
- end
2832
- else
2833
- raise InvalidArgument, 'No viable editor found in config or environment.'
2834
- end
2835
- elsif options[:app] || options[:bundle_id]
2836
- if options[:app]
2837
- `open -a "#{options[:app]}" "#{config_file}"`
2838
- elsif options[:bundle_id]
2839
- `open -b #{options[:bundle_id]} "#{config_file}"`
2840
- end
2841
- else
2842
- editor = options[:editor] || Doing::Util.find_default_editor('config')
2843
-
2844
- raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2845
-
2846
- if Doing::Util.exec_available(editor.split(/ /).first)
2847
- system %(#{editor} "#{config_file}")
2848
- else
2849
- `open -a "#{editor}" "#{config_file}"`
2850
- end
2851
- end
2852
- else
2853
- editor = options[:editor] || Doing::Util.default_editor
2854
- raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor.split(/ /).first)
2855
-
2856
- system %(#{editor} "#{config_file}")
2857
- end
2858
- end
2859
- end
2860
-
2861
- # @@config.update @@config.refresh
2862
- c.desc 'Update default config file, adding any missing keys'
2863
- c.command %i[update refresh] do |update|
2864
- update.action do |_global, options, args|
2865
- config.configure({rewrite: true, ignore_local: true})
2866
- Doing.logger.warn('Config:', 'config refreshed')
2867
- end
2868
- end
2869
-
2870
- # @@config.undo
2871
- c.desc 'Undo the last change to a config file'
2872
- c.command :undo do |undo|
2873
- undo.action do |_global, options, args|
2874
- config_file = config.choose_config
2875
- Doing::Util::Backup.restore_last_backup(config_file, count: 1)
2876
- end
2877
- end
2878
-
2879
- # @@config.get @@config.dump
2880
- c.desc 'Output a key\'s value'
2881
- c.arg 'KEY_PATH'
2882
- c.command %i[get dump] do |dump|
2883
- dump.example 'doing config get', desc: 'Output the entire configuration'
2884
- dump.example 'doing config get timer_format --output raw', desc: 'Output the value of timer_format as a plain string'
2885
- dump.example 'doing config get doing_file', desc: 'Output the value of the doing_file setting, respecting local configurations'
2886
- dump.example 'doing config get -o json plug.plugpath', desc: 'Key path is fuzzy matched: output the value of plugins->plugin_path as JSON'
2887
-
2888
- dump.desc 'Format for output (json|yaml|raw)'
2889
- dump.arg_name 'FORMAT'
2890
- dump.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
2891
-
2892
- dump.action do |_global, options, args|
2893
-
2894
- keypath = args.join('.')
2895
- cfg = config.value_for_key(keypath)
2896
- real_path = config.resolve_key_path(keypath)
2897
-
2898
- if cfg
2899
- val = cfg.map {|k, v| v }[0]
2900
- if real_path.count.positive?
2901
- nested_cfg = {}
2902
- nested_cfg.deep_set(real_path, val)
2903
- else
2904
- nested_cfg = val
2905
- end
2906
-
2907
- if options[:output] =~ /^r/
2908
- if val.is_a?(Hash)
2909
- $stdout.puts YAML.dump(val)
2910
- elsif val.is_a?(Array)
2911
- $stdout.puts val.join(', ')
2912
- else
2913
- $stdout.puts val.to_s
2914
- end
2915
- else
2916
- $stdout.puts case options[:output]
2917
- when /^j/
2918
- JSON.pretty_generate(val)
2919
- else
2920
- YAML.dump(nested_cfg)
2921
- end
2922
- end
2923
- else
2924
- Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
2925
- end
2926
- Doing.logger.output_results
2927
- return
2928
- end
2929
- end
2930
-
2931
- # @@config.set
2932
- c.desc 'Set a key\'s value in the config file'
2933
- c.arg 'KEY VALUE'
2934
- c.command :set do |set|
2935
- set.example 'doing config set timer_format human', desc: 'Set the value of timer_format to "human"'
2936
- set.example 'doing config set plug.plugpath ~/my_plugins', desc: 'Key path is fuzzy matched: set the value of plugins->plugin_path'
2937
-
2938
- set.desc 'Delete specified key'
2939
- set.switch %i[r remove], default_value: false, negatable: false
2940
-
2941
- set.action do |_global, options, args|
2942
- if args.count < 2 && !options[:remove]
2943
- raise InvalidArgument, 'config set requires at least two arguments, key path and value'
2944
-
2945
- end
2946
-
2947
- value = options[:remove] ? nil : args.pop
2948
- keypath = args.join('.')
2949
- real_path = config.resolve_key_path(keypath, create: true)
2950
- old_value = settings.dig(*real_path)
2951
- old_type = old_value&.class.to_s || nil
2952
-
2953
- if old_value.is_a?(Hash) && !options[:remove]
2954
- Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
2955
- didyou = 'Did you mean:'
2956
- old_value.keys.each do |k|
2957
- Doing.logger.log_now(:warn, "#{didyou}", "#{keypath}.#{k}?")
2958
- didyou = '..........or:'
2959
- end
2960
- raise InvalidArgument, 'Config value is a mapping, can not be set to a single value'
2961
-
2962
- end
2963
-
2964
- config_file = config.choose_config(create: true)
2965
-
2966
- cfg = YAML.safe_load_file(config_file) || {}
2967
-
2968
- $stderr.puts "Updating #{config_file}".yellow
2969
-
2970
- if options[:remove]
2971
- cfg.deep_set(real_path, nil)
2972
- $stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
2973
- else
2974
- current_value = cfg.dig(*real_path)
2975
- cfg.deep_set(real_path, value.set_type(old_type))
2976
- $stderr.puts "#{' Key path:'.yellow} #{real_path.join('->').boldwhite}"
2977
- $stderr.puts "#{'Inherited:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
2978
- $stderr.puts "#{' Current:'.yellow} #{ (current_value ? current_value.to_s : 'empty').boldwhite }"
2979
- $stderr.puts "#{' New:'.yellow} #{value.set_type(old_type).to_s.boldwhite}"
2980
- end
2981
-
2982
- res = Doing::Prompt.yn('Update selected config', default_response: true)
2983
-
2984
- raise UserCancelled, 'Cancelled' unless res
2985
-
2986
- Doing::Util.write_to_file(config_file, YAML.dump(cfg), backup: true)
2987
- Doing.logger.warn('Config:', "#{config_file} updated")
2988
- end
2989
- end
2990
- end
2991
-
2992
- # @@open
2993
- desc 'Open the "doing" file in an editor'
2994
- long_desc "`doing open` defaults to using the editors->doing_file setting
2995
- in #{config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
2996
- command :open do |c|
2997
- c.example 'doing open', desc: 'Open the doing file in the default editor'
2998
- c.desc 'Open with editor command (e.g. vim, mate)'
2999
- c.arg_name 'COMMAND'
3000
- c.flag %i[e editor]
3001
-
3002
- if `uname` =~ /Darwin/
3003
- c.desc 'Open with app name'
3004
- c.arg_name 'APP_NAME'
3005
- c.flag %i[a app]
3006
-
3007
- c.desc 'Open with app bundle id'
3008
- c.arg_name 'BUNDLE_ID'
3009
- c.flag %i[b bundle_id]
3010
- end
3011
-
3012
- c.action do |_global_options, options, _args|
3013
- params = options.dup
3014
- params.delete_if do |k, v|
3015
- k.instance_of?(String) || v.nil? || v == false
3016
- end
3017
-
3018
- if options[:editor]
3019
- raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor].split(/ /).first)
3020
-
3021
- editor = TTY::Which.which(options[:editor])
3022
- system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
3023
- elsif `uname` =~ /Darwin/
3024
- if options[:app]
3025
- system %(open -a "#{options[:app]}" "#{File.expand_path(wwid.doing_file)}")
3026
- elsif options[:bundle_id]
3027
- system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(wwid.doing_file)}")
3028
- elsif Doing::Util.find_default_editor('doing_file')
3029
- editor = Doing::Util.find_default_editor('doing_file')
3030
- if Doing::Util.exec_available(editor.split(/ /).first)
3031
- system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
3032
- else
3033
- system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
3034
- end
3035
- else
3036
- system %(open "#{File.expand_path(wwid.doing_file)}")
3037
- end
3038
- else
3039
- raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
3040
-
3041
- system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
3042
- end
3043
- end
3044
- end
3045
-
3046
- # @@tag_dir
3047
- desc 'Set the default tags for the current directory'
3048
- long_desc 'Adds default_tags to a .doingrc file in the current directory. Any entry created in this directory or its
3049
- subdirectories will be tagged with the default tags. You can modify these any time using the `config set` commnand or
3050
- manually editing the .doingrc file.'
3051
- arg_name 'TAG [TAG..]'
3052
- command :tag_dir do |c|
3053
- c.example 'doing tag_dir project1 project2', desc: 'Add @project1 and @project to to any entries in the current directory'
3054
- c.example 'doing tag_dir --remove', desc: 'Clear the default tags for the directory'
3055
-
3056
- c.desc 'Remove all default_tags from the local .doingrc'
3057
- c.switch %i[r remove], negatable: false
3058
-
3059
- c.action do |global, options, args|
3060
- tags = args.join(' ').gsub(/ *, */, ' ').split(' ')
3061
-
3062
- cfg_cmd = commands[:config]
3063
- set_cmd = cfg_cmd.commands[:set]
3064
- set_options = {}
3065
- if options[:remove]
3066
- set_args = ['default_tags']
3067
- set_options[:remove] = true
3068
- else
3069
- set_args = ['default_tags', tags.join(',')]
3070
- end
3071
- action = set_cmd.send(:get_action, nil)
3072
- return action.call(global, set_options, set_args)
3073
- end
3074
- end
3075
-
185
+ add_commands(%w[config open commands])
3076
186
  ## File handling/batch modification commands
3077
-
3078
- # @@archive @@move
3079
- desc 'Move entries between sections'
3080
- long_desc %(Argument can be a section name to move all entries from a section,
3081
- or start with an "@" to move entries matching a tag.
3082
-
3083
- Default with no argument moves items from the "#{settings['current_section']}" section to Archive.)
3084
- arg_name 'SECTION_OR_TAG'
3085
- default_value settings['current_section']
3086
- command %i[archive move] do |c|
3087
- c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section'
3088
- c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive'
3089
- c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section'
3090
- c.example 'doing move Later --tag project1 --to Currently', desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)'
3091
-
3092
- c.desc 'How many items to keep (ignored if archiving by tag or search)'
3093
- c.arg_name 'X'
3094
- c.flag %i[k keep], must_match: /^\d+$/, type: Integer
3095
-
3096
- c.desc 'Move entries to'
3097
- c.arg_name 'SECTION_NAME'
3098
- c.flag %i[t to], default_value: 'Archive'
3099
-
3100
- c.desc 'Label moved items with @from(SECTION_NAME)'
3101
- c.switch [:label], default_value: true, negatable: true
3102
-
3103
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands'
3104
- c.arg_name 'TAG'
3105
- c.flag [:tag], type: TagArray
3106
-
3107
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
3108
- c.arg_name 'BOOLEAN'
3109
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
3110
-
3111
- c.desc 'Search filter'
3112
- c.arg_name 'QUERY'
3113
- c.flag [:search]
3114
-
3115
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
3116
- c.arg_name 'QUERY'
3117
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
3118
-
3119
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
3120
- # c.switch [:fuzzy], default_value: false, negatable: false
3121
-
3122
- c.desc 'Force exact search string matching (case sensitive)'
3123
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
3124
-
3125
- c.desc 'Show items that *don\'t* match search string'
3126
- c.switch [:not], default_value: false, negatable: false
3127
-
3128
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
3129
- c.arg_name 'TYPE'
3130
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
3131
-
3132
- c.desc 'Archive entries older than date
3133
- (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
3134
- c.arg_name 'DATE_STRING'
3135
- c.flag [:before], type: DateEndString
3136
-
3137
- c.action do |_global_options, options, args|
3138
- options[:fuzzy] = false
3139
- if args.empty?
3140
- section = settings['current_section']
3141
- tags = []
3142
- elsif args[0] =~ /^all/i
3143
- section = 'all'
3144
- elsif args[0] =~ /^@\S+/
3145
- section = 'all'
3146
- tags = args.map { |t| t.sub(/^@/, '').strip }
3147
- else
3148
- section = args[0].cap_first
3149
- tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
3150
- end
3151
-
3152
- raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
3153
-
3154
- tags.concat(options[:tag]) if options[:tag]
3155
-
3156
- search = nil
3157
-
3158
- options[:case] = options[:case].normalize_case
3159
-
3160
- if options[:search]
3161
- search = options[:search]
3162
- search.sub!(/^'?/, "'") if options[:exact]
3163
- end
3164
-
3165
- opts = options.dup
3166
- opts[:search] = search
3167
- opts[:bool] = options[:bool].normalize_bool
3168
- opts[:destination] = options[:to]
3169
- opts[:tags] = tags
3170
-
3171
- wwid.archive(section, opts)
3172
- end
3173
- end
3174
-
3175
- # @@import
3176
- desc 'Import entries from an external source'
3177
- long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
3178
- arg_name 'PATH'
3179
- command :import do |c|
3180
- c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
3181
- c.example 'doing import --type doing --tag imported --no-autotag ~/doing_backup.md', desc: 'Import an Doing archive, tag all entries with @imported, skip autotagging'
3182
- c.example 'doing import --type doing --from "10/1 to 10/15" ~/doing_backup.md', desc: 'Import a Doing archive, only importing entries between two dates'
3183
-
3184
- c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
3185
- c.arg_name 'TYPE'
3186
- c.flag :type, default_value: 'doing'
3187
-
3188
- c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
3189
- c.arg_name 'QUERY'
3190
- c.flag [:search]
3191
-
3192
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
3193
- # c.switch [:fuzzy], default_value: false, negatable: false
3194
-
3195
- c.desc 'Force exact search string matching (case sensitive)'
3196
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
3197
-
3198
- c.desc 'Import items that *don\'t* match search/tag/date filters'
3199
- c.switch [:not], default_value: false, negatable: false
3200
-
3201
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
3202
- c.arg_name 'TYPE'
3203
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
3204
-
3205
- c.desc 'Only import items with recorded time intervals'
3206
- c.switch [:only_timed], default_value: false, negatable: false
3207
-
3208
- c.desc 'Target section'
3209
- c.arg_name 'NAME'
3210
- c.flag %i[s section]
3211
-
3212
- c.desc 'Tag all imported entries'
3213
- c.arg_name 'TAGS'
3214
- c.flag %i[t tag]
3215
-
3216
- c.desc 'Autotag entries'
3217
- c.switch :autotag, negatable: true, default_value: true
3218
-
3219
- c.desc 'Prefix entries with'
3220
- c.arg_name 'PREFIX'
3221
- c.flag :prefix
3222
-
3223
- # TODO: Allow time range filtering
3224
- c.desc 'Import entries older than date'
3225
- c.arg_name 'DATE_STRING'
3226
- c.flag [:before], type: DateBeginString
3227
-
3228
- c.desc 'Import entries newer than date'
3229
- c.arg_name 'DATE_STRING'
3230
- c.flag [:after], type: DateEndString
3231
-
3232
- c.desc %(
3233
- Date range to import. Date range argument should be quoted. Date specifications can be natural language.
3234
- To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
3235
- Has no effect unless the import plugin has implemented date range filtering.
3236
- )
3237
- c.arg_name 'DATE_OR_RANGE'
3238
- c.flag %i[f from], type: DateRangeString
3239
-
3240
- c.desc 'Allow entries that overlap existing times'
3241
- c.switch [:overlap], negatable: true
3242
-
3243
- c.action do |_global_options, options, args|
3244
- options[:fuzzy] = false
3245
- if options[:section]
3246
- options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
3247
- end
3248
-
3249
- if options[:search]
3250
- search = options[:search]
3251
- search.sub!(/^'?/, "'") if options[:exact]
3252
- options[:search] = search
3253
- end
3254
-
3255
- if options[:from]
3256
- options[:date_filter] = options[:from]
3257
-
3258
- raise InvalidTimeExpression, 'Unrecognized date string' unless options[:date_filter][0]
3259
- elsif options[:before] || options[:after]
3260
- options[:date_filter] = [nil, nil]
3261
- options[:date_filter][1] = options[:before] || Time.now + (1 << 64)
3262
- options[:date_filter][0] = options[:after] || Time.now - (1 << 64)
3263
- end
3264
-
3265
- options[:case] = options[:case].normalize_case
3266
-
3267
- if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
3268
- options[:no_overlap] = !options[:overlap]
3269
- wwid.import(args, options)
3270
- wwid.write(wwid.doing_file)
3271
- else
3272
- raise InvalidPluginType, "Invalid import type: #{options[:type]}"
3273
- end
3274
- end
3275
- end
3276
-
3277
- # @@rotate
3278
- desc 'Move entries to archive file'
3279
- long_desc 'As your doing file grows, commands can get slow. Given that your historical data (and your archive section)
3280
- probably aren\'t providing any useful insights a year later, use this command to "rotate" old entries out to an archive
3281
- file. You\'ll still have access to all historical data, but it won\'t be slowing down daily operation.'
3282
- command :rotate do |c|
3283
- c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
3284
- 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'
3285
- c.example 'doing rotate --tag project1,done --bool AND', desc: 'Move entries tagged @project1 and @done to a secondary file'
3286
-
3287
- c.desc 'How many items to keep in each section (most recent)'
3288
- c.arg_name 'X'
3289
- c.flag %i[k keep], must_match: /^\d+$/, type: Integer
3290
-
3291
- c.desc 'Section to rotate'
3292
- c.arg_name 'SECTION_NAME'
3293
- c.flag %i[s section], default_value: 'All'
3294
-
3295
- c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands'
3296
- c.arg_name 'TAG'
3297
- c.flag [:tag]
3298
-
3299
- c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
3300
- c.arg_name 'BOOLEAN'
3301
- c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
3302
-
3303
- c.desc 'Search filter'
3304
- c.arg_name 'QUERY'
3305
- c.flag [:search]
3306
-
3307
- c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
3308
- c.arg_name 'QUERY'
3309
- c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
3310
-
3311
- # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
3312
- # c.switch [:fuzzy], default_value: false, negatable: false
3313
-
3314
- c.desc 'Force exact search string matching (case sensitive)'
3315
- c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
3316
-
3317
- c.desc 'Rotate items that *don\'t* match search string or tag filter'
3318
- c.switch [:not], default_value: false, negatable: false
3319
-
3320
- c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
3321
- c.arg_name 'TYPE'
3322
- c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
3323
-
3324
- c.desc 'Rotate entries older than date
3325
- (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
3326
- c.arg_name 'DATE_STRING'
3327
- c.flag [:before]
3328
-
3329
- c.action do |_global_options, options, args|
3330
- options[:fuzzy] = false
3331
- if options[:section] && options[:section] !~ /^all$/i
3332
- options[:section] = wwid.guess_section(options[:section])
3333
- end
3334
-
3335
- options[:bool] = options[:bool].normalize_bool
3336
-
3337
- options[:case] = options[:case].normalize_case
3338
-
3339
- search = nil
3340
-
3341
- if options[:search]
3342
- search = options[:search]
3343
- search.sub!(/^'?/, "'") if options[:exact]
3344
- options[:search] = search
3345
- end
3346
-
3347
- wwid.rotate(options)
3348
- end
3349
- end
3350
-
3351
- ## Utility commands
3352
-
3353
- # @@colors
3354
- desc 'List available color variables for configuration templates and views'
3355
- command :colors do |c|
3356
- c.action do |_global_options, _options, _args|
3357
- bgs = []
3358
- fgs = []
3359
- colors::attributes.each do |color|
3360
- if color.to_s =~ /bg/
3361
- bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}")
3362
- else
3363
- fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
3364
- end
3365
- end
3366
- out = []
3367
- out << fgs.join("\n")
3368
- out << bgs.join("\n")
3369
- Doing::Pager.page out.join("\n")
3370
- end
3371
- end
3372
-
3373
- # @@completion
3374
- desc 'Generate shell completion scripts'
3375
- long_desc 'Generates the necessary scripts to add command line completion to various shells, so typing \'doing\' and hitting
3376
- tab will offer completions of subcommands and their options.'
3377
- command :completion do |c|
3378
- c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
3379
- c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
3380
- c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
3381
- c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
3382
-
3383
- c.desc 'Shell to generate for (bash, zsh, fish)'
3384
- c.arg_name 'SHELL'
3385
- c.flag %i[t type], must_match: /^(?:[bzf](?:[ai]?sh)?|all)$/i, default_value: 'zsh'
3386
-
3387
- c.desc 'File to write output to'
3388
- c.arg_name 'PATH'
3389
- c.flag %i[f file], default_value: 'STDOUT'
3390
-
3391
- c.action do |_global_options, options, _args|
3392
- script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
3393
-
3394
- Doing::Completion.generate_completion(type: options[:type], file: options[:file])
3395
- end
3396
- end
3397
-
3398
- # @@plugins
3399
- desc 'List installed plugins'
3400
- long_desc %(Lists available plugins, including user-installed plugins.
3401
-
3402
- Export plugins are available with the `--output` flag on commands that support it.
3403
-
3404
- Import plugins are available using `doing import --type PLUGIN`.
3405
- )
3406
- command :plugins do |c|
3407
- c.example 'doing plugins', desc: 'List all plugins'
3408
- c.example 'doing plugins -t import', desc: 'List all import plugins'
3409
-
3410
- c.desc 'List plugins of type (import, export)'
3411
- c.arg_name 'TYPE'
3412
- c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
3413
-
3414
- c.desc 'List in single column for completion'
3415
- c.switch %i[c column], negatable: false, default_value: false
3416
-
3417
- c.action do |_global_options, options, _args|
3418
- Doing::Plugins.list_plugins(options)
3419
- end
3420
- end
3421
-
3422
- # @@sections
3423
- desc 'List sections'
3424
- command :sections do |c|
3425
- c.desc 'List in single column'
3426
- c.switch %i[c column], negatable: false, default_value: false
3427
-
3428
- c.action do |_global_options, options, _args|
3429
- joiner = options[:column] ? "\n" : "\t"
3430
- print wwid.content.section_titles.join(joiner)
3431
- end
3432
- end
3433
-
3434
- # @@template
3435
- desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
3436
- long_desc %(
3437
- Templates are printed to STDOUT for piping to a file.
3438
- Save them and use them in the configuration file under export_templates.
3439
- )
3440
- arg_name 'TYPE', must_match: Doing::Plugins.template_regex
3441
- command :template do |c|
3442
- c.example 'doing template haml > ~/styles/my_doing.haml', desc: 'Output the haml template and save it to a file'
3443
-
3444
- c.desc 'List all available templates'
3445
- c.switch %i[l list], negatable: false
3446
-
3447
- c.desc 'List in single column for completion'
3448
- c.switch %i[c column]
3449
-
3450
- c.desc 'Save template to file instead of STDOUT'
3451
- c.switch %i[s save], default_value: false, negatable: false
3452
-
3453
- c.desc 'Save template to alternate location'
3454
- c.arg_name 'DIRECTORY'
3455
- c.flag %i[p path], default_value: File.join(Doing::Util.user_home, '.config', 'doing', 'templates')
3456
-
3457
- c.action do |_global_options, options, args|
3458
- if options[:list] || options[:column]
3459
- if options[:column]
3460
- $stdout.print Doing::Plugins.plugin_templates.join("\n")
3461
- else
3462
- $stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
3463
- end
3464
- return
3465
- end
3466
-
3467
- if args.empty?
3468
- type = Doing::Prompt.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
3469
- type.sub!(/ \(.*?\)$/, '').strip!
3470
- options[:save] = Doing::Prompt.yn("Save to #{options[:path]}? (No outputs to STDOUT)", default_response: false)
3471
- else
3472
- type = args[0]
3473
- end
3474
-
3475
- raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
3476
-
3477
- if options[:save]
3478
- Doing::Plugins.template_for_trigger(type, save_to: options[:path])
3479
- else
3480
- $stdout.puts Doing::Plugins.template_for_trigger(type, save_to: nil)
3481
- end
3482
-
3483
- # case args[0]
3484
- # when /html|haml/i
3485
- # $stdout.puts wwid.haml_template
3486
- # when /css/i
3487
- # $stdout.puts wwid.css_template
3488
- # when /markdown|md|erb/i
3489
- # $stdout.puts wwid.markdown_template
3490
- # else
3491
- # exit_now! 'Invalid type specified, must be HAML or CSS'
3492
- # end
3493
- end
3494
- end
3495
-
3496
- # @@views
3497
- desc 'List available custom views'
3498
- command :views do |c|
3499
- c.desc 'List in single column'
3500
- c.switch %i[c column], default_value: false
3501
-
3502
- c.action do |_global_options, options, _args|
3503
- joiner = options[:column] ? "\n" : "\t"
3504
- print wwid.views.join(joiner)
3505
- end
3506
- end
3507
-
187
+ add_commands(%w[archive import rotate])
3508
188
  ## History commands
3509
-
3510
- # @@undo
3511
- desc 'Undo the last X changes to the Doing file'
3512
- long_desc 'Reverts the last X commands that altered the doing file.
3513
- All changes performed by a single command are undone at once.
3514
-
3515
- Specify a number to jump back multiple revisions, or use --select for an interactive menu.'
3516
- arg_name 'COUNT'
3517
- command :undo do |c|
3518
- c.example 'doing undo', desc: 'Undo the most recent change to the doing file'
3519
- c.example 'doing undo 5', desc: 'Undo the last 5 changes to the doing file'
3520
- c.example 'doing undo --interactive', desc: 'Select from a menu of available revisions'
3521
- c.example 'doing undo --redo', desc: 'Undo the last undo command'
3522
-
3523
- c.desc 'Specify alternate doing file'
3524
- c.arg_name 'PATH'
3525
- c.flag %i[f file], default_value: wwid.doing_file
3526
-
3527
- c.desc 'Select from recent backups'
3528
- c.switch %i[i interactive], negatable: false
3529
-
3530
- c.desc 'Remove old backups, retaining X files'
3531
- c.arg_name 'COUNT'
3532
- c.flag %i[p prune], type: Integer
3533
-
3534
- c.desc 'Redo last undo. Note: you cannot undo a redo'
3535
- c.switch %i[r redo]
3536
-
3537
- c.action do |_global_options, options, args|
3538
- file = options[:file] || wwid.doing_file
3539
- count = args.empty? ? 1 : args[0].to_i
3540
- raise InvalidArgument, "Invalid count specified for undo" unless count&.positive?
3541
-
3542
- if options[:prune]
3543
- Doing::Util::Backup.prune_backups(file, options[:prune])
3544
- elsif options[:redo]
3545
- if options[:interactive]
3546
- Doing::Util::Backup.select_redo(file)
3547
- else
3548
- Doing::Util::Backup.redo_backup(file, count: count)
3549
- end
3550
- else
3551
- if options[:interactive]
3552
- Doing::Util::Backup.select_backup(file)
3553
- else
3554
- Doing::Util::Backup.restore_last_backup(file, count: count)
3555
- end
3556
- end
3557
- end
3558
- end
3559
-
3560
- # @@redo
3561
- long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo'
3562
- arg_name 'COUNT'
3563
- command :redo do |c|
3564
- c.desc 'Specify alternate doing file'
3565
- c.arg_name 'PATH'
3566
- c.flag %i[f file], default_value: wwid.doing_file
3567
-
3568
- c.desc 'Select from an interactive menu'
3569
- c.switch %i[i interactive]
3570
-
3571
- c.action do |_global, options, args|
3572
- file = options[:file] || wwid.doing_file
3573
- count = args.empty? ? 1 : args[0].to_i
3574
- raise InvalidArgument, "Invalid count specified for redo" unless count&.positive?
3575
- if options[:interactive]
3576
- Doing::Util::Backup.select_redo(file)
3577
- else
3578
- Doing::Util::Backup.redo_backup(file, count: count)
3579
- end
3580
- end
3581
- end
3582
-
3583
- # @@changelog @@changes
3584
- desc 'List recent changes in Doing'
3585
- long_desc 'Display a formatted list of changes in recent versions, latest at the top'
3586
- command %i[changelog changes] do |c|
3587
- c.action do |_global_options, options, args|
3588
- changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
3589
- if File.exist?(changelog)
3590
- parsed = TTY::Markdown.parse(IO.read(changelog), width: 80, symbols: {override: {bullet: "•"}})
3591
- Doing::Pager.paginate = true
3592
- Doing::Pager.page parsed
3593
- else
3594
- raise "Error locating changelog"
3595
- end
3596
- end
3597
- end
3598
-
189
+ add_commands(%w[undo redo])
3599
190
  ## Hidden commands
191
+ add_commands(%w[commands_accepting install_fzf])
192
+ ## Optional commands
193
+ add_commands(%w[again cancel flag meanwhile reset tags today yesterday since add_section tag_dir colors completion plugins sections template views changes])
3600
194
 
3601
- # @@commands_accepting
3602
- arg_name 'OPTION'
3603
- command :commands_accepting do |c|
3604
- c.desc 'Output in single column for completion'
3605
- c.switch %i[c column]
3606
-
3607
- c.action do |g, o, a|
3608
- a.each do |option|
3609
- cmds = []
3610
- commands.each do |cmd, v|
3611
- v.flags.merge(v.switches).each do |n, flag|
3612
- if flag.name == option.to_sym || flag.aliases&.include?(option.to_sym)
3613
- cmds.push(cmd)
3614
- end
3615
- end
3616
- end
3617
-
3618
- if o[:column]
3619
- puts cmds.sort
3620
- else
3621
- puts "Commands accepting --#{option}: #{cmds.sort.join(', ')}"
3622
- end
3623
- end
3624
- end
3625
- end
3626
-
3627
- # @@install_fzf
3628
- command :install_fzf do |c|
3629
- c.desc 'Force reinstall'
3630
- c.switch %i[r reinstall], default_value: false
3631
-
3632
- c.desc 'Uninstall'
3633
- c.switch %i[u uninstall], default_value: false, negatable: false
3634
-
3635
- c.action do |g, o, a|
3636
- if o[:uninstall]
3637
- Doing::Prompt.uninstall_fzf
3638
- else
3639
- Doing.logger.warn('fzf:', 'force reinstall') if o[:reinstall]
3640
- res = Doing::Prompt.install_fzf(force: o[:reinstall])
3641
- end
3642
- end
3643
- end
3644
-
3645
- ## Doing::Hooks
3646
195
 
3647
196
  pre do |global, _command, _options, _args|
3648
- # global[:pager] ||= settings['paginate']
197
+ # global[:pager] ||= @settings['paginate']
3649
198
  Doing::Pager.paginate = global[:pager]
3650
199
 
3651
200
  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
@@ -3683,7 +232,7 @@ post do |global, _command, _options, _args|
3683
232
  end
3684
233
 
3685
234
  around do |global, command, options, arguments, code|
3686
- # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
235
+ # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{@settings['paginate']}, Pager: #{Doing::Pager.paginate}")
3687
236
  if env_log_level.nil?
3688
237
  Doing.logger.adjust_verbosity(global)
3689
238
  end
@@ -3708,28 +257,28 @@ around do |global, command, options, arguments, code|
3708
257
  Doing.config.force_answer = global[:default] ? true : false
3709
258
  end
3710
259
 
3711
- if global[:config_file] && global[:config_file] != config.config_file
3712
- 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))
3713
- 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))
260
+ if global[:config_file] && global[:config_file] != @config.config_file
261
+ 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))
262
+ 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))
3714
263
 
3715
264
  cf = File.expand_path(global[:config_file])
3716
265
  raise MissingConfigFile, "Config file not found (#{global[:config_file]})" unless File.exist?(cf)
3717
266
 
3718
- config.config_file = cf
3719
- settings = config.configure({ ignore_local: true })
267
+ @config.config_file = cf
268
+ @settings = @config.configure({ ignore_local: true })
3720
269
  end
3721
270
  Doing.logger.benchmark(:init, :start)
3722
271
  if global[:doing_file]
3723
- wwid.init_doing_file(global[:doing_file])
272
+ @wwid.init_doing_file(global[:doing_file])
3724
273
  else
3725
- wwid.init_doing_file
274
+ @wwid.init_doing_file
3726
275
  end
3727
276
  Doing.logger.benchmark(:init, :finish)
3728
- wwid.auto_tag = !global[:noauto]
277
+ @wwid.auto_tag = !global[:noauto]
3729
278
 
3730
- settings[:include_notes] = false unless global[:notes]
279
+ @settings[:include_notes] = false unless global[:notes]
3731
280
 
3732
- global[:wwid] = wwid
281
+ global[:wwid] = @wwid
3733
282
 
3734
283
  code.call
3735
284
  end