doing 1.0.73 → 1.0.78

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b7d7f6e02a2f50af048b4a2547d8f43fe2346741a83737e8087d9a163c1a480
4
- data.tar.gz: e082a72f034f52e0f149c62d56c8ed8980d2d9225333d5a381258ee8da19bd8c
3
+ metadata.gz: 64076f13651c82428d05f490870a3a8a2507dc783cfaeebe7658538523aaef25
4
+ data.tar.gz: 691df494543c3ff71911a4ca87135adba9504fb445d51180e9c5914372c5d35b
5
5
  SHA512:
6
- metadata.gz: 67b469cd1850b4e0cb8b151fb2ed3d471c4adfa1304d842a07211ad01cbf1b2310519384ffd45fb8807453d79aef6ea553842f92d039c905b63b90f83c8336f1
7
- data.tar.gz: 235747013bbd1abb651cbe73d97de52ec3f6578a6edf5da8571ae1b4c6aba6384df7c6b3c7baa256c93ef596b1611745bd79735685ab7cfafc5ca521cabe64b5
6
+ metadata.gz: be005b1f54f3e170030f88a6df19567478985965b56bb50a842fe3d5c87d7afb750e165d4888da45466f3dba16205a94d1baf5cbfe723f0a18dffe960d944d00
7
+ data.tar.gz: 48e5cbec09157db3c974b5d0c242c394884a77f47ce5cf4e009b7dedf47daccba581709aefb534266415884ab71fad03c0a4d22385e3abc9d3e33576f3aaad17
data/README.md CHANGED
@@ -27,7 +27,7 @@ If there's something I want to look at later but doesn't need to be added to a t
27
27
 
28
28
  ## Installation
29
29
 
30
- The current version of `doing` is <!--VER-->1.0.72<!--END VER-->.
30
+ The current version of `doing` is <!--VER-->1.0.76<!--END VER-->.
31
31
 
32
32
  $ [sudo] gem install doing
33
33
 
