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,310 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Doing
         
     | 
| 
      
 4 
     | 
    
         
            +
              ##
         
     | 
| 
      
 5 
     | 
    
         
            +
              ## @brief      Configuration object
         
     | 
| 
      
 6 
     | 
    
         
            +
              ##
         
     | 
| 
      
 7 
     | 
    
         
            +
              class Configuration
         
     | 
| 
      
 8 
     | 
    
         
            +
                attr_reader :settings
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                attr_writer :ignore_local
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                MissingConfigFile = Class.new(RuntimeError)
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                DEFAULTS = {
         
     | 
| 
      
 15 
     | 
    
         
            +
                  'autotag' => {
         
     | 
| 
      
 16 
     | 
    
         
            +
                    'whitelist' => [],
         
     | 
| 
      
 17 
     | 
    
         
            +
                    'synonyms' => {}
         
     | 
| 
      
 18 
     | 
    
         
            +
                  },
         
     | 
| 
      
 19 
     | 
    
         
            +
                  'editors' => {
         
     | 
| 
      
 20 
     | 
    
         
            +
                    'default' => ENV['DOING_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR'],
         
     | 
| 
      
 21 
     | 
    
         
            +
                    'doing_file' => nil,
         
     | 
| 
      
 22 
     | 
    
         
            +
                    'config' => nil
         
     | 
| 
      
 23 
     | 
    
         
            +
                  },
         
     | 
| 
      
 24 
     | 
    
         
            +
                  'plugins' => {
         
     | 
| 
      
 25 
     | 
    
         
            +
                    'plugin_path' => File.join(Util.user_home, '.config', 'doing', 'plugins'),
         
     | 
| 
      
 26 
     | 
    
         
            +
                    'command_path' => File.join(Util.user_home, '.config', 'doing', 'commands')
         
     | 
| 
      
 27 
     | 
    
         
            +
                  },
         
     | 
| 
      
 28 
     | 
    
         
            +
                  'doing_file' => '~/what_was_i_doing.md',
         
     | 
| 
      
 29 
     | 
    
         
            +
                  'current_section' => 'Currently',
         
     | 
| 
      
 30 
     | 
    
         
            +
                  'paginate' => false,
         
     | 
| 
      
 31 
     | 
    
         
            +
                  'never_time' => [],
         
     | 
| 
      
 32 
     | 
    
         
            +
                  'never_finish' => [],
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  'templates' => {
         
     | 
| 
      
 35 
     | 
    
         
            +
                    'default' => {
         
     | 
| 
      
 36 
     | 
    
         
            +
                      'date_format' => '%Y-%m-%d %H:%M',
         
     | 
| 
      
 37 
     | 
    
         
            +
                      'template' => '%date | %title%note',
         
     | 
| 
      
 38 
     | 
    
         
            +
                      'wrap_width' => 0,
         
     | 
| 
      
 39 
     | 
    
         
            +
                      'order' => 'asc'
         
     | 
| 
      
 40 
     | 
    
         
            +
                    },
         
     | 
| 
      
 41 
     | 
    
         
            +
                    'today' => {
         
     | 
| 
      
 42 
     | 
    
         
            +
                      'date_format' => '%_I:%M%P',
         
     | 
| 
      
 43 
     | 
    
         
            +
                      'template' => '%date: %title %interval%note',
         
     | 
| 
      
 44 
     | 
    
         
            +
                      'wrap_width' => 0,
         
     | 
| 
      
 45 
     | 
    
         
            +
                      'order' => 'asc'
         
     | 
| 
      
 46 
     | 
    
         
            +
                    },
         
     | 
| 
      
 47 
     | 
    
         
            +
                    'last' => {
         
     | 
| 
      
 48 
     | 
    
         
            +
                      'date_format' => '%-I:%M%P on %a',
         
     | 
| 
      
 49 
     | 
    
         
            +
                      'template' => '%title (at %date)%odnote',
         
     | 
| 
      
 50 
     | 
    
         
            +
                      'wrap_width' => 88
         
     | 
| 
      
 51 
     | 
    
         
            +
                    },
         
     | 
| 
      
 52 
     | 
    
         
            +
                    'recent' => {
         
     | 
| 
      
 53 
     | 
    
         
            +
                      'date_format' => '%_I:%M%P',
         
     | 
| 
      
 54 
     | 
    
         
            +
                      'template' => '%shortdate: %title (%section)',
         
     | 
| 
      
 55 
     | 
    
         
            +
                      'wrap_width' => 88,
         
     | 
| 
      
 56 
     | 
    
         
            +
                      'count' => 10,
         
     | 
| 
      
 57 
     | 
    
         
            +
                      'order' => 'asc'
         
     | 
| 
      
 58 
     | 
    
         
            +
                    }
         
     | 
| 
      
 59 
     | 
    
         
            +
                  },
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  'export_templates' => {},
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                  'views' => {
         
     | 
| 
      
 64 
     | 
    
         
            +
                    'done' => {
         
     | 
| 
      
 65 
     | 
    
         
            +
                      'date_format' => '%_I:%M%P',
         
     | 
| 
      
 66 
     | 
    
         
            +
                      'template' => '%date | %title%note',
         
     | 
| 
      
 67 
     | 
    
         
            +
                      'wrap_width' => 0,
         
     | 
| 
      
 68 
     | 
    
         
            +
                      'section' => 'All',
         
     | 
| 
      
 69 
     | 
    
         
            +
                      'count' => 0,
         
     | 
| 
      
 70 
     | 
    
         
            +
                      'order' => 'desc',
         
     | 
| 
      
 71 
     | 
    
         
            +
                      'tags' => 'done complete cancelled',
         
     | 
| 
      
 72 
     | 
    
         
            +
                      'tags_bool' => 'OR'
         
     | 
| 
      
 73 
     | 
    
         
            +
                    },
         
     | 
| 
      
 74 
     | 
    
         
            +
                    'color' => {
         
     | 
| 
      
 75 
     | 
    
         
            +
                      'date_format' => '%F %_I:%M%P',
         
     | 
| 
      
 76 
     | 
    
         
            +
                      'template' => '%boldblack%date %boldgreen| %boldwhite%title%default%note',
         
     | 
| 
      
 77 
     | 
    
         
            +
                      'wrap_width' => 0,
         
     | 
| 
      
 78 
     | 
    
         
            +
                      'section' => 'Currently',
         
     | 
| 
      
 79 
     | 
    
         
            +
                      'count' => 10,
         
     | 
| 
      
 80 
     | 
    
         
            +
                      'order' => 'asc'
         
     | 
| 
      
 81 
     | 
    
         
            +
                    }
         
     | 
| 
      
 82 
     | 
    
         
            +
                  },
         
     | 
| 
      
 83 
     | 
    
         
            +
                  'marker_tag' => 'flagged',
         
     | 
| 
      
 84 
     | 
    
         
            +
                  'marker_color' => 'red',
         
     | 
| 
      
 85 
     | 
    
         
            +
                  'default_tags' => [],
         
     | 
| 
      
 86 
     | 
    
         
            +
                  'tag_sort' => 'name',
         
     | 
| 
      
 87 
     | 
    
         
            +
                  'include_notes' => true
         
     | 
| 
      
 88 
     | 
    
         
            +
                }
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                def initialize(file = nil, options: {})
         
     | 
