doing 1.0.7pre → 1.0.8pre

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
  SHA1:
3
- metadata.gz: 416eb3b0c0b4892d106c8e6431a5925b393e587c
4
- data.tar.gz: 47b5a29f17ce836404630379612a0c82e0811a97
3
+ metadata.gz: fe65d7638817ae91c0b28db474b25e1db315c32d
4
+ data.tar.gz: 2f3f3b3100bd5b9495be019787201b773d7f9e16
5
5
  SHA512:
6
- metadata.gz: 4fb329e66f43e85a03a2c256d1ef0b68db8b8c917c974de5d39c5de805c71b9c2b7a0e4a5357cc4e5fe854ffdf2bfcb266ec60b1537e8426736023179b5dd541
7
- data.tar.gz: a1057480b5758c883d6602a6e50b328b68972b9566888516a84a0b5d57a76c25e56031e725f6036bab35581a4bb9c37ead73b4a54cf0d7b59a800dd473587610
6
+ metadata.gz: 945b6b6794717f6dfaa41b180209e28cf5cd219852c2098a862e940cb6989010c3aa79fdec5010bfffc82c43bc177667988d6357427d4f6c936efa26950b77e9
7
+ data.tar.gz: afcc84a5dd4b5723e0b003aa68c444a44ead9b99b56e7e625508482bdd92982acb4c8dcee2859191b912cc3c0500c1dc5d7d37896c569f657c13314a5ffe0fc7
data/README.md CHANGED
@@ -32,6 +32,10 @@ _Side note:_ I actually use the library behind this utility as part of another s
32
32
 
33
33
  $ [sudo] gem install doing
34
34
 
35
+ To install the _latest_ version, use `--pre`:
36
+
37
+ $ [sudo] gem install --pre doing
38
+
35
39
  Only use `sudo` if your environment requires it. If you're using the system Ruby on a Mac, for example, it will likely be necessary. If `gem install doing` fails, then run `sudo gem install doing` and provide your administrator password.
36
40
 
37
41
  Run `doing config` to open your `~/.doingrc` file in the editor defined in the $EDITOR environment variable. Set up your `doing_file` right away (where you want entries to be stored), and cover the rest after you've read the docs.
@@ -120,6 +124,7 @@ The config also contains templates for various command outputs. Include placehol
120
124
  - `%shortdate`: a custom date formatter that removes the day/month/year from the entry if they match the current day/month/year
121
125
  - `%note`: Any note in the entry will be included here, a newline and tabs are automatically added.
122
126
  - `%odnote`: The notes with a leading tab removed (outdented note)
127
+ - `%chompnote`: Notes on one line, beginning and trailing whitespace removed.
123
128
  - `%hr`: a horizontal rule (`-`) the width of the terminal
124
129
  - `%hr_under`: a horizontal rule (`_`) the width of the terminal
125
130
  - `%n`: inserts a newline
@@ -256,6 +261,8 @@ Outputs:
256
261
 
