na 1.2.79 → 1.2.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -2
- data/.rubocop_todo.yml +33 -538
- data/CHANGELOG.md +55 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +27 -10
- data/README.md +47 -6
- data/Rakefile +6 -0
- data/bin/commands/next.rb +4 -0
- data/bin/commands/scan.rb +84 -0
- data/bin/commands/update.rb +1 -1
- data/bin/na +18 -12
- data/lib/na/action.rb +181 -83
- data/lib/na/actions.rb +91 -66
- data/lib/na/array.rb +11 -7
- data/lib/na/benchmark.rb +57 -0
- data/lib/na/colors.rb +113 -92
- data/lib/na/editor.rb +22 -22
- data/lib/na/hash.rb +32 -9
- data/lib/na/help_monkey_patch.rb +9 -1
- data/lib/na/next_action.rb +327 -248
- data/lib/na/pager.rb +60 -32
- data/lib/na/project.rb +14 -1
- data/lib/na/prompt.rb +25 -3
- data/lib/na/string.rb +91 -130
- data/lib/na/theme.rb +47 -39
- data/lib/na/todo.rb +182 -145
- data/lib/na/version.rb +3 -1
- data/lib/na.rb +4 -1
- data/na.gemspec +4 -2
- data/scripts/generate-fish-completions.rb +18 -21
- data/src/_README.md +14 -4
- data/test_performance.rb +78 -0
- metadata +55 -24
data/lib/na/action.rb
CHANGED
|
@@ -12,12 +12,20 @@ module NA
|
|
|
12
12
|
@file = file
|
|
13
13
|
@project = project
|
|
14
14
|
@parent = parent
|
|
15
|
-
@action = action.gsub(
|
|
15
|
+
@action = action.gsub('{', '\\{')
|
|
16
16
|
@tags = scan_tags
|
|
17
17
|
@line = idx
|
|
18
18
|
@note = note
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Update the action string and note with priority, tags, and completion status
|
|
22
|
+
#
|
|
23
|
+
# @param priority [Integer] Priority value to set
|
|
24
|
+
# @param finish [Boolean] Mark as finished
|
|
25
|
+
# @param add_tag [Array<String>] Tags to add
|
|
26
|
+
# @param remove_tag [Array<String>] Tags to remove
|
|
27
|
+
# @param note [Array<String>] Notes to set
|
|
28
|
+
# @return [void]
|
|
21
29
|
def process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
|
|
22
30
|
string = @action.dup
|
|
23
31
|
|
|
@@ -44,6 +52,9 @@ module NA
|
|
|
44
52
|
@note = note unless note.empty?
|
|
45
53
|
end
|
|
46
54
|
|
|
55
|
+
# String representation of the action
|
|
56
|
+
#
|
|
57
|
+
# @return [String]
|
|
47
58
|
def to_s
|
|
48
59
|
note = if @note.count.positive?
|
|
49
60
|
"\n#{@note.join("\n")}"
|
|
@@ -53,102 +64,153 @@ module NA
|
|
|
53
64
|
"(#{@file}:#{@line}) #{@project}:#{@parent.join('>')} | #{@action}#{note}"
|
|
54
65
|
end
|
|
55
66
|
|
|
67
|
+
# Pretty string representation of the action with color formatting
|
|
68
|
+
#
|
|
69
|
+
# @return [String]
|
|
56
70
|
def to_s_pretty
|
|
57
71
|
note = if @note.count.positive?
|
|
58
72
|
"\n#{@note.join("\n")}"
|
|
59
73
|
else
|
|
60
74
|
''
|
|
61
75
|
end
|
|
62
|
-
"#{NA.theme[:filename]}#{File.basename(@file)}:#{@line}#{NA.theme[:bracket]}[#{NA.theme[:project]}#{@project}:#{@parent.join(
|
|
76
|
+
"{x}#{NA.theme[:filename]}#{File.basename(@file)}:#{@line}{x}#{NA.theme[:bracket]}[{x}#{NA.theme[:project]}#{@project}:#{@parent.join('>')}{x}#{NA.theme[:bracket]}]{x} | #{NA.theme[:action]}#{@action}{x}#{NA.theme[:note]}#{note}"
|
|
63
77
|
end
|
|
64
78
|
|
|
79
|
+
# Inspect the action object
|
|
80
|
+
#
|
|
81
|
+
# @return [String]
|
|
65
82
|
def inspect
|
|
66
83
|
<<~EOINSPECT
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
@file: #{@file}
|
|
85
|
+
@project: #{@project}
|
|
86
|
+
@parent: #{@parent.join('>')}
|
|
87
|
+
@action: #{@action}
|
|
88
|
+
@tags: #{@tags}
|
|
89
|
+
@note: #{@note}
|
|
73
90
|
EOINSPECT
|
|
74
91
|
end
|
|
75
92
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
##
|
|
93
|
+
#
|
|
94
|
+
# Pretty print an action with color and template formatting
|
|
95
|
+
#
|
|
96
|
+
# @param extension [String] File extension
|
|
97
|
+
# @param template [Hash] Color template
|
|
98
|
+
# @param regexes [Array] Regexes to highlight
|
|
99
|
+
# @param notes [Boolean] Include notes
|
|
100
|
+
# @param detect_width [Boolean] Detect terminal width
|
|
101
|
+
# @return [String]
|
|
86
102
|
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
|
-
|
|
103
|
+
NA::Benchmark.measure('Action.pretty') do
|
|
104
|
+
# Use cached theme instead of loading every time
|
|
105
|
+
theme = NA.theme
|
|
106
|
+
template = theme.merge(template)
|
|
107
|
+
|
|
108
|
+
# Pre-compute common template parts to avoid repeated processing
|
|
109
|
+
output_template = template[:templates][:output]
|
|
110
|
+
needs_filename = output_template.include?('%filename')
|
|
111
|
+
needs_parents = output_template.include?('%parents') || output_template.include?('%parent')
|
|
112
|
+
needs_project = output_template.include?('%project')
|
|
113
|
+
|
|
114
|
+
# Create the hierarchical parent string (optimized)
|
|
115
|
+
parents = if needs_parents && @parent.any?
|
|
116
|
+
parent_parts = @parent.map { |par| "#{template[:parent]}#{par}" }.join(template[:parent_divider])
|
|
117
|
+
NA::Color.template("{x}#{template[:bracket]}[#{template[:error]}#{parent_parts}{x}#{template[:bracket]}]{x} ")
|
|
118
|
+
else
|
|
119
|
+
''
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Create the project string (optimized)
|
|
123
|
+
project = if needs_project && !@project.empty?
|
|
124
|
+
NA::Color.template("{x}#{template[:project]}#{@project}{x} ")
|
|
125
|
+
else
|
|
126
|
+
''
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Create the source filename string (optimized)
|
|
130
|
+
filename = if needs_filename
|
|
131
|
+
path = @file.sub(%r{^\./}, '').sub(/#{Dir.home}/, '~')
|
|
132
|
+
if File.dirname(path) == '.'
|
|
133
|
+
fname = NA.include_ext ? File.basename(path) : File.basename(path, ".#{extension}")
|
|
134
|
+
fname = "./#{fname}" if NA.show_cwd_indicator
|
|
135
|
+
NA::Color.template("#{template[:filename]}#{fname} {x}")
|
|
136
|
+
else
|
|
137
|
+
colored = (NA.include_ext ? path : path.sub(/\.#{extension}$/, '')).highlight_filename
|
|
138
|
+
NA::Color.template("#{template[:filename]}#{colored} {x}")
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
''
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# colorize the action and highlight tags (optimized)
|
|
145
|
+
action_text = @action.dup
|
|
146
|
+
action_text.gsub!(/\{(.*?)\}/, '\\{\1\\}')
|
|
147
|
+
action_text = action_text.sub(/ @#{NA.na_tag}\b/, '')
|
|
148
|
+
action = NA::Color.template("#{template[:action]}#{action_text}{x}")
|
|
149
|
+
action = action.highlight_tags(color: template[:tags],
|
|
150
|
+
parens: template[:value_parens],
|
|
151
|
+
value: template[:values],
|
|
152
|
+
last_color: template[:action])
|
|
153
|
+
|
|
154
|
+
# Handle notes and wrapping (optimized)
|
|
155
|
+
note = ''
|
|
156
|
+
if @note.any?
|
|
157
|
+
if notes
|
|
158
|
+
if detect_width
|
|
159
|
+
# Cache width calculation
|
|
160
|
+
width = @cached_width ||= TTY::Screen.columns
|
|
161
|
+
# Calculate indent more efficiently - avoid repeated template processing
|
|
162
|
+
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
163
|
+
base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/,
|
|
164
|
+
parents)
|
|
165
|
+
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
166
|
+
note = NA::Color.template(@note.wrap(width, indent, template[:note]))
|
|
167
|
+
else
|
|
168
|
+
note = NA::Color.template("\n#{@note.map { |l| " {x}#{template[:note]}• #{l}{x}" }.join("\n")}")
|
|
169
|
+
end
|
|
170
|
+
else
|
|
171
|
+
action += "{x}#{template[:note]}*"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Wrap action if needed (optimized)
|
|
176
|
+
if detect_width && !action.empty?
|
|
177
|
+
width = @cached_width ||= TTY::Screen.columns
|
|
178
|
+
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
179
|
+
base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/, parents)
|
|
180
|
+
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
181
|
+
action = action.wrap(width, indent)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Replace variables in template string and output colorized (optimized)
|
|
185
|
+
final_output = output_template.dup
|
|
186
|
+
final_output.gsub!('%filename', filename)
|
|
187
|
+
final_output.gsub!('%project', project)
|
|
188
|
+
final_output.gsub!(/%parents?/, parents)
|
|
189
|
+
final_output.gsub!('%action', action.highlight_search(regexes))
|
|
190
|
+
final_output.gsub!('%note', note)
|
|
191
|
+
final_output.gsub!('\\{', '{')
|
|
139
192
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.gsub(/%project/, project)
|
|
143
|
-
.gsub(/%parents?/, parents)
|
|
144
|
-
.gsub(/%action/, action.highlight_search(regexes))
|
|
145
|
-
.gsub(/%note/, note)).gsub(/\\\{/, '{')
|
|
193
|
+
NA::Color.template(final_output)
|
|
194
|
+
end
|
|
146
195
|
end
|
|
147
196
|
|
|
197
|
+
# Check if action tags match any, all, and none criteria
|
|
198
|
+
#
|
|
199
|
+
# @param any [Array] Tags to match any
|
|
200
|
+
# @param all [Array] Tags to match all
|
|
201
|
+
# @param none [Array] Tags to match none
|
|
202
|
+
# @return [Boolean]
|
|
148
203
|
def tags_match?(any: [], all: [], none: [])
|
|
149
204
|
tag_matches_any(any) && tag_matches_all(all) && tag_matches_none(none)
|
|
150
205
|
end
|
|
151
206
|
|
|
207
|
+
# Check if action or note matches any, all, and none search criteria
|
|
208
|
+
#
|
|
209
|
+
# @param any [Array] Regexes to match any
|
|
210
|
+
# @param all [Array] Regexes to match all
|
|
211
|
+
# @param none [Array] Regexes to match none
|
|
212
|
+
# @param include_note [Boolean] Include note in search
|
|
213
|
+
# @return [Boolean]
|
|
152
214
|
def search_match?(any: [], all: [], none: [], include_note: true)
|
|
153
215
|
search_matches_any(any, include_note: include_note) &&
|
|
154
216
|
search_matches_all(all, include_note: include_note) &&
|
|
@@ -157,32 +219,54 @@ module NA
|
|
|
157
219
|
|
|
158
220
|
private
|
|
159
221
|
|
|
222
|
+
# Check if action and note do not match any regexes
|
|
223
|
+
#
|
|
224
|
+
# @param regexes [Array] Regexes to check
|
|
225
|
+
# @param include_note [Boolean] Include note in search
|
|
226
|
+
# @return [Boolean]
|
|
160
227
|
def search_matches_none(regexes, include_note: true)
|
|
161
228
|
regexes.each do |rx|
|
|
162
|
-
|
|
163
|
-
|
|
229
|
+
regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
|
|
230
|
+
note_matches = include_note && @note.join(' ').match(regex)
|
|
231
|
+
return false if @action.match(regex) || note_matches
|
|
164
232
|
end
|
|
165
233
|
true
|
|
166
234
|
end
|
|
167
235
|
|
|
236
|
+
# Check if action or note matches any regexes
|
|
237
|
+
#
|
|
238
|
+
# @param regexes [Array] Regexes to check
|
|
239
|
+
# @param include_note [Boolean] Include note in search
|
|
240
|
+
# @return [Boolean]
|
|
168
241
|
def search_matches_any(regexes, include_note: true)
|
|
169
242
|
return true if regexes.empty?
|
|
170
243
|
|
|
171
244
|
regexes.each do |rx|
|
|
172
|
-
|
|
173
|
-
|
|
245
|
+
regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
|
|
246
|
+
note_matches = include_note && @note.join(' ').match(regex)
|
|
247
|
+
return true if @action.match(regex) || note_matches
|
|
174
248
|
end
|
|
175
249
|
false
|
|
176
250
|
end
|
|
177
251
|
|
|
252
|
+
# Check if action or note matches all regexes
|
|
253
|
+
#
|
|
254
|
+
# @param regexes [Array] Regexes to check
|
|
255
|
+
# @param include_note [Boolean] Include note in search
|
|
256
|
+
# @return [Boolean]
|
|
178
257
|
def search_matches_all(regexes, include_note: true)
|
|
179
258
|
regexes.each do |rx|
|
|
180
|
-
|
|
181
|
-
|
|
259
|
+
regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
|
|
260
|
+
note_matches = include_note && @note.join(' ').match(regex)
|
|
261
|
+
return false unless @action.match(regex) || note_matches
|
|
182
262
|
end
|
|
183
263
|
true
|
|
184
264
|
end
|
|
185
265
|
|
|
266
|
+
# Check if none of the tags match
|
|
267
|
+
#
|
|
268
|
+
# @param tags [Array] Tags to check
|
|
269
|
+
# @return [Boolean]
|
|
186
270
|
def tag_matches_none(tags)
|
|
187
271
|
tags.each do |tag|
|
|
188
272
|
return false if compare_tag(tag)
|
|
@@ -190,6 +274,10 @@ module NA
|
|
|
190
274
|
true
|
|
191
275
|
end
|
|
192
276
|
|
|
277
|
+
# Check if any of the tags match
|
|
278
|
+
#
|
|
279
|
+
# @param tags [Array] Tags to check
|
|
280
|
+
# @return [Boolean]
|
|
193
281
|
def tag_matches_any(tags)
|
|
194
282
|
return true if tags.empty?
|
|
195
283
|
|
|
@@ -199,6 +287,10 @@ module NA
|
|
|
199
287
|
false
|
|
200
288
|
end
|
|
201
289
|
|
|
290
|
+
# Check if all of the tags match
|
|
291
|
+
#
|
|
292
|
+
# @param tags [Array] Tags to check
|
|
293
|
+
# @return [Boolean]
|
|
202
294
|
def tag_matches_all(tags)
|
|
203
295
|
tags.each do |tag|
|
|
204
296
|
return false unless compare_tag(tag)
|
|
@@ -206,8 +298,13 @@ module NA
|
|
|
206
298
|
true
|
|
207
299
|
end
|
|
208
300
|
|
|
301
|
+
# Compare a tag against the action's tags with optional value comparison
|
|
302
|
+
#
|
|
303
|
+
# @param tag [Hash] Tag criteria
|
|
304
|
+
# @return [Boolean]
|
|
209
305
|
def compare_tag(tag)
|
|
210
|
-
|
|
306
|
+
tag_regex = tag[:tag].is_a?(Regexp) ? tag[:tag] : Regexp.new(tag[:tag], Regexp::IGNORECASE)
|
|
307
|
+
keys = @tags.keys.delete_if { |k| k !~ tag_regex }
|
|
211
308
|
return false if keys.empty?
|
|
212
309
|
|
|
213
310
|
key = keys[0]
|
|
@@ -220,6 +317,7 @@ module NA
|
|
|
220
317
|
|
|
221
318
|
begin
|
|
222
319
|
tag_date = Time.parse(tag_val)
|
|
320
|
+
require 'chronic' unless defined?(Chronic)
|
|
223
321
|
date = Chronic.parse(val)
|
|
224
322
|
|
|
225
323
|
raise ArgumentError if date.nil?
|
data/lib/na/actions.rb
CHANGED
|
@@ -5,89 +5,114 @@ module NA
|
|
|
5
5
|
class Actions < Array
|
|
6
6
|
def initialize(actions = [])
|
|
7
7
|
super
|
|
8
|
-
concat(actions)
|
|
9
8
|
end
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## @option config [Boolean] :no_files Whether to include files in the output
|
|
23
|
-
##
|
|
24
|
-
## @return [String] The output string
|
|
25
|
-
##
|
|
10
|
+
# Pretty print a list of actions
|
|
11
|
+
#
|
|
12
|
+
# @param depth [Integer] The depth of the action
|
|
13
|
+
# @param config [Hash] The configuration options
|
|
14
|
+
# @option config [Array] :files The files to include in the output
|
|
15
|
+
# @option config [Array] :regexes The regexes to match against
|
|
16
|
+
# @option config [Boolean] :notes Whether to include notes in the output
|
|
17
|
+
# @option config [Boolean] :nest Whether to nest the output
|
|
18
|
+
# @option config [Boolean] :nest_projects Whether to nest projects in the output
|
|
19
|
+
# @option config [Boolean] :no_files Whether to include files in the output
|
|
20
|
+
# @return [String] The output string
|
|
26
21
|
def output(depth, config = {})
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
NA::Benchmark.measure('Actions.output') do
|
|
23
|
+
defaults = {
|
|
24
|
+
files: nil,
|
|
25
|
+
regexes: [],
|
|
26
|
+
notes: false,
|
|
27
|
+
nest: false,
|
|
28
|
+
nest_projects: false,
|
|
29
|
+
no_files: false
|
|
30
|
+
}
|
|
31
|
+
config = defaults.merge(config)
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
return if config[:files].nil?
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
if config[:nest]
|
|
36
|
+
template = NA.theme[:templates][:default]
|
|
37
|
+
template = NA.theme[:templates][:no_file] if config[:no_files]
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
parent_files = {}
|
|
40
|
+
out = []
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
if config[:nest_projects]
|
|
43
|
+
each do |action|
|
|
44
|
+
parent_files[action.file] ||= []
|
|
45
|
+
parent_files[action.file].push(action)
|
|
46
|
+
end
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
parent_files.each do |file, acts|
|
|
49
|
+
projects = NA.project_hierarchy(acts)
|
|
50
|
+
out.push("#{file.sub(%r{^./}, '').shorten_path}:")
|
|
51
|
+
out.concat(NA.output_children(projects, 0))
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
template = NA.theme[:templates][:default]
|
|
55
|
+
template = NA.theme[:templates][:no_file] if config[:no_files]
|
|
56
|
+
|
|
57
|
+
each do |action|
|
|
58
|
+
parent_files[action.file] ||= []
|
|
59
|
+
parent_files[action.file].push(action)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
parent_files.each do |file, acts|
|
|
63
|
+
out.push("#{file.sub(%r{^\./}, '')}:")
|
|
64
|
+
acts.each do |a|
|
|
65
|
+
out.push("\t- [#{a.parent.join('/')}] #{a.action}")
|
|
66
|
+
out.push("\t\t#{a.note.join("\n\t\t")}") unless a.note.empty?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
56
69
|
end
|
|
70
|
+
NA::Pager.page out.join("\n")
|
|
57
71
|
else
|
|
58
|
-
template
|
|
59
|
-
template =
|
|
72
|
+
# Optimize template selection
|
|
73
|
+
template = if config[:no_files]
|
|
74
|
+
NA.theme[:templates][:no_file]
|
|
75
|
+
elsif config[:files]&.count&.positive?
|
|
76
|
+
config[:files].count == 1 ? NA.theme[:templates][:single_file] : NA.theme[:templates][:multi_file]
|
|
77
|
+
elsif depth > 1
|
|
78
|
+
NA.theme[:templates][:multi_file]
|
|
79
|
+
else
|
|
80
|
+
NA.theme[:templates][:default]
|
|
81
|
+
end
|
|
82
|
+
template += '%note' if config[:notes]
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
# Show './' for current directory only when listing also includes subdir files
|
|
85
|
+
if template == NA.theme[:templates][:multi_file]
|
|
86
|
+
has_subdir = config[:files]&.any? { |f| File.dirname(f) != '.' } || depth > 1
|
|
87
|
+
NA.show_cwd_indicator = !has_subdir.nil?
|
|
88
|
+
else
|
|
89
|
+
NA.show_cwd_indicator = false
|
|
64
90
|
end
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
# Skip debug output if not verbose
|
|
93
|
+
config[:files]&.each { |f| NA.notify(f, debug: true) } if config[:files] && NA.verbose
|
|
94
|
+
|
|
95
|
+
# Optimize output generation - compile all output first, then apply regexes
|
|
96
|
+
output = String.new
|
|
97
|
+
NA::Benchmark.measure('Generate action strings') do
|
|
98
|
+
each_with_index do |action, idx|
|
|
99
|
+
# Generate raw output without regex processing
|
|
100
|
+
output << action.pretty(template: { templates: { output: template } }, regexes: [], notes: config[:notes])
|
|
101
|
+
output << "\n" unless idx == size - 1
|
|
71
102
|
end
|
|
72
103
|
end
|
|
73
|
-
end
|
|
74
|
-
NA::Pager.page out.join("\n")
|
|
75
|
-
else
|
|
76
|
-
template = if config[:no_files]
|
|
77
|
-
NA.theme[:templates][:no_file]
|
|
78
|
-
elsif config[:files].count.positive?
|
|
79
|
-
config[:files].count == 1 ? NA.theme[:templates][:single_file] : NA.theme[:templates][:multi_file]
|
|
80
|
-
elsif NA.find_files(depth: depth).count > 1
|
|
81
|
-
depth > 1 ? NA.theme[:templates][:multi_file] : NA.theme[:templates][:single_file]
|
|
82
|
-
else
|
|
83
|
-
NA.theme[:templates][:default]
|
|
84
|
-
end
|
|
85
|
-
template += "%note" if config[:notes]
|
|
86
104
|
|
|
87
|
-
|
|
105
|
+
# Apply regex highlighting to the entire output at once
|
|
106
|
+
if config[:regexes].any?
|
|
107
|
+
NA::Benchmark.measure('Apply regex highlighting') do
|
|
108
|
+
output = output.highlight_search(config[:regexes])
|
|
109
|
+
end
|
|
110
|
+
end
|
|
88
111
|
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
NA::Benchmark.measure('Pager.page call') do
|
|
113
|
+
NA::Pager.page(output)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
91
116
|
end
|
|
92
117
|
end
|
|
93
118
|
end
|
data/lib/na/array.rb
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ::Array
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## @return [Array] Array without "bad" elements
|
|
9
|
-
##
|
|
4
|
+
# Like Array#compact -- removes nil items, but also
|
|
5
|
+
# removes empty strings, zero or negative numbers and FalseClass items
|
|
6
|
+
#
|
|
7
|
+
# @return [Array] Array without "bad" elements
|
|
10
8
|
def remove_bad
|
|
11
9
|
compact.map { |x| x.is_a?(String) ? x.strip : x }.select(&:good?)
|
|
12
10
|
end
|
|
13
11
|
|
|
12
|
+
# Wrap each string in the array to the given width and indent, with color
|
|
13
|
+
#
|
|
14
|
+
# @param width [Integer] Maximum line width
|
|
15
|
+
# @param indent [Integer] Indentation spaces
|
|
16
|
+
# @param color [String] Color code to apply
|
|
17
|
+
# @return [Array, String] Wrapped and colorized lines
|
|
14
18
|
def wrap(width, indent, color)
|
|
15
19
|
return map { |l| "#{color} #{l.wrap(width, 2)}" } if width < 60
|
|
16
20
|
|
|
17
21
|
map! do |l|
|
|
18
|
-
"#{color}#{' ' * indent
|
|
22
|
+
"#{color}#{' ' * indent}• #{l.wrap(width, indent)}{x}"
|
|
19
23
|
end
|
|
20
24
|
"\n#{join("\n")}"
|
|
21
25
|
end
|
data/lib/na/benchmark.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NA
|
|
4
|
+
module Benchmark
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :enabled, :timings
|
|
7
|
+
|
|
8
|
+
# Initialize benchmarking state
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
11
|
+
def init
|
|
12
|
+
@enabled = %w[1 true].include?(ENV.fetch('NA_BENCHMARK', nil))
|
|
13
|
+
@timings = []
|
|
14
|
+
@start_time = Time.now
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Measure the execution time of a block
|
|
18
|
+
#
|
|
19
|
+
# @param label [String] Label for the measurement
|
|
20
|
+
# @return [Object] Result of the block
|
|
21
|
+
def measure(label)
|
|
22
|
+
return yield unless @enabled
|
|
23
|
+
|
|
24
|
+
start = Time.now
|
|
25
|
+
result = yield
|
|
26
|
+
duration = ((Time.now - start) * 1000).round(2)
|
|
27
|
+
@timings << { label: label, duration: duration, timestamp: (start - @start_time) * 1000 }
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Output a performance report to STDERR
|
|
32
|
+
#
|
|
33
|
+
# @return [void]
|
|
34
|
+
def report
|
|
35
|
+
return unless @enabled
|
|
36
|
+
|
|
37
|
+
total = @timings.sum { |t| t[:duration] }
|
|
38
|
+
warn "\n#{NA::Color.template('{y}=== NA Performance Report ===')}"
|
|
39
|
+
warn NA::Color.template("{dw}Total: {bw}#{total.round(2)}ms{x}")
|
|
40
|
+
warn NA::Color.template("{dw}GC Count: {bw}#{GC.count}{x}") if defined?(GC)
|
|
41
|
+
if defined?(GC)
|
|
42
|
+
warn NA::Color.template("{dw}Memory: {bw}#{(GC.stat[:heap_live_slots] * 40 / 1024.0).round(1)}KB{x}")
|
|
43
|
+
end
|
|
44
|
+
warn ''
|
|
45
|
+
|
|
46
|
+
@timings.each do |timing|
|
|
47
|
+
pct = total.positive? ? ((timing[:duration] / total) * 100).round(1) : 0
|
|
48
|
+
bar = '█' * [(pct / 2).round, 50].min
|
|
49
|
+
warn NA::Color.template(
|
|
50
|
+
"{dw}[{y}#{bar.ljust(25)}{dw}] {bw}#{timing[:duration].to_s.rjust(7)}ms {dw}(#{pct.to_s.rjust(5)}%) {x}#{timing[:label]}"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
warn NA::Color.template("{y}#{'=' * 50}{x}\n")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|