na 1.2.35 → 1.2.37
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 +28 -0
- data/Gemfile.lock +1 -1
- data/README.md +79 -2
- data/bin/commands/complete.rb +4 -0
- data/bin/commands/edit.rb +6 -5
- data/bin/commands/find.rb +27 -19
- data/bin/commands/next.rb +22 -18
- data/bin/commands/prompt.rb +2 -0
- data/bin/commands/tagged.rb +32 -25
- data/bin/commands/todos.rb +3 -3
- data/bin/commands/undo.rb +22 -0
- data/bin/commands/update.rb +12 -2
- data/lib/na/action.rb +27 -1
- data/lib/na/actions.rb +87 -0
- data/lib/na/editor.rb +123 -0
- data/lib/na/next_action.rb +203 -471
- data/lib/na/todo.rb +183 -0
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +3 -0
- data/src/_README.md +36 -12
- metadata +6 -2
data/lib/na/next_action.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
# Next Action methods
|
4
4
|
module NA
|
5
5
|
class << self
|
6
|
+
include NA::Editor
|
7
|
+
|
6
8
|
attr_accessor :verbose, :extension, :na_tag, :command_line, :command, :globals, :global_file, :cwd_is, :cwd, :stdin
|
7
9
|
|
8
10
|
##
|
@@ -16,7 +18,11 @@ module NA
|
|
16
18
|
def notify(msg, exit_code: false, debug: false)
|
17
19
|
return if debug && !NA.verbose
|
18
20
|
|
19
|
-
|
21
|
+
if debug
|
22
|
+
$stderr.puts NA::Color.template("{xdw}#{msg}{x}")
|
23
|
+
else
|
24
|
+
$stderr.puts NA::Color.template("{x}#{msg}{x}")
|
25
|
+
end
|
20
26
|
Process.exit exit_code if exit_code
|
21
27
|
end
|
22
28
|
|
@@ -45,125 +51,6 @@ module NA
|
|
45
51
|
res.empty? ? default : res =~ /y/i
|
46
52
|
end
|
47
53
|
|
48
|
-
def default_editor
|
49
|
-
editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
50
|
-
|
51
|
-
if editor.good? && TTY::Which.exist?(editor)
|
52
|
-
return editor
|
53
|
-
end
|
54
|
-
|
55
|
-
notify('No EDITOR environment variable, testing available editors', debug: true)
|
56
|
-
editors = %w[vim vi code subl mate mvim nano emacs]
|
57
|
-
editors.each do |ed|
|
58
|
-
try = TTY::Which.which(ed)
|
59
|
-
if try
|
60
|
-
notify("Using editor #{try}", debug: true)
|
61
|
-
return try
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
notify('{br}No editor found{x}', exit_code: 5)
|
66
|
-
|
67
|
-
nil
|
68
|
-
end
|
69
|
-
|
70
|
-
def editor_with_args
|
71
|
-
args_for_editor(default_editor)
|
72
|
-
end
|
73
|
-
|
74
|
-
def args_for_editor(editor)
|
75
|
-
return editor if editor =~ /-\S/
|
76
|
-
|
77
|
-
args = case editor
|
78
|
-
when /^(subl|code|mate)$/
|
79
|
-
['-w']
|
80
|
-
when /^(vim|mvim)$/
|
81
|
-
['-f']
|
82
|
-
else
|
83
|
-
[]
|
84
|
-
end
|
85
|
-
"#{editor} #{args.join(' ')}"
|
86
|
-
end
|
87
|
-
|
88
|
-
##
|
89
|
-
## Create a process for an editor and wait for the file handle to return
|
90
|
-
##
|
91
|
-
## @param input [String] Text input for editor
|
92
|
-
##
|
93
|
-
def fork_editor(input = '', message: :default)
|
94
|
-
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
95
|
-
|
96
|
-
notify('{br}No EDITOR variable defined in environment{x}', exit_code: 5) if default_editor.nil?
|
97
|
-
|
98
|
-
tmpfile = Tempfile.new(['na_temp', '.na'])
|
99
|
-
|
100
|
-
File.open(tmpfile.path, 'w+') do |f|
|
101
|
-
f.puts input
|
102
|
-
unless message.nil?
|
103
|
-
f.puts message == :default ? '# First line is the action, lines after are added as a note' : message
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
pid = Process.fork { system("#{editor_with_args} #{tmpfile.path}") }
|
108
|
-
|
109
|
-
trap('INT') do
|
110
|
-
begin
|
111
|
-
Process.kill(9, pid)
|
112
|
-
rescue StandardError
|
113
|
-
Errno::ESRCH
|
114
|
-
end
|
115
|
-
tmpfile.unlink
|
116
|
-
tmpfile.close!
|
117
|
-
exit 0
|
118
|
-
end
|
119
|
-
|
120
|
-
Process.wait(pid)
|
121
|
-
|
122
|
-
begin
|
123
|
-
if $?.exitstatus == 0
|
124
|
-
input = IO.read(tmpfile.path)
|
125
|
-
else
|
126
|
-
exit_now! 'Cancelled'
|
127
|
-
end
|
128
|
-
ensure
|
129
|
-
tmpfile.close
|
130
|
-
tmpfile.unlink
|
131
|
-
end
|
132
|
-
|
133
|
-
input.split(/\n/).delete_if(&:ignore?).join("\n")
|
134
|
-
end
|
135
|
-
|
136
|
-
##
|
137
|
-
## Takes a multi-line string and formats it as an entry
|
138
|
-
##
|
139
|
-
## @param input [String] The string to parse
|
140
|
-
##
|
141
|
-
## @return [Array] [[String]title, [Note]note]
|
142
|
-
##
|
143
|
-
def format_input(input)
|
144
|
-
notify('No content in entry', exit_code: 1) if input.nil? || input.strip.empty?
|
145
|
-
|
146
|
-
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
147
|
-
title = input_lines[0]&.strip
|
148
|
-
notify('{br}No content in first line{x}', exit_code: 1) if title.nil? || title.strip.empty?
|
149
|
-
|
150
|
-
title.expand_date_tags
|
151
|
-
|
152
|
-
note = if input_lines.length > 1
|
153
|
-
input_lines[1..-1]
|
154
|
-
else
|
155
|
-
[]
|
156
|
-
end
|
157
|
-
|
158
|
-
|
159
|
-
unless note.empty?
|
160
|
-
note.map!(&:strip)
|
161
|
-
note.delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
|
162
|
-
end
|
163
|
-
|
164
|
-
[title, note]
|
165
|
-
end
|
166
|
-
|
167
54
|
##
|
168
55
|
## Helper function to colorize the Y/N prompt
|
169
56
|
##
|
@@ -216,19 +103,6 @@ module NA
|
|
216
103
|
notify("{y}Created {bw}#{target}")
|
217
104
|
end
|
218
105
|
|
219
|
-
##
|
220
|
-
## Use the *nix `find` command to locate files matching NA.extension
|
221
|
-
##
|
222
|
-
## @param depth [Number] The depth at which to search
|
223
|
-
##
|
224
|
-
def find_files(depth: 1)
|
225
|
-
return [NA.global_file] if NA.global_file
|
226
|
-
|
227
|
-
files = `find . -maxdepth #{depth} -name "*.#{NA.extension}"`.strip.split("\n")
|
228
|
-
files.each { |f| save_working_dir(File.expand_path(f)) }
|
229
|
-
files
|
230
|
-
end
|
231
|
-
|
232
106
|
##
|
233
107
|
## Select from multiple files
|
234
108
|
##
|
@@ -257,37 +131,41 @@ module NA
|
|
257
131
|
end
|
258
132
|
|
259
133
|
def find_projects(target)
|
260
|
-
|
261
|
-
projects
|
134
|
+
todo = NA::Todo.new(require_na: false, file_path: target)
|
135
|
+
todo.projects
|
262
136
|
end
|
263
137
|
|
264
138
|
def find_actions(target, search, tagged = nil, all: false, done: false)
|
265
|
-
|
139
|
+
todo = NA::Todo.new({ search: search,
|
140
|
+
require_na: false,
|
141
|
+
file_path: target,
|
142
|
+
tag: tagged,
|
143
|
+
done: done })
|
266
144
|
|
267
|
-
unless actions.count.positive?
|
145
|
+
unless todo.actions.count.positive?
|
268
146
|
NA.notify("{r}No matching actions found in {bw}#{File.basename(target, ".#{NA.extension}")}")
|
269
147
|
return
|
270
148
|
end
|
271
149
|
|
272
|
-
return [projects, actions] if actions.count == 1 || all
|
150
|
+
return [todo.projects, todo.actions] if todo.actions.count == 1 || all
|
273
151
|
|
274
|
-
options = actions.map { |action| "#{action.line} % #{action.parent.join('/')} : #{action.action}" }
|
152
|
+
options = todo.actions.map { |action| "#{action.line} % #{action.parent.join('/')} : #{action.action}" }
|
275
153
|
res = choose_from(options, prompt: 'Make a selection: ', multiple: true, sorted: true)
|
276
154
|
|
277
155
|
NA.notify('{r}Cancelled', exit_code: 1) unless res && res.length.positive?
|
278
156
|
|
279
|
-
selected =
|
157
|
+
selected = NA::Actions.new
|
280
158
|
res.each do |result|
|
281
159
|
idx = result.match(/^(\d+)(?= % )/)[1]
|
282
|
-
action = actions.select { |a| a.line == idx.to_i }.first
|
160
|
+
action = todo.actions.select { |a| a.line == idx.to_i }.first
|
283
161
|
selected.push(action)
|
284
162
|
end
|
285
|
-
[projects, selected]
|
163
|
+
[todo.projects, selected]
|
286
164
|
end
|
287
165
|
|
288
166
|
def insert_project(target, project, projects)
|
289
167
|
path = project.split(%r{[:/]})
|
290
|
-
|
168
|
+
todo = NA::Todo.new(file_path: target)
|
291
169
|
built = []
|
292
170
|
last_match = nil
|
293
171
|
final_match = nil
|
@@ -295,7 +173,7 @@ module NA
|
|
295
173
|
matches = nil
|
296
174
|
path.each_with_index do |part, i|
|
297
175
|
built.push(part)
|
298
|
-
matches = projects.select { |proj| proj.project =~ /^#{built.join(':')}/i }
|
176
|
+
matches = todo.projects.select { |proj| proj.project =~ /^#{built.join(':')}/i }
|
299
177
|
if matches.count.zero?
|
300
178
|
final_match = last_match
|
301
179
|
new_path = path.slice(i, path.count - i)
|
@@ -315,12 +193,12 @@ module NA
|
|
315
193
|
end
|
316
194
|
|
317
195
|
if new_path.join('') =~ /Archive/i
|
318
|
-
line = projects.last.last_line
|
196
|
+
line = todo.projects.last.last_line
|
319
197
|
content = content.split(/\n/).insert(line, input.join("\n")).join("\n")
|
320
198
|
else
|
321
199
|
split = content.split(/\n/)
|
322
|
-
before = split.slice(0, projects.first.line).join("\n")
|
323
|
-
after = split.slice(projects.first.line, split.count - projects.first.line).join("\n")
|
200
|
+
before = split.slice(0, todo.projects.first.line).join("\n")
|
201
|
+
after = split.slice(todo.projects.first.line, split.count - todo.projects.first.line).join("\n")
|
324
202
|
content = "#{before}\n#{input.join("\n")}\n#{after}"
|
325
203
|
end
|
326
204
|
|
@@ -344,34 +222,6 @@ module NA
|
|
344
222
|
new_project
|
345
223
|
end
|
346
224
|
|
347
|
-
def process_action(action, priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
|
348
|
-
string = action.action
|
349
|
-
|
350
|
-
if priority&.positive?
|
351
|
-
string.gsub!(/(?<=\A| )@priority\(\d+\)/, '').strip!
|
352
|
-
string += " @priority(#{priority})"
|
353
|
-
end
|
354
|
-
|
355
|
-
add_tag.each do |tag|
|
356
|
-
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
357
|
-
string.strip!
|
358
|
-
string += " @#{tag}"
|
359
|
-
end
|
360
|
-
|
361
|
-
remove_tag.each do |tag|
|
362
|
-
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
363
|
-
string.strip!
|
364
|
-
end
|
365
|
-
|
366
|
-
string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
|
367
|
-
|
368
|
-
action.action = string
|
369
|
-
action.action.expand_date_tags
|
370
|
-
action.note = note unless note.empty?
|
371
|
-
|
372
|
-
action
|
373
|
-
end
|
374
|
-
|
375
225
|
def update_action(target,
|
376
226
|
search,
|
377
227
|
add: nil,
|
@@ -410,14 +260,14 @@ module NA
|
|
410
260
|
|
411
261
|
if add.is_a?(Action)
|
412
262
|
add_tag ||= []
|
413
|
-
|
263
|
+
add.process(priority: priority, finish: finish, add_tag: add_tag, remove_tag: remove_tag)
|
414
264
|
|
415
265
|
projects = find_projects(target)
|
416
266
|
|
417
267
|
target_proj = if target_proj
|
418
268
|
projects.select { |proj| proj.project =~ /^#{target_proj.project}$/ }.first
|
419
269
|
else
|
420
|
-
projects.select { |proj| proj.project =~ /^#{
|
270
|
+
projects.select { |proj| proj.project =~ /^#{add.parent.join(':')}$/ }.first
|
421
271
|
end
|
422
272
|
|
423
273
|
NA.notify("{r}Error parsing project #{target}", exit_code: 1) if target_proj.nil?
|
@@ -425,9 +275,9 @@ module NA
|
|
425
275
|
indent = "\t" * target_proj.indent
|
426
276
|
note = note.split("\n") unless note.is_a?(Array)
|
427
277
|
note = if note.empty?
|
428
|
-
|
278
|
+
add.note
|
429
279
|
else
|
430
|
-
overwrite ? note :
|
280
|
+
overwrite ? note : add.note.concat(note)
|
431
281
|
end
|
432
282
|
|
433
283
|
note = note.empty? ? '' : "\n#{indent}\t\t#{note.join("\n#{indent}\t\t").strip}"
|
@@ -449,9 +299,9 @@ module NA
|
|
449
299
|
target_line = target_proj.line + 1
|
450
300
|
end
|
451
301
|
|
452
|
-
contents.insert(target_line, "#{indent}\t- #{
|
302
|
+
contents.insert(target_line, "#{indent}\t- #{add.action}#{note}")
|
453
303
|
|
454
|
-
notify(
|
304
|
+
notify(add.pretty)
|
455
305
|
else
|
456
306
|
_, actions = find_actions(target, search, tagged, done: done, all: all)
|
457
307
|
|
@@ -464,12 +314,13 @@ module NA
|
|
464
314
|
projects = shift_index_after(projects, action.line, action.note.count + 1)
|
465
315
|
|
466
316
|
if edit
|
467
|
-
|
317
|
+
editor_content = "#{action.action}\n#{action.note.join("\n")}"
|
318
|
+
new_action, new_note = Editor.format_input(Editor.fork_editor(editor_content))
|
468
319
|
action.action = new_action
|
469
320
|
action.note = new_note
|
470
321
|
end
|
471
322
|
|
472
|
-
action
|
323
|
+
action.process(priority: priority, finish: finish, add_tag: add_tag, remove_tag: remove_tag)
|
473
324
|
|
474
325
|
target_proj = if target_proj
|
475
326
|
projects.select { |proj| proj.project =~ /^#{target_proj.project}$/ }.first
|
@@ -504,7 +355,6 @@ module NA
|
|
504
355
|
target_line = target_proj.line + 1
|
505
356
|
end
|
506
357
|
|
507
|
-
|
508
358
|
contents.insert(target_line, "#{indent}\t- #{action.action}#{note}")
|
509
359
|
|
510
360
|
notify(action.pretty)
|
@@ -560,6 +410,11 @@ module NA
|
|
560
410
|
parents
|
561
411
|
end
|
562
412
|
|
413
|
+
# Output an Omnifocus-friendly action list
|
414
|
+
#
|
415
|
+
# @param children The children
|
416
|
+
# @param level The indent level
|
417
|
+
#
|
563
418
|
def output_children(children, level = 1)
|
564
419
|
out = []
|
565
420
|
indent = "\t" * level
|
@@ -595,220 +450,190 @@ module NA
|
|
595
450
|
out
|
596
451
|
end
|
597
452
|
|
453
|
+
def edit_file(file: nil, app: nil)
|
454
|
+
os_open(file, app: app) if file && File.exist?(file)
|
455
|
+
end
|
456
|
+
|
598
457
|
##
|
599
|
-
##
|
458
|
+
## Use the *nix `find` command to locate files matching NA.extension
|
600
459
|
##
|
601
|
-
## @param
|
602
|
-
## @param depth [Number] The depth
|
603
|
-
## @param files [Array] The files actions originally came from
|
604
|
-
## @param regexes [Array] The regexes used to gather actions
|
460
|
+
## @param depth [Number] The depth at which to search
|
605
461
|
##
|
606
|
-
def
|
607
|
-
return if
|
608
|
-
|
609
|
-
if nest
|
610
|
-
template = '%parent%action'
|
611
|
-
|
612
|
-
parent_files = {}
|
613
|
-
out = []
|
614
|
-
|
615
|
-
if nest_projects
|
616
|
-
actions.each do |action|
|
617
|
-
if parent_files.key?(action.file)
|
618
|
-
parent_files[action.file].push(action)
|
619
|
-
else
|
620
|
-
parent_files[action.file] = [action]
|
621
|
-
end
|
622
|
-
end
|
623
|
-
|
624
|
-
parent_files.each do |file, acts|
|
625
|
-
projects = project_hierarchy(acts)
|
626
|
-
out.push("#{file.sub(%r{^./}, '').shorten_path}:")
|
627
|
-
out.concat(output_children(projects, 0))
|
628
|
-
end
|
629
|
-
else
|
630
|
-
template = '%parent%action'
|
631
|
-
|
632
|
-
actions.each do |action|
|
633
|
-
if parent_files.key?(action.file)
|
634
|
-
parent_files[action.file].push(action)
|
635
|
-
else
|
636
|
-
parent_files[action.file] = [action]
|
637
|
-
end
|
638
|
-
end
|
462
|
+
def find_files(depth: 1)
|
463
|
+
return [NA.global_file] if NA.global_file
|
639
464
|
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
out.push("\t- [#{a.parent.join('/')}] #{a.action}")
|
644
|
-
out.push("\t\t#{a.note.join("\n\t\t")}") unless a.note.empty?
|
645
|
-
end
|
646
|
-
end
|
647
|
-
end
|
648
|
-
NA::Pager.page out.join("\n")
|
649
|
-
else
|
650
|
-
template = if files.count.positive?
|
651
|
-
if files.count == 1
|
652
|
-
'%parent%action'
|
653
|
-
else
|
654
|
-
'%filename%parent%action'
|
655
|
-
end
|
656
|
-
elsif find_files(depth: depth).count > 1
|
657
|
-
if depth > 1
|
658
|
-
'%filename%parent%action'
|
659
|
-
else
|
660
|
-
'%project%parent%action'
|
661
|
-
end
|
662
|
-
else
|
663
|
-
'%parent%action'
|
664
|
-
end
|
665
|
-
template += '%note' if notes
|
666
|
-
|
667
|
-
files.map { |f| notify("{dw}#{f}", debug: true) } if files
|
668
|
-
|
669
|
-
output = actions.map { |action| action.pretty(template: { output: template }, regexes: regexes, notes: notes) }
|
670
|
-
NA::Pager.page(output.join("\n"))
|
671
|
-
end
|
465
|
+
files = `find . -maxdepth #{depth} -name "*.#{NA.extension}"`.strip.split("\n")
|
466
|
+
files.each { |f| save_working_dir(File.expand_path(f)) }
|
467
|
+
files
|
672
468
|
end
|
673
469
|
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
if negate
|
706
|
-
optional_tag.push(t) if t[:negate]
|
707
|
-
required_tag.push(t) if t[:required] && t[:negate]
|
708
|
-
negated_tag.push(t) unless t[:negate]
|
709
|
-
else
|
710
|
-
optional_tag.push(t) unless t[:negate]
|
711
|
-
required_tag.push(t) if t[:required] && !t[:negate]
|
712
|
-
negated_tag.push(t) if t[:negate]
|
713
|
-
end
|
714
|
-
end
|
715
|
-
end
|
716
|
-
|
717
|
-
unless search.nil? || search.empty?
|
718
|
-
if regex || search.is_a?(String)
|
719
|
-
if negate
|
720
|
-
negated.push(search)
|
721
|
-
else
|
722
|
-
optional.push(search)
|
723
|
-
required.push(search)
|
724
|
-
end
|
725
|
-
else
|
726
|
-
search.each do |t|
|
727
|
-
opt, req, neg = parse_search(t, negate)
|
728
|
-
optional.concat(opt)
|
729
|
-
required.concat(req)
|
730
|
-
negated.concat(neg)
|
731
|
-
end
|
732
|
-
end
|
470
|
+
def find_files_matching(options = {})
|
471
|
+
defaults = {
|
472
|
+
depth: 1,
|
473
|
+
done: false,
|
474
|
+
file_path: nil,
|
475
|
+
negate: false,
|
476
|
+
project: nil,
|
477
|
+
query: nil,
|
478
|
+
regex: false,
|
479
|
+
require_na: true,
|
480
|
+
search: nil,
|
481
|
+
tag: nil
|
482
|
+
}
|
483
|
+
opts = defaults.merge(options)
|
484
|
+
|
485
|
+
files = find_files(depth: options[:depth])
|
486
|
+
|
487
|
+
files.delete_if do |file|
|
488
|
+
todo = NA::Todo.new({
|
489
|
+
depth: options[:depth],
|
490
|
+
done: options[:done],
|
491
|
+
file_path: file,
|
492
|
+
negate: options[:negate],
|
493
|
+
project: options[:project],
|
494
|
+
query: options[:query],
|
495
|
+
regex: options[:regex],
|
496
|
+
require_na: options[:require_na],
|
497
|
+
search: options[:search],
|
498
|
+
tag: options[:tag]
|
499
|
+
})
|
500
|
+
todo.actions.length.zero?
|
733
501
|
end
|
734
502
|
|
735
|
-
files
|
736
|
-
|
737
|
-
elsif query.nil?
|
738
|
-
find_files(depth: depth)
|
739
|
-
else
|
740
|
-
match_working_dir(query)
|
741
|
-
end
|
742
|
-
|
743
|
-
files.each do |file|
|
744
|
-
save_working_dir(File.expand_path(file))
|
745
|
-
content = file.read_file
|
746
|
-
indent_level = 0
|
747
|
-
parent = []
|
748
|
-
in_action = false
|
749
|
-
content.split("\n").each.with_index do |line, idx|
|
750
|
-
if line.project?
|
751
|
-
in_action = false
|
752
|
-
proj = line.project
|
753
|
-
indent = line.indent_level
|
754
|
-
|
755
|
-
if indent.zero? # top level project
|
756
|
-
parent = [proj]
|
757
|
-
elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
|
758
|
-
parent.slice!(indent, parent.count - indent)
|
759
|
-
parent.push(proj)
|
760
|
-
else # if indent level is greater, append project to parent
|
761
|
-
parent.push(proj)
|
762
|
-
end
|
503
|
+
files
|
504
|
+
end
|
763
505
|
|
764
|
-
|
506
|
+
##
|
507
|
+
## Find a matching path using semi-fuzzy matching.
|
508
|
+
## Search tokens can include ! and + to negate or make
|
509
|
+
## required.
|
510
|
+
##
|
511
|
+
## @param search [Array] search tokens to
|
512
|
+
## match
|
513
|
+
## @param distance [Integer] allowed distance
|
514
|
+
## between characters
|
515
|
+
## @param require_last [Boolean] require regex to
|
516
|
+
## match last element of path
|
517
|
+
##
|
518
|
+
## @return [Array] array of matching directories/todo files
|
519
|
+
##
|
520
|
+
def match_working_dir(search, distance: 1, require_last: true)
|
521
|
+
file = database_path
|
522
|
+
NA.notify('{r}No na database found', exit_code: 1) unless File.exist?(file)
|
765
523
|
|
766
|
-
|
767
|
-
elsif line.blank?
|
768
|
-
in_action = false
|
769
|
-
elsif line.action?
|
770
|
-
in_action = false
|
524
|
+
dirs = file.read_file.split("\n")
|
771
525
|
|
772
|
-
|
773
|
-
|
526
|
+
optional = search.filter { |s| !s[:negate] }.map { |t| t[:token] }
|
527
|
+
required = search.filter { |s| s[:required] }.map { |t| t[:token] }
|
528
|
+
negated = search.filter { |s| s[:negate] }.map { |t| t[:token] }
|
774
529
|
|
775
|
-
|
530
|
+
optional.push('*') if optional.count.zero? && required.count.zero? && negated.count.positive?
|
531
|
+
if optional == negated
|
532
|
+
required = ['*']
|
533
|
+
optional = ['*']
|
534
|
+
end
|
776
535
|
|
777
|
-
|
536
|
+
NA.notify("{dw}Optional directory regex: {x}#{optional.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
|
537
|
+
NA.notify("{dw}Required directory regex: {x}#{required.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
|
538
|
+
NA.notify("{dw}Negated directory regex: {x}#{negated.map { |t| t.dir_to_rx(distance: distance, require_last: false) }}", debug: true)
|
778
539
|
|
779
|
-
|
540
|
+
if require_last
|
541
|
+
dirs.delete_if { |d| !d.sub(/\.#{NA.extension}$/, '').dir_matches(any: optional, all: required, none: negated) }
|
542
|
+
else
|
543
|
+
dirs.delete_if do |d|
|
544
|
+
!d.sub(/\.#{NA.extension}$/, '')
|
545
|
+
.dir_matches(any: optional, all: required, none: negated, distance: 2, require_last: false)
|
546
|
+
end
|
547
|
+
end
|
780
548
|
|
781
|
-
|
549
|
+
dirs = dirs.sort.uniq
|
550
|
+
if dirs.empty? && require_last
|
551
|
+
NA.notify('{y}No matches, loosening search', debug: true)
|
552
|
+
match_working_dir(search, distance: 2, require_last: false)
|
553
|
+
else
|
554
|
+
dirs
|
555
|
+
end
|
556
|
+
end
|
782
557
|
|
783
|
-
|
784
|
-
|
785
|
-
|
558
|
+
##
|
559
|
+
## Save a todo file path to the database
|
560
|
+
##
|
561
|
+
## @param todo_file The todo file path
|
562
|
+
##
|
563
|
+
def save_working_dir(todo_file)
|
564
|
+
file = database_path
|
565
|
+
content = File.exist?(file) ? file.read_file : ''
|
566
|
+
dirs = content.split(/\n/)
|
567
|
+
dirs.push(File.expand_path(todo_file))
|
568
|
+
dirs.sort!.uniq!
|
569
|
+
File.open(file, 'w') { |f| f.puts dirs.join("\n") }
|
570
|
+
end
|
786
571
|
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
572
|
+
##
|
573
|
+
## Save a backed-up file to the database
|
574
|
+
##
|
575
|
+
## @param file The file
|
576
|
+
##
|
577
|
+
def save_modified_file(file)
|
578
|
+
db = database_path(file: 'last_modified.txt')
|
579
|
+
file = File.expand_path(file)
|
580
|
+
files = IO.read(db).split(/\n/).map(&:strip)
|
581
|
+
files.delete(file)
|
582
|
+
files << file
|
583
|
+
File.open(db, 'w') { |f| f.puts(files.join("\n")) }
|
584
|
+
end
|
791
585
|
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
586
|
+
##
|
587
|
+
## Get the last modified file from the database
|
588
|
+
##
|
589
|
+
## @param search The search
|
590
|
+
##
|
591
|
+
def last_modified_file(search: nil)
|
592
|
+
db = database_path(file: 'last_modified.txt')
|
593
|
+
files = IO.read(db).split(/\n/).map(&:strip)
|
594
|
+
files.delete_if { |f| f !~ Regexp.new(search.dir_to_rx(require_last: true)) } if search
|
595
|
+
files.last
|
596
|
+
end
|
796
597
|
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
598
|
+
##
|
599
|
+
## Get last modified file and restore a backup
|
600
|
+
##
|
601
|
+
## @param search The search
|
602
|
+
##
|
603
|
+
def restore_last_modified_file(search: nil)
|
604
|
+
file = last_modified_file(search: search)
|
605
|
+
if file
|
606
|
+
restore_modified_file(file)
|
607
|
+
else
|
608
|
+
NA.notify('{br}No matching file found')
|
805
609
|
end
|
610
|
+
end
|
806
611
|
|
807
|
-
|
612
|
+
##
|
613
|
+
## Restore a file from backup
|
614
|
+
##
|
615
|
+
## @param file The file
|
616
|
+
##
|
617
|
+
def restore_modified_file(file)
|
618
|
+
bak_file = File.join(File.dirname(file), ".#{File.basename(file)}.bak")
|
619
|
+
if File.exist?(bak_file)
|
620
|
+
FileUtils.mv(bak_file, file)
|
621
|
+
NA.notify("{bg}Backup restored for #{file}")
|
622
|
+
else
|
623
|
+
NA.notify("{br}Backup file for #{file} not found")
|
624
|
+
end
|
808
625
|
end
|
809
626
|
|
810
|
-
|
811
|
-
|
627
|
+
##
|
628
|
+
## Get path to database of known todo files
|
629
|
+
##
|
630
|
+
## @return [String] File path
|
631
|
+
##
|
632
|
+
def database_path(file: 'tdlist.txt')
|
633
|
+
db_dir = File.expand_path('~/.local/share/na')
|
634
|
+
# Create directory if needed
|
635
|
+
FileUtils.mkdir_p(db_dir) unless File.directory?(db_dir)
|
636
|
+
File.join(db_dir, file)
|
812
637
|
end
|
813
638
|
|
814
639
|
##
|
@@ -848,7 +673,7 @@ module NA
|
|
848
673
|
elsif !file_path.nil?
|
849
674
|
[file_path]
|
850
675
|
elsif query.nil?
|
851
|
-
find_files(depth: depth)
|
676
|
+
NA.find_files(depth: depth)
|
852
677
|
else
|
853
678
|
match_working_dir(query)
|
854
679
|
end
|
@@ -879,7 +704,7 @@ module NA
|
|
879
704
|
end
|
880
705
|
|
881
706
|
dirs.map! do |dir|
|
882
|
-
"{
|
707
|
+
"{xdg}#{dir.sub(/^#{ENV['HOME']}/, '~').sub(%r{/([^/]+)\.#{NA.extension}$}, '/{xby}\1{x}')}"
|
883
708
|
end
|
884
709
|
|
885
710
|
puts NA::Color.template(dirs.join("\n"))
|
@@ -951,18 +776,6 @@ module NA
|
|
951
776
|
NA.notify("Opened #{file} in #{editor}", exit_code: 0)
|
952
777
|
end
|
953
778
|
|
954
|
-
##
|
955
|
-
## Get path to database of known todo files
|
956
|
-
##
|
957
|
-
## @return [String] File path
|
958
|
-
##
|
959
|
-
def database_path(file: 'tdlist.txt')
|
960
|
-
db_dir = File.expand_path('~/.local/share/na')
|
961
|
-
# Create directory if needed
|
962
|
-
FileUtils.mkdir_p(db_dir) unless File.directory?(db_dir)
|
963
|
-
File.join(db_dir, file)
|
964
|
-
end
|
965
|
-
|
966
779
|
##
|
967
780
|
## Create a backup file
|
968
781
|
##
|
@@ -972,58 +785,10 @@ module NA
|
|
972
785
|
file = ".#{File.basename(target)}.bak"
|
973
786
|
backup = File.join(File.dirname(target), file)
|
974
787
|
FileUtils.cp(target, backup)
|
788
|
+
save_modified_file(target)
|
975
789
|
NA.notify("{dw}Backup file created at #{backup}", debug: true)
|
976
790
|
end
|
977
791
|
|
978
|
-
##
|
979
|
-
## Find a matching path using semi-fuzzy matching.
|
980
|
-
## Search tokens can include ! and + to negate or make
|
981
|
-
## required.
|
982
|
-
##
|
983
|
-
## @param search [Array] search tokens to
|
984
|
-
## match
|
985
|
-
## @param distance [Integer] allowed distance
|
986
|
-
## between characters
|
987
|
-
## @param require_last [Boolean] require regex to
|
988
|
-
## match last element of path
|
989
|
-
##
|
990
|
-
## @return [Array] array of matching directories/todo files
|
991
|
-
##
|
992
|
-
def match_working_dir(search, distance: 1, require_last: true)
|
993
|
-
file = database_path
|
994
|
-
notify('{r}No na database found', exit_code: 1) unless File.exist?(file)
|
995
|
-
|
996
|
-
dirs = file.read_file.split("\n")
|
997
|
-
|
998
|
-
optional = search.filter { |s| !s[:negate] }.map { |t| t[:token] }
|
999
|
-
required = search.filter { |s| s[:required] }.map { |t| t[:token] }
|
1000
|
-
negated = search.filter { |s| s[:negate] }.map { |t| t[:token] }
|
1001
|
-
|
1002
|
-
optional.push('*') if optional.count.zero? && required.count.zero? && negated.count.positive?
|
1003
|
-
if optional == negated
|
1004
|
-
required = ['*']
|
1005
|
-
optional = ['*']
|
1006
|
-
end
|
1007
|
-
|
1008
|
-
NA.notify("{dw}Optional directory regex: {x}#{optional.map(&:dir_to_rx)}", debug: true)
|
1009
|
-
NA.notify("{dw}Required directory regex: {x}#{required.map(&:dir_to_rx)}", debug: true)
|
1010
|
-
NA.notify("{dw}Negated directory regex: {x}#{negated.map { |t| t.dir_to_rx(distance: 1, require_last: false) }}", debug: true)
|
1011
|
-
|
1012
|
-
if require_last
|
1013
|
-
dirs.delete_if { |d| !d.sub(/\.#{NA.extension}$/, '').dir_matches(any: optional, all: required, none: negated) }
|
1014
|
-
else
|
1015
|
-
dirs.delete_if { |d| !d.sub(/\.#{NA.extension}$/, '').dir_matches(any: optional, all: required, none: negated, distance: 2, require_last: false) }
|
1016
|
-
end
|
1017
|
-
|
1018
|
-
dirs = dirs.sort.uniq
|
1019
|
-
if dirs.empty? && require_last
|
1020
|
-
NA.notify("{y}No matches, loosening search", debug: true)
|
1021
|
-
match_working_dir(search, distance: 2, require_last: false)
|
1022
|
-
else
|
1023
|
-
dirs
|
1024
|
-
end
|
1025
|
-
end
|
1026
|
-
|
1027
792
|
private
|
1028
793
|
|
1029
794
|
##
|
@@ -1074,39 +839,6 @@ module NA
|
|
1074
839
|
multiple ? res.split(/\n/) : res
|
1075
840
|
end
|
1076
841
|
|
1077
|
-
def parse_search(tag, negate)
|
1078
|
-
required = []
|
1079
|
-
optional = []
|
1080
|
-
negated = []
|
1081
|
-
new_rx = tag[:token].to_s.wildcard_to_rx
|
1082
|
-
|
1083
|
-
if negate
|
1084
|
-
optional.push(new_rx) if tag[:negate]
|
1085
|
-
required.push(new_rx) if tag[:required] && tag[:negate]
|
1086
|
-
negated.push(new_rx) unless tag[:negate]
|
1087
|
-
else
|
1088
|
-
optional.push(new_rx) unless tag[:negate]
|
1089
|
-
required.push(new_rx) if tag[:required] && !tag[:negate]
|
1090
|
-
negated.push(new_rx) if tag[:negate]
|
1091
|
-
end
|
1092
|
-
|
1093
|
-
[optional, required, negated]
|
1094
|
-
end
|
1095
|
-
|
1096
|
-
##
|
1097
|
-
## Save a todo file path to the database
|
1098
|
-
##
|
1099
|
-
## @param todo_file The todo file path
|
1100
|
-
##
|
1101
|
-
def save_working_dir(todo_file)
|
1102
|
-
file = database_path
|
1103
|
-
content = File.exist?(file) ? file.read_file : ''
|
1104
|
-
dirs = content.split(/\n/)
|
1105
|
-
dirs.push(File.expand_path(todo_file))
|
1106
|
-
dirs.sort!.uniq!
|
1107
|
-
File.open(file, 'w') { |f| f.puts dirs.join("\n") }
|
1108
|
-
end
|
1109
|
-
|
1110
842
|
##
|
1111
843
|
## macOS open command
|
1112
844
|
##
|