doing 1.0.93 → 2.0.6.pre

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 (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