na 1.2.38 → 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 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