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.
@@ -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("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
29
- NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
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.each do |arg|
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
- NA.delete_search(arg) if options[:delete]
43
+ args.each do |arg|
44
+ searches = NA.load_searches
35
45
 
36
- keys = searches.keys.delete_if { |k| k !~ /#{arg}/ }
37
- NA.notify("{r}Search #{arg} not found", exit_code: 1) if keys.empty?
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
- key = keys[0]
40
- cmd = Shellwords.shellsplit(searches[key])
41
- run(cmd)
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
@@ -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 *=, ^=, and $= for text comparisons.
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>[+\-!])?(?<tag>[^ =<>$\^]+?) *(?:(?<op>[=<>]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
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('{br}No actions matched search', exit_code: 1) if tags.empty? && tokens.empty?
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],
@@ -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.action do |_global_options, _options, args|
12
- if args.count.positive?
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
- tokens = [{ token: '*', required: all_req, negate: false }]
16
- args.each do |arg|
17
- arg.split(/ *, */).each do |a|
18
- m = a.match(/^(?<req>[+!-])?(?<tok>.*?)$/)
19
- tokens.push({
20
- token: m['tok'],
21
- required: all_req || (!m['req'].nil? && m['req'] == '+'),
22
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
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
- NA.list_todos(query: tokens)
37
+ NA.list_todos(query: tokens)
38
+ end
29
39
  end
30
40
  end
31
41
  end
@@ -103,18 +103,9 @@ class App
103
103
 
104
104
  action = if args.count.positive?
105
105
  args.join(' ').strip
106
- elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
107
- opts = [
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 !~ /[+!\-]/ && !options[:or]
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
- puts 'Empty input, cancelled'
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(' ') !~ /[+!\-]/ && !options[:or]
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>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
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
- puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
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('{r}File not found', exit_code: 1) unless File.exist?(file)
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('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
198
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
211
199
  else
212
- NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
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
- depth: options[:depth],
218
- done: options[:done],
219
- project: target_proj,
220
- regex: options[:regex],
221
- require_na: false,
222
- search: tokens,
223
- tag: tags
224
- })
225
- NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
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('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
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('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
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
- add_tag.each do |tag|
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
- remove_tag.each do |tag|
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
- # Default colorization, can be overridden with full or partial template variable
78
- default_template = {
79
- file: '{xbk}',
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 = "{dc}[{x}#{parents}{dc}]{x} "
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.sub(/#{File.basename(@file, ".#{extension}")}$/, "{dw}#{File.basename(@file, ".#{extension}")}{x}")
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 =~ /#{val.wildcard_to_rx}/i
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 = '%parent%action'
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 = '%parent%action'
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
- '%parent%action'
65
+ NA.theme[:templates][:single_file]
66
66
  else
67
- '%filename%parent%action'
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
- '%filename%parent%action'
71
+ NA.theme[:templates][:multi_file]
72
72
  else
73
- '%project%parent%action'
73
+ NA.theme[:templates][:single_file]
74
74
  end
75
75
  else
76
- '%parent%action'
76
+ NA.theme[:templates][:default]
77
77
  end
78
78
  template += '%note' if notes
79
79
 
80
- files.map { |f| NA.notify("{dw}#{f}", debug: true) } if files
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)/.freeze
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-7]|[0-9])?m/.freeze
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
- editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
6
-
7
- if editor.good? && TTY::Which.exist?(editor)
8
- return editor
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
- notify('No EDITOR environment variable, testing available editors', debug: true)
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('{br}No editor found{x}', exit_code: 5)
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('{br}No EDITOR variable defined in environment{x}', exit_code: 5) if default_editor.nil?
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('No content in entry', exit_code: 1) if input.nil? || input.strip.empty?
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('{br}No content in first line{x}', exit_code: 1) if title.nil? || title.strip.empty?
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