| 
      
 91 
     | 
    
         
            +
                  if file
         
     | 
| 
      
 92 
     | 
    
         
            +
                    cf = File.expand_path(file)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    # raise MissingConfigFile, "Config not found (#{cf})" unless File.exist?(cf)
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                    @config_file = cf
         
     | 
| 
      
 96 
     | 
    
         
            +
                  end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                  @settings = configure(options)
         
     | 
| 
      
 99 
     | 
    
         
            +
                end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                def additional_configs
         
     | 
| 
      
 102 
     | 
    
         
            +
                  @additional_configs ||= find_local_config
         
     | 
| 
      
 103 
     | 
    
         
            +
                end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                def value_for_key(keypath = '')
         
     | 
| 
      
 106 
     | 
    
         
            +
                  cfg = @settings
         
     | 
| 
      
 107 
     | 
    
         
            +
                  unless keypath =~ /^[.*]?$/
         
     | 
| 
      
 108 
     | 
    
         
            +
                    paths = keypath.split(/[:.]/)
         
     | 
| 
      
 109 
     | 
    
         
            +
                    while paths.length.positive? && !cfg.nil?
         
     | 
| 
      
 110 
     | 
    
         
            +
                      path = paths.shift
         
     | 
| 
      
 111 
     | 
    
         
            +
                      new_cfg = nil
         
     | 
| 
      
 112 
     | 
    
         
            +
                      cfg.each do |key, val|
         
     | 
| 
      
 113 
     | 
    
         
            +
                        next unless key =~ /#{path.to_rx(2)}/
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                        new_cfg = val
         
     | 
| 
      
 116 
     | 
    
         
            +
                        break
         
     | 
| 
      
 117 
     | 
    
         
            +
                      end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                      if new_cfg.nil?
         
     | 
| 
      
 120 
     | 
    
         
            +
                        Doing.logger.error("Key match not found: #{path}")
         
     | 
| 
      
 121 
     | 
    
         
            +
                        break
         
     | 
| 
      
 122 
     | 
    
         
            +
                      end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                      cfg = new_cfg
         
     | 
| 
      
 125 
     | 
    
         
            +
                    end
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                  cfg
         
     | 
| 
      
 129 
     | 
    
         
            +
                end
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                # It takes the input, fills in the defaults where values do not exist.
         
     | 
| 
      
 132 
     | 
    
         
            +
                #
         
     | 
| 
      
 133 
     | 
    
         
            +
                # user_config - a Hash or Configuration of overrides.
         
     | 
| 
      
 134 
     | 
    
         
            +
                #
         
     | 
| 
      
 135 
     | 
    
         
            +
                # Returns a Configuration filled with defaults.
         
     | 
| 
      
 136 
     | 
    
         
            +
                def from(user_config)
         
     | 
| 
      
 137 
     | 
    
         
            +
                  Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
         
     | 
| 
      
 138 
     | 
    
         
            +
                end
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                def config_file
         
     | 
| 
      
 141 
     | 
    
         
            +
                  @config_file ||= File.join(Util.user_home, '.doingrc')
         
     | 
| 
      
 142 
     | 
    
         
            +
                end
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                def config_file=(file)
         
     | 
| 
      
 145 
     | 
    
         
            +
                  @config_file = file
         
     | 
| 
      
 146 
     | 
    
         
            +
                end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                ##
         
     | 
| 
      
 149 
     | 
    
         
            +
                ## @brief      Read user configuration and merge with defaults
         
     | 
| 
      
 150 
     | 
    
         
            +
                ##
         
     | 
| 
      
 151 
     | 
    
         
            +
                ## @param      opt   (Hash) Additional Options
         
     | 
| 
      
 152 
     | 
    
         
            +
                ##
         
     | 
| 
      
 153 
     | 
    
         
            +
                def configure(opt = {})
         
     | 
| 
      
 154 
     | 
    
         
            +
                  @ignore_local = opt[:ignore_local] if opt[:ignore_local]
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                  config = read_config.dup
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                  plugin_config = Util.deep_merge_hashes(DEFAULTS['plugins'], config['plugins'] || {})
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                  load_plugins(plugin_config['plugin_path'])
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                  Plugins.plugins.each do |_type, plugins|
         
     | 
| 
      
 163 
     | 
    
         
            +
                    plugins.each do |title, plugin|
         
     | 
| 
      
 164 
     | 
    
         
            +
                      plugin_config[title] = plugin[:config] if plugin[:config] && !plugin[:config].empty?
         
     | 
| 
      
 165 
     | 
    
         
            +
                      config['export_templates'][title] ||= nil if plugin[:templates] && !plugin[:templates].empty?
         
     | 
| 
      
 166 
     | 
    
         
            +
                    end
         
     | 
| 
      
 167 
     | 
    
         
            +
                  end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                  config = Util.deep_merge_hashes({
         
     | 
| 
      
 170 
     | 
    
         
            +
                                                    'plugins' => plugin_config
         
     | 
| 
      
 171 
     | 
    
         
            +
                                                  }, config)
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                  config = find_deprecations(config)
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
                  if !File.exist?(config_file) || opt[:rewrite]
         
     | 
| 
      
 176 
     | 
    
         
            +
                    Util.write_to_file(config_file, YAML.dump(config), backup: true)
         
     | 
| 
      
 177 
     | 
    
         
            +
                    Doing.logger.warn('Config:', "Config file written to #{config_file}")
         
     | 
| 
      
 178 
     | 
    
         
            +
                  end
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                  Hooks.trigger :post_config, self
         
     | 
| 
      
 181 
     | 
    
         
            +
             
     | 
| 
      
 182 
     | 
    
         
            +
                  # config = local_config.deep_merge(config) unless @ignore_local
         
     | 
| 
      
 183 
     | 
    
         
            +
                  config = Util.deep_merge_hashes(config, local_config) unless @ignore_local
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
                  Hooks.trigger :post_local_config, self
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                  config
         
     | 
| 
      
 188 
     | 
    
         
            +
                end
         
     | 
| 
      
 189 
     | 
    
         
            +
             
     | 
| 
      
 190 
     | 
    
         
            +
                private
         
     | 
| 
      
 191 
     | 
    
         
            +
             
     | 
| 
      
 192 
     | 
    
         
            +
                def find_deprecations(config)
         
     | 
| 
      
 193 
     | 
    
         
            +
                  deprecated = false
         
     | 
| 
      
 194 
     | 
    
         
            +
                  if config.key?('editor')
         
     | 
| 
      
 195 
     | 
    
         
            +
                    deprecated = true
         
     | 
| 
      
 196 
     | 
    
         
            +
                    config['editors']['default'] ||= config['editor']
         
     | 
| 
      
 197 
     | 
    
         
            +
                    config.delete('editor')
         
     | 
| 
      
 198 
     | 
    
         
            +
                    Doing.logger.debug('Deprecated:', "config key 'editor' is now 'editors->default', please update your config.")
         
     | 
| 
      
 199 
     | 
    
         
            +
                  end
         
     | 
| 
      
 200 
     | 
    
         
            +
             
     | 
| 
      
 201 
     | 
    
         
            +
                  if config.key?('config_editor_app') && !config['editors']['config']
         
     | 
| 
      
 202 
     | 
    
         
            +
                    deprecated = true
         
     | 
| 
      
 203 
     | 
    
         
            +
                    config['editors']['config'] = config['config_editor_app']
         
     | 
| 
      
 204 
     | 
    
         
            +
                    config.delete('config_editor_app')
         
     | 
| 
      
 205 
     | 
    
         
            +
                    Doing.logger.debug('Deprecated:', "config key 'config_editor_app' is now 'editors->config', please update your config.")
         
     | 
| 
      
 206 
     | 
    
         
            +
                  end
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                  if config.key?('editor_app') && !config['editors']['doing_file']
         
     | 
| 
      
 209 
     | 
    
         
            +
                    deprecated = true
         
     | 
