doing 1.0.82 → 1.0.86

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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -4
  3. data/bin/doing +173 -40
  4. data/lib/doing/version.rb +1 -1
  5. data/lib/doing/wwid.rb +177 -46
  6. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 146bc31206860d02e7f7e3d3d04b12cd84ecf794171af8f25ca679f49c6a98d0
4
- data.tar.gz: 994568054f7f905bbe6a1982b00be636a68bac2719b4f6740ce04bf5cbcb208a
3
+ metadata.gz: 2af84d93e54c8366719e285303dc11d966f9b83361ae4ac20ca172536465f4ee
4
+ data.tar.gz: 7fa72f0ec1ba1a18e45d80c1604910cabb00c8dad485b96d05bd8c3e33a5b4ae
5
5
  SHA512:
6
- metadata.gz: d3c606a1bf94fc65241841ca4e9899488f8271569492fb33d4ecf49c46007bb1f678c5d6995656a17fbd003d387942bf43b0575b09d9a78bfc64852504224586
7
- data.tar.gz: 5032b06cc50cc867e29f99973b6f03e4256f746f5643db7275a17621252db5e825fb1aa1b07439c0a3f087a2cfe96f6bc3a9966c98693f8a3a994880c81b6857
6
+ metadata.gz: 158707f75c6375fb5d684457d0b17c874fb2dc0f9b2e5318a24f055665ed246e69149211fb45487acac2174b2ca9db6ebd62b071af0c283377d5e2fa78f663c2
7
+ data.tar.gz: 80bf8ff4da513c0d911afdf5bbeb2589f6a08bf46eb746c5eb610f5ddac1bdc7d7469f1f3eb399e86c4a671358df704c98596841c2f8964d3f81ebe985de1435
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.85<!--END VER-->.
31
31
 
32
32
  $ [sudo] gem install doing
33
33
 
@@ -92,6 +92,7 @@ A basic configuration looks like this:
92
92
  date_format: '%_I:%M%P'
93
93
  template: '%date > %title%odnote'
94
94
  wrap_width: 50
95
+ count: 10
95
96
  autotag:
96
97
  whitelist:
97
98
  - coding
@@ -239,13 +240,15 @@ and output my recent entries like this:
239
240
 
240
241
  $
241
242
 
243
+ The recent template can include a `count` key to specify the number of entries shown when run without an argument. Default is 10.
244
+
242
245
  ### Custom views
243
246
 
244
247
  You can create your own "views" in the `~/.doingrc` file and view them with `doing view view_name`. Just add a section like this:
245
248
 
246
249
  views:
247
250
  old:
248
- section: Old
251
+ section: Archive
249
252
  count: 5
250
253
  wrap_width: 0
251
254
  date_format: '%F %_I:%M%P'
@@ -253,6 +256,10 @@ You can create your own "views" in the `~/.doingrc` file and view them with `doi
253
256
  order: asc
254
257
  tags: done finished cancelled
255
258
  tags_bool: ANY
259
+ only_timed: false
260
+ tag_sort: time
261
+ tag_order: asc
262
+ totals: true
256
263
 
257
264
  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
265
 
@@ -276,7 +283,9 @@ You can add new sections with `doing add_section section_name`. You can also cre
276
283
 
277
284
  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
285
 
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`.
286
+ 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`.
287
+
288
+ 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
289
 
281
290
  Regarding colors, you can use them to create very nice displays if you're outputting to a color terminal. Example:
282
291
 
@@ -291,7 +300,7 @@ Outputs:
291
300
 
292
301
  ![](http://ckyp.us/XKpj+)
293
302
 
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.
303
+ 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
304
 
296
305
  ### Colors
297
306
 
data/bin/doing CHANGED
@@ -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]
@@ -308,7 +318,7 @@ command :select do |c|
308
318
  c.arg_name 'FILE'
309
319
  c.flag %i[save_to]
310
320
 
311
- 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)'
312
322
  c.arg_name 'FORMAT'
313
323
  c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i
314
324
 
@@ -323,10 +333,6 @@ command :later do |c|
323
333
  c.desc "Edit entry with #{ENV['EDITOR']}"
324
334
  c.switch %i[e editor], negatable: false, default_value: false
325
335
 
326
- c.desc 'Edit entry with specified app'
327
- c.arg_name 'APP'
328
- c.flag %i[a app]
329
-
330
336
  c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
331
337
  c.arg_name 'DATE_STRING'
332
338
  c.flag %i[b back]
@@ -343,7 +349,7 @@ command :later do |c|
343
349
  date = Time.now
344
350
  end
345
351
 
346
- if options[:e] || (args.empty? && $stdin.stat.size.zero?)
352
+ if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
347
353
  exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
348
354
 
349
355
  input = args.empty? ? '' : args.join(' ')
@@ -399,7 +405,7 @@ command %i[done did] do |c|
399
405
 
400
406
  c.desc 'Section'
401
407
  c.arg_name 'NAME'
402
- c.flag %i[s section], default_value: wwid.current_section
408
+ c.flag %i[s section]
403
409
 
404
410
  c.desc "Edit entry with #{ENV['EDITOR']}"
405
411
  c.switch %i[e editor], negatable: false, default_value: false
@@ -440,9 +446,13 @@ command %i[done did] do |c|
440
446
  donedate = options[:date] ? "(#{finish_date.strftime('%F %R')})" : ''
441
447
  end
442
448
 
443
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
449
+ if options[:section]
450
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
451
+ else
452
+ section = wwid.config['current_section']
453
+ end
444
454
 
445
- if options[:e]
455
+ if options[:editor]
446
456
  exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
447
457
 
448
458
  input = ''
@@ -497,7 +507,7 @@ command :cancel do |c|
497
507
 
498
508
  c.desc 'Section'
499
509
  c.arg_name 'NAME'
500
- c.flag %i[s section], default_value: wwid.current_section
510
+ c.flag %i[s section]
501
511
 
502
512
  c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)'
503
513
  c.arg_name 'TAG'
@@ -511,7 +521,11 @@ command :cancel do |c|
511
521
  c.switch %i[u unfinished], negatable: false, default_value: false
512
522
 
513
523
  c.action do |_global_options, options, args|
514
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
524
+ if options[:section]
525
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
526
+ else
527
+ section = wwid.config['current_section']
528
+ end
515
529
 
516
530
  if options[:tag].nil?
517
531
  tags = []
@@ -564,6 +578,10 @@ command :finish do |c|
564
578
  c.arg_name 'INTERVAL'
565
579
  c.flag %i[t took]
566
580
 
581
+ c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
582
+ c.arg_name 'DATE_STRING'
583
+ c.flag [:at]
584
+
567
585
  c.desc 'Finish the last X entries containing TAG.
568
586
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
569
587
  c.arg_name 'TAG'
@@ -590,17 +608,31 @@ command :finish do |c|
590
608
 
591
609
  c.desc 'Section'
592
610
  c.arg_name 'NAME'
593
- c.flag %i[s section], default_value: wwid.current_section
611
+ c.flag %i[s section]
594
612
 
595
613
  c.action do |_global_options, options, args|
596
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
614
+ if options[:section]
615
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
616
+ else
617
+ section = wwid.config['current_section']
618
+ end
597
619
 
598
620
  unless options[:auto]
621
+ if options[:took]
622
+ took = wwid.chronify_qty(options[:took])
623
+ exit_now! 'Unable to parse date string for --took' if took.nil?
624
+ end
625
+
599
626
  exit_now! '--back and --took cannot be used together' if options[:back] && options[:took]
600
627
 
601
628
  exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag]
602
629
 
603
- if options[:back]
630
+ if options[:at]
631
+ finish_date = wwid.chronify(options[:at])
632
+ exit_now! 'Unable to parse date string for --at' if finish_date.nil?
633
+
634
+ date = options[:took] ? finish_date - took : finish_date
635
+ elsif options[:back]
604
636
  date = wwid.chronify(options[:back])
605
637
 
606
638
  exit_now! 'Unable to parse date string' if date.nil?
@@ -826,7 +858,7 @@ desc 'Mark last entry as highlighted'
826
858
  command [:mark, :flag] do |c|
827
859
  c.desc 'Section'
828
860
  c.arg_name 'NAME'
829
- c.flag %i[s section], default_value: wwid.current_section
861
+ c.flag %i[s section]
830
862
 
831
863
  c.desc 'Remove mark'
832
864
  c.switch %i[r remove], negatable: false, default_value: false
@@ -1065,7 +1097,12 @@ command :recent do |c|
1065
1097
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
1066
1098
 
1067
1099
  unless global_options[:version]
1068
- count = args.empty? ? 10 : args[0].to_i
1100
+ if wwid.config['templates']['recent'].key?('count')
1101
+ config_count = wwid.config['templates']['recent']['count'].to_i
1102
+ else
1103
+ config_count = 10
1104
+ end
1105
+ count = args.empty? ? config_count : args[0].to_i
1069
1106
  options[:t] = true if options[:totals]
1070
1107
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1071
1108
  tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil
@@ -1285,7 +1322,7 @@ command :last do |c|
1285
1322
 
1286
1323
  end
1287
1324
 
1288
- if options[:e]
1325
+ if options[:editor]
1289
1326
  wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
1290
1327
  else
1291
1328
  puts wwid.last(times: true, section: options[:s],
@@ -1344,13 +1381,14 @@ command :colors do |c|
1344
1381
  end
1345
1382
 
1346
1383
  desc 'Display a user-created view'
1384
+ long_desc 'Command line options override associated view settings'
1347
1385
  arg_name 'VIEW_NAME'
1348
1386
  command :view do |c|
1349
- c.desc 'Section (override view settings)'
1387
+ c.desc 'Section'
1350
1388
  c.arg_name 'NAME'
1351
1389
  c.flag %i[s section]
1352
1390
 
1353
- c.desc 'Count to display (override view settings)'
1391
+ c.desc 'Count to display'
1354
1392
  c.arg_name 'COUNT'
1355
1393
  c.flag %i[c count], must_match: /^\d+$/, type: Integer
1356
1394
 
@@ -1368,12 +1406,14 @@ command :view do |c|
1368
1406
  c.switch [:color], default_value: true, negatable: true
1369
1407
 
1370
1408
  c.desc 'Sort tags by (name|time)'
1371
- default = 'time'
1372
- default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1373
1409
  c.arg_name 'KEY'
1374
- c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1410
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i
1375
1411
 
1376
- c.desc 'Only show items with recorded time intervals'
1412
+ c.desc 'Tag sort direction (asc|desc)'
1413
+ c.arg_name 'DIRECTION'
1414
+ c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1415
+
1416
+ c.desc 'Only show items with recorded time intervals (override view settings)'
1377
1417
  c.switch [:only_timed], default_value: false, negatable: false
1378
1418
 
1379
1419
  c.action do |_global_options, options, args|
@@ -1383,7 +1423,11 @@ command :view do |c|
1383
1423
  wwid.guess_view(args[0])
1384
1424
  end
1385
1425
 
1386
- section = wwid.guess_section(options[:s]) || options[:s].cap_first if options[:s]
1426
+ if options[:section]
1427
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1428
+ else
1429
+ section = wwid.config['current_section']
1430
+ end
1387
1431
 
1388
1432
  view = wwid.get_view(title)
1389
1433
  if view
@@ -1422,10 +1466,31 @@ command :view do |c|
1422
1466
  end
1423
1467
  order = view.key?('order') ? view['order'] : 'asc'
1424
1468
 
1425
- options[:t] = true if options[:totals]
1469
+ totals = if options[:totals]
1470
+ true
1471
+ else
1472
+ view.key?('totals') ? view['totals'] : false
1473
+ end
1474
+
1475
+ options[:t] = true if totals
1426
1476
  options[:output]&.downcase!
1427
- options[:sort_tags] = options[:tag_sort] =~ /^n/i
1428
1477
 
1478
+ options[:sort_tags] = if options[:tag_sort]
1479
+ options[:tag_sort] =~ /^n/i ? true : false
1480
+ elsif view.key?('tag_sort')
1481
+ view['tag_sort'] =~ /^n/i ? true : false
1482
+ else
1483
+ false
1484
+ end
1485
+
1486
+ tag_order = if options[:tag_order]
1487
+ options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1488
+ elsif view.key?('tag_order')
1489
+ view['tag_order'] =~ /^d/i ? 'desc' : 'asc'
1490
+ else
1491
+ 'asc'
1492
+ end
1493
+ warn "TAG ORDER: #{options[:tag_order]}"
1429
1494
  opts = {
1430
1495
  count: count,
1431
1496
  format: format,
@@ -1436,10 +1501,11 @@ command :view do |c|
1436
1501
  section: section,
1437
1502
  sort_tags: options[:sort_tags],
1438
1503
  tag_filter: tag_filter,
1504
+ tag_order: tag_order,
1439
1505
  tags_color: tags_color,
1440
1506
  template: template,
1441
1507
  times: options[:t],
1442
- totals: options[:totals]
1508
+ totals: totals
1443
1509
  }
1444
1510
 
1445
1511
  puts wwid.list_section(opts)
@@ -1489,6 +1555,11 @@ command :archive do |c|
1489
1555
  c.arg_name 'QUERY'
1490
1556
  c.flag [:search]
1491
1557
 
1558
+ c.desc 'Archive entries older than date
1559
+ (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
1560
+ c.arg_name 'DATE_STRING'
1561
+ c.flag [:before]
1562
+
1492
1563
  c.action do |_global_options, options, args|
1493
1564
  if args.empty?
1494
1565
  section = wwid.current_section
@@ -1518,6 +1589,7 @@ command :archive do |c|
1518
1589
  'AND'
1519
1590
  end
1520
1591
  opts = {
1592
+ before: options[:before],
1521
1593
  bool: options[:bool],
1522
1594
  destination: options[:to],
1523
1595
  keep: options[:keep],
@@ -1528,17 +1600,64 @@ command :archive do |c|
1528
1600
  end
1529
1601
  end
1530
1602
 
1603
+ desc 'Move entries to archive file'
1604
+ command :rotate do |c|
1605
+ c.desc 'How many items to keep in each section (most recent)'
1606
+ c.arg_name 'X'
1607
+ c.flag %i[k keep], must_match: /^\d+$/, type: Integer
1608
+
1609
+ c.desc 'Section to rotate'
1610
+ c.arg_name 'SECTION_NAME'
1611
+ c.flag %i[s section], default_value: 'All'
1612
+
1613
+ c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
1614
+ c.arg_name 'TAG'
1615
+ c.flag [:tag]
1616
+
1617
+ c.desc 'Tag boolean (AND|OR|NOT)'
1618
+ c.arg_name 'BOOLEAN'
1619
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
1620
+
1621
+ c.desc 'Search filter'
1622
+ c.arg_name 'QUERY'
1623
+ c.flag [:search]
1624
+
1625
+ c.desc 'Rotate entries older than date
1626
+ (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
1627
+ c.arg_name 'DATE_STRING'
1628
+ c.flag [:before]
1629
+
1630
+ c.action do |_global_options, options, args|
1631
+ if options[:section] && options[:section] !~ /^all$/i
1632
+ options[:section] = wwid.guess_section(options[:section])
1633
+ end
1634
+
1635
+ options[:bool] = case options[:bool]
1636
+ when /(and|all)/i
1637
+ 'AND'
1638
+ when /(any|or)/i
1639
+ 'OR'
1640
+ when /(not|none)/i
1641
+ 'NOT'
1642
+ else
1643
+ 'AND'
1644
+ end
1645
+
1646
+ wwid.rotate(options)
1647
+ end
1648
+ end
1649
+
1531
1650
  desc 'Open the "doing" file in an editor'
1532
1651
  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'})"
1533
1652
  command :open do |c|
1534
1653
  if `uname` =~ /Darwin/
1535
1654
  c.desc 'Open with app name'
1536
1655
  c.arg_name 'APP_NAME'
1537
- c.flag [:a]
1656
+ c.flag %i[a app]
1538
1657
 
1539
1658
  c.desc 'Open with app bundle id'
1540
1659
  c.arg_name 'BUNDLE_ID'
1541
- c.flag [:b]
1660
+ c.flag %i[b bundle_id]
1542
1661
  end
1543
1662
  c.desc "Open with $EDITOR (#{ENV['EDITOR']})"
1544
1663
  c.switch %i[e editor], negatable: false, default_value: false
@@ -1549,11 +1668,11 @@ command :open do |c|
1549
1668
  k.instance_of?(String) || v.nil? || v == false
1550
1669
  end
1551
1670
  if `uname` =~ /Darwin/
1552
- if options[:a]
1671
+ if options[:app]
1553
1672
  system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
1554
- elsif options[:b]
1673
+ elsif options[:bundle_id]
1555
1674
  system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
1556
- elsif options[:e]
1675
+ elsif options[:editor]
1557
1676
  exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
1558
1677
 
1559
1678
  system %($EDITOR "#{File.expand_path(wwid.doing_file)}")
@@ -1637,12 +1756,15 @@ command :import do |c|
1637
1756
 
1638
1757
  c.desc 'Target section'
1639
1758
  c.arg_name 'NAME'
1640
- c.flag %i[s section], default_value: wwid.current_section
1759
+ c.flag %i[s section]
1641
1760
 
1642
1761
  c.desc 'Tag all imported entries'
1643
1762
  c.arg_name 'TAGS'
1644
1763
  c.flag :tag
1645
1764
 
1765
+ c.desc 'Autotag entries'
1766
+ c.switch :autotag, negatable: true, default_value: true
1767
+
1646
1768
  c.desc 'Prefix entries with'
1647
1769
  c.arg_name 'PREFIX'
1648
1770
  c.flag :prefix
@@ -1652,11 +1774,22 @@ command :import do |c|
1652
1774
 
1653
1775
  c.action do |_global_options, options, args|
1654
1776
 
1655
- section = wwid.guess_section(options[:s]) || options[:s].cap_first
1777
+ if options[:section]
1778
+ section = wwid.guess_section(options[:section]) || options[:section].cap_first
1779
+ else
1780
+ section = wwid.config['current_section']
1781
+ end
1656
1782
 
1657
1783
  if options[:type] =~ /^tim/i
1658
1784
  args.each do |path|
1659
- wwid.import_timing(path, { section: section, tag: options[:tag], prefix: options[:prefix], no_overlap: !options[:overlap] })
1785
+ options = {
1786
+ autotag: options[:autotag],
1787
+ no_overlap: !options[:overlap],
1788
+ prefix: options[:prefix],
1789
+ section: section,
1790
+ tag: options[:tag]
1791
+ }
1792
+ wwid.import_timing(path, options)
1660
1793
  wwid.write(wwid.doing_file)
1661
1794
  end
1662
1795
  else
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.82'
2
+ VERSION = '1.0.86'
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)
@@ -123,7 +124,8 @@ class WWID
123
124
  @config['templates']['recent'] ||= {
124
125
  'date_format' => '%_I:%M%P',
125
126
  'template' => '%shortdate: %title (%section)',
126
- 'wrap_width' => 88
127
+ 'wrap_width' => 88,
128
+ 'count' => 10
127
129
  }
128
130
  @config['views'] ||= {
129
131
  'done' => {
@@ -424,8 +426,8 @@ class WWID
424
426
  ## @param guessed (Boolean) already guessed and failed
425
427
  ##
426
428
  def guess_section(frag, guessed: false)
427
- return 'All' if frag =~ /all/i
428
-
429
+ return 'All' if frag =~ /^all$/i
430
+ frag ||= @current_section
429
431
  sections.each { |section| return section.cap_first if frag.downcase == section.downcase }
430
432
  section = false
431
433
  re = frag.split('').join('.*?')
@@ -583,17 +585,17 @@ class WWID
583
585
  end
584
586
 
585
587
  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
588
+ item_a['date'] == item_b['date'] ? get_interval(item_a, formatted: false, record: false) == get_interval(item_b, formatted: false, record: false) : false
587
589
  end
588
590
 
589
591
  def overlapping_time?(item_a, item_b)
590
592
  return true if same_time?(item_a, item_b)
591
593
 
592
594
  start_a = item_a['date']
593
- interval = get_interval(item_a, false)
595
+ interval = get_interval(item_a, formatted: false, record: false)
594
596
  end_a = interval ? start_a + interval.to_i : start_a
595
597
  start_b = item_b['date']
596
- interval = get_interval(item_b, false)
598
+ interval = get_interval(item_b, formatted: false, record: false)
597
599
  end_b = interval ? start_b + interval.to_i : start_b
598
600
  (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
601
  end
@@ -626,6 +628,7 @@ class WWID
626
628
  def import_timing(path, opt = {})
627
629
  section = opt[:section] || @current_section
628
630
  opt[:no_overlap] ||= false
631
+ opt[:autotag] ||= @auto_tag
629
632
 
630
633
  add_section(section) unless @content.has_key?(section)
631
634
 
@@ -656,7 +659,7 @@ class WWID
656
659
  title += " @#{tag}"
657
660
  end
658
661
  end
659
- title = autotag(title) if @auto_tag
662
+ title = autotag(title) if opt[:autotag]
660
663
  title += " @done(#{end_time.strftime('%Y-%m-%d %H:%M')})"
661
664
  title.gsub!(/ +/, ' ')
662
665
  title.strip!
@@ -1098,7 +1101,6 @@ class WWID
1098
1101
  items = @content[section]['items'].dup.sort_by { |item| item['date'] }.reverse
1099
1102
  idx = 0
1100
1103
  done_date = Time.now
1101
- next_start = Time.now
1102
1104
  count = (opt[:count]).zero? ? items.length : opt[:count]
1103
1105
  items.map! do |item|
1104
1106
  break if idx == count
@@ -1117,8 +1119,8 @@ class WWID
1117
1119
  end
1118
1120
  else
1119
1121
  if opt[:sequential]
1120
- done_date = next_start - 1
1121
- next_start = item['date']
1122
+ next_entry = next_item(item)
1123
+ done_date = next_entry['date'] - 60 if next_entry
1122
1124
  elsif opt[:took]
1123
1125
  if item['date'] + opt[:took] > Time.now
1124
1126
  item['date'] = Time.now - opt[:took]
@@ -1207,6 +1209,24 @@ class WWID
1207
1209
  return new_item
1208
1210
  end
1209
1211
 
1212
+ ##
1213
+ ## @brief Get next item in the index
1214
+ ##
1215
+ ## @param old_item
1216
+ ##
1217
+ def next_item(old_item)
1218
+ section = old_item['section']
1219
+
1220
+ section_items = @content[section]['items'].sort_by { |entry| entry['date'] }
1221
+ idx = section_items.index(old_item)
1222
+
1223
+ if section_items.size > idx
1224
+ section_items[idx + 1]
1225
+ else
1226
+ nil
1227
+ end
1228
+ end
1229
+
1210
1230
  ##
1211
1231
  ## @brief Delete an item from the index
1212
1232
  ##
@@ -1476,7 +1496,7 @@ class WWID
1476
1496
  ##
1477
1497
  ## @param file (String) The filepath to write to
1478
1498
  ##
1479
- def write(file = nil)
1499
+ def write(file = nil, backup: true)
1480
1500
  output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
1481
1501
 
1482
1502
  @content.each do |title, section|
@@ -1488,7 +1508,7 @@ class WWID
1488
1508
  $stdout.puts output
1489
1509
  else
1490
1510
  file = File.expand_path(file)
1491
- if File.exist?(file)
1511
+ if File.exist?(file) && backup
1492
1512
  # Create a backup copy for the undo command
1493
1513
  FileUtils.cp(file, "#{file}~")
1494
1514
  end
@@ -1520,6 +1540,88 @@ class WWID
1520
1540
  end
1521
1541
  end
1522
1542
 
1543
+ ##
1544
+ ## @brief Rename doing file with date and start fresh one
1545
+ ##
1546
+ def rotate(opt = {})
1547
+ count = opt[:keep] || 0
1548
+ tags = []
1549
+ tags.concat(opt[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if opt[:tag]
1550
+ bool = opt[:bool] || :and
1551
+ sect = opt[:section] !~ /^all$/i ? guess_section(opt[:section]) : 'all'
1552
+
1553
+ if sect =~ /^all$/i
1554
+ all_sections = sections.dup
1555
+ else
1556
+ all_sections = [sect]
1557
+ end
1558
+
1559
+ counter = 0
1560
+ new_content = {}
1561
+
1562
+
1563
+ all_sections.each do |section|
1564
+ items = @content[section]['items'].dup
1565
+ new_content[section] = {}
1566
+ new_content[section]['original'] = @content[section]['original']
1567
+ new_content[section]['items'] = []
1568
+
1569
+ moved_items = []
1570
+ if !tags.empty? || opt[:search] || opt[:before]
1571
+ if opt[:before]
1572
+ time_string = opt[:before]
1573
+ time_string += ' 12am' if time_string !~ /(\d+:\d+|\d+[ap])/
1574
+ cutoff = chronify(time_string)
1575
+ end
1576
+
1577
+ items.delete_if do |item|
1578
+ if ((!tags.empty? && item.has_tags?(tags, bool)) || (opt[:search] && item.matches_search?(opt[:search].to_s)) || (opt[:before] && item['date'] < cutoff))
1579
+ moved_items.push(item)
1580
+ counter += 1
1581
+ true
1582
+ else
1583
+ false
1584
+ end
1585
+ end
1586
+ @content[section]['items'] = items
1587
+ new_content[section]['items'] = moved_items
1588
+ @results.push("Rotated #{moved_items.length} items from #{section}")
1589
+ else
1590
+ new_content[section]['items'] = []
1591
+ moved_items = []
1592
+
1593
+ count = items.length if items.length < count
1594
+
1595
+ if items.count > count
1596
+ moved_items.concat(items[count..-1])
1597
+ else
1598
+ moved_items.concat(items)
1599
+ end
1600
+
1601
+ @content[section]['items'] = if count.zero?
1602
+ []
1603
+ else
1604
+ items[0..count - 1]
1605
+ end
1606
+ new_content[section]['items'] = moved_items
1607
+
1608
+ @results.push("Rotated #{items.length - count} items from #{section}")
1609
+ end
1610
+ end
1611
+
1612
+ write(@doing_file)
1613
+
1614
+ file = @doing_file.sub(/(\.\w+)$/, "_#{Time.now.strftime('%Y-%m-%d')}\\1")
1615
+ if File.exist?(file)
1616
+ init_doing_file(file)
1617
+ @content.deep_merge(new_content)
1618
+ else
1619
+ @content = new_content
1620
+ end
1621
+
1622
+ write(file, backup: false)
1623
+ end
1624
+
1523
1625
  ##
1524
1626
  ## @brief Generate a menu of sections and allow user selection
1525
1627
  ##
@@ -1569,20 +1671,21 @@ class WWID
1569
1671
  def list_section(opt = {})
1570
1672
  opt[:count] ||= 0
1571
1673
  count = opt[:count] - 1
1572
- opt[:section] ||= nil
1573
- opt[:format] ||= @default_date_format
1574
- opt[:template] ||= @default_template
1575
1674
  opt[:age] ||= 'newest'
1675
+ opt[:date_filter] ||= []
1676
+ opt[:format] ||= @default_date_format
1677
+ opt[:only_timed] ||= false
1576
1678
  opt[:order] ||= 'desc'
1577
- opt[:today] ||= false
1679
+ opt[:search] ||= false
1680
+ opt[:section] ||= nil
1681
+ opt[:sort_tags] ||= false
1578
1682
  opt[:tag_filter] ||= false
1683
+ opt[:tag_order] ||= 'asc'
1579
1684
  opt[:tags_color] ||= false
1685
+ opt[:template] ||= @default_template
1580
1686
  opt[:times] ||= false
1687
+ opt[:today] ||= false
1581
1688
  opt[:totals] ||= false
1582
- opt[:sort_tags] ||= false
1583
- opt[:search] ||= false
1584
- opt[:only_timed] ||= false
1585
- opt[:date_filter] ||= []
1586
1689
 
1587
1690
  # opt[:highlight] ||= true
1588
1691
  section = ''
@@ -1635,7 +1738,7 @@ class WWID
1635
1738
 
1636
1739
  if opt[:only_timed]
1637
1740
  items.delete_if do |item|
1638
- get_interval(item) == false
1741
+ get_interval(item, record: false) == false
1639
1742
  end
1640
1743
  end
1641
1744
 
@@ -1670,7 +1773,7 @@ class WWID
1670
1773
  arr = i['note'].map { |line| line.strip }.delete_if { |e| e =~ /^\s*$/ }
1671
1774
  note = arr.join("\n") unless arr.nil?
1672
1775
  end
1673
- interval = get_interval(i, false) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1776
+ interval = get_interval(i, formatted: false) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1674
1777
  interval ||= 0
1675
1778
  output.push(CSV.generate_line([i['date'], i['title'], note, interval, i['section']]))
1676
1779
  end
@@ -1689,7 +1792,7 @@ class WWID
1689
1792
  end
1690
1793
  if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1691
1794
  end_date = Time.parse(Regexp.last_match(1))
1692
- interval = get_interval(i, false)
1795
+ interval = get_interval(i, formatted: false)
1693
1796
  end
1694
1797
  end_date ||= ''
1695
1798
  interval ||= 0
@@ -1738,7 +1841,7 @@ class WWID
1738
1841
  out = {
1739
1842
  'section' => section,
1740
1843
  'items' => items_out,
1741
- 'timers' => tag_times('json', opt[:sort_tags])
1844
+ 'timers' => tag_times(format: 'json', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order])
1742
1845
  }.to_json
1743
1846
  elsif opt[:output] == 'timeline'
1744
1847
  template = <<~EOTEMPLATE
@@ -1819,7 +1922,7 @@ class WWID
1819
1922
  css_template
1820
1923
  end
1821
1924
 
1822
- totals = opt[:totals] ? tag_times('html', opt[:sort_tags]) : ''
1925
+ totals = opt[:totals] ? tag_times(format: 'html', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
1823
1926
  engine = Haml::Engine.new(template)
1824
1927
  out = engine.render(Object.new,
1825
1928
  { :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
@@ -1860,7 +1963,7 @@ class WWID
1860
1963
 
1861
1964
  output.sub!(/%date/, item['date'].strftime(opt[:format]))
1862
1965
 
1863
- interval = get_interval(item) if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1966
+ interval = get_interval(item, record: true) if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
1864
1967
  interval ||= ''
1865
1968
  output.sub!(/%interval/, interval)
1866
1969
 
@@ -1910,7 +2013,8 @@ class WWID
1910
2013
 
1911
2014
  out += "#{output}\n"
1912
2015
  end
1913
- out += tag_times('text', opt[:sort_tags]) if opt[:totals]
2016
+
2017
+ out += tag_times(format: 'text', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
1914
2018
  end
1915
2019
  out
1916
2020
  end
@@ -1922,7 +2026,7 @@ class WWID
1922
2026
  ## @param section (String) The source section
1923
2027
  ## @param options (Hash) Options
1924
2028
  ##
1925
- def archive(section = 'Currently', options = {})
2029
+ def archive(section = @current_section, options = {})
1926
2030
  count = options[:keep] || 0
1927
2031
  destination = options[:destination] || 'Archive'
1928
2032
  tags = options[:tags] || []
@@ -1937,7 +2041,7 @@ class WWID
1937
2041
  destination = guess_section(destination)
1938
2042
 
1939
2043
  if sections.include?(destination) && (sections.include?(section) || archive_all)
1940
- do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label] })
2044
+ do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
1941
2045
  write(doing_file)
1942
2046
  else
1943
2047
  exit_now! 'Either source or destination does not exist'
@@ -1970,9 +2074,15 @@ class WWID
1970
2074
  items = @content[section]['items'].dup
1971
2075
 
1972
2076
  moved_items = []
1973
- if !tags.empty? || opt[:search]
2077
+ if !tags.empty? || opt[:search] || opt[:before]
2078
+ if opt[:before]
2079
+ time_string = opt[:before]
2080
+ time_string += ' 12am' if time_string !~ /(\d+:\d+|\d+[ap])/
2081
+ cutoff = chronify(time_string)
2082
+ end
2083
+
1974
2084
  items.delete_if do |item|
1975
- if (!tags.empty? && item.has_tags?(tags, bool) || (opt[:search] && item.matches_search?(opt[:search].to_s)))
2085
+ if ((!tags.empty? && item.has_tags?(tags, bool)) || (opt[:search] && item.matches_search?(opt[:search].to_s)) || (opt[:before] && item['date'] < cutoff))
1976
2086
  moved_items.push(item)
1977
2087
  counter += 1
1978
2088
  true
@@ -1981,7 +2091,7 @@ class WWID
1981
2091
  end
1982
2092
  end
1983
2093
  moved_items.each do |item|
1984
- if label && section != 'Currently'
2094
+ if label && section != @current_section
1985
2095
  item['title'] =
1986
2096
  item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
1987
2097
  end
@@ -1994,7 +2104,7 @@ class WWID
1994
2104
  count = items.length if items.length < count
1995
2105
 
1996
2106
  items.map! do |item|
1997
- if label && section != 'Currently'
2107
+ if label && section != @current_section
1998
2108
  item['title'] =
1999
2109
  item['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
2000
2110
  end
@@ -2173,11 +2283,15 @@ class WWID
2173
2283
  end
2174
2284
 
2175
2285
  ##
2176
- ## @brief Get total elapsed time for all tags in selection
2286
+ ## @brief Get total elapsed time for all tags in
2287
+ ## selection
2177
2288
  ##
2178
- ## @param format (String) return format (html, json, or text)
2289
+ ## @param format (String) return format (html,
2290
+ ## json, or text)
2291
+ ## @param sort_by_name (Boolean) Sort by name if true, otherwise by time
2292
+ ## @param sort_order (String) The sort order (asc or desc)
2179
2293
  ##
2180
- def tag_times(format = 'text', sort_by_name = false)
2294
+ def tag_times(format: 'text', sort_by_name: false, sort_order: 'asc')
2181
2295
  return '' if @timers.empty?
2182
2296
 
2183
2297
  max = @timers.keys.sort_by { |k| k.length }.reverse[0].length + 1
@@ -2186,11 +2300,13 @@ class WWID
2186
2300
 
2187
2301
  tags_data = @timers.delete_if { |_k, v| v == 0 }
2188
2302
  sorted_tags_data = if sort_by_name
2189
- tags_data.sort_by { |k, _v| k }.reverse
2303
+ tags_data.sort_by { |k, _v| k }
2190
2304
  else
2191
2305
  tags_data.sort_by { |_k, v| v }
2192
2306
  end
2193
2307
 
2308
+ sorted_tags_data.reverse! if sort_order =~ /^asc/i
2309
+
2194
2310
  if format == 'html'
2195
2311
  output = <<EOS
2196
2312
  <table>
@@ -2324,19 +2440,20 @@ EOS
2324
2440
  ## @param item (Hash) The entry
2325
2441
  ## @param formatted (Bool) Return human readable time (default seconds)
2326
2442
  ##
2327
- def get_interval(item, formatted = true)
2443
+ def get_interval(item, formatted: true, record: true)
2328
2444
  done = nil
2329
2445
  start = nil
2330
2446
 
2331
2447
  if @interval_cache.keys.include? item['title']
2332
2448
  seconds = @interval_cache[item['title']]
2449
+ record_tag_times(item, seconds) if record
2333
2450
  return seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
2334
2451
  end
2335
2452
 
2336
2453
  if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
2337
2454
  done = Time.parse(Regexp.last_match(1))
2338
2455
  else
2339
- return nil
2456
+ return false
2340
2457
  end
2341
2458
 
2342
2459
  start = if item['title'] =~ /@start\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
@@ -2347,20 +2464,34 @@ EOS
2347
2464
 
2348
2465
  seconds = (done - start).to_i
2349
2466
 
2467
+ if record
2468
+ record_tag_times(item, seconds)
2469
+ end
2470
+
2471
+ @interval_cache[item['title']] = seconds
2472
+
2473
+ return seconds > 0 ? seconds : false unless formatted
2474
+
2475
+ seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
2476
+ end
2477
+
2478
+ ##
2479
+ ## @brief Record times for item tags
2480
+ ##
2481
+ ## @param item The item
2482
+ ##
2483
+ def record_tag_times(item, seconds)
2484
+ return if @recorded_items.include?(item)
2485
+
2350
2486
  item['title'].scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
2351
2487
  k = m[0] == 'done' ? 'All' : m[0].downcase
2352
- if @timers.has_key?(k)
2488
+ if @timers.key?(k)
2353
2489
  @timers[k] += seconds
2354
2490
  else
2355
2491
  @timers[k] = seconds
2356
2492
  end
2493
+ @recorded_items.push(item)
2357
2494
  end
2358
-
2359
- @interval_cache[item['title']] = seconds
2360
-
2361
- return seconds unless formatted
2362
-
2363
- seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
2364
2495
  end
2365
2496
 
2366
2497
  ##
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.82
4
+ version: 1.0.86
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-26 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