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
 
    
        data/lib/doing/wwid.rb
    CHANGED
    
    | 
         @@ -1,4 +1,5 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            #!/usr/bin/ruby
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
       2 
3 
     | 
    
         | 
| 
       3 
4 
     | 
    
         
             
            require 'deep_merge'
         
     | 
| 
       4 
5 
     | 
    
         
             
            require 'open3'
         
     | 
| 
         @@ -6,2681 +7,2153 @@ require 'pp' 
     | 
|
| 
       6 
7 
     | 
    
         
             
            require 'shellwords'
         
     | 
| 
       7 
8 
     | 
    
         
             
            require 'erb'
         
     | 
| 
       8 
9 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
            ## 
     | 
| 
       11 
     | 
    
         
            -
            ##
         
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
               
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
                 
     | 
| 
       21 
     | 
    
         
            -
                @ 
     | 
| 
       22 
     | 
    
         
            -
                 
     | 
| 
       23 
     | 
    
         
            -
                 
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
      
 10 
     | 
    
         
            +
            module Doing
         
     | 
| 
      
 11 
     | 
    
         
            +
              ##
         
     | 
| 
      
 12 
     | 
    
         
            +
              ## @brief      Main "What Was I Doing" methods
         
     | 
| 
      
 13 
     | 
    
         
            +
              ##
         
     | 
| 
      
 14 
     | 
    
         
            +
              class WWID
         
     | 
| 
      
 15 
     | 
    
         
            +
                attr_reader   :additional_configs, :current_section, :doing_file, :content
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                attr_accessor :config, :config_file, :auto_tag, :default_option
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # include Util
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                ##
         
     | 
| 
      
 22 
     | 
    
         
            +
                ## @brief      Initializes the object.
         
     | 
| 
      
 23 
     | 
    
         
            +
                ##
         
     | 
| 
      
 24 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @timers = {}
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @recorded_items = []
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @content = {}
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @doingrc_needs_update = false
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @default_config_file = '.doingrc'
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @auto_tag = true
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @user_home = Util.user_home
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                ##
         
     | 
| 
      
 35 
     | 
    
         
            +
                ## @brief      Logger
         
     | 
| 
      
 36 
     | 
    
         
            +
                ##
         
     | 
| 
      
 37 
     | 
    
         
            +
                ## Responds to :debug, :info, :warn, and :error
         
     | 
| 
      
 38 
     | 
    
         
            +
                ##
         
     | 
| 
      
 39 
     | 
    
         
            +
                ## Each method takes a topic, and a message or block
         
     | 
| 
      
 40 
     | 
    
         
            +
                ##
         
     | 
| 
      
 41 
     | 
    
         
            +
                ## Example: debug('Hooks', 'Hook 1 triggered')
         
     | 
| 
      
 42 
     | 
    
         
            +
                ##
         
     | 
| 
      
 43 
     | 
    
         
            +
                def logger
         
     | 
| 
      
 44 
     | 
    
         
            +
                  @logger ||= Doing.logger
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                ##
         
     | 
| 
      
 48 
     | 
    
         
            +
                ## @brief      Initializes the doing file.
         
     | 
| 
      
 49 
     | 
    
         
            +
                ##
         
     | 
| 
      
 50 
     | 
    
         
            +
                ## @param      path  (String) Override path to a doing file, optional
         
     | 
| 
      
 51 
     | 
    
         
            +
                ##
         
     | 
| 
      
 52 
     | 
    
         
            +
                def init_doing_file(path = nil)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  @doing_file =  File.expand_path(@config['doing_file'])
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  if path.nil?
         
     | 
| 
      
 56 
     | 
    
         
            +
                    create(@doing_file) unless File.exist?(@doing_file)
         
     | 
| 
      
 57 
     | 
    
         
            +
                    input = IO.read(@doing_file)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
         
     | 
| 
      
 59 
     | 
    
         
            +
                  elsif File.exist?(File.expand_path(path)) && File.file?(File.expand_path(path)) && File.stat(File.expand_path(path)).size.positive?
         
     | 
| 
      
 60 
     | 
    
         
            +
                    @doing_file = File.expand_path(path)
         
     | 
| 
      
 61 
     | 
    
         
            +
                    input = IO.read(File.expand_path(path))
         
     | 
| 
      
 62 
     | 
    
         
            +
                    input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
         
     | 
| 
      
 63 
     | 
    
         
            +
                  elsif path.length < 256
         
     | 
| 
      
 64 
     | 
    
         
            +
                    @doing_file = File.expand_path(path)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    create(path)
         
     | 
| 
      
 66 
     | 
    
         
            +
                    input = IO.read(File.expand_path(path))
         
     | 
| 
      
 67 
     | 
    
         
            +
                    input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
       27 
69 
     | 
    
         | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
      
 70 
     | 
    
         
            +
                  @other_content_top = []
         
     | 
| 
      
 71 
     | 
    
         
            +
                  @other_content_bottom = []
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                  section = 'Uncategorized'
         
     | 
| 
      
 74 
     | 
    
         
            +
                  lines = input.split(/[\n\r]/)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  current = 0
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                  lines.each do |line|
         
     | 
| 
      
 78 
     | 
    
         
            +
                    next if line =~ /^\s*$/
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                    if line =~ /^(\S[\S ]+):\s*(@\S+\s*)*$/
         
     | 
| 
      
 81 
     | 
    
         
            +
                      section = Regexp.last_match(1)
         
     | 
| 
      
 82 
     | 
    
         
            +
                      @content[section] = {}
         
     | 
| 
      
 83 
     | 
    
         
            +
                      @content[section][:original] = line
         
     | 
| 
      
 84 
     | 
    
         
            +
                      @content[section][:items] = []
         
     | 
| 
      
 85 
     | 
    
         
            +
                      current = 0
         
     | 
| 
      
 86 
     | 
    
         
            +
                    elsif line =~ /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
         
     | 
| 
      
 87 
     | 
    
         
            +
                      date = Regexp.last_match(1).strip
         
     | 
| 
      
 88 
     | 
    
         
            +
                      title = Regexp.last_match(2).strip
         
     | 
| 
      
 89 
     | 
    
         
            +
                      item = Item.new(date, title, section)
         
     | 
| 
      
 90 
     | 
    
         
            +
                      @content[section][:items].push(item)
         
     | 
| 
      
 91 
     | 
    
         
            +
                      current += 1
         
     | 
| 
      
 92 
     | 
    
         
            +
                    elsif current.zero?
         
     | 
| 
      
 93 
     | 
    
         
            +
                      # if content[section][:items].length - 1 == current
         
     | 
| 
      
 94 
     | 
    
         
            +
                      @other_content_top.push(line)
         
     | 
| 
      
 95 
     | 
    
         
            +
                    elsif line =~ /^\S/
         
     | 
| 
      
 96 
     | 
    
         
            +
                      @other_content_bottom.push(line)
         
     | 
| 
      
 97 
     | 
    
         
            +
                    else
         
     | 
| 
      
 98 
     | 
    
         
            +
                      prev_item = @content[section][:items][current - 1]
         
     | 
| 
      
 99 
     | 
    
         
            +
                      prev_item.note = Note.new unless prev_item.note
         
     | 
| 
       36 
100 
     | 
    
         | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
      
 101 
     | 
    
         
            +
                      prev_item.note.add(line)
         
     | 
| 
      
 102 
     | 
    
         
            +
                      # end
         
     | 
| 
      
 103 
     | 
    
         
            +
                    end
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
                  Hooks.trigger :post_read, self
         
     | 
| 
      
 106 
     | 
    
         
            +
                end
         
     | 
| 
       38 
107 
     | 
    
         | 
| 
       39 
     | 
    
         
            -
                 
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
      
 108 
     | 
    
         
            +
                ##
         
     | 
| 
      
 109 
     | 
    
         
            +
                ## @brief      Create a new doing file
         
     | 
| 
      
 110 
     | 
    
         
            +
                ##
         
     | 
| 
      
 111 
     | 
    
         
            +
                def create(filename = nil)
         
     | 
| 
      
 112 
     | 
    
         
            +
                  filename = @doing_file if filename.nil?
         
     | 
| 
      
 113 
     | 
    
         
            +
                  return if File.exist?(filename) && File.stat(filename).size.positive?
         
     | 
| 
       41 
114 
     | 
    
         | 
| 
       42 
     | 
    
         
            -
                   
     | 
| 
      
 115 
     | 
    
         
            +
                  File.open(filename, 'w+') do |f|
         
     | 
| 
      
 116 
     | 
    
         
            +
                    f.puts "#{@config['current_section']}:"
         
     | 
| 
      
 117 
     | 
    
         
            +
                  end
         
     | 
| 
       43 
118 
     | 
    
         
             
                end
         
     | 
| 
       44 
119 
     | 
    
         | 
| 
       45 
     | 
    
         
            -
                 
     | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
      
 120 
     | 
    
         
            +
                ##
         
     | 
| 
      
 121 
     | 
    
         
            +
                ## @brief      Create a process for an editor and wait for the file handle to return
         
     | 
| 
      
 122 
     | 
    
         
            +
                ##
         
     | 
| 
      
 123 
     | 
    
         
            +
                ## @param      input  (String) Text input for editor
         
     | 
| 
      
 124 
     | 
    
         
            +
                ##
         
     | 
| 
      
 125 
     | 
    
         
            +
                def fork_editor(input = '')
         
     | 
| 
      
 126 
     | 
    
         
            +
                  # raise Errors::NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
         
     | 
| 
       47 
127 
     | 
    
         | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
              ## @brief      Reads a configuration.
         
     | 
| 
       50 
     | 
    
         
            -
              ##
         
     | 
| 
       51 
     | 
    
         
            -
              def read_config(opt = {})
         
     | 
| 
       52 
     | 
    
         
            -
                @config_file ||= if Dir.respond_to?('home')
         
     | 
| 
       53 
     | 
    
         
            -
                                   File.join(Dir.home, @default_config_file)
         
     | 
| 
       54 
     | 
    
         
            -
                                 else
         
     | 
| 
       55 
     | 
    
         
            -
                                   File.join(File.expand_path('~'), @default_config_file)
         
     | 
| 
       56 
     | 
    
         
            -
                                 end
         
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
                additional_configs = if opt[:ignore_local]
         
     | 
| 
       59 
     | 
    
         
            -
                                       []
         
     | 
| 
       60 
     | 
    
         
            -
                                     else
         
     | 
| 
       61 
     | 
    
         
            -
                                       find_local_config
         
     | 
| 
       62 
     | 
    
         
            -
                                     end
         
     | 
| 
      
 128 
     | 
    
         
            +
                  raise Errors::MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
         
     | 
| 
       63 
129 
     | 
    
         | 
| 
       64 
     | 
    
         
            -
             
     | 
| 
       65 
     | 
    
         
            -
                  @local_config = {}
         
     | 
| 
      
 130 
     | 
    
         
            +
                  tmpfile = Tempfile.new(['doing', '.md'])
         
     | 
| 
       66 
131 
     | 
    
         | 
| 
       67 
     | 
    
         
            -
                   
     | 
| 
       68 
     | 
    
         
            -
             
     | 
| 
       69 
     | 
    
         
            -
                     
     | 
| 
       70 
     | 
    
         
            -
                    @local_config = @local_config.deep_merge(new_config)
         
     | 
| 
      
 132 
     | 
    
         
            +
                  File.open(tmpfile.path, 'w+') do |f|
         
     | 
| 
      
 133 
     | 
    
         
            +
                    f.puts input
         
     | 
| 
      
 134 
     | 
    
         
            +
                    f.puts "\n# The first line is the entry title, any lines after that are added as a note"
         
     | 
| 
       71 
135 
     | 
    
         
             
                  end
         
     | 
| 
       72 
136 
     | 
    
         | 
| 
       73 
     | 
    
         
            -
                  #  
     | 
| 
       74 
     | 
    
         
            -
                rescue StandardError
         
     | 
| 
       75 
     | 
    
         
            -
                  @config = {}
         
     | 
| 
       76 
     | 
    
         
            -
                  @local_config = {}
         
     | 
| 
       77 
     | 
    
         
            -
                  # exit_now! "error reading config"
         
     | 
| 
       78 
     | 
    
         
            -
                end
         
     | 
| 
       79 
     | 
    
         
            -
              end
         
     | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
              ##
         
     | 
| 
       82 
     | 
    
         
            -
              ## @brief      Read user configuration and merge with defaults
         
     | 
| 
       83 
     | 
    
         
            -
              ##
         
     | 
| 
       84 
     | 
    
         
            -
              ## @param      opt   (Hash) Additional Options
         
     | 
| 
       85 
     | 
    
         
            -
              ##
         
     | 
| 
       86 
     | 
    
         
            -
              def configure(opt = {})
         
     | 
| 
       87 
     | 
    
         
            -
                @timers = {}
         
     | 
| 
       88 
     | 
    
         
            -
                @recorded_items = []
         
     | 
| 
       89 
     | 
    
         
            -
                opt[:ignore_local] ||= false
         
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
                @config_file ||= File.join(@user_home, @default_config_file)
         
     | 
| 
       92 
     | 
    
         
            -
             
     | 
| 
       93 
     | 
    
         
            -
                read_config({ ignore_local: opt[:ignore_local] })
         
     | 
| 
       94 
     | 
    
         
            -
             
     | 
| 
       95 
     | 
    
         
            -
                @config = {} if @config.nil?
         
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
                @config['autotag'] ||= {}
         
     | 
| 
       98 
     | 
    
         
            -
                @config['autotag']['whitelist'] ||= []
         
     | 
| 
       99 
     | 
    
         
            -
                @config['autotag']['synonyms'] ||= {}
         
     | 
| 
       100 
     | 
    
         
            -
                @config['doing_file'] ||= '~/what_was_i_doing.md'
         
     | 
| 
       101 
     | 
    
         
            -
                @config['current_section'] ||= 'Currently'
         
     | 
| 
       102 
     | 
    
         
            -
                @config['config_editor_app'] ||= nil
         
     | 
| 
       103 
     | 
    
         
            -
                @config['editor_app'] ||= nil
         
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
                @config['html_template'] ||= {}
         
     | 
| 
       106 
     | 
    
         
            -
                @config['html_template']['haml'] ||= nil
         
     | 
| 
       107 
     | 
    
         
            -
                @config['html_template']['css'] ||= nil
         
     | 
| 
       108 
     | 
    
         
            -
                @config['html_template']['markdown'] ||= nil
         
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
                @config['templates'] ||= {}
         
     | 
| 
       111 
     | 
    
         
            -
                @config['templates']['default'] ||= {
         
     | 
| 
       112 
     | 
    
         
            -
                  'date_format' => '%Y-%m-%d %H:%M',
         
     | 
| 
       113 
     | 
    
         
            -
                  'template' => '%date | %title%note',
         
     | 
| 
       114 
     | 
    
         
            -
                  'wrap_width' => 0
         
     | 
| 
       115 
     | 
    
         
            -
                }
         
     | 
| 
       116 
     | 
    
         
            -
                @config['templates']['today'] ||= {
         
     | 
| 
       117 
     | 
    
         
            -
                  'date_format' => '%_I:%M%P',
         
     | 
| 
       118 
     | 
    
         
            -
                  'template' => '%date: %title %interval%note',
         
     | 
| 
       119 
     | 
    
         
            -
                  'wrap_width' => 0
         
     | 
| 
       120 
     | 
    
         
            -
                }
         
     | 
| 
       121 
     | 
    
         
            -
                @config['templates']['last'] ||= {
         
     | 
| 
       122 
     | 
    
         
            -
                  'date_format' => '%-I:%M%P on %a',
         
     | 
| 
       123 
     | 
    
         
            -
                  'template' => '%title (at %date)%odnote',
         
     | 
| 
       124 
     | 
    
         
            -
                  'wrap_width' => 88
         
     | 
| 
       125 
     | 
    
         
            -
                }
         
     | 
| 
       126 
     | 
    
         
            -
                @config['templates']['recent'] ||= {
         
     | 
| 
       127 
     | 
    
         
            -
                  'date_format' => '%_I:%M%P',
         
     | 
| 
       128 
     | 
    
         
            -
                  'template' => '%shortdate: %title (%section)',
         
     | 
| 
       129 
     | 
    
         
            -
                  'wrap_width' => 88,
         
     | 
| 
       130 
     | 
    
         
            -
                  'count' => 10
         
     | 
| 
       131 
     | 
    
         
            -
                }
         
     | 
| 
       132 
     | 
    
         
            -
                @config['views'] ||= {
         
     | 
| 
       133 
     | 
    
         
            -
                  'done' => {
         
     | 
| 
       134 
     | 
    
         
            -
                    'date_format' => '%_I:%M%P',
         
     | 
| 
       135 
     | 
    
         
            -
                    'template' => '%date | %title%note',
         
     | 
| 
       136 
     | 
    
         
            -
                    'wrap_width' => 0,
         
     | 
| 
       137 
     | 
    
         
            -
                    'section' => 'All',
         
     | 
| 
       138 
     | 
    
         
            -
                    'count' => 0,
         
     | 
| 
       139 
     | 
    
         
            -
                    'order' => 'desc',
         
     | 
| 
       140 
     | 
    
         
            -
                    'tags' => 'done complete cancelled',
         
     | 
| 
       141 
     | 
    
         
            -
                    'tags_bool' => 'OR'
         
     | 
| 
       142 
     | 
    
         
            -
                  },
         
     | 
| 
       143 
     | 
    
         
            -
                  'color' => {
         
     | 
| 
       144 
     | 
    
         
            -
                    'date_format' => '%F %_I:%M%P',
         
     | 
| 
       145 
     | 
    
         
            -
                    'template' => '%boldblack%date %boldgreen| %boldwhite%title%default%note',
         
     | 
| 
       146 
     | 
    
         
            -
                    'wrap_width' => 0,
         
     | 
| 
       147 
     | 
    
         
            -
                    'section' => 'Currently',
         
     | 
| 
       148 
     | 
    
         
            -
                    'count' => 10,
         
     | 
| 
       149 
     | 
    
         
            -
                    'order' => 'asc'
         
     | 
| 
       150 
     | 
    
         
            -
                  }
         
     | 
| 
       151 
     | 
    
         
            -
                }
         
     | 
| 
       152 
     | 
    
         
            -
                @config['marker_tag'] ||= 'flagged'
         
     | 
| 
       153 
     | 
    
         
            -
                @config['marker_color'] ||= 'red'
         
     | 
| 
       154 
     | 
    
         
            -
                @config['default_tags'] ||= []
         
     | 
| 
       155 
     | 
    
         
            -
                @config['tag_sort'] ||= 'time'
         
     | 
| 
       156 
     | 
    
         
            -
             
     | 
| 
       157 
     | 
    
         
            -
                @current_section = config['current_section']
         
     | 
| 
       158 
     | 
    
         
            -
                @default_template = config['templates']['default']['template']
         
     | 
| 
       159 
     | 
    
         
            -
                @default_date_format = config['templates']['default']['date_format']
         
     | 
| 
      
 137 
     | 
    
         
            +
                  pid = Process.fork { system("#{Util.editor_with_args} #{tmpfile.path}") }
         
     | 
| 
       160 
138 
     | 
    
         | 
| 
       161 
     | 
    
         
            -
             
     | 
| 
       162 
     | 
    
         
            -
             
     | 
| 
       163 
     | 
    
         
            -
             
     | 
| 
       164 
     | 
    
         
            -
             
     | 
| 
       165 
     | 
    
         
            -
             
     | 
| 
       166 
     | 
    
         
            -
             
     | 
| 
       167 
     | 
    
         
            -
             
     | 
| 
      
 139 
     | 
    
         
            +
                  trap('INT') do
         
     | 
| 
      
 140 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 141 
     | 
    
         
            +
                      Process.kill(9, pid)
         
     | 
| 
      
 142 
     | 
    
         
            +
                    rescue StandardError
         
     | 
| 
      
 143 
     | 
    
         
            +
                      Errno::ESRCH
         
     | 
| 
      
 144 
     | 
    
         
            +
                    end
         
     | 
| 
      
 145 
     | 
    
         
            +
                    tmpfile.unlink
         
     | 
| 
      
 146 
     | 
    
         
            +
                    tmpfile.close!
         
     | 
| 
      
 147 
     | 
    
         
            +
                    exit 0
         
     | 
| 
      
 148 
     | 
    
         
            +
                  end
         
     | 
| 
       168 
149 
     | 
    
         | 
| 
       169 
     | 
    
         
            -
             
     | 
| 
      
 150 
     | 
    
         
            +
                  Process.wait(pid)
         
     | 
| 
       170 
151 
     | 
    
         | 
| 
       171 
     | 
    
         
            -
             
     | 
| 
      
 152 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 153 
     | 
    
         
            +
                    if $?.exitstatus == 0
         
     | 
| 
      
 154 
     | 
    
         
            +
                      input = IO.read(tmpfile.path)
         
     | 
| 
      
 155 
     | 
    
         
            +
                    else
         
     | 
| 
      
 156 
     | 
    
         
            +
                      exit_now! 'Cancelled'
         
     | 
| 
      
 157 
     | 
    
         
            +
                    end
         
     | 
| 
      
 158 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 159 
     | 
    
         
            +
                    tmpfile.close
         
     | 
| 
      
 160 
     | 
    
         
            +
                    tmpfile.unlink
         
     | 
| 
      
 161 
     | 
    
         
            +
                  end
         
     | 
| 
       172 
162 
     | 
    
         | 
| 
       173 
     | 
    
         
            -
             
     | 
| 
       174 
     | 
    
         
            -
                 
     | 
| 
       175 
     | 
    
         
            -
             
     | 
| 
       176 
     | 
    
         
            -
             
     | 
| 
      
 163 
     | 
    
         
            +
                  input.split(/\n/).delete_if(&:ignore?).join("\n")
         
     | 
| 
      
 164 
     | 
    
         
            +
                end
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                ##
         
     | 
| 
      
 167 
     | 
    
         
            +
                ## @brief      Takes a multi-line string and formats it as an entry
         
     | 
| 
      
 168 
     | 
    
         
            +
                ##
         
     | 
| 
      
 169 
     | 
    
         
            +
                ## @return     (Array) [(String)title, (Array)note]
         
     | 
| 
      
 170 
     | 
    
         
            +
                ##
         
     | 
| 
      
 171 
     | 
    
         
            +
                ## @param      input  (String) The string to parse
         
     | 
| 
      
 172 
     | 
    
         
            +
                ##
         
     | 
| 
      
 173 
     | 
    
         
            +
                ## @return     (Array) [(String)title, (Note)note]
         
     | 
| 
      
 174 
     | 
    
         
            +
                ##
         
     | 
| 
      
 175 
     | 
    
         
            +
                def format_input(input)
         
     | 
| 
      
 176 
     | 
    
         
            +
                  raise Errors::EmptyInput, 'No content in entry' if input.nil? || input.strip.empty?
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                  input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
         
     | 
| 
      
 179 
     | 
    
         
            +
                  title = input_lines[0]&.strip
         
     | 
| 
      
 180 
     | 
    
         
            +
                  raise Errors::EmptyInput, 'No content in first line' if title.nil? || title.strip.empty?
         
     | 
| 
      
 181 
     | 
    
         
            +
             
     | 
| 
      
 182 
     | 
    
         
            +
                  note = Note.new
         
     | 
| 
      
 183 
     | 
    
         
            +
                  note.add(input_lines[1..-1]) if input_lines.length > 1
         
     | 
| 
      
 184 
     | 
    
         
            +
                  # If title line ends in a parenthetical, use that as the note
         
     | 
| 
      
 185 
     | 
    
         
            +
                  if note.empty? && title =~ /\s+\(.*?\)$/
         
     | 
| 
      
 186 
     | 
    
         
            +
                    title.sub!(/\s+\((.*?)\)$/) do
         
     | 
| 
      
 187 
     | 
    
         
            +
                      m = Regexp.last_match
         
     | 
| 
      
 188 
     | 
    
         
            +
                      note.add(m[1])
         
     | 
| 
      
 189 
     | 
    
         
            +
                      ''
         
     | 
| 
      
 190 
     | 
    
         
            +
                    end
         
     | 
| 
      
 191 
     | 
    
         
            +
                  end
         
     | 
| 
       177 
192 
     | 
    
         | 
| 
       178 
     | 
    
         
            -
             
     | 
| 
       179 
     | 
    
         
            -
             
     | 
| 
       180 
     | 
    
         
            -
             
     | 
| 
       181 
     | 
    
         
            -
             
     | 
| 
       182 
     | 
    
         
            -
             
     | 
| 
       183 
     | 
    
         
            -
             
     | 
| 
       184 
     | 
    
         
            -
                 
     | 
| 
       185 
     | 
    
         
            -
             
     | 
| 
       186 
     | 
    
         
            -
                 
     | 
| 
       187 
     | 
    
         
            -
             
     | 
| 
       188 
     | 
    
         
            -
                 
     | 
| 
       189 
     | 
    
         
            -
             
     | 
| 
       190 
     | 
    
         
            -
             
     | 
| 
       191 
     | 
    
         
            -
                  input 
     | 
| 
       192 
     | 
    
         
            -
                 
     | 
| 
       193 
     | 
    
         
            -
             
     | 
| 
       194 
     | 
    
         
            -
             
     | 
| 
       195 
     | 
    
         
            -
             
     | 
| 
       196 
     | 
    
         
            -
             
     | 
| 
       197 
     | 
    
         
            -
                   
     | 
| 
       198 
     | 
    
         
            -
             
     | 
| 
       199 
     | 
    
         
            -
                   
     | 
| 
       200 
     | 
    
         
            -
             
     | 
| 
       201 
     | 
    
         
            -
             
     | 
| 
      
 193 
     | 
    
         
            +
                  note.strip_lines!
         
     | 
| 
      
 194 
     | 
    
         
            +
                  note.compress
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                  [title, note]
         
     | 
| 
      
 197 
     | 
    
         
            +
                end
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
                ##
         
     | 
| 
      
 200 
     | 
    
         
            +
                ## @brief      Converts input string into a Time object when input takes on the
         
     | 
| 
      
 201 
     | 
    
         
            +
                ##             following formats:
         
     | 
| 
      
 202 
     | 
    
         
            +
                ##             - interval format e.g. '1d2h30m', '45m' etc.
         
     | 
| 
      
 203 
     | 
    
         
            +
                ##             - a semantic phrase e.g. 'yesterday 5:30pm'
         
     | 
| 
      
 204 
     | 
    
         
            +
                ##             - a strftime e.g. '2016-03-15 15:32:04 PDT'
         
     | 
| 
      
 205 
     | 
    
         
            +
                ##
         
     | 
| 
      
 206 
     | 
    
         
            +
                ## @param      input  (String) String to chronify
         
     | 
| 
      
 207 
     | 
    
         
            +
                ##
         
     | 
| 
      
 208 
     | 
    
         
            +
                ## @return     (DateTime) result
         
     | 
| 
      
 209 
     | 
    
         
            +
                ##
         
     | 
| 
      
 210 
     | 
    
         
            +
                def chronify(input, future: false, guess: :begin)
         
     | 
| 
      
 211 
     | 
    
         
            +
                  now = Time.now
         
     | 
| 
      
 212 
     | 
    
         
            +
                  raise Errors::InvalidTimeExpression, "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
                  secs_ago = if input.match(/^(\d+)$/)
         
     | 
| 
      
 215 
     | 
    
         
            +
                               # plain number, assume minutes
         
     | 
| 
      
 216 
     | 
    
         
            +
                               Regexp.last_match(1).to_i * 60
         
     | 
| 
      
 217 
     | 
    
         
            +
                             elsif (m = input.match(/^(?:(?<day>\d+)d)?(?:(?<hour>\d+)h)?(?:(?<min>\d+)m)?$/i))
         
     | 
| 
      
 218 
     | 
    
         
            +
                               # day/hour/minute format e.g. 1d2h30m
         
     | 
| 
      
 219 
     | 
    
         
            +
                               [[m['day'], 24 * 3600],
         
     | 
| 
      
 220 
     | 
    
         
            +
                                [m['hour'], 3600],
         
     | 
| 
      
 221 
     | 
    
         
            +
                                [m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+)
         
     | 
| 
      
 222 
     | 
    
         
            +
                             end
         
     | 
| 
       202 
223 
     | 
    
         | 
| 
       203 
     | 
    
         
            -
             
     | 
| 
       204 
     | 
    
         
            -
             
     | 
| 
       205 
     | 
    
         
            -
             
     | 
| 
       206 
     | 
    
         
            -
                section = 'Uncategorized'
         
     | 
| 
       207 
     | 
    
         
            -
                lines = input.split(/[\n\r]/)
         
     | 
| 
       208 
     | 
    
         
            -
                current = 0
         
     | 
| 
       209 
     | 
    
         
            -
             
     | 
| 
       210 
     | 
    
         
            -
                lines.each do |line|
         
     | 
| 
       211 
     | 
    
         
            -
                  next if line =~ /^\s*$/
         
     | 
| 
       212 
     | 
    
         
            -
             
     | 
| 
       213 
     | 
    
         
            -
                  if line =~ /^(\S[\S ]+):\s*(@\S+\s*)*$/
         
     | 
| 
       214 
     | 
    
         
            -
                    section = Regexp.last_match(1)
         
     | 
| 
       215 
     | 
    
         
            -
                    @content[section] = {}
         
     | 
| 
       216 
     | 
    
         
            -
                    @content[section]['original'] = line
         
     | 
| 
       217 
     | 
    
         
            -
                    @content[section]['items'] = []
         
     | 
| 
       218 
     | 
    
         
            -
                    current = 0
         
     | 
| 
       219 
     | 
    
         
            -
                  elsif line =~ /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
         
     | 
| 
       220 
     | 
    
         
            -
                    date = Time.parse(Regexp.last_match(1))
         
     | 
| 
       221 
     | 
    
         
            -
                    title = Regexp.last_match(2)
         
     | 
| 
       222 
     | 
    
         
            -
                    @content[section]['items'].push({ 'title' => title, 'date' => date, 'section' => section })
         
     | 
| 
       223 
     | 
    
         
            -
                    current += 1
         
     | 
| 
       224 
     | 
    
         
            -
                  elsif current.zero?
         
     | 
| 
       225 
     | 
    
         
            -
                    # if content[section]['items'].length - 1 == current
         
     | 
| 
       226 
     | 
    
         
            -
                    @other_content_top.push(line)
         
     | 
| 
       227 
     | 
    
         
            -
                  elsif line =~ /^\S/
         
     | 
| 
       228 
     | 
    
         
            -
                    @other_content_bottom.push(line)
         
     | 
| 
      
 224 
     | 
    
         
            +
                  if secs_ago
         
     | 
| 
      
 225 
     | 
    
         
            +
                    now - secs_ago
         
     | 
| 
       229 
226 
     | 
    
         
             
                  else
         
     | 
| 
       230 
     | 
    
         
            -
                     
     | 
| 
       231 
     | 
    
         
            -
             
     | 
| 
       232 
     | 
    
         
            -
                    @content[section]['items'][current - 1]['note'].push(line.chomp)
         
     | 
| 
       233 
     | 
    
         
            -
                    # end
         
     | 
| 
      
 227 
     | 
    
         
            +
                    Chronic.parse(input, { guess: guess, context: future ? :future : :past, ambiguous_time_range: 8 })
         
     | 
| 
       234 
228 
     | 
    
         
             
                  end
         
     | 
| 
       235 
229 
     | 
    
         
             
                end
         
     | 
| 
       236 
     | 
    
         
            -
              end
         
     | 
| 
       237 
     | 
    
         
            -
             
     | 
| 
       238 
     | 
    
         
            -
              ##
         
     | 
| 
       239 
     | 
    
         
            -
              ## @brief      Return the contents of the ERB template for Markdown output
         
     | 
| 
       240 
     | 
    
         
            -
              ##
         
     | 
| 
       241 
     | 
    
         
            -
              ## @return     (String) ERB template
         
     | 
| 
       242 
     | 
    
         
            -
              ##
         
     | 
| 
       243 
     | 
    
         
            -
              def markdown_template
         
     | 
| 
       244 
     | 
    
         
            -
                IO.read(File.join(File.dirname(__FILE__), '../templates/doing-markdown.erb'))
         
     | 
| 
       245 
     | 
    
         
            -
              end
         
     | 
| 
       246 
230 
     | 
    
         | 
| 
       247 
     | 
    
         
            -
             
     | 
| 
       248 
     | 
    
         
            -
             
     | 
| 
       249 
     | 
    
         
            -
             
     | 
| 
       250 
     | 
    
         
            -
             
     | 
| 
       251 
     | 
    
         
            -
             
     | 
| 
       252 
     | 
    
         
            -
             
     | 
| 
       253 
     | 
    
         
            -
                 
     | 
| 
       254 
     | 
    
         
            -
             
     | 
| 
      
 231 
     | 
    
         
            +
                ##
         
     | 
| 
      
 232 
     | 
    
         
            +
                ## @brief      Converts simple strings into seconds that can be added to a Time
         
     | 
| 
      
 233 
     | 
    
         
            +
                ##             object
         
     | 
| 
      
 234 
     | 
    
         
            +
                ##
         
     | 
| 
      
 235 
     | 
    
         
            +
                ## @param      qty   (String) HH:MM or XX[dhm][[XXhm][XXm]] (1d2h30m, 45m,
         
     | 
| 
      
 236 
     | 
    
         
            +
                ##                   1.5d, 1h20m, etc.)
         
     | 
| 
      
 237 
     | 
    
         
            +
                ##
         
     | 
| 
      
 238 
     | 
    
         
            +
                ## @return     (Integer) seconds
         
     | 
| 
      
 239 
     | 
    
         
            +
                ##
         
     | 
| 
      
 240 
     | 
    
         
            +
                def chronify_qty(qty)
         
     | 
| 
      
 241 
     | 
    
         
            +
                  minutes = 0
         
     | 
| 
      
 242 
     | 
    
         
            +
                  case qty.strip
         
     | 
| 
      
 243 
     | 
    
         
            +
                  when /^(\d+):(\d\d)$/
         
     | 
| 
      
 244 
     | 
    
         
            +
                    minutes += Regexp.last_match(1).to_i * 60
         
     | 
| 
      
 245 
     | 
    
         
            +
                    minutes += Regexp.last_match(2).to_i
         
     | 
| 
      
 246 
     | 
    
         
            +
                  when /^(\d+(?:\.\d+)?)([hmd])?$/
         
     | 
| 
      
 247 
     | 
    
         
            +
                    amt = Regexp.last_match(1)
         
     | 
| 
      
 248 
     | 
    
         
            +
                    type = Regexp.last_match(2).nil? ? 'm' : Regexp.last_match(2)
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
                    minutes = case type.downcase
         
     | 
| 
      
 251 
     | 
    
         
            +
                              when 'm'
         
     | 
| 
      
 252 
     | 
    
         
            +
                                amt.to_i
         
     | 
| 
      
 253 
     | 
    
         
            +
                              when 'h'
         
     | 
| 
      
 254 
     | 
    
         
            +
                                (amt.to_f * 60).round
         
     | 
| 
      
 255 
     | 
    
         
            +
                              when 'd'
         
     | 
| 
      
 256 
     | 
    
         
            +
                                (amt.to_f * 60 * 24).round
         
     | 
| 
      
 257 
     | 
    
         
            +
                              else
         
     | 
| 
      
 258 
     | 
    
         
            +
                                minutes
         
     | 
| 
      
 259 
     | 
    
         
            +
                              end
         
     | 
| 
      
 260 
     | 
    
         
            +
                  end
         
     | 
| 
      
 261 
     | 
    
         
            +
                  minutes * 60
         
     | 
| 
      
 262 
     | 
    
         
            +
                end
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
                ##
         
     | 
| 
      
 265 
     | 
    
         
            +
                ## @brief      List sections
         
     | 
| 
      
 266 
     | 
    
         
            +
                ##
         
     | 
| 
      
 267 
     | 
    
         
            +
                ## @return     (Array) section titles
         
     | 
| 
      
 268 
     | 
    
         
            +
                ##
         
     | 
| 
      
 269 
     | 
    
         
            +
                def sections
         
     | 
| 
      
 270 
     | 
    
         
            +
                  @content.keys
         
     | 
| 
      
 271 
     | 
    
         
            +
                end
         
     | 
| 
      
 272 
     | 
    
         
            +
             
     | 
| 
      
 273 
     | 
    
         
            +
                ##
         
     | 
| 
      
 274 
     | 
    
         
            +
                ## @brief      Adds a section.
         
     | 
| 
      
 275 
     | 
    
         
            +
                ##
         
     | 
| 
      
 276 
     | 
    
         
            +
                ## @param      title  (String) The new section title
         
     | 
| 
      
 277 
     | 
    
         
            +
                ##
         
     | 
| 
      
 278 
     | 
    
         
            +
                def add_section(title)
         
     | 
| 
      
 279 
     | 
    
         
            +
                  if @content.key?(title.cap_first)
         
     | 
| 
      
 280 
     | 
    
         
            +
                    logger.debug('Skipped': 'Section already exists')
         
     | 
| 
      
 281 
     | 
    
         
            +
                    return
         
     | 
| 
      
 282 
     | 
    
         
            +
                  end
         
     | 
| 
       255 
283 
     | 
    
         | 
| 
       256 
     | 
    
         
            -
             
     | 
| 
       257 
     | 
    
         
            -
             
     | 
| 
       258 
     | 
    
         
            -
             
     | 
| 
       259 
     | 
    
         
            -
              ## @return     (String) CSS template
         
     | 
| 
       260 
     | 
    
         
            -
              ##
         
     | 
| 
       261 
     | 
    
         
            -
              def css_template
         
     | 
| 
       262 
     | 
    
         
            -
                IO.read(File.join(File.dirname(__FILE__), '../templates/doing.css'))
         
     | 
| 
       263 
     | 
    
         
            -
              end
         
     | 
| 
      
 284 
     | 
    
         
            +
                  @content[title.cap_first] = { :original => "#{title}:", :items => [] }
         
     | 
| 
      
 285 
     | 
    
         
            +
                  logger.info('New section:', %("#{title.cap_first}"))
         
     | 
| 
      
 286 
     | 
    
         
            +
                end
         
     | 
| 
       264 
287 
     | 
    
         | 
| 
       265 
     | 
    
         
            -
             
     | 
| 
       266 
     | 
    
         
            -
             
     | 
| 
       267 
     | 
    
         
            -
             
     | 
| 
       268 
     | 
    
         
            -
             
     | 
| 
       269 
     | 
    
         
            -
                 
     | 
| 
       270 
     | 
    
         
            -
                 
     | 
| 
      
 288 
     | 
    
         
            +
                ##
         
     | 
| 
      
 289 
     | 
    
         
            +
                ## @brief      Attempt to match a string with an existing section
         
     | 
| 
      
 290 
     | 
    
         
            +
                ##
         
     | 
| 
      
 291 
     | 
    
         
            +
                ## @param      frag     (String) The user-provided string
         
     | 
| 
      
 292 
     | 
    
         
            +
                ## @param      guessed  (Boolean) already guessed and failed
         
     | 
| 
      
 293 
     | 
    
         
            +
                ##
         
     | 
| 
      
 294 
     | 
    
         
            +
                def guess_section(frag, guessed: false, suggest: false)
         
     | 
| 
      
 295 
     | 
    
         
            +
                  return 'All' if frag =~ /^all$/i
         
     | 
| 
      
 296 
     | 
    
         
            +
                  frag ||= @config['current_section']
         
     | 
| 
       271 
297 
     | 
    
         | 
| 
       272 
     | 
    
         
            -
             
     | 
| 
       273 
     | 
    
         
            -
                   
     | 
| 
       274 
     | 
    
         
            -
             
     | 
| 
       275 
     | 
    
         
            -
             
     | 
| 
      
 298 
     | 
    
         
            +
                  sections.each { |sect| return sect.cap_first if frag.downcase == sect.downcase }
         
     | 
| 
      
 299 
     | 
    
         
            +
                  section = false
         
     | 
| 
      
 300 
     | 
    
         
            +
                  re = frag.split('').join('.*?')
         
     | 
| 
      
 301 
     | 
    
         
            +
                  sections.each do |sect|
         
     | 
| 
      
 302 
     | 
    
         
            +
                    next unless sect =~ /#{re}/i
         
     | 
| 
       276 
303 
     | 
    
         | 
| 
       277 
     | 
    
         
            -
             
     | 
| 
       278 
     | 
    
         
            -
             
     | 
| 
       279 
     | 
    
         
            -
             
     | 
| 
       280 
     | 
    
         
            -
             
     | 
| 
       281 
     | 
    
         
            -
              ##
         
     | 
| 
       282 
     | 
    
         
            -
              def fork_editor(input = '')
         
     | 
| 
       283 
     | 
    
         
            -
                tmpfile = Tempfile.new(['doing', '.md'])
         
     | 
| 
      
 304 
     | 
    
         
            +
                    logger.debug('Match:', %(Assuming "#{sect}" from "#{frag}"))
         
     | 
| 
      
 305 
     | 
    
         
            +
                    section = sect
         
     | 
| 
      
 306 
     | 
    
         
            +
                    break
         
     | 
| 
      
 307 
     | 
    
         
            +
                  end
         
     | 
| 
       284 
308 
     | 
    
         | 
| 
       285 
     | 
    
         
            -
             
     | 
| 
       286 
     | 
    
         
            -
                  f.puts input
         
     | 
| 
       287 
     | 
    
         
            -
                  f.puts "\n# The first line is the entry title, any lines after that are added as a note"
         
     | 
| 
       288 
     | 
    
         
            -
                end
         
     | 
| 
      
 309 
     | 
    
         
            +
                  return section if suggest
         
     | 
| 
       289 
310 
     | 
    
         | 
| 
       290 
     | 
    
         
            -
             
     | 
| 
      
 311 
     | 
    
         
            +
                  unless section || guessed
         
     | 
| 
      
 312 
     | 
    
         
            +
                    alt = guess_view(frag, guessed: true, suggest: true)
         
     | 
| 
      
 313 
     | 
    
         
            +
                    if alt
         
     | 
| 
      
 314 
     | 
    
         
            +
                      meant_view = yn("Did you mean `doing view #{alt}`?", default_response: 'n')
         
     | 
| 
      
 315 
     | 
    
         
            +
                      raise Errors::InvalidSection, "Run again with `doing view #{alt}`" if meant_view
         
     | 
| 
      
 316 
     | 
    
         
            +
                    end
         
     | 
| 
       291 
317 
     | 
    
         | 
| 
       292 
     | 
    
         
            -
             
     | 
| 
       293 
     | 
    
         
            -
                  begin
         
     | 
| 
       294 
     | 
    
         
            -
                    Process.kill(9, pid)
         
     | 
| 
       295 
     | 
    
         
            -
                  rescue StandardError
         
     | 
| 
       296 
     | 
    
         
            -
                    Errno::ESRCH
         
     | 
| 
       297 
     | 
    
         
            -
                  end
         
     | 
| 
       298 
     | 
    
         
            -
                  tmpfile.unlink
         
     | 
| 
       299 
     | 
    
         
            -
                  tmpfile.close!
         
     | 
| 
       300 
     | 
    
         
            -
                  exit 0
         
     | 
| 
       301 
     | 
    
         
            -
                end
         
     | 
| 
      
 318 
     | 
    
         
            +
                    res = yn("Section #{frag} not found, create it", default_response: 'n')
         
     | 
| 
       302 
319 
     | 
    
         | 
| 
       303 
     | 
    
         
            -
             
     | 
| 
      
 320 
     | 
    
         
            +
                    if res
         
     | 
| 
      
 321 
     | 
    
         
            +
                      add_section(frag.cap_first)
         
     | 
| 
      
 322 
     | 
    
         
            +
                      write(@doing_file)
         
     | 
| 
      
 323 
     | 
    
         
            +
                      return frag.cap_first
         
     | 
| 
      
 324 
     | 
    
         
            +
                    end
         
     | 
| 
       304 
325 
     | 
    
         | 
| 
       305 
     | 
    
         
            -
             
     | 
| 
       306 
     | 
    
         
            -
                   
     | 
| 
       307 
     | 
    
         
            -
             
     | 
| 
      
 326 
     | 
    
         
            +
                    raise Errors::InvalidSection, "Unknown section: #{frag}"
         
     | 
| 
      
 327 
     | 
    
         
            +
                  end
         
     | 
| 
      
 328 
     | 
    
         
            +
                  section ? section.cap_first : guessed
         
     | 
| 
      
 329 
     | 
    
         
            +
                end
         
     | 
| 
      
 330 
     | 
    
         
            +
             
     | 
| 
      
 331 
     | 
    
         
            +
                ##
         
     | 
| 
      
 332 
     | 
    
         
            +
                ## @brief      Ask a yes or no question in the terminal
         
     | 
| 
      
 333 
     | 
    
         
            +
                ##
         
     | 
| 
      
 334 
     | 
    
         
            +
                ## @param      question     (String) The question to ask
         
     | 
| 
      
 335 
     | 
    
         
            +
                ## @param      default      (Bool)   default response if no input
         
     | 
| 
      
 336 
     | 
    
         
            +
                ##
         
     | 
| 
      
 337 
     | 
    
         
            +
                ## @return     (Bool) yes or no
         
     | 
| 
      
 338 
     | 
    
         
            +
                ##
         
     | 
| 
      
 339 
     | 
    
         
            +
                def yn(question, default_response: false)
         
     | 
| 
      
 340 
     | 
    
         
            +
                  if default_response.is_a?(String)
         
     | 
| 
      
 341 
     | 
    
         
            +
                    default = default_response =~ /y/i ? true : false
         
     | 
| 
       308 
342 
     | 
    
         
             
                  else
         
     | 
| 
       309 
     | 
    
         
            -
                     
     | 
| 
      
 343 
     | 
    
         
            +
                    default = default_response
         
     | 
| 
       310 
344 
     | 
    
         
             
                  end
         
     | 
| 
       311 
     | 
    
         
            -
                ensure
         
     | 
| 
       312 
     | 
    
         
            -
                  tmpfile.close
         
     | 
| 
       313 
     | 
    
         
            -
                  tmpfile.unlink
         
     | 
| 
       314 
     | 
    
         
            -
                end
         
     | 
| 
       315 
     | 
    
         
            -
             
     | 
| 
       316 
     | 
    
         
            -
                input.split(/\n/).delete_if {|line| line =~ /^#/ }.join("\n")
         
     | 
| 
       317 
     | 
    
         
            -
              end
         
     | 
| 
       318 
345 
     | 
    
         | 
| 
       319 
     | 
    
         
            -
             
     | 
| 
       320 
     | 
    
         
            -
             
     | 
| 
       321 
     | 
    
         
            -
              #
         
     | 
| 
       322 
     | 
    
         
            -
              # @return     (Array) [(String)title, (Array)note]
         
     | 
| 
       323 
     | 
    
         
            -
              #
         
     | 
| 
       324 
     | 
    
         
            -
              # @param      input  (String) The string to parse
         
     | 
| 
       325 
     | 
    
         
            -
              #
         
     | 
| 
       326 
     | 
    
         
            -
              def format_input(input)
         
     | 
| 
       327 
     | 
    
         
            -
                exit_now! 'No content in entry' if input.nil? || input.strip.empty?
         
     | 
| 
       328 
     | 
    
         
            -
             
     | 
| 
       329 
     | 
    
         
            -
                input_lines = input.split(/[\n\r]+/).delete_if {|line| line =~ /^#/ || line =~ /^\s*$/ }
         
     | 
| 
       330 
     | 
    
         
            -
                title = input_lines[0]&.strip
         
     | 
| 
       331 
     | 
    
         
            -
                exit_now! 'No content in first line' if title.nil? || title.strip.empty?
         
     | 
| 
       332 
     | 
    
         
            -
             
     | 
| 
       333 
     | 
    
         
            -
                note = input_lines.length > 1 ? input_lines[1..-1] : []
         
     | 
| 
       334 
     | 
    
         
            -
                # If title line ends in a parenthetical, use that as the note
         
     | 
| 
       335 
     | 
    
         
            -
                if note.empty? && title =~ /\s+\(.*?\)$/
         
     | 
| 
       336 
     | 
    
         
            -
                  title.sub!(/\s+\((.*?)\)$/) do
         
     | 
| 
       337 
     | 
    
         
            -
                    m = Regexp.last_match
         
     | 
| 
       338 
     | 
    
         
            -
                    note.push(m[1])
         
     | 
| 
       339 
     | 
    
         
            -
                    ''
         
     | 
| 
       340 
     | 
    
         
            -
                  end
         
     | 
| 
       341 
     | 
    
         
            -
                end
         
     | 
| 
      
 346 
     | 
    
         
            +
                  # if global --default is set, answer default
         
     | 
| 
      
 347 
     | 
    
         
            +
                  return default if @default_option
         
     | 
| 
       342 
348 
     | 
    
         | 
| 
       343 
     | 
    
         
            -
             
     | 
| 
       344 
     | 
    
         
            -
             
     | 
| 
      
 349 
     | 
    
         
            +
                  # if this isn't an interactive shell, answer default
         
     | 
| 
      
 350 
     | 
    
         
            +
                  return default unless $stdout.isatty
         
     | 
| 
       345 
351 
     | 
    
         | 
| 
       346 
     | 
    
         
            -
             
     | 
| 
       347 
     | 
    
         
            -
             
     | 
| 
      
 352 
     | 
    
         
            +
                  # clear the buffer
         
     | 
| 
      
 353 
     | 
    
         
            +
                  if ARGV&.length
         
     | 
| 
      
 354 
     | 
    
         
            +
                    ARGV.length.times do
         
     | 
| 
      
 355 
     | 
    
         
            +
                      ARGV.shift
         
     | 
| 
      
 356 
     | 
    
         
            +
                    end
         
     | 
| 
      
 357 
     | 
    
         
            +
                  end
         
     | 
| 
      
 358 
     | 
    
         
            +
                  system 'stty cbreak'
         
     | 
| 
       348 
359 
     | 
    
         | 
| 
       349 
     | 
    
         
            -
             
     | 
| 
       350 
     | 
    
         
            -
             
     | 
| 
       351 
     | 
    
         
            -
             
     | 
| 
       352 
     | 
    
         
            -
             
     | 
| 
       353 
     | 
    
         
            -
              #             - a semantic phrase e.g. 'yesterday 5:30pm'
         
     | 
| 
       354 
     | 
    
         
            -
              #             - a strftime e.g. '2016-03-15 15:32:04 PDT'
         
     | 
| 
       355 
     | 
    
         
            -
              #
         
     | 
| 
       356 
     | 
    
         
            -
              # @param      input  (String) String to chronify
         
     | 
| 
       357 
     | 
    
         
            -
              #
         
     | 
| 
       358 
     | 
    
         
            -
              # @return     (DateTime) result
         
     | 
| 
       359 
     | 
    
         
            -
              #
         
     | 
| 
       360 
     | 
    
         
            -
              def chronify(input)
         
     | 
| 
       361 
     | 
    
         
            -
                now = Time.now
         
     | 
| 
       362 
     | 
    
         
            -
                exit_now! "Invalid time expression #{input.inspect}" if input.to_s.strip == ''
         
     | 
| 
       363 
     | 
    
         
            -
             
     | 
| 
       364 
     | 
    
         
            -
                secs_ago = if input.match(/^(\d+)$/)
         
     | 
| 
       365 
     | 
    
         
            -
                             # plain number, assume minutes
         
     | 
| 
       366 
     | 
    
         
            -
                             Regexp.last_match(1).to_i * 60
         
     | 
| 
       367 
     | 
    
         
            -
                           elsif (m = input.match(/^(?:(?<day>\d+)d)?(?:(?<hour>\d+)h)?(?:(?<min>\d+)m)?$/i))
         
     | 
| 
       368 
     | 
    
         
            -
                             # day/hour/minute format e.g. 1d2h30m
         
     | 
| 
       369 
     | 
    
         
            -
                             [[m['day'], 24 * 3600],
         
     | 
| 
       370 
     | 
    
         
            -
                              [m['hour'], 3600],
         
     | 
| 
       371 
     | 
    
         
            -
                              [m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+)
         
     | 
| 
       372 
     | 
    
         
            -
                           end
         
     | 
| 
       373 
     | 
    
         
            -
             
     | 
| 
       374 
     | 
    
         
            -
                if secs_ago
         
     | 
| 
       375 
     | 
    
         
            -
                  now - secs_ago
         
     | 
| 
       376 
     | 
    
         
            -
                else
         
     | 
| 
       377 
     | 
    
         
            -
                  Chronic.parse(input, { context: :past, ambiguous_time_range: 8 })
         
     | 
| 
       378 
     | 
    
         
            -
                end
         
     | 
| 
       379 
     | 
    
         
            -
              end
         
     | 
| 
      
 360 
     | 
    
         
            +
                  cw = Color.white
         
     | 
| 
      
 361 
     | 
    
         
            +
                  cbw = Color.boldwhite
         
     | 
| 
      
 362 
     | 
    
         
            +
                  cbg = Color.boldgreen
         
     | 
| 
      
 363 
     | 
    
         
            +
                  cd = Color.default
         
     | 
| 
       380 
364 
     | 
    
         | 
| 
       381 
     | 
    
         
            -
             
     | 
| 
       382 
     | 
    
         
            -
             
     | 
| 
       383 
     | 
    
         
            -
              #             object
         
     | 
| 
       384 
     | 
    
         
            -
              #
         
     | 
| 
       385 
     | 
    
         
            -
              # @param      qty   (String) HH:MM or XX[dhm][[XXhm][XXm]] (1d2h30m, 45m,
         
     | 
| 
       386 
     | 
    
         
            -
              #                   1.5d, 1h20m, etc.)
         
     | 
| 
       387 
     | 
    
         
            -
              #
         
     | 
| 
       388 
     | 
    
         
            -
              # @return     (Integer) seconds
         
     | 
| 
       389 
     | 
    
         
            -
              #
         
     | 
| 
       390 
     | 
    
         
            -
              def chronify_qty(qty)
         
     | 
| 
       391 
     | 
    
         
            -
                minutes = 0
         
     | 
| 
       392 
     | 
    
         
            -
                case qty.strip
         
     | 
| 
       393 
     | 
    
         
            -
                when /^(\d+):(\d\d)$/
         
     | 
| 
       394 
     | 
    
         
            -
                  minutes += Regexp.last_match(1).to_i * 60
         
     | 
| 
       395 
     | 
    
         
            -
                  minutes += Regexp.last_match(2).to_i
         
     | 
| 
       396 
     | 
    
         
            -
                when /^(\d+(?:\.\d+)?)([hmd])?$/
         
     | 
| 
       397 
     | 
    
         
            -
                  amt = Regexp.last_match(1)
         
     | 
| 
       398 
     | 
    
         
            -
                  type = Regexp.last_match(2).nil? ? 'm' : Regexp.last_match(2)
         
     | 
| 
       399 
     | 
    
         
            -
             
     | 
| 
       400 
     | 
    
         
            -
                  minutes = case type.downcase
         
     | 
| 
       401 
     | 
    
         
            -
                            when 'm'
         
     | 
| 
       402 
     | 
    
         
            -
                              amt.to_i
         
     | 
| 
       403 
     | 
    
         
            -
                            when 'h'
         
     | 
| 
       404 
     | 
    
         
            -
                              (amt.to_f * 60).round
         
     | 
| 
       405 
     | 
    
         
            -
                            when 'd'
         
     | 
| 
       406 
     | 
    
         
            -
                              (amt.to_f * 60 * 24).round
         
     | 
| 
      
 365 
     | 
    
         
            +
                  options = unless default.nil?
         
     | 
| 
      
 366 
     | 
    
         
            +
                              "#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
         
     | 
| 
       407 
367 
     | 
    
         
             
                            else
         
     | 
| 
       408 
     | 
    
         
            -
                               
     | 
| 
      
 368 
     | 
    
         
            +
                              "#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
         
     | 
| 
       409 
369 
     | 
    
         
             
                            end
         
     | 
| 
       410 
     | 
    
         
            -
             
     | 
| 
       411 
     | 
    
         
            -
             
     | 
| 
       412 
     | 
    
         
            -
             
     | 
| 
       413 
     | 
    
         
            -
             
     | 
| 
       414 
     | 
    
         
            -
             
     | 
| 
       415 
     | 
    
         
            -
             
     | 
| 
       416 
     | 
    
         
            -
             
     | 
| 
       417 
     | 
    
         
            -
             
     | 
| 
       418 
     | 
    
         
            -
             
     | 
| 
       419 
     | 
    
         
            -
             
     | 
| 
       420 
     | 
    
         
            -
             
     | 
| 
       421 
     | 
    
         
            -
             
     | 
| 
       422 
     | 
    
         
            -
             
     | 
| 
       423 
     | 
    
         
            -
             
     | 
| 
       424 
     | 
    
         
            -
             
     | 
| 
       425 
     | 
    
         
            -
             
     | 
| 
       426 
     | 
    
         
            -
             
     | 
| 
       427 
     | 
    
         
            -
               
     | 
| 
       428 
     | 
    
         
            -
             
     | 
| 
       429 
     | 
    
         
            -
                 
     | 
| 
       430 
     | 
    
         
            -
             
     | 
| 
       431 
     | 
    
         
            -
             
     | 
| 
      
 370 
     | 
    
         
            +
                  $stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
         
     | 
| 
      
 371 
     | 
    
         
            +
                  res = $stdin.sysread 1
         
     | 
| 
      
 372 
     | 
    
         
            +
                  puts
         
     | 
| 
      
 373 
     | 
    
         
            +
                  system 'stty cooked'
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
                  res.chomp!
         
     | 
| 
      
 376 
     | 
    
         
            +
                  res.downcase!
         
     | 
| 
      
 377 
     | 
    
         
            +
             
     | 
| 
      
 378 
     | 
    
         
            +
                  return default if res.empty?
         
     | 
| 
      
 379 
     | 
    
         
            +
             
     | 
| 
      
 380 
     | 
    
         
            +
                  res =~ /y/i ? true : false
         
     | 
| 
      
 381 
     | 
    
         
            +
                end
         
     | 
| 
      
 382 
     | 
    
         
            +
             
     | 
| 
      
 383 
     | 
    
         
            +
                ##
         
     | 
| 
      
 384 
     | 
    
         
            +
                ## @brief      Attempt to match a string with an existing view
         
     | 
| 
      
 385 
     | 
    
         
            +
                ##
         
     | 
| 
      
 386 
     | 
    
         
            +
                ## @param      frag     (String) The user-provided string
         
     | 
| 
      
 387 
     | 
    
         
            +
                ## @param      guessed  (Boolean) already guessed
         
     | 
| 
      
 388 
     | 
    
         
            +
                ##
         
     | 
| 
      
 389 
     | 
    
         
            +
                def guess_view(frag, guessed: false, suggest: false)
         
     | 
| 
      
 390 
     | 
    
         
            +
                  views.each { |view| return view if frag.downcase == view.downcase }
         
     | 
| 
      
 391 
     | 
    
         
            +
                  view = false
         
     | 
| 
      
 392 
     | 
    
         
            +
                  re = frag.split('').join('.*?')
         
     | 
| 
      
 393 
     | 
    
         
            +
                  views.each do |v|
         
     | 
| 
      
 394 
     | 
    
         
            +
                    next unless v =~ /#{re}/i
         
     | 
| 
      
 395 
     | 
    
         
            +
             
     | 
| 
      
 396 
     | 
    
         
            +
                    logger.debug('Match:', %(Assuming "#{v}" from "#{frag}"))
         
     | 
| 
      
 397 
     | 
    
         
            +
                    view = v
         
     | 
| 
      
 398 
     | 
    
         
            +
                    break
         
     | 
| 
      
 399 
     | 
    
         
            +
                  end
         
     | 
| 
      
 400 
     | 
    
         
            +
                  unless view || guessed
         
     | 
| 
      
 401 
     | 
    
         
            +
                    guess = guess_section(frag, guessed: true, suggest: true)
         
     | 
| 
      
 402 
     | 
    
         
            +
                    exit_now! "Did you mean `doing show #{guess}`?" if guess
         
     | 
| 
       432 
403 
     | 
    
         | 
| 
       433 
     | 
    
         
            -
             
     | 
| 
       434 
     | 
    
         
            -
              ## @brief      Attempt to match a string with an existing section
         
     | 
| 
       435 
     | 
    
         
            -
              ##
         
     | 
| 
       436 
     | 
    
         
            -
              ## @param      frag     (String) The user-provided string
         
     | 
| 
       437 
     | 
    
         
            -
              ## @param      guessed  (Boolean) already guessed and failed
         
     | 
| 
       438 
     | 
    
         
            -
              ##
         
     | 
| 
       439 
     | 
    
         
            -
              def guess_section(frag, guessed: false)
         
     | 
| 
       440 
     | 
    
         
            -
                return 'All' if frag =~ /^all$/i
         
     | 
| 
       441 
     | 
    
         
            -
                frag ||= @current_section
         
     | 
| 
       442 
     | 
    
         
            -
                sections.each { |section| return section.cap_first if frag.downcase == section.downcase }
         
     | 
| 
       443 
     | 
    
         
            -
                section = false
         
     | 
| 
       444 
     | 
    
         
            -
                re = frag.split('').join('.*?')
         
     | 
| 
       445 
     | 
    
         
            -
                sections.each do |sect|
         
     | 
| 
       446 
     | 
    
         
            -
                  next unless sect =~ /#{re}/i
         
     | 
| 
       447 
     | 
    
         
            -
             
     | 
| 
       448 
     | 
    
         
            -
                  warn "Assuming you meant #{sect}"
         
     | 
| 
       449 
     | 
    
         
            -
                  section = sect
         
     | 
| 
       450 
     | 
    
         
            -
                  break
         
     | 
| 
       451 
     | 
    
         
            -
                end
         
     | 
| 
       452 
     | 
    
         
            -
                unless section || guessed
         
     | 
| 
       453 
     | 
    
         
            -
                  alt = guess_view(frag, true)
         
     | 
| 
       454 
     | 
    
         
            -
                  exit_now! "Did you mean `doing view #{alt}`?" if alt
         
     | 
| 
      
 404 
     | 
    
         
            +
                    raise Errors::InvalidView, "Unknown view: #{frag}"
         
     | 
| 
       455 
405 
     | 
    
         | 
| 
       456 
     | 
    
         
            -
                   
     | 
| 
      
 406 
     | 
    
         
            +
                  end
         
     | 
| 
      
 407 
     | 
    
         
            +
                  view
         
     | 
| 
      
 408 
     | 
    
         
            +
                end
         
     | 
| 
      
 409 
     | 
    
         
            +
             
     | 
| 
      
 410 
     | 
    
         
            +
                ##
         
     | 
| 
      
 411 
     | 
    
         
            +
                ## @brief      Adds an entry
         
     | 
| 
      
 412 
     | 
    
         
            +
                ##
         
     | 
| 
      
 413 
     | 
    
         
            +
                ## @param      title    (String) The entry title
         
     | 
| 
      
 414 
     | 
    
         
            +
                ## @param      section  (String) The section to add to
         
     | 
| 
      
 415 
     | 
    
         
            +
                ## @param      opt      (Hash) Additional Options {:date, :note, :back, :timed}
         
     | 
| 
      
 416 
     | 
    
         
            +
                ##
         
     | 
| 
      
 417 
     | 
    
         
            +
                def add_item(title, section = nil, opt = {})
         
     | 
| 
      
 418 
     | 
    
         
            +
                  section ||= @config['current_section']
         
     | 
| 
      
 419 
     | 
    
         
            +
                  add_section(section) unless @content.key?(section)
         
     | 
| 
      
 420 
     | 
    
         
            +
                  opt[:date] ||= Time.now
         
     | 
| 
      
 421 
     | 
    
         
            +
                  opt[:note] ||= []
         
     | 
| 
      
 422 
     | 
    
         
            +
                  opt[:back] ||= Time.now
         
     | 
| 
      
 423 
     | 
    
         
            +
                  opt[:timed] ||= false
         
     | 
| 
      
 424 
     | 
    
         
            +
             
     | 
| 
      
 425 
     | 
    
         
            +
                  opt[:note] = opt[:note].lines if opt[:note].is_a?(String)
         
     | 
| 
      
 426 
     | 
    
         
            +
             
     | 
| 
      
 427 
     | 
    
         
            +
                  title = [title.strip.cap_first]
         
     | 
| 
      
 428 
     | 
    
         
            +
                  title = title.join(' ')
         
     | 
| 
      
 429 
     | 
    
         
            +
             
     | 
| 
      
 430 
     | 
    
         
            +
                  if @auto_tag
         
     | 
| 
      
 431 
     | 
    
         
            +
                    title = autotag(title)
         
     | 
| 
      
 432 
     | 
    
         
            +
                    title.add_tags!(@config['default_tags']) unless @config['default_tags'].empty?
         
     | 
| 
      
 433 
     | 
    
         
            +
                  end
         
     | 
| 
       457 
434 
     | 
    
         | 
| 
       458 
     | 
    
         
            -
                   
     | 
| 
       459 
     | 
    
         
            -
             
     | 
| 
       460 
     | 
    
         
            -
             
     | 
| 
       461 
     | 
    
         
            -
             
     | 
| 
      
 435 
     | 
    
         
            +
                  title.gsub!(/ +/, ' ')
         
     | 
| 
      
 436 
     | 
    
         
            +
                  entry = Item.new(opt[:back], title.strip, section)
         
     | 
| 
      
 437 
     | 
    
         
            +
                  entry.note = opt[:note].map(&:chomp) unless opt[:note].join('').strip == ''
         
     | 
| 
      
 438 
     | 
    
         
            +
                  items = @content[section][:items]
         
     | 
| 
      
 439 
     | 
    
         
            +
                  if opt[:timed]
         
     | 
| 
      
 440 
     | 
    
         
            +
                    items.reverse!
         
     | 
| 
      
 441 
     | 
    
         
            +
                    items.each_with_index do |i, x|
         
     | 
| 
      
 442 
     | 
    
         
            +
                      next if i.title =~ / @done/
         
     | 
| 
      
 443 
     | 
    
         
            +
             
     | 
| 
      
 444 
     | 
    
         
            +
                      items[x].title = "#{i.title} @done(#{opt[:back].strftime('%F %R')})"
         
     | 
| 
      
 445 
     | 
    
         
            +
                      break
         
     | 
| 
      
 446 
     | 
    
         
            +
                    end
         
     | 
| 
      
 447 
     | 
    
         
            +
                    items.reverse!
         
     | 
| 
       462 
448 
     | 
    
         
             
                  end
         
     | 
| 
       463 
449 
     | 
    
         | 
| 
       464 
     | 
    
         
            -
                   
     | 
| 
      
 450 
     | 
    
         
            +
                  items.push(entry)
         
     | 
| 
      
 451 
     | 
    
         
            +
                  logger.count(:added)
         
     | 
| 
      
 452 
     | 
    
         
            +
                  logger.debug('Entry added:', %("#{entry.title}" to #{section}))
         
     | 
| 
       465 
453 
     | 
    
         
             
                end
         
     | 
| 
       466 
     | 
    
         
            -
                section ? section.cap_first : guessed
         
     | 
| 
       467 
     | 
    
         
            -
              end
         
     | 
| 
       468 
454 
     | 
    
         | 
| 
       469 
     | 
    
         
            -
             
     | 
| 
       470 
     | 
    
         
            -
             
     | 
| 
       471 
     | 
    
         
            -
             
     | 
| 
       472 
     | 
    
         
            -
             
     | 
| 
       473 
     | 
    
         
            -
             
     | 
| 
       474 
     | 
    
         
            -
             
     | 
| 
       475 
     | 
    
         
            -
             
     | 
| 
       476 
     | 
    
         
            -
              ##
         
     | 
| 
       477 
     | 
    
         
            -
              def yn(question, default_response: false)
         
     | 
| 
       478 
     | 
    
         
            -
                default = default_response ? default_response : 'n'
         
     | 
| 
      
 455 
     | 
    
         
            +
                ##
         
     | 
| 
      
 456 
     | 
    
         
            +
                ## @brief      Remove items from a list that already exist in @content
         
     | 
| 
      
 457 
     | 
    
         
            +
                ##
         
     | 
| 
      
 458 
     | 
    
         
            +
                ## @param      items       (Array) The items to deduplicate
         
     | 
| 
      
 459 
     | 
    
         
            +
                ## @param      no_overlap  (Boolean) Remove items with overlapping time spans
         
     | 
| 
      
 460 
     | 
    
         
            +
                ##
         
     | 
| 
      
 461 
     | 
    
         
            +
                def dedup(items, no_overlap = false)
         
     | 
| 
       479 
462 
     | 
    
         | 
| 
       480 
     | 
    
         
            -
             
     | 
| 
       481 
     | 
    
         
            -
             
     | 
| 
       482 
     | 
    
         
            -
             
     | 
| 
       483 
     | 
    
         
            -
                # clear the buffer
         
     | 
| 
       484 
     | 
    
         
            -
                if ARGV&.length
         
     | 
| 
       485 
     | 
    
         
            -
                  ARGV.length.times do
         
     | 
| 
       486 
     | 
    
         
            -
                    ARGV.shift
         
     | 
| 
      
 463 
     | 
    
         
            +
                  combined = []
         
     | 
| 
      
 464 
     | 
    
         
            +
                  @content.each do |_k, v|
         
     | 
| 
      
 465 
     | 
    
         
            +
                    combined += v[:items]
         
     | 
| 
       487 
466 
     | 
    
         
             
                  end
         
     | 
| 
       488 
     | 
    
         
            -
                end
         
     | 
| 
       489 
     | 
    
         
            -
                system 'stty cbreak'
         
     | 
| 
       490 
     | 
    
         
            -
             
     | 
| 
       491 
     | 
    
         
            -
                cw = colors['white']
         
     | 
| 
       492 
     | 
    
         
            -
                cbw = colors['boldwhite']
         
     | 
| 
       493 
     | 
    
         
            -
                cbg = colors['boldgreen']
         
     | 
| 
       494 
     | 
    
         
            -
                cd = colors['default']
         
     | 
| 
       495 
467 
     | 
    
         | 
| 
       496 
     | 
    
         
            -
             
     | 
| 
       497 
     | 
    
         
            -
             
     | 
| 
       498 
     | 
    
         
            -
             
     | 
| 
       499 
     | 
    
         
            -
             
     | 
| 
       500 
     | 
    
         
            -
             
     | 
| 
       501 
     | 
    
         
            -
             
     | 
| 
       502 
     | 
    
         
            -
             
     | 
| 
       503 
     | 
    
         
            -
             
     | 
| 
       504 
     | 
    
         
            -
             
     | 
| 
       505 
     | 
    
         
            -
             
     | 
| 
       506 
     | 
    
         
            -
                res.chomp!
         
     | 
| 
       507 
     | 
    
         
            -
                res.downcase!
         
     | 
| 
       508 
     | 
    
         
            -
             
     | 
| 
       509 
     | 
    
         
            -
                res = default.downcase if res == ''
         
     | 
| 
       510 
     | 
    
         
            -
             
     | 
| 
       511 
     | 
    
         
            -
                res =~ /y/i
         
     | 
| 
       512 
     | 
    
         
            -
              end
         
     | 
| 
       513 
     | 
    
         
            -
             
     | 
| 
       514 
     | 
    
         
            -
              ##
         
     | 
| 
       515 
     | 
    
         
            -
              ## @brief      Attempt to match a string with an existing view
         
     | 
| 
       516 
     | 
    
         
            -
              ##
         
     | 
| 
       517 
     | 
    
         
            -
              ## @param      frag     (String) The user-provided string
         
     | 
| 
       518 
     | 
    
         
            -
              ## @param      guessed  (Boolean) already guessed
         
     | 
| 
       519 
     | 
    
         
            -
              ##
         
     | 
| 
       520 
     | 
    
         
            -
              def guess_view(frag, guessed = false)
         
     | 
| 
       521 
     | 
    
         
            -
                views.each { |view| return view if frag.downcase == view.downcase }
         
     | 
| 
       522 
     | 
    
         
            -
                view = false
         
     | 
| 
       523 
     | 
    
         
            -
                re = frag.split('').join('.*?')
         
     | 
| 
       524 
     | 
    
         
            -
                views.each do |v|
         
     | 
| 
       525 
     | 
    
         
            -
                  next unless v =~ /#{re}/i
         
     | 
| 
       526 
     | 
    
         
            -
             
     | 
| 
       527 
     | 
    
         
            -
                  warn "Assuming you meant #{v}"
         
     | 
| 
       528 
     | 
    
         
            -
                  view = v
         
     | 
| 
       529 
     | 
    
         
            -
                  break
         
     | 
| 
       530 
     | 
    
         
            -
                end
         
     | 
| 
       531 
     | 
    
         
            -
                unless view || guessed
         
     | 
| 
       532 
     | 
    
         
            -
                  alt = guess_section(frag, guessed: true)
         
     | 
| 
       533 
     | 
    
         
            -
                  if alt
         
     | 
| 
       534 
     | 
    
         
            -
                    exit_now! "Did you mean `doing show #{alt}`?"
         
     | 
| 
       535 
     | 
    
         
            -
                  else
         
     | 
| 
       536 
     | 
    
         
            -
                    exit_now! "Unknown view: #{frag}"
         
     | 
| 
      
 468 
     | 
    
         
            +
                  items.delete_if do |item|
         
     | 
| 
      
 469 
     | 
    
         
            +
                    duped = false
         
     | 
| 
      
 470 
     | 
    
         
            +
                    combined.each do |comp|
         
     | 
| 
      
 471 
     | 
    
         
            +
                      duped = no_overlap ? item.overlapping_time?(comp) : item.same_time?(comp)
         
     | 
| 
      
 472 
     | 
    
         
            +
                      break if duped
         
     | 
| 
      
 473 
     | 
    
         
            +
                    end
         
     | 
| 
      
 474 
     | 
    
         
            +
                    logger.count(:skipped, level: :debug, message: 'overlapping entry') if duped
         
     | 
| 
      
 475 
     | 
    
         
            +
                    logger.log_now(:debug, 'Skipped:', "overlapping entry: #{item.title}") if duped
         
     | 
| 
      
 476 
     | 
    
         
            +
                    duped
         
     | 
| 
       537 
477 
     | 
    
         
             
                  end
         
     | 
| 
       538 
478 
     | 
    
         
             
                end
         
     | 
| 
       539 
     | 
    
         
            -
                view
         
     | 
| 
       540 
     | 
    
         
            -
              end
         
     | 
| 
       541 
479 
     | 
    
         | 
| 
       542 
     | 
    
         
            -
             
     | 
| 
       543 
     | 
    
         
            -
             
     | 
| 
       544 
     | 
    
         
            -
             
     | 
| 
       545 
     | 
    
         
            -
             
     | 
| 
       546 
     | 
    
         
            -
             
     | 
| 
       547 
     | 
    
         
            -
             
     | 
| 
       548 
     | 
    
         
            -
             
     | 
| 
       549 
     | 
    
         
            -
             
     | 
| 
       550 
     | 
    
         
            -
             
     | 
| 
       551 
     | 
    
         
            -
             
     | 
| 
       552 
     | 
    
         
            -
             
     | 
| 
       553 
     | 
    
         
            -
             
     | 
| 
       554 
     | 
    
         
            -
             
     | 
| 
       555 
     | 
    
         
            -
                opt[:timed] ||= false
         
     | 
| 
       556 
     | 
    
         
            -
             
     | 
| 
       557 
     | 
    
         
            -
                opt[:note] = [opt[:note]] if opt[:note].instance_of?(String)
         
     | 
| 
       558 
     | 
    
         
            -
             
     | 
| 
       559 
     | 
    
         
            -
                title = [title.strip.cap_first]
         
     | 
| 
       560 
     | 
    
         
            -
                title = title.join(' ')
         
     | 
| 
       561 
     | 
    
         
            -
             
     | 
| 
       562 
     | 
    
         
            -
                if @auto_tag
         
     | 
| 
       563 
     | 
    
         
            -
                  title = autotag(title)
         
     | 
| 
       564 
     | 
    
         
            -
                  unless @config['default_tags'].empty?
         
     | 
| 
       565 
     | 
    
         
            -
                    default_tags = @config['default_tags'].map do |t|
         
     | 
| 
       566 
     | 
    
         
            -
                      next if t.nil?
         
     | 
| 
       567 
     | 
    
         
            -
             
     | 
| 
       568 
     | 
    
         
            -
                      dt = t.sub(/^ *@/, '').chomp
         
     | 
| 
       569 
     | 
    
         
            -
                      if title =~ /@#{dt}/
         
     | 
| 
       570 
     | 
    
         
            -
                        ''
         
     | 
| 
       571 
     | 
    
         
            -
                      else
         
     | 
| 
       572 
     | 
    
         
            -
                        " @#{dt}"
         
     | 
| 
      
 480 
     | 
    
         
            +
                ##
         
     | 
| 
      
 481 
     | 
    
         
            +
                ## @brief      Imports external entries
         
     | 
| 
      
 482 
     | 
    
         
            +
                ##
         
     | 
| 
      
 483 
     | 
    
         
            +
                ## @param      path     (String) Path to JSON report file
         
     | 
| 
      
 484 
     | 
    
         
            +
                ## @param      opt      (Hash) Additional Options
         
     | 
| 
      
 485 
     | 
    
         
            +
                ##
         
     | 
| 
      
 486 
     | 
    
         
            +
                def import(paths, opt = {})
         
     | 
| 
      
 487 
     | 
    
         
            +
                  Plugins.plugins[:import].each do |_, options|
         
     | 
| 
      
 488 
     | 
    
         
            +
                    next unless opt[:type] =~ /^(#{options[:trigger].normalize_trigger})$/i
         
     | 
| 
      
 489 
     | 
    
         
            +
             
     | 
| 
      
 490 
     | 
    
         
            +
                    if paths.count.positive?
         
     | 
| 
      
 491 
     | 
    
         
            +
                      paths.each do |path|
         
     | 
| 
      
 492 
     | 
    
         
            +
                        options[:class].import(self, path, options: opt)
         
     | 
| 
       573 
493 
     | 
    
         
             
                      end
         
     | 
| 
      
 494 
     | 
    
         
            +
                    else
         
     | 
| 
      
 495 
     | 
    
         
            +
                      options[:class].import(self, nil, options: opt)
         
     | 
| 
       574 
496 
     | 
    
         
             
                    end
         
     | 
| 
       575 
     | 
    
         
            -
                    default_tags.delete_if { |t| t == '' }
         
     | 
| 
       576 
     | 
    
         
            -
                    title += default_tags.join(' ')
         
     | 
| 
       577 
     | 
    
         
            -
                  end
         
     | 
| 
       578 
     | 
    
         
            -
                end
         
     | 
| 
       579 
     | 
    
         
            -
                title.gsub!(/ +/, ' ')
         
     | 
| 
       580 
     | 
    
         
            -
                entry = { 'title' => title.strip, 'date' => opt[:back] }
         
     | 
| 
       581 
     | 
    
         
            -
                entry['note'] = opt[:note].map(&:chomp) unless opt[:note].join('').strip == ''
         
     | 
| 
       582 
     | 
    
         
            -
                items = @content[section]['items']
         
     | 
| 
       583 
     | 
    
         
            -
                if opt[:timed]
         
     | 
| 
       584 
     | 
    
         
            -
                  items.reverse!
         
     | 
| 
       585 
     | 
    
         
            -
                  items.each_with_index do |i, x|
         
     | 
| 
       586 
     | 
    
         
            -
                    next if i['title'] =~ / @done/
         
     | 
| 
       587 
     | 
    
         
            -
             
     | 
| 
       588 
     | 
    
         
            -
                    items[x]['title'] = "#{i['title']} @done(#{opt[:back].strftime('%F %R')})"
         
     | 
| 
       589 
497 
     | 
    
         
             
                    break
         
     | 
| 
       590 
498 
     | 
    
         
             
                  end
         
     | 
| 
       591 
     | 
    
         
            -
                  items.reverse!
         
     | 
| 
       592 
499 
     | 
    
         
             
                end
         
     | 
| 
       593 
     | 
    
         
            -
                items.push(entry)
         
     | 
| 
       594 
     | 
    
         
            -
                @content[section]['items'] = items
         
     | 
| 
       595 
     | 
    
         
            -
                @results.push(%(Added "#{entry['title']}" to #{section}))
         
     | 
| 
       596 
     | 
    
         
            -
              end
         
     | 
| 
       597 
500 
     | 
    
         | 
| 
       598 
     | 
    
         
            -
             
     | 
| 
       599 
     | 
    
         
            -
                 
     | 
| 
       600 
     | 
    
         
            -
             
     | 
| 
      
 501 
     | 
    
         
            +
                ##
         
     | 
| 
      
 502 
     | 
    
         
            +
                ## @brief      Return the content of the last note for a given section
         
     | 
| 
      
 503 
     | 
    
         
            +
                ##
         
     | 
| 
      
 504 
     | 
    
         
            +
                ## @param      section  (String) The section to retrieve from, default
         
     | 
| 
      
 505 
     | 
    
         
            +
                ##                      All
         
     | 
| 
      
 506 
     | 
    
         
            +
                ##
         
     | 
| 
      
 507 
     | 
    
         
            +
                def last_note(section = 'All')
         
     | 
| 
      
 508 
     | 
    
         
            +
                  section = guess_section(section)
         
     | 
| 
       601 
509 
     | 
    
         | 
| 
       602 
     | 
    
         
            -
             
     | 
| 
       603 
     | 
    
         
            -
                return true if same_time?(item_a, item_b)
         
     | 
| 
      
 510 
     | 
    
         
            +
                  last_item = last_entry({ section: section })
         
     | 
| 
       604 
511 
     | 
    
         | 
| 
       605 
     | 
    
         
            -
             
     | 
| 
       606 
     | 
    
         
            -
                interval = get_interval(item_a, formatted: false, record: false)
         
     | 
| 
       607 
     | 
    
         
            -
                end_a = interval ? start_a + interval.to_i : start_a
         
     | 
| 
       608 
     | 
    
         
            -
                start_b = item_b['date']
         
     | 
| 
       609 
     | 
    
         
            -
                interval = get_interval(item_b,  formatted: false, record: false)
         
     | 
| 
       610 
     | 
    
         
            -
                end_b = interval ? start_b + interval.to_i : start_b
         
     | 
| 
       611 
     | 
    
         
            -
                (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
         
     | 
| 
       612 
     | 
    
         
            -
              end
         
     | 
| 
      
 512 
     | 
    
         
            +
                  raise Errors::NoEntryError, 'No entry found' unless last_item
         
     | 
| 
       613 
513 
     | 
    
         | 
| 
       614 
     | 
    
         
            -
             
     | 
| 
      
 514 
     | 
    
         
            +
                  logger.log_now(:info, 'Edit note:', last_item.title)
         
     | 
| 
       615 
515 
     | 
    
         | 
| 
       616 
     | 
    
         
            -
             
     | 
| 
       617 
     | 
    
         
            -
             
     | 
| 
       618 
     | 
    
         
            -
                  combined += v['items']
         
     | 
| 
      
 516 
     | 
    
         
            +
                  note = last_item.note&.to_s || ''
         
     | 
| 
      
 517 
     | 
    
         
            +
                  "#{last_item.title}\n# EDIT BELOW THIS LINE ------------\n#{note}"
         
     | 
| 
       619 
518 
     | 
    
         
             
                end
         
     | 
| 
       620 
519 
     | 
    
         | 
| 
       621 
     | 
    
         
            -
                 
     | 
| 
       622 
     | 
    
         
            -
                   
     | 
| 
       623 
     | 
    
         
            -
                   
     | 
| 
       624 
     | 
    
         
            -
                     
     | 
| 
       625 
     | 
    
         
            -
                    break if duped
         
     | 
| 
      
 520 
     | 
    
         
            +
                def reset_item(item, resume: false)
         
     | 
| 
      
 521 
     | 
    
         
            +
                  item.date = Time.now
         
     | 
| 
      
 522 
     | 
    
         
            +
                  if resume
         
     | 
| 
      
 523 
     | 
    
         
            +
                    item.tag('done', remove: true)
         
     | 
| 
       626 
524 
     | 
    
         
             
                  end
         
     | 
| 
       627 
     | 
    
         
            -
                  #  
     | 
| 
       628 
     | 
    
         
            -
                   
     | 
| 
      
 525 
     | 
    
         
            +
                  Doing.logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
         
     | 
| 
      
 526 
     | 
    
         
            +
                  item
         
     | 
| 
       629 
527 
     | 
    
         
             
                end
         
     | 
| 
       630 
     | 
    
         
            -
              end
         
     | 
| 
       631 
528 
     | 
    
         | 
| 
       632 
     | 
    
         
            -
             
     | 
| 
       633 
     | 
    
         
            -
             
     | 
| 
       634 
     | 
    
         
            -
             
     | 
| 
       635 
     | 
    
         
            -
             
     | 
| 
       636 
     | 
    
         
            -
             
     | 
| 
       637 
     | 
    
         
            -
              ## @param      opt      (Hash) Additional Options
         
     | 
| 
       638 
     | 
    
         
            -
              ##
         
     | 
| 
       639 
     | 
    
         
            -
              def import_timing(path, opt = {})
         
     | 
| 
       640 
     | 
    
         
            -
                section = opt[:section] || @current_section
         
     | 
| 
       641 
     | 
    
         
            -
                opt[:no_overlap] ||= false
         
     | 
| 
       642 
     | 
    
         
            -
                opt[:autotag] ||= @auto_tag
         
     | 
| 
       643 
     | 
    
         
            -
             
     | 
| 
       644 
     | 
    
         
            -
                add_section(section) unless @content.has_key?(section)
         
     | 
| 
       645 
     | 
    
         
            -
             
     | 
| 
       646 
     | 
    
         
            -
                add_tags = opt[:tag] ? opt[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '@') }.join(' ') : ''
         
     | 
| 
       647 
     | 
    
         
            -
                prefix = opt[:prefix] ? opt[:prefix] : '[Timing.app]'
         
     | 
| 
       648 
     | 
    
         
            -
                exit_now! "File not found" unless File.exist?(File.expand_path(path))
         
     | 
| 
       649 
     | 
    
         
            -
             
     | 
| 
       650 
     | 
    
         
            -
                data = JSON.parse(IO.read(File.expand_path(path)))
         
     | 
| 
       651 
     | 
    
         
            -
                new_items = []
         
     | 
| 
       652 
     | 
    
         
            -
                data.each do |entry|
         
     | 
| 
       653 
     | 
    
         
            -
                  # Only process task entries
         
     | 
| 
       654 
     | 
    
         
            -
                  next if entry.key?('activityType') && entry['activityType'] != 'Task'
         
     | 
| 
       655 
     | 
    
         
            -
                  # Only process entries with a start and end date
         
     | 
| 
       656 
     | 
    
         
            -
                  next unless entry.key?('startDate') && entry.key?('endDate')
         
     | 
| 
       657 
     | 
    
         
            -
             
     | 
| 
       658 
     | 
    
         
            -
                  # Round down seconds and convert UTC to local time
         
     | 
| 
       659 
     | 
    
         
            -
                  start_time = Time.parse(entry['startDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
         
     | 
| 
       660 
     | 
    
         
            -
                  end_time = Time.parse(entry['endDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
         
     | 
| 
       661 
     | 
    
         
            -
                  next unless start_time && end_time
         
     | 
| 
       662 
     | 
    
         
            -
             
     | 
| 
       663 
     | 
    
         
            -
                  tags = entry['project'].split(/ ▸ /).map {|proj| proj.gsub(/[^a-z0-9]+/i, '').downcase }
         
     | 
| 
       664 
     | 
    
         
            -
                  title = "#{prefix} "
         
     | 
| 
       665 
     | 
    
         
            -
                  title += entry.key?('activityTitle') && entry['activityTitle'] != '(Untitled Task)' ? entry['activityTitle'] : 'Working on'
         
     | 
| 
       666 
     | 
    
         
            -
                  tags.each do |tag|
         
     | 
| 
       667 
     | 
    
         
            -
                    if title =~ /\b#{tag}\b/i
         
     | 
| 
       668 
     | 
    
         
            -
                      title.sub!(/\b#{tag}\b/i, "@#{tag}")
         
     | 
| 
      
 529 
     | 
    
         
            +
                def repeat_item(item, opt = {})
         
     | 
| 
      
 530 
     | 
    
         
            +
                  original = item.dup
         
     | 
| 
      
 531 
     | 
    
         
            +
                  if item.should_finish?
         
     | 
| 
      
 532 
     | 
    
         
            +
                    if item.should_time?
         
     | 
| 
      
 533 
     | 
    
         
            +
                      item.title.tag!('done', value: Time.now.strftime('%F %R'))
         
     | 
| 
       669 
534 
     | 
    
         
             
                    else
         
     | 
| 
       670 
     | 
    
         
            -
                      title 
     | 
| 
      
 535 
     | 
    
         
            +
                      item.title.tag!('done')
         
     | 
| 
       671 
536 
     | 
    
         
             
                    end
         
     | 
| 
       672 
537 
     | 
    
         
             
                  end
         
     | 
| 
       673 
     | 
    
         
            -
                  title = autotag(title) if opt[:autotag]
         
     | 
| 
       674 
     | 
    
         
            -
                  title += " @done(#{end_time.strftime('%Y-%m-%d %H:%M')})"
         
     | 
| 
       675 
     | 
    
         
            -
                  title.gsub!(/ +/, ' ')
         
     | 
| 
       676 
     | 
    
         
            -
                  title.strip!
         
     | 
| 
       677 
     | 
    
         
            -
                  new_entry = { 'title' => title, 'date' => start_time, 'section' => section }
         
     | 
| 
       678 
     | 
    
         
            -
                  new_entry['note'] = entry['notes'].split(/\n/).map(&:chomp) if entry.key?('notes')
         
     | 
| 
       679 
     | 
    
         
            -
                  new_items.push(new_entry)
         
     | 
| 
       680 
     | 
    
         
            -
                end
         
     | 
| 
       681 
     | 
    
         
            -
                total = new_items.count
         
     | 
| 
       682 
     | 
    
         
            -
                new_items = dedup(new_items, opt[:no_overlap])
         
     | 
| 
       683 
     | 
    
         
            -
                dups = total - new_items.count
         
     | 
| 
       684 
     | 
    
         
            -
                @results.push(%(Skipped #{dups} items with overlapping times)) if dups > 0
         
     | 
| 
       685 
     | 
    
         
            -
                @content[section]['items'].concat(new_items)
         
     | 
| 
       686 
     | 
    
         
            -
                @results.push(%(Imported #{new_items.count} items to #{section}))
         
     | 
| 
       687 
     | 
    
         
            -
              end
         
     | 
| 
       688 
     | 
    
         
            -
             
     | 
| 
       689 
     | 
    
         
            -
              ##
         
     | 
| 
       690 
     | 
    
         
            -
              ## @brief      Return the content of the last note for a given section
         
     | 
| 
       691 
     | 
    
         
            -
              ##
         
     | 
| 
       692 
     | 
    
         
            -
              ## @param      section  (String) The section to retrieve from, default
         
     | 
| 
       693 
     | 
    
         
            -
              ##                      All
         
     | 
| 
       694 
     | 
    
         
            -
              ##
         
     | 
| 
       695 
     | 
    
         
            -
              def last_note(section = 'All')
         
     | 
| 
       696 
     | 
    
         
            -
                section = guess_section(section)
         
     | 
| 
       697 
     | 
    
         
            -
                if section =~ /^all$/i
         
     | 
| 
       698 
     | 
    
         
            -
                  combined = { 'items' => [] }
         
     | 
| 
       699 
     | 
    
         
            -
                  @content.each do |_k, v|
         
     | 
| 
       700 
     | 
    
         
            -
                    combined['items'] += v['items']
         
     | 
| 
       701 
     | 
    
         
            -
                  end
         
     | 
| 
       702 
     | 
    
         
            -
                  section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
         
     | 
| 
       703 
     | 
    
         
            -
                end
         
     | 
| 
       704 
538 
     | 
    
         | 
| 
       705 
     | 
    
         
            -
             
     | 
| 
      
 539 
     | 
    
         
            +
                  # Remove @done tag
         
     | 
| 
      
 540 
     | 
    
         
            +
                  title = item.title.sub(/\s*@done(\(.*?\))?/, '').chomp
         
     | 
| 
      
 541 
     | 
    
         
            +
                  section = opt[:in].nil? ? item.section : guess_section(opt[:in])
         
     | 
| 
      
 542 
     | 
    
         
            +
                  @auto_tag = false
         
     | 
| 
       706 
543 
     | 
    
         | 
| 
       707 
     | 
    
         
            -
             
     | 
| 
       708 
     | 
    
         
            -
                warn "Editing note for #{last_item['title']}"
         
     | 
| 
       709 
     | 
    
         
            -
                note = ''
         
     | 
| 
       710 
     | 
    
         
            -
                note = last_item['note'].map(&:strip).join("\n") unless last_item['note'].nil?
         
     | 
| 
       711 
     | 
    
         
            -
                "#{last_item['title']}\n# EDIT BELOW THIS LINE ------------\n#{note}"
         
     | 
| 
       712 
     | 
    
         
            -
              end
         
     | 
| 
      
 544 
     | 
    
         
            +
                  note = opt[:note] || Note.new
         
     | 
| 
       713 
545 
     | 
    
         | 
| 
       714 
     | 
    
         
            -
             
     | 
| 
       715 
     | 
    
         
            -
             
     | 
| 
       716 
     | 
    
         
            -
             
     | 
| 
       717 
     | 
    
         
            -
             
     | 
| 
       718 
     | 
    
         
            -
             
     | 
| 
       719 
     | 
    
         
            -
              def restart_last(opt = {})
         
     | 
| 
       720 
     | 
    
         
            -
                opt[:section] ||= 'all'
         
     | 
| 
       721 
     | 
    
         
            -
                opt[:note] ||= []
         
     | 
| 
       722 
     | 
    
         
            -
                opt[:tag] ||= []
         
     | 
| 
       723 
     | 
    
         
            -
                opt[:tag_bool] ||= :and
         
     | 
| 
       724 
     | 
    
         
            -
             
     | 
| 
       725 
     | 
    
         
            -
                last = last_entry(opt)
         
     | 
| 
       726 
     | 
    
         
            -
                if last.nil?
         
     | 
| 
       727 
     | 
    
         
            -
                  @results.push(%(No previous entry found))
         
     | 
| 
       728 
     | 
    
         
            -
                  return
         
     | 
| 
       729 
     | 
    
         
            -
                end
         
     | 
| 
       730 
     | 
    
         
            -
                unless last.has_tags?(['done'], 'ALL')
         
     | 
| 
       731 
     | 
    
         
            -
                  new_item = last.dup
         
     | 
| 
       732 
     | 
    
         
            -
                  new_item['title'] += " @done(#{Time.now.strftime('%F %R')})"
         
     | 
| 
       733 
     | 
    
         
            -
                  update_item(last, new_item)
         
     | 
| 
       734 
     | 
    
         
            -
                end
         
     | 
| 
       735 
     | 
    
         
            -
                # Remove @done tag
         
     | 
| 
       736 
     | 
    
         
            -
                title = last['title'].sub(/\s*@done(\(.*?\))?/, '').chomp
         
     | 
| 
       737 
     | 
    
         
            -
                section = opt[:in].nil? ? last['section'] : guess_section(opt[:in])
         
     | 
| 
       738 
     | 
    
         
            -
                @auto_tag = false
         
     | 
| 
       739 
     | 
    
         
            -
                add_item(title, section, { note: opt[:note], back: opt[:date], timed: true })
         
     | 
| 
       740 
     | 
    
         
            -
                write(@doing_file)
         
     | 
| 
       741 
     | 
    
         
            -
              end
         
     | 
| 
      
 546 
     | 
    
         
            +
                  if opt[:editor]
         
     | 
| 
      
 547 
     | 
    
         
            +
                    to_edit = title
         
     | 
| 
      
 548 
     | 
    
         
            +
                    to_edit += "\n#{note.to_s}" unless note.empty?
         
     | 
| 
      
 549 
     | 
    
         
            +
                    new_item = fork_editor(to_edit)
         
     | 
| 
      
 550 
     | 
    
         
            +
                    title, note = format_input(new_item)
         
     | 
| 
       742 
551 
     | 
    
         | 
| 
       743 
     | 
    
         
            -
             
     | 
| 
       744 
     | 
    
         
            -
             
     | 
| 
       745 
     | 
    
         
            -
             
     | 
| 
       746 
     | 
    
         
            -
              ## @param      opt   (Hash) Additional Options
         
     | 
| 
       747 
     | 
    
         
            -
              ##
         
     | 
| 
       748 
     | 
    
         
            -
              def last_entry(opt = {})
         
     | 
| 
       749 
     | 
    
         
            -
                opt[:tag_bool] ||= :and
         
     | 
| 
       750 
     | 
    
         
            -
                opt[:section] ||= @current_section
         
     | 
| 
       751 
     | 
    
         
            -
             
     | 
| 
       752 
     | 
    
         
            -
                sec_arr = []
         
     | 
| 
       753 
     | 
    
         
            -
             
     | 
| 
       754 
     | 
    
         
            -
                if opt[:section].nil?
         
     | 
| 
       755 
     | 
    
         
            -
                  sec_arr = [@current_section]
         
     | 
| 
       756 
     | 
    
         
            -
                elsif opt[:section].instance_of?(String)
         
     | 
| 
       757 
     | 
    
         
            -
                  if opt[:section] =~ /^all$/i
         
     | 
| 
       758 
     | 
    
         
            -
                    combined = { 'items' => [] }
         
     | 
| 
       759 
     | 
    
         
            -
                    @content.each do |_k, v|
         
     | 
| 
       760 
     | 
    
         
            -
                      combined['items'] += v['items']
         
     | 
| 
      
 552 
     | 
    
         
            +
                    if title.nil? || title.empty?
         
     | 
| 
      
 553 
     | 
    
         
            +
                      logger.debug('Skipped:', 'No content provided')
         
     | 
| 
      
 554 
     | 
    
         
            +
                      return
         
     | 
| 
       761 
555 
     | 
    
         
             
                    end
         
     | 
| 
       762 
     | 
    
         
            -
                    items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
         
     | 
| 
       763 
     | 
    
         
            -
                    sec_arr.push(items[0]['section'])
         
     | 
| 
       764 
     | 
    
         
            -
                  else
         
     | 
| 
       765 
     | 
    
         
            -
                    sec_arr = [guess_section(opt[:section])]
         
     | 
| 
       766 
556 
     | 
    
         
             
                  end
         
     | 
| 
       767 
     | 
    
         
            -
                end
         
     | 
| 
       768 
     | 
    
         
            -
             
     | 
| 
       769 
     | 
    
         
            -
                all_items = []
         
     | 
| 
       770 
     | 
    
         
            -
                sec_arr.each do |section|
         
     | 
| 
       771 
     | 
    
         
            -
                  all_items.concat(@content[section]['items'].dup) if @content.key?(section)
         
     | 
| 
       772 
     | 
    
         
            -
                end
         
     | 
| 
       773 
557 
     | 
    
         | 
| 
       774 
     | 
    
         
            -
             
     | 
| 
       775 
     | 
    
         
            -
                   
     | 
| 
       776 
     | 
    
         
            -
             
     | 
| 
       777 
     | 
    
         
            -
                  all_items.select! { |item| item.matches_search?(opt[:search]) }
         
     | 
| 
      
 558 
     | 
    
         
            +
                  update_item(original, item)
         
     | 
| 
      
 559 
     | 
    
         
            +
                  add_item(title, section, { note: note, back: opt[:date], timed: true })
         
     | 
| 
      
 560 
     | 
    
         
            +
                  write(@doing_file)
         
     | 
| 
       778 
561 
     | 
    
         
             
                end
         
     | 
| 
       779 
562 
     | 
    
         | 
| 
       780 
     | 
    
         
            -
                 
     | 
| 
       781 
     | 
    
         
            -
             
     | 
| 
      
 563 
     | 
    
         
            +
                ##
         
     | 
| 
      
 564 
     | 
    
         
            +
                ## @brief      Restart the last entry
         
     | 
| 
      
 565 
     | 
    
         
            +
                ##
         
     | 
| 
      
 566 
     | 
    
         
            +
                ## @param      opt   (Hash) Additional Options
         
     | 
| 
      
 567 
     | 
    
         
            +
                ##
         
     | 
| 
      
 568 
     | 
    
         
            +
                def repeat_last(opt = {})
         
     | 
| 
      
 569 
     | 
    
         
            +
                  opt[:section] ||= 'all'
         
     | 
| 
      
 570 
     | 
    
         
            +
                  opt[:note] ||= []
         
     | 
| 
      
 571 
     | 
    
         
            +
                  opt[:tag] ||= []
         
     | 
| 
      
 572 
     | 
    
         
            +
                  opt[:tag_bool] ||= :and
         
     | 
| 
      
 573 
     | 
    
         
            +
             
     | 
| 
      
 574 
     | 
    
         
            +
                  last = last_entry(opt)
         
     | 
| 
      
 575 
     | 
    
         
            +
                  if last.nil?
         
     | 
| 
      
 576 
     | 
    
         
            +
                    logger.debug('Skipped:', 'No previous entry found')
         
     | 
| 
      
 577 
     | 
    
         
            +
                    return
         
     | 
| 
      
 578 
     | 
    
         
            +
                  end
         
     | 
| 
       782 
579 
     | 
    
         | 
| 
       783 
     | 
    
         
            -
             
     | 
| 
       784 
     | 
    
         
            -
             
     | 
| 
       785 
     | 
    
         
            -
              ##
         
     | 
| 
       786 
     | 
    
         
            -
              ## @return     (String) The selected option
         
     | 
| 
       787 
     | 
    
         
            -
              ##
         
     | 
| 
       788 
     | 
    
         
            -
              def choose_from(options, prompt: 'Make a selection: ', multiple: false, fzf_args: [])
         
     | 
| 
       789 
     | 
    
         
            -
                fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
         
     | 
| 
       790 
     | 
    
         
            -
                fzf_args << '-1'
         
     | 
| 
       791 
     | 
    
         
            -
                fzf_args << %(--prompt "#{prompt}")
         
     | 
| 
       792 
     | 
    
         
            -
                fzf_args << '--multi' if multiple
         
     | 
| 
       793 
     | 
    
         
            -
                header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
         
     | 
| 
       794 
     | 
    
         
            -
                fzf_args << %(--header "#{header}")
         
     | 
| 
       795 
     | 
    
         
            -
                res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
         
     | 
| 
       796 
     | 
    
         
            -
                return false if res.strip.size.zero?
         
     | 
| 
       797 
     | 
    
         
            -
             
     | 
| 
       798 
     | 
    
         
            -
                res
         
     | 
| 
       799 
     | 
    
         
            -
              end
         
     | 
| 
      
 580 
     | 
    
         
            +
                  repeat_item(last, opt)
         
     | 
| 
      
 581 
     | 
    
         
            +
                end
         
     | 
| 
       800 
582 
     | 
    
         | 
| 
       801 
     | 
    
         
            -
             
     | 
| 
       802 
     | 
    
         
            -
             
     | 
| 
       803 
     | 
    
         
            -
             
     | 
| 
       804 
     | 
    
         
            -
             
     | 
| 
       805 
     | 
    
         
            -
             
     | 
| 
       806 
     | 
    
         
            -
             
     | 
| 
       807 
     | 
    
         
            -
             
     | 
| 
      
 583 
     | 
    
         
            +
                ##
         
     | 
| 
      
 584 
     | 
    
         
            +
                ## @brief      Get the last entry
         
     | 
| 
      
 585 
     | 
    
         
            +
                ##
         
     | 
| 
      
 586 
     | 
    
         
            +
                ## @param      opt   (Hash) Additional Options
         
     | 
| 
      
 587 
     | 
    
         
            +
                ##
         
     | 
| 
      
 588 
     | 
    
         
            +
                def last_entry(opt = {})
         
     | 
| 
      
 589 
     | 
    
         
            +
                  opt[:tag_bool] ||= :and
         
     | 
| 
      
 590 
     | 
    
         
            +
                  opt[:section] ||= @config['current_section']
         
     | 
| 
       808 
591 
     | 
    
         | 
| 
       809 
     | 
    
         
            -
             
     | 
| 
      
 592 
     | 
    
         
            +
                  items = filter_items([], opt: opt)
         
     | 
| 
       810 
593 
     | 
    
         | 
| 
      
 594 
     | 
    
         
            +
                  logger.debug('Filtered:', "Parameters matched #{items.count} entries")
         
     | 
| 
       811 
595 
     | 
    
         | 
| 
       812 
     | 
    
         
            -
             
     | 
| 
       813 
     | 
    
         
            -
             
     | 
| 
       814 
     | 
    
         
            -
             
     | 
| 
       815 
     | 
    
         
            -
             
     | 
| 
      
 596 
     | 
    
         
            +
                  if opt[:interactive]
         
     | 
| 
      
 597 
     | 
    
         
            +
                    last_entry = choose_from_items(items, {
         
     | 
| 
      
 598 
     | 
    
         
            +
                      menu: true,
         
     | 
| 
      
 599 
     | 
    
         
            +
                      header: '',
         
     | 
| 
      
 600 
     | 
    
         
            +
                      prompt: 'Select an entry > ',
         
     | 
| 
      
 601 
     | 
    
         
            +
                      multiple: false,
         
     | 
| 
      
 602 
     | 
    
         
            +
                      sort: false,
         
     | 
| 
      
 603 
     | 
    
         
            +
                      show_if_single: true
         
     | 
| 
      
 604 
     | 
    
         
            +
                    }, include_section: opt[:section] =~ /^all$/i )
         
     | 
| 
      
 605 
     | 
    
         
            +
                  else
         
     | 
| 
      
 606 
     | 
    
         
            +
                    last_entry = items.max_by { |item| item.date }
         
     | 
| 
       816 
607 
     | 
    
         
             
                  end
         
     | 
| 
       817 
     | 
    
         
            -
             
     | 
| 
       818 
     | 
    
         
            -
             
     | 
| 
       819 
     | 
    
         
            -
                  items = @content[section]['items']
         
     | 
| 
      
 608 
     | 
    
         
            +
             
     | 
| 
      
 609 
     | 
    
         
            +
                  last_entry
         
     | 
| 
       820 
610 
     | 
    
         
             
                end
         
     | 
| 
       821 
611 
     | 
    
         | 
| 
      
 612 
     | 
    
         
            +
                ##
         
     | 
| 
      
 613 
     | 
    
         
            +
                ## @brief      Generate a menu of options and allow user selection
         
     | 
| 
      
 614 
     | 
    
         
            +
                ##
         
     | 
| 
      
 615 
     | 
    
         
            +
                ## @return     (String) The selected option
         
     | 
| 
      
 616 
     | 
    
         
            +
                ##
         
     | 
| 
      
 617 
     | 
    
         
            +
                def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
         
     | 
| 
      
 618 
     | 
    
         
            +
                  return nil unless $stdout.isatty
         
     | 
| 
       822 
619 
     | 
    
         | 
| 
       823 
     | 
    
         
            -
             
     | 
| 
       824 
     | 
    
         
            -
                   
     | 
| 
       825 
     | 
    
         
            -
             
     | 
| 
       826 
     | 
    
         
            -
             
     | 
| 
       827 
     | 
    
         
            -
             
     | 
| 
       828 
     | 
    
         
            -
             
     | 
| 
       829 
     | 
    
         
            -
             
     | 
| 
       830 
     | 
    
         
            -
                   
     | 
| 
       831 
     | 
    
         
            -
                   
     | 
| 
       832 
     | 
    
         
            -
                    out.concat([
         
     | 
| 
       833 
     | 
    
         
            -
                      ' (',
         
     | 
| 
       834 
     | 
    
         
            -
                      item['section'],
         
     | 
| 
       835 
     | 
    
         
            -
                      ') '
         
     | 
| 
       836 
     | 
    
         
            -
                    ])
         
     | 
| 
       837 
     | 
    
         
            -
                  end
         
     | 
| 
       838 
     | 
    
         
            -
                  out.join('')
         
     | 
| 
       839 
     | 
    
         
            -
                end
         
     | 
| 
       840 
     | 
    
         
            -
                fzf_args = [
         
     | 
| 
       841 
     | 
    
         
            -
                  %(--header="Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit"),
         
     | 
| 
       842 
     | 
    
         
            -
                  %(--prompt="Select entries to act on > "),
         
     | 
| 
       843 
     | 
    
         
            -
                  '-1',
         
     | 
| 
       844 
     | 
    
         
            -
                  '-m',
         
     | 
| 
       845 
     | 
    
         
            -
                  '--bind ctrl-a:select-all',
         
     | 
| 
       846 
     | 
    
         
            -
                  %(-q "#{opt[:query]}")
         
     | 
| 
       847 
     | 
    
         
            -
                ]
         
     | 
| 
       848 
     | 
    
         
            -
                if !opt[:menu]
         
     | 
| 
       849 
     | 
    
         
            -
                  exit_now! "Can't skip menu when no query is provided" unless opt[:query]
         
     | 
| 
       850 
     | 
    
         
            -
             
     | 
| 
       851 
     | 
    
         
            -
                  fzf_args.concat([%(--filter="#{opt[:query]}"), '--no-sort'])
         
     | 
| 
       852 
     | 
    
         
            -
                end
         
     | 
| 
      
 620 
     | 
    
         
            +
                  fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
         
     | 
| 
      
 621 
     | 
    
         
            +
                  # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
         
     | 
| 
      
 622 
     | 
    
         
            +
                  fzf_args << %(--prompt "#{prompt}")
         
     | 
| 
      
 623 
     | 
    
         
            +
                  fzf_args << '--multi' if multiple
         
     | 
| 
      
 624 
     | 
    
         
            +
                  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
         
     | 
| 
      
 625 
     | 
    
         
            +
                  fzf_args << %(--header "#{header}")
         
     | 
| 
      
 626 
     | 
    
         
            +
                  options.sort! if sorted
         
     | 
| 
      
 627 
     | 
    
         
            +
                  res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
         
     | 
| 
      
 628 
     | 
    
         
            +
                  return false if res.strip.size.zero?
         
     | 
| 
       853 
629 
     | 
    
         | 
| 
       854 
     | 
    
         
            -
             
     | 
| 
       855 
     | 
    
         
            -
                selected = []
         
     | 
| 
       856 
     | 
    
         
            -
                res.split(/\n/).each do |item|
         
     | 
| 
       857 
     | 
    
         
            -
                  idx = item.match(/^(\d+)\)/)[1].to_i
         
     | 
| 
       858 
     | 
    
         
            -
                  selected.push(items[idx])
         
     | 
| 
      
 630 
     | 
    
         
            +
                  res
         
     | 
| 
       859 
631 
     | 
    
         
             
                end
         
     | 
| 
       860 
632 
     | 
    
         | 
| 
       861 
     | 
    
         
            -
                 
     | 
| 
       862 
     | 
    
         
            -
                   
     | 
| 
       863 
     | 
    
         
            -
                   
     | 
| 
      
 633 
     | 
    
         
            +
                def all_tags(items, opt: {})
         
     | 
| 
      
 634 
     | 
    
         
            +
                  all_tags = []
         
     | 
| 
      
 635 
     | 
    
         
            +
                  items.each { |item| all_tags.concat(item.tags).uniq! }
         
     | 
| 
      
 636 
     | 
    
         
            +
                  all_tags.sort
         
     | 
| 
       864 
637 
     | 
    
         
             
                end
         
     | 
| 
       865 
638 
     | 
    
         | 
| 
       866 
     | 
    
         
            -
                 
     | 
| 
       867 
     | 
    
         
            -
             
     | 
| 
       868 
     | 
    
         
            -
             
     | 
| 
       869 
     | 
    
         
            -
                   
     | 
| 
       870 
     | 
    
         
            -
             
     | 
| 
       871 
     | 
    
         
            -
                     
     | 
| 
      
 639 
     | 
    
         
            +
                def tag_groups(items, opt: {})
         
     | 
| 
      
 640 
     | 
    
         
            +
                  all_items = filter_items(items, opt: opt)
         
     | 
| 
      
 641 
     | 
    
         
            +
                  tags = all_tags(all_items, opt: {})
         
     | 
| 
      
 642 
     | 
    
         
            +
                  tag_groups = {}
         
     | 
| 
      
 643 
     | 
    
         
            +
                  tags.each do |tag|
         
     | 
| 
      
 644 
     | 
    
         
            +
                    tag_groups[tag] ||= []
         
     | 
| 
      
 645 
     | 
    
         
            +
                    tag_groups[tag] = filter_items(all_items, opt: { tag: tag, tag_bool: :or })
         
     | 
| 
       872 
646 
     | 
    
         
             
                  end
         
     | 
| 
       873 
     | 
    
         
            -
                end
         
     | 
| 
       874 
647 
     | 
    
         | 
| 
       875 
     | 
    
         
            -
             
     | 
| 
       876 
     | 
    
         
            -
             
     | 
| 
       877 
     | 
    
         
            -
             
     | 
| 
       878 
     | 
    
         
            -
             
     | 
| 
       879 
     | 
    
         
            -
             
     | 
| 
       880 
     | 
    
         
            -
             
     | 
| 
       881 
     | 
    
         
            -
             
     | 
| 
       882 
     | 
    
         
            -
             
     | 
| 
       883 
     | 
    
         
            -
             
     | 
| 
       884 
     | 
    
         
            -
             
     | 
| 
       885 
     | 
    
         
            -
             
     | 
| 
       886 
     | 
    
         
            -
             
     | 
| 
       887 
     | 
    
         
            -
             
     | 
| 
       888 
     | 
    
         
            -
             
     | 
| 
       889 
     | 
    
         
            -
             
     | 
| 
       890 
     | 
    
         
            -
             
     | 
| 
       891 
     | 
    
         
            -
             
     | 
| 
       892 
     | 
    
         
            -
             
     | 
| 
       893 
     | 
    
         
            -
             
     | 
| 
       894 
     | 
    
         
            -
             
     | 
| 
       895 
     | 
    
         
            -
             
     | 
| 
       896 
     | 
    
         
            -
             
     | 
| 
       897 
     | 
    
         
            -
             
     | 
| 
       898 
     | 
    
         
            -
             
     | 
| 
       899 
     | 
    
         
            -
             
     | 
| 
       900 
     | 
    
         
            -
             
     | 
| 
       901 
     | 
    
         
            -
             
     | 
| 
       902 
     | 
    
         
            -
             
     | 
| 
       903 
     | 
    
         
            -
             
     | 
| 
       904 
     | 
    
         
            -
             
     | 
| 
       905 
     | 
    
         
            -
             
     | 
| 
       906 
     | 
    
         
            -
             
     | 
| 
       907 
     | 
    
         
            -
             
     | 
| 
       908 
     | 
    
         
            -
             
     | 
| 
       909 
     | 
    
         
            -
                      opt[:output] = output_format.strip
         
     | 
| 
       910 
     | 
    
         
            -
                      res = opt[:force] ? false : yn('Save to file?', default_response: 'n')
         
     | 
| 
       911 
     | 
    
         
            -
                      if res
         
     | 
| 
       912 
     | 
    
         
            -
                        print "#{colors['yellow']}File path/name: #{colors['reset']}"
         
     | 
| 
       913 
     | 
    
         
            -
                        filename = STDIN.gets.strip
         
     | 
| 
       914 
     | 
    
         
            -
                        return if filename.empty?
         
     | 
| 
       915 
     | 
    
         
            -
                        opt[:save_to] = filename
         
     | 
| 
       916 
     | 
    
         
            -
                      end
         
     | 
| 
       917 
     | 
    
         
            -
                    when /archive/
         
     | 
| 
       918 
     | 
    
         
            -
                      opt[:archive] = true
         
     | 
| 
       919 
     | 
    
         
            -
                    when /delete/
         
     | 
| 
       920 
     | 
    
         
            -
                      opt[:delete] = true
         
     | 
| 
       921 
     | 
    
         
            -
                    when /edit/
         
     | 
| 
       922 
     | 
    
         
            -
                      opt[:editor] = true
         
     | 
| 
       923 
     | 
    
         
            -
                    when /finish/
         
     | 
| 
       924 
     | 
    
         
            -
                      opt[:finish] = true
         
     | 
| 
       925 
     | 
    
         
            -
                    when /cancel/
         
     | 
| 
       926 
     | 
    
         
            -
                      opt[:cancel] = true
         
     | 
| 
       927 
     | 
    
         
            -
                    when /move/
         
     | 
| 
       928 
     | 
    
         
            -
                      section = choose_section.strip
         
     | 
| 
       929 
     | 
    
         
            -
                      opt[:move] = section.strip unless section =~ /^ *$/
         
     | 
| 
       930 
     | 
    
         
            -
                    when /flag/
         
     | 
| 
       931 
     | 
    
         
            -
                      opt[:flag] = true
         
     | 
| 
       932 
     | 
    
         
            -
                    end
         
     | 
| 
      
 648 
     | 
    
         
            +
                  tag_groups
         
     | 
| 
      
 649 
     | 
    
         
            +
                end
         
     | 
| 
      
 650 
     | 
    
         
            +
             
     | 
| 
      
 651 
     | 
    
         
            +
                ##
         
     | 
| 
      
 652 
     | 
    
         
            +
                ## @brief      Filter items based on search criteria
         
     | 
| 
      
 653 
     | 
    
         
            +
                ##
         
     | 
| 
      
 654 
     | 
    
         
            +
                ## @param      items  (Array) The items to filter (if empty, filters all items)
         
     | 
| 
      
 655 
     | 
    
         
            +
                ## @param      opt    (Hash) The filter parameters
         
     | 
| 
      
 656 
     | 
    
         
            +
                ##
         
     | 
| 
      
 657 
     | 
    
         
            +
                ## Available filter options in opt object
         
     | 
| 
      
 658 
     | 
    
         
            +
                ##
         
     | 
| 
      
 659 
     | 
    
         
            +
                ## - +:section+ (String)
         
     | 
| 
      
 660 
     | 
    
         
            +
                ## - +:unfinished+ (Boolean)
         
     | 
| 
      
 661 
     | 
    
         
            +
                ## - +:tag+ (Array or comma-separated string)
         
     | 
| 
      
 662 
     | 
    
         
            +
                ## - +:tag_bool+ (:and, :or, :not)
         
     | 
| 
      
 663 
     | 
    
         
            +
                ## - +:search+ (string, optional regex with //)
         
     | 
| 
      
 664 
     | 
    
         
            +
                ## - +:date_filter+ (Array[(Time)start, (Time)end])
         
     | 
| 
      
 665 
     | 
    
         
            +
                ## - +:only_timed+ (Boolean)
         
     | 
| 
      
 666 
     | 
    
         
            +
                ## - +:before+ (Date/Time string, unparsed)
         
     | 
| 
      
 667 
     | 
    
         
            +
                ## - +:after+ (Date/Time string, unparsed)
         
     | 
| 
      
 668 
     | 
    
         
            +
                ## - +:today+ (Boolean)
         
     | 
| 
      
 669 
     | 
    
         
            +
                ## - +:yesterday+ (Boolean)
         
     | 
| 
      
 670 
     | 
    
         
            +
                ## - +:count+ (Number to return)
         
     | 
| 
      
 671 
     | 
    
         
            +
                ## - +:age+ (String, 'old' or 'new')
         
     | 
| 
      
 672 
     | 
    
         
            +
                ##
         
     | 
| 
      
 673 
     | 
    
         
            +
                def filter_items(items = [], opt: {})
         
     | 
| 
      
 674 
     | 
    
         
            +
                  if items.nil? || items.empty?
         
     | 
| 
      
 675 
     | 
    
         
            +
                    section = opt[:section] ? guess_section(opt[:section]) : 'All'
         
     | 
| 
      
 676 
     | 
    
         
            +
             
     | 
| 
      
 677 
     | 
    
         
            +
                    items = if section =~ /^all$/i
         
     | 
| 
      
 678 
     | 
    
         
            +
                              @content.each_with_object([]) { |(_k, v), arr| arr.concat(v[:items].dup) }
         
     | 
| 
      
 679 
     | 
    
         
            +
                            else
         
     | 
| 
      
 680 
     | 
    
         
            +
                              @content[section][:items].dup
         
     | 
| 
      
 681 
     | 
    
         
            +
                            end
         
     | 
| 
       933 
682 
     | 
    
         
             
                  end
         
     | 
| 
       934 
     | 
    
         
            -
                end
         
     | 
| 
       935 
683 
     | 
    
         | 
| 
       936 
     | 
    
         
            -
             
     | 
| 
       937 
     | 
    
         
            -
                   
     | 
| 
       938 
     | 
    
         
            -
             
     | 
| 
       939 
     | 
    
         
            -
                     
     | 
| 
       940 
     | 
    
         
            -
                     
     | 
| 
       941 
     | 
    
         
            -
                  end
         
     | 
| 
       942 
     | 
    
         
            -
                  return
         
     | 
| 
       943 
     | 
    
         
            -
                end
         
     | 
| 
      
 684 
     | 
    
         
            +
                  items.sort_by! { |item| [item.date, item.title.downcase] }.reverse
         
     | 
| 
      
 685 
     | 
    
         
            +
                  filtered_items = items.select do |item|
         
     | 
| 
      
 686 
     | 
    
         
            +
                    keep = true
         
     | 
| 
      
 687 
     | 
    
         
            +
                    finished = opt[:unfinished] && item.tags?('done', :and)
         
     | 
| 
      
 688 
     | 
    
         
            +
                    keep = false if finished
         
     | 
| 
       944 
689 
     | 
    
         | 
| 
       945 
     | 
    
         
            -
             
     | 
| 
       946 
     | 
    
         
            -
             
     | 
| 
       947 
     | 
    
         
            -
             
     | 
| 
       948 
     | 
    
         
            -
             
     | 
| 
       949 
     | 
    
         
            -
                      untag_item(item, tag)
         
     | 
| 
       950 
     | 
    
         
            -
                    else
         
     | 
| 
       951 
     | 
    
         
            -
                      tag_item(item, tag, date: false)
         
     | 
| 
      
 690 
     | 
    
         
            +
                    if keep && opt[:tag]
         
     | 
| 
      
 691 
     | 
    
         
            +
                      opt[:tag_bool] ||= :and
         
     | 
| 
      
 692 
     | 
    
         
            +
                      tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.tags?(opt[:tag], opt[:tag_bool])
         
     | 
| 
      
 693 
     | 
    
         
            +
                      keep = false unless tag_match
         
     | 
| 
       952 
694 
     | 
    
         
             
                    end
         
     | 
| 
       953 
     | 
    
         
            -
                  end
         
     | 
| 
       954 
     | 
    
         
            -
                end
         
     | 
| 
       955 
695 
     | 
    
         | 
| 
       956 
     | 
    
         
            -
             
     | 
| 
       957 
     | 
    
         
            -
             
     | 
| 
       958 
     | 
    
         
            -
             
     | 
| 
       959 
     | 
    
         
            -
                    if opt[:remove]
         
     | 
| 
       960 
     | 
    
         
            -
                      untag_item(item, tag)
         
     | 
| 
       961 
     | 
    
         
            -
                    else
         
     | 
| 
       962 
     | 
    
         
            -
                      tag_item(item, tag, date: !opt[:cancel])
         
     | 
| 
      
 696 
     | 
    
         
            +
                    if keep && opt[:search]
         
     | 
| 
      
 697 
     | 
    
         
            +
                      search_match = opt[:search].nil? || opt[:search].empty? ? true : item.search(opt[:search])
         
     | 
| 
      
 698 
     | 
    
         
            +
                      keep = false unless search_match
         
     | 
| 
       963 
699 
     | 
    
         
             
                    end
         
     | 
| 
       964 
     | 
    
         
            -
                  end
         
     | 
| 
       965 
     | 
    
         
            -
                end
         
     | 
| 
       966 
700 
     | 
    
         | 
| 
       967 
     | 
    
         
            -
             
     | 
| 
       968 
     | 
    
         
            -
             
     | 
| 
       969 
     | 
    
         
            -
             
     | 
| 
       970 
     | 
    
         
            -
             
     | 
| 
       971 
     | 
    
         
            -
                       
     | 
| 
       972 
     | 
    
         
            -
             
     | 
| 
       973 
     | 
    
         
            -
             
     | 
| 
      
 701 
     | 
    
         
            +
                    if keep && opt[:date_filter]&.length == 2
         
     | 
| 
      
 702 
     | 
    
         
            +
                      start_date = opt[:date_filter][0]
         
     | 
| 
      
 703 
     | 
    
         
            +
                      end_date = opt[:date_filter][1]
         
     | 
| 
      
 704 
     | 
    
         
            +
             
     | 
| 
      
 705 
     | 
    
         
            +
                      in_date_range = if end_date
         
     | 
| 
      
 706 
     | 
    
         
            +
                                        item.date >= start_date && item.date <= end_date
         
     | 
| 
      
 707 
     | 
    
         
            +
                                      else
         
     | 
| 
      
 708 
     | 
    
         
            +
                                        item.date.strftime('%F') == start_date.strftime('%F')
         
     | 
| 
      
 709 
     | 
    
         
            +
                                      end
         
     | 
| 
      
 710 
     | 
    
         
            +
                      keep = false unless in_date_range
         
     | 
| 
       974 
711 
     | 
    
         
             
                    end
         
     | 
| 
       975 
     | 
    
         
            -
                  end
         
     | 
| 
       976 
     | 
    
         
            -
                end
         
     | 
| 
       977 
712 
     | 
    
         | 
| 
       978 
     | 
    
         
            -
             
     | 
| 
       979 
     | 
    
         
            -
             
     | 
| 
       980 
     | 
    
         
            -
             
     | 
| 
       981 
     | 
    
         
            -
             
     | 
| 
      
 713 
     | 
    
         
            +
                    keep = false if keep && opt[:only_timed] && !item.interval
         
     | 
| 
      
 714 
     | 
    
         
            +
             
     | 
| 
      
 715 
     | 
    
         
            +
                    if keep && opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
         
     | 
| 
      
 716 
     | 
    
         
            +
                      keep = item.tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool'])
         
     | 
| 
      
 717 
     | 
    
         
            +
                    end
         
     | 
| 
      
 718 
     | 
    
         
            +
             
     | 
| 
      
 719 
     | 
    
         
            +
                    if keep && opt[:before]
         
     | 
| 
      
 720 
     | 
    
         
            +
                      time_string = opt[:before]
         
     | 
| 
      
 721 
     | 
    
         
            +
                      cutoff = chronify(time_string, guess: :begin)
         
     | 
| 
      
 722 
     | 
    
         
            +
                      keep = cutoff && item.date <= cutoff
         
     | 
| 
      
 723 
     | 
    
         
            +
                    end
         
     | 
| 
       982 
724 
     | 
    
         | 
| 
       983 
     | 
    
         
            -
             
     | 
| 
      
 725 
     | 
    
         
            +
                    if keep && opt[:after]
         
     | 
| 
      
 726 
     | 
    
         
            +
                      time_string = opt[:after]
         
     | 
| 
      
 727 
     | 
    
         
            +
                      cutoff = chronify(time_string, guess: :end)
         
     | 
| 
      
 728 
     | 
    
         
            +
                      keep = cutoff && item.date >= cutoff
         
     | 
| 
      
 729 
     | 
    
         
            +
                    end
         
     | 
| 
       984 
730 
     | 
    
         | 
| 
       985 
     | 
    
         
            -
             
     | 
| 
      
 731 
     | 
    
         
            +
                    if keep && opt[:today]
         
     | 
| 
      
 732 
     | 
    
         
            +
                      keep = item.date >= Date.today.to_time && item.date < Date.today.next_day.to_time
         
     | 
| 
      
 733 
     | 
    
         
            +
                    elsif keep && opt[:yesterday]
         
     | 
| 
      
 734 
     | 
    
         
            +
                      keep = item.date >= Date.today.prev_day.to_time && item.date < Date.today.to_time
         
     | 
| 
      
 735 
     | 
    
         
            +
                    end
         
     | 
| 
       986 
736 
     | 
    
         | 
| 
       987 
     | 
    
         
            -
             
     | 
| 
      
 737 
     | 
    
         
            +
                    keep
         
     | 
| 
      
 738 
     | 
    
         
            +
                  end
         
     | 
| 
      
 739 
     | 
    
         
            +
                  count = opt[:count] && opt[:count].positive? ? opt[:count] : filtered_items.length
         
     | 
| 
       988 
740 
     | 
    
         | 
| 
       989 
     | 
    
         
            -
                   
     | 
| 
       990 
     | 
    
         
            -
                     
     | 
| 
       991 
     | 
    
         
            -
             
     | 
| 
       992 
     | 
    
         
            -
                     
     | 
| 
       993 
     | 
    
         
            -
                    editable_items << editable
         
     | 
| 
      
 741 
     | 
    
         
            +
                  if opt[:age] =~ /^o/i
         
     | 
| 
      
 742 
     | 
    
         
            +
                    filtered_items.slice(0, count).reverse
         
     | 
| 
      
 743 
     | 
    
         
            +
                  else
         
     | 
| 
      
 744 
     | 
    
         
            +
                    filtered_items.reverse.slice(0, count)
         
     | 
| 
       994 
745 
     | 
    
         
             
                  end
         
     | 
| 
       995 
     | 
    
         
            -
                  divider = "\n-----------\n"
         
     | 
| 
       996 
     | 
    
         
            -
                  input = editable_items.map(&:strip).join(divider) + "\n\n# You may delete entries, but leave all divider lines in place"
         
     | 
| 
       997 
746 
     | 
    
         | 
| 
       998 
     | 
    
         
            -
             
     | 
| 
      
 747 
     | 
    
         
            +
                end
         
     | 
| 
       999 
748 
     | 
    
         | 
| 
       1000 
     | 
    
         
            -
             
     | 
| 
      
 749 
     | 
    
         
            +
                ##
         
     | 
| 
      
 750 
     | 
    
         
            +
                ## @brief      Display an interactive menu of entries
         
     | 
| 
      
 751 
     | 
    
         
            +
                ##
         
     | 
| 
      
 752 
     | 
    
         
            +
                ## @param      opt   (Hash) Additional options
         
     | 
| 
      
 753 
     | 
    
         
            +
                ##
         
     | 
| 
      
 754 
     | 
    
         
            +
                def interactive(opt = {})
         
     | 
| 
      
 755 
     | 
    
         
            +
                  section = opt[:section] ? guess_section(opt[:section]) : 'All'
         
     | 
| 
      
 756 
     | 
    
         
            +
                  opt[:query] = opt[:search] if opt[:search] && !opt[:query]
         
     | 
| 
      
 757 
     | 
    
         
            +
                  opt[:multiple] = true
         
     | 
| 
      
 758 
     | 
    
         
            +
                  items = filter_items([], opt: { section: section, search: opt[:search] })
         
     | 
| 
       1001 
759 
     | 
    
         | 
| 
       1002 
     | 
    
         
            -
             
     | 
| 
       1003 
     | 
    
         
            -
                    title = input_lines[0]&.strip
         
     | 
| 
      
 760 
     | 
    
         
            +
                  selection = choose_from_items(items, opt, include_section: section =~ /^all$/i)
         
     | 
| 
       1004 
761 
     | 
    
         | 
| 
       1005 
     | 
    
         
            -
             
     | 
| 
       1006 
     | 
    
         
            -
             
     | 
| 
       1007 
     | 
    
         
            -
                     
     | 
| 
       1008 
     | 
    
         
            -
             
     | 
| 
      
 762 
     | 
    
         
            +
                  if selection.empty?
         
     | 
| 
      
 763 
     | 
    
         
            +
                    logger.debug('Skipped:', 'No selection')
         
     | 
| 
      
 764 
     | 
    
         
            +
                    return
         
     | 
| 
      
 765 
     | 
    
         
            +
                  end
         
     | 
| 
      
 766 
     | 
    
         
            +
             
     | 
| 
      
 767 
     | 
    
         
            +
                  act_on(selection, opt)
         
     | 
| 
      
 768 
     | 
    
         
            +
                end
         
     | 
| 
      
 769 
     | 
    
         
            +
             
     | 
| 
      
 770 
     | 
    
         
            +
                def choose_from_items(items, opt = {}, include_section: false)
         
     | 
| 
      
 771 
     | 
    
         
            +
                  return nil unless $stdout.isatty
         
     | 
| 
       1009 
772 
     | 
    
         | 
| 
       1010 
     | 
    
         
            -
             
     | 
| 
       1011 
     | 
    
         
            -
                      note.delete_if { |line| line =~ /^\s*$/ || line =~ /^#/ }
         
     | 
| 
      
 773 
     | 
    
         
            +
                  return nil unless items.count.positive?
         
     | 
| 
       1012 
774 
     | 
    
         | 
| 
       1013 
     | 
    
         
            -
             
     | 
| 
       1014 
     | 
    
         
            -
             
     | 
| 
      
 775 
     | 
    
         
            +
                  opt[:header] ||= "Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit"
         
     | 
| 
      
 776 
     | 
    
         
            +
                  opt[:prompt] ||= "Select entries to act on > "
         
     | 
| 
       1015 
777 
     | 
    
         | 
| 
       1016 
     | 
    
         
            -
             
     | 
| 
       1017 
     | 
    
         
            -
             
     | 
| 
       1018 
     | 
    
         
            -
             
     | 
| 
       1019 
     | 
    
         
            -
                       
     | 
| 
       1020 
     | 
    
         
            -
                       
     | 
| 
      
 778 
     | 
    
         
            +
                  pad = items.length.to_s.length
         
     | 
| 
      
 779 
     | 
    
         
            +
                  options = items.map.with_index do |item, i|
         
     | 
| 
      
 780 
     | 
    
         
            +
                    out = [
         
     | 
| 
      
 781 
     | 
    
         
            +
                      format("%#{pad}d", i),
         
     | 
| 
      
 782 
     | 
    
         
            +
                      ') ',
         
     | 
| 
      
 783 
     | 
    
         
            +
                      format('%13s', item.date.relative_date),
         
     | 
| 
      
 784 
     | 
    
         
            +
                      ' | ',
         
     | 
| 
      
 785 
     | 
    
         
            +
                      item.title
         
     | 
| 
      
 786 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 787 
     | 
    
         
            +
                    if include_section
         
     | 
| 
      
 788 
     | 
    
         
            +
                      out.concat([
         
     | 
| 
      
 789 
     | 
    
         
            +
                        ' (',
         
     | 
| 
      
 790 
     | 
    
         
            +
                        item.section,
         
     | 
| 
      
 791 
     | 
    
         
            +
                        ') '
         
     | 
| 
      
 792 
     | 
    
         
            +
                      ])
         
     | 
| 
       1021 
793 
     | 
    
         
             
                    end
         
     | 
| 
      
 794 
     | 
    
         
            +
                    out.join('')
         
     | 
| 
       1022 
795 
     | 
    
         
             
                  end
         
     | 
| 
       1023 
796 
     | 
    
         | 
| 
       1024 
     | 
    
         
            -
                   
     | 
| 
       1025 
     | 
    
         
            -
                end
         
     | 
| 
      
 797 
     | 
    
         
            +
                  fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
         
     | 
| 
       1026 
798 
     | 
    
         | 
| 
       1027 
     | 
    
         
            -
             
     | 
| 
       1028 
     | 
    
         
            -
             
     | 
| 
       1029 
     | 
    
         
            -
                     
     | 
| 
       1030 
     | 
    
         
            -
                     
     | 
| 
       1031 
     | 
    
         
            -
             
     | 
| 
      
 799 
     | 
    
         
            +
                  fzf_args = [
         
     | 
| 
      
 800 
     | 
    
         
            +
                    %(--header="#{opt[:header]}"),
         
     | 
| 
      
 801 
     | 
    
         
            +
                    %(--prompt="#{opt[:prompt].sub(/ *$/, ' ')}"),
         
     | 
| 
      
 802 
     | 
    
         
            +
                    opt[:multiple] ? '--multi' : '--no-multi',
         
     | 
| 
      
 803 
     | 
    
         
            +
                    '-0',
         
     | 
| 
      
 804 
     | 
    
         
            +
                    '--bind ctrl-a:select-all',
         
     | 
| 
      
 805 
     | 
    
         
            +
                    %(-q "#{opt[:query]}")
         
     | 
| 
      
 806 
     | 
    
         
            +
                  ]
         
     | 
| 
      
 807 
     | 
    
         
            +
                  fzf_args.push('-1') unless opt[:show_if_single]
         
     | 
| 
       1032 
808 
     | 
    
         | 
| 
       1033 
     | 
    
         
            -
                   
     | 
| 
       1034 
     | 
    
         
            -
             
     | 
| 
      
 809 
     | 
    
         
            +
                  unless opt[:menu]
         
     | 
| 
      
 810 
     | 
    
         
            +
                    raise Errors::InvalidArgument, "Can't skip menu when no query is provided" unless opt[:query]
         
     | 
| 
       1035 
811 
     | 
    
         | 
| 
       1036 
     | 
    
         
            -
             
     | 
| 
       1037 
     | 
    
         
            -
                  when /doing/
         
     | 
| 
       1038 
     | 
    
         
            -
                    options[:template] = '- %date | %title%note'
         
     | 
| 
       1039 
     | 
    
         
            -
                  else
         
     | 
| 
       1040 
     | 
    
         
            -
                    options[:output] = opt[:output]
         
     | 
| 
      
 812 
     | 
    
         
            +
                    fzf_args.concat([%(--filter="#{opt[:query]}"), opt[:sort] ? '' : '--no-sort'])
         
     | 
| 
       1041 
813 
     | 
    
         
             
                  end
         
     | 
| 
       1042 
814 
     | 
    
         | 
| 
       1043 
     | 
    
         
            -
                   
     | 
| 
      
 815 
     | 
    
         
            +
                  res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
         
     | 
| 
      
 816 
     | 
    
         
            +
                  selected = []
         
     | 
| 
      
 817 
     | 
    
         
            +
                  res.split(/\n/).each do |item|
         
     | 
| 
      
 818 
     | 
    
         
            +
                    idx = item.match(/^ *(\d+)\)/)[1].to_i
         
     | 
| 
      
 819 
     | 
    
         
            +
                    selected.push(items[idx])
         
     | 
| 
      
 820 
     | 
    
         
            +
                  end
         
     | 
| 
       1044 
821 
     | 
    
         | 
| 
       1045 
     | 
    
         
            -
                   
     | 
| 
       1046 
     | 
    
         
            -
             
     | 
| 
       1047 
     | 
    
         
            -
                    if File.exist?(file)
         
     | 
| 
       1048 
     | 
    
         
            -
                      # Create a backup copy for the undo command
         
     | 
| 
       1049 
     | 
    
         
            -
                      FileUtils.cp(file, "#{file}~")
         
     | 
| 
       1050 
     | 
    
         
            -
                    end
         
     | 
| 
      
 822 
     | 
    
         
            +
                  opt[:multiple] ? selected : selected[0]
         
     | 
| 
      
 823 
     | 
    
         
            +
                end
         
     | 
| 
       1051 
824 
     | 
    
         | 
| 
       1052 
     | 
    
         
            -
             
     | 
| 
       1053 
     | 
    
         
            -
             
     | 
| 
      
 825 
     | 
    
         
            +
                def act_on(items, opt = {})
         
     | 
| 
      
 826 
     | 
    
         
            +
                  actions = %i[editor delete tag flag finish cancel archive output save_to]
         
     | 
| 
      
 827 
     | 
    
         
            +
                  has_action = false
         
     | 
| 
      
 828 
     | 
    
         
            +
                  actions.each do |a|
         
     | 
| 
      
 829 
     | 
    
         
            +
                    if opt[a]
         
     | 
| 
      
 830 
     | 
    
         
            +
                      has_action = true
         
     | 
| 
      
 831 
     | 
    
         
            +
                      break
         
     | 
| 
       1054 
832 
     | 
    
         
             
                    end
         
     | 
| 
       1055 
     | 
    
         
            -
             
     | 
| 
       1056 
     | 
    
         
            -
                    @results.push("Export saved to #{file}")
         
     | 
| 
       1057 
     | 
    
         
            -
                  else
         
     | 
| 
       1058 
     | 
    
         
            -
                    puts output
         
     | 
| 
       1059 
833 
     | 
    
         
             
                  end
         
     | 
| 
       1060 
     | 
    
         
            -
                end
         
     | 
| 
       1061 
     | 
    
         
            -
              end
         
     | 
| 
       1062 
834 
     | 
    
         | 
| 
       1063 
     | 
    
         
            -
             
     | 
| 
       1064 
     | 
    
         
            -
             
     | 
| 
       1065 
     | 
    
         
            -
             
     | 
| 
       1066 
     | 
    
         
            -
             
     | 
| 
       1067 
     | 
    
         
            -
             
     | 
| 
       1068 
     | 
    
         
            -
             
     | 
| 
       1069 
     | 
    
         
            -
             
     | 
| 
       1070 
     | 
    
         
            -
             
     | 
| 
       1071 
     | 
    
         
            -
             
     | 
| 
       1072 
     | 
    
         
            -
             
     | 
| 
       1073 
     | 
    
         
            -
             
     | 
| 
       1074 
     | 
    
         
            -
             
     | 
| 
       1075 
     | 
    
         
            -
             
     | 
| 
       1076 
     | 
    
         
            -
             
     | 
| 
       1077 
     | 
    
         
            -
             
     | 
| 
       1078 
     | 
    
         
            -
             
     | 
| 
       1079 
     | 
    
         
            -
             
     | 
| 
       1080 
     | 
    
         
            -
             
     | 
| 
       1081 
     | 
    
         
            -
             
     | 
| 
       1082 
     | 
    
         
            -
             
     | 
| 
       1083 
     | 
    
         
            -
             
     | 
| 
       1084 
     | 
    
         
            -
             
     | 
| 
       1085 
     | 
    
         
            -
             
     | 
| 
       1086 
     | 
    
         
            -
             
     | 
| 
       1087 
     | 
    
         
            -
                     
     | 
| 
       1088 
     | 
    
         
            -
             
     | 
| 
       1089 
     | 
    
         
            -
             
     | 
| 
       1090 
     | 
    
         
            -
             
     | 
| 
       1091 
     | 
    
         
            -
             
     | 
| 
       1092 
     | 
    
         
            -
             
     | 
| 
       1093 
     | 
    
         
            -
                       
     | 
| 
       1094 
     | 
    
         
            -
                         
     | 
| 
       1095 
     | 
    
         
            -
             
     | 
| 
       1096 
     | 
    
         
            -
             
     | 
| 
       1097 
     | 
    
         
            -
             
     | 
| 
       1098 
     | 
    
         
            -
             
     | 
| 
       1099 
     | 
    
         
            -
             
     | 
| 
       1100 
     | 
    
         
            -
             
     | 
| 
       1101 
     | 
    
         
            -
             
     | 
| 
       1102 
     | 
    
         
            -
                         
     | 
| 
      
 835 
     | 
    
         
            +
                  unless has_action
         
     | 
| 
      
 836 
     | 
    
         
            +
                    actions = [
         
     | 
| 
      
 837 
     | 
    
         
            +
                                 'add tag',
         
     | 
| 
      
 838 
     | 
    
         
            +
                                 'remove tag',
         
     | 
| 
      
 839 
     | 
    
         
            +
                                 'cancel',
         
     | 
| 
      
 840 
     | 
    
         
            +
                                 'delete',
         
     | 
| 
      
 841 
     | 
    
         
            +
                                 'finish',
         
     | 
| 
      
 842 
     | 
    
         
            +
                                 'flag',
         
     | 
| 
      
 843 
     | 
    
         
            +
                                 'archive',
         
     | 
| 
      
 844 
     | 
    
         
            +
                                 'move',
         
     | 
| 
      
 845 
     | 
    
         
            +
                                 'edit',
         
     | 
| 
      
 846 
     | 
    
         
            +
                                 'output formatted'
         
     | 
| 
      
 847 
     | 
    
         
            +
                               ]
         
     | 
| 
      
 848 
     | 
    
         
            +
             
     | 
| 
      
 849 
     | 
    
         
            +
                    actions.concat(['resume/repeat', 'begin/reset']) if items.count == 1
         
     | 
| 
      
 850 
     | 
    
         
            +
             
     | 
| 
      
 851 
     | 
    
         
            +
                    choice = choose_from(actions,
         
     | 
| 
      
 852 
     | 
    
         
            +
                                         prompt: 'What do you want to do with the selected items? > ',
         
     | 
| 
      
 853 
     | 
    
         
            +
                                         multiple: true,
         
     | 
| 
      
 854 
     | 
    
         
            +
                                         sorted: false,
         
     | 
| 
      
 855 
     | 
    
         
            +
                                         fzf_args: ['--height=60%', '--tac', '--no-sort'])
         
     | 
| 
      
 856 
     | 
    
         
            +
                    return unless choice
         
     | 
| 
      
 857 
     | 
    
         
            +
             
     | 
| 
      
 858 
     | 
    
         
            +
                    to_do = choice.strip.split(/\n/)
         
     | 
| 
      
 859 
     | 
    
         
            +
                    to_do.each do |action|
         
     | 
| 
      
 860 
     | 
    
         
            +
                      case action
         
     | 
| 
      
 861 
     | 
    
         
            +
                      when /resume/
         
     | 
| 
      
 862 
     | 
    
         
            +
                        opt[:resume] = true
         
     | 
| 
      
 863 
     | 
    
         
            +
                      when /reset/
         
     | 
| 
      
 864 
     | 
    
         
            +
                        opt[:reset] = true
         
     | 
| 
      
 865 
     | 
    
         
            +
                      when /(add|remove) tag/
         
     | 
| 
      
 866 
     | 
    
         
            +
                        type = action =~ /^add/ ? 'add' : 'remove'
         
     | 
| 
      
 867 
     | 
    
         
            +
                        raise Errors::InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
         
     | 
| 
      
 868 
     | 
    
         
            +
             
     | 
| 
      
 869 
     | 
    
         
            +
                        print "#{Color.yellow}Tag to #{type}: #{Color.reset}"
         
     | 
| 
      
 870 
     | 
    
         
            +
                        tag = $stdin.gets
         
     | 
| 
      
 871 
     | 
    
         
            +
                        next if tag =~ /^ *$/
         
     | 
| 
      
 872 
     | 
    
         
            +
             
     | 
| 
      
 873 
     | 
    
         
            +
                        opt[:tag] = tag.strip.sub(/^@/, '')
         
     | 
| 
      
 874 
     | 
    
         
            +
                        opt[:remove] = true if type == 'remove'
         
     | 
| 
      
 875 
     | 
    
         
            +
                      when /output formatted/
         
     | 
| 
      
 876 
     | 
    
         
            +
                        output_format = choose_from(Plugins.available_plugins(type: :export).sort,
         
     | 
| 
      
 877 
     | 
    
         
            +
                                                    prompt: 'Which output format? > ',
         
     | 
| 
      
 878 
     | 
    
         
            +
                                                    fzf_args: ['--height=60%', '--tac', '--no-sort'])
         
     | 
| 
      
 879 
     | 
    
         
            +
                        next if tag =~ /^ *$/
         
     | 
| 
      
 880 
     | 
    
         
            +
             
     | 
| 
      
 881 
     | 
    
         
            +
                        unless output_format
         
     | 
| 
      
 882 
     | 
    
         
            +
                          raise Errors::UserCancelled, 'Cancelled'
         
     | 
| 
      
 883 
     | 
    
         
            +
                        end
         
     | 
| 
      
 884 
     | 
    
         
            +
             
     | 
| 
      
 885 
     | 
    
         
            +
                        opt[:output] = output_format.strip
         
     | 
| 
      
 886 
     | 
    
         
            +
                        res = opt[:force] ? false : yn('Save to file?', default_response: 'n')
         
     | 
| 
      
 887 
     | 
    
         
            +
                        if res
         
     | 
| 
      
 888 
     | 
    
         
            +
                          print "#{Color.yellow}File path/name: #{Color.reset}"
         
     | 
| 
      
 889 
     | 
    
         
            +
                          filename = $stdin.gets.strip
         
     | 
| 
      
 890 
     | 
    
         
            +
                          next if filename.empty?
         
     | 
| 
      
 891 
     | 
    
         
            +
             
     | 
| 
      
 892 
     | 
    
         
            +
                          opt[:save_to] = filename
         
     | 
| 
      
 893 
     | 
    
         
            +
                        end
         
     | 
| 
      
 894 
     | 
    
         
            +
                      when /archive/
         
     | 
| 
      
 895 
     | 
    
         
            +
                        opt[:archive] = true
         
     | 
| 
      
 896 
     | 
    
         
            +
                      when /delete/
         
     | 
| 
      
 897 
     | 
    
         
            +
                        opt[:delete] = true
         
     | 
| 
      
 898 
     | 
    
         
            +
                      when /edit/
         
     | 
| 
      
 899 
     | 
    
         
            +
                        opt[:editor] = true
         
     | 
| 
      
 900 
     | 
    
         
            +
                      when /finish/
         
     | 
| 
      
 901 
     | 
    
         
            +
                        opt[:finish] = true
         
     | 
| 
      
 902 
     | 
    
         
            +
                      when /cancel/
         
     | 
| 
      
 903 
     | 
    
         
            +
                        opt[:cancel] = true
         
     | 
| 
      
 904 
     | 
    
         
            +
                      when /move/
         
     | 
| 
      
 905 
     | 
    
         
            +
                        section = choose_section.strip
         
     | 
| 
      
 906 
     | 
    
         
            +
                        opt[:move] = section.strip unless section =~ /^ *$/
         
     | 
| 
      
 907 
     | 
    
         
            +
                      when /flag/
         
     | 
| 
      
 908 
     | 
    
         
            +
                        opt[:flag] = true
         
     | 
| 
       1103 
909 
     | 
    
         
             
                      end
         
     | 
| 
       1104 
     | 
    
         
            -
                    else
         
     | 
| 
       1105 
     | 
    
         
            -
                      sec_arr = sections
         
     | 
| 
       1106 
910 
     | 
    
         
             
                    end
         
     | 
| 
       1107 
     | 
    
         
            -
                  else
         
     | 
| 
       1108 
     | 
    
         
            -
                    sec_arr = [guess_section(opt[:section])]
         
     | 
| 
       1109 
911 
     | 
    
         
             
                  end
         
     | 
| 
       1110 
     | 
    
         
            -
                end
         
     | 
| 
       1111 
912 
     | 
    
         | 
| 
       1112 
     | 
    
         
            -
             
     | 
| 
       1113 
     | 
    
         
            -
             
     | 
| 
       1114 
     | 
    
         
            -
             
     | 
| 
       1115 
     | 
    
         
            -
                     
     | 
| 
       1116 
     | 
    
         
            -
             
     | 
| 
       1117 
     | 
    
         
            -
             
     | 
| 
       1118 
     | 
    
         
            -
             
     | 
| 
       1119 
     | 
    
         
            -
             
     | 
| 
       1120 
     | 
    
         
            -
             
     | 
| 
       1121 
     | 
    
         
            -
             
     | 
| 
       1122 
     | 
    
         
            -
                      tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.has_tags?(opt[:tag], opt[:tag_bool])
         
     | 
| 
       1123 
     | 
    
         
            -
                      search_match = opt[:search].nil? || opt[:search].empty? ? true : item.matches_search?(opt[:search])
         
     | 
| 
       1124 
     | 
    
         
            -
             
     | 
| 
       1125 
     | 
    
         
            -
                      if tag_match && search_match && !finished
         
     | 
| 
       1126 
     | 
    
         
            -
                        if opt[:autotag]
         
     | 
| 
       1127 
     | 
    
         
            -
                          new_title = autotag(item['title']) if @auto_tag
         
     | 
| 
       1128 
     | 
    
         
            -
                          if new_title == item['title']
         
     | 
| 
       1129 
     | 
    
         
            -
                            @results.push(%(Autotag: No changes))
         
     | 
| 
       1130 
     | 
    
         
            -
                          else
         
     | 
| 
       1131 
     | 
    
         
            -
                            @results.push("Tags updated: #{new_title}")
         
     | 
| 
       1132 
     | 
    
         
            -
                            item['title'] = new_title
         
     | 
| 
       1133 
     | 
    
         
            -
                          end
         
     | 
| 
      
 913 
     | 
    
         
            +
                  if opt[:resume] || opt[:reset]
         
     | 
| 
      
 914 
     | 
    
         
            +
                    if items.count > 1
         
     | 
| 
      
 915 
     | 
    
         
            +
                      logger.error('Error:', 'resume and restart can only be used on a single entry')
         
     | 
| 
      
 916 
     | 
    
         
            +
                    else
         
     | 
| 
      
 917 
     | 
    
         
            +
                      item = items[0]
         
     | 
| 
      
 918 
     | 
    
         
            +
                      if opt[:resume] && !opt[:reset]
         
     | 
| 
      
 919 
     | 
    
         
            +
                        repeat_item(item, { editor: opt[:editor] })
         
     | 
| 
      
 920 
     | 
    
         
            +
                      elsif opt[:reset]
         
     | 
| 
      
 921 
     | 
    
         
            +
                        if item.tags?('done', :and) && !opt[:resume]
         
     | 
| 
      
 922 
     | 
    
         
            +
                          res = opt[:force] ? true : yn('Remove @done tag?', default_response: 'y')
         
     | 
| 
       1134 
923 
     | 
    
         
             
                        else
         
     | 
| 
       1135 
     | 
    
         
            -
                           
     | 
| 
       1136 
     | 
    
         
            -
             
     | 
| 
      
 924 
     | 
    
         
            +
                          res = opt[:resume]
         
     | 
| 
      
 925 
     | 
    
         
            +
                        end
         
     | 
| 
      
 926 
     | 
    
         
            +
                        update_item(item, reset_item(item, resume: res))
         
     | 
| 
      
 927 
     | 
    
         
            +
                      end
         
     | 
| 
      
 928 
     | 
    
         
            +
                      write(@doing_file)
         
     | 
| 
      
 929 
     | 
    
         
            +
                    end
         
     | 
| 
      
 930 
     | 
    
         
            +
                    return
         
     | 
| 
      
 931 
     | 
    
         
            +
                  end
         
     | 
| 
       1137 
932 
     | 
    
         | 
| 
       1138 
     | 
    
         
            -
             
     | 
| 
       1139 
     | 
    
         
            -
             
     | 
| 
       1140 
     | 
    
         
            -
             
     | 
| 
       1141 
     | 
    
         
            -
             
     | 
| 
       1142 
     | 
    
         
            -
             
     | 
| 
       1143 
     | 
    
         
            -
             
     | 
| 
       1144 
     | 
    
         
            -
             
     | 
| 
       1145 
     | 
    
         
            -
             
     | 
| 
       1146 
     | 
    
         
            -
                              done_date = Time.now
         
     | 
| 
       1147 
     | 
    
         
            -
                            else
         
     | 
| 
       1148 
     | 
    
         
            -
                              done_date = item['date'] + opt[:took]
         
     | 
| 
       1149 
     | 
    
         
            -
                            end
         
     | 
| 
       1150 
     | 
    
         
            -
                          elsif opt[:back]
         
     | 
| 
       1151 
     | 
    
         
            -
                            if opt[:back].is_a? Integer
         
     | 
| 
       1152 
     | 
    
         
            -
                              done_date = item['date'] + opt[:back]
         
     | 
| 
       1153 
     | 
    
         
            -
                            else
         
     | 
| 
       1154 
     | 
    
         
            -
                              done_date = item['date'] + (opt[:back] - item['date'])
         
     | 
| 
       1155 
     | 
    
         
            -
                            end
         
     | 
| 
       1156 
     | 
    
         
            -
                          else
         
     | 
| 
       1157 
     | 
    
         
            -
                            done_date = Time.now
         
     | 
| 
       1158 
     | 
    
         
            -
                          end
         
     | 
| 
      
 933 
     | 
    
         
            +
                  if opt[:delete]
         
     | 
| 
      
 934 
     | 
    
         
            +
                    res = opt[:force] ? true : yn("Delete #{items.size} items?", default_response: 'y')
         
     | 
| 
      
 935 
     | 
    
         
            +
                    if res
         
     | 
| 
      
 936 
     | 
    
         
            +
                      items.each { |item| delete_item(item) }
         
     | 
| 
      
 937 
     | 
    
         
            +
                      write(@doing_file)
         
     | 
| 
      
 938 
     | 
    
         
            +
                    end
         
     | 
| 
      
 939 
     | 
    
         
            +
                    return
         
     | 
| 
      
 940 
     | 
    
         
            +
                  end
         
     | 
| 
       1159 
941 
     | 
    
         | 
| 
       1160 
     | 
    
         
            -
             
     | 
| 
       1161 
     | 
    
         
            -
             
     | 
| 
       1162 
     | 
    
         
            -
             
     | 
| 
       1163 
     | 
    
         
            -
             
     | 
| 
       1164 
     | 
    
         
            -
             
     | 
| 
       1165 
     | 
    
         
            -
             
     | 
| 
       1166 
     | 
    
         
            -
                              if opt[:rename]
         
     | 
| 
       1167 
     | 
    
         
            -
                                replacement = tag
         
     | 
| 
       1168 
     | 
    
         
            -
                                tag = opt[:rename]
         
     | 
| 
       1169 
     | 
    
         
            -
                              end
         
     | 
| 
      
 942 
     | 
    
         
            +
                  if opt[:flag]
         
     | 
| 
      
 943 
     | 
    
         
            +
                    tag = @config['marker_tag'] || 'flagged'
         
     | 
| 
      
 944 
     | 
    
         
            +
                    items.map! do |item|
         
     | 
| 
      
 945 
     | 
    
         
            +
                      tag_item(item, tag, date: false, remove: opt[:remove])
         
     | 
| 
      
 946 
     | 
    
         
            +
                    end
         
     | 
| 
      
 947 
     | 
    
         
            +
                  end
         
     | 
| 
       1170 
948 
     | 
    
         | 
| 
       1171 
     | 
    
         
            -
             
     | 
| 
       1172 
     | 
    
         
            -
             
     | 
| 
       1173 
     | 
    
         
            -
             
     | 
| 
       1174 
     | 
    
         
            -
             
     | 
| 
       1175 
     | 
    
         
            -
             
     | 
| 
      
 949 
     | 
    
         
            +
                  if opt[:finish] || opt[:cancel]
         
     | 
| 
      
 950 
     | 
    
         
            +
                    tag = 'done'
         
     | 
| 
      
 951 
     | 
    
         
            +
                    items.map! do |item|
         
     | 
| 
      
 952 
     | 
    
         
            +
                      if item.should_finish?
         
     | 
| 
      
 953 
     | 
    
         
            +
                        should_date = !opt[:cancel] && item.should_time?
         
     | 
| 
      
 954 
     | 
    
         
            +
                        tag_item(item, tag, date: should_date, remove: opt[:remove])
         
     | 
| 
      
 955 
     | 
    
         
            +
                      end
         
     | 
| 
      
 956 
     | 
    
         
            +
                    end
         
     | 
| 
      
 957 
     | 
    
         
            +
                  end
         
     | 
| 
       1176 
958 
     | 
    
         | 
| 
       1177 
     | 
    
         
            -
             
     | 
| 
       1178 
     | 
    
         
            -
             
     | 
| 
       1179 
     | 
    
         
            -
             
     | 
| 
       1180 
     | 
    
         
            -
             
     | 
| 
       1181 
     | 
    
         
            -
             
     | 
| 
       1182 
     | 
    
         
            -
             
     | 
| 
       1183 
     | 
    
         
            -
                                end
         
     | 
| 
      
 959 
     | 
    
         
            +
                  if opt[:tag]
         
     | 
| 
      
 960 
     | 
    
         
            +
                    tag = opt[:tag]
         
     | 
| 
      
 961 
     | 
    
         
            +
                    items.map! do |item|
         
     | 
| 
      
 962 
     | 
    
         
            +
                      tag_item(item, tag, date: false, remove: opt[:remove])
         
     | 
| 
      
 963 
     | 
    
         
            +
                    end
         
     | 
| 
      
 964 
     | 
    
         
            +
                  end
         
     | 
| 
       1184 
965 
     | 
    
         | 
| 
       1185 
     | 
    
         
            -
             
     | 
| 
      
 966 
     | 
    
         
            +
                  if opt[:archive] || opt[:move]
         
     | 
| 
      
 967 
     | 
    
         
            +
                    section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
         
     | 
| 
      
 968 
     | 
    
         
            +
                    items.map! {|item| move_item(item, section) }
         
     | 
| 
      
 969 
     | 
    
         
            +
                  end
         
     | 
| 
       1186 
970 
     | 
    
         | 
| 
       1187 
     | 
    
         
            -
             
     | 
| 
       1188 
     | 
    
         
            -
                              end
         
     | 
| 
       1189 
     | 
    
         
            -
                            elsif title !~ /@#{tag}/
         
     | 
| 
       1190 
     | 
    
         
            -
                              title.chomp!
         
     | 
| 
       1191 
     | 
    
         
            -
                              title += if opt[:date]
         
     | 
| 
       1192 
     | 
    
         
            -
                                         " @#{tag}(#{done_date.strftime('%F %R')})"
         
     | 
| 
       1193 
     | 
    
         
            -
                                       else
         
     | 
| 
       1194 
     | 
    
         
            -
                                         " @#{tag}"
         
     | 
| 
       1195 
     | 
    
         
            -
                                       end
         
     | 
| 
       1196 
     | 
    
         
            -
                              @results.push(%(Added @#{tag}: "#{title}" in #{section}))
         
     | 
| 
       1197 
     | 
    
         
            -
                            end
         
     | 
| 
       1198 
     | 
    
         
            -
                          end
         
     | 
| 
       1199 
     | 
    
         
            -
                          item['title'] = title
         
     | 
| 
       1200 
     | 
    
         
            -
                        end
         
     | 
| 
      
 971 
     | 
    
         
            +
                  write(@doing_file)
         
     | 
| 
       1201 
972 
     | 
    
         | 
| 
       1202 
     | 
    
         
            -
             
     | 
| 
       1203 
     | 
    
         
            -
                      end
         
     | 
| 
      
 973 
     | 
    
         
            +
                  if opt[:editor]
         
     | 
| 
       1204 
974 
     | 
    
         | 
| 
       1205 
     | 
    
         
            -
             
     | 
| 
       1206 
     | 
    
         
            -
                    end
         
     | 
| 
      
 975 
     | 
    
         
            +
                    editable_items = []
         
     | 
| 
       1207 
976 
     | 
    
         | 
| 
       1208 
     | 
    
         
            -
                     
     | 
| 
       1209 
     | 
    
         
            -
             
     | 
| 
       1210 
     | 
    
         
            -
             
     | 
| 
       1211 
     | 
    
         
            -
                       
     | 
| 
       1212 
     | 
    
         
            -
                       
     | 
| 
       1213 
     | 
    
         
            -
                        i['title'].sub!(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{i['section']})")
         
     | 
| 
       1214 
     | 
    
         
            -
                        i
         
     | 
| 
       1215 
     | 
    
         
            -
                      end.concat(@content['Archive']['items'])
         
     | 
| 
       1216 
     | 
    
         
            -
                      # slice [count] items off of [section] items
         
     | 
| 
       1217 
     | 
    
         
            -
                      @content[opt[:section]]['items'] = @content[opt[:section]]['items'][opt[:count]..-1]
         
     | 
| 
       1218 
     | 
    
         
            -
                      # overwrite archive section with concatenated array
         
     | 
| 
       1219 
     | 
    
         
            -
                      @content['Archive']['items'] = archived
         
     | 
| 
       1220 
     | 
    
         
            -
                      # log it
         
     | 
| 
       1221 
     | 
    
         
            -
                      result = opt[:count] == 1 ? '1 entry' : "#{opt[:count]} entries"
         
     | 
| 
       1222 
     | 
    
         
            -
                      @results.push("Archived #{result} from #{section}")
         
     | 
| 
       1223 
     | 
    
         
            -
                    elsif opt[:archive] && (opt[:count]).zero?
         
     | 
| 
       1224 
     | 
    
         
            -
                      @results.push('Archiving is skipped when operating on all entries') if (opt[:count]).zero?
         
     | 
| 
      
 977 
     | 
    
         
            +
                    items.each do |item|
         
     | 
| 
      
 978 
     | 
    
         
            +
                      editable = "#{item.date} | #{item.title}"
         
     | 
| 
      
 979 
     | 
    
         
            +
                      old_note = item.note ? item.note.to_s : nil
         
     | 
| 
      
 980 
     | 
    
         
            +
                      editable += "\n#{old_note}" unless old_note.nil?
         
     | 
| 
      
 981 
     | 
    
         
            +
                      editable_items << editable
         
     | 
| 
       1225 
982 
     | 
    
         
             
                    end
         
     | 
| 
       1226 
     | 
    
         
            -
             
     | 
| 
       1227 
     | 
    
         
            -
                     
     | 
| 
       1228 
     | 
    
         
            -
                  end
         
     | 
| 
       1229 
     | 
    
         
            -
                end
         
     | 
| 
      
 983 
     | 
    
         
            +
                    divider = "\n-----------\n"
         
     | 
| 
      
 984 
     | 
    
         
            +
                    input = editable_items.map(&:strip).join(divider) + "\n\n# You may delete entries, but leave all divider lines in place"
         
     | 
| 
       1230 
985 
     | 
    
         | 
| 
       1231 
     | 
    
         
            -
             
     | 
| 
       1232 
     | 
    
         
            -
              end
         
     | 
| 
      
 986 
     | 
    
         
            +
                    new_items = fork_editor(input).split(/#{divider}/)
         
     | 
| 
       1233 
987 
     | 
    
         | 
| 
       1234 
     | 
    
         
            -
             
     | 
| 
       1235 
     | 
    
         
            -
              ## @brief      Move item from current section to
         
     | 
| 
       1236 
     | 
    
         
            -
              ##             destination section
         
     | 
| 
       1237 
     | 
    
         
            -
              ##
         
     | 
| 
       1238 
     | 
    
         
            -
              ## @param      item     The item
         
     | 
| 
       1239 
     | 
    
         
            -
              ## @param      section  The destination section
         
     | 
| 
       1240 
     | 
    
         
            -
              ##
         
     | 
| 
       1241 
     | 
    
         
            -
              ## @return     Updated item
         
     | 
| 
       1242 
     | 
    
         
            -
              ##
         
     | 
| 
       1243 
     | 
    
         
            -
              def move_item(item, section)
         
     | 
| 
       1244 
     | 
    
         
            -
                old_section = item['section']
         
     | 
| 
       1245 
     | 
    
         
            -
                new_item = item.dup
         
     | 
| 
       1246 
     | 
    
         
            -
                new_item['section'] = section
         
     | 
| 
       1247 
     | 
    
         
            -
             
     | 
| 
       1248 
     | 
    
         
            -
                section_items = @content[old_section]['items']
         
     | 
| 
       1249 
     | 
    
         
            -
                section_items.delete(item)
         
     | 
| 
       1250 
     | 
    
         
            -
                @content[old_section]['items'] = section_items
         
     | 
| 
       1251 
     | 
    
         
            -
             
     | 
| 
       1252 
     | 
    
         
            -
                archive_items = @content[section]['items']
         
     | 
| 
       1253 
     | 
    
         
            -
                archive_items.push(new_item)
         
     | 
| 
       1254 
     | 
    
         
            -
                # archive_items = archive_items.sort_by { |item| item['date'] }
         
     | 
| 
       1255 
     | 
    
         
            -
                @content[section]['items'] = archive_items
         
     | 
| 
       1256 
     | 
    
         
            -
             
     | 
| 
       1257 
     | 
    
         
            -
                @results.push("Entry moved to #{section}: #{new_item['title']}")
         
     | 
| 
       1258 
     | 
    
         
            -
                return new_item
         
     | 
| 
       1259 
     | 
    
         
            -
              end
         
     | 
| 
      
 988 
     | 
    
         
            +
                    new_items.each_with_index do |new_item, i|
         
     | 
| 
       1260 
989 
     | 
    
         | 
| 
       1261 
     | 
    
         
            -
             
     | 
| 
       1262 
     | 
    
         
            -
             
     | 
| 
       1263 
     | 
    
         
            -
              ##
         
     | 
| 
       1264 
     | 
    
         
            -
              ## @param      old_item
         
     | 
| 
       1265 
     | 
    
         
            -
              ##
         
     | 
| 
       1266 
     | 
    
         
            -
              def next_item(old_item)
         
     | 
| 
       1267 
     | 
    
         
            -
                combined = { 'items' => [] }
         
     | 
| 
       1268 
     | 
    
         
            -
                @content.each do |_k, v|
         
     | 
| 
       1269 
     | 
    
         
            -
                  combined['items'] += v['items']
         
     | 
| 
       1270 
     | 
    
         
            -
                end
         
     | 
| 
       1271 
     | 
    
         
            -
                items = combined['items'].dup.sort_by { |item| item['date'] }.reverse
         
     | 
| 
       1272 
     | 
    
         
            -
                idx = items.index(old_item)
         
     | 
| 
      
 990 
     | 
    
         
            +
                      input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
         
     | 
| 
      
 991 
     | 
    
         
            +
                      title = input_lines[0]&.strip
         
     | 
| 
       1273 
992 
     | 
    
         | 
| 
       1274 
     | 
    
         
            -
             
     | 
| 
       1275 
     | 
    
         
            -
             
     | 
| 
       1276 
     | 
    
         
            -
             
     | 
| 
       1277 
     | 
    
         
            -
             
     | 
| 
       1278 
     | 
    
         
            -
                end
         
     | 
| 
       1279 
     | 
    
         
            -
              end
         
     | 
| 
      
 993 
     | 
    
         
            +
                      if title.nil? || title =~ /^#{divider.strip}$/ || title.strip.empty?
         
     | 
| 
      
 994 
     | 
    
         
            +
                        delete_item(items[i])
         
     | 
| 
      
 995 
     | 
    
         
            +
                      else
         
     | 
| 
      
 996 
     | 
    
         
            +
                        note = input_lines.length > 1 ? input_lines[1..-1] : []
         
     | 
| 
       1280 
997 
     | 
    
         | 
| 
       1281 
     | 
    
         
            -
             
     | 
| 
       1282 
     | 
    
         
            -
             
     | 
| 
       1283 
     | 
    
         
            -
              ##
         
     | 
| 
       1284 
     | 
    
         
            -
              ## @param      old_item
         
     | 
| 
       1285 
     | 
    
         
            -
              ##
         
     | 
| 
       1286 
     | 
    
         
            -
              def delete_item(old_item)
         
     | 
| 
       1287 
     | 
    
         
            -
                section = old_item['section']
         
     | 
| 
      
 998 
     | 
    
         
            +
                        note.map!(&:strip)
         
     | 
| 
      
 999 
     | 
    
         
            +
                        note.delete_if(&:ignore?)
         
     | 
| 
       1288 
1000 
     | 
    
         | 
| 
       1289 
     | 
    
         
            -
             
     | 
| 
       1290 
     | 
    
         
            -
             
     | 
| 
       1291 
     | 
    
         
            -
                @results.push("Entry deleted: #{deleted['title']}")
         
     | 
| 
       1292 
     | 
    
         
            -
                @content[section]['items'] = section_items
         
     | 
| 
       1293 
     | 
    
         
            -
              end
         
     | 
| 
      
 1001 
     | 
    
         
            +
                        date = title.match(/^([\d\-: ]+) \| /)[1]
         
     | 
| 
      
 1002 
     | 
    
         
            +
                        title.sub!(/^([\d\-: ]+) \| /, '')
         
     | 
| 
       1294 
1003 
     | 
    
         | 
| 
       1295 
     | 
    
         
            -
             
     | 
| 
       1296 
     | 
    
         
            -
             
     | 
| 
       1297 
     | 
    
         
            -
             
     | 
| 
       1298 
     | 
    
         
            -
             
     | 
| 
       1299 
     | 
    
         
            -
             
     | 
| 
       1300 
     | 
    
         
            -
             
     | 
| 
       1301 
     | 
    
         
            -
              def untag_item(old_item, tags)
         
     | 
| 
       1302 
     | 
    
         
            -
                title = old_item['title'].dup
         
     | 
| 
       1303 
     | 
    
         
            -
                if tags.is_a? ::String
         
     | 
| 
       1304 
     | 
    
         
            -
                  tags = tags.split(/ *, */).map {|t| t.strip.gsub(/\*/,'[^ (]*') }
         
     | 
| 
       1305 
     | 
    
         
            -
                end
         
     | 
| 
      
 1004 
     | 
    
         
            +
                        item = items[i]
         
     | 
| 
      
 1005 
     | 
    
         
            +
                        item.title = title
         
     | 
| 
      
 1006 
     | 
    
         
            +
                        item.note = note
         
     | 
| 
      
 1007 
     | 
    
         
            +
                        item.date = Time.parse(date) || items[i].date
         
     | 
| 
      
 1008 
     | 
    
         
            +
                      end
         
     | 
| 
      
 1009 
     | 
    
         
            +
                    end
         
     | 
| 
       1306 
1010 
     | 
    
         | 
| 
       1307 
     | 
    
         
            -
             
     | 
| 
       1308 
     | 
    
         
            -
                  if title =~ /@#{tag}/
         
     | 
| 
       1309 
     | 
    
         
            -
                    title.chomp!
         
     | 
| 
       1310 
     | 
    
         
            -
                    title.gsub!(/ +@#{tag}(\(.*?\))?/, '')
         
     | 
| 
       1311 
     | 
    
         
            -
                    new_item = old_item.dup
         
     | 
| 
       1312 
     | 
    
         
            -
                    new_item['title'] = title
         
     | 
| 
       1313 
     | 
    
         
            -
                    update_item(old_item, new_item)
         
     | 
| 
       1314 
     | 
    
         
            -
                    return new_item
         
     | 
| 
       1315 
     | 
    
         
            -
                  else
         
     | 
| 
       1316 
     | 
    
         
            -
                    @results.push(%(Item isn't tagged @#{tag}: "#{title}" in #{old_item['section']}))
         
     | 
| 
       1317 
     | 
    
         
            -
                    return old_item
         
     | 
| 
      
 1011 
     | 
    
         
            +
                    write(@doing_file)
         
     | 
| 
       1318 
1012 
     | 
    
         
             
                  end
         
     | 
| 
       1319 
     | 
    
         
            -
                end
         
     | 
| 
       1320 
     | 
    
         
            -
              end
         
     | 
| 
       1321 
1013 
     | 
    
         | 
| 
       1322 
     | 
    
         
            -
             
     | 
| 
       1323 
     | 
    
         
            -
             
     | 
| 
       1324 
     | 
    
         
            -
             
     | 
| 
       1325 
     | 
    
         
            -
             
     | 
| 
       1326 
     | 
    
         
            -
             
     | 
| 
       1327 
     | 
    
         
            -
              ## @param      date      (Boolean) Include timestamp?
         
     | 
| 
       1328 
     | 
    
         
            -
              ##
         
     | 
| 
       1329 
     | 
    
         
            -
              def tag_item(old_item, tags, remove: false, date: false)
         
     | 
| 
       1330 
     | 
    
         
            -
                title = old_item['title'].dup
         
     | 
| 
       1331 
     | 
    
         
            -
                if tags.is_a? ::String
         
     | 
| 
       1332 
     | 
    
         
            -
                  tags = tags.split(/ *, */).map(&:strip)
         
     | 
| 
       1333 
     | 
    
         
            -
                end
         
     | 
| 
      
 1014 
     | 
    
         
            +
                  if opt[:output]
         
     | 
| 
      
 1015 
     | 
    
         
            +
                    items.map! do |item|
         
     | 
| 
      
 1016 
     | 
    
         
            +
                      item.title = "#{item.title} @project(#{item.section})"
         
     | 
| 
      
 1017 
     | 
    
         
            +
                      item
         
     | 
| 
      
 1018 
     | 
    
         
            +
                    end
         
     | 
| 
       1334 
1019 
     | 
    
         | 
| 
       1335 
     | 
    
         
            -
             
     | 
| 
       1336 
     | 
    
         
            -
             
     | 
| 
       1337 
     | 
    
         
            -
             
     | 
| 
       1338 
     | 
    
         
            -
             
     | 
| 
       1339 
     | 
    
         
            -
                    if  
     | 
| 
       1340 
     | 
    
         
            -
                       
     | 
| 
      
 1020 
     | 
    
         
            +
                    @content = { 'Export' => { :original => 'Export:', :items => items } }
         
     | 
| 
      
 1021 
     | 
    
         
            +
                    options = { section: 'Export' }
         
     | 
| 
      
 1022 
     | 
    
         
            +
             
     | 
| 
      
 1023 
     | 
    
         
            +
             
     | 
| 
      
 1024 
     | 
    
         
            +
                    if opt[:output] =~ /doing/
         
     | 
| 
      
 1025 
     | 
    
         
            +
                      options[:output] = 'template'
         
     | 
| 
      
 1026 
     | 
    
         
            +
                      options[:template] = '- %date | %title%note'
         
     | 
| 
       1341 
1027 
     | 
    
         
             
                    else
         
     | 
| 
       1342 
     | 
    
         
            -
                       
     | 
| 
      
 1028 
     | 
    
         
            +
                      options[:output] = opt[:output]
         
     | 
| 
      
 1029 
     | 
    
         
            +
                      options[:template] = opt[:template] || nil
         
     | 
| 
       1343 
1030 
     | 
    
         
             
                    end
         
     | 
| 
       1344 
     | 
    
         
            -
                    new_item = old_item.dup
         
     | 
| 
       1345 
     | 
    
         
            -
                    new_item['title'] = title
         
     | 
| 
       1346 
     | 
    
         
            -
                    update_item(old_item, new_item)
         
     | 
| 
       1347 
     | 
    
         
            -
                    return new_item
         
     | 
| 
       1348 
     | 
    
         
            -
                  else
         
     | 
| 
       1349 
     | 
    
         
            -
                    @results.push(%(Item already @#{tag}: "#{title}" in #{old_item['section']}))
         
     | 
| 
       1350 
     | 
    
         
            -
                    return old_item
         
     | 
| 
       1351 
     | 
    
         
            -
                  end
         
     | 
| 
       1352 
     | 
    
         
            -
                end
         
     | 
| 
       1353 
     | 
    
         
            -
              end
         
     | 
| 
       1354 
1031 
     | 
    
         | 
| 
       1355 
     | 
    
         
            -
             
     | 
| 
       1356 
     | 
    
         
            -
              ## @brief      Update an item in the index with a modified item
         
     | 
| 
       1357 
     | 
    
         
            -
              ##
         
     | 
| 
       1358 
     | 
    
         
            -
              ## @param      old_item  The old item
         
     | 
| 
       1359 
     | 
    
         
            -
              ## @param      new_item  The new item
         
     | 
| 
       1360 
     | 
    
         
            -
              ##
         
     | 
| 
       1361 
     | 
    
         
            -
              def update_item(old_item, new_item)
         
     | 
| 
       1362 
     | 
    
         
            -
                section = old_item['section']
         
     | 
| 
       1363 
     | 
    
         
            -
             
     | 
| 
       1364 
     | 
    
         
            -
                section_items = @content[section]['items']
         
     | 
| 
       1365 
     | 
    
         
            -
                s_idx = section_items.index(old_item)
         
     | 
| 
      
 1032 
     | 
    
         
            +
                    output = list_section(options)
         
     | 
| 
       1366 
1033 
     | 
    
         | 
| 
       1367 
     | 
    
         
            -
             
     | 
| 
       1368 
     | 
    
         
            -
             
     | 
| 
       1369 
     | 
    
         
            -
             
     | 
| 
       1370 
     | 
    
         
            -
             
     | 
| 
      
 1034 
     | 
    
         
            +
                    if opt[:save_to]
         
     | 
| 
      
 1035 
     | 
    
         
            +
                      file = File.expand_path(opt[:save_to])
         
     | 
| 
      
 1036 
     | 
    
         
            +
                      if File.exist?(file)
         
     | 
| 
      
 1037 
     | 
    
         
            +
                        # Create a backup copy for the undo command
         
     | 
| 
      
 1038 
     | 
    
         
            +
                        FileUtils.cp(file, "#{file}~")
         
     | 
| 
      
 1039 
     | 
    
         
            +
                      end
         
     | 
| 
       1371 
1040 
     | 
    
         | 
| 
       1372 
     | 
    
         
            -
             
     | 
| 
       1373 
     | 
    
         
            -
             
     | 
| 
       1374 
     | 
    
         
            -
             
     | 
| 
       1375 
     | 
    
         
            -
              ## @param      section  (String) The section, default "All"
         
     | 
| 
       1376 
     | 
    
         
            -
              ##
         
     | 
| 
       1377 
     | 
    
         
            -
              def edit_last(section: 'All', options: {})
         
     | 
| 
       1378 
     | 
    
         
            -
                section = guess_section(section)
         
     | 
| 
      
 1041 
     | 
    
         
            +
                      File.open(file, 'w+') do |f|
         
     | 
| 
      
 1042 
     | 
    
         
            +
                        f.puts output
         
     | 
| 
      
 1043 
     | 
    
         
            +
                      end
         
     | 
| 
       1379 
1044 
     | 
    
         | 
| 
       1380 
     | 
    
         
            -
             
     | 
| 
       1381 
     | 
    
         
            -
             
     | 
| 
       1382 
     | 
    
         
            -
             
     | 
| 
       1383 
     | 
    
         
            -
                     
     | 
| 
      
 1045 
     | 
    
         
            +
                      logger.warn('File written:', file)
         
     | 
| 
      
 1046 
     | 
    
         
            +
                    else
         
     | 
| 
      
 1047 
     | 
    
         
            +
                      Doing::Pager.page output
         
     | 
| 
      
 1048 
     | 
    
         
            +
                    end
         
     | 
| 
       1384 
1049 
     | 
    
         
             
                  end
         
     | 
| 
       1385 
     | 
    
         
            -
                  # section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
         
     | 
| 
       1386 
     | 
    
         
            -
                else
         
     | 
| 
       1387 
     | 
    
         
            -
                  items = @content[section]['items']
         
     | 
| 
       1388 
1050 
     | 
    
         
             
                end
         
     | 
| 
       1389 
1051 
     | 
    
         | 
| 
       1390 
     | 
    
         
            -
                 
     | 
| 
      
 1052 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1053 
     | 
    
         
            +
                ## @brief      Tag an item from the index
         
     | 
| 
      
 1054 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1055 
     | 
    
         
            +
                ## @param      item    (Item) The item to tag
         
     | 
| 
      
 1056 
     | 
    
         
            +
                ## @param      tags    (string) The tag to apply
         
     | 
| 
      
 1057 
     | 
    
         
            +
                ## @param      remove  (Boolean) remove tags
         
     | 
| 
      
 1058 
     | 
    
         
            +
                ## @param      date    (Boolean) Include timestamp?
         
     | 
| 
      
 1059 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1060 
     | 
    
         
            +
                def tag_item(item, tags, remove: false, date: false)
         
     | 
| 
      
 1061 
     | 
    
         
            +
                  added = []
         
     | 
| 
      
 1062 
     | 
    
         
            +
                  removed = []
         
     | 
| 
       1391 
1063 
     | 
    
         | 
| 
       1392 
     | 
    
         
            -
             
     | 
| 
      
 1064 
     | 
    
         
            +
                  tags = tags.to_tags if tags.is_a? ::String
         
     | 
| 
       1393 
1065 
     | 
    
         | 
| 
       1394 
     | 
    
         
            -
             
     | 
| 
       1395 
     | 
    
         
            -
             
     | 
| 
       1396 
     | 
    
         
            -
             
     | 
| 
       1397 
     | 
    
         
            -
             
     | 
| 
       1398 
     | 
    
         
            -
             
     | 
| 
       1399 
     | 
    
         
            -
             
     | 
| 
       1400 
     | 
    
         
            -
             
     | 
| 
       1401 
     | 
    
         
            -
                elsif options[:search]
         
     | 
| 
       1402 
     | 
    
         
            -
                  items.each_with_index do |item, i|
         
     | 
| 
       1403 
     | 
    
         
            -
                    if item.matches_search?(options[:search])
         
     | 
| 
       1404 
     | 
    
         
            -
                      idx = i
         
     | 
| 
       1405 
     | 
    
         
            -
                      break
         
     | 
| 
      
 1066 
     | 
    
         
            +
                  done_date = Time.now
         
     | 
| 
      
 1067 
     | 
    
         
            +
             
     | 
| 
      
 1068 
     | 
    
         
            +
                  tags.each do |tag|
         
     | 
| 
      
 1069 
     | 
    
         
            +
                    bool = remove ? :and : :not
         
     | 
| 
      
 1070 
     | 
    
         
            +
                    if item.tags?(tag, bool)
         
     | 
| 
      
 1071 
     | 
    
         
            +
                      item.tag(tag, remove: remove, value: date ? done_date.strftime('%F %R') : nil)
         
     | 
| 
      
 1072 
     | 
    
         
            +
                      remove ? removed.push(tag) : added.push(tag)
         
     | 
| 
       1406 
1073 
     | 
    
         
             
                    end
         
     | 
| 
       1407 
1074 
     | 
    
         
             
                  end
         
     | 
| 
       1408 
     | 
    
         
            -
                else
         
     | 
| 
       1409 
     | 
    
         
            -
                  idx = 0
         
     | 
| 
       1410 
     | 
    
         
            -
                end
         
     | 
| 
       1411 
1075 
     | 
    
         | 
| 
       1412 
     | 
    
         
            -
             
     | 
| 
       1413 
     | 
    
         
            -
             
     | 
| 
       1414 
     | 
    
         
            -
                   
     | 
| 
       1415 
     | 
    
         
            -
                end
         
     | 
| 
      
 1076 
     | 
    
         
            +
                  log_change(tags_added: added, tags_removed: removed, count: 1)
         
     | 
| 
      
 1077 
     | 
    
         
            +
             
     | 
| 
      
 1078 
     | 
    
         
            +
                  item
         
     | 
| 
      
 1079 
     | 
    
         
            +
                end
         
     | 
| 
      
 1080 
     | 
    
         
            +
             
     | 
| 
      
 1081 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1082 
     | 
    
         
            +
                ## @brief      Tag the last entry or X entries
         
     | 
| 
      
 1083 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1084 
     | 
    
         
            +
                ## @param      opt   (Hash) Additional Options
         
     | 
| 
      
 1085 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1086 
     | 
    
         
            +
                def tag_last(opt = {})
         
     | 
| 
      
 1087 
     | 
    
         
            +
                  opt[:count] ||= 1
         
     | 
| 
      
 1088 
     | 
    
         
            +
                  opt[:archive] ||= false
         
     | 
| 
      
 1089 
     | 
    
         
            +
                  opt[:tags] ||= ['done']
         
     | 
| 
      
 1090 
     | 
    
         
            +
                  opt[:sequential] ||= false
         
     | 
| 
      
 1091 
     | 
    
         
            +
                  opt[:date] ||= false
         
     | 
| 
      
 1092 
     | 
    
         
            +
                  opt[:remove] ||= false
         
     | 
| 
      
 1093 
     | 
    
         
            +
                  opt[:autotag] ||= false
         
     | 
| 
      
 1094 
     | 
    
         
            +
                  opt[:back] ||= false
         
     | 
| 
      
 1095 
     | 
    
         
            +
                  opt[:unfinished] ||= false
         
     | 
| 
      
 1096 
     | 
    
         
            +
                  opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
         
     | 
| 
      
 1097 
     | 
    
         
            +
             
     | 
| 
      
 1098 
     | 
    
         
            +
                  items = filter_items([], opt: opt)
         
     | 
| 
      
 1099 
     | 
    
         
            +
             
     | 
| 
      
 1100 
     | 
    
         
            +
                  logger.info('Skipped:', 'no items matched your search') if items.empty?
         
     | 
| 
      
 1101 
     | 
    
         
            +
             
     | 
| 
      
 1102 
     | 
    
         
            +
                  if opt[:interactive]
         
     | 
| 
      
 1103 
     | 
    
         
            +
                    items = choose_from_items(items, {
         
     | 
| 
      
 1104 
     | 
    
         
            +
                      menu: true,
         
     | 
| 
      
 1105 
     | 
    
         
            +
                      header: '',
         
     | 
| 
      
 1106 
     | 
    
         
            +
                      prompt: 'Select entries to tag > ',
         
     | 
| 
      
 1107 
     | 
    
         
            +
                      multiple: true,
         
     | 
| 
      
 1108 
     | 
    
         
            +
                      sort: true,
         
     | 
| 
      
 1109 
     | 
    
         
            +
                      show_if_single: true
         
     | 
| 
      
 1110 
     | 
    
         
            +
                    }, include_section: opt[:section] =~ /^all$/i )
         
     | 
| 
      
 1111 
     | 
    
         
            +
             
     | 
| 
      
 1112 
     | 
    
         
            +
                    return if items.nil?
         
     | 
| 
      
 1113 
     | 
    
         
            +
                  end
         
     | 
| 
       1416 
1114 
     | 
    
         | 
| 
       1417 
     | 
    
         
            -
             
     | 
| 
       1418 
     | 
    
         
            -
             
     | 
| 
       1419 
     | 
    
         
            -
             
     | 
| 
       1420 
     | 
    
         
            -
             
     | 
| 
       1421 
     | 
    
         
            -
             
     | 
| 
       1422 
     | 
    
         
            -
             
     | 
| 
       1423 
     | 
    
         
            -
             
     | 
| 
       1424 
     | 
    
         
            -
             
     | 
| 
       1425 
     | 
    
         
            -
             
     | 
| 
       1426 
     | 
    
         
            -
             
     | 
| 
       1427 
     | 
    
         
            -
             
     | 
| 
       1428 
     | 
    
         
            -
             
     | 
| 
       1429 
     | 
    
         
            -
             
     | 
| 
       1430 
     | 
    
         
            -
             
     | 
| 
       1431 
     | 
    
         
            -
             
     | 
| 
       1432 
     | 
    
         
            -
             
     | 
| 
       1433 
     | 
    
         
            -
             
     | 
| 
       1434 
     | 
    
         
            -
             
     | 
| 
       1435 
     | 
    
         
            -
             
     | 
| 
       1436 
     | 
    
         
            -
             
     | 
| 
       1437 
     | 
    
         
            -
             
     | 
| 
       1438 
     | 
    
         
            -
             
     | 
| 
       1439 
     | 
    
         
            -
             
     | 
| 
      
 1115 
     | 
    
         
            +
                  items.each do |item|
         
     | 
| 
      
 1116 
     | 
    
         
            +
                    added = []
         
     | 
| 
      
 1117 
     | 
    
         
            +
                    removed = []
         
     | 
| 
      
 1118 
     | 
    
         
            +
             
     | 
| 
      
 1119 
     | 
    
         
            +
                    if opt[:autotag]
         
     | 
| 
      
 1120 
     | 
    
         
            +
                      new_title = autotag(item.title) if @auto_tag
         
     | 
| 
      
 1121 
     | 
    
         
            +
                      if new_title == item.title
         
     | 
| 
      
 1122 
     | 
    
         
            +
                        logger.count(:skipped, level: :debug, message: '%count unchaged %items')
         
     | 
| 
      
 1123 
     | 
    
         
            +
                        # logger.debug('Autotag:', 'No changes')
         
     | 
| 
      
 1124 
     | 
    
         
            +
                      else
         
     | 
| 
      
 1125 
     | 
    
         
            +
                        logger.count(:added_tags)
         
     | 
| 
      
 1126 
     | 
    
         
            +
                        logger.debug('Tags updated:', new_title)
         
     | 
| 
      
 1127 
     | 
    
         
            +
                        item.title = new_title
         
     | 
| 
      
 1128 
     | 
    
         
            +
                      end
         
     | 
| 
      
 1129 
     | 
    
         
            +
                    else
         
     | 
| 
      
 1130 
     | 
    
         
            +
                      if opt[:sequential]
         
     | 
| 
      
 1131 
     | 
    
         
            +
                        next_entry = next_item(item)
         
     | 
| 
      
 1132 
     | 
    
         
            +
             
     | 
| 
      
 1133 
     | 
    
         
            +
                        done_date = if next_entry.nil?
         
     | 
| 
      
 1134 
     | 
    
         
            +
                                      Time.now
         
     | 
| 
      
 1135 
     | 
    
         
            +
                                    else
         
     | 
| 
      
 1136 
     | 
    
         
            +
                                      next_entry.date - 60
         
     | 
| 
      
 1137 
     | 
    
         
            +
                                    end
         
     | 
| 
      
 1138 
     | 
    
         
            +
                      elsif opt[:took]
         
     | 
| 
      
 1139 
     | 
    
         
            +
                        if item.date + opt[:took] > Time.now
         
     | 
| 
      
 1140 
     | 
    
         
            +
                          item.date = Time.now - opt[:took]
         
     | 
| 
      
 1141 
     | 
    
         
            +
                          done_date = Time.now
         
     | 
| 
      
 1142 
     | 
    
         
            +
                        else
         
     | 
| 
      
 1143 
     | 
    
         
            +
                          done_date = item.date + opt[:took]
         
     | 
| 
      
 1144 
     | 
    
         
            +
                        end
         
     | 
| 
      
 1145 
     | 
    
         
            +
                      elsif opt[:back]
         
     | 
| 
      
 1146 
     | 
    
         
            +
                        done_date = if opt[:back].is_a? Integer
         
     | 
| 
      
 1147 
     | 
    
         
            +
                                      item.date + opt[:back]
         
     | 
| 
      
 1148 
     | 
    
         
            +
                                    else
         
     | 
| 
      
 1149 
     | 
    
         
            +
                                      item.date + (opt[:back] - item.date)
         
     | 
| 
      
 1150 
     | 
    
         
            +
                                    end
         
     | 
| 
      
 1151 
     | 
    
         
            +
                      else
         
     | 
| 
      
 1152 
     | 
    
         
            +
                        done_date = Time.now
         
     | 
| 
      
 1153 
     | 
    
         
            +
                      end
         
     | 
| 
       1440 
1154 
     | 
    
         | 
| 
       1441 
     | 
    
         
            -
             
     | 
| 
       1442 
     | 
    
         
            -
             
     | 
| 
       1443 
     | 
    
         
            -
              ##
         
     | 
| 
       1444 
     | 
    
         
            -
              ## @param      section  (String) The section, default "All"
         
     | 
| 
       1445 
     | 
    
         
            -
              ## @param      note     (String) The note to add
         
     | 
| 
       1446 
     | 
    
         
            -
              ## @param      replace  (Bool) Should replace existing note
         
     | 
| 
       1447 
     | 
    
         
            -
              ##
         
     | 
| 
       1448 
     | 
    
         
            -
              def note_last(section, note, replace: false)
         
     | 
| 
       1449 
     | 
    
         
            -
                section = guess_section(section)
         
     | 
| 
      
 1155 
     | 
    
         
            +
                      opt[:tags].each do |tag|
         
     | 
| 
      
 1156 
     | 
    
         
            +
                        if tag == 'done' && !item.should_finish?
         
     | 
| 
       1450 
1157 
     | 
    
         | 
| 
       1451 
     | 
    
         
            -
             
     | 
| 
       1452 
     | 
    
         
            -
             
     | 
| 
       1453 
     | 
    
         
            -
             
     | 
| 
       1454 
     | 
    
         
            -
             
     | 
| 
       1455 
     | 
    
         
            -
                  end
         
     | 
| 
       1456 
     | 
    
         
            -
                  section = combined['items'].dup.sort_by { |item| item['date'] }.reverse[0]['section']
         
     | 
| 
       1457 
     | 
    
         
            -
                end
         
     | 
| 
      
 1158 
     | 
    
         
            +
                          Doing.logger.debug('Skipped:', "Item in never_finish: #{item.title}")
         
     | 
| 
      
 1159 
     | 
    
         
            +
                          logger.count(:skipped, level: :debug)
         
     | 
| 
      
 1160 
     | 
    
         
            +
                          next
         
     | 
| 
      
 1161 
     | 
    
         
            +
                        end
         
     | 
| 
       1458 
1162 
     | 
    
         | 
| 
       1459 
     | 
    
         
            -
             
     | 
| 
       1460 
     | 
    
         
            -
             
     | 
| 
       1461 
     | 
    
         
            -
             
     | 
| 
       1462 
     | 
    
         
            -
             
     | 
| 
       1463 
     | 
    
         
            -
             
     | 
| 
       1464 
     | 
    
         
            -
             
     | 
| 
       1465 
     | 
    
         
            -
             
     | 
| 
       1466 
     | 
    
         
            -
             
     | 
| 
       1467 
     | 
    
         
            -
             
     | 
| 
       1468 
     | 
    
         
            -
             
     | 
| 
       1469 
     | 
    
         
            -
             
     | 
| 
       1470 
     | 
    
         
            -
             
     | 
| 
       1471 
     | 
    
         
            -
             
     | 
| 
       1472 
     | 
    
         
            -
             
     | 
| 
       1473 
     | 
    
         
            -
             
     | 
| 
       1474 
     | 
    
         
            -
             
     | 
| 
       1475 
     | 
    
         
            -
             
     | 
| 
       1476 
     | 
    
         
            -
             
     | 
| 
       1477 
     | 
    
         
            -
             
     | 
| 
       1478 
     | 
    
         
            -
             
     | 
| 
       1479 
     | 
    
         
            -
             
     | 
| 
       1480 
     | 
    
         
            -
             
     | 
| 
       1481 
     | 
    
         
            -
             
     | 
| 
       1482 
     | 
    
         
            -
                  items[0]['note'] = note
         
     | 
| 
       1483 
     | 
    
         
            -
                  @results.push(%(Added note to "#{title}")) unless note.empty?
         
     | 
| 
       1484 
     | 
    
         
            -
                end
         
     | 
| 
      
 1163 
     | 
    
         
            +
                        tag = tag.strip
         
     | 
| 
      
 1164 
     | 
    
         
            +
                        if opt[:remove] || opt[:rename]
         
     | 
| 
      
 1165 
     | 
    
         
            +
                          rename_to = nil
         
     | 
| 
      
 1166 
     | 
    
         
            +
                          if opt[:rename]
         
     | 
| 
      
 1167 
     | 
    
         
            +
                            rename_to = tag
         
     | 
| 
      
 1168 
     | 
    
         
            +
                            tag = opt[:rename]
         
     | 
| 
      
 1169 
     | 
    
         
            +
                          end
         
     | 
| 
      
 1170 
     | 
    
         
            +
                          old_title = item.title.dup
         
     | 
| 
      
 1171 
     | 
    
         
            +
                          item.title.tag!(tag, remove: opt[:remove], rename_to: rename_to, regex: opt[:regex])
         
     | 
| 
      
 1172 
     | 
    
         
            +
                          if old_title != item.title
         
     | 
| 
      
 1173 
     | 
    
         
            +
                            removed << tag
         
     | 
| 
      
 1174 
     | 
    
         
            +
                            added << rename_to if rename_to
         
     | 
| 
      
 1175 
     | 
    
         
            +
                          else
         
     | 
| 
      
 1176 
     | 
    
         
            +
                            logger.count(:skipped, level: :debug)
         
     | 
| 
      
 1177 
     | 
    
         
            +
                          end
         
     | 
| 
      
 1178 
     | 
    
         
            +
                        else
         
     | 
| 
      
 1179 
     | 
    
         
            +
                          old_title = item.title.dup
         
     | 
| 
      
 1180 
     | 
    
         
            +
                          should_date = opt[:date] && item.should_time?
         
     | 
| 
      
 1181 
     | 
    
         
            +
                          item.title.tag!(tag, value: should_date ? done_date.strftime('%F %R') : nil)
         
     | 
| 
      
 1182 
     | 
    
         
            +
                          added << tag if old_title != item.title
         
     | 
| 
      
 1183 
     | 
    
         
            +
                        end
         
     | 
| 
      
 1184 
     | 
    
         
            +
                      end
         
     | 
| 
      
 1185 
     | 
    
         
            +
                    end
         
     | 
| 
       1485 
1186 
     | 
    
         | 
| 
       1486 
     | 
    
         
            -
             
     | 
| 
      
 1187 
     | 
    
         
            +
                    log_change(tags_added: added, tags_removed: removed)
         
     | 
| 
       1487 
1188 
     | 
    
         | 
| 
       1488 
     | 
    
         
            -
             
     | 
| 
      
 1189 
     | 
    
         
            +
                    item.note.add(opt[:note]) if opt[:note]
         
     | 
| 
       1489 
1190 
     | 
    
         | 
| 
       1490 
     | 
    
         
            -
             
     | 
| 
       1491 
     | 
    
         
            -
             
     | 
| 
       1492 
     | 
    
         
            -
             
     | 
| 
       1493 
     | 
    
         
            -
             
     | 
| 
       1494 
     | 
    
         
            -
             
     | 
| 
       1495 
     | 
    
         
            -
              #             (@meanwhile)
         
     | 
| 
       1496 
     | 
    
         
            -
              #
         
     | 
| 
       1497 
     | 
    
         
            -
              # @param      tag   (String) Tag to replace
         
     | 
| 
       1498 
     | 
    
         
            -
              # @param      opt   (Hash) Additional Options
         
     | 
| 
       1499 
     | 
    
         
            -
              #
         
     | 
| 
       1500 
     | 
    
         
            -
              def stop_start(target_tag, opt = {})
         
     | 
| 
       1501 
     | 
    
         
            -
                tag = target_tag.dup
         
     | 
| 
       1502 
     | 
    
         
            -
                opt[:section] ||= @current_section
         
     | 
| 
       1503 
     | 
    
         
            -
                opt[:archive] ||= false
         
     | 
| 
       1504 
     | 
    
         
            -
                opt[:back] ||= Time.now
         
     | 
| 
       1505 
     | 
    
         
            -
                opt[:new_item] ||= false
         
     | 
| 
       1506 
     | 
    
         
            -
                opt[:note] ||= false
         
     | 
| 
       1507 
     | 
    
         
            -
             
     | 
| 
       1508 
     | 
    
         
            -
                opt[:section] = guess_section(opt[:section])
         
     | 
| 
       1509 
     | 
    
         
            -
             
     | 
| 
       1510 
     | 
    
         
            -
                tag.sub!(/^@/, '')
         
     | 
| 
       1511 
     | 
    
         
            -
             
     | 
| 
       1512 
     | 
    
         
            -
                found_items = 0
         
     | 
| 
       1513 
     | 
    
         
            -
                @content[opt[:section]]['items'].each_with_index do |item, i|
         
     | 
| 
       1514 
     | 
    
         
            -
                  next unless item['title'] =~ /@#{tag}/
         
     | 
| 
       1515 
     | 
    
         
            -
             
     | 
| 
       1516 
     | 
    
         
            -
                  title = item['title'].gsub(/(^| )@(#{tag}|done)(\([^)]*\))?/, '')
         
     | 
| 
       1517 
     | 
    
         
            -
                  title += " @done(#{opt[:back].strftime('%F %R')})"
         
     | 
| 
       1518 
     | 
    
         
            -
             
     | 
| 
       1519 
     | 
    
         
            -
                  @content[opt[:section]]['items'][i]['title'] = title
         
     | 
| 
       1520 
     | 
    
         
            -
                  found_items += 1
         
     | 
| 
       1521 
     | 
    
         
            -
             
     | 
| 
       1522 
     | 
    
         
            -
                  if opt[:archive] && opt[:section] != 'Archive'
         
     | 
| 
       1523 
     | 
    
         
            -
                    @results.push(%(Completed and archived "#{@content[opt[:section]]['items'][i]['title']}"))
         
     | 
| 
       1524 
     | 
    
         
            -
                    archive_item = @content[opt[:section]]['items'][i]
         
     | 
| 
       1525 
     | 
    
         
            -
                    archive_item['title'] = i['title'].sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{i['section']})")
         
     | 
| 
       1526 
     | 
    
         
            -
                    @content['Archive']['items'].push(archive_item)
         
     | 
| 
       1527 
     | 
    
         
            -
                    @content[opt[:section]]['items'].delete_at(i)
         
     | 
| 
       1528 
     | 
    
         
            -
                  else
         
     | 
| 
       1529 
     | 
    
         
            -
                    @results.push(%(Completed "#{@content[opt[:section]]['items'][i]['title']}"))
         
     | 
| 
      
 1191 
     | 
    
         
            +
                    if opt[:archive] && opt[:section] != 'Archive' && (opt[:count]).positive?
         
     | 
| 
      
 1192 
     | 
    
         
            +
                      move_item(item, 'Archive', label: true)
         
     | 
| 
      
 1193 
     | 
    
         
            +
                    elsif opt[:archive] && opt[:count].zero?
         
     | 
| 
      
 1194 
     | 
    
         
            +
                      logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
         
     | 
| 
      
 1195 
     | 
    
         
            +
                    end
         
     | 
| 
       1530 
1196 
     | 
    
         
             
                  end
         
     | 
| 
      
 1197 
     | 
    
         
            +
             
     | 
| 
      
 1198 
     | 
    
         
            +
                  write(@doing_file)
         
     | 
| 
       1531 
1199 
     | 
    
         
             
                end
         
     | 
| 
       1532 
1200 
     | 
    
         | 
| 
       1533 
     | 
    
         
            -
                 
     | 
| 
      
 1201 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1202 
     | 
    
         
            +
                ## @brief      Move item from current section to
         
     | 
| 
      
 1203 
     | 
    
         
            +
                ##             destination section
         
     | 
| 
      
 1204 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1205 
     | 
    
         
            +
                ## @param      item     The item
         
     | 
| 
      
 1206 
     | 
    
         
            +
                ## @param      section  The destination section
         
     | 
| 
      
 1207 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1208 
     | 
    
         
            +
                ## @return     Updated item
         
     | 
| 
      
 1209 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1210 
     | 
    
         
            +
                def move_item(item, section, label: true)
         
     | 
| 
      
 1211 
     | 
    
         
            +
                  from = item.section
         
     | 
| 
      
 1212 
     | 
    
         
            +
                  new_item = @content[item.section][:items].delete(item)
         
     | 
| 
      
 1213 
     | 
    
         
            +
                  new_item.title.sub!(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{from})") if label
         
     | 
| 
      
 1214 
     | 
    
         
            +
                  new_item.section = section
         
     | 
| 
      
 1215 
     | 
    
         
            +
             
     | 
| 
      
 1216 
     | 
    
         
            +
                  @content[section][:items].concat([new_item])
         
     | 
| 
      
 1217 
     | 
    
         
            +
             
     | 
| 
      
 1218 
     | 
    
         
            +
                  logger.count(section == 'Archive' ? :archived : :moved)
         
     | 
| 
      
 1219 
     | 
    
         
            +
                  logger.debug("Entry #{section == 'Archive' ? 'archived' : 'moved'}:",
         
     | 
| 
      
 1220 
     | 
    
         
            +
                              "#{new_item.title.truncate(60)} from #{from} to #{section}")
         
     | 
| 
      
 1221 
     | 
    
         
            +
                  new_item
         
     | 
| 
      
 1222 
     | 
    
         
            +
                end
         
     | 
| 
      
 1223 
     | 
    
         
            +
             
     | 
| 
      
 1224 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1225 
     | 
    
         
            +
                ## @brief      Get next item in the index
         
     | 
| 
      
 1226 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1227 
     | 
    
         
            +
                ## @param      item
         
     | 
| 
      
 1228 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1229 
     | 
    
         
            +
                def next_item(item, options = {})
         
     | 
| 
      
 1230 
     | 
    
         
            +
                  items = filter_items([], opt: options)
         
     | 
| 
      
 1231 
     | 
    
         
            +
             
     | 
| 
      
 1232 
     | 
    
         
            +
                  idx = items.index(item)
         
     | 
| 
      
 1233 
     | 
    
         
            +
             
     | 
| 
      
 1234 
     | 
    
         
            +
                  idx.positive? ? items[idx - 1] : nil
         
     | 
| 
      
 1235 
     | 
    
         
            +
                end
         
     | 
| 
      
 1236 
     | 
    
         
            +
             
     | 
| 
      
 1237 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1238 
     | 
    
         
            +
                ## @brief      Delete an item from the index
         
     | 
| 
      
 1239 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1240 
     | 
    
         
            +
                ## @param      item  The item
         
     | 
| 
      
 1241 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1242 
     | 
    
         
            +
                def delete_item(item)
         
     | 
| 
      
 1243 
     | 
    
         
            +
                  section = item.section
         
     | 
| 
      
 1244 
     | 
    
         
            +
             
     | 
| 
      
 1245 
     | 
    
         
            +
                  section_items = @content[section][:items]
         
     | 
| 
      
 1246 
     | 
    
         
            +
                  deleted = section_items.delete(item)
         
     | 
| 
      
 1247 
     | 
    
         
            +
                  logger.count(:deleted)
         
     | 
| 
      
 1248 
     | 
    
         
            +
                  logger.debug('Entry deleted:', deleted.title)
         
     | 
| 
      
 1249 
     | 
    
         
            +
                end
         
     | 
| 
      
 1250 
     | 
    
         
            +
             
     | 
| 
      
 1251 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1252 
     | 
    
         
            +
                ## @brief      Update an item in the index with a modified item
         
     | 
| 
      
 1253 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1254 
     | 
    
         
            +
                ## @param      old_item  The old item
         
     | 
| 
      
 1255 
     | 
    
         
            +
                ## @param      new_item  The new item
         
     | 
| 
      
 1256 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1257 
     | 
    
         
            +
                def update_item(old_item, new_item)
         
     | 
| 
      
 1258 
     | 
    
         
            +
                  section = old_item.section
         
     | 
| 
      
 1259 
     | 
    
         
            +
             
     | 
| 
      
 1260 
     | 
    
         
            +
                  section_items = @content[section][:items]
         
     | 
| 
      
 1261 
     | 
    
         
            +
                  s_idx = section_items.index { |item| item.equal?(old_item) }
         
     | 
| 
      
 1262 
     | 
    
         
            +
             
     | 
| 
      
 1263 
     | 
    
         
            +
                  unless s_idx
         
     | 
| 
      
 1264 
     | 
    
         
            +
                    Doing.logger.error('Fail to update:', 'Could not find item in index')
         
     | 
| 
      
 1265 
     | 
    
         
            +
                    raise Errors::ItemNotFound, 'Unable to find item in index, did it mutate?'
         
     | 
| 
      
 1266 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1267 
     | 
    
         
            +
             
     | 
| 
      
 1268 
     | 
    
         
            +
                  return if section_items[s_idx].equal?(new_item)
         
     | 
| 
       1534 
1269 
     | 
    
         | 
| 
       1535 
     | 
    
         
            -
             
     | 
| 
       1536 
     | 
    
         
            -
                   
     | 
| 
       1537 
     | 
    
         
            -
                   
     | 
| 
       1538 
     | 
    
         
            -
                   
     | 
| 
       1539 
     | 
    
         
            -
                  add_item(title.cap_first, opt[:section], { note: note.join(' ').rstrip, back: opt[:back] })
         
     | 
| 
      
 1270 
     | 
    
         
            +
                  section_items[s_idx] = new_item
         
     | 
| 
      
 1271 
     | 
    
         
            +
                  logger.count(:updated)
         
     | 
| 
      
 1272 
     | 
    
         
            +
                  logger.debug('Entry updated:', section_items[s_idx].title.truncate(60))
         
     | 
| 
      
 1273 
     | 
    
         
            +
                  new_item
         
     | 
| 
       1540 
1274 
     | 
    
         
             
                end
         
     | 
| 
       1541 
1275 
     | 
    
         | 
| 
       1542 
     | 
    
         
            -
                 
     | 
| 
       1543 
     | 
    
         
            -
             
     | 
| 
      
 1276 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1277 
     | 
    
         
            +
                ## @brief      Edit the last entry
         
     | 
| 
      
 1278 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1279 
     | 
    
         
            +
                ## @param      section  (String) The section, default "All"
         
     | 
| 
      
 1280 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1281 
     | 
    
         
            +
                def edit_last(section: 'All', options: {})
         
     | 
| 
      
 1282 
     | 
    
         
            +
                  options[:section] = guess_section(section)
         
     | 
| 
       1544 
1283 
     | 
    
         | 
| 
       1545 
     | 
    
         
            -
             
     | 
| 
       1546 
     | 
    
         
            -
              ## @brief      Write content to file or STDOUT
         
     | 
| 
       1547 
     | 
    
         
            -
              ##
         
     | 
| 
       1548 
     | 
    
         
            -
              ## @param      file  (String) The filepath to write to
         
     | 
| 
       1549 
     | 
    
         
            -
              ##
         
     | 
| 
       1550 
     | 
    
         
            -
              def write(file = nil, backup: true)
         
     | 
| 
       1551 
     | 
    
         
            -
                output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
         
     | 
| 
      
 1284 
     | 
    
         
            +
                  item = last_entry(options)
         
     | 
| 
       1552 
1285 
     | 
    
         | 
| 
       1553 
     | 
    
         
            -
             
     | 
| 
       1554 
     | 
    
         
            -
             
     | 
| 
       1555 
     | 
    
         
            -
             
     | 
| 
       1556 
     | 
    
         
            -
                end
         
     | 
| 
       1557 
     | 
    
         
            -
                output += @other_content_bottom.join("\n") unless @other_content_bottom.nil?
         
     | 
| 
       1558 
     | 
    
         
            -
                if file.nil?
         
     | 
| 
       1559 
     | 
    
         
            -
                  $stdout.puts output
         
     | 
| 
       1560 
     | 
    
         
            -
                else
         
     | 
| 
       1561 
     | 
    
         
            -
                  file = File.expand_path(file)
         
     | 
| 
       1562 
     | 
    
         
            -
                  if File.exist?(file) && backup
         
     | 
| 
       1563 
     | 
    
         
            -
                    # Create a backup copy for the undo command
         
     | 
| 
       1564 
     | 
    
         
            -
                    FileUtils.cp(file, "#{file}~")
         
     | 
| 
      
 1286 
     | 
    
         
            +
                  if item.nil?
         
     | 
| 
      
 1287 
     | 
    
         
            +
                    logger.debug('Skipped:', 'No entries found')
         
     | 
| 
      
 1288 
     | 
    
         
            +
                    return
         
     | 
| 
       1565 
1289 
     | 
    
         
             
                  end
         
     | 
| 
       1566 
1290 
     | 
    
         | 
| 
       1567 
     | 
    
         
            -
                   
     | 
| 
       1568 
     | 
    
         
            -
             
     | 
| 
       1569 
     | 
    
         
            -
                   
     | 
| 
      
 1291 
     | 
    
         
            +
                  content = [item.title.dup]
         
     | 
| 
      
 1292 
     | 
    
         
            +
                  content << item.note.to_s unless item.note.empty?
         
     | 
| 
      
 1293 
     | 
    
         
            +
                  new_item = fork_editor(content.join("\n"))
         
     | 
| 
      
 1294 
     | 
    
         
            +
                  title, note = format_input(new_item)
         
     | 
| 
       1570 
1295 
     | 
    
         | 
| 
       1571 
     | 
    
         
            -
                  if  
     | 
| 
       1572 
     | 
    
         
            -
                     
     | 
| 
       1573 
     | 
    
         
            -
             
     | 
| 
       1574 
     | 
    
         
            -
             
     | 
| 
       1575 
     | 
    
         
            -
             
     | 
| 
       1576 
     | 
    
         
            -
                     
     | 
| 
       1577 
     | 
    
         
            -
             
     | 
| 
       1578 
     | 
    
         
            -
             
     | 
| 
       1579 
     | 
    
         
            -
              end
         
     | 
| 
      
 1296 
     | 
    
         
            +
                  if title.nil? || title.empty?
         
     | 
| 
      
 1297 
     | 
    
         
            +
                    logger.debug('Skipped:', 'No content provided')
         
     | 
| 
      
 1298 
     | 
    
         
            +
                  elsif title == item.title && note.equal?(item.note)
         
     | 
| 
      
 1299 
     | 
    
         
            +
                    logger.debug('Skipped:', 'No change in content')
         
     | 
| 
      
 1300 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1301 
     | 
    
         
            +
                    item.title = title
         
     | 
| 
      
 1302 
     | 
    
         
            +
                    item.note.add(note, replace: true)
         
     | 
| 
      
 1303 
     | 
    
         
            +
                    logger.info('Edited:', item.title)
         
     | 
| 
       1580 
1304 
     | 
    
         | 
| 
       1581 
     | 
    
         
            -
             
     | 
| 
       1582 
     | 
    
         
            -
             
     | 
| 
       1583 
     | 
    
         
            -
              ##
         
     | 
| 
       1584 
     | 
    
         
            -
              ## @param      file  (String) The filepath to restore
         
     | 
| 
       1585 
     | 
    
         
            -
              ##
         
     | 
| 
       1586 
     | 
    
         
            -
              def restore_backup(file)
         
     | 
| 
       1587 
     | 
    
         
            -
                if File.exist?(file + '~')
         
     | 
| 
       1588 
     | 
    
         
            -
                  puts file + '~'
         
     | 
| 
       1589 
     | 
    
         
            -
                  FileUtils.cp(file + '~', file)
         
     | 
| 
       1590 
     | 
    
         
            -
                  @results.push("Restored #{file}")
         
     | 
| 
      
 1305 
     | 
    
         
            +
                    write(@doing_file)
         
     | 
| 
      
 1306 
     | 
    
         
            +
                  end
         
     | 
| 
       1591 
1307 
     | 
    
         
             
                end
         
     | 
| 
       1592 
     | 
    
         
            -
              end
         
     | 
| 
       1593 
1308 
     | 
    
         | 
| 
       1594 
     | 
    
         
            -
             
     | 
| 
       1595 
     | 
    
         
            -
             
     | 
| 
       1596 
     | 
    
         
            -
             
     | 
| 
       1597 
     | 
    
         
            -
             
     | 
| 
       1598 
     | 
    
         
            -
                 
     | 
| 
       1599 
     | 
    
         
            -
                 
     | 
| 
       1600 
     | 
    
         
            -
                 
     | 
| 
       1601 
     | 
    
         
            -
                 
     | 
| 
       1602 
     | 
    
         
            -
                 
     | 
| 
       1603 
     | 
    
         
            -
             
     | 
| 
       1604 
     | 
    
         
            -
                 
     | 
| 
       1605 
     | 
    
         
            -
                   
     | 
| 
       1606 
     | 
    
         
            -
             
     | 
| 
       1607 
     | 
    
         
            -
                   
     | 
| 
       1608 
     | 
    
         
            -
             
     | 
| 
      
 1309 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1310 
     | 
    
         
            +
                ## @brief      Accepts one tag and the raw text of a new item if the passed tag
         
     | 
| 
      
 1311 
     | 
    
         
            +
                ##             is on any item, it's replaced with @done. if new_item is not
         
     | 
| 
      
 1312 
     | 
    
         
            +
                ##             nil, it's tagged with the passed tag and inserted. This is for
         
     | 
| 
      
 1313 
     | 
    
         
            +
                ##             use where only one instance of a given tag should exist
         
     | 
| 
      
 1314 
     | 
    
         
            +
                ##             (@meanwhile)
         
     | 
| 
      
 1315 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1316 
     | 
    
         
            +
                ## @param      tag   (String) Tag to replace
         
     | 
| 
      
 1317 
     | 
    
         
            +
                ## @param      opt   (Hash) Additional Options
         
     | 
| 
      
 1318 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1319 
     | 
    
         
            +
                def stop_start(target_tag, opt = {})
         
     | 
| 
      
 1320 
     | 
    
         
            +
                  tag = target_tag.dup
         
     | 
| 
      
 1321 
     | 
    
         
            +
                  opt[:section] ||= @config['current_section']
         
     | 
| 
      
 1322 
     | 
    
         
            +
                  opt[:archive] ||= false
         
     | 
| 
      
 1323 
     | 
    
         
            +
                  opt[:back] ||= Time.now
         
     | 
| 
      
 1324 
     | 
    
         
            +
                  opt[:new_item] ||= false
         
     | 
| 
      
 1325 
     | 
    
         
            +
                  opt[:note] ||= false
         
     | 
| 
       1609 
1326 
     | 
    
         | 
| 
       1610 
     | 
    
         
            -
             
     | 
| 
       1611 
     | 
    
         
            -
                new_content = {}
         
     | 
| 
      
 1327 
     | 
    
         
            +
                  opt[:section] = guess_section(opt[:section])
         
     | 
| 
       1612 
1328 
     | 
    
         | 
| 
      
 1329 
     | 
    
         
            +
                  tag.sub!(/^@/, '')
         
     | 
| 
       1613 
1330 
     | 
    
         | 
| 
       1614 
     | 
    
         
            -
             
     | 
| 
       1615 
     | 
    
         
            -
                  items = @content[section]['items'].dup
         
     | 
| 
       1616 
     | 
    
         
            -
                  new_content[section] = {}
         
     | 
| 
       1617 
     | 
    
         
            -
                  new_content[section]['original'] = @content[section]['original']
         
     | 
| 
       1618 
     | 
    
         
            -
                  new_content[section]['items'] = []
         
     | 
| 
      
 1331 
     | 
    
         
            +
                  found_items = 0
         
     | 
| 
       1619 
1332 
     | 
    
         | 
| 
       1620 
     | 
    
         
            -
                   
     | 
| 
       1621 
     | 
    
         
            -
             
     | 
| 
       1622 
     | 
    
         
            -
                    if opt[:before]
         
     | 
| 
       1623 
     | 
    
         
            -
                      time_string = opt[:before]
         
     | 
| 
       1624 
     | 
    
         
            -
                      time_string += ' 12am' if time_string !~ /(\d+:\d+|\d+[ap])/
         
     | 
| 
       1625 
     | 
    
         
            -
                      cutoff = chronify(time_string)
         
     | 
| 
       1626 
     | 
    
         
            -
                    end
         
     | 
| 
      
 1333 
     | 
    
         
            +
                  @content[opt[:section]][:items].each_with_index do |item, i|
         
     | 
| 
      
 1334 
     | 
    
         
            +
                    next unless item.title =~ /@#{tag}/
         
     | 
| 
       1627 
1335 
     | 
    
         | 
| 
       1628 
     | 
    
         
            -
                     
     | 
| 
       1629 
     | 
    
         
            -
             
     | 
| 
       1630 
     | 
    
         
            -
                        moved_items.push(item)
         
     | 
| 
       1631 
     | 
    
         
            -
                        counter += 1
         
     | 
| 
       1632 
     | 
    
         
            -
                        true
         
     | 
| 
       1633 
     | 
    
         
            -
                      else
         
     | 
| 
       1634 
     | 
    
         
            -
                        false
         
     | 
| 
       1635 
     | 
    
         
            -
                      end
         
     | 
| 
       1636 
     | 
    
         
            -
                    end
         
     | 
| 
       1637 
     | 
    
         
            -
                    @content[section]['items'] = items
         
     | 
| 
       1638 
     | 
    
         
            -
                    new_content[section]['items'] = moved_items
         
     | 
| 
       1639 
     | 
    
         
            -
                    @results.push("Rotated #{moved_items.length} items from #{section}")
         
     | 
| 
       1640 
     | 
    
         
            -
                  else
         
     | 
| 
       1641 
     | 
    
         
            -
                    new_content[section]['items'] = []
         
     | 
| 
       1642 
     | 
    
         
            -
                    moved_items = []
         
     | 
| 
      
 1336 
     | 
    
         
            +
                    item.title.add_tags!([tag, 'done'], remove: true)
         
     | 
| 
      
 1337 
     | 
    
         
            +
                    item.tag('done', value: opt[:back].strftime('%F %R'))
         
     | 
| 
       1643 
1338 
     | 
    
         | 
| 
       1644 
     | 
    
         
            -
                     
     | 
| 
      
 1339 
     | 
    
         
            +
                    found_items += 1
         
     | 
| 
       1645 
1340 
     | 
    
         | 
| 
       1646 
     | 
    
         
            -
                    if  
     | 
| 
       1647 
     | 
    
         
            -
                       
     | 
| 
      
 1341 
     | 
    
         
            +
                    if opt[:archive] && opt[:section] != 'Archive'
         
     | 
| 
      
 1342 
     | 
    
         
            +
                      item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
         
     | 
| 
      
 1343 
     | 
    
         
            +
                      move_item(item, 'Archive', label: false)
         
     | 
| 
      
 1344 
     | 
    
         
            +
                      logger.count(:completed_archived)
         
     | 
| 
      
 1345 
     | 
    
         
            +
                      logger.debug('Completed/archived:', item.title)
         
     | 
| 
       1648 
1346 
     | 
    
         
             
                    else
         
     | 
| 
       1649 
     | 
    
         
            -
                       
     | 
| 
      
 1347 
     | 
    
         
            +
                      logger.count(:completed)
         
     | 
| 
      
 1348 
     | 
    
         
            +
                      logger.debug('Completed:', item.title)
         
     | 
| 
       1650 
1349 
     | 
    
         
             
                    end
         
     | 
| 
      
 1350 
     | 
    
         
            +
                  end
         
     | 
| 
       1651 
1351 
     | 
    
         | 
| 
       1652 
     | 
    
         
            -
             
     | 
| 
       1653 
     | 
    
         
            -
                                                   []
         
     | 
| 
       1654 
     | 
    
         
            -
                                                 else
         
     | 
| 
       1655 
     | 
    
         
            -
                                                   items[0..count - 1]
         
     | 
| 
       1656 
     | 
    
         
            -
                                                 end
         
     | 
| 
       1657 
     | 
    
         
            -
                    new_content[section]['items'] = moved_items
         
     | 
| 
      
 1352 
     | 
    
         
            +
                  logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
         
     | 
| 
       1658 
1353 
     | 
    
         | 
| 
       1659 
     | 
    
         
            -
             
     | 
| 
      
 1354 
     | 
    
         
            +
                  if opt[:new_item]
         
     | 
| 
      
 1355 
     | 
    
         
            +
                    title, note = format_input(opt[:new_item])
         
     | 
| 
      
 1356 
     | 
    
         
            +
                    note.add(opt[:note]) if opt[:note]
         
     | 
| 
      
 1357 
     | 
    
         
            +
                    title.tag!(tag)
         
     | 
| 
      
 1358 
     | 
    
         
            +
                    add_item(title.cap_first, opt[:section], { note: note, back: opt[:back] })
         
     | 
| 
       1660 
1359 
     | 
    
         
             
                  end
         
     | 
| 
      
 1360 
     | 
    
         
            +
             
     | 
| 
      
 1361 
     | 
    
         
            +
                  write(@doing_file)
         
     | 
| 
       1661 
1362 
     | 
    
         
             
                end
         
     | 
| 
       1662 
1363 
     | 
    
         | 
| 
       1663 
     | 
    
         
            -
                 
     | 
| 
      
 1364 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1365 
     | 
    
         
            +
                ## @brief      Write content to file or STDOUT
         
     | 
| 
      
 1366 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1367 
     | 
    
         
            +
                ## @param      file  (String) The filepath to write to
         
     | 
| 
      
 1368 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1369 
     | 
    
         
            +
                def write(file = nil, backup: true)
         
     | 
| 
      
 1370 
     | 
    
         
            +
                  Hooks.trigger :pre_write, self, file
         
     | 
| 
      
 1371 
     | 
    
         
            +
                  output = wrapped_content
         
     | 
| 
       1664 
1372 
     | 
    
         | 
| 
       1665 
     | 
    
         
            -
             
     | 
| 
       1666 
     | 
    
         
            -
             
     | 
| 
       1667 
     | 
    
         
            -
                   
     | 
| 
       1668 
     | 
    
         
            -
             
     | 
| 
       1669 
     | 
    
         
            -
             
     | 
| 
       1670 
     | 
    
         
            -
                   
     | 
| 
      
 1373 
     | 
    
         
            +
                  if file.nil?
         
     | 
| 
      
 1374 
     | 
    
         
            +
                    $stdout.puts output
         
     | 
| 
      
 1375 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1376 
     | 
    
         
            +
                    Util.write_to_file(file, output, backup: backup)
         
     | 
| 
      
 1377 
     | 
    
         
            +
                    run_after if @config.key?('run_after')
         
     | 
| 
      
 1378 
     | 
    
         
            +
                  end
         
     | 
| 
       1671 
1379 
     | 
    
         
             
                end
         
     | 
| 
       1672 
1380 
     | 
    
         | 
| 
       1673 
     | 
    
         
            -
                 
     | 
| 
       1674 
     | 
    
         
            -
             
     | 
| 
       1675 
     | 
    
         
            -
             
     | 
| 
       1676 
     | 
    
         
            -
              ##
         
     | 
| 
       1677 
     | 
    
         
            -
              ## @brief      Generate a menu of sections and allow user selection
         
     | 
| 
       1678 
     | 
    
         
            -
              ##
         
     | 
| 
       1679 
     | 
    
         
            -
              ## @return     (String) The selected section name
         
     | 
| 
       1680 
     | 
    
         
            -
              ##
         
     | 
| 
       1681 
     | 
    
         
            -
              def choose_section
         
     | 
| 
       1682 
     | 
    
         
            -
                choice = choose_from(sections.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
         
     | 
| 
       1683 
     | 
    
         
            -
                choice ? choice.strip : choice
         
     | 
| 
       1684 
     | 
    
         
            -
              end
         
     | 
| 
      
 1381 
     | 
    
         
            +
                def wrapped_content
         
     | 
| 
      
 1382 
     | 
    
         
            +
                  output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
         
     | 
| 
       1685 
1383 
     | 
    
         | 
| 
       1686 
     | 
    
         
            -
             
     | 
| 
       1687 
     | 
    
         
            -
             
     | 
| 
       1688 
     | 
    
         
            -
             
     | 
| 
       1689 
     | 
    
         
            -
             
     | 
| 
       1690 
     | 
    
         
            -
              ##
         
     | 
| 
       1691 
     | 
    
         
            -
              def views
         
     | 
| 
       1692 
     | 
    
         
            -
                @config.has_key?('views') ? @config['views'].keys : []
         
     | 
| 
       1693 
     | 
    
         
            -
              end
         
     | 
| 
      
 1384 
     | 
    
         
            +
                  @content.each do |title, section|
         
     | 
| 
      
 1385 
     | 
    
         
            +
                    output += "#{section[:original]}\n"
         
     | 
| 
      
 1386 
     | 
    
         
            +
                    output += list_section({ section: title, template: "\t- %date | %title%t2note", highlight: false, wrap_width: 0 })
         
     | 
| 
      
 1387 
     | 
    
         
            +
                  end
         
     | 
| 
       1694 
1388 
     | 
    
         | 
| 
       1695 
     | 
    
         
            -
             
     | 
| 
       1696 
     | 
    
         
            -
             
     | 
| 
       1697 
     | 
    
         
            -
              ##
         
     | 
| 
       1698 
     | 
    
         
            -
              ## @return     (String) The selected view name
         
     | 
| 
       1699 
     | 
    
         
            -
              ##
         
     | 
| 
       1700 
     | 
    
         
            -
              def choose_view
         
     | 
| 
       1701 
     | 
    
         
            -
                choice = choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
         
     | 
| 
       1702 
     | 
    
         
            -
                choice ? choice.strip : choice
         
     | 
| 
       1703 
     | 
    
         
            -
              end
         
     | 
| 
      
 1389 
     | 
    
         
            +
                  output + @other_content_bottom.join("\n") unless @other_content_bottom.nil?
         
     | 
| 
      
 1390 
     | 
    
         
            +
                end
         
     | 
| 
       1704 
1391 
     | 
    
         | 
| 
       1705 
     | 
    
         
            -
             
     | 
| 
       1706 
     | 
    
         
            -
             
     | 
| 
       1707 
     | 
    
         
            -
             
     | 
| 
       1708 
     | 
    
         
            -
             
     | 
| 
       1709 
     | 
    
         
            -
             
     | 
| 
       1710 
     | 
    
         
            -
             
     | 
| 
       1711 
     | 
    
         
            -
             
     | 
| 
      
 1392 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1393 
     | 
    
         
            +
                ## @brief      Restore a backed up version of a file
         
     | 
| 
      
 1394 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1395 
     | 
    
         
            +
                ## @param      file  (String) The filepath to restore
         
     | 
| 
      
 1396 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1397 
     | 
    
         
            +
                def restore_backup(file)
         
     | 
| 
      
 1398 
     | 
    
         
            +
                  if File.exist?("#{file}~")
         
     | 
| 
      
 1399 
     | 
    
         
            +
                    FileUtils.cp("#{file}~", file)
         
     | 
| 
      
 1400 
     | 
    
         
            +
                    logger.warn('File update:', "Restored #{file.sub(/^#{@user_home}/, '~')}")
         
     | 
| 
      
 1401 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1402 
     | 
    
         
            +
                    logger.error('Restore error:', 'No backup file found')
         
     | 
| 
      
 1403 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1404 
     | 
    
         
            +
                end
         
     | 
| 
       1712 
1405 
     | 
    
         | 
| 
       1713 
     | 
    
         
            -
                 
     | 
| 
       1714 
     | 
    
         
            -
             
     | 
| 
      
 1406 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1407 
     | 
    
         
            +
                ## @brief      Rename doing file with date and start fresh one
         
     | 
| 
      
 1408 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1409 
     | 
    
         
            +
                def rotate(opt = {})
         
     | 
| 
      
 1410 
     | 
    
         
            +
                  keep = opt[:keep] || 0
         
     | 
| 
      
 1411 
     | 
    
         
            +
                  tags = []
         
     | 
| 
      
 1412 
     | 
    
         
            +
                  tags.concat(opt[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if opt[:tag]
         
     | 
| 
      
 1413 
     | 
    
         
            +
                  bool  = opt[:bool] || :and
         
     | 
| 
      
 1414 
     | 
    
         
            +
                  sect = opt[:section] !~ /^all$/i ? guess_section(opt[:section]) : 'all'
         
     | 
| 
       1715 
1415 
     | 
    
         | 
| 
       1716 
     | 
    
         
            -
             
     | 
| 
       1717 
     | 
    
         
            -
             
     | 
| 
       1718 
     | 
    
         
            -
              ##             This is a fucking mess. I mean, Jesus Christ.
         
     | 
| 
       1719 
     | 
    
         
            -
              ##
         
     | 
| 
       1720 
     | 
    
         
            -
              ## @param      opt   (Hash) Additional Options
         
     | 
| 
       1721 
     | 
    
         
            -
              ##
         
     | 
| 
       1722 
     | 
    
         
            -
              def list_section(opt = {})
         
     | 
| 
       1723 
     | 
    
         
            -
                opt[:count] ||= 0
         
     | 
| 
       1724 
     | 
    
         
            -
                count = opt[:count] - 1
         
     | 
| 
       1725 
     | 
    
         
            -
                opt[:age] ||= 'newest'
         
     | 
| 
       1726 
     | 
    
         
            -
                opt[:date_filter] ||= []
         
     | 
| 
       1727 
     | 
    
         
            -
                opt[:format] ||= @default_date_format
         
     | 
| 
       1728 
     | 
    
         
            -
                opt[:only_timed] ||= false
         
     | 
| 
       1729 
     | 
    
         
            -
                opt[:order] ||= 'desc'
         
     | 
| 
       1730 
     | 
    
         
            -
                opt[:search] ||= false
         
     | 
| 
       1731 
     | 
    
         
            -
                opt[:section] ||= nil
         
     | 
| 
       1732 
     | 
    
         
            -
                opt[:sort_tags] ||= false
         
     | 
| 
       1733 
     | 
    
         
            -
                opt[:tag_filter] ||= false
         
     | 
| 
       1734 
     | 
    
         
            -
                opt[:tag_order] ||= 'asc'
         
     | 
| 
       1735 
     | 
    
         
            -
                opt[:tags_color] ||= false
         
     | 
| 
       1736 
     | 
    
         
            -
                opt[:template] ||= @default_template
         
     | 
| 
       1737 
     | 
    
         
            -
                opt[:times] ||= false
         
     | 
| 
       1738 
     | 
    
         
            -
                opt[:today] ||= false
         
     | 
| 
       1739 
     | 
    
         
            -
                opt[:totals] ||= false
         
     | 
| 
       1740 
     | 
    
         
            -
             
     | 
| 
       1741 
     | 
    
         
            -
                # opt[:highlight] ||= true
         
     | 
| 
       1742 
     | 
    
         
            -
                section = ''
         
     | 
| 
       1743 
     | 
    
         
            -
                is_single = true
         
     | 
| 
       1744 
     | 
    
         
            -
                if opt[:section].nil?
         
     | 
| 
       1745 
     | 
    
         
            -
                  section = choose_section
         
     | 
| 
       1746 
     | 
    
         
            -
                  opt[:section] = @content[section]
         
     | 
| 
       1747 
     | 
    
         
            -
                elsif opt[:section].instance_of?(String)
         
     | 
| 
       1748 
     | 
    
         
            -
                  if opt[:section] =~ /^all$/i
         
     | 
| 
       1749 
     | 
    
         
            -
                    is_single = false
         
     | 
| 
       1750 
     | 
    
         
            -
                    combined = { 'items' => [] }
         
     | 
| 
       1751 
     | 
    
         
            -
                    @content.each do |_k, v|
         
     | 
| 
       1752 
     | 
    
         
            -
                      combined['items'] += v['items']
         
     | 
| 
       1753 
     | 
    
         
            -
                    end
         
     | 
| 
       1754 
     | 
    
         
            -
                    section = if opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
         
     | 
| 
       1755 
     | 
    
         
            -
                                opt[:tag_filter]['tags'].map do |tag|
         
     | 
| 
       1756 
     | 
    
         
            -
                                  "@#{tag}"
         
     | 
| 
       1757 
     | 
    
         
            -
                                end.join(' + ')
         
     | 
| 
       1758 
     | 
    
         
            -
                              else
         
     | 
| 
       1759 
     | 
    
         
            -
                                'doing'
         
     | 
| 
       1760 
     | 
    
         
            -
                              end
         
     | 
| 
       1761 
     | 
    
         
            -
                    opt[:section] = combined
         
     | 
| 
      
 1416 
     | 
    
         
            +
                  if sect =~ /^all$/i
         
     | 
| 
      
 1417 
     | 
    
         
            +
                    all_sections = sections.dup
         
     | 
| 
       1762 
1418 
     | 
    
         
             
                  else
         
     | 
| 
       1763 
     | 
    
         
            -
                     
     | 
| 
       1764 
     | 
    
         
            -
                    opt[:section] = @content[section]
         
     | 
| 
      
 1419 
     | 
    
         
            +
                    all_sections = [sect]
         
     | 
| 
       1765 
1420 
     | 
    
         
             
                  end
         
     | 
| 
       1766 
     | 
    
         
            -
                end
         
     | 
| 
       1767 
1421 
     | 
    
         | 
| 
       1768 
     | 
    
         
            -
             
     | 
| 
      
 1422 
     | 
    
         
            +
                  counter = 0
         
     | 
| 
      
 1423 
     | 
    
         
            +
                  new_content = {}
         
     | 
| 
      
 1424 
     | 
    
         
            +
             
     | 
| 
       1769 
1425 
     | 
    
         | 
| 
       1770 
     | 
    
         
            -
             
     | 
| 
      
 1426 
     | 
    
         
            +
                  all_sections.each do |section|
         
     | 
| 
      
 1427 
     | 
    
         
            +
                    items = @content[section][:items].dup
         
     | 
| 
      
 1428 
     | 
    
         
            +
                    new_content[section] = {}
         
     | 
| 
      
 1429 
     | 
    
         
            +
                    new_content[section][:original] = @content[section][:original]
         
     | 
| 
      
 1430 
     | 
    
         
            +
                    new_content[section][:items] = []
         
     | 
| 
       1771 
1431 
     | 
    
         | 
| 
       1772 
     | 
    
         
            -
             
     | 
| 
       1773 
     | 
    
         
            -
             
     | 
| 
       1774 
     | 
    
         
            -
             
     | 
| 
       1775 
     | 
    
         
            -
             
     | 
| 
       1776 
     | 
    
         
            -
             
     | 
| 
       1777 
     | 
    
         
            -
                       
     | 
| 
      
 1432 
     | 
    
         
            +
                    moved_items = []
         
     | 
| 
      
 1433 
     | 
    
         
            +
                    if !tags.empty? || opt[:search] || opt[:before]
         
     | 
| 
      
 1434 
     | 
    
         
            +
                      if opt[:before]
         
     | 
| 
      
 1435 
     | 
    
         
            +
                        time_string = opt[:before]
         
     | 
| 
      
 1436 
     | 
    
         
            +
                        cutoff = chronify(time_string, guess: :begin)
         
     | 
| 
      
 1437 
     | 
    
         
            +
                      end
         
     | 
| 
      
 1438 
     | 
    
         
            +
             
     | 
| 
      
 1439 
     | 
    
         
            +
                      items.delete_if do |item|
         
     | 
| 
      
 1440 
     | 
    
         
            +
                        if ((!tags.empty? && item.tags?(tags, bool)) || (opt[:search] && item.search(opt[:search].to_s)) || (opt[:before] && item.date < cutoff))
         
     | 
| 
      
 1441 
     | 
    
         
            +
                          moved_items.push(item)
         
     | 
| 
      
 1442 
     | 
    
         
            +
                          counter += 1
         
     | 
| 
      
 1443 
     | 
    
         
            +
                          true
         
     | 
| 
      
 1444 
     | 
    
         
            +
                        else
         
     | 
| 
      
 1445 
     | 
    
         
            +
                          false
         
     | 
| 
      
 1446 
     | 
    
         
            +
                        end
         
     | 
| 
      
 1447 
     | 
    
         
            +
                      end
         
     | 
| 
      
 1448 
     | 
    
         
            +
                      @content[section][:items] = items
         
     | 
| 
      
 1449 
     | 
    
         
            +
                      new_content[section][:items] = moved_items
         
     | 
| 
      
 1450 
     | 
    
         
            +
                      logger.warn('Rotated:', "#{moved_items.length} items from #{section}")
         
     | 
| 
       1778 
1451 
     | 
    
         
             
                    else
         
     | 
| 
       1779 
     | 
    
         
            -
                       
     | 
| 
       1780 
     | 
    
         
            -
             
     | 
| 
       1781 
     | 
    
         
            -
                  end
         
     | 
| 
       1782 
     | 
    
         
            -
                end
         
     | 
| 
      
 1452 
     | 
    
         
            +
                      new_content[section][:items] = []
         
     | 
| 
      
 1453 
     | 
    
         
            +
                      moved_items = []
         
     | 
| 
       1783 
1454 
     | 
    
         | 
| 
       1784 
     | 
    
         
            -
             
     | 
| 
       1785 
     | 
    
         
            -
                  items.select! { |item| item.has_tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool']) }
         
     | 
| 
       1786 
     | 
    
         
            -
                end
         
     | 
| 
      
 1455 
     | 
    
         
            +
                      count = items.length < keep ? items.length : keep
         
     | 
| 
       1787 
1456 
     | 
    
         | 
| 
       1788 
     | 
    
         
            -
             
     | 
| 
       1789 
     | 
    
         
            -
             
     | 
| 
       1790 
     | 
    
         
            -
             
     | 
| 
      
 1457 
     | 
    
         
            +
                      if items.count > count
         
     | 
| 
      
 1458 
     | 
    
         
            +
                        moved_items.concat(items[count..-1])
         
     | 
| 
      
 1459 
     | 
    
         
            +
                      else
         
     | 
| 
      
 1460 
     | 
    
         
            +
                        moved_items.concat(items)
         
     | 
| 
      
 1461 
     | 
    
         
            +
                      end
         
     | 
| 
       1791 
1462 
     | 
    
         | 
| 
       1792 
     | 
    
         
            -
             
     | 
| 
       1793 
     | 
    
         
            -
             
     | 
| 
       1794 
     | 
    
         
            -
             
     | 
| 
      
 1463 
     | 
    
         
            +
                      @content[section][:items] = if count.zero?
         
     | 
| 
      
 1464 
     | 
    
         
            +
                                                     []
         
     | 
| 
      
 1465 
     | 
    
         
            +
                                                   else
         
     | 
| 
      
 1466 
     | 
    
         
            +
                                                     items[0..count - 1]
         
     | 
| 
      
 1467 
     | 
    
         
            +
                                                   end
         
     | 
| 
      
 1468 
     | 
    
         
            +
                      new_content[section][:items] = moved_items
         
     | 
| 
      
 1469 
     | 
    
         
            +
             
     | 
| 
      
 1470 
     | 
    
         
            +
                      logger.warn('Rotated:', "#{items.length - count} items from #{section}")
         
     | 
| 
      
 1471 
     | 
    
         
            +
                    end
         
     | 
| 
       1795 
1472 
     | 
    
         
             
                  end
         
     | 
| 
       1796 
     | 
    
         
            -
                end
         
     | 
| 
       1797 
1473 
     | 
    
         | 
| 
       1798 
     | 
    
         
            -
             
     | 
| 
       1799 
     | 
    
         
            -
             
     | 
| 
       1800 
     | 
    
         
            -
                   
     | 
| 
       1801 
     | 
    
         
            -
                   
     | 
| 
       1802 
     | 
    
         
            -
             
     | 
| 
       1803 
     | 
    
         
            -
                     
     | 
| 
      
 1474 
     | 
    
         
            +
                  write(@doing_file)
         
     | 
| 
      
 1475 
     | 
    
         
            +
             
     | 
| 
      
 1476 
     | 
    
         
            +
                  file = @doing_file.sub(/(\.\w+)$/, "_#{Time.now.strftime('%Y-%m-%d')}\\1")
         
     | 
| 
      
 1477 
     | 
    
         
            +
                  if File.exist?(file)
         
     | 
| 
      
 1478 
     | 
    
         
            +
                    init_doing_file(file)
         
     | 
| 
      
 1479 
     | 
    
         
            +
                    @content.deep_merge(new_content)
         
     | 
| 
      
 1480 
     | 
    
         
            +
                    logger.warn('File update:', "Added entries to existing file: #{file}")
         
     | 
| 
      
 1481 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1482 
     | 
    
         
            +
                    @content = new_content
         
     | 
| 
      
 1483 
     | 
    
         
            +
                    logger.warn('File update:', "Created new file: #{file}")
         
     | 
| 
       1804 
1484 
     | 
    
         
             
                  end
         
     | 
| 
       1805 
     | 
    
         
            -
                end
         
     | 
| 
       1806 
1485 
     | 
    
         | 
| 
       1807 
     | 
    
         
            -
             
     | 
| 
       1808 
     | 
    
         
            -
             
     | 
| 
       1809 
     | 
    
         
            -
             
     | 
| 
       1810 
     | 
    
         
            -
             
     | 
| 
       1811 
     | 
    
         
            -
                   
     | 
| 
       1812 
     | 
    
         
            -
             
     | 
| 
      
 1486 
     | 
    
         
            +
                  write(file, backup: false)
         
     | 
| 
      
 1487 
     | 
    
         
            +
                end
         
     | 
| 
      
 1488 
     | 
    
         
            +
             
     | 
| 
      
 1489 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1490 
     | 
    
         
            +
                ## @brief      Generate a menu of sections and allow user selection
         
     | 
| 
      
 1491 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1492 
     | 
    
         
            +
                ## @return     (String) The selected section name
         
     | 
| 
      
 1493 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1494 
     | 
    
         
            +
                def choose_section
         
     | 
| 
      
 1495 
     | 
    
         
            +
                  choice = choose_from(sections.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
         
     | 
| 
      
 1496 
     | 
    
         
            +
                  choice ? choice.strip : choice
         
     | 
| 
      
 1497 
     | 
    
         
            +
                end
         
     | 
| 
      
 1498 
     | 
    
         
            +
             
     | 
| 
      
 1499 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1500 
     | 
    
         
            +
                ## @brief      List available views
         
     | 
| 
      
 1501 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1502 
     | 
    
         
            +
                ## @return     (Array) View names
         
     | 
| 
      
 1503 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1504 
     | 
    
         
            +
                def views
         
     | 
| 
      
 1505 
     | 
    
         
            +
                  @config.has_key?('views') ? @config['views'].keys : []
         
     | 
| 
      
 1506 
     | 
    
         
            +
                end
         
     | 
| 
      
 1507 
     | 
    
         
            +
             
     | 
| 
      
 1508 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1509 
     | 
    
         
            +
                ## @brief      Generate a menu of views and allow user selection
         
     | 
| 
      
 1510 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1511 
     | 
    
         
            +
                ## @return     (String) The selected view name
         
     | 
| 
      
 1512 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1513 
     | 
    
         
            +
                def choose_view
         
     | 
| 
      
 1514 
     | 
    
         
            +
                  choice = choose_from(views.sort, prompt: 'Choose a view > ', fzf_args: ['--height=60%'])
         
     | 
| 
      
 1515 
     | 
    
         
            +
                  choice ? choice.strip : choice
         
     | 
| 
      
 1516 
     | 
    
         
            +
                end
         
     | 
| 
      
 1517 
     | 
    
         
            +
             
     | 
| 
      
 1518 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1519 
     | 
    
         
            +
                ## @brief      Gets a view from configuration
         
     | 
| 
      
 1520 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1521 
     | 
    
         
            +
                ## @param      title  (String) The title of the view to retrieve
         
     | 
| 
      
 1522 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1523 
     | 
    
         
            +
                def get_view(title)
         
     | 
| 
      
 1524 
     | 
    
         
            +
                  return @config['views'][title] if @config['views'].has_key?(title)
         
     | 
| 
      
 1525 
     | 
    
         
            +
             
     | 
| 
      
 1526 
     | 
    
         
            +
                  false
         
     | 
| 
      
 1527 
     | 
    
         
            +
                end
         
     | 
| 
      
 1528 
     | 
    
         
            +
             
     | 
| 
      
 1529 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1530 
     | 
    
         
            +
                ## @brief      Display contents of a section based on options
         
     | 
| 
      
 1531 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1532 
     | 
    
         
            +
                ## @param      opt   (Hash) Additional Options
         
     | 
| 
      
 1533 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1534 
     | 
    
         
            +
                def list_section(opt = {})
         
     | 
| 
      
 1535 
     | 
    
         
            +
                  opt[:count] ||= 0
         
     | 
| 
      
 1536 
     | 
    
         
            +
                  opt[:age] ||= 'newest'
         
     | 
| 
      
 1537 
     | 
    
         
            +
                  opt[:format] ||= @config.dig('templates', 'default', 'date_format')
         
     | 
| 
      
 1538 
     | 
    
         
            +
                  opt[:order] ||= @config.dig('templates', 'default', 'order') || 'asc'
         
     | 
| 
      
 1539 
     | 
    
         
            +
                  opt[:tag_order] ||= 'asc'
         
     | 
| 
      
 1540 
     | 
    
         
            +
                  opt[:tags_color] ||= false
         
     | 
| 
      
 1541 
     | 
    
         
            +
                  opt[:template] ||= @config.dig('templates', 'default', 'template')
         
     | 
| 
      
 1542 
     | 
    
         
            +
             
     | 
| 
      
 1543 
     | 
    
         
            +
                  # opt[:highlight] ||= true
         
     | 
| 
      
 1544 
     | 
    
         
            +
                  title = ''
         
     | 
| 
      
 1545 
     | 
    
         
            +
                  is_single = true
         
     | 
| 
      
 1546 
     | 
    
         
            +
                  if opt[:section].nil?
         
     | 
| 
      
 1547 
     | 
    
         
            +
                    opt[:section] = choose_section
         
     | 
| 
      
 1548 
     | 
    
         
            +
                    title = opt[:section]
         
     | 
| 
      
 1549 
     | 
    
         
            +
                  elsif opt[:section].instance_of?(String)
         
     | 
| 
      
 1550 
     | 
    
         
            +
                    if opt[:section] =~ /^all$/i
         
     | 
| 
      
 1551 
     | 
    
         
            +
                      title = if opt[:page_title]
         
     | 
| 
      
 1552 
     | 
    
         
            +
                                opt[:page_title]
         
     | 
| 
      
 1553 
     | 
    
         
            +
                              elsif opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
         
     | 
| 
      
 1554 
     | 
    
         
            +
                                opt[:tag_filter]['tags'].map { |tag| "@#{tag}" }.join(' + ')
         
     | 
| 
      
 1555 
     | 
    
         
            +
                              else
         
     | 
| 
      
 1556 
     | 
    
         
            +
                                'doing'
         
     | 
| 
      
 1557 
     | 
    
         
            +
                              end
         
     | 
| 
      
 1558 
     | 
    
         
            +
                    else
         
     | 
| 
      
 1559 
     | 
    
         
            +
                      title = guess_section(opt[:section])
         
     | 
| 
      
 1560 
     | 
    
         
            +
                    end
         
     | 
| 
       1813 
1561 
     | 
    
         
             
                  end
         
     | 
| 
       1814 
     | 
    
         
            -
                end
         
     | 
| 
       1815 
1562 
     | 
    
         | 
| 
       1816 
     | 
    
         
            -
             
     | 
| 
       1817 
     | 
    
         
            -
                  items.delete_if do |item|
         
     | 
| 
       1818 
     | 
    
         
            -
                    item['date'] < Date.today.to_time
         
     | 
| 
       1819 
     | 
    
         
            -
                  end.reverse!
         
     | 
| 
       1820 
     | 
    
         
            -
                  section = Time.now.strftime('%A, %B %d')
         
     | 
| 
       1821 
     | 
    
         
            -
                elsif opt[:yesterday]
         
     | 
| 
       1822 
     | 
    
         
            -
                  items.delete_if do |item|
         
     | 
| 
       1823 
     | 
    
         
            -
                    item['date'] <= Date.today.prev_day.to_time or
         
     | 
| 
       1824 
     | 
    
         
            -
                      item['date'] >= Date.today.to_time
         
     | 
| 
       1825 
     | 
    
         
            -
                  end.reverse!
         
     | 
| 
       1826 
     | 
    
         
            -
                elsif opt[:age] =~ /oldest/i
         
     | 
| 
       1827 
     | 
    
         
            -
                  items = items[0..count]
         
     | 
| 
       1828 
     | 
    
         
            -
                else
         
     | 
| 
       1829 
     | 
    
         
            -
                  items = items.reverse[0..count]
         
     | 
| 
       1830 
     | 
    
         
            -
                end
         
     | 
| 
      
 1563 
     | 
    
         
            +
                  items = filter_items([], opt: opt).reverse
         
     | 
| 
       1831 
1564 
     | 
    
         | 
| 
       1832 
     | 
    
         
            -
             
     | 
| 
      
 1565 
     | 
    
         
            +
                  items.reverse! if opt[:order] =~ /^d/i
         
     | 
| 
       1833 
1566 
     | 
    
         | 
| 
       1834 
     | 
    
         
            -
                out = ''
         
     | 
| 
       1835 
1567 
     | 
    
         | 
| 
       1836 
     | 
    
         
            -
             
     | 
| 
      
 1568 
     | 
    
         
            +
                  if opt[:interactive]
         
     | 
| 
      
 1569 
     | 
    
         
            +
                    opt[:menu] = !opt[:force]
         
     | 
| 
      
 1570 
     | 
    
         
            +
                    opt[:query] = '' # opt[:search]
         
     | 
| 
      
 1571 
     | 
    
         
            +
                    opt[:multiple] = true
         
     | 
| 
      
 1572 
     | 
    
         
            +
                    selected = choose_from_items(items, opt, include_section: opt[:section] =~ /^all$/i )
         
     | 
| 
       1837 
1573 
     | 
    
         | 
| 
       1838 
     | 
    
         
            -
             
     | 
| 
       1839 
     | 
    
         
            -
             
     | 
| 
       1840 
     | 
    
         
            -
             
     | 
| 
       1841 
     | 
    
         
            -
                  items.each do |i|
         
     | 
| 
       1842 
     | 
    
         
            -
                    note = ''
         
     | 
| 
       1843 
     | 
    
         
            -
                    if i['note']
         
     | 
| 
       1844 
     | 
    
         
            -
                      arr = i['note'].map { |line| line.strip }.delete_if { |e| e =~ /^\s*$/ }
         
     | 
| 
       1845 
     | 
    
         
            -
                      note = arr.join("\n") unless arr.nil?
         
     | 
| 
       1846 
     | 
    
         
            -
                    end
         
     | 
| 
       1847 
     | 
    
         
            -
                    interval = get_interval(i, formatted: false) if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
         
     | 
| 
       1848 
     | 
    
         
            -
                    interval ||= 0
         
     | 
| 
       1849 
     | 
    
         
            -
                    output.push(CSV.generate_line([i['date'], i['title'], note, interval, i['section']]))
         
     | 
| 
       1850 
     | 
    
         
            -
                  end
         
     | 
| 
       1851 
     | 
    
         
            -
                  out = output.join('')
         
     | 
| 
       1852 
     | 
    
         
            -
                when /^(json|timeline)/i
         
     | 
| 
       1853 
     | 
    
         
            -
                  items_out = []
         
     | 
| 
       1854 
     | 
    
         
            -
                  max = items[-1]['date'].strftime('%F')
         
     | 
| 
       1855 
     | 
    
         
            -
                  min = items[0]['date'].strftime('%F')
         
     | 
| 
       1856 
     | 
    
         
            -
                  items.each_with_index do |i, index|
         
     | 
| 
       1857 
     | 
    
         
            -
                    if String.method_defined? :force_encoding
         
     | 
| 
       1858 
     | 
    
         
            -
                      title = i['title'].force_encoding('utf-8')
         
     | 
| 
       1859 
     | 
    
         
            -
                      note = i['note'].map { |line| line.force_encoding('utf-8').strip } if i['note']
         
     | 
| 
       1860 
     | 
    
         
            -
                    else
         
     | 
| 
       1861 
     | 
    
         
            -
                      title = i['title']
         
     | 
| 
       1862 
     | 
    
         
            -
                      note = i['note'].map { |line| line.strip } if i['note']
         
     | 
| 
       1863 
     | 
    
         
            -
                    end
         
     | 
| 
       1864 
     | 
    
         
            -
                    if i['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
         
     | 
| 
       1865 
     | 
    
         
            -
                      end_date = Time.parse(Regexp.last_match(1))
         
     | 
| 
       1866 
     | 
    
         
            -
                      interval = get_interval(i, formatted: false)
         
     | 
| 
       1867 
     | 
    
         
            -
                    end
         
     | 
| 
       1868 
     | 
    
         
            -
                    end_date ||= ''
         
     | 
| 
       1869 
     | 
    
         
            -
                    interval ||= 0
         
     | 
| 
       1870 
     | 
    
         
            -
                    note ||= ''
         
     | 
| 
       1871 
     | 
    
         
            -
             
     | 
| 
       1872 
     | 
    
         
            -
                    tags = []
         
     | 
| 
       1873 
     | 
    
         
            -
                    attributes = {}
         
     | 
| 
       1874 
     | 
    
         
            -
                    skip_tags = %w[meanwhile done cancelled flagged]
         
     | 
| 
       1875 
     | 
    
         
            -
                    i['title'].scan(/@([^(\s]+)(?:\((.*?)\))?/).each do |tag|
         
     | 
| 
       1876 
     | 
    
         
            -
                      tags.push(tag[0]) unless skip_tags.include?(tag[0])
         
     | 
| 
       1877 
     | 
    
         
            -
                      attributes[tag[0]] = tag[1] if tag[1]
         
     | 
| 
      
 1574 
     | 
    
         
            +
                    if selected.empty?
         
     | 
| 
      
 1575 
     | 
    
         
            +
                      logger.debug('Skipped:', 'No selection')
         
     | 
| 
      
 1576 
     | 
    
         
            +
                      return
         
     | 
| 
       1878 
1577 
     | 
    
         
             
                    end
         
     | 
| 
       1879 
1578 
     | 
    
         | 
| 
       1880 
     | 
    
         
            -
                     
     | 
| 
      
 1579 
     | 
    
         
            +
                    act_on(selected, opt)
         
     | 
| 
      
 1580 
     | 
    
         
            +
                    return
         
     | 
| 
      
 1581 
     | 
    
         
            +
                  end
         
     | 
| 
       1881 
1582 
     | 
    
         | 
| 
       1882 
     | 
    
         
            -
                      i = {
         
     | 
| 
       1883 
     | 
    
         
            -
                        date: i['date'],
         
     | 
| 
       1884 
     | 
    
         
            -
                        end_date: end_date,
         
     | 
| 
       1885 
     | 
    
         
            -
                        title: title.strip, #+ " #{note}"
         
     | 
| 
       1886 
     | 
    
         
            -
                        note: note.instance_of?(Array) ? note.map(&:strip).join("\n") : note,
         
     | 
| 
       1887 
     | 
    
         
            -
                        time: '%02d:%02d:%02d' % fmt_time(interval),
         
     | 
| 
       1888 
     | 
    
         
            -
                        tags: tags
         
     | 
| 
       1889 
     | 
    
         
            -
                      }
         
     | 
| 
       1890 
1583 
     | 
    
         | 
| 
       1891 
     | 
    
         
            -
             
     | 
| 
      
 1584 
     | 
    
         
            +
                  opt[:output] ||= 'template'
         
     | 
| 
       1892 
1585 
     | 
    
         | 
| 
       1893 
     | 
    
         
            -
             
     | 
| 
      
 1586 
     | 
    
         
            +
                  opt[:wrap_width] ||= @config['templates']['default']['wrap_width']
         
     | 
| 
       1894 
1587 
     | 
    
         | 
| 
       1895 
     | 
    
         
            -
             
     | 
| 
       1896 
     | 
    
         
            -
             
     | 
| 
       1897 
     | 
    
         
            -
                        'id' => index + 1,
         
     | 
| 
       1898 
     | 
    
         
            -
                        'content' => title.strip, #+ " #{note}"
         
     | 
| 
       1899 
     | 
    
         
            -
                        'title' => title.strip + " (#{'%02d:%02d:%02d' % fmt_time(interval)})",
         
     | 
| 
       1900 
     | 
    
         
            -
                        'start' => i['date'].strftime('%F %T'),
         
     | 
| 
       1901 
     | 
    
         
            -
                        'type' => 'point'
         
     | 
| 
       1902 
     | 
    
         
            -
                      }
         
     | 
| 
      
 1588 
     | 
    
         
            +
                  output(items, title, is_single, opt)
         
     | 
| 
      
 1589 
     | 
    
         
            +
                end
         
     | 
| 
       1903 
1590 
     | 
    
         | 
| 
       1904 
     | 
    
         
            -
             
     | 
| 
       1905 
     | 
    
         
            -
             
     | 
| 
       1906 
     | 
    
         
            -
             
     | 
| 
       1907 
     | 
    
         
            -
             
     | 
| 
       1908 
     | 
    
         
            -
             
     | 
| 
       1909 
     | 
    
         
            -
             
     | 
| 
      
 1591 
     | 
    
         
            +
                def output(items, title, is_single, opt = {})
         
     | 
| 
      
 1592 
     | 
    
         
            +
                  out = nil
         
     | 
| 
      
 1593 
     | 
    
         
            +
             
     | 
| 
      
 1594 
     | 
    
         
            +
                  raise Errors::InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
         
     | 
| 
      
 1595 
     | 
    
         
            +
             
     | 
| 
      
 1596 
     | 
    
         
            +
                  export_options = { page_title: title, is_single: is_single, options: opt }
         
     | 
| 
      
 1597 
     | 
    
         
            +
             
     | 
| 
      
 1598 
     | 
    
         
            +
                  Plugins.plugins[:export].each do |_, options|
         
     | 
| 
      
 1599 
     | 
    
         
            +
                    next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
         
     | 
| 
      
 1600 
     | 
    
         
            +
             
     | 
| 
      
 1601 
     | 
    
         
            +
                    out = options[:class].render(self, items, variables: export_options)
         
     | 
| 
      
 1602 
     | 
    
         
            +
                    break
         
     | 
| 
       1910 
1603 
     | 
    
         
             
                  end
         
     | 
| 
       1911 
     | 
    
         
            -
                  if opt[:output] == 'json'
         
     | 
| 
       1912 
     | 
    
         
            -
                    puts JSON.pretty_generate({
         
     | 
| 
       1913 
     | 
    
         
            -
                      'section' => section,
         
     | 
| 
       1914 
     | 
    
         
            -
                      'items' => items_out,
         
     | 
| 
       1915 
     | 
    
         
            -
                      'timers' => tag_times(format: :json, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order])
         
     | 
| 
       1916 
     | 
    
         
            -
                    })
         
     | 
| 
       1917 
     | 
    
         
            -
                  elsif opt[:output] == 'timeline'
         
     | 
| 
       1918 
     | 
    
         
            -
                    template = <<~EOTEMPLATE
         
     | 
| 
       1919 
     | 
    
         
            -
                                <!doctype html>
         
     | 
| 
       1920 
     | 
    
         
            -
                                <html>
         
     | 
| 
       1921 
     | 
    
         
            -
                                <head>
         
     | 
| 
       1922 
     | 
    
         
            -
                                  <link href="https://unpkg.com/vis-timeline@7.4.9/dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
         
     | 
| 
       1923 
     | 
    
         
            -
                                  <script src="https://unpkg.com/vis-timeline@7.4.9/dist/vis-timeline-graph2d.min.js"></script>
         
     | 
| 
       1924 
     | 
    
         
            -
                                </head>
         
     | 
| 
       1925 
     | 
    
         
            -
                                <body>
         
     | 
| 
       1926 
     | 
    
         
            -
                                  <div id="mytimeline"></div>
         
     | 
| 
       1927 
     | 
    
         
            -
                      #{'          '}
         
     | 
| 
       1928 
     | 
    
         
            -
                                  <script type="text/javascript">
         
     | 
| 
       1929 
     | 
    
         
            -
                                    // DOM element where the Timeline will be attached
         
     | 
| 
       1930 
     | 
    
         
            -
                                    var container = document.getElementById('mytimeline');
         
     | 
| 
       1931 
     | 
    
         
            -
                      #{'          '}
         
     | 
| 
       1932 
     | 
    
         
            -
                                    // Create a DataSet with data (enables two way data binding)
         
     | 
| 
       1933 
     | 
    
         
            -
                                    var data = new vis.DataSet(#{items_out.to_json});
         
     | 
| 
       1934 
     | 
    
         
            -
                      #{'          '}
         
     | 
| 
       1935 
     | 
    
         
            -
                                    // Configuration for the Timeline
         
     | 
| 
       1936 
     | 
    
         
            -
                                    var options = {
         
     | 
| 
       1937 
     | 
    
         
            -
                                      width: '100%',
         
     | 
| 
       1938 
     | 
    
         
            -
                                      height: '800px',
         
     | 
| 
       1939 
     | 
    
         
            -
                                      margin: {
         
     | 
| 
       1940 
     | 
    
         
            -
                                        item: 20
         
     | 
| 
       1941 
     | 
    
         
            -
                                      },
         
     | 
| 
       1942 
     | 
    
         
            -
                                      stack: true,
         
     | 
| 
       1943 
     | 
    
         
            -
                                      min: '#{min}',
         
     | 
| 
       1944 
     | 
    
         
            -
                                      max: '#{max}'
         
     | 
| 
       1945 
     | 
    
         
            -
                                    };
         
     | 
| 
       1946 
     | 
    
         
            -
                      #{'          '}
         
     | 
| 
       1947 
     | 
    
         
            -
                                    // Create a Timeline
         
     | 
| 
       1948 
     | 
    
         
            -
                                    var timeline = new vis.Timeline(container, data, options);
         
     | 
| 
       1949 
     | 
    
         
            -
                                  </script>
         
     | 
| 
       1950 
     | 
    
         
            -
                                </body>
         
     | 
| 
       1951 
     | 
    
         
            -
                                </html>
         
     | 
| 
       1952 
     | 
    
         
            -
                    EOTEMPLATE
         
     | 
| 
       1953 
     | 
    
         
            -
                    return template
         
     | 
| 
       1954 
     | 
    
         
            -
                  end
         
     | 
| 
       1955 
     | 
    
         
            -
                when /^html$/i
         
     | 
| 
       1956 
     | 
    
         
            -
                  page_title = section
         
     | 
| 
       1957 
     | 
    
         
            -
                  items_out = []
         
     | 
| 
       1958 
     | 
    
         
            -
                  items.each do |i|
         
     | 
| 
       1959 
     | 
    
         
            -
                    # if i.has_key?('note')
         
     | 
| 
       1960 
     | 
    
         
            -
                    #   note = '<span class="note">' + i['note'].map{|n| n.strip }.join('<br>') + '</span>'
         
     | 
| 
       1961 
     | 
    
         
            -
                    # else
         
     | 
| 
       1962 
     | 
    
         
            -
                    #   note = ''
         
     | 
| 
       1963 
     | 
    
         
            -
                    # end
         
     | 
| 
       1964 
     | 
    
         
            -
                    if String.method_defined? :force_encoding
         
     | 
| 
       1965 
     | 
    
         
            -
                      title = i['title'].force_encoding('utf-8').link_urls
         
     | 
| 
       1966 
     | 
    
         
            -
                      note = i['note'].map { |line| line.force_encoding('utf-8').strip.link_urls } if i['note']
         
     | 
| 
       1967 
     | 
    
         
            -
                    else
         
     | 
| 
       1968 
     | 
    
         
            -
                      title = i['title'].link_urls
         
     | 
| 
       1969 
     | 
    
         
            -
                      note = i['note'].map { |line| line.strip.link_urls } if i['note']
         
     | 
| 
       1970 
     | 
    
         
            -
                    end
         
     | 
| 
       1971 
1604 
     | 
    
         | 
| 
       1972 
     | 
    
         
            -
             
     | 
| 
       1973 
     | 
    
         
            -
             
     | 
| 
      
 1605 
     | 
    
         
            +
                  out
         
     | 
| 
      
 1606 
     | 
    
         
            +
                end
         
     | 
| 
       1974 
1607 
     | 
    
         | 
| 
       1975 
     | 
    
         
            -
             
     | 
| 
       1976 
     | 
    
         
            -
             
     | 
| 
       1977 
     | 
    
         
            -
             
     | 
| 
       1978 
     | 
    
         
            -
             
     | 
| 
       1979 
     | 
    
         
            -
             
     | 
| 
       1980 
     | 
    
         
            -
             
     | 
| 
       1981 
     | 
    
         
            -
             
     | 
| 
      
 1608 
     | 
    
         
            +
                def load_plugins
         
     | 
| 
      
 1609 
     | 
    
         
            +
                  if @config.key?('plugins') && @config['plugins']['plugin_path']
         
     | 
| 
      
 1610 
     | 
    
         
            +
                    add_dir = @config['plugins']['plugin_path']
         
     | 
| 
      
 1611 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1612 
     | 
    
         
            +
                    add_dir = File.join(@user_home, '.config', 'doing', 'plugins')
         
     | 
| 
      
 1613 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 1614 
     | 
    
         
            +
                      FileUtils.mkdir_p(add_dir) if add_dir
         
     | 
| 
      
 1615 
     | 
    
         
            +
                    rescue
         
     | 
| 
      
 1616 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 1617 
     | 
    
         
            +
                    end
         
     | 
| 
       1982 
1618 
     | 
    
         
             
                  end
         
     | 
| 
       1983 
1619 
     | 
    
         | 
| 
       1984 
     | 
    
         
            -
                   
     | 
| 
       1985 
     | 
    
         
            -
             
     | 
| 
       1986 
     | 
    
         
            -
                             else
         
     | 
| 
       1987 
     | 
    
         
            -
                               haml_template
         
     | 
| 
       1988 
     | 
    
         
            -
                             end
         
     | 
| 
      
 1620 
     | 
    
         
            +
                  Plugins.load_plugins(add_dir)
         
     | 
| 
      
 1621 
     | 
    
         
            +
                end
         
     | 
| 
       1989 
1622 
     | 
    
         | 
| 
       1990 
     | 
    
         
            -
             
     | 
| 
       1991 
     | 
    
         
            -
             
     | 
| 
       1992 
     | 
    
         
            -
             
     | 
| 
       1993 
     | 
    
         
            -
             
     | 
| 
       1994 
     | 
    
         
            -
             
     | 
| 
      
 1623 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1624 
     | 
    
         
            +
                ## @brief      Move entries from a section to Archive or other specified
         
     | 
| 
      
 1625 
     | 
    
         
            +
                ##             section
         
     | 
| 
      
 1626 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1627 
     | 
    
         
            +
                ## @param      section      (String) The source section
         
     | 
| 
      
 1628 
     | 
    
         
            +
                ## @param      options      (Hash) Options
         
     | 
| 
      
 1629 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1630 
     | 
    
         
            +
                def archive(section = @config['current_section'], options = {})
         
     | 
| 
      
 1631 
     | 
    
         
            +
                  count       = options[:keep] || 0
         
     | 
| 
      
 1632 
     | 
    
         
            +
                  destination = options[:destination] || 'Archive'
         
     | 
| 
      
 1633 
     | 
    
         
            +
                  tags        = options[:tags] || []
         
     | 
| 
      
 1634 
     | 
    
         
            +
                  bool        = options[:bool] || :and
         
     | 
| 
       1995 
1635 
     | 
    
         | 
| 
       1996 
     | 
    
         
            -
                   
     | 
| 
       1997 
     | 
    
         
            -
                   
     | 
| 
       1998 
     | 
    
         
            -
                   
     | 
| 
       1999 
     | 
    
         
            -
                                     { :@items => items_out, :@page_title => page_title, :@style => style, :@totals => totals })
         
     | 
| 
       2000 
     | 
    
         
            -
                when /^taskpaper$/i
         
     | 
| 
       2001 
     | 
    
         
            -
                  options = opt
         
     | 
| 
       2002 
     | 
    
         
            -
                  options[:highlight] = false
         
     | 
| 
       2003 
     | 
    
         
            -
                  options[:wrap_width] = 0
         
     | 
| 
       2004 
     | 
    
         
            -
                  options[:tags_color] = false
         
     | 
| 
       2005 
     | 
    
         
            -
                  options[:output] = nil
         
     | 
| 
       2006 
     | 
    
         
            -
                  options[:template] = '- %title @date(%date)%note'
         
     | 
| 
       2007 
     | 
    
         
            -
             
     | 
| 
       2008 
     | 
    
         
            -
                  out = list_section(options)
         
     | 
| 
       2009 
     | 
    
         
            -
                when /^(md|markdown|gfm)$/i
         
     | 
| 
       2010 
     | 
    
         
            -
                  page_title = section
         
     | 
| 
       2011 
     | 
    
         
            -
                  all_items = []
         
     | 
| 
       2012 
     | 
    
         
            -
                  items.each do |i|
         
     | 
| 
       2013 
     | 
    
         
            -
                    if String.method_defined? :force_encoding
         
     | 
| 
       2014 
     | 
    
         
            -
                      title = i['title'].force_encoding('utf-8').link_urls({format: :markdown})
         
     | 
| 
       2015 
     | 
    
         
            -
                      note = i['note'].map { |line| line.force_encoding('utf-8').strip.link_urls({format: :markdown}) } if i['note']
         
     | 
| 
       2016 
     | 
    
         
            -
                    else
         
     | 
| 
       2017 
     | 
    
         
            -
                      title = i['title'].link_urls({format: :markdown})
         
     | 
| 
       2018 
     | 
    
         
            -
                      note = i['note'].map { |line| line.strip.link_urls({format: :markdown}) } if i['note']
         
     | 
| 
       2019 
     | 
    
         
            -
                    end
         
     | 
| 
      
 1636 
     | 
    
         
            +
                  section = choose_section if section.nil? || section =~ /choose/i
         
     | 
| 
      
 1637 
     | 
    
         
            +
                  archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
         
     | 
| 
      
 1638 
     | 
    
         
            +
                  section = guess_section(section) unless archive_all
         
     | 
| 
       2020 
1639 
     | 
    
         | 
| 
       2021 
     | 
    
         
            -
             
     | 
| 
      
 1640 
     | 
    
         
            +
                  add_section('Archive') if destination =~ /^archive$/i && !sections.include?('Archive')
         
     | 
| 
       2022 
1641 
     | 
    
         | 
| 
       2023 
     | 
    
         
            -
             
     | 
| 
       2024 
     | 
    
         
            -
                    interval ||= false
         
     | 
| 
      
 1642 
     | 
    
         
            +
                  destination = guess_section(destination)
         
     | 
| 
       2025 
1643 
     | 
    
         | 
| 
       2026 
     | 
    
         
            -
             
     | 
| 
      
 1644 
     | 
    
         
            +
                  if sections.include?(destination) && (sections.include?(section) || archive_all)
         
     | 
| 
      
 1645 
     | 
    
         
            +
                    do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
         
     | 
| 
      
 1646 
     | 
    
         
            +
                    write(doing_file)
         
     | 
| 
      
 1647 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1648 
     | 
    
         
            +
                    raise Errors::InvalidArgument, 'Either source or destination does not exist'
         
     | 
| 
      
 1649 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1650 
     | 
    
         
            +
                end
         
     | 
| 
       2027 
1651 
     | 
    
         | 
| 
       2028 
     | 
    
         
            -
             
     | 
| 
       2029 
     | 
    
         
            -
             
     | 
| 
       2030 
     | 
    
         
            -
             
     | 
| 
       2031 
     | 
    
         
            -
             
     | 
| 
       2032 
     | 
    
         
            -
             
     | 
| 
       2033 
     | 
    
         
            -
                       
     | 
| 
       2034 
     | 
    
         
            -
             
     | 
| 
       2035 
     | 
    
         
            -
             
     | 
| 
       2036 
     | 
    
         
            -
             
     | 
| 
      
 1652 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1653 
     | 
    
         
            +
                ## @brief      Helper function, performs the actual archiving
         
     | 
| 
      
 1654 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1655 
     | 
    
         
            +
                ## @param      section      (String) The source section
         
     | 
| 
      
 1656 
     | 
    
         
            +
                ## @param      destination  (String) The destination section
         
     | 
| 
      
 1657 
     | 
    
         
            +
                ## @param      opt          (Hash) Additional Options
         
     | 
| 
      
 1658 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1659 
     | 
    
         
            +
                def do_archive(sect, destination, opt = {})
         
     | 
| 
      
 1660 
     | 
    
         
            +
                  count = opt[:count] || 0
         
     | 
| 
      
 1661 
     | 
    
         
            +
                  tags  = opt[:tags] || []
         
     | 
| 
      
 1662 
     | 
    
         
            +
                  bool  = opt[:bool] || :and
         
     | 
| 
      
 1663 
     | 
    
         
            +
                  label = opt[:label] || true
         
     | 
| 
      
 1664 
     | 
    
         
            +
             
     | 
| 
      
 1665 
     | 
    
         
            +
                  if sect =~ /^all$/i
         
     | 
| 
      
 1666 
     | 
    
         
            +
                    all_sections = sections.dup
         
     | 
| 
      
 1667 
     | 
    
         
            +
                    all_sections.delete(destination)
         
     | 
| 
      
 1668 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1669 
     | 
    
         
            +
                    all_sections = [sect]
         
     | 
| 
       2037 
1670 
     | 
    
         
             
                  end
         
     | 
| 
       2038 
1671 
     | 
    
         | 
| 
       2039 
     | 
    
         
            -
                   
     | 
| 
       2040 
     | 
    
         
            -
                               IO.read(File.expand_path(@config['html_template']['markdown']))
         
     | 
| 
       2041 
     | 
    
         
            -
                             else
         
     | 
| 
       2042 
     | 
    
         
            -
                               markdown_template
         
     | 
| 
       2043 
     | 
    
         
            -
                             end
         
     | 
| 
      
 1672 
     | 
    
         
            +
                  counter = 0
         
     | 
| 
       2044 
1673 
     | 
    
         | 
| 
       2045 
     | 
    
         
            -
                   
     | 
| 
      
 1674 
     | 
    
         
            +
                  all_sections.each do |section|
         
     | 
| 
      
 1675 
     | 
    
         
            +
                    items = @content[section][:items].dup
         
     | 
| 
       2046 
1676 
     | 
    
         | 
| 
       2047 
     | 
    
         
            -
             
     | 
| 
       2048 
     | 
    
         
            -
             
     | 
| 
       2049 
     | 
    
         
            -
             
     | 
| 
       2050 
     | 
    
         
            -
             
     | 
| 
       2051 
     | 
    
         
            -
             
     | 
| 
       2052 
     | 
    
         
            -
             
     | 
| 
       2053 
     | 
    
         
            -
                      flag = colors[@config['marker_color']]
         
     | 
| 
       2054 
     | 
    
         
            -
                      reset = colors['default']
         
     | 
| 
       2055 
     | 
    
         
            -
                    else
         
     | 
| 
       2056 
     | 
    
         
            -
                      flag = ''
         
     | 
| 
       2057 
     | 
    
         
            -
                      reset = ''
         
     | 
| 
       2058 
     | 
    
         
            -
                    end
         
     | 
| 
      
 1677 
     | 
    
         
            +
                    moved_items = []
         
     | 
| 
      
 1678 
     | 
    
         
            +
                    if !tags.empty? || opt[:search] || opt[:before]
         
     | 
| 
      
 1679 
     | 
    
         
            +
                      if opt[:before]
         
     | 
| 
      
 1680 
     | 
    
         
            +
                        time_string = opt[:before]
         
     | 
| 
      
 1681 
     | 
    
         
            +
                        cutoff = chronify(time_string, guess: :begin)
         
     | 
| 
      
 1682 
     | 
    
         
            +
                      end
         
     | 
| 
       2059 
1683 
     | 
    
         | 
| 
       2060 
     | 
    
         
            -
             
     | 
| 
       2061 
     | 
    
         
            -
             
     | 
| 
       2062 
     | 
    
         
            -
             
     | 
| 
      
 1684 
     | 
    
         
            +
                      items.delete_if do |item|
         
     | 
| 
      
 1685 
     | 
    
         
            +
                        if ((!tags.empty? && item.tags?(tags, bool)) || (opt[:search] && item.search(opt[:search].to_s)) || (opt[:before] && item.date < cutoff))
         
     | 
| 
      
 1686 
     | 
    
         
            +
                          moved_items.push(item)
         
     | 
| 
      
 1687 
     | 
    
         
            +
                          counter += 1
         
     | 
| 
      
 1688 
     | 
    
         
            +
                          true
         
     | 
| 
      
 1689 
     | 
    
         
            +
                        else
         
     | 
| 
      
 1690 
     | 
    
         
            +
                          false
         
     | 
| 
      
 1691 
     | 
    
         
            +
                        end
         
     | 
| 
       2063 
1692 
     | 
    
         
             
                      end
         
     | 
| 
       2064 
     | 
    
         
            -
                       
     | 
| 
       2065 
     | 
    
         
            -
                         
     | 
| 
       2066 
     | 
    
         
            -
             
     | 
| 
       2067 
     | 
    
         
            -
             
     | 
| 
       2068 
     | 
    
         
            -
             
     | 
| 
       2069 
     | 
    
         
            -
             
     | 
| 
       2070 
     | 
    
         
            -
             
     | 
| 
      
 1693 
     | 
    
         
            +
                      moved_items.each do |item|
         
     | 
| 
      
 1694 
     | 
    
         
            +
                        if label
         
     | 
| 
      
 1695 
     | 
    
         
            +
                          item.title = if section == @config['current_section']
         
     | 
| 
      
 1696 
     | 
    
         
            +
                                         item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
         
     | 
| 
      
 1697 
     | 
    
         
            +
                                       else
         
     | 
| 
      
 1698 
     | 
    
         
            +
                                         item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
         
     | 
| 
      
 1699 
     | 
    
         
            +
                                       end
         
     | 
| 
      
 1700 
     | 
    
         
            +
                          logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
         
     | 
| 
       2071 
1701 
     | 
    
         
             
                        end
         
     | 
| 
       2072 
1702 
     | 
    
         
             
                      end
         
     | 
| 
       2073 
     | 
    
         
            -
                      note = "\n#{note_lines.join("\n").chomp}"
         
     | 
| 
       2074 
     | 
    
         
            -
                    else
         
     | 
| 
       2075 
     | 
    
         
            -
                      note = ''
         
     | 
| 
       2076 
     | 
    
         
            -
                    end
         
     | 
| 
       2077 
     | 
    
         
            -
                    output = opt[:template].dup
         
     | 
| 
       2078 
1703 
     | 
    
         | 
| 
       2079 
     | 
    
         
            -
             
     | 
| 
       2080 
     | 
    
         
            -
                       
     | 
| 
       2081 
     | 
    
         
            -
             
     | 
| 
       2082 
     | 
    
         
            -
             
     | 
| 
       2083 
     | 
    
         
            -
                        m
         
     | 
| 
      
 1704 
     | 
    
         
            +
                      @content[section][:items] = items
         
     | 
| 
      
 1705 
     | 
    
         
            +
                      @content[destination][:items].concat(moved_items)
         
     | 
| 
      
 1706 
     | 
    
         
            +
                      if moved_items.length.positive?
         
     | 
| 
      
 1707 
     | 
    
         
            +
                        logger.info('Archived:', "#{moved_items.length} items from #{section} to #{destination}")
         
     | 
| 
       2084 
1708 
     | 
    
         
             
                      end
         
     | 
| 
       2085 
     | 
    
         
            -
                     
     | 
| 
       2086 
     | 
    
         
            -
             
     | 
| 
       2087 
     | 
    
         
            -
                    output.sub!(/%date/, item['date'].strftime(opt[:format]))
         
     | 
| 
       2088 
     | 
    
         
            -
             
     | 
| 
       2089 
     | 
    
         
            -
                    interval = get_interval(item, record: true) if item['title'] =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
         
     | 
| 
       2090 
     | 
    
         
            -
                    interval ||= ''
         
     | 
| 
       2091 
     | 
    
         
            -
                    output.sub!(/%interval/, interval)
         
     | 
| 
      
 1709 
     | 
    
         
            +
                    else
         
     | 
| 
      
 1710 
     | 
    
         
            +
                      count = items.length if items.length < count
         
     | 
| 
       2092 
1711 
     | 
    
         | 
| 
       2093 
     | 
    
         
            -
             
     | 
| 
       2094 
     | 
    
         
            -
             
     | 
| 
       2095 
     | 
    
         
            -
             
     | 
| 
      
 1712 
     | 
    
         
            +
                      items.map! do |item|
         
     | 
| 
      
 1713 
     | 
    
         
            +
                        if label
         
     | 
| 
      
 1714 
     | 
    
         
            +
                          item.title = if section == @config['current_section']
         
     | 
| 
      
 1715 
     | 
    
         
            +
                                         item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, '\1')
         
     | 
| 
      
 1716 
     | 
    
         
            +
                                       else
         
     | 
| 
      
 1717 
     | 
    
         
            +
                                         item.title.sub(/(?: ?@from\(.*?\))?(.*)$/, "\\1 @from(#{section})")
         
     | 
| 
      
 1718 
     | 
    
         
            +
                                       end
         
     | 
| 
      
 1719 
     | 
    
         
            +
                          logger.debug('Moved:', "#{item.title} from #{section} to #{destination}")
         
     | 
| 
      
 1720 
     | 
    
         
            +
                        end
         
     | 
| 
      
 1721 
     | 
    
         
            +
                        item
         
     | 
| 
      
 1722 
     | 
    
         
            +
                      end
         
     | 
| 
       2096 
1723 
     | 
    
         | 
| 
       2097 
     | 
    
         
            -
             
     | 
| 
       2098 
     | 
    
         
            -
             
     | 
| 
       2099 
     | 
    
         
            -
                        flag + item['title'].gsub(/(.{1,#{opt[:wrap_width]}})(\s+|\Z)/, "\\1\n\t ").chomp + reset
         
     | 
| 
      
 1724 
     | 
    
         
            +
                      if items.count > count
         
     | 
| 
      
 1725 
     | 
    
         
            +
                        @content[destination][:items].concat(items[count..-1])
         
     | 
| 
       2100 
1726 
     | 
    
         
             
                      else
         
     | 
| 
       2101 
     | 
    
         
            -
                         
     | 
| 
      
 1727 
     | 
    
         
            +
                        @content[destination][:items].concat(items)
         
     | 
| 
       2102 
1728 
     | 
    
         
             
                      end
         
     | 
| 
       2103 
     | 
    
         
            -
                    end
         
     | 
| 
       2104 
1729 
     | 
    
         | 
| 
       2105 
     | 
    
         
            -
             
     | 
| 
       2106 
     | 
    
         
            -
             
     | 
| 
       2107 
     | 
    
         
            -
             
     | 
| 
       2108 
     | 
    
         
            -
             
     | 
| 
       2109 
     | 
    
         
            -
             
     | 
| 
       2110 
     | 
    
         
            -
             
     | 
| 
       2111 
     | 
    
         
            -
                                    
     | 
| 
       2112 
     | 
    
         
            -
             
     | 
| 
       2113 
     | 
    
         
            -
             
     | 
| 
       2114 
     | 
    
         
            -
                      output.gsub!(/(\s|m)(@[^ (]+)/, "\\1#{colors[opt[:tags_color]]}\\2#{last_color}")
         
     | 
| 
       2115 
     | 
    
         
            -
                    end
         
     | 
| 
       2116 
     | 
    
         
            -
                    output.sub!(/%note/, note)
         
     | 
| 
       2117 
     | 
    
         
            -
                    output.sub!(/%odnote/, note.gsub(/^\t*/, ''))
         
     | 
| 
       2118 
     | 
    
         
            -
                    output.sub!(/%chompnote/, note.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' '))
         
     | 
| 
       2119 
     | 
    
         
            -
                    output.gsub!(/%hr(_under)?/) do |_m|
         
     | 
| 
       2120 
     | 
    
         
            -
                      o = ''
         
     | 
| 
       2121 
     | 
    
         
            -
                      `tput cols`.to_i.times do
         
     | 
| 
       2122 
     | 
    
         
            -
                        o += Regexp.last_match(1).nil? ? '-' : '_'
         
     | 
| 
       2123 
     | 
    
         
            -
                      end
         
     | 
| 
       2124 
     | 
    
         
            -
                      o
         
     | 
| 
      
 1730 
     | 
    
         
            +
                      @content[section][:items] = if count.zero?
         
     | 
| 
      
 1731 
     | 
    
         
            +
                                                    []
         
     | 
| 
      
 1732 
     | 
    
         
            +
                                                  else
         
     | 
| 
      
 1733 
     | 
    
         
            +
                                                    items[0..count - 1]
         
     | 
| 
      
 1734 
     | 
    
         
            +
                                                  end
         
     | 
| 
      
 1735 
     | 
    
         
            +
                      logger.count(destination == 'Archive' ? :archived : :moved,
         
     | 
| 
      
 1736 
     | 
    
         
            +
                                   count: items.length - count,
         
     | 
| 
      
 1737 
     | 
    
         
            +
                                   message: "%count %items from #{section} to #{destination}")
         
     | 
| 
      
 1738 
     | 
    
         
            +
                      # logger.info('Archived:', "#{items.length - count} items from #{section} to #{destination}")
         
     | 
| 
       2125 
1739 
     | 
    
         
             
                    end
         
     | 
| 
       2126 
     | 
    
         
            -
                    output.gsub!(/%n/, "\n")
         
     | 
| 
       2127 
     | 
    
         
            -
                    output.gsub!(/%t/, "\t")
         
     | 
| 
       2128 
     | 
    
         
            -
             
     | 
| 
       2129 
     | 
    
         
            -
                    out += "#{output}\n"
         
     | 
| 
       2130 
1740 
     | 
    
         
             
                  end
         
     | 
| 
       2131 
     | 
    
         
            -
             
     | 
| 
       2132 
     | 
    
         
            -
                  out += tag_times(format: :text, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
         
     | 
| 
       2133 
1741 
     | 
    
         
             
                end
         
     | 
| 
       2134 
     | 
    
         
            -
                out
         
     | 
| 
       2135 
     | 
    
         
            -
              end
         
     | 
| 
       2136 
1742 
     | 
    
         | 
| 
       2137 
     | 
    
         
            -
             
     | 
| 
       2138 
     | 
    
         
            -
             
     | 
| 
       2139 
     | 
    
         
            -
             
     | 
| 
       2140 
     | 
    
         
            -
             
     | 
| 
       2141 
     | 
    
         
            -
             
     | 
| 
       2142 
     | 
    
         
            -
             
     | 
| 
       2143 
     | 
    
         
            -
             
     | 
| 
       2144 
     | 
    
         
            -
             
     | 
| 
       2145 
     | 
    
         
            -
             
     | 
| 
       2146 
     | 
    
         
            -
             
     | 
| 
       2147 
     | 
    
         
            -
             
     | 
| 
       2148 
     | 
    
         
            -
             
     | 
| 
      
 1743 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1744 
     | 
    
         
            +
                ## @brief      Show all entries from the current day
         
     | 
| 
      
 1745 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1746 
     | 
    
         
            +
                ## @param      times   (Boolean) show times
         
     | 
| 
      
 1747 
     | 
    
         
            +
                ## @param      output  (String) output format
         
     | 
| 
      
 1748 
     | 
    
         
            +
                ## @param      opt     (Hash) Options
         
     | 
| 
      
 1749 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1750 
     | 
    
         
            +
                def today(times = true, output = nil, opt = {})
         
     | 
| 
      
 1751 
     | 
    
         
            +
                  opt[:totals] ||= false
         
     | 
| 
      
 1752 
     | 
    
         
            +
                  opt[:sort_tags] ||= false
         
     | 
| 
      
 1753 
     | 
    
         
            +
             
     | 
| 
      
 1754 
     | 
    
         
            +
                  cfg = @config['templates']['today']
         
     | 
| 
      
 1755 
     | 
    
         
            +
                  options = {
         
     | 
| 
      
 1756 
     | 
    
         
            +
                    after: opt[:after],
         
     | 
| 
      
 1757 
     | 
    
         
            +
                    before: opt[:before],
         
     | 
| 
      
 1758 
     | 
    
         
            +
                    count: 0,
         
     | 
| 
      
 1759 
     | 
    
         
            +
                    format: cfg['date_format'],
         
     | 
| 
      
 1760 
     | 
    
         
            +
                    order: 'asc',
         
     | 
| 
      
 1761 
     | 
    
         
            +
                    output: output,
         
     | 
| 
      
 1762 
     | 
    
         
            +
                    section: opt[:section],
         
     | 
| 
      
 1763 
     | 
    
         
            +
                    sort_tags: opt[:sort_tags],
         
     | 
| 
      
 1764 
     | 
    
         
            +
                    template: cfg['template'],
         
     | 
| 
      
 1765 
     | 
    
         
            +
                    times: times,
         
     | 
| 
      
 1766 
     | 
    
         
            +
                    today: true,
         
     | 
| 
      
 1767 
     | 
    
         
            +
                    totals: opt[:totals],
         
     | 
| 
      
 1768 
     | 
    
         
            +
                    wrap_width: cfg['wrap_width']
         
     | 
| 
      
 1769 
     | 
    
         
            +
                  }
         
     | 
| 
      
 1770 
     | 
    
         
            +
                  list_section(options)
         
     | 
| 
      
 1771 
     | 
    
         
            +
                end
         
     | 
| 
      
 1772 
     | 
    
         
            +
             
     | 
| 
      
 1773 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1774 
     | 
    
         
            +
                ## @brief      Display entries within a date range
         
     | 
| 
      
 1775 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1776 
     | 
    
         
            +
                ## @param      dates    (Array) [start, end]
         
     | 
| 
      
 1777 
     | 
    
         
            +
                ## @param      section  (String) The section
         
     | 
| 
      
 1778 
     | 
    
         
            +
                ## @param      times    (Bool) Show times
         
     | 
| 
      
 1779 
     | 
    
         
            +
                ## @param      output   (String) Output format
         
     | 
| 
      
 1780 
     | 
    
         
            +
                ## @param      opt      (Hash) Additional Options
         
     | 
| 
      
 1781 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1782 
     | 
    
         
            +
                def list_date(dates, section, times = nil, output = nil, opt = {})
         
     | 
| 
      
 1783 
     | 
    
         
            +
                  opt[:totals] ||= false
         
     | 
| 
      
 1784 
     | 
    
         
            +
                  opt[:sort_tags] ||= false
         
     | 
| 
      
 1785 
     | 
    
         
            +
                  section = guess_section(section)
         
     | 
| 
      
 1786 
     | 
    
         
            +
                  # :date_filter expects an array with start and end date
         
     | 
| 
      
 1787 
     | 
    
         
            +
                  dates = [dates, dates] if dates.instance_of?(String)
         
     | 
| 
      
 1788 
     | 
    
         
            +
             
     | 
| 
      
 1789 
     | 
    
         
            +
                  list_section({ section: section, count: 0, order: 'asc', date_filter: dates, times: times,
         
     | 
| 
      
 1790 
     | 
    
         
            +
                                 output: output, totals: opt[:totals], sort_tags: opt[:sort_tags] })
         
     | 
| 
      
 1791 
     | 
    
         
            +
                end
         
     | 
| 
      
 1792 
     | 
    
         
            +
             
     | 
| 
      
 1793 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1794 
     | 
    
         
            +
                ## @brief      Show entries from the previous day
         
     | 
| 
      
 1795 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1796 
     | 
    
         
            +
                ## @param      section  (String) The section
         
     | 
| 
      
 1797 
     | 
    
         
            +
                ## @param      times    (Bool) Show times
         
     | 
| 
      
 1798 
     | 
    
         
            +
                ## @param      output   (String) Output format
         
     | 
| 
      
 1799 
     | 
    
         
            +
                ## @param      opt      (Hash) Additional Options
         
     | 
| 
      
 1800 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1801 
     | 
    
         
            +
                def yesterday(section, times = nil, output = nil, opt = {})
         
     | 
| 
      
 1802 
     | 
    
         
            +
                  opt[:totals] ||= false
         
     | 
| 
      
 1803 
     | 
    
         
            +
                  opt[:sort_tags] ||= false
         
     | 
| 
      
 1804 
     | 
    
         
            +
                  section = guess_section(section)
         
     | 
| 
      
 1805 
     | 
    
         
            +
                  y = (Time.now - (60 * 60 * 24)).strftime('%Y-%m-%d')
         
     | 
| 
      
 1806 
     | 
    
         
            +
                  opt[:after] = "#{y} #{opt[:after]}" if opt[:after]
         
     | 
| 
      
 1807 
     | 
    
         
            +
                  opt[:before] = "#{y} #{opt[:before]}" if opt[:before]
         
     | 
| 
      
 1808 
     | 
    
         
            +
             
     | 
| 
      
 1809 
     | 
    
         
            +
                  options = {
         
     | 
| 
      
 1810 
     | 
    
         
            +
                    after: opt[:after],
         
     | 
| 
      
 1811 
     | 
    
         
            +
                    before: opt[:before],
         
     | 
| 
      
 1812 
     | 
    
         
            +
                    count: 0,
         
     | 
| 
      
 1813 
     | 
    
         
            +
                    order: opt[:order],
         
     | 
| 
      
 1814 
     | 
    
         
            +
                    output: output,
         
     | 
| 
      
 1815 
     | 
    
         
            +
                    section: section,
         
     | 
| 
      
 1816 
     | 
    
         
            +
                    sort_tags: opt[:sort_tags],
         
     | 
| 
      
 1817 
     | 
    
         
            +
                    tag_order: opt[:tag_order],
         
     | 
| 
      
 1818 
     | 
    
         
            +
                    times: times,
         
     | 
| 
      
 1819 
     | 
    
         
            +
                    totals: opt[:totals],
         
     | 
| 
      
 1820 
     | 
    
         
            +
                    yesterday: true
         
     | 
| 
      
 1821 
     | 
    
         
            +
                  }
         
     | 
| 
       2149 
1822 
     | 
    
         | 
| 
       2150 
     | 
    
         
            -
             
     | 
| 
       2151 
     | 
    
         
            -
                 
     | 
| 
       2152 
     | 
    
         
            -
             
     | 
| 
      
 1823 
     | 
    
         
            +
                  list_section(options)
         
     | 
| 
      
 1824 
     | 
    
         
            +
                end
         
     | 
| 
      
 1825 
     | 
    
         
            +
             
     | 
| 
      
 1826 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1827 
     | 
    
         
            +
                ## @brief      Show recent entries
         
     | 
| 
      
 1828 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1829 
     | 
    
         
            +
                ## @param      count    (Integer) The number to show
         
     | 
| 
      
 1830 
     | 
    
         
            +
                ## @param      section  (String) The section to show from, default Currently
         
     | 
| 
      
 1831 
     | 
    
         
            +
                ## @param      opt      (Hash) Additional Options
         
     | 
| 
      
 1832 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1833 
     | 
    
         
            +
                def recent(count = 10, section = nil, opt = {})
         
     | 
| 
      
 1834 
     | 
    
         
            +
                  times = opt[:t] || true
         
     | 
| 
      
 1835 
     | 
    
         
            +
                  opt[:totals] ||= false
         
     | 
| 
      
 1836 
     | 
    
         
            +
                  opt[:sort_tags] ||= false
         
     | 
| 
      
 1837 
     | 
    
         
            +
             
     | 
| 
      
 1838 
     | 
    
         
            +
                  cfg = @config['templates']['recent']
         
     | 
| 
      
 1839 
     | 
    
         
            +
                  section ||= @config['current_section']
         
     | 
| 
      
 1840 
     | 
    
         
            +
                  section = guess_section(section)
         
     | 
| 
      
 1841 
     | 
    
         
            +
             
     | 
| 
      
 1842 
     | 
    
         
            +
                  list_section({ section: section, wrap_width: cfg['wrap_width'], count: count,
         
     | 
| 
      
 1843 
     | 
    
         
            +
                                 format: cfg['date_format'], template: cfg['template'],
         
     | 
| 
      
 1844 
     | 
    
         
            +
                                 order: 'asc', times: times, totals: opt[:totals],
         
     | 
| 
      
 1845 
     | 
    
         
            +
                                 sort_tags: opt[:sort_tags], tags_color: opt[:tags_color] })
         
     | 
| 
      
 1846 
     | 
    
         
            +
                end
         
     | 
| 
      
 1847 
     | 
    
         
            +
             
     | 
| 
      
 1848 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1849 
     | 
    
         
            +
                ## @brief      Show the last entry
         
     | 
| 
      
 1850 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1851 
     | 
    
         
            +
                ## @param      times    (Bool) Show times
         
     | 
| 
      
 1852 
     | 
    
         
            +
                ## @param      section  (String) Section to pull from, default Currently
         
     | 
| 
      
 1853 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1854 
     | 
    
         
            +
                def last(times: true, section: nil, options: {})
         
     | 
| 
      
 1855 
     | 
    
         
            +
                  section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
         
     | 
| 
      
 1856 
     | 
    
         
            +
                  cfg = @config['templates']['last']
         
     | 
| 
      
 1857 
     | 
    
         
            +
             
     | 
| 
      
 1858 
     | 
    
         
            +
                  opts = {
         
     | 
| 
      
 1859 
     | 
    
         
            +
                    section: section,
         
     | 
| 
      
 1860 
     | 
    
         
            +
                    wrap_width: cfg['wrap_width'],
         
     | 
| 
      
 1861 
     | 
    
         
            +
                    count: 1,
         
     | 
| 
      
 1862 
     | 
    
         
            +
                    format: cfg['date_format'],
         
     | 
| 
      
 1863 
     | 
    
         
            +
                    template: cfg['template'],
         
     | 
| 
      
 1864 
     | 
    
         
            +
                    times: times
         
     | 
| 
      
 1865 
     | 
    
         
            +
                  }
         
     | 
| 
       2153 
1866 
     | 
    
         | 
| 
       2154 
     | 
    
         
            -
             
     | 
| 
      
 1867 
     | 
    
         
            +
                  if options[:tag]
         
     | 
| 
      
 1868 
     | 
    
         
            +
                    opts[:tag_filter] = {
         
     | 
| 
      
 1869 
     | 
    
         
            +
                      'tags' => options[:tag],
         
     | 
| 
      
 1870 
     | 
    
         
            +
                      'bool' => options[:tag_bool]
         
     | 
| 
      
 1871 
     | 
    
         
            +
                    }
         
     | 
| 
      
 1872 
     | 
    
         
            +
                  end
         
     | 
| 
       2155 
1873 
     | 
    
         | 
| 
       2156 
     | 
    
         
            -
             
     | 
| 
      
 1874 
     | 
    
         
            +
                  opts[:search] = options[:search] if options[:search]
         
     | 
| 
       2157 
1875 
     | 
    
         | 
| 
       2158 
     | 
    
         
            -
             
     | 
| 
       2159 
     | 
    
         
            -
                  do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before] })
         
     | 
| 
       2160 
     | 
    
         
            -
                  write(doing_file)
         
     | 
| 
       2161 
     | 
    
         
            -
                else
         
     | 
| 
       2162 
     | 
    
         
            -
                  exit_now! 'Either source or destination does not exist'
         
     | 
| 
      
 1876 
     | 
    
         
            +
                  list_section(opts)
         
     | 
| 
       2163 
1877 
     | 
    
         
             
                end
         
     | 
| 
       2164 
     | 
    
         
            -
              end
         
     | 
| 
       2165 
1878 
     | 
    
         | 
| 
       2166 
     | 
    
         
            -
             
     | 
| 
       2167 
     | 
    
         
            -
             
     | 
| 
       2168 
     | 
    
         
            -
             
     | 
| 
       2169 
     | 
    
         
            -
             
     | 
| 
       2170 
     | 
    
         
            -
             
     | 
| 
       2171 
     | 
    
         
            -
             
     | 
| 
       2172 
     | 
    
         
            -
             
     | 
| 
       2173 
     | 
    
         
            -
             
     | 
| 
       2174 
     | 
    
         
            -
             
     | 
| 
       2175 
     | 
    
         
            -
             
     | 
| 
       2176 
     | 
    
         
            -
                bool  = opt[:bool] || :and
         
     | 
| 
       2177 
     | 
    
         
            -
                label = opt[:label] || true
         
     | 
| 
       2178 
     | 
    
         
            -
             
     | 
| 
       2179 
     | 
    
         
            -
                if sect =~ /^all$/i
         
     | 
| 
       2180 
     | 
    
         
            -
                  all_sections = sections.dup
         
     | 
| 
       2181 
     | 
    
         
            -
                  all_sections.delete(destination)
         
     | 
| 
       2182 
     | 
    
         
            -
                else
         
     | 
| 
       2183 
     | 
    
         
            -
                  all_sections = [sect]
         
     | 
| 
       2184 
     | 
    
         
            -
                end
         
     | 
| 
      
 1879 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1880 
     | 
    
         
            +
                ## @brief Uses 'autotag' configuration to turn keywords into tags for time tracking.
         
     | 
| 
      
 1881 
     | 
    
         
            +
                ## Does not repeat tags in a title, and only converts the first instance of an
         
     | 
| 
      
 1882 
     | 
    
         
            +
                ## untagged keyword
         
     | 
| 
      
 1883 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1884 
     | 
    
         
            +
                ## @param      text  (String) The text to tag
         
     | 
| 
      
 1885 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1886 
     | 
    
         
            +
                def autotag(text)
         
     | 
| 
      
 1887 
     | 
    
         
            +
                  return unless text
         
     | 
| 
      
 1888 
     | 
    
         
            +
                  return text unless @auto_tag
         
     | 
| 
       2185 
1889 
     | 
    
         | 
| 
       2186 
     | 
    
         
            -
             
     | 
| 
      
 1890 
     | 
    
         
            +
                  original = text.dup
         
     | 
| 
       2187 
1891 
     | 
    
         | 
| 
       2188 
     | 
    
         
            -
             
     | 
| 
       2189 
     | 
    
         
            -
                   
     | 
| 
      
 1892 
     | 
    
         
            +
                  current_tags = text.scan(/@\w+/)
         
     | 
| 
      
 1893 
     | 
    
         
            +
                  whitelisted = []
         
     | 
| 
      
 1894 
     | 
    
         
            +
                  @config['autotag']['whitelist'].each do |tag|
         
     | 
| 
      
 1895 
     | 
    
         
            +
                    next if text =~ /@#{tag}\b/i
         
     | 
| 
       2190 
1896 
     | 
    
         | 
| 
       2191 
     | 
    
         
            -
             
     | 
| 
       2192 
     | 
    
         
            -
             
     | 
| 
       2193 
     | 
    
         
            -
             
     | 
| 
       2194 
     | 
    
         
            -
                       
     | 
| 
       2195 
     | 
    
         
            -
                      time_string += ' 12am' if time_string !~ /(\d+:\d+|\d+[ap])/
         
     | 
| 
       2196 
     | 
    
         
            -
                      cutoff = chronify(time_string)
         
     | 
| 
      
 1897 
     | 
    
         
            +
                    text.sub!(/(?<!@)\b(#{tag.strip})\b/i) do |m|
         
     | 
| 
      
 1898 
     | 
    
         
            +
                      m.downcase! if tag =~ /[a-z]/
         
     | 
| 
      
 1899 
     | 
    
         
            +
                      whitelisted.push("@#{m}")
         
     | 
| 
      
 1900 
     | 
    
         
            +
                      "@#{m}"
         
     | 
| 
       2197 
1901 
     | 
    
         
             
                    end
         
     | 
| 
      
 1902 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1903 
     | 
    
         
            +
                  tail_tags = []
         
     | 
| 
      
 1904 
     | 
    
         
            +
                  @config['autotag']['synonyms'].each do |tag, v|
         
     | 
| 
      
 1905 
     | 
    
         
            +
                    v.each do |word|
         
     | 
| 
      
 1906 
     | 
    
         
            +
                      next unless text =~ /\b#{word}\b/i
         
     | 
| 
       2198 
1907 
     | 
    
         | 
| 
       2199 
     | 
    
         
            -
             
     | 
| 
       2200 
     | 
    
         
            -
                      if ((!tags.empty? && item.has_tags?(tags, bool)) || (opt[:search] && item.matches_search?(opt[:search].to_s)) || (opt[:before] && item['date'] < cutoff))
         
     | 
| 
       2201 
     | 
    
         
            -
                        moved_items.push(item)
         
     | 
| 
       2202 
     | 
    
         
            -
                        counter += 1
         
     | 
| 
       2203 
     | 
    
         
            -
                        true
         
     | 
| 
       2204 
     | 
    
         
            -
                      else
         
     | 
| 
       2205 
     | 
    
         
            -
                        false
         
     | 
| 
       2206 
     | 
    
         
            -
                      end
         
     | 
| 
      
 1908 
     | 
    
         
            +
                      tail_tags.push(tag) unless current_tags.include?("@#{tag}") || whitelisted.include?("@#{tag}")
         
     | 
| 
       2207 
1909 
     | 
    
         
             
                    end
         
     | 
| 
       2208 
     | 
    
         
            -
             
     | 
| 
       2209 
     | 
    
         
            -
             
     | 
| 
       2210 
     | 
    
         
            -
             
     | 
| 
       2211 
     | 
    
         
            -
             
     | 
| 
       2212 
     | 
    
         
            -
             
     | 
| 
       2213 
     | 
    
         
            -
             
     | 
| 
       2214 
     | 
    
         
            -
             
     | 
| 
       2215 
     | 
    
         
            -
             
     | 
| 
       2216 
     | 
    
         
            -
             
     | 
| 
       2217 
     | 
    
         
            -
             
     | 
| 
       2218 
     | 
    
         
            -
             
     | 
| 
       2219 
     | 
    
         
            -
             
     | 
| 
       2220 
     | 
    
         
            -
             
     | 
| 
       2221 
     | 
    
         
            -
             
     | 
| 
       2222 
     | 
    
         
            -
             
     | 
| 
       2223 
     | 
    
         
            -
                         
     | 
| 
       2224 
     | 
    
         
            -
                           
     | 
| 
      
 1910 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1911 
     | 
    
         
            +
                  if @config['autotag'].key? 'transform'
         
     | 
| 
      
 1912 
     | 
    
         
            +
                    @config['autotag']['transform'].each do |tag|
         
     | 
| 
      
 1913 
     | 
    
         
            +
                      next unless tag =~ /\S+:\S+/
         
     | 
| 
      
 1914 
     | 
    
         
            +
             
     | 
| 
      
 1915 
     | 
    
         
            +
                      rx, r = tag.split(/:/)
         
     | 
| 
      
 1916 
     | 
    
         
            +
                      r.gsub!(/\$/, '\\')
         
     | 
| 
      
 1917 
     | 
    
         
            +
                      rx.sub!(/^@/, '')
         
     | 
| 
      
 1918 
     | 
    
         
            +
                      regex = Regexp.new('@' + rx + '\b')
         
     | 
| 
      
 1919 
     | 
    
         
            +
             
     | 
| 
      
 1920 
     | 
    
         
            +
                      matches = text.scan(regex)
         
     | 
| 
      
 1921 
     | 
    
         
            +
                      next unless matches
         
     | 
| 
      
 1922 
     | 
    
         
            +
             
     | 
| 
      
 1923 
     | 
    
         
            +
                      matches.each do |m|
         
     | 
| 
      
 1924 
     | 
    
         
            +
                        new_tag = r
         
     | 
| 
      
 1925 
     | 
    
         
            +
                        if m.is_a?(Array)
         
     | 
| 
      
 1926 
     | 
    
         
            +
                          index = 1
         
     | 
| 
      
 1927 
     | 
    
         
            +
                          m.each do |v|
         
     | 
| 
      
 1928 
     | 
    
         
            +
                            new_tag.gsub!('\\' + index.to_s, v)
         
     | 
| 
      
 1929 
     | 
    
         
            +
                            index += 1
         
     | 
| 
      
 1930 
     | 
    
         
            +
                          end
         
     | 
| 
      
 1931 
     | 
    
         
            +
                        end
         
     | 
| 
      
 1932 
     | 
    
         
            +
                        tail_tags.push(new_tag)
         
     | 
| 
       2225 
1933 
     | 
    
         
             
                      end
         
     | 
| 
       2226 
     | 
    
         
            -
                      item
         
     | 
| 
       2227 
1934 
     | 
    
         
             
                    end
         
     | 
| 
       2228 
     | 
    
         
            -
             
     | 
| 
       2229 
     | 
    
         
            -
                    if items.count > count
         
     | 
| 
       2230 
     | 
    
         
            -
                      @content[destination]['items'].concat(items[count..-1])
         
     | 
| 
       2231 
     | 
    
         
            -
                    else
         
     | 
| 
       2232 
     | 
    
         
            -
                      @content[destination]['items'].concat(items)
         
     | 
| 
       2233 
     | 
    
         
            -
                    end
         
     | 
| 
       2234 
     | 
    
         
            -
             
     | 
| 
       2235 
     | 
    
         
            -
                    @content[section]['items'] = if count.zero?
         
     | 
| 
       2236 
     | 
    
         
            -
                                                   []
         
     | 
| 
       2237 
     | 
    
         
            -
                                                 else
         
     | 
| 
       2238 
     | 
    
         
            -
                                                   items[0..count - 1]
         
     | 
| 
       2239 
     | 
    
         
            -
                                                 end
         
     | 
| 
       2240 
     | 
    
         
            -
             
     | 
| 
       2241 
     | 
    
         
            -
                    @results.push("Archived #{items.length - count} items from #{section} to #{destination}")
         
     | 
| 
       2242 
1935 
     | 
    
         
             
                  end
         
     | 
| 
       2243 
     | 
    
         
            -
                end
         
     | 
| 
       2244 
     | 
    
         
            -
              end
         
     | 
| 
       2245 
1936 
     | 
    
         | 
| 
       2246 
     | 
    
         
            -
             
     | 
| 
       2247 
     | 
    
         
            -
             
     | 
| 
       2248 
     | 
    
         
            -
             
     | 
| 
       2249 
     | 
    
         
            -
             
     | 
| 
       2250 
     | 
    
         
            -
             
     | 
| 
       2251 
     | 
    
         
            -
             
     | 
| 
       2252 
     | 
    
         
            -
             
     | 
| 
       2253 
     | 
    
         
            -
             
     | 
| 
       2254 
     | 
    
         
            -
             
     | 
| 
       2255 
     | 
    
         
            -
                color['green'] = "\033[0;0;32m"
         
     | 
| 
       2256 
     | 
    
         
            -
                color['yellow'] = "\033[0;0;33m"
         
     | 
| 
       2257 
     | 
    
         
            -
                color['blue'] = "\033[0;0;34m"
         
     | 
| 
       2258 
     | 
    
         
            -
                color['magenta'] = "\033[0;0;35m"
         
     | 
| 
       2259 
     | 
    
         
            -
                color['cyan'] = "\033[0;0;36m"
         
     | 
| 
       2260 
     | 
    
         
            -
                color['white'] = "\033[0;0;37m"
         
     | 
| 
       2261 
     | 
    
         
            -
                color['bgblack'] = "\033[40m"
         
     | 
| 
       2262 
     | 
    
         
            -
                color['bgred'] = "\033[41m"
         
     | 
| 
       2263 
     | 
    
         
            -
                color['bggreen'] = "\033[42m"
         
     | 
| 
       2264 
     | 
    
         
            -
                color['bgyellow'] = "\033[43m"
         
     | 
| 
       2265 
     | 
    
         
            -
                color['bgblue'] = "\033[44m"
         
     | 
| 
       2266 
     | 
    
         
            -
                color['bgmagenta'] = "\033[45m"
         
     | 
| 
       2267 
     | 
    
         
            -
                color['bgcyan'] = "\033[46m"
         
     | 
| 
       2268 
     | 
    
         
            -
                color['bgwhite'] = "\033[47m"
         
     | 
| 
       2269 
     | 
    
         
            -
                color['boldblack'] = "\033[1;30m"
         
     | 
| 
       2270 
     | 
    
         
            -
                color['boldred'] = "\033[1;31m"
         
     | 
| 
       2271 
     | 
    
         
            -
                color['boldgreen'] = "\033[0;1;32m"
         
     | 
| 
       2272 
     | 
    
         
            -
                color['boldyellow'] = "\033[0;1;33m"
         
     | 
| 
       2273 
     | 
    
         
            -
                color['boldblue'] = "\033[0;1;34m"
         
     | 
| 
       2274 
     | 
    
         
            -
                color['boldmagenta'] = "\033[0;1;35m"
         
     | 
| 
       2275 
     | 
    
         
            -
                color['boldcyan'] = "\033[0;1;36m"
         
     | 
| 
       2276 
     | 
    
         
            -
                color['boldwhite'] = "\033[0;1;37m"
         
     | 
| 
       2277 
     | 
    
         
            -
                color['boldbgblack'] = "\033[1;40m"
         
     | 
| 
       2278 
     | 
    
         
            -
                color['boldbgred'] = "\033[1;41m"
         
     | 
| 
       2279 
     | 
    
         
            -
                color['boldbggreen'] = "\033[1;42m"
         
     | 
| 
       2280 
     | 
    
         
            -
                color['boldbgyellow'] = "\033[1;43m"
         
     | 
| 
       2281 
     | 
    
         
            -
                color['boldbgblue'] = "\033[1;44m"
         
     | 
| 
       2282 
     | 
    
         
            -
                color['boldbgmagenta'] = "\033[1;45m"
         
     | 
| 
       2283 
     | 
    
         
            -
                color['boldbgcyan'] = "\033[1;46m"
         
     | 
| 
       2284 
     | 
    
         
            -
                color['boldbgwhite'] = "\033[1;47m"
         
     | 
| 
       2285 
     | 
    
         
            -
                color['softpurple'] = "\033[0;35;40m"
         
     | 
| 
       2286 
     | 
    
         
            -
                color['hotpants'] = "\033[7;34;40m"
         
     | 
| 
       2287 
     | 
    
         
            -
                color['knightrider'] = "\033[7;30;40m"
         
     | 
| 
       2288 
     | 
    
         
            -
                color['flamingo'] = "\033[7;31;47m"
         
     | 
| 
       2289 
     | 
    
         
            -
                color['yeller'] = "\033[1;37;43m"
         
     | 
| 
       2290 
     | 
    
         
            -
                color['whiteboard'] = "\033[1;30;47m"
         
     | 
| 
       2291 
     | 
    
         
            -
                color['default'] = "\033[0;39m"
         
     | 
| 
       2292 
     | 
    
         
            -
                color
         
     | 
| 
       2293 
     | 
    
         
            -
              end
         
     | 
| 
       2294 
     | 
    
         
            -
             
     | 
| 
       2295 
     | 
    
         
            -
              ##
         
     | 
| 
       2296 
     | 
    
         
            -
              ## @brief      Show all entries from the current day
         
     | 
| 
       2297 
     | 
    
         
            -
              ##
         
     | 
| 
       2298 
     | 
    
         
            -
              ## @param      times   (Boolean) show times
         
     | 
| 
       2299 
     | 
    
         
            -
              ## @param      output  (String) output format
         
     | 
| 
       2300 
     | 
    
         
            -
              ## @param      opt     (Hash) Options
         
     | 
| 
       2301 
     | 
    
         
            -
              ##
         
     | 
| 
       2302 
     | 
    
         
            -
              def today(times = true, output = nil, opt = {})
         
     | 
| 
       2303 
     | 
    
         
            -
                opt[:totals] ||= false
         
     | 
| 
       2304 
     | 
    
         
            -
                opt[:sort_tags] ||= false
         
     | 
| 
       2305 
     | 
    
         
            -
             
     | 
| 
       2306 
     | 
    
         
            -
                cfg = @config['templates']['today']
         
     | 
| 
       2307 
     | 
    
         
            -
                options = {
         
     | 
| 
       2308 
     | 
    
         
            -
                  after: opt[:after],
         
     | 
| 
       2309 
     | 
    
         
            -
                  before: opt[:before],
         
     | 
| 
       2310 
     | 
    
         
            -
                  count: 0,
         
     | 
| 
       2311 
     | 
    
         
            -
                  format: cfg['date_format'],
         
     | 
| 
       2312 
     | 
    
         
            -
                  order: 'asc',
         
     | 
| 
       2313 
     | 
    
         
            -
                  output: output,
         
     | 
| 
       2314 
     | 
    
         
            -
                  section: opt[:section],
         
     | 
| 
       2315 
     | 
    
         
            -
                  sort_tags: opt[:sort_tags],
         
     | 
| 
       2316 
     | 
    
         
            -
                  template: cfg['template'],
         
     | 
| 
       2317 
     | 
    
         
            -
                  times: times,
         
     | 
| 
       2318 
     | 
    
         
            -
                  today: true,
         
     | 
| 
       2319 
     | 
    
         
            -
                  totals: opt[:totals],
         
     | 
| 
       2320 
     | 
    
         
            -
                  wrap_width: cfg['wrap_width']
         
     | 
| 
       2321 
     | 
    
         
            -
                }
         
     | 
| 
       2322 
     | 
    
         
            -
                list_section(options)
         
     | 
| 
       2323 
     | 
    
         
            -
              end
         
     | 
| 
      
 1937 
     | 
    
         
            +
                  logger.debug('Autotag:', "Whitelisted tags: #{whitelisted.join(', ')}") unless whitelisted.empty?
         
     | 
| 
      
 1938 
     | 
    
         
            +
                  new_tags = whitelisted
         
     | 
| 
      
 1939 
     | 
    
         
            +
                  unless tail_tags.empty?
         
     | 
| 
      
 1940 
     | 
    
         
            +
                    tags = tail_tags.uniq.map { |t| "@#{t}".cyan }.join(' ')
         
     | 
| 
      
 1941 
     | 
    
         
            +
                    logger.debug('Autotag:', "Synonym tags: #{tags}")
         
     | 
| 
      
 1942 
     | 
    
         
            +
                    tags_a = tail_tags.map { |t| "@#{t}" }
         
     | 
| 
      
 1943 
     | 
    
         
            +
                    text.add_tags!(tags_a.join(' '))
         
     | 
| 
      
 1944 
     | 
    
         
            +
                    new_tags.concat(tags_a)
         
     | 
| 
      
 1945 
     | 
    
         
            +
                  end
         
     | 
| 
       2324 
1946 
     | 
    
         | 
| 
       2325 
     | 
    
         
            -
             
     | 
| 
       2326 
     | 
    
         
            -
             
     | 
| 
       2327 
     | 
    
         
            -
             
     | 
| 
       2328 
     | 
    
         
            -
             
     | 
| 
       2329 
     | 
    
         
            -
             
     | 
| 
       2330 
     | 
    
         
            -
              ## @param      times    (Bool) Show times
         
     | 
| 
       2331 
     | 
    
         
            -
              ## @param      output   (String) Output format
         
     | 
| 
       2332 
     | 
    
         
            -
              ## @param      opt      (Hash) Additional Options
         
     | 
| 
       2333 
     | 
    
         
            -
              ##
         
     | 
| 
       2334 
     | 
    
         
            -
              def list_date(dates, section, times = nil, output = nil, opt = {})
         
     | 
| 
       2335 
     | 
    
         
            -
                opt[:totals] ||= false
         
     | 
| 
       2336 
     | 
    
         
            -
                opt[:sort_tags] ||= false
         
     | 
| 
       2337 
     | 
    
         
            -
                section = guess_section(section)
         
     | 
| 
       2338 
     | 
    
         
            -
                # :date_filter expects an array with start and end date
         
     | 
| 
       2339 
     | 
    
         
            -
                dates = [dates, dates] if dates.instance_of?(String)
         
     | 
| 
       2340 
     | 
    
         
            -
             
     | 
| 
       2341 
     | 
    
         
            -
                list_section({ section: section, count: 0, order: 'asc', date_filter: dates, times: times,
         
     | 
| 
       2342 
     | 
    
         
            -
                               output: output, totals: opt[:totals], sort_tags: opt[:sort_tags] })
         
     | 
| 
       2343 
     | 
    
         
            -
              end
         
     | 
| 
      
 1947 
     | 
    
         
            +
                  unless text == original
         
     | 
| 
      
 1948 
     | 
    
         
            +
                    logger.info('Autotag:', "added #{new_tags.join(', ')} to \"#{text}\"")
         
     | 
| 
      
 1949 
     | 
    
         
            +
                  else
         
     | 
| 
      
 1950 
     | 
    
         
            +
                    logger.debug('Autotag:', "no change to \"#{text}\"")
         
     | 
| 
      
 1951 
     | 
    
         
            +
                  end
         
     | 
| 
       2344 
1952 
     | 
    
         | 
| 
       2345 
     | 
    
         
            -
             
     | 
| 
       2346 
     | 
    
         
            -
             
     | 
| 
       2347 
     | 
    
         
            -
              ##
         
     | 
| 
       2348 
     | 
    
         
            -
              ## @param      section  (String) The section
         
     | 
| 
       2349 
     | 
    
         
            -
              ## @param      times    (Bool) Show times
         
     | 
| 
       2350 
     | 
    
         
            -
              ## @param      output   (String) Output format
         
     | 
| 
       2351 
     | 
    
         
            -
              ## @param      opt      (Hash) Additional Options
         
     | 
| 
       2352 
     | 
    
         
            -
              ##
         
     | 
| 
       2353 
     | 
    
         
            -
              def yesterday(section, times = nil, output = nil, opt = {})
         
     | 
| 
       2354 
     | 
    
         
            -
                opt[:totals] ||= false
         
     | 
| 
       2355 
     | 
    
         
            -
                opt[:sort_tags] ||= false
         
     | 
| 
       2356 
     | 
    
         
            -
                section = guess_section(section)
         
     | 
| 
       2357 
     | 
    
         
            -
                y = (Time.now - (60 * 60 * 24)).strftime('%Y-%m-%d')
         
     | 
| 
       2358 
     | 
    
         
            -
                opt[:after] = "#{y} #{opt[:after]}" if opt[:after]
         
     | 
| 
       2359 
     | 
    
         
            -
                opt[:before] = "#{y} #{opt[:before]}" if opt[:before]
         
     | 
| 
       2360 
     | 
    
         
            -
             
     | 
| 
       2361 
     | 
    
         
            -
                options = {
         
     | 
| 
       2362 
     | 
    
         
            -
                  after: opt[:after],
         
     | 
| 
       2363 
     | 
    
         
            -
                  before: opt[:before],
         
     | 
| 
       2364 
     | 
    
         
            -
                  count: 0,
         
     | 
| 
       2365 
     | 
    
         
            -
                  order: 'asc',
         
     | 
| 
       2366 
     | 
    
         
            -
                  output: output,
         
     | 
| 
       2367 
     | 
    
         
            -
                  section: section,
         
     | 
| 
       2368 
     | 
    
         
            -
                  sort_tags: opt[:sort_tags],
         
     | 
| 
       2369 
     | 
    
         
            -
                  tag_order: opt[:tag_order],
         
     | 
| 
       2370 
     | 
    
         
            -
                  times: times,
         
     | 
| 
       2371 
     | 
    
         
            -
                  totals: opt[:totals],
         
     | 
| 
       2372 
     | 
    
         
            -
                  yesterday: true
         
     | 
| 
       2373 
     | 
    
         
            -
                }
         
     | 
| 
       2374 
     | 
    
         
            -
             
     | 
| 
       2375 
     | 
    
         
            -
                list_section(options)
         
     | 
| 
       2376 
     | 
    
         
            -
              end
         
     | 
| 
      
 1953 
     | 
    
         
            +
                  text
         
     | 
| 
      
 1954 
     | 
    
         
            +
                end
         
     | 
| 
       2377 
1955 
     | 
    
         | 
| 
       2378 
     | 
    
         
            -
             
     | 
| 
       2379 
     | 
    
         
            -
             
     | 
| 
       2380 
     | 
    
         
            -
             
     | 
| 
       2381 
     | 
    
         
            -
             
     | 
| 
       2382 
     | 
    
         
            -
             
     | 
| 
       2383 
     | 
    
         
            -
             
     | 
| 
       2384 
     | 
    
         
            -
               
     | 
| 
       2385 
     | 
    
         
            -
             
     | 
| 
       2386 
     | 
    
         
            -
                 
     | 
| 
       2387 
     | 
    
         
            -
                 
     | 
| 
       2388 
     | 
    
         
            -
             
     | 
| 
       2389 
     | 
    
         
            -
             
     | 
| 
       2390 
     | 
    
         
            -
                cfg = @config['templates']['recent']
         
     | 
| 
       2391 
     | 
    
         
            -
                section ||= @current_section
         
     | 
| 
       2392 
     | 
    
         
            -
                section = guess_section(section)
         
     | 
| 
       2393 
     | 
    
         
            -
             
     | 
| 
       2394 
     | 
    
         
            -
                list_section({ section: section, wrap_width: cfg['wrap_width'], count: count,
         
     | 
| 
       2395 
     | 
    
         
            -
                               format: cfg['date_format'], template: cfg['template'],
         
     | 
| 
       2396 
     | 
    
         
            -
                               order: 'asc', times: times, totals: opt[:totals],
         
     | 
| 
       2397 
     | 
    
         
            -
                               sort_tags: opt[:sort_tags], tags_color: opt[:tags_color] })
         
     | 
| 
       2398 
     | 
    
         
            -
              end
         
     | 
| 
      
 1956 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1957 
     | 
    
         
            +
                ## @brief      Get total elapsed time for all tags in
         
     | 
| 
      
 1958 
     | 
    
         
            +
                ##             selection
         
     | 
| 
      
 1959 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1960 
     | 
    
         
            +
                ## @param      format        (String) return format (html,
         
     | 
| 
      
 1961 
     | 
    
         
            +
                ##                           json, or text)
         
     | 
| 
      
 1962 
     | 
    
         
            +
                ## @param      sort_by_name  (Boolean) Sort by name if true, otherwise by time
         
     | 
| 
      
 1963 
     | 
    
         
            +
                ## @param      sort_order    (String) The sort order (asc or desc)
         
     | 
| 
      
 1964 
     | 
    
         
            +
                ##
         
     | 
| 
      
 1965 
     | 
    
         
            +
                def tag_times(format: :text, sort_by_name: false, sort_order: 'asc')
         
     | 
| 
      
 1966 
     | 
    
         
            +
                  return '' if @timers.empty?
         
     | 
| 
       2399 
1967 
     | 
    
         | 
| 
       2400 
     | 
    
         
            -
             
     | 
| 
       2401 
     | 
    
         
            -
              ## @brief      Show the last entry
         
     | 
| 
       2402 
     | 
    
         
            -
              ##
         
     | 
| 
       2403 
     | 
    
         
            -
              ## @param      times    (Bool) Show times
         
     | 
| 
       2404 
     | 
    
         
            -
              ## @param      section  (String) Section to pull from, default Currently
         
     | 
| 
       2405 
     | 
    
         
            -
              ##
         
     | 
| 
       2406 
     | 
    
         
            -
              def last(times: true, section: nil, options: {})
         
     | 
| 
       2407 
     | 
    
         
            -
                section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
         
     | 
| 
       2408 
     | 
    
         
            -
                cfg = @config['templates']['last']
         
     | 
| 
       2409 
     | 
    
         
            -
             
     | 
| 
       2410 
     | 
    
         
            -
                opts = {
         
     | 
| 
       2411 
     | 
    
         
            -
                  section: section,
         
     | 
| 
       2412 
     | 
    
         
            -
                  wrap_width: cfg['wrap_width'],
         
     | 
| 
       2413 
     | 
    
         
            -
                  count: 1,
         
     | 
| 
       2414 
     | 
    
         
            -
                  format: cfg['date_format'],
         
     | 
| 
       2415 
     | 
    
         
            -
                  template: cfg['template'],
         
     | 
| 
       2416 
     | 
    
         
            -
                  times: times
         
     | 
| 
       2417 
     | 
    
         
            -
                }
         
     | 
| 
       2418 
     | 
    
         
            -
             
     | 
| 
       2419 
     | 
    
         
            -
                if options[:tag]
         
     | 
| 
       2420 
     | 
    
         
            -
                  opts[:tag_filter] = {
         
     | 
| 
       2421 
     | 
    
         
            -
                    'tags' => options[:tag],
         
     | 
| 
       2422 
     | 
    
         
            -
                    'bool' => options[:tag_bool]
         
     | 
| 
       2423 
     | 
    
         
            -
                  }
         
     | 
| 
       2424 
     | 
    
         
            -
                end
         
     | 
| 
      
 1968 
     | 
    
         
            +
                  max = @timers.keys.sort_by { |k| k.length }.reverse[0].length + 1
         
     | 
| 
       2425 
1969 
     | 
    
         | 
| 
       2426 
     | 
    
         
            -
             
     | 
| 
      
 1970 
     | 
    
         
            +
                  total = @timers.delete('All')
         
     | 
| 
       2427 
1971 
     | 
    
         | 
| 
       2428 
     | 
    
         
            -
             
     | 
| 
       2429 
     | 
    
         
            -
             
     | 
| 
      
 1972 
     | 
    
         
            +
                  tags_data = @timers.delete_if { |_k, v| v == 0 }
         
     | 
| 
      
 1973 
     | 
    
         
            +
                  sorted_tags_data = if sort_by_name
         
     | 
| 
      
 1974 
     | 
    
         
            +
                                       tags_data.sort_by { |k, _v| k }
         
     | 
| 
      
 1975 
     | 
    
         
            +
                                     else
         
     | 
| 
      
 1976 
     | 
    
         
            +
                                       tags_data.sort_by { |_k, v| v }
         
     | 
| 
      
 1977 
     | 
    
         
            +
                                     end
         
     | 
| 
       2430 
1978 
     | 
    
         | 
| 
       2431 
     | 
    
         
            -
             
     | 
| 
       2432 
     | 
    
         
            -
             
     | 
| 
       2433 
     | 
    
         
            -
             
     | 
| 
       2434 
     | 
    
         
            -
             
     | 
| 
       2435 
     | 
    
         
            -
             
     | 
| 
       2436 
     | 
    
         
            -
             
     | 
| 
       2437 
     | 
    
         
            -
             
     | 
| 
       2438 
     | 
    
         
            -
             
     | 
| 
       2439 
     | 
    
         
            -
             
     | 
| 
       2440 
     | 
    
         
            -
             
     | 
| 
       2441 
     | 
    
         
            -
             
     | 
| 
       2442 
     | 
    
         
            -
             
     | 
| 
       2443 
     | 
    
         
            -
             
     | 
| 
       2444 
     | 
    
         
            -
             
     | 
| 
       2445 
     | 
    
         
            -
             
     | 
| 
       2446 
     | 
    
         
            -
             
     | 
| 
       2447 
     | 
    
         
            -
             
     | 
| 
       2448 
     | 
    
         
            -
             
     | 
| 
       2449 
     | 
    
         
            -
             
     | 
| 
       2450 
     | 
    
         
            -
             
     | 
| 
       2451 
     | 
    
         
            -
             
     | 
| 
       2452 
     | 
    
         
            -
             
     | 
| 
       2453 
     | 
    
         
            -
             
     | 
| 
       2454 
     | 
    
         
            -
             
     | 
| 
       2455 
     | 
    
         
            -
             
     | 
| 
       2456 
     | 
    
         
            -
             
     | 
| 
       2457 
     | 
    
         
            -
             
     | 
| 
       2458 
     | 
    
         
            -
             
     | 
| 
       2459 
     | 
    
         
            -
                     
     | 
| 
       2460 
     | 
    
         
            -
                    < 
     | 
| 
       2461 
     | 
    
         
            -
                    <colgroup>
         
     | 
| 
       2462 
     | 
    
         
            -
                    <col style="text-align:left;"/>
         
     | 
| 
       2463 
     | 
    
         
            -
                    <col style="text-align:left;"/>
         
     | 
| 
       2464 
     | 
    
         
            -
                    </colgroup>
         
     | 
| 
       2465 
     | 
    
         
            -
                    <thead>
         
     | 
| 
      
 1979 
     | 
    
         
            +
                  sorted_tags_data.reverse! if sort_order =~ /^asc/i
         
     | 
| 
      
 1980 
     | 
    
         
            +
                  case format
         
     | 
| 
      
 1981 
     | 
    
         
            +
                  when :html
         
     | 
| 
      
 1982 
     | 
    
         
            +
             
     | 
| 
      
 1983 
     | 
    
         
            +
                    output = <<EOS
         
     | 
| 
      
 1984 
     | 
    
         
            +
                      <table>
         
     | 
| 
      
 1985 
     | 
    
         
            +
                      <caption id="tagtotals">Tag Totals</caption>
         
     | 
| 
      
 1986 
     | 
    
         
            +
                      <colgroup>
         
     | 
| 
      
 1987 
     | 
    
         
            +
                      <col style="text-align:left;"/>
         
     | 
| 
      
 1988 
     | 
    
         
            +
                      <col style="text-align:left;"/>
         
     | 
| 
      
 1989 
     | 
    
         
            +
                      </colgroup>
         
     | 
| 
      
 1990 
     | 
    
         
            +
                      <thead>
         
     | 
| 
      
 1991 
     | 
    
         
            +
                      <tr>
         
     | 
| 
      
 1992 
     | 
    
         
            +
                        <th style="text-align:left;">project</th>
         
     | 
| 
      
 1993 
     | 
    
         
            +
                        <th style="text-align:left;">time</th>
         
     | 
| 
      
 1994 
     | 
    
         
            +
                      </tr>
         
     | 
| 
      
 1995 
     | 
    
         
            +
                      </thead>
         
     | 
| 
      
 1996 
     | 
    
         
            +
                      <tbody>
         
     | 
| 
      
 1997 
     | 
    
         
            +
            EOS
         
     | 
| 
      
 1998 
     | 
    
         
            +
                    sorted_tags_data.reverse.each do |k, v|
         
     | 
| 
      
 1999 
     | 
    
         
            +
                      if v > 0
         
     | 
| 
      
 2000 
     | 
    
         
            +
                        output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{'%02d:%02d:%02d' % fmt_time(v)}</td></tr>\n"
         
     | 
| 
      
 2001 
     | 
    
         
            +
                      end
         
     | 
| 
      
 2002 
     | 
    
         
            +
                    end
         
     | 
| 
      
 2003 
     | 
    
         
            +
                    tail = <<EOS
         
     | 
| 
      
 2004 
     | 
    
         
            +
                    <tr>
         
     | 
| 
      
 2005 
     | 
    
         
            +
                      <td style="text-align:left;" colspan="2"></td>
         
     | 
| 
      
 2006 
     | 
    
         
            +
                    </tr>
         
     | 
| 
      
 2007 
     | 
    
         
            +
                    </tbody>
         
     | 
| 
      
 2008 
     | 
    
         
            +
                    <tfoot>
         
     | 
| 
       2466 
2009 
     | 
    
         
             
                    <tr>
         
     | 
| 
       2467 
     | 
    
         
            -
                      < 
     | 
| 
       2468 
     | 
    
         
            -
                      < 
     | 
| 
      
 2010 
     | 
    
         
            +
                      <td style="text-align:left;"><strong>Total</strong></td>
         
     | 
| 
      
 2011 
     | 
    
         
            +
                      <td style="text-align:left;">#{'%02d:%02d:%02d' % fmt_time(total)}</td>
         
     | 
| 
       2469 
2012 
     | 
    
         
             
                    </tr>
         
     | 
| 
       2470 
     | 
    
         
            -
                    </ 
     | 
| 
       2471 
     | 
    
         
            -
                     
     | 
| 
      
 2013 
     | 
    
         
            +
                    </tfoot>
         
     | 
| 
      
 2014 
     | 
    
         
            +
                    </table>
         
     | 
| 
       2472 
2015 
     | 
    
         
             
            EOS
         
     | 
| 
       2473 
     | 
    
         
            -
             
     | 
| 
       2474 
     | 
    
         
            -
             
     | 
| 
       2475 
     | 
    
         
            -
             
     | 
| 
      
 2016 
     | 
    
         
            +
                    output + tail
         
     | 
| 
      
 2017 
     | 
    
         
            +
                  when :markdown
         
     | 
| 
      
 2018 
     | 
    
         
            +
                    pad = sorted_tags_data.map {|k, v| k }.group_by(&:size).max.last[0].length
         
     | 
| 
      
 2019 
     | 
    
         
            +
                    output = <<~EOS
         
     | 
| 
      
 2020 
     | 
    
         
            +
              | #{' ' * (pad - 7) }project | time     |
         
     | 
| 
      
 2021 
     | 
    
         
            +
              | #{'-' * (pad - 1)}: | :------- |
         
     | 
| 
      
 2022 
     | 
    
         
            +
                    EOS
         
     | 
| 
      
 2023 
     | 
    
         
            +
                    sorted_tags_data.reverse.each do |k, v|
         
     | 
| 
      
 2024 
     | 
    
         
            +
                      if v > 0
         
     | 
| 
      
 2025 
     | 
    
         
            +
                        output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' % fmt_time(v)} |\n"
         
     | 
| 
      
 2026 
     | 
    
         
            +
                      end
         
     | 
| 
       2476 
2027 
     | 
    
         
             
                    end
         
     | 
| 
       2477 
     | 
    
         
            -
             
     | 
| 
       2478 
     | 
    
         
            -
             
     | 
| 
       2479 
     | 
    
         
            -
                   
     | 
| 
       2480 
     | 
    
         
            -
                     
     | 
| 
       2481 
     | 
    
         
            -
             
     | 
| 
       2482 
     | 
    
         
            -
             
     | 
| 
       2483 
     | 
    
         
            -
             
     | 
| 
       2484 
     | 
    
         
            -
             
     | 
| 
       2485 
     | 
    
         
            -
             
     | 
| 
       2486 
     | 
    
         
            -
             
     | 
| 
       2487 
     | 
    
         
            -
             
     | 
| 
       2488 
     | 
    
         
            -
                  </tfoot>
         
     | 
| 
       2489 
     | 
    
         
            -
                  </table>
         
     | 
| 
       2490 
     | 
    
         
            -
            EOS
         
     | 
| 
       2491 
     | 
    
         
            -
                  output + tail
         
     | 
| 
       2492 
     | 
    
         
            -
                when :markdown
         
     | 
| 
       2493 
     | 
    
         
            -
                  pad = sorted_tags_data.map {|k, v| k }.group_by(&:size).max.last[0].length
         
     | 
| 
       2494 
     | 
    
         
            -
                  output = <<-EOS
         
     | 
| 
       2495 
     | 
    
         
            -
            | #{' ' * (pad - 7) }project | time     |
         
     | 
| 
       2496 
     | 
    
         
            -
            | #{'-' * (pad - 1)}: | :------- |
         
     | 
| 
       2497 
     | 
    
         
            -
                  EOS
         
     | 
| 
       2498 
     | 
    
         
            -
                  sorted_tags_data.reverse.each do |k, v|
         
     | 
| 
       2499 
     | 
    
         
            -
                    if v > 0
         
     | 
| 
       2500 
     | 
    
         
            -
                      output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' % fmt_time(v)} |\n"
         
     | 
| 
      
 2028 
     | 
    
         
            +
                    tail = "[Tag Totals]"
         
     | 
| 
      
 2029 
     | 
    
         
            +
                    output + tail
         
     | 
| 
      
 2030 
     | 
    
         
            +
                  when :json
         
     | 
| 
      
 2031 
     | 
    
         
            +
                    output = []
         
     | 
| 
      
 2032 
     | 
    
         
            +
                    sorted_tags_data.reverse.each do |k, v|
         
     | 
| 
      
 2033 
     | 
    
         
            +
                      d, h, m = fmt_time(v)
         
     | 
| 
      
 2034 
     | 
    
         
            +
                      output << {
         
     | 
| 
      
 2035 
     | 
    
         
            +
                        'tag' => k,
         
     | 
| 
      
 2036 
     | 
    
         
            +
                        'seconds' => v,
         
     | 
| 
      
 2037 
     | 
    
         
            +
                        'formatted' => format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
         
     | 
| 
      
 2038 
     | 
    
         
            +
                      }
         
     | 
| 
       2501 
2039 
     | 
    
         
             
                    end
         
     | 
| 
       2502 
     | 
    
         
            -
             
     | 
| 
       2503 
     | 
    
         
            -
                   
     | 
| 
       2504 
     | 
    
         
            -
             
     | 
| 
       2505 
     | 
    
         
            -
             
     | 
| 
       2506 
     | 
    
         
            -
             
     | 
| 
       2507 
     | 
    
         
            -
             
     | 
| 
       2508 
     | 
    
         
            -
             
     | 
| 
       2509 
     | 
    
         
            -
                       
     | 
| 
       2510 
     | 
    
         
            -
                       
     | 
| 
       2511 
     | 
    
         
            -
                      ' 
     | 
| 
       2512 
     | 
    
         
            -
                    }
         
     | 
| 
       2513 
     | 
    
         
            -
                  end
         
     | 
| 
       2514 
     | 
    
         
            -
                  output
         
     | 
| 
       2515 
     | 
    
         
            -
                else
         
     | 
| 
       2516 
     | 
    
         
            -
                  output = []
         
     | 
| 
       2517 
     | 
    
         
            -
                  sorted_tags_data.reverse.each do |k, v|
         
     | 
| 
       2518 
     | 
    
         
            -
                    spacer = ''
         
     | 
| 
       2519 
     | 
    
         
            -
                    (max - k.length).times do
         
     | 
| 
       2520 
     | 
    
         
            -
                      spacer += ' '
         
     | 
| 
      
 2040 
     | 
    
         
            +
                    output
         
     | 
| 
      
 2041 
     | 
    
         
            +
                  else
         
     | 
| 
      
 2042 
     | 
    
         
            +
                    output = []
         
     | 
| 
      
 2043 
     | 
    
         
            +
                    sorted_tags_data.reverse.each do |k, v|
         
     | 
| 
      
 2044 
     | 
    
         
            +
                      spacer = ''
         
     | 
| 
      
 2045 
     | 
    
         
            +
                      (max - k.length).times do
         
     | 
| 
      
 2046 
     | 
    
         
            +
                        spacer += ' '
         
     | 
| 
      
 2047 
     | 
    
         
            +
                      end
         
     | 
| 
      
 2048 
     | 
    
         
            +
                      d, h, m = fmt_time(v)
         
     | 
| 
      
 2049 
     | 
    
         
            +
                      output.push("#{k}:#{spacer}#{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}")
         
     | 
| 
       2521 
2050 
     | 
    
         
             
                    end
         
     | 
| 
       2522 
     | 
    
         
            -
                    output.push("#{k}:#{spacer}#{'%02d:%02d:%02d' % fmt_time(v)}")
         
     | 
| 
       2523 
     | 
    
         
            -
                  end
         
     | 
| 
       2524 
2051 
     | 
    
         | 
| 
       2525 
     | 
    
         
            -
             
     | 
| 
       2526 
     | 
    
         
            -
             
     | 
| 
       2527 
     | 
    
         
            -
             
     | 
| 
       2528 
     | 
    
         
            -
             
     | 
| 
       2529 
     | 
    
         
            -
              end
         
     | 
| 
       2530 
     | 
    
         
            -
             
     | 
| 
       2531 
     | 
    
         
            -
              # @brief Uses 'autotag' configuration to turn keywords into tags for time tracking.
         
     | 
| 
       2532 
     | 
    
         
            -
              # Does not repeat tags in a title, and only converts the first instance of an
         
     | 
| 
       2533 
     | 
    
         
            -
              # untagged keyword
         
     | 
| 
       2534 
     | 
    
         
            -
              #
         
     | 
| 
       2535 
     | 
    
         
            -
              # @param      text  (String) The text to tag
         
     | 
| 
       2536 
     | 
    
         
            -
              #
         
     | 
| 
       2537 
     | 
    
         
            -
              def autotag(text)
         
     | 
| 
       2538 
     | 
    
         
            -
                return unless text
         
     | 
| 
       2539 
     | 
    
         
            -
                return text unless @auto_tag
         
     | 
| 
       2540 
     | 
    
         
            -
             
     | 
| 
       2541 
     | 
    
         
            -
                current_tags = text.scan(/@\w+/)
         
     | 
| 
       2542 
     | 
    
         
            -
                whitelisted = []
         
     | 
| 
       2543 
     | 
    
         
            -
                @config['autotag']['whitelist'].each do |tag|
         
     | 
| 
       2544 
     | 
    
         
            -
                  next if text =~ /@#{tag}\b/i
         
     | 
| 
       2545 
     | 
    
         
            -
             
     | 
| 
       2546 
     | 
    
         
            -
                  text.sub!(/(?<!@)(#{tag.strip})\b/i) do |m|
         
     | 
| 
       2547 
     | 
    
         
            -
                    m.downcase! if tag =~ /[a-z]/
         
     | 
| 
       2548 
     | 
    
         
            -
                    whitelisted.push("@#{m}")
         
     | 
| 
       2549 
     | 
    
         
            -
                    "@#{m}"
         
     | 
| 
      
 2052 
     | 
    
         
            +
                    output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
         
     | 
| 
      
 2053 
     | 
    
         
            +
                    d, h, m = fmt_time(total)
         
     | 
| 
      
 2054 
     | 
    
         
            +
                    output += "\n\nTotal tracked: #{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}\n"
         
     | 
| 
      
 2055 
     | 
    
         
            +
                    output
         
     | 
| 
       2550 
2056 
     | 
    
         
             
                  end
         
     | 
| 
       2551 
2057 
     | 
    
         
             
                end
         
     | 
| 
       2552 
     | 
    
         
            -
                tail_tags = []
         
     | 
| 
       2553 
     | 
    
         
            -
                @config['autotag']['synonyms'].each do |tag, v|
         
     | 
| 
       2554 
     | 
    
         
            -
                  v.each do |word|
         
     | 
| 
       2555 
     | 
    
         
            -
                    next unless text =~ /\b#{word}\b/i
         
     | 
| 
       2556 
2058 
     | 
    
         | 
| 
       2557 
     | 
    
         
            -
             
     | 
| 
      
 2059 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2060 
     | 
    
         
            +
                ## @brief      Gets the interval between entry's start
         
     | 
| 
      
 2061 
     | 
    
         
            +
                ##             date and @done date
         
     | 
| 
      
 2062 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2063 
     | 
    
         
            +
                ## @param      item       (Hash) The entry
         
     | 
| 
      
 2064 
     | 
    
         
            +
                ## @param      formatted  (Bool) Return human readable
         
     | 
| 
      
 2065 
     | 
    
         
            +
                ##                        time (default seconds)
         
     | 
| 
      
 2066 
     | 
    
         
            +
                ## @param      record     (Bool) Add the interval to the
         
     | 
| 
      
 2067 
     | 
    
         
            +
                ##                        total for each tag
         
     | 
| 
      
 2068 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2069 
     | 
    
         
            +
                ## @return     Interval in seconds, or [d, h, m] array if
         
     | 
| 
      
 2070 
     | 
    
         
            +
                ##             formatted is true. False if no end date or
         
     | 
| 
      
 2071 
     | 
    
         
            +
                ##             interval is 0
         
     | 
| 
      
 2072 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2073 
     | 
    
         
            +
                def get_interval(item, formatted: true, record: true)
         
     | 
| 
      
 2074 
     | 
    
         
            +
                  if item.interval
         
     | 
| 
      
 2075 
     | 
    
         
            +
                    seconds = item.interval
         
     | 
| 
      
 2076 
     | 
    
         
            +
                    record_tag_times(item, seconds) if record
         
     | 
| 
      
 2077 
     | 
    
         
            +
                    return seconds.positive? ? seconds : false unless formatted
         
     | 
| 
      
 2078 
     | 
    
         
            +
             
     | 
| 
      
 2079 
     | 
    
         
            +
                    return seconds.positive? ? format('%02d:%02d:%02d', *fmt_time(seconds)) : false
         
     | 
| 
       2558 
2080 
     | 
    
         
             
                  end
         
     | 
| 
       2559 
     | 
    
         
            -
             
     | 
| 
       2560 
     | 
    
         
            -
             
     | 
| 
       2561 
     | 
    
         
            -
             
     | 
| 
       2562 
     | 
    
         
            -
             
     | 
| 
       2563 
     | 
    
         
            -
             
     | 
| 
       2564 
     | 
    
         
            -
             
     | 
| 
       2565 
     | 
    
         
            -
             
     | 
| 
       2566 
     | 
    
         
            -
             
     | 
| 
       2567 
     | 
    
         
            -
             
     | 
| 
       2568 
     | 
    
         
            -
             
     | 
| 
       2569 
     | 
    
         
            -
             
     | 
| 
       2570 
     | 
    
         
            -
             
     | 
| 
       2571 
     | 
    
         
            -
             
     | 
| 
       2572 
     | 
    
         
            -
                     
     | 
| 
       2573 
     | 
    
         
            -
             
     | 
| 
       2574 
     | 
    
         
            -
                       
     | 
| 
       2575 
     | 
    
         
            -
             
     | 
| 
       2576 
     | 
    
         
            -
             
     | 
| 
       2577 
     | 
    
         
            -
                          new_tag = new_tag.gsub('\\' + index.to_s, v)
         
     | 
| 
       2578 
     | 
    
         
            -
                          index += 1
         
     | 
| 
       2579 
     | 
    
         
            -
                        end
         
     | 
| 
       2580 
     | 
    
         
            -
                      end
         
     | 
| 
       2581 
     | 
    
         
            -
                      tail_tags.push(new_tag)
         
     | 
| 
      
 2081 
     | 
    
         
            +
             
     | 
| 
      
 2082 
     | 
    
         
            +
                  false
         
     | 
| 
      
 2083 
     | 
    
         
            +
                end
         
     | 
| 
      
 2084 
     | 
    
         
            +
             
     | 
| 
      
 2085 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2086 
     | 
    
         
            +
                ## @brief      Record times for item tags
         
     | 
| 
      
 2087 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2088 
     | 
    
         
            +
                ## @param      item  The item
         
     | 
| 
      
 2089 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2090 
     | 
    
         
            +
                def record_tag_times(item, seconds)
         
     | 
| 
      
 2091 
     | 
    
         
            +
                  item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
         
     | 
| 
      
 2092 
     | 
    
         
            +
                  return if @recorded_items.include?(item_hash)
         
     | 
| 
      
 2093 
     | 
    
         
            +
                  item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
         
     | 
| 
      
 2094 
     | 
    
         
            +
                    k = m[0] == 'done' ? 'All' : m[0].downcase
         
     | 
| 
      
 2095 
     | 
    
         
            +
                    if @timers.key?(k)
         
     | 
| 
      
 2096 
     | 
    
         
            +
                      @timers[k] += seconds
         
     | 
| 
      
 2097 
     | 
    
         
            +
                    else
         
     | 
| 
      
 2098 
     | 
    
         
            +
                      @timers[k] = seconds
         
     | 
| 
       2582 
2099 
     | 
    
         
             
                    end
         
     | 
| 
      
 2100 
     | 
    
         
            +
                    @recorded_items.push(item_hash)
         
     | 
| 
       2583 
2101 
     | 
    
         
             
                  end
         
     | 
| 
       2584 
2102 
     | 
    
         
             
                end
         
     | 
| 
       2585 
     | 
    
         
            -
                @results.push("Whitelisted tags: #{whitelisted.join(', ')}") if whitelisted.length > 0
         
     | 
| 
       2586 
     | 
    
         
            -
                if tail_tags.length > 0
         
     | 
| 
       2587 
     | 
    
         
            -
                  tags = tail_tags.uniq.map { |t| '@' + t }.join(' ')
         
     | 
| 
       2588 
     | 
    
         
            -
                  @results.push("Synonym tags: #{tags}")
         
     | 
| 
       2589 
     | 
    
         
            -
                  text + ' ' + tags
         
     | 
| 
       2590 
     | 
    
         
            -
                else
         
     | 
| 
       2591 
     | 
    
         
            -
                  text
         
     | 
| 
       2592 
     | 
    
         
            -
                end
         
     | 
| 
       2593 
     | 
    
         
            -
              end
         
     | 
| 
       2594 
2103 
     | 
    
         | 
| 
       2595 
     | 
    
         
            -
             
     | 
| 
      
 2104 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2105 
     | 
    
         
            +
                ## @brief      Format human readable time from seconds
         
     | 
| 
      
 2106 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2107 
     | 
    
         
            +
                ## @param      seconds  The seconds
         
     | 
| 
      
 2108 
     | 
    
         
            +
                ##
         
     | 
| 
      
 2109 
     | 
    
         
            +
                def fmt_time(seconds)
         
     | 
| 
      
 2110 
     | 
    
         
            +
                  return [0, 0, 0] if seconds.nil?
         
     | 
| 
       2596 
2111 
     | 
    
         | 
| 
       2597 
     | 
    
         
            -
             
     | 
| 
       2598 
     | 
    
         
            -
             
     | 
| 
       2599 
     | 
    
         
            -
             
     | 
| 
       2600 
     | 
    
         
            -
             
     | 
| 
       2601 
     | 
    
         
            -
             
     | 
| 
       2602 
     | 
    
         
            -
             
     | 
| 
       2603 
     | 
    
         
            -
             
     | 
| 
       2604 
     | 
    
         
            -
             
     | 
| 
       2605 
     | 
    
         
            -
             
     | 
| 
       2606 
     | 
    
         
            -
             
     | 
| 
       2607 
     | 
    
         
            -
             
     | 
| 
       2608 
     | 
    
         
            -
                   
     | 
| 
       2609 
     | 
    
         
            -
                  record_tag_times(item, seconds) if record
         
     | 
| 
       2610 
     | 
    
         
            -
                  return seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
         
     | 
| 
      
 2112 
     | 
    
         
            +
                  if seconds.class == String && seconds =~ /(\d+):(\d+):(\d+)/
         
     | 
| 
      
 2113 
     | 
    
         
            +
                    h = Regexp.last_match(1)
         
     | 
| 
      
 2114 
     | 
    
         
            +
                    m = Regexp.last_match(2)
         
     | 
| 
      
 2115 
     | 
    
         
            +
                    s = Regexp.last_match(3)
         
     | 
| 
      
 2116 
     | 
    
         
            +
                    seconds = (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
         
     | 
| 
      
 2117 
     | 
    
         
            +
                  end
         
     | 
| 
      
 2118 
     | 
    
         
            +
                  minutes = (seconds / 60).to_i
         
     | 
| 
      
 2119 
     | 
    
         
            +
                  hours = (minutes / 60).to_i
         
     | 
| 
      
 2120 
     | 
    
         
            +
                  days = (hours / 24).to_i
         
     | 
| 
      
 2121 
     | 
    
         
            +
                  hours = (hours % 24).to_i
         
     | 
| 
      
 2122 
     | 
    
         
            +
                  minutes = (minutes % 60).to_i
         
     | 
| 
      
 2123 
     | 
    
         
            +
                  [days, hours, minutes]
         
     | 
| 
       2611 
2124 
     | 
    
         
             
                end
         
     | 
| 
       2612 
2125 
     | 
    
         | 
| 
       2613 
     | 
    
         
            -
                 
     | 
| 
       2614 
     | 
    
         
            -
                  done = Time.parse(Regexp.last_match(1))
         
     | 
| 
       2615 
     | 
    
         
            -
                else
         
     | 
| 
       2616 
     | 
    
         
            -
                  return false
         
     | 
| 
       2617 
     | 
    
         
            -
                end
         
     | 
| 
      
 2126 
     | 
    
         
            +
                private
         
     | 
| 
       2618 
2127 
     | 
    
         | 
| 
       2619 
     | 
    
         
            -
                 
     | 
| 
       2620 
     | 
    
         
            -
             
     | 
| 
       2621 
     | 
    
         
            -
                        else
         
     | 
| 
       2622 
     | 
    
         
            -
                          item['date']
         
     | 
| 
       2623 
     | 
    
         
            -
                        end
         
     | 
| 
      
 2128 
     | 
    
         
            +
                def run_after
         
     | 
| 
      
 2129 
     | 
    
         
            +
                  return unless @config.key?('run_after')
         
     | 
| 
       2624 
2130 
     | 
    
         | 
| 
       2625 
     | 
    
         
            -
             
     | 
| 
      
 2131 
     | 
    
         
            +
                  _, stderr, status = Open3.capture3(@config['run_after'])
         
     | 
| 
      
 2132 
     | 
    
         
            +
                  return unless status.exitstatus.positive?
         
     | 
| 
       2626 
2133 
     | 
    
         | 
| 
       2627 
     | 
    
         
            -
             
     | 
| 
       2628 
     | 
    
         
            -
                   
     | 
| 
      
 2134 
     | 
    
         
            +
                  logger.log_now(:error, 'Script error:', "Error running #{@config['run_after']}")
         
     | 
| 
      
 2135 
     | 
    
         
            +
                  logger.log_now(:error, 'STDERR output:', stderr)
         
     | 
| 
       2629 
2136 
     | 
    
         
             
                end
         
     | 
| 
       2630 
2137 
     | 
    
         | 
| 
       2631 
     | 
    
         
            -
                 
     | 
| 
       2632 
     | 
    
         
            -
             
     | 
| 
       2633 
     | 
    
         
            -
             
     | 
| 
       2634 
     | 
    
         
            -
             
     | 
| 
       2635 
     | 
    
         
            -
                seconds > 0 ? '%02d:%02d:%02d' % fmt_time(seconds) : false
         
     | 
| 
       2636 
     | 
    
         
            -
              end
         
     | 
| 
       2637 
     | 
    
         
            -
             
     | 
| 
       2638 
     | 
    
         
            -
              ##
         
     | 
| 
       2639 
     | 
    
         
            -
              ## @brief      Record times for item tags
         
     | 
| 
       2640 
     | 
    
         
            -
              ##
         
     | 
| 
       2641 
     | 
    
         
            -
              ## @param      item  The item
         
     | 
| 
       2642 
     | 
    
         
            -
              ##
         
     | 
| 
       2643 
     | 
    
         
            -
              def record_tag_times(item, seconds)
         
     | 
| 
       2644 
     | 
    
         
            -
                return if @recorded_items.include?(item)
         
     | 
| 
       2645 
     | 
    
         
            -
             
     | 
| 
       2646 
     | 
    
         
            -
                item['title'].scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
         
     | 
| 
       2647 
     | 
    
         
            -
                  k = m[0] == 'done' ? 'All' : m[0].downcase
         
     | 
| 
       2648 
     | 
    
         
            -
                  if @timers.key?(k)
         
     | 
| 
       2649 
     | 
    
         
            -
                    @timers[k] += seconds
         
     | 
| 
      
 2138 
     | 
    
         
            +
                def log_change(tags_added: [], tags_removed: [], count: 1)
         
     | 
| 
      
 2139 
     | 
    
         
            +
                  if tags_added.empty? && tags_removed.empty?
         
     | 
| 
      
 2140 
     | 
    
         
            +
                    logger.count(:skipped, level: :debug, message: '%count %items with no change', count: count)
         
     | 
| 
       2650 
2141 
     | 
    
         
             
                  else
         
     | 
| 
       2651 
     | 
    
         
            -
                    @timers[k] = seconds
         
     | 
| 
       2652 
     | 
    
         
            -
                  end
         
     | 
| 
       2653 
     | 
    
         
            -
                  @recorded_items.push(item)
         
     | 
| 
       2654 
     | 
    
         
            -
                end
         
     | 
| 
       2655 
     | 
    
         
            -
              end
         
     | 
| 
       2656 
2142 
     | 
    
         | 
| 
       2657 
     | 
    
         
            -
             
     | 
| 
       2658 
     | 
    
         
            -
             
     | 
| 
       2659 
     | 
    
         
            -
             
     | 
| 
       2660 
     | 
    
         
            -
             
     | 
| 
       2661 
     | 
    
         
            -
             
     | 
| 
       2662 
     | 
    
         
            -
             
     | 
| 
       2663 
     | 
    
         
            -
             
     | 
| 
       2664 
     | 
    
         
            -
             
     | 
| 
       2665 
     | 
    
         
            -
                if seconds =~ /(\d+):(\d+):(\d+)/
         
     | 
| 
       2666 
     | 
    
         
            -
                  h = Regexp.last_match(1)
         
     | 
| 
       2667 
     | 
    
         
            -
                  m = Regexp.last_match(2)
         
     | 
| 
       2668 
     | 
    
         
            -
                  s = Regexp.last_match(3)
         
     | 
| 
       2669 
     | 
    
         
            -
                  seconds = (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
         
     | 
| 
       2670 
     | 
    
         
            -
                end
         
     | 
| 
       2671 
     | 
    
         
            -
                minutes = (seconds / 60).to_i
         
     | 
| 
       2672 
     | 
    
         
            -
                hours = (minutes / 60).to_i
         
     | 
| 
       2673 
     | 
    
         
            -
                days = (hours / 24).to_i
         
     | 
| 
       2674 
     | 
    
         
            -
                hours = (hours % 24).to_i
         
     | 
| 
       2675 
     | 
    
         
            -
                minutes = (minutes % 60).to_i
         
     | 
| 
       2676 
     | 
    
         
            -
                [days, hours, minutes]
         
     | 
| 
       2677 
     | 
    
         
            -
              end
         
     | 
| 
      
 2143 
     | 
    
         
            +
                    if tags_added.empty?
         
     | 
| 
      
 2144 
     | 
    
         
            +
                      logger.count(:skipped, level: :debug, message: 'no tags added to %count %items')
         
     | 
| 
      
 2145 
     | 
    
         
            +
                      # logger.debug('No tags added:', %("#{item.title}" in #{item.section}))
         
     | 
| 
      
 2146 
     | 
    
         
            +
                    else
         
     | 
| 
      
 2147 
     | 
    
         
            +
                      logger.count(:added_tags, tag: tags_added, message: '%tags added to %count %items')
         
     | 
| 
      
 2148 
     | 
    
         
            +
                      # logger.info('Added tags:', %(#{did_add} to "#{item.title}" in #{item.section}))
         
     | 
| 
      
 2149 
     | 
    
         
            +
                    end
         
     | 
| 
       2678 
2150 
     | 
    
         | 
| 
       2679 
     | 
    
         
            -
             
     | 
| 
       2680 
     | 
    
         
            -
             
     | 
| 
       2681 
     | 
    
         
            -
             
     | 
| 
       2682 
     | 
    
         
            -
             
     | 
| 
       2683 
     | 
    
         
            -
             
     | 
| 
      
 2151 
     | 
    
         
            +
                    if tags_removed.empty?
         
     | 
| 
      
 2152 
     | 
    
         
            +
                      logger.count(:skipped, level: :debug, message: 'no tags removed from %count %items')
         
     | 
| 
      
 2153 
     | 
    
         
            +
                    else
         
     | 
| 
      
 2154 
     | 
    
         
            +
                      logger.count(:removed_tags, tag: tags_removed, message: '%tags removed from %count %items')
         
     | 
| 
      
 2155 
     | 
    
         
            +
                    end
         
     | 
| 
      
 2156 
     | 
    
         
            +
                  end
         
     | 
| 
       2684 
2157 
     | 
    
         
             
                end
         
     | 
| 
       2685 
2158 
     | 
    
         
             
              end
         
     | 
| 
       2686 
2159 
     | 
    
         
             
            end
         
     |