doing 1.0.87 → 1.0.91

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: 2540726a17efa50ef0cddc29353d8c14a4d921a89fb302711225053faacfbc4d
4
- data.tar.gz: e4f59cf1e2feda50df653bbbd981e1ab4ad75d807eaf47a8afbad9d98032d27b
3
+ metadata.gz: 365e58ed7377d560d531ce2a1585d115c0a1b73fc3039621a2fef230f52de5dc
4
+ data.tar.gz: 0efbf78887502113048946df9942e58ad751921b4fc997efd7556ba40e428bb5
5
5
  SHA512:
6
- metadata.gz: dabcbefd24a3d099d71eb14c03f71250067e509b385d573405219d3cfaea8a66f6ec526a32547401f249f0fdd82774415ed0da1c95e233aada2f54135de49558
7
- data.tar.gz: 5b5a5d7800110de9aeaf636b6b6b4947c1923e55ce4f455d2316483c37fcfb30461b818df78c24097ef66f574ed57bdeebf50f77e4f806161212c4d36c86eabd
6
+ metadata.gz: 53fba5968f465d3dc40821360d60f2ff63573b8e5b47e9eb851fbf6592a3a7baea0436efb118d08d4f92e10f035a5f9ee5fa47ae5df65aa6929c40146a0a6f5f
7
+ data.tar.gz: 6a98c715220355434ed2e05e4582d04ddd7eade5f4588ea4e881a025654ac5e0d90df1f845f73406a50bda14d887540f34254ffe8de50252840a2c070cbb5c75
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **A command line tool for remembering what you were doing and tracking what you've done.**
4
4
 
5
- _If you're one of the rare people like me who find this useful, feel free to [buy me some coffee](http://brettterpstra.com/donate/)._
5
+ _If you're one of the rarev people like me who find this useful, feel free to [buy me some coffee](http://brettterpstra.com/donate/)._
6
6
 
7
7
  ## Contents
8
8
 
@@ -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.86<!--END VER-->.
30
+ The current version of `doing` is <!--VER-->1.0.90<!--END VER-->.
31
31
 
32
32
  $ [sudo] gem install doing
33
33
 
@@ -104,6 +104,7 @@ A basic configuration looks like this:
104
104
  html_template:
105
105
  haml:
106
106
  css:
107
+ markdown:
107
108
 
108
109
 
109
110
  The config file is stored in `~/.doingrc`, and a skeleton file is created on the first run. Just run `doing` on its own to create the file.
@@ -114,7 +115,7 @@ Any options found in a `.doingrc` anywhere in the hierarchy between your current
114
115
 
115
116
  Possible uses:
116
117
 
117
- - Define custom HTML output on a per-project basis using the html_template option for custom templates. Customize time tracking reports based on project or client.
118
+ - Define custom HTML or Markdown output on a per-project basis using the html_template option for custom templates. Customize time tracking reports based on project or client.
118
119
  - Define `default_tags` for a project so that every time you `doing now` from within that project directory or its subfolders, it gets tagged with that project automatically.
119
120
 
120
121
  Any part of the configuration can be copied into these local files and modified. You only need to include the parts you want to change or add.
@@ -191,7 +192,7 @@ And it outputs:
191
192
  12:45pm > I think this thing (doing) is ready to document and distribute
192
193
  $
193
194
 
194
- You can get pretty clever and include line breaks and other formatting inside of double quotes. If you want multiline templates, just use `\n` in the template line, and after the next run it will be rewritten as proper YAML automatically.
195
+ You can get pretty clever and include line breaks and other formatting inside of double quotes. If you want multi-line templates, just use `\n` in the template line, and after the next run it will be rewritten as proper YAML automatically.
195
196
 
196
197
  For example, this block:
197
198
 
@@ -300,7 +301,7 @@ Outputs:
300
301
 
