na 1.2.37 → 1.2.39

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,6 +7,10 @@ module NA
7
7
 
8
8
  attr_accessor :verbose, :extension, :na_tag, :command_line, :command, :globals, :global_file, :cwd_is, :cwd, :stdin
9
9
 
10
+ def theme
11
+ @theme ||= NA::Theme.load_theme
12
+ end
13
+
10
14
  ##
11
15
  ## Output to STDERR
12
16
  ##
@@ -19,7 +23,7 @@ module NA
19
23
  return if debug && !NA.verbose
20
24
 
21
25
  if debug
22
- $stderr.puts NA::Color.template("{xdw}#{msg}{x}")
26
+ $stderr.puts NA::Color.template("{x}#{NA.theme[:debug]}#{msg}{x}")
23
27
  else
24
28
  $stderr.puts NA::Color.template("{x}#{msg}{x}")
25
29
  end
@@ -100,7 +104,7 @@ module NA
100
104
  f.puts(content)
101
105
  end
102
106
  save_working_dir(target)
103
- notify("{y}Created {bw}#{target}")
107
+ notify("#{NA.theme[:warning]}Created #{NA.theme[:file]}#{target}")
104
108
  end
105
109
 
106
110
  ##
@@ -116,7 +120,7 @@ module NA
116
120
  def select_file(files, multiple: false)
117
121
  res = choose_from(files, prompt: multiple ? 'Select files' : 'Select a file', multiple: multiple)
118
122
 
119
- notify('{r}No file selected, cancelled', exit_code: 1) unless res && res.length.positive?
123
+ notify("#{NA.theme[:error]}No file selected, cancelled", exit_code: 1) unless res && res.length.positive?
120
124
 
121
125
  res
122
126
  end
@@ -143,7 +147,7 @@ module NA
143
147
  done: done })
144
148
 
145
149
  unless todo.actions.count.positive?
