na 1.1.11 → 1.1.12

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: b309ba145cf0859f1a9011cf62268261766338d79cb956810fab9c64863c37f2
4
- data.tar.gz: 6fc7e065d4d85bf70b9122634414274e6a44d640ffffeeee7409e9102115f11d
3
+ metadata.gz: da1e1113960a99932feda46ee18cd7a8b9e8d309f479c9e3a6ec323d97db686a
4
+ data.tar.gz: 871fcae47831f04efcff94b7034abb70d32cb47b2f504247b94705ad015c191f
5
5
  SHA512:
6
- metadata.gz: 63ee0b1ac9370533379956e8cd5f6571c8d39731c12c02404b2912d5cb97f49d32c988e4d23c12c50d1b2080412296bc58351680122abd26281580c49d93f722
7
- data.tar.gz: b7500123c7ea092acc42ccba0873942fe871bd201405e3ef2faa66f3d155c3d0ac60b530078d962114a4d0d66bdb8ea427b1452873c14e2b06419cb2675e822e
6
+ metadata.gz: c8e8c92c9d3a3ffa4a00fe8fcdd8ace94681e19f9ac53a3d8b988bcf4bd69d3b032765b4a870e3d9f660cd58b18783e97e8333cdb252164cb60f0062495210b5
7
+ data.tar.gz: de5596e85883b893afbe2ca43833fe60f82fd96ab12e1f2506d84d13a90644d5d403b748b6598c9fb4c4d4db3998bdd0e560a1c071f4f7c2946b5f75dd45c728
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ### 1.1.12
2
+
3
+ 2022-10-06 05:42
4
+
5
+ #### NEW
6
+
7
+ - `na add -d X` to allow adding new actions to todo files in subdirectories
8
+ - You can now perform <>= queries on tag values (`na tagged "priority>=3"`)
9
+ - You can now perform string matches on tag values (`na tagged "note*=markdown"`)
10
+ - You can use `--project X` to display only actions within a specific project. Specify subprojects with a path, e.g. `na/bugs`. Partial matches allowed, works with `next`, `find`, and `tagged`
11
+ - Find and tagged recognize * and ? as wildcards
12
+ - --regex flag for find command
13
+ - --invert command (like grep -v) for find
14
+ - -v/--invert for tagged command
15
+
16
+ #### IMPROVED
17
+
18
+ - Require value 1-9 for --depth option
19
+
1
20
  ### 1.1.11
2
21
 
3
22
  2022-10-05 08:56
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.1.11)
4
+ na (1.1.12)
5
5
  gli (~> 2.21.0)
6
6
  tty-reader (~> 0.9, >= 0.9.0)
7
7
  tty-screen (~> 0.8, >= 0.8.1)
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.1.11
12
+ The current version of `na` is 1.1.12
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.
data/bin/na CHANGED
@@ -20,7 +20,7 @@ class App
20
20
 
21
21
  desc 'File extension to consider a todo file'
22
22
  default_value 'taskpaper'
23
- arg_name 'FILE_EXTENSION'
23
+ arg_name 'EXT'
24
24
  flag :ext
25
25
 
26
26
  desc 'Tag to consider a next action'
@@ -48,7 +48,7 @@ class App
48
48
  desc 'Recurse to depth'
49
49
  arg_name 'DEPTH'
50
50
  default_value 1
51
- flag %i[d depth], type: :integer, must_match: /^\d+$/
51
+ flag %i[d depth], type: :integer, must_match: /^[1-9]$/
52
52
 
53
53
  desc 'Display verbose output'
54
54
  switch %i[debug]
@@ -59,13 +59,19 @@ class App
59
59
  c.example 'na next', desc: 'display the next actions from any todo files in the current directory'
60
60
  c.example 'na next -d 3', desc: 'display the next actions from the current directory, traversing 3 levels deep'
61
61
  c.example 'na next marked', desc: 'display next actions for a project you visited in the past'
62
+
62
63
  c.desc 'Recurse to depth'
