na 1.2.80 → 1.2.81
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/.rubocop.yml +8 -2
 - data/.rubocop_todo.yml +33 -538
 - data/CHANGELOG.md +19 -0
 - data/Gemfile +2 -0
 - data/Gemfile.lock +27 -10
 - data/README.md +46 -5
 - data/Rakefile +6 -0
 - data/bin/commands/next.rb +4 -0
 - data/bin/commands/scan.rb +84 -0
 - data/bin/commands/update.rb +1 -1
 - data/bin/na +7 -7
 - data/lib/na/action.rb +101 -35
 - data/lib/na/actions.rb +79 -77
 - data/lib/na/array.rb +11 -7
 - data/lib/na/benchmark.rb +21 -9
 - data/lib/na/colors.rb +84 -86
 - data/lib/na/editor.rb +22 -22
 - data/lib/na/hash.rb +32 -9
 - data/lib/na/help_monkey_patch.rb +9 -1
 - data/lib/na/next_action.rb +314 -245
 - data/lib/na/pager.rb +38 -14
 - data/lib/na/project.rb +14 -1
 - data/lib/na/prompt.rb +25 -3
 - data/lib/na/string.rb +90 -130
 - data/lib/na/theme.rb +37 -31
 - data/lib/na/todo.rb +149 -131
 - data/lib/na/version.rb +3 -1
 - data/lib/na.rb +1 -0
 - data/na.gemspec +4 -2
 - data/scripts/generate-fish-completions.rb +18 -21
 - data/src/_README.md +14 -4
 - data/test_performance.rb +5 -5
 - metadata +53 -24
 
    
        data/lib/na/next_action.rb
    CHANGED
    
    | 
         @@ -5,27 +5,20 @@ module NA 
     | 
|
| 
       5 
5 
     | 
    
         
             
              class << self
         
     | 
| 
       6 
6 
     | 
    
         
             
                include NA::Editor
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
     | 
    
         
            -
                attr_accessor :verbose, :extension, :include_ext, :na_tag, :command_line, :command, :globals, :global_file, 
     | 
| 
      
 8 
     | 
    
         
            +
                attr_accessor :verbose, :extension, :include_ext, :na_tag, :command_line, :command, :globals, :global_file,
         
     | 
| 
      
 9 
     | 
    
         
            +
                              :cwd_is, :cwd, :stdin, :show_cwd_indicator
         
     | 
| 
       9 
10 
     | 
    
         | 
| 
       10 
11 
     | 
    
         
             
                def theme
         
     | 
| 
       11 
12 
     | 
    
         
             
                  @theme ||= NA::Theme.load_theme
         
     | 
| 
       12 
13 
     | 
    
         
             
                end
         
     | 
| 
       13 
14 
     | 
    
         | 
| 
       14 
     | 
    
         
            -
                ##
         
     | 
| 
       15 
     | 
    
         
            -
                ## Output to STDERR
         
     | 
| 
       16 
     | 
    
         
            -
                ##
         
     | 
| 
       17 
     | 
    
         
            -
                ## @param      msg        [String] The message
         
     | 
| 
       18 
     | 
    
         
            -
                ## @param      exit_code  [Number] The exit code, no
         
     | 
| 
       19 
     | 
    
         
            -
                ##                        exit if false
         
     | 
| 
       20 
     | 
    
         
            -
                ## @param      debug      [Boolean] only display message if running :verbose
         
     | 
| 
       21 
     | 
    
         
            -
                ##
         
     | 
| 
       22 
15 
     | 
    
         
             
                def notify(msg, exit_code: false, debug: false)
         
     | 
| 
       23 
16 
     | 
    
         
             
                  return if debug && !NA.verbose
         
     | 
| 
       24 
17 
     | 
    
         | 
| 
       25 
18 
     | 
    
         
             
                  if debug
         
     | 
| 
       26 
     | 
    
         
            -
                     
     | 
| 
      
 19 
     | 
    
         
            +
                    warn NA::Color.template("{x}#{NA.theme[:debug]}#{msg}{x}")
         
     | 
| 
       27 
20 
     | 
    
         
             
                  else
         
     | 
| 
       28 
     | 
    
         
            -
                     
     | 
| 
      
 21 
     | 
    
         
            +
                    warn NA::Color.template("{x}#{msg}{x}")
         
     | 
| 
       29 
22 
     | 
    
         
             
                  end
         
     | 
| 
       30 
23 
     | 
    
         
             
                  Process.exit exit_code if exit_code
         
     | 
| 
       31 
24 
     | 
    
         
             
                end
         
     | 
| 
         @@ -38,16 +31,16 @@ module NA 
     | 
|
| 
       38 
31 
     | 
    
         
             
                  }
         
     | 
| 
       39 
32 
     | 
    
         
             
                end
         
     | 
| 
       40 
33 
     | 
    
         | 
| 
       41 
     | 
    
         
            -
                 
     | 
| 
       42 
     | 
    
         
            -
                 
     | 
| 
       43 
     | 
    
         
            -
                 
     | 
| 
       44 
     | 
    
         
            -
                 
     | 
| 
       45 
     | 
    
         
            -
                 
     | 
| 
       46 
     | 
    
         
            -
                 
     | 
| 
       47 
     | 
    
         
            -
                 
     | 
| 
       48 
     | 
    
         
            -
                 
     | 
| 
       49 
     | 
    
         
            -
                 
     | 
| 
       50 
     | 
    
         
            -
                 
     | 
| 
      
 34 
     | 
    
         
            +
                #
         
     | 
| 
      
 35 
     | 
    
         
            +
                # Display and read a Yes/No prompt
         
     | 
| 
      
 36 
     | 
    
         
            +
                #
         
     | 
| 
      
 37 
     | 
    
         
            +
                # @param      prompt   [String] The prompt string
         
     | 
| 
      
 38 
     | 
    
         
            +
                # @param      default  [Boolean] default value if
         
     | 
| 
      
 39 
     | 
    
         
            +
                #                      return is pressed or prompt is
         
     | 
| 
      
 40 
     | 
    
         
            +
                #                      skipped
         
     | 
| 
      
 41 
     | 
    
         
            +
                #
         
     | 
| 
      
 42 
     | 
    
         
            +
                # @return     [Boolean] result
         
     | 
| 
      
 43 
     | 
    
         
            +
                #
         
     | 
| 
       51 
44 
     | 
    
         
             
                def yn(prompt, default: true)
         
     | 
| 
       52 
45 
     | 
    
         
             
                  return default unless $stdout.isatty
         
     | 
| 
       53 
46 
     | 
    
         | 
| 
         @@ -63,14 +56,14 @@ module NA 
     | 
|
| 
       63 
56 
     | 
    
         
             
                  res.empty? ? default : res =~ /y/i
         
     | 
| 
       64 
57 
     | 
    
         
             
                end
         
     | 
| 
       65 
58 
     | 
    
         | 
| 
       66 
     | 
    
         
            -
                 
     | 
| 
       67 
     | 
    
         
            -
                 
     | 
| 
       68 
     | 
    
         
            -
                 
     | 
| 
       69 
     | 
    
         
            -
                 
     | 
| 
       70 
     | 
    
         
            -
                 
     | 
| 
       71 
     | 
    
         
            -
                 
     | 
| 
       72 
     | 
    
         
            -
                 
     | 
| 
       73 
     | 
    
         
            -
                 
     | 
| 
      
 59 
     | 
    
         
            +
                #
         
     | 
| 
      
 60 
     | 
    
         
            +
                # Helper function to colorize the Y/N prompt
         
     | 
| 
      
 61 
     | 
    
         
            +
                #
         
     | 
| 
      
 62 
     | 
    
         
            +
                # @param      choices  [Array] The choices with
         
     | 
| 
      
 63 
     | 
    
         
            +
                #                      default capitalized
         
     | 
| 
      
 64 
     | 
    
         
            +
                #
         
     | 
| 
      
 65 
     | 
    
         
            +
                # @return     [String] colorized string
         
     | 
| 
      
 66 
     | 
    
         
            +
                #
         
     | 
| 
       74 
67 
     | 
    
         
             
                def color_single_options(choices = %w[y n])
         
     | 
| 
       75 
68 
     | 
    
         
             
                  out = []
         
     | 
| 
       76 
69 
     | 
    
         
             
                  choices.each do |choice|
         
     | 
| 
         @@ -84,51 +77,48 @@ module NA 
     | 
|
| 
       84 
77 
     | 
    
         
             
                  NA::Color.template("{xg}[#{out.join('/')}{xg}]{x}")
         
     | 
| 
       85 
78 
     | 
    
         
             
                end
         
     | 
| 
       86 
79 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
                 
     | 
| 
       88 
     | 
    
         
            -
                 
     | 
| 
       89 
     | 
    
         
            -
                 
     | 
| 
       90 
     | 
    
         
            -
                 
     | 
| 
       91 
     | 
    
         
            -
                 
     | 
| 
       92 
     | 
    
         
            -
                 
     | 
| 
      
 80 
     | 
    
         
            +
                #
         
     | 
| 
      
 81 
     | 
    
         
            +
                # Create a new todo file
         
     | 
| 
      
 82 
     | 
    
         
            +
                #
         
     | 
| 
      
 83 
     | 
    
         
            +
                # @param      target    [String] The target path
         
     | 
| 
      
 84 
     | 
    
         
            +
                # @param      basename  [String] The project base name
         
     | 
| 
      
 85 
     | 
    
         
            +
                #
         
     | 
| 
       93 
86 
     | 
    
         
             
                def create_todo(target, basename, template: nil)
         
     | 
| 
       94 
87 
     | 
    
         
             
                  File.open(target, 'w') do |f|
         
     | 
| 
       95 
     | 
    
         
            -
                    if template && File.exist?(template)
         
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
             
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
       99 
     | 
    
         
            -
             
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
             
     | 
| 
       102 
     | 
    
         
            -
             
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
             
     | 
| 
       106 
     | 
    
         
            -
             
     | 
| 
       107 
     | 
    
         
            -
             
     | 
| 
       108 
     | 
    
         
            -
             
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
             
     | 
| 
       111 
     | 
    
         
            -
             
     | 
| 
      
 88 
     | 
    
         
            +
                    content = if template && File.exist?(template)
         
     | 
| 
      
 89 
     | 
    
         
            +
                                File.read(template)
         
     | 
| 
      
 90 
     | 
    
         
            +
                              else
         
     | 
| 
      
 91 
     | 
    
         
            +
                                <<~ENDCONTENT
         
     | 
| 
      
 92 
     | 
    
         
            +
                                  Inbox:
         
     | 
| 
      
 93 
     | 
    
         
            +
                                  #{basename}:
         
     | 
| 
      
 94 
     | 
    
         
            +
                                  \tFeature Requests:
         
     | 
| 
      
 95 
     | 
    
         
            +
                                  \tIdeas:
         
     | 
| 
      
 96 
     | 
    
         
            +
                                  \tBugs:
         
     | 
| 
      
 97 
     | 
    
         
            +
                                  Archive:
         
     | 
| 
      
 98 
     | 
    
         
            +
                                  Search Definitions:
         
     | 
| 
      
 99 
     | 
    
         
            +
                                  \tTop Priority @search(@priority = 5 and not @done)
         
     | 
| 
      
 100 
     | 
    
         
            +
                                  \tHigh Priority @search(@priority > 3 and not @done)
         
     | 
| 
      
 101 
     | 
    
         
            +
                                  \tMaybe @search(@maybe)
         
     | 
| 
      
 102 
     | 
    
         
            +
                                  \tNext @search(@#{NA.na_tag} and not @done and not project = "Archive")
         
     | 
| 
      
 103 
     | 
    
         
            +
                                ENDCONTENT
         
     | 
| 
      
 104 
     | 
    
         
            +
                              end
         
     | 
| 
       112 
105 
     | 
    
         
             
                    f.puts(content)
         
     | 
| 
       113 
106 
     | 
    
         
             
                  end
         
     | 
| 
       114 
107 
     | 
    
         
             
                  save_working_dir(target)
         
     | 
| 
       115 
108 
     | 
    
         
             
                  notify("#{NA.theme[:warning]}Created #{NA.theme[:file]}#{target}")
         
     | 
| 
       116 
109 
     | 
    
         
             
                end
         
     | 
| 
       117 
110 
     | 
    
         | 
| 
       118 
     | 
    
         
            -
                 
     | 
| 
       119 
     | 
    
         
            -
                 
     | 
| 
       120 
     | 
    
         
            -
                 
     | 
| 
       121 
     | 
    
         
            -
                 
     | 
| 
       122 
     | 
    
         
            -
                 
     | 
| 
       123 
     | 
    
         
            -
                 
     | 
| 
       124 
     | 
    
         
            -
                 
     | 
| 
       125 
     | 
    
         
            -
                ## @param      multiple  [Boolean] allow multiple selections
         
     | 
| 
       126 
     | 
    
         
            -
                ##
         
     | 
| 
       127 
     | 
    
         
            -
                ## @return [String, Array] array if multiple
         
     | 
| 
      
 111 
     | 
    
         
            +
                # Select from multiple files
         
     | 
| 
      
 112 
     | 
    
         
            +
                #
         
     | 
| 
      
 113 
     | 
    
         
            +
                # If `gum` or `fzf` are available, they'll be used (in that order).
         
     | 
| 
      
 114 
     | 
    
         
            +
                #
         
     | 
| 
      
 115 
     | 
    
         
            +
                # @param files [Array<String>] The files to select from
         
     | 
| 
      
 116 
     | 
    
         
            +
                # @param multiple [Boolean] Allow multiple selections
         
     | 
| 
      
 117 
     | 
    
         
            +
                # @return [String, Array<String>] Selected file(s)
         
     | 
| 
       128 
118 
     | 
    
         
             
                def select_file(files, multiple: false)
         
     | 
| 
       129 
119 
     | 
    
         
             
                  res = choose_from(files, prompt: multiple ? 'Select files' : 'Select a file', multiple: multiple)
         
     | 
| 
       130 
120 
     | 
    
         | 
| 
       131 
     | 
    
         
            -
                  notify("#{NA.theme[:error]}No file selected, cancelled", exit_code: 1) unless res 
     | 
| 
      
 121 
     | 
    
         
            +
                  notify("#{NA.theme[:error]}No file selected, cancelled", exit_code: 1) unless res&.length&.positive?
         
     | 
| 
       132 
122 
     | 
    
         | 
| 
       133 
123 
     | 
    
         
             
                  res
         
     | 
| 
       134 
124 
     | 
    
         
             
                end
         
     | 
| 
         @@ -142,11 +132,25 @@ module NA 
     | 
|
| 
       142 
132 
     | 
    
         
             
                  end
         
     | 
| 
       143 
133 
     | 
    
         
             
                end
         
     | 
| 
       144 
134 
     | 
    
         | 
| 
      
 135 
     | 
    
         
            +
                # Find all projects in a todo file
         
     | 
| 
      
 136 
     | 
    
         
            +
                #
         
     | 
| 
      
 137 
     | 
    
         
            +
                # @param target [String] Path to the todo file
         
     | 
| 
      
 138 
     | 
    
         
            +
                # @return [Array<NA::Project>] List of projects
         
     | 
| 
       145 
139 
     | 
    
         
             
                def find_projects(target)
         
     | 
| 
       146 
140 
     | 
    
         
             
                  todo = NA::Todo.new(require_na: false, file_path: target)
         
     | 
| 
       147 
141 
     | 
    
         
             
                  todo.projects
         
     | 
| 
       148 
142 
     | 
    
         
             
                end
         
     | 
| 
       149 
143 
     | 
    
         | 
| 
      
 144 
     | 
    
         
            +
                # Find actions in a todo file matching criteria
         
     | 
| 
      
 145 
     | 
    
         
            +
                #
         
     | 
| 
      
 146 
     | 
    
         
            +
                # @param target [String] Path to the todo file
         
     | 
| 
      
 147 
     | 
    
         
            +
                # @param search [String, nil] Search string
         
     | 
| 
      
 148 
     | 
    
         
            +
                # @param tagged [String, nil] Tag to filter
         
     | 
| 
      
 149 
     | 
    
         
            +
                # @param all [Boolean] Return all actions
         
     | 
| 
      
 150 
     | 
    
         
            +
                # @param done [Boolean] Include done actions
         
     | 
| 
      
 151 
     | 
    
         
            +
                # @param project [String, nil] Project name
         
     | 
| 
      
 152 
     | 
    
         
            +
                # @param search_note [Boolean] Search notes
         
     | 
| 
      
 153 
     | 
    
         
            +
                # @return [Array] Projects and actions
         
     | 
| 
       150 
154 
     | 
    
         
             
                def find_actions(target, search, tagged = nil, all: false, done: false, project: nil, search_note: true)
         
     | 
| 
       151 
155 
     | 
    
         
             
                  todo = NA::Todo.new({ search: search,
         
     | 
| 
       152 
156 
     | 
    
         
             
                                        search_note: search_note,
         
     | 
| 
         @@ -157,8 +161,9 @@ module NA 
     | 
|
| 
       157 
161 
     | 
    
         
             
                                        done: done })
         
     | 
| 
       158 
162 
     | 
    
         | 
| 
       159 
163 
     | 
    
         
             
                  unless todo.actions.count.positive?
         
     | 
| 
       160 
     | 
    
         
            -
                    NA.notify("#{NA.theme[:error]}No matching actions found in #{File.basename(target, 
     | 
| 
       161 
     | 
    
         
            -
             
     | 
| 
      
 164 
     | 
    
         
            +
                    NA.notify("#{NA.theme[:error]}No matching actions found in #{File.basename(target,
         
     | 
| 
      
 165 
     | 
    
         
            +
                                                                                               ".#{NA.extension}").highlight_filename}")
         
     | 
| 
      
 166 
     | 
    
         
            +
                    return [todo.projects, NA::Actions.new]
         
     | 
| 
       162 
167 
     | 
    
         
             
                  end
         
     | 
| 
       163 
168 
     | 
    
         | 
| 
       164 
169 
     | 
    
         
             
                  return [todo.projects, todo.actions] if todo.actions.count == 1 || all
         
     | 
| 
         @@ -166,7 +171,10 @@ module NA 
     | 
|
| 
       166 
171 
     | 
    
         
             
                  options = todo.actions.map { |action| "#{action.line} % #{action.parent.join('/')} : #{action.action}" }
         
     | 
| 
       167 
172 
     | 
    
         
             
                  res = choose_from(options, prompt: 'Make a selection: ', multiple: true, sorted: true)
         
     | 
| 
       168 
173 
     | 
    
         | 
| 
       169 
     | 
    
         
            -
                   
     | 
| 
      
 174 
     | 
    
         
            +
                  unless res&.length&.positive?
         
     | 
| 
      
 175 
     | 
    
         
            +
                    NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
         
     | 
| 
      
 176 
     | 
    
         
            +
                    return [todo.projects, NA::Actions.new]
         
     | 
| 
      
 177 
     | 
    
         
            +
                  end
         
     | 
| 
       170 
178 
     | 
    
         | 
| 
       171 
179 
     | 
    
         
             
                  selected = NA::Actions.new
         
     | 
| 
       172 
180 
     | 
    
         
             
                  res.each do |result|
         
     | 
| 
         @@ -177,7 +185,13 @@ module NA 
     | 
|
| 
       177 
185 
     | 
    
         
             
                  [todo.projects, selected]
         
     | 
| 
       178 
186 
     | 
    
         
             
                end
         
     | 
| 
       179 
187 
     | 
    
         | 
| 
       180 
     | 
    
         
            -
                 
     | 
| 
      
 188 
     | 
    
         
            +
                # Insert a new project into a todo file
         
     | 
| 
      
 189 
     | 
    
         
            +
                #
         
     | 
| 
      
 190 
     | 
    
         
            +
                # @param target [String] Path to the todo file
         
     | 
| 
      
 191 
     | 
    
         
            +
                # @param project [String] Project name
         
     | 
| 
      
 192 
     | 
    
         
            +
                # @param projects [Array<NA::Project>] Existing projects
         
     | 
| 
      
 193 
     | 
    
         
            +
                # @return [NA::Project] The new project
         
     | 
| 
      
 194 
     | 
    
         
            +
                def insert_project(target, project, _projects)
         
     | 
| 
       181 
195 
     | 
    
         
             
                  path = project.split(%r{[:/]})
         
     | 
| 
       182 
196 
     | 
    
         
             
                  todo = NA::Todo.new(file_path: target)
         
     | 
| 
       183 
197 
     | 
    
         
             
                  built = []
         
     | 
| 
         @@ -207,11 +221,11 @@ module NA 
     | 
|
| 
       207 
221 
     | 
    
         
             
                      indent += 1
         
     | 
| 
       208 
222 
     | 
    
         
             
                    end
         
     | 
| 
       209 
223 
     | 
    
         | 
| 
       210 
     | 
    
         
            -
                    if new_path.join 
     | 
| 
      
 224 
     | 
    
         
            +
                    if new_path.join =~ /Archive/i
         
     | 
| 
       211 
225 
     | 
    
         
             
                      line = todo.projects.last&.last_line || 0
         
     | 
| 
       212 
     | 
    
         
            -
                      content = content.split( 
     | 
| 
      
 226 
     | 
    
         
            +
                      content = content.split("\n").insert(line, input.join("\n")).join("\n")
         
     | 
| 
       213 
227 
     | 
    
         
             
                    else
         
     | 
| 
       214 
     | 
    
         
            -
                      split = content.split( 
     | 
| 
      
 228 
     | 
    
         
            +
                      split = content.split("\n")
         
     | 
| 
       215 
229 
     | 
    
         
             
                      line = todo.projects.first&.line || 0
         
     | 
| 
       216 
230 
     | 
    
         
             
                      before = split.slice(0, line).join("\n")
         
     | 
| 
       217 
231 
     | 
    
         
             
                      after = split.slice(line, split.count - 0).join("\n")
         
     | 
| 
         @@ -227,8 +241,9 @@ module NA 
     | 
|
| 
       227 
241 
     | 
    
         
             
                      input.push("#{"\t" * indent}#{part.cap_first}:")
         
     | 
| 
       228 
242 
     | 
    
         
             
                      indent += 1
         
     | 
| 
       229 
243 
     | 
    
         
             
                    end
         
     | 
| 
       230 
     | 
    
         
            -
                    content = content.split( 
     | 
| 
       231 
     | 
    
         
            -
                    new_project = NA::Project.new(path.map(&:cap_first).join(':'), indent - 1, line + input.count - 1, 
     | 
| 
      
 244 
     | 
    
         
            +
                    content = content.split("\n").insert(line, input.join("\n")).join("\n")
         
     | 
| 
      
 245 
     | 
    
         
            +
                    new_project = NA::Project.new(path.map(&:cap_first).join(':'), indent - 1, line + input.count - 1,
         
     | 
| 
      
 246 
     | 
    
         
            +
                                                  line + input.count - 1)
         
     | 
| 
       232 
247 
     | 
    
         
             
                  end
         
     | 
| 
       233 
248 
     | 
    
         | 
| 
       234 
249 
     | 
    
         
             
                  File.open(target, 'w') do |f|
         
     | 
| 
         @@ -238,6 +253,28 @@ module NA 
     | 
|
| 
       238 
253 
     | 
    
         
             
                  new_project
         
     | 
| 
       239 
254 
     | 
    
         
             
                end
         
     | 
| 
       240 
255 
     | 
    
         | 
| 
      
 256 
     | 
    
         
            +
                # Update actions in a todo file (add, edit, delete, move, etc.)
         
     | 
| 
      
 257 
     | 
    
         
            +
                #
         
     | 
| 
      
 258 
     | 
    
         
            +
                # @param target [String] Path to the todo file
         
     | 
| 
      
 259 
     | 
    
         
            +
                # @param search [String, nil] Search string
         
     | 
| 
      
 260 
     | 
    
         
            +
                # @param search_note [Boolean] Search notes
         
     | 
| 
      
 261 
     | 
    
         
            +
                # @param add [Action, nil] Action to add
         
     | 
| 
      
 262 
     | 
    
         
            +
                # @param add_tag [Array<String>] Tags to add
         
     | 
| 
      
 263 
     | 
    
         
            +
                # @param all [Boolean] Update all matching actions
         
     | 
| 
      
 264 
     | 
    
         
            +
                # @param append [Boolean] Append to project
         
     | 
| 
      
 265 
     | 
    
         
            +
                # @param delete [Boolean] Delete matching actions
         
     | 
| 
      
 266 
     | 
    
         
            +
                # @param done [Boolean] Mark as done
         
     | 
| 
      
 267 
     | 
    
         
            +
                # @param edit [Boolean] Edit matching actions
         
     | 
| 
      
 268 
     | 
    
         
            +
                # @param finish [Boolean] Mark as finished
         
     | 
| 
      
 269 
     | 
    
         
            +
                # @param note [Array<String>] Notes to add
         
     | 
| 
      
 270 
     | 
    
         
            +
                # @param overwrite [Boolean] Overwrite notes
         
     | 
| 
      
 271 
     | 
    
         
            +
                # @param priority [Integer] Priority value
         
     | 
| 
      
 272 
     | 
    
         
            +
                # @param project [String, nil] Project name
         
     | 
| 
      
 273 
     | 
    
         
            +
                # @param move [String, nil] Move to project
         
     | 
| 
      
 274 
     | 
    
         
            +
                # @param remove_tag [Array<String>] Tags to remove
         
     | 
| 
      
 275 
     | 
    
         
            +
                # @param replace [String, nil] Replacement text
         
     | 
| 
      
 276 
     | 
    
         
            +
                # @param tagged [String, nil] Tag to filter
         
     | 
| 
      
 277 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       241 
278 
     | 
    
         
             
                def update_action(target,
         
     | 
| 
       242 
279 
     | 
    
         
             
                                  search,
         
     | 
| 
       243 
280 
     | 
    
         
             
                                  search_note: true,
         
     | 
| 
         @@ -257,7 +294,6 @@ module NA 
     | 
|
| 
       257 
294 
     | 
    
         
             
                                  remove_tag: [],
         
     | 
| 
       258 
295 
     | 
    
         
             
                                  replace: nil,
         
     | 
| 
       259 
296 
     | 
    
         
             
                                  tagged: nil)
         
     | 
| 
       260 
     | 
    
         
            -
             
     | 
| 
       261 
297 
     | 
    
         
             
                  projects = find_projects(target)
         
     | 
| 
       262 
298 
     | 
    
         
             
                  affected_actions = []
         
     | 
| 
       263 
299 
     | 
    
         | 
| 
         @@ -265,9 +301,11 @@ module NA 
     | 
|
| 
       265 
301 
     | 
    
         | 
| 
       266 
302 
     | 
    
         
             
                  if move
         
     | 
| 
       267 
303 
     | 
    
         
             
                    move = move.sub(/:$/, '')
         
     | 
| 
       268 
     | 
    
         
            -
                    target_proj = projects.select { |pr| pr.project =~ /#{move.gsub( 
     | 
| 
      
 304 
     | 
    
         
            +
                    target_proj = projects.select { |pr| pr.project =~ /#{move.gsub(':', '.*?:.*?')}/i }.first
         
     | 
| 
       269 
305 
     | 
    
         
             
                    if target_proj.nil?
         
     | 
| 
       270 
     | 
    
         
            -
                      res = NA.yn( 
     | 
| 
      
 306 
     | 
    
         
            +
                      res = NA.yn(
         
     | 
| 
      
 307 
     | 
    
         
            +
                        NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{move}#{NA.theme[:warning]} doesn't exist, add it"), default: true
         
     | 
| 
      
 308 
     | 
    
         
            +
                      )
         
     | 
| 
       271 
309 
     | 
    
         
             
                      if res
         
     | 
| 
       272 
310 
     | 
    
         
             
                        target_proj = insert_project(target, move, projects)
         
     | 
| 
       273 
311 
     | 
    
         
             
                        projects << target_proj
         
     | 
| 
         @@ -277,7 +315,7 @@ module NA 
     | 
|
| 
       277 
315 
     | 
    
         
             
                    end
         
     | 
| 
       278 
316 
     | 
    
         
             
                  end
         
     | 
| 
       279 
317 
     | 
    
         | 
| 
       280 
     | 
    
         
            -
                  contents = target.read_file.split( 
     | 
| 
      
 318 
     | 
    
         
            +
                  contents = target.read_file.split("\n")
         
     | 
| 
       281 
319 
     | 
    
         | 
| 
       282 
320 
     | 
    
         
             
                  if add.is_a?(Action)
         
     | 
| 
       283 
321 
     | 
    
         
             
                    add_tag ||= []
         
     | 
| 
         @@ -305,7 +343,9 @@ module NA 
     | 
|
| 
       305 
343 
     | 
    
         
             
                    end
         
     | 
| 
       306 
344 
     | 
    
         | 
| 
       307 
345 
     | 
    
         
             
                    if target_proj.nil?
         
     | 
| 
       308 
     | 
    
         
            -
                      res = NA.yn( 
     | 
| 
      
 346 
     | 
    
         
            +
                      res = NA.yn(
         
     | 
| 
      
 347 
     | 
    
         
            +
                        NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{add.project}#{NA.theme[:warning]} doesn't exist, add it"), default: true
         
     | 
| 
      
 348 
     | 
    
         
            +
                      )
         
     | 
| 
       309 
349 
     | 
    
         | 
| 
       310 
350 
     | 
    
         
             
                      if res
         
     | 
| 
       311 
351 
     | 
    
         
             
                        target_proj = insert_project(target, project, projects)
         
     | 
| 
         @@ -314,7 +354,10 @@ module NA 
     | 
|
| 
       314 
354 
     | 
    
         
             
                        NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
         
     | 
| 
       315 
355 
     | 
    
         
             
                      end
         
     | 
| 
       316 
356 
     | 
    
         | 
| 
       317 
     | 
    
         
            -
                       
     | 
| 
      
 357 
     | 
    
         
            +
                      if target_proj.nil?
         
     | 
| 
      
 358 
     | 
    
         
            +
                        NA.notify("#{NA.theme[:error]}Error parsing project #{NA.theme[:filename]}#{target}",
         
     | 
| 
      
 359 
     | 
    
         
            +
                                  exit_code: 1)
         
     | 
| 
      
 360 
     | 
    
         
            +
                      end
         
     | 
| 
       318 
361 
     | 
    
         | 
| 
       319 
362 
     | 
    
         
             
                      projects = find_projects(target)
         
     | 
| 
       320 
363 
     | 
    
         
             
                      contents = target.read_file.split("\n")
         
     | 
| 
         @@ -352,15 +395,16 @@ module NA 
     | 
|
| 
       352 
395 
     | 
    
         
             
                    notify(add.pretty)
         
     | 
| 
       353 
396 
     | 
    
         | 
| 
       354 
397 
     | 
    
         
             
                    # Track affected action and description
         
     | 
| 
       355 
     | 
    
         
            -
                    changes = [ 
     | 
| 
       356 
     | 
    
         
            -
                    changes <<  
     | 
| 
      
 398 
     | 
    
         
            +
                    changes = ['added']
         
     | 
| 
      
 399 
     | 
    
         
            +
                    changes << 'finished' if finish
         
     | 
| 
       357 
400 
     | 
    
         
             
                    changes << "priority=#{priority}" if priority.to_i.positive?
         
     | 
| 
       358 
401 
     | 
    
         
             
                    changes << "tags+#{add_tag.join(',')}" unless add_tag.nil? || add_tag.empty?
         
     | 
| 
       359 
402 
     | 
    
         
             
                    changes << "tags-#{remove_tag.join(',')}" unless remove_tag.nil? || remove_tag.empty?
         
     | 
| 
       360 
     | 
    
         
            -
                    changes <<  
     | 
| 
      
 403 
     | 
    
         
            +
                    changes << 'note updated' unless note.nil? || note.empty?
         
     | 
| 
       361 
404 
     | 
    
         
             
                    affected_actions << { action: add, desc: changes.join(', ') }
         
     | 
| 
       362 
405 
     | 
    
         
             
                  else
         
     | 
| 
       363 
     | 
    
         
            -
                    _, actions = find_actions(target, search, tagged, done: done, all: all, project: project, 
     | 
| 
      
 406 
     | 
    
         
            +
                    _, actions = find_actions(target, search, tagged, done: done, all: all, project: project,
         
     | 
| 
      
 407 
     | 
    
         
            +
                                                                      search_note: search_note)
         
     | 
| 
       364 
408 
     | 
    
         | 
| 
       365 
409 
     | 
    
         
             
                    return if actions.nil?
         
     | 
| 
       366 
410 
     | 
    
         | 
| 
         @@ -425,15 +469,15 @@ module NA 
     | 
|
| 
       425 
469 
     | 
    
         | 
| 
       426 
470 
     | 
    
         
             
                      # Track affected action and description
         
     | 
| 
       427 
471 
     | 
    
         
             
                      changes = []
         
     | 
| 
       428 
     | 
    
         
            -
                      changes <<  
     | 
| 
       429 
     | 
    
         
            -
                      changes <<  
     | 
| 
      
 472 
     | 
    
         
            +
                      changes << 'finished' if finish
         
     | 
| 
      
 473 
     | 
    
         
            +
                      changes << 'edited' if edit
         
     | 
| 
       430 
474 
     | 
    
         
             
                      changes << "priority=#{priority}" if priority.to_i.positive?
         
     | 
| 
       431 
475 
     | 
    
         
             
                      changes << "tags+#{add_tag.join(',')}" unless add_tag.nil? || add_tag.empty?
         
     | 
| 
       432 
476 
     | 
    
         
             
                      changes << "tags-#{remove_tag.join(',')}" unless remove_tag.nil? || remove_tag.empty?
         
     | 
| 
       433 
     | 
    
         
            -
                      changes <<  
     | 
| 
      
 477 
     | 
    
         
            +
                      changes << 'text replaced' if replace
         
     | 
| 
       434 
478 
     | 
    
         
             
                      changes << "moved to #{target_proj.project}" if target_proj
         
     | 
| 
       435 
     | 
    
         
            -
                      changes <<  
     | 
| 
       436 
     | 
    
         
            -
                      changes = [ 
     | 
| 
      
 479 
     | 
    
         
            +
                      changes << 'note updated' unless note.nil? || note.empty?
         
     | 
| 
      
 480 
     | 
    
         
            +
                      changes = ['updated'] if changes.empty?
         
     | 
| 
       437 
481 
     | 
    
         
             
                      affected_actions << { action: action, desc: changes.join(', ') }
         
     | 
| 
       438 
482 
     | 
    
         
             
                    end
         
     | 
| 
       439 
483 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -454,23 +498,23 @@ module NA 
     | 
|
| 
       454 
498 
     | 
    
         
             
                      action_color = delete ? NA.theme[:error] : NA.theme[:success]
         
     | 
| 
       455 
499 
     | 
    
         
             
                      notify("  #{entry[:action].to_s_pretty} — #{action_color}#{entry[:desc]}")
         
     | 
| 
       456 
500 
     | 
    
         
             
                    end
         
     | 
| 
      
 501 
     | 
    
         
            +
                  elsif add
         
     | 
| 
      
 502 
     | 
    
         
            +
                    notify("#{NA.theme[:success]}Task added to #{NA.theme[:filename]}#{target}")
         
     | 
| 
       457 
503 
     | 
    
         
             
                  else
         
     | 
| 
       458 
     | 
    
         
            -
                     
     | 
| 
       459 
     | 
    
         
            -
                      notify("#{NA.theme[:success]}Task added to #{NA.theme[:filename]}#{target}")
         
     | 
| 
       460 
     | 
    
         
            -
                    else
         
     | 
| 
       461 
     | 
    
         
            -
                      notify("#{NA.theme[:success]}Task updated in #{NA.theme[:filename]}#{target}")
         
     | 
| 
       462 
     | 
    
         
            -
                    end
         
     | 
| 
      
 504 
     | 
    
         
            +
                    notify("#{NA.theme[:success]}Task updated in #{NA.theme[:filename]}#{target}")
         
     | 
| 
       463 
505 
     | 
    
         
             
                  end
         
     | 
| 
       464 
506 
     | 
    
         
             
                end
         
     | 
| 
       465 
507 
     | 
    
         | 
| 
       466 
     | 
    
         
            -
                 
     | 
| 
       467 
     | 
    
         
            -
                 
     | 
| 
       468 
     | 
    
         
            -
                 
     | 
| 
       469 
     | 
    
         
            -
                 
     | 
| 
       470 
     | 
    
         
            -
                 
     | 
| 
       471 
     | 
    
         
            -
                 
     | 
| 
       472 
     | 
    
         
            -
                 
     | 
| 
       473 
     | 
    
         
            -
                 
     | 
| 
      
 508 
     | 
    
         
            +
                # Add an action to a todo file
         
     | 
| 
      
 509 
     | 
    
         
            +
                #
         
     | 
| 
      
 510 
     | 
    
         
            +
                # @param file [String] Path to the todo file
         
     | 
| 
      
 511 
     | 
    
         
            +
                # @param project [String] Project name
         
     | 
| 
      
 512 
     | 
    
         
            +
                # @param action [String] Action text
         
     | 
| 
      
 513 
     | 
    
         
            +
                # @param note [Array<String>] Notes
         
     | 
| 
      
 514 
     | 
    
         
            +
                # @param priority [Integer] Priority value
         
     | 
| 
      
 515 
     | 
    
         
            +
                # @param finish [Boolean] Mark as finished
         
     | 
| 
      
 516 
     | 
    
         
            +
                # @param append [Boolean] Append to project
         
     | 
| 
      
 517 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       474 
518 
     | 
    
         
             
                def add_action(file, project, action, note = [], priority: 0, finish: false, append: false)
         
     | 
| 
       475 
519 
     | 
    
         
             
                  parent = project.split(%r{[:/]})
         
     | 
| 
       476 
520 
     | 
    
         | 
| 
         @@ -484,18 +528,21 @@ module NA 
     | 
|
| 
       484 
528 
     | 
    
         | 
| 
       485 
529 
     | 
    
         
             
                  action = Action.new(file, project, parent, action, nil, note)
         
     | 
| 
       486 
530 
     | 
    
         | 
| 
       487 
     | 
    
         
            -
                  update_action(file, nil, add: action, project: project, add_tag: add_tag, priority: priority, finish: finish, 
     | 
| 
      
 531 
     | 
    
         
            +
                  update_action(file, nil, add: action, project: project, add_tag: add_tag, priority: priority, finish: finish,
         
     | 
| 
      
 532 
     | 
    
         
            +
                                           append: append)
         
     | 
| 
       488 
533 
     | 
    
         
             
                end
         
     | 
| 
       489 
534 
     | 
    
         | 
| 
      
 535 
     | 
    
         
            +
                # Build a nested hash representing project hierarchy from actions
         
     | 
| 
      
 536 
     | 
    
         
            +
                #
         
     | 
| 
      
 537 
     | 
    
         
            +
                # @param actions [Array<Action>] List of actions
         
     | 
| 
      
 538 
     | 
    
         
            +
                # @return [Hash] Nested hierarchy
         
     | 
| 
       490 
539 
     | 
    
         
             
                def project_hierarchy(actions)
         
     | 
| 
       491 
540 
     | 
    
         
             
                  parents = { actions: [] }
         
     | 
| 
       492 
541 
     | 
    
         
             
                  actions.each do |a|
         
     | 
| 
       493 
542 
     | 
    
         
             
                    parent = a.parent
         
     | 
| 
       494 
543 
     | 
    
         
             
                    current_parent = parents
         
     | 
| 
       495 
544 
     | 
    
         
             
                    parent.each do |par|
         
     | 
| 
       496 
     | 
    
         
            -
                       
     | 
| 
       497 
     | 
    
         
            -
                        current_parent[par] = { actions: [] }
         
     | 
| 
       498 
     | 
    
         
            -
                      end
         
     | 
| 
      
 545 
     | 
    
         
            +
                      current_parent[par] = { actions: [] } unless current_parent.key?(par)
         
     | 
| 
       499 
546 
     | 
    
         
             
                      current_parent = current_parent[par]
         
     | 
| 
       500 
547 
     | 
    
         
             
                    end
         
     | 
| 
       501 
548 
     | 
    
         | 
| 
         @@ -512,14 +559,14 @@ module NA 
     | 
|
| 
       512 
559 
     | 
    
         
             
                def output_children(children, level = 1)
         
     | 
| 
       513 
560 
     | 
    
         
             
                  out = []
         
     | 
| 
       514 
561 
     | 
    
         
             
                  indent = "\t" * level
         
     | 
| 
      
 562 
     | 
    
         
            +
                  return out if children.nil? || children.empty?
         
     | 
| 
      
 563 
     | 
    
         
            +
             
     | 
| 
       515 
564 
     | 
    
         
             
                  children.each do |k, v|
         
     | 
| 
       516 
565 
     | 
    
         
             
                    if k.to_s =~ /actions/
         
     | 
| 
       517 
566 
     | 
    
         
             
                      indent += "\t"
         
     | 
| 
       518 
     | 
    
         
            -
             
     | 
| 
       519 
     | 
    
         
            -
                      v.each do |a|
         
     | 
| 
      
 567 
     | 
    
         
            +
                      v&.each do |a|
         
     | 
| 
       520 
568 
     | 
    
         
             
                        item = "#{indent}- #{a.action}"
         
     | 
| 
       521 
     | 
    
         
            -
             
     | 
| 
       522 
     | 
    
         
            -
                        unless a.tags.empty?
         
     | 
| 
      
 569 
     | 
    
         
            +
                        unless a.tags.nil? || a.tags.empty?
         
     | 
| 
       523 
570 
     | 
    
         
             
                          tags = []
         
     | 
| 
       524 
571 
     | 
    
         
             
                          a.tags.each do |key, val|
         
     | 
| 
       525 
572 
     | 
    
         
             
                            next if key =~ /^(due|flagged|done)$/
         
     | 
| 
         @@ -528,12 +575,9 @@ module NA 
     | 
|
| 
       528 
575 
     | 
    
         
             
                            tag += "-#{val}" unless val.nil? || val.empty?
         
     | 
| 
       529 
576 
     | 
    
         
             
                            tags.push(tag)
         
     | 
| 
       530 
577 
     | 
    
         
             
                          end
         
     | 
| 
       531 
     | 
    
         
            -
             
     | 
| 
       532 
578 
     | 
    
         
             
                          item += " @tags(#{tags.join(',')})" unless tags.empty?
         
     | 
| 
       533 
579 
     | 
    
         
             
                        end
         
     | 
| 
       534 
     | 
    
         
            -
             
     | 
| 
       535 
580 
     | 
    
         
             
                        item += "\n#{indent}\t#{a.note.join("\n#{indent}\t")}" unless a.note.empty?
         
     | 
| 
       536 
     | 
    
         
            -
             
     | 
| 
       537 
581 
     | 
    
         
             
                        out.push(item)
         
     | 
| 
       538 
582 
     | 
    
         
             
                      end
         
     | 
| 
       539 
583 
     | 
    
         
             
                    else
         
     | 
| 
         @@ -544,26 +588,42 @@ module NA 
     | 
|
| 
       544 
588 
     | 
    
         
             
                  out
         
     | 
| 
       545 
589 
     | 
    
         
             
                end
         
     | 
| 
       546 
590 
     | 
    
         | 
| 
      
 591 
     | 
    
         
            +
                # Open a file in the specified editor/application
         
     | 
| 
      
 592 
     | 
    
         
            +
                #
         
     | 
| 
      
 593 
     | 
    
         
            +
                # @param file [String, nil] Path to the file
         
     | 
| 
      
 594 
     | 
    
         
            +
                # @param app [String, nil] Application to use
         
     | 
| 
      
 595 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       547 
596 
     | 
    
         
             
                def edit_file(file: nil, app: nil)
         
     | 
| 
       548 
597 
     | 
    
         
             
                  os_open(file, app: app) if file && File.exist?(file)
         
     | 
| 
       549 
598 
     | 
    
         
             
                end
         
     | 
| 
       550 
599 
     | 
    
         | 
| 
       551 
     | 
    
         
            -
                 
     | 
| 
       552 
     | 
    
         
            -
                 
     | 
| 
       553 
     | 
    
         
            -
                 
     | 
| 
       554 
     | 
    
         
            -
                 
     | 
| 
       555 
     | 
    
         
            -
                 
     | 
| 
       556 
     | 
    
         
            -
                def find_files(depth: 1)
         
     | 
| 
      
 600 
     | 
    
         
            +
                # Locate files matching NA.extension up to a given depth
         
     | 
| 
      
 601 
     | 
    
         
            +
                #
         
     | 
| 
      
 602 
     | 
    
         
            +
                # @param depth [Integer] The depth at which to search
         
     | 
| 
      
 603 
     | 
    
         
            +
                # @param include_hidden [Boolean] Whether to include hidden directories/files
         
     | 
| 
      
 604 
     | 
    
         
            +
                # @return [Array<String>] List of matching file paths
         
     | 
| 
      
 605 
     | 
    
         
            +
                def find_files(depth: 1, include_hidden: false)
         
     | 
| 
       557 
606 
     | 
    
         
             
                  NA::Benchmark.measure("find_files (depth=#{depth})") do
         
     | 
| 
       558 
607 
     | 
    
         
             
                    return [NA.global_file] if NA.global_file
         
     | 
| 
       559 
608 
     | 
    
         | 
| 
       560 
     | 
    
         
            -
                    pattern  
     | 
| 
       561 
     | 
    
         
            -
             
     | 
| 
       562 
     | 
    
         
            -
             
     | 
| 
       563 
     | 
    
         
            -
             
     | 
| 
       564 
     | 
    
         
            -
             
     | 
| 
       565 
     | 
    
         
            -
             
     | 
| 
       566 
     | 
    
         
            -
             
     | 
| 
      
 609 
     | 
    
         
            +
                    # Build a brace-expanded pattern list covering 1..depth levels, e.g.:
         
     | 
| 
      
 610 
     | 
    
         
            +
                    # depth=1 -> "*.ext"
         
     | 
| 
      
 611 
     | 
    
         
            +
                    # depth=3 -> "{*.ext,*/*.ext,*/*/*.ext}"
         
     | 
| 
      
 612 
     | 
    
         
            +
                    ext = NA.extension
         
     | 
| 
      
 613 
     | 
    
         
            +
                    patterns = (1..[depth.to_i, 1].max).map do |d|
         
     | 
| 
      
 614 
     | 
    
         
            +
                      prefix = d > 1 ? ('*/' * (d - 1)) : ''
         
     | 
| 
      
 615 
     | 
    
         
            +
                      "#{prefix}*.#{ext}"
         
     | 
| 
      
 616 
     | 
    
         
            +
                    end
         
     | 
| 
      
 617 
     | 
    
         
            +
                    pattern = patterns.length == 1 ? patterns.first : "{#{patterns.join(',')}}"
         
     | 
| 
      
 618 
     | 
    
         
            +
             
     | 
| 
      
 619 
     | 
    
         
            +
                    files = Dir.glob(pattern, File::FNM_DOTMATCH)
         
     | 
| 
      
 620 
     | 
    
         
            +
                    # Exclude hidden directories/files unless explicitly requested
         
     | 
| 
      
 621 
     | 
    
         
            +
                    unless include_hidden
         
     | 
| 
      
 622 
     | 
    
         
            +
                      files.reject! do |f|
         
     | 
| 
      
 623 
     | 
    
         
            +
                        # reject any path segment beginning with '.' (excluding '.' and '..')
         
     | 
| 
      
 624 
     | 
    
         
            +
                        f.split('/').any? { |seg| seg.start_with?('.') && seg !~ /^\.\.?$/ }
         
     | 
| 
      
 625 
     | 
    
         
            +
                      end
         
     | 
| 
      
 626 
     | 
    
         
            +
                    end
         
     | 
| 
       567 
627 
     | 
    
         
             
                    files.each { |f| save_working_dir(File.expand_path(f)) }
         
     | 
| 
       568 
628 
     | 
    
         
             
                    files
         
     | 
| 
       569 
629 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -575,15 +635,15 @@ module NA 
     | 
|
| 
       575 
635 
     | 
    
         
             
                    done: false,
         
     | 
| 
       576 
636 
     | 
    
         
             
                    file_path: nil,
         
     | 
| 
       577 
637 
     | 
    
         
             
                    negate: false,
         
     | 
| 
      
 638 
     | 
    
         
            +
                    hidden: false,
         
     | 
| 
       578 
639 
     | 
    
         
             
                    project: nil,
         
     | 
| 
       579 
640 
     | 
    
         
             
                    query: nil,
         
     | 
| 
       580 
641 
     | 
    
         
             
                    regex: false,
         
     | 
| 
       581 
     | 
    
         
            -
                    require_na: true,
         
     | 
| 
       582 
642 
     | 
    
         
             
                    search: nil,
         
     | 
| 
       583 
643 
     | 
    
         
             
                    tag: nil
         
     | 
| 
       584 
644 
     | 
    
         
             
                  }
         
     | 
| 
       585 
645 
     | 
    
         
             
                  options = defaults.merge(options)
         
     | 
| 
       586 
     | 
    
         
            -
                  files = find_files(depth: options[:depth])
         
     | 
| 
      
 646 
     | 
    
         
            +
                  files = find_files(depth: options[:depth], include_hidden: options[:hidden])
         
     | 
| 
       587 
647 
     | 
    
         | 
| 
       588 
648 
     | 
    
         
             
                  files.delete_if do |file|
         
     | 
| 
       589 
649 
     | 
    
         
             
                    cmd_options = {
         
     | 
| 
         @@ -605,20 +665,13 @@ module NA 
     | 
|
| 
       605 
665 
     | 
    
         
             
                  files
         
     | 
| 
       606 
666 
     | 
    
         
             
                end
         
     | 
| 
       607 
667 
     | 
    
         | 
| 
       608 
     | 
    
         
            -
                 
     | 
| 
       609 
     | 
    
         
            -
                 
     | 
| 
       610 
     | 
    
         
            -
                 
     | 
| 
       611 
     | 
    
         
            -
                 
     | 
| 
       612 
     | 
    
         
            -
                 
     | 
| 
       613 
     | 
    
         
            -
                 
     | 
| 
       614 
     | 
    
         
            -
                 
     | 
| 
       615 
     | 
    
         
            -
                ## @param      distance      [Integer] allowed distance
         
     | 
| 
       616 
     | 
    
         
            -
                ##                           between characters
         
     | 
| 
       617 
     | 
    
         
            -
                ## @param      require_last  [Boolean] require regex to
         
     | 
| 
       618 
     | 
    
         
            -
                ##                           match last element of path
         
     | 
| 
       619 
     | 
    
         
            -
                ##
         
     | 
| 
       620 
     | 
    
         
            -
                ## @return     [Array] array of matching directories/todo files
         
     | 
| 
       621 
     | 
    
         
            -
                ##
         
     | 
| 
      
 668 
     | 
    
         
            +
                # Find a matching path using semi-fuzzy matching.
         
     | 
| 
      
 669 
     | 
    
         
            +
                # Search tokens can include ! and + to negate or make required.
         
     | 
| 
      
 670 
     | 
    
         
            +
                #
         
     | 
| 
      
 671 
     | 
    
         
            +
                # @param search [Array<Hash>] Search tokens to match
         
     | 
| 
      
 672 
     | 
    
         
            +
                # @param distance [Integer] Allowed distance between characters
         
     | 
| 
      
 673 
     | 
    
         
            +
                # @param require_last [Boolean] Require regex to match last element of path
         
     | 
| 
      
 674 
     | 
    
         
            +
                # @return [Array<String>] Array of matching directories/todo files
         
     | 
| 
       622 
675 
     | 
    
         
             
                def match_working_dir(search, distance: 1, require_last: true)
         
     | 
| 
       623 
676 
     | 
    
         
             
                  file = database_path
         
     | 
| 
       624 
677 
     | 
    
         
             
                  NA.notify("#{NA.theme[:error]}No na database found", exit_code: 1) unless File.exist?(file)
         
     | 
| 
         @@ -637,7 +690,9 @@ module NA 
     | 
|
| 
       637 
690 
     | 
    
         | 
| 
       638 
691 
     | 
    
         
             
                  NA.notify("Optional directory regex: {x}#{optional.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
         
     | 
| 
       639 
692 
     | 
    
         
             
                  NA.notify("Required directory regex: {x}#{required.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
         
     | 
| 
       640 
     | 
    
         
            -
                  NA.notify("Negated directory regex: {x}#{negated.map  
     | 
| 
      
 693 
     | 
    
         
            +
                  NA.notify("Negated directory regex: {x}#{negated.map do |t|
         
     | 
| 
      
 694 
     | 
    
         
            +
                    t.dir_to_rx(distance: distance, require_last: false)
         
     | 
| 
      
 695 
     | 
    
         
            +
                  end}", debug: true)
         
     | 
| 
       641 
696 
     | 
    
         | 
| 
       642 
697 
     | 
    
         
             
                  if require_last
         
     | 
| 
       643 
698 
     | 
    
         
             
                    dirs.delete_if { |d| !d.sub(/\.#{NA.extension}$/, '').dir_matches(any: optional, all: required, none: negated) }
         
     | 
| 
         @@ -673,32 +728,30 @@ module NA 
     | 
|
| 
       673 
728 
     | 
    
         
             
                  out
         
     | 
| 
       674 
729 
     | 
    
         
             
                end
         
     | 
| 
       675 
730 
     | 
    
         | 
| 
       676 
     | 
    
         
            -
                 
     | 
| 
       677 
     | 
    
         
            -
                 
     | 
| 
       678 
     | 
    
         
            -
                 
     | 
| 
       679 
     | 
    
         
            -
                 
     | 
| 
       680 
     | 
    
         
            -
                ##
         
     | 
| 
      
 731 
     | 
    
         
            +
                # Save a todo file path to the database
         
     | 
| 
      
 732 
     | 
    
         
            +
                #
         
     | 
| 
      
 733 
     | 
    
         
            +
                # @param todo_file [String] The todo file path
         
     | 
| 
      
 734 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       681 
735 
     | 
    
         
             
                def save_working_dir(todo_file)
         
     | 
| 
       682 
736 
     | 
    
         
             
                  NA::Benchmark.measure('save_working_dir') do
         
     | 
| 
       683 
737 
     | 
    
         
             
                    file = database_path
         
     | 
| 
       684 
738 
     | 
    
         
             
                    content = File.exist?(file) ? file.read_file : ''
         
     | 
| 
       685 
     | 
    
         
            -
                    dirs = content.split( 
     | 
| 
      
 739 
     | 
    
         
            +
                    dirs = content.split("\n")
         
     | 
| 
       686 
740 
     | 
    
         
             
                    dirs.push(File.expand_path(todo_file))
         
     | 
| 
       687 
741 
     | 
    
         
             
                    dirs.sort!.uniq!
         
     | 
| 
       688 
742 
     | 
    
         
             
                    File.open(file, 'w') { |f| f.puts dirs.join("\n") }
         
     | 
| 
       689 
743 
     | 
    
         
             
                  end
         
     | 
| 
       690 
744 
     | 
    
         
             
                end
         
     | 
| 
       691 
745 
     | 
    
         | 
| 
       692 
     | 
    
         
            -
                 
     | 
| 
       693 
     | 
    
         
            -
                 
     | 
| 
       694 
     | 
    
         
            -
                 
     | 
| 
       695 
     | 
    
         
            -
                 
     | 
| 
       696 
     | 
    
         
            -
                ##
         
     | 
| 
      
 746 
     | 
    
         
            +
                # Save a backed-up file to the database
         
     | 
| 
      
 747 
     | 
    
         
            +
                #
         
     | 
| 
      
 748 
     | 
    
         
            +
                # @param file [String] The file
         
     | 
| 
      
 749 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       697 
750 
     | 
    
         
             
                def save_modified_file(file)
         
     | 
| 
       698 
751 
     | 
    
         
             
                  db = database_path(file: 'last_modified.txt')
         
     | 
| 
       699 
752 
     | 
    
         
             
                  file = File.expand_path(file)
         
     | 
| 
       700 
753 
     | 
    
         
             
                  if File.exist? db
         
     | 
| 
       701 
     | 
    
         
            -
                    files =  
     | 
| 
      
 754 
     | 
    
         
            +
                    files = File.read(db).split("\n").map(&:strip)
         
     | 
| 
       702 
755 
     | 
    
         
             
                    files.delete(file)
         
     | 
| 
       703 
756 
     | 
    
         
             
                    files << file
         
     | 
| 
       704 
757 
     | 
    
         
             
                    File.open(db, 'w') { |f| f.puts(files.join("\n")) }
         
     | 
| 
         @@ -707,22 +760,20 @@ module NA 
     | 
|
| 
       707 
760 
     | 
    
         
             
                  end
         
     | 
| 
       708 
761 
     | 
    
         
             
                end
         
     | 
| 
       709 
762 
     | 
    
         | 
| 
       710 
     | 
    
         
            -
                 
     | 
| 
       711 
     | 
    
         
            -
                 
     | 
| 
       712 
     | 
    
         
            -
                 
     | 
| 
       713 
     | 
    
         
            -
                 
     | 
| 
       714 
     | 
    
         
            -
                ##
         
     | 
| 
      
 763 
     | 
    
         
            +
                # Get the last modified file from the database
         
     | 
| 
      
 764 
     | 
    
         
            +
                #
         
     | 
| 
      
 765 
     | 
    
         
            +
                # @param search [String, nil] Optional search string
         
     | 
| 
      
 766 
     | 
    
         
            +
                # @return [String, nil] Last modified file path
         
     | 
| 
       715 
767 
     | 
    
         
             
                def last_modified_file(search: nil)
         
     | 
| 
       716 
768 
     | 
    
         
             
                  files = backup_files
         
     | 
| 
       717 
769 
     | 
    
         
             
                  files.delete_if { |f| f !~ Regexp.new(search.dir_to_rx(require_last: true)) } if search
         
     | 
| 
       718 
770 
     | 
    
         
             
                  files.last
         
     | 
| 
       719 
771 
     | 
    
         
             
                end
         
     | 
| 
       720 
772 
     | 
    
         | 
| 
       721 
     | 
    
         
            -
                 
     | 
| 
       722 
     | 
    
         
            -
                 
     | 
| 
       723 
     | 
    
         
            -
                 
     | 
| 
       724 
     | 
    
         
            -
                 
     | 
| 
       725 
     | 
    
         
            -
                ##
         
     | 
| 
      
 773 
     | 
    
         
            +
                # Get last modified file and restore a backup
         
     | 
| 
      
 774 
     | 
    
         
            +
                #
         
     | 
| 
      
 775 
     | 
    
         
            +
                # @param search [String, nil] Optional search string
         
     | 
| 
      
 776 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       726 
777 
     | 
    
         
             
                def restore_last_modified_file(search: nil)
         
     | 
| 
       727 
778 
     | 
    
         
             
                  file = last_modified_file(search: search)
         
     | 
| 
       728 
779 
     | 
    
         
             
                  if file
         
     | 
| 
         @@ -732,22 +783,23 @@ module NA 
     | 
|
| 
       732 
783 
     | 
    
         
             
                  end
         
     | 
| 
       733 
784 
     | 
    
         
             
                end
         
     | 
| 
       734 
785 
     | 
    
         | 
| 
       735 
     | 
    
         
            -
                 
     | 
| 
       736 
     | 
    
         
            -
                 
     | 
| 
       737 
     | 
    
         
            -
                 
     | 
| 
       738 
     | 
    
         
            -
                ## @return     [Array] list of file paths
         
     | 
| 
       739 
     | 
    
         
            -
                ##
         
     | 
| 
      
 786 
     | 
    
         
            +
                # Get list of backed up files
         
     | 
| 
      
 787 
     | 
    
         
            +
                #
         
     | 
| 
      
 788 
     | 
    
         
            +
                # @return [Array<String>] List of file paths
         
     | 
| 
       740 
789 
     | 
    
         
             
                def backup_files
         
     | 
| 
       741 
790 
     | 
    
         
             
                  db = database_path(file: 'last_modified.txt')
         
     | 
| 
       742 
791 
     | 
    
         
             
                  if File.exist?(db)
         
     | 
| 
       743 
     | 
    
         
            -
                     
     | 
| 
      
 792 
     | 
    
         
            +
                    File.read(db).strip.split("\n").map(&:strip)
         
     | 
| 
       744 
793 
     | 
    
         
             
                  else
         
     | 
| 
       745 
794 
     | 
    
         
             
                    NA.notify("#{NA.theme[:error]}Backup database not found")
         
     | 
| 
       746 
     | 
    
         
            -
                    File.open(db, 'w' 
     | 
| 
      
 795 
     | 
    
         
            +
                    File.open(db, 'w', &:puts)
         
     | 
| 
       747 
796 
     | 
    
         
             
                    []
         
     | 
| 
       748 
797 
     | 
    
         
             
                  end
         
     | 
| 
       749 
798 
     | 
    
         
             
                end
         
     | 
| 
       750 
799 
     | 
    
         | 
| 
      
 800 
     | 
    
         
            +
                # Move deprecated backup files to new backup folder
         
     | 
| 
      
 801 
     | 
    
         
            +
                #
         
     | 
| 
      
 802 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       751 
803 
     | 
    
         
             
                def move_deprecated_backups
         
     | 
| 
       752 
804 
     | 
    
         
             
                  backup_files.each do |file|
         
     | 
| 
       753 
805 
     | 
    
         
             
                    if File.exist?(old_backup_path(file))
         
     | 
| 
         @@ -757,15 +809,18 @@ module NA 
     | 
|
| 
       757 
809 
     | 
    
         
             
                  end
         
     | 
| 
       758 
810 
     | 
    
         
             
                end
         
     | 
| 
       759 
811 
     | 
    
         | 
| 
      
 812 
     | 
    
         
            +
                # Get the old backup file path for a file
         
     | 
| 
      
 813 
     | 
    
         
            +
                #
         
     | 
| 
      
 814 
     | 
    
         
            +
                # @param file [String] The file
         
     | 
| 
      
 815 
     | 
    
         
            +
                # @return [String] Old backup file path
         
     | 
| 
       760 
816 
     | 
    
         
             
                def old_backup_path(file)
         
     | 
| 
       761 
817 
     | 
    
         
             
                  File.join(File.dirname(file), ".#{File.basename(file)}.bak")
         
     | 
| 
       762 
818 
     | 
    
         
             
                end
         
     | 
| 
       763 
819 
     | 
    
         | 
| 
       764 
     | 
    
         
            -
                 
     | 
| 
       765 
     | 
    
         
            -
                 
     | 
| 
       766 
     | 
    
         
            -
                 
     | 
| 
       767 
     | 
    
         
            -
                 
     | 
| 
       768 
     | 
    
         
            -
                ##
         
     | 
| 
      
 820 
     | 
    
         
            +
                # Get the backup file path for a file
         
     | 
| 
      
 821 
     | 
    
         
            +
                #
         
     | 
| 
      
 822 
     | 
    
         
            +
                # @param file [String] The file
         
     | 
| 
      
 823 
     | 
    
         
            +
                # @return [String] Backup file path
         
     | 
| 
       769 
824 
     | 
    
         
             
                def backup_path(file)
         
     | 
| 
       770 
825 
     | 
    
         
             
                  backup_home = File.expand_path('~/.local/share/na/backup')
         
     | 
| 
       771 
826 
     | 
    
         
             
                  backup = old_backup_path(file)
         
     | 
| 
         @@ -777,6 +832,10 @@ module NA 
     | 
|
| 
       777 
832 
     | 
    
         
             
                  backup_target
         
     | 
| 
       778 
833 
     | 
    
         
             
                end
         
     | 
| 
       779 
834 
     | 
    
         | 
| 
      
 835 
     | 
    
         
            +
                # Remove entries for missing backup files from the database
         
     | 
| 
      
 836 
     | 
    
         
            +
                #
         
     | 
| 
      
 837 
     | 
    
         
            +
                # @param file [String, nil] Optional file to filter
         
     | 
| 
      
 838 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       780 
839 
     | 
    
         
             
                def weed_modified_files(file = nil)
         
     | 
| 
       781 
840 
     | 
    
         
             
                  files = backup_files
         
     | 
| 
       782 
841 
     | 
    
         | 
| 
         @@ -787,11 +846,10 @@ module NA 
     | 
|
| 
       787 
846 
     | 
    
         
             
                  File.open(database_path(file: 'last_modified.txt'), 'w') { |f| f.puts files.join("\n") }
         
     | 
| 
       788 
847 
     | 
    
         
             
                end
         
     | 
| 
       789 
848 
     | 
    
         | 
| 
       790 
     | 
    
         
            -
                 
     | 
| 
       791 
     | 
    
         
            -
                 
     | 
| 
       792 
     | 
    
         
            -
                 
     | 
| 
       793 
     | 
    
         
            -
                 
     | 
| 
       794 
     | 
    
         
            -
                ##
         
     | 
| 
      
 849 
     | 
    
         
            +
                # Restore a file from backup
         
     | 
| 
      
 850 
     | 
    
         
            +
                #
         
     | 
| 
      
 851 
     | 
    
         
            +
                # @param file [String] The file
         
     | 
| 
      
 852 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       795 
853 
     | 
    
         
             
                def restore_modified_file(file)
         
     | 
| 
       796 
854 
     | 
    
         
             
                  bak_file = backup_path(file)
         
     | 
| 
       797 
855 
     | 
    
         
             
                  if File.exist?(bak_file)
         
     | 
| 
         @@ -804,11 +862,10 @@ module NA 
     | 
|
| 
       804 
862 
     | 
    
         
             
                  weed_modified_files(file)
         
     | 
| 
       805 
863 
     | 
    
         
             
                end
         
     | 
| 
       806 
864 
     | 
    
         | 
| 
       807 
     | 
    
         
            -
                 
     | 
| 
       808 
     | 
    
         
            -
                 
     | 
| 
       809 
     | 
    
         
            -
                 
     | 
| 
       810 
     | 
    
         
            -
                 
     | 
| 
       811 
     | 
    
         
            -
                ##
         
     | 
| 
      
 865 
     | 
    
         
            +
                # Get path to database of known todo files
         
     | 
| 
      
 866 
     | 
    
         
            +
                #
         
     | 
| 
      
 867 
     | 
    
         
            +
                # @param file [String] The database filename (default: 'tdlist.txt')
         
     | 
| 
      
 868 
     | 
    
         
            +
                # @return [String] File path
         
     | 
| 
       812 
869 
     | 
    
         
             
                def database_path(file: 'tdlist.txt')
         
     | 
| 
       813 
870 
     | 
    
         
             
                  db_dir = File.expand_path('~/.local/share/na')
         
     | 
| 
       814 
871 
     | 
    
         
             
                  # Create directory if needed
         
     | 
| 
         @@ -816,11 +873,11 @@ module NA 
     | 
|
| 
       816 
873 
     | 
    
         
             
                  File.join(db_dir, file)
         
     | 
| 
       817 
874 
     | 
    
         
             
                end
         
     | 
| 
       818 
875 
     | 
    
         | 
| 
       819 
     | 
    
         
            -
                 
     | 
| 
       820 
     | 
    
         
            -
                 
     | 
| 
       821 
     | 
    
         
            -
                 
     | 
| 
       822 
     | 
    
         
            -
                 
     | 
| 
       823 
     | 
    
         
            -
                 
     | 
| 
      
 876 
     | 
    
         
            +
                # Platform-agnostic open command
         
     | 
| 
      
 877 
     | 
    
         
            +
                #
         
     | 
| 
      
 878 
     | 
    
         
            +
                # @param file [String] The file to open
         
     | 
| 
      
 879 
     | 
    
         
            +
                # @param app [String, nil] Optional application to use
         
     | 
| 
      
 880 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       824 
881 
     | 
    
         
             
                def os_open(file, app: nil)
         
     | 
| 
       825 
882 
     | 
    
         
             
                  os = RbConfig::CONFIG['target_os']
         
     | 
| 
       826 
883 
     | 
    
         
             
                  case os
         
     | 
| 
         @@ -833,9 +890,9 @@ module NA 
     | 
|
| 
       833 
890 
     | 
    
         
             
                  end
         
     | 
| 
       834 
891 
     | 
    
         
             
                end
         
     | 
| 
       835 
892 
     | 
    
         | 
| 
       836 
     | 
    
         
            -
                 
     | 
| 
       837 
     | 
    
         
            -
                 
     | 
| 
       838 
     | 
    
         
            -
                 
     | 
| 
      
 893 
     | 
    
         
            +
                #
         
     | 
| 
      
 894 
     | 
    
         
            +
                # Remove entries from cache database that no longer exist
         
     | 
| 
      
 895 
     | 
    
         
            +
                #
         
     | 
| 
       839 
896 
     | 
    
         
             
                def weed_cache_file
         
     | 
| 
       840 
897 
     | 
    
         
             
                  db_dir = File.expand_path('~/.local/share/na')
         
     | 
| 
       841 
898 
     | 
    
         
             
                  db_file = 'tdlist.txt'
         
     | 
| 
         @@ -863,7 +920,7 @@ module NA 
     | 
|
| 
       863 
920 
     | 
    
         | 
| 
       864 
921 
     | 
    
         
             
                  projects = find_projects(target)
         
     | 
| 
       865 
922 
     | 
    
         
             
                  projects.each do |proj|
         
     | 
| 
       866 
     | 
    
         
            -
                    parts = proj.project.split( 
     | 
| 
      
 923 
     | 
    
         
            +
                    parts = proj.project.split(':')
         
     | 
| 
       867 
924 
     | 
    
         
             
                    output = if paths
         
     | 
| 
       868 
925 
     | 
    
         
             
                               "{bg}#{parts.join('{bw}/{bg}')}{x}"
         
     | 
| 
       869 
926 
     | 
    
         
             
                             else
         
     | 
| 
         @@ -883,12 +940,10 @@ module NA 
     | 
|
| 
       883 
940 
     | 
    
         
             
                           content = File.exist?(file) ? file.read_file.strip : ''
         
     | 
| 
       884 
941 
     | 
    
         
             
                           notify("#{NA.theme[:error]}Database empty", exit_code: 1) if content.empty?
         
     | 
| 
       885 
942 
     | 
    
         | 
| 
       886 
     | 
    
         
            -
                           content.split( 
     | 
| 
      
 943 
     | 
    
         
            +
                           content.split("\n")
         
     | 
| 
       887 
944 
     | 
    
         
             
                         end
         
     | 
| 
       888 
945 
     | 
    
         | 
| 
       889 
     | 
    
         
            -
                  dirs.map! 
     | 
| 
       890 
     | 
    
         
            -
                    dir.highlight_filename
         
     | 
| 
       891 
     | 
    
         
            -
                  end
         
     | 
| 
      
 946 
     | 
    
         
            +
                  dirs.map!(&:highlight_filename)
         
     | 
| 
       892 
947 
     | 
    
         | 
| 
       893 
948 
     | 
    
         
             
                  puts NA::Color.template(dirs.join("\n"))
         
     | 
| 
       894 
949 
     | 
    
         
             
                end
         
     | 
| 
         @@ -896,7 +951,7 @@ module NA 
     | 
|
| 
       896 
951 
     | 
    
         
             
                def save_search(title, search)
         
     | 
| 
       897 
952 
     | 
    
         
             
                  file = database_path(file: 'saved_searches.yml')
         
     | 
| 
       898 
953 
     | 
    
         
             
                  searches = load_searches
         
     | 
| 
       899 
     | 
    
         
            -
                  title = title.gsub(/[^a- 
     | 
| 
      
 954 
     | 
    
         
            +
                  title = title.gsub(/[^a-zA-Z0-9]/, '_').gsub(/_+/, '_').downcase
         
     | 
| 
       900 
955 
     | 
    
         | 
| 
       901 
956 
     | 
    
         
             
                  if searches.key?(title)
         
     | 
| 
       902 
957 
     | 
    
         
             
                    res = yn('Overwrite existing definition?', default: true)
         
     | 
| 
         @@ -909,10 +964,13 @@ module NA 
     | 
|
| 
       909 
964 
     | 
    
         
             
                  NA.notify("#{NA.theme[:success]}Search #{NA.theme[:filename]}#{title}#{NA.theme[:success]} saved", exit_code: 0)
         
     | 
| 
       910 
965 
     | 
    
         
             
                end
         
     | 
| 
       911 
966 
     | 
    
         | 
| 
      
 967 
     | 
    
         
            +
                # Load saved search definitions from YAML file
         
     | 
| 
      
 968 
     | 
    
         
            +
                #
         
     | 
| 
      
 969 
     | 
    
         
            +
                # @return [Hash] Hash of saved searches
         
     | 
| 
       912 
970 
     | 
    
         
             
                def load_searches
         
     | 
| 
       913 
971 
     | 
    
         
             
                  file = database_path(file: 'saved_searches.yml')
         
     | 
| 
       914 
972 
     | 
    
         
             
                  if File.exist?(file)
         
     | 
| 
       915 
     | 
    
         
            -
                    searches = YAML. 
     | 
| 
      
 973 
     | 
    
         
            +
                    searches = YAML.load(file.read_file)
         
     | 
| 
       916 
974 
     | 
    
         
             
                  else
         
     | 
| 
       917 
975 
     | 
    
         
             
                    searches = {
         
     | 
| 
       918 
976 
     | 
    
         
             
                      'soon' => 'tagged "due<in 2 days,due>yesterday"',
         
     | 
| 
         @@ -925,6 +983,10 @@ module NA 
     | 
|
| 
       925 
983 
     | 
    
         
             
                  searches
         
     | 
| 
       926 
984 
     | 
    
         
             
                end
         
     | 
| 
       927 
985 
     | 
    
         | 
| 
      
 986 
     | 
    
         
            +
                # Delete saved search definitions by name
         
     | 
| 
      
 987 
     | 
    
         
            +
                #
         
     | 
| 
      
 988 
     | 
    
         
            +
                # @param strings [Array<String>, String, nil] Names of searches to delete
         
     | 
| 
      
 989 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       928 
990 
     | 
    
         
             
                def delete_search(strings = nil)
         
     | 
| 
       929 
991 
     | 
    
         
             
                  NA.notify("#{NA.theme[:error]}Name of search required", exit_code: 1) if strings.nil? || strings.empty?
         
     | 
| 
       930 
992 
     | 
    
         | 
| 
         @@ -933,7 +995,7 @@ module NA 
     | 
|
| 
       933 
995 
     | 
    
         | 
| 
       934 
996 
     | 
    
         
             
                  strings = [strings] unless strings.is_a? Array
         
     | 
| 
       935 
997 
     | 
    
         | 
| 
       936 
     | 
    
         
            -
                  searches = YAML. 
     | 
| 
      
 998 
     | 
    
         
            +
                  searches = YAML.load(file.read_file)
         
     | 
| 
       937 
999 
     | 
    
         
             
                  keys = searches.keys.delete_if { |k| k !~ /(#{strings.map(&:wildcard_to_rx).join('|')})/ }
         
     | 
| 
       938 
1000 
     | 
    
         | 
| 
       939 
1001 
     | 
    
         
             
                  NA.notify("#{NA.theme[:error]}No search named #{strings.join(', ')} found", exit_code: 1) if keys.empty?
         
     | 
| 
         @@ -947,9 +1009,14 @@ module NA 
     | 
|
| 
       947 
1009 
     | 
    
         | 
| 
       948 
1010 
     | 
    
         
             
                  File.open(file, 'w') { |f| f.puts(YAML.dump(searches)) }
         
     | 
| 
       949 
1011 
     | 
    
         | 
| 
       950 
     | 
    
         
            -
                  NA.notify( 
     | 
| 
      
 1012 
     | 
    
         
            +
                  NA.notify(
         
     | 
| 
      
 1013 
     | 
    
         
            +
                    "#{NA.theme[:warning]}Deleted {bw}#{keys.count}{x}#{NA.theme[:warning]} #{keys.count > 1 ? 'searches' : 'search'}", exit_code: 0
         
     | 
| 
      
 1014 
     | 
    
         
            +
                  )
         
     | 
| 
       951 
1015 
     | 
    
         
             
                end
         
     | 
| 
       952 
1016 
     | 
    
         | 
| 
      
 1017 
     | 
    
         
            +
                # Edit saved search definitions in the default editor
         
     | 
| 
      
 1018 
     | 
    
         
            +
                #
         
     | 
| 
      
 1019 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       953 
1020 
     | 
    
         
             
                def edit_searches
         
     | 
| 
       954 
1021 
     | 
    
         
             
                  file = database_path(file: 'saved_searches.yml')
         
     | 
| 
       955 
1022 
     | 
    
         
             
                  searches = load_searches
         
     | 
| 
         @@ -963,23 +1030,22 @@ module NA 
     | 
|
| 
       963 
1030 
     | 
    
         
             
                  NA.notify("#{NA.theme[:success]}Opened #{file} in #{editor}", exit_code: 0)
         
     | 
| 
       964 
1031 
     | 
    
         
             
                end
         
     | 
| 
       965 
1032 
     | 
    
         | 
| 
       966 
     | 
    
         
            -
                 
     | 
| 
       967 
     | 
    
         
            -
                 
     | 
| 
       968 
     | 
    
         
            -
                 
     | 
| 
       969 
     | 
    
         
            -
                 
     | 
| 
       970 
     | 
    
         
            -
                ##
         
     | 
| 
      
 1033 
     | 
    
         
            +
                # Create a backup file
         
     | 
| 
      
 1034 
     | 
    
         
            +
                #
         
     | 
| 
      
 1035 
     | 
    
         
            +
                # @param target [String] The file to back up
         
     | 
| 
      
 1036 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
       971 
1037 
     | 
    
         
             
                def backup_file(target)
         
     | 
| 
       972 
1038 
     | 
    
         
             
                  FileUtils.cp(target, backup_path(target))
         
     | 
| 
       973 
1039 
     | 
    
         
             
                  save_modified_file(target)
         
     | 
| 
       974 
1040 
     | 
    
         
             
                  NA.notify("#{NA.theme[:warning]}Backup file created for #{target.highlight_filename}", debug: true)
         
     | 
| 
       975 
1041 
     | 
    
         
             
                end
         
     | 
| 
       976 
1042 
     | 
    
         | 
| 
       977 
     | 
    
         
            -
                 
     | 
| 
       978 
     | 
    
         
            -
                 
     | 
| 
       979 
     | 
    
         
            -
                 
     | 
| 
       980 
     | 
    
         
            -
                 
     | 
| 
       981 
     | 
    
         
            -
                 
     | 
| 
       982 
     | 
    
         
            -
                 
     | 
| 
      
 1043 
     | 
    
         
            +
                #
         
     | 
| 
      
 1044 
     | 
    
         
            +
                # Request terminal input from user, readline style
         
     | 
| 
      
 1045 
     | 
    
         
            +
                #
         
     | 
| 
      
 1046 
     | 
    
         
            +
                # @param      options  [Hash] The options
         
     | 
| 
      
 1047 
     | 
    
         
            +
                # @param      prompt   [String] The prompt
         
     | 
| 
      
 1048 
     | 
    
         
            +
                #
         
     | 
| 
       983 
1049 
     | 
    
         
             
                def request_input(options, prompt: 'Enter text')
         
     | 
| 
       984 
1050 
     | 
    
         
             
                  if $stdin.isatty && TTY::Which.exist?('gum') && (options[:tagged].nil? || options[:tagged].empty?)
         
     | 
| 
       985 
1051 
     | 
    
         
             
                    opts = [%(--placeholder "#{prompt}"),
         
     | 
| 
         @@ -992,18 +1058,18 @@ module NA 
     | 
|
| 
       992 
1058 
     | 
    
         
             
                  end
         
     | 
| 
       993 
1059 
     | 
    
         
             
                end
         
     | 
| 
       994 
1060 
     | 
    
         | 
| 
       995 
     | 
    
         
            -
                 
     | 
| 
       996 
     | 
    
         
            -
                 
     | 
| 
       997 
     | 
    
         
            -
                 
     | 
| 
       998 
     | 
    
         
            -
                 
     | 
| 
       999 
     | 
    
         
            -
                 
     | 
| 
       1000 
     | 
    
         
            -
                 
     | 
| 
       1001 
     | 
    
         
            -
                 
     | 
| 
       1002 
     | 
    
         
            -
                 
     | 
| 
       1003 
     | 
    
         
            -
                 
     | 
| 
       1004 
     | 
    
         
            -
                 
     | 
| 
       1005 
     | 
    
         
            -
                 
     | 
| 
       1006 
     | 
    
         
            -
                 
     | 
| 
      
 1061 
     | 
    
         
            +
                #
         
     | 
| 
      
 1062 
     | 
    
         
            +
                # Generate a menu of options and allow user selection
         
     | 
| 
      
 1063 
     | 
    
         
            +
                #
         
     | 
| 
      
 1064 
     | 
    
         
            +
                # @return     [String] The selected option
         
     | 
| 
      
 1065 
     | 
    
         
            +
                #
         
     | 
| 
      
 1066 
     | 
    
         
            +
                # @param      options   [Array] The options from which to choose
         
     | 
| 
      
 1067 
     | 
    
         
            +
                # @param      prompt    [String] The prompt
         
     | 
| 
      
 1068 
     | 
    
         
            +
                # @param      multiple  [Boolean] If true, allow multiple selections
         
     | 
| 
      
 1069 
     | 
    
         
            +
                # @param      sorted    [Boolean] If true, sort selections alphanumerically
         
     | 
| 
      
 1070 
     | 
    
         
            +
                # @param      fzf_args  [Array] Additional fzf arguments
         
     | 
| 
      
 1071 
     | 
    
         
            +
                #
         
     | 
| 
      
 1072 
     | 
    
         
            +
                # @return [String, Array] array if multiple is true
         
     | 
| 
       1007 
1073 
     | 
    
         
             
                def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
         
     | 
| 
       1008 
1074 
     | 
    
         
             
                  return nil unless $stdout.isatty
         
     | 
| 
       1009 
1075 
     | 
    
         | 
| 
         @@ -1031,12 +1097,14 @@ module NA 
     | 
|
| 
       1031 
1097 
     | 
    
         
             
                          reader = TTY::Reader.new
         
     | 
| 
       1032 
1098 
     | 
    
         
             
                          puts
         
     | 
| 
       1033 
1099 
     | 
    
         
             
                          options.each.with_index do |f, i|
         
     | 
| 
       1034 
     | 
    
         
            -
                            puts NA::Color.template(format( 
     | 
| 
      
 1100 
     | 
    
         
            +
                            puts NA::Color.template(format(
         
     | 
| 
      
 1101 
     | 
    
         
            +
                                                      "#{NA.theme[:prompt]}%<idx> 2d{xw}) #{NA.theme[:filename]}%<action>s{x}\n", idx: i + 1, action: f
         
     | 
| 
      
 1102 
     | 
    
         
            +
                                                    ))
         
     | 
| 
       1035 
1103 
     | 
    
         
             
                          end
         
     | 
| 
       1036 
1104 
     | 
    
         
             
                          result = reader.read_line(NA::Color.template("#{NA.theme[:prompt]}#{prompt}{x}")).strip
         
     | 
| 
       1037 
1105 
     | 
    
         
             
                          if multiple
         
     | 
| 
       1038 
1106 
     | 
    
         
             
                            mult_res = []
         
     | 
| 
       1039 
     | 
    
         
            -
                            result = result.gsub( 
     | 
| 
      
 1107 
     | 
    
         
            +
                            result = result.gsub(',', ' ').gsub(/ +/, ' ').split(/ /)
         
     | 
| 
       1040 
1108 
     | 
    
         
             
                            result.each do |r|
         
     | 
| 
       1041 
1109 
     | 
    
         
             
                              mult_res << options[r.to_i - 1] if r.to_i&.positive?
         
     | 
| 
       1042 
1110 
     | 
    
         
             
                            end
         
     | 
| 
         @@ -1046,19 +1114,20 @@ module NA 
     | 
|
| 
       1046 
1114 
     | 
    
         
             
                          end
         
     | 
| 
       1047 
1115 
     | 
    
         
             
                        end
         
     | 
| 
       1048 
1116 
     | 
    
         | 
| 
       1049 
     | 
    
         
            -
                  return false if res&.strip&. 
     | 
| 
      
 1117 
     | 
    
         
            +
                  return false if res&.strip&.empty?
         
     | 
| 
      
 1118 
     | 
    
         
            +
             
     | 
| 
       1050 
1119 
     | 
    
         
             
                  # pp NA::Color.uncolor(NA::Color.template(res))
         
     | 
| 
       1051 
     | 
    
         
            -
                  multiple ? NA::Color.uncolor(NA::Color.template(res)).split( 
     | 
| 
      
 1120 
     | 
    
         
            +
                  multiple ? NA::Color.uncolor(NA::Color.template(res)).split("\n") : NA::Color.uncolor(NA::Color.template(res))
         
     | 
| 
       1052 
1121 
     | 
    
         
             
                end
         
     | 
| 
       1053 
1122 
     | 
    
         | 
| 
       1054 
1123 
     | 
    
         
             
                private
         
     | 
| 
       1055 
1124 
     | 
    
         | 
| 
       1056 
     | 
    
         
            -
                 
     | 
| 
       1057 
     | 
    
         
            -
                 
     | 
| 
       1058 
     | 
    
         
            -
                 
     | 
| 
       1059 
     | 
    
         
            -
                 
     | 
| 
       1060 
     | 
    
         
            -
                 
     | 
| 
       1061 
     | 
    
         
            -
                 
     | 
| 
      
 1125 
     | 
    
         
            +
                #
         
     | 
| 
      
 1126 
     | 
    
         
            +
                # macOS open command
         
     | 
| 
      
 1127 
     | 
    
         
            +
                #
         
     | 
| 
      
 1128 
     | 
    
         
            +
                # @param      file  The file
         
     | 
| 
      
 1129 
     | 
    
         
            +
                # @param      app   The application
         
     | 
| 
      
 1130 
     | 
    
         
            +
                #
         
     | 
| 
       1062 
1131 
     | 
    
         
             
                def darwin_open(file, app: nil)
         
     | 
| 
       1063 
1132 
     | 
    
         
             
                  if app
         
     | 
| 
       1064 
1133 
     | 
    
         
             
                    `open -a "#{app}" #{Shellwords.escape(file)}`
         
     | 
| 
         @@ -1067,20 +1136,20 @@ module NA 
     | 
|
| 
       1067 
1136 
     | 
    
         
             
                  end
         
     | 
| 
       1068 
1137 
     | 
    
         
             
                end
         
     | 
| 
       1069 
1138 
     | 
    
         | 
| 
       1070 
     | 
    
         
            -
                 
     | 
| 
       1071 
     | 
    
         
            -
                 
     | 
| 
       1072 
     | 
    
         
            -
                 
     | 
| 
       1073 
     | 
    
         
            -
                 
     | 
| 
       1074 
     | 
    
         
            -
                 
     | 
| 
      
 1139 
     | 
    
         
            +
                #
         
     | 
| 
      
 1140 
     | 
    
         
            +
                # Windows open command
         
     | 
| 
      
 1141 
     | 
    
         
            +
                #
         
     | 
| 
      
 1142 
     | 
    
         
            +
                # @param      file  The file
         
     | 
| 
      
 1143 
     | 
    
         
            +
                #
         
     | 
| 
       1075 
1144 
     | 
    
         
             
                def win_open(file)
         
     | 
| 
       1076 
1145 
     | 
    
         
             
                  `start #{Shellwords.escape(file)}`
         
     | 
| 
       1077 
1146 
     | 
    
         
             
                end
         
     | 
| 
       1078 
1147 
     | 
    
         | 
| 
       1079 
     | 
    
         
            -
                 
     | 
| 
       1080 
     | 
    
         
            -
                 
     | 
| 
       1081 
     | 
    
         
            -
                 
     | 
| 
       1082 
     | 
    
         
            -
                 
     | 
| 
       1083 
     | 
    
         
            -
                 
     | 
| 
      
 1148 
     | 
    
         
            +
                #
         
     | 
| 
      
 1149 
     | 
    
         
            +
                # Linux open command
         
     | 
| 
      
 1150 
     | 
    
         
            +
                #
         
     | 
| 
      
 1151 
     | 
    
         
            +
                # @param      file  The file
         
     | 
| 
      
 1152 
     | 
    
         
            +
                #
         
     | 
| 
       1084 
1153 
     | 
    
         
             
                def linux_open(file)
         
     | 
| 
       1085 
1154 
     | 
    
         
             
                  if TTY::Which.exist?('xdg-open')
         
     | 
| 
       1086 
1155 
     | 
    
         
             
                    `xdg-open #{Shellwords.escape(file)}`
         
     |