na 1.2.94 → 1.2.95
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/.cursor/commands/changelog.md +5 -2
- data/.rubocop_todo.yml +34 -12
- data/CHANGELOG.md +23 -0
- data/Gemfile.lock +23 -23
- data/README.md +146 -5
- data/bin/commands/add.rb +19 -1
- data/bin/commands/find.rb +61 -10
- data/bin/commands/next.rb +51 -0
- data/bin/commands/saved.rb +43 -8
- data/lib/na/action.rb +3 -1
- data/lib/na/next_action.rb +804 -3
- data/lib/na/todo.rb +42 -3
- data/lib/na/version.rb +1 -1
- data/src/_README.md +145 -4
- metadata +1 -1
data/bin/commands/next.rb
CHANGED
|
@@ -136,6 +136,39 @@ class App
|
|
|
136
136
|
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# Detect TaskPaper-style @search() syntax in QUERY arguments and delegate
|
|
140
|
+
# directly to the TaskPaper search runner (supports item paths and
|
|
141
|
+
# advanced TaskPaper predicates).
|
|
142
|
+
joined_args = args.join(' ')
|
|
143
|
+
if joined_args =~ /@search\((.+)\)/
|
|
144
|
+
inner = Regexp.last_match(1)
|
|
145
|
+
expr = "@search(#{inner})"
|
|
146
|
+
|
|
147
|
+
file_path = options[:file] ? File.expand_path(options[:file]) : nil
|
|
148
|
+
NA::Pager.paginate = false if options[:omnifocus]
|
|
149
|
+
|
|
150
|
+
NA.run_taskpaper_search(
|
|
151
|
+
expr,
|
|
152
|
+
file: file_path,
|
|
153
|
+
options: {
|
|
154
|
+
depth: depth,
|
|
155
|
+
notes: options[:notes],
|
|
156
|
+
nest: options[:nest],
|
|
157
|
+
omnifocus: options[:omnifocus],
|
|
158
|
+
no_file: options[:no_file],
|
|
159
|
+
times: options[:times],
|
|
160
|
+
human: options[:human],
|
|
161
|
+
search_notes: options[:search_notes],
|
|
162
|
+
invert: false,
|
|
163
|
+
regex: options[:regex],
|
|
164
|
+
project: options[:project],
|
|
165
|
+
done: options[:done],
|
|
166
|
+
require_na: true
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
|
|
139
172
|
if options[:exact] || options[:regex]
|
|
140
173
|
search = options[:search].join(" ")
|
|
141
174
|
else
|
|
@@ -263,6 +296,13 @@ class App
|
|
|
263
296
|
|
|
264
297
|
file_path = options[:file] ? File.expand_path(options[:file]) : nil
|
|
265
298
|
|
|
299
|
+
# Support TaskPaper-style item paths in --project when value starts with '/'
|
|
300
|
+
project_filter_paths = nil
|
|
301
|
+
if options[:project]&.start_with?('/')
|
|
302
|
+
project_filter_paths = NA.resolve_item_path(path: options[:project], file: file_path, depth: depth)
|
|
303
|
+
options[:project] = nil
|
|
304
|
+
end
|
|
305
|
+
|
|
266
306
|
todo = NA::Todo.new({ depth: depth,
|
|
267
307
|
hidden: options[:hidden],
|
|
268
308
|
done: options[:done],
|
|
@@ -279,6 +319,17 @@ class App
|
|
|
279
319
|
end
|
|
280
320
|
NA::Pager.paginate = false if options[:omnifocus]
|
|
281
321
|
|
|
322
|
+
# Apply item-path project filters, if any
|
|
323
|
+
if project_filter_paths && project_filter_paths.any?
|
|
324
|
+
todo.actions.delete_if do |a|
|
|
325
|
+
parents = Array(a.parent)
|
|
326
|
+
path = parents.join(':')
|
|
327
|
+
project_filter_paths.none? do |p|
|
|
328
|
+
path =~ /\A#{Regexp.escape(p)}(?::|\z)/i
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
282
333
|
# If a plugin is specified, transform actions in memory for display only
|
|
283
334
|
if options[:plugin]
|
|
284
335
|
NA::Plugins.ensure_plugins_home
|
data/bin/commands/saved.rb
CHANGED
|
@@ -27,29 +27,64 @@ class App
|
|
|
27
27
|
NA.edit_searches if options[:edit]
|
|
28
28
|
|
|
29
29
|
if args.empty? && !options[:select]
|
|
30
|
-
|
|
30
|
+
yaml_searches = NA.load_searches
|
|
31
|
+
taskpaper_searches = NA.load_taskpaper_searches(depth: 1)
|
|
31
32
|
NA.notify("#{NA.theme[:success]}Saved searches stored in #{NA.database_path(file: 'saved_searches.yml').highlight_filename}")
|
|
32
|
-
|
|
33
|
+
lines = yaml_searches.map do |k, v|
|
|
34
|
+
"#{NA.theme[:filename]}#{k}: #{NA.theme[:values]}#{v}"
|
|
35
|
+
end
|
|
36
|
+
unless taskpaper_searches.empty?
|
|
37
|
+
lines << "#{NA.theme[:prompt]}TaskPaper saved searches:"
|
|
38
|
+
lines.concat(
|
|
39
|
+
taskpaper_searches.map do |k, v|
|
|
40
|
+
"#{NA.theme[:filename]}#{k}: #{NA.theme[:values]}#{v[:expr]} #{NA.theme[:note]}(#{File.basename(v[:file])})"
|
|
41
|
+
end
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
NA.notify(lines.join("\n"))
|
|
33
45
|
else
|
|
34
46
|
NA.delete_search(args.join(',').split(/[ ,]/)) if options[:delete]
|
|
35
47
|
|
|
36
48
|
if options[:select]
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
yaml_searches = NA.load_searches
|
|
50
|
+
taskpaper_searches = NA.load_taskpaper_searches(depth: 1)
|
|
51
|
+
combined = {}
|
|
52
|
+
yaml_searches.each { |k, v| combined[k] = { source: :yaml, value: v } }
|
|
53
|
+
taskpaper_searches.each { |k, v| combined[k] ||= { source: :taskpaper, value: v } }
|
|
54
|
+
|
|
55
|
+
res = NA.choose_from(
|
|
56
|
+
combined.map do |k, info|
|
|
57
|
+
val = info[:source] == :yaml ? info[:value] : info[:value][:expr]
|
|
58
|
+
"#{NA.theme[:filename]}#{k} #{NA.theme[:value]}(#{val})"
|
|
59
|
+
end,
|
|
60
|
+
multiple: true
|
|
61
|
+
)
|
|
39
62
|
NA.notify("#{NA.theme[:error]}Nothing selected", exit_code: 0) if res&.empty?
|
|
40
63
|
args = res.map { |r| r.match(/(\S+)(?= \()/)[1] }
|
|
41
64
|
end
|
|
42
65
|
|
|
43
66
|
args.each do |arg|
|
|
44
|
-
|
|
67
|
+
yaml_searches = NA.load_searches
|
|
68
|
+
taskpaper_searches = NA.load_taskpaper_searches(depth: 1)
|
|
69
|
+
all_keys = (yaml_searches.keys + taskpaper_searches.keys).uniq
|
|
45
70
|
|
|
46
|
-
keys =
|
|
71
|
+
keys = all_keys.delete_if { |k| k !~ /#{arg.wildcard_to_rx}/ }
|
|
47
72
|
NA.notify("#{NA.theme[:error]}Search #{arg} not found", exit_code: 1) if keys.empty?
|
|
48
73
|
|
|
49
74
|
keys.each do |key|
|
|
50
75
|
NA.notify("#{NA.theme[:prompt]}Saved search #{NA.theme[:filename]}#{key}#{NA.theme[:warning]}:")
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
if yaml_searches.key?(key)
|
|
77
|
+
value = yaml_searches[key]
|
|
78
|
+
if value.to_s.strip =~ /\A@search\(.+\)\s*\z/
|
|
79
|
+
NA.run_taskpaper_search(value)
|
|
80
|
+
else
|
|
81
|
+
cmd = Shellwords.shellsplit(value)
|
|
82
|
+
run(cmd)
|
|
83
|
+
end
|
|
84
|
+
elsif taskpaper_searches.key?(key)
|
|
85
|
+
info = taskpaper_searches[key]
|
|
86
|
+
NA.run_taskpaper_search(info[:expr], file: info[:file])
|
|
87
|
+
end
|
|
53
88
|
end
|
|
54
89
|
end
|
|
55
90
|
end
|
data/lib/na/action.rb
CHANGED
|
@@ -422,7 +422,9 @@ module NA
|
|
|
422
422
|
return false if keys.empty?
|
|
423
423
|
|
|
424
424
|
key = keys[0]
|
|
425
|
-
|
|
425
|
+
# If no comparator is provided, treat this as an "existence" check:
|
|
426
|
+
# any value for this tag name is acceptable.
|
|
427
|
+
return true if tag[:comp].nil? || tag[:comp].to_s.empty?
|
|
426
428
|
|
|
427
429
|
tag_val = @tags[key]
|
|
428
430
|
val = tag[:value]
|