na 1.2.38 → 1.2.39

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13c74e296d8e9d6e252949394c5471a27b1c7d3619026383fc75590dcb5d6046
4
- data.tar.gz: c67deb62ee0ad5c2b3cf35b5ca39cebe2f6780e675cb48fbaec260068111a33a
3
+ metadata.gz: e55b998c33447bd876e7c26c1fcf7f136a5f3de847715f8565882b0df4d078f2
4
+ data.tar.gz: 8ce2a06c64b3e45291c185ffe9a38f9a6cb0fa3dc4b0eaf9f2de9a38bc295701
5
5
  SHA512:
6
- metadata.gz: 4fd8a183c2fcadf9d128bab65267b841bdd253db40071cb87c3fa0dbfd1be0517d667ba64f21b28f0a24adfbc08cbefe79c3638ac63736698de34a21c81d0658
7
- data.tar.gz: 8cb24b17379576cf4f86f709a277c2e075fb41c905d67750f09500471f5948f0d3cd2e0d66d9f29a073172e6808c8d4f2b2dd0afe3b6b223ff04ec493ccd8289
6
+ metadata.gz: cca3d6c0f98d5c30fdc5c22f6a10958b6888e08f2c22b92d13d698152465f756e491258fd4290f42f85062b7718068deabc43a07e4ea92891370f0ba35024a00
7
+ data.tar.gz: e20114aef91f0c9aa2edb15b95d335d86afa5aee03b6a4e214b2fdca7fa6acbc7b178e6f00d4348dd3329d6f88122f044591271f2886b732f8d046673e628403
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ### 1.2.39
2
+
3
+ 2023-09-06 04:25
4
+
5
+ #### NEW
6
+
7
+ - Add `--save NAME` to `na next` to save more complex queries and run with `na saved NAME` (or just `na NAME`)
8
+ - `na saved --select` flag to allow interactive selection of search(es)
9
+
10
+ #### IMPROVED
11
+
12
+ - Allow `na saved --delete` to handle multiple arguments
13
+ - Allow wildcards when deleting saved searches
14
+ - Refactor request for input, no change to user experience
15
+ - Refined wildcard (?*) handling
16
+ - When displaying actions wider than the screen, wrap at words and indent 2 spaces from start of action (after prefix)
17
+
1
18
  ### 1.2.38
2
19
 
3
20
  2023-09-03 11:25
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.38)
4
+ na (1.2.39)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
  gli (~> 2.21.0)
7
7
  mdless (~> 1.0, >= 1.0.32)
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is 1.2.38
12
+ The current version of `na` is 1.2.39
13
13
  .
14
14
 
15
15
  `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
@@ -77,7 +77,7 @@ SYNOPSIS
77
77
  na [global options] command [command options] [arguments...]
78
78
 
79
79
  VERSION
80
- 1.2.38
80
+ 1.2.39
81
81
 
82
82
  GLOBAL OPTIONS
83
83
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -292,6 +292,7 @@ COMMAND OPTIONS
292
292
  --omnifocus - Output actions nested by file and project
293
293
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
294
294
  --regex - Search query is regular expression
295
+ --save=TITLE - Save this search for future use (default: none)
295
296
  --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
296
297
  -t, --tag=TAG - Alternate tag to search for (default: none)
297
298
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
@@ -353,6 +354,7 @@ DESCRIPTION
353
354
  COMMAND OPTIONS
354
355
  -d, --delete - Delete the specified search definition
355
356
  -e, --edit - Open the saved search file in $EDITOR
357
+ -s, --select - Interactively select a saved search to run
356
358
 
357
359
  EXAMPLES
358
360
 
@@ -403,6 +405,7 @@ COMMAND OPTIONS
403
405
  --omnifocus - Output actions nested by file and project
404
406
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
405
407
  --regex - Search query is regular expression
408
+ --save=TITLE - Save this search for future use (default: none)
406
409
  --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
407
410
  -t, --tag=TAG - Alternate tag to search for (default: none)
408
411
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
data/bin/commands/add.rb CHANGED
@@ -126,11 +126,8 @@ class App
126
126
 
127
127
  action = if args.count.positive?
128
128
  args.join(' ').strip
129
- elsif $stdin.isatty && TTY::Which.exist?('gum')
130
- `gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
131
- elsif $stdin.isatty
132
- NA.notify("#{NA.theme[:prompt]}Enter task:")
133
- reader.read_line(NA::Color.template("#{NA.theme[:warning]}> #{NA.theme[:action]}")).strip
129
+ else
130
+ NA.request_input(options, prompt: 'Enter a task')
134
131
  end
135
132
 
136
133
  if action.nil? || action.empty?