301
302
  ![](http://ckyp.us/XKpj+)
302
303
 
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.
304
+ You can also specify a default output format for a view. Most of the optional output formats override the template specification (`html`, `csv`, `json`, `markdown`). If the `view` command is used with the `-o` flag, it will override what's specified for the view in the config.
304
305
 
305
306
  ### Colors
306
307
 
@@ -342,13 +343,13 @@ And a few special colors you'll just have to try out to see (or just run `doing
342
343
 
343
344
  Any time you use one of the foreground colors it will reset the bold and background settings to their default automatically. You can force a reset to default terminal colors using `%default`.
344
345
 
345
- ### HTML Templates
346
+ ### HTML/Markdown Templates
346
347
 
347
- For commands that provide an HTML output option, you can customize the templates used for markup and CSS. The markup uses [HAML](http://haml.info/), and the styles are pure CSS.
348
+ For commands that provide an HTML or Markdown output option, you can customize the templates used for markup and CSS. The HTML markup uses [HAML](http://haml.info/), and the styles are pure CSS. The Markdown template uses Ruby ERB templating.
348
349
 
349
- To export the default configurations for customization, use `doing templates --type=[HAML|CSS]`. This will output to STDOUT where you can pipe it to a file, e.g. `doing templates --type=HAML > my_template.haml`. You can modify the markup, the CSS, or both.
350
+ To export the default configurations for customization, use `doing templates --type=[HAML|CSS|MARKDOWN]`. This will output to STDOUT where you can pipe it to a file, e.g. `doing templates --type=HAML > my_template.haml`. You can modify the markup, the CSS, or both.
350
351
 
351
- Once you have either or both of the template files, edit `.doingrc` and look for the `html_template:` section. There are two subvalues, `haml:` and `css:`. Add the path to the templates you want to use. A tilde may be substituted for your home directory, e.g. `css: ~/styles/doing.css`.
352
+ Once you have a template file prepared, edit `.doingrc` and look for the `html_template:` section. There are three sub-values, `haml:`, `css:`, and `markdown:`. Add the template paths to the appropriate keys. A tilde may be substituted for your home directory, e.g. `css: ~/styles/doing.css`.
352
353
 
353
354
  ### Autotagging
354
355
 
@@ -545,9 +546,11 @@ If you have a use for it, you can use `-o csv` on the show or view commands to o
545
546
 
546
547
  `doing yesterday` is great for stand-ups (thanks to [Sean Collins](https://github.com/sc68cal) for that!). Note that you can show yesterday's activity from an alternate section by using the section name as an argument (e.g. `doing yesterday archive`).
547
548
 
549
+ All of the display commands (view, show, today, yesterday, search) support date ranges in addition to other filtering options. Results can be narrowed to a date/time span using `--before DATE` and `--after DATE`. The `DATE` accepts a natural language string but works best in formats like `10/20/21` or `2021-05-13 3pm` (or shortened to just `5/13 3pm`). For the `today` and `yesterday` commands the `DATE` should just be a time, e.g. `12pm` or `15:00`, and results will be filtered within the timespan for just that day. Both flags are optional, and you can use just `--before` to get everything older than a certain date, or use just `--after` to get everything newer.
550
+
548
551
  `doing on` allows for full date ranges and filtering. `doing on saturday`, or `doing on one month to today` will give you ranges. You can use the same terms with the `show` command by adding the `-f` or `--from` flag. `doing show @done --from "monday to friday"` will give you all of your completed items for the last week (assuming it's the weekend). There's also `doing since` a simple alias for `doing on PAST_DATE to now`, e.g. `doing since monday`.
549
552
 
550
- You can also show entries matching a search string with `doing grep` (synonym `doing search`). If you want to search with regular expressions or for an exact match, surround your search query with forward slashes, e.g. `doing search /project name/`. If you pass a search string without slashes, it's treated as a fuzzy search string, meaning matches can be found as long as the characters in the search string are in order and with no more than three other characters between each. By default searches are across all sections, but you can limit it to one with the `-s SECTION_NAME` flag. Searches can be displayed with the default template, or output as HTML, CSV, or JSON.
553
+ You can also show entries matching a search string with `doing grep` (synonym `doing search`). If you want to search with regular expressions or for an exact match, surround your search query with forward slashes, e.g. `doing search /project name/`. If you pass a search string without slashes, it's treated as a fuzzy search string, meaning matches can be found as long as the characters in the search string are in order and with no more than three other characters between each. By default searches are across all sections, but you can limit it to one with the `-s SECTION_NAME` flag. Searches can be displayed with the default template, or output as HTML, Markdown, CSV, or JSON.
551
554
 
552
555
  ##### Modifying the last entry
553
556
 
@@ -616,7 +619,7 @@ Doing can currently only import tasks from Timing.app reports. If you want to sy
616
619
  7. Include short entries if desired
617
620
  8. Export the report to a new file
618
621
 
619
- Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is the name of the section you want to import the entries to (defauts to Currently), and PATH is the path to the JSON file. You can also add a tag (or tags) to all entries, or a custom prefix.
622
+ Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is the name of the section you want to import the entries to (defaults to Currently), and PATH is the path to the JSON file. You can also add a tag (or tags) to all entries, or a custom prefix.
620
623
 
621
624
  (`--type timing` is the only option right now, so it doesn't need to be included)
622
625
 
@@ -716,7 +719,7 @@ Ruby is rife with encoding inconsistencies across platforms and versions. Feel f
716
719
  ### Support
717
720
 
718
721
 
719
- As a free project, `doing` isn't heavily supported, but you can get support from myself and other users on GitHub. If you run into a replicatable bug in your environment, please [post an issue](https://github.com/ttscoff/doing/issues) and include your platform, OS version, and the result of `ruby -v`, along with a copy/paste of the error message. To get a more verbose error message, try running `GLI_DEBUG=true doing [...]` for a full trace.
722
+ As a free project, `doing` isn't heavily supported, but you can get support from myself and other users on GitHub. If you run into a replicable bug in your environment, please [post an issue](https://github.com/ttscoff/doing/issues) and include your platform, OS version, and the result of `ruby -v`, along with a copy/paste of the error message. To get a more verbose error message, try running `GLI_DEBUG=true doing [...]` for a full trace.
720
723
 
721
724
  Please try not to email me directly about GitHub projects.
722
725
 
data/bin/doing CHANGED
@@ -243,23 +243,25 @@ command :meanwhile do |c|
243
243
  end
244
244
  end
245
245
 
246
- desc 'Output HTML and CSS templates for customization'
246
+ desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
247
247
  long_desc %(
248
248
  Templates are printed to STDOUT for piping to a file.
249
249
  Save them and use them in the configuration file under html_template.
250
250
 
251
251
  Example `doing template HAML > ~/styles/my_doing.haml`
252
252
  )
253
- arg_name 'TYPE', must_match: /^(?:html|haml|css)/i
253
+ arg_name 'TYPE', must_match: /^(?:html|haml|css|markdown|md|erb)/i
254
254
  command :template do |c|
255
255
  c.action do |_global_options, options, args|
256
- exit_now! 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
256
+ exit_now! 'No type specified, use `doing template [HAML|CSS|MARKDOWN]`' if args.empty?
257
257
 
258
258
  case args[0]
259
259
  when /html|haml/i
260
260
  $stdout.puts wwid.haml_template
261
261
  when /css/i
262
262
  $stdout.puts wwid.css_template
263
+ when /markdown|md|erb/i
264
+ $stdout.puts wwid.markdown_template
263
265
  else
264
266
  exit_now! 'Invalid type specified, must be HAML or CSS'
265
267
  end
@@ -292,10 +294,13 @@ command :select do |c|
292
294
  c.arg_name 'SECTION'
293
295
  c.flag %i[m move]
294
296
 
295
- c.desc 'Initial search query for filtering'
297
+ c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
296
298
  c.arg_name 'QUERY'
297
299
  c.flag %i[q query]
298
300
 
301
+ c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
302
+ c.switch %i[menu], negatable: true, default_value: true
303
+
299
304
  c.desc 'Cancel selected items (add @done without timestamp)'
300
305
  c.switch %i[c cancel], negatable: false, default_value: false
301
306
 
@@ -311,18 +316,20 @@ command :select do |c|
311
316
  c.desc 'Add flag to selected item(s)'
312
317
  c.switch %i[flag], negatable: false, default_value: false
313
318
 
314
- c.desc 'Perform action without confirmation'
319
+ c.desc 'Perform action without confirmation.'
315
320
  c.switch %i[force], negatable: false, default_value: false
316
321
 
317
322
  c.desc 'Save selected entries to file using --output format'
318
323
  c.arg_name 'FILE'
319
324
  c.flag %i[save_to]
320
325
 
321
- c.desc 'Output entries to format (doing|taskpaper|csv|html|json|template|timeline)'
326
+ c.desc 'Output entries to format (doing|taskpaper|csv|html|json|template|timeline|taskpaper|markdown)'
322
327
  c.arg_name 'FORMAT'
323
- c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i
328
+ c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline|taskpaper|markdown|md)$/i
324
329
 
325
330
  c.action do |_global_options, options, args|
331
+ exit_now! "--no-menu requires --query" if !options[:menu] && !options[:query]
332
+
326
333
  wwid.interactive(options)
327
334
  end
328
335
  end
@@ -599,7 +606,7 @@ command :finish do |c|
599
606
  c.switch %i[u unfinished], negatable: false, default_value: false
600
607
 
601
608
  c.desc %(Auto-generate finish dates from next entry's start time.
602
- Automatically generate completion dates 1 minute before next start date.
609
+ Automatically generate completion dates 1 minute before next item (in any section) began.
603
610
  --auto overrides the --date and --back parameters.)
604
611
  c.switch [:auto], negatable: false, default_value: false
605
612
 
@@ -733,6 +740,19 @@ command [:again, :resume] do |c|
733
740
  end
734
741
 
735
742
  desc 'Add tag(s) to last entry'
743
+ long_desc 'Add (or remove) tags from the last entry, or from multiple entries
744
+ (with `--count`), entries matching a search (with `--search), or entries
745
+ containing another tag (with `--tag`).
746
+
747
+ When removing tags with `-r`, wildcards are allowed (`*` to match
748
+ multiple characters, `?` to match a single character). With `--regex`,
749
+ regular expressions will be interpreted instead of wildcards.
750
+
751
+ For all tag removals the match is case insensitive by default, but if
752
+ the tag search string contains any uppercase letters, the match will
753
+ become case sensitive automatically.
754
+
755
+ Tag name arguments do not need to be prefixed with @.'
736
756
  arg_name 'TAG', :multiple
737
757
  command :tag do |c|
738
758
  c.desc 'Section'
@@ -743,6 +763,10 @@ command :tag do |c|
743
763
  c.arg_name 'COUNT'
744
764
  c.flag %i[c count], default_value: 1
745
765
 
766
+ c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
767
+ c.arg_name 'ORIG_TAG'
768
+ c.flag %i[rename]
769
+
746
770
  c.desc 'Don\'t ask permission to tag all entries when count is 0'
747
771
  c.switch %i[force], negatable: false, default_value: false
748
772
 
@@ -752,6 +776,9 @@ command :tag do |c|
752
776
  c.desc 'Remove given tag(s)'
753
777
  c.switch %i[r remove], negatable: false, default_value: false
754
778
 
779
+ c.desc 'Interpret tag string as regular expression (with --remove)'
780
+ c.switch %i[regex], negatable: false, default_value: false
781
+
755
782
  c.desc 'Tag last entry (or entries) not marked @done'
756
783
  c.switch %i[u unfinished], negatable: false, default_value: false
757
784
 
@@ -838,19 +865,26 @@ command :tag do |c|
838
865
  exit_now! 'Cancelled' unless res
839
866
  end
840
867
 
841
- opts = {
842
- autotag: options[:a],
843
- count: count,
844
- date: options[:date],
845
- remove: options[:r],
846
- search: options[:search],
847
- section: section,
848
- tag: search_tags,
849
- tag_bool: options[:bool],
850
- tags: tags,
851
- unfinished: options[:unfinished]
852
- }
853
- wwid.tag_last(opts)
868
+ options[:count] = count
869
+ options[:section] = section
870
+ options[:tag] = search_tags
871
+ options[:tags] = tags
872
+ options[:tag_bool] = options[:bool]
873
+
874
+ # opts = {
875
+ # autotag: options[:a],
876
+ # count: count,
877
+ # date: options[:date],
878
+ # iregex: options[:iregex]
879
+ # remove: options[:r],
880
+ # search: options[:search],
881
+ # section: section,
882
+ # tag: search_tags,
883
+ # tag_bool: options[:bool],
884
+ # tags: tags,
885
+ # unfinished: options[:unfinished]
886
+ # }
887
+ wwid.tag_last(options)
854
888
  end
855
889
  end
856
890
 
@@ -943,9 +977,9 @@ command :show do |c|
943
977
  c.desc 'Only show items with recorded time intervals'
944
978
  c.switch [:only_timed], default_value: false, negatable: false
945
979
 
946
- c.desc 'Output to export format (csv|html|json|template|timeline)'
980
+ c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
947
981
  c.arg_name 'FORMAT'
948
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
982
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
949
983
  c.action do |_global_options, options, args|
950
984
  tag_filter = false
951
985
  tags = []
@@ -1063,9 +1097,9 @@ command [:grep, :search] do |c|
1063
1097
  c.arg_name 'DATE_STRING'
1064
1098
  c.flag [:after]
1065
1099
 
1066
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1100
+ c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1067
1101
  c.arg_name 'FORMAT'
1068
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1102
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1069
1103
 
1070
1104
  c.desc 'Show time intervals on @done tasks'
1071
1105
  c.switch %i[t times], default_value: true, negatable: true
@@ -1173,9 +1207,9 @@ command :today do |c|
1173
1207
  c.arg_name 'KEY'
1174
1208
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1175
1209
 
1176
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1210
+ c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1177
1211
  c.arg_name 'FORMAT'
1178
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1212
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1179
1213
 
1180
1214
  c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
1181
1215
  c.arg_name 'TIME_STRING'
@@ -1221,9 +1255,9 @@ command :on do |c|
1221
1255
  c.arg_name 'KEY'
1222
1256
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1223
1257
 
1224
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1258
+ c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1225
1259
  c.arg_name 'FORMAT'
1226
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1260
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1227
1261
 
1228
1262
  c.action do |_global_options, options, args|
1229
1263
  exit_now! 'Missing date argument' if args.empty?
@@ -1274,9 +1308,9 @@ command :since do |c|
1274
1308
  c.arg_name 'KEY'
1275
1309
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1276
1310
 
1277
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1311
+ c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1278
1312
  c.arg_name 'FORMAT'
1279
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1313
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1280
1314
 
1281
1315
  c.action do |_global_options, options, args|
1282
1316
  exit_now! 'Missing date argument' if args.empty?
@@ -1308,9 +1342,9 @@ command :yesterday do |c|
1308
1342
  c.arg_name 'NAME'
1309
1343
  c.flag %i[s section], default_value: 'All'
1310
1344
 
1311
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1345
+ c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1312
1346
  c.arg_name 'FORMAT'
1313
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1347
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1314
1348
 
1315
1349
  c.desc 'Show time intervals on @done tasks'
1316
1350
  c.switch %i[t times], default_value: true, negatable: true
@@ -1324,10 +1358,33 @@ command :yesterday do |c|
1324
1358
  c.arg_name 'KEY'
1325
1359
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1326
1360
 
1361
+ c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
1362
+ c.arg_name 'TIME_STRING'
1363
+ c.flag [:before]
1364
+
1365
+ c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)'
1366
+ c.arg_name 'TIME_STRING'
1367
+ c.flag [:after]
1368
+
1369
+ c.desc 'Tag sort direction (asc|desc)'
1370
+ c.arg_name 'DIRECTION'
1371
+ c.flag [:tag_order], must_match: /^(?:a(?:sc)?|d(?:esc)?)$/i
1372
+
1327
1373
  c.action do |_global_options, options, _args|
1374
+ tag_order = if options[:tag_order]
1375
+ options[:tag_order] =~ /^d/i ? 'desc' : 'asc'
1376
+ else
1377
+ 'asc'
1378
+ end
1328
1379
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1329
- puts wwid.yesterday(options[:s], options[:t], options[:o],
1330
- { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
1380
+ opt = {
1381
+ after: options[:after],
1382
+ before: options[:before],
1383
+ sort_tags: options[:sort_tags],
1384
+ tag_order: options[:tag_order],
1385
+ totals: options[:totals]
1386
+ }
1387
+ puts wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
1331
1388
  end
1332
1389
  end
1333
1390
 
@@ -1440,9 +1497,9 @@ command :view do |c|
1440
1497
  c.arg_name 'COUNT'
1441
1498
  c.flag %i[c count], must_match: /^\d+$/, type: Integer
1442
1499
 
1443
- c.desc 'Output to export format (csv|html|json|template|timeline)'
1500
+ c.desc 'Output to export format (csv|html|json|template|timeline|taskpaper|markdown)'
1444
1501
  c.arg_name 'FORMAT'
1445
- c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1502
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline|taskpaper|markdown|md)$/i
1446
1503
 
1447
1504
  c.desc 'Show time intervals on @done tasks'
1448
1505
  c.switch %i[t times], default_value: true, negatable: true
@@ -1904,6 +1961,7 @@ end
1904
1961
  post do |global, _command, _options, _args|
1905
1962
  # Use skips_post before a command to skip this
1906
1963
  # block on that command only
1964
+
1907
1965
  if global[:stdout]
1908
1966
  $stdout.print wwid.results.join("\n")
1909
1967
  else
data/lib/doing/helpers.rb CHANGED
@@ -1,3 +1,20 @@
1
+ ##
2
+ ## @brief Date helpers
3
+ ##
4
+ class ::Time
5
+ def relative_date
6
+ if self > Date.today.to_time
7
+ strftime(' %_I:%M%P')
8
+ elsif self > (Date.today - 6).to_time
9
+ strftime('%a %_I:%M%P')
10
+ elsif self.year == Date.today.year
11
+ strftime('%m/%d %_I:%M%P')
12
+ else
13
+ strftime('%m/%d/%Y %_I:%M%P')
14
+ end
15
+ end
16
+ end
17
+
1
18
  ##
2
19
  ## @brief Hash helpers
3
20
  ##
@@ -84,29 +101,82 @@ class ::String
84
101
  end
85
102
 
86
103
 
104
+ ##
105
+ ## @brief Remove duplicate tags, leaving only first occurrence
106
+ ##
107
+ ## @return Deduplicated string
108
+ ##
109
+ def dedup_tags!
110
+ replace dedup_tags
111
+ end
112
+
113
+ def dedup_tags
114
+ item = self.dup
115
+ tags = item.scan(/(?<=^| )(\@(\S+?)(\([^)]+\))?)(?=\b|$)/).uniq
116
+ tags.each do |tag|
117
+ found = false
118
+ item.gsub!(/( |^)#{tag[0]}\b/) do |m|
119
+ if found
120
+ ''
121
+ else
122
+ found = true
123
+ m
124
+ end
125
+ end
126
+ end
127
+ item
128
+ end
129
+
87
130
  ##
88
131
  ## @brief Turn raw urls into HTML links
89
132
  ##
90
133
  ## @param opt (Hash) Additional Options
91
134
  ##
135
+ def link_urls!(opt = {})
136
+ replace link_urls(opt)
137
+ end
138
+
92
139
  def link_urls(opt = {})
93
140
  opt[:format] ||= :html
94
- if opt[:format] == :html
95
- gsub(%r{(?mi)((http|https)://)([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
96
- m = Regexp.last_match
97
- proto = m[1].nil? ? 'http://' : ''
98
- %(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
99
- end.gsub(/<(\w+:.*?)>/) do |match|
141
+ str = self.dup
142
+
143
+ if :format == :markdown
144
+ # Remove <self-linked> formatting
145
+ str.gsub!(/<(.*?)>/) do |match|
100
146
  m = Regexp.last_match
101
- if m[1] =~ /<a href/
102
- match
147
+ if m[1] =~ /^https?:/
148
+ m[1]
103
149
  else
104
- %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
150
+ match
105
151
  end
106
152
  end
107
- else
108
- self
109
153
  end
154
+
155
+ # Replace qualified urls
156
+ str.gsub!(%r{(?mi)(?<!["'\[(\\])((http|https)://)([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
157
+ m = Regexp.last_match
158
+ proto = m[1].nil? ? 'http://' : ''
159
+ case opt[:format]
160
+ when :html
161
+ %(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
162
+ when :markdown
163
+ "[#{m[0]}](#{proto}#{m[0]})"
164
+ else
165
+ m[0]
166
+ end
167
+ end
168
+
169
+ # Clean up unlinked <urls>
170
+ str.gsub!(/<(\w+:.*?)>/) do |match|
171
+ m = Regexp.last_match
172
+ if m[1] =~ /<a href/
173
+ match
174
+ else
175
+ %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
176
+ end
177
+ end
178
+
179
+ str
110
180
  end
111
181
  end
112
182
 
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.87'
2
+ VERSION = '1.0.91'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -4,6 +4,7 @@ require 'deep_merge'
4
4
  require 'open3'
5
5
  require 'pp'
6
6
  require 'shellwords'
7
+ require 'erb'
7
8
 
8
9
  ##
9
10
  ## @brief Main "What Was I Doing" methods
@@ -104,6 +105,7 @@ class WWID
104
105
  @config['html_template'] ||= {}
105
106
  @config['html_template']['haml'] ||= nil
106
107
  @config['html_template']['css'] ||= nil
108
+ @config['html_template']['markdown'] ||= nil
107
109
 
108
110
  @config['templates'] ||= {}
109
111
  @config['templates']['default'] ||= {
@@ -233,6 +235,15 @@ class WWID
233
235
  end
234
236
  end
235
237
 
238
+ ##
239
+ ## @brief Return the contents of the ERB template for Markdown output
240
+ ##
241
+ ## @return (String) ERB template
242
+ ##
243
+ def markdown_template
244
+ IO.read(File.join(File.dirname(__FILE__), '../templates/doing-markdown.erb'))
245
+ end
246
+
236
247
  ##
237
248
  ## @brief Return the contents of the HAML template for HTML output
238
249
  ##
@@ -817,7 +828,7 @@ class WWID
817
828
  ' | ',
818
829
  item['title']
819
830
  ]
820
- if opt[:section] =~ /^all/i
831
+ if section =~ /^all/i
821
832
  out.concat([
822
833
  ' (',
823
834
  item['section'],
@@ -834,6 +845,12 @@ class WWID
834
845
  '--bind ctrl-a:select-all',
835
846
  %(-q "#{opt[:query]}")
836
847
  ]
848
+ if !opt[:menu]
849
+ exit_now! "Can't skip menu when no query is provided" unless opt[:query]
850
+
851
+ fzf_args.concat([%(--filter="#{opt[:query]}"), '--no-sort'])
852
+ end
853
+
837
854
  res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
838
855
  selected = []
839
856
  res.split(/\n/).each do |item|
@@ -879,8 +896,7 @@ class WWID
879
896
  when /(add|remove) tag/
880
897
  type = action =~ /^add/ ? 'add' : 'remove'
881
898
  if opt[:tag]
882
- warn "'add tag' and 'remove tag' can not be used together"
883
- Process.exit 1
899
+ exit_now! "'add tag' and 'remove tag' can not be used together"
884
900
  end
885
901
  print "#{colors['yellow']}Tag to #{type}: #{colors['reset']}"
886
902
  tag = STDIN.gets
@@ -1020,8 +1036,6 @@ class WWID
1020
1036
  case opt[:output]
1021
1037
  when /doing/
1022
1038
  options[:template] = '- %date | %title%note'
1023
- when /taskpaper/
1024
- options[:template] = '- %title @date(%date)%note'
1025
1039
  else
1026
1040
  options[:output] = opt[:output]
1027
1041
  end
@@ -1120,7 +1134,12 @@ class WWID
1120
1134
  else
1121
1135
  if opt[:sequential]
1122
1136
  next_entry = next_item(item)
1123
- done_date = next_entry['date'] - 60 if next_entry
1137
+
1138
+ if next_entry.nil?
1139
+ done_date = Time.now
1140
+ else
1141
+ done_date = next_entry['date'] - 60
1142
+ end
1124
1143
  elsif opt[:took]
1125
1144
  if item['date'] + opt[:took] > Time.now
1126
1145
  item['date'] = Time.now - opt[:took]
@@ -1141,10 +1160,31 @@ class WWID
1141
1160
  title = item['title']
1142
1161
  opt[:tags].each do |tag|
1143
1162
  tag = tag.strip
1144
- if opt[:remove]
1145
- if title =~ /@#{tag}\b/
1146
- title.gsub!(/(^| )@#{tag}(\([^)]*\))?/, '')
1147
- @results.push(%(Removed @#{tag}: "#{title}" in #{section}))
1163
+ if opt[:remove] || opt[:rename]
1164
+ case_sensitive = tag !~ /[A-Z]/
1165
+ replacement = ''
1166
+ if opt[:rename]
1167
+ replacement = tag
1168
+ tag = opt[:rename]
1169
+ end
1170
+
1171
+ if opt[:regex]
1172
+ rx_tag = tag.gsub(/\./, '\S')
1173
+ else
1174
+ rx_tag = tag.gsub(/\?/, '.').gsub(/\*/, '\S*?')
1175
+ end
1176
+
1177
+ if title =~ / @#{rx_tag}\b/
1178
+ rx = Regexp.new("(^| )@#{rx_tag}(\\([^)]*\\))?(?=\\b|$)", case_sensitive)
1179
+ removed_tags = []
1180
+ title.gsub!(rx) do |mtch|
1181
+ removed_tags.push(mtch.strip.sub(/\(.*?\)$/, ''))
1182
+ replacement
1183
+ end
1184
+
1185
+ title.dedup_tags!
1186
+
1187
+ @results.push(%(Removed #{removed_tags.join(', ')}: "#{title}" in #{section}))
1148
1188
  end
1149
1189
  elsif title !~ /@#{tag}/
1150
1190
  title.chomp!
@@ -1191,6 +1231,15 @@ class WWID
1191
1231
  write(@doing_file)
1192
1232
  end
1193
1233
 
1234
+ ##
1235
+ ## @brief Move item from current section to
1236
+ ## destination section
1237
+ ##
1238
+ ## @param item The item
1239
+ ## @param section The destination section
1240
+ ##
1241
+ ## @return Updated item
1242
+ ##
1194
1243
  def move_item(item, section)
1195
1244
  old_section = item['section']
1196
1245
  new_item = item.dup
@@ -1215,13 +1264,15 @@ class WWID
1215
1264
  ## @param old_item
1216
1265
  ##
1217
1266
  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)
1267
+ combined = { 'items' => [] }
1268
+ @content.each do |_k, v|
1269
+ combined['items'] += v['items']
1270
+ end
1271
+ items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
1272
+ idx = items.index(old_item)
1222
1273
 
1223
- if section_items.size > idx
1224
- section_items[idx + 1]
1274
+ if idx > 0
1275
+ items[idx - 1]
1225
1276
  else
1226
1277
  nil
1227
1278
  end
@@ -1689,11 +1740,13 @@ class WWID
1689
1740
 
1690
1741
  # opt[:highlight] ||= true
1691
1742
  section = ''
1743
+ is_single = true
1692
1744
  if opt[:section].nil?
1693
1745
  section = choose_section
1694
1746
  opt[:section] = @content[section]
1695
1747
  elsif opt[:section].instance_of?(String)
1696
1748
  if opt[:section] =~ /^all$/i
1749
+ is_single = false
1697
1750
  combined = { 'items' => [] }
1698
1751
  @content.each do |_k, v|
1699
1752
  combined['items'] += v['items']
@@ -1780,7 +1833,7 @@ class WWID
1780
1833
 
1781
1834
  out = ''
1782
1835
 
1783
- exit_now! 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline)$/i)
1836
+ exit_now! 'Unknown output format' if opt[:output] && (opt[:output] !~ /^(template|html|csv|json|timeline|markdown|md|taskpaper)$/i)
1784
1837
 
1785
1838
  case opt[:output]
1786
1839
  when /^csv$/i
@@ -1856,11 +1909,11 @@ class WWID
1856
1909
  end
1857
1910
  end
1858
1911
  if opt[:output] == 'json'
1859
- out = {
1912
+ puts JSON.pretty_generate({
1860
1913
  'section' => section,
1861
1914
  'items' => items_out,
1862
- 'timers' => tag_times(format: 'json', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order])
1863
- }.to_json
1915
+ 'timers' => tag_times(format: :json, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order])
1916
+ })
1864
1917
  elsif opt[:output] == 'timeline'
1865
1918
  template = <<~EOTEMPLATE
1866
1919
  <!doctype html>
@@ -1940,10 +1993,60 @@ class WWID
1940
1993
  css_template
1941
1994
  end
1942
1995
 
1943
- totals = opt[:totals] ? tag_times(format: 'html', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
1996
+ totals = opt[:totals] ? tag_times(format: :html, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
1944
1997
  engine = Haml::Engine.new(template)
1945
1998
  out = engine.render(Object.new,
1946
1999
  { :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
2000
+ when /^taskpaper$/i
2001
+ options = opt
2002
+ options[:highlight] = false
2003
+ options[:wrap_width] = 0
2004
+ options[:tags_color] = false
2005
+ options[:output] = nil
2006
+ options[:template] = '- %title @date(%date)%note'
2007
+
2008
+ out = list_section(options)
2009
+ when /^(md|markdown|gfm)$/i
2010
+ page_title = section
2011
+ all_items = []
2012
+ items.each do |i|
2013
+ if String.method_defined? :force_encoding
2014
+ title = i['title'].force_encoding('utf-8').link_urls({format: :markdown})
2015
+ note = i['note'].map { |line| line.force_encoding('utf-8').strip.link_urls({format: :markdown}) } if i['note']
2016
+ else
2017
+ title = i['title'].link_urls({format: :markdown})
2018
+ note = i['note'].map { |line| line.strip.link_urls({format: :markdown}) } if i['note']
2019
+ end
2020
+
2021
+ title = "#{title} @project(#{i['section']})" unless is_single
2022
+
2023
+ interval = get_interval(i) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
2024
+ interval ||= false
2025
+
2026
+ done = i['title'] =~ /(?<= |^)@done/ ? 'x' : ' '
2027
+
2028
+ all_items << {
2029
+ date: i['date'].strftime('%a %-I:%M%p'),
2030
+ shortdate: i['date'].relative_date,
2031
+ done: done,
2032
+ note: note,
2033
+ section: i['section'],
2034
+ time: interval,
2035
+ title: title.strip
2036
+ }
2037
+ end
2038
+
2039
+ template = if @config['html_template']['markdown'] && File.exist?(File.expand_path(@config['html_template']['markdown']))
2040
+ IO.read(File.expand_path(@config['html_template']['markdown']))
2041
+ else
2042
+ markdown_template
2043
+ end
2044
+
2045
+ totals = opt[:totals] ? tag_times(format: :markdown, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
2046
+
2047
+ mdx = MarkdownExport.new(page_title, all_items, totals)
2048
+ engine = ERB.new(template)
2049
+ out = engine.result(mdx.get_binding)
1947
2050
  else
1948
2051
  items.each do |item|
1949
2052
  if opt[:highlight] && item['title'] =~ /@#{@config['marker_tag']}\b/i
@@ -1958,7 +2061,9 @@ class WWID
1958
2061
  note_lines = item['note'].delete_if do |line|
1959
2062
  line =~ /^\s*$/
1960
2063
  end
1961
- note_lines.map! { |line| "\t\t#{line.sub(/^\t*/, '').sub(/^-/, '—')} " }
2064
+ note_lines.map! { |line|
2065
+ "\t#{line.sub(/^\t*(— )?/, '').sub(/^- /, '— ')} "
2066
+ }
1962
2067
  if opt[:wrap_width]&.positive?
1963
2068
  width = opt[:wrap_width]
1964
2069
  note_lines.map! do |line|
@@ -1986,15 +2091,7 @@ class WWID
1986
2091
  output.sub!(/%interval/, interval)
1987
2092
 
1988
2093
  output.sub!(/%shortdate/) do
1989
- if item['date'] > Date.today.to_time
1990
- item['date'].strftime(' %_I:%M%P')
1991
- elsif item['date'] > (Date.today - 6).to_time
1992
- item['date'].strftime('%a %_I:%M%P')
1993
- elsif item['date'].year == Date.today.year
1994
- item['date'].strftime('%m/%d %_I:%M%P')
1995
- else
1996
- item['date'].strftime('%m/%d/%Y %_I:%M%P')
1997
- end
2094
+ item['date'].relative_date
1998
2095
  end
1999
2096
 
2000
2097
  output.sub!(/%title/) do |_m|
@@ -2032,7 +2129,7 @@ class WWID
2032
2129
  out += "#{output}\n"
2033
2130
  end
2034
2131
 
2035
- out += tag_times(format: 'text', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
2132
+ out += tag_times(format: :text, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
2036
2133
  end
2037
2134
  out
2038
2135
  end
@@ -2257,8 +2354,25 @@ class WWID
2257
2354
  opt[:totals] ||= false
2258
2355
  opt[:sort_tags] ||= false
2259
2356
  section = guess_section(section)
2260
- list_section({ section: section, count: 0, order: 'asc', yesterday: true, times: times,
2261
- output: output, totals: opt[:totals], sort_tags: opt[:sort_tags] })
2357
+ y = (Time.now - (60 * 60 * 24)).strftime('%Y-%m-%d')
2358
+ opt[:after] = "#{y} #{opt[:after]}" if opt[:after]
2359
+ opt[:before] = "#{y} #{opt[:before]}" if opt[:before]
2360
+
2361
+ options = {
2362
+ after: opt[:after],
2363
+ before: opt[:before],
2364
+ count: 0,
2365
+ order: 'asc',
2366
+ output: output,
2367
+ section: section,
2368
+ sort_tags: opt[:sort_tags],
2369
+ tag_order: opt[:tag_order],
2370
+ times: times,
2371
+ totals: opt[:totals],
2372
+ yesterday: true
2373
+ }
2374
+
2375
+ list_section(options)
2262
2376
  end
2263
2377
 
2264
2378
  ##
@@ -2323,7 +2437,7 @@ class WWID
2323
2437
  ## @param sort_by_name (Boolean) Sort by name if true, otherwise by time
2324
2438
  ## @param sort_order (String) The sort order (asc or desc)
2325
2439
  ##
2326
- def tag_times(format: 'text', sort_by_name: false, sort_order: 'asc')
2440
+ def tag_times(format: :text, sort_by_name: false, sort_order: 'asc')
2327
2441
  return '' if @timers.empty?
2328
2442
 
2329
2443
  max = @timers.keys.sort_by { |k| k.length }.reverse[0].length + 1
@@ -2338,8 +2452,9 @@ class WWID
2338
2452
  end
2339
2453
 
2340
2454
  sorted_tags_data.reverse! if sort_order =~ /^asc/i
2455
+ case format
2456
+ when :html
2341
2457
 
2342
- if format == 'html'
2343
2458
  output = <<EOS
2344
2459
  <table>
2345
2460
  <caption id="tagtotals">Tag Totals</caption>
@@ -2374,7 +2489,20 @@ EOS
2374
2489
  </table>
2375
2490
  EOS
2376
2491
  output + tail
2377
- elsif format == 'json'
2492
+ when :markdown
2493
+ pad = sorted_tags_data.map {|k, v| k }.group_by(&:size).max.last[0].length
2494
+ output = <<-EOS
2495
+ | #{' ' * (pad - 7) }project | time |
2496
+ | #{'-' * (pad - 1)}: | :------- |
2497
+ EOS
2498
+ sorted_tags_data.reverse.each do |k, v|
2499
+ if v > 0
2500
+ output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' % fmt_time(v)} |\n"
2501
+ end
2502
+ end
2503
+ tail = "[Tag Totals]"
2504
+ output + tail
2505
+ when :json
2378
2506
  output = []
2379
2507
  sorted_tags_data.reverse.each do |k, v|
2380
2508
  output << {
data/lib/doing.rb CHANGED
@@ -9,5 +9,6 @@ require 'chronic'
9
9
  require 'haml'
10
10
  require 'json'
11
11
  require 'doing/helpers'
12
+ require 'doing/markdown_export'
12
13
  require 'doing/wwid'
13
- # require 'doing/markdown_document_listener'
14
+ 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.87
4
+ version: 1.0.91
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-10-14 00:00:00.000000000 Z
11
+ date: 2021-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake