na 1.2.37 → 1.2.39

Sign up to get free protection for your applications and to get access to all the features.
@@ -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