na 1.2.37 → 1.2.39
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 +39 -0
- data/Gemfile.lock +1 -1
- data/README.md +82 -6
- data/bin/commands/add.rb +11 -16
- data/bin/commands/edit.rb +15 -27
- data/bin/commands/find.rb +16 -9
- data/bin/commands/init.rb +1 -1
- data/bin/commands/next.rb +65 -27
- data/bin/commands/projects.rb +1 -1
- data/bin/commands/saved.rb +22 -11
- data/bin/commands/tagged.rb +7 -7
- data/bin/commands/todos.rb +24 -14
- data/bin/commands/update.rb +24 -36
- data/bin/na +2 -1
- data/lib/na/action.rb +24 -26
- data/lib/na/actions.rb +8 -8
- data/lib/na/colors.rb +24 -2
- data/lib/na/editor.rb +13 -11
- data/lib/na/hash.rb +31 -0
- data/lib/na/next_action.rb +90 -49
- data/lib/na/pager.rb +1 -1
- data/lib/na/prompt.rb +6 -6
- data/lib/na/string.rb +53 -7
- data/lib/na/theme.rb +71 -0
- data/lib/na/todo.rb +2 -2
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/src/_README.md +35 -15
- metadata +3 -2
data/bin/commands/saved.rb
CHANGED
|
@@ -20,27 +20,38 @@ class App
|
|
|
20
20
|
c.desc 'Delete the specified search definition'
|
|
21
21
|
c.switch %i[d delete], negatable: false
|
|
22
22
|
|
|
23
|
+
c.desc 'Interactively select a saved search to run'
|
|
24
|
+
c.switch %i[s select], negatable: false
|
|
25
|
+
|
|
23
26
|
c.action do |_global_options, options, args|
|
|
24
27
|
NA.edit_searches if options[:edit]
|
|
25
28
|
|
|
26
|
-
if args.empty?
|
|
29
|
+
if args.empty? && !options[:select]
|
|
27
30
|
searches = NA.load_searches
|
|
28
|
-
NA.notify("{
|
|
29
|
-
NA.notify(searches.map { |k, v| "{
|
|
31
|
+
NA.notify("#{NA.theme[:success]}Saved searches stored in #{NA.database_path(file: 'saved_searches.yml').highlight_filename}")
|
|
32
|
+
NA.notify(searches.map { |k, v| "#{NA.theme[:filename]}#{k}: #{NA.theme[:values]}#{v}" }.join("\n"))
|
|
30
33
|
else
|
|
31
|
-
args.
|
|
34
|
+
NA.delete_search(args.join(',').split(/[ ,]/)) if options[:delete]
|
|
35
|
+
|
|
36
|
+
if options[:select]
|
|
32
37
|
searches = NA.load_searches
|
|
38
|
+
res = NA.choose_from(searches.map { |k, v| "#{NA.theme[:filename]}#{k} #{NA.theme[:value]}(#{v})" }, multiple: true)
|
|
39
|
+
NA.notify("#{NA.theme[:error]}Nothing selected", exit_code: 0) if res&.empty?
|
|
40
|
+
args = res.map { |r| r.match(/(\S+)(?= \()/)[1] }
|
|
41
|
+
end
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
args.each do |arg|
|
|
44
|
+
searches = NA.load_searches
|
|
35
45
|
|
|
36
|
-
keys = searches.keys.delete_if { |k| k !~ /#{arg}/ }
|
|
37
|
-
NA.notify("{
|
|
46
|
+
keys = searches.keys.delete_if { |k| k !~ /#{arg.wildcard_to_rx}/ }
|
|
47
|
+
NA.notify("#{NA.theme[:error]}Search #{arg} not found", exit_code: 1) if keys.empty?
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
keys.each do |key|
|
|
50
|
+
NA.notify("#{NA.theme[:prompt]}Saved search #{NA.theme[:filename]}#{key}#{NA.theme[:warning]}:")
|
|
51
|
+
cmd = Shellwords.shellsplit(searches[key])
|
|
52
|
+
run(cmd)
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
|
-
exit
|
|
44
55
|
end
|
|
45
56
|
end
|
|
46
57
|
end
|
data/bin/commands/tagged.rb
CHANGED
|
@@ -6,7 +6,7 @@ class App
|
|
|
6
6
|
long_desc 'Finds actions with tags matching the arguments. An action is shown if it
|
|
7
7
|
contains all of the tags listed. Add a + before a tag to make it required
|
|
8
8
|
and others optional. You can specify values using TAG=VALUE pairs.
|
|
9
|
-
Use <, >, and = for numeric comparisons, and *=, ^=,
|
|
9
|
+
Use <, >, and = for numeric comparisons, and *=, ^=, $=, or =~ (regex) for text comparisons.
|
|
10
10
|
Date comparisons use natural language (`na tagged "due<=today"`) and
|
|
11
11
|
are detected automatically.'
|
|
12
12
|
arg_name 'TAG[=VALUE]'
|
|
@@ -38,7 +38,7 @@ class App
|
|
|
38
38
|
|
|
39
39
|
c.desc 'Filter results using search terms'
|
|
40
40
|
c.arg_name 'QUERY'
|
|
41
|
-
c.flag %i[search], multiple: true
|
|
41
|
+
c.flag %i[search find grep], multiple: true
|
|
42
42
|
|
|
43
43
|
c.desc 'Search query is regular expression'
|
|
44
44
|
c.switch %i[regex], negatable: false
|
|
@@ -79,9 +79,9 @@ class App
|
|
|
79
79
|
|
|
80
80
|
tags = []
|
|
81
81
|
|
|
82
|
-
all_req = args.join(' ') !~ /[+!-]/ && !options[:or]
|
|
82
|
+
all_req = args.join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
|
|
83
83
|
args.join(',').split(/ *, */).each do |arg|
|
|
84
|
-
m = arg.match(/^(?<req>[
|
|
84
|
+
m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
|
|
85
85
|
next if m.nil?
|
|
86
86
|
|
|
87
87
|
tags.push({
|
|
@@ -106,7 +106,7 @@ class App
|
|
|
106
106
|
tokens = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
|
|
107
107
|
else
|
|
108
108
|
tokens = []
|
|
109
|
-
all_req = options[:search].join(' ') !~ /[+!-]/ && !options[:or]
|
|
109
|
+
all_req = options[:search].join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
|
|
110
110
|
|
|
111
111
|
options[:search].join(' ').split(/ /).each do |arg|
|
|
112
112
|
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
|
@@ -122,7 +122,7 @@ class App
|
|
|
122
122
|
todos = nil
|
|
123
123
|
if options[:in]
|
|
124
124
|
todos = []
|
|
125
|
-
all_req = options[:in] !~ /[+!-]/ && !options[:or]
|
|
125
|
+
all_req = options[:in] !~ /(?<=[, ])[+!-]/ && !options[:or]
|
|
126
126
|
options[:in].split(/ *, */).each do |a|
|
|
127
127
|
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
|
128
128
|
todos.push({
|
|
@@ -133,7 +133,7 @@ class App
|
|
|
133
133
|
end
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
-
NA.notify(
|
|
136
|
+
NA.notify("#{NA.theme[:error]}No actions matched search", exit_code: 1) if tags.empty? && tokens.empty?
|
|
137
137
|
|
|
138
138
|
todo = NA::Todo.new({ depth: depth,
|
|
139
139
|
done: options[:done],
|
data/bin/commands/todos.rb
CHANGED
|
@@ -8,24 +8,34 @@ class App
|
|
|
8
8
|
/, :, or a space, e.g. `na todos code/marked`'
|
|
9
9
|
arg_name 'QUERY', optional: true
|
|
10
10
|
command %i[todos] do |c|
|
|
11
|
-
c.
|
|
12
|
-
|
|
13
|
-
all_req = args.join(' ') !~ /[+!-]/
|
|
11
|
+
c.desc 'Open the todo database in an editor for manual modification'
|
|
12
|
+
c.switch %i[e edit]
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
c.action do |_global_options, options, args|
|
|
15
|
+
if options[:edit]
|
|
16
|
+
system("#{NA::Editor.default_editor(prefer_git_editor: false)} #{NA.database_path}")
|
|
17
|
+
editor = NA::Editor.default_editor(prefer_git_editor: false).highlight_filename
|
|
18
|
+
database = NA.database_path.highlight_filename
|
|
19
|
+
NA.notify("{b}#{NA.theme[:success]}Opened #{database}#{NA.theme[:success]} in #{editor}")
|
|
20
|
+
else
|
|
21
|
+
if args.count.positive?
|
|
22
|
+
all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
|
|
23
|
+
|
|
24
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
|
25
|
+
args.each do |arg|
|
|
26
|
+
arg.split(/ *, */).each do |a|
|
|
27
|
+
m = a.match(/^(?<req>[+!-])?(?<tok>.*?)$/)
|
|
28
|
+
tokens.push({
|
|
29
|
+
token: m['tok'],
|
|
30
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
|
31
|
+
negate: (!m['req'].nil? && m['req'] =~ /[!-]/) ? true : false
|
|
32
|
+
})
|
|
33
|
+
end
|
|
24
34
|
end
|
|
25
35
|
end
|
|
26
|
-
end
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
NA.list_todos(query: tokens)
|
|
38
|
+
end
|
|
29
39
|
end
|
|
30
40
|
end
|
|
31
41
|
end
|
data/bin/commands/update.rb
CHANGED
|
@@ -103,18 +103,9 @@ class App
|
|
|
103
103
|
|
|
104
104
|
action = if args.count.positive?
|
|
105
105
|
args.join(' ').strip
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
%(--placeholder "Enter a task to search for"),
|
|
109
|
-
'--char-limit=500',
|
|
110
|
-
"--width=#{TTY::Screen.columns}"
|
|
111
|
-
]
|
|
112
|
-
`gum input #{opts.join(' ')}`.strip
|
|
113
|
-
elsif $stdin.isatty && options[:tagged].empty?
|
|
114
|
-
puts NA::Color.template('{bm}Enter search string:{x}')
|
|
115
|
-
reader.read_line(NA::Color.template('{by}> {bw}')).strip
|
|
106
|
+
else
|
|
107
|
+
NA.request_input(options, prompt: 'Enter a task to search for')
|
|
116
108
|
end
|
|
117
|
-
|
|
118
109
|
if action
|
|
119
110
|
tokens = nil
|
|
120
111
|
if options[:exact]
|
|
@@ -123,35 +114,34 @@ class App
|
|
|
123
114
|
tokens = Regexp.new(action, Regexp::IGNORECASE)
|
|
124
115
|
else
|
|
125
116
|
tokens = []
|
|
126
|
-
all_req = action !~ /[
|
|
117
|
+
all_req = action !~ /[+!-]/ && !options[:or]
|
|
127
118
|
|
|
128
119
|
action.split(/ /).each do |arg|
|
|
129
120
|
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
|
130
121
|
tokens.push({
|
|
131
122
|
token: m['tok'],
|
|
132
123
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
|
133
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
|
124
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
|
134
125
|
})
|
|
135
126
|
end
|
|
136
127
|
end
|
|
137
128
|
end
|
|
138
129
|
|
|
139
130
|
if (action.nil? || action.empty?) && options[:tagged].empty?
|
|
140
|
-
|
|
141
|
-
Process.exit 1
|
|
131
|
+
NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
|
|
142
132
|
end
|
|
143
133
|
|
|
144
|
-
all_req = options[:tagged].join(' ') !~ /[
|
|
134
|
+
all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
|
|
145
135
|
tags = []
|
|
146
136
|
options[:tagged].join(',').split(/ *, */).each do |arg|
|
|
147
|
-
m = arg.match(/^(?<req>[
|
|
137
|
+
m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
|
|
148
138
|
|
|
149
139
|
tags.push({
|
|
150
140
|
tag: m['tag'].wildcard_to_rx,
|
|
151
141
|
comp: m['op'],
|
|
152
142
|
value: m['val'],
|
|
153
143
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
|
154
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
|
144
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
|
155
145
|
})
|
|
156
146
|
end
|
|
157
147
|
|
|
@@ -170,7 +160,7 @@ class App
|
|
|
170
160
|
args << '--width $(tput cols)'
|
|
171
161
|
`gum write #{args.join(' ')}`.strip.split("\n")
|
|
172
162
|
else
|
|
173
|
-
|
|
163
|
+
NA.notify("#{NA.theme[:prompt]}Enter a note, {bw}CTRL-d#{NA.theme[:prompt]} to end editing:#{NA.theme[:action]}")
|
|
174
164
|
reader.read_multiline
|
|
175
165
|
end
|
|
176
166
|
end
|
|
@@ -182,13 +172,11 @@ class App
|
|
|
182
172
|
options[:project]
|
|
183
173
|
elsif NA.cwd_is == :project
|
|
184
174
|
NA.cwd
|
|
185
|
-
else
|
|
186
|
-
nil
|
|
187
175
|
end
|
|
188
176
|
|
|
189
177
|
if options[:file]
|
|
190
178
|
file = File.expand_path(options[:file])
|
|
191
|
-
NA.notify(
|
|
179
|
+
NA.notify("#{NA.theme[:error]}File not found", exit_code: 1) unless File.exist?(file)
|
|
192
180
|
|
|
193
181
|
targets = [file]
|
|
194
182
|
elsif options[:todo]
|
|
@@ -198,7 +186,7 @@ class App
|
|
|
198
186
|
todo.push({
|
|
199
187
|
token: m['tok'],
|
|
200
188
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
|
201
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
|
189
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
|
202
190
|
})
|
|
203
191
|
end
|
|
204
192
|
dirs = NA.match_working_dir(todo)
|
|
@@ -207,25 +195,25 @@ class App
|
|
|
207
195
|
targets = [dirs[0]]
|
|
208
196
|
elsif dirs.count.positive?
|
|
209
197
|
targets = NA.select_file(dirs, multiple: true)
|
|
210
|
-
NA.notify(
|
|
198
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
|
|
211
199
|
else
|
|
212
|
-
NA.notify(
|
|
200
|
+
NA.notify("#{NA.theme[:error]}Todo not found", exit_code: 1) unless targets && targets.count.positive?
|
|
213
201
|
|
|
214
202
|
end
|
|
215
203
|
else
|
|
216
204
|
files = NA.find_files_matching({
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
NA.notify(
|
|
205
|
+
depth: options[:depth],
|
|
206
|
+
done: options[:done],
|
|
207
|
+
project: target_proj,
|
|
208
|
+
regex: options[:regex],
|
|
209
|
+
require_na: false,
|
|
210
|
+
search: tokens,
|
|
211
|
+
tag: tags
|
|
212
|
+
})
|
|
213
|
+
NA.notify("#{NA.theme[:error]}No todo file found", exit_code: 1) if files.count.zero?
|
|
226
214
|
|
|
227
215
|
targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
|
|
228
|
-
NA.notify(
|
|
216
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
|
|
229
217
|
|
|
230
218
|
end
|
|
231
219
|
|
|
@@ -234,7 +222,7 @@ class App
|
|
|
234
222
|
options[:project] = 'Archive'
|
|
235
223
|
end
|
|
236
224
|
|
|
237
|
-
NA.notify(
|
|
225
|
+
NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
|
238
226
|
|
|
239
227
|
targets.each do |target|
|
|
240
228
|
NA.update_action(target, tokens,
|
data/bin/na
CHANGED
|
@@ -37,6 +37,7 @@ class App
|
|
|
37
37
|
default_command :next
|
|
38
38
|
|
|
39
39
|
NA::Color.coloring = $stdin.isatty
|
|
40
|
+
NA::Pager.paginate = $stdin.isatty
|
|
40
41
|
|
|
41
42
|
desc 'Add a next action (deprecated, for backwards compatibility)'
|
|
42
43
|
switch %i[a add], negatable: false
|
|
@@ -84,7 +85,7 @@ class App
|
|
|
84
85
|
|
|
85
86
|
pre do |global, _command, _options, _args|
|
|
86
87
|
NA.verbose = global[:debug]
|
|
87
|
-
NA::Pager.paginate = global[:pager]
|
|
88
|
+
NA::Pager.paginate = global[:pager] && $stdout.isatty
|
|
88
89
|
NA::Color.coloring = global[:color]
|
|
89
90
|
NA.extension = global[:ext]
|
|
90
91
|
NA.na_tag = global[:na_tag]
|
data/lib/na/action.rb
CHANGED
|
@@ -26,15 +26,15 @@ module NA
|
|
|
26
26
|
string += " @priority(#{priority})"
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
remove_tag.each do |tag|
|
|
30
30
|
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
|
31
31
|
string.strip!
|
|
32
|
-
string += " @#{tag}"
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
add_tag.each do |tag|
|
|
36
35
|
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
|
37
36
|
string.strip!
|
|
37
|
+
string += " @#{tag}"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
|
|
@@ -73,26 +73,15 @@ module NA
|
|
|
73
73
|
## highlight (searches)
|
|
74
74
|
## @param notes [Boolean] Include notes
|
|
75
75
|
##
|
|
76
|
-
def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
parent: '{c}',
|
|
81
|
-
parent_divider: '{xw}/',
|
|
82
|
-
action: '{bg}',
|
|
83
|
-
project: '{xbk}',
|
|
84
|
-
tags: '{m}',
|
|
85
|
-
value_parens: '{m}',
|
|
86
|
-
values: '{y}',
|
|
87
|
-
output: '%filename%parents| %action',
|
|
88
|
-
note: '{dw}'
|
|
89
|
-
}
|
|
90
|
-
template = default_template.merge(template)
|
|
76
|
+
def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true)
|
|
77
|
+
theme = NA::Theme.load_theme
|
|
78
|
+
template = theme.merge(template)
|
|
79
|
+
|
|
91
80
|
# Create the hierarchical parent string
|
|
92
81
|
parents = @parent.map do |par|
|
|
93
|
-
NA::Color.template("#{template[:parent]}#{par}")
|
|
82
|
+
NA::Color.template("{x}#{template[:parent]}#{par}")
|
|
94
83
|
end.join(NA::Color.template(template[:parent_divider]))
|
|
95
|
-
parents = "{
|
|
84
|
+
parents = "#{NA.theme[:bracket]}[#{NA.theme[:error]}#{parents}#{NA.theme[:bracket]}]{x} "
|
|
96
85
|
|
|
97
86
|
# Create the project string
|
|
98
87
|
project = NA::Color.template("#{template[:project]}#{@project}{x} ")
|
|
@@ -101,7 +90,7 @@ module NA
|
|
|
101
90
|
file = @file.sub(%r{^\./}, '').sub(/#{ENV['HOME']}/, '~')
|
|
102
91
|
file = file.sub(/\.#{extension}$/, '')
|
|
103
92
|
# colorize the basename
|
|
104
|
-
file = file.
|
|
93
|
+
file = file.highlight_filename
|
|
105
94
|
file_tpl = "#{template[:file]}#{file} {x}"
|
|
106
95
|
filename = NA::Color.template(file_tpl)
|
|
107
96
|
|
|
@@ -119,6 +108,13 @@ module NA
|
|
|
119
108
|
value: template[:values],
|
|
120
109
|
last_color: template[:action])
|
|
121
110
|
|
|
111
|
+
if detect_width
|
|
112
|
+
width = TTY::Screen.columns
|
|
113
|
+
prefix = NA::Color.uncolor(pretty(template: { output: template[:output].sub(/%action/, '') }, detect_width: false))
|
|
114
|
+
indent = prefix.length
|
|
115
|
+
action = action.wrap(width, indent)
|
|
116
|
+
end
|
|
117
|
+
|
|
122
118
|
# Replace variables in template string and output colorized
|
|
123
119
|
NA::Color.template(template[:output].gsub(/%filename/, filename)
|
|
124
120
|
.gsub(/%project/, project)
|
|
@@ -200,13 +196,13 @@ module NA
|
|
|
200
196
|
tag_date = Time.parse(tag_val)
|
|
201
197
|
date = Chronic.parse(val)
|
|
202
198
|
|
|
199
|
+
raise ArgumentError if date.nil?
|
|
200
|
+
|
|
203
201
|
unless val =~ /(\d:\d|a[mp]|now)/i
|
|
204
202
|
tag_date = Time.parse(tag_date.strftime('%Y-%m-%d 12:00'))
|
|
205
203
|
date = Time.parse(date.strftime('%Y-%m-%d 12:00'))
|
|
206
204
|
end
|
|
207
205
|
|
|
208
|
-
# NA.notify("{dw}Comparing #{tag_date} #{tag[:comp]} #{date}{x}", debug: true)
|
|
209
|
-
|
|
210
206
|
case tag[:comp]
|
|
211
207
|
when /^>$/
|
|
212
208
|
tag_date > date
|
|
@@ -227,7 +223,7 @@ module NA
|
|
|
227
223
|
else
|
|
228
224
|
false
|
|
229
225
|
end
|
|
230
|
-
rescue
|
|
226
|
+
rescue ArgumentError
|
|
231
227
|
case tag[:comp]
|
|
232
228
|
when /^>$/
|
|
233
229
|
tag_val.to_f > val.to_f
|
|
@@ -239,12 +235,14 @@ module NA
|
|
|
239
235
|
tag_val.to_f >= val.to_f
|
|
240
236
|
when /^==?$/
|
|
241
237
|
tag_val =~ /^#{val.wildcard_to_rx}$/
|
|
238
|
+
when /^=~$/
|
|
239
|
+
tag_val =~ Regexp.new(val, Regexp::IGNORECASE)
|
|
242
240
|
when /^\$=$/
|
|
243
241
|
tag_val =~ /#{val.wildcard_to_rx}$/i
|
|
244
242
|
when /^\*=$/
|
|
245
|
-
tag_val =~
|
|
243
|
+
tag_val =~ /.*?#{val.wildcard_to_rx}.*?/i
|
|
246
244
|
when /^\^=$/
|
|
247
|
-
tag_val =~ /^#{val.wildcard_to_rx}/
|
|
245
|
+
tag_val =~ /^#{val.wildcard_to_rx}/i
|
|
248
246
|
else
|
|
249
247
|
false
|
|
250
248
|
end
|
data/lib/na/actions.rb
CHANGED
|
@@ -20,7 +20,7 @@ module NA
|
|
|
20
20
|
return if files.nil?
|
|
21
21
|
|
|
22
22
|
if nest
|
|
23
|
-
template =
|
|
23
|
+
template = NA.theme[:templates][:default]
|
|
24
24
|
|
|
25
25
|
parent_files = {}
|
|
26
26
|
out = []
|
|
@@ -40,7 +40,7 @@ module NA
|
|
|
40
40
|
out.concat(NA.output_children(projects, 0))
|
|
41
41
|
end
|
|
42
42
|
else
|
|
43
|
-
template =
|
|
43
|
+
template = NA.theme[:templates][:default]
|
|
44
44
|
|
|
45
45
|
each do |action|
|
|
46
46
|
if parent_files.key?(action.file)
|
|
@@ -62,22 +62,22 @@ module NA
|
|
|
62
62
|
else
|
|
63
63
|
template = if files.count.positive?
|
|
64
64
|
if files.count == 1
|
|
65
|
-
|
|
65
|
+
NA.theme[:templates][:single_file]
|
|
66
66
|
else
|
|
67
|
-
|
|
67
|
+
NA.theme[:templates][:multi_file]
|
|
68
68
|
end
|
|
69
69
|
elsif NA.find_files(depth: depth).count > 1
|
|
70
70
|
if depth > 1
|
|
71
|
-
|
|
71
|
+
NA.theme[:templates][:multi_file]
|
|
72
72
|
else
|
|
73
|
-
|
|
73
|
+
NA.theme[:templates][:single_file]
|
|
74
74
|
end
|
|
75
75
|
else
|
|
76
|
-
|
|
76
|
+
NA.theme[:templates][:default]
|
|
77
77
|
end
|
|
78
78
|
template += '%note' if notes
|
|
79
79
|
|
|
80
|
-
files.map { |f| NA.notify(
|
|
80
|
+
files.map { |f| NA.notify(f, debug: true) } if files
|
|
81
81
|
|
|
82
82
|
output = map { |action| action.pretty(template: { output: template }, regexes: regexes, notes: notes) }
|
|
83
83
|
NA::Pager.page(output.join("\n"))
|
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|
|
|
@@ -312,7 +334,7 @@ module NA
|
|
|
312
334
|
|
|
313
335
|
# Regular expression that is used to scan for ANSI-sequences while
|
|
314
336
|
# uncoloring strings.
|
|
315
|
-
COLORED_REGEXP = /\e\[(?:(?:[349]|10)[0-
|
|
337
|
+
COLORED_REGEXP = /\e\[(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+m/
|
|
316
338
|
|
|
317
339
|
# Returns an uncolored version of the string, that is all
|
|
318
340
|
# ANSI-sequences are stripped from the string.
|
data/lib/na/editor.rb
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
module NA
|
|
2
2
|
module Editor
|
|
3
3
|
class << self
|
|
4
|
-
def default_editor
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
return editor if editor&.good? && TTY::Which.exist?(editor)
|
|
12
|
+
|
|
13
|
+
NA.notify("No EDITOR environment variable, testing available editors", debug: true)
|
|
12
14
|
editors = %w[vim vi code subl mate mvim nano emacs]
|
|
13
15
|
editors.each do |ed|
|
|
14
16
|
try = TTY::Which.which(ed)
|
|
15
17
|
if try
|
|
16
|
-
notify("Using editor #{try}", debug: true)
|
|
18
|
+
NA.notify("Using editor #{try}", debug: true)
|
|
17
19
|
return try
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
notify(
|
|
23
|
+
NA.notify("#{NA.theme[:error]}No editor found", exit_code: 5)
|
|
22
24
|
|
|
23
25
|
nil
|
|
24
26
|
end
|
|
@@ -49,7 +51,7 @@ module NA
|
|
|
49
51
|
def fork_editor(input = '', message: :default)
|
|
50
52
|
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
|
51
53
|
|
|
52
|
-
notify(
|
|
54
|
+
NA.notify("#{NA.theme[:error]}No EDITOR variable defined in environment", exit_code: 5) if default_editor.nil?
|
|
53
55
|
|
|
54
56
|
tmpfile = Tempfile.new(['na_temp', '.na'])
|
|
55
57
|
|
|
@@ -97,11 +99,11 @@ module NA
|
|
|
97
99
|
## @return [Array] [[String]title, [Note]note]
|
|
98
100
|
##
|
|
99
101
|
def format_input(input)
|
|
100
|
-
notify(
|
|
102
|
+
NA.notify("#{NA.theme[:error]}No content in entry", exit_code: 1) if input.nil? || input.strip.empty?
|
|
101
103
|
|
|
102
104
|
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
|
103
105
|
title = input_lines[0]&.strip
|
|
104
|
-
notify(
|
|
106
|
+
NA.notify("#{NA.theme[:error]}No content in first line", exit_code: 1) if title.nil? || title.strip.empty?
|
|
105
107
|
|
|
106
108
|
title.expand_date_tags
|
|
107
109
|
|
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
|