na 1.2.37 → 1.2.39
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/Gemfile.lock +1 -1
- data/README.md +82 -6
- data/bin/commands/add.rb +11 -16
- data/bin/commands/edit.rb +15 -27
- data/bin/commands/find.rb +16 -9
- data/bin/commands/init.rb +1 -1
- data/bin/commands/next.rb +65 -27
- data/bin/commands/projects.rb +1 -1
- data/bin/commands/saved.rb +22 -11
- data/bin/commands/tagged.rb +7 -7
- data/bin/commands/todos.rb +24 -14
- data/bin/commands/update.rb +24 -36
- data/bin/na +2 -1
- data/lib/na/action.rb +24 -26
- data/lib/na/actions.rb +8 -8
- data/lib/na/colors.rb +24 -2
- data/lib/na/editor.rb +13 -11
- data/lib/na/hash.rb +31 -0
- data/lib/na/next_action.rb +90 -49
- data/lib/na/pager.rb +1 -1
- data/lib/na/prompt.rb +6 -6
- data/lib/na/string.rb +53 -7
- data/lib/na/theme.rb +71 -0
- data/lib/na/todo.rb +2 -2
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/src/_README.md +35 -15
- metadata +3 -2
data/lib/na/next_action.rb
CHANGED
@@ -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("{
|
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("{
|
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(
|
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("{
|
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(
|
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
|
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
|
-
|
201
|
-
|
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("{
|
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(
|
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("{
|
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
|
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(
|
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("
|
537
|
-
NA.notify("
|
538
|
-
NA.notify("
|
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.
|
557
|
+
dirs = dirs.sort_by { |d| File.basename(d) }.uniq
|
550
558
|
if dirs.empty? && require_last
|
551
|
-
NA.notify(
|
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(
|
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("{
|
629
|
+
NA.notify("#{NA.theme[:success]}Backup restored for #{file.highlight_filename}")
|
622
630
|
else
|
623
|
-
NA.notify("{
|
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(
|
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
|
-
|
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(
|
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("{
|
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(
|
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(
|
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
|
-
|
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(
|
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("{
|
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(
|
782
|
+
NA.notify("#{NA.theme[:error]}No search definitions found", exit_code: 1) unless searches.count.positive?
|
771
783
|
|
772
|
-
editor =
|
773
|
-
NA.notify(
|
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("{
|
801
|
+
NA.notify("#{NA.theme[:warning]}Backup file created at #{backup.highlight_filename}", debug: true)
|
790
802
|
end
|
791
803
|
|
792
|
-
|
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
|
-
|
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
|
826
|
-
|
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("{
|
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
|
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(
|
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
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(
|
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(
|
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(
|
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("{
|
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("{
|
95
|
-
NA.notify("{
|
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:
|
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(/(
|
96
|
-
|
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:
|
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("
|
55
|
-
NA.notify("
|
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