doing 1.0.70 → 1.0.75

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -2
  3. data/bin/doing +208 -62
  4. data/lib/doing/version.rb +1 -1
  5. data/lib/doing/wwid.rb +235 -28
  6. metadata +8 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10dc5d92f3e7079a9525d39c47fe0f69ef22f64ae5400cd9da1925f9b0fc1964
4
- data.tar.gz: ec08df722ce07bbfcee808bfd4bd9eabe5470ef173f5d11e47fc52356b2c2c95
3
+ metadata.gz: 4acc94f7b7b30e01c77c1f5b026e6b815cd1d55b19d2afa7c9a029a46f201659
4
+ data.tar.gz: e72ee1110adb862162888690ff17002d8cb17e6810362c46994282293b4c8d4c
5
5
  SHA512:
6
- metadata.gz: c56a9a328d2ed31e70633333be5f7666d9586154fba7cc59f34116f1a972156fb4dd7d087e59452bf860167e618dad815da8a113fe0e4cf9f12faafa98d68d72
7
- data.tar.gz: 3152021d8017af18e4046736f1f994bd1821f95bece924ca9ad78de997ad3473cbfd411389ede1dfe6313a1c4fde3debdc9907f3f2da899b1bab823a02a932e7
6
+ metadata.gz: ff6f4bb0dad93e88beb67b1e96b8d094a6b6b8afb5785d79472c3946e6ac510f8f1d233133972347ba66e256056296d6905753192d7c81152dc2a186f535acda
7
+ data.tar.gz: 1d7147a5055e593ff5a5b0116ba71525f66d171b042437836c4112ded26a2c68a8b1a1e1b793b19901b0e69e4da24f9759dbdecfaff0e9563e74f92d049a825e
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.69<!--END VER-->.
30
+ The current version of `doing` is <!--VER-->1.0.74<!--END VER-->.
31
31
 
32
32
  $ [sudo] gem install doing
33
33
 
@@ -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
 
@@ -617,6 +617,32 @@ Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is t
617
617
  # Import to default section (Currently) and prefix entries with '[Imported]'
618
618
  doing import --prefix="[Imported]" "~/Desktop/All Activities.json"
619
619
 
620
+ #### Interactive Usage
621
+
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
+
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
+
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
+
628
+ Run `doing help select` for a list of options:
629
+
630
+ -a, --archive - Archive selected items
631
+ -c, --cancel - Cancel selected items (add @done without timestamp)
632
+ -d, --delete - Delete selected items
633
+ -e, --editor - Edit selected item(s)
634
+ -f, --finish - Add @done with current time to selected item(s)
635
+ --flag - Add flag to selected item(s)
636
+ -m, --move=SECTION - Move selected items to section (default: none)
637
+ -s, --section=SECTION - Select from a specific section (default: none)
638
+ -t, --tag=TAG - Tag selected entries (default: none)
639
+
640
+ For example, `doing select -d -a` would present the menu, and then mark selected entries as @done (with timestamp) and move them to the Archive section.
641
+
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
+
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
+
620
646
  ---
621
647
 
622
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,19 +243,64 @@ 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
- case options[:t]
248
+ case args[0]
249
249
  when /html|haml/i
250
250
  $stdout.puts wwid.haml_template
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
+ 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>
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.'
263
+ command :select do |c|
264
+ c.desc 'Select from a specific section'
265
+ c.arg_name 'SECTION'
266
+ c.flag %i[s section]
267
+
268
+ c.desc 'Tag selected entries'
269
+ c.arg_name 'TAG'
270
+ c.flag %i[t tag]
271
+
272
+ # c.desc 'Add @done to selected item(s), using start time of next item as the finish time'
273
+ # c.switch %i[a auto], negatable: false, default_value: false
274
+
275
+ c.desc 'Archive selected items'
276
+ c.switch %i[a archive], negatable: false, default_value: false
277
+
278
+ c.desc 'Move selected items to section'
279
+ c.arg_name 'SECTION'
280
+ c.flag %i[m move]
281
+
282
+ c.desc 'Cancel selected items (add @done without timestamp)'
283
+ c.switch %i[c cancel], negatable: false, default_value: false
284
+
285
+ c.desc 'Delete selected items'
286
+ c.switch %i[d delete], negatable: false, default_value: false
287
+
288
+ c.desc 'Edit selected item(s)'
289
+ c.switch %i[e editor], negatable: false, default_value: false
290
+
291
+ c.desc 'Add @done with current time to selected item(s)'
292
+ c.switch %i[f finish], negatable: false, default_value: false
293
+
294
+ c.desc 'Add flag to selected item(s)'
295
+ c.switch %i[flag], negatable: false, default_value: false
296
+
297
+ c.action do |_global_options, options, args|
298
+ section = options[:section] || 'All'
299
+ edit = options[:editor]
300
+ wwid.interactive(options)
301
+ end
302
+ end
303
+
259
304
  desc 'Add an item to the Later section'
260
305
  arg_name 'ENTRY'
261
306
  command :later do |c|