257
262
  ![](http://ckyp.us/XKpj+)
258
263
 
264
+ 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.
265
+
259
266
  ## Usage
260
267
 
261
268
  doing [global options] command [command options] [arguments...]
@@ -298,18 +305,48 @@ All of these commands accept a `-e` argument. This opens your command line edito
298
305
  tag - Tag last entry
299
306
  note - Add a note to the last entry
300
307
 
308
+ ##### Finishing
309
+
301
310
  `doing finish` by itself is the same as `doing done` by itself. It adds `@done(timestamp)` to the last entry. It also accepts a numeric argument to complete X number of tasks back in history. Add `-a` to also archive the affected entries.
302
311
 
303
312
  `doing finish` also provides an `--auto` flag, which you can use to set the end time of any entry to 1 minute before the start time of the next. Running a command such as `doing finish --auto 10` will go through the last 10 entries and sequentially update any without a `@done` tag with one set to the time just before the next entry in the list.
304
313
 
305
314
  As mentioned above, `finish` also accepts `--back "2 hours"` (sets the finish date from time now minus interval) or `--took 30m` (sets the finish date to time started plus interval) so you can accurately add times to completed tasks, even if you don't do it in the moment.
306
315
 
316
+
317
+ ##### Tagging and Autotagging
318
+
307
319
  `tag` adds one or more tags to the last entry, or specify a count with `-c X`. Tags are specified as basic arguments, separated by spaces. For example:
308
320
 
309
321
  doing tag -c 3 client cancelled
310
322
 
311
323
  ... will mark the last three entries as "@client @cancelled." Add `-r` as a switch to remove the listed tags instead.
312
324
 
325
+ You can optionally define keywords for common tasks and projects in your `.doingrc` file. When these keywords appear in an item title, they'll automatically be converted into @tags. The "whitelist" tags are exact (but case insensitive) matches. You can also define "synonyms" which will add a tag at the end based on keywords associated with it.
326
+
327
+ To add autotagging, include a section like this in your `~/.doingrc` file:
328
+
329
+ autotag:
330
+ whitelist:
331
+ - doing
332
+ - mindmeister
333
+ - marked
334
+ - playing
335
+ - working
336
+ - writing
337
+ synonyms:
338
+ playing:
339
+ - hacking
340
+ - tweaking
341
+ - toying
342
+ - messing
343
+ writing:
344
+ - blogging
345
+ - posting
346
+ - publishing
347
+
348
+ ##### Annotating
349
+
313
350
  `note` lets you append a note to the last entry. You can specify a section to grab the last entry from with `-s section_name`. `-e` will open your $EDITOR for typing the note, but you can also just include it on the command line after any flags. You can also pipe a note in on STDIN (`echo "fun stuff"|doing note`). If you don't use the `-r` switch, new notes will be appended to the existing notes, and using the `-e` switch will let you edit and add to an existing note. The `-r` switch will remove/replace a note; if there's new note text passed when using the `-r` switch, it will replace any existing note. If the `-r` switch is used alone, any existing note will be removed.
314
351
 
315
352
  You can also add notes at the time of entry by using the `-n` or `--note` flag with `doing now`, `doing later`, or `doing done`. If you pass text to any of the creation commands which has multiple lines, everything after the first line break will become the note.
@@ -331,10 +368,12 @@ Use `-c X` to limit the displayed results. Combine it with `-a newest` or `-a ol
331
368
 
332
369
  The `show` command can also show the time spent on a task if it has a `@done(date)` tag with the `-t` option. This requires that you include a `%interval` token in template -> default in the config. You can also include `@start(date)` tags, which override the timestamp when calculating the intervals.
333
370
 
334
- If you have a use for it, you can use `--csv` on the show or view commands to output the results as a comma-separated CSV to STDOUT. Redirect to a file to save it: `doing show all done --csv > ~/Desktop/done.csv`.
371
+ If you have a use for it, you can use `-o csv` on the show or view commands to output the results as a comma-separated CSV to STDOUT. Redirect to a file to save it: `doing show all done -o csv > ~/Desktop/done.csv`. You can do the same with `-o json`.
335
372
 
336
373
  `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`).
337
374
 
375
+ `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).
376
+
338
377
  #### Views
339
378
 
340
379
  view - Display a user-created view
@@ -452,8 +491,16 @@ I'll try to document some of the code structure as I flesh it out. I'm currently
452
491
 
453
492
  ### Changelog
454
493
 
494
+ #### 1.0.8pre
495
+
496
+ * JSON output option to view commands
497
+ * Added autotagging to tag command
498
+ * date filtering, improved date language
499
+ * added doing on command
500
+ * let view templates define output format (csv, json, html, template)
501
+ * add %chompnote template variable (item note with newlines and extra whitespace stripped)
455
502
 
456
- #### 1.0.7 / 2014-09-23
503
+ #### 1.0.7pre
457
504
 
458
505
  * fix for -v option
459
506
  * Slightly fuzzier searching in the grep command
data/bin/doing CHANGED
@@ -442,25 +442,35 @@ command :tag do |c|
442
442
  c.default_value false
443
443
  c.switch [:r,:remove], :negatable => false, :default_value => false
444
444
 
445
+ c.desc 'Autotag entries based on autotag configuration in ~/.doingrc'
446
+ c.default_value false
447
+ c.switch [:a,:autotag], :negatable => false, :default_value => false
448
+
445
449
  c.action do |global_options,options,args|
446
- if args.length == 0
450
+ if args.length == 0 && !options[:a]
447
451
  raise "You must specify at least one tag"
448
452
  else
449
453
 
450
454
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
451
455
 
452
- if args.join("") =~ /,/
453
- tags = args.join("").split(/,/)
456
+ unless options[:a]
457
+ if args.join("") =~ /,/
458
+ tags = args.join("").split(/,/)
459
+ else
460
+ tags = args.join(" ").split(" ") # in case tags are quoted as one arg
461
+ end
462
+
463
+ tags.map!{|tag| tag.sub(/^@/,'').strip }
454
464
  else
455
- tags = args.join(" ").split(" ") # in case tags are quoted as one arg
465
+ tags = []
456
466
  end
457
467
 
458
- tags.map!{|tag| tag.sub(/^@/,'').strip }
459
-
460
468
  count = options[:c].to_i
