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
|