doing 1.0.69 → 1.0.74

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 +26 -4
  3. data/bin/doing +209 -63
  4. data/lib/doing/version.rb +1 -1
  5. data/lib/doing/wwid.rb +239 -34
  6. metadata +8 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf5235acbab516c74ea7ff28c610dcbc49b9d9f108056c1b3c60ac0100bd9ec5
4
- data.tar.gz: ca6abc5bd66a950d78ab16aabc781660b0ea5ee3f6dc50bb8a9fd0b986cce21e
3
+ metadata.gz: 652cc285b42ce068d4640a6aff95d2058d1fba60715e41ce4599bfcbe7051507
4
+ data.tar.gz: 7a940801f8fa9d78b27688b6c92f4fcdb4b5765c28888cb6f1218245d4535376
5
5
  SHA512:
6
- metadata.gz: b9f62baa0a4348212c54c1f9f3aa5bca041d81bc5061221310a85747b05c0b9b7af9036d309fcffd78e192a7a415f462ceeb96f8c276cef320249651f4890d35
7
- data.tar.gz: cc61feaf78e9a3d6865bbb3d5fecdff00659b9b00a7430971012bfca0856e45ee87efd864dde603a7a6e6abf6a571f01f0e8d60afbbb7ebe060364f65c908b6b
6
+ metadata.gz: df8f8c529b18238a5d069b79c9e43382f2751d18f1dd022e7d918467f31dadd6b6a50780034b516d4d9b321ddcc4d3ebb7a40db9b96a4059adf49eda119663f9
7
+ data.tar.gz: b67333df19e976fe0cbde7645c6cc8a9098813a023090f7e52bd60052283202f2dff8406beacb28d921d7c947dac2d885caab7fe3388c578a17b73181a4cbbc5
data/README.md CHANGED
@@ -25,11 +25,9 @@ While I'm working, I have hourly reminders to record what I'm working on, and I
25
25
 
26
26
  If there's something I want to look at later but doesn't need to be added to a task list or tracker, I can type `doing later check out the pinboard bookmarks from macdrifter`. When I get back to my computer --- or just need a refresher after a distraction --- I can type `doing last` to see what the last thing on my plate was. I can also type `doing recent` (or just `doing`) to get a list of the last few entries. `doing today` gives me everything since midnight for the current day, making it easy to see what I've accomplished over a sleepless night.
27
27
 