data/bin/commands/edit.rb CHANGED
@@ -43,16 +43,8 @@ class App
43
43
  options[:edit] = true
44
44
  action = if args.count.positive?
45
45
  args.join(' ').strip
46
- elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
47
- opts = [
48
- %(--placeholder "Enter a task to search for"),
49
- '--char-limit=500',
50
- "--width=#{TTY::Screen.columns}"
51
- ]
52
- `gum input #{opts.join(' ')}`.strip
53
- elsif $stdin.isatty && options[:tagged].empty?
54
- NA.notify("#{NA.theme[:prompt]}Enter search string:")
55
- reader.read_line(NA::Color.template("#{NA.theme[:warning]}> #{NA.theme[:action]}")).strip
46
+ else
47
+ NA.request_input(options, prompt: 'Enter a task to search for')
56
48
  end
57
49
 
58
50
  NA.notify("#{NA.theme[:error]}Empty input", exit_code: 1) if (action.nil? || action.empty?) && options[:tagged].empty?
@@ -85,7 +77,7 @@ class App
85
77
  all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
86
78
  tags = []
87
79
  options[:tagged].join(',').split(/ *, */).each do |arg|
88
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>*$\^~]+?)(?:(?<op>[=<>~]{1,2}|[*$\^]=)(?<val>.*?))?$/)
80
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^~]+?)(?:(?<op>[=<>~]{1,2}|[*$\^]=)(?<val>.*?))?$/)
89
81
 
