na 1.2.35 → 1.2.37
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
##
|