461
469
 
462
470
  if count == 0
463
- if options[:r]
471
+ if options[:a]
472
+ print "Are you sure you want to autotag all records? (y/N) "
473
+ elsif options[:r]
464
474
  print "Are you sure you want to remove #{tags.join(" and ")} from all records? (y/N) "
465
475
  else
466
476
  print "Are you sure you want to add #{tags.join(" and ")} to all records? (y/N) "
@@ -473,7 +483,7 @@ command :tag do |c|
473
483
  end
474
484
  end
475
485
 
476
- wwid.tag_last({:tags => tags, :count => count, :section => section, :date => options[:d], :remove => options[:r]})
486
+ wwid.tag_last({:tags => tags, :count => count, :section => section, :date => options[:d], :remove => options[:r], :autotag => options[:a]})
477
487
  end
478
488
  end
479
489
  end
@@ -495,8 +505,6 @@ command :mark do |c|
495
505
  end
496
506
  end
497
507
 
498
-
499
-
500
508
  desc 'List all entries'
501
509
  long_desc 'The argument can be a section name, tag(s) or both. "pick" or "choose" as an argument will offer a section menu.'
502
510
  arg_name 'section [tags]'
@@ -517,8 +525,9 @@ command :show do |c|
517
525
  c.default_value 'asc'
518
526
  c.flag [:s,:sort], :default_value => 'asc'
519
527
 
520
- c.desc 'Output to export format (csv|html)'
521
- c.flag [:o,:output]
528
+ c.desc 'Date range to show, or a single day to filter date on.'
529
+ c.long_desc 'Date range argument should be quoted. Date specifications can be natural language. To specify a range, use "to," or "through,".\n\ndoing show --from "monday to friday"'
530
+ c.flag [:f,:from]
522
531
 
523
532
  c.desc 'Show time intervals on @done tasks'
524
533
  c.default_value true
@@ -532,7 +541,10 @@ command :show do |c|
532
541
  c.default_value false
533
542
  c.switch [:only_timed], :default_value => false, :negatable => false
534
543
 
544
+ c.desc 'Output to export format (csv|html|json)'
545
+ c.flag [:o,:output]
535
546
  c.action do |global_options,options,args|
547
+
536
548
  tag_filter = false
537
549
  tags = []
538
550
  if args.length > 0
@@ -552,7 +564,7 @@ command :show do |c|
552
564
  if args.length > 0
553
565
  args.each {|arg|
554
566
  if arg =~ /,/
555
- arg_tags = arg.split(/,/).each {|tag|
567
+ arg.split(/,/).each {|tag|
556
568
  tags.push(tag.strip.sub(/^@/,''))
557
569
  }
558
570
  else
@@ -571,9 +583,23 @@ command :show do |c|
571
583
  }
572
584
  end
573
585
 
586
+ if options[:f]
587
+ date_string = options[:f]
588
+ if date_string =~ / (to|through|thru|(un)?til|-+) /
589
+ dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
590
+ start = wwid.chronify(dates[0])
591
+ finish = wwid.chronify(dates[2])
592
+ else
593
+ start = wwid.chronify(date_string)
594
+ finish = false
595
+ end
596
+ exit_now! "Unrecognized date string" unless start
597
+ dates = [start,finish]
598
+ end
599
+
574
600
  options[:t] = true if options[:totals]
575
601
 
576
- puts wwid.list_section({:section => section, :count => options[:c].to_i, :tag_filter => tag_filter, :age => options[:a], :order => options[:s], :output => options[:output], :times => options[:t], :totals => options[:totals], :highlight => true, :only_timed => options[:only_timed]})
602
+ puts wwid.list_section({:section => section, :date_filter => dates, :count => options[:c].to_i, :tag_filter => tag_filter, :age => options[:a], :order => options[:s], :output => options[:output], :times => options[:t], :totals => options[:totals], :highlight => true, :only_timed => options[:only_timed]})
577
603
 
578
604
  end
579
605
  end
@@ -586,7 +612,7 @@ command :grep do |c|
586
612
  c.default_value "all"
587
613
  c.flag [:s,:section], :default_value => "All"
588
614
 
589
- c.desc 'Output to export format (csv|html)'
615
+ c.desc 'Output to export format (csv|html|json)'
590
616
  c.flag [:o,:output]
591
617
 
592
618
  c.desc 'Show time intervals on @done tasks'
@@ -656,7 +682,7 @@ command :today do |c|
656
682
  c.default_value false
657
683
  c.switch [:totals], :default_value => false, :negatable => true
658
684
 
659
- c.desc 'Output to export format (csv|html)'
685
+ c.desc 'Output to export format (csv|html|json)'
660
686
  c.flag [:o,:output]
661
687
 
662
688
  c.action do |global_options,options,args|
@@ -668,11 +694,52 @@ command :today do |c|
668
694
  end
669
695
  end
670
696
 
697
+ desc 'List entries for a date'
698
+ long_desc 'Date argument can be natural language. "thursday" would be interpreted as "last thursday," and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates, it will create a range.'
699
+ arg_name 'date_string'
700
+ command :on do |c|
701
+ c.desc 'Section'
702
+ c.arg_name 'section_name'
703
+ c.default_value 'All'
704
+ c.flag [:s,:section], :default_value => 'All'
705
+
706
+ c.desc 'Show time intervals on @done tasks'
707
+ c.default_value true
708
+ c.switch [:t,:times], :default_value => true
709
+
710
+ c.desc 'Show time totals at the end of output'
711
+ c.default_value false
712
+ c.switch [:totals], :default_value => false, :negatable => true
713
+
714
+ c.desc 'Output to export format (csv|html|json)'
715
+ c.flag [:o,:output]
716
+
717
+ c.action do |global_options,options,args|
718
+
719
+ date_string = args.join(" ")
720
+
721
+ if date_string =~ / (to|through|thru) /
722
+ dates = date_string.split(/ (to|through|thru) /)
723
+ start = wwid.chronify(dates[0])
724
+ finish = wwid.chronify(dates[2])
725
+ else
726
+ start = wwid.chronify(date_string)
727
+ finish = false
728
+ end
729
+ exit_now! "Unrecognized date string" unless start
730
+
731
+ options[:t] = true if options[:totals]
732
+
733
+ puts wwid.list_date([start,finish],options[:s],options[:t],options[:output],{:totals => options[:totals]}).chomp
734
+
735
+ end
736
+ end
737
+
671
738
  desc 'List entries from yesterday'
672
739
  default_value wwid.current_section
673
740
  arg_name 'section'
674
741
  command :yesterday do |c|
675
- c.desc 'Output to export format (csv|html)'
742
+ c.desc 'Output to export format (csv|html|json)'
676
743
  c.flag [:o,:output]
677
744
 
678
745
  c.desc 'Show time intervals on @done tasks'
@@ -752,7 +819,7 @@ command :view do |c|
752
819
  c.desc 'Count to display (override view settings)'
753
820
  c.flag [:c,:count], :must_match => /^\d+$/, :type => Integer
754
821
 
755
- c.desc 'Output to export format (csv|html)'
822
+ c.desc 'Output to export format (csv|html|json)'
756
823
  c.flag [:o,:output]
757
824
 
758
825
  c.desc 'Show time intervals on @done tasks'
@@ -801,14 +868,22 @@ command :view do |c|
801
868
  tag_filter['bool'] = view.has_key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].upcase : "OR"
802
869
  end
803
870
  end
871
+
872
+ # If the -o/--output flag was specified, override any default in the view template
873
+ options[:o] ||= view.has_key?('output_format') ? view['output_format'] : "template"
874
+
804
875
  count = options[:c] ? options[:c] : view.has_key?('count') ? view['count'] : 10
805
876
  section = options[:s] ? section : view.has_key?('section') ? view['section'] : wwid.current_section
806
877
  order = view.has_key?('order') ? view['order'] : "asc"
807
878
  options[:t] = true if options[:totals]
808
879
  options[:output].downcase! if options[:output]
809
- puts wwid.list_section({:section => section, :count => count, :template => template, :format => format, :order => order, :tag_filter => tag_filter, :output => options[:output], :tags_color => tags_color, :times => options[:t], :highlight => true, :totals => options[:totals], :only_timed => only_timed })
880
+ puts wwid.list_section({:section => section, :count => count, :template => template, :format => format, :order => order, :tag_filter => tag_filter, :output => options[:o], :tags_color => tags_color, :times => options[:t], :highlight => true, :totals => options[:totals], :only_timed => only_timed })
810
881
  else
811
- raise "View #{title} not found in config"
882
+ if title.class == FalseClass
883
+ exit_now! "Cancelled"
884
+ else
885
+ raise "View #{title} not found in config"
886
+ end
812
887
  end
813
888
  end
814
889
  end
@@ -953,7 +1028,7 @@ end
953
1028
 
954
1029
  pre do |global,command,options,args|
955
1030
  if global[:doing_file]
956
- wwid.init_doing_file(input=global[:doing_file])
1031
+ wwid.init_doing_file(global[:doing_file])
957
1032
  else
958
1033
  wwid.init_doing_file
959
1034
  end
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.7pre'
2
+ VERSION = '1.0.8pre'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -226,22 +226,29 @@ class WWID
226
226
  # Returns:
227
227
  # seconds(Integer)
228
228
  def chronify(input)
229
- if input =~ /^(\d+)([mhd])?$/i
230
- amt = $1
231
- type = $2.nil? ? "m" : $2
232
- input = case type.downcase
233
- when 'm'
234
- amt + " minutes ago"
235
- when 'h'
236
- amt + " hours ago"
237
- when 'd'
238
- amt + " days ago"
239
- else
240
- input
229
+
230
+ had_to_try = Time.parse(input) rescue false
231
+
232
+ if had_to_try.class == FalseClass
233
+ if input =~ /^(\d+)([mhd])?$/i
234
+ amt = $1
235
+ type = $2.nil? ? "m" : $2
236
+ input = case type.downcase
237
+ when 'm'
238
+ amt + " minutes ago"
239
+ when 'h'
240
+ amt + " hours ago"
241
+ when 'd'
242
+ amt + " days ago"
243
+ else
244
+ input
245
+ end
241
246
  end
242
- end
243
247
 
244
- Chronic.parse(input, {:context => :past, :ambiguous_time_range => 8})
248
+ Chronic.parse(input, {:context => :past, :ambiguous_time_range => 8})
249
+ else
250
+ had_to_try
251
+ end
245
252
  end
246
253
 
247
254
 
@@ -260,11 +267,11 @@ class WWID
260
267
 
261
268
  minutes = case type.downcase
262
269
  when 'm'
263
- $1.to_i
270
+ amt.to_i
264
271
  when 'h'
265
- ($1.to_f * 60).round
272
+ (amt.to_f * 60).round
266
273
  when 'd'
267
- ($1.to_f * 60 * 24).round
274
+ (amt.to_f * 60 * 24).round
268
275
  else
269
276
  minutes
270
277
  end
@@ -341,7 +348,8 @@ class WWID
341
348
  opt[:timed] ||= false
342
349
 
343
350
  title = [title.strip.cap_first] + @config['default_tags'].map{|t| '@' + t.sub(/^ *@/,'').chomp}
344
- entry = {'title' => title.join(' '), 'date' => opt[:back]}
351
+ title = autotag(title.join(' '))
352
+ entry = {'title' => title, 'date' => opt[:back]}
345
353
  unless opt[:note] =~ /^\s*$/s
346
354
  entry['note'] = opt[:note]
347
355
  end
@@ -382,6 +390,7 @@ class WWID
382
390
  opt[:sequential] ||= false
383
391
  opt[:date] ||= false
384
392
  opt[:remove] ||= false
393
+ opt[:autotag] ||= false
385
394
  opt[:back] ||= Time.now
386
395
 
387
396
 
@@ -408,38 +417,44 @@ class WWID
408
417
  count = opt[:count] == 0 ? items.length : opt[:count]
