na 1.2.13 → 1.2.14

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: 2d39100de907ee238aac6e9f51cb976e0736822294bc602cec67886aecba12e0
4
- data.tar.gz: 9936c5bb6fb340d1a9ef756b3b6245560021685e7a538c0a72d10e300ecf527d
3
+ metadata.gz: da1fc18252895707dfb04a598aca5ac535b4c65959b564d11d56497796a09249
4
+ data.tar.gz: 7d483b22ddd0015b02bbea54c5026ba11b3cfc58597d790eed5d0902bbb285ba
5
5
  SHA512:
6
- metadata.gz: 3c6c082c7f2d104916449a1a984191c3e9c06f53f64a6c5860200569d2981e677dc0fc05a47472f186fee46867ccdb0fc5a52925789242ebef87ae441d36f758
7
- data.tar.gz: f5f3a48fecd5debb35e9d9e03eb3e6f85565912f478395100e63934edfae0b32171a7b4619c474645c7562de426de24e0d6d62312dff9e198423c6850823d6ca
6
+ metadata.gz: bfde6ccd9c352ef8932df8937218701b54db37994d1d0642323b4bc69a5bb6bf566588a9a4908a37312dc7b9787547427bf68609b124dfe752c3acec5b807bb8
7
+ data.tar.gz: 87ce30671ba3cc2bf415744df17df187e834966db481c7e5b2310eb873315a68061541f9342c0578f51d8d93f8a0accdde6df6030393d1d28aaacbfc33ac170e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ### 1.2.14
2
+
3
+ 2022-11-12 10:57
4
+
5
+ #### NEW
6
+
7
+ - `na find --tagged` allows narrowing search results with tag queries
8
+ - `na tagged --search` allows narrowing tag results with text search
9
+ - `na next` accepts --tagged and --search (as well as --exact and --regex) for filtering actions
10
+
11
+ #### FIXED
12
+
13
+ - Error when a todo file contained a task without a project
14
+
1
15
  ### 1.2.13
2
16
 
3
17
  2022-11-01 12:43
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.13)
4
+ na (1.2.14)
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.13
12
+ The current version of `na` is 1.2.14
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.13
80
+ 1.2.14
81
81
 
82
82
  GLOBAL OPTIONS
83
83
  -a, --[no-]add - Add a next action (deprecated, for backwards compatibility)
@@ -214,6 +214,7 @@ COMMAND OPTIONS
214
214
  -o, --or - Combine search tokens with OR, displaying actions matching ANY of the terms
215
215
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
216
216
  --save=TITLE - Save this search for future use (default: none)
217
+ --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
217
218
  -v, --invert - Show actions not matching search pattern
218
219
  -x, --exact - Match pattern exactly
219
220
 
@@ -269,10 +270,14 @@ DESCRIPTION
269
270
  COMMAND OPTIONS
270
271
  -d, --depth=DEPTH - Recurse to depth (default: none)
271
272
  --[no-]done - Include @done actions
273
+ --[no-]exact - Search query is exact text match (not tokens)
272
274
  --in, --todo=TODO_FILE - Display matches from a known todo file (may be used more than once, default: none)
273
275
  --[no-]notes - Include notes in output
274
276
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
277
+ --[no-]regex - Search query is regular expression
278
+ --search=QUERY - Filter results using search terms (may be used more than once, default: none)
275
279
  -t, --tag=TAG - Alternate tag to search for (default: none)
280
+ --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
276
281
 
277
282
  EXAMPLES
278
283
 
@@ -369,10 +374,14 @@ DESCRIPTION
369
374
  COMMAND OPTIONS
370
375
  -d, --depth=DEPTH - Recurse to depth (default: none)
371
376
  --[no-]done - Include @done actions
377
+ --[no-]exact - Search query is exact text match (not tokens)
372
378
  --in, --todo=TODO_FILE - Display matches from a known todo file (may be used more than once, default: none)
373
379
  --[no-]notes - Include notes in output
374
380
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
381
+ --[no-]regex - Search query is regular expression
382
+ --search=QUERY - Filter results using search terms (may be used more than once, default: none)
375
383
  -t, --tag=TAG - Alternate tag to search for (default: none)
384
+ --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
376
385
 
377
386
  EXAMPLES
378
387
 
data/bin/na CHANGED
@@ -95,6 +95,20 @@ class App
95
95
  c.arg_name 'PROJECT[/SUBPROJECT]'
96
96
  c.flag %i[proj project]
97
97
 
98
+ c.desc 'Match actions containing tag. Allows value comparisons'
99
+ c.arg_name 'TAG'
100
+ c.flag %i[tagged], multiple: true
101
+
102
+ c.desc 'Filter results using search terms'
103
+ c.arg_name 'QUERY'
104
+ c.flag %i[search], multiple: true
105
+
106
+ c.desc 'Search query is regular expression'
107
+ c.switch %i[regex]
108
+
109
+ c.desc 'Search query is exact text match (not tokens)'
110
+ c.switch %i[exact]
111
+
98
112
  c.desc 'Include notes in output'
99
113
  c.switch %i[notes], negatable: true, default_value: false
100
114
 
@@ -117,6 +131,20 @@ class App
117
131
  options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
118
132
  end
119
133
 
134
+ all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
135
+ tags = []
136
+ options[:tagged].join(',').split(/ *, */).each do |arg|
137
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
138
+
139
+ tags.push({
140
+ tag: m['tag'].wildcard_to_rx,
141
+ comp: m['op'],
142
+ value: m['val'],
143
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
144
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
145
+ })
146
+ end
147
+
120
148
  args.concat(options[:in])
121
149
  if args.count.positive?
122
150
  all_req = args.join(' ') !~ /[+!\-]/
@@ -134,14 +162,37 @@ class App
134
162
  end
135
163
  end
136
164
 
165
+ search = nil
166
+ if options[:search]
167
+ if options[:exact]
168
+ search = options[:search].join(' ')
169
+ elsif options[:regex]
170
+ search = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
171
+ else
172
+ search = []
173
+ all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
174
+
175
+ options[:search].join(' ').split(/ /).each do |arg|
176
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
177
+ search.push({
178
+ token: m['tok'],
179
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
180
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
181
+ })
182
+ end
183
+ end
184
+ end
185
+
137
186
  NA.na_tag = options[:tag] unless options[:tag].nil?
138
187
  require_na = true
139
188
 
140
- tag = [{ tag: tag, value: nil }, { tag: 'done', value: nil, negate: true}]
189
+ tag = [{ tag: NA.na_tag, value: nil }, { tag: 'done', value: nil, negate: true }]
190
+ tag.concat(tags)
141
191
  files, actions, = NA.parse_actions(depth: depth,
142
192
  done: options[:done],
143
193
  query: tokens,
144
194
  tag: tag,
195
+ search: search,
145
196
  project: options[:project],
146
197
  require_na: require_na)
147
198
 
@@ -587,6 +638,10 @@ class App
587
638
  c.arg_name 'PROJECT[/SUBPROJECT]'
588
639
  c.flag %i[proj project]
589
640
 
641
+ c.desc 'Match actions containing tag. Allows value comparisons'
642
+ c.arg_name 'TAG'
643
+ c.flag %i[tagged], multiple: true
644
+
590
645
  c.desc 'Include @done actions'
591
646
  c.switch %i[done]
592
647
 
@@ -600,7 +655,7 @@ class App
600
655
  c.action do |global_options, options, args|
601
656
  if options[:save]
602
657
  title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
603
- NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '')}")
658
+ NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
604
659
  end
605
660
 
606
661
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
@@ -608,6 +663,21 @@ class App
608
663
  else
609
664
  options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
610
665
  end
666
+
667
+ all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
668
+ tags = []
669
+ options[:tagged].join(',').split(/ *, */).each do |arg|
670
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
671
+
672
+ tags.push({
673
+ tag: m['tag'].wildcard_to_rx,
674
+ comp: m['op'],
675
+ value: m['val'],
676
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
677
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
678
+ })
679
+ end
680
+
611
681
  tokens = nil