146
- NA.notify("{r}No matching actions found in {bw}#{File.basename(target, ".#{NA.extension}")}")
150
+ NA.notify("#{NA.theme[:error]}No matching actions found in #{File.basename(target, ".#{NA.extension}").highlight_filename}")
147
151
  return
148
152
  end
149
153
 
@@ -152,7 +156,7 @@ module NA
152
156
  options = todo.actions.map { |action| "#{action.line} % #{action.parent.join('/')} : #{action.action}" }
153
157
  res = choose_from(options, prompt: 'Make a selection: ', multiple: true, sorted: true)
154
158
 
155
- NA.notify('{r}Cancelled', exit_code: 1) unless res && res.length.positive?
159
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless res && res.length.positive?
156
160
 
157
161
  selected = NA::Actions.new
158
162
  res.each do |result|
@@ -193,12 +197,13 @@ module NA
193
197
  end
194
198
 
195
199
  if new_path.join('') =~ /Archive/i
196
- line = todo.projects.last.last_line
200
+ line = todo.projects.last&.last_line || 0
197
201
  content = content.split(/\n/).insert(line, input.join("\n")).join("\n")
198
202
  else
199
203
  split = content.split(/\n/)
200
- before = split.slice(0, todo.projects.first.line).join("\n")
201
- after = split.slice(todo.projects.first.line, split.count - todo.projects.first.line).join("\n")
204
+ line = todo.projects.first&.line || 0
205
+ before = split.slice(0, line).join("\n")
206
+ after = split.slice(line, split.count - 0).join("\n")
202
207
  content = "#{before}\n#{input.join("\n")}\n#{after}"
203
208
  end
204
209
 
@@ -247,11 +252,11 @@ module NA
247
252
  project = project.sub(/:$/, '')
248
253
  target_proj = projects.select { |pr| pr.project =~ /#{project.gsub(/:/, '.*?:.*?')}/i }.first
249
254
  if target_proj.nil?
250
- res = NA.yn(NA::Color.template("{y}Project {bw}#{project}{xy} doesn't exist, add it"), default: true)
255
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{project}#{NA.theme[:warning]} doesn't exist, add it"), default: true)
251
256
  if res
252
257
  target_proj = insert_project(target, project, projects)
253
258
  else
254
- NA.notify('{x}Cancelled', exit_code: 1)
259
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
255
260
  end
256
261
  end
257
262
  end
@@ -270,7 +275,7 @@ module NA
270
275
  projects.select { |proj| proj.project =~ /^#{add.parent.join(':')}$/ }.first
271
276
  end
272
277
 
273
- NA.notify("{r}Error parsing project #{target}", exit_code: 1) if target_proj.nil?
278
+ NA.notify("#{NA.theme[:error]}Error parsing project #{NA.theme[:filename]}#{target}", exit_code: 1) if target_proj.nil?
274
279
 
275
280
  indent = "\t" * target_proj.indent
276
281
  note = note.split("\n") unless note.is_a?(Array)
@@ -364,7 +369,11 @@ module NA
364
369
  backup_file(target)
365
370
  File.open(target, 'w') { |f| f.puts contents.join("\n") }
366
371
 
367
- add ? notify("{by}Task added to {bw}#{target}") : notify("{by}Task updated in {bw}#{target}")
372
+ if add
373
+ notify("#{NA.theme[:success]}Task added to #{NA.theme[:filename]}#{target}")
374
+ else
375
+ notify("#{NA.theme[:success]}Task updated in #{NA.theme[:filename]}#{target}")
376
+ end
368
377
  end
369
378
 
370
379
  ##
@@ -385,7 +394,6 @@ module NA
385
394
  else
386
395
  project = NA.cwd
387
396
  end
388
- puts [add_tag, project]
389
397
  end
390
398
 
391
399
  action = Action.new(file, project, parent, action, nil, note)
@@ -519,12 +527,12 @@ module NA
519
527
  ##
520
528
  def match_working_dir(search, distance: 1, require_last: true)
521
529
  file = database_path
522
- NA.notify('{r}No na database found', exit_code: 1) unless File.exist?(file)
530
+ NA.notify("#{NA.theme[:error]}No na database found", exit_code: 1) unless File.exist?(file)
523
531
 
524
532
  dirs = file.read_file.split("\n")
525
533
 
526
534
  optional = search.filter { |s| !s[:negate] }.map { |t| t[:token] }
527
- required = search.filter { |s| s[:required] }.map { |t| t[:token] }
535
+ required = search.filter { |s| s[:required] && !s[:negate] }.map { |t| t[:token] }
528
536
  negated = search.filter { |s| s[:negate] }.map { |t| t[:token] }
529
537
 
530
538
  optional.push('*') if optional.count.zero? && required.count.zero? && negated.count.positive?
@@ -533,9 +541,9 @@ module NA
533
541
  optional = ['*']
534
542
  end
535
543
 
536
- NA.notify("{dw}Optional directory regex: {x}#{optional.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
537
- NA.notify("{dw}Required directory regex: {x}#{required.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
538
- NA.notify("{dw}Negated directory regex: {x}#{negated.map { |t| t.dir_to_rx(distance: distance, require_last: false) }}", debug: true)
544
+ NA.notify("Optional directory regex: {x}#{optional.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
545
+ NA.notify("Required directory regex: {x}#{required.map { |t| t.dir_to_rx(distance: distance) }}", debug: true)
546
+ NA.notify("Negated directory regex: {x}#{negated.map { |t| t.dir_to_rx(distance: distance, require_last: false) }}", debug: true)
539
547
 
540
548
  if require_last
541
549
  dirs.delete_if { |d| !d.sub(/\.#{NA.extension}$/, '').dir_matches(any: optional, all: required, none: negated) }
@@ -546,9 +554,9 @@ module NA
546
554
  end
547
555
  end
548
556
 
549
- dirs = dirs.sort.uniq
557
+ dirs = dirs.sort_by { |d| File.basename(d) }.uniq
550
558
  if dirs.empty? && require_last
551
- NA.notify('{y}No matches, loosening search', debug: true)
559
+ NA.notify("#{NA.theme[:warning]}No matches, loosening search", debug: true)
552
560
  match_working_dir(search, distance: 2, require_last: false)
553
561
  else
554
562
  dirs
@@ -605,7 +613,7 @@ module NA
605
613
  if file
606
614
  restore_modified_file(file)
607
615
  else
608
- NA.notify('{br}No matching file found')
616
+ NA.notify("#{NA.theme[:error]}No matching file found")
609
617
  end
610
618
  end
611
619
 
@@ -618,9 +626,9 @@ module NA
618
626
  bak_file = File.join(File.dirname(file), ".#{File.basename(file)}.bak")
619
627
  if File.exist?(bak_file)
620
628
  FileUtils.mv(bak_file, file)
621
- NA.notify("{bg}Backup restored for #{file}")
629
+ NA.notify("#{NA.theme[:success]}Backup restored for #{file.highlight_filename}")
622
630
  else
623
- NA.notify("{br}Backup file for #{file} not found")
631
+ NA.notify("#{NA.theme[:error]}Backup file for #{file.highlight_filename} not found")
624
632
  end
625
633
  end
626
634
 
@@ -698,13 +706,13 @@ module NA
698
706
  else
699
707
  file = database_path
700
708
  content = File.exist?(file) ? file.read_file.strip : ''
701
- notify('{br}Database empty', exit_code: 1) if content.empty?
709
+ notify("#{NA.theme[:error]}Database empty", exit_code: 1) if content.empty?
702
710
 
703
711
  content.split(/\n/)
704
712
  end
705
713
 
706
714
  dirs.map! do |dir|
707
- "{xdg}#{dir.sub(/^#{ENV['HOME']}/, '~').sub(%r{/([^/]+)\.#{NA.extension}$}, '/{xby}\1{x}')}"
715
+ dir.highlight_filename
708
716
  end
709
717
 
710
718
  puts NA::Color.template(dirs.join("\n"))
@@ -717,13 +725,13 @@ module NA
717
725
 
718
726
  if searches.key?(title)
719
727
  res = yn('Overwrite existing definition?', default: true)
720
- notify('{r}Cancelled', exit_code: 0) unless res
728
+ notify("#{NA.theme[:error]}Cancelled", exit_code: 0) unless res
721
729
 
722
730
  end
723
731
 
724
732
  searches[title] = search
725
733
  File.open(file, 'w') { |f| f.puts(YAML.dump(searches)) }
726
- NA.notify("{y}Search #{title} saved", exit_code: 0)
734
+ NA.notify("#{NA.theme[:success]}Search #{NA.theme[:filename]}#{title}#{NA.theme[:success]} saved", exit_code: 0)
727
735
  end
728
736
 
729
737
  def load_searches
@@ -743,37 +751,41 @@ module NA
743
751
  end
744
752
 
745
753
  def delete_search(strings = nil)
746
- NA.notify('{r}Name search required', exit_code: 1) if strings.nil? || strings.empty?
754
+ NA.notify("#{NA.theme[:error]}Name of search required", exit_code: 1) if strings.nil? || strings.empty?
747
755
 
748
756
  file = database_path(file: 'saved_searches.yml')
749
- NA.notify('{r}No search definitions file found', exit_code: 1) unless File.exist?(file)
757
+ NA.notify("#{NA.theme[:error]}No search definitions file found", exit_code: 1) unless File.exist?(file)
758
+
759
+ strings = [strings] unless strings.is_a? Array
750
760
 
751
761
  searches = YAML.safe_load(file.read_file)
752
- keys = searches.keys.delete_if { |k| k !~ /(#{strings.join('|')})/ }
762
+ keys = searches.keys.delete_if { |k| k !~ /(#{strings.map(&:wildcard_to_rx).join('|')})/ }
753
763
 
754
- res = yn(NA::Color.template(%({y}Remove #{keys.count > 1 ? 'searches' : 'search'} {bw}"#{keys.join(', ')}"{x})),
764
+ NA.notify("#{NA.theme[:error]}No search named #{strings.join(', ')} found", exit_code: 1) if keys.empty?
765
+
766
+ res = yn(NA::Color.template(%(#{NA.theme[:warning]}Remove #{keys.count > 1 ? 'searches' : 'search'} #{NA.theme[:filename]}"#{keys.join(', ')}"{x})),
755
767
  default: false)
756
768
 
757
- NA.notify('{r}Cancelled', exit_code: 1) unless res
769
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless res
758
770
 
759
771
  searches.delete_if { |k| keys.include?(k) }
760
772
 
761
773
  File.open(file, 'w') { |f| f.puts(YAML.dump(searches)) }
762
774
 
763
- NA.notify("{y}Deleted {bw}#{keys.count}{xy} #{keys.count > 1 ? 'searches' : 'search'}", exit_code: 0)
775
+ NA.notify("#{NA.theme[:warning]}Deleted {bw}#{keys.count}{x}#{NA.theme[:warning]} #{keys.count > 1 ? 'searches' : 'search'}", exit_code: 0)
764
776
  end
765
777
 
766
778
  def edit_searches
767
779
  file = database_path(file: 'saved_searches.yml')
768
780
  searches = load_searches
769
781
 
770
- NA.notify('{r}No search definitions found', exit_code: 1) unless searches.count.positive?
782
+ NA.notify("#{NA.theme[:error]}No search definitions found", exit_code: 1) unless searches.count.positive?
771
783
 
772
- editor = ENV['EDITOR']
773
- NA.notify('{r}No $EDITOR defined', exit_code: 1) unless editor && TTY::Which.exist?(editor)
784
+ editor = NA.default_editor
785
+ NA.notify("#{NA.theme[:error]}No $EDITOR defined", exit_code: 1) unless editor && TTY::Which.exist?(editor)
774
786
 
775
787
  system %(#{editor} "#{file}")
776
- NA.notify("Opened #{file} in #{editor}", exit_code: 0)
788
+ NA.notify("#{NA.theme[:success]}Opened #{file} in #{editor}", exit_code: 0)
777
789
  end
778
790
 
779
791
  ##
@@ -786,10 +798,26 @@ module NA
786
798
  backup = File.join(File.dirname(target), file)
787
799
  FileUtils.cp(target, backup)
788
800
  save_modified_file(target)
789
- NA.notify("{dw}Backup file created at #{backup}", debug: true)
801
+ NA.notify("#{NA.theme[:warning]}Backup file created at #{backup.highlight_filename}", debug: true)
790
802
  end
791
803
 
792
- private
804
+ ##
805
+ ## Request terminal input from user, readline style
806
+ ##
807
+ ## @param options [Hash] The options
808
+ ## @param prompt [String] The prompt
809
+ ##
810
+ def request_input(options, prompt: 'Enter text')
811
+ if $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
812
+ opts = [%(--placeholder "#{prompt}"),
813
+ '--char-limit=500',
814
+ "--width=#{TTY::Screen.columns}"]
815
+ `gum input #{opts.join(' ')}`.strip
816
+ elsif $stdin.isatty && options[:tagged].empty?
817
+ NA.notify("#{NA.theme[:prompt]}#{prompt}:")
818
+ reader.read_line(NA::Color.template("#{NA.theme[:filename]}> #{NA.theme[:action]}")).strip
819
+ end
820
+ end
793
821
 
794
822
  ##
795
823
  ## Generate a menu of options and allow user selection
@@ -815,30 +843,43 @@ module NA
815
843
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
816
844
  default_args << %(--header="#{header}")
817
845
  default_args.concat(fzf_args)
818
- `echo #{Shellwords.escape(options.join("\n"))}|#{TTY::Which.which('fzf')} #{default_args.join(' ')}`.strip
846
+ options = NA::Color.uncolor(NA::Color.template(options.join("\n")))
847
+ `echo #{Shellwords.escape(options)}|#{TTY::Which.which('fzf')} #{default_args.join(' ')}`.strip
819
848
  elsif TTY::Which.exist?('gum')
820
849
  args = [
821
850
  '--cursor.foreground="151"',
822
851
  '--item.foreground=""'
823
852
  ]
824
853
  args.push '--no-limit' if multiple
825
- puts NS::Color.template("{bw}#{prompt}{x}")
826
- `echo #{Shellwords.escape(options.join("\n"))}|#{TTY::Which.which('gum')} choose #{args.join(' ')}`.strip
854
+ puts NA::Color.template("#{NA.theme[:prompt]}#{prompt}{x}")
855
+ options = NA::Color.uncolor(NA::Color.template(options.join("\n")))
856
+ `echo #{Shellwords.escape(options)}|#{TTY::Which.which('gum')} choose #{args.join(' ')}`.strip
827
857
  else
828
858
  reader = TTY::Reader.new
829
859
  puts
830
860
  options.each.with_index do |f, i|
831
- puts NA::Color.template(format("{bw}%<idx> 2d{xw}) {y}%<action>s{x}\n", idx: i + 1, action: f))
861
+ puts NA::Color.template(format("#{NA.theme[:prompt]}%<idx> 2d{xw}) #{NA.theme[:filename]}%<action>s{x}\n", idx: i + 1, action: f))
862
+ end
863
+ result = reader.read_line(NA::Color.template("#{NA.theme[:prompt]}#{prompt}{x}")).strip
864
+ if multiple
865
+ mult_res = []
866
+ result = result.gsub(/,/, ' ').gsub(/ +/, ' ').split(/ /)
867
+ result.each do |r|
868
+ mult_res << options[r.to_i - 1] if r.to_i&.positive?
869
+ end
870
+ mult_res.join("\n")
871
+ else
872
+ result.to_i&.positive? ? options[result.to_i - 1] : nil
832
873
  end
833
- result = reader.read_line(NA::Color.template("{bw}#{prompt}{x}")).strip
834
- result.to_i&.positive? ? options[result.to_i - 1] : nil
835
874
  end
836
875
 
837
- return false if res.strip.size.zero?
838
-
839
- multiple ? res.split(/\n/) : res
876
+ return false if res&.strip&.size&.zero?
877
+ pp NA::Color.uncolor(NA::Color.template(res))
878
+ multiple ? NA::Color.uncolor(NA::Color.template(res)).split(/\n/) : NA::Color.uncolor(NA::Color.template(res))
840
879
  end
841
880
 
881
+ private
882
+
842
883
  ##
843
884
  ## macOS open command
844
885
  ##
@@ -871,7 +912,7 @@ module NA
871
912
  if TTY::Which.exist?('xdg-open')
872
913
  `xdg-open #{Shellwords.escape(file)}`
873
914
  else
874
- notify('{r}Unable to determine executable for `xdg-open`.')
915
+ notify("#{NA.theme[:error]}Unable to determine executable for `xdg-open`.")
875
916
  end
876
917
  end
877
918
  end
data/lib/na/pager.rb CHANGED
@@ -44,7 +44,7 @@ module NA
44
44
  IO.select [input]
45
45
 
46
46
  begin
47
- NA.notify("{dw}Pager #{pager}", debug: true)
47
+ NA.notify("#{NA.theme[:debug]}Pager #{pager}", debug: true)
48
48
  exec(pager)
49
49
  rescue SystemCallError => e
50
50
  raise Errors::DoingStandardError, "Pager error, #{e}"
data/lib/na/prompt.rb CHANGED
@@ -14,7 +14,7 @@ module NA
14
14
  when :tag
15
15
  'na tagged $(basename "$PWD")'
16
16
  else
17
- NA.notify('When using a global file, a prompt hook requires `--cwd_as [tag|project]`', exit_code: 1)
17
+ NA.notify("#{NA.theme[:error]}When using a global file, a prompt hook requires `--cwd_as [tag|project]`", exit_code: 1)
18
18
  end
19
19
  else
20
20
  'na next'
@@ -31,7 +31,7 @@ module NA
31
31
  when :tag
32
32
  'na tagged (basename "$PWD")'
33
33
  else
34
- NA.notify('When using a global file, a prompt hook requires `--cwd_as [tag|project]`', exit_code: 1)
34
+ NA.notify("#{NA.theme[:error]}When using a global file, a prompt hook requires `--cwd_as [tag|project]`", exit_code: 1)
35
35
  end
36
36
  else
37
37
  'na next'
@@ -50,7 +50,7 @@ module NA
50
50
  when :tag
51
51
  'na tagged $(basename "$PWD")'
52
52
  else
53
- NA.notify('When using a global file, a prompt hook requires `--cwd_as [tag|project]`', exit_code: 1)
53
+ NA.notify("#{NA.theme[:error]}When using a global file, a prompt hook requires `--cwd_as [tag|project]`", exit_code: 1)
54
54
  end
55
55
  else
56
56
  'na next'
@@ -83,7 +83,7 @@ module NA
83
83
  def show_prompt_hook(shell)
84
84
  file = prompt_file(shell)
85
85
 
86
- NA.notify("{bw}# Add this to {y}#{file}{x}")
86
+ NA.notify("#{NA.theme[:warning]}# Add this to #{NA.theme[:filename]}#{file}")
87
87
  puts prompt_hook(shell)
88
88
  end
89
89
 
@@ -91,8 +91,8 @@ module NA
91
91
  file = prompt_file(shell)
92
92
 
93
93
  File.open(File.expand_path(file), 'a') { |f| f.puts prompt_hook(shell) }
94
- NA.notify("{y}Added {bw}#{shell}{xy} prompt hook to {bw}#{file}{xy}.{x}")
95
- NA.notify("{y}You may need to close the current terminal and open a new one to enable the script.{x}")
94
+ NA.notify("#{NA.theme[:success]}Added #{NA.theme[:filename]}#{shell}{x}#{NA.theme[:success]} prompt hook to #{NA.theme[:filename]}#{file}#{NA.theme[:success]}.")
95
+ NA.notify("#{NA.theme[:warning]}You may need to close the current terminal and open a new one to enable the script.")
96
96
  end
97
97
  end
98
98
  end
data/lib/na/string.rb CHANGED
@@ -6,6 +6,15 @@ REGEX_TIME = /^#{REGEX_CLOCK}$/i.freeze
6
6
 
7
7
  # String helpers
8
8
  class ::String
9
+ ##
10
+ ## Insert a comment character at the start of every line
11
+ ##
12
+ ## @param char [String] The character to insert (default #)
13
+ ##
14
+ def comment(char = "#")
15
+ split(/\n/).map { |l| "# #{l}" }.join("\n")
16
+ end
17
+
9
18
  ##
10
19
  ## Tests if object is nil or empty
11
20
  ##
@@ -75,6 +84,17 @@ class ::String
75
84
  self =~ /@#{NA.na_tag}\b/
76
85
  end
77
86
 
87
+ ##
88
+ ## Colorize the dirname and filename of a path
89
+ ##
90
+ ## @return Colorized string
91
+ ##
92
+ def highlight_filename
93
+ dir = File.dirname(self).shorten_path
94
+ file = File.basename(self, ".#{NA.extension}")
95
+ "#{NA.theme[:dirname]}#{dir}/#{NA.theme[:filename]}#{file}{x}"
96
+ end
97
+
78
98
  ##
79
99
  ## Colorize @tags with ANSI escapes
80
100
  ##
@@ -88,12 +108,18 @@ class ::String
88
108
  ##
89
109
  ## @return [String] string with @tags highlighted
90
110
  ##
91
- def highlight_tags(color: '{m}', value: '{y}', parens: '{m}', last_color: '{xg}')
111
+ def highlight_tags(color: NA.theme[:tags], value: NA.theme[:value], parens: NA.theme[:value_parens], last_color: NA.theme[:action])
92
112
  tag_color = NA::Color.template(color)
93
113
  paren_color = NA::Color.template(parens)
94
114
  value_color = NA::Color.template(value)
95
- gsub(/(\s|m)(@[^ ("']+)(?:(\()(.*?)(\)))?/,
96
- "\\1#{tag_color}\\2#{paren_color}\\3#{value_color}\\4#{paren_color}\\5#{last_color}")
115
+ gsub(/(?<pre>\s|m)(?<tag>@[^ ("']+)(?:(?<lparen>\()(?<val>.*?)(?<rparen>\)))?/) do
116
+ m = Regexp.last_match
117
+ if m['val']
118
+ "#{m['pre']}#{tag_color}#{m['tag']}#{paren_color}(#{value_color}#{m['val']}#{paren_color})#{last_color}"
119
+ else
120
+ "#{m['pre']}#{tag_color}#{m['tag']}#{last_color}"
121
+ end
122
+ end
97
123
  end
98
124
 
99
125
  ##
@@ -106,13 +132,12 @@ class ::String
106
132
  ## @param last_color [String] Color to restore after
107
133
  ## highlight
108
134
  ##
109
- def highlight_search(regexes, color: '{y}', last_color: '{xg}')
135
+ def highlight_search(regexes, color: NA.theme[:search_highlight], last_color: NA.theme[:action])
110
136
  string = dup
111
- color = NA::Color.template(color)
137
+ color = NA::Color.template(color.dup)
112
138
  regexes.each do |rx|
113
139
  next if rx.nil?
114
-
115
- rx = Regexp.new(rx.wildcard_to_rx, Regexp::IGNORECASE) if rx.is_a?(String)
140
+ rx = Regexp.new(rx, Regexp::IGNORECASE) if rx.is_a?(String)
116
141
 
117
142
  string.gsub!(rx) do
118
143
  m = Regexp.last_match
@@ -123,6 +148,27 @@ class ::String
123
148
  string
124
149
  end
125
150
 
151
+ def wrap(width, indent)
152
+ output = []
153
+ line = []
154
+ length = indent
155
+ gsub!(/(@\S+)\((.*?)\)/) { "#{Regexp.last_match(1)}(#{Regexp.last_match(2).gsub(/ /, '†')})" }
156
+
157
+ split(' ').each do |word|
158
+ uncolored = NA::Color.uncolor(word)
159
+ if (length + uncolored.length + 1) < width
160
+ line << word
161
+ length += uncolored.length + 1
162
+ else
163
+ output << line.join(' ')
164
+ line = [word]
165
+ length = indent + uncolored.length + 1
166
+ end
167
+ end
168
+ output << line.join(' ')
169
+ output.join("\n" + ' ' * (indent + 2)).gsub(/†/, ' ')
170
+ end
171
+
126
172
  # Returns the last escape sequence from a string.
127
173
  #
128
174
  # @note Actually returns all escape codes, with the
data/lib/na/theme.rb ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NA
4
+ module Theme
5
+ class << self
6
+ def template_help
7
+ <<~EOHELP
8
+ Use {X} placeholders to apply colors. Available colors are:
9
+
10
+ w: white, k: black, g: green, l: blue,
11
+ y: yellow, c: cyan, m: magenta, r: red,
12
+ W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
13
+ Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
14
+ d: dark, b: bold, u: underline, i: italic, x: reset
15
+
16
+ Multiple placeholders can be combined in a single {} pair.
17
+
18
+ You can also use {#RGB} and {#RRGGBB} to specify hex colors.
19
+ Add a b before the # to make the hex a background color ({b#fa0}).
20
+
21
+
22
+ EOHELP
23
+ end
24
+
25
+ def load_theme(template: {})
26
+ # Default colorization, can be overridden with full or partial template variable
27
+ default_template = {
28
+ parent: '{c}',
29
+ bracket: '{dc}',
30
+ parent_divider: '{xw}/',
31
+ action: '{bg}',
32
+ project: '{xbk}',
33
+ tags: '{m}',
34
+ value_parens: '{m}',
35
+ values: '{c}',
36
+ search_highlight: '{y}',
37
+ note: '{dw}',
38
+ dirname: '{dw}',
39
+ filename: '{xb}{#eccc87}',
40
+ prompt: '{m}',
41
+ success: '{bg}',
42
+ error: '{b}{#b61d2a}',
43
+ warning: '{by}',
44
+ debug: '{dw}',
45
+ templates: {
46
+ output: '%filename%parents| %action',
47
+ default: '%parent%action',
48
+ single_file: '%parent%action',
49
+ multi_file: '%filename%parent%action'
50
+ }
51
+ }
52
+
53
+ # Load custom theme
54
+ theme_file = NA.database_path(file: 'theme.yaml')
55
+ theme = if File.exist?(theme_file)
56
+ YAML.load(IO.read(theme_file)) || {}
57
+ else
58
+ {}
59
+ end
60
+ theme = default_template.merge(theme)
61
+
62
+ File.open(theme_file, 'w') do |f|
63
+ f.puts template_help.comment
64
+ f.puts YAML.dump(theme)
65
+ end
66
+
67
+ theme.merge(template)
68
+ end
69
+ end
70
+ end
71
+ end
data/lib/na/todo.rb CHANGED
@@ -51,8 +51,8 @@ module NA
51
51
  negated_tag = []
52
52
  projects = []
53
53
 
54
- NA.notify("{dw}Tags: #{settings[:tag]}", debug:true)
55
- NA.notify("{dw}Search: #{settings[:search]}", debug:true)
54
+ NA.notify("Tags: #{settings[:tag]}", debug:true)
55
+ NA.notify("Search: #{settings[:search]}", debug:true)
56
56
 
57
57
  settings[:tag]&.each do |t|
58
58
  unless t[:tag].nil?
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.37'
2
+ VERSION = '1.2.39'
3
3
  end
data/lib/na.rb CHANGED
@@ -13,6 +13,7 @@ require 'na/hash'
13
13
  require 'na/colors'
14
14
  require 'na/string'
15
15
  require 'na/array'
16
+ require 'na/theme'
16
17
  require 'na/todo'
17
18
  require 'na/actions'
18
19
  require 'na/project'