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/actions.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NA
|
4
|
+
# Actions controller
|
5
|
+
class Actions < Array
|
6
|
+
def initialize(actions = [])
|
7
|
+
super
|
8
|
+
concat(actions)
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
## Pretty print a list of actions
|
13
|
+
##
|
14
|
+
## @param actions [Array] The actions
|
15
|
+
## @param depth [Number] The depth
|
16
|
+
## @param files [Array] The files actions originally came from
|
17
|
+
## @param regexes [Array] The regexes used to gather actions
|
18
|
+
##
|
19
|
+
def output(depth, files: nil, regexes: [], notes: false, nest: false, nest_projects: false)
|
20
|
+
return if files.nil?
|
21
|
+
|
22
|
+
if nest
|
23
|
+
template = '%parent%action'
|
24
|
+
|
25
|
+
parent_files = {}
|
26
|
+
out = []
|
27
|
+
|
28
|
+
if nest_projects
|
29
|
+
each do |action|
|
30
|
+
if parent_files.key?(action.file)
|
31
|
+
parent_files[action.file].push(action)
|
32
|
+
else
|
33
|
+
parent_files[action.file] = [action]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
parent_files.each do |file, acts|
|
38
|
+
projects = NA.project_hierarchy(acts)
|
39
|
+
out.push("#{file.sub(%r{^./}, '').shorten_path}:")
|
40
|
+
out.concat(NA.output_children(projects, 0))
|
41
|
+
end
|
42
|
+
else
|
43
|
+
template = '%parent%action'
|
44
|
+
|
45
|
+
each do |action|
|
46
|
+
if parent_files.key?(action.file)
|
47
|
+
parent_files[action.file].push(action)
|
48
|
+
else
|
49
|
+
parent_files[action.file] = [action]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
parent_files.each do |k, v|
|
54
|
+
out.push("#{k.sub(%r{^\./}, '')}:")
|
55
|
+
v.each do |a|
|
56
|
+
out.push("\t- [#{a.parent.join('/')}] #{a.action}")
|
57
|
+
out.push("\t\t#{a.note.join("\n\t\t")}") unless a.note.empty?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
NA::Pager.page out.join("\n")
|
62
|
+
else
|
63
|
+
template = if files.count.positive?
|
64
|
+
if files.count == 1
|
65
|
+
'%parent%action'
|
66
|
+
else
|
67
|
+
'%filename%parent%action'
|
68
|
+
end
|
69
|
+
elsif NA.find_files(depth: depth).count > 1
|
70
|
+
if depth > 1
|
71
|
+
'%filename%parent%action'
|
72
|
+
else
|
73
|
+
'%project%parent%action'
|
74
|
+
end
|
75
|
+
else
|
76
|
+
'%parent%action'
|
77
|
+
end
|
78
|
+
template += '%note' if notes
|
79
|
+
|
80
|
+
files.map { |f| NA.notify("{dw}#{f}", debug: true) } if files
|
81
|
+
|
82
|
+
output = map { |action| action.pretty(template: { output: template }, regexes: regexes, notes: notes) }
|
83
|
+
NA::Pager.page(output.join("\n"))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/na/editor.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
module NA
|
2
|
+
module Editor
|
3
|
+
class << self
|
4
|
+
def default_editor
|
5
|
+
editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
6
|
+
|
7
|
+
if editor.good? && TTY::Which.exist?(editor)
|
8
|
+
return editor
|
9
|
+
end
|
10
|
+
|
11
|
+
notify('No EDITOR environment variable, testing available editors', debug: true)
|
12
|
+
editors = %w[vim vi code subl mate mvim nano emacs]
|
13
|
+
editors.each do |ed|
|
14
|
+
try = TTY::Which.which(ed)
|
15
|
+
if try
|
16
|
+
notify("Using editor #{try}", debug: true)
|
17
|
+
return try
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
notify('{br}No editor found{x}', exit_code: 5)
|
22
|
+
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def editor_with_args
|
27
|
+
args_for_editor(default_editor)
|
28
|
+
end
|
29
|
+
|
30
|
+
def args_for_editor(editor)
|
31
|
+
return editor if editor =~ /-\S/
|
32
|
+
|
33
|
+
args = case editor
|
34
|
+
when /^(subl|code|mate)$/
|
35
|
+
['-w']
|
36
|
+
when /^(vim|mvim)$/
|
37
|
+
['-f']
|
38
|
+
else
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
"#{editor} #{args.join(' ')}"
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
## Create a process for an editor and wait for the file handle to return
|
46
|
+
##
|
47
|
+
## @param input [String] Text input for editor
|
48
|
+
##
|
49
|
+
def fork_editor(input = '', message: :default)
|
50
|
+
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
51
|
+
|
52
|
+
notify('{br}No EDITOR variable defined in environment{x}', exit_code: 5) if default_editor.nil?
|
53
|
+
|
54
|
+
tmpfile = Tempfile.new(['na_temp', '.na'])
|
55
|
+
|
56
|
+
File.open(tmpfile.path, 'w+') do |f|
|
57
|
+
f.puts input
|
58
|
+
unless message.nil?
|
59
|
+
f.puts message == :default ? '# First line is the action, lines after are added as a note' : message
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
pid = Process.fork { system("#{editor_with_args} #{tmpfile.path}") }
|
64
|
+
|
65
|
+
trap('INT') do
|
66
|
+
begin
|
67
|
+
Process.kill(9, pid)
|
68
|
+
rescue StandardError
|
69
|
+
Errno::ESRCH
|
70
|
+
end
|
71
|
+
tmpfile.unlink
|
72
|
+
tmpfile.close!
|
73
|
+
exit 0
|
74
|
+
end
|
75
|
+
|
76
|
+
Process.wait(pid)
|
77
|
+
|
78
|
+
begin
|
79
|
+
if $?.exitstatus == 0
|
80
|
+
input = IO.read(tmpfile.path)
|
81
|
+
else
|
82
|
+
exit_now! 'Cancelled'
|
83
|
+
end
|
84
|
+
ensure
|
85
|
+
tmpfile.close
|
86
|
+
tmpfile.unlink
|
87
|
+
end
|
88
|
+
|
89
|
+
input.split(/\n/).delete_if(&:ignore?).join("\n")
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
## Takes a multi-line string and formats it as an entry
|
94
|
+
##
|
95
|
+
## @param input [String] The string to parse
|
96
|
+
##
|
97
|
+
## @return [Array] [[String]title, [Note]note]
|
98
|
+
##
|
99
|
+
def format_input(input)
|
100
|
+
notify('No content in entry', exit_code: 1) if input.nil? || input.strip.empty?
|
101
|
+
|
102
|
+
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
103
|
+
title = input_lines[0]&.strip
|
104
|
+
notify('{br}No content in first line{x}', exit_code: 1) if title.nil? || title.strip.empty?
|
105
|
+
|
106
|
+
title.expand_date_tags
|
107
|
+
|
108
|
+
note = if input_lines.length > 1
|
109
|
+
input_lines[1..-1]
|
110
|
+
else
|
111
|
+
[]
|
112
|
+
end
|
113
|
+
|
114
|
+
unless note.empty?
|
115
|
+
note.map!(&:strip)
|
116
|
+
note.delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
|
117
|
+
end
|
118
|
+
|
119
|
+
[title, note]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|