612
682
  if options[:exact]
613
683
  tokens = args.join(' ')
@@ -644,6 +714,7 @@ class App
644
714
  done: options[:done],
645
715
  query: todo,
646
716
  search: tokens,
717
+ tag: tags,
647
718
  negate: options[:invert],
648
719
  regex: options[:regex],
649
720
  project: options[:project],
@@ -692,6 +763,16 @@ class App
692
763
  c.arg_name 'PROJECT[/SUBPROJECT]'
693
764
  c.flag %i[proj project]
694
765
 
766
+ c.desc 'Filter results using search terms'
767
+ c.arg_name 'QUERY'
768
+ c.flag %i[search], multiple: true
769
+
770
+ c.desc 'Search query is regular expression'
771
+ c.switch %i[regex]
772
+
773
+ c.desc 'Search query is exact text match (not tokens)'
774
+ c.switch %i[exact]
775
+
695
776
  c.desc 'Include @done actions'
696
777
  c.switch %i[done]
697
778
 
@@ -705,7 +786,7 @@ class App
705
786
  c.action do |global_options, options, args|
706
787
  if options[:save]
707
788
  title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
708
- NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '')}")
789
+ NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
709
790
  end
710
791
 
711
792
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
@@ -733,6 +814,27 @@ class App
733
814
  tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
734
815
  tags.push({ tag: 'done', value: nil, negate: true}) unless search_for_done
735
816
 
817
+ tokens = nil
818
+ if options[:search]
819
+ if options[:exact]
820
+ tokens = options[:search].join(' ')
821
+ elsif options[:regex]
822
+ tokens = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
823
+ else
824
+ tokens = []
825
+ all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
826
+
827
+ options[:search].join(' ').split(/ /).each do |arg|
828
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
829
+ tokens.push({
830
+ token: m['tok'],
831
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
832
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
833
+ })
834
+ end
835
+ end
836
+ end
837
+
736
838
  todo = nil
737
839
  if options[:in]
738
840
  todo = []
@@ -749,11 +851,17 @@ class App
749
851
  files, actions, = NA.parse_actions(depth: depth,
750
852
  done: options[:done],
751
853
  query: todo,
854
+ search: tokens,
752
855
  tag: tags,
753
856
  negate: options[:invert],
754
857
  project: options[:project],
755
858
  require_na: false)
756
- regexes = tags.delete_if { |token| token[:negate] }.map { |token| token[:token] }
859
+ # regexes = tags.delete_if { |token| token[:negate] }.map { |token| token[:token] }
860
+ regexes = if tokens.is_a?(Array)
861
+ tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
862
+ else
863
+ [tokens]
864
+ end
757
865
  NA.output_actions(actions, depth, files: files, regexes: regexes, notes: options[:notes])
758
866
  end
759
867
  end
@@ -512,36 +512,36 @@ module NA
512
512
  last_line = 0
513
513
  in_action = false
514
514
  content.split("\n").each.with_index do |line, idx|
515
- if line !~ /^[ \t]*- / && line =~ /^([ \t]*)([^\-][^@()]+?): *(@\S+ *)*$/
515
+ if line.project?
516
516
  in_action = false
517
- proj = Regexp.last_match(2)
517
+ proj = line.project
518
518
  indent = line.indent_level
519
519
 
520
- if indent.zero?
520
+ if indent.zero? # top level project
521
521
  parent = [proj]
522
- elsif indent <= indent_level
522
+ elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
523
523
  parent.slice!(indent, parent.count - indent)
524
524
  parent.push(proj)
525
- elsif indent > indent_level
525
+ else # if indent level is greater, append project to parent
526
526
  parent.push(proj)
527
527
  end
528
528
 
529
529
  projects.push(NA::Project.new(parent.join(':'), indent, idx, idx))
530
530
 
531
531
  indent_level = indent
532
- elsif line.strip =~ /^$/
532
+ elsif line.blank?
533
533
  in_action = false
534
- elsif line =~ /^[ \t]*- /
534
+ elsif line.action?
535
535
  in_action = false
536
536
 
