doing 2.1.0pre → 2.1.4pre
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/.yardoc/checksums +13 -9
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +42 -10
- data/Gemfile.lock +23 -1
- data/README.md +1 -1
- data/Rakefile +2 -0
- data/bin/doing +421 -156
- data/doc/Array.html +1 -1
- data/doc/Doing/Color.html +1 -1
- data/doc/Doing/Completion.html +1 -1
- data/doc/Doing/Configuration.html +81 -90
- data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/doc/Doing/Errors/EmptyInput.html +1 -1
- data/doc/Doing/Errors/NoResults.html +1 -1
- data/doc/Doing/Errors/PluginException.html +1 -1
- data/doc/Doing/Errors/UserCancelled.html +1 -1
- data/doc/Doing/Errors/WrongCommand.html +1 -1
- data/doc/Doing/Errors.html +1 -1
- data/doc/Doing/Hooks.html +1 -1
- data/doc/Doing/Item.html +84 -20
- data/doc/Doing/Items.html +35 -1
- data/doc/Doing/LogAdapter.html +1 -1
- data/doc/Doing/Note.html +1 -1
- data/doc/Doing/Pager.html +1 -1
- data/doc/Doing/Plugins.html +1 -1
- data/doc/Doing/Prompt.html +70 -18
- data/doc/Doing/Section.html +1 -1
- data/doc/Doing/Util.html +16 -4
- data/doc/Doing/WWID.html +27 -147
- data/doc/Doing.html +3 -3
- data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/doc/GLI/Commands.html +1 -1
- data/doc/GLI.html +1 -1
- data/doc/Hash.html +1 -1
- data/doc/Status.html +1 -1
- data/doc/String.html +344 -4
- data/doc/Symbol.html +1 -1
- data/doc/Time.html +70 -2
- data/doc/_index.html +125 -4
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +2 -2
- data/doc/index.html +2 -2
- data/doc/method_list.html +537 -193
- data/doc/top-level-namespace.html +2 -2
- data/doing.gemspec +2 -0
- data/doing.rdoc +276 -75
- data/lib/completion/doing.bash +20 -20
- data/lib/doing/boolean_term_parser.rb +86 -0
- data/lib/doing/configuration.rb +36 -19
- data/lib/doing/item.rb +102 -9
- data/lib/doing/items.rb +6 -0
- data/lib/doing/phrase_parser.rb +124 -0
- data/lib/doing/plugins/export/template_export.rb +29 -2
- data/lib/doing/prompt.rb +21 -11
- data/lib/doing/string.rb +47 -3
- data/lib/doing/string_chronify.rb +85 -0
- data/lib/doing/time.rb +32 -0
- data/lib/doing/util.rb +2 -5
- data/lib/doing/util_backup.rb +235 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +224 -124
- data/lib/doing.rb +7 -0
- metadata +46 -2
data/bin/doing
CHANGED
@@ -25,7 +25,7 @@ version Doing::VERSION
|
|
25
25
|
hide_commands_without_desc true
|
26
26
|
autocomplete_commands true
|
27
27
|
|
28
|
-
REGEX_BOOL = /^(?:and|all|any|or|not|none)$/i
|
28
|
+
REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i
|
29
29
|
REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i
|
30
30
|
|
31
31
|
InvalidExportType = Class.new(RuntimeError)
|
@@ -146,9 +146,9 @@ command %i[now next] do |c|
|
|
146
146
|
|
147
147
|
c.action do |_global_options, options, args|
|
148
148
|
if options[:back]
|
149
|
-
date =
|
149
|
+
date = options[:back].chronify(guess: :begin)
|
150
150
|
|
151
|
-
raise InvalidTimeExpression.new('unable to parse date string', topic: '
|
151
|
+
raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
|
152
152
|
else
|
153
153
|
date = Time.now
|
154
154
|
end
|
@@ -195,7 +195,16 @@ command %i[now next] do |c|
|
|
195
195
|
end
|
196
196
|
|
197
197
|
desc 'Reset the start time of an entry'
|
198
|
+
long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
|
199
|
+
If no argument is provided, the start time will be reset to the current time.
|
200
|
+
If a date string is provided as an argument, the start time will be set to the parsed result.'
|
201
|
+
arg_name 'DATE_STRING'
|
198
202
|
command %i[reset begin] do |c|
|
203
|
+
c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
|
204
|
+
c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
|
205
|
+
c.example 'doing reset 3pm', desc: 'Reset the start time of the last entry to 3pm of the current day'
|
206
|
+
c.example 'doing begin --tag todo --resume', desc: 'alias for reset. Updates the last @todo entry to the current time, removing @done tag.'
|
207
|
+
|
199
208
|
c.desc 'Limit search to section'
|
200
209
|
c.arg_name 'NAME'
|
201
210
|
c.flag %i[s section], default_value: 'All'
|
@@ -203,7 +212,7 @@ command %i[reset begin] do |c|
|
|
203
212
|
c.desc 'Resume entry (remove @done)'
|
204
213
|
c.switch %i[r resume], default_value: true
|
205
214
|
|
206
|
-
c.desc 'Reset last entry matching tag'
|
215
|
+
c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?).'
|
207
216
|
c.arg_name 'TAG'
|
208
217
|
c.flag [:tag]
|
209
218
|
|
@@ -226,12 +235,19 @@ command %i[reset begin] do |c|
|
|
226
235
|
|
227
236
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
228
237
|
c.arg_name 'BOOLEAN'
|
229
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
238
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
230
239
|
|
231
240
|
c.desc 'Select from a menu of matching entries'
|
232
241
|
c.switch %i[i interactive], negatable: false, default_value: false
|
233
242
|
|
234
243
|
c.action do |global_options, options, args|
|
244
|
+
if args.count > 0
|
245
|
+
reset_date = args.join(' ').chronify(guess: :begin)
|
246
|
+
raise InvalidArgument, 'Invalid date string' unless reset_date
|
247
|
+
else
|
248
|
+
reset_date = Time.now
|
249
|
+
end
|
250
|
+
|
235
251
|
options[:fuzzy] = false
|
236
252
|
if options[:section]
|
237
253
|
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
@@ -259,7 +275,7 @@ command %i[reset begin] do |c|
|
|
259
275
|
sort: false,
|
260
276
|
show_if_single: true)
|
261
277
|
else
|
262
|
-
last_entry = items.last
|
278
|
+
last_entry = items.reverse.last
|
263
279
|
end
|
264
280
|
|
265
281
|
unless last_entry
|
@@ -267,7 +283,7 @@ command %i[reset begin] do |c|
|
|
267
283
|
return
|
268
284
|
end
|
269
285
|
|
270
|
-
wwid.reset_item(last_entry, resume: options[:resume])
|
286
|
+
wwid.reset_item(last_entry, date: reset_date, resume: options[:resume])
|
271
287
|
|
272
288
|
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
273
289
|
|
@@ -301,7 +317,7 @@ command :note do |c|
|
|
301
317
|
c.desc "Replace/Remove last entry's note (default append)"
|
302
318
|
c.switch %i[r remove], negatable: false, default_value: false
|
303
319
|
|
304
|
-
c.desc 'Add/remove note from last entry matching tag'
|
320
|
+
c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?).'
|
305
321
|
c.arg_name 'TAG'
|
306
322
|
c.flag [:tag]
|
307
323
|
|
@@ -322,9 +338,9 @@ command :note do |c|
|
|
322
338
|
c.arg_name 'TYPE'
|
323
339
|
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
324
340
|
|
325
|
-
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
341
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
326
342
|
c.arg_name 'BOOLEAN'
|
327
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
343
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
328
344
|
|
329
345
|
c.desc 'Select item for new note from a menu of matching entries'
|
330
346
|
c.switch %i[i interactive], negatable: false, default_value: false
|
@@ -394,6 +410,10 @@ command :note do |c|
|
|
394
410
|
end
|
395
411
|
|
396
412
|
desc 'Finish any running @meanwhile tasks and optionally create a new one'
|
413
|
+
long_desc 'The @meanwhile tag allows you to have long-running entries that encompass smaller entries.
|
414
|
+
This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
|
415
|
+
big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
|
416
|
+
itself to mark the entry as @done.'
|
397
417
|
arg_name 'ENTRY'
|
398
418
|
command :meanwhile do |c|
|
399
419
|
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'
|
@@ -421,7 +441,7 @@ command :meanwhile do |c|
|
|
421
441
|
|
422
442
|
c.action do |_global_options, options, args|
|
423
443
|
if options[:back]
|
424
|
-
date =
|
444
|
+
date = options[:back].chronify(guess: :begin)
|
425
445
|
|
426
446
|
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
427
447
|
else
|
@@ -521,8 +541,19 @@ long_desc 'List all entries and select with typeahead fuzzy matching.
|
|
521
541
|
|
522
542
|
Multiple selections are allowed, hit tab to add the highlighted entry to the
|
523
543
|
selection, and use ctrl-a to select all visible items. Return processes the
|
524
|
-
selected entries.
|
544
|
+
selected entries.
|
545
|
+
|
546
|
+
Search in the menu by typing:
|
547
|
+
|
548
|
+
sbtrkt fuzzy-match Items that match sbtrkt
|
549
|
+
|
550
|
+
\'wild exact-match (quoted) Items that include wild
|
551
|
+
|
552
|
+
!fire inverse-exact-match Items that do not include fire'
|
525
553
|
command :select do |c|
|
554
|
+
c.example 'doing select', desc: 'Select from all entries. A menu of available actions will be presented after confirming the selection.'
|
555
|
+
c.example 'doing select --editor', desc: 'Select entries from a menu and batch edit them in your default editor'
|
556
|
+
c.example 'doing select --after "yesterday 12pm" --tag project1', desc: 'Display a menu of entries created after noon yesterday, add @project1 to selected entries'
|
526
557
|
c.desc 'Select from a specific section'
|
527
558
|
c.arg_name 'SECTION'
|
528
559
|
c.flag %i[s section]
|
@@ -548,6 +579,25 @@ command :select do |c|
|
|
548
579
|
c.arg_name 'QUERY'
|
549
580
|
c.flag %i[q query search]
|
550
581
|
|
582
|
+
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.'
|
583
|
+
c.arg_name 'DATE_STRING'
|
584
|
+
c.flag [:before]
|
585
|
+
|
586
|
+
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.'
|
587
|
+
c.arg_name 'DATE_STRING'
|
588
|
+
c.flag [:after]
|
589
|
+
|
590
|
+
c.desc %(
|
591
|
+
Date range to show, or a single day to filter date on.
|
592
|
+
Date range argument should be quoted. Date specifications can be natural language.
|
593
|
+
To specify a range, use "to" or "through": `doing select --from "monday 8am to friday 5pm"`.
|
594
|
+
|
595
|
+
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
596
|
+
by time of day.
|
597
|
+
)
|
598
|
+
c.arg_name 'DATE_OR_RANGE'
|
599
|
+
c.flag [:from]
|
600
|
+
|
551
601
|
c.desc 'Force exact search string matching (case sensitive)'
|
552
602
|
c.switch %i[x exact], default_value: false, negatable: false
|
553
603
|
|
@@ -620,7 +670,7 @@ command :later do |c|
|
|
620
670
|
|
621
671
|
c.action do |_global_options, options, args|
|
622
672
|
if options[:back]
|
623
|
-
date =
|
673
|
+
date = options[:back].chronify(guess: :begin)
|
624
674
|
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
625
675
|
else
|
626
676
|
date = Time.now
|
@@ -661,6 +711,9 @@ command :later do |c|
|
|
661
711
|
end
|
662
712
|
|
663
713
|
desc 'Add a completed item with @done(date). No argument finishes last entry.'
|
714
|
+
desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
|
715
|
+
You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
|
716
|
+
way to add entries in post and maintain accurate (albeit manual) time tracking.'
|
664
717
|
arg_name 'ENTRY'
|
665
718
|
command %i[done did] do |c|
|
666
719
|
c.example 'doing done', desc: 'Tag the last entry @done'
|
@@ -715,19 +768,19 @@ command %i[done did] do |c|
|
|
715
768
|
donedate = nil
|
716
769
|
|
717
770
|
if options[:took]
|
718
|
-
took =
|
771
|
+
took = options[:took].chronify_qty
|
719
772
|
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
720
773
|
end
|
721
774
|
|
722
775
|
if options[:back]
|
723
|
-
date =
|
776
|
+
date = options[:back].chronify(guess: :begin)
|
724
777
|
raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
725
778
|
else
|
726
779
|
date = options[:took] ? Time.now - took : Time.now
|
727
780
|
end
|
728
781
|
|
729
782
|
if options[:at]
|
730
|
-
finish_date =
|
783
|
+
finish_date = options[:at].chronify(guess: :begin)
|
731
784
|
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
732
785
|
|
733
786
|
date = options[:took] ? finish_date - took : finish_date
|
@@ -871,13 +924,13 @@ command :cancel do |c|
|
|
871
924
|
c.arg_name 'NAME'
|
872
925
|
c.flag %i[s section]
|
873
926
|
|
874
|
-
c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)'
|
927
|
+
c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?).'
|
875
928
|
c.arg_name 'TAG'
|
876
929
|
c.flag [:tag]
|
877
930
|
|
878
|
-
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
931
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
879
932
|
c.arg_name 'BOOLEAN'
|
880
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
933
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
881
934
|
|
882
935
|
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")'
|
883
936
|
c.arg_name 'QUERY'
|
@@ -978,7 +1031,7 @@ command :finish do |c|
|
|
978
1031
|
c.flag [:at]
|
979
1032
|
|
980
1033
|
c.desc 'Finish the last X entries containing TAG.
|
981
|
-
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
1034
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
982
1035
|
c.arg_name 'TAG'
|
983
1036
|
c.flag [:tag]
|
984
1037
|
|
@@ -999,9 +1052,9 @@ command :finish do |c|
|
|
999
1052
|
c.arg_name 'TYPE'
|
1000
1053
|
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1001
1054
|
|
1002
|
-
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
1055
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
1003
1056
|
c.arg_name 'BOOLEAN'
|
1004
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
1057
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
1005
1058
|
|
1006
1059
|
c.desc 'Remove done tag'
|
1007
1060
|
c.switch %i[r remove], negatable: false, default_value: false
|
@@ -1028,7 +1081,7 @@ command :finish do |c|
|
|
1028
1081
|
options[:fuzzy] = false
|
1029
1082
|
unless options[:auto]
|
1030
1083
|
if options[:took]
|
1031
|
-
took =
|
1084
|
+
took = options[:took].chronify_qty
|
1032
1085
|
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
1033
1086
|
end
|
1034
1087
|
|
@@ -1037,21 +1090,21 @@ command :finish do |c|
|
|
1037
1090
|
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
1038
1091
|
|
1039
1092
|
if options[:at]
|
1040
|
-
finish_date =
|
1093
|
+
finish_date = options[:at].chronify(guess: :begin)
|
1041
1094
|
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
1042
1095
|
|
1043
1096
|
date = options[:took] ? finish_date - took : finish_date
|
1044
1097
|
elsif options[:back]
|
1045
|
-
date =
|
1098
|
+
date = options[:back].chronify()
|
1046
1099
|
|
1047
1100
|
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
1048
|
-
elsif options[:took]
|
1049
|
-
date = wwid.chronify_qty(options[:took])
|
1050
1101
|
else
|
1051
1102
|
date = Time.now
|
1052
1103
|
end
|
1053
1104
|
end
|
1054
1105
|
|
1106
|
+
options[:took] = options[:took].chronify_qty if options[:took]
|
1107
|
+
|
1055
1108
|
if options[:tag].nil?
|
1056
1109
|
tags = []
|
1057
1110
|
else
|
@@ -1091,6 +1144,7 @@ command :finish do |c|
|
|
1091
1144
|
tag: tags,
|
1092
1145
|
tag_bool: options[:bool].normalize_bool,
|
1093
1146
|
tags: ['done'],
|
1147
|
+
took: options[:took],
|
1094
1148
|
unfinished: options[:unfinished]
|
1095
1149
|
}
|
1096
1150
|
|
@@ -1099,7 +1153,14 @@ command :finish do |c|
|
|
1099
1153
|
end
|
1100
1154
|
|
1101
1155
|
desc 'Repeat last entry as new entry'
|
1156
|
+
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.'
|
1102
1157
|
command %i[again resume] do |c|
|
1158
|
+
c.example 'doing resume', desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
|
1159
|
+
c.example 'doing again', desc: 'again is an alias for resume'
|
1160
|
+
c.example 'doing resume --editor', desc: 'Repeat the last entry, opening the new entry in the default editor'
|
1161
|
+
c.example 'doing resume --tag project1 --in Projects', desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
|
1162
|
+
c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
|
1163
|
+
|
1103
1164
|
c.desc 'Get last entry from a specific section'
|
1104
1165
|
c.arg_name 'NAME'
|
1105
1166
|
c.flag %i[s section], default_value: 'All'
|
@@ -1108,7 +1169,7 @@ command %i[again resume] do |c|
|
|
1108
1169
|
c.arg_name 'SECTION_NAME'
|
1109
1170
|
c.flag [:in]
|
1110
1171
|
|
1111
|
-
c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma.'
|
1172
|
+
c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
|
1112
1173
|
c.arg_name 'TAG'
|
1113
1174
|
c.flag [:tag]
|
1114
1175
|
|
@@ -1130,9 +1191,9 @@ command %i[again resume] do |c|
|
|
1130
1191
|
c.arg_name 'TYPE'
|
1131
1192
|
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1132
1193
|
|
1133
|
-
c.desc 'Boolean used to combine multiple tags'
|
1194
|
+
c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
|
1134
1195
|
c.arg_name 'BOOLEAN'
|
1135
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
1196
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
1136
1197
|
|
1137
1198
|
c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
|
1138
1199
|
c.switch %i[e editor], negatable: false, default_value: false
|
@@ -1166,6 +1227,46 @@ command %i[again resume] do |c|
|
|
1166
1227
|
end
|
1167
1228
|
end
|
1168
1229
|
|
1230
|
+
desc 'List all tags in the current Doing file'
|
1231
|
+
command :tags do |c|
|
1232
|
+
c.desc 'Section'
|
1233
|
+
c.arg_name 'SECTION_NAME'
|
1234
|
+
c.flag %i[s section], default_value: 'All'
|
1235
|
+
|
1236
|
+
c.desc 'Show count of occurrences'
|
1237
|
+
c.switch %i[c counts]
|
1238
|
+
|
1239
|
+
c.desc 'Sort by name or count'
|
1240
|
+
c.arg_name 'SORT_ORDER'
|
1241
|
+
c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
|
1242
|
+
|
1243
|
+
c.desc 'Sort order (asc/desc)'
|
1244
|
+
c.arg_name 'ORDER'
|
1245
|
+
c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
1246
|
+
|
1247
|
+
c.action do |_global, options, args|
|
1248
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1249
|
+
|
1250
|
+
items = wwid.content.in_section(section)
|
1251
|
+
tags = wwid.all_tags(items, counts: true)
|
1252
|
+
|
1253
|
+
if options[:sort] =~ /^n/i
|
1254
|
+
tags = tags.sort_by { |tag, count| tag }
|
1255
|
+
else
|
1256
|
+
tags = tags.sort_by { |tag, count| count }
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
tags.reverse! if options[:order].normalize_order == 'desc'
|
1260
|
+
|
1261
|
+
if options[:counts]
|
1262
|
+
tags.each { |t, c| puts "#{t} (#{c})" }
|
1263
|
+
else
|
1264
|
+
tags.each { |t, c| puts "#{t}" }
|
1265
|
+
end
|
1266
|
+
end
|
1267
|
+
end
|
1268
|
+
|
1269
|
+
|
1169
1270
|
desc 'Add tag(s) to last entry'
|
1170
1271
|
long_desc 'Add (or remove) tags from the last entry, or from multiple entries
|
1171
1272
|
(with `--count`), entries matching a search (with `--search`), or entries
|
@@ -1219,7 +1320,7 @@ command :tag do |c|
|
|
1219
1320
|
c.switch %i[a autotag], negatable: false, default_value: false
|
1220
1321
|
|
1221
1322
|
c.desc 'Tag the last X entries containing TAG.
|
1222
|
-
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
1323
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
1223
1324
|
c.arg_name 'TAG'
|
1224
1325
|
c.flag [:tag]
|
1225
1326
|
|
@@ -1240,9 +1341,9 @@ command :tag do |c|
|
|
1240
1341
|
c.arg_name 'TYPE'
|
1241
1342
|
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1242
1343
|
|
1243
|
-
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
1344
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
1244
1345
|
c.arg_name 'BOOLEAN'
|
1245
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
1346
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
1246
1347
|
|
1247
1348
|
c.desc 'Select item(s) to tag from a menu of matching entries'
|
1248
1349
|
c.switch %i[i interactive], negatable: false, default_value: false
|
@@ -1330,8 +1431,9 @@ command :tag do |c|
|
|
1330
1431
|
end
|
1331
1432
|
|
1332
1433
|
desc 'Mark last entry as flagged'
|
1333
|
-
command [
|
1434
|
+
command %i[mark flag] do |c|
|
1334
1435
|
c.example 'doing flag', desc: 'Add @flagged to the last entry created'
|
1436
|
+
c.example 'doing mark', desc: 'mark is an alias for flag'
|
1335
1437
|
c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
|
1336
1438
|
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'
|
1337
1439
|
|
@@ -1356,7 +1458,7 @@ command [:mark, :flag] do |c|
|
|
1356
1458
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
1357
1459
|
|
1358
1460
|
c.desc 'Flag the last entry containing TAG.
|
1359
|
-
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
1461
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
1360
1462
|
c.arg_name 'TAG'
|
1361
1463
|
c.flag [:tag]
|
1362
1464
|
|
@@ -1377,9 +1479,9 @@ command [:mark, :flag] do |c|
|
|
1377
1479
|
c.arg_name 'TYPE'
|
1378
1480
|
c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
|
1379
1481
|
|
1380
|
-
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
1482
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
1381
1483
|
c.arg_name 'BOOLEAN'
|
1382
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
1484
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
1383
1485
|
|
1384
1486
|
c.desc 'Select item(s) to flag from a menu of matching entries'
|
1385
1487
|
c.switch %i[i interactive], negatable: false, default_value: false
|
@@ -1453,7 +1555,11 @@ end
|
|
1453
1555
|
desc 'List all entries'
|
1454
1556
|
long_desc %(
|
1455
1557
|
The argument can be a section name, @tag(s) or both.
|
1456
|
-
"pick" or "choose" as an argument will offer a section menu.
|
1558
|
+
"pick" or "choose" as an argument will offer a section menu. Run with `--menu` to get a menu of available tags.
|
1559
|
+
|
1560
|
+
Show tags by passing @tagname arguments. Multiple tags can be combined, and you can specify the boolean used to
|
1561
|
+
combine them with `--bool (AND|OR|NOT)`. You can also use @+tagname to require a tag to match, or @-tagname to ignore
|
1562
|
+
entries containing tagname. +/- operators require `--bool PATTERN` (which is the default).
|
1457
1563
|
)
|
1458
1564
|
arg_name '[SECTION|@TAGS]'
|
1459
1565
|
command :show do |c|
|
@@ -1465,13 +1571,13 @@ command :show do |c|
|
|
1465
1571
|
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.'
|
1466
1572
|
c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
|
1467
1573
|
|
1468
|
-
c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
|
1574
|
+
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.'
|
1469
1575
|
c.arg_name 'TAG'
|
1470
1576
|
c.flag [:tag]
|
1471
1577
|
|
1472
|
-
c.desc 'Tag boolean (AND,OR,NOT)'
|
1578
|
+
c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
|
1473
1579
|
c.arg_name 'BOOLEAN'
|
1474
|
-
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: '
|
1580
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
1475
1581
|
|
1476
1582
|
c.desc 'Max count to show'
|
1477
1583
|
c.arg_name 'MAX'
|
@@ -1481,14 +1587,26 @@ command :show do |c|
|
|
1481
1587
|
c.arg_name 'AGE'
|
1482
1588
|
c.flag %i[a age], default_value: 'newest'
|
1483
1589
|
|
1484
|
-
c.desc '
|
1590
|
+
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.'
|
1485
1591
|
c.arg_name 'DATE_STRING'
|
1486
1592
|
c.flag [:before]
|
1487
1593
|
|
1488
|
-
c.desc '
|
1594
|
+
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.'
|
1489
1595
|
c.arg_name 'DATE_STRING'
|
1490
1596
|
c.flag [:after]
|
1491
1597
|
|
1598
|
+
c.desc %(
|
1599
|
+
Date range to show, or a single day to filter date on.
|
1600
|
+
Date range argument should be quoted. Date specifications can be natural language.
|
1601
|
+
To specify a range, use "to" or "through": `doing show --from "monday 8am to friday 5pm"`.
|
1602
|
+
|
1603
|
+
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
1604
|
+
by time of day.
|
1605
|
+
)
|
1606
|
+
|
1607
|
+
c.arg_name 'DATE_OR_RANGE'
|
1608
|
+
c.flag [:from]
|
1609
|
+
|
1492
1610
|
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
1493
1611
|
c.arg_name 'QUERY'
|
1494
1612
|
c.flag [:search]
|
@@ -1510,17 +1628,12 @@ command :show do |c|
|
|
1510
1628
|
c.arg_name 'ORDER'
|
1511
1629
|
c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
1512
1630
|
|
1513
|
-
c.desc %(
|
1514
|
-
Date range to show, or a single day to filter date on.
|
1515
|
-
Date range argument should be quoted. Date specifications can be natural language.
|
1516
|
-
To specify a range, use "to" or "through": `doing show --from "monday to friday"`
|
1517
|
-
)
|
1518
|
-
c.arg_name 'DATE_OR_RANGE'
|
1519
|
-
c.flag %i[f from]
|
1520
|
-
|
1521
1631
|
c.desc 'Show time intervals on @done tasks'
|
1522
1632
|
c.switch %i[t times], default_value: true, negatable: true
|
1523
1633
|
|
1634
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
1635
|
+
c.switch [:duration]
|
1636
|
+
|
1524
1637
|
c.desc 'Show intervals with totals at the end of output'
|
1525
1638
|
c.switch [:totals], default_value: false, negatable: false
|
1526
1639
|
|
@@ -1537,6 +1650,9 @@ command :show do |c|
|
|
1537
1650
|
c.desc 'Only show items with recorded time intervals'
|
1538
1651
|
c.switch [:only_timed], default_value: false, negatable: false
|
1539
1652
|
|
1653
|
+
c.desc 'Select section or tag to display from a menu'
|
1654
|
+
c.switch %i[m menu], negatable: false, default_value: false
|
1655
|
+
|
1540
1656
|
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1541
1657
|
c.switch %i[i interactive], negatable: false, default_value: false
|
1542
1658
|
|
@@ -1549,15 +1665,17 @@ command :show do |c|
|
|
1549
1665
|
|
1550
1666
|
tag_filter = false
|
1551
1667
|
tags = []
|
1668
|
+
|
1552
1669
|
if args.length.positive?
|
1553
1670
|
case args[0]
|
1554
1671
|
when /^all$/i
|
1555
1672
|
section = 'All'
|
1556
1673
|
args.shift
|
1557
1674
|
when /^(choose|pick)$/i
|
1558
|
-
section = wwid.choose_section
|
1675
|
+
section = wwid.choose_section(include_all: true)
|
1676
|
+
|
1559
1677
|
args.shift
|
1560
|
-
when
|
1678
|
+
when /^[@+-]/
|
1561
1679
|
section = 'All'
|
1562
1680
|
else
|
1563
1681
|
begin
|
@@ -1580,7 +1698,14 @@ command :show do |c|
|
|
1580
1698
|
end
|
1581
1699
|
end
|
1582
1700
|
else
|
1583
|
-
section = settings['current_section']
|
1701
|
+
section = options[:menu] ? wwid.choose_section(include_all: true) : settings['current_section']
|
1702
|
+
end
|
1703
|
+
|
1704
|
+
if options[:menu]
|
1705
|
+
tag = wwid.choose_tag(section, include_all: true)
|
1706
|
+
raise UserCancelled unless tag
|
1707
|
+
|
1708
|
+
tags.concat(tag.split(/ +/).map { |t| t.strip.sub(/^@/, '') }) if tag =~ /^@/
|
1584
1709
|
end
|
1585
1710
|
|
1586
1711
|
tags.concat(options[:tag].to_tags) if options[:tag]
|
@@ -1592,21 +1717,6 @@ command :show do |c|
|
|
1592
1717
|
}
|
1593
1718
|
end
|
1594
1719
|
|
1595
|
-
if options[:from]
|
1596
|
-
|
1597
|
-
date_string = options[:from]
|
1598
|
-
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
1599
|
-
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
1600
|
-
start = wwid.chronify(dates[0], guess: :begin)
|
1601
|
-
finish = wwid.chronify(dates[2], guess: :end)
|
1602
|
-
else
|
1603
|
-
start = wwid.chronify(date_string, guess: :begin)
|
1604
|
-
finish = false
|
1605
|
-
end
|
1606
|
-
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
1607
|
-
dates = [start, finish]
|
1608
|
-
end
|
1609
|
-
|
1610
1720
|
options[:times] = true if options[:totals]
|
1611
1721
|
|
1612
1722
|
template = settings['templates']['default'].deep_merge({
|
@@ -1627,7 +1737,6 @@ command :show do |c|
|
|
1627
1737
|
opt = options.dup
|
1628
1738
|
opt[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1629
1739
|
opt[:count] = options[:count].to_i
|
1630
|
-
opt[:date_filter] = dates
|
1631
1740
|
opt[:highlight] = true
|
1632
1741
|
opt[:order] = options[:sort].normalize_order
|
1633
1742
|
opt[:section] = section
|
@@ -1641,11 +1750,11 @@ command :show do |c|
|
|
1641
1750
|
end
|
1642
1751
|
|
1643
1752
|
desc 'Search for entries'
|
1644
|
-
long_desc
|
1645
|
-
|
1753
|
+
long_desc %(
|
1754
|
+
Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
|
1646
1755
|
|
1647
|
-
|
1648
|
-
|
1756
|
+
To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
|
1757
|
+
)
|
1649
1758
|
|
1650
1759
|
arg_name 'SEARCH_PATTERN'
|
1651
1760
|
command %i[grep search] do |c|
|
@@ -1658,14 +1767,25 @@ command %i[grep search] do |c|
|
|
1658
1767
|
c.arg_name 'NAME'
|
1659
1768
|
c.flag %i[s section], default_value: 'All'
|
1660
1769
|
|
1661
|
-
c.desc '
|
1770
|
+
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.'
|
1662
1771
|
c.arg_name 'DATE_STRING'
|
1663
1772
|
c.flag [:before]
|
1664
1773
|
|
1665
|
-
c.desc '
|
1774
|
+
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.'
|
1666
1775
|
c.arg_name 'DATE_STRING'
|
1667
1776
|
c.flag [:after]
|
1668
1777
|
|
1778
|
+
c.desc %(
|
1779
|
+
Date range to show, or a single day to filter date on.
|
1780
|
+
Date range argument should be quoted. Date specifications can be natural language.
|
1781
|
+
To specify a range, use "to" or "through": `doing search --from "monday 8am to friday 5pm"`.
|
1782
|
+
|
1783
|
+
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
1784
|
+
by time of day.
|
1785
|
+
)
|
1786
|
+
c.arg_name 'DATE_OR_RANGE'
|
1787
|
+
c.flag [:from]
|
1788
|
+
|
1669
1789
|
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1670
1790
|
c.arg_name 'FORMAT'
|
1671
1791
|
c.flag %i[o output]
|
@@ -1673,6 +1793,9 @@ command %i[grep search] do |c|
|
|
1673
1793
|
c.desc 'Show time intervals on @done tasks'
|
1674
1794
|
c.switch %i[t times], default_value: true, negatable: true
|
1675
1795
|
|
1796
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
1797
|
+
c.switch [:duration]
|
1798
|
+
|
1676
1799
|
c.desc 'Show intervals with totals at the end of output'
|
1677
1800
|
c.switch [:totals], default_value: false, negatable: false
|
1678
1801
|
|
@@ -1742,6 +1865,9 @@ command :recent do |c|
|
|
1742
1865
|
c.desc 'Show time intervals on @done tasks'
|
1743
1866
|
c.switch %i[t times], default_value: true, negatable: true
|
1744
1867
|
|
1868
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
1869
|
+
c.switch [:duration]
|
1870
|
+
|
1745
1871
|
c.desc 'Show intervals with totals at the end of output'
|
1746
1872
|
c.switch [:totals], default_value: false, negatable: false
|
1747
1873
|
|
@@ -1781,7 +1907,8 @@ command :recent do |c|
|
|
1781
1907
|
tags_color: tags_color,
|
1782
1908
|
times: options[:times],
|
1783
1909
|
totals: options[:totals],
|
1784
|
-
interactive: options[:interactive]
|
1910
|
+
interactive: options[:interactive],
|
1911
|
+
duration: options[:duration]
|
1785
1912
|
}
|
1786
1913
|
|
1787
1914
|
Doing::Pager::page wwid.recent(count, section.cap_first, opts)
|
@@ -1791,6 +1918,8 @@ command :recent do |c|
|
|
1791
1918
|
end
|
1792
1919
|
|
1793
1920
|
desc 'List entries from today'
|
1921
|
+
long_desc 'List entries from the current day. Use --before, --after, and
|
1922
|
+
--from to specify time ranges.'
|
1794
1923
|
command :today do |c|
|
1795
1924
|
c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
|
1796
1925
|
c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
|
@@ -1804,6 +1933,9 @@ command :today do |c|
|
|
1804
1933
|
c.desc 'Show time intervals on @done tasks'
|
1805
1934
|
c.switch %i[t times], default_value: true, negatable: true
|
1806
1935
|
|
1936
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
1937
|
+
c.switch [:duration]
|
1938
|
+
|
1807
1939
|
c.desc 'Show time totals at the end of output'
|
1808
1940
|
c.switch [:totals], default_value: false, negatable: false
|
1809
1941
|
|
@@ -1825,19 +1957,20 @@ command :today do |c|
|
|
1825
1957
|
c.arg_name 'TIME_STRING'
|
1826
1958
|
c.flag [:after]
|
1827
1959
|
|
1960
|
+
c.desc %(
|
1961
|
+
Time range to show `doing today --from "12pm to 4pm"`
|
1962
|
+
)
|
1963
|
+
c.arg_name 'DATE_OR_RANGE'
|
1964
|
+
c.flag [:from]
|
1965
|
+
|
1828
1966
|
c.action do |_global_options, options, _args|
|
1829
1967
|
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1830
1968
|
|
1831
1969
|
options[:times] = true if options[:totals]
|
1832
1970
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
1836
|
-
section: options[:section],
|
1837
|
-
sort_tags: options[:sort_tags],
|
1838
|
-
totals: options[:totals]
|
1839
|
-
}
|
1840
|
-
Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
|
1971
|
+
filter_options = %i[after before duration from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
|
1972
|
+
|
1973
|
+
Doing::Pager.page wwid.today(options[:times], options[:output], filter_options).chomp
|
1841
1974
|
end
|
1842
1975
|
end
|
1843
1976
|
|
@@ -1858,6 +1991,9 @@ command :on do |c|
|
|
1858
1991
|
c.desc 'Show time intervals on @done tasks'
|
1859
1992
|
c.switch %i[t times], default_value: true, negatable: true
|
1860
1993
|
|
1994
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
1995
|
+
c.switch [:duration]
|
1996
|
+
|
1861
1997
|
c.desc 'Show time totals at the end of output'
|
1862
1998
|
c.switch [:totals], default_value: false, negatable: false
|
1863
1999
|
|
@@ -1880,10 +2016,10 @@ command :on do |c|
|
|
1880
2016
|
|
1881
2017
|
if date_string =~ / (to|through|thru) /
|
1882
2018
|
dates = date_string.split(/ (to|through|thru) /)
|
1883
|
-
start =
|
1884
|
-
finish =
|
2019
|
+
start = dates[0].chronify(guess: :begin)
|
2020
|
+
finish = dates[2].chronify(guess: :end)
|
1885
2021
|
else
|
1886
|
-
start =
|
2022
|
+
start = date_string.chronify(guess: :begin)
|
1887
2023
|
finish = false
|
1888
2024
|
end
|
1889
2025
|
|
@@ -1897,7 +2033,7 @@ command :on do |c|
|
|
1897
2033
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1898
2034
|
|
1899
2035
|
Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
|
1900
|
-
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
2036
|
+
{ duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1901
2037
|
end
|
1902
2038
|
end
|
1903
2039
|
|
@@ -1916,6 +2052,9 @@ command :since do |c|
|
|
1916
2052
|
c.desc 'Show time intervals on @done tasks'
|
1917
2053
|
c.switch %i[t times], default_value: true, negatable: true
|
1918
2054
|
|
2055
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
2056
|
+
c.switch [:duration]
|
2057
|
+
|
1919
2058
|
c.desc 'Show time totals at the end of output'
|
1920
2059
|
c.switch [:totals], default_value: false, negatable: false
|
1921
2060
|
|
@@ -1939,7 +2078,7 @@ command :since do |c|
|
|
1939
2078
|
date_string.sub!(/(day) (\d)/, '\1 at \2')
|
1940
2079
|
date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
|
1941
2080
|
|
1942
|
-
start =
|
2081
|
+
start = date_string.chronify(guess: :begin)
|
1943
2082
|
finish = Time.now
|
1944
2083
|
|
1945
2084
|
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
@@ -1950,11 +2089,13 @@ command :since do |c|
|
|
1950
2089
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1951
2090
|
|
1952
2091
|
Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
|
1953
|
-
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
2092
|
+
{ duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1954
2093
|
end
|
1955
2094
|
end
|
1956
2095
|
|
1957
2096
|
desc 'List entries from yesterday'
|
2097
|
+
desc 'Show only entries with start times within the previous 24 hour period. Use --before, --after, and --from to limit to
|
2098
|
+
time spans within the day.'
|
1958
2099
|
command :yesterday do |c|
|
1959
2100
|
c.example 'doing yesterday', desc: 'List all entries from the previous day'
|
1960
2101
|
c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
|
@@ -1971,6 +2112,9 @@ command :yesterday do |c|
|
|
1971
2112
|
c.desc 'Show time intervals on @done tasks'
|
1972
2113
|
c.switch %i[t times], default_value: true, negatable: true
|
1973
2114
|
|
2115
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
2116
|
+
c.switch [:duration]
|
2117
|
+
|
1974
2118
|
c.desc 'Show time totals at the end of output'
|
1975
2119
|
c.switch [:totals], default_value: false, negatable: false
|
1976
2120
|
|
@@ -1987,6 +2131,12 @@ command :yesterday do |c|
|
|
1987
2131
|
c.arg_name 'TIME_STRING'
|
1988
2132
|
c.flag [:after]
|
1989
2133
|
|
2134
|
+
c.desc %(
|
2135
|
+
Time range to show, e.g. `doing yesterday --from "1am to 8am"`
|
2136
|
+
)
|
2137
|
+
c.arg_name 'TIME_RANGE'
|
2138
|
+
c.flag [:from]
|
2139
|
+
|
1990
2140
|
c.desc 'Tag sort direction (asc|desc)'
|
1991
2141
|
c.arg_name 'DIRECTION'
|
1992
2142
|
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
@@ -1996,9 +2146,17 @@ command :yesterday do |c|
|
|
1996
2146
|
|
1997
2147
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1998
2148
|
|
2149
|
+
if options[:from]
|
2150
|
+
options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
|
2151
|
+
"yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
|
2152
|
+
end.join(' to ')
|
2153
|
+
end
|
2154
|
+
|
1999
2155
|
opt = {
|
2000
2156
|
after: options[:after],
|
2001
2157
|
before: options[:before],
|
2158
|
+
duration: options[:duration],
|
2159
|
+
from: options[:from],
|
2002
2160
|
sort_tags: options[:sort_tags],
|
2003
2161
|
tag_order: options[:tag_order].normalize_order,
|
2004
2162
|
totals: options[:totals],
|
@@ -2009,6 +2167,8 @@ command :yesterday do |c|
|
|
2009
2167
|
end
|
2010
2168
|
|
2011
2169
|
desc 'Show the last entry, optionally edit'
|
2170
|
+
long_desc 'Shows the last entry. Using --search and --tag filters, you can view/edit the last entry matching a filter,
|
2171
|
+
allowing `doing last` to target historical entries.'
|
2012
2172
|
command :last do |c|
|
2013
2173
|
c.example 'doing last', desc: 'Show the most recent entry in all sections'
|
2014
2174
|
c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
|
@@ -2025,18 +2185,21 @@ command :last do |c|
|
|
2025
2185
|
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
2026
2186
|
c.switch %i[e editor], negatable: false, default_value: false
|
2027
2187
|
|
2028
|
-
c.desc 'Tag filter, combine multiple tags with a comma.'
|
2188
|
+
c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
|
2029
2189
|
c.arg_name 'TAG'
|
2030
2190
|
c.flag [:tag]
|
2031
2191
|
|
2032
|
-
c.desc 'Tag boolean'
|
2192
|
+
c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
|
2033
2193
|
c.arg_name 'BOOLEAN'
|
2034
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
2194
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
2035
2195
|
|
2036
2196
|
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
2037
2197
|
c.arg_name 'QUERY'
|
2038
2198
|
c.flag [:search]
|
2039
2199
|
|
2200
|
+
c.desc 'Show elapsed time if entry is not tagged @done'
|
2201
|
+
c.switch [:duration]
|
2202
|
+
|
2040
2203
|
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
2041
2204
|
# c.switch [:fuzzy], default_value: false, negatable: false
|
2042
2205
|
|
@@ -2059,6 +2222,8 @@ command :last do |c|
|
|
2059
2222
|
else
|
2060
2223
|
tags = options[:tag].to_tags
|
2061
2224
|
options[:bool] = case options[:bool]
|
2225
|
+
when /^p/i
|
2226
|
+
:pattern
|
2062
2227
|
when /(any|or)/i
|
2063
2228
|
:or
|
2064
2229
|
when /(not|none)/i
|
@@ -2082,7 +2247,15 @@ command :last do |c|
|
|
2082
2247
|
wwid.edit_last(section: options[:section], options: { search: search, fuzzy: options[:fuzzy], case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
|
2083
2248
|
else
|
2084
2249
|
Doing::Pager::page wwid.last(times: true, section: options[:section],
|
2085
|
-
options: {
|
2250
|
+
options: {
|
2251
|
+
duration: options[:duration],
|
2252
|
+
search: search,
|
2253
|
+
fuzzy: options[:fuzzy],
|
2254
|
+
case: options[:case],
|
2255
|
+
negate: options[:not],
|
2256
|
+
tag: tags,
|
2257
|
+
tag_bool: options[:bool]
|
2258
|
+
}).strip
|
2086
2259
|
end
|
2087
2260
|
end
|
2088
2261
|
end
|
@@ -2163,6 +2336,8 @@ command :plugins do |c|
|
|
2163
2336
|
end
|
2164
2337
|
|
2165
2338
|
desc 'Generate shell completion scripts'
|
2339
|
+
desc 'Generates the necessary scripts to add command line completion to various shells, so typing \'doing\' and hitting
|
2340
|
+
tab will offer completions of subcommands and their options.'
|
2166
2341
|
command :completion do |c|
|
2167
2342
|
c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
|
2168
2343
|
c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
|
@@ -2185,7 +2360,8 @@ command :completion do |c|
|
|
2185
2360
|
end
|
2186
2361
|
|
2187
2362
|
desc 'Display a user-created view'
|
2188
|
-
long_desc '
|
2363
|
+
long_desc 'Views are defined in your configuration (use `doing config` to edit).
|
2364
|
+
Command line options override view configuration.'
|
2189
2365
|
arg_name 'VIEW_NAME'
|
2190
2366
|
command :view do |c|
|
2191
2367
|
c.example 'doing view color', desc: 'Display entries according to config for view "color"'
|
@@ -2206,19 +2382,22 @@ command :view do |c|
|
|
2206
2382
|
c.desc 'Show time intervals on @done tasks'
|
2207
2383
|
c.switch %i[t times], default_value: true, negatable: true
|
2208
2384
|
|
2385
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
2386
|
+
c.switch [:duration]
|
2387
|
+
|
2209
2388
|
c.desc 'Show intervals with totals at the end of output'
|
2210
2389
|
c.switch [:totals], default_value: false, negatable: false
|
2211
2390
|
|
2212
2391
|
c.desc 'Include colors in output'
|
2213
2392
|
c.switch [:color], default_value: true, negatable: true
|
2214
2393
|
|
2215
|
-
c.desc 'Tag filter, combine multiple tags with a comma.'
|
2394
|
+
c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
|
2216
2395
|
c.arg_name 'TAG'
|
2217
2396
|
c.flag [:tag]
|
2218
2397
|
|
2219
|
-
c.desc 'Tag boolean (AND,OR,NOT)'
|
2398
|
+
c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
|
2220
2399
|
c.arg_name 'BOOLEAN'
|
2221
|
-
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: '
|
2400
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
2222
2401
|
|
2223
2402
|
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
2224
2403
|
c.arg_name 'QUERY'
|
@@ -2245,14 +2424,25 @@ command :view do |c|
|
|
2245
2424
|
c.arg_name 'DIRECTION'
|
2246
2425
|
c.flag [:tag_order], must_match: REGEX_SORT_ORDER
|
2247
2426
|
|
2248
|
-
c.desc 'View entries older than date'
|
2427
|
+
c.desc 'View entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
2249
2428
|
c.arg_name 'DATE_STRING'
|
2250
2429
|
c.flag [:before]
|
2251
2430
|
|
2252
|
-
c.desc 'View entries newer than date'
|
2431
|
+
c.desc 'View entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
2253
2432
|
c.arg_name 'DATE_STRING'
|
2254
2433
|
c.flag [:after]
|
2255
2434
|
|
2435
|
+
c.desc %(
|
2436
|
+
Date range to show, or a single day to filter date on.
|
2437
|
+
Date range argument should be quoted. Date specifications can be natural language.
|
2438
|
+
To specify a range, use "to" or "through": `doing view --from "monday 8am to friday 5pm" view_name`.
|
2439
|
+
|
2440
|
+
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
2441
|
+
by time of day.
|
2442
|
+
)
|
2443
|
+
c.arg_name 'DATE_OR_RANGE'
|
2444
|
+
c.flag [:from]
|
2445
|
+
|
2256
2446
|
c.desc 'Only show items with recorded time intervals (override view settings)'
|
2257
2447
|
c.switch [:only_timed], default_value: false, negatable: false
|
2258
2448
|
|
@@ -2301,16 +2491,22 @@ command :view do |c|
|
|
2301
2491
|
tag_filter = false
|
2302
2492
|
if options[:tag]
|
2303
2493
|
tag_filter = { 'tags' => [], 'bool' => 'OR' }
|
2304
|
-
|
2305
|
-
tag_filter['bool'] =
|
2494
|
+
bool = options[:bool].normalize_bool
|
2495
|
+
tag_filter['bool'] = bool
|
2496
|
+
tag_filter['tags'] = if bool == :pattern
|
2497
|
+
options[:tag]
|
2498
|
+
else
|
2499
|
+
options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
|
2500
|
+
end
|
2306
2501
|
elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?)
|
2307
2502
|
tag_filter = { 'tags' => [], 'bool' => 'OR' }
|
2503
|
+
bool = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :pattern
|
2504
|
+
tag_filter['bool'] = bool
|
2308
2505
|
tag_filter['tags'] = if view['tags'].instance_of?(Array)
|
2309
|
-
view['tags'].map(&:strip)
|
2506
|
+
bool == :pattern ? view['tags'].join(' ').strip : view['tags'].map(&:strip)
|
2310
2507
|
else
|
2311
|
-
view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
|
2508
|
+
bool == :pattern ? view['tags'].strip : view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
|
2312
2509
|
end
|
2313
|
-
tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
|
2314
2510
|
end
|
2315
2511
|
|
2316
2512
|
# If the -o/--output flag was specified, override any default in the view template
|
@@ -2346,27 +2542,8 @@ command :view do |c|
|
|
2346
2542
|
else
|
2347
2543
|
false
|
2348
2544
|
end
|
2349
|
-
if view.key?('after') && !options[:after]
|
2350
|
-
options[:after] = view['after']
|
2351
|
-
end
|
2352
2545
|
|
2353
|
-
if view.key?(
|
2354
|
-
options[:before] = view['before']
|
2355
|
-
end
|
2356
|
-
|
2357
|
-
if view.key?('from')
|
2358
|
-
date_string = view['from']
|
2359
|
-
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
2360
|
-
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
2361
|
-
start = wwid.chronify(dates[0], guess: :begin)
|
2362
|
-
finish = wwid.chronify(dates[2], guess: :end)
|
2363
|
-
else
|
2364
|
-
start = wwid.chronify(date_string, guess: :begin)
|
2365
|
-
finish = false
|
2366
|
-
end
|
2367
|
-
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
2368
|
-
dates = [start, finish]
|
2369
|
-
end
|
2546
|
+
%w[before after from duration].each { |k| options[k.to_sym] = view[k] if view.key?(k) && !options[k.to_sym] }
|
2370
2547
|
|
2371
2548
|
options[:case] = options[:case].normalize_case
|
2372
2549
|
|
@@ -2378,22 +2555,21 @@ command :view do |c|
|
|
2378
2555
|
end
|
2379
2556
|
|
2380
2557
|
opts = options.dup
|
2381
|
-
opts[:search] = search
|
2382
|
-
opts[:output] = output_format
|
2383
2558
|
opts[:count] = count
|
2384
2559
|
opts[:format] = date_format
|
2385
2560
|
opts[:highlight] = options[:color]
|
2386
2561
|
opts[:only_timed] = only_timed
|
2387
2562
|
opts[:order] = order
|
2563
|
+
opts[:output] = options[:interactive] ? nil : options[:output]
|
2564
|
+
opts[:output] = output_format
|
2565
|
+
opts[:page_title] = page_title
|
2566
|
+
opts[:search] = search
|
2388
2567
|
opts[:section] = section
|
2389
2568
|
opts[:tag_filter] = tag_filter
|
2390
2569
|
opts[:tag_order] = tag_order
|
2391
2570
|
opts[:tags_color] = tags_color
|
2392
2571
|
opts[:template] = template
|
2393
2572
|
opts[:totals] = totals
|
2394
|
-
opts[:page_title] = page_title
|
2395
|
-
opts[:date_filter] = dates
|
2396
|
-
opts[:output] = options[:interactive] ? nil : options[:output]
|
2397
2573
|
|
2398
2574
|
Doing::Pager.page wwid.list_section(opts)
|
2399
2575
|
elsif title.instance_of?(FalseClass)
|
@@ -2439,13 +2615,13 @@ command %i[archive move] do |c|
|
|
2439
2615
|
c.desc 'Label moved items with @from(SECTION_NAME)'
|
2440
2616
|
c.switch [:label], default_value: true, negatable: true
|
2441
2617
|
|
2442
|
-
c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
|
2618
|
+
c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
|
2443
2619
|
c.arg_name 'TAG'
|
2444
2620
|
c.flag [:tag]
|
2445
2621
|
|
2446
|
-
c.desc 'Tag boolean (AND|OR|NOT)'
|
2622
|
+
c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
|
2447
2623
|
c.arg_name 'BOOLEAN'
|
2448
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
2624
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
2449
2625
|
|
2450
2626
|
c.desc 'Search filter'
|
2451
2627
|
c.arg_name 'QUERY'
|
@@ -2508,6 +2684,9 @@ command %i[archive move] do |c|
|
|
2508
2684
|
end
|
2509
2685
|
|
2510
2686
|
desc 'Move entries to archive file'
|
2687
|
+
long_desc 'As your doing file grows, commands can get slow. Given that your historical data (and your archive section)
|
2688
|
+
probably aren\'t providing any useful insights a year later, use this command to "rotate" old entries out to an archive
|
2689
|
+
file. You\'ll still have access to all historical data, but it won\'t be slowing down daily operation.'
|
2511
2690
|
command :rotate do |c|
|
2512
2691
|
c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
|
2513
2692
|
c.example 'doing rotate --section Archive --keep 10', desc: 'Move entries in the Archive section to a secondary file, keeping the most recent 10 entries'
|
@@ -2521,13 +2700,13 @@ command :rotate do |c|
|
|
2521
2700
|
c.arg_name 'SECTION_NAME'
|
2522
2701
|
c.flag %i[s section], default_value: 'All'
|
2523
2702
|
|
2524
|
-
c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
|
2703
|
+
c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?). Added for compatibility with other commands.'
|
2525
2704
|
c.arg_name 'TAG'
|
2526
2705
|
c.flag [:tag]
|
2527
2706
|
|
2528
|
-
c.desc 'Tag boolean (AND|OR|NOT)'
|
2707
|
+
c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
|
2529
2708
|
c.arg_name 'BOOLEAN'
|
2530
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: '
|
2709
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
2531
2710
|
|
2532
2711
|
c.desc 'Search filter'
|
2533
2712
|
c.arg_name 'QUERY'
|
@@ -2574,8 +2753,10 @@ command :rotate do |c|
|
|
2574
2753
|
end
|
2575
2754
|
|
2576
2755
|
desc 'Open the "doing" file in an editor'
|
2577
|
-
long_desc "`doing open` defaults to using the
|
2756
|
+
long_desc "`doing open` defaults to using the editors->doing_file setting
|
2757
|
+
in #{config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
|
2578
2758
|
command :open do |c|
|
2759
|
+
c.example 'doing open', desc: 'Open the doing file in the default editor'
|
2579
2760
|
c.desc 'Open with editor command (e.g. vim, mate)'
|
2580
2761
|
c.arg_name 'COMMAND'
|
2581
2762
|
c.flag %i[e editor]
|
@@ -2597,7 +2778,7 @@ command :open do |c|
|
|
2597
2778
|
end
|
2598
2779
|
|
2599
2780
|
if options[:editor]
|
2600
|
-
raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor])
|
2781
|
+
raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor].split(/ /).first)
|
2601
2782
|
|
2602
2783
|
editor = TTY::Which.which(options[:editor])
|
2603
2784
|
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
@@ -2608,7 +2789,7 @@ command :open do |c|
|
|
2608
2789
|
system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(wwid.doing_file)}")
|
2609
2790
|
elsif Doing::Util.find_default_editor('doing_file')
|
2610
2791
|
editor = Doing::Util.find_default_editor('doing_file')
|
2611
|
-
if Doing::Util.exec_available(editor)
|
2792
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
2612
2793
|
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
2613
2794
|
else
|
2614
2795
|
system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
|
@@ -2703,7 +2884,7 @@ command :config do |c|
|
|
2703
2884
|
if options[:default]
|
2704
2885
|
editor = Doing::Util.find_default_editor('config')
|
2705
2886
|
if editor
|
2706
|
-
if Doing::Util.exec_available(editor)
|
2887
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
2707
2888
|
system %(#{editor} "#{config_file}")
|
2708
2889
|
else
|
2709
2890
|
`open -a "#{editor}" "#{config_file}"`
|
@@ -2722,7 +2903,7 @@ command :config do |c|
|
|
2722
2903
|
|
2723
2904
|
raise MissingEditor, 'No viable editor defined in config or environment' unless editor
|
2724
2905
|
|
2725
|
-
if Doing::Util.exec_available(editor)
|
2906
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
2726
2907
|
system %(#{editor} "#{config_file}")
|
2727
2908
|
else
|
2728
2909
|
`open -a "#{editor}" "#{config_file}"`
|
@@ -2730,7 +2911,7 @@ command :config do |c|
|
|
2730
2911
|
end
|
2731
2912
|
else
|
2732
2913
|
editor = options[:editor] || Doing::Util.default_editor
|
2733
|
-
raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
|
2914
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor.split(/ /).first)
|
2734
2915
|
|
2735
2916
|
system %(#{editor} "#{config_file}")
|
2736
2917
|
end
|
@@ -2821,9 +3002,21 @@ command :config do |c|
|
|
2821
3002
|
|
2822
3003
|
value = options[:remove] ? nil : args.pop
|
2823
3004
|
keypath = args.join('.')
|
2824
|
-
|
2825
|
-
|
2826
|
-
|
3005
|
+
real_path = config.resolve_key_path(keypath, create: true)
|
3006
|
+
|
3007
|
+
old_value = settings.dig(*real_path) || nil
|
3008
|
+
old_type = old_value&.class.to_s || nil
|
3009
|
+
|
3010
|
+
if old_value.is_a?(Hash) && !options[:remove]
|
3011
|
+
Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
|
3012
|
+
didyou = 'Did you mean:'
|
3013
|
+
old_value.keys.each do |k|
|
3014
|
+
Doing.logger.log_now(:warn, "#{didyou}", "#{keypath}.#{k}?")
|
3015
|
+
didyou = '..........or:'
|
3016
|
+
end
|
3017
|
+
raise InvalidArgument, 'Config value is a mapping, can not be set to a single value'
|
3018
|
+
|
3019
|
+
end
|
2827
3020
|
|
2828
3021
|
config_file = config.choose_config
|
2829
3022
|
cfg = YAML.safe_load_file(config_file) || {}
|
@@ -2834,11 +3027,11 @@ command :config do |c|
|
|
2834
3027
|
cfg.deep_set(real_path, nil)
|
2835
3028
|
$stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
|
2836
3029
|
else
|
2837
|
-
|
2838
|
-
|
3030
|
+
cfg.deep_set(real_path, value.set_type(old_type))
|
3031
|
+
|
2839
3032
|
$stderr.puts "#{'Key path:'.yellow} #{real_path.join('->').boldwhite}"
|
2840
|
-
$stderr.puts "#{'Previous:'.yellow} #{old_value.to_s.boldwhite}"
|
2841
|
-
$stderr.puts "#{' New:'.yellow} #{value.set_type.to_s.boldwhite}"
|
3033
|
+
$stderr.puts "#{'Previous:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
|
3034
|
+
$stderr.puts "#{' New:'.yellow} #{value.set_type(old_type).to_s.boldwhite}"
|
2842
3035
|
end
|
2843
3036
|
|
2844
3037
|
res = Doing::Prompt.yn('Update selected config', default_response: true)
|
@@ -2851,15 +3044,79 @@ command :config do |c|
|
|
2851
3044
|
end
|
2852
3045
|
end
|
2853
3046
|
|
2854
|
-
desc 'Undo the last
|
3047
|
+
desc 'Undo the last X changes to the Doing file'
|
3048
|
+
long_desc 'Reverts the last X commands that altered the doing file.
|
3049
|
+
All changes performed by a single command are undone at once.
|
3050
|
+
|
3051
|
+
Specify a number to jump back multiple revisions, or use --select for an interactive menu.'
|
3052
|
+
arg_name 'COUNT'
|
2855
3053
|
command :undo do |c|
|
3054
|
+
c.example 'doing undo', desc: 'Undo the most recent change to the doing file'
|
3055
|
+
c.example 'doing undo 5', desc: 'Undo the last 5 changes to the doing file'
|
3056
|
+
c.example 'doing undo --interactive', desc: 'Select from a menu of available revisions'
|
3057
|
+
c.example 'doing undo --redo', desc: 'Undo the last undo command'
|
3058
|
+
|
2856
3059
|
c.desc 'Specify alternate doing file'
|
2857
3060
|
c.arg_name 'PATH'
|
2858
3061
|
c.flag %i[f file], default_value: wwid.doing_file
|
2859
3062
|
|
2860
|
-
c.
|
3063
|
+
c.desc 'Select from recent backups'
|
3064
|
+
c.switch %i[i interactive], negatable: false
|
3065
|
+
|
3066
|
+
c.desc 'Remove old backups, retaining X files'
|
3067
|
+
c.arg_name 'COUNT'
|
3068
|
+
c.flag %i[p prune], type: Integer
|
3069
|
+
|
3070
|
+
c.desc 'Redo last undo. Note: you cannot undo a redo.'
|
3071
|
+
c.switch %i[r redo]
|
3072
|
+
|
3073
|
+
c.action do |_global_options, options, args|
|
3074
|
+
file = options[:file] || wwid.doing_file
|
3075
|
+
count = args.empty? ? 1 : args[0].to_i
|
3076
|
+
raise InvalidArgument, "Invalid count specified for undo" unless count&.positive?
|
3077
|
+
|
3078
|
+
if options[:prune]
|
3079
|
+
Doing::Util::Backup.prune_backups(file, options[:prune])
|
3080
|
+
elsif options[:redo]
|
3081
|
+
Doing::Util::Backup.redo_backup(file, count: count)
|
3082
|
+
else
|
3083
|
+
if options[:interactive]
|
3084
|
+
Doing::Util::Backup.select_backup(file)
|
3085
|
+
else
|
3086
|
+
Doing::Util::Backup.restore_last_backup(file, count: count)
|
3087
|
+
end
|
3088
|
+
end
|
3089
|
+
end
|
3090
|
+
end
|
3091
|
+
|
3092
|
+
long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo.'
|
3093
|
+
arg_name 'COUNT'
|
3094
|
+
command :redo do |c|
|
3095
|
+
c.desc 'Specify alternate doing file'
|
3096
|
+
c.arg_name 'PATH'
|
3097
|
+
c.flag %i[f file], default_value: wwid.doing_file
|
3098
|
+
|
3099
|
+
c.action do |_global, options, args|
|
2861
3100
|
file = options[:file] || wwid.doing_file
|
2862
|
-
|
3101
|
+
count = args.empty? ? 1 : args[0].to_i
|
3102
|
+
raise InvalidArgument, "Invalid count specified for redo" unless count&.positive?
|
3103
|
+
|
3104
|
+
Doing::Util::Backup.redo_backup(file, count: count)
|
3105
|
+
end
|
3106
|
+
end
|
3107
|
+
|
3108
|
+
desc 'List recent changes in Doing'
|
3109
|
+
long_desc 'Display a formatted list of changes in recent versions, latest at the top'
|
3110
|
+
command %i[changelog changes] do |c|
|
3111
|
+
c.action do |_global_options, options, args|
|
3112
|
+
changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
|
3113
|
+
if File.exist?(changelog)
|
3114
|
+
parsed = TTY::Markdown.parse(IO.read(changelog), width: 80, symbols: {override: {bullet: "•"}})
|
3115
|
+
Doing::Pager.paginate = true
|
3116
|
+
Doing::Pager.page parsed
|
3117
|
+
else
|
3118
|
+
raise "Error locating changelog"
|
3119
|
+
end
|
2863
3120
|
end
|
2864
3121
|
end
|
2865
3122
|
|
@@ -2867,6 +3124,10 @@ desc 'Import entries from an external source'
|
|
2867
3124
|
long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
|
2868
3125
|
arg_name 'PATH'
|
2869
3126
|
command :import do |c|
|
3127
|
+
c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
|
3128
|
+
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'
|
3129
|
+
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'
|
3130
|
+
|
2870
3131
|
c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
|
2871
3132
|
c.arg_name 'TYPE'
|
2872
3133
|
c.flag :type, default_value: 'doing'
|
@@ -2906,6 +3167,7 @@ command :import do |c|
|
|
2906
3167
|
c.arg_name 'PREFIX'
|
2907
3168
|
c.flag :prefix
|
2908
3169
|
|
3170
|
+
# TODO: Allow time range filtering
|
2909
3171
|
c.desc 'Import entries older than date'
|
2910
3172
|
c.arg_name 'DATE_STRING'
|
2911
3173
|
c.flag [:before]
|
@@ -2935,10 +3197,10 @@ command :import do |c|
|
|
2935
3197
|
date_string = options[:from]
|
2936
3198
|
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
2937
3199
|
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
2938
|
-
start =
|
2939
|
-
finish =
|
3200
|
+
start = dates[0].chronify(guess: :begin)
|
3201
|
+
finish = dates[2].chronify(guess: :end)
|
2940
3202
|
else
|
2941
|
-
start =
|
3203
|
+
start = date_string.chronify(guess: :begin)
|
2942
3204
|
finish = false
|
2943
3205
|
end
|
2944
3206
|
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
@@ -3005,10 +3267,13 @@ around do |global, command, options, arguments, code|
|
|
3005
3267
|
|
3006
3268
|
if global[:yes]
|
3007
3269
|
Doing::Prompt.force_answer = true
|
3270
|
+
Doing.config.force_answer = true
|
3008
3271
|
elsif global[:no]
|
3009
3272
|
Doing::Prompt.force_answer = false
|
3273
|
+
Doing.config.force_answer = false
|
3010
3274
|
else
|
3011
3275
|
Doing::Prompt.default_answer = global[:default]
|
3276
|
+
Doing.config.force_answer = global[:default] ? true : false
|
3012
3277
|
end
|
3013
3278
|
|
3014
3279
|
if global[:config_file] && global[:config_file] != config.config_file
|