na 1.2.78 → 1.2.80
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/CHANGELOG.md +55 -0
 - data/Gemfile.lock +2 -2
 - data/README.md +3 -3
 - data/bin/commands/update.rb +17 -0
 - data/bin/na +12 -6
 - data/lib/na/action.rb +106 -65
 - data/lib/na/actions.rb +44 -21
 - data/lib/na/benchmark.rb +45 -0
 - data/lib/na/colors.rb +34 -11
 - data/lib/na/next_action.rb +80 -15
 - data/lib/na/pager.rb +27 -23
 - data/lib/na/string.rb +1 -0
 - data/lib/na/theme.rb +10 -8
 - data/lib/na/todo.rb +60 -41
 - data/lib/na/version.rb +1 -1
 - data/lib/na.rb +3 -1
 - data/src/_README.md +1 -1
 - data/test_performance.rb +78 -0
 - metadata +3 -1
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 65f46379443596b7f52385686e09c6b558c2f6eaaab614a34cd55432921fc1f7
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 5050ee855d909fb050ecde79cae733fcfa5d411b6f28d48e88661569f0ed9989
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 2aa83793e127693a7481e4f0294ff98067f7b32ad288445d6b23ce2744f2f975ecc4b854c48eb8faee91f2e5fc720e05732001003eb04a0ccede62b48bf3c685
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 21560b1ae71aa1559070ad8768d0ddacaa3e60a6287bff35bc07a0c3ffb6208c23d83da39baee6ffc5e470d76d2c4e6793163ee08c2e2a7fd6ae6763af1900fc
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -1,3 +1,58 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            ### 1.2.80
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            2025-10-23 05:26
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            #### CHANGED
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            - Default behavior: git integration now opt-in with --repo-top flag
         
     | 
| 
      
 8 
     | 
    
         
            +
            - Pager execution: use spawn for better performance than fork+exec
         
     | 
| 
      
 9 
     | 
    
         
            +
            - Color template processing: cache compiled templates and colors hash
         
     | 
| 
      
 10 
     | 
    
         
            +
            - Action processing: batch regex highlighting instead of per-action processing
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            #### NEW
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            - Add comprehensive benchmarking system with NA_BENCHMARK=1
         
     | 
| 
      
 15 
     | 
    
         
            +
            - Add template caching in Color.template to avoid repeated regex processing
         
     | 
| 
      
 16 
     | 
    
         
            +
            - Add colors hash caching to eliminate repeated hash creation
         
     | 
| 
      
 17 
     | 
    
         
            +
            - Add smart pagination that skips pager for small outputs (<2000 chars, <50 lines)
         
     | 
| 
      
 18 
     | 
    
         
            +
            - Add --repo-top flag to make git integration opt-in instead of default
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            #### IMPROVED
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            - Optimize performance
         
     | 
| 
      
 23 
     | 
    
         
            +
            - Theme loading now uses cached NA.theme instead of loading on every Action.pretty call
         
     | 
| 
      
 24 
     | 
    
         
            +
            - Action.pretty method with conditional processing and optimized string operations
         
     | 
| 
      
 25 
     | 
    
         
            +
            - Actions.output with batch regex processing - compile all output first, then apply regexes once
         
     | 
| 
      
 26 
     | 
    
         
            +
            - Pager performance using spawn instead of fork+exec and removing IO.select delays
         
     | 
| 
      
 27 
     | 
    
         
            +
            - Lazy loading of heavy gems (git, chronic, mdless) only when needed
         
     | 
| 
      
 28 
     | 
    
         
            +
            - String operations in Action.pretty with pre-computed template parts
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            #### FIXED
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            - Performance variability caused by git system calls running by default
         
     | 
| 
      
 33 
     | 
    
         
            +
            - Pager overhead causing 300-479ms delays on small outputs
         
     | 
| 
      
 34 
     | 
    
         
            +
            - Repeated regex compilation in color template processing
         
     | 
| 
      
 35 
     | 
    
         
            +
            - Theme loading bottleneck in Action.pretty method
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            ### 1.2.79
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            2025-09-29 06:51
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            #### NEW
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            - Track affected actions in `update_action` and output per-action summaries
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            #### IMPROVED
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
            - Prompt to select a project when multiple suffix matches are found
         
     | 
| 
      
 48 
     | 
    
         
            +
            - Distinguish summaries: Task deleted vs Task updated/added
         
     | 
| 
      
 49 
     | 
    
         
            +
            - Display affected actions using `action.to_s_pretty` with colored change descriptions
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            #### FIXED
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            - Resolve project matching for `na add --to Ideas` by supporting unique suffix matches (e.g. `rnkd:Ideas`)
         
     | 
| 
      
 54 
     | 
    
         
            +
            - Validate `na update` requires at least one actionable option; error with No action specified, see `na help update`
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
       1 
56 
     | 
    
         
             
            ### 1.2.78
         
     | 
| 
       2 
57 
     | 
    
         | 
| 
       3 
58 
     | 
    
         
             
            2025-06-02 10:07
         
     | 
    
        data/Gemfile.lock
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            PATH
         
     | 
| 
       2 
2 
     | 
    
         
             
              remote: .
         
     | 
| 
       3 
3 
     | 
    
         
             
              specs:
         
     | 