@@ -277,17 +322,17 @@ command :later do |c|
277
322
  c.action do |_global_options, options, args|
278
323
  if options[:back]
279
324
  date = wwid.chronify(options[:back])
280
- raise 'Unable to parse date string' if date.nil?
325
+ exit_now! 'Unable to parse date string' if date.nil?
281
326
  else
282
327
  date = Time.now
283
328
  end
284
329
 
285
330
  if options[:e] || (args.empty? && $stdin.stat.size.zero?)
286
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
331
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
287
332
 
288
333
  input = args.empty? ? '' : args.join(' ')
289
334
  input = wwid.fork_editor(input).strip
290
- raise 'No content' unless input && !input.empty?
335
+ exit_now! 'No content' unless input && !input.empty?
291
336
 
292
337
  title, note = wwid.format_input(input)
293
338
  note.push(options[:n]) if options[:n]
@@ -304,7 +349,7 @@ command :later do |c|
304
349
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
305
350
  wwid.write(wwid.doing_file)
306
351
  else
307
- raise 'You must provide content when creating a new entry'
352
+ exit_now! 'You must provide content when creating a new entry'
308
353
  end
309
354
  end
310
355
  end
@@ -352,19 +397,19 @@ command %i[done did] do |c|
352
397
 
353
398
  if options[:took]
354
399
  took = wwid.chronify_qty(options[:took])
355
- raise 'Unable to parse date string for --took' if took.nil?
400
+ exit_now! 'Unable to parse date string for --took' if took.nil?
356
401
  end
357
402
 
358
403
  if options[:back]
359
404
  date = wwid.chronify(options[:back])
360
- raise 'Unable to parse date string for --back' if date.nil?
405
+ exit_now! 'Unable to parse date string for --back' if date.nil?
361
406
  else
362
407
  date = options[:took] ? Time.now - took : Time.now
363
408
  end
364
409
 
365
410
  if options[:at]
366
411
  finish_date = wwid.chronify(options[:at])
367
- raise 'Unable to parse date string for --at' if finish_date.nil?
412
+ exit_now! 'Unable to parse date string for --at' if finish_date.nil?
368
413
 
369
414
  date = options[:took] ? finish_date - took : finish_date
370
415
  elsif options[:took]
@@ -382,12 +427,12 @@ command %i[done did] do |c|
382
427
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
383
428
 
384
429
  if options[:e]
385
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
430
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
386
431
 
387
432
  input = ''
388
433
  input += args.join(' ') unless args.empty?
389
434
  input = wwid.fork_editor(input).strip
390
- raise 'No content' unless input && !input.empty?
435
+ exit_now! 'No content' unless input && !input.empty?
391
436
 
392
437
  title, note = wwid.format_input(input)
393
438
  title += " @done#{donedate}"
@@ -422,7 +467,7 @@ command %i[done did] do |c|
422
467
  wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date })
423
468
  wwid.write(wwid.doing_file)
424
469
  else
425
- raise 'You must provide content when creating a new entry'
470
+ exit_now! 'You must provide content when creating a new entry'
426
471
  end
427
472
  end
428
473
  end
@@ -468,9 +513,9 @@ command :cancel do |c|
468
513
  end
469
514
  end
470
515
 
471
- raise 'Only one argument allowed' if args.length > 1
516
+ exit_now! 'Only one argument allowed' if args.length > 1
472
517
 
473
- raise 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
518
+ exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
474
519
 
475
520
  count = args[0] ? args[0].to_i : 1
