na 1.2.37 → 1.2.38

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -111,8 +111,8 @@ class App
111
111
  ]
112
112
  `gum input #{opts.join(' ')}`.strip
113
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
114
+ NA.notify("#{NA.theme[:prompt]}Enter search string:")
115
+ reader.read_line(NA::Color.template("#{NA.theme[:filename]}> #{NA.theme[:action]}")).strip
116
116
  end
117
117
 
118
118
  if action
@@ -123,35 +123,34 @@ class App
123
123
  tokens = Regexp.new(action, Regexp::IGNORECASE)
124
124
  else
125
125
  tokens = []
126
- all_req = action !~ /[+!\-]/ && !options[:or]
126
+ all_req = action !~ /[+!-]/ && !options[:or]
127
127
 
128
128
  action.split(/ /).each do |arg|
129
129
  m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
130
130
  tokens.push({
131
131
  token: m['tok'],
132
132
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
133
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
133
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
134
134
  })
135
135
  end
136
136
  end
137
137
  end
138
138
 
139
139
  if (action.nil? || action.empty?) && options[:tagged].empty?
140
- puts 'Empty input, cancelled'
141
- Process.exit 1
140
+ NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
142
141
  end
143
142
 
144
- all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
143
+ all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
145
144
  tags = []
146
145
  options[:tagged].join(',').split(/ *, */).each do |arg|
147
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
146
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
148
147
 
149
148
  tags.push({
150
149
  tag: m['tag'].wildcard_to_rx,
151
150
  comp: m['op'],
152
151
  value: m['val'],
153
152
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
154
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
153
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
155
154
  })
156
155
  end
157
156
 
@@ -170,7 +169,7 @@ class App
170
169
  args << '--width $(tput cols)'
171
170
  `gum write #{args.join(' ')}`.strip.split("\n")
172
171
  else
173
- puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
172
+ NA.notify("#{NA.theme[:prompt]}Enter a note, {bw}CTRL-d#{NA.theme[:prompt]} to end editing:#{NA.theme[:action]}")
174
173
  reader.read_multiline
175
174
  end
176
175
  end
@@ -182,13 +181,11 @@ class App
182
181
  options[:project]
183
182
  elsif NA.cwd_is == :project
184
183
  NA.cwd
185
- else
186
- nil
187
184
  end
188
185
 
189
186
  if options[:file]
190
187
  file = File.expand_path(options[:file])
191
- NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
188
+ NA.notify("#{NA.theme[:error]}File not found", exit_code: 1) unless File.exist?(file)
192
189
 
193
190
  targets = [file]
194
191
  elsif options[:todo]
@@ -198,7 +195,7 @@ class App
198
195
  todo.push({
199
196
  token: m['tok'],
200
197
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
201
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
198
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
202
199
  })
203
200
  end
204
201
  dirs = NA.match_working_dir(todo)
@@ -207,25 +204,25 @@ class App
207
204
  targets = [dirs[0]]
208
205
  elsif dirs.count.positive?
209
206
  targets = NA.select_file(dirs, multiple: true)
210
- NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
207
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
211
208
  else
212
- NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
209
+ NA.notify("#{NA.theme[:error]}Todo not found", exit_code: 1) unless targets && targets.count.positive?
213
210
 
214
211
  end
215
212
  else
216
213
  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?
214
+ depth: options[:depth],
215
+ done: options[:done],
216
+ project: target_proj,
217
+ regex: options[:regex],
218
+ require_na: false,
219
+ search: tokens,
220
+ tag: tags
221
+ })
222
+ NA.notify("#{NA.theme[:error]}No todo file found", exit_code: 1) if files.count.zero?
226
223
 
227
224
  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?
225
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
229
226
 
230
227
  end
231
228
 
@@ -234,7 +231,7 @@ class App
234
231
  options[:project] = 'Archive'
235
232
  end
236
233
 
237
- NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
234
+ NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
238
235
 
239
236
  targets.each do |target|
240
237
  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/
@@ -74,25 +74,14 @@ module NA
74
74
  ## @param notes [Boolean] Include notes
75
75
  ##
76
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)
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
 
@@ -200,13 +189,13 @@ module NA
200
189
  tag_date = Time.parse(tag_val)
201
190
  date = Chronic.parse(val)
202
191
 
192
+ raise ArgumentError if date.nil?
193
+
203
194
  unless val =~ /(\d:\d|a[mp]|now)/i
204
195
  tag_date = Time.parse(tag_date.strftime('%Y-%m-%d 12:00'))
205
196
  date = Time.parse(date.strftime('%Y-%m-%d 12:00'))
206
197
  end
207
198
 
208
- # NA.notify("{dw}Comparing #{tag_date} #{tag[:comp]} #{date}{x}", debug: true)
209
-
210
199
  case tag[:comp]
211
200
  when /^>$/
212
201
  tag_date > date
@@ -227,7 +216,7 @@ module NA
227
216
  else
228
217
  false
229
218
  end
230
- rescue
219
+ rescue ArgumentError
231
220
  case tag[:comp]
232
221
  when /^>$/
233
222
  tag_val.to_f > val.to_f
@@ -239,12 +228,14 @@ module NA
239
228
  tag_val.to_f >= val.to_f
240
229
  when /^==?$/
241
230
  tag_val =~ /^#{val.wildcard_to_rx}$/
231
+ when /^=~$/
232
+ tag_val =~ Regexp.new(val, Regexp::IGNORECASE)
242
233
  when /^\$=$/
243
234
  tag_val =~ /#{val.wildcard_to_rx}$/i
244
235
  when /^\*=$/
245
- tag_val =~ /#{val.wildcard_to_rx}/i
236
+ tag_val =~ /.*?#{val.wildcard_to_rx}.*?/i
246
237
  when /^\^=$/
247
- tag_val =~ /^#{val.wildcard_to_rx}/
238
+ tag_val =~ /^#{val.wildcard_to_rx}/i
248
239
  else
249
240
  false
250
241
  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|
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