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