409
418
  items.map! {|item|
410
419
  break if index == count
411
- if opt[:sequential]
412
- done_date = next_start - 1
413
- next_start = item['date']
414
- elsif opt[:back].instance_of? Fixnum
415
- done_date = item['date'] + opt[:back]
416
- else
417
- done_date = opt[:back]
418
- end
419
420
 
420
- title = item['title']
421
- opt[:tags].each {|tag|
422
- tag.strip!
423
- if opt[:remove]
424
- if title =~ /@#{tag}/
425
- title.gsub!(/(^| )@#{tag}(\([^\)]*\))?/,'')
426
- @results.push("Removed @#{tag}: #{title}")
427
- end
421
+ unless opt[:autotag]
422
+ if opt[:sequential]
423
+ done_date = next_start - 1
424
+ next_start = item['date']
425
+ elsif opt[:back].instance_of? Fixnum
426
+ done_date = item['date'] + opt[:back]
428
427
  else
429
- unless title =~ /@#{tag}/
430
- title.chomp!
431
- if opt[:date]
432
- title += " @#{tag}(#{done_date.strftime('%F %R')})"
433
- else
434
- title += " @#{tag}"
428
+ done_date = opt[:back]
429
+ end
430
+
431
+ title = item['title']
432
+ opt[:tags].each {|tag|
433
+ tag.strip!
434
+ if opt[:remove]
435
+ if title =~ /@#{tag}/
436
+ title.gsub!(/(^| )@#{tag}(\([^\)]*\))?/,'')
437
+ @results.push("Removed @#{tag}: #{title}")
438
+ end
439
+ else
440
+ unless title =~ /@#{tag}/
441
+ title.chomp!
442
+ if opt[:date]
443
+ title += " @#{tag}(#{done_date.strftime('%F %R')})"
444
+ else
445
+ title += " @#{tag}"
446
+ end
447
+ @results.push("Added @#{tag}: #{title}")
435
448
  end
436
- @results.push("Added @#{tag}: #{title}")
437
449
  end
438
- end
439
- }
450
+ }
451
+ item['title'] = title
452
+ else
453
+ item['title'] = autotag(item['title'])
454
+ end
440
455
 
441
- item['title'] = title
442
456
  index += 1
457
+
443
458
  item
444
459
  }
445
460
 
@@ -628,6 +643,7 @@ class WWID
628
643
  opt[:totals] ||= false
629
644
  opt[:search] ||= false
630
645
  opt[:only_timed] ||= false
646
+ opt[:date_filter] ||= []
631
647
 
632
648
  # opt[:highlight] ||= true
633
649
  section = ""
@@ -655,6 +671,18 @@ class WWID
655
671
 
656
672
  items = opt[:section]['items'].sort_by{|item| item['date'] }
657
673
 
674
+ if opt[:date_filter].length == 2
675
+ start_date = opt[:date_filter][0]
676
+ end_date = opt[:date_filter][1]
677
+ items.keep_if {|item|
678
+ if end_date
679
+ item['date'] >= start_date && item['date'] <= end_date
680
+ else
681
+ item['date'].strftime('%F') == start_date.strftime('%F')
682
+ end
683
+ }
684
+ end
685
+
658
686
  if opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
659
687
  items.delete_if {|item|
660
688
  if opt[:tag_filter]['bool'] =~ /(AND|ALL)/
@@ -717,9 +745,8 @@ class WWID
717
745
  out = ""
718
746
 
719
747
  if opt[:output]
720
- raise "Unknown output format" unless opt[:output] =~ /(html|csv)/
748
+ raise "Unknown output format" unless opt[:output] =~ /(template|html|csv|json|timeline)/
721
749
  end
722
-
723
750
  if opt[:output] == "csv"
724
751
  output = [CSV.generate_line(['date','title','note','timer'])]
725
752
  items.each {|i|
@@ -735,6 +762,104 @@ class WWID
735
762
  output.push(CSV.generate_line([i['date'],i['title'],note,interval]))
736
763
  }
737
764
  out = output.join("")
765
+ elsif opt[:output] == "json" || opt[:output] == "timeline"
766
+
767
+ items_out = []
768
+ max = items[-1]['date'].strftime('%F')
769
+ min = items[0]['date'].strftime('%F')
770
+ items.each_with_index {|i,index|
771
+ if RUBY_VERSION.to_f > 1.8
772
+ title = i['title'].force_encoding('utf-8')
773
+ note = i['note'].map {|line| line.force_encoding('utf-8').strip } if i['note']
774
+ else
775
+ title = i['title']
776
+ note = i['note'].map { |line| line.strip }
777
+ end
778
+
779
+ if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
780
+ end_date = Time.parse($1)
781
+ interval = get_interval(i,false)
782
+ end
783
+ end_date ||= ""
784
+ interval ||= 0
785
+ note ||= ""
786
+
787
+ tags = []
788
+ skip_tags = ['meanwhile', 'done', 'cancelled', 'flagged']
789
+ i['title'].scan(/@([^\(\s]+)(?:\((.*?)\))?/).each {|tag|
790
+ tags.push(tag[0]) unless skip_tags.include?(tag[0])
791
+ }
792
+ if opt[:output] == "json"
793
+ items_out << {
794
+ :date => i['date'],
795
+ :end_date => end_date,
796
+ :title => title.strip, #+ " #{note}"
797
+ :note => note.class == Array ? note.join("\n") : note,
798
+ :time => "%02d:%02d:%02d" % fmt_time(interval),
799
+ :tags => tags
800
+ }
801
+ elsif opt[:output] == "timeline"
802
+ new_item = {
803
+ 'id' => index + 1,
804
+ 'content' => title.strip, #+ " #{note}"
805
+ 'title' => title.strip + " (#{"%02d:%02d:%02d" % fmt_time(interval)})",
806
+ 'start' => i['date'].strftime('%F'),
807
+ 'type' => 'point'
808
+ }
809
+
810
+ if interval && interval > 0
811
+ new_item['end'] = end_date.strftime('%F')
812
+ if interval > 3600 * 3
813
+ new_item['type'] = 'range'
814
+ end
815
+ end
816
+ items_out.push(new_item)
817
+ end
818
+ }
819
+ if opt[:output] == "json"
820
+ out = {
821
+ 'section' => section,
822
+ 'items' => items_out,
823
+ 'timers' => tag_times("json")
824
+ }.to_json
825
+ elsif opt[:output] == "timeline"
826
+ template =<<EOTEMPLATE
827
+ <!doctype html>
828
+ <html>
829
+ <head>
830
+ <link href="http://visjs.org/dist/vis.css" rel="stylesheet" type="text/css" />
831
+ <script src="http://visjs.org/dist/vis.js"></script>
832
+ </head>
833
+ <body>
834
+ <div id="mytimeline"></div>
835
+
836
+ <script type="text/javascript">
837
+ // DOM element where the Timeline will be attached
838
+ var container = document.getElementById('mytimeline');
839
+
840
+ // Create a DataSet with data (enables two way data binding)
841
+ var data = new vis.DataSet(#{items_out.to_json});
842
+
843
+ // Configuration for the Timeline
844
+ var options = {
845
+ width: '100%',
846
+ height: '800px',
847
+ margin: {
848
+ item: 20
849
+ },
850
+ stack: true,
851
+ min: '#{min}',
852
+ max: '#{max}'
853
+ };
854
+
855
+ // Create a Timeline
856
+ var timeline = new vis.Timeline(container, data, options);
857
+ </script>
858
+ </body>
859
+ </html>
860
+ EOTEMPLATE
861
+ return template
862
+ end
738
863
  elsif opt[:output] == "html"
739
864
  page_title = section
740
865
  items_out = []
@@ -764,34 +889,34 @@ class WWID
764
889
  :time => interval
765
890
  }
766
891
  }
892
+
767
893
  style = "body{background:#fff;color:#333;font-family:Helvetica,arial,freesans,clean,sans-serif;font-size:16px;line-height:120%;text-align:justify;padding:20px}h1{text-align:left;position:relative;left:220px;margin-bottom:1em}ul{list-style-position:outside;position:relative;left:170px;margin-right:170px;text-align:left}ul li{list-style-type:none;border-left:solid 1px #ccc;padding-left:10px;line-height:2;position:relative}ul li .date{font-size:14px;position:absolute;left:-122px;color:#7d9ca2;text-align:right;width:110px;line-height:2}ul li .tag{color:#999}ul li .note{display:block;color:#666;padding:0 0 0 22px;line-height:1.4;font-size:15px}ul li .note:before{content:'\\25BA';font-weight:300;position:absolute;left:40px;font-size:8px;color:#aaa;line-height:3}ul li:hover .note{display:block}span.time{color:#729953;float:left;position:relative;padding:0 5px;font-size:15px;border-bottom:dashed 1px #ccc;text-align:right;background:#f9fced;margin-right:4px}table td{border-bottom:solid 1px #ddd;height:24px}caption{text-align:left;border-bottom:solid 1px #aaa;margin:10px 0}table{width:400px;margin:50px 0 0 211px}th{padding-bottom:10px}th,td{padding-right:20px}table{max-width:400px;margin:50px 0 0 221px}"
768
894
  template =<<EOT
769
895
  !!!
770
896
  %html
771
- %head
772
- %meta{"charset" => "utf-8"}/
773
- %meta{"content" => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/
774
- %title what are you doing?
775
- %style= @style
776
- %body
777
- %header
778
- %h1= @page_title
779
- %article
780
- %ul
781
- - @items.each do |i|
782
- %li
783
- %span.date= i[:date]
784
- = i[:title]
785
- - if i[:time] && i[:time] != "00:00:00"
786
- %span.time= i[:time]
787
- - if i[:note]
788
- %span.note= i[:note].map{|n| n.strip }.join('<br>')
789
- = @totals
897
+ %head
898
+ %meta{"charset" => "utf-8"}/
899
+ %meta{"content" => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/
900
+ %title what are you doing?
901
+ %style= @style
902
+ %body
903
+ %header
904
+ %h1= @page_title
905
+ %article
906
+ %ul
907
+ - @items.each do |i|
908
+ %li
909
+ %span.date= i[:date]
910
+ = i[:title]
911
+ - if i[:time] && i[:time] != "00:00:00"
912
+ %span.time= i[:time]
913
+ - if i[:note]
914
+ %span.note= i[:note].map{|n| n.strip }.join('<br>')
915
+ = @totals
790
916
  EOT
791
917
  totals = opt[:totals] ? tag_times("html") : ""
792
918
  engine = Haml::Engine.new(template)
793
919
  puts engine.render(Object.new, { :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
794
-
795
920
  else
796
921
  items.each {|item|
797
922
 
@@ -857,6 +982,7 @@ EOT
857
982
  end
858
983
  output.sub!(/%note/,note)
859
984
  output.sub!(/%odnote/,note.gsub(/^\t*/,""))
985
+ output.sub!(/%chompnote/,note.gsub(/\n+/,' ').gsub(/(^\s*|\s*$)/,'').gsub(/\s+/,' '))
860
986
  output.gsub!(/%hr(_under)?/) do |m|
861
987
  o = ""
862
988
  `tput cols`.to_i.times do
@@ -1022,6 +1148,17 @@ EOT
1022
1148
  list_section({:section => @current_section, :wrap_width => cfg['wrap_width'], :count => 0, :format => cfg['date_format'], :template => cfg['template'], :order => "asc", :today => true, :times => times, :output => output, :totals => opt[:totals]})
1023
1149
  end
1024
1150
 
1151
+ def list_date(dates,section,times=nil,output=nil,opt={})
1152
+ opt[:totals] ||= false
1153
+ section = guess_section(section)
1154
+ # :date_filter expects an array with start and end date
1155
+ if dates.class == String
1156
+ dates = [dates, dates]
1157
+ end
1158
+
1159
+ list_section({:section => section, :count => 0, :order => "asc", :date_filter => dates, :times => times, :output => output, :totals => opt[:totals] })
1160
+ end
1161
+
1025
1162
  def yesterday(section,times=nil,output=nil,opt={})
1026
1163
  opt[:totals] ||= false
1027
1164
  section = guess_section(section)
@@ -1085,6 +1222,16 @@ EOS
1085
1222
  </table>
1086
1223
  EOS
1087
1224
  output + tail
1225
+ elsif format == "json"
1226
+ output = []
1227
+ @timers.delete_if { |k,v| v == 0}.sort_by{|k,v| v }.reverse.each {|k,v|
1228
+ output << {
1229
+ 'tag' => k,
1230
+ 'seconds' => v,
1231
+ 'formatted' => "%02d:%02d:%02d" % fmt_time(v)
1232
+ }
1233
+ }
1234
+ output
1088
1235
  else
1089
1236
  output = []
1090
1237
  @timers.delete_if { |k,v| v == 0}.sort_by{|k,v| v }.reverse.each {|k,v|
@@ -1101,6 +1248,30 @@ EOS
1101
1248
  end
1102
1249
  end
1103
1250
 
1251
+ # Uses autotag: configuration to turn keywords into tags for time tracking.
1252
+ # Does not repeat tags in a title, and only converts the first instance of
1253
+ # an untagged keyword
1254
+ def autotag(title)
1255
+ return unless title
1256
+ @config['autotag']['whitelist'].each {|tag|
1257
+ title.sub!(/(?<!@)(#{tag.strip})\b/i,'@\1') unless title =~ /@#{tag}\b/i
1258
+ }
1259
+ tail_tags = []
1260
+ @config['autotag']['synonyms'].each {|tag, v|
1261
+ v.each {|word|
1262
+ if title =~ /\b#{word}\b/i
1263
+ tail_tags.push(tag)
1264
+ end
1265
+ }
1266
+ }
1267
+ title + tail_tags.uniq.map {|t| '@'+t }.join(' ')
1268
+ end
1269
+
1270
+ def autotag_item(item)
1271
+ item['title'] = autotag(item['title'])
1272
+ item
1273
+ end
1274
+
1104
1275
  private
1105
1276
 
1106
1277
  def get_interval(item, formatted=true)
data/lib/doing.rb CHANGED
@@ -7,6 +7,7 @@ require 'csv'
7
7
  require 'tempfile'
8
8
  require 'chronic'
9
9
  require 'haml'
10
+ require 'json'
10
11
  require 'doing/wwid.rb'
11
12
 
12
13
  DOING_CONFIG_NAME = ".doingrc"
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.7pre
4
+ version: 1.0.8pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-24 00:00:00.000000000 Z
11
+ date: 2014-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -120,6 +120,20 @@ dependencies:
120
120
  - - '>='
121
121
  - !ruby/object:Gem::Version
122
122
  version: '0'
123
+ - !ruby/object:Gem::Dependency
124
+ name: json
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ~>
128
+ - !ruby/object:Gem::Version
129
+ version: 1.8.1
130
+ type: :runtime
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ~>
135
+ - !ruby/object:Gem::Version
136
+ version: 1.8.1
123
137
  description: A tool for managing a TaskPaper-like file of recent activites. Perfect
124
138
  for the late-night hacker on too much caffeine to remember what they accomplished
125
139
  at 2 in the morning.