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