| 
      
 210 
     | 
    
         
            +
                    config['editors']['doing_file'] = config['editor_app']
         
     | 
| 
      
 211 
     | 
    
         
            +
                    config.delete('editor_app')
         
     | 
| 
      
 212 
     | 
    
         
            +
                    Doing.logger.debug('Deprecated:', "config key 'editor_app' is now 'editors->doing_file', please update your config.")
         
     | 
| 
      
 213 
     | 
    
         
            +
                  end
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                  Doing.logger.warn('Deprecated:', 'outdated keys found, please run `doing config --update`.') if deprecated
         
     | 
| 
      
 216 
     | 
    
         
            +
                  config
         
     | 
| 
      
 217 
     | 
    
         
            +
                end
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
                ##
         
     | 
| 
      
 220 
     | 
    
         
            +
                ## @brief      Read local configurations
         
     | 
| 
      
 221 
     | 
    
         
            +
                ##
         
     | 
| 
      
 222 
     | 
    
         
            +
                ## @return     Hash of config options
         
     | 
| 
      
 223 
     | 
    
         
            +
                ##
         
     | 
| 
      
 224 
     | 
    
         
            +
                def local_config
         
     | 
| 
      
 225 
     | 
    
         
            +
                  return {} if @ignore_local
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
      
 227 
     | 
    
         
            +
                  local_configs = read_local_configs || {}
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
                  if additional_configs&.count
         
     | 
| 
      
 230 
     | 
    
         
            +
                    file_list = additional_configs.map { |p| p.sub(/^#{Util.user_home}/, '~') }.join(', ')
         
     | 
| 
      
 231 
     | 
    
         
            +
                    Doing.logger.debug('Config:', "Local config files found: #{file_list}")
         
     | 
| 
      
 232 
     | 
    
         
            +
                  end
         
     | 
| 
      
 233 
     | 
    
         
            +
             
     | 
| 
      
 234 
     | 
    
         
            +
                  local_configs
         
     | 
| 
      
 235 
     | 
    
         
            +
                end
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
      
 237 
     | 
    
         
            +
                def read_local_configs
         
     | 
| 
      
 238 
     | 
    
         
            +
                  local_configs = {}
         
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 241 
     | 
    
         
            +
                    additional_configs.each do |cfg|
         
     | 
| 
      
 242 
     | 
    
         
            +
                      local_configs.deep_merge(Util.safe_load_file(cfg))
         
     | 
| 
      
 243 
     | 
    
         
            +
                    end
         
     | 
| 
      
 244 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 245 
     | 
    
         
            +
                    Doing.logger.error('Config:', 'Error reading local configuration(s)')
         
     | 
| 
      
 246 
     | 
    
         
            +
                  end
         
     | 
| 
      
 247 
     | 
    
         
            +
             
     | 
| 
      
 248 
     | 
    
         
            +
                  local_configs
         
     | 
| 
      
 249 
     | 
    
         
            +
                end
         
     | 
| 
      
 250 
     | 
    
         
            +
             
     | 
| 
      
 251 
     | 
    
         
            +
                ##
         
     | 
| 
      
 252 
     | 
    
         
            +
                ## @brief      Reads a configuration.
         
     | 
| 
      
 253 
     | 
    
         
            +
                ##
         
     | 
| 
      
 254 
     | 
    
         
            +
                def read_config
         
     | 
| 
      
 255 
     | 
    
         
            +
                  unless File.exist?(config_file)
         
     | 
| 
      
 256 
     | 
    
         
            +
                    Doing.logger.info('Config:', 'Config file doesn\'t exist, using default configuration' )
         
     | 
| 
      
 257 
     | 
    
         
            +
                    return {}.deep_merge(DEFAULTS)
         
     | 
| 
      
 258 
     | 
    
         
            +
                  end
         
     | 
| 
      
 259 
     | 
    
         
            +
             
     | 
| 
      
 260 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 261 
     | 
    
         
            +
                    user_config = Util.safe_load_file(config_file)
         
     | 
| 
      
 262 
     | 
    
         
            +
                    if user_config.key?('html_template')
         
     | 
| 
      
 263 
     | 
    
         
            +
                      user_config['export_templates'] ||= {}
         
     | 
| 
      
 264 
     | 
    
         
            +
                      user_config['export_templates'].deep_merge(user_config.delete('html_template'))
         
     | 
| 
      
 265 
     | 
    
         
            +
                    end
         
     | 
| 
      
 266 
     | 
    
         
            +
             
     | 
| 
      
 267 
     | 
    
         
            +
                    user_config['include_notes'] = user_config.delete(':include_notes') if user_config.key?(':include_notes')
         
     | 
| 
      
 268 
     | 
    
         
            +
             
     | 
| 
      
 269 
     | 
    
         
            +
                    user_config.deep_merge(DEFAULTS)
         
     | 
| 
      
 270 
     | 
    
         
            +
                  rescue StandardError => e
         
     | 
| 
      
 271 
     | 
    
         
            +
                    Doing.logger.error('Config:', 'Error reading default configuration')
         
     | 
| 
      
 272 
     | 
    
         
            +
                    Doing.logger.error('Error:', e.message)
         
     | 
| 
      
 273 
     | 
    
         
            +
                    user_config = DEFAULTS
         
     | 
| 
      
 274 
     | 
    
         
            +
                  end
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
                  user_config
         
     | 
| 
      
 277 
     | 
    
         
            +
                end
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
                ##
         
     | 
| 
      
 280 
     | 
    
         
            +
                ## @brief      Finds a project-specific configuration file
         
     | 
| 
      
 281 
     | 
    
         
            +
                ##
         
     | 
| 
      
 282 
     | 
    
         
            +
                ## @return     (String) A file path
         
     | 
| 
      
 283 
     | 
    
         
            +
                ##
         
     | 
| 
      
 284 
     | 
    
         
            +
                def find_local_config
         
     | 
| 
      
 285 
     | 
    
         
            +
                  dir = Dir.pwd
         
     | 
| 
      
 286 
     | 
    
         
            +
             
     | 
| 
      
 287 
     | 
    
         
            +
                  local_config_files = []
         
     | 
| 
      
 288 
     | 
    
         
            +
             
     | 
| 
      
 289 
     | 
    
         
            +
                  while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
         
     | 
| 
      
 290 
     | 
    
         
            +
                    local_config_files.push(File.join(dir, '.doingrc')) if File.exist? File.join(dir, '.doingrc')
         
     | 
| 
      
 291 
     | 
    
         
            +
             
     | 
| 
      
 292 
     | 
    
         
            +
                    dir = File.dirname(dir)
         
     | 
| 
      
 293 
     | 
    
         
            +
                  end
         
     | 
| 
      
 294 
     | 
    
         
            +
             
     | 
| 
      
 295 
     | 
    
         
            +
                  local_config_files.delete(config_file)
         
     | 
| 
      
 296 
     | 
    
         
            +
             
     | 
| 
      
 297 
     | 
    
         
            +
                  local_config_files
         
     | 
| 
      
 298 
     | 
    
         
            +
                end
         
     | 
| 
      
 299 
     | 
    
         
            +
             
     | 
| 
      
 300 
     | 
    
         
            +
                def load_plugins(add_dir = nil)
         
     | 
| 
      
 301 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 302 
     | 
    
         
            +
                    FileUtils.mkdir_p(add_dir) if add_dir && !File.exist?(add_dir)
         
     | 
| 
      
 303 
     | 
    
         
            +
                  rescue
         
     | 
| 
      
 304 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 305 
     | 
    
         
            +
                  end
         
     | 
| 
      
 306 
     | 
    
         
            +
             
     | 
| 
      
 307 
     | 
    
         
            +
                  Plugins.load_plugins(add_dir)
         
     | 
| 
      
 308 
     | 
    
         
            +
                end
         
     | 
| 
      
 309 
     | 
    
         
            +
              end
         
     | 
| 
      
 310 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/doing/errors.rb
    ADDED
    
    | 
         @@ -0,0 +1,102 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Doing
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Errors
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                class UserCancelled < ::StandardError
         
     | 
| 
      
 7 
     | 
    
         
            +
                  def initialize(msg='Cancelled')
         
     | 
| 
      
 8 
     | 
    
         
            +
                    Doing.logger.output_results
         
     | 
| 
      
 9 
     | 
    
         
            +
                    Doing.logger.log_now(:warn, 'Exited:', msg)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    Process.exit 1
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                class EmptyInput < ::StandardError
         
     | 
| 
      
 15 
     | 
    
         
            +
                  def initialize(msg='No input')
         
     | 
| 
      
 16 
     | 
    
         
            +
                    Doing.logger.output_results
         
     | 
| 
      
 17 
     | 
    
         
            +
                    Doing.logger.log_now(:warn, 'Exited:', 'Input empty')
         
     | 
| 
      
 18 
     | 
    
         
            +
                    Process.exit 1
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                class DoingStandardError < ::StandardError
         
     | 
| 
      
 23 
     | 
    
         
            +
                  def initialize(msg='')
         
     | 
| 
      
 24 
     | 
    
         
            +
                    Doing.logger.output_results
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    super
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                class DoingRuntimeError < ::RuntimeError
         
     | 
| 
      
 31 
     | 
    
         
            +
                  def initialize(msg='')
         
     | 
| 
      
 32 
     | 
    
         
            +
                    Doing.logger.output_results
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    super
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                class NoResults < ::StandardError
         
     | 
| 
      
 39 
     | 
    
         
            +
                  def initialize(msg='No results')
         
     | 
| 
      
 40 
     | 
    
         
            +
                    Doing.logger.output_results
         
     | 
| 
      
 41 
     | 
    
         
            +
                    Process.exit 0
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                class DoingNoTraceError < ::StandardError
         
     | 
| 
      
 47 
     | 
    
         
            +
                  def initialize(msg = nil, level = nil)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    level ||= :error
         
     | 
| 
      
 49 
     | 
    
         
            +
                    Doing.logger.output_results
         
     | 
| 
      
 50 
     | 
    
         
            +
                    if msg
         
     | 
| 
      
 51 
     | 
    
         
            +
                      Doing.logger.log_now(level, msg)
         
     | 
| 
      
 52 
     | 
    
         
            +
                    end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                    Process.exit 1
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                class PluginException < ::StandardError
         
     | 
| 
      
 59 
     | 
    
         
            +
                  attr_reader :plugin
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  def initialize(msg = 'Plugin error', type: nil, plugin: nil)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    @plugin = plugin || 'Unknown Plugin'
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    type ||= 'Unknown'
         
     | 
| 
      
 65 
     | 
    
         
            +
                    @type = case type.to_s
         
     | 
| 
      
 66 
     | 
    
         
            +
                            when /^i/
         
     | 
| 
      
 67 
     | 
    
         
            +
                              'Import plugin'
         
     | 
| 
      
 68 
     | 
    
         
            +
                            when /^e/
         
     | 
| 
      
 69 
     | 
    
         
            +
                              'Export plugin'
         
     | 
| 
      
 70 
     | 
    
         
            +
                            else
         
     | 
| 
      
 71 
     | 
    
         
            +
                              type.to_s
         
     | 
| 
      
 72 
     | 
    
         
            +
                            end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    msg = "(#{@type}: #{@plugin}) #{msg}"
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                    Doing.logger.error('Plugin Error:', msg)
         
     | 
| 
      
 77 
     | 
    
         
            +
                    Doing.logger.output_results
         
     | 
| 
      
 78 
     | 
    
         
            +
                    Process.exit 1
         
     | 
| 
      
 79 
     | 
    
         
            +
                  end
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                HookUnavailable = Class.new(PluginException)
         
     | 
| 
      
 83 
     | 
    
         
            +
                InvalidPluginType = Class.new(PluginException)
         
     | 
| 
      
 84 
     | 
    
         
            +
                PluginUncallable = Class.new(PluginException)
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                InvalidArgument = Class.new(DoingRuntimeError)
         
     | 
| 
      
 87 
     | 
    
         
            +
                MissingArgument = Class.new(DoingRuntimeError)
         
     | 
| 
      
 88 
     | 
    
         
            +
                MissingFile = Class.new(DoingRuntimeError)
         
     | 
| 
      
 89 
     | 
    
         
            +
                MissingEditor = Class.new(DoingRuntimeError)
         
     | 
| 
      
 90 
     | 
    
         
            +
                NonInteractive = Class.new(StandardError)
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                NoEntryError = Class.new(DoingRuntimeError)
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                InvalidTimeExpression = Class.new(DoingRuntimeError)
         
     | 
| 
      
 95 
     | 
    
         
            +
                InvalidSection = Class.new(DoingRuntimeError)
         
     | 
| 
      
 96 
     | 
    
         
            +
                InvalidView = Class.new(DoingRuntimeError)
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                ItemNotFound = Class.new(DoingRuntimeError)
         
     | 
| 
      
 99 
     | 
    
         
            +
                # FatalException = Class.new(::RuntimeError)
         
     | 
| 
      
 100 
     | 
    
         
            +
                # InvalidPluginName = Class.new(FatalException)
         
     | 
| 
      
 101 
     | 
    
         
            +
              end
         
     | 
| 
      
 102 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/doing/hash.rb
    ADDED
    
    | 
         @@ -0,0 +1,31 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Doing
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Hash helpers
         
     | 
| 
      
 5 
     | 
    
         
            +
              class ::Hash
         
     | 
| 
      
 6 
     | 
    
         
            +
                ##
         
     | 
| 
      
 7 
     | 
    
         
            +
                ## @brief      Freeze all values in a hash
         
     | 
| 
      
 8 
     | 
    
         
            +
                ##
         
     | 
| 
      
 9 
     | 
    
         
            +
                ## @return     { description_of_the_return_value }
         
     | 
| 
      
 10 
     | 
    
         
            +
                ##
         
     | 
| 
      
 11 
     | 
    
         
            +
                def deep_freeze
         
     | 
| 
      
 12 
     | 
    
         
            +
                  map { |k, v| v.is_a?(Hash) ? v.deep_freeze : v.freeze }.freeze
         
     | 
| 
      
 13 
     | 
    
         
            +
                end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def deep_freeze!
         
     | 
| 
      
 16 
     | 
    
         
            +
                  replace deep_freeze
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # Turn all keys into string
         
     | 
| 
      
 20 
     | 
    
         
            +
                #
         
     | 
| 
      
 21 
     | 
    
         
            +
                # Return a copy of the hash where all its keys are strings
         
     | 
| 
      
 22 
     | 
    
         
            +
                def stringify_keys
         
     | 
| 
      
 23 
     | 
    
         
            +
                  each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v.is_a?(Hash) ? v.stringify_keys : v }
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                # Turn all keys into symbols
         
     | 
| 
      
 27 
     | 
    
         
            +
                def symbolize_keys
         
     | 
| 
      
 28 
     | 
    
         
            +
                  each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/doing/hooks.rb
    ADDED
    
    | 
         @@ -0,0 +1,59 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Doing
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Hook manager
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Hooks
         
     | 
| 
      
 6 
     | 
    
         
            +
                DEFAULT_PRIORITY = 20
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                @registry = {
         
     | 
| 
      
 9 
     | 
    
         
            +
                  post_config: [],
         
     | 
| 
      
 10 
     | 
    
         
            +
                  post_local_config: [],
         
     | 
| 
      
 11 
     | 
    
         
            +
                  post_read: [],
         
     | 
| 
      
 12 
     | 
    
         
            +
                  pre_write: [],
         
     | 
| 
      
 13 
     | 
    
         
            +
                  post_write: []
         
     | 
| 
      
 14 
     | 
    
         
            +
                }
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                # map of all hooks and their priorities
         
     | 
| 
      
 17 
     | 
    
         
            +
                @hook_priority = {}
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # register hook(s) to be called later, public API
         
     | 
| 
      
 20 
     | 
    
         
            +
                def self.register(event, priority: DEFAULT_PRIORITY, &block)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  register_one(event, priority_value(priority), &block)
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                # Ensure the priority is a Fixnum
         
     | 
| 
      
 25 
     | 
    
         
            +
                def self.priority_value(priority)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  return priority if priority.is_a?(Integer)
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  PRIORITY_MAP[priority] || DEFAULT_PRIORITY
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                # register a single hook to be called later, internal API
         
     | 
| 
      
 32 
     | 
    
         
            +
                def self.register_one(event, priority, &block)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  unless @registry[event]
         
     | 
| 
      
 34 
     | 
    
         
            +
                    raise Doing::Errors::HookUnavailable, "Invalid hook. Doing only supports #{@registry.keys.inspect}"
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  raise Doing::Errors::PluginUncallable, 'Hooks must respond to :call' unless block.respond_to? :call
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  Doing.logger.debug('Hook Manager:', "Registered #{event} hook") if ENV['DOING_PLUGIN_DEBUG']
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  insert_hook event, priority, &block
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def self.insert_hook(event, priority, &block)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  @hook_priority[block] = [-priority, @hook_priority.size]
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @registry[event] << block
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                def self.trigger(event, *args)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  hooks = @registry[event]
         
     | 
| 
      
 51 
     | 
    
         
            +
                  return if hooks.nil? || hooks.empty?
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  # sort and call hooks according to priority and load order
         
     | 
| 
      
 54 
     | 
    
         
            +
                  hooks.sort_by { |h| @hook_priority[h] }.each do |hook|
         
     | 
| 
      
 55 
     | 
    
         
            +
                    hook.call(*args)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  end
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/doing/item.rb
    ADDED
    
    | 
         @@ -0,0 +1,155 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Doing
         
     | 
| 
      
 4 
     | 
    
         
            +
              ##
         
     | 
| 
      
 5 
     | 
    
         
            +
              ## @brief      This class describes a single WWID item
         
     | 
| 
      
 6 
     | 
    
         
            +
              ##
         
     | 
| 
      
 7 
     | 
    
         
            +
              class Item
         
     | 
| 
      
 8 
     | 
    
         
            +
                attr_accessor :date, :title, :section, :note
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def initialize(date, title, section, note = nil)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @date = date.is_a?(Time) ? date : Time.parse(date)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @title = title
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @section = section
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @note = Note.new(note)
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                # def date=(new_date)
         
     | 
| 
      
 18 
     | 
    
         
            +
                #   @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
         
     | 
| 
      
 19 
     | 
    
         
            +
                # end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def interval
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @interval ||= calc_interval
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                def end_date
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def equal?(other)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  return false if @title.strip != other.title.strip
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  return false if @date != other.date
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  return false unless @note.equal?(other.note)
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  true
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                def same_time?(item_b)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  date == item_b.date ? interval == item_b.interval : false
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def overlapping_time?(item_b)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  return true if same_time?(item_b)
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  start_a = date
         
     | 
| 
      
 47 
     | 
    
         
            +
                  interval = interval
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end_a = interval ? start_a + interval.to_i : start_a
         
     | 
| 
      
 49 
     | 
    
         
            +
                  start_b = item_b.date
         
     | 
| 
      
 50 
     | 
    
         
            +
                  interval = item_b.interval
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end_b = interval ? start_b + interval.to_i : start_b
         
     | 
| 
      
 52 
     | 
    
         
            +
                  (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                def tag(tag, value: nil, remove: false, rename_to: nil, regex: false)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  @title.tag!(tag, value: value, remove: remove, rename_to: rename_to, regex: regex).strip!
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                def tags
         
     | 
| 
      
 60 
     | 
    
         
            +
                  @title.scan(/(?<= |\A)@([^\s(]+)/).map {|tag| tag[0]}.sort.uniq
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                def tags?(tags, bool = :and)
         
     | 
| 
      
 64 
     | 
    
         
            +
                  tags = split_tags(tags)
         
     | 
| 
      
 65 
     | 
    
         
            +
                  bool = bool.normalize_bool
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  case bool
         
     | 
| 
      
 68 
     | 
    
         
            +
                  when :and
         
     | 
| 
      
 69 
     | 
    
         
            +
                    all_tags?(tags)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  when :not
         
     | 
| 
      
 71 
     | 
    
         
            +
                    no_tags?(tags)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  else
         
     | 
| 
      
 73 
     | 
    
         
            +
                    any_tags?(tags)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  end
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                def search(search)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  text = @title + @note.to_s
         
     | 
| 
      
 79 
     | 
    
         
            +
                  pattern = case search.strip
         
     | 
| 
      
 80 
     | 
    
         
            +
                            when %r{^/.*?/$}
         
     | 
| 
      
 81 
     | 
    
         
            +
                              search.sub(%r{/(.*?)/}, '\1')
         
     | 
| 
      
 82 
     | 
    
         
            +
                            when /^'/
         
     | 
| 
      
 83 
     | 
    
         
            +
                              case_sensitive = true
         
     | 
| 
      
 84 
     | 
    
         
            +
                              search.sub(/^'(.*?)'?$/, '\1')
         
     | 
| 
      
 85 
     | 
    
         
            +
                            else
         
     | 
| 
      
 86 
     | 
    
         
            +
                              case_sensitive = true if search =~ /[A-Z]/
         
     | 
| 
      
 87 
     | 
    
         
            +
                              search.split('').join('.{0,3}')
         
     | 
| 
      
 88 
     | 
    
         
            +
                            end
         
     | 
| 
      
 89 
     | 
    
         
            +
                  rx = Regexp.new(pattern, !case_sensitive)
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                  text =~ rx
         
     | 
| 
      
 92 
     | 
    
         
            +
                end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                def should_finish?
         
     | 
| 
      
 95 
     | 
    
         
            +
                  should?('never_finish')
         
     | 
| 
      
 96 
     | 
    
         
            +
                end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                def should_time?
         
     | 
| 
      
 99 
     | 
    
         
            +
                  should?('never_time')
         
     | 
| 
      
 100 
     | 
    
         
            +
                end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                private
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                def should?(key)
         
     | 
| 
      
 105 
     | 
    
         
            +
                  config = Doing.config.settings
         
     | 
| 
      
 106 
     | 
    
         
            +
                  return true unless config[key].is_a?(Array)
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                  config[key].each do |tag|
         
     | 
| 
      
 109 
     | 
    
         
            +
                    if tag =~ /^@/
         
     | 
| 
      
 110 
     | 
    
         
            +
                      return false if tags?(tag.sub(/^@/, '').downcase)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    elsif section.downcase == tag.downcase
         
     | 
| 
      
 112 
     | 
    
         
            +
                      return false
         
     | 
| 
      
 113 
     | 
    
         
            +
                    end
         
     | 
| 
      
 114 
     | 
    
         
            +
                  end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                  true
         
     | 
| 
      
 117 
     | 
    
         
            +
                end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                def calc_interval
         
     | 
| 
      
 120 
     | 
    
         
            +
                  done = end_date
         
     | 
| 
      
 121 
     | 
    
         
            +
                  return nil if done.nil?
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                  start = @date
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                  t = (done - start).to_i
         
     | 
| 
      
 126 
     | 
    
         
            +
                  t > 0 ? t : nil
         
     | 
| 
      
 127 
     | 
    
         
            +
                end
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
                def all_tags?(tags)
         
     | 
| 
      
 130 
     | 
    
         
            +
                  tags.each do |tag|
         
     | 
| 
      
 131 
     | 
    
         
            +
                    return false unless @title =~ /@#{tag}/
         
     | 
| 
      
 132 
     | 
    
         
            +
                  end
         
     | 
| 
      
 133 
     | 
    
         
            +
                  true
         
     | 
| 
      
 134 
     | 
    
         
            +
                end
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                def no_tags?(tags)
         
     | 
| 
      
 137 
     | 
    
         
            +
                  tags.each do |tag|
         
     | 
| 
      
 138 
     | 
    
         
            +
                    return false if @title =~ /@#{tag}/
         
     | 
| 
      
 139 
     | 
    
         
            +
                  end
         
     | 
| 
      
 140 
     | 
    
         
            +
                  true
         
     | 
| 
      
 141 
     | 
    
         
            +
                end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                def any_tags?(tags)
         
     | 
| 
      
 144 
     | 
    
         
            +
                  tags.each do |tag|
         
     | 
| 
      
 145 
     | 
    
         
            +
                    return true if @title =~ /@#{tag}/
         
     | 
| 
      
 146 
     | 
    
         
            +
                  end
         
     | 
| 
      
 147 
     | 
    
         
            +
                  false
         
     | 
| 
      
 148 
     | 
    
         
            +
                end
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                def split_tags(tags)
         
     | 
| 
      
 151 
     | 
    
         
            +
                  tags = tags.split(/ *, */) if tags.is_a? String
         
     | 
| 
      
 152 
     | 
    
         
            +
                  tags.map { |t| t.strip.sub(/^@/, '') }
         
     | 
| 
      
 153 
     | 
    
         
            +
                end
         
     | 
| 
      
 154 
     | 
    
         
            +
              end
         
     | 
| 
      
 155 
     | 
    
         
            +
            end
         
     |