63
64
  c.arg_name 'DEPTH'
64
- c.flag %i[d depth], type: :integer, must_match: /^\d+$/
65
+ c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
65
66
 
66
67
  c.desc 'Alternate tag to search for'
68
+ c.arg_name 'TAG'
67
69
  c.flag %i[t tag]
68
70
 
71
+ c.desc 'Show actions from a specific project'
72
+ c.arg_name 'PROJECT[/SUBPROJECT]'
73
+ c.flag %i[proj project]
74
+
69
75
  c.action do |global_options, options, args|
70
76
  if global_options[:add]
71
77
  cmd = ['add']
@@ -100,6 +106,7 @@ class App
100
106
  files, actions = NA.parse_actions(depth: depth,
101
107
  query: tokens,
102
108
  tag: tag,
109
+ project: options[:project],
103
110
  require_na: require_na)
104
111
 
105
112
  NA.output_actions(actions, depth, files: files)
@@ -122,9 +129,11 @@ class App
122
129
  c.switch %i[n note], negatable: false
123
130
 
124
131
  c.desc 'Add a priority level 1-5'
132
+ c.arg_name 'PRIO'
125
133
  c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
126
134
 
127
135
  c.desc 'Add action to specific project'
136
+ c.arg_name 'PROJECT'
128
137
  c.default_value 'Inbox'
129
138
  c.flag %i[to]
130
139
 
@@ -139,6 +148,10 @@ class App
139
148
  c.arg_name 'PATH'
140
149
  c.flag %i[f file]
141
150
 
151
+ c.desc 'Search for files X directories deep'
152
+ c.arg_name 'DEPTH'
153
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
154
+
142
155
  c.action do |_global_options, options, args|
143
156
  reader = TTY::Reader.new
144
157
  action = if args.count.positive?
@@ -195,7 +208,7 @@ class App
195
208
  end
196
209
  end
197
210
  else
198
- files = NA.find_files(depth: 1)
211
+ files = NA.find_files(depth: options[:depth])
199
212
  if files.count.zero?
200
213
  print NA::Color.template('{by}No todo file found, create one? {w}(y/{g}N{w}){x} ')
201
214
  res = reader.read_char
@@ -221,19 +234,28 @@ class App
221
234
  long_desc 'Search tokens are separated by spaces. Actions matching any token in the pattern will be shown
222
235
  (partial matches allowed). Add a + before a token to make it required, e.g. `na find +feature +maybe`'
223
236
  arg_name 'PATTERN'
224
- command %i[find] do |c|
237
+ command %i[find grep] do |c|
225
238
  c.example 'na find feature +idea +swift', desc: 'Find all actions containing feature, idea, and swift'
226
239
  c.example 'na find -x feature idea', desc: 'Find all actions containing the exact text "feature idea"'
227
240
  c.example 'na find -d 3 swift obj-c', desc: 'Find all actions 3 directories deep containing either swift or obj-c'
228
241
 
242
+ c.desc 'Interpret search pattern as regular expression'
243
+ c.switch %i[e regex], negatable: false
244
+
229
245
  c.desc 'Match pattern exactly'
230
246
  c.switch %i[x exact], negatable: false
231
247
 
232
248
  c.desc 'Recurse to depth'
233
249
  c.arg_name 'DEPTH'
234
- c.default_value 1
235
250
  c.flag %i[d depth], type: :integer, must_match: /^\d+$/
236
251
 
252
+ c.desc 'Show actions from a specific project'
253
+ c.arg_name 'PROJECT[/SUBPROJECT]'
254
+ c.flag %i[proj project]
255
+
256
+ c.desc 'Show actions not matching search pattern'
257
+ c.switch %i[v invert], negatable: false
258
+
237
259
  c.action do |global_options, options, args|
238
260
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
239
261
  3
@@ -243,6 +265,8 @@ class App
243
265
  tokens = nil
244
266
  if options[:exact]
245
267
  tokens = args.join(' ')
268
+ elsif options[:regex]
269
+ tokens = Regexp.new(args.join(' '), Regexp::IGNORECASE)
246
270
  else
247
271
  tokens = []
248
272
  args.each do |arg|
@@ -257,6 +281,9 @@ class App
257
281
 
258
282
  files, actions = NA.parse_actions(depth: depth,
259
283
  search: tokens,
284
+ negate: options[:invert],
285
+ regex: options[:regex],
286
+ project: options[:project],
260
287
  require_na: false)
261
288
  NA.output_actions(actions, depth, files: files)
262
289
  end
@@ -277,6 +304,13 @@ class App
277
304
  c.default_value 1
278
305
  c.flag %i[d depth], type: :integer, must_match: /^\d+$/
279
306
 
307
+ c.desc 'Show actions from a specific project'
308
+ c.arg_name 'PROJECT[/SUBPROJECT]'
309
+ c.flag %i[proj project]
310
+
311
+ c.desc 'Show actions not matching tags'
312
+ c.switch %i[v invert], negatable: false
313
+
280
314
  c.action do |global_options, options, args|
281
315
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
282
316
  3
@@ -285,9 +319,9 @@ class App
285
319
  end
286
320
 
287
321
  tags = []
288
- args.each do |arg|
322
+ args.join(',').split(/ *, */).each do |arg|
289
323
  # TODO: <> comparisons do nothing right now
