doing 2.1.24 → 2.1.28

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