doing 1.0.80 → 1.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83c61397f2a1827dbe04f16255c1739c8d6a194b636afa4b1181437ffaca2ee8
4
- data.tar.gz: b5da9fcae50fb7c90ba70a4b3d28948f551945c92170559f8bb693cda921894b
3
+ metadata.gz: c11e6c75768b8dc2db3a36c5cdd572ad03eb884e118848ebbe413df2988a53df
4
+ data.tar.gz: a3f45b396e4c6b06bab65d24007f0437e72687c6811f559ece8f6c5ca2e7bbba
5
5
  SHA512:
6
- metadata.gz: 181eb1fa0f18e0f4c7678e48b2ae8daf07a85e88842accdf3de7c522dbc022c175067e0226d9faa21e493a654175c80595df1853d97027d6708884cbe24f82e6
7
- data.tar.gz: a472b2013051b3525af41577ea0020b5f43e720e2b5d8598eb07b229555310dadd7a1b2cc0a106763def82f2b1920a9acf41f9ac07d077f35543b0b90ed2a631
6
+ metadata.gz: 7be6844f9f6caa072014afeded310cbaddf2a15380fde5498aea0f980dd5e8448ca44f5c5d9692a2075818f632bbbd19cce93e0db338c621a487abed8259f555
7
+ data.tar.gz: d0af857fee4113e2055af031059901b56e2ff94e849f9d3a0d167d8edde0519e4d6a6ebdeef55050ee313ed628ba6fa2fbd17e73cfc66143e57e7a2c42f93c8f
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.79<!--END VER-->.
30
+ The current version of `doing` is <!--VER-->1.0.83<!--END VER-->.
31
31
 
32
32
  $ [sudo] gem install doing
33
33
 
@@ -245,7 +245,7 @@ You can create your own "views" in the `~/.doingrc` file and view them with `doi
245
245
 
246
246
  views:
247
247
  old:
248
- section: Old
248
+ section: Archive
249
249
  count: 5
250
250
  wrap_width: 0
251
251
  date_format: '%F %_I:%M%P'
@@ -253,6 +253,10 @@ You can create your own "views" in the `~/.doingrc` file and view them with `doi
253
253
  order: asc
254
254
  tags: done finished cancelled
255
255
  tags_bool: ANY
256
+ only_timed: false
257
+ tag_sort: time
258
+ tag_order: asc
259
+ totals: true
256
260
 
257
261
  You can add additional custom views. Just nest them under the `views` key (indented two spaces from the edge). Multiple views would look like this:
258
262
 
@@ -276,7 +280,9 @@ You can add new sections with `doing add_section section_name`. You can also cre
276
280
 
277
281
  The `tags` and `tags_bool` keys allow you to specify tags that the view is filtered by. You can list multiple tags separated by spaces, and then use `tags_bool` to specify `ALL`, `ANY`, or `NONE` to determine how it handles the multiple tags.
278
282
 
279
- The `order` key defines the sort order of the output. This is applied _after_ the tasks are retrieved and cut off at the maximum number specified in `count`.
283
+ The `order` key defines the sort order of the output (asc or desc). This is applied _after_ the tasks are retrieved and cut off at the maximum number specified in `count`.
284
+
285
+ You can include tag timers and totals in the output with `totals: true`. Control tag output using `tag_sort` (name or title) and `tag_order` (asc or desc). You can also output only timed entries using `only_timed: true`. All of these options can be overridden using flags on the `doing view` command.
280
286
 
281
287
  Regarding colors, you can use them to create very nice displays if you're outputting to a color terminal. Example:
282
288
 
@@ -291,7 +297,7 @@ Outputs:
291
297
 
