na 1.2.37 → 1.2.38

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.
@@ -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