doing 2.0.25 → 2.1.0pre
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/.yardoc/checksums +18 -15
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile.lock +8 -1
- data/README.md +1 -1
- data/Rakefile +23 -4
- data/bin/doing +205 -127
- data/doc/Array.html +354 -1
- data/doc/Doing/Color.html +104 -92
- data/doc/Doing/Completion.html +216 -0
- data/doc/Doing/Configuration.html +340 -5
- data/doc/Doing/Content.html +229 -0
- data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/doc/Doing/Errors/EmptyInput.html +1 -1
- data/doc/Doing/Errors/NoResults.html +1 -1
- data/doc/Doing/Errors/PluginException.html +1 -1
- data/doc/Doing/Errors/UserCancelled.html +1 -1
- data/doc/Doing/Errors/WrongCommand.html +1 -1
- data/doc/Doing/Errors.html +1 -1
- data/doc/Doing/Hooks.html +1 -1
- data/doc/Doing/Item.html +337 -49
- data/doc/Doing/Items.html +444 -35
- data/doc/Doing/LogAdapter.html +139 -51
- data/doc/Doing/Note.html +253 -22
- data/doc/Doing/Pager.html +74 -36
- data/doc/Doing/Plugins.html +1 -1
- data/doc/Doing/Prompt.html +674 -0
- data/doc/Doing/Section.html +354 -0
- data/doc/Doing/Util.html +57 -1
- data/doc/Doing/WWID.html +477 -670
- data/doc/Doing/WWIDFile.html +398 -0
- data/doc/Doing.html +5 -5
- data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/doc/GLI/Commands.html +1 -1
- data/doc/GLI.html +1 -1
- data/doc/Hash.html +97 -1
- data/doc/Status.html +37 -3
- data/doc/String.html +599 -23
- data/doc/Symbol.html +3 -3
- data/doc/Time.html +1 -1
- data/doc/_index.html +22 -1
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +8 -2
- data/doc/index.html +8 -2
- data/doc/method_list.html +453 -173
- data/doc/top-level-namespace.html +1 -1
- data/doing.gemspec +3 -0
- data/doing.rdoc +40 -12
- data/example_plugin.rb +3 -3
- data/lib/completion/_doing.zsh +1 -1
- data/lib/completion/doing.bash +8 -8
- data/lib/completion/doing.fish +1 -1
- data/lib/doing/array.rb +36 -0
- data/lib/doing/colors.rb +70 -66
- data/lib/doing/completion.rb +6 -0
- data/lib/doing/configuration.rb +69 -28
- data/lib/doing/hash.rb +37 -0
- data/lib/doing/item.rb +77 -12
- data/lib/doing/items.rb +125 -0
- data/lib/doing/log_adapter.rb +55 -3
- data/lib/doing/note.rb +53 -1
- data/lib/doing/pager.rb +49 -38
- data/lib/doing/plugins/export/markdown_export.rb +4 -4
- data/lib/doing/plugins/export/template_export.rb +2 -2
- data/lib/doing/plugins/import/calendar_import.rb +4 -4
- data/lib/doing/plugins/import/doing_import.rb +5 -7
- data/lib/doing/plugins/import/timing_import.rb +3 -3
- data/lib/doing/prompt.rb +206 -0
- data/lib/doing/section.rb +30 -0
- data/lib/doing/string.rb +103 -27
- data/lib/doing/util.rb +14 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +306 -621
- data/lib/doing.rb +6 -2
- data/lib/examples/plugins/capture_thing_import.rb +162 -0
- metadata +73 -5
- data/lib/doing/wwidfile.rb +0 -117
| @@ -34,9 +34,7 @@ module Doing | |
| 34 34 | 
             
                  tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
         | 
| 35 35 | 
             
                  prefix = options[:prefix] || ''
         | 
| 36 36 |  | 
| 37 | 
            -
                  @old_items =  | 
| 38 | 
            -
             | 
| 39 | 
            -
                  wwid.content.each { |_, v| @old_items.concat(v[:items]) }
         | 
| 37 | 
            +
                  @old_items = wwid.content.dup
         | 
| 40 38 |  | 
| 41 39 | 
             
                  new_items = read_doing_file(path)
         | 
| 42 40 |  | 
| @@ -46,7 +44,7 @@ module Doing | |
| 46 44 | 
             
                  new_items = wwid.filter_items(new_items, opt: options)
         | 
| 47 45 |  | 
| 48 46 | 
             
                  skipped = total - new_items.count
         | 
| 49 | 
            -
                  Doing.logger.debug('Skipped:' | 
| 47 | 
            +
                  Doing.logger.debug('Skipped:', %(#{skipped} items that didn't match filter criteria)) if skipped.positive?
         | 
| 50 48 |  | 
| 51 49 | 
             
                  imported = []
         | 
| 52 50 |  | 
| @@ -76,13 +74,13 @@ module Doing | |
| 76 74 | 
             
                  dups = new_items.count - imported.count
         | 
| 77 75 | 
             
                  Doing.logger.info('Skipped:', %(#{dups} duplicate items)) if dups.positive?
         | 
| 78 76 |  | 
| 79 | 
            -
                  imported = wwid.dedup(imported, !options[:overlap])
         | 
| 77 | 
            +
                  imported = wwid.dedup(imported, no_overlap: !options[:overlap])
         | 
| 80 78 | 
             
                  overlaps = new_items.count - imported.count - dups
         | 
| 81 79 | 
             
                  Doing.logger.debug('Skipped:', "#{overlaps} items with overlapping times") if overlaps.positive?
         | 
| 82 80 |  | 
| 83 81 | 
             
                  imported.each do |item|
         | 
| 84 | 
            -
                    wwid.add_section(item.section) unless wwid.content. | 
| 85 | 
            -
                    wwid.content | 
| 82 | 
            +
                    wwid.content.add_section(item.section) unless wwid.content.section?(item.section)
         | 
| 83 | 
            +
                    wwid.content.push(item)
         | 
| 86 84 | 
             
                  end
         | 
| 87 85 |  | 
| 88 86 | 
             
                  Doing.logger.info('Imported:', "#{imported.count} items")
         | 
| @@ -27,7 +27,7 @@ module Doing | |
| 27 27 | 
             
                  section = options[:section] || wwid.config['current_section']
         | 
| 28 28 | 
             
                  options[:no_overlap] ||= false
         | 
| 29 29 | 
             
                  options[:autotag] ||= wwid.auto_tag
         | 
| 30 | 
            -
                  wwid.add_section(section) unless wwid.content. | 
| 30 | 
            +
                  wwid.content.add_section(section) unless wwid.content.section?(section)
         | 
| 31 31 |  | 
| 32 32 | 
             
                  add_tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
         | 
| 33 33 | 
             
                  prefix = options[:prefix] || '[Timing.app]'
         | 
| @@ -73,11 +73,11 @@ module Doing | |
| 73 73 | 
             
                  filtered = skipped - new_items.count
         | 
| 74 74 | 
             
                  Doing.logger.debug('Skipped:' , %(#{filtered} items that didn't match filter criteria)) if filtered.positive?
         | 
| 75 75 |  | 
| 76 | 
            -
                  new_items = wwid.dedup(new_items, options[:no_overlap])
         | 
| 76 | 
            +
                  new_items = wwid.dedup(new_items, no_overlap: options[:no_overlap])
         | 
| 77 77 | 
             
                  dups = filtered - new_items.count
         | 
| 78 78 | 
             
                  Doing.logger.debug('Skipped:' , %(#{dups} items with overlapping times)) if dups.positive?
         | 
| 79 79 |  | 
| 80 | 
            -
                  wwid.content | 
| 80 | 
            +
                  wwid.content.concat(new_items)
         | 
| 81 81 | 
             
                  Doing.logger.info('Imported:', %(#{new_items.count} items to #{section}))
         | 
| 82 82 | 
             
                end
         | 
| 83 83 |  | 
    
        data/lib/doing/prompt.rb
    ADDED
    
    | @@ -0,0 +1,206 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Doing
         | 
| 4 | 
            +
              # Terminal Prompt methods
         | 
| 5 | 
            +
              module Prompt
         | 
| 6 | 
            +
                class << self
         | 
| 7 | 
            +
                  attr_writer :force_answer, :default_answer
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  include Color
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def force_answer
         | 
| 12 | 
            +
                    @force_answer ||= nil
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def default_answer
         | 
| 16 | 
            +
                    @default_answer ||= false
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  ##
         | 
| 20 | 
            +
                  ## Ask a yes or no question in the terminal
         | 
| 21 | 
            +
                  ##
         | 
| 22 | 
            +
                  ## @param      question          [String] The question
         | 
| 23 | 
            +
                  ##                               to ask
         | 
| 24 | 
            +
                  ## @param      default_response  (Bool)   default
         | 
| 25 | 
            +
                  ##                               response if no input
         | 
| 26 | 
            +
                  ##
         | 
| 27 | 
            +
                  ## @return     (Bool) yes or no
         | 
| 28 | 
            +
                  ##
         | 
| 29 | 
            +
                  def yn(question, default_response: false)
         | 
| 30 | 
            +
                    unless @force_answer.nil?
         | 
| 31 | 
            +
                      return @force_answer
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    default = if default_response.is_a?(String)
         | 
| 35 | 
            +
                                default_response =~ /y/i ? true : false
         | 
| 36 | 
            +
                              else
         | 
| 37 | 
            +
                                default_response
         | 
| 38 | 
            +
                              end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    # if global --default is set, answer default
         | 
| 41 | 
            +
                    return default if @default_answer
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    # if this isn't an interactive shell, answer default
         | 
| 44 | 
            +
                    return default unless $stdout.isatty
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    # clear the buffer
         | 
| 47 | 
            +
                    if ARGV&.length
         | 
| 48 | 
            +
                      ARGV.length.times do
         | 
| 49 | 
            +
                        ARGV.shift
         | 
| 50 | 
            +
                      end
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                    system 'stty cbreak'
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    cw = white
         | 
| 55 | 
            +
                    cbw = boldwhite
         | 
| 56 | 
            +
                    cbg = boldgreen
         | 
| 57 | 
            +
                    cd = Color.default
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    options = unless default.nil?
         | 
| 60 | 
            +
                                "#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
         | 
| 61 | 
            +
                              else
         | 
| 62 | 
            +
                                "#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
         | 
| 63 | 
            +
                              end
         | 
| 64 | 
            +
                    $stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
         | 
| 65 | 
            +
                    res = $stdin.sysread 1
         | 
| 66 | 
            +
                    puts
         | 
| 67 | 
            +
                    system 'stty cooked'
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    res.chomp!
         | 
| 70 | 
            +
                    res.downcase!
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    return default if res.empty?
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    res =~ /y/i ? true : false
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def fzf
         | 
| 78 | 
            +
                    @fzf ||= install_fzf
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def install_fzf
         | 
| 82 | 
            +
                    fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
         | 
| 83 | 
            +
                    FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
         | 
| 84 | 
            +
                    fzf_bin = File.join(fzf_dir, 'bin/fzf')
         | 
| 85 | 
            +
                    return fzf_bin if File.exist?(fzf_bin)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    prev_level = Doing.logger.level
         | 
| 88 | 
            +
                    Doing.logger.adjust_verbosity({ log_level: :info })
         | 
| 89 | 
            +
                    Doing.logger.log_now(:warn, 'Compiling and installing fzf -- this will only happen once')
         | 
| 90 | 
            +
                    Doing.logger.log_now(:warn, 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                    system("'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
         | 
| 93 | 
            +
                    unless File.exist?(fzf_bin)
         | 
| 94 | 
            +
                      Doing.logger.log_now(:warn, 'Error installing, trying again as root')
         | 
| 95 | 
            +
                      system("sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                    raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues') unless File.exist?(fzf_bin)
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    Doing.logger.info("fzf installed to #{fzf}")
         | 
| 100 | 
            +
                    Doing.logger.adjust_verbosity({ log_level: prev_level })
         | 
| 101 | 
            +
                    fzf_bin
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  ##
         | 
| 105 | 
            +
                  ## Generate a menu of options and allow user selection
         | 
| 106 | 
            +
                  ##
         | 
| 107 | 
            +
                  ## @return     [String] The selected option
         | 
| 108 | 
            +
                  ##
         | 
| 109 | 
            +
                  def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
         | 
| 110 | 
            +
                    return nil unless $stdout.isatty
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
         | 
| 113 | 
            +
                    fzf_args << %(--prompt "#{prompt}")
         | 
| 114 | 
            +
                    fzf_args << '--multi' if multiple
         | 
| 115 | 
            +
                    header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
         | 
| 116 | 
            +
                    fzf_args << %(--header "#{header}")
         | 
| 117 | 
            +
                    options.sort! if sorted
         | 
| 118 | 
            +
                    res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
         | 
| 119 | 
            +
                    return false if res.strip.size.zero?
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                    res
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  ##
         | 
| 125 | 
            +
                  ## Create an interactive menu to select from a set of Items
         | 
| 126 | 
            +
                  ##
         | 
| 127 | 
            +
                  ## @param      items            [Array] list of items
         | 
| 128 | 
            +
                  ## @param      opt              [Hash] options
         | 
| 129 | 
            +
                  ## @param      include_section  [Boolean] include section
         | 
| 130 | 
            +
                  ##
         | 
| 131 | 
            +
                  ## @option opt [String] :header
         | 
| 132 | 
            +
                  ## @option opt [String] :prompt
         | 
| 133 | 
            +
                  ## @option opt [String] :query
         | 
| 134 | 
            +
                  ## @option opt [Boolean] :show_if_single
         | 
| 135 | 
            +
                  ## @option opt [Boolean] :menu
         | 
| 136 | 
            +
                  ## @option opt [Boolean] :sort
         | 
| 137 | 
            +
                  ## @option opt [Boolean] :multiple
         | 
| 138 | 
            +
                  ## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
         | 
| 139 | 
            +
                  ##
         | 
| 140 | 
            +
                  def choose_from_items(items, **opt)
         | 
| 141 | 
            +
                    return items unless $stdout.isatty
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                    return nil unless items.count.positive?
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                    case_sensitive = opt.fetch(:case, :smart).normalize_case
         | 
| 146 | 
            +
                    header = opt.fetch(:header, 'Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit')
         | 
| 147 | 
            +
                    prompt = opt.fetch(:prompt, 'Select entries to act on > ')
         | 
| 148 | 
            +
                    query = opt.fetch(:query) { opt.fetch(:search, '') }
         | 
| 149 | 
            +
                    include_section = opt.fetch(:include_section, false)
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                    pad = items.length.to_s.length
         | 
| 152 | 
            +
                    options = items.map.with_index do |item, i|
         | 
| 153 | 
            +
                      out = [
         | 
| 154 | 
            +
                        format("%#{pad}d", i),
         | 
| 155 | 
            +
                        ') ',
         | 
| 156 | 
            +
                        format('%13s', item.date.relative_date),
         | 
| 157 | 
            +
                        ' | ',
         | 
| 158 | 
            +
                        item.title
         | 
| 159 | 
            +
                      ]
         | 
| 160 | 
            +
                      if include_section
         | 
| 161 | 
            +
                        out.concat([
         | 
| 162 | 
            +
                          ' (',
         | 
| 163 | 
            +
                          item.section,
         | 
| 164 | 
            +
                          ') '
         | 
| 165 | 
            +
                        ])
         | 
| 166 | 
            +
                      end
         | 
| 167 | 
            +
                      out.join('')
         | 
| 168 | 
            +
                    end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                    fzf_args = [
         | 
| 171 | 
            +
                      %(--header="#{header}"),
         | 
| 172 | 
            +
                      %(--prompt="#{prompt.sub(/ *$/, ' ')}"),
         | 
| 173 | 
            +
                      opt.fetch(:multiple) ? '--multi' : '--no-multi',
         | 
| 174 | 
            +
                      '-0',
         | 
| 175 | 
            +
                      '--bind ctrl-a:select-all',
         | 
| 176 | 
            +
                      %(-q "#{query}"),
         | 
| 177 | 
            +
                      '--info=inline'
         | 
| 178 | 
            +
                    ]
         | 
| 179 | 
            +
                    fzf_args.push('-1') unless opt.fetch(:show_if_single)
         | 
| 180 | 
            +
                    fzf_args << case case_sensitive
         | 
| 181 | 
            +
                                when :sensitive
         | 
| 182 | 
            +
                                  '+i'
         | 
| 183 | 
            +
                                when :ignore
         | 
| 184 | 
            +
                                  '-i'
         | 
| 185 | 
            +
                                end
         | 
| 186 | 
            +
                    fzf_args << '-e' if opt.fetch(:exact, false)
         | 
| 187 | 
            +
             | 
| 188 | 
            +
             | 
| 189 | 
            +
                    unless opt.fetch(:menu)
         | 
| 190 | 
            +
                      raise InvalidArgument, "Can't skip menu when no query is provided" unless query && !query.empty?
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                      fzf_args.concat([%(--filter="#{query}"), opt.fetch(:sort) ? '' : '--no-sort'])
         | 
| 193 | 
            +
                    end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                    res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
         | 
| 196 | 
            +
                    selected = []
         | 
| 197 | 
            +
                    res.split(/\n/).each do |item|
         | 
| 198 | 
            +
                      idx = item.match(/^ *(\d+)\)/)[1].to_i
         | 
| 199 | 
            +
                      selected.push(items[idx])
         | 
| 200 | 
            +
                    end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                    opt.fetch(:multiple) ? selected : selected[0]
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
              end
         | 
| 206 | 
            +
            end
         | 
| @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Doing
         | 
| 4 | 
            +
              # Section Object
         | 
| 5 | 
            +
              class Section
         | 
| 6 | 
            +
                attr_accessor :original, :title
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(title, original: nil)
         | 
| 9 | 
            +
                  super()
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  @title = title
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  @original = if original.nil?
         | 
| 14 | 
            +
                                "#{title}:"
         | 
| 15 | 
            +
                              else
         | 
| 16 | 
            +
                                original =~ /:(\s+@\S+(\(.*?\))?)*$/ ? original : "#{original}:"
         | 
| 17 | 
            +
                              end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # Outputs section title
         | 
| 21 | 
            +
                def to_s
         | 
| 22 | 
            +
                  @title
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                # @private
         | 
| 26 | 
            +
                def inspect
         | 
| 27 | 
            +
                  %(#<Doing::Section @title="#{@title}" @original="#{@original}">)
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
    
        data/lib/doing/string.rb
    CHANGED
    
    | @@ -20,8 +20,7 @@ module Doing | |
| 20 20 | 
             
                ## can be separated by up to *distance* characters in
         | 
| 21 21 | 
             
                ## haystack, spaces indicate unlimited distance.
         | 
| 22 22 | 
             
                ##
         | 
| 23 | 
            -
                ## @example    "this word".to_rx(2) =>
         | 
| 24 | 
            -
                ## /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/
         | 
| 23 | 
            +
                ## @example    `"this word".to_rx(2) => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/`
         | 
| 25 24 | 
             
                ##
         | 
| 26 25 | 
             
                ## @param      distance   [Integer] Allowed distance
         | 
| 27 26 | 
             
                ##                        between characters
         | 
| @@ -63,6 +62,15 @@ module Doing | |
| 63 62 | 
             
                  end
         | 
| 64 63 | 
             
                end
         | 
| 65 64 |  | 
| 65 | 
            +
                # Compress multiple spaces to single space
         | 
| 66 | 
            +
                def compress
         | 
| 67 | 
            +
                  gsub(/ +/, ' ').strip
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def compress!
         | 
| 71 | 
            +
                  replace compress
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 66 74 | 
             
                ## @param (see #highlight_tags)
         | 
| 67 75 | 
             
                def highlight_tags!(color = 'yellow')
         | 
| 68 76 | 
             
                  replace highlight_tags(color)
         | 
| @@ -289,11 +297,29 @@ module Doing | |
| 289 297 | 
             
                  title
         | 
| 290 298 | 
             
                end
         | 
| 291 299 |  | 
| 292 | 
            -
                 | 
| 293 | 
            -
             | 
| 300 | 
            +
                ##
         | 
| 301 | 
            +
                ## Add, rename, or remove a tag in place
         | 
| 302 | 
            +
                ##
         | 
| 303 | 
            +
                ## @see #tag
         | 
| 304 | 
            +
                ##
         | 
| 305 | 
            +
                def tag!(tag, **options)
         | 
| 306 | 
            +
                  replace tag(tag, **options)
         | 
| 294 307 | 
             
                end
         | 
| 295 308 |  | 
| 296 | 
            -
                 | 
| 309 | 
            +
                ##
         | 
| 310 | 
            +
                ## Add, rename, or remove a tag
         | 
| 311 | 
            +
                ##
         | 
| 312 | 
            +
                ## @param      tag        The tag
         | 
| 313 | 
            +
                ## @param      value      [String] Value for tag (@tag(value))
         | 
| 314 | 
            +
                ## @param      remove     [Boolean] Remove the tag instead of adding
         | 
| 315 | 
            +
                ## @param      rename_to  [String] Replace tag with this tag
         | 
| 316 | 
            +
                ## @param      regex      [Boolean] Tag is regular expression
         | 
| 317 | 
            +
                ## @param      single     [Boolean] Operating on a single item (for logging)
         | 
| 318 | 
            +
                ## @param      force      [Boolean] With rename_to, add tag if it doesn't exist
         | 
| 319 | 
            +
                ##
         | 
| 320 | 
            +
                ## @return     [String] The string with modified tags
         | 
| 321 | 
            +
                ##
         | 
| 322 | 
            +
                def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false)
         | 
| 297 323 | 
             
                  log_level = single ? :info : :debug
         | 
| 298 324 | 
             
                  title = dup
         | 
| 299 325 | 
             
                  title.chomp!
         | 
| @@ -307,13 +333,14 @@ module Doing | |
| 307 333 | 
             
                           end
         | 
| 308 334 |  | 
| 309 335 | 
             
                  if remove || rename_to
         | 
| 310 | 
            -
                     | 
| 336 | 
            +
                    rx = Regexp.new("(?<=^| )@#{rx_tag}(?<parens>\\((?<value>[^)]*)\\))?(?= |$)", case_sensitive)
         | 
| 337 | 
            +
                    m = title.match(rx)
         | 
| 311 338 |  | 
| 312 | 
            -
                     | 
| 313 | 
            -
             | 
| 339 | 
            +
                    if m.nil? && rename_to && force
         | 
| 340 | 
            +
                      title.tag!(rename_to, value: value, single: single)
         | 
| 341 | 
            +
                    elsif m
         | 
| 314 342 | 
             
                      title.gsub!(rx) do
         | 
| 315 | 
            -
                        m  | 
| 316 | 
            -
                        rename_to ? "#{m[1]}@#{rename_to}#{m[2]}" : m[1]
         | 
| 343 | 
            +
                        rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : ''
         | 
| 317 344 | 
             
                      end
         | 
| 318 345 |  | 
| 319 346 | 
             
                      title.dedup_tags!
         | 
| @@ -373,9 +400,16 @@ module Doing | |
| 373 400 | 
             
                  title
         | 
| 374 401 | 
             
                end
         | 
| 375 402 |  | 
| 376 | 
            -
                # Returns the last escape sequence from a string
         | 
| 403 | 
            +
                # Returns the last escape sequence from a string.
         | 
| 377 404 | 
             
                #
         | 
| 378 | 
            -
                #  | 
| 405 | 
            +
                # Actually returns all escape codes, with the assumption
         | 
| 406 | 
            +
                # that the result of inserting them will generate the
         | 
| 407 | 
            +
                # same color as was set at the end of the string.
         | 
| 408 | 
            +
                # Because you can send modifiers like dark and bold
         | 
| 409 | 
            +
                # separate from color codes, only using the last code
         | 
| 410 | 
            +
                # may not render the same style.
         | 
| 411 | 
            +
                #
         | 
| 412 | 
            +
                # @return     [String]  All escape codes in string
         | 
| 379 413 | 
             
                #
         | 
| 380 414 | 
             
                def last_color
         | 
| 381 415 | 
             
                  scan(/\e\[[\d;]+m/).join('')
         | 
| @@ -386,17 +420,20 @@ module Doing | |
| 386 420 | 
             
                ##
         | 
| 387 421 | 
             
                ## @param      opt   [Hash] Additional Options
         | 
| 388 422 | 
             
                ##
         | 
| 389 | 
            -
                def link_urls!(opt | 
| 390 | 
            -
                   | 
| 423 | 
            +
                def link_urls!(**opt)
         | 
| 424 | 
            +
                  fmt = opt.fetch(:format, :html)
         | 
| 425 | 
            +
                  replace link_urls(format: fmt)
         | 
| 391 426 | 
             
                end
         | 
| 392 427 |  | 
| 393 | 
            -
                def link_urls(opt | 
| 394 | 
            -
                  opt | 
| 428 | 
            +
                def link_urls(**opt)
         | 
| 429 | 
            +
                  fmt = opt.fetch(:format, :html)
         | 
| 430 | 
            +
                  return self unless fmt
         | 
| 431 | 
            +
             | 
| 395 432 | 
             
                  str = dup
         | 
| 396 433 |  | 
| 397 | 
            -
                  str = str.remove_self_links if  | 
| 434 | 
            +
                  str = str.remove_self_links if fmt == :markdown
         | 
| 398 435 |  | 
| 399 | 
            -
                  str.replace_qualified_urls(format:  | 
| 436 | 
            +
                  str.replace_qualified_urls(format: fmt).clean_unlinked_urls
         | 
| 400 437 | 
             
                end
         | 
| 401 438 |  | 
| 402 439 | 
             
                # Remove <self-linked> formatting
         | 
| @@ -412,21 +449,24 @@ module Doing | |
| 412 449 | 
             
                end
         | 
| 413 450 |  | 
| 414 451 | 
             
                # Replace qualified urls
         | 
| 415 | 
            -
                def replace_qualified_urls( | 
| 416 | 
            -
                   | 
| 452 | 
            +
                def replace_qualified_urls(**options)
         | 
| 453 | 
            +
                  fmt = options.fetch(:format, :html)
         | 
| 417 454 | 
             
                  gsub(%r{(?mi)(?x:
         | 
| 418 455 | 
             
                  (?<!["'\[(\\])
         | 
| 419 | 
            -
                  ((http|https)://)
         | 
| 420 | 
            -
                  ([\w\-]+( | 
| 421 | 
            -
                  ([\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
         | 
| 456 | 
            +
                  (?<protocol>(?:http|https)://)
         | 
| 457 | 
            +
                  (?<domain>[\w\-]+(?:\.[\w\-]+)+)
         | 
| 458 | 
            +
                  (?<path>[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
         | 
| 422 459 | 
             
                  )}) do |_match|
         | 
| 423 460 | 
             
                    m = Regexp.last_match
         | 
| 424 | 
            -
                     | 
| 425 | 
            -
                     | 
| 461 | 
            +
                    url = "#{m['domain']}#{m['path']}"
         | 
| 462 | 
            +
                    proto = m['protocol'].nil? ? 'http://' : m['protocol']
         | 
| 463 | 
            +
                    case fmt
         | 
| 464 | 
            +
                    when :terminal
         | 
| 465 | 
            +
                      TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
         | 
| 426 466 | 
             
                    when :html
         | 
| 427 | 
            -
                      %(<a href="#{proto}#{ | 
| 467 | 
            +
                      %(<a href="#{proto}#{url}" title="Link to #{m['domain']}">[#{url}]</a>)
         | 
| 428 468 | 
             
                    when :markdown
         | 
| 429 | 
            -
                      "[#{ | 
| 469 | 
            +
                      "[#{url}](#{proto}#{url})"
         | 
| 430 470 | 
             
                    else
         | 
| 431 471 | 
             
                      m[0]
         | 
| 432 472 | 
             
                    end
         | 
| @@ -444,5 +484,41 @@ module Doing | |
| 444 484 | 
             
                    end
         | 
| 445 485 | 
             
                  end
         | 
| 446 486 | 
             
                end
         | 
| 487 | 
            +
             | 
| 488 | 
            +
                def set_type(kind = nil)
         | 
| 489 | 
            +
                  if kind
         | 
| 490 | 
            +
                    case kind.to_s
         | 
| 491 | 
            +
                    when /^a/i
         | 
| 492 | 
            +
                      gsub(/^\[ *| *\]$/, '').split(/ *, */)
         | 
| 493 | 
            +
                    when /^i/i
         | 
| 494 | 
            +
                      to_i
         | 
| 495 | 
            +
                    when /^f/i
         | 
| 496 | 
            +
                      to_f
         | 
| 497 | 
            +
                    when /^sy/i
         | 
| 498 | 
            +
                      sub(/^:/, '').to_sym
         | 
| 499 | 
            +
                    when /^b/i
         | 
| 500 | 
            +
                      self =~ /^(true|yes)$/ ? true : false
         | 
| 501 | 
            +
                    else
         | 
| 502 | 
            +
                      to_s
         | 
| 503 | 
            +
                    end
         | 
| 504 | 
            +
                  else
         | 
| 505 | 
            +
                    case self
         | 
| 506 | 
            +
                    when / *, */
         | 
| 507 | 
            +
                      gsub(/^\[ *| *\]$/, '').split(/ *, */)
         | 
| 508 | 
            +
                    when /^[0-9]+$/
         | 
| 509 | 
            +
                      to_i
         | 
| 510 | 
            +
                    when /^[0-9]+\.[0-9]+$/
         | 
| 511 | 
            +
                      to_f
         | 
| 512 | 
            +
                    when /^:\w+/
         | 
| 513 | 
            +
                      sub(/^:/, '').to_sym
         | 
| 514 | 
            +
                    when /^(true|yes)$/i
         | 
| 515 | 
            +
                      true
         | 
| 516 | 
            +
                    when /^(false|no)$/i
         | 
| 517 | 
            +
                      false
         | 
| 518 | 
            +
                    else
         | 
| 519 | 
            +
                      to_s
         | 
| 520 | 
            +
                    end
         | 
| 521 | 
            +
                  end
         | 
| 522 | 
            +
                end
         | 
| 447 523 | 
             
              end
         | 
| 448 524 | 
             
            end
         | 
    
        data/lib/doing/util.rb
    CHANGED
    
    | @@ -21,11 +21,17 @@ module Doing | |
| 21 21 | 
             
                def exec_available(cli)
         | 
| 22 22 | 
             
                  return false if cli.nil?
         | 
| 23 23 |  | 
| 24 | 
            -
                   | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 24 | 
            +
                  !TTY::Which.which(cli).nil?
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                ##
         | 
| 28 | 
            +
                ## Return the first valid executable from a list of commands
         | 
| 29 | 
            +
                ##
         | 
| 30 | 
            +
                ## @example `Doing::Util.first_available_exec('bat', 'less -Xr', 'more -r', 'cat')`
         | 
| 31 | 
            +
                ##
         | 
| 32 | 
            +
                def first_available_exec(*commands)
         | 
| 33 | 
            +
                  commands.compact.map(&:strip).reject(&:empty?).uniq
         | 
| 34 | 
            +
                  .find { |cmd| exec_available(cmd.split.first) }
         | 
| 29 35 | 
             
                end
         | 
| 30 36 |  | 
| 31 37 | 
             
                def merge_default_proc(target, overwrite)
         | 
| @@ -116,6 +122,7 @@ module Doing | |
| 116 122 |  | 
| 117 123 | 
             
                  File.open(file, 'w+') do |f|
         | 
| 118 124 | 
             
                    f.puts content
         | 
| 125 | 
            +
                    Doing.logger.debug('Write:', "File written: #{file}")
         | 
| 119 126 | 
             
                  end
         | 
| 120 127 |  | 
| 121 128 | 
             
                  Hooks.trigger :post_write, file
         | 
| @@ -183,7 +190,8 @@ module Doing | |
| 183 190 | 
             
                  Doing.logger.debug('ENV:', 'No EDITOR environment variable, testing available editors')
         | 
| 184 191 | 
             
                  editors = %w[vim vi code subl mate mvim nano emacs]
         | 
| 185 192 | 
             
                  editors.each do |ed|
         | 
| 186 | 
            -
                    return ed if  | 
| 193 | 
            +
                    return TTY::Which.which(ed) if TTY::Which.which(ed)
         | 
| 194 | 
            +
             | 
| 187 195 | 
             
                    Doing.logger.debug('ENV:', "#{ed} not available")
         | 
| 188 196 | 
             
                  end
         | 
| 189 197 |  | 
    
        data/lib/doing/version.rb
    CHANGED