292
298
  ![](http://ckyp.us/XKpj+)
293
299
 
294
- You can also specify a default output format for a view. Most of the optional output formats override the template specification (`html`, `csv`, `json`). If the `view` command is used with the `-o` flag, it will override what's specified in the file.
300
+ You can also specify a default output format for a view. Most of the optional output formats override the template specification (`html`, `csv`, `json`). If the `view` command is used with the `-o` flag, it will override what's specified for the view in the config.
295
301
 
296
302
  ### Colors
297
303
 
data/bin/doing CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env ruby
1
+ #!/usr/bin/env ruby -W1
2
2
  # frozen_string_literal: true
3
3
 
4
4
  $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
@@ -55,7 +55,7 @@ arg_name 'ENTRY'
55
55
  command %i[now next] do |c|
56
56
  c.desc 'Section'
57
57
  c.arg_name 'NAME'
58
- c.flag %i[s section], default_value: wwid.current_section
58
+ c.flag %i[s section]
59
59
 
60
60
  c.desc "Edit entry with #{ENV['EDITOR']}"
61
61
  c.switch %i[e editor], negatable: false, default_value: false
@@ -84,7 +84,11 @@ command %i[now next] do |c|
84
84
  date = Time.now
85
85
  end
86
86
 
87
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
87
+ if options[:section]
88
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
89
+ else
90
+ options[:section] = wwid.config['current_section']
91
+ end
88
92
 
89
93
  if options[:e] || (args.empty? && $stdin.stat.size.zero?)
90
94
  exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
@@ -135,7 +139,9 @@ command :note do |c|
135
139
  c.switch %i[r remove], negatable: false, default_value: false
136
140
 
137
141
  c.action do |_global_options, options, args|
138
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
142
+ if options[:section]
143
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
144
+ end
139
145
 
140
146
  if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
141
147
  exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
@@ -176,7 +182,7 @@ arg_name 'ENTRY'
176
182
  command :meanwhile do |c|
177
183
  c.desc 'Section'
178
184
  c.arg_name 'NAME'
179
- c.flag %i[s section], default_value: wwid.current_section
185
+ c.flag %i[s section]
180
186
 
181
187
  c.desc "Edit entry with #{ENV['EDITOR']}"
182
188
  c.switch %i[e editor], negatable: false, default_value: false
@@ -201,7 +207,11 @@ command :meanwhile do |c|
201
207
  date = Time.now
202
208
  end
203
209
 
204
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
210
+ if options[:section]
211
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
212
+ else
213
+ section = wwid.config['current_section']
214
+ end
205
215
  input = ''
206
216
 
207
217
  if options[:e]
@@ -301,11 +311,14 @@ command :select do |c|
301
311
  c.desc 'Add flag to selected item(s)'
302
312
  c.switch %i[flag], negatable: false, default_value: false
303
313
 
314
+ c.desc 'Perform action without confirmation'
315
+ c.switch %i[force], negatable: false, default_value: false
316
+
304
317
  c.desc 'Save selected entries to file using --output format'
305
318
  c.arg_name 'FILE'
306
319
  c.flag %i[save_to]
307
320
 
308
- c.desc 'Output format for export (doing|taskpaper|csv|html|json|template|timeline)'
321
+ c.desc 'Output entries to format (doing|taskpaper|csv|html|json|template|timeline)'
309
322
  c.arg_name 'FORMAT'
310
323
  c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i
311
324
 
@@ -396,7 +409,7 @@ command %i[done did] do |c|
396
409
 
397
410
  c.desc 'Section'
398
411
  c.arg_name 'NAME'
399
- c.flag %i[s section], default_value: wwid.current_section
412
+ c.flag %i[s section]
400
413
 
401
414
  c.desc "Edit entry with #{ENV['EDITOR']}"
402
415
  c.switch %i[e editor], negatable: false, default_value: false
@@ -437,7 +450,11 @@ command %i[done did] do |c|
437
450
  donedate = options[:date] ? "(#{finish_date.strftime('%F %R')})" : ''
438
451
  end
439
452
 
440
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
453
+ if options[:section]
454
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
455
+ else
456
+ section = wwid.config['current_section']
457
+ end
441
458
 
442
459
  if options[:e]
443
460
  exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
@@ -494,7 +511,7 @@ command :cancel do |c|
494
511
 
495
512
  c.desc 'Section'
496
513
  c.arg_name 'NAME'
497
- c.flag %i[s section], default_value: wwid.current_section
514
+ c.flag %i[s section]
498
515
 
499
516
  c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)'
500
517
  c.arg_name 'TAG'
@@ -508,7 +525,11 @@ command :cancel do |c|
508
525
  c.switch %i[u unfinished], negatable: false, default_value: false
509
526
 
510
527
  c.action do |_global_options, options, args|
511
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
528
+ if options[:section]
529
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
530
+ else
531
+ section = wwid.config['current_section']
532
+ end
512
533
 
513
534
  if options[:tag].nil?
514
535
  tags = []
@@ -561,6 +582,10 @@ command :finish do |c|
561
582
  c.arg_name 'INTERVAL'
562
583
  c.flag %i[t took]
563
584
 
585
+ c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
586
+ c.arg_name 'DATE_STRING'
587
+ c.flag [:at]
588
+
564
589
  c.desc 'Finish the last X entries containing TAG.
565
590
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
566
591
  c.arg_name 'TAG'
@@ -587,17 +612,31 @@ command :finish do |c|
587
612
 
588
613
  c.desc 'Section'
589
614
  c.arg_name 'NAME'
590
- c.flag %i[s section], default_value: wwid.current_section
615
+ c.flag %i[s section]
591
616
 
592
617
  c.action do |_global_options, options, args|
593
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
618
+ if options[:section]
619
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
620
+ else
621
+ section = wwid.config['current_section']
622
+ end
594
623
 
595
624
  unless options[:auto]
625
+ if options[:took]
626
+ took = wwid.chronify_qty(options[:took])
627
+ exit_now! 'Unable to parse date string for --took' if took.nil?
628
+ end
629
+
596
630
  exit_now! '--back and --took cannot be used together' if options[:back] && options[:took]
597
631
 
598
632
  exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
599
633
 
600
- if options[:back]
634
+ if options[:at]
635
+ finish_date = wwid.chronify(options[:at])
636
+ exit_now! 'Unable to parse date string for --at' if finish_date.nil?
637
+
638
+ date = options[:took] ? finish_date - took : finish_date
639
+ elsif options[:back]
601
640
  date = wwid.chronify(options[:back])
602
641
 
603
642
  exit_now! 'Unable to parse date string' if date.nil?
@@ -823,7 +862,7 @@ desc 'Mark last entry as highlighted'
823
862
  command [:mark, :flag] do |c|
824
863
  c.desc 'Section'
825
864
  c.arg_name 'NAME'
826
- c.flag %i[s section], default_value: wwid.current_section
865
+ c.flag %i[s section]
827
866
 