290
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>]+)(?:(?<op>[=<>]+)(?<val>\S+))?$/)
324
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>*$\^]+)(?:(?<op>[=<>*$\^]+)(?<val>\S+))?$/)
291
325
  tags.push({
292
326
  tag: m['tag'],
293
327
  comp: m['op'],
@@ -299,6 +333,8 @@ class App
299
333
 
300
334
  files, actions = NA.parse_actions(depth: depth,
301
335
  tag: tags,
336
+ negate: options[:invert],
337
+ project: options[:project],
302
338
  require_na: false)
303
339
  NA.output_actions(actions, depth, files: files)
304
340
  end
data/lib/na/action.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module NA
4
4
  class Action < Hash
5
- attr_reader :file, :project, :parent, :action
5
+ attr_reader :file, :project, :parent, :action, :tags
6
6
 
7
7
  def initialize(file, project, parent, action)
8
8
  super()
@@ -11,6 +11,7 @@ module NA
11
11
  @project = project
12
12
  @parent = parent
13
13
  @action = action
14
+ @tags = scan_tags
14
15
  end
15
16
 
16
17
  def to_s
@@ -68,5 +69,106 @@ module NA
68
69
  .gsub(/%parents?/, parents)
69
70
  .gsub(/%action/, action))
70
71
  end
72
+
73
+ def tags_match?(any: [], all: [], none: [])
74
+ tag_matches_any(any) && tag_matches_all(all) && tag_matches_none(none)
75
+ end
76
+
77
+ def search_match?(any: [], all: [], none: [])
78
+ search_matches_any(any) && search_matches_all(all) && search_matches_none(none)
79
+ end
80
+
81
+ private
82
+
83
+ def search_matches_none(regexes)
84
+ regexes.each do |rx|
85
+ return false if @action.match(Regexp.new(rx, Regexp::IGNORECASE))
86
+ end
87
+ true
88
+ end
89
+
90
+ def search_matches_any(regexes)
91
+ return true if regexes.empty?
92
+
93
+ regexes.each do |rx|
94
+ return true if @action.match(Regexp.new(rx, Regexp::IGNORECASE))
95
+ end
96
+ false
97
+ end
98
+
99
+ def search_matches_all(regexes)
100
+ regexes.each do |rx|
101
+ return false unless @action.match(Regexp.new(rx, Regexp::IGNORECASE))
102
+ end
103
+ true
104
+ end
105
+
106
+ def tag_matches_none(tags)
107
+ tags.each do |tag|
108
+ return false if compare_tag(tag)
109
+ end
110
+ true
111
+ end
112
+
113
+ def tag_matches_any(tags)
114
+ return true if tags.empty?
115
+
116
+ tags.each do |tag|
117
+ return true if compare_tag(tag)
118
+ end
119
+ false
120
+ end
121
+
122
+ def tag_matches_all(tags)
123
+ tags.each do |tag|
124
+ return false unless compare_tag(tag)
125
+ end
126
+ true
127
+ end
128
+
129
+ def compare_tag(tag)
130
+ return false unless @tags.key?(tag[:tag])
131
+
132
+ return true if tag[:comp].nil?
133
+
134
+ tag_val = @tags[tag[:tag]]
135
+ val = tag[:value]
136
+
137
+ return false if tag_val.nil?
138
+
139
+ return case tag[:comp]
140
+ when /^>$/
141
+ tag_val.to_f > val.to_f
142
+ when /^<$/
143
+ tag_val.to_f < val.to_f
144
+ when /^<=$/
145
+ tag_val.to_f <= val.to_f
146
+ when /^>=$/
147
+ tag_val.to_f >= val.to_f
148
+ when /^==?$/
149
+ tag_val =~ /^#{val.wildcard_to_rx}$/
150
+ when /^\$=$/
151
+ tag_val =~ /#{val.wildcard_to_rx}$/i
152
+ when /^\*=$/
153
+ tag_val =~ /#{val.wildcard_to_rx}/i
154
+ when /^\^=$/
155
+ tag_val =~ /^#{val.wildcard_to_rx}/
156
+ else
157
+ false
158
+ end
159
+ end
160
+
161
+ def scan_tags
162
+ tags = {}
163
+ rx = /(?<= |^)@(?<tag>\S+?)(?:\((?<value>.*?)\))?(?= |$)/
164
+ all_tags = []
165
+ @action.scan(rx) { all_tags << Regexp.last_match }
166
+ all_tags.each do |m|
167
+ tag = m.named_captures.symbolize_keys
168
+ tags[tag[:tag]] = tag[:value]
169
+ end
170
+
171
+ tags
172
+ end
71
173
  end
72
174
  end
data/lib/na/hash.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ::Hash
4
+ def symbolize_keys
5
+ each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
6
+ end
7
+ end
@@ -103,34 +103,50 @@ module NA
103
103
  puts actions.map { |action| action.pretty(template: { output: template }) }
104
104
  end
105
105
 
106
- def parse_actions(depth: 1, query: nil, tag: nil, search: nil, require_na: true)
106
+ def parse_actions(depth: 1, query: nil, tag: nil, search: nil, negate: false, regex: false, project: nil, require_na: true)
107
107
  actions = []
108
108
  required = []
109
109
  optional = []
110
110
  negated = []
111
+ required_tag = []
112
+ optional_tag = []
113
+ negated_tag = []
111
114
 
112
115
  tag&.each do |t|
113
116
  unless t[:tag].nil?
114
- new_rx = " @#{t[:tag]}\\b"
115
- new_rx = "#{new_rx}\\(#{t[:value]}\\)" if t[:value]
116
-
117
- optional.push(new_rx) unless t[:negate]
118
- required.push(new_rx) if t[:required] && !t[:negate]
119
- negated.push(new_rx) if t[:negate]
117
+ if negate
118
+ optional_tag.push(t) if t[:negate]
119
+ required_tag.push(t) if t[:required] && t[:negate]
120
+ negated_tag.push(t) unless t[:negate]
121
+ else
122
+ optional_tag.push(t) unless t[:negate]
123
+ required_tag.push(t) if t[:required] && !t[:negate]
124
+ negated_tag.push(t) if t[:negate]
125
+ end
120
126
  end
121
127
  end
122
128
 
123
129
  unless search.nil?
124
- if search.is_a?(String)
125
- optional.push(search)
126
- required.push(search)
130
+ if regex || search.is_a?(String)
131
+ if negate
132
+ negated.push(search)
133
+ else
134
+ optional.push(search)
135
+ required.push(search)
136
+ end
127
137
  else
128
138
  search.each do |t|
129
- new_rx = t[:token].to_s
130
-
131
- optional.push(new_rx) unless t[:negate]
132
- required.push(new_rx) if t[:required] && !t[:negate]
133
- negated.push(new_rx) if t[:negate]
139
+ new_rx = t[:token].to_s.wildcard_to_rx
140
+
141
+ if negate
142
+ optional.push(new_rx) if t[:negate]
143
+ required.push(new_rx) if t[:required] && t[:negate]
144
+ negated.push(new_rx) unless t[:negate]
145
+ else
146
+ optional.push(new_rx) unless t[:negate]
147
+ required.push(new_rx) if t[:required] && !t[:negate]
148
+ negated.push(new_rx) if t[:negate]
149
+ end
134
150
  end
135
151
  end
136
152
  end
@@ -166,13 +182,24 @@ module NA
166
182
  elsif line =~ /^[ \t]*- / && line !~ / @done/
167
183
  next if require_na && line !~ /@#{NA.na_tag}\b/
168
184
 
169
- unless optional.empty? && required.empty? && negated.empty?
170
- next unless line.matches(any: optional, all: required, none: negated)
185
+ action = line.sub(/^[ \t]*- /, '').sub(/ @#{NA.na_tag}\b/, '')
186
+ new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action)
187
+
188
+ has_search = !optional.empty? || !required.empty? || !negated.empty?
189
+ next if has_search && !new_action.search_match?(any: optional,
190
+ all: required,
191
+ none: negated)
171
192
 
193
+ if project
194
+ rx = project.split(%r{[/:]}).join('.*?/.*?')
195
+ next unless parent.join('/') =~ Regexp.new(rx, Regexp::IGNORECASE)
172
196
  end
173
197
 
174
- action = line.sub(/^[ \t]*- /, '').sub(/ @#{NA.na_tag}\b/, '')
175
- new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action)
198
+ has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
199
+ next if has_tag && !new_action.tags_match?(any: optional_tag,
200
+ all: required_tag,
201
+ none: negated_tag)
202
+
176
203
  actions.push(new_action)
177
204
  end
178
205
  end
data/lib/na/string.rb CHANGED
@@ -49,6 +49,10 @@ class ::String
49
49
  true
50
50
  end
51
51
 
52
+ def wildcard_to_rx
53
+ gsub(/\./, '\\.').gsub(/\*/, '.*?').gsub(/\?/, '.')
54
+ end
55
+
52
56
  def cap_first!
53
57
  replace cap_first
54
58
  end
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.1.11'
2
+ VERSION = '1.1.12'
3
3
  end
data/lib/na.rb CHANGED
@@ -1,14 +1,14 @@
1
- require 'na/version.rb'
1
+ # frozen_string_literal: true
2
2
 
3
- # Add requires for other files you add to your project here, so
4
- # you just need to require this one file in your bin file
3
+ require 'na/version'
5
4
  require 'fileutils'
6
5
  require 'shellwords'
7
6
  require 'tty-screen'
8
7
  require 'tty-reader'
9
8
  require 'tty-which'
10
- require 'na/colors.rb'
11
- require 'na/string.rb'
12
- require 'na/action.rb'
13
- require 'na/next_action.rb'
14
- require 'na/prompt.rb'
9
+ require 'na/hash'
10
+ require 'na/colors'
11
+ require 'na/string'
12
+ require 'na/action'
13
+ require 'na/next_action'
14
+ require 'na/prompt'
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.1.10<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.1.11<!--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.1.11
4
+ version: 1.1.12
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-10-05 00:00:00.000000000 Z
11
+ date: 2022-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -169,6 +169,7 @@ files:
169
169
  - lib/na.rb
170
170
  - lib/na/action.rb
171
171
  - lib/na/colors.rb
172
+ - lib/na/hash.rb
172
173
  - lib/na/next_action.rb
173
174
  - lib/na/prompt.rb
174
175
  - lib/na/string.rb