28
- _Side note:_ I actually use the library behind this utility as part of another script that mirrors entries in [Day One](http://dayoneapp.com) that have the tag `wwid`. I can use the hourly writing reminders and enter my stuff in the quick entry popup. Someday I'll get around to cleaning that up and putting it out there.
29
-
30
28
  ## Installation
31
29
 
32
- The current version of `doing` is <!--VER-->1.0.68<!--END VER-->.
30
+ The current version of `doing` is <!--VER-->1.0.73<!--END VER-->.
33
31
 
34
32
  $ [sudo] gem install doing
35
33
 
@@ -538,7 +536,7 @@ If you have a use for it, you can use `-o csv` on the show or view commands to o
538
536
 
539
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`).
540
538
 
541
- `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`.
542
540
 
543
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.
544
542
 
@@ -619,6 +617,30 @@ Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is t
619
617
  # Import to default section (Currently) and prefix entries with '[Imported]'
620
618
  doing import --prefix="[Imported]" "~/Desktop/All Activities.json"
621
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.
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.
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
+
622
644
  ---
623
645
 
624
646
  ## 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,23 +397,23 @@ 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]
371
- finish_date = options[:back] ? date + took : nil
416
+ finish_date = date + took
372
417
  elsif options[:back]
373
418
  finish_date = date
374
419
  else
@@ -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.69'
2
+ VERSION = '1.0.74'
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,136 @@ 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`
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.join("\n---\n") + "\n\n# You may delete entries, but leave all --- lines in place").split(/\n---\n/)
865
+
866
+ new_items.each_with_index do |new_item, i|
867
+ input_lines = new_item.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
868
+ title = input_lines[0]&.strip
869
+ if title.nil? || title =~ /^---$/ || title.strip.empty?
870
+ delete_item(selected[i])
871
+ else
872
+ note = input_lines.length > 1 ? input_lines[1..-1] : []
873
+
874
+ note.map!(&:strip)
875
+ note.delete_if { |line| line =~ /^\s*$/ || line =~ /^#/ }
876
+
877
+ date = title.match(/^([\d\-: ]+) \| /)[1]
878
+ title.sub!(/^([\d\-: ]+) \| /, '')
879
+
880
+ item = selected[i].dup
881
+ item['title'] = title
882
+ item['note'] = note
883
+ item['date'] = Time.parse(date)
884
+ update_item(selected[i], item)
885
+ end
886
+ end
887
+
888
+ write(@doing_file)
889
+ end
890
+ end
891
+
768
892
  ##
769
893
  ## @brief Tag the last entry or X entries
770
894
  ##
771
895
  ## @param opt (Hash) Additional Options
772
896
  ##
773
897
  def tag_last(opt = {})
774
- opt[:section] ||= @current_section
898
+ opt[:section] ||= nil
775
899
  opt[:count] ||= 1
776
900
  opt[:archive] ||= false
777
901
  opt[:tags] ||= ['done']
@@ -786,7 +910,11 @@ class WWID
786
910
  sec_arr = []
787
911
 
788
912
  if opt[:section].nil?
789
- sec_arr = [@current_section]
913
+ if opt[:search] || opt[:tag]
914
+ sec_arr = sections
915
+ else
916
+ sec_arr = [@current_section]
917
+ end
790
918
  elsif opt[:section].instance_of?(String)
791
919
  if opt[:section] =~ /^all$/i
792
920
  if opt[:count] == 1
@@ -797,7 +925,11 @@ class WWID
797
925
  items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
798
926
  sec_arr.push(items[0]['section'])
799
927
  elsif opt[:count] > 1
800
- raise 'A count greater than one requires a section to be specified'
928
+ if opt[:search] || opt[:tag]
929
+ sec_arr = sections
930
+ else
931
+ exit_now! 'A count greater than one requires a section to be specified'
932
+ end
801
933
  else
802
934
  sec_arr = sections
803
935
  end
@@ -834,12 +966,6 @@ class WWID
834
966
  if opt[:sequential]
835
967
  done_date = next_start - 1
836
968
  next_start = item['date']
837
- elsif opt[:back]
838
- if opt[:back].is_a? Integer
839
- done_date = item['date'] + opt[:back]
840
- else
841
- done_date = item['date'] + (opt[:back] - item['date'])
842
- end
843
969
  elsif opt[:took]
844
970
  if item['date'] + opt[:took] > Time.now
845
971
  item['date'] = Time.now - opt[:took]
@@ -847,6 +973,12 @@ class WWID
847
973
  else
848
974
  done_date = item['date'] + opt[:took]
849
975
  end
976
+ elsif opt[:back]
977
+ if opt[:back].is_a? Integer
978
+ done_date = item['date'] + opt[:back]
979
+ else
980
+ done_date = item['date'] + (opt[:back] - item['date'])
981
+ end
850
982
  else
851
983
  done_date = Time.now
852
984
  end
@@ -897,13 +1029,78 @@ class WWID
897
1029
  @results.push('Archiving is skipped when operating on all entries') if (opt[:count]).zero?
898
1030
  end
899
1031
  else
900
- raise "Section not found: #{section}"
1032
+ exit_now! "Section not found: #{section}"
901
1033
  end
902
1034
  end
903
1035
 
904
1036
  write(@doing_file)
905
1037
  end
906
1038
 
1039
+ def move_item(item, section)
1040
+ old_section = item['section']
1041
+ new_item = item.dup
1042
+ new_item['section'] = section
1043
+
1044
+ section_items = @content[old_section]['items']
1045
+ section_items.delete(item)
1046
+ @content[old_section]['items'] = section_items
1047
+
1048
+ archive_items = @content[section]['items']
1049
+ archive_items.push(new_item)
1050
+ # archive_items = archive_items.sort_by { |item| item['date'] }
1051
+ @content[section]['items'] = archive_items
1052
+
1053
+ @results.push("Entry moved to #{section}: #{new_item['title']}")
1054
+ return new_item
1055
+ end
1056
+
1057
+ ##
1058
+ ## @brief Delete an item from the index
1059
+ ##
1060
+ ## @param old_item
1061
+ ##
1062
+ def delete_item(old_item)
1063
+ section = old_item['section']
1064
+
1065
+ section_items = @content[section]['items']
1066
+ deleted = section_items.delete(old_item)
1067
+ @results.push("Entry deleted: #{deleted['title']}")
1068
+ @content[section]['items'] = section_items
1069
+ end
1070
+
1071
+ ##
1072
+ ## @brief Tag an item from the index
1073
+ ##
1074
+ ## @param old_item (Item) The item to tag
1075
+ ## @param tag (string) The tag to apply
1076
+ ## @param date (Boolean) Include timestamp?
1077
+ ##
1078
+ def tag_item(old_item, tag, date: false)
1079
+ title = old_item['title'].dup
1080
+ done_date = Time.now
1081
+ if title !~ /@#{tag}/
1082
+ title.chomp!
1083
+ if date
1084
+ title += " @#{tag}(#{done_date.strftime('%F %R')})"
1085
+ else
1086
+ title += " @#{tag}"
1087
+ end
1088
+ new_item = old_item.dup
1089
+ new_item['title'] = title
1090
+ update_item(old_item, new_item)
1091
+ return new_item
1092
+ else
1093
+ @results.push(%(Item already @#{tag}: "#{title}" in #{old_item['section']}))
1094
+ return old_item
1095
+ end
1096
+ end
1097
+
1098
+ ##
1099
+ ## @brief Update an item in the index with a modified item
1100
+ ##
1101
+ ## @param old_item The old item
1102
+ ## @param new_item The new item
1103
+ ##
907
1104
  def update_item(old_item, new_item)
908
1105
  section = old_item['section']
909
1106
 
@@ -1002,7 +1199,7 @@ class WWID
1002
1199
  section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
1003
1200
  end
1004
1201
 
1005
- raise "Section #{section} not found" unless @content.key?(section)
1202
+ exit_now! "Section #{section} not found" unless @content.key?(section)
1006
1203
 
1007
1204
  # sort_section(opt[:section])
1008
1205
  items = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse
@@ -1238,7 +1435,7 @@ class WWID
1238
1435
  end
1239
1436
  end
1240
1437
 
1241
- raise 'Invalid section object' unless opt[:section].instance_of? Hash
1438
+ exit_now! 'Invalid section object' unless opt[:section].instance_of? Hash
1242
1439
 
1243
1440
  items = opt[:section]['items'].sort_by { |item| item['date'] }
1244
1441
 
@@ -1288,7 +1485,7 @@ class WWID
1288
1485
 
1289
1486
  out = ''
1290
1487
 
1291
- raise 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1488
+ exit_now! 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1292
1489
 
1293
1490
  case opt[:output]
1294
1491
  when /^csv$/i
@@ -1345,13 +1542,13 @@ class WWID
1345
1542
  'id' => index + 1,
1346
1543
  'content' => title.strip, #+ " #{note}"
1347
1544
  'title' => title.strip + " (#{'%02d:%02d:%02d' % fmt_time(interval)})",
1348
- 'start' => i['date'].strftime('%F'),
1545
+ 'start' => i['date'].strftime('%F %T'),
1349
1546
  'type' => 'point'
1350
1547
  }
1351
1548
 
1352
- if interval && interval > 0
1353
- new_item['end'] = end_date.strftime('%F')
1354
- new_item['type'] = 'range' if interval > 3600 * 3
1549
+ if interval && interval.to_i > 0
1550
+ new_item['end'] = end_date.strftime('%F %T')
1551
+ new_item['type'] = 'range' if interval.to_i > 3600 * 3
1355
1552
  end
1356
1553
  items_out.push(new_item)
1357
1554
  end
@@ -1367,8 +1564,8 @@ class WWID
1367
1564
  <!doctype html>
1368
1565
  <html>
1369
1566
  <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>
1567
+ <link href="https://unpkg.com/vis-timeline@7.4.9/dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
1568
+ <script src="https://unpkg.com/vis-timeline@7.4.9/dist/vis-timeline-graph2d.min.js"></script>
1372
1569
  </head>
1373
1570
  <body>
1374
1571
  <div id="mytimeline"></div>
@@ -1561,7 +1758,7 @@ class WWID
1561
1758
  do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label] })
1562
1759
  write(doing_file)
1563
1760
  else
1564
- raise 'Either source or destination does not exist'
1761
+ exit_now! 'Either source or destination does not exist'
1565
1762
  end
1566
1763
  end
1567
1764
 
@@ -2005,4 +2202,12 @@ EOS
2005
2202
  minutes = (minutes % 60).to_i
2006
2203
  [days, hours, minutes]
2007
2204
  end
2205
+
2206
+ def exec_available(cli)
2207
+ if File.exists?(File.expand_path(cli))
2208
+ File.executable?(File.expand_path(cli))
2209
+ else
2210
+ system "which #{cli}", :out => File::NULL, :err => File::NULL
2211
+ end
2212
+ end
2008
2213
  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.69
4
+ version: 1.0.74
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-08-01 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