828
867
  c.desc 'Remove mark'
829
868
  c.switch %i[r remove], negatable: false, default_value: false
@@ -1306,7 +1345,8 @@ desc 'Select a section to display from a menu'
1306
1345
  command :choose do |c|
1307
1346
  c.action do |_global_options, _options, _args|
1308
1347
  section = wwid.choose_section
1309
- puts wwid.list_section({ section: section.cap_first, count: 0 })
1348
+
1349
+ puts wwid.list_section({ section: section.cap_first, count: 0 }) if section
1310
1350
  end
1311
1351
  end
1312
1352
 
@@ -1340,13 +1380,14 @@ command :colors do |c|
1340
1380
  end
1341
1381
 
1342
1382
  desc 'Display a user-created view'
1383
+ long_desc 'Command line options override associated view settings'
1343
1384
  arg_name 'VIEW_NAME'
1344
1385
  command :view do |c|
1345
- c.desc 'Section (override view settings)'
1386
+ c.desc 'Section'
1346
1387
  c.arg_name 'NAME'
1347
1388
  c.flag %i[s section]
1348
1389
 
1349
- c.desc 'Count to display (override view settings)'
1390
+ c.desc 'Count to display'
1350
1391
  c.arg_name 'COUNT'
1351
1392
  c.flag %i[c count], must_match: /^\d+$/, type: Integer
1352
1393
 
@@ -1364,12 +1405,14 @@ command :view do |c|
1364
1405
  c.switch [:color], default_value: true, negatable: true
1365
1406
 
1366
1407
  c.desc 'Sort tags by (name|time)'
1367
- default = 'time'
1368
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1369
1408
  c.arg_name 'KEY'
1370
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1409
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i
1371
1410
 
1372
- c.desc 'Only show items with recorded time intervals'
1411
+ c.desc 'Tag sort direction (asc|desc)'
1412
+ c.arg_name 'DIRECTION'
1413
+ c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1414
+
1415
+ c.desc 'Only show items with recorded time intervals (override view settings)'
1373
1416
  c.switch [:only_timed], default_value: false, negatable: false
1374
1417
 
1375
1418
  c.action do |_global_options, options, args|
@@ -1379,7 +1422,11 @@ command :view do |c|
1379
1422
  wwid.guess_view(args[0])
1380
1423
  end
1381
1424
 
1382
- section = wwid.guess_section(options[:s]) || options[:s].cap_first if options[:s]
1425
+ if options[:section]
1426
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1427
+ else
1428
+ section = wwid.config['current_section']
1429
+ end
1383
1430
 
1384
1431
  view = wwid.get_view(title)
1385
1432
  if view
@@ -1418,10 +1465,31 @@ command :view do |c|
1418
1465
  end
1419
1466
  order = view.key?('order') ? view['order'] : 'asc'
1420
1467
 
1421
- options[:t] = true if options[:totals]
1468
+ totals = if options[:totals]
1469
+ true
1470
+ else
1471
+ view.key?('totals') ? view['totals'] : false
1472
+ end
1473
+
1474
+ options[:t] = true if totals
1422
1475
  options[:output]&.downcase!
1423
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
1424
1476
 
1477
+ options[:sort_tags] = if options[:tag_sort]
1478
+ options[:tag_sort] =~ /^n/i ? true : false
1479
+ elsif view.key?('tag_sort')
1480
+ view['tag_sort'] =~ /^n/i ? true : false
1481
+ else
1482
+ false
1483
+ end
1484
+
1485
+ tag_order = if options[:tag_order]
1486
+ options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1487
+ elsif view.key?('tag_order')
1488
+ view['tag_order'] =~ /^d/i ? 'desc' : 'asc'
1489
+ else
1490
+ 'asc'
1491
+ end
1492
+ warn "TAG ORDER: #{options[:tag_order]}"
1425
1493
  opts = {
1426
1494
  count: count,
1427
1495
  format: format,
@@ -1432,10 +1500,11 @@ command :view do |c|
1432
1500
  section: section,
1433
1501
  sort_tags: options[:sort_tags],
1434
1502
  tag_filter: tag_filter,
1503
+ tag_order: tag_order,
1435
1504
  tags_color: tags_color,
1436
1505
  template: template,
1437
1506
  times: options[:t],
1438
- totals: options[:totals]
1507
+ totals: totals
1439
1508
  }
1440
1509
 
1441
1510
  puts wwid.list_section(opts)
@@ -1524,6 +1593,48 @@ command :archive do |c|
1524
1593
  end
1525
1594
  end
1526
1595
 
1596
+ desc 'Move entries to archive file'
1597
+ command :rotate do |c|
1598
+ c.desc 'How many items to keep in each section (most recent)'
1599
+ c.arg_name 'X'
1600
+ c.flag %i[k keep], must_match: /^\d+$/, type: Integer
1601
+
1602
+ c.desc 'Section to rotate'
1603
+ c.arg_name 'SECTION_NAME'
1604
+ c.flag %i[s section], default_value: 'All'
1605
+
1606
+ c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
1607
+ c.arg_name 'TAG'
1608
+ c.flag [:tag]
1609
+
1610
+ c.desc 'Tag boolean (AND|OR|NOT)'
1611
+ c.arg_name 'BOOLEAN'
1612
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
1613
+
1614
+ c.desc 'Search filter'
1615
+ c.arg_name 'QUERY'
1616
+ c.flag [:search]
1617
+
1618
+ c.action do |_global_options, options, args|
1619
+ if options[:section] && options[:section] !~ /^all$/i
1620
+ options[:section] = wwid.guess_section(options[:section])
1621
+ end
1622
+
1623
+ options[:bool] = case options[:bool]
1624
+ when /(and|all)/i
1625
+ 'AND'
1626
+ when /(any|or)/i
1627
+ 'OR'
1628
+ when /(not|none)/i
1629
+ 'NOT'
1630
+ else
1631
+ 'AND'
1632
+ end
1633
+
1634
+ wwid.rotate(options)
1635
+ end
1636
+ end
1637
+
1527
1638
  desc 'Open the "doing" file in an editor'
1528
1639
  long_desc "`doing open` defaults to using the editor_app setting in #{wwid.config_file} (#{wwid.config.key?('editor_app') ? wwid.config['editor_app'] : 'not set'})"
1529
1640
  command :open do |c|
@@ -1633,12 +1744,15 @@ command :import do |c|
1633
1744
 
1634
1745
  c.desc 'Target section'
1635
1746
  c.arg_name 'NAME'
1636
- c.flag %i[s section], default_value: wwid.current_section
1747
+ c.flag %i[s section]
1637
1748
 
1638
1749
  c.desc 'Tag all imported entries'
1639
1750
  c.arg_name 'TAGS'
1640
1751
  c.flag :tag
1641
1752
 
1753
+ c.desc 'Autotag entries'
1754
+ c.switch :autotag, negatable: true, default_value: true
1755
+
1642
1756
  c.desc 'Prefix entries with'
1643
1757
  c.arg_name 'PREFIX'
1644
1758
  c.flag :prefix
@@ -1648,11 +1762,22 @@ command :import do |c|
1648
1762
 
1649
1763
  c.action do |_global_options, options, args|
1650
1764
 
1651
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
1765
+ if options[:section]
1766
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1767
+ else
1768
+ section = wwid.config['current_section']
1769
+ end
1652
1770
 
1653
1771
  if options[:type] =~ /^tim/i
1654
1772
  args.each do |path|
1655
- wwid.import_timing(path, { section: section, tag: options[:tag], prefix: options[:prefix], no_overlap: !options[:overlap] })
1773
+ options = {
1774
+ autotag: options[:autotag],
1775
+ no_overlap: !options[:overlap],
1776
+ prefix: options[:prefix],
1777
+ section: section,
1778
+ tag: options[:tag]
1779
+ }
1780
+ wwid.import_timing(path, options)
1656
1781
  wwid.write(wwid.doing_file)
1657
1782
  end
1658
1783
  else
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.80'
2
+ VERSION = '1.0.84'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -84,6 +84,7 @@ class WWID
84
84
  ##
85
85
  def configure(opt = {})
86
86
  @timers = {}
87
+ @recorded_items = []
87
88
  opt[:ignore_local] ||= false
88
89
 
89
90
  @config_file ||= File.join(@user_home, @default_config_file)
@@ -424,8 +425,8 @@ class WWID
424
425
  ## @param guessed (Boolean) already guessed and failed
425
426
  ##
426
427
  def guess_section(frag, guessed: false)
427
- return 'All' if frag =~ /all/i
428
-
428
+ return 'All' if frag =~ /^all$/i
429
+ frag ||= @current_section
429
430
  sections.each { |section| return section.cap_first if frag.downcase == section.downcase }
430
431
  section = false
431
432
  re = frag.split('').join('.*?')
@@ -583,17 +584,17 @@ class WWID
583
584
  end
584
585
 
585
586
  def same_time?(item_a, item_b)
586
- item_a['date'] == item_b['date'] ? get_interval(item_a, false) == get_interval(item_b, false) : false
587
+ item_a['date'] == item_b['date'] ? get_interval(item_a, formatted: false, record: false) == get_interval(item_b, formatted: false, record: false) : false
587
588
  end
588
589
 
589
590
  def overlapping_time?(item_a, item_b)
590
591
  return true if same_time?(item_a, item_b)
591
592
 
592
593
  start_a = item_a['date']
593
- interval = get_interval(item_a, false)
594
+ interval = get_interval(item_a, formatted: false, record: false)
594
595
  end_a = interval ? start_a + interval.to_i : start_a
595
596
  start_b = item_b['date']
596
- interval = get_interval(item_b, false)
597
+ interval = get_interval(item_b, formatted: false, record: false)
597
598
  end_b = interval ? start_b + interval.to_i : start_b
