na 1.2.35 → 1.2.38
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/Gemfile.lock +1 -1
- data/README.md +156 -6
- data/bin/commands/add.rb +11 -13
- data/bin/commands/complete.rb +4 -0
- data/bin/commands/edit.rb +21 -24
- data/bin/commands/find.rb +36 -23
- data/bin/commands/init.rb +1 -1
- data/bin/commands/next.rb +70 -37
- data/bin/commands/projects.rb +1 -1
- data/bin/commands/prompt.rb +2 -0
- data/bin/commands/saved.rb +3 -4
- data/bin/commands/tagged.rb +37 -30
- data/bin/commands/todos.rb +24 -14
- data/bin/commands/undo.rb +22 -0
- data/bin/commands/update.rb +28 -21
- data/bin/na +2 -1
- data/lib/na/action.rb +39 -22
- data/lib/na/actions.rb +87 -0
- data/lib/na/colors.rb +23 -1
- data/lib/na/editor.rb +125 -0
- data/lib/na/hash.rb +31 -0
- data/lib/na/next_action.rb +237 -498
- data/lib/na/pager.rb +1 -1
- data/lib/na/prompt.rb +6 -6
- data/lib/na/string.rb +23 -3
- data/lib/na/theme.rb +71 -0
- data/lib/na/todo.rb +183 -0
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +4 -0
- data/src/_README.md +45 -1
- metadata +7 -2
data/lib/na/action.rb
CHANGED
@@ -18,6 +18,32 @@ module NA
|
|
18
18
|
@note = note
|
19
19
|
end
|
20
20
|
|
21
|
+
def process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
|
22
|
+
string = @action.dup
|
23
|
+
|
24
|
+
if priority&.positive?
|
25
|
+
string.gsub!(/(?<=\A| )@priority\(\d+\)/, '').strip!
|
26
|
+
string += " @priority(#{priority})"
|
27
|
+
end
|
28
|
+
|
29
|
+
remove_tag.each do |tag|
|
30
|
+
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
31
|
+
string.strip!
|
32
|
+
end
|
33
|
+
|
34
|
+
add_tag.each do |tag|
|
35
|
+
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
36
|
+
string.strip!
|
37
|
+
string += " @#{tag}"
|
38
|
+
end
|
39
|
+
|
40
|
+
string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
|
41
|
+
|
42
|
+
@action = string
|
43
|
+
@action.expand_date_tags
|
44
|
+
@note = note unless note.empty?
|
45
|
+
end
|
46
|
+
|
21
47
|
def to_s
|
22
48
|
note = if @note.count.positive?
|
23
49
|
"\n#{@note.join("\n")}"
|
@@ -48,25 +74,14 @@ module NA
|
|
48
74
|
## @param notes [Boolean] Include notes
|
49
75
|
##
|
50
76
|
def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
parent: '{c}',
|
55
|
-
parent_divider: '{xw}/',
|
56
|
-
action: '{bg}',
|
57
|
-
project: '{xbk}',
|
58
|
-
tags: '{m}',
|
59
|
-
value_parens: '{m}',
|
60
|
-
values: '{y}',
|
61
|
-
output: '%filename%parents| %action',
|
62
|
-
note: '{dw}'
|
63
|
-
}
|
64
|
-
template = default_template.merge(template)
|
77
|
+
theme = NA::Theme.load_theme
|
78
|
+
template = theme.merge(template)
|
79
|
+
|
65
80
|
# Create the hierarchical parent string
|
66
81
|
parents = @parent.map do |par|
|
67
|
-
NA::Color.template("#{template[:parent]}#{par}")
|
82
|
+
NA::Color.template("{x}#{template[:parent]}#{par}")
|
68
83
|
end.join(NA::Color.template(template[:parent_divider]))
|
69
|
-
parents = "{
|
84
|
+
parents = "#{NA.theme[:bracket]}[#{NA.theme[:error]}#{parents}#{NA.theme[:bracket]}]{x} "
|
70
85
|
|
71
86
|
# Create the project string
|
72
87
|
project = NA::Color.template("#{template[:project]}#{@project}{x} ")
|
@@ -75,7 +90,7 @@ module NA
|
|
75
90
|
file = @file.sub(%r{^\./}, '').sub(/#{ENV['HOME']}/, '~')
|
76
91
|
file = file.sub(/\.#{extension}$/, '')
|
77
92
|
# colorize the basename
|
78
|
-
file = file.
|
93
|
+
file = file.highlight_filename
|
79
94
|
file_tpl = "#{template[:file]}#{file} {x}"
|
80
95
|
filename = NA::Color.template(file_tpl)
|
81
96
|
|
@@ -174,13 +189,13 @@ module NA
|
|
174
189
|
tag_date = Time.parse(tag_val)
|
175
190
|
date = Chronic.parse(val)
|
176
191
|
|
192
|
+
raise ArgumentError if date.nil?
|
193
|
+
|
177
194
|
unless val =~ /(\d:\d|a[mp]|now)/i
|
178
195
|
tag_date = Time.parse(tag_date.strftime('%Y-%m-%d 12:00'))
|
179
196
|
date = Time.parse(date.strftime('%Y-%m-%d 12:00'))
|
180
197
|
end
|
181
198
|
|
182
|
-
puts "Comparing #{tag_date} #{tag[:comp]} #{date}" if NA.verbose
|
183
|
-
|
184
199
|
case tag[:comp]
|
185
200
|
when /^>$/
|
186
201
|
tag_date > date
|
@@ -201,7 +216,7 @@ module NA
|
|
201
216
|
else
|
202
217
|
false
|
203
218
|
end
|
204
|
-
rescue
|
219
|
+
rescue ArgumentError
|
205
220
|
case tag[:comp]
|
206
221
|
when /^>$/
|
207
222
|
tag_val.to_f > val.to_f
|
@@ -213,12 +228,14 @@ module NA
|
|
213
228
|
tag_val.to_f >= val.to_f
|
214
229
|
when /^==?$/
|
215
230
|
tag_val =~ /^#{val.wildcard_to_rx}$/
|
231
|
+
when /^=~$/
|
232
|
+
tag_val =~ Regexp.new(val, Regexp::IGNORECASE)
|
216
233
|
when /^\$=$/
|
217
234
|
tag_val =~ /#{val.wildcard_to_rx}$/i
|
218
235
|
when /^\*=$/
|
219
|
-
tag_val =~
|
236
|
+
tag_val =~ /.*?#{val.wildcard_to_rx}.*?/i
|
220
237
|
when /^\^=$/
|
221
|
-
tag_val =~ /^#{val.wildcard_to_rx}/
|
238
|
+
tag_val =~ /^#{val.wildcard_to_rx}/i
|
222
239
|
else
|
223
240
|
false
|
224
241
|
end
|
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 = NA.theme[:templates][:default]
|
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 = NA.theme[:templates][:default]
|
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
|
+
NA.theme[:templates][:single_file]
|
66
|
+
else
|
67
|
+
NA.theme[:templates][:multi_file]
|
68
|
+
end
|
69
|
+
elsif NA.find_files(depth: depth).count > 1
|
70
|
+
if depth > 1
|
71
|
+
NA.theme[:templates][:multi_file]
|
72
|
+
else
|
73
|
+
NA.theme[:templates][:single_file]
|
74
|
+
end
|
75
|
+
else
|
76
|
+
NA.theme[:templates][:default]
|
77
|
+
end
|
78
|
+
template += '%note' if notes
|
79
|
+
|
80
|
+
files.map { |f| NA.notify(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/colors.rb
CHANGED
@@ -5,7 +5,7 @@ module NA
|
|
5
5
|
# Terminal output color functions.
|
6
6
|
module Color
|
7
7
|
# Regexp to match excape sequences
|
8
|
-
ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)
|
8
|
+
ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/
|
9
9
|
|
10
10
|
# All available color names. Available as methods and string extensions.
|
11
11
|
#
|
@@ -214,9 +214,15 @@ module NA
|
|
214
214
|
## m: magenta, r: red, b: bold, u: underline, i: italic,
|
215
215
|
## x: reset (remove background, color, emphasis)
|
216
216
|
##
|
217
|
+
## Also accepts {#RGB} and {#RRGGBB} strings. Put a b before
|
218
|
+
## the hash to make it a background color
|
219
|
+
##
|
217
220
|
## @example Convert a templated string
|
218
221
|
## Color.template('{Rwb}Warning:{x} {w}you look a little {g}ill{x}')
|
219
222
|
##
|
223
|
+
## @example Convert using RGB colors
|
224
|
+
## Color.template('{#f0a}This is an RGB color')
|
225
|
+
##
|
220
226
|
## @param input [String, Array] The template
|
221
227
|
## string. If this is an array, the
|
222
228
|
## elements will be joined with a
|
@@ -228,6 +234,11 @@ module NA
|
|
228
234
|
input = input.join(' ') if input.is_a? Array
|
229
235
|
return input.gsub(/(?<!\\)\{(\w+)\}/i, '') unless NA::Color.coloring?
|
230
236
|
|
237
|
+
input = input.gsub(/(?<!\\)\{((?:[fb]g?)?#[a-f0-9]{3,6})\}/i) do
|
238
|
+
hex = Regexp.last_match(1)
|
239
|
+
rgb(hex)
|
240
|
+
end
|
241
|
+
|
231
242
|
fmt = input.gsub(/%/, '%%')
|
232
243
|
fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
|
233
244
|
Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
|
@@ -301,6 +312,17 @@ module NA
|
|
301
312
|
is_bg = hex.match(/^bg?#/) ? true : false
|
302
313
|
hex_string = hex.sub(/^([fb]g?)?#/, '')
|
303
314
|
|
315
|
+
if hex_string.length == 3
|
316
|
+
parts = hex_string.match(/(?<r>.)(?<g>.)(?<b>.)/)
|
317
|
+
|
318
|
+
t = []
|
319
|
+
%w[r g b].each do |e|
|
320
|
+
t << parts[e]
|
321
|
+
t << parts[e]
|
322
|
+
end
|
323
|
+
hex_string = t.join('')
|
324
|
+
end
|
325
|
+
|
304
326
|
parts = hex_string.match(/(?<r>..)(?<g>..)(?<b>..)/)
|
305
327
|
t = []
|
306
328
|
%w[r g b].each do |e|
|
data/lib/na/editor.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
module NA
|
2
|
+
module Editor
|
3
|
+
class << self
|
4
|
+
def default_editor(prefer_git_editor: true)
|
5
|
+
if prefer_git_editor
|
6
|
+
editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
7
|
+
else
|
8
|
+
editor ||= ENV['NA_EDITOR'] || ENV['EDITOR'] || ENV['GIT_EDITOR']
|
9
|
+
end
|
10
|
+
|
11
|
+
return editor if editor&.good? && TTY::Which.exist?(editor)
|
12
|
+
|
13
|
+
NA.notify("No EDITOR environment variable, testing available editors", debug: true)
|
14
|
+
editors = %w[vim vi code subl mate mvim nano emacs]
|
15
|
+
editors.each do |ed|
|
16
|
+
try = TTY::Which.which(ed)
|
17
|
+
if try
|
18
|
+
NA.notify("Using editor #{try}", debug: true)
|
19
|
+
return try
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
NA.notify("#{NA.theme[:error]}No editor found", exit_code: 5)
|
24
|
+
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def editor_with_args
|
29
|
+
args_for_editor(default_editor)
|
30
|
+
end
|
31
|
+
|
32
|
+
def args_for_editor(editor)
|
33
|
+
return editor if editor =~ /-\S/
|
34
|
+
|
35
|
+
args = case editor
|
36
|
+
when /^(subl|code|mate)$/
|
37
|
+
['-w']
|
38
|
+
when /^(vim|mvim)$/
|
39
|
+
['-f']
|
40
|
+
else
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
"#{editor} #{args.join(' ')}"
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
## Create a process for an editor and wait for the file handle to return
|
48
|
+
##
|
49
|
+
## @param input [String] Text input for editor
|
50
|
+
##
|
51
|
+
def fork_editor(input = '', message: :default)
|
52
|
+
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
53
|
+
|
54
|
+
NA.notify("#{NA.theme[:error]}No EDITOR variable defined in environment", exit_code: 5) if default_editor.nil?
|
55
|
+
|
56
|
+
tmpfile = Tempfile.new(['na_temp', '.na'])
|
57
|
+
|
58
|
+
File.open(tmpfile.path, 'w+') do |f|
|
59
|
+
f.puts input
|
60
|
+
unless message.nil?
|
61
|
+
f.puts message == :default ? '# First line is the action, lines after are added as a note' : message
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
pid = Process.fork { system("#{editor_with_args} #{tmpfile.path}") }
|
66
|
+
|
67
|
+
trap('INT') do
|
68
|
+
begin
|
69
|
+
Process.kill(9, pid)
|
70
|
+
rescue StandardError
|
71
|
+
Errno::ESRCH
|
72
|
+
end
|
73
|
+
tmpfile.unlink
|
74
|
+
tmpfile.close!
|
75
|
+
exit 0
|
76
|
+
end
|
77
|
+
|
78
|
+
Process.wait(pid)
|
79
|
+
|
80
|
+
begin
|
81
|
+
if $?.exitstatus == 0
|
82
|
+
input = IO.read(tmpfile.path)
|
83
|
+
else
|
84
|
+
exit_now! 'Cancelled'
|
85
|
+
end
|
86
|
+
ensure
|
87
|
+
tmpfile.close
|
88
|
+
tmpfile.unlink
|
89
|
+
end
|
90
|
+
|
91
|
+
input.split(/\n/).delete_if(&:ignore?).join("\n")
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
## Takes a multi-line string and formats it as an entry
|
96
|
+
##
|
97
|
+
## @param input [String] The string to parse
|
98
|
+
##
|
99
|
+
## @return [Array] [[String]title, [Note]note]
|
100
|
+
##
|
101
|
+
def format_input(input)
|
102
|
+
NA.notify("#{NA.theme[:error]}No content in entry", exit_code: 1) if input.nil? || input.strip.empty?
|
103
|
+
|
104
|
+
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
105
|
+
title = input_lines[0]&.strip
|
106
|
+
NA.notify("#{NA.theme[:error]}No content in first line", exit_code: 1) if title.nil? || title.strip.empty?
|
107
|
+
|
108
|
+
title.expand_date_tags
|
109
|
+
|
110
|
+
note = if input_lines.length > 1
|
111
|
+
input_lines[1..-1]
|
112
|
+
else
|
113
|
+
[]
|
114
|
+
end
|
115
|
+
|
116
|
+
unless note.empty?
|
117
|
+
note.map!(&:strip)
|
118
|
+
note.delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
|
119
|
+
end
|
120
|
+
|
121
|
+
[title, note]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/na/hash.rb
CHANGED
@@ -4,4 +4,35 @@ class ::Hash
|
|
4
4
|
def symbolize_keys
|
5
5
|
each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
|
6
6
|
end
|
7
|
+
|
8
|
+
##
|
9
|
+
## Freeze all values in a hash
|
10
|
+
##
|
11
|
+
## @return Hash with all values frozen
|
12
|
+
##
|
13
|
+
def deep_freeze
|
14
|
+
chilled = {}
|
15
|
+
each do |k, v|
|
16
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_freeze : v.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
chilled.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def deep_freeze!
|
23
|
+
replace deep_thaw.deep_freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def deep_thaw
|
27
|
+
chilled = {}
|
28
|
+
each do |k, v|
|
29
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_thaw : v.dup
|
30
|
+
end
|
31
|
+
|
32
|
+
chilled.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def deep_thaw!
|
36
|
+
replace deep_thaw
|
37
|
+
end
|
7
38
|
end
|