doing 2.1.25 → 2.1.29

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