598
599
  (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
599
600
  end
@@ -626,6 +627,7 @@ class WWID
626
627
  def import_timing(path, opt = {})
627
628
  section = opt[:section] || @current_section
628
629
  opt[:no_overlap] ||= false
630
+ opt[:autotag] ||= @auto_tag
629
631
 
630
632
  add_section(section) unless @content.has_key?(section)
631
633
 
@@ -656,7 +658,7 @@ class WWID
656
658
  title += " @#{tag}"
657
659
  end
658
660
  end
659
- title = autotag(title) if @auto_tag
661
+ title = autotag(title) if opt[:autotag]
660
662
  title += " @done(#{end_time.strftime('%Y-%m-%d %H:%M')})"
661
663
  title.gsub!(/ +/, ' ')
662
664
  title.strip!
@@ -757,7 +759,7 @@ class WWID
757
759
  all_items.concat(@content[section]['items'].dup) if @content.key?(section)
758
760
  end
759
761
 
760
- if opt[:tag] && opt[:tag].length.positive?
762
+ if opt[:tag]&.length
761
763
  all_items.select! { |item| item.has_tags?(opt[:tag], opt[:tag_bool]) }
762
764
  elsif opt[:search]&.length
763
765
  all_items.select! { |item| item.matches_search?(opt[:search]) }
@@ -771,9 +773,14 @@ class WWID
771
773
  ##
772
774
  ## @return (String) The selected option
773
775
  ##
774
- def choose_from(options, prompt)
776
+ def choose_from(options, prompt: 'Make a selection: ', multiple: false, fzf_args: [])
775
777
  fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
776
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} --prompt "#{prompt}"`
778
+ fzf_args << '-1'
779
+ fzf_args << %(--prompt "#{prompt}")
780
+ fzf_args << '--multi' if multiple
781
+ header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
782
+ fzf_args << %(--header "#{header}")
783
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
777
784
  return false if res.strip.size.zero?
778
785
 
779
786
  res
@@ -807,7 +814,7 @@ class WWID
807
814
  ') ',
808
815
  item['date'],
809
816
  ' | ',
810
- item['title'],
817
+ item['title']
811
818
  ]
812
819
  if opt[:section] =~ /^all/i
813
820
  out.concat([
@@ -818,8 +825,15 @@ class WWID
818
825
  end
819
826
  out.join('')
820
827
  end
821
-
822
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} --header="Arrows to navigate, tab to mark for selection, enter to perform action" --prompt="Select entries to act on> " -m --bind ctrl-a:select-all -q "#{opt[:query]}"`
828
+ fzf_args = [
829
+ %(--header="Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit"),
830
+ %(--prompt="Select entries to act on > "),
831
+ '-1',
832
+ '-m',
833
+ '--bind ctrl-a:select-all',
834
+ %(-q "#{opt[:query]}")
835
+ ]
836
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
823
837
  selected = []
824
838
  res.split(/\n/).each do |item|
825
839
  idx = item.match(/^(\d+)\)/)[1].to_i
@@ -841,62 +855,71 @@ class WWID
841
855
  end
842
856
 
843
857
  unless has_action
844
- action = choose_from(
845
- [
846
- 'add tag',
847
- 'remove tag',
848
- 'archive',
849
- 'cancel',
850
- 'delete',
851
- 'edit',
852
- 'finish',
853
- 'flag',
854
- 'move',
855
- 'output format'
856
- ],
857
- 'What do you want to do with the selected items? > ')
858
- case action
859
- when /(add|remove) tag/
860
- print 'Enter tag: '
861
- tag = STDIN.gets
862
- return if tag =~ /^ *$/
863
- opt[:tag] = tag.strip
864
- opt[:remove] = true if action =~ /remove tag/
865
- when /output format/
866
- output_format = choose_from(%w[doing taskpaper json timeline html csv], 'Which output format? > ')
867
- return if tag =~ /^ *$/
868
- opt[:output] = output_format.strip
869
- res = yn('Save to file?', default_response: 'n')
870
- if res
871
- print 'File path/name: '
872
- filename = STDIN.gets.strip
873
- return if filename.empty?
874
- opt[:save_to] = filename
858
+ choice = choose_from([
859
+ 'add tag',
860
+ 'remove tag',
861
+ 'cancel',
862
+ 'delete',
863
+ 'finish',
864
+ 'flag',
865
+ 'archive',
866
+ 'move',
867
+ 'edit',
868
+ 'output formatted'
869
+ ],
870
+ prompt: 'What do you want to do with the selected items? > ',
871
+ multiple: true,
872
+ fzf_args: ['--height=60%', '--tac', '--no-sort'])
873
+ return unless choice
874
+
875
+ to_do = choice.strip.split(/\n/)
876
+ to_do.each do |action|
877
+ case action
878
+ when /(add|remove) tag/
879
+ type = action =~ /^add/ ? 'add' : 'remove'
880
+ if opt[:tag]
881
+ warn "'add tag' and 'remove tag' can not be used together"
882
+ Process.exit 1
883
+ end
884
+ print "#{colors['yellow']}Tag to #{type}: #{colors['reset']}"
885
+ tag = STDIN.gets
886
+ return if tag =~ /^ *$/
887
+ opt[:tag] = tag.strip.sub(/^@/, '')
888
+ opt[:remove] = true if type == 'remove'
889
+ when /output formatted/
890
+ output_format = choose_from(%w[doing taskpaper json timeline html csv].sort, prompt: 'Which output format? > ', fzf_args: ['--height=60%', '--tac', '--no-sort'])
891
+ return if tag =~ /^ *$/
892
+ opt[:output] = output_format.strip
893
+ res = opt[:force] ? false : yn('Save to file?', default_response: 'n')
894
+ if res
895
+ print "#{colors['yellow']}File path/name: #{colors['reset']}"
896
+ filename = STDIN.gets.strip
897
+ return if filename.empty?
898
+ opt[:save_to] = filename
899
+ end
900
+ when /archive/
901
+ opt[:archive] = true
902
+ when /delete/
903
+ opt[:delete] = true
904
+ when /edit/
905
+ opt[:editor] = true
906
+ when /finish/
907
+ opt[:finish] = true
908
+ when /cancel/
909
+ opt[:cancel] = true
910
+ when /move/
911
+ section = choose_section.strip
912
+ opt[:move] = section.strip unless section =~ /^ *$/
913
+ when /flag/
914
+ opt[:flag] = true
875
915
  end
876
- when /archive/
877
- opt[:archive] = true
878
- when /delete/
879
- opt[:delete] = true
880
- when /edit/
881
- opt[:editor] = true
882
- when /finish/
883
- opt[:finish] = true
884
- when /cancel/
885
- opt[:cancel] = true
886
- when /move/
887
- section = choose_section.strip
888
- return if section =~ /^ *$/
889
- opt[:move] = section.strip
890
- when /flag/
891
- opt[:flag] = true
892
916
  end
893
917
  end
894
918
 
895
-
896
919
  if opt[:delete]
897
- res = yn("Delete #{selected.size} items?", default_response: 'y')
920
+ res = opt[:force] ? true : yn("Delete #{selected.size} items?", default_response: 'y')
898
921
  if res
899
- selected.each {|item| delete_item(item) }
922
+ selected.each { |item| delete_item(item) }
900
923
  write(@doing_file)
901
924
  end
902
925
  return
@@ -990,13 +1013,16 @@ class WWID
990
1013
  item
991
1014
  end
992
1015
 
993
- @content = {'Export' => {'original' => 'Export:', 'items' => selected}}
994
- options = {section: 'Export'}
1016
+ @content = { 'Export' => { 'original' => 'Export:', 'items' => selected } }
1017
+ options = { section: 'Export' }
995
1018
 
996
- if opt[:output] !~ /(doing|taskpaper)/
997
- options[:output] = opt[:output]
998
- else
1019
+ case opt[:output]
1020
+ when /doing/
999
1021
  options[:template] = '- %date | %title%note'
1022
+ when /taskpaper/
1023
+ options[:template] = '- %title @date(%date)%note'
1024
+ else
1025
+ options[:output] = opt[:output]
1000
1026
  end
1001
1027
 
1002
1028
  output = list_section(options)
@@ -1078,7 +1104,6 @@ class WWID
1078
1104
  count = (opt[:count]).zero? ? items.length : opt[:count]
1079
1105
  items.map! do |item|
1080
1106
  break if idx == count
1081
-
1082
1107
  finished = opt[:unfinished] && item.has_tags?('done', :and)
1083
1108
  tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.has_tags?(opt[:tag], opt[:tag_bool])
1084
1109
  search_match = opt[:search].nil? || opt[:search].empty? ? true : item.matches_search?(opt[:search])
@@ -1497,13 +1522,85 @@ class WWID
1497
1522
  end
1498
1523
  end
1499
1524
 