| 
       4 
     | 
    
         
            -
                na (1.2. 
     | 
| 
      
 4 
     | 
    
         
            +
                na (1.2.80)
         
     | 
| 
       5 
5 
     | 
    
         
             
                  chronic (~> 0.10, >= 0.10.2)
         
     | 
| 
       6 
6 
     | 
    
         
             
                  git (~> 3.0.0)
         
     | 
| 
       7 
7 
     | 
    
         
             
                  gli (~> 2.21.0)
         
     | 
| 
         @@ -31,7 +31,7 @@ GEM 
     | 
|
| 
       31 
31 
     | 
    
         
             
                  public_suffix (>= 2.0.2, < 7.0)
         
     | 
| 
       32 
32 
     | 
    
         
             
                base64 (0.3.0)
         
     | 
| 
       33 
33 
     | 
    
         
             
                benchmark (0.4.1)
         
     | 
| 
       34 
     | 
    
         
            -
                bigdecimal (3.2. 
     | 
| 
      
 34 
     | 
    
         
            +
                bigdecimal (3.2.3)
         
     | 
| 
       35 
35 
     | 
    
         
             
                chronic (0.10.2)
         
     | 
| 
       36 
36 
     | 
    
         
             
                concurrent-ruby (1.3.5)
         
     | 
| 
       37 
37 
     | 
    
         
             
                connection_pool (2.5.3)
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -9,7 +9,7 @@ 
     | 
|
| 
       9 
9 
     | 
    
         
             
            _If you're one of the rare people like me who find this useful, feel free to
         
     | 
| 
       10 
10 
     | 
    
         
             
            [buy me some coffee][donate]._
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
            The current version of `na` is 1.2. 
     | 
| 
      
 12 
     | 
    
         
            +
            The current version of `na` is 1.2.80.
         
     | 
| 
       13 
13 
     | 
    
         | 
| 
       14 
14 
     | 
    
         
             
            `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder. 
         
     | 
| 
       15 
15 
     | 
    
         | 
| 
         @@ -76,7 +76,7 @@ SYNOPSIS 
     | 
|
| 
       76 
76 
     | 
    
         
             
                na [global options] command [command options] [arguments...]
         
     | 
| 
       77 
77 
     | 
    
         | 
| 
       78 
78 
     | 
    
         
             
            VERSION
         
     | 
| 
       79 
     | 
    
         
            -
                1.2. 
     | 
| 
      
 79 
     | 
    
         
            +
                1.2.80
         
     | 
| 
       80 
80 
     | 
    
         | 
| 
       81 
81 
     | 
    
         
             
            GLOBAL OPTIONS
         
     | 
| 
       82 
82 
     | 
    
         
             
                -a, --add               - Add a next action (deprecated, for backwards compatibility)
         
     | 
| 
         @@ -93,7 +93,7 @@ GLOBAL OPTIONS 
     | 
|
| 
       93 
93 
     | 
    
         
             
                -p, --priority=PRIORITY - Set a priority 0-5 (deprecated, for backwards compatibility) (default: none)
         
     | 
| 
       94 
94 
     | 
    
         
             
                --[no-]pager            - Enable pagination (default: enabled)
         
     | 
| 
       95 
95 
     | 
    
         
             
                -r, --[no-]recurse      - Recurse 3 directories deep (deprecated, for backwards compatability)
         
     | 
| 
       96 
     | 
    
         
            -
                --[no-]repo 
     | 
| 
      
 96 
     | 
    
         
            +
                --[no-]repo-top         - Use a taskpaper file named after the git repository (enables git integration)
         
     | 
| 
       97 
97 
     | 
    
         
             
                -t, --na_tag=TAG        - Tag to consider a next action (default: na)
         
     | 
| 
       98 
98 
     | 
    
         
             
                --template=arg          - Provide a template for new/blank todo files, use initconfig to make permanent (default: none)
         
     | 
| 
       99 
99 
     | 
    
         
             
                --version               - Display the program version
         
     | 
    
        data/bin/commands/update.rb
    CHANGED
    
    | 
         @@ -189,6 +189,23 @@ class App 
     | 
|
| 
       189 
189 
     | 
    
         
             
                  note = stdin_note.empty? ? [] : stdin_note
         
     | 
| 
       190 
190 
     | 
    
         
             
                  note.concat(line_note) unless line_note.nil? || line_note.empty?
         
     | 
| 
       191 
191 
     | 
    
         | 
| 
      
 192 
     | 
    
         
            +
                  # Require at least one actionable option to be provided
         
     | 
| 
      
 193 
     | 
    
         
            +
                  actionable = [
         
     | 
| 
      
 194 
     | 
    
         
            +
                    options[:note],
         
     | 
| 
      
 195 
     | 
    
         
            +
                    (options[:priority].to_i if options[:priority]).to_i > 0,
         
     | 
| 
      
 196 
     | 
    
         
            +
                    !options[:move].to_s.empty?,
         
     | 
| 
      
 197 
     | 
    
         
            +
                    !(options[:tag].nil? || options[:tag].empty?),
         
     | 
| 
      
 198 
     | 
    
         
            +
                    !(options[:remove].nil? || options[:remove].empty?),
         
     | 
| 
      
 199 
     | 
    
         
            +
                    !options[:replace].to_s.empty?,
         
     | 
| 
      
 200 
     | 
    
         
            +
                    options[:finish],
         
     | 
| 
      
 201 
     | 
    
         
            +
                    options[:archive],
         
     | 
| 
      
 202 
     | 
    
         
            +
                    options[:restore],
         
     | 
| 
      
 203 
     | 
    
         
            +
                    options[:delete],
         
     | 
| 
      
 204 
     | 
    
         
            +
                    options[:edit]
         
     | 
| 
      
 205 
     | 
    
         
            +
                  ].any?
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
                  NA.notify("#{NA.theme[:error]}No action specified, see `na help update`", exit_code: 1) unless actionable
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
       192 
209 
     | 
    
         
             
                  target_proj = if options[:move]
         
     | 
| 
       193 
210 
     | 
    
         
             
                                  options[:move]
         
     | 
| 
       194 
211 
     | 
    
         
             
                                elsif NA.cwd_is == :project
         
     | 
    
        data/bin/na
    CHANGED
    
    | 
         @@ -5,9 +5,13 @@ $LOAD_PATH.unshift File.join(__dir__, '..', 'lib') 
     | 
|
| 
       5 
5 
     | 
    
         
             
            require 'gli'
         
     | 
| 
       6 
6 
     | 
    
         
             
            require 'na/help_monkey_patch'
         
     | 
| 
       7 
7 
     | 
    
         
             
            require 'na'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'na/benchmark'
         
     | 
| 
       8 
9 
     | 
    
         
             
            require 'fcntl'
         
     | 
| 
       9 
10 
     | 
    
         
             
            require 'tempfile'
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
            NA::Benchmark.init
         
     | 
| 
      
 13 
     | 
    
         
            +
            NA::Benchmark.measure('Gem loading') { nil }  # Measures time up to this point
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
       11 
15 
     | 
    
         
             
            # Search for XDG compliant config first. Default to ~/.na.rc for compatibility
         
     | 
| 
       12 
16 
     | 
    
         
             
            def self.find_config_file
         
     | 
| 
       13 
17 
     | 
    
         
             
              home = ENV['HOME']
         
     | 
| 
         @@ -75,9 +79,9 @@ class App 
     | 
|
| 
       75 
79 
     | 
    
         
             
              arg_name 'PATH'
         
     | 
| 
       76 
80 
     | 
    
         
             
              flag %i[f file]
         
     | 
| 
       77 
81 
     | 
    
         | 
| 
       78 
     | 
    
         
            -
              desc 'Use a taskpaper file named after the git repository'
         
     | 
| 
       79 
     | 
    
         
            -
              arg_name ' 
     | 
| 
       80 
     | 
    
         
            -
              switch %i[repo],  
     | 
| 
      
 82 
     | 
    
         
            +
              desc 'Use a taskpaper file named after the git repository (enables git integration)'
         
     | 
| 
      
 83 
     | 
    
         
            +
              arg_name 'REPO_TOP'
         
     | 
| 
      
 84 
     | 
    
         
            +
              switch %i[repo-top], default_value: false
         
     | 
| 
       81 
85 
     | 
    
         | 
| 
       82 
86 
     | 
    
         
             
              desc 'Provide a template for new/blank todo files, use initconfig to make permanent'
         
     | 
| 
       83 
87 
     | 
    
         
             
              flag %[template]
         
     | 
| 
         @@ -125,8 +129,8 @@ class App 
     | 
|
| 
       125 
129 
     | 
    
         
             
                            end
         
     | 
| 
       126 
130 
     | 
    
         | 
| 
       127 
131 
     | 
    
         
             
                # start of git repo addition ==================================
         
     | 
| 
       128 
     | 
    
         
            -
                #  
     | 
| 
       129 
     | 
    
         
            -
                if global[: 
     | 
| 
      
 132 
     | 
    
         
            +
                # Use git repo if --repo-top flag is specified
         
     | 
| 
      
 133 
     | 
    
         
            +
                if global[:repo_top]
         
     | 
| 
       130 
134 
     | 
    
         
             
                  begin
         
     | 
| 
       131 
135 
     | 
    
         
             
                    require 'git'
         
     | 
| 
       132 
136 
     | 
    
         | 
| 
         @@ -209,4 +213,6 @@ ARGV.each do |arg| 
     | 
|
| 
       209 
213 
     | 
    
         
             
            end
         
     | 
| 
       210 
214 
     | 
    
         
             
            NA.command = NA.command_line[0]
         
     | 
| 
       211 
215 
     | 
    
         | 
| 
       212 
     | 
    
         
            -
             
     | 
| 
      
 216 
     | 
    
         
            +
            exit_code = App.run(ARGV)
         
     | 
| 
      
 217 
     | 
    
         
            +
            NA::Benchmark.report
         
     | 
| 
      
 218 
     | 
    
         
            +
            exit exit_code
         
     | 
    
        data/lib/na/action.rb
    CHANGED
    
    | 
         @@ -53,6 +53,15 @@ module NA 
     | 
|
| 
       53 
53 
     | 
    
         
             
                  "(#{@file}:#{@line}) #{@project}:#{@parent.join('>')} | #{@action}#{note}"
         
     | 
| 
       54 
54 
     | 
    
         
             
                end
         
     | 
| 
       55 
55 
     | 
    
         | 
| 
      
 56 
     | 
    
         
            +
                def to_s_pretty
         
     | 
| 
      
 57 
     | 
    
         
            +
                  note = if @note.count.positive?
         
     | 
| 
      
 58 
     | 
    
         
            +
                           "\n#{@note.join("\n")}"
         
     | 
| 
      
 59 
     | 
    
         
            +
                         else
         
     | 
| 
      
 60 
     | 
    
         
            +
                           ''
         
     | 
| 
      
 61 
     | 
    
         
            +
                         end
         
     | 
| 
      
 62 
     | 
    
         
            +
                  "#{NA.theme[:filename]}#{File.basename(@file)}:#{@line}#{NA.theme[:bracket]}[#{NA.theme[:project]}#{@project}:#{@parent.join(">")}#{NA.theme[:bracket]}]{x} | #{NA.theme[:action]}#{@action}#{NA.theme[:note]}#{note}"
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
       56 
65 
     | 
    
         
             
                def inspect
         
     | 
| 
       57 
66 
     | 
    
         
             
                  <<~EOINSPECT
         
     | 
| 
       58 
67 
     | 
    
         
             
                  @file: #{@file}
         
     | 
| 
         @@ -75,65 +84,92 @@ module NA 
     | 
|
| 
       75 
84 
     | 
    
         
             
                ## @param      notes      [Boolean] Include notes
         
     | 
| 
       76 
85 
     | 
    
         
             
                ##
         
     | 
| 
       77 
86 
     | 
    
         
             
                def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true)
         
     | 
| 
       78 
     | 
    
         
            -
                   
     | 
| 
       79 
     | 
    
         
            -
             
     | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
             
     | 
| 
       82 
     | 
    
         
            -
             
     | 
| 
       83 
     | 
    
         
            -
                     
     | 
| 
       84 
     | 
    
         
            -
             
     | 
| 
       85 
     | 
    
         
            -
             
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
             
     | 
| 
       88 
     | 
    
         
            -
             
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
     | 
    
         
            -
             
     | 
| 
       93 
     | 
    
         
            -
             
     | 
| 
       94 
     | 
    
         
            -
             
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
             
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
       99 
     | 
    
         
            -
             
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
             
     | 
| 
       102 
     | 
    
         
            -
             
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
             
     | 
| 
       106 
     | 
    
         
            -
             
     | 
| 
       107 
     | 
    
         
            -
             
     | 
| 
       108 
     | 
    
         
            -
             
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
             
     | 
| 
       111 
     | 
    
         
            -
             
     | 
| 
       112 
     | 
    
         
            -
             
     | 
| 
       113 
     | 
    
         
            -
             
     | 
| 
       114 
     | 
    
         
            -
             
     | 
| 
       115 
     | 
    
         
            -
             
     | 
| 
       116 
     | 
    
         
            -
             
     | 
| 
       117 
     | 
    
         
            -
             
     | 
| 
       118 
     | 
    
         
            -
             
     | 
| 
       119 
     | 
    
         
            -
             
     | 
| 
       120 
     | 
    
         
            -
             
     | 
| 
       121 
     | 
    
         
            -
             
     | 
| 
       122 
     | 
    
         
            -
             
     | 
| 
       123 
     | 
    
         
            -
             
     | 
| 
       124 
     | 
    
         
            -
             
     | 
| 
       125 
     | 
    
         
            -
             
     | 
| 
       126 
     | 
    
         
            -
             
     | 
| 
       127 
     | 
    
         
            -
             
     | 
| 
       128 
     | 
    
         
            -
             
     | 
| 
       129 
     | 
    
         
            -
             
     | 
| 
      
 87 
     | 
    
         
            +
                  NA::Benchmark.measure('Action.pretty') do
         
     | 
| 
      
 88 
     | 
    
         
            +
                    # Use cached theme instead of loading every time
         
     | 
| 
      
 89 
     | 
    
         
            +
                    theme = NA.theme
         
     | 
| 
      
 90 
     | 
    
         
            +
                    template = theme.merge(template)
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                    # Pre-compute common template parts to avoid repeated processing
         
     | 
| 
      
 93 
     | 
    
         
            +
                    output_template = template[:templates][:output]
         
     | 
| 
      
 94 
     | 
    
         
            +
                    needs_filename = output_template.include?('%filename')
         
     | 
| 
      
 95 
     | 
    
         
            +
                    needs_parents = output_template.include?('%parents') || output_template.include?('%parent')
         
     | 
| 
      
 96 
     | 
    
         
            +
                    needs_project = output_template.include?('%project')
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                    # Create the hierarchical parent string (optimized)
         
     | 
| 
      
 99 
     | 
    
         
            +
                    parents = if needs_parents && @parent.any?
         
     | 
| 
      
 100 
     | 
    
         
            +
                                parent_parts = @parent.map { |par| "#{template[:parent]}#{par}" }.join(template[:parent_divider])
         
     | 
| 
      
 101 
     | 
    
         
            +
                                NA::Color.template("{x}#{template[:bracket]}[#{template[:error]}#{parent_parts}#{template[:bracket]}]{x} ")
         
     | 
| 
      
 102 
     | 
    
         
            +
                              else
         
     | 
| 
      
 103 
     | 
    
         
            +
                                ''
         
     | 
| 
      
 104 
     | 
    
         
            +
                              end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                    # Create the project string (optimized)
         
     | 
| 
      
 107 
     | 
    
         
            +
                    project = if needs_project && !@project.empty?
         
     | 
| 
      
 108 
     | 
    
         
            +
                                NA::Color.template("#{template[:project]}#{@project}{x} ")
         
     | 
| 
      
 109 
     | 
    
         
            +
                              else
         
     | 
| 
      
 110 
     | 
    
         
            +
                                ''
         
     | 
| 
      
 111 
     | 
    
         
            +
                              end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                    # Create the source filename string (optimized)
         
     | 
| 
      
 114 
     | 
    
         
            +
                    filename = if needs_filename
         
     | 
| 
      
 115 
     | 
    
         
            +
                                 file = @file.sub(%r{^\./}, '').sub(/#{ENV['HOME']}/, '~')
         
     | 
| 
      
 116 
     | 
    
         
            +
                                 file = file.sub(/\.#{extension}$/, '') unless NA.include_ext
         
     | 
| 
      
 117 
     | 
    
         
            +
                                 file = file.highlight_filename
         
     | 
| 
      
 118 
     | 
    
         
            +
                                 NA::Color.template("#{template[:filename]}#{file} {x}")
         
     | 
| 
      
 119 
     | 
    
         
            +
                               else
         
     | 
| 
      
 120 
     | 
    
         
            +
                                 ''
         
     | 
| 
      
 121 
     | 
    
         
            +
                               end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                    # colorize the action and highlight tags (optimized)
         
     | 
| 
      
 124 
     | 
    
         
            +
                    action_text = @action.dup
         
     | 
| 
      
 125 
     | 
    
         
            +
                    action_text.gsub!(/\{(.*?)\}/, '\\{\1\\}')
         
     | 
| 
      
 126 
     | 
    
         
            +
                    action_text = action_text.sub(/ @#{NA.na_tag}\b/, '')
         
     | 
| 
      
 127 
     | 
    
         
            +
                    action = NA::Color.template("#{template[:action]}#{action_text}{x}")
         
     | 
| 
      
 128 
     | 
    
         
            +
                    action = action.highlight_tags(color: template[:tags],
         
     | 
| 
      
 129 
     | 
    
         
            +
                                                   parens: template[:value_parens],
         
     | 
| 
      
 130 
     | 
    
         
            +
                                                   value: template[:values],
         
     | 
| 
      
 131 
     | 
    
         
            +
                                                   last_color: template[:action])
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                    # Handle notes and wrapping (optimized)
         
     | 
| 
      
 134 
     | 
    
         
            +
                    note = ''
         
     | 
| 
      
 135 
     | 
    
         
            +
                    if @note.any?
         
     | 
| 
      
 136 
     | 
    
         
            +
                      if notes
         
     | 
| 
      
 137 
     | 
    
         
            +
                        if detect_width
         
     | 
| 
      
 138 
     | 
    
         
            +
                          # Cache width calculation
         
     | 
| 
      
 139 
     | 
    
         
            +
                          width = @cached_width ||= TTY::Screen.columns
         
     | 
| 
      
 140 
     | 
    
         
            +
                          # Calculate indent more efficiently - avoid repeated template processing
         
     | 
| 
      
 141 
     | 
    
         
            +
                          base_template = output_template.gsub(/%action/, '').gsub(/%note/, '')
         
     | 
| 
      
 142 
     | 
    
         
            +
                          base_output = base_template.gsub(/%filename/, filename).gsub(/%project/, project).gsub(/%parents?/, parents)
         
     | 
| 
      
 143 
     | 
    
         
            +
                          indent = NA::Color.uncolor(NA::Color.template(base_output)).length
         
     | 
| 
      
 144 
     | 
    
         
            +
                          note = NA::Color.template(@note.wrap(width, indent, template[:note]))
         
     | 
| 
      
 145 
     | 
    
         
            +
                        else
         
     | 
| 
      
 146 
     | 
    
         
            +
                          note = NA::Color.template("\n#{@note.map { |l| "  #{template[:note]}• #{l}{x}" }.join("\n")}")
         
     | 
| 
      
 147 
     | 
    
         
            +
                        end
         
     | 
| 
      
 148 
     | 
    
         
            +
                      else
         
     | 
| 
      
 149 
     | 
    
         
            +
                        action += "#{template[:note]}*"
         
     | 
| 
      
 150 
     | 
    
         
            +
                      end
         
     | 
| 
      
 151 
     | 
    
         
            +
                    end
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                    # Wrap action if needed (optimized)
         
     | 
| 
      
 154 
     | 
    
         
            +
                    if detect_width && !action.empty?
         
     | 
| 
      
 155 
     | 
    
         
            +
                      width = @cached_width ||= TTY::Screen.columns
         
     | 
| 
      
 156 
     | 
    
         
            +
                      base_template = output_template.gsub(/%action/, '').gsub(/%note/, '')
         
     | 
| 
      
 157 
     | 
    
         
            +
                      base_output = base_template.gsub(/%filename/, filename).gsub(/%project/, project).gsub(/%parents?/, parents)
         
     | 
| 
      
 158 
     | 
    
         
            +
                      indent = NA::Color.uncolor(NA::Color.template(base_output)).length
         
     | 
| 
      
 159 
     | 
    
         
            +
                      action = action.wrap(width, indent)
         
     | 
| 
      
 160 
     | 
    
         
            +
                    end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                    # Replace variables in template string and output colorized (optimized)
         
     | 
| 
      
 163 
     | 
    
         
            +
                    final_output = output_template.dup
         
     | 
| 
      
 164 
     | 
    
         
            +
                    final_output.gsub!(/%filename/, filename)
         
     | 
| 
      
 165 
     | 
    
         
            +
                    final_output.gsub!(/%project/, project)
         
     | 
| 
      
 166 
     | 
    
         
            +
                    final_output.gsub!(/%parents?/, parents)
         
     | 
| 
      
 167 
     | 
    
         
            +
                    final_output.gsub!(/%action/, action.highlight_search(regexes))
         
     | 
| 
      
 168 
     | 
    
         
            +
                    final_output.gsub!(/%note/, note)
         
     | 
| 
      
 169 
     | 
    
         
            +
                    final_output.gsub!(/\\\{/, '{')
         
     | 
| 
       130 
170 
     | 
    
         | 
| 
       131 
     | 
    
         
            -
             
     | 
| 
       132 
     | 
    
         
            -
                   
     | 
| 
       133 
     | 
    
         
            -
                                      .gsub(/%project/, project)
         
     | 
| 
       134 
     | 
    
         
            -
                                      .gsub(/%parents?/, parents)
         
     | 
| 
       135 
     | 
    
         
            -
                                      .gsub(/%action/, action.highlight_search(regexes))
         
     | 
| 
       136 
     | 
    
         
            -
                                      .gsub(/%note/, note)).gsub(/\\\{/, '{')
         
     | 
| 
      
 171 
     | 
    
         
            +
                    NA::Color.template(final_output)
         
     | 
| 
      
 172 
     | 
    
         
            +
                  end
         
     | 
| 
       137 
173 
     | 
    
         
             
                end
         
     | 
| 
       138 
174 
     | 
    
         | 
| 
       139 
175 
     | 
    
         
             
                def tags_match?(any: [], all: [], none: [])
         
     | 
| 
         @@ -150,8 +186,9 @@ module NA 
     | 
|
| 
       150 
186 
     | 
    
         | 
| 
       151 
187 
     | 
    
         
             
                def search_matches_none(regexes, include_note: true)
         
     | 
| 
       152 
188 
     | 
    
         
             
                  regexes.each do |rx|
         
     | 
| 
       153 
     | 
    
         
            -
                     
     | 
| 
       154 
     | 
    
         
            -
                     
     | 
| 
      
 189 
     | 
    
         
            +
                    regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
         
     | 
| 
      
 190 
     | 
    
         
            +
                    note_matches = include_note && @note.join(' ').match(regex)
         
     | 
| 
      
 191 
     | 
    
         
            +
                    return false if @action.match(regex) || note_matches
         
     | 
| 
       155 
192 
     | 
    
         
             
                  end
         
     | 
| 
       156 
193 
     | 
    
         
             
                  true
         
     | 
| 
       157 
194 
     | 
    
         
             
                end
         
     | 
| 
         @@ -160,16 +197,18 @@ module NA 
     | 
|
| 
       160 
197 
     | 
    
         
             
                  return true if regexes.empty?
         
     | 
| 
       161 
198 
     | 
    
         | 
| 
       162 
199 
     | 
    
         
             
                  regexes.each do |rx|
         
     | 
| 
       163 
     | 
    
         
            -
                     
     | 
| 
       164 
     | 
    
         
            -
                     
     | 
| 
      
 200 
     | 
    
         
            +
                    regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
         
     | 
| 
      
 201 
     | 
    
         
            +
                    note_matches = include_note && @note.join(' ').match(regex)
         
     | 
| 
      
 202 
     | 
    
         
            +
                    return true if @action.match(regex) || note_matches
         
     | 
| 
       165 
203 
     | 
    
         
             
                  end
         
     | 
| 
       166 
204 
     | 
    
         
             
                  false
         
     | 
| 
       167 
205 
     | 
    
         
             
                end
         
     | 
| 
       168 
206 
     | 
    
         | 
| 
       169 
207 
     | 
    
         
             
                def search_matches_all(regexes, include_note: true)
         
     | 
| 
       170 
208 
     | 
    
         
             
                  regexes.each do |rx|
         
     | 
| 
       171 
     | 
    
         
            -
                     
     | 
| 
       172 
     | 
    
         
            -
                     
     | 
| 
      
 209 
     | 
    
         
            +
                    regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
         
     | 
| 
      
 210 
     | 
    
         
            +
                    note_matches = include_note && @note.join(' ').match(regex)
         
     | 
| 
      
 211 
     | 
    
         
            +
                    return false unless @action.match(regex) || note_matches
         
     | 
| 
       173 
212 
     | 
    
         
             
                  end
         
     | 
| 
       174 
213 
     | 
    
         
             
                  true
         
     | 
| 
       175 
214 
     | 
    
         
             
                end
         
     | 
| 
         @@ -198,7 +237,8 @@ module NA 
     | 
|
| 
       198 
237 
     | 
    
         
             
                end
         
     | 
| 
       199 
238 
     | 
    
         | 
| 
       200 
239 
     | 
    
         
             
                def compare_tag(tag)
         
     | 
| 
       201 
     | 
    
         
            -
                   
     | 
| 
      
 240 
     | 
    
         
            +
                  tag_regex = tag[:tag].is_a?(Regexp) ? tag[:tag] : Regexp.new(tag[:tag], Regexp::IGNORECASE)
         
     | 
| 
      
 241 
     | 
    
         
            +
                  keys = @tags.keys.delete_if { |k| k !~ tag_regex }
         
     | 
| 
       202 
242 
     | 
    
         
             
                  return false if keys.empty?
         
     | 
| 
       203 
243 
     | 
    
         | 
| 
       204 
244 
     | 
    
         
             
                  key = keys[0]
         
     | 
| 
         @@ -211,6 +251,7 @@ module NA 
     | 
|
| 
       211 
251 
     | 
    
         | 
| 
       212 
252 
     | 
    
         
             
                  begin
         
     | 
| 
       213 
253 
     | 
    
         
             
                    tag_date = Time.parse(tag_val)
         
     | 
| 
      
 254 
     | 
    
         
            +
                    require 'chronic' unless defined?(Chronic)
         
     | 
| 
       214 
255 
     | 
    
         
             
                    date = Chronic.parse(val)
         
     | 
| 
       215 
256 
     | 
    
         | 
| 
       216 
257 
     | 
    
         
             
                    raise ArgumentError if date.nil?
         
     | 
    
        data/lib/na/actions.rb
    CHANGED
    
    | 
         @@ -24,15 +24,16 @@ module NA 
     | 
|
| 
       24 
24 
     | 
    
         
             
                ## @return [String] The output string
         
     | 
| 
       25 
25 
     | 
    
         
             
                ##
         
     | 
| 
       26 
26 
     | 
    
         
             
                def output(depth, config = {})
         
     | 
| 
       27 
     | 
    
         
            -
                   
     | 
| 
       28 
     | 
    
         
            -
                     
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
      
 27 
     | 
    
         
            +
                  NA::Benchmark.measure('Actions.output') do
         
     | 
| 
      
 28 
     | 
    
         
            +
                    defaults = {
         
     | 
| 
      
 29 
     | 
    
         
            +
                      files: nil,
         
     | 
| 
      
 30 
     | 
    
         
            +
                      regexes: [],
         
     | 
| 
      
 31 
     | 
    
         
            +
                      notes: false,
         
     | 
| 
      
 32 
     | 
    
         
            +
                      nest: false,
         
     | 
| 
      
 33 
     | 
    
         
            +
                      nest_projects: false,
         
     | 
| 
      
 34 
     | 
    
         
            +
                      no_files: false,
         
     | 
| 
      
 35 
     | 
    
         
            +
                    }
         
     | 
| 
      
 36 
     | 
    
         
            +
                    config = defaults.merge(config)
         
     | 
| 
       36 
37 
     | 
    
         | 
| 
       37 
38 
     | 
    
         
             
                  return if config[:files].nil?
         
     | 
| 
       38 
39 
     | 
    
         | 
| 
         @@ -73,21 +74,43 @@ module NA 
     | 
|
| 
       73 
74 
     | 
    
         
             
                    end
         
     | 
| 
       74 
75 
     | 
    
         
             
                    NA::Pager.page out.join("\n")
         
     | 
| 
       75 
76 
     | 
    
         
             
                  else
         
     | 
| 
       76 
     | 
    
         
            -
                     
     | 
| 
       77 
     | 
    
         
            -
             
     | 
| 
       78 
     | 
    
         
            -
             
     | 
| 
       79 
     | 
    
         
            -
             
     | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
             
     | 
| 
       82 
     | 
    
         
            -
             
     | 
| 
       83 
     | 
    
         
            -
             
     | 
| 
       84 
     | 
    
         
            -
             
     | 
| 
      
 77 
     | 
    
         
            +
                    # Optimize template selection
         
     | 
| 
      
 78 
     | 
    
         
            +
                    template = case
         
     | 
| 
      
 79 
     | 
    
         
            +
                    when config[:no_files]
         
     | 
| 
      
 80 
     | 
    
         
            +
                      NA.theme[:templates][:no_file]
         
     | 
| 
      
 81 
     | 
    
         
            +
                    when config[:files]&.count&.positive?
         
     | 
| 
      
 82 
     | 
    
         
            +
                      config[:files].count == 1 ? NA.theme[:templates][:single_file] : NA.theme[:templates][:multi_file]
         
     | 
| 
      
 83 
     | 
    
         
            +
                    when depth > 1
         
     | 
| 
      
 84 
     | 
    
         
            +
                      NA.theme[:templates][:multi_file]
         
     | 
| 
      
 85 
     | 
    
         
            +
                    else
         
     | 
| 
      
 86 
     | 
    
         
            +
                      NA.theme[:templates][:default]
         
     | 
| 
      
 87 
     | 
    
         
            +
                    end
         
     | 
| 
       85 
88 
     | 
    
         
             
                    template += "%note" if config[:notes]
         
     | 
| 
       86 
89 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
                     
     | 
| 
      
 90 
     | 
    
         
            +
                    # Skip debug output if not verbose
         
     | 
| 
      
 91 
     | 
    
         
            +
                    config[:files]&.each { |f| NA.notify(f, debug: true) } if config[:files] && NA.verbose
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                    # Optimize output generation - compile all output first, then apply regexes
         
     | 
| 
      
 94 
     | 
    
         
            +
                    output = String.new
         
     | 
| 
      
 95 
     | 
    
         
            +
                    NA::Benchmark.measure('Generate action strings') do
         
     | 
| 
      
 96 
     | 
    
         
            +
                      each_with_index do |action, idx|
         
     | 
| 
      
 97 
     | 
    
         
            +
                        # Generate raw output without regex processing
         
     | 
| 
      
 98 
     | 
    
         
            +
                        output << action.pretty(template: { templates: { output: template } }, regexes: [], notes: config[:notes])
         
     | 
| 
      
 99 
     | 
    
         
            +
                        output << "\n" unless idx == size - 1
         
     | 
| 
      
 100 
     | 
    
         
            +
                      end
         
     | 
| 
      
 101 
     | 
    
         
            +
                    end
         
     | 
| 
       88 
102 
     | 
    
         | 
| 
       89 
     | 
    
         
            -
                     
     | 
| 
       90 
     | 
    
         
            -
                     
     | 
| 
      
 103 
     | 
    
         
            +
                    # Apply regex highlighting to the entire output at once
         
     | 
| 
      
 104 
     | 
    
         
            +
                    if config[:regexes].any?
         
     | 
| 
      
 105 
     | 
    
         
            +
                      NA::Benchmark.measure('Apply regex highlighting') do
         
     | 
| 
      
 106 
     | 
    
         
            +
                        output = output.highlight_search(config[:regexes])
         
     | 
| 
      
 107 
     | 
    
         
            +
                      end
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                    NA::Benchmark.measure('Pager.page call') do
         
     | 
| 
      
 111 
     | 
    
         
            +
                      NA::Pager.page(output)
         
     | 
| 
      
 112 
     | 
    
         
            +
                    end
         
     | 
| 
      
 113 
     | 
    
         
            +
                  end
         
     | 
| 
       91 
114 
     | 
    
         
             
                  end
         
     | 
| 
       92 
115 
     | 
    
         
             
                end
         
     | 
| 
       93 
116 
     | 
    
         
             
              end
         
     | 
    
        data/lib/na/benchmark.rb
    ADDED
    
    | 
         @@ -0,0 +1,45 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module NA
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Benchmark
         
     | 
| 
      
 5 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 6 
     | 
    
         
            +
                  attr_accessor :enabled, :timings
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def init
         
     | 
| 
      
 9 
     | 
    
         
            +
                    @enabled = ENV['NA_BENCHMARK'] == '1' || ENV['NA_BENCHMARK'] == 'true'
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @timings = []
         
     | 
| 
      
 11 
     | 
    
         
            +
                    @start_time = Time.now
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  def measure(label)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    return yield unless @enabled
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    start = Time.now
         
     | 
| 
      
 18 
     | 
    
         
            +
                    result = yield
         
     | 
| 
      
 19 
     | 
    
         
            +
                    duration = ((Time.now - start) * 1000).round(2)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    @timings << { label: label, duration: duration, timestamp: (start - @start_time) * 1000 }
         
     | 
| 
      
 21 
     | 
    
         
            +
                    result
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def report
         
     | 
| 
      
 25 
     | 
    
         
            +
                    return unless @enabled
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    total = @timings.sum { |t| t[:duration] }
         
     | 
| 
      
 28 
     | 
    
         
            +
                    $stderr.puts "\n#{NA::Color.template('{y}=== NA Performance Report ===')}"
         
     | 
| 
      
 29 
     | 
    
         
            +
                    $stderr.puts NA::Color.template("{dw}Total: {bw}#{total.round(2)}ms{x}")
         
     | 
| 
      
 30 
     | 
    
         
            +
                    $stderr.puts NA::Color.template("{dw}GC Count: {bw}#{GC.count}{x}") if defined?(GC)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    $stderr.puts NA::Color.template("{dw}Memory: {bw}#{(GC.stat[:heap_live_slots] * 40 / 1024.0).round(1)}KB{x}") if defined?(GC)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    $stderr.puts ""
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    @timings.each do |timing|
         
     | 
| 
      
 35 
     | 
    
         
            +
                      pct = total > 0 ? ((timing[:duration] / total) * 100).round(1) : 0
         
     | 
| 
      
 36 
     | 
    
         
            +
                      bar = '█' * [(pct / 2).round, 50].min
         
     | 
| 
      
 37 
     | 
    
         
            +
                      $stderr.puts NA::Color.template(
         
     | 
| 
      
 38 
     | 
    
         
            +
                        "{dw}[{y}#{bar.ljust(25)}{dw}] {bw}#{timing[:duration].to_s.rjust(7)}ms {dw}(#{pct.to_s.rjust(5)}%) {x}#{timing[:label]}"
         
     | 
| 
      
 39 
     | 
    
         
            +
                      )
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
                    $stderr.puts NA::Color.template("{y}#{'=' * 50}{x}\n")
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/na/colors.rb
    CHANGED
    
    | 
         @@ -196,6 +196,24 @@ module NA 
     | 
|
| 
       196 
196 
     | 
    
         | 
| 
       197 
197 
     | 
    
         
             
                  attr_writer :coloring
         
     | 
| 
       198 
198 
     | 
    
         | 
| 
      
 199 
     | 
    
         
            +
                  # Cache for compiled templates to avoid repeated regex processing
         
     | 
| 
      
 200 
     | 
    
         
            +
                  def template_cache
         
     | 
| 
      
 201 
     | 
    
         
            +
                    @template_cache ||= {}
         
     | 
| 
      
 202 
     | 
    
         
            +
                  end
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                  def clear_template_cache
         
     | 
| 
      
 205 
     | 
    
         
            +
                    @template_cache = {}
         
     | 
| 
      
 206 
     | 
    
         
            +
                  end
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                  # Pre-computed colors hash (expensive to create, so we cache it)
         
     | 
| 
      
 209 
     | 
    
         
            +
                  def colors_hash
         
     | 
| 
      
 210 
     | 
    
         
            +
                    @colors_hash ||= { w: white, k: black, g: green, l: blue,
         
     | 
| 
      
 211 
     | 
    
         
            +
                                      y: yellow, c: cyan, m: magenta, r: red,
         
     | 
| 
      
 212 
     | 
    
         
            +
                                      W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
         
     | 
| 
      
 213 
     | 
    
         
            +
                                      Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
         
     | 
| 
      
 214 
     | 
    
         
            +
                                      d: dark, b: bold, u: underline, i: italic, x: reset }
         
     | 
| 
      
 215 
     | 
    
         
            +
                  end
         
     | 
| 
      
 216 
     | 
    
         
            +
             
     | 
| 
       199 
217 
     | 
    
         
             
                  ##
         
     | 
| 
       200 
218 
     | 
    
         
             
                  ## Enables colored output
         
     | 
| 
       201 
219 
     | 
    
         
             
                  ##
         
     | 
| 
         @@ -236,23 +254,28 @@ module NA 
     | 
|
| 
       236 
254 
     | 
    
         
             
                    input = input.join(' ') if input.is_a? Array
         
     | 
| 
       237 
255 
     | 
    
         
             
                    return input.gsub(/(?<!\\)\{#?(\w+)\}/i, '') unless NA::Color.coloring?
         
     | 
| 
       238 
256 
     | 
    
         | 
| 
       239 
     | 
    
         
            -
                     
     | 
| 
       240 
     | 
    
         
            -
             
     | 
| 
       241 
     | 
    
         
            -
             
     | 
| 
       242 
     | 
    
         
            -
             
     | 
| 
      
 257 
     | 
    
         
            +
                    # Check cache first
         
     | 
| 
      
 258 
     | 
    
         
            +
                    cache_key = input.hash
         
     | 
| 
      
 259 
     | 
    
         
            +
                    return template_cache[cache_key] if template_cache.key?(cache_key)
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
                    # Process hex colors first
         
     | 
| 
      
 262 
     | 
    
         
            +
                    processed_input = input.gsub(/(?<!\\)\{((?:[fb]g?)?#[a-f0-9]{3,6})\}/i) do
         
     | 
| 
      
 263 
     | 
    
         
            +
                      hex = Regexp.last_match(1)
         
     | 
| 
      
 264 
     | 
    
         
            +
                      rgb(hex)
         
     | 
| 
      
 265 
     | 
    
         
            +
                    end
         
     | 
| 
       243 
266 
     | 
    
         | 
| 
       244 
     | 
    
         
            -
                     
     | 
| 
      
 267 
     | 
    
         
            +
                    # Convert to format string
         
     | 
| 
      
 268 
     | 
    
         
            +
                    fmt = processed_input.gsub(/%/, '%%')
         
     | 
| 
       245 
269 
     | 
    
         
             
                    fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
         
     | 
| 
       246 
270 
     | 
    
         
             
                      Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
         
     | 
| 
       247 
271 
     | 
    
         
             
                    end
         
     | 
| 
       248 
272 
     | 
    
         | 
| 
       249 
     | 
    
         
            -
                     
     | 
| 
       250 
     | 
    
         
            -
             
     | 
| 
       251 
     | 
    
         
            -
                               W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
         
     | 
| 
       252 
     | 
    
         
            -
                               Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
         
     | 
| 
       253 
     | 
    
         
            -
                               d: dark, b: bold, u: underline, i: italic, x: reset }
         
     | 
| 
      
 273 
     | 
    
         
            +
                    # Use pre-computed colors hash
         
     | 
| 
      
 274 
     | 
    
         
            +
                    result = format(fmt, colors_hash)
         
     | 
| 
       254 
275 
     | 
    
         | 
| 
       255 
     | 
    
         
            -
                     
     | 
| 
      
 276 
     | 
    
         
            +
                    # Cache the result
         
     | 
| 
      
 277 
     | 
    
         
            +
                    template_cache[cache_key] = result
         
     | 
| 
      
 278 
     | 
    
         
            +
                    result
         
     | 
| 
       256 
279 
     | 
    
         
             
                  end
         
     | 
| 
       257 
280 
     | 
    
         
             
                end
         
     | 
| 
       258 
281 
     | 
    
         | 
    
        data/lib/na/next_action.rb
    CHANGED
    
    | 
         @@ -259,6 +259,7 @@ module NA 
     | 
|
| 
       259 
259 
     | 
    
         
             
                                  tagged: nil)
         
     | 
| 
       260 
260 
     | 
    
         | 
| 
       261 
261 
     | 
    
         
             
                  projects = find_projects(target)
         
     | 
| 
      
 262 
     | 
    
         
            +
                  affected_actions = []
         
     | 
| 
       262 
263 
     | 
    
         | 
| 
       263 
264 
     | 
    
         
             
                  target_proj = nil
         
     | 
| 
       264 
265 
     | 
    
         | 
| 
         @@ -287,9 +288,22 @@ module NA 
     | 
|
| 
       287 
288 
     | 
    
         
             
                    target_proj = if target_proj
         
     | 
| 
       288 
289 
     | 
    
         
             
                                    projects.select { |proj| proj.project =~ /^#{target_proj.project}$/i }.first
         
     | 
| 
       289 
290 
     | 
    
         
             
                                  else
         
     | 
| 
      
 291 
     | 
    
         
            +
                                    # First try exact full-path match
         
     | 
| 
       290 
292 
     | 
    
         
             
                                    projects.select { |proj| proj.project =~ /^#{add.parent.join(':')}$/i }.first
         
     | 
| 
       291 
293 
     | 
    
         
             
                                  end
         
     | 
| 
       292 
294 
     | 
    
         | 
| 
      
 295 
     | 
    
         
            +
                    # If no exact match, try unique suffix match (e.g., :Ideas at end)
         
     | 
| 
      
 296 
     | 
    
         
            +
                    if target_proj.nil?
         
     | 
| 
      
 297 
     | 
    
         
            +
                      leaf = Regexp.escape(add.parent.join(':'))
         
     | 
| 
      
 298 
     | 
    
         
            +
                      suffix_matches = projects.select { |proj| proj.project =~ /(^|:)#{leaf}$/i }
         
     | 
| 
      
 299 
     | 
    
         
            +
                      if suffix_matches.count == 1
         
     | 
| 
      
 300 
     | 
    
         
            +
                        target_proj = suffix_matches.first
         
     | 
| 
      
 301 
     | 
    
         
            +
                      elsif suffix_matches.count > 1 && $stdout.isatty
         
     | 
| 
      
 302 
     | 
    
         
            +
                        choice = choose_from(suffix_matches.map(&:project), prompt: 'Select a target project: ', multiple: false)
         
     | 
| 
      
 303 
     | 
    
         
            +
                        target_proj = projects.select { |proj| proj.project == choice }.first if choice
         
     | 
| 
      
 304 
     | 
    
         
            +
                      end
         
     | 
| 
      
 305 
     | 
    
         
            +
                    end
         
     | 
| 
      
 306 
     | 
    
         
            +
             
     | 
| 
       293 
307 
     | 
    
         
             
                    if target_proj.nil?
         
     | 
| 
       294 
308 
     | 
    
         
             
                      res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{add.project}#{NA.theme[:warning]} doesn't exist, add it"), default: true)
         
     | 
| 
       295 
309 
     | 
    
         | 
| 
         @@ -336,6 +350,15 @@ module NA 
     | 
|
| 
       336 
350 
     | 
    
         
             
                    contents.insert(target_line, "#{indent}\t- #{add.action}#{note}")
         
     | 
| 
       337 
351 
     | 
    
         | 
| 
       338 
352 
     | 
    
         
             
                    notify(add.pretty)
         
     | 
| 
      
 353 
     | 
    
         
            +
             
     | 
| 
      
 354 
     | 
    
         
            +
                    # Track affected action and description
         
     | 
| 
      
 355 
     | 
    
         
            +
                    changes = ["added"]
         
     | 
| 
      
 356 
     | 
    
         
            +
                    changes << "finished" if finish
         
     | 
| 
      
 357 
     | 
    
         
            +
                    changes << "priority=#{priority}" if priority.to_i.positive?
         
     | 
| 
      
 358 
     | 
    
         
            +
                    changes << "tags+#{add_tag.join(',')}" unless add_tag.nil? || add_tag.empty?
         
     | 
| 
      
 359 
     | 
    
         
            +
                    changes << "tags-#{remove_tag.join(',')}" unless remove_tag.nil? || remove_tag.empty?
         
     | 
| 
      
 360 
     | 
    
         
            +
                    changes << "note updated" unless note.nil? || note.empty?
         
     | 
| 
      
 361 
     | 
    
         
            +
                    affected_actions << { action: add, desc: changes.join(', ') }
         
     | 
| 
       339 
362 
     | 
    
         
             
                  else
         
     | 
| 
       340 
363 
     | 
    
         
             
                    _, actions = find_actions(target, search, tagged, done: done, all: all, project: project, search_note: search_note)
         
     | 
| 
       341 
364 
     | 
    
         | 
| 
         @@ -343,7 +366,11 @@ module NA 
     | 
|
| 
       343 
366 
     | 
    
         | 
| 
       344 
367 
     | 
    
         
             
                    actions.sort_by(&:line).reverse.each do |action|
         
     | 
| 
       345 
368 
     | 
    
         
             
                      contents.slice!(action.line, action.note.count + 1)
         
     | 
| 
       346 
     | 
    
         
            -
                       
     | 
| 
      
 369 
     | 
    
         
            +
                      if delete
         
     | 
| 
      
 370 
     | 
    
         
            +
                        # Track deletion before skipping re-insert
         
     | 
| 
      
 371 
     | 
    
         
            +
                        affected_actions << { action: action, desc: 'deleted' }
         
     | 
| 
      
 372 
     | 
    
         
            +
                        next
         
     | 
| 
      
 373 
     | 
    
         
            +
                      end
         
     | 
| 
       347 
374 
     | 
    
         | 
| 
       348 
375 
     | 
    
         
             
                      projects = shift_index_after(projects, action.line, action.note.count + 1)
         
     | 
| 
       349 
376 
     | 
    
         | 
| 
         @@ -395,16 +422,44 @@ module NA 
     | 
|
| 
       395 
422 
     | 
    
         
             
                      contents.insert(target_line, "#{indent}\t- #{action.action}#{note}")
         
     | 
| 
       396 
423 
     | 
    
         | 
| 
       397 
424 
     | 
    
         
             
                      notify(action.pretty)
         
     | 
| 
      
 425 
     | 
    
         
            +
             
     | 
| 
      
 426 
     | 
    
         
            +
                      # Track affected action and description
         
     | 
| 
      
 427 
     | 
    
         
            +
                      changes = []
         
     | 
| 
      
 428 
     | 
    
         
            +
                      changes << "finished" if finish
         
     | 
| 
      
 429 
     | 
    
         
            +
                      changes << "edited" if edit
         
     | 
| 
      
 430 
     | 
    
         
            +
                      changes << "priority=#{priority}" if priority.to_i.positive?
         
     | 
| 
      
 431 
     | 
    
         
            +
                      changes << "tags+#{add_tag.join(',')}" unless add_tag.nil? || add_tag.empty?
         
     | 
| 
      
 432 
     | 
    
         
            +
                      changes << "tags-#{remove_tag.join(',')}" unless remove_tag.nil? || remove_tag.empty?
         
     | 
| 
      
 433 
     | 
    
         
            +
                      changes << "text replaced" if replace
         
     | 
| 
      
 434 
     | 
    
         
            +
                      changes << "moved to #{target_proj.project}" if target_proj
         
     | 
| 
      
 435 
     | 
    
         
            +
                      changes << "note updated" unless note.nil? || note.empty?
         
     | 
| 
      
 436 
     | 
    
         
            +
                      changes = ["updated"] if changes.empty?
         
     | 
| 
      
 437 
     | 
    
         
            +
                      affected_actions << { action: action, desc: changes.join(', ') }
         
     | 
| 
       398 
438 
     | 
    
         
             
                    end
         
     | 
| 
       399 
439 
     | 
    
         
             
                  end
         
     | 
| 
       400 
440 
     | 
    
         | 
| 
       401 
441 
     | 
    
         
             
                  backup_file(target)
         
     | 
| 
       402 
442 
     | 
    
         
             
                  File.open(target, 'w') { |f| f.puts contents.join("\n") }
         
     | 
| 
       403 
443 
     | 
    
         | 
| 
       404 
     | 
    
         
            -
                  if  
     | 
| 
       405 
     | 
    
         
            -
                     
     | 
| 
      
 444 
     | 
    
         
            +
                  if affected_actions.any?
         
     | 
| 
      
 445 
     | 
    
         
            +
                    if affected_actions.all? { |e| e[:desc] =~ /^deleted/ }
         
     | 
| 
      
 446 
     | 
    
         
            +
                      notify("#{NA.theme[:success]}Task deleted in #{NA.theme[:filename]}#{target}")
         
     | 
| 
      
 447 
     | 
    
         
            +
                    elsif add
         
     | 
| 
      
 448 
     | 
    
         
            +
                      notify("#{NA.theme[:success]}Task added to #{NA.theme[:filename]}#{target}")
         
     | 
| 
      
 449 
     | 
    
         
            +
                    else
         
     | 
| 
      
 450 
     | 
    
         
            +
                      notify("#{NA.theme[:success]}Task updated in #{NA.theme[:filename]}#{target}")
         
     | 
| 
      
 451 
     | 
    
         
            +
                    end
         
     | 
| 
      
 452 
     | 
    
         
            +
             
     | 
| 
      
 453 
     | 
    
         
            +
                    affected_actions.reverse.each do |entry|
         
     | 
| 
      
 454 
     | 
    
         
            +
                      action_color = delete ? NA.theme[:error] : NA.theme[:success]
         
     | 
| 
      
 455 
     | 
    
         
            +
                      notify("  #{entry[:action].to_s_pretty} — #{action_color}#{entry[:desc]}")
         
     | 
| 
      
 456 
     | 
    
         
            +
                    end
         
     | 
| 
       406 
457 
     | 
    
         
             
                  else
         
     | 
| 
       407 
     | 
    
         
            -
                     
     | 
| 
      
 458 
     | 
    
         
            +
                    if add
         
     | 
| 
      
 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
         
     | 
| 
       408 
463 
     | 
    
         
             
                  end
         
     | 
| 
       409 
464 
     | 
    
         
             
                end
         
     | 
| 
       410 
465 
     | 
    
         | 
| 
         @@ -499,11 +554,19 @@ module NA 
     | 
|
| 
       499 
554 
     | 
    
         
             
                ## @param      depth  [Number] The depth at which to search
         
     | 
| 
       500 
555 
     | 
    
         
             
                ##
         
     | 
| 
       501 
556 
     | 
    
         
             
                def find_files(depth: 1)
         
     | 
| 
       502 
     | 
    
         
            -
                   
     | 
| 
       503 
     | 
    
         
            -
             
     | 
| 
       504 
     | 
    
         
            -
             
     | 
| 
       505 
     | 
    
         
            -
             
     | 
| 
       506 
     | 
    
         
            -
             
     | 
| 
      
 557 
     | 
    
         
            +
                  NA::Benchmark.measure("find_files (depth=#{depth})") do
         
     | 
| 
      
 558 
     | 
    
         
            +
                    return [NA.global_file] if NA.global_file
         
     | 
| 
      
 559 
     | 
    
         
            +
             
     | 
| 
      
 560 
     | 
    
         
            +
                    pattern = if depth == 1
         
     | 
| 
      
 561 
     | 
    
         
            +
                                "*.#{NA.extension}"
         
     | 
| 
      
 562 
     | 
    
         
            +
                              else
         
     | 
| 
      
 563 
     | 
    
         
            +
                                "{#{'*,' * (depth - 1)}*}.#{NA.extension}"
         
     | 
| 
      
 564 
     | 
    
         
            +
                              end
         
     | 
| 
      
 565 
     | 
    
         
            +
             
     | 
| 
      
 566 
     | 
    
         
            +
                    files = Dir.glob(pattern, File::FNM_DOTMATCH).reject { |f| f.start_with?('.') }
         
     | 
| 
      
 567 
     | 
    
         
            +
                    files.each { |f| save_working_dir(File.expand_path(f)) }
         
     | 
| 
      
 568 
     | 
    
         
            +
                    files
         
     | 
| 
      
 569 
     | 
    
         
            +
                  end
         
     | 
| 
       507 
570 
     | 
    
         
             
                end
         
     | 
| 
       508 
571 
     | 
    
         | 
| 
       509 
572 
     | 
    
         
             
                def find_files_matching(options = {})
         
     | 
| 
         @@ -616,12 +679,14 @@ module NA 
     | 
|
| 
       616 
679 
     | 
    
         
             
                ## @param      todo_file  The todo file path
         
     | 
| 
       617 
680 
     | 
    
         
             
                ##
         
     | 
| 
       618 
681 
     | 
    
         
             
                def save_working_dir(todo_file)
         
     | 
| 
       619 
     | 
    
         
            -
                   
     | 
| 
       620 
     | 
    
         
            -
             
     | 
| 
       621 
     | 
    
         
            -
             
     | 
| 
       622 
     | 
    
         
            -
             
     | 
| 
       623 
     | 
    
         
            -
             
     | 
| 
       624 
     | 
    
         
            -
             
     | 
| 
      
 682 
     | 
    
         
            +
                  NA::Benchmark.measure('save_working_dir') do
         
     | 
| 
      
 683 
     | 
    
         
            +
                    file = database_path
         
     | 
| 
      
 684 
     | 
    
         
            +
                    content = File.exist?(file) ? file.read_file : ''
         
     | 
| 
      
 685 
     | 
    
         
            +
                    dirs = content.split(/\n/)
         
     | 
| 
      
 686 
     | 
    
         
            +
                    dirs.push(File.expand_path(todo_file))
         
     | 
| 
      
 687 
     | 
    
         
            +
                    dirs.sort!.uniq!
         
     | 
| 
      
 688 
     | 
    
         
            +
                    File.open(file, 'w') { |f| f.puts dirs.join("\n") }
         
     | 
| 
      
 689 
     | 
    
         
            +
                  end
         
     | 
| 
       625 
690 
     | 
    
         
             
                end
         
     | 
| 
       626 
691 
     | 
    
         | 
| 
       627 
692 
     | 
    
         
             
                ##
         
     | 
    
        data/lib/na/pager.rb
    CHANGED
    
    | 
         @@ -29,38 +29,37 @@ module NA 
     | 
|
| 
       29 
29 
     | 
    
         
             
                      return
         
     | 
| 
       30 
30 
     | 
    
         
             
                    end
         
     | 
| 
       31 
31 
     | 
    
         | 
| 
      
 32 
     | 
    
         
            +
                    # Skip pagination for small outputs (faster than starting a pager)
         
     | 
| 
      
 33 
     | 
    
         
            +
                    if text.length < 2000 && text.lines.count < 50
         
     | 
| 
      
 34 
     | 
    
         
            +
                      puts text
         
     | 
| 
      
 35 
     | 
    
         
            +
                      return
         
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
       32 
38 
     | 
    
         
             
                    pager = which_pager
         
     | 
| 
      
 39 
     | 
    
         
            +
                    return false unless pager
         
     | 
| 
       33 
40 
     | 
    
         | 
| 
      
 41 
     | 
    
         
            +
                    # Optimized pager execution - use spawn instead of fork+exec
         
     | 
| 
       34 
42 
     | 
    
         
             
                    read_io, write_io = IO.pipe
         
     | 
| 
       35 
43 
     | 
    
         | 
| 
       36 
     | 
    
         
            -
                     
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
                     
     | 
| 
       39 
     | 
    
         
            -
                      write_io.close
         
     | 
| 
       40 
     | 
    
         
            -
                      input.reopen(read_io)
         
     | 
| 
       41 
     | 
    
         
            -
                      read_io.close
         
     | 
| 
       42 
     | 
    
         
            -
             
     | 
| 
       43 
     | 
    
         
            -
                      # Wait until we have input before we start the pager
         
     | 
| 
       44 
     | 
    
         
            -
                      IO.select [input]
         
     | 
| 
       45 
     | 
    
         
            -
             
     | 
| 
       46 
     | 
    
         
            -
                      begin
         
     | 
| 
       47 
     | 
    
         
            -
                        NA.notify("#{NA.theme[:debug]}Pager #{pager}", debug: true)
         
     | 
| 
       48 
     | 
    
         
            -
                        exec(pager)
         
     | 
| 
       49 
     | 
    
         
            -
                      rescue SystemCallError => e
         
     | 
| 
       50 
     | 
    
         
            -
                        raise Errors::DoingStandardError, "Pager error, #{e}"
         
     | 
| 
       51 
     | 
    
         
            -
                      end
         
     | 
| 
       52 
     | 
    
         
            -
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                    # Use spawn for better performance than fork+exec
         
     | 
| 
      
 45 
     | 
    
         
            +
                    pid = spawn(pager, in: read_io, out: $stdout, err: $stderr)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    read_io.close
         
     | 
| 
       53 
47 
     | 
    
         | 
| 
       54 
48 
     | 
    
         
             
                    begin
         
     | 
| 
       55 
     | 
    
         
            -
                       
     | 
| 
      
 49 
     | 
    
         
            +
                      # Write data to pager
         
     | 
| 
       56 
50 
     | 
    
         
             
                      write_io.write(text)
         
     | 
| 
       57 
51 
     | 
    
         
             
                      write_io.close
         
     | 
| 
       58 
     | 
    
         
            -
                    rescue SystemCallError # => e
         
     | 
| 
       59 
     | 
    
         
            -
                      # raise Errors::DoingStandardError, "Pager error, #{e}"
         
     | 
| 
       60 
     | 
    
         
            -
                    end
         
     | 
| 
       61 
52 
     | 
    
         | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
     | 
    
         
            -
             
     | 
| 
      
 53 
     | 
    
         
            +
                      # Wait for pager to complete
         
     | 
| 
      
 54 
     | 
    
         
            +
                      _, status = Process.waitpid2(pid)
         
     | 
| 
      
 55 
     | 
    
         
            +
                      status.success?
         
     | 
| 
      
 56 
     | 
    
         
            +
                    rescue SystemCallError => e
         
     | 
| 
      
 57 
     | 
    
         
            +
                      # Clean up on error
         
     | 
| 
      
 58 
     | 
    
         
            +
                      write_io.close rescue nil
         
     | 
| 
      
 59 
     | 
    
         
            +
                      Process.kill('TERM', pid) rescue nil
         
     | 
| 
      
 60 
     | 
    
         
            +
                      Process.waitpid(pid) rescue nil
         
     | 
| 
      
 61 
     | 
    
         
            +
                      false
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
       64 
63 
     | 
    
         
             
                  end
         
     | 
| 
       65 
64 
     | 
    
         | 
| 
       66 
65 
     | 
    
         
             
                  private
         
     | 
| 
         @@ -89,6 +88,11 @@ module NA 
     | 
|
| 
       89 
88 
     | 
    
         
             
                  def which_pager
         
     | 
| 
       90 
89 
     | 
    
         
             
                    @which_pager ||= find_executable(*pagers)
         
     | 
| 
       91 
90 
     | 
    
         
             
                  end
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                  # Clear pager cache (useful for testing)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  def clear_pager_cache
         
     | 
| 
      
 94 
     | 
    
         
            +
                    @which_pager = nil
         
     | 
| 
      
 95 
     | 
    
         
            +
                  end
         
     | 
| 
       92 
96 
     | 
    
         
             
                end
         
     | 
| 
       93 
97 
     | 
    
         
             
              end
         
     | 
| 
       94 
98 
     | 
    
         
             
            end
         
     | 
    
        data/lib/na/string.rb
    CHANGED
    
    | 
         @@ -355,6 +355,7 @@ class ::String 
     | 
|
| 
       355 
355 
     | 
    
         
             
                  date_string = 'today' if date_string.match(REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
         
     | 
| 
       356 
356 
     | 
    
         
             
                  date_string = "#{options[:context].to_s} #{date_string}" if date_string =~ REGEX_TIME && options[:context]
         
     | 
| 
       357 
357 
     | 
    
         | 
| 
      
 358 
     | 
    
         
            +
                  require 'chronic' unless defined?(Chronic)
         
     | 
| 
       358 
359 
     | 
    
         
             
                  res = Chronic.parse(date_string, {
         
     | 
| 
       359 
360 
     | 
    
         
             
                                        guess: options.fetch(:guess, :begin),
         
     | 
| 
       360 
361 
     | 
    
         
             
                                        context: options.fetch(:future, false) ? :future : :past,
         
     | 
    
        data/lib/na/theme.rb
    CHANGED
    
    | 
         @@ -23,8 +23,9 @@ module NA 
     | 
|
| 
       23 
23 
     | 
    
         
             
                  end
         
     | 
| 
       24 
24 
     | 
    
         | 
| 
       25 
25 
     | 
    
         
             
                  def load_theme(template: {})
         
     | 
| 
       26 
     | 
    
         
            -
                     
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
      
 26 
     | 
    
         
            +
                    NA::Benchmark.measure('Theme.load_theme') do
         
     | 
| 
      
 27 
     | 
    
         
            +
                      # Default colorization, can be overridden with full or partial template variable
         
     | 
| 
      
 28 
     | 
    
         
            +
                      default_template = {
         
     | 
| 
       28 
29 
     | 
    
         
             
                      parent: '{c}',
         
     | 
| 
       29 
30 
     | 
    
         
             
                      bracket: '{dc}',
         
     | 
| 
       30 
31 
     | 
    
         
             
                      parent_divider: '{xw}/',
         
     | 
| 
         @@ -58,14 +59,15 @@ module NA 
     | 
|
| 
       58 
59 
     | 
    
         
             
                            else
         
     | 
| 
       59 
60 
     | 
    
         
             
                              {}
         
     | 
| 
       60 
61 
     | 
    
         
             
                            end
         
     | 
| 
       61 
     | 
    
         
            -
             
     | 
| 
      
 62 
     | 
    
         
            +
                      theme = default_template.deep_merge(theme)
         
     | 
| 
       62 
63 
     | 
    
         | 
| 
       63 
     | 
    
         
            -
             
     | 
| 
       64 
     | 
    
         
            -
             
     | 
| 
       65 
     | 
    
         
            -
             
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
      
 64 
     | 
    
         
            +
                      File.open(theme_file, 'w') do |f|
         
     | 
| 
      
 65 
     | 
    
         
            +
                        f.puts template_help.comment
         
     | 
| 
      
 66 
     | 
    
         
            +
                        f.puts YAML.dump(theme)
         
     | 
| 
      
 67 
     | 
    
         
            +
                      end
         
     | 
| 
       67 
68 
     | 
    
         | 
| 
       68 
     | 
    
         
            -
             
     | 
| 
      
 69 
     | 
    
         
            +
                      theme.merge(template)
         
     | 
| 
      
 70 
     | 
    
         
            +
                    end
         
     | 
| 
       69 
71 
     | 
    
         
             
                  end
         
     | 
| 
       70 
72 
     | 
    
         
             
                end
         
     | 
| 
       71 
73 
     | 
    
         
             
              end
         
     | 
    
        data/lib/na/todo.rb
    CHANGED
    
    | 
         @@ -27,21 +27,22 @@ module NA 
     | 
|
| 
       27 
27 
     | 
    
         
             
                ## @option      file_path   [String] file path to parse
         
     | 
| 
       28 
28 
     | 
    
         
             
                ##
         
     | 
| 
       29 
29 
     | 
    
         
             
                def parse(options)
         
     | 
| 
       30 
     | 
    
         
            -
                   
     | 
| 
       31 
     | 
    
         
            -
                     
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
     | 
    
         
            -
             
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
       41 
     | 
    
         
            -
             
     | 
| 
       42 
     | 
    
         
            -
             
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
      
 30 
     | 
    
         
            +
                  NA::Benchmark.measure('Todo.parse') do
         
     | 
| 
      
 31 
     | 
    
         
            +
                    defaults = {
         
     | 
| 
      
 32 
     | 
    
         
            +
                      depth: 1,
         
     | 
| 
      
 33 
     | 
    
         
            +
                      done: false,
         
     | 
| 
      
 34 
     | 
    
         
            +
                      file_path: nil,
         
     | 
| 
      
 35 
     | 
    
         
            +
                      negate: false,
         
     | 
| 
      
 36 
     | 
    
         
            +
                      project: nil,
         
     | 
| 
      
 37 
     | 
    
         
            +
                      query: nil,
         
     | 
| 
      
 38 
     | 
    
         
            +
                      regex: false,
         
     | 
| 
      
 39 
     | 
    
         
            +
                      require_na: true,
         
     | 
| 
      
 40 
     | 
    
         
            +
                      search: nil,
         
     | 
| 
      
 41 
     | 
    
         
            +
                      search_note: true,
         
     | 
| 
      
 42 
     | 
    
         
            +
                      tag: nil
         
     | 
| 
      
 43 
     | 
    
         
            +
                    }
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                    settings = defaults.merge(options)
         
     | 
| 
       45 
46 
     | 
    
         | 
| 
       46 
47 
     | 
    
         
             
                  actions = NA::Actions.new
         
     | 
| 
       47 
48 
     | 
    
         
             
                  required = []
         
     | 
| 
         @@ -87,6 +88,11 @@ module NA 
     | 
|
| 
       87 
88 
     | 
    
         
             
                    end
         
     | 
| 
       88 
89 
     | 
    
         
             
                  end
         
     | 
| 
       89 
90 
     | 
    
         | 
| 
      
 91 
     | 
    
         
            +
                  # Pre-compile regexes for better performance
         
     | 
| 
      
 92 
     | 
    
         
            +
                  optional = optional.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
         
     | 
| 
      
 93 
     | 
    
         
            +
                  required = required.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
         
     | 
| 
      
 94 
     | 
    
         
            +
                  negated = negated.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
       90 
96 
     | 
    
         
             
                  files = if !settings[:file_path].nil?
         
     | 
| 
       91 
97 
     | 
    
         
             
                            [settings[:file_path]]
         
     | 
| 
       92 
98 
     | 
    
         
             
                          elsif settings[:query].nil?
         
     | 
| 
         @@ -96,14 +102,21 @@ module NA 
     | 
|
| 
       96 
102 
     | 
    
         
             
                          end
         
     | 
| 
       97 
103 
     | 
    
         | 
| 
       98 
104 
     | 
    
         
             
                  NA.notify("Files: #{files.join(', ')}", debug: true)
         
     | 
| 
       99 
     | 
    
         
            -
                   
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
             
     | 
| 
       102 
     | 
    
         
            -
             
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
                     
     | 
| 
       106 
     | 
    
         
            -
             
     | 
| 
      
 105 
     | 
    
         
            +
                  # Cache project regex compilation outside the line loop for better performance
         
     | 
| 
      
 106 
     | 
    
         
            +
                  project_regex = if settings[:project]
         
     | 
| 
      
 107 
     | 
    
         
            +
                                    rx = settings[:project].split(%r{[/:]}).join('.*?/')
         
     | 
| 
      
 108 
     | 
    
         
            +
                                    Regexp.new("#{rx}.*?", Regexp::IGNORECASE)
         
     | 
| 
      
 109 
     | 
    
         
            +
                                  end
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                    files.each do |file|
         
     | 
| 
      
 112 
     | 
    
         
            +
                      NA::Benchmark.measure("Parse file: #{File.basename(file)}") do
         
     | 
| 
      
 113 
     | 
    
         
            +
                        NA.save_working_dir(File.expand_path(file))
         
     | 
| 
      
 114 
     | 
    
         
            +
                        content = file.read_file
         
     | 
| 
      
 115 
     | 
    
         
            +
                        indent_level = 0
         
     | 
| 
      
 116 
     | 
    
         
            +
                        parent = []
         
     | 
| 
      
 117 
     | 
    
         
            +
                        in_yaml = false
         
     | 
| 
      
 118 
     | 
    
         
            +
                        in_action = false
         
     | 
| 
      
 119 
     | 
    
         
            +
                        content.split(/\n/).each.with_index do |line, idx|
         
     | 
| 
       107 
120 
     | 
    
         
             
                      if in_yaml && line !~ /^(---|~~~)\s*$/
         
     | 
| 
       108 
121 
     | 
    
         
             
                        NA.notify("YAML: #{line}", debug: true)
         
     | 
| 
       109 
122 
     | 
    
         
             
                      elsif line =~ /^(---|~~~)\s*$/
         
     | 
| 
         @@ -130,20 +143,22 @@ module NA 
     | 
|
| 
       130 
143 
     | 
    
         
             
                      elsif line.action?
         
     | 
| 
       131 
144 
     | 
    
         
             
                        in_action = false
         
     | 
| 
       132 
145 
     | 
    
         | 
| 
       133 
     | 
    
         
            -
                         
     | 
| 
       134 
     | 
    
         
            -
                        new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
         
     | 
| 
       135 
     | 
    
         
            -
             
     | 
| 
       136 
     | 
    
         
            -
                        projects[-1].last_line = idx if projects.count.positive?
         
     | 
| 
       137 
     | 
    
         
            -
             
     | 
| 
      
 146 
     | 
    
         
            +
                        # Early exits before creating Action object
         
     | 
| 
       138 
147 
     | 
    
         
             
                        next if line.done? && !settings[:done]
         
     | 
| 
       139 
148 
     | 
    
         | 
| 
       140 
149 
     | 
    
         
             
                        next if settings[:require_na] && !line.na?
         
     | 
| 
       141 
150 
     | 
    
         | 
| 
       142 
     | 
    
         
            -
                        if  
     | 
| 
       143 
     | 
    
         
            -
                           
     | 
| 
       144 
     | 
    
         
            -
                          next unless parent.join('/') =~ Regexp.new("#{rx}.*?", Regexp::IGNORECASE)
         
     | 
| 
      
 151 
     | 
    
         
            +
                        if project_regex
         
     | 
| 
      
 152 
     | 
    
         
            +
                          next unless parent.join('/') =~ project_regex
         
     | 
| 
       145 
153 
     | 
    
         
             
                        end
         
     | 
| 
       146 
154 
     | 
    
         | 
| 
      
 155 
     | 
    
         
            +
                        # Only create Action if we passed basic filters
         
     | 
| 
      
 156 
     | 
    
         
            +
                        action = line.action
         
     | 
| 
      
 157 
     | 
    
         
            +
                        new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
         
     | 
| 
      
 158 
     | 
    
         
            +
             
     | 
| 
      
 159 
     | 
    
         
            +
                        projects[-1].last_line = idx if projects.count.positive?
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                        # Tag matching
         
     | 
| 
       147 
162 
     | 
    
         
             
                        has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
         
     | 
| 
       148 
163 
     | 
    
         
             
                        next if has_tag && !new_action.tags_match?(any: optional_tag,
         
     | 
| 
       149 
164 
     | 
    
         
             
                                                                   all: required_tag,
         
     | 
| 
         @@ -155,19 +170,23 @@ module NA 
     | 
|
| 
       155 
170 
     | 
    
         
             
                        actions[-1].note.push(line.strip) if actions.count.positive?
         
     | 
| 
       156 
171 
     | 
    
         
             
                        projects[-1].last_line = idx if projects.count.positive?
         
     | 
| 
       157 
172 
     | 
    
         
             
                      end
         
     | 
| 
      
 173 
     | 
    
         
            +
                        end
         
     | 
| 
      
 174 
     | 
    
         
            +
                        projects = projects.dup
         
     | 
| 
      
 175 
     | 
    
         
            +
                      end
         
     | 
| 
       158 
176 
     | 
    
         
             
                    end
         
     | 
| 
       159 
     | 
    
         
            -
                    projects = projects.dup
         
     | 
| 
       160 
     | 
    
         
            -
                  end
         
     | 
| 
       161 
177 
     | 
    
         | 
| 
       162 
     | 
    
         
            -
             
     | 
| 
       163 
     | 
    
         
            -
             
     | 
| 
       164 
     | 
    
         
            -
             
     | 
| 
       165 
     | 
    
         
            -
             
     | 
| 
       166 
     | 
    
         
            -
             
     | 
| 
       167 
     | 
    
         
            -
             
     | 
| 
       168 
     | 
    
         
            -
             
     | 
| 
      
 178 
     | 
    
         
            +
                    NA::Benchmark.measure('Filter actions by search') do
         
     | 
| 
      
 179 
     | 
    
         
            +
                      actions.delete_if do |new_action|
         
     | 
| 
      
 180 
     | 
    
         
            +
                        has_search = !optional.empty? || !required.empty? || !negated.empty?
         
     | 
| 
      
 181 
     | 
    
         
            +
                        has_search && !new_action.search_match?(any: optional,
         
     | 
| 
      
 182 
     | 
    
         
            +
                                                               all: required,
         
     | 
| 
      
 183 
     | 
    
         
            +
                                                               none: negated,
         
     | 
| 
      
 184 
     | 
    
         
            +
                                                               include_note: settings[:search_note])
         
     | 
| 
      
 185 
     | 
    
         
            +
                      end
         
     | 
| 
      
 186 
     | 
    
         
            +
                    end
         
     | 
| 
       169 
187 
     | 
    
         | 
| 
       170 
     | 
    
         
            -
             
     | 
| 
      
 188 
     | 
    
         
            +
                    [files, actions, projects]
         
     | 
| 
      
 189 
     | 
    
         
            +
                  end
         
     | 
| 
       171 
190 
     | 
    
         
             
                end
         
     | 
| 
       172 
191 
     | 
    
         | 
| 
       173 
192 
     | 
    
         
             
                def parse_search(tag, negate)
         
     | 
    
        data/lib/na/version.rb
    CHANGED
    
    
    
        data/lib/na.rb
    CHANGED
    
    | 
         @@ -1,11 +1,13 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            require 'na/benchmark'
         
     | 
| 
       3 
4 
     | 
    
         
             
            require 'na/version'
         
     | 
| 
       4 
5 
     | 
    
         
             
            require 'na/pager'
         
     | 
| 
       5 
6 
     | 
    
         
             
            require 'time'
         
     | 
| 
       6 
7 
     | 
    
         
             
            require 'fileutils'
         
     | 
| 
       7 
8 
     | 
    
         
             
            require 'shellwords'
         
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
      
 9 
     | 
    
         
            +
            # Lazy load heavy gems - only load when needed
         
     | 
| 
      
 10 
     | 
    
         
            +
            # require 'chronic'  # Loaded in action.rb and string.rb when needed
         
     | 
| 
       9 
11 
     | 
    
         
             
            require 'tty-screen'
         
     | 
| 
       10 
12 
     | 
    
         
             
            require 'tty-reader'
         
     | 
| 
       11 
13 
     | 
    
         
             
            require 'tty-which'
         
     | 
    
        data/src/_README.md
    CHANGED
    
    | 
         @@ -9,7 +9,7 @@ 
     | 
|
| 
       9 
9 
     | 
    
         
             
            _If you're one of the rare people like me who find this useful, feel free to
         
     | 
| 
       10 
10 
     | 
    
         
             
            [buy me some coffee][donate]._
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
            The current version of `na` is <!--VER-->1.2. 
     | 
| 
      
 12 
     | 
    
         
            +
            The current version of `na` is <!--VER-->1.2.79<!--END VER-->.
         
     | 
| 
       13 
13 
     | 
    
         | 
| 
       14 
14 
     | 
    
         
             
            `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder. 
         
     | 
| 
       15 
15 
     | 
    
         | 
    
        data/test_performance.rb
    ADDED
    
    | 
         @@ -0,0 +1,78 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env ruby
         
     | 
| 
      
 2 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            # Simple performance test script that doesn't require bundler
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative 'lib/na/benchmark'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            # Mock the required dependencies
         
     | 
| 
      
 8 
     | 
    
         
            +
            module NA
         
     | 
| 
      
 9 
     | 
    
         
            +
              module Color
         
     | 
| 
      
 10 
     | 
    
         
            +
                def self.template(input)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  input.to_s  # Simple mock
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              module Theme
         
     | 
| 
      
 16 
     | 
    
         
            +
                def self.load_theme
         
     | 
| 
      
 17 
     | 
    
         
            +
                  {
         
     | 
| 
      
 18 
     | 
    
         
            +
                    parent: '{c}',
         
     | 
| 
      
 19 
     | 
    
         
            +
                    bracket: '{dc}',
         
     | 
| 
      
 20 
     | 
    
         
            +
                    parent_divider: '{xw}/',
         
     | 
| 
      
 21 
     | 
    
         
            +
                    action: '{bg}',
         
     | 
| 
      
 22 
     | 
    
         
            +
                    project: '{xbk}',
         
     | 
| 
      
 23 
     | 
    
         
            +
                    templates: {
         
     | 
| 
      
 24 
     | 
    
         
            +
                      output: '%parent%action',
         
     | 
| 
      
 25 
     | 
    
         
            +
                      default: '%parent%action'
         
     | 
| 
      
 26 
     | 
    
         
            +
                    }
         
     | 
| 
      
 27 
     | 
    
         
            +
                  }
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
              end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
              def self.theme
         
     | 
| 
      
 32 
     | 
    
         
            +
                @theme ||= Theme.load_theme
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              def self.notify(msg, debug: false)
         
     | 
| 
      
 36 
     | 
    
         
            +
                puts msg if debug
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
            end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            # Initialize benchmark
         
     | 
| 
      
 41 
     | 
    
         
            +
            NA::Benchmark.init
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            # Test the optimizations
         
     | 
| 
      
 44 
     | 
    
         
            +
            puts "Testing performance optimizations..."
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
            # Test 1: Theme caching
         
     | 
| 
      
 47 
     | 
    
         
            +
            NA::Benchmark.measure('Theme loading (first time)') do
         
     | 
| 
      
 48 
     | 
    
         
            +
              theme1 = NA::Theme.load_theme
         
     | 
| 
      
 49 
     | 
    
         
            +
            end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            NA::Benchmark.measure('Theme loading (cached)') do
         
     | 
| 
      
 52 
     | 
    
         
            +
              theme2 = NA.theme
         
     | 
| 
      
 53 
     | 
    
         
            +
            end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
            # Test 2: Color template caching
         
     | 
| 
      
 56 
     | 
    
         
            +
            NA::Benchmark.measure('Color template (first time)') do
         
     | 
| 
      
 57 
     | 
    
         
            +
              NA::Color.template('{bg}Test action{x}')
         
     | 
| 
      
 58 
     | 
    
         
            +
            end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            NA::Benchmark.measure('Color template (cached)') do
         
     | 
| 
      
 61 
     | 
    
         
            +
              NA::Color.template('{bg}Test action{x}')
         
     | 
| 
      
 62 
     | 
    
         
            +
            end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
            # Test 3: Multiple operations
         
     | 
| 
      
 65 
     | 
    
         
            +
            NA::Benchmark.measure('Multiple theme calls') do
         
     | 
| 
      
 66 
     | 
    
         
            +
              100.times do
         
     | 
| 
      
 67 
     | 
    
         
            +
                NA.theme
         
     | 
| 
      
 68 
     | 
    
         
            +
              end
         
     | 
| 
      
 69 
     | 
    
         
            +
            end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            NA::Benchmark.measure('Multiple color templates') do
         
     | 
| 
      
 72 
     | 
    
         
            +
              100.times do
         
     | 
| 
      
 73 
     | 
    
         
            +
                NA::Color.template('{bg}Action {c}#{rand(1000)}{x}')
         
     | 
| 
      
 74 
     | 
    
         
            +
              end
         
     | 
| 
      
 75 
     | 
    
         
            +
            end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
            # Report results
         
     | 
| 
      
 78 
     | 
    
         
            +
            NA::Benchmark.report
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: na
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 1.2. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.2.80
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Brett Terpstra
         
     | 
| 
         @@ -261,6 +261,7 @@ files: 
     | 
|
| 
       261 
261 
     | 
    
         
             
            - lib/na/action.rb
         
     | 
| 
       262 
262 
     | 
    
         
             
            - lib/na/actions.rb
         
     | 
| 
       263 
263 
     | 
    
         
             
            - lib/na/array.rb
         
     | 
| 
      
 264 
     | 
    
         
            +
            - lib/na/benchmark.rb
         
     | 
| 
       264 
265 
     | 
    
         
             
            - lib/na/colors.rb
         
     | 
| 
       265 
266 
     | 
    
         
             
            - lib/na/editor.rb
         
     | 
| 
       266 
267 
     | 
    
         
             
            - lib/na/hash.rb
         
     | 
| 
         @@ -281,6 +282,7 @@ files: 
     | 
|
| 
       281 
282 
     | 
    
         
             
            - src/_README.md
         
     | 
| 
       282 
283 
     | 
    
         
             
            - test.md
         
     | 
| 
       283 
284 
     | 
    
         
             
            - test2.txt
         
     | 
| 
      
 285 
     | 
    
         
            +
            - test_performance.rb
         
     | 
| 
       284 
286 
     | 
    
         
             
            homepage: https://brettterpstra.com/projects/na/
         
     | 
| 
       285 
287 
     | 
    
         
             
            licenses:
         
     | 
| 
       286 
288 
     | 
    
         
             
            - MIT
         
     |