doing 2.1.24 → 2.1.28

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