537
- action = line.sub(/^[ \t]*- /, '')
537
+ action = line.action
538
538
  new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
539
539
 
540
540
  projects[-1].last_line = idx if projects.count.positive?
541
541
 
542
- next if line =~ /@done/ && !done
542
+ next if line.done? && !done
543
543
 
544
- next if require_na && line !~ /@#{NA.na_tag}\b/
544
+ next if require_na && !line.na?
545
545
 
546
546
  has_search = !optional.empty? || !required.empty? || !negated.empty?
547
547
 
@@ -600,11 +600,11 @@ module NA
600
600
  db_dir = File.expand_path('~/.local/share/na')
601
601
  db_file = 'tdlist.txt'
602
602
  file = File.join(db_dir, db_file)
603
- if File.exist?(file)
604
- dirs = file.read_file.split("\n")
605
- dirs.delete_if { |f| !File.exist?(f) }
606
- File.open(file, 'w') { |f| f.puts dirs.join("\n") }
607
- end
603
+ return unless File.exist?(file)
604
+
605
+ dirs = file.read_file.split("\n")
606
+ dirs.delete_if { |f| !File.exist?(f) }
607
+ File.open(file, 'w') { |f| f.puts dirs.join("\n") }
608
608
  end
609
609
 
610
610
  def list_projects(query: [], file_path: nil, depth: 1, paths: true)
@@ -624,7 +624,7 @@ module NA
624
624
  output = if paths
625
625
  "{bg}#{parts.join('{bw}/{bg}')}{x}"
626
626
  else
627
- parts.fill("{bw}—{bg}", 0..-2)
627
+ parts.fill('{bw}—{bg}', 0..-2)
628
628
  "{bg}#{parts.join(' ')}{x}"
629
629
  end
630
630
 
@@ -633,15 +633,15 @@ module NA
633
633
  end
634
634
 
635
635
  def list_todos(query: [])
636
- if query
637
- dirs = match_working_dir(query, distance: 2, require_last: false)
638
- else
639
- file = database_path
640
- content = File.exist?(file) ? file.read_file.strip : ''
641
- notify('{br}Database empty', exit_code: 1) if content.empty?
642
-
643
- dirs = content.split(/\n/)
644
- end
636
+ dirs = if query
637
+ match_working_dir(query, distance: 2, require_last: false)
638
+ else
639
+ file = database_path
640
+ content = File.exist?(file) ? file.read_file.strip : ''
641
+ notify('{br}Database empty', exit_code: 1) if content.empty?
642
+
643
+ content.split(/\n/)
644
+ end
645
645
 
646
646
  dirs.map! do |dir|
647
647
  "{xg}#{dir.sub(/^#{ENV['HOME']}/, '~').sub(%r{/([^/]+)\.#{NA.extension}$}, '/{xbw}\1{x}')}"
data/lib/na/string.rb CHANGED
@@ -22,6 +22,35 @@ class ::String
22
22
  prefix[1].gsub(/ /, "\t").scan(/\t/).count
23
23
  end
24
24
 
25
+ def action?
26
+ self =~ /^[ \t]*- /
27
+ end
28
+
29
+ def blank?
30
+ strip =~ /^$/
31
+ end
32
+
33
+ def project?
34
+ !action? && self =~ /:( +@\S+(\([^)]*\))?)*$/
35
+ end
36
+
37
+ def project
38
+ m = match(/^([ \t]*)([^\-][^@()]+?): *(@\S+ *)*$/)
39
+ m ? m[2] : nil
40
+ end
41
+
42
+ def action
43
+ sub(/^[ \t]*- /, '')
44
+ end
45
+
46
+ def done?
47
+ self =~ /@done/
48
+ end
49
+
50
+ def na?
51
+ self =~ /@#{NA.na_tag}\b/
52
+ end
53
+
25
54
  ##
26
55
  ## Colorize @tags with ANSI escapes
27
56
  ##
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.13'
2
+ VERSION = '1.2.14'
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.12<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.13<!--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.13
4
+ version: 1.2.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-01 00:00:00.000000000 Z
11
+ date: 2022-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake