doing 1.0.80 → 1.0.84

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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