@@ -268,7 +268,7 @@ You can add additional custom views. Just nest them under the `views` key (inden
268
268
  count: 5
269
269
  wrap_width: 0
270
270
  date_format: '%F %_I:%M%P'
271
- template: '%date | %title%note'
271
+ template: '%date | %title%note'
272
272
 
273
273
  The `section` key is the default section to pull entries from. Count and section can be overridden at runtime with the `-c` and `-s` flags. Setting `section` to `All` will combine all sections in the output.
274
274
 
@@ -505,7 +505,7 @@ You can include a `transform` section in the autotag config which contains pairs
505
505
  transform:
506
506
  - (\w+)-\d+:$1
507
507
 
508
- This creates a search pattern looking for a string of word characters followed by a hyphen and one or more digits, e.g. `@projecttag-12`. Do not include the @ symbol in the pattern. The replacement (`$1`) indicates that the first matched group (in parenthesis) should be used to generate the new tag, resulting in `@projecttag` being added to the entry.
508
+ This creates a search pattern looking for a string of word characters followed by a hyphen and one or more digits, e.g. `@projecttag-12`. Do not include the @ symbol in the pattern. The replacement (`$l`) indicates that the first matched group (in parenthesis) should be used to generate the new tag, resulting in `@projecttag` being added to the entry.
509
509
 
510
510
  ##### Annotating
511
511
 
@@ -536,7 +536,7 @@ If you have a use for it, you can use `-o csv` on the show or view commands to o
536
536
 
537
537
  `doing yesterday` is great for stand-ups (thanks to [Sean Collins](https://github.com/sc68cal) for that!). Note that you can show yesterday's activity from an alternate section by using the section name as an argument (e.g. `doing yesterday archive`).
538
538
 
539
- `doing on` allows for full date ranges and filtering. `doing on saturday`, or `doing on one month to today` will give you ranges. You can use the same terms with the `show` command by adding the `-f` or `--from` flag. `doing show @done --from "monday to friday"` will give you all of your completed items for the last week (assuming it's the weekend).
539
+ `doing on` allows for full date ranges and filtering. `doing on saturday`, or `doing on one month to today` will give you ranges. You can use the same terms with the `show` command by adding the `-f` or `--from` flag. `doing show @done --from "monday to friday"` will give you all of your completed items for the last week (assuming it's the weekend). There's also `doing since` a simple alias for `doing on PAST_DATE to now`, e.g. `doing since monday`.
540
540
 
541
541
  You can also show entries matching a search string with `doing grep` (synonym `doing search`). If you want to search with regular expressions or for an exact match, surround your search query with forward slashes, e.g. `doing search /project name/`. If you pass a search string without slashes, it's treated as a fuzzy search string, meaning matches can be found as long as the characters in the search string are in order and with no more than three other characters between each. By default searches are across all sections, but you can limit it to one with the `-s SECTION_NAME` flag. Searches can be displayed with the default template, or output as HTML, CSV, or JSON.
542
542
 
@@ -621,9 +621,9 @@ Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is t
621
621
 
622
622
  If you have `fzf` installed (<https://github.com/junegunn/fzf>), you can use `doing select` to get a menu of all your items (or items in a given section) which can be searched with fuzzy matching. The menu allows multiple selections to be acted on directly.
623
623
 
624
- To use the menu, type a search string or use the arrow keys to navigate up and down. Press tab on an entry you'd like to perform an action on. A marker will show up on the left indicating the entry is selected. Repeat the process and select as many entries as needed. When you hit Return, the selection will be passed back to doing.
624
+ To use the menu, type a search string or use the arrow keys to navigate up and down. Press tab on an entry you'd like to perform an action on. A marker will show up on the left indicating the entry is selected. Repeat the process and select as many entries as needed. When you hit Return, the selection will be passed back to doing. Use Control-A to select all visible entries.
625
625
 
626
- Doing can perform several functions with this menu. Not all of doing's features are available, but the core functionality you'd need is there, plus you can open all selected entries in your editor, make changes to them, and when you save and close the entries are updated accordingly. This allows editing of everything from timestamps to tags to notes.
626
+ Doing can perform several functions with this menu. Not all of doing's features are available, but the core functionality you'd need is there, plus you can open the selected entries on one page in your text editor, make changes to them, and when you save and close the entries are updated accordingly. This allows editing of everything from timestamps to tags to notes.
627
627
 
628
628
  Run `doing help select` for a list of options:
629
629
 
@@ -641,6 +641,8 @@ For example, `doing select -d -a` would present the menu, and then mark selected
641
641
 
642
642
  Multiple actions can be performed at once by combining options. You can also combine the `--editor` switch with any other options. Other actions will be performed first, then the entries --- with any modifications performed --- will be presented in the editor for tweaking.
643
643
 
644
+ **Note:** when using the `--editor` flag to open selections in your text editor, entries will be separated by `---` lines. These must remain in place for doing to track the changes. You can do anything you want to the entries, modify dates, change text, add notes, etc., as long as you leave the dividers in place. You can even delete an entry entirely, leaving the dividers around the missing line and the entry will be removed from your doing file when you save and exit the editor.
645
+
644
646
  ---
645
647
 
646
648
  ## Extras
data/bin/doing CHANGED
@@ -79,7 +79,7 @@ command %i[now next] do |c|
79
79
  if options[:back]
80
80
  date = wwid.chronify(options[:back])
81
81
 
82
- raise 'Unable to parse date string' if date.nil?
82
+ exit_now! 'Unable to parse date string' if date.nil?
83
83
  else
84
84
  date = Time.now
85
85
  end
@@ -87,13 +87,13 @@ command %i[now next] do |c|
87
87
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
88
88
 
89
89
  if options[:e] || (args.empty? && $stdin.stat.size.zero?)
90
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
90
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
91
91
 
92
92
  input = ''
93
93
  input += args.join(' ') unless args.empty?
94
94
  input = wwid.fork_editor(input).strip
95
95
 
96
- raise 'No content' if input.empty?
96
+ exit_now! 'No content' if input.empty?
97
97
 
98
98
  title, note = wwid.format_input(input)
99
99
  note.push(options[:n]) if options[:n]
@@ -111,7 +111,7 @@ command %i[now next] do |c|
111
111
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
112
112
  wwid.write(wwid.doing_file)
113
113
  else
114
- raise 'You must provide content when creating a new entry'
114
+ exit_now! 'You must provide content when creating a new entry'
115
115
  end
116
116
  end
117
117
  end
@@ -138,7 +138,7 @@ command :note do |c|
138
138
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
139
139
 
140
140
  if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
141
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
141
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
142
142
 
143
143
  input = !args.empty? ? args.join(' ') : ''
144
144
 
@@ -147,11 +147,11 @@ command :note do |c|
147
147
  input = prev_input + input
148
148
 
149
149
  input = wwid.fork_editor(input).strip
150
- raise 'No content, cancelled' unless input
150
+ exit_now! 'No content, cancelled' unless input
151
151
 
152
152
  _title, note = wwid.format_input(input)
153
153
 
154
- raise 'No note content' unless note
154
+ exit_now! 'No note content' unless note
155
155
 
156
156
  wwid.note_last(section, note, replace: true)
157
157
  elsif !args.empty?
@@ -165,7 +165,7 @@ command :note do |c|
165
165
  elsif options[:r]
166
166
  wwid.note_last(section, [], replace: true)
167
167
  else
168
- raise 'You must provide content when adding a note'
168
+ exit_now! 'You must provide content when adding a note'
169
169
  end
170
170
  wwid.write(wwid.doing_file)
171
171
  end
@@ -196,7 +196,7 @@ command :meanwhile do |c|
196
196
  if options[:back]
197
197
  date = wwid.chronify(options[:back])
198
198
 
199
- raise 'Unable to parse date string' if date.nil?
199
+ exit_now! 'Unable to parse date string' if date.nil?
200
200
  else
201
201
  date = Time.now
202
202
  end
@@ -205,7 +205,7 @@ command :meanwhile do |c|
205
205
  input = ''
206
206
 
207
207
  if options[:e]
208
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
208
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
209
209
 
210
210
  input += args.join(' ') unless args.empty?
211
211
  input = wwid.fork_editor(input).strip
@@ -243,7 +243,7 @@ long_desc %(
243
243
  arg_name 'TYPE', must_match: /^(?:html|haml|css)/i
244
244
  command :template do |c|
245
245
  c.action do |_global_options, options, args|
246
- raise 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
246
+ exit_now! 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
247
247
 
248
248
  case args[0]
249
249
  when /html|haml/i
@@ -251,15 +251,15 @@ command :template do |c|
251
251
  when /css/i
252
252
  $stdout.puts wwid.css_template
253
253
  else
254
- raise 'Invalid type specified, must be HAML or CSS'
254
+ exit_now! 'Invalid type specified, must be HAML or CSS'
255
255
  end
256
256
  end
257
257
  end
258
258
 
259
259
  desc 'Display an interactive menu to perform operations (requires fzf)'
260
- long_desc 'Requires that `fzf` be installed and available in your path. <https://github.com/junegunn/fzf>
260
+ long_desc 'List all entries and select with typeahead fuzzy matching.
261
261
 
262
- List all entries and select with typeahead fuzzy matching. Multiple selections are allowed, hit tab to add the highlighted entry to the selection. Return processes the selected entries.'
262
+ Multiple selections are allowed, hit tab to add the highlighted entry to the selection. Return processes the selected entries.'
263
263
  command :select do |c|
264
264
  c.desc 'Select from a specific section'
265
265
  c.arg_name 'SECTION'
@@ -269,6 +269,9 @@ command :select do |c|
269
269
  c.arg_name 'TAG'
270
270
  c.flag %i[t tag]
271
271
 
272
+ c.desc 'Reverse -c, -f, --flag, and -t (remove instead of adding)'
273
+ c.switch %i[r remove], negatable: false
274
+
272
275
  # c.desc 'Add @done to selected item(s), using start time of next item as the finish time'
273
276
  # c.switch %i[a auto], negatable: false, default_value: false
274
277
 
@@ -279,6 +282,10 @@ command :select do |c|
279
282
  c.arg_name 'SECTION'
280
283
  c.flag %i[m move]
281
284
 
285
+ c.desc 'Initial search query for filtering'
286
+ c.arg_name 'QUERY'
287
+ c.flag %i[q query]
288
+
282
289
  c.desc 'Cancel selected items (add @done without timestamp)'
283
290
  c.switch %i[c cancel], negatable: false, default_value: false
284
291
 
@@ -294,9 +301,15 @@ command :select do |c|
294
301
  c.desc 'Add flag to selected item(s)'
295
302
  c.switch %i[flag], negatable: false, default_value: false
296
303
 
304
+ c.desc 'Save selected entries to file using --output format'
305
+ c.arg_name 'FILE'
306
+ c.flag %i[save_to]
307
+
308
+ c.desc 'Output format for export (doing|taskpaper|csv|html|json|template|timeline)'
309
+ c.arg_name 'FORMAT'
310
+ c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i
311
+
297
312
  c.action do |_global_options, options, args|
298
- section = options[:section] || 'All'
299
- edit = options[:editor]
300
313
  wwid.interactive(options)
301
314
  end
302
315
  end
@@ -322,17 +335,17 @@ command :later do |c|
322
335
  c.action do |_global_options, options, args|
323
336
  if options[:back]
324
337
  date = wwid.chronify(options[:back])
325
- raise 'Unable to parse date string' if date.nil?
338
+ exit_now! 'Unable to parse date string' if date.nil?
326
339
  else
327
340
  date = Time.now
328
341
  end
329
342
 
330
343
  if options[:e] || (args.empty? && $stdin.stat.size.zero?)
331
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
344
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
332
345
 
333
346
  input = args.empty? ? '' : args.join(' ')
334
347
  input = wwid.fork_editor(input).strip
335
- raise 'No content' unless input && !input.empty?
348
+ exit_now! 'No content' unless input && !input.empty?
336
349
 
337
350
  title, note = wwid.format_input(input)
338
351
  note.push(options[:n]) if options[:n]
@@ -349,7 +362,7 @@ command :later do |c|
349
362
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
350
363
  wwid.write(wwid.doing_file)
351
364
  else
352
- raise 'You must provide content when creating a new entry'
365
+ exit_now! 'You must provide content when creating a new entry'
353
366
  end
354
367
  end
355
368
  end
@@ -397,19 +410,19 @@ command %i[done did] do |c|
397
410
 
398
411
  if options[:took]
399
412
  took = wwid.chronify_qty(options[:took])
400
- raise 'Unable to parse date string for --took' if took.nil?
413
+ exit_now! 'Unable to parse date string for --took' if took.nil?
401
414
  end
402
415
 
403
416
  if options[:back]
404
417
  date = wwid.chronify(options[:back])
405
- raise 'Unable to parse date string for --back' if date.nil?
418
+ exit_now! 'Unable to parse date string for --back' if date.nil?
406
419
  else
407
420
  date = options[:took] ? Time.now - took : Time.now
408
421
  end
409
422
 
410
423
  if options[:at]
411
424
  finish_date = wwid.chronify(options[:at])
412
- raise 'Unable to parse date string for --at' if finish_date.nil?
425
+ exit_now! 'Unable to parse date string for --at' if finish_date.nil?
413
426
 
414
427
  date = options[:took] ? finish_date - took : finish_date
415
428
  elsif options[:took]
@@ -427,12 +440,12 @@ command %i[done did] do |c|
427
440
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
428
441
 
429
442
  if options[:e]
430
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
443
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
431
444
 
432
445
  input = ''
433
446
  input += args.join(' ') unless args.empty?
434
447
  input = wwid.fork_editor(input).strip
435
- raise 'No content' unless input && !input.empty?
448
+ exit_now! 'No content' unless input && !input.empty?
436
449
 
437
450
  title, note = wwid.format_input(input)
438
451
  title += " @done#{donedate}"
@@ -467,7 +480,7 @@ command %i[done did] do |c|
467
480
  wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date })
468
481
  wwid.write(wwid.doing_file)
469
482
  else
470
- raise 'You must provide content when creating a new entry'
483
+ exit_now! 'You must provide content when creating a new entry'
471
484
  end
472
485
  end
473
486
  end
@@ -513,9 +526,9 @@ command :cancel do |c|
513
526
  end
514
527
  end
515
528
 
516
- raise 'Only one argument allowed' if args.length > 1
529
+ exit_now! 'Only one argument allowed' if args.length > 1
517
530
 
518
- raise 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
531
+ exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
519
532
 
520
533
  count = args[0] ? args[0].to_i : 1
521
534
  opts = {
@@ -580,14 +593,14 @@ command :finish do |c|
580
593
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
581
594
 
582
595
  unless options[:auto]
583
- raise '--back and --took cannot be used together' if options[:back] && options[:took]
596
+ exit_now! '--back and --took cannot be used together' if options[:back] && options[:took]
584
597
 
585
- raise '--search and --tag cannot be used together' if options[:search] && options[:tag]
598
+ exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
586
599
 
587
600
  if options[:back]
588
601
  date = wwid.chronify(options[:back])
589
602
 
590
- raise 'Unable to parse date string' if date.nil?
603
+ exit_now! 'Unable to parse date string' if date.nil?
591
604
  elsif options[:took]
592
605
  date = wwid.chronify_qty(options[:took])
593
606
  else
@@ -611,9 +624,9 @@ command :finish do |c|
611
624
  end
612
625
  end
613
626
 
614
- raise 'Only one argument allowed' if args.length > 1
627
+ exit_now! 'Only one argument allowed' if args.length > 1
615
628
 
616
- raise 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
629
+ exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
617
630
 
618
631
  count = args[0] ? args[0].to_i : 1
619
632
  opts = {
@@ -695,6 +708,9 @@ command :tag do |c|
695
708
  c.arg_name 'COUNT'
696
709
  c.flag %i[c count], default_value: 1
697
710
 
711
+ c.desc 'Don\'t ask permission to tag all entries when count is 0'
712
+ c.switch %i[force], negatable: false, default_value: false
713
+
698
714
  c.desc 'Include current date/time with tag'
699
715
  c.switch %i[d date], negatable: false, default_value: false
700
716
 
@@ -707,12 +723,48 @@ command :tag do |c|
707
723
  c.desc 'Autotag entries based on autotag configuration in ~/.doingrc'
708
724
  c.switch %i[a autotag], negatable: false, default_value: false
709
725
 
726
+ c.desc 'Tag the last X entries containing TAG.
727
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
728
+ c.arg_name 'TAG'
729
+ c.flag [:tag]
730
+
731
+ c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/")'
732
+ c.arg_name 'QUERY'
733
+ c.flag [:search]
734
+
735
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
736
+ c.arg_name 'BOOLEAN'
737
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
738
+
710
739
  c.action do |_global_options, options, args|
711
- raise 'You must specify at least one tag' if args.empty? && !options[:a]
740
+ exit_now! 'You must specify at least one tag' if args.empty? && !options[:a]
712
741
 
713
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
742
+ exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
714
743
 
715
- if options[:a]
744
+ section = 'All'
745
+
746
+ if options[:section]
747
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
748
+ end
749
+
750
+
751
+ if options[:tag].nil?
752
+ search_tags = []
753
+ else
754
+ search_tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
755
+ options[:bool] = case options[:bool]
756
+ when /(and|all)/i
757
+ 'AND'
758
+ when /(any|or)/i
759
+ 'OR'
760
+ when /(not|none)/i
761
+ 'NOT'
762
+ else
763
+ 'AND'
764
+ end
765
+ end
766
+
767
+ if options[:autotag]
716
768
  tags = []
717
769
  else
718
770
  tags = if args.join('') =~ /,/
@@ -724,10 +776,19 @@ command :tag do |c|
724
776
  tags.map! { |tag| tag.sub(/^@/, '').strip }
725
777
  end
726
778
 
727
- count = options[:c].to_i
779
+ count = options[:count].to_i
780
+
781
+ if count.zero? && !options[:force]
782
+ if options[:search]
783
+ section_q = ' matching your search terms'
784
+ elsif options[:tag]
785
+ section_q = ' matching your tag search'
786
+ elsif section == 'All'
787
+ section_q = ''
788
+ else
789
+ section_q = " in section #{section}"
790
+ end
728
791
 
729
- if count.zero?
730
- section_q = section == 'All' ? '' : " in section #{section}"
731
792
 
732
793
  question = if options[:a]
733
794
  "Are you sure you want to autotag all records#{section_q}"
@@ -739,14 +800,18 @@ command :tag do |c|
739
800
 
740
801
  res = wwid.yn(question, default_response: false)
741
802
 
742
- raise 'Cancelled' unless res
803
+ exit_now! 'Cancelled' unless res
743
804
  end
805
+
744
806
  opts = {
745
807
  autotag: options[:a],
746
808
  count: count,
747
809
  date: options[:date],
748
810
  remove: options[:r],
811
+ search: options[:search],
749
812
  section: section,
813
+ tag: search_tags,
814
+ tag_bool: options[:bool],
750
815
  tags: tags,
751
816
  unfinished: options[:unfinished]
752
817
  }
@@ -813,10 +878,10 @@ command :show do |c|
813
878
  c.flag %i[f from]
814
879
 
815
880
  c.desc 'Show time intervals on @done tasks'
816
- c.switch %i[t times], default_value: true
881
+ c.switch %i[t times], default_value: true, negatable: true
817
882
 
818
883
  c.desc 'Show intervals with totals at the end of output'
819
- c.switch [:totals], default_value: false, negatable: true
884
+ c.switch [:totals], default_value: false, negatable: false
820
885
 
821
886
  c.desc 'Sort tags by (name|time)'
822
887
  default = 'time'
@@ -845,7 +910,7 @@ command :show do |c|
845
910
  section = 'All'
846
911
  else
847
912
  section = wwid.guess_section(args[0])
848
- raise "No such section: #{args[0]}" unless section
913
+ exit_now! "No such section: #{args[0]}" unless section
849
914
 
850
915
  args.shift
851
916
  end
@@ -935,10 +1000,10 @@ command [:grep, :search] do |c|
935
1000
  c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
936
1001
 
937
1002
  c.desc 'Show time intervals on @done tasks'
938
- c.switch %i[t times], default_value: true
1003
+ c.switch %i[t times], default_value: true, negatable: true
939
1004
 
940
1005
  c.desc 'Show intervals with totals at the end of output'
941
- c.switch [:totals], default_value: false, negatable: true
1006
+ c.switch [:totals], default_value: false, negatable: false
942
1007
 
943
1008
  c.desc 'Sort tags by (name|time)'
944
1009
  default = 'time'
@@ -982,10 +1047,10 @@ command :recent do |c|
982
1047
  c.flag %i[s section], default_value: 'All'
983
1048
 
984
1049
  c.desc 'Show time intervals on @done tasks'
985
- c.switch %i[t times], default_value: true
1050
+ c.switch %i[t times], default_value: true, negatable: true
986
1051
 
987
1052
  c.desc 'Show intervals with totals at the end of output'
988
- c.switch [:totals], default_value: false, negatable: true
1053
+ c.switch [:totals], default_value: false, negatable: false
989
1054
 
990
1055
  c.desc 'Sort tags by (name|time)'
991
1056
  default = 'time'
@@ -1022,10 +1087,10 @@ command :today do |c|
1022
1087
  c.flag %i[s section], default_value: 'All'
1023
1088
 
1024
1089
  c.desc 'Show time intervals on @done tasks'
1025
- c.switch %i[t times], default_value: true
1090
+ c.switch %i[t times], default_value: true, negatable: true
1026
1091
 
1027
1092
  c.desc 'Show time totals at the end of output'
1028
- c.switch [:totals], default_value: false, negatable: true
1093
+ c.switch [:totals], default_value: false, negatable: false
1029
1094
 
1030
1095
  c.desc 'Sort tags by (name|time)'
1031
1096
  default = 'time'
@@ -1057,10 +1122,10 @@ command :on do |c|
1057
1122
  c.flag %i[s section], default_value: 'All'
1058
1123
 
1059
1124
  c.desc 'Show time intervals on @done tasks'
1060
- c.switch %i[t times], default_value: true
1125
+ c.switch %i[t times], default_value: true, negatable: true
1061
1126
 
1062
1127
  c.desc 'Show time totals at the end of output'
1063
- c.switch [:totals], default_value: false, negatable: true
1128
+ c.switch [:totals], default_value: false, negatable: false
1064
1129
 
1065
1130
  c.desc 'Sort tags by (name|time)'
1066
1131
  default = 'time'
@@ -1100,6 +1165,55 @@ command :on do |c|
1100
1165
  end
1101
1166
  end
1102
1167
 
1168
+ desc 'List entries since a date'
1169
+ long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday,"
1170
+ and "2d" would be interpreted as "two days ago.")
1171
+ arg_name 'DATE_STRING'
1172
+ command :since do |c|
1173
+ c.desc 'Section'
1174
+ c.arg_name 'NAME'
1175
+ c.flag %i[s section], default_value: 'All'
1176
+
1177
+ c.desc 'Show time intervals on @done tasks'
1178
+ c.switch %i[t times], default_value: true, negatable: true
1179
+
1180
+ c.desc 'Show time totals at the end of output'
1181
+ c.switch [:totals], default_value: false, negatable: false
1182
+
1183
+ c.desc 'Sort tags by (name|time)'
1184
+ default = 'time'
1185
+ default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1186
+ c.arg_name 'KEY'
1187
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1188
+
1189
+ c.desc 'Output to export format (csv|html|json|template|timeline)'
1190
+ c.arg_name 'FORMAT'
1191
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1192
+
1193
+ c.action do |_global_options, options, args|
1194
+ exit_now! 'Missing date argument' if args.empty?
1195
+
1196
+ date_string = args.join(' ')
1197
+
1198
+ date_string += ' at midnight' unless date_string =~ /(\d:|\d *[ap]m?|midnight|noon)/i
1199
+ date_string.sub!(/(day) (\d)/, '\1 at \2') if date_string =~ /day \d/
1200
+
1201
+ start = wwid.chronify(date_string)
1202
+ finish = Time.now
1203
+
1204
+ exit_now! 'Unrecognized date string' unless start
1205
+
1206
+ message = "Date interpreted as #{start} through the current time"
1207
+ wwid.results.push(message)
1208
+
1209
+ options[:t] = true if options[:totals]
1210
+ options[:sort_tags] = options[:tag_sort] =~ /^n/i
1211
+
1212
+ puts wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1213
+ { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1214
+ end
1215
+ end
1216
+
1103
1217
  desc 'List entries from yesterday'
1104
1218
  command :yesterday do |c|
1105
1219
  c.desc 'Specify a section'
@@ -1111,10 +1225,10 @@ command :yesterday do |c|
1111
1225
  c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1112
1226
 
1113
1227
  c.desc 'Show time intervals on @done tasks'
1114
- c.switch %i[t times], default_value: true
1228
+ c.switch %i[t times], default_value: true, negatable: true
1115
1229
 
1116
1230
  c.desc 'Show time totals at the end of output'
1117
- c.switch [:totals], default_value: false, negatable: true
1231
+ c.switch [:totals], default_value: false, negatable: false
1118
1232
 
1119
1233
  c.desc 'Sort tags by (name|time)'
1120
1234
  default = 'time'
@@ -1151,7 +1265,7 @@ command :last do |c|
1151
1265
  c.flag [:search]
1152
1266
 
1153
1267
  c.action do |_global_options, options, _args|
1154
- raise '--tag and --search cannot be used together' if options[:tag] && options[:search]
1268
+ exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search]
1155
1269
 
1156
1270
  if options[:tag].nil?
1157
1271
  tags = []
@@ -1200,7 +1314,7 @@ desc 'Add a new section to the "doing" file'
1200
1314
  arg_name 'SECTION_NAME'
1201
1315
  command :add_section do |c|
1202
1316
  c.action do |_global_options, _options, args|
1203
- raise "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1317
+ exit_now! "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1204
1318
 
1205
1319
  wwid.add_section(args[0].cap_first)
1206
1320
  wwid.write(wwid.doing_file)
@@ -1241,10 +1355,10 @@ command :view do |c|
1241
1355
  c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1242
1356
 
1243
1357
  c.desc 'Show time intervals on @done tasks'
1244
- c.switch %i[t times], default_value: true
1358
+ c.switch %i[t times], default_value: true, negatable: true
1245
1359
 
1246
1360
  c.desc 'Show intervals with totals at the end of output'
1247
- c.switch [:totals], default_value: false, negatable: true
1361
+ c.switch [:totals], default_value: false, negatable: false
1248
1362
 
1249
1363
  c.desc 'Include colors in output'
1250
1364
  c.switch [:color], default_value: true, negatable: true
@@ -1256,7 +1370,7 @@ command :view do |c|
1256
1370
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1257
1371
 
1258
1372
  c.desc 'Only show items with recorded time intervals'
1259
- c.switch [:only_timed], default_value: false, negatable: true
1373
+ c.switch [:only_timed], default_value: false, negatable: false
1260
1374
 
1261
1375
  c.action do |_global_options, options, args|
1262
1376
  title = if args.empty?
@@ -1328,7 +1442,7 @@ command :view do |c|
1328
1442
  elsif title.instance_of?(FalseClass)
1329
1443
  exit_now! 'Cancelled'
1330
1444
  else
1331
- raise "View #{title} not found in config"
1445
+ exit_now! "View #{title} not found in config"
1332
1446
  end
1333
1447
  end
1334
1448
  end
@@ -1385,7 +1499,7 @@ command :archive do |c|
1385
1499
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
1386
1500
  end
1387
1501
 
1388
- raise '--keep and --count can\'t be used together' if options[:keep] && options[:count]
1502
+ exit_now! '--keep and --count can\'t be used together' if options[:keep] && options[:count]
1389
1503
 
1390
1504
  tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
1391
1505
 
@@ -1436,7 +1550,7 @@ command :open do |c|
1436
1550
  elsif options[:b]
1437
1551
  system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
1438
1552
  elsif options[:e]
1439
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1553
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1440
1554
 
1441
1555
  system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
1442
1556
  elsif wwid.config.key?('editor_app') && !wwid.config['editor_app'].nil?
@@ -1446,7 +1560,7 @@ command :open do |c|
1446
1560
  end
1447
1561
 
1448
1562
  else
1449
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1563
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1450
1564
 
1451
1565
  system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
1452
1566
  end
@@ -1483,13 +1597,13 @@ command :config do |c|
1483
1597
  `open -b #{options[:b]} "#{wwid.config_file}"`
1484
1598
  end
1485
1599
  else
1486
- raise 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1600
+ exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1487
1601
 
1488
1602
  editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1489
1603
  system %(#{editor} "#{wwid.config_file}")
1490
1604
  end
1491
1605
  else
1492
- raise 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1606
+ exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1493
1607
 
1494
1608
  editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1495
1609
  system %(#{editor} "#{wwid.config_file}")
@@ -1542,7 +1656,7 @@ command :import do |c|
1542
1656
  wwid.write(wwid.doing_file)
1543
1657
  end
1544
1658
  else
1545
- raise 'Invalid import type'
1659
+ exit_now! 'Invalid import type'
1546
1660
  end
1547
1661
  end
1548
1662
  end
data/lib/doing/helpers.rb CHANGED
@@ -92,7 +92,7 @@ class ::String
92
92
  def link_urls(opt = {})
93
93
  opt[:format] ||= :html
94
94
  if opt[:format] == :html
95
- gsub(%r{(?mi)((http|https)://)?([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
95
+ gsub(%r{(?mi)((http|https)://)([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
96
96
  m = Regexp.last_match
97
97
  proto = m[1].nil? ? 'http://' : ''
98
98
  %(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.73'
2
+ VERSION = '1.0.78'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -73,7 +73,7 @@ class WWID
73
73
  rescue StandardError
74
74
  @config = {}
75
75
  @local_config = {}
76
- # raise "error reading config"
76
+ # exit_now! "error reading config"
77
77
  end
78
78
  end
79
79
 
@@ -158,7 +158,7 @@ class WWID
158
158
 
159
159
  # if ENV['DOING_DEBUG'].to_i == 3
160
160
  # if @config['default_tags'].length > 0
161
- # raise "DEFAULT CONFIG CHANGED"
161
+ # exit_now! "DEFAULT CONFIG CHANGED"
162
162
  # end
163
163
  # end
164
164
 
@@ -293,14 +293,14 @@ class WWID
293
293
  if $?.exitstatus == 0
294
294
  input = IO.read(tmpfile.path)
295
295
  else
296
- raise 'Cancelled'
296
+ exit_now! 'Cancelled'
297
297
  end
298
298
  ensure
299
299
  tmpfile.close
300
300
  tmpfile.unlink
301
301
  end
302
302
 
303
- input.split(/\n/).delete_if {|line| line =~ /^#/ }.join("\n").strip
303
+ input.split(/\n/).delete_if {|line| line =~ /^#/ }.join("\n")
304
304
  end
305
305
 
306
306
  #
@@ -311,11 +311,11 @@ class WWID
311
311
  # @param input (String) The string to parse
312
312
  #
313
313
  def format_input(input)
314
- raise 'No content in entry' if input.nil? || input.strip.empty?
314
+ exit_now! 'No content in entry' if input.nil? || input.strip.empty?
315
315
 
316
316
  input_lines = input.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
317
317
  title = input_lines[0]&.strip
318
- raise 'No content in first line' if title.nil? || title.strip.empty?
318
+ exit_now! 'No content in first line' if title.nil? || title.strip.empty?
319
319
 
320
320
  note = input_lines.length > 1 ? input_lines[1..-1] : []
321
321
  # If title line ends in a parenthetical, use that as the note
@@ -346,7 +346,7 @@ class WWID
346
346
  #
347
347
  def chronify(input)
348
348
  now = Time.now
349
- raise "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
349
+ exit_now! "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
350
350
 
351
351
  secs_ago = if input.match(/^(\d+)$/)
352
352
  # plain number, assume minutes
@@ -438,7 +438,7 @@ class WWID
438
438
  end
439
439
  unless section || guessed
440
440
  alt = guess_view(frag, true)
441
- raise "Did you mean `doing view #{alt}`?" if alt
441
+ exit_now! "Did you mean `doing view #{alt}`?" if alt
442
442
 
443
443
  res = yn("Section #{frag} not found, create it", default_response: false)
444
444
 
@@ -448,7 +448,7 @@ class WWID
448
448
  return frag.cap_first
449
449
  end
450
450
 
451
- raise "Unknown section: #{frag}"
451
+ exit_now! "Unknown section: #{frag}"
452
452
  end
453
453
  section ? section.cap_first : guessed
454
454
  end
@@ -462,7 +462,7 @@ class WWID
462
462
  ## @return (Bool) yes or no
463
463
  ##
464
464
  def yn(question, default_response: false)
465
- default = default_response ? 'y' : 'n'
465
+ default = default_response ? default_response : 'n'
466
466
 
467
467
  # if this isn't an interactive shell, answer default
468
468
  return default.downcase == 'y' unless $stdout.isatty
@@ -518,9 +518,9 @@ class WWID
518
518
  unless view || guessed
519
519
  alt = guess_section(frag, guessed: true)
520
520
  if alt
521
- raise "Did you mean `doing show #{alt}`?"
521
+ exit_now! "Did you mean `doing show #{alt}`?"
522
522
  else
523
- raise "Unknown view: #{frag}"
523
+ exit_now! "Unknown view: #{frag}"
524
524
  end
525
525
  end
526
526
  view
@@ -631,7 +631,7 @@ class WWID
631
631
 
632
632
  add_tags = opt[:tag] ? opt[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '@') }.join(' ') : ''
633
633
  prefix = opt[:prefix] ? opt[:prefix] : '[Timing.app]'
634
- raise "File not found" unless File.exist?(File.expand_path(path))
634
+ exit_now! "File not found" unless File.exist?(File.expand_path(path))
635
635
 
636
636
  data = JSON.parse(IO.read(File.expand_path(path)))
637
637
  new_items = []
@@ -688,7 +688,7 @@ class WWID
688
688
  section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
689
689
  end
690
690
 
691
- raise "Section #{section} not found" unless @content.key?(section)
691
+ exit_now! "Section #{section} not found" unless @content.key?(section)
692
692
 
693
693
  last_item = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse[0]
694
694
  warn "Editing note for #{last_item['title']}"
@@ -766,6 +766,22 @@ class WWID
766
766
  all_items.max_by { |item| item['date'] }
767
767
  end
768
768
 
769
+ ##
770
+ ## @brief Generate a menu of options and allow user selection
771
+ ##
772
+ ## @return (String) The selected option
773
+ ##
774
+ def choose_from(options, prompt)
775
+ puts prompt
776
+ options.each_with_index do |section, i|
777
+ puts format('% 3d: %s', i + 1, section)
778
+ end
779
+ print "#{colors['green']}> #{colors['default']}"
780
+ num = STDIN.gets
781
+ return false if num =~ /^[a-z ]*$/i
782
+
783
+ options[num.to_i - 1]
784
+ end
769
785
 
770
786
  ##
771
787
  ## @brief Display an interactive menu of entries
@@ -773,7 +789,7 @@ class WWID
773
789
  ## @param opt (Hash) Additional options
774
790
  ##
775
791
  def interactive(opt = {})
776
- raise "Select command requires that fzf be installed" unless exec_available('fzf')
792
+ fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
777
793
 
778
794
  section = opt[:section] ? guess_section(opt[:section]) : 'All'
779
795
 
@@ -807,7 +823,7 @@ class WWID
807
823
  out.join('')
808
824
  end
809
825
 
810
- res = `echo #{Shellwords.escape(options.join("\n"))}|fzf -m`
826
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} -m --bind ctrl-a:select-all -q "#{opt[:query]}"`
811
827
  selected = []
812
828
  res.split(/\n/).each do |item|
813
829
  idx = item.match(/^(\d+)\)/)[1].to_i
@@ -819,6 +835,59 @@ class WWID
819
835
  return
820
836
  end
821
837
 
838
+ unless opt[:delete] || opt[:flag] || opt[:finish] || opt[:cancel] || opt[:tag] || opt[:archive] || opt[:output] || opt[:save_to]
839
+ action = choose_from(
840
+ [
841
+ "add tag",
842
+ "remove tag",
843
+ "archive",
844
+ "cancel",
845
+ "delete",
846
+ "edit",
847
+ "finish",
848
+ "flag",
849
+ "move",
850
+ "output format"
851
+ ],
852
+ 'What do you want to do with the selected items?')
853
+ case action
854
+ when /(add|remove) tag/
855
+ print "Enter tag: "
856
+ tag = STDIN.gets
857
+ return if tag =~ /^ *$/
858
+ opt[:tag] = tag.strip
859
+ opt[:remove] = true if action =~ /remove tag/
860
+ when /output format/
861
+ output_format = choose_from(%w[doing taskpaper json timeline html csv], 'Which output format?')
862
+ return if tag =~ /^ *$/
863
+ opt[:output] = output_format.strip
864
+ res = yn("Save to file?", default_response: 'n')
865
+ if res
866
+ print "File path/name: "
867
+ filename = STDIN.gets.strip
868
+ return if filename.empty?
869
+ opt[:save_to] = filename
870
+ end
871
+ when /archive/
872
+ opt[:archive] = true
873
+ when /delete/
874
+ opt[:delete] = true
875
+ when /edit/
876
+ opt[:editor] = true
877
+ when /finish/
878
+ opt[:finish] = true
879
+ when /cancel/
880
+ opt[:cancel] = true
881
+ when /move/
882
+ section = choose_section.strip
883
+ return if section =~ /^ *$/
884
+ opt[:move] = section.strip
885
+ when /flag/
886
+ opt[:flag] = true
887
+ end
888
+ end
889
+
890
+
822
891
  if opt[:delete]
823
892
  res = yn("Delete #{selected.size} items?", default_response: 'y')
824
893
  if res
@@ -830,17 +899,35 @@ class WWID
830
899
 
831
900
  if opt[:flag]
832
901
  tag = @config['marker_tag'] || 'flagged'
833
- selected.map! {|item| tag_item(item, tag, date: false) }
902
+ selected.map! do |item|
903
+ if opt[:remove]
904
+ untag_item(item, tag)
905
+ else
906
+ tag_item(item, tag, date: false)
907
+ end
908
+ end
834
909
  end
835
910
 
836
911
  if opt[:finish] || opt[:cancel]
837
912
  tag = 'done'
838
- selected.map! {|item| tag_item(item, tag, date: !opt[:cancel])}
913
+ selected.map! do |item|
914
+ if opt[:remove]
915
+ untag_item(item, tag)
916
+ else
917
+ tag_item(item, tag, date: !opt[:cancel])
918
+ end
919
+ end
839
920
  end
840
921
 
841
922
  if opt[:tag]
842
923
  tag = opt[:tag]
843
- selected.map! {|item| tag_item(item, tag, date: false)}
924
+ selected.map! do |item|
925
+ if opt[:remove]
926
+ untag_item(item, tag)
927
+ else
928
+ tag_item(item, tag, date: false)
929
+ end
930
+ end
844
931
  end
845
932
 
846
933
  if opt[:archive] || opt[:move]
@@ -860,13 +947,17 @@ class WWID
860
947
  editable += "\n#{old_note}" unless old_note.nil?
861
948
  editable_items << editable
862
949
  end
950
+ divider = "\n-----------\n"
951
+ input = editable_items.map(&:strip).join(divider) + "\n\n# You may delete entries, but leave all divider lines in place"
863
952
 
864
- new_items = fork_editor(editable_items.join("\n---\n") + "\n\n# You may delete entries, but leave all --- lines in place").split(/\n---\n/)
953
+ new_items = fork_editor(input).split(/#{divider}/)
865
954
 
866
955
  new_items.each_with_index do |new_item, i|
956
+
867
957
  input_lines = new_item.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
868
958
  title = input_lines[0]&.strip
869
- if title.nil? || title =~ /^---$/ || title.strip.empty?
959
+
960
+ if title.nil? || title =~ /^#{divider.strip}$/ || title.strip.empty?
870
961
  delete_item(selected[i])
871
962
  else
872
963
  note = input_lines.length > 1 ? input_lines[1..-1] : []
@@ -880,13 +971,47 @@ class WWID
880
971
  item = selected[i].dup
881
972
  item['title'] = title
882
973
  item['note'] = note
883
- item['date'] = Time.parse(date)
974
+ item['date'] = Time.parse(date) || selected[i]['date']
884
975
  update_item(selected[i], item)
885
976
  end
886
977
  end
887
978
 
888
979
  write(@doing_file)
889
980
  end
981
+
982
+ if opt[:output]
983
+ selected.map! do |item|
984
+ item['title'] = "#{item['title']} @project(#{item['section']})"
985
+ item
986
+ end
987
+
988
+ @content = {'Export' => {'original' => 'Export:', 'items' => selected}}
989
+ options = {section: 'Export'}
990
+
991
+ if opt[:output] !~ /(doing|taskpaper)/
992
+ options[:output] = opt[:output]
993
+ else
994
+ options[:template] = '- %date | %title%note'
995
+ end
996
+
997
+ output = list_section(options)
998
+
999
+ if opt[:save_to]
1000
+ file = File.expand_path(opt[:save_to])
1001
+ if File.exist?(file)
1002
+ # Create a backup copy for the undo command
1003
+ FileUtils.cp(file, "#{file}~")
1004
+ end
1005
+
1006
+ File.open(file, 'w+') do |f|
1007
+ f.puts output
1008
+ end
1009
+
1010
+ @results.push("Export saved to #{file}")
1011
+ else
1012
+ puts output
1013
+ end
1014
+ end
890
1015
  end
891
1016
 
892
1017
  ##
@@ -895,7 +1020,7 @@ class WWID
895
1020
  ## @param opt (Hash) Additional Options
896
1021
  ##
897
1022
  def tag_last(opt = {})
898
- opt[:section] ||= @current_section
1023
+ opt[:section] ||= nil
899
1024
  opt[:count] ||= 1
900
1025
  opt[:archive] ||= false
901
1026
  opt[:tags] ||= ['done']
@@ -910,7 +1035,11 @@ class WWID
910
1035
  sec_arr = []
911
1036
 
912
1037
  if opt[:section].nil?
913
- sec_arr = [@current_section]
1038
+ if opt[:search] || opt[:tag]
1039
+ sec_arr = sections
1040
+ else
1041
+ sec_arr = [@current_section]
1042
+ end
914
1043
  elsif opt[:section].instance_of?(String)
915
1044
  if opt[:section] =~ /^all$/i
916
1045
  if opt[:count] == 1
@@ -921,7 +1050,11 @@ class WWID
921
1050
  items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
922
1051
  sec_arr.push(items[0]['section'])
923
1052
  elsif opt[:count] > 1
924
- raise 'A count greater than one requires a section to be specified'
1053
+ if opt[:search] || opt[:tag]
1054
+ sec_arr = sections
1055
+ else
1056
+ exit_now! 'A count greater than one requires a section to be specified'
1057
+ end
925
1058
  else
926
1059
  sec_arr = sections
927
1060
  end
@@ -1021,7 +1154,7 @@ class WWID
1021
1154
  @results.push('Archiving is skipped when operating on all entries') if (opt[:count]).zero?
1022
1155
  end
1023
1156
  else
1024
- raise "Section not found: #{section}"
1157
+ exit_now! "Section not found: #{section}"
1025
1158
  end
1026
1159
  end
1027
1160
 
@@ -1060,6 +1193,28 @@ class WWID
1060
1193
  @content[section]['items'] = section_items
1061
1194
  end
1062
1195
 
1196
+ ##
1197
+ ## @brief Remove a tag on an item from the index
1198
+ ##
1199
+ ## @param old_item (Item) The item to tag
1200
+ ## @param tag (string) The tag to remove
1201
+ ##
1202
+ def untag_item(old_item, tag)
1203
+ title = old_item['title'].dup
1204
+
1205
+ if title =~ /@#{tag}/
1206
+ title.chomp!
1207
+ title.gsub!(/ +@#{tag}(\(.*?\))?/, '')
1208
+ new_item = old_item.dup
1209
+ new_item['title'] = title
1210
+ update_item(old_item, new_item)
1211
+ return new_item
1212
+ else
1213
+ @results.push(%(Item isn't tagged @#{tag}: "#{title}" in #{old_item['section']}))
1214
+ return old_item
1215
+ end
1216
+ end
1217
+
1063
1218
  ##
1064
1219
  ## @brief Tag an item from the index
1065
1220
  ##
@@ -1067,7 +1222,7 @@ class WWID
1067
1222
  ## @param tag (string) The tag to apply
1068
1223
  ## @param date (Boolean) Include timestamp?
1069
1224
  ##
1070
- def tag_item(old_item, tag, date: false)
1225
+ def tag_item(old_item, tag, remove: false, date: false)
1071
1226
  title = old_item['title'].dup
1072
1227
  done_date = Time.now
1073
1228
  if title !~ /@#{tag}/
@@ -1191,7 +1346,7 @@ class WWID
1191
1346
  section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
1192
1347
  end
1193
1348
 
1194
- raise "Section #{section} not found" unless @content.key?(section)
1349
+ exit_now! "Section #{section} not found" unless @content.key?(section)
1195
1350
 
1196
1351
  # sort_section(opt[:section])
1197
1352
  items = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse
@@ -1297,10 +1452,10 @@ class WWID
1297
1452
  if File.exist?(file)
1298
1453
  # Create a backup copy for the undo command
1299
1454
  FileUtils.cp(file, "#{file}~")
1455
+ end
1300
1456
 
1301
- File.open(file, 'w+') do |f|
1302
- f.puts output
1303
- end
1457
+ File.open(file, 'w+') do |f|
1458
+ f.puts output
1304
1459
  end
1305
1460
 
1306
1461
  if @config.key?('run_after')
@@ -1427,7 +1582,7 @@ class WWID
1427
1582
  end
1428
1583
  end
1429
1584
 
1430
- raise 'Invalid section object' unless opt[:section].instance_of? Hash
1585
+ exit_now! 'Invalid section object' unless opt[:section].instance_of? Hash
1431
1586
 
1432
1587
  items = opt[:section]['items'].sort_by { |item| item['date'] }
1433
1588
 
@@ -1477,7 +1632,7 @@ class WWID
1477
1632
 
1478
1633
  out = ''
1479
1634
 
1480
- raise 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1635
+ exit_now! 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1481
1636
 
1482
1637
  case opt[:output]
1483
1638
  when /^csv$/i
@@ -1514,13 +1669,16 @@ class WWID
1514
1669
  note ||= ''
1515
1670
 
1516
1671
  tags = []
1672
+ attributes = {}
1517
1673
  skip_tags = %w[meanwhile done cancelled flagged]
1518
1674
  i['title'].scan(/@([^(\s]+)(?:\((.*?)\))?/).each do |tag|
1519
1675
  tags.push(tag[0]) unless skip_tags.include?(tag[0])
1676
+ attributes[tag[0]] = tag[1] if tag[1]
1520
1677
  end
1678
+
1521
1679
  if opt[:output] == 'json'
1522
1680
 
1523
- items_out << {
1681
+ i = {
1524
1682
  date: i['date'],
1525
1683
  end_date: end_date,
1526
1684
  title: title.strip, #+ " #{note}"
@@ -1529,6 +1687,10 @@ class WWID
1529
1687
  tags: tags
1530
1688
  }
1531
1689
 
1690
+ attributes.each { |attr, val| i[attr.to_sym] = val }
1691
+
1692
+ items_out << i
1693
+
1532
1694
  elsif opt[:output] == 'timeline'
1533
1695
  new_item = {
1534
1696
  'id' => index + 1,
@@ -1632,7 +1794,7 @@ class WWID
1632
1794
 
1633
1795
  totals = opt[:totals] ? tag_times('html', opt[:sort_tags]) : ''
1634
1796
  engine = Haml::Engine.new(template)
1635
- puts engine.render(Object.new,
1797
+ out = engine.render(Object.new,
1636
1798
  { :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
1637
1799
  else
1638
1800
  items.each do |item|
@@ -1644,11 +1806,12 @@ class WWID
1644
1806
  reset = ''
1645
1807
  end
1646
1808
 
1647
- if (item.has_key?('note') && !item['note'].empty?) && @config[:include_notes]
1809
+ if (item.key?('note') && !item['note'].empty?) && @config[:include_notes]
1648
1810
  note_lines = item['note'].delete_if do |line|
1649
- line =~ /^\s*$/
1650
- end.map { |line| "\t\t" + line.sub(/^\t*/, '').sub(/^-/, '—') + ' ' }
1651
- if opt[:wrap_width] && opt[:wrap_width] > 0
1811
+ line =~ /^\s*$/
1812
+ end
1813
+ note_lines.map! { |line| "\t\t#{line.sub(/^\t*/, '').sub(/^-/, '—')} " }
1814
+ if opt[:wrap_width]&.positive?
1652
1815
  width = opt[:wrap_width]
1653
1816
  note_lines.map! do |line|
1654
1817
  line.strip.gsub(/(.{1,#{width}})(\s+|\Z)/, "\t\\1\n")
@@ -1661,7 +1824,7 @@ class WWID
1661
1824
  output = opt[:template].dup
1662
1825
 
1663
1826
  output.gsub!(/%[a-z]+/) do |m|
1664
- if colors.has_key?(m.sub(/^%/, ''))
1827
+ if colors.key?(m.sub(/^%/, ''))
1665
1828
  colors[m.sub(/^%/, '')]
1666
1829
  else
1667
1830
  m
@@ -1750,7 +1913,7 @@ class WWID
1750
1913
  do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label] })
1751
1914
  write(doing_file)
1752
1915
  else
1753
- raise 'Either source or destination does not exist'
1916
+ exit_now! 'Either source or destination does not exist'
1754
1917
  end
1755
1918
  end
1756
1919
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.73
4
+ version: 1.0.78
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-16 00:00:00.000000000 Z
11
+ date: 2021-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 6.2.1
39
+ version: 6.3.1
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 6.2.1
46
+ version: 6.3.1
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: aruba
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -62,16 +62,16 @@ dependencies:
62
62
  name: test-unit
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - ">="
65
+ - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0'
67
+ version: 3.4.4
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
- - - ">="
72
+ - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '0'
74
+ version: 3.4.4
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: gli
77
77
  requirement: !ruby/object:Gem::Requirement