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/next_action.rb
CHANGED
|
@@ -5,27 +5,20 @@ module NA
|
|
|
5
5
|
class << self
|
|
6
6
|
include NA::Editor
|
|
7
7
|
|
|
8
|
-
attr_accessor :verbose, :extension, :include_ext, :na_tag, :command_line, :command, :globals, :global_file,
|
|
8
|
+
attr_accessor :verbose, :extension, :include_ext, :na_tag, :command_line, :command, :globals, :global_file,
|
|
9
|
+
:cwd_is, :cwd, :stdin, :show_cwd_indicator
|
|
9
10
|
|
|
10
11
|
def theme
|
|
11
12
|
@theme ||= NA::Theme.load_theme
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
##
|
|
15
|
-
## Output to STDERR
|
|
16
|
-
##
|
|
17
|
-
## @param msg [String] The message
|
|
18
|
-
## @param exit_code [Number] The exit code, no
|
|
19
|
-
## exit if false
|
|
20
|
-
## @param debug [Boolean] only display message if running :verbose
|
|
21
|
-
##
|
|
22
15
|
def notify(msg, exit_code: false, debug: false)
|
|
23
16
|
return if debug && !NA.verbose
|
|
24
17
|
|
|
25
18
|
if debug
|
|
26
|
-
|
|
19
|
+
warn NA::Color.template("{x}#{NA.theme[:debug]}#{msg}{x}")
|
|
27
20
|
else
|
|
28
|
-
|
|
21
|
+
warn NA::Color.template("{x}#{msg}{x}")
|
|
29
22
|
end
|
|
30
23
|
Process.exit exit_code if exit_code
|
|
31
24
|
end
|
|
@@ -38,16 +31,16 @@ module NA
|
|
|
38
31
|
}
|
|
39
32
|
end
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
#
|
|
35
|
+
# Display and read a Yes/No prompt
|
|
36
|
+
#
|
|
37
|
+
# @param prompt [String] The prompt string
|
|
38
|
+
# @param default [Boolean] default value if
|
|
39
|
+
# return is pressed or prompt is
|
|
40
|
+
# skipped
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean] result
|
|
43
|
+
#
|
|
51
44
|
def yn(prompt, default: true)
|
|
52
45
|
return default unless $stdout.isatty
|
|
53
46
|
|
|
@@ -63,14 +56,14 @@ module NA
|
|
|
63
56
|
res.empty? ? default : res =~ /y/i
|
|
64
57
|
end
|
|
65
58
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
#
|
|
60
|
+
# Helper function to colorize the Y/N prompt
|
|
61
|
+
#
|
|
62
|
+
# @param choices [Array] The choices with
|
|
63
|
+
# default capitalized
|
|
64
|
+
#
|
|
65
|
+
# @return [String] colorized string
|
|
66
|
+
#
|
|
74
67
|
def color_single_options(choices = %w[y n])
|
|
75
68
|
out = []
|
|
76
69
|
choices.each do |choice|
|
|
@@ -84,51 +77,48 @@ module NA
|
|
|
84
77
|
NA::Color.template("{xg}[#{out.join('/')}{xg}]{x}")
|
|
85
78
|
end
|
|
86
79
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
#
|
|
81
|
+
# Create a new todo file
|
|
82
|
+
#
|
|
83
|
+
# @param target [String] The target path
|
|
84
|
+
# @param basename [String] The project base name
|
|
85
|
+
#
|
|
93
86
|
def create_todo(target, basename, template: nil)
|
|
94
87
|
File.open(target, 'w') do |f|
|
|
95
|
-
if template && File.exist?(template)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
content = if template && File.exist?(template)
|
|
89
|
+
File.read(template)
|
|
90
|
+
else
|
|
91
|
+
<<~ENDCONTENT
|
|
92
|
+
Inbox:
|
|
93
|
+
#{basename}:
|
|
94
|
+
\tFeature Requests:
|
|
95
|
+
\tIdeas:
|
|
96
|
+
\tBugs:
|
|
97
|
+
Archive:
|
|
98
|
+
Search Definitions:
|
|
99
|
+
\tTop Priority @search(@priority = 5 and not @done)
|
|
100
|
+
\tHigh Priority @search(@priority > 3 and not @done)
|
|
101
|
+
\tMaybe @search(@maybe)
|
|
102
|
+
\tNext @search(@#{NA.na_tag} and not @done and not project = "Archive")
|
|
103
|
+
ENDCONTENT
|
|
104
|
+
end
|
|
112
105
|
f.puts(content)
|
|
113
106
|
end
|
|
114
107
|
save_working_dir(target)
|
|
115
108
|
notify("#{NA.theme[:warning]}Created #{NA.theme[:file]}#{target}")
|
|
116
109
|
end
|
|
117
110
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
## @param multiple [Boolean] allow multiple selections
|
|
126
|
-
##
|
|
127
|
-
## @return [String, Array] array if multiple
|
|
111
|
+
# Select from multiple files
|
|
112
|
+
#
|
|
113
|
+
# If `gum` or `fzf` are available, they'll be used (in that order).
|
|
114
|
+
#
|
|
115
|
+
# @param files [Array<String>] The files to select from
|
|
116
|
+
# @param multiple [Boolean] Allow multiple selections
|
|
117
|
+
# @return [String, Array<String>] Selected file(s)
|
|
128
118
|
def select_file(files, multiple: false)
|
|
129
119
|
res = choose_from(files, prompt: multiple ? 'Select files' : 'Select a file', multiple: multiple)
|
|
130
120
|
|
|
131
|
-
notify("#{NA.theme[:error]}No file selected, cancelled", exit_code: 1) unless res
|
|
121
|
+
notify("#{NA.theme[:error]}No file selected, cancelled", exit_code: 1) unless res&.length&.positive?
|
|
132
122
|
|
|
133
123
|
res
|
|
134
124
|
end
|
|
@@ -142,11 +132,25 @@ module NA
|
|
|
142
132
|
end
|
|
143
133
|
end
|
|
144
134
|
|
|
135
|
+
# Find all projects in a todo file
|
|
136
|
+
#
|
|
137
|
+
# @param target [String] Path to the todo file
|
|
138
|
+
# @return [Array<NA::Project>] List of projects
|
|
145
139
|
def find_projects(target)
|
|
146
140
|
todo = NA::Todo.new(require_na: false, file_path: target)
|
|
147
141
|
todo.projects
|
|
148
142
|
end
|
|
149
143
|
|
|
144
|
+
# Find actions in a todo file matching criteria
|
|
145
|
+
#
|
|
146
|
+
# @param target [String] Path to the todo file
|
|
147
|
+
# @param search [String, nil] Search string
|
|
148
|
+
# @param tagged [String, nil] Tag to filter
|
|
149
|
+
# @param all [Boolean] Return all actions
|
|
150
|
+
# @param done [Boolean] Include done actions
|
|
151
|
+
# @param project [String, nil] Project name
|
|
152
|
+
# @param search_note [Boolean] Search notes
|
|
153
|
+
# @return [Array] Projects and actions
|
|
150
154
|
def find_actions(target, search, tagged = nil, all: false, done: false, project: nil, search_note: true)
|
|
151
155
|
todo = NA::Todo.new({ search: search,
|
|
152
156
|
search_note: search_note,
|
|
@@ -157,8 +161,9 @@ module NA
|
|
|
157
161
|
done: done })
|
|
158
162
|
|
|
159
163
|
unless todo.actions.count.positive?
|
|
160
|
-
NA.notify("#{NA.theme[:error]}No matching actions found in #{File.basename(target,
|
|
161
|
-
|
|
164
|
+
NA.notify("#{NA.theme[:error]}No matching actions found in #{File.basename(target,
|
|
165
|
+
".#{NA.extension}").highlight_filename}")
|
|
166
|
+
return [todo.projects, NA::Actions.new]
|
|
162
167
|
end
|
|
163
168
|
|
|
164
169
|
return [todo.projects, todo.actions] if todo.actions.count == 1 || all
|
|
@@ -166,7 +171,10 @@ module NA
|
|
|
166
171
|
options = todo.actions.map { |action| "#{action.line} % #{action.parent.join('/')} : #{action.action}" }
|
|
167
172
|
res = choose_from(options, prompt: 'Make a selection: ', multiple: true, sorted: true)
|
|
168
173
|
|
|
169
|
-
|
|
174
|
+
unless res&.length&.positive?
|
|
175
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
|
|
176
|
+
return [todo.projects, NA::Actions.new]
|
|
177
|
+
end
|
|
170
178
|
|
|
171
179
|
selected = NA::Actions.new
|
|
172
180
|
res.each do |result|
|
|
@@ -177,7 +185,13 @@ module NA
|
|
|
177
185
|
[todo.projects, selected]
|
|
178
186
|
end
|
|
179
187
|
|
|
180
|
-
|
|
188
|
+
# Insert a new project into a todo file
|
|
189
|
+
#
|
|
190
|
+
# @param target [String] Path to the todo file
|
|
191
|
+
# @param project [String] Project name
|
|
192
|
+
# @param projects [Array<NA::Project>] Existing projects
|
|
193
|
+
# @return [NA::Project] The new project
|
|
194
|
+
def insert_project(target, project, _projects)
|
|
181
195
|
path = project.split(%r{[:/]})
|
|
182
196
|
todo = NA::Todo.new(file_path: target)
|
|
183
197
|
built = []
|
|
@@ -207,11 +221,11 @@ module NA
|
|
|
207
221
|
indent += 1
|
|
208
222
|
end
|
|
209
223
|
|
|
210
|
-
if new_path.join
|
|
224
|
+
if new_path.join =~ /Archive/i
|
|
211
225
|
line = todo.projects.last&.last_line || 0
|
|
212
|
-
content = content.split(
|
|
226
|
+
content = content.split("\n").insert(line, input.join("\n")).join("\n")
|
|
213
227
|
else
|
|
214
|
-
split = content.split(
|
|
228
|
+
split = content.split("\n")
|
|
215
229
|
line = todo.projects.first&.line || 0
|
|
216
230
|
before = split.slice(0, line).join("\n")
|
|
217
231
|
after = split.slice(line, split.count - 0).join("\n")
|
|
@@ -227,8 +241,9 @@ module NA
|
|
|
227
241
|
input.push("#{"\t" * indent}#{part.cap_first}:")
|
|
228
242
|
indent += 1
|
|
229
243
|
end
|
|
230
|
-
content = content.split(
|
|
231
|
-
new_project = NA::Project.new(path.map(&:cap_first).join(':'), indent - 1, line + input.count - 1,
|
|
244
|
+
content = content.split("\n").insert(line, input.join("\n")).join("\n")
|
|
245
|
+
new_project = NA::Project.new(path.map(&:cap_first).join(':'), indent - 1, line + input.count - 1,
|
|
246
|
+
line + input.count - 1)
|
|
232
247
|
end
|
|
233
248
|
|
|
234
249
|
File.open(target, 'w') do |f|
|
|
@@ -238,6 +253,28 @@ module NA
|
|
|
238
253
|
new_project
|
|
239
254
|
end
|
|
240
255
|
|
|
256
|
+
# Update actions in a todo file (add, edit, delete, move, etc.)
|
|
257
|
+
#
|
|
258
|
+
# @param target [String] Path to the todo file
|
|
259
|
+
# @param search [String, nil] Search string
|
|
260
|
+
# @param search_note [Boolean] Search notes
|
|
261
|
+
# @param add [Action, nil] Action to add
|
|
262
|
+
# @param add_tag [Array<String>] Tags to add
|
|
263
|
+
# @param all [Boolean] Update all matching actions
|
|
264
|
+
# @param append [Boolean] Append to project
|
|
265
|
+
# @param delete [Boolean] Delete matching actions
|
|
266
|
+
# @param done [Boolean] Mark as done
|
|
267
|
+
# @param edit [Boolean] Edit matching actions
|
|
268
|
+
# @param finish [Boolean] Mark as finished
|
|
269
|
+
# @param note [Array<String>] Notes to add
|
|
270
|
+
# @param overwrite [Boolean] Overwrite notes
|
|
271
|
+
# @param priority [Integer] Priority value
|
|
272
|
+
# @param project [String, nil] Project name
|
|
273
|
+
# @param move [String, nil] Move to project
|
|
274
|
+
# @param remove_tag [Array<String>] Tags to remove
|
|
275
|
+
# @param replace [String, nil] Replacement text
|
|
276
|
+
# @param tagged [String, nil] Tag to filter
|
|
277
|
+
# @return [void]
|
|
241
278
|
def update_action(target,
|
|
242
279
|
search,
|
|
243
280
|
search_note: true,
|
|
@@ -257,7 +294,6 @@ module NA
|
|
|
257
294
|
remove_tag: [],
|
|
258
295
|
replace: nil,
|
|
259
296
|
tagged: nil)
|
|
260
|
-
|
|
261
297
|
projects = find_projects(target)
|
|
262
298
|
affected_actions = []
|
|
263
299
|
|
|
@@ -265,9 +301,11 @@ module NA
|
|
|
265
301
|
|
|
266
302
|
if move
|
|
267
303
|
move = move.sub(/:$/, '')
|
|
268
|
-
target_proj = projects.select { |pr| pr.project =~ /#{move.gsub(
|
|
304
|
+
target_proj = projects.select { |pr| pr.project =~ /#{move.gsub(':', '.*?:.*?')}/i }.first
|
|
269
305
|
if target_proj.nil?
|
|
270
|
-
res = NA.yn(
|
|
306
|
+
res = NA.yn(
|
|
307
|
+
NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{move}#{NA.theme[:warning]} doesn't exist, add it"), default: true
|
|
308
|
+
)
|
|
271
309
|
if res
|
|
272
310
|
target_proj = insert_project(target, move, projects)
|
|
273
311
|
projects << target_proj
|
|
@@ -277,7 +315,7 @@ module NA
|
|
|
277
315
|
end
|
|
278
316
|
end
|
|
279
317
|
|
|
280
|
-
contents = target.read_file.split(
|
|
318
|
+
contents = target.read_file.split("\n")
|
|
281
319
|
|
|
282
320
|
if add.is_a?(Action)
|
|
283
321
|
add_tag ||= []
|
|
@@ -305,7 +343,9 @@ module NA
|
|
|
305
343
|
end
|
|
306
344
|
|
|
307
345
|
if target_proj.nil?
|
|
308
|
-
res = NA.yn(
|
|
346
|
+
res = NA.yn(
|
|
347
|
+
NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{add.project}#{NA.theme[:warning]} doesn't exist, add it"), default: true
|
|
348
|
+
)
|
|
309
349
|
|
|
310
350
|
if res
|
|
311
351
|
target_proj = insert_project(target, project, projects)
|
|
@@ -314,7 +354,10 @@ module NA
|
|
|
314
354
|
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
|
|
315
355
|
end
|
|
316
356
|
|
|
317
|
-
|
|
357
|
+
if target_proj.nil?
|
|
358
|
+
NA.notify("#{NA.theme[:error]}Error parsing project #{NA.theme[:filename]}#{target}",
|
|
359
|
+
exit_code: 1)
|
|
360
|
+
end
|
|
318
361
|
|
|
319
362
|
projects = find_projects(target)
|
|
320
363
|
contents = target.read_file.split("\n")
|
|
@@ -352,15 +395,16 @@ module NA
|
|
|
352
395
|
notify(add.pretty)
|
|
353
396
|
|
|
354
397
|
# Track affected action and description
|
|
355
|
-
changes = [
|
|
356
|
-
changes <<
|
|
398
|
+
changes = ['added']
|
|
399
|
+
changes << 'finished' if finish
|
|
357
400
|
changes << "priority=#{priority}" if priority.to_i.positive?
|
|
358
401
|
changes << "tags+#{add_tag.join(',')}" unless add_tag.nil? || add_tag.empty?
|
|
359
402
|
changes << "tags-#{remove_tag.join(',')}" unless remove_tag.nil? || remove_tag.empty?
|
|
360
|
-
changes <<
|
|
403
|
+
changes << 'note updated' unless note.nil? || note.empty?
|
|
361
404
|
affected_actions << { action: add, desc: changes.join(', ') }
|
|
362
405
|
else
|
|
363
|
-
_, actions = find_actions(target, search, tagged, done: done, all: all, project: project,
|
|
406
|
+
_, actions = find_actions(target, search, tagged, done: done, all: all, project: project,
|
|
407
|
+
search_note: search_note)
|
|
364
408
|
|
|
365
409
|
return if actions.nil?
|
|
366
410
|
|
|
@@ -425,15 +469,15 @@ module NA
|
|
|
425
469
|
|
|
426
470
|
# Track affected action and description
|
|
427
471
|
changes = []
|
|
428
|
-
changes <<
|
|
429
|
-
changes <<
|
|
472
|
+
changes << 'finished' if finish
|
|
473
|
+
changes << 'edited' if edit
|
|
430
474
|
changes << "priority=#{priority}" if priority.to_i.positive?
|
|
431
475
|
changes << "tags+#{add_tag.join(',')}" unless add_tag.nil? || add_tag.empty?
|
|
432
476
|
changes << "tags-#{remove_tag.join(',')}" unless remove_tag.nil? || remove_tag.empty?
|
|
433
|
-
changes <<
|
|
477
|
+
changes << 'text replaced' if replace
|
|
434
478
|
changes << "moved to #{target_proj.project}" if target_proj
|
|
435
|
-
changes <<
|
|
436
|
-
changes = [
|
|
479
|
+
changes << 'note updated' unless note.nil? || note.empty?
|
|
480
|
+
changes = ['updated'] if changes.empty?
|
|
437
481
|
affected_actions << { action: action, desc: changes.join(', ') }
|
|
438
482
|
end
|
|
439
483
|
end
|
|
@@ -454,23 +498,23 @@ module NA
|
|
|
454
498
|
action_color = delete ? NA.theme[:error] : NA.theme[:success]
|
|
455
499
|
notify(" #{entry[:action].to_s_pretty} — #{action_color}#{entry[:desc]}")
|
|
456
500
|
end
|
|
501
|
+
elsif add
|
|
502
|
+
notify("#{NA.theme[:success]}Task added to #{NA.theme[:filename]}#{target}")
|
|
457
503
|
else
|
|
458
|
-
|
|
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
|
|
504
|
+
notify("#{NA.theme[:success]}Task updated in #{NA.theme[:filename]}#{target}")
|
|
463
505
|
end
|
|
464
506
|
end
|
|
465
507
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
508
|
+
# Add an action to a todo file
|
|
509
|
+
#
|
|
510
|
+
# @param file [String] Path to the todo file
|
|
511
|
+
# @param project [String] Project name
|
|
512
|
+
# @param action [String] Action text
|
|
513
|
+
# @param note [Array<String>] Notes
|
|
514
|
+
# @param priority [Integer] Priority value
|
|
515
|
+
# @param finish [Boolean] Mark as finished
|
|
516
|
+
# @param append [Boolean] Append to project
|
|
517
|
+
# @return [void]
|
|
474
518
|
def add_action(file, project, action, note = [], priority: 0, finish: false, append: false)
|
|
475
519
|
parent = project.split(%r{[:/]})
|
|
476
520
|
|
|
@@ -484,18 +528,21 @@ module NA
|
|
|
484
528
|
|
|
485
529
|
action = Action.new(file, project, parent, action, nil, note)
|
|
486
530
|
|
|
487
|
-
update_action(file, nil, add: action, project: project, add_tag: add_tag, priority: priority, finish: finish,
|
|
531
|
+
update_action(file, nil, add: action, project: project, add_tag: add_tag, priority: priority, finish: finish,
|
|
532
|
+
append: append)
|
|
488
533
|
end
|
|
489
534
|
|
|
535
|
+
# Build a nested hash representing project hierarchy from actions
|
|
536
|
+
#
|
|
537
|
+
# @param actions [Array<Action>] List of actions
|
|
538
|
+
# @return [Hash] Nested hierarchy
|
|
490
539
|
def project_hierarchy(actions)
|
|
491
540
|
parents = { actions: [] }
|
|
492
541
|
actions.each do |a|
|
|
493
542
|
parent = a.parent
|
|
494
543
|
current_parent = parents
|
|
495
544
|
parent.each do |par|
|
|
496
|
-
|
|
497
|
-
current_parent[par] = { actions: [] }
|
|
498
|
-
end
|
|
545
|
+
current_parent[par] = { actions: [] } unless current_parent.key?(par)
|
|
499
546
|
current_parent = current_parent[par]
|
|
500
547
|
end
|
|
501
548
|
|
|
@@ -512,14 +559,14 @@ module NA
|
|
|
512
559
|
def output_children(children, level = 1)
|
|
513
560
|
out = []
|
|
514
561
|
indent = "\t" * level
|
|
562
|
+
return out if children.nil? || children.empty?
|
|
563
|
+
|
|
515
564
|
children.each do |k, v|
|
|
516
565
|
if k.to_s =~ /actions/
|
|
517
566
|
indent += "\t"
|
|
518
|
-
|
|
519
|
-
v.each do |a|
|
|
567
|
+
v&.each do |a|
|
|
520
568
|
item = "#{indent}- #{a.action}"
|
|
521
|
-
|
|
522
|
-
unless a.tags.empty?
|
|
569
|
+
unless a.tags.nil? || a.tags.empty?
|
|
523
570
|
tags = []
|
|
524
571
|
a.tags.each do |key, val|
|
|
525
572
|
next if key =~ /^(due|flagged|done)$/
|
|
@@ -528,12 +575,9 @@ module NA
|
|
|
528
575
|
tag += "-#{val}" unless val.nil? || val.empty?
|
|
529
576
|
tags.push(tag)
|
|
530
577
|
end
|
|
531
|
-
|
|
532
578
|
item += " @tags(#{tags.join(',')})" unless tags.empty?
|
|
533
579
|
end
|
|
534
|
-
|
|
535
580
|
item += "\n#{indent}\t#{a.note.join("\n#{indent}\t")}" unless a.note.empty?
|
|
536
|
-
|
|
537
581
|
out.push(item)
|
|
538
582
|
end
|
|
539
583
|
else
|
|
@@ -544,21 +588,45 @@ module NA
|
|
|
544
588
|
out
|
|
545
589
|
end
|
|
546
590
|
|
|
591
|
+
# Open a file in the specified editor/application
|
|
592
|
+
#
|
|
593
|
+
# @param file [String, nil] Path to the file
|
|
594
|
+
# @param app [String, nil] Application to use
|
|
595
|
+
# @return [void]
|
|
547
596
|
def edit_file(file: nil, app: nil)
|
|
548
597
|
os_open(file, app: app) if file && File.exist?(file)
|
|
549
598
|
end
|
|
550
599
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
def find_files(depth: 1)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
600
|
+
# Locate files matching NA.extension up to a given depth
|
|
601
|
+
#
|
|
602
|
+
# @param depth [Integer] The depth at which to search
|
|
603
|
+
# @param include_hidden [Boolean] Whether to include hidden directories/files
|
|
604
|
+
# @return [Array<String>] List of matching file paths
|
|
605
|
+
def find_files(depth: 1, include_hidden: false)
|
|
606
|
+
NA::Benchmark.measure("find_files (depth=#{depth})") do
|
|
607
|
+
return [NA.global_file] if NA.global_file
|
|
608
|
+
|
|
609
|
+
# Build a brace-expanded pattern list covering 1..depth levels, e.g.:
|
|
610
|
+
# depth=1 -> "*.ext"
|
|
611
|
+
# depth=3 -> "{*.ext,*/*.ext,*/*/*.ext}"
|
|
612
|
+
ext = NA.extension
|
|
613
|
+
patterns = (1..[depth.to_i, 1].max).map do |d|
|
|
614
|
+
prefix = d > 1 ? ('*/' * (d - 1)) : ''
|
|
615
|
+
"#{prefix}*.#{ext}"
|
|
616
|
+
end
|
|
617
|
+
pattern = patterns.length == 1 ? patterns.first : "{#{patterns.join(',')}}"
|
|
618
|
+
|
|
619
|
+
files = Dir.glob(pattern, File::FNM_DOTMATCH)
|
|
620
|
+
# Exclude hidden directories/files unless explicitly requested
|
|
621
|
+
unless include_hidden
|
|
622
|
+
files.reject! do |f|
|
|
623
|
+
# reject any path segment beginning with '.' (excluding '.' and '..')
|
|
624
|
+
f.split('/').any? { |seg| seg.start_with?('.') && seg !~ /^\.\.?$/ }
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
files.each { |f| save_working_dir(File.expand_path(f)) }
|
|
628
|
+
files
|
|
629
|
+
end
|
|
562
630
|
end
|
|
563
631
|
|
|
564
632
|
def find_files_matching(options = {})
|
|
@@ -567,15 +635,15 @@ module NA
|
|
|
567
635
|
done: false,
|
|
568
636
|
file_path: nil,
|
|
569
637
|
negate: false,
|
|
638
|
+
hidden: false,
|
|
570
639
|
project: nil,
|
|
571
640
|
query: nil,
|
|
572
641
|
regex: false,
|
|
573
|
-
require_na: true,
|
|
574
642
|
search: nil,
|
|
575
643
|
tag: nil
|
|
576
644
|
}
|
|
577
645
|
options = defaults.merge(options)
|
|
578
|
-
files = find_files(depth: options[:depth])
|
|
646
|
+
files = find_files(depth: options[:depth], include_hidden: options[:hidden])
|
|
579
647
|
|
|
580
648
|
files.delete_if do |file|
|
|
581
649
|
cmd_options = {
|
|
@@ -597,20 +665,13 @@ module NA
|
|
|
597
665
|
files
|
|
598
666
|
end
|
|
599
667
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
## @param distance [Integer] allowed distance
|
|
608
|
-
## between characters
|
|
609
|
-
## @param require_last [Boolean] require regex to
|
|
610
|
-
## match last element of path
|
|
611
|
-
##
|
|
612
|
-
## @return [Array] array of matching directories/todo files
|
|
613
|
-
##
|
|
668
|
+
# Find a matching path using semi-fuzzy matching.
|
|
669
|
+
# Search tokens can include ! and + to negate or make required.
|
|
670
|
+
#
|
|
671
|
+
# @param search [Array<Hash>] Search tokens to match
|
|
672
|
+
# @param distance [Integer] Allowed distance between characters
|
|
673
|
+
# @param require_last [Boolean] Require regex to match last element of path
|
|
674
|
+
# @return [Array<String>] Array of matching directories/todo files
|
|
614
675
|
def match_working_dir(search, distance: 1, require_last: true)
|
|
615
676
|
file = database_path
|
|
616
677
|
NA.notify("#{NA.theme[:error]}No na database found", exit_code: 1) unless File.exist?(file)
|
|
@@ -629,7 +690,9 @@ module NA
|
|
|
629
690
|
|
|
630
691
|
NA.notify("Optional directory regex: {x}#{optional.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
|
|
631
692
|
NA.notify("Required directory regex: {x}#{required.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
|
|
632
|
-
NA.notify("Negated directory regex: {x}#{negated.map
|
|
693
|
+
NA.notify("Negated directory regex: {x}#{negated.map do |t|
|
|
694
|
+
t.dir_to_rx(distance: distance, require_last: false)
|
|
695
|
+
end}", debug: true)
|
|
633
696
|
|
|
634
697
|
if require_last
|
|
635
698
|
dirs.delete_if { |d| !d.sub(/\.#{NA.extension}$/, '').dir_matches(any: optional, all: required, none: negated) }
|
|
@@ -665,30 +728,30 @@ module NA
|
|
|
665
728
|
out
|
|
666
729
|
end
|
|
667
730
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
##
|
|
731
|
+
# Save a todo file path to the database
|
|
732
|
+
#
|
|
733
|
+
# @param todo_file [String] The todo file path
|
|
734
|
+
# @return [void]
|
|
673
735
|
def save_working_dir(todo_file)
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
736
|
+
NA::Benchmark.measure('save_working_dir') do
|
|
737
|
+
file = database_path
|
|
738
|
+
content = File.exist?(file) ? file.read_file : ''
|
|
739
|
+
dirs = content.split("\n")
|
|
740
|
+
dirs.push(File.expand_path(todo_file))
|
|
741
|
+
dirs.sort!.uniq!
|
|
742
|
+
File.open(file, 'w') { |f| f.puts dirs.join("\n") }
|
|
743
|
+
end
|
|
680
744
|
end
|
|
681
745
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
##
|
|
746
|
+
# Save a backed-up file to the database
|
|
747
|
+
#
|
|
748
|
+
# @param file [String] The file
|
|
749
|
+
# @return [void]
|
|
687
750
|
def save_modified_file(file)
|
|
688
751
|
db = database_path(file: 'last_modified.txt')
|
|
689
752
|
file = File.expand_path(file)
|
|
690
753
|
if File.exist? db
|
|
691
|
-
files =
|
|
754
|
+
files = File.read(db).split("\n").map(&:strip)
|
|
692
755
|
files.delete(file)
|
|
693
756
|
files << file
|
|
694
757
|
File.open(db, 'w') { |f| f.puts(files.join("\n")) }
|
|
@@ -697,22 +760,20 @@ module NA
|
|
|
697
760
|
end
|
|
698
761
|
end
|
|
699
762
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
##
|
|
763
|
+
# Get the last modified file from the database
|
|
764
|
+
#
|
|
765
|
+
# @param search [String, nil] Optional search string
|
|
766
|
+
# @return [String, nil] Last modified file path
|
|
705
767
|
def last_modified_file(search: nil)
|
|
706
768
|
files = backup_files
|
|
707
769
|
files.delete_if { |f| f !~ Regexp.new(search.dir_to_rx(require_last: true)) } if search
|
|
708
770
|
files.last
|
|
709
771
|
end
|
|
710
772
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
##
|
|
773
|
+
# Get last modified file and restore a backup
|
|
774
|
+
#
|
|
775
|
+
# @param search [String, nil] Optional search string
|
|
776
|
+
# @return [void]
|
|
716
777
|
def restore_last_modified_file(search: nil)
|
|
717
778
|
file = last_modified_file(search: search)
|
|
718
779
|
if file
|
|
@@ -722,22 +783,23 @@ module NA
|
|
|
722
783
|
end
|
|
723
784
|
end
|
|
724
785
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
## @return [Array] list of file paths
|
|
729
|
-
##
|
|
786
|
+
# Get list of backed up files
|
|
787
|
+
#
|
|
788
|
+
# @return [Array<String>] List of file paths
|
|
730
789
|
def backup_files
|
|
731
790
|
db = database_path(file: 'last_modified.txt')
|
|
732
791
|
if File.exist?(db)
|
|
733
|
-
|
|
792
|
+
File.read(db).strip.split("\n").map(&:strip)
|
|
734
793
|
else
|
|
735
794
|
NA.notify("#{NA.theme[:error]}Backup database not found")
|
|
736
|
-
File.open(db, 'w'
|
|
795
|
+
File.open(db, 'w', &:puts)
|
|
737
796
|
[]
|
|
738
797
|
end
|
|
739
798
|
end
|
|
740
799
|
|
|
800
|
+
# Move deprecated backup files to new backup folder
|
|
801
|
+
#
|
|
802
|
+
# @return [void]
|
|
741
803
|
def move_deprecated_backups
|
|
742
804
|
backup_files.each do |file|
|
|
743
805
|
if File.exist?(old_backup_path(file))
|
|
@@ -747,15 +809,18 @@ module NA
|
|
|
747
809
|
end
|
|
748
810
|
end
|
|
749
811
|
|
|
812
|
+
# Get the old backup file path for a file
|
|
813
|
+
#
|
|
814
|
+
# @param file [String] The file
|
|
815
|
+
# @return [String] Old backup file path
|
|
750
816
|
def old_backup_path(file)
|
|
751
817
|
File.join(File.dirname(file), ".#{File.basename(file)}.bak")
|
|
752
818
|
end
|
|
753
819
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
##
|
|
820
|
+
# Get the backup file path for a file
|
|
821
|
+
#
|
|
822
|
+
# @param file [String] The file
|
|
823
|
+
# @return [String] Backup file path
|
|
759
824
|
def backup_path(file)
|
|
760
825
|
backup_home = File.expand_path('~/.local/share/na/backup')
|
|
761
826
|
backup = old_backup_path(file)
|
|
@@ -767,6 +832,10 @@ module NA
|
|
|
767
832
|
backup_target
|
|
768
833
|
end
|
|
769
834
|
|
|
835
|
+
# Remove entries for missing backup files from the database
|
|
836
|
+
#
|
|
837
|
+
# @param file [String, nil] Optional file to filter
|
|
838
|
+
# @return [void]
|
|
770
839
|
def weed_modified_files(file = nil)
|
|
771
840
|
files = backup_files
|
|
772
841
|
|
|
@@ -777,11 +846,10 @@ module NA
|
|
|
777
846
|
File.open(database_path(file: 'last_modified.txt'), 'w') { |f| f.puts files.join("\n") }
|
|
778
847
|
end
|
|
779
848
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
##
|
|
849
|
+
# Restore a file from backup
|
|
850
|
+
#
|
|
851
|
+
# @param file [String] The file
|
|
852
|
+
# @return [void]
|
|
785
853
|
def restore_modified_file(file)
|
|
786
854
|
bak_file = backup_path(file)
|
|
787
855
|
if File.exist?(bak_file)
|
|
@@ -794,11 +862,10 @@ module NA
|
|
|
794
862
|
weed_modified_files(file)
|
|
795
863
|
end
|
|
796
864
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
##
|
|
865
|
+
# Get path to database of known todo files
|
|
866
|
+
#
|
|
867
|
+
# @param file [String] The database filename (default: 'tdlist.txt')
|
|
868
|
+
# @return [String] File path
|
|
802
869
|
def database_path(file: 'tdlist.txt')
|
|
803
870
|
db_dir = File.expand_path('~/.local/share/na')
|
|
804
871
|
# Create directory if needed
|
|
@@ -806,11 +873,11 @@ module NA
|
|
|
806
873
|
File.join(db_dir, file)
|
|
807
874
|
end
|
|
808
875
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
876
|
+
# Platform-agnostic open command
|
|
877
|
+
#
|
|
878
|
+
# @param file [String] The file to open
|
|
879
|
+
# @param app [String, nil] Optional application to use
|
|
880
|
+
# @return [void]
|
|
814
881
|
def os_open(file, app: nil)
|
|
815
882
|
os = RbConfig::CONFIG['target_os']
|
|
816
883
|
case os
|
|
@@ -823,9 +890,9 @@ module NA
|
|
|
823
890
|
end
|
|
824
891
|
end
|
|
825
892
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
893
|
+
#
|
|
894
|
+
# Remove entries from cache database that no longer exist
|
|
895
|
+
#
|
|
829
896
|
def weed_cache_file
|
|
830
897
|
db_dir = File.expand_path('~/.local/share/na')
|
|
831
898
|
db_file = 'tdlist.txt'
|
|
@@ -853,7 +920,7 @@ module NA
|
|
|
853
920
|
|
|
854
921
|
projects = find_projects(target)
|
|
855
922
|
projects.each do |proj|
|
|
856
|
-
parts = proj.project.split(
|
|
923
|
+
parts = proj.project.split(':')
|
|
857
924
|
output = if paths
|
|
858
925
|
"{bg}#{parts.join('{bw}/{bg}')}{x}"
|
|
859
926
|
else
|
|
@@ -873,12 +940,10 @@ module NA
|
|
|
873
940
|
content = File.exist?(file) ? file.read_file.strip : ''
|
|
874
941
|
notify("#{NA.theme[:error]}Database empty", exit_code: 1) if content.empty?
|
|
875
942
|
|
|
876
|
-
content.split(
|
|
943
|
+
content.split("\n")
|
|
877
944
|
end
|
|
878
945
|
|
|
879
|
-
dirs.map!
|
|
880
|
-
dir.highlight_filename
|
|
881
|
-
end
|
|
946
|
+
dirs.map!(&:highlight_filename)
|
|
882
947
|
|
|
883
948
|
puts NA::Color.template(dirs.join("\n"))
|
|
884
949
|
end
|
|
@@ -886,7 +951,7 @@ module NA
|
|
|
886
951
|
def save_search(title, search)
|
|
887
952
|
file = database_path(file: 'saved_searches.yml')
|
|
888
953
|
searches = load_searches
|
|
889
|
-
title = title.gsub(/[^a-
|
|
954
|
+
title = title.gsub(/[^a-zA-Z0-9]/, '_').gsub(/_+/, '_').downcase
|
|
890
955
|
|
|
891
956
|
if searches.key?(title)
|
|
892
957
|
res = yn('Overwrite existing definition?', default: true)
|
|
@@ -899,10 +964,13 @@ module NA
|
|
|
899
964
|
NA.notify("#{NA.theme[:success]}Search #{NA.theme[:filename]}#{title}#{NA.theme[:success]} saved", exit_code: 0)
|
|
900
965
|
end
|
|
901
966
|
|
|
967
|
+
# Load saved search definitions from YAML file
|
|
968
|
+
#
|
|
969
|
+
# @return [Hash] Hash of saved searches
|
|
902
970
|
def load_searches
|
|
903
971
|
file = database_path(file: 'saved_searches.yml')
|
|
904
972
|
if File.exist?(file)
|
|
905
|
-
searches = YAML.
|
|
973
|
+
searches = YAML.load(file.read_file)
|
|
906
974
|
else
|
|
907
975
|
searches = {
|
|
908
976
|
'soon' => 'tagged "due<in 2 days,due>yesterday"',
|
|
@@ -915,6 +983,10 @@ module NA
|
|
|
915
983
|
searches
|
|
916
984
|
end
|
|
917
985
|
|
|
986
|
+
# Delete saved search definitions by name
|
|
987
|
+
#
|
|
988
|
+
# @param strings [Array<String>, String, nil] Names of searches to delete
|
|
989
|
+
# @return [void]
|
|
918
990
|
def delete_search(strings = nil)
|
|
919
991
|
NA.notify("#{NA.theme[:error]}Name of search required", exit_code: 1) if strings.nil? || strings.empty?
|
|
920
992
|
|
|
@@ -923,7 +995,7 @@ module NA
|
|
|
923
995
|
|
|
924
996
|
strings = [strings] unless strings.is_a? Array
|
|
925
997
|
|
|
926
|
-
searches = YAML.
|
|
998
|
+
searches = YAML.load(file.read_file)
|
|
927
999
|
keys = searches.keys.delete_if { |k| k !~ /(#{strings.map(&:wildcard_to_rx).join('|')})/ }
|
|
928
1000
|
|
|
929
1001
|
NA.notify("#{NA.theme[:error]}No search named #{strings.join(', ')} found", exit_code: 1) if keys.empty?
|
|
@@ -937,9 +1009,14 @@ module NA
|
|
|
937
1009
|
|
|
938
1010
|
File.open(file, 'w') { |f| f.puts(YAML.dump(searches)) }
|
|
939
1011
|
|
|
940
|
-
NA.notify(
|
|
1012
|
+
NA.notify(
|
|
1013
|
+
"#{NA.theme[:warning]}Deleted {bw}#{keys.count}{x}#{NA.theme[:warning]} #{keys.count > 1 ? 'searches' : 'search'}", exit_code: 0
|
|
1014
|
+
)
|
|
941
1015
|
end
|
|
942
1016
|
|
|
1017
|
+
# Edit saved search definitions in the default editor
|
|
1018
|
+
#
|
|
1019
|
+
# @return [void]
|
|
943
1020
|
def edit_searches
|
|
944
1021
|
file = database_path(file: 'saved_searches.yml')
|
|
945
1022
|
searches = load_searches
|
|
@@ -953,23 +1030,22 @@ module NA
|
|
|
953
1030
|
NA.notify("#{NA.theme[:success]}Opened #{file} in #{editor}", exit_code: 0)
|
|
954
1031
|
end
|
|
955
1032
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
##
|
|
1033
|
+
# Create a backup file
|
|
1034
|
+
#
|
|
1035
|
+
# @param target [String] The file to back up
|
|
1036
|
+
# @return [void]
|
|
961
1037
|
def backup_file(target)
|
|
962
1038
|
FileUtils.cp(target, backup_path(target))
|
|
963
1039
|
save_modified_file(target)
|
|
964
1040
|
NA.notify("#{NA.theme[:warning]}Backup file created for #{target.highlight_filename}", debug: true)
|
|
965
1041
|
end
|
|
966
1042
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1043
|
+
#
|
|
1044
|
+
# Request terminal input from user, readline style
|
|
1045
|
+
#
|
|
1046
|
+
# @param options [Hash] The options
|
|
1047
|
+
# @param prompt [String] The prompt
|
|
1048
|
+
#
|
|
973
1049
|
def request_input(options, prompt: 'Enter text')
|
|
974
1050
|
if $stdin.isatty && TTY::Which.exist?('gum') && (options[:tagged].nil? || options[:tagged].empty?)
|
|
975
1051
|
opts = [%(--placeholder "#{prompt}"),
|
|
@@ -982,18 +1058,18 @@ module NA
|
|
|
982
1058
|
end
|
|
983
1059
|
end
|
|
984
1060
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1061
|
+
#
|
|
1062
|
+
# Generate a menu of options and allow user selection
|
|
1063
|
+
#
|
|
1064
|
+
# @return [String] The selected option
|
|
1065
|
+
#
|
|
1066
|
+
# @param options [Array] The options from which to choose
|
|
1067
|
+
# @param prompt [String] The prompt
|
|
1068
|
+
# @param multiple [Boolean] If true, allow multiple selections
|
|
1069
|
+
# @param sorted [Boolean] If true, sort selections alphanumerically
|
|
1070
|
+
# @param fzf_args [Array] Additional fzf arguments
|
|
1071
|
+
#
|
|
1072
|
+
# @return [String, Array] array if multiple is true
|
|
997
1073
|
def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
|
|
998
1074
|
return nil unless $stdout.isatty
|
|
999
1075
|
|
|
@@ -1021,12 +1097,14 @@ module NA
|
|
|
1021
1097
|
reader = TTY::Reader.new
|
|
1022
1098
|
puts
|
|
1023
1099
|
options.each.with_index do |f, i|
|
|
1024
|
-
puts NA::Color.template(format(
|
|
1100
|
+
puts NA::Color.template(format(
|
|
1101
|
+
"#{NA.theme[:prompt]}%<idx> 2d{xw}) #{NA.theme[:filename]}%<action>s{x}\n", idx: i + 1, action: f
|
|
1102
|
+
))
|
|
1025
1103
|
end
|
|
1026
1104
|
result = reader.read_line(NA::Color.template("#{NA.theme[:prompt]}#{prompt}{x}")).strip
|
|
1027
1105
|
if multiple
|
|
1028
1106
|
mult_res = []
|
|
1029
|
-
result = result.gsub(
|
|
1107
|
+
result = result.gsub(',', ' ').gsub(/ +/, ' ').split(/ /)
|
|
1030
1108
|
result.each do |r|
|
|
1031
1109
|
mult_res << options[r.to_i - 1] if r.to_i&.positive?
|
|
1032
1110
|
end
|
|
@@ -1036,19 +1114,20 @@ module NA
|
|
|
1036
1114
|
end
|
|
1037
1115
|
end
|
|
1038
1116
|
|
|
1039
|
-
return false if res&.strip&.
|
|
1117
|
+
return false if res&.strip&.empty?
|
|
1118
|
+
|
|
1040
1119
|
# pp NA::Color.uncolor(NA::Color.template(res))
|
|
1041
|
-
multiple ? NA::Color.uncolor(NA::Color.template(res)).split(
|
|
1120
|
+
multiple ? NA::Color.uncolor(NA::Color.template(res)).split("\n") : NA::Color.uncolor(NA::Color.template(res))
|
|
1042
1121
|
end
|
|
1043
1122
|
|
|
1044
1123
|
private
|
|
1045
1124
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1125
|
+
#
|
|
1126
|
+
# macOS open command
|
|
1127
|
+
#
|
|
1128
|
+
# @param file The file
|
|
1129
|
+
# @param app The application
|
|
1130
|
+
#
|
|
1052
1131
|
def darwin_open(file, app: nil)
|
|
1053
1132
|
if app
|
|
1054
1133
|
`open -a "#{app}" #{Shellwords.escape(file)}`
|
|
@@ -1057,20 +1136,20 @@ module NA
|
|
|
1057
1136
|
end
|
|
1058
1137
|
end
|
|
1059
1138
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1139
|
+
#
|
|
1140
|
+
# Windows open command
|
|
1141
|
+
#
|
|
1142
|
+
# @param file The file
|
|
1143
|
+
#
|
|
1065
1144
|
def win_open(file)
|
|
1066
1145
|
`start #{Shellwords.escape(file)}`
|
|
1067
1146
|
end
|
|
1068
1147
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1148
|
+
#
|
|
1149
|
+
# Linux open command
|
|
1150
|
+
#
|
|
1151
|
+
# @param file The file
|
|
1152
|
+
#
|
|
1074
1153
|
def linux_open(file)
|
|
1075
1154
|
if TTY::Which.exist?('xdg-open')
|
|
1076
1155
|
`xdg-open #{Shellwords.escape(file)}`
|