na 1.2.79 → 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 +36 -0
- data/Gemfile.lock +2 -2
- data/README.md +3 -3
- data/bin/na +12 -6
- data/lib/na/action.rb +97 -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 +21 -11
- 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,39 @@
|
|
|
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
|
+
|
|
1
37
|
### 1.2.79
|
|
2
38
|
|
|
3
39
|
2025-09-29 06:51
|
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/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
|
@@ -84,65 +84,92 @@ module NA
|
|
|
84
84
|
## @param notes [Boolean] Include notes
|
|
85
85
|
##
|
|
86
86
|
def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true)
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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!(/\\\{/, '{')
|
|
139
170
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.gsub(/%project/, project)
|
|
143
|
-
.gsub(/%parents?/, parents)
|
|
144
|
-
.gsub(/%action/, action.highlight_search(regexes))
|
|
145
|
-
.gsub(/%note/, note)).gsub(/\\\{/, '{')
|
|
171
|
+
NA::Color.template(final_output)
|
|
172
|
+
end
|
|
146
173
|
end
|
|
147
174
|
|
|
148
175
|
def tags_match?(any: [], all: [], none: [])
|
|
@@ -159,8 +186,9 @@ module NA
|
|
|
159
186
|
|
|
160
187
|
def search_matches_none(regexes, include_note: true)
|
|
161
188
|
regexes.each do |rx|
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
164
192
|
end
|
|
165
193
|
true
|
|
166
194
|
end
|
|
@@ -169,16 +197,18 @@ module NA
|
|
|
169
197
|
return true if regexes.empty?
|
|
170
198
|
|
|
171
199
|
regexes.each do |rx|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
174
203
|
end
|
|
175
204
|
false
|
|
176
205
|
end
|
|
177
206
|
|
|
178
207
|
def search_matches_all(regexes, include_note: true)
|
|
179
208
|
regexes.each do |rx|
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
182
212
|
end
|
|
183
213
|
true
|
|
184
214
|
end
|
|
@@ -207,7 +237,8 @@ module NA
|
|
|
207
237
|
end
|
|
208
238
|
|
|
209
239
|
def compare_tag(tag)
|
|
210
|
-
|
|
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 }
|
|
211
242
|
return false if keys.empty?
|
|
212
243
|
|
|
213
244
|
key = keys[0]
|
|
@@ -220,6 +251,7 @@ module NA
|
|
|
220
251
|
|
|
221
252
|
begin
|
|
222
253
|
tag_date = Time.parse(tag_val)
|
|
254
|
+
require 'chronic' unless defined?(Chronic)
|
|
223
255
|
date = Chronic.parse(val)
|
|
224
256
|
|
|
225
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
|
@@ -554,11 +554,19 @@ module NA
|
|
|
554
554
|
## @param depth [Number] The depth at which to search
|
|
555
555
|
##
|
|
556
556
|
def find_files(depth: 1)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
|
562
570
|
end
|
|
563
571
|
|
|
564
572
|
def find_files_matching(options = {})
|
|
@@ -671,12 +679,14 @@ module NA
|
|
|
671
679
|
## @param todo_file The todo file path
|
|
672
680
|
##
|
|
673
681
|
def save_working_dir(todo_file)
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
680
690
|
end
|
|
681
691
|
|
|
682
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
|