1525
+ ##
1526
+ ## @brief Rename doing file with date and start fresh one
1527
+ ##
1528
+ def rotate(opt = {})
1529
+ count = opt[:keep] || 0
1530
+ tags = []
1531
+ tags.concat(opt[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if opt[:tag]
1532
+ bool = opt[:bool] || :and
1533
+ sect = opt[:section] !~ /^all$/i ? guess_section(opt[:section]) : 'all'
1534
+
1535
+ if sect =~ /^all$/i
1536
+ all_sections = sections.dup
1537
+ else
1538
+ all_sections = [sect]
1539
+ end
1540
+
1541
+ counter = 0
1542
+ new_content = {}
1543
+
1544
+
1545
+ all_sections.each do |section|
1546
+ items = @content[section]['items'].dup
1547
+ new_content[section] = {}
1548
+ new_content[section]['original'] = @content[section]['original']
1549
+ new_content[section]['items'] = []
1550
+
1551
+ moved_items = []
1552
+ if !tags.empty? || opt[:search]
1553
+ items.delete_if do |item|
1554
+ if ((!tags.empty? && item.has_tags?(tags, bool)) || (opt[:search] && item.matches_search?(opt[:search].to_s)))
1555
+ moved_items.push(item)
1556
+ counter += 1
1557
+ true
1558
+ else
1559
+ false
1560
+ end
1561
+ end
1562
+ @content[section]['items'] = items
1563
+ new_content[section]['items'] = moved_items
1564
+ @results.push("Rotated #{moved_items.length} items from #{section}")
1565
+ else
1566
+ new_content[section]['items'] = []
1567
+ moved_items = []
1568
+
1569
+ count = items.length if items.length < count
1570
+
1571
+ if items.count > count
1572
+ moved_items.concat(items[count..-1])
1573
+ else
1574
+ moved_items.concat(items)
1575
+ end
1576
+
1577
+ @content[section]['items'] = if count.zero?
1578
+ []
1579
+ else
1580
+ items[0..count - 1]
1581
+ end
1582
+ new_content[section]['items'] = moved_items
1583
+
1584
+ @results.push("Rotated #{items.length - count} items from #{section}")
1585
+ end
1586
+ end
1587
+
1588
+ write(@doing_file)
1589
+
1590
+ file = @doing_file.sub(/(\.\w+)$/, "_#{Time.now.strftime('%Y-%m-%d%H:%M')}\\1")
1591
+ @content = new_content
1592
+
1593
+ write(file)
1594
+ end
1595
+
1500
1596
  ##
1501
1597
  ## @brief Generate a menu of sections and allow user selection
1502
1598
  ##
1503
1599
  ## @return (String) The selected section name
1504
1600
  ##
1505
1601
  def choose_section
1506
- choose_from(sections, 'Choose a section > ').strip
1602
+ choice = choose_from(sections.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
1603
+ choice ? choice.strip : choice
1507
1604
  end
1508
1605
 
1509
1606
  ##
@@ -1521,7 +1618,8 @@ class WWID
1521
1618
  ## @return (String) The selected view name
1522
1619
  ##
1523
1620
  def choose_view
1524
- choose_from(views, 'Choose a view > ').strip
1621
+ choice = choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
1622
+ choice ? choice.strip : choice
1525
1623
  end
1526
1624
 
1527
1625
  ##
@@ -1544,20 +1642,21 @@ class WWID
1544
1642
  def list_section(opt = {})
1545
1643
  opt[:count] ||= 0
1546
1644
  count = opt[:count] - 1
1547
- opt[:section] ||= nil
1548
- opt[:format] ||= @default_date_format
1549
- opt[:template] ||= @default_template
1550
1645
  opt[:age] ||= 'newest'
1646
+ opt[:date_filter] ||= []
1647
+ opt[:format] ||= @default_date_format
1648
+ opt[:only_timed] ||= false
1551
1649
  opt[:order] ||= 'desc'
1552
- opt[:today] ||= false
1650
+ opt[:search] ||= false
1651
+ opt[:section] ||= nil
1652
+ opt[:sort_tags] ||= false
1553
1653
  opt[:tag_filter] ||= false
1654
+ opt[:tag_order] ||= 'asc'
1554
1655
  opt[:tags_color] ||= false
1656
+ opt[:template] ||= @default_template
1555
1657
  opt[:times] ||= false
1658
+ opt[:today] ||= false
1556
1659
  opt[:totals] ||= false
1557
- opt[:sort_tags] ||= false
1558
- opt[:search] ||= false
1559
- opt[:only_timed] ||= false
1560
- opt[:date_filter] ||= []
1561
1660
 
1562
1661
  # opt[:highlight] ||= true
1563
1662
  section = ''
@@ -1610,7 +1709,7 @@ class WWID
1610
1709
 
1611
1710
  if opt[:only_timed]
1612
1711
  items.delete_if do |item|
1613
- get_interval(item) == false
1712
+ get_interval(item, record: false) == false
1614
1713
  end
1615
1714
  end
1616
1715
 
@@ -1645,7 +1744,7 @@ class WWID
1645
1744
  arr = i['note'].map { |line| line.strip }.delete_if { |e| e =~ /^\s*$/ }
1646
1745
  note = arr.join("\n") unless arr.nil?
1647
1746
  end
1648
- interval = get_interval(i, false) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1747
+ interval = get_interval(i, formatted: false) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1649
1748
  interval ||= 0
1650
1749
  output.push(CSV.generate_line([i['date'], i['title'], note, interval, i['section']]))
1651
1750
  end
@@ -1664,7 +1763,7 @@ class WWID
1664
1763
  end
1665
1764
  if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1666
1765
  end_date = Time.parse(Regexp.last_match(1))
1667
- interval = get_interval(i, false)
1766
+ interval = get_interval(i, formatted: false)
1668
1767
  end
1669
1768
  end_date ||= ''
1670
1769
  interval ||= 0
@@ -1713,7 +1812,7 @@ class WWID
1713
1812
  out = {
1714
1813
  'section' => section,
1715
1814
  'items' => items_out,
1716
- 'timers' => tag_times('json', opt[:sort_tags])
1815
+ 'timers' => tag_times(format: 'json', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order])
1717
1816
  }.to_json
1718
1817
  elsif opt[:output] == 'timeline'
1719
1818
  template = <<~EOTEMPLATE
@@ -1794,7 +1893,7 @@ class WWID
1794
1893
  css_template
1795
1894
  end
1796
1895
 
1797
- totals = opt[:totals] ? tag_times('html', opt[:sort_tags]) : ''
1896
+ totals = opt[:totals] ? tag_times(format: 'html', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
1798
1897
  engine = Haml::Engine.new(template)
1799
1898
  out = engine.render(Object.new,
1800
1899
  { :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
@@ -1835,7 +1934,7 @@ class WWID
1835
1934
 
1836
1935
  output.sub!(/%date/, item['date'].strftime(opt[:format]))
1837
1936
 
1838
- interval = get_interval(item) if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1937
+ interval = get_interval(item, record: true) if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1839
1938
  interval ||= ''
1840
1939
  output.sub!(/%interval/, interval)
1841
1940
 
@@ -1885,7 +1984,8 @@ class WWID
1885
1984
 
1886
1985
  out += "#{output}\n"
1887
1986
  end
1888
- out += tag_times('text', opt[:sort_tags]) if opt[:totals]
1987
+
1988
+ out += tag_times(format: 'text', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
1889
1989
  end
1890
1990
  out
1891
1991
  end
@@ -1897,7 +1997,7 @@ class WWID
1897
1997
  ## @param section (String) The source section
1898
1998
  ## @param options (Hash) Options
1899
1999
  ##
1900
- def archive(section = 'Currently', options = {})
2000
+ def archive(section = @current_section, options = {})
1901
2001
  count = options[:keep] || 0
1902
2002
  destination = options[:destination] || 'Archive'
1903
2003
  tags = options[:tags] || []
@@ -1956,7 +2056,7 @@ class WWID
1956
2056
  end
1957
2057
  end
1958
2058
  moved_items.each do |item|
1959
- if label && section != 'Currently'
2059
+ if label && section != @current_section
1960
2060
  item['title'] =
1961
2061
  item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
1962
2062
  end
@@ -1969,7 +2069,7 @@ class WWID
1969
2069
  count = items.length if items.length < count
1970
2070
 
1971
2071
  items.map! do |item|
1972
- if label && section != 'Currently'
2072
+ if label && section != @current_section
1973
2073
  item['title'] =
1974
2074
  item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
1975
2075
  end
@@ -2148,11 +2248,15 @@ class WWID
2148
2248
  end
2149
2249
 
2150
2250
  ##
2151
- ## @brief Get total elapsed time for all tags in selection
2251
+ ## @brief Get total elapsed time for all tags in
2252
+ ## selection
2152
2253
  ##
2153
- ## @param format (String) return format (html, json, or text)
2254
+ ## @param format (String) return format (html,
2255
+ ## json, or text)
2256
+ ## @param sort_by_name (Boolean) Sort by name if true, otherwise by time
2257
+ ## @param sort_order (String) The sort order (asc or desc)
2154
2258
  ##
2155
- def tag_times(format = 'text', sort_by_name = false)
2259
+ def tag_times(format: 'text', sort_by_name: false, sort_order: 'asc')
2156
2260
  return '' if @timers.empty?
2157
2261
 
2158
2262
  max = @timers.keys.sort_by { |k| k.length }.reverse[0].length + 1
@@ -2161,11 +2265,13 @@ class WWID
2161
2265
 
2162
2266
  tags_data = @timers.delete_if { |_k, v| v == 0 }
2163
2267
  sorted_tags_data = if sort_by_name
2164
- tags_data.sort_by { |k, _v| k }.reverse
2268
+ tags_data.sort_by { |k, _v| k }
2165
2269
  else
2166
2270
  tags_data.sort_by { |_k, v| v }
2167
2271
  end
2168
2272
 
2273
+ sorted_tags_data.reverse! if sort_order =~ /^asc/i
2274
+
2169
2275
  if format == 'html'
2170
2276
  output = <<EOS
2171
2277
  <table>
@@ -2299,19 +2405,20 @@ EOS
2299
2405
  ## @param item (Hash) The entry
2300
2406
  ## @param formatted (Bool) Return human readable time (default seconds)
2301
2407
  ##
2302
- def get_interval(item, formatted = true)
2408
+ def get_interval(item, formatted: true, record: true)
2303
2409
  done = nil
2304
2410
  start = nil
2305
2411
 
2306
2412
  if @interval_cache.keys.include? item['title']
2307
2413
  seconds = @interval_cache[item['title']]
2414
+ record_tag_times(item, seconds) if record
2308
2415
  return seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
2309
2416
  end
2310
2417
 
2311
2418
  if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
2312
2419
  done = Time.parse(Regexp.last_match(1))
2313
2420
  else
2314
- return nil
2421
+ return false
2315
2422
  end
2316
2423
 
2317
2424
  start = if item['title'] =~ /@start\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
@@ -2322,20 +2429,34 @@ EOS
2322
2429
 
2323
2430
  seconds = (done - start).to_i
2324
2431
 
2432
+ if record
2433
+ record_tag_times(item, seconds)
2434
+ end
2435
+
2436
+ @interval_cache[item['title']] = seconds
2437
+
2438
+ return seconds > 0 ? seconds : false unless formatted
2439
+
2440
+ seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
2441
+ end
2442
+
2443
+ ##
2444
+ ## @brief Record times for item tags
2445
+ ##
2446
+ ## @param item The item
2447
+ ##
2448
+ def record_tag_times(item, seconds)
2449
+ return if @recorded_items.include?(item)
2450
+
2325
2451
  item['title'].scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
2326
2452
  k = m[0] == 'done' ? 'All' : m[0].downcase
2327
- if @timers.has_key?(k)
2453
+ if @timers.key?(k)
2328
2454
  @timers[k] += seconds
2329
2455
  else
2330
2456
  @timers[k] = seconds
2331
2457
  end
2458
+ @recorded_items.push(item)
2332
2459
  end
2333
-
2334
- @interval_cache[item['title']] = seconds
2335
-
2336
- return seconds unless formatted
2337
-
2338
- seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
2339
2460
  end
2340
2461
 
2341
2462
  ##
data/lib/doing.rb CHANGED
@@ -8,5 +8,6 @@ require 'tempfile'
8
8
  require 'chronic'
9
9
  require 'haml'
10
10
  require 'json'
11
- require 'doing/helpers.rb'
12
- require 'doing/wwid.rb'
11
+ require 'doing/helpers'
12
+ require 'doing/wwid'
13
+ # require 'doing/markdown_document_listener'
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.80
4
+ version: 1.0.84
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-25 00:00:00.000000000 Z
11
+ date: 2021-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake