na 1.1.11 → 1.1.12

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