doing 1.0.93 → 2.0.6.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +616 -0
  4. data/COMMANDS.md +1181 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +110 -0
  7. data/LICENSE +23 -0
  8. data/README.md +15 -699
  9. data/Rakefile +79 -0
  10. data/_config.yml +1 -0
  11. data/bin/doing +1055 -494
  12. data/doing.gemspec +34 -0
  13. data/doing.rdoc +1839 -0
  14. data/example_plugin.rb +209 -0
  15. data/generate_completions.sh +5 -0
  16. data/img/doing-colors.jpg +0 -0
  17. data/img/doing-printf-wrap-800.jpg +0 -0
  18. data/img/doing-show-note-formatting-800.jpg +0 -0
  19. data/lib/completion/_doing.zsh +203 -0
  20. data/lib/completion/doing.bash +449 -0
  21. data/lib/completion/doing.fish +329 -0
  22. data/lib/doing/array.rb +8 -0
  23. data/lib/doing/cli_status.rb +70 -0
  24. data/lib/doing/colors.rb +136 -0
  25. data/lib/doing/configuration.rb +312 -0
  26. data/lib/doing/errors.rb +109 -0
  27. data/lib/doing/hash.rb +31 -0
  28. data/lib/doing/hooks.rb +59 -0
  29. data/lib/doing/item.rb +155 -0
  30. data/lib/doing/log_adapter.rb +344 -0
  31. data/lib/doing/markdown_document_listener.rb +174 -0
  32. data/lib/doing/note.rb +59 -0
  33. data/lib/doing/pager.rb +95 -0
  34. data/lib/doing/plugin_manager.rb +208 -0
  35. data/lib/doing/plugins/export/csv_export.rb +48 -0
  36. data/lib/doing/plugins/export/html_export.rb +83 -0
  37. data/lib/doing/plugins/export/json_export.rb +140 -0
  38. data/lib/doing/plugins/export/markdown_export.rb +85 -0
  39. data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
  40. data/lib/doing/plugins/export/template_export.rb +141 -0
  41. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  42. data/lib/doing/plugins/import/calendar_import.rb +76 -0
  43. data/lib/doing/plugins/import/doing_import.rb +144 -0
  44. data/lib/doing/plugins/import/timing_import.rb +78 -0
  45. data/lib/doing/string.rb +348 -0
  46. data/lib/doing/symbol.rb +16 -0
  47. data/lib/doing/time.rb +18 -0
  48. data/lib/doing/util.rb +186 -0
  49. data/lib/doing/version.rb +1 -1
  50. data/lib/doing/wwid.rb +1868 -2349
  51. data/lib/doing/wwidfile.rb +117 -0
  52. data/lib/doing.rb +43 -3
  53. data/lib/examples/commands/autotag.rb +63 -0
  54. data/lib/examples/commands/wiki.rb +81 -0
  55. data/lib/examples/plugins/hooks.rb +22 -0
  56. data/lib/examples/plugins/say_export.rb +202 -0
  57. data/lib/examples/plugins/templates/wiki.css +169 -0
  58. data/lib/examples/plugins/templates/wiki.haml +27 -0
  59. data/lib/examples/plugins/templates/wiki_index.haml +18 -0
  60. data/lib/examples/plugins/wiki_export.rb +87 -0
  61. data/lib/templates/doing-markdown.erb +5 -0
  62. data/man/doing.1 +964 -0
  63. data/man/doing.1.html +711 -0
  64. data/man/doing.1.ronn +600 -0
  65. data/package-lock.json +3 -0
  66. data/rdoc_to_mmd.rb +42 -0
  67. data/rdocfixer.rb +13 -0
  68. data/scripts/generate_bash_completions.rb +211 -0
  69. data/scripts/generate_fish_completions.rb +204 -0
  70. data/scripts/generate_zsh_completions.rb +168 -0
  71. metadata +82 -7
  72. data/lib/doing/helpers.rb +0 -191
  73. data/lib/doing/markdown_export.rb +0 -16
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # title: Template Export
4
+ # description: Default export option using configured template placeholders
5
+ # author: Brett Terpstra
6
+ # url: https://brettterpstra.com
7
+ module Doing
8
+ class TemplateExport
9
+ include Doing::Color
10
+ include Doing::Util
11
+
12
+ def self.settings
13
+ {
14
+ trigger: 'template|doing'
15
+ }
16
+ end
17
+
18
+ def self.render(wwid, items, variables: {})
19
+ return if items.nil?
20
+
21
+ opt = variables[:options]
22
+
23
+ out = ''
24
+ items.each do |item|
25
+ if opt[:highlight] && item.title =~ /@#{wwid.config['marker_tag']}\b/i
26
+ flag = Doing::Color.send(wwid.config['marker_color'])
27
+ reset = Doing::Color.default
28
+ else
29
+ flag = ''
30
+ reset = ''
31
+ end
32
+
33
+ if (!item.note.empty?) && wwid.config['include_notes']
34
+ note = item.note.map(&:strip).delete_if(&:empty?)
35
+ note.map! { |line| "#{line.sub(/^\t*/, '')} " }
36
+
37
+ if opt[:wrap_width]&.positive?
38
+ width = opt[:wrap_width]
39
+ note.map! { |line| line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n") }
40
+ note = note.join("\n").split(/\n/).delete_if(&:empty?)
41
+ end
42
+ else
43
+ note = []
44
+ end
45
+ output = opt[:template].dup
46
+
47
+ output.gsub!(/%[a-z]+/) do |m|
48
+ if Doing::Color.respond_to?(m.sub(/^%/, ''))
49
+ Doing::Color.send(m.sub(/^%/, ''))
50
+ else
51
+ m
52
+ end
53
+ end
54
+
55
+ output.sub!(/%(\d+)?date/) do
56
+ pad = Regexp.last_match(1).to_i
57
+ format("%#{pad}s", item.date.strftime(opt[:format]))
58
+ end
59
+
60
+ interval = wwid.get_interval(item, record: true) if opt[:times]
61
+
62
+ interval ||= ''
63
+ output.sub!(/%interval/, interval)
64
+
65
+ output.sub!(/%(\d+)?shortdate/) do
66
+ pad = Regexp.last_match(1) || 13
67
+ format("%#{pad}s", item.date.relative_date)
68
+ end
69
+
70
+ output.sub!(/%section/, item.section) if item.section
71
+
72
+ title_offset = Doing::Color.uncolor(output).match(/%(-?\d+)?([ _t]\d+)?title/).begin(0)
73
+ output.sub!(/%(-?\d+)?(([ _t])(\d+))?title(.*?)$/) do
74
+ m = Regexp.last_match
75
+ pad = m[1].to_i
76
+ indent = ''
77
+ if m[2]
78
+ char = m[3] =~ /t/ ? "\t" : " "
79
+ indent = char * m[4].to_i
80
+ end
81
+ after = m[5]
82
+ if opt[:wrap_width]&.positive? || pad.positive?
83
+ width = pad.positive? ? pad : opt[:wrap_width]
84
+ item.title.wrap(width, pad: pad, indent: indent, offset: title_offset, prefix: flag, after: after, reset: reset)
85
+ # flag + item.title.gsub(/(.{#{opt[:wrap_width]}})(?=\s+|\Z)/, "\\1\n ").sub(/\s*$/, '') + reset
86
+ else
87
+ format("%s%#{pad}s%s%s", flag, item.title.sub(/\s*$/, ''), reset, after)
88
+ end
89
+ end
90
+
91
+ # output.sub!(/(?i-m)^([\s\S]*?)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)([\s\S]*?)$/, '\1\3\2')
92
+ if opt[:tags_color]
93
+ output.highlight_tags!(opt[:tags_color])
94
+ end
95
+
96
+ if note.empty?
97
+ output.gsub!(/%([io]d|(\^.)?(([ _t]|[^a-z0-9])?\d+)?(.[ _t]?)?)?note/, '')
98
+ else
99
+ output.sub!(/%note/, "\n#{note.map { |l| "\t#{l.strip} " }.join("\n")}")
100
+ output.sub!(/%idnote/, "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}")
101
+ output.sub!(/%odnote/, "\n#{note.map { |l| "#{l.strip} " }.join("\n")}")
102
+ output.sub!(/(?mi)%(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])?(?<icount>\d+))?(?<prefix>.[ _t]?)?note/) do
103
+ m = Regexp.last_match
104
+ mark = m['mchar'] || ''
105
+ indent = if m['ichar']
106
+ char = m['ichar'] =~ /t/ ? "\t" : ' '
107
+ char * m['icount'].to_i
108
+ else
109
+ ''
110
+ end
111
+ prefix = m['prefix'] || ''
112
+ "\n#{note.map { |l| "#{mark}#{indent}#{prefix}#{l.strip} " }.join("\n")}"
113
+ end
114
+ output.sub!(/%chompnote/) do |_m|
115
+ chomp_note = note.map do |l|
116
+ l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ')
117
+ end
118
+ chomp_note.join(' ')
119
+ end
120
+ end
121
+
122
+ output.gsub!(/%hr(_under)?/) do |_m|
123
+ o = ''
124
+ `tput cols`.to_i.times do
125
+ o += Regexp.last_match(1).nil? ? '-' : '_'
126
+ end
127
+ o
128
+ end
129
+ output.gsub!(/%n/, "\n")
130
+ output.gsub!(/%t/, "\t")
131
+
132
+ out += "#{output}\n"
133
+ end
134
+ # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
135
+ out += wwid.tag_times(format: wwid.config['timer_format'].to_sym, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
136
+ out
137
+ end
138
+
139
+ Doing::Plugins.register ['template', 'doing'], :export, self
140
+ end
141
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # title: Calendar.app Import
4
+ # description: Import entries from a Calendar.app calendar
5
+ # author: Brett Terpstra
6
+ # url: https://brettterpstra.com
7
+ require 'json'
8
+
9
+ module Doing
10
+ ##
11
+ ## @brief Plugin for importing from Calendar.app on macOS
12
+ ##
13
+ class CalendarImport
14
+ include Doing::Util
15
+
16
+ def self.settings
17
+ {
18
+ trigger: 'i?cal(?:endar)?'
19
+ }
20
+ end
21
+
22
+ def self.import(wwid, _path, options: {})
23
+ limit_start = options[:start].to_i
24
+ limit_end = options[:end].to_i
25
+
26
+ section = options[:section] || wwid.config['current_section']
27
+ options[:no_overlap] ||= false
28
+ options[:autotag] ||= wwid.auto_tag
29
+
30
+ wwid.add_section(section) unless wwid.content.key?(section)
31
+
32
+ tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
33
+ prefix = options[:prefix] || '[Calendar.app]'
34
+
35
+ script = File.join(File.dirname(__FILE__), 'cal_to_json.scpt')
36
+ res = `/usr/bin/osascript "#{script}" #{limit_start} #{limit_end}`.strip
37
+ data = JSON.parse(res)
38
+
39
+ new_items = []
40
+ data.each do |entry|
41
+ # Only process entries with a start and end date
42
+ next unless entry.key?('start') && entry.key?('end')
43
+
44
+ # Round down seconds and convert UTC to local time
45
+ start_time = Time.parse(entry['start']).getlocal
46
+ end_time = Time.parse(entry['end']).getlocal
47
+ next unless start_time && end_time
48
+
49
+ title = "#{prefix} "
50
+ title += entry['name']
51
+ tags.each do |tag|
52
+ if title =~ /\b#{tag}\b/i
53
+ title.sub!(/\b#{tag}\b/i, "@#{tag}")
54
+ else
55
+ title += " @#{tag}"
56
+ end
57
+ end
58
+ title = wwid.autotag(title) if options[:autotag]
59
+ title += " @done(#{end_time.strftime('%Y-%m-%d %H:%M')})"
60
+ title.gsub!(/ +/, ' ')
61
+ title.strip!
62
+ new_entry = { 'title' => title, 'date' => start_time, 'section' => section }
63
+ new_entry.note = entry['notes'].split(/\n/).map(&:chomp) if entry.key?('notes')
64
+ new_items.push(new_entry)
65
+ end
66
+ total = new_items.count
67
+ new_items = wwid.dedup(new_items, options[:no_overlap])
68
+ dups = total - new_items.count
69
+ Doing.logger.info(%(Skipped #{dups} items with overlapping times)) if dups.positive?
70
+ wwid.content[section][:items].concat(new_items)
71
+ Doing.logger.info(%(Imported #{new_items.count} items to #{section}))
72
+ end
73
+
74
+ Doing::Plugins.register 'calendar', :import, self
75
+ end
76
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # title: Doing Format Import
4
+ # description: Import entries from a Doing-formatted file
5
+ # author: Brett Terpstra
6
+ # url: https://brettterpstra.com
7
+ module Doing
8
+ class DoingImport
9
+ include Doing::Util
10
+
11
+ def self.settings
12
+ {
13
+ trigger: 'doing'
14
+ }
15
+ end
16
+
17
+ ##
18
+ ## @brief Imports a Doing file
19
+ ##
20
+ ## @param wwid WWID object
21
+ ## @param path (String) Path to Doing file
22
+ ## @param options (Hash) Additional Options
23
+ ##
24
+ ## @return Nothing
25
+ ##
26
+ def self.import(wwid, path, options: {})
27
+ exit_now! 'Path to Doing file required' if path.nil?
28
+
29
+ exit_now! 'File not found' unless File.exist?(File.expand_path(path))
30
+
31
+ options[:no_overlap] ||= false
32
+ options[:autotag] ||= wwid.auto_tag
33
+
34
+ tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
35
+ prefix = options[:prefix] || ''
36
+
37
+ @old_items = []
38
+
39
+ wwid.content.each { |_, v| @old_items.concat(v[:items]) }
40
+
41
+ new_items = read_doing_file(path)
42
+
43
+ if options[:date_filter]
44
+ new_items = wwid.filter_items(new_items, opt: { count: 0, date_filter: options[:date_filter] })
45
+ end
46
+
47
+ if options[:before] || options[:after]
48
+ new_items = wwid.filter_items(new_items, opt: { count: 0, before: options[:before], after: options[:after] })
49
+ end
50
+
51
+ imported = []
52
+
53
+ new_items.each do |item|
54
+ next if duplicate?(item)
55
+
56
+ title = "#{prefix} #{item.title}"
57
+ tags.each do |tag|
58
+ if title =~ /\b#{tag}\b/i
59
+ title.sub!(/\b#{tag}\b/i, "@#{tag}")
60
+ else
61
+ title += " @#{tag}"
62
+ end
63
+ end
64
+ title = wwid.autotag(title) if options[:autotag]
65
+ title.gsub!(/ +/, ' ')
66
+ title.strip!
67
+ section = options[:section] || item.section
68
+ section ||= wwid.config['current_section']
69
+
70
+ new_item = Item.new(item.date, title, section)
71
+ new_item.note = item.note
72
+
73
+ imported.push(new_item)
74
+ end
75
+
76
+ dups = new_items.count - imported.count
77
+ Doing.logger.info('Skipped:', %(#{dups} duplicate items)) if dups.positive?
78
+
79
+ imported = wwid.dedup(imported, !options[:overlap])
80
+ overlaps = new_items.count - imported.count - dups
81
+ Doing.logger.debug('Skipped:', "#{overlaps} items with overlapping times") if overlaps.positive?
82
+
83
+ imported.each do |item|
84
+ wwid.add_section(item.section) unless wwid.content.key?(item.section)
85
+ wwid.content[item.section][:items].push(item)
86
+ end
87
+
88
+ Doing.logger.info('Imported:', "#{imported.count} items")
89
+ end
90
+
91
+ def self.duplicate?(item)
92
+ @old_items.each do |oi|
93
+ return true if item.equal?(oi)
94
+ end
95
+
96
+ false
97
+ end
98
+
99
+ def self.read_doing_file(path)
100
+ doing_file = File.expand_path(path)
101
+
102
+ return nil unless File.exist?(doing_file) && File.file?(doing_file) && File.stat(doing_file).size.positive?
103
+
104
+ input = IO.read(doing_file)
105
+ input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
106
+
107
+ lines = input.split(/[\n\r]/)
108
+ current = 0
109
+
110
+ items = []
111
+ section = ''
112
+
113
+ lines.each do |line|
114
+ next if line =~ /^\s*$/
115
+
116
+ case line
117
+ when /^(\S[\S ]+):\s*(@\S+\s*)*$/
118
+ section = Regexp.last_match(1)
119
+ current = 0
120
+ when /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
121
+ date = Regexp.last_match(1).strip
122
+ title = Regexp.last_match(2).strip
123
+ item = Item.new(date, title, section)
124
+ items.push(item)
125
+ current += 1
126
+ when /^\S/
127
+ next
128
+ else
129
+ next if current.zero?
130
+
131
+ prev_item = items[current - 1]
132
+ prev_item.note = Note.new unless prev_item.note
133
+
134
+ prev_item.note.add(line)
135
+ # end
136
+ end
137
+ end
138
+
139
+ items
140
+ end
141
+
142
+ Doing::Plugins.register 'doing', :import, self
143
+ end
144
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # title: Timing.app Import
4
+ # description: Import entries from a Timing.app report (JSON)
5
+ # author: Brett Terpstra
6
+ # url: https://brettterpstra.com
7
+ module Doing
8
+ class TimingImport
9
+ include Doing::Util
10
+
11
+ def self.settings
12
+ {
13
+ trigger: 'tim(?:ing)?'
14
+ }
15
+ end
16
+
17
+ ##
18
+ ## @brief Imports a Timing report
19
+ ##
20
+ ## @param path (String) Path to JSON report file
21
+ ## @param options (Hash) Additional Options
22
+ ##
23
+ def self.import(wwid, path, options: {})
24
+ exit_now! 'Path to JSON report required' if path.nil?
25
+ section = options[:section] || wwid.config['current_section']
26
+ options[:no_overlap] ||= false
27
+ options[:autotag] ||= wwid.auto_tag
28
+ wwid.add_section(section) unless wwid.content.key?(section)
29
+
30
+ add_tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
31
+ prefix = options[:prefix] || '[Timing.app]'
32
+ exit_now! 'File not found' unless File.exist?(File.expand_path(path))
33
+
34
+ data = JSON.parse(IO.read(File.expand_path(path)))
35
+ new_items = []
36
+ data.each do |entry|
37
+ # Only process task entries
38
+ next if entry.key?('activityType') && entry['activityType'] != 'Task'
39
+ # Only process entries with a start and end date
40
+ next unless entry.key?('startDate') && entry.key?('endDate')
41
+
42
+ # Round down seconds and convert UTC to local time
43
+ start_time = Time.parse(entry['startDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
44
+ end_time = Time.parse(entry['endDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
45
+ next unless start_time && end_time
46
+
47
+ tags = entry['project'].split(/ ▸ /).map { |proj| proj.gsub(/[^a-z0-9]+/i, '').downcase }
48
+ tags.concat(add_tags)
49
+ title = "#{prefix} "
50
+ title += entry.key?('activityTitle') && entry['activityTitle'] != '(Untitled Task)' ? entry['activityTitle'] : 'Working on'
51
+ tags.each do |tag|
52
+ if title =~ /\b#{tag}\b/i
53
+ title.sub!(/\b#{tag}\b/i, "@#{tag}")
54
+ else
55
+ title += " @#{tag}"
56
+ end
57
+ end
58
+ title = wwid.autotag(title) if options[:autotag]
59
+ title += " @done(#{end_time.strftime('%Y-%m-%d %H:%M')})"
60
+ title.gsub!(/ +/, ' ')
61
+ title.strip!
62
+ new_item = Item.new(start_time, title, section)
63
+ new_item.note.add(entry['notes']) if entry.key?('notes')
64
+ new_items.push(new_item)
65
+ end
66
+ total = new_items.count
67
+ skipped = data.count - total
68
+ Doing.logger.debug('Skipped:' , %(#{skipped} items, invalid type or no time interval)) if skipped.positive?
69
+ new_items = wwid.dedup(new_items, options[:no_overlap])
70
+ dups = total - new_items.count
71
+ Doing.logger.debug('Skipped:' , %(#{dups} items with overlapping times)) if dups.positive?
72
+ wwid.content[section][:items].concat(new_items)
73
+ Doing.logger.info('Imported:', %(#{new_items.count} items to #{section}))
74
+ end
75
+
76
+ Doing::Plugins.register 'timing', :import, self
77
+ end
78
+ end