doing 2.1.25 → 2.1.29

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