90
82
  tags.push({
91
83
  tag: m['tag'].wildcard_to_rx,
data/bin/commands/find.rb CHANGED
@@ -61,7 +61,8 @@ class App
61
61
 
62
62
  if options[:save]
63
63
  title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
64
- NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
64
+ cmd = NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')
65
+ NA.save_search(title, cmd)
65
66
  end
66
67
 
67
68
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
@@ -95,7 +96,7 @@ class App
95
96
  all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
96
97
  tags = []
97
98
  options[:tagged].join(',').split(/ *, */).each do |arg|
98
- m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
99
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
99
100
 
100
101
  tags.push({
101
102
  tag: m['tag'].wildcard_to_rx,
@@ -121,8 +122,9 @@ class App
121
122
 
122
123
  search.split(/ /).each do |arg|
123
124
  m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
125
+
124
126
  tokens.push({
125
- token: Regexp.escape(m['tok']),
127
+ token: m['tok'],
126
128
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
127
129
  negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
128
130
  })
@@ -155,7 +157,7 @@ class App
155
157
  })
156
158
 
157
159
  regexes = if tokens.is_a?(Array)
158
- tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
160
+ tokens.delete_if { |token| token[:negate] }.map { |token| token[:token].wildcard_to_rx }
159
161
  else
160
162
  [tokens]
161
163
  end
data/bin/commands/next.rb CHANGED
@@ -57,6 +57,10 @@ class App
57
57
  c.desc 'Output actions nested by file and project'
58
58
  c.switch %i[omnifocus], negatable: false
59
59
 
60
+ c.desc 'Save this search for future use'
61
+ c.arg_name 'TITLE'
62
+ c.flag %i[save]
63
+
60
64
  c.action do |global_options, options, args|
61
65
  if global_options[:add]
62
66
  cmd = ['add']
@@ -67,6 +71,11 @@ class App
67
71
  exit run(cmd)
68
72
  end
69
73
 
74
+ if options[:save]
75
+ title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
76
+ NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
77
+ end
78
+
70
79
  options[:nest] = true if options[:omnifocus]
71
80
 
72
81
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
@@ -100,7 +109,7 @@ class App
100
109
  all_req = options[:tagged].join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
101
110
  tags = []
102
111
  options[:tagged].join(',').split(/ *, */).each do |arg|
103
- m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
112
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
104
113
 
105
114
  tags.push({
106
115
  tag: m['tag'].wildcard_to_rx,
@@ -20,25 +20,37 @@ class App
20
20
  c.desc 'Delete the specified search definition'
21
21
  c.switch %i[d delete], negatable: false
22
22
 
23
+ c.desc 'Interactively select a saved search to run'
24
+ c.switch %i[s select], negatable: false
25
+
23
26
  c.action do |_global_options, options, args|
24
27
  NA.edit_searches if options[:edit]
25
28
 
26
- if args.empty?
29
+ if args.empty? && !options[:select]
27
30
  searches = NA.load_searches
28
31
  NA.notify("#{NA.theme[:success]}Saved searches stored in #{NA.database_path(file: 'saved_searches.yml').highlight_filename}")
29
32
  NA.notify(searches.map { |k, v| "#{NA.theme[:filename]}#{k}: #{NA.theme[:values]}#{v}" }.join("\n"))
30
33
  else
31
- args.each do |arg|
34
+ NA.delete_search(args.join(',').split(/[ ,]/)) if options[:delete]
35
+
36
+ if options[:select]
32
37
  searches = NA.load_searches
38
+ res = NA.choose_from(searches.map { |k, v| "#{NA.theme[:filename]}#{k} #{NA.theme[:value]}(#{v})" }, multiple: true)
39
+ NA.notify("#{NA.theme[:error]}Nothing selected", exit_code: 0) if res&.empty?
40
+ args = res.map { |r| r.match(/(\S+)(?= \()/)[1] }
41
+ end
33
42
 
34
- NA.delete_search(arg) if options[:delete]
43
+ args.each do |arg|
44
+ searches = NA.load_searches
35
45
 
36
- keys = searches.keys.delete_if { |k| k !~ /#{arg}/ }
46
+ keys = searches.keys.delete_if { |k| k !~ /#{arg.wildcard_to_rx}/ }
37
47
  NA.notify("#{NA.theme[:error]}Search #{arg} not found", exit_code: 1) if keys.empty?
38
48
 
39
- key = keys[0]
40
- cmd = Shellwords.shellsplit(searches[key])
41
- run(cmd)
49
+ keys.each do |key|
50
+ NA.notify("#{NA.theme[:prompt]}Saved search #{NA.theme[:filename]}#{key}#{NA.theme[:warning]}:")
51
+ cmd = Shellwords.shellsplit(searches[key])
52
+ run(cmd)
53
+ end
42
54
  end
43
55
  end
44
56
  end
@@ -81,7 +81,7 @@ class App
81
81
 
82
82
  all_req = args.join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
83
83
  args.join(',').split(/ *, */).each do |arg|
84
- m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
84
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
85
85
  next if m.nil?
86
86
 
87
87
  tags.push({
@@ -103,18 +103,9 @@ class App
103
103
 
104
104
  action = if args.count.positive?
105
105
  args.join(' ').strip
106
- elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
107
- opts = [
108
- %(--placeholder "Enter a task to search for"),
109
- '--char-limit=500',
110
- "--width=#{TTY::Screen.columns}"
111
- ]
112
- `gum input #{opts.join(' ')}`.strip
113
- elsif $stdin.isatty && options[:tagged].empty?
114
- NA.notify("#{NA.theme[:prompt]}Enter search string:")
115
- reader.read_line(NA::Color.template("#{NA.theme[:filename]}> #{NA.theme[:action]}")).strip
106
+ else
107
+ NA.request_input(options, prompt: 'Enter a task to search for')
116
108
  end
117
-
118
109
  if action
119
110
  tokens = nil
120
111
  if options[:exact]
@@ -143,7 +134,7 @@ class App
143
134
  all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
144
135
  tags = []
145
136
  options[:tagged].join(',').split(/ *, */).each do |arg|
146
- m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
137
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
147
138
 
148
139
  tags.push({
149
140
  tag: m['tag'].wildcard_to_rx,
data/lib/na/action.rb CHANGED
@@ -73,7 +73,7 @@ module NA
73
73
  ## highlight (searches)
74
74
  ## @param notes [Boolean] Include notes
75
75
  ##
76
- def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false)
76
+ def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true)
77
77
  theme = NA::Theme.load_theme
78
78
  template = theme.merge(template)
79
79
 
@@ -108,6 +108,13 @@ module NA
108
108
  value: template[:values],
109
109
  last_color: template[:action])
110
110
 
111
+ if detect_width
112
+ width = TTY::Screen.columns
113
+ prefix = NA::Color.uncolor(pretty(template: { output: template[:output].sub(/%action/, '') }, detect_width: false))
114
+ indent = prefix.length
115
+ action = action.wrap(width, indent)
116
+ end
117
+
111
118
  # Replace variables in template string and output colorized
112
119
  NA::Color.template(template[:output].gsub(/%filename/, filename)
113
120
  .gsub(/%project/, project)
data/lib/na/colors.rb CHANGED
@@ -334,7 +334,7 @@ module NA
334
334
 
335
335
  # Regular expression that is used to scan for ANSI-sequences while
336
336
  # uncoloring strings.
337
- COLORED_REGEXP = /\e\[(?:(?:[349]|10)[0-7]|[0-9])?m/.freeze
337
+ COLORED_REGEXP = /\e\[(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+m/
338
338
 
339
339
  # Returns an uncolored version of the string, that is all
340
340
  # ANSI-sequences are stripped from the string.
@@ -197,12 +197,13 @@ module NA
197
197
  end
198
198
 
199
199
  if new_path.join('') =~ /Archive/i
200
- line = todo.projects.last.last_line
200
+ line = todo.projects.last&.last_line || 0
201
201
  content = content.split(/\n/).insert(line, input.join("\n")).join("\n")
202
202
  else
203
203
  split = content.split(/\n/)
204
- before = split.slice(0, todo.projects.first.line).join("\n")
205
- 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")
206
207
  content = "#{before}\n#{input.join("\n")}\n#{after}"
207
208
  end
208
209
 
@@ -755,10 +756,14 @@ module NA
755
756
  file = database_path(file: 'saved_searches.yml')
756
757
  NA.notify("#{NA.theme[:error]}No search definitions file found", exit_code: 1) unless File.exist?(file)
757
758
 
759
+ strings = [strings] unless strings.is_a? Array
760
+
758
761
  searches = YAML.safe_load(file.read_file)
759
- keys = searches.keys.delete_if { |k| k !~ /(#{strings.join('|')})/ }
762
+ keys = searches.keys.delete_if { |k| k !~ /(#{strings.map(&:wildcard_to_rx).join('|')})/ }
763
+
764
+ NA.notify("#{NA.theme[:error]}No search named #{strings.join(', ')} found", exit_code: 1) if keys.empty?
760
765
 
761
- res = yn(NA::Color.template(%(#{NA.theme[:warning]}Remove #{keys.count > 1 ? 'searches' : 'search'} #{NA.theme[:file]}"#{keys.join(', ')}"{x})),
766
+ res = yn(NA::Color.template(%(#{NA.theme[:warning]}Remove #{keys.count > 1 ? 'searches' : 'search'} #{NA.theme[:filename]}"#{keys.join(', ')}"{x})),
762
767
  default: false)
763
768
 
764
769
  NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless res
@@ -796,7 +801,23 @@ module NA
796
801
  NA.notify("#{NA.theme[:warning]}Backup file created at #{backup.highlight_filename}", debug: true)
797
802
  end
798
803
 
799
- 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
800
821
 
801
822
  ##
802
823
  ## Generate a menu of options and allow user selection
@@ -822,30 +843,43 @@ module NA
822
843
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
823
844
  default_args << %(--header="#{header}")
824
845
  default_args.concat(fzf_args)
825
- `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
826
848
  elsif TTY::Which.exist?('gum')
827
849
  args = [
828
850
  '--cursor.foreground="151"',
829
851
  '--item.foreground=""'
830
852
  ]
831
853
  args.push '--no-limit' if multiple
832
- puts NS::Color.template("#{NA.theme[:prompt]}#{prompt}{x}")
833
- `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
834
857
  else
835
858
  reader = TTY::Reader.new
836
859
  puts
837
860
  options.each.with_index do |f, i|
838
- puts NA::Color.template(format("#{NA.theme[:prompt]}%<idx> 2d{xw}) #{NA.theme[:file]}%<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))
839
862
  end
840
863
  result = reader.read_line(NA::Color.template("#{NA.theme[:prompt]}#{prompt}{x}")).strip
841
- result.to_i&.positive? ? options[result.to_i - 1] : nil
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
873
+ end
842
874
  end
843
875
 
844
- return false if res.strip.size.zero?
845
-
846
- 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))
847
879
  end
848
880
 
881
+ private
882
+
849
883
  ##
850
884
  ## macOS open command
851
885
  ##
data/lib/na/string.rb CHANGED
@@ -112,8 +112,14 @@ class ::String
112
112
  tag_color = NA::Color.template(color)
113
113
  paren_color = NA::Color.template(parens)
114
114
  value_color = NA::Color.template(value)
115
- gsub(/(\s|m)(@[^ ("']+)(?:(\()(.*?)(\)))?/,
116
- "\\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
117
123
  end
118
124
 
119
125
  ##
@@ -131,8 +137,7 @@ class ::String
131
137
  color = NA::Color.template(color.dup)
132
138
  regexes.each do |rx|
133
139
  next if rx.nil?
134
-
135
- 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)
136
141
 
137
142
  string.gsub!(rx) do
138
143
  m = Regexp.last_match
@@ -143,6 +148,27 @@ class ::String
143
148
  string
144
149
  end
145
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
+
146
172
  # Returns the last escape sequence from a string.
147
173
  #
148
174
  # @note Actually returns all escape codes, with the
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.38'
2
+ VERSION = '1.2.39'
3
3
  end
data/src/_README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is <!--VER-->1.2.37<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.38<!--END VER-->.
13
13
 
14
14
  `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
15
15
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: na
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.38
4
+ version: 1.2.39
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-03 00:00:00.000000000 Z
11
+ date: 2023-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake