doing 1.0.91 → 2.0.3.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.
- checksums.yaml +4 -4
 - data/AUTHORS +19 -0
 - data/CHANGELOG.md +590 -0
 - data/COMMANDS.md +1181 -0
 - data/Gemfile +2 -0
 - data/Gemfile.lock +110 -0
 - data/LICENSE +23 -0
 - data/README.md +15 -699
 - data/Rakefile +79 -0
 - data/_config.yml +1 -0
 - data/bin/doing +1012 -486
 - data/doing.fish +278 -0
 - data/doing.gemspec +34 -0
 - data/doing.rdoc +1759 -0
 - data/example_plugin.rb +209 -0
 - data/generate_completions.sh +4 -0
 - data/img/doing-colors.jpg +0 -0
 - data/img/doing-printf-wrap-800.jpg +0 -0
 - data/img/doing-show-note-formatting-800.jpg +0 -0
 - data/lib/completion/_doing.zsh +151 -0
 - data/lib/completion/doing.bash +416 -0
 - data/lib/completion/doing.fish +278 -0
 - data/lib/doing/array.rb +8 -0
 - data/lib/doing/cli_status.rb +66 -0
 - data/lib/doing/colors.rb +136 -0
 - data/lib/doing/configuration.rb +310 -0
 - data/lib/doing/errors.rb +102 -0
 - data/lib/doing/hash.rb +31 -0
 - data/lib/doing/hooks.rb +59 -0
 - data/lib/doing/item.rb +155 -0
 - data/lib/doing/log_adapter.rb +342 -0
 - data/lib/doing/markdown_document_listener.rb +174 -0
 - data/lib/doing/note.rb +59 -0
 - data/lib/doing/pager.rb +95 -0
 - data/lib/doing/plugin_manager.rb +208 -0
 - data/lib/doing/plugins/export/csv_export.rb +48 -0
 - data/lib/doing/plugins/export/html_export.rb +83 -0
 - data/lib/doing/plugins/export/json_export.rb +140 -0
 - data/lib/doing/plugins/export/markdown_export.rb +85 -0
 - data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
 - data/lib/doing/plugins/export/template_export.rb +141 -0
 - data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
 - data/lib/doing/plugins/import/calendar_import.rb +76 -0
 - data/lib/doing/plugins/import/doing_import.rb +144 -0
 - data/lib/doing/plugins/import/timing_import.rb +78 -0
 - data/lib/doing/string.rb +346 -0
 - data/lib/doing/symbol.rb +16 -0
 - data/lib/doing/time.rb +18 -0
 - data/lib/doing/util.rb +186 -0
 - data/lib/doing/version.rb +1 -1
 - data/lib/doing/wwid.rb +1831 -2358
 - data/lib/doing/wwidfile.rb +117 -0
 - data/lib/doing.rb +44 -4
 - data/lib/examples/commands/wiki.rb +80 -0
 - data/lib/examples/plugins/hooks.rb +22 -0
 - data/lib/examples/plugins/say_export.rb +202 -0
 - data/lib/examples/plugins/templates/wiki.css +169 -0
 - data/lib/examples/plugins/templates/wiki.haml +27 -0
 - data/lib/examples/plugins/templates/wiki_index.haml +18 -0
 - data/lib/examples/plugins/wiki_export.rb +87 -0
 - data/lib/templates/doing-markdown.erb +5 -0
 - data/man/doing.1 +964 -0
 - data/man/doing.1.html +711 -0
 - data/man/doing.1.ronn +600 -0
 - data/package-lock.json +3 -0
 - data/rdoc_to_mmd.rb +42 -0
 - data/rdocfixer.rb +13 -0
 - data/scripts/generate_bash_completions.rb +210 -0
 - data/scripts/generate_fish_completions.rb +201 -0
 - data/scripts/generate_zsh_completions.rb +164 -0
 - metadata +82 -6
 - data/lib/doing/helpers.rb +0 -191
 
| 
         @@ -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: :text, 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
         
     | 
| 
         Binary file 
     | 
| 
         @@ -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
         
     |