476
521
  opts = {
@@ -535,14 +580,14 @@ command :finish do |c|
535
580
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
536
581
 
537
582
  unless options[:auto]
538
- raise '--back and --took cannot be used together' if options[:back] && options[:took]
583
+ exit_now! '--back and --took cannot be used together' if options[:back] && options[:took]
539
584
 
540
- raise '--search and --tag cannot be used together' if options[:search] && options[:tag]
585
+ exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
541
586
 
542
587
  if options[:back]
543
588
  date = wwid.chronify(options[:back])
544
589
 
545
- raise 'Unable to parse date string' if date.nil?
590
+ exit_now! 'Unable to parse date string' if date.nil?
546
591
  elsif options[:took]
547
592
  date = wwid.chronify_qty(options[:took])
548
593
  else
@@ -566,9 +611,9 @@ command :finish do |c|
566
611
  end
567
612
  end
568
613
 
569
- raise 'Only one argument allowed' if args.length > 1
614
+ exit_now! 'Only one argument allowed' if args.length > 1
570
615
 
571
- raise 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
616
+ exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
572
617
 
573
618
  count = args[0] ? args[0].to_i : 1
574
619
  opts = {
@@ -650,6 +695,9 @@ command :tag do |c|
650
695
  c.arg_name 'COUNT'
651
696
  c.flag %i[c count], default_value: 1
652
697
 
698
+ c.desc 'Don\'t ask permission to tag all entries when count is 0'
699
+ c.switch %i[force], negatable: false, default_value: false
700
+
653
701
  c.desc 'Include current date/time with tag'
654
702
  c.switch %i[d date], negatable: false, default_value: false
655
703
 
@@ -662,12 +710,48 @@ command :tag do |c|
662
710
  c.desc 'Autotag entries based on autotag configuration in ~/.doingrc'
663
711
  c.switch %i[a autotag], negatable: false, default_value: false
664
712
 
713
+ c.desc 'Tag the last X entries containing TAG.
714
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
715
+ c.arg_name 'TAG'
716
+ c.flag [:tag]
717
+
718
+ c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/")'
719
+ c.arg_name 'QUERY'
720
+ c.flag [:search]
721
+
722
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
723
+ c.arg_name 'BOOLEAN'
724
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
725
+
665
726
  c.action do |_global_options, options, args|
666
- raise 'You must specify at least one tag' if args.empty? && !options[:a]
727
+ exit_now! 'You must specify at least one tag' if args.empty? && !options[:a]
728
+
729
+ exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
730
+
731
+ section = 'All'
732
+
733
+ if options[:section]
734
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
735
+ end
667
736
 
668
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
669
737
 
670
- if options[:a]
738
+ if options[:tag].nil?
739
+ search_tags = []
740
+ else
741
+ search_tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
742
+ options[:bool] = case options[:bool]
743
+ when /(and|all)/i
744
+ 'AND'
745
+ when /(any|or)/i
746
+ 'OR'
747
+ when /(not|none)/i
748
+ 'NOT'
749
+ else
750
+ 'AND'
751
+ end
752
+ end
753
+
754
+ if options[:autotag]
671
755
  tags = []
672
756
  else
673
757
  tags = if args.join('') =~ /,/
@@ -679,10 +763,19 @@ command :tag do |c|
679
763
  tags.map! { |tag| tag.sub(/^@/, '').strip }
680
764
  end
681
765
 
682
- count = options[:c].to_i
766
+ count = options[:count].to_i
767
+
768
+ if count.zero? && !options[:force]
769
+ if options[:search]
770
+ section_q = ' matching your search terms'
771
+ elsif options[:tag]
772
+ section_q = ' matching your tag search'
773
+ elsif section == 'All'
774
+ section_q = ''
775
+ else
776
+ section_q = " in section #{section}"
777
+ end
683
778
 
684
- if count.zero?
685
- section_q = section == 'All' ? '' : " in section #{section}"
686
779
 
687
780
  question = if options[:a]
688
781
  "Are you sure you want to autotag all records#{section_q}"
@@ -694,14 +787,18 @@ command :tag do |c|
694
787
 
695
788
  res = wwid.yn(question, default_response: false)
696
789
 
697
- raise 'Cancelled' unless res
790
+ exit_now! 'Cancelled' unless res
698
791
  end
792
+
699
793
  opts = {
700
794
  autotag: options[:a],
701
795
  count: count,
702
796
  date: options[:date],
703
797
  remove: options[:r],
798
+ search: options[:search],
704
799
  section: section,
800
+ tag: search_tags,
801
+ tag_bool: options[:bool],
705
802
  tags: tags,
706
803
  unfinished: options[:unfinished]
707
804
  }
@@ -768,10 +865,10 @@ command :show do |c|
768
865
  c.flag %i[f from]
769
866
 
770
867
  c.desc 'Show time intervals on @done tasks'
771
- c.switch %i[t times], default_value: true
868
+ c.switch %i[t times], default_value: true, negatable: true
772
869
 
773
870
  c.desc 'Show intervals with totals at the end of output'
774
- c.switch [:totals], default_value: false, negatable: true
871
+ c.switch [:totals], default_value: false, negatable: false
775
872
 
776
873
  c.desc 'Sort tags by (name|time)'
777
874
  default = 'time'
@@ -800,7 +897,7 @@ command :show do |c|
800
897
  section = 'All'
801
898
  else
802
899
  section = wwid.guess_section(args[0])
803
- raise "No such section: #{args[0]}" unless section
900
+ exit_now! "No such section: #{args[0]}" unless section
804
901
 
805
902
  args.shift
806
903
  end
@@ -890,10 +987,10 @@ command [:grep, :search] do |c|
890
987
  c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
891
988
 
892
989
  c.desc 'Show time intervals on @done tasks'
893
- c.switch %i[t times], default_value: true
990
+ c.switch %i[t times], default_value: true, negatable: true
894
991
 
895
992
  c.desc 'Show intervals with totals at the end of output'
896
- c.switch [:totals], default_value: false, negatable: true
993
+ c.switch [:totals], default_value: false, negatable: false
897
994
 
898
995
  c.desc 'Sort tags by (name|time)'
899
996
  default = 'time'
@@ -937,10 +1034,10 @@ command :recent do |c|
937
1034
  c.flag %i[s section], default_value: 'All'
938
1035
 
939
1036
  c.desc 'Show time intervals on @done tasks'
940
- c.switch %i[t times], default_value: true
1037
+ c.switch %i[t times], default_value: true, negatable: true
941
1038
 
942
1039
  c.desc 'Show intervals with totals at the end of output'
943
- c.switch [:totals], default_value: false, negatable: true
1040
+ c.switch [:totals], default_value: false, negatable: false
944
1041
 
945
1042
  c.desc 'Sort tags by (name|time)'
946
1043
  default = 'time'
@@ -977,10 +1074,10 @@ command :today do |c|
977
1074
  c.flag %i[s section], default_value: 'All'
978
1075
 
979
1076
  c.desc 'Show time intervals on @done tasks'
980
- c.switch %i[t times], default_value: true
1077
+ c.switch %i[t times], default_value: true, negatable: true
981
1078
 
982
1079
  c.desc 'Show time totals at the end of output'
983
- c.switch [:totals], default_value: false, negatable: true
1080
+ c.switch [:totals], default_value: false, negatable: false
984
1081
 
985
1082
  c.desc 'Sort tags by (name|time)'
986
1083
  default = 'time'
@@ -1012,10 +1109,10 @@ command :on do |c|
1012
1109
  c.flag %i[s section], default_value: 'All'
1013
1110
 
1014
1111
  c.desc 'Show time intervals on @done tasks'
1015
- c.switch %i[t times], default_value: true
1112
+ c.switch %i[t times], default_value: true, negatable: true
1016
1113
 
1017
1114
  c.desc 'Show time totals at the end of output'
1018
- c.switch [:totals], default_value: false, negatable: true
1115
+ c.switch [:totals], default_value: false, negatable: false
1019
1116
 
1020
1117
  c.desc 'Sort tags by (name|time)'
1021
1118
  default = 'time'
@@ -1055,6 +1152,55 @@ command :on do |c|
1055
1152
  end
1056
1153
  end
1057
1154
 
1155
+ desc 'List entries since a date'
1156
+ long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday,"
1157
+ and "2d" would be interpreted as "two days ago.")
1158
+ arg_name 'DATE_STRING'
1159
+ command :since do |c|
1160
+ c.desc 'Section'
1161
+ c.arg_name 'NAME'
1162
+ c.flag %i[s section], default_value: 'All'
1163
+
1164
+ c.desc 'Show time intervals on @done tasks'
1165
+ c.switch %i[t times], default_value: true, negatable: true
1166
+
1167
+ c.desc 'Show time totals at the end of output'
1168
+ c.switch [:totals], default_value: false, negatable: false
1169
+
1170
+ c.desc 'Sort tags by (name|time)'
1171
+ default = 'time'
1172
+ default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1173
+ c.arg_name 'KEY'
1174
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1175
+
1176
+ c.desc 'Output to export format (csv|html|json|template|timeline)'
1177
+ c.arg_name 'FORMAT'
1178
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1179
+
1180
+ c.action do |_global_options, options, args|
1181
+ exit_now! 'Missing date argument' if args.empty?
1182
+
1183
+ date_string = args.join(' ')
1184
+
1185
+ date_string += ' at midnight' unless date_string =~ /(\d:|\d *[ap]m?|midnight|noon)/i
1186
+ date_string.sub!(/(day) (\d)/, '\1 at \2') if date_string =~ /day \d/
1187
+
1188
+ start = wwid.chronify(date_string)
1189
+ finish = Time.now
1190
+
1191
+ exit_now! 'Unrecognized date string' unless start
1192
+
1193
+ message = "Date interpreted as #{start} through the current time"
1194
+ wwid.results.push(message)
1195
+
1196
+ options[:t] = true if options[:totals]
1197
+ options[:sort_tags] = options[:tag_sort] =~ /^n/i
1198
+
1199
+ puts wwid.list_date([start, finish], options[:s], options[:t], options[:output],
1200
+ { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1201
+ end
1202
+ end
1203
+
1058
1204
  desc 'List entries from yesterday'
1059
1205
  command :yesterday do |c|
1060
1206
  c.desc 'Specify a section'
@@ -1066,10 +1212,10 @@ command :yesterday do |c|
1066
1212
  c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1067
1213
 
1068
1214
  c.desc 'Show time intervals on @done tasks'
1069
- c.switch %i[t times], default_value: true
1215
+ c.switch %i[t times], default_value: true, negatable: true
1070
1216
 
1071
1217
  c.desc 'Show time totals at the end of output'
1072
- c.switch [:totals], default_value: false, negatable: true
1218
+ c.switch [:totals], default_value: false, negatable: false
1073
1219
 
1074
1220
  c.desc 'Sort tags by (name|time)'
1075
1221
  default = 'time'
@@ -1106,7 +1252,7 @@ command :last do |c|
1106
1252
  c.flag [:search]
1107
1253
 
1108
1254
  c.action do |_global_options, options, _args|
1109
- raise '--tag and --search cannot be used together' if options[:tag] && options[:search]
1255
+ exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search]
1110
1256
 
1111
1257
  if options[:tag].nil?
1112
1258
  tags = []
@@ -1155,7 +1301,7 @@ desc 'Add a new section to the "doing" file'
1155
1301
  arg_name 'SECTION_NAME'
1156
1302
  command :add_section do |c|
1157
1303
  c.action do |_global_options, _options, args|
1158
- raise "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1304
+ exit_now! "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1159
1305
 
1160
1306
  wwid.add_section(args[0].cap_first)
1161
1307
  wwid.write(wwid.doing_file)
@@ -1196,10 +1342,10 @@ command :view do |c|
1196
1342
  c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1197
1343
 
1198
1344
  c.desc 'Show time intervals on @done tasks'
1199
- c.switch %i[t times], default_value: true
1345
+ c.switch %i[t times], default_value: true, negatable: true
1200
1346
 
1201
1347
  c.desc 'Show intervals with totals at the end of output'
1202
- c.switch [:totals], default_value: false, negatable: true
1348
+ c.switch [:totals], default_value: false, negatable: false
1203
1349
 
1204
1350
  c.desc 'Include colors in output'
1205
1351
  c.switch [:color], default_value: true, negatable: true
@@ -1211,7 +1357,7 @@ command :view do |c|
1211
1357
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1212
1358
 
1213
1359
  c.desc 'Only show items with recorded time intervals'
1214
- c.switch [:only_timed], default_value: false, negatable: true
1360
+ c.switch [:only_timed], default_value: false, negatable: false
1215
1361
 
1216
1362
  c.action do |_global_options, options, args|
1217
1363
  title = if args.empty?
@@ -1283,7 +1429,7 @@ command :view do |c|
1283
1429
  elsif title.instance_of?(FalseClass)
1284
1430
  exit_now! 'Cancelled'
1285
1431
  else
1286
- raise "View #{title} not found in config"
1432
+ exit_now! "View #{title} not found in config"
1287
1433
  end
1288
1434
  end
1289
1435
  end
@@ -1340,7 +1486,7 @@ command :archive do |c|
1340
1486
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
1341
1487
  end
1342
1488
 
1343
- raise '--keep and --count can\'t be used together' if options[:keep] && options[:count]
1489
+ exit_now! '--keep and --count can\'t be used together' if options[:keep] && options[:count]
1344
1490
 
1345
1491
  tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
1346
1492
 
@@ -1391,7 +1537,7 @@ command :open do |c|
1391
1537
  elsif options[:b]
1392
1538
  system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
1393
1539
  elsif options[:e]
1394
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1540
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1395
1541
 
1396
1542
  system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
1397
1543
  elsif wwid.config.key?('editor_app') && !wwid.config['editor_app'].nil?
@@ -1401,7 +1547,7 @@ command :open do |c|
1401
1547
  end
1402
1548
 
1403
1549
  else
1404
- raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1550
+ exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1405
1551
 
1406
1552
  system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
1407
1553
  end
@@ -1438,13 +1584,13 @@ command :config do |c|
1438
1584
  `open -b #{options[:b]} "#{wwid.config_file}"`
1439
1585
  end
1440
1586
  else
1441
- raise 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1587
+ exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1442
1588
 
1443
1589
  editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1444
1590
  system %(#{editor} "#{wwid.config_file}")
1445
1591
  end
1446
1592
  else
1447
- raise 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1593
+ exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil?
1448
1594
 
1449
1595
  editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
1450
1596
  system %(#{editor} "#{wwid.config_file}")
@@ -1497,7 +1643,7 @@ command :import do |c|
1497
1643
  wwid.write(wwid.doing_file)
1498
1644
  end
1499
1645
  else
1500
- raise 'Invalid import type'
1646
+ exit_now! 'Invalid import type'
1501
1647
  end
1502
1648
  end
1503
1649
  end
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.70'
2
+ VERSION = '1.0.75'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'deep_merge'
4
4
  require 'open3'
5
5
  require 'pp'
6
+ require 'shellwords'
6
7
 
7
8
  ##
8
9
  ## @brief Main "What Was I Doing" methods
@@ -72,7 +73,7 @@ class WWID
72
73
  rescue StandardError
73
74
  @config = {}
74
75
  @local_config = {}
75
- # raise "error reading config"
76
+ # exit_now! "error reading config"
76
77
  end
77
78
  end
78
79
 
@@ -157,7 +158,7 @@ class WWID
157
158
 
158
159
  # if ENV['DOING_DEBUG'].to_i == 3
159
160
  # if @config['default_tags'].length > 0
160
- # raise "DEFAULT CONFIG CHANGED"
161
+ # exit_now! "DEFAULT CONFIG CHANGED"
161
162
  # end
162
163
  # end
163
164
 
@@ -292,7 +293,7 @@ class WWID
292
293
  if $?.exitstatus == 0
293
294
  input = IO.read(tmpfile.path)
294
295
  else
295
- raise 'Cancelled'
296
+ exit_now! 'Cancelled'
296
297
  end
297
298
  ensure
298
299
  tmpfile.close
@@ -310,16 +311,16 @@ class WWID
310
311
  # @param input (String) The string to parse
311
312
  #
312
313
  def format_input(input)
313
- raise 'No content in entry' if input.nil? || input.strip.empty?
314
+ exit_now! 'No content in entry' if input.nil? || input.strip.empty?
314
315
 
315
316
  input_lines = input.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
316
317
  title = input_lines[0]&.strip
317
- 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?
318
319
 
319
320
  note = input_lines.length > 1 ? input_lines[1..-1] : []
320
321
  # If title line ends in a parenthetical, use that as the note
321
- if note.empty? && title =~ /\(.*?\)$/
322
- title.sub!(/\((.*?)\)$/) do
322
+ if note.empty? && title =~ /\s+\(.*?\)$/
323
+ title.sub!(/\s+\((.*?)\)$/) do
323
324
  m = Regexp.last_match
324
325
  note.push(m[1])
325
326
  ''
@@ -345,7 +346,7 @@ class WWID
345
346
  #
346
347
  def chronify(input)
347
348
  now = Time.now
348
- raise "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
349
+ exit_now! "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
349
350
 
350
351
  secs_ago = if input.match(/^(\d+)$/)
351
352
  # plain number, assume minutes
@@ -437,7 +438,7 @@ class WWID
437
438
  end
438
439
  unless section || guessed
439
440
  alt = guess_view(frag, true)
440
- raise "Did you mean `doing view #{alt}`?" if alt
441
+ exit_now! "Did you mean `doing view #{alt}`?" if alt
441
442
 
442
443
  res = yn("Section #{frag} not found, create it", default_response: false)
443
444
 
@@ -447,7 +448,7 @@ class WWID
447
448
  return frag.cap_first
448
449
  end
449
450
 
450
- raise "Unknown section: #{frag}"
451
+ exit_now! "Unknown section: #{frag}"
451
452
  end
452
453
  section ? section.cap_first : guessed
453
454
  end
@@ -517,9 +518,9 @@ class WWID
517
518
  unless view || guessed
518
519
  alt = guess_section(frag, guessed: true)
519
520
  if alt
520
- raise "Did you mean `doing show #{alt}`?"
521
+ exit_now! "Did you mean `doing show #{alt}`?"
521
522
  else
522
- raise "Unknown view: #{frag}"
523
+ exit_now! "Unknown view: #{frag}"
523
524
  end
524
525
  end
525
526
  view
@@ -630,7 +631,7 @@ class WWID
630
631
 
631
632
  add_tags = opt[:tag] ? opt[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '@') }.join(' ') : ''
632
633
  prefix = opt[:prefix] ? opt[:prefix] : '[Timing.app]'
633
- raise "File not found" unless File.exist?(File.expand_path(path))
634
+ exit_now! "File not found" unless File.exist?(File.expand_path(path))
634
635
 
635
636
  data = JSON.parse(IO.read(File.expand_path(path)))
636
637
  new_items = []
@@ -687,7 +688,7 @@ class WWID
687
688
  section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
688
689
  end
689
690
 
690
- raise "Section #{section} not found" unless @content.key?(section)
691
+ exit_now! "Section #{section} not found" unless @content.key?(section)
691
692
 
692
693
  last_item = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse[0]
693
694
  warn "Editing note for #{last_item['title']}"
@@ -765,13 +766,138 @@ class WWID
765
766
  all_items.max_by { |item| item['date'] }
766
767
  end
767
768
 
769
+
770
+ ##
771
+ ## @brief Display an interactive menu of entries
772
+ ##
773
+ ## @param opt (Hash) Additional options
774
+ ##
775
+ def interactive(opt = {})
776
+ exit_now! "Select command requires that fzf be installed" unless exec_available('fzf')
777
+
778
+ section = opt[:section] ? guess_section(opt[:section]) : 'All'
779
+
780
+
781
+ if section =~ /^all$/i
782
+ combined = { 'items' => [] }
783
+ @content.each do |_k, v|
784
+ combined['items'] += v['items']
785
+ end
786
+ items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
787
+ else
788
+ items = @content[section]['items']
789
+ end
790
+
791
+
792
+ options = items.map.with_index do |item, i|
793
+ out = [
794
+ i,
795
+ ') ',
796
+ item['date'],
797
+ ' | ',
798
+ item['title'],
799
+ ]
800
+ if opt[:section] =~ /^all/i
801
+ out.concat([
802
+ ' (',
803
+ item['section'],
804
+ ') '
805
+ ])
806
+ end
807
+ out.join('')
808
+ end
809
+
810
+ res = `echo #{Shellwords.escape(options.join("\n"))}|fzf -m --bind ctrl-a:select-all`
811
+ selected = []
812
+ res.split(/\n/).each do |item|
813
+ idx = item.match(/^(\d+)\)/)[1].to_i
814
+ selected.push(items[idx])
815
+ end
816
+
817
+ if selected.empty?
818
+ @results.push("No selection")
819
+ return
820
+ end
821
+
822
+ if opt[:delete]
823
+ res = yn("Delete #{selected.size} items?", default_response: 'y')
824
+ if res
825
+ selected.each {|item| delete_item(item) }
826
+ write(@doing_file)
827
+ end
828
+ return
829
+ end
830
+
831
+ if opt[:flag]
832
+ tag = @config['marker_tag'] || 'flagged'
833
+ selected.map! {|item| tag_item(item, tag, date: false) }
834
+ end
835
+
836
+ if opt[:finish] || opt[:cancel]
837
+ tag = 'done'
838
+ selected.map! {|item| tag_item(item, tag, date: !opt[:cancel])}
839
+ end
840
+
841
+ if opt[:tag]
842
+ tag = opt[:tag]
843
+ selected.map! {|item| tag_item(item, tag, date: false)}
844
+ end
845
+
846
+ if opt[:archive] || opt[:move]
847
+ section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
848
+ selected.map! {|item| move_item(item, section) }
849
+ end
850
+
851
+ write(@doing_file)
852
+
853
+ if opt[:editor]
854
+
855
+ editable_items = []
856
+
857
+ selected.each do |item|
858
+ editable = "#{item['date']} | #{item['title']}"
859
+ old_note = item['note'] ? item['note'].map(&:strip).join("\n") : nil
860
+ editable += "\n#{old_note}" unless old_note.nil?
861
+ editable_items << editable
862
+ end
863
+
864
+ new_items = fork_editor(editable_items.map(&:strip).join("\n---\n") + "\n\n# You may delete entries, but leave all divider lines in place").split(/\n---\n/)
865
+
866
+ new_items.each_with_index do |new_item, i|
867
+
868
+ input_lines = new_item.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
869
+ title = input_lines[0]&.strip
870
+
871
+ if title.nil? || title =~ /^---$/ || title.strip.empty?
872
+ delete_item(selected[i])
873
+ else
874
+ note = input_lines.length > 1 ? input_lines[1..-1] : []
875
+
876
+ note.map!(&:strip)
877
+ note.delete_if { |line| line =~ /^\s*$/ || line =~ /^#/ }
878
+
879
+ date = title.match(/^([\d\-: ]+) \| /)[1]
880
+ title.sub!(/^([\d\-: ]+) \| /, '')
881
+
882
+ item = selected[i].dup
883
+ item['title'] = title
884
+ item['note'] = note
885
+ item['date'] = Time.parse(date) || selected[i]['date']
886
+ update_item(selected[i], item)
887
+ end
888
+ end
889
+
890
+ write(@doing_file)
891
+ end
892
+ end
893
+
768
894
  ##
769
895
  ## @brief Tag the last entry or X entries
770
896
  ##
771
897
  ## @param opt (Hash) Additional Options
772
898
  ##
773
899
  def tag_last(opt = {})
774
- opt[:section] ||= @current_section
900
+ opt[:section] ||= nil
775
901
  opt[:count] ||= 1
776
902
  opt[:archive] ||= false
777
903
  opt[:tags] ||= ['done']
@@ -786,7 +912,11 @@ class WWID
786
912
  sec_arr = []
787
913
 
788
914
  if opt[:section].nil?
789
- sec_arr = [@current_section]
915
+ if opt[:search] || opt[:tag]
916
+ sec_arr = sections
917
+ else
918
+ sec_arr = [@current_section]
919
+ end
790
920
  elsif opt[:section].instance_of?(String)
791
921
  if opt[:section] =~ /^all$/i
792
922
  if opt[:count] == 1
@@ -797,7 +927,11 @@ class WWID
797
927
  items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
798
928
  sec_arr.push(items[0]['section'])
799
929
  elsif opt[:count] > 1
800
- raise 'A count greater than one requires a section to be specified'
930
+ if opt[:search] || opt[:tag]
931
+ sec_arr = sections
932
+ else
933
+ exit_now! 'A count greater than one requires a section to be specified'
934
+ end
801
935
  else
802
936
  sec_arr = sections
803
937
  end
@@ -897,13 +1031,78 @@ class WWID
897
1031
  @results.push('Archiving is skipped when operating on all entries') if (opt[:count]).zero?
898
1032
  end
899
1033
  else
900
- raise "Section not found: #{section}"
1034
+ exit_now! "Section not found: #{section}"
901
1035
  end
902
1036
  end
903
1037
 
904
1038
  write(@doing_file)
905
1039
  end
906
1040
 
1041
+ def move_item(item, section)
1042
+ old_section = item['section']
1043
+ new_item = item.dup
1044
+ new_item['section'] = section
1045
+
1046
+ section_items = @content[old_section]['items']
1047
+ section_items.delete(item)
1048
+ @content[old_section]['items'] = section_items
1049
+
1050
+ archive_items = @content[section]['items']
1051
+ archive_items.push(new_item)
1052
+ # archive_items = archive_items.sort_by { |item| item['date'] }
1053
+ @content[section]['items'] = archive_items
1054
+
1055
+ @results.push("Entry moved to #{section}: #{new_item['title']}")
1056
+ return new_item
1057
+ end
1058
+
1059
+ ##
1060
+ ## @brief Delete an item from the index
1061
+ ##
1062
+ ## @param old_item
1063
+ ##
1064
+ def delete_item(old_item)
1065
+ section = old_item['section']
1066
+
1067
+ section_items = @content[section]['items']
1068
+ deleted = section_items.delete(old_item)
1069
+ @results.push("Entry deleted: #{deleted['title']}")
1070
+ @content[section]['items'] = section_items
1071
+ end
1072
+
1073
+ ##
1074
+ ## @brief Tag an item from the index
1075
+ ##
1076
+ ## @param old_item (Item) The item to tag
1077
+ ## @param tag (string) The tag to apply
1078
+ ## @param date (Boolean) Include timestamp?
1079
+ ##
1080
+ def tag_item(old_item, tag, date: false)
1081
+ title = old_item['title'].dup
1082
+ done_date = Time.now
1083
+ if title !~ /@#{tag}/
1084
+ title.chomp!
1085
+ if date
1086
+ title += " @#{tag}(#{done_date.strftime('%F %R')})"
1087
+ else
1088
+ title += " @#{tag}"
1089
+ end
1090
+ new_item = old_item.dup
1091
+ new_item['title'] = title
1092
+ update_item(old_item, new_item)
1093
+ return new_item
1094
+ else
1095
+ @results.push(%(Item already @#{tag}: "#{title}" in #{old_item['section']}))
1096
+ return old_item
1097
+ end
1098
+ end
1099
+
1100
+ ##
1101
+ ## @brief Update an item in the index with a modified item
1102
+ ##
1103
+ ## @param old_item The old item
1104
+ ## @param new_item The new item
1105
+ ##
907
1106
  def update_item(old_item, new_item)
908
1107
  section = old_item['section']
909
1108
 
@@ -1002,7 +1201,7 @@ class WWID
1002
1201
  section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
1003
1202
  end
1004
1203
 
1005
- raise "Section #{section} not found" unless @content.key?(section)
1204
+ exit_now! "Section #{section} not found" unless @content.key?(section)
1006
1205
 
1007
1206
  # sort_section(opt[:section])
1008
1207
  items = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse
@@ -1238,7 +1437,7 @@ class WWID
1238
1437
  end
1239
1438
  end
1240
1439
 
1241
- raise 'Invalid section object' unless opt[:section].instance_of? Hash
1440
+ exit_now! 'Invalid section object' unless opt[:section].instance_of? Hash
1242
1441
 
1243
1442
  items = opt[:section]['items'].sort_by { |item| item['date'] }
1244
1443
 
@@ -1288,7 +1487,7 @@ class WWID
1288
1487
 
1289
1488
  out = ''
1290
1489
 
1291
- raise 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1490
+ exit_now! 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1292
1491
 
1293
1492
  case opt[:output]
1294
1493
  when /^csv$/i
@@ -1345,13 +1544,13 @@ class WWID
1345
1544
  'id' => index + 1,
1346
1545
  'content' => title.strip, #+ " #{note}"
1347
1546
  'title' => title.strip + " (#{'%02d:%02d:%02d' % fmt_time(interval)})",
1348
- 'start' => i['date'].strftime('%F'),
1547
+ 'start' => i['date'].strftime('%F %T'),
1349
1548
  'type' => 'point'
1350
1549
  }
1351
1550
 
1352
- if interval && interval > 0
1353
- new_item['end'] = end_date.strftime('%F')
1354
- new_item['type'] = 'range' if interval > 3600 * 3
1551
+ if interval && interval.to_i > 0
1552
+ new_item['end'] = end_date.strftime('%F %T')
1553
+ new_item['type'] = 'range' if interval.to_i > 3600 * 3
1355
1554
  end
1356
1555
  items_out.push(new_item)
1357
1556
  end
@@ -1367,8 +1566,8 @@ class WWID
1367
1566
  <!doctype html>
1368
1567
  <html>
1369
1568
  <head>
1370
- <link href="http://visjs.org/dist/vis.css" rel="stylesheet" type="text/css" />
1371
- <script src="http://visjs.org/dist/vis.js"></script>
1569
+ <link href="https://unpkg.com/vis-timeline@7.4.9/dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
1570
+ <script src="https://unpkg.com/vis-timeline@7.4.9/dist/vis-timeline-graph2d.min.js"></script>
1372
1571
  </head>
1373
1572
  <body>
1374
1573
  <div id="mytimeline"></div>
@@ -1561,7 +1760,7 @@ class WWID
1561
1760
  do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label] })
1562
1761
  write(doing_file)
1563
1762
  else
1564
- raise 'Either source or destination does not exist'
1763
+ exit_now! 'Either source or destination does not exist'
1565
1764
  end
1566
1765
  end
1567
1766
 
@@ -2005,4 +2204,12 @@ EOS
2005
2204
  minutes = (minutes % 60).to_i
2006
2205
  [days, hours, minutes]
2007
2206
  end
2207
+
2208
+ def exec_available(cli)
2209
+ if File.exists?(File.expand_path(cli))
2210
+ File.executable?(File.expand_path(cli))
2211
+ else
2212
+ system "which #{cli}", :out => File::NULL, :err => File::NULL
2213
+ end
2214
+ end
2008
2215
  end
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.70
4
+ version: 1.0.75
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-15 00:00:00.000000000 Z
11
+ date: 2021-09-16 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