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