na 1.1.22 → 1.1.24

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: 584d55c84c26fddeb65bf537b4743b5cac4308f39d796d336d95c900fc966deb
4
- data.tar.gz: 73ea4289a1e43ce0b67f66f803a0db07359fe3f077358328a58c172c06daa107
3
+ metadata.gz: 627281aeb89624e4b083114fe2247e459383a0a00517901003deb4d361e6b6ed
4
+ data.tar.gz: 4c811e25d7b2a882c058bc04465d08777ba6275192b4b4f262a6be05ff927d7f
5
5
  SHA512:
6
- metadata.gz: 89945d2a8df1c1ea11360da4258c09f9b5c8ad38f41ca5ef98a68b99c33b41fb0b718919cc680eb356903f284cef8dd4271acea94af7f8c4adca031d445a6a26
7
- data.tar.gz: 7c3f8c30bf5a4c69270641aadfb036303c42ee8678a09c2e8b5faad7883aff0ac0139caf465f9d37b84cf170ed4ee072ad20d4872b8c760b97d0e2ca1f3b6e2e
6
+ metadata.gz: 3d8197e283363015d007cd04748f82452306724b172ea68a4928fe368c19ce842af4c16d0f1ed05d7e885a7f76f5757c1e95e58181a7b1e75295d146797d8a18
7
+ data.tar.gz: 216f752c87e8143f0332309a4c6a00cff3c6d53f0645d189bc03acfe2f68319618a9c4fa9bae0fbeca23a74a7fd555cef17cd612676fca1ebc263af75c378b8d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ### 1.1.24
2
+
3
+ 2022-10-12 08:27
4
+
5
+ #### FIXED
6
+
7
+ - Force utf-8 encoding when reading files, should fix invalid byte sequence errors
8
+
9
+ ### 1.1.23
10
+
11
+ 2022-10-07 10:02
12
+
13
+ #### NEW
14
+
15
+ - Saved searches. Add `--save TITLE` to `tagged` or `find` commands to save the parameters for use with `na saved TITLE`
16
+
17
+ #### FIXED
18
+
19
+ - Restore wildcard capability of tag searches
20
+
1
21
  ### 1.1.22
2
22
 
3
23
  2022-10-07 05:58
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.1.22)
4
+ na (1.1.24)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
  gli (~> 2.21.0)
7
7
  tty-reader (~> 0.9, >= 0.9.0)
@@ -30,6 +30,7 @@ GEM
30
30
 
31
31
  PLATFORMS
32
32
  arm64-darwin-20
33
+ arm64-darwin-21
33
34
 
34
35
  DEPENDENCIES
35
36
  minitest (~> 5.14)
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.22
12
+ The current version of `na` is 1.1.24
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.
@@ -59,7 +59,7 @@ SYNOPSIS
59
59
  na [global options] command [command options] [arguments...]
60
60
 
61
61
  VERSION
62
- 1.1.22
62
+ 1.1.24
63
63
 
64
64
  GLOBAL OPTIONS
65
65
  -a, --[no-]add - Add a next action (deprecated, for backwards compatibility)
@@ -82,6 +82,7 @@ COMMANDS
82
82
  initconfig - Initialize the config file using current global options
83
83
  next, show - Show next actions
84
84
  prompt - Show or install prompt hooks for the current shell
85
+ saved - Execute a saved search
85
86
  tagged - Find actions matching a tag
86
87
  todos - Show list of known todo files
87
88
  ```
@@ -174,6 +175,7 @@ COMMAND OPTIONS
174
175
  --in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
175
176
  -o, --or - Combine search tokens with OR, displaying actions matching ANY of the terms
176
177
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
178
+ --save=TITLE - Save this search for future use (default: none)
177
179
  -v, --invert - Show actions not matching search pattern
178
180
  -x, --exact - Match pattern exactly
179
181
 
data/bin/na CHANGED
@@ -271,7 +271,16 @@ class App
271
271
  c.desc 'Show actions not matching search pattern'
272
272
  c.switch %i[v invert], negatable: false
273
273
 
274
+ c.desc 'Save this search for future use'
275
+ c.arg_name 'TITLE'
276
+ c.flag %i[save]
277
+
274
278
  c.action do |global_options, options, args|
279
+ if options[:save]
280
+ title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
281
+ NA.save_search(title, "find #{NA.command_line.map { |c| "\"#{c}\"" }.join(' ')}")
282
+ end
283
+
275
284
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
276
285
  3
277
286
  else
@@ -359,7 +368,16 @@ class App
359
368
  c.desc 'Show actions not matching tags'
360
369
  c.switch %i[v invert], negatable: false
361
370
 
371
+ c.desc 'Save this search for future use'
372
+ c.arg_name 'TITLE'
373
+ c.flag %i[save]
374
+
362
375
  c.action do |global_options, options, args|
376
+ if options[:save]
377
+ title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
378
+ NA.save_search(title, "tagged #{NA.command_line.map { |cmd| "\"#{cmd}\"" }.join(' ')}")
379
+ end
380
+
363
381
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
364
382
  3
365
383
  else
@@ -371,9 +389,10 @@ class App
371
389
  all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
372
390
  args.join(',').split(/ *, */).each do |arg|
373
391
  # TODO: <> comparisons do nothing right now
374
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>*$\^]+)(?:(?<op>[=<>*$\^]+)(?<val>.*?))?$/)
392
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
393
+
375
394
  tags.push({
376
- tag: m['tag'],
395
+ tag: m['tag'].wildcard_to_rx,
377
396
  comp: m['op'],
378
397
  value: m['val'],
379
398
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
@@ -387,9 +406,9 @@ class App
387
406
  options[:in].split(/ *, */).each do |a|
388
407
  m = a.match(/^(?<req>\+)?(?<tok>.*?)$/)
389
408
  todo.push({
390
- token: m['tok'],
391
- required: !m['req'].nil?
392
- })
409
+ token: m['tok'],
410
+ required: !m['req'].nil?
411
+ })
393
412
  end
394
413
  end
395
414
 
@@ -545,6 +564,26 @@ class App
545
564
  end
546
565
  end
547
566
 
567
+ desc 'Execute a saved search'
568
+ long_desc 'Run without argument to list saved searches'
569
+ arg_name 'SEARCH_TITLE', optional: true
570
+ command %i[saved] do |c|
571
+ c.action do |_global_options, _options, args|
572
+ searches = NA.load_searches
573
+ if args.empty?
574
+ NA.notify("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
575
+ NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
576
+ else
577
+ keys = searches.keys.delete_if { |k| k !~ /#{args[0]}/ }
578
+ NA.notify("{r}Search #{args[0]} not found", exit_code: 1) if keys.empty?
579
+
580
+ key = keys[0]
581
+ cmd = Shellwords.shellsplit(searches[key])
582
+ exit run(cmd)
583
+ end
584
+ end
585
+ end
586
+
548
587
  pre do |global, _command, _options, _args|
549
588
  NA.verbose = global[:debug]
550
589
  NA.extension = global[:ext]
@@ -574,6 +613,7 @@ class App
574
613
  end
575
614
  end
576
615
 
616
+ NA.command_line = ARGV
577
617
  @command = ARGV[0]
578
618
  $first_arg = ARGV[1]
579
619
 
data/lib/na/action.rb CHANGED
@@ -127,11 +127,14 @@ module NA
127
127
  end
128
128
 
129
129
  def compare_tag(tag)
130
- return false unless @tags.key?(tag[:tag])
130
+ keys = @tags.keys.delete_if { |k| k !~ Regexp.new(tag[:tag], Regexp::IGNORECASE) }
131
+ return false if keys.empty?
132
+
133
+ key = keys[0]
131
134
 
132
135
  return true if tag[:comp].nil?
133
136
 
134
- tag_val = @tags[tag[:tag]]
137
+ tag_val = @tags[key]
135
138
  val = tag[:value]
136
139
 
137
140
  return false if tag_val.nil?
@@ -3,7 +3,7 @@
3
3
  # Next Action methods
4
4
  module NA
5
5
  class << self
6
- attr_accessor :verbose, :extension, :na_tag
6
+ attr_accessor :verbose, :extension, :na_tag, :command_line
7
7
 
8
8
  ##
9
9
  ## Output to STDERR
@@ -15,11 +15,57 @@ module NA
15
15
  return if debug && !@verbose
16
16
 
17
17
  $stderr.puts NA::Color.template("{x}#{msg}{x}")
18
- if exit_code && exit_code.is_a?(Number)
18
+ if exit_code
19
19
  Process.exit exit_code
20
20
  end
21
21
  end
22
22
 
23
+ ##
24
+ ## Display and read a Yes/No prompt
25
+ ##
26
+ ## @param prompt [String] The prompt string
27
+ ## @param default [Boolean] default value if
28
+ ## return is pressed or prompt is
29
+ ## skipped
30
+ ##
31
+ ## @return [Boolean] result
32
+ ##
33
+ def yn(prompt, default: true)
34
+ return default unless $stdout.isatty
35
+
36
+ tty_state = `stty -g`
37
+ system 'stty raw -echo cbreak isig'
38
+ yn = color_single_options(default ? %w[Y n] : %w[y N])
39
+ $stdout.syswrite "\e[1;37m#{prompt} #{yn}\e[1;37m? \e[0m"
40
+ res = $stdin.sysread 1
41
+ res.chomp!
42
+ puts
43
+ system 'stty cooked'
44
+ system "stty #{tty_state}"
45
+ res.empty? ? default : res =~ /y/i
46
+ end
47
+
48
+ ##
49
+ ## Helper function to colorize the Y/N prompt
50
+ ##
51
+ ## @param choices [Array] The choices with
52
+ ## default capitalized
53
+ ##
54
+ ## @return [String] colorized string
55
+ ##
56
+ def color_single_options(choices = %w[y n])
57
+ out = []
58
+ choices.each do |choice|
59
+ case choice
60
+ when /[A-Z]/
61
+ out.push(NA::Color.template("{bw}#{choice}{x}"))
62
+ else
63
+ out.push(NA::Color.template("{dw}#{choice}{xg}"))
64
+ end
65
+ end
66
+ NA::Color.template("{xg}[#{out.join('/')}{xg}]{x}")
67
+ end
68
+
23
69
  ##
24
70
  ## Create a new todo file
25
71
  ##
@@ -98,7 +144,7 @@ module NA
98
144
  ## @param note [String] The note
99
145
  ##
100
146
  def add_action(file, project, action, note = nil)
101
- content = IO.read(file)
147
+ content = file.read_file
102
148
  # Insert the target project at the top if it doesn't exist
103
149
  unless content =~ /^[ \t]*#{project}:/i
104
150
  content = "#{project.cap_first}:\n#{content}"
@@ -209,7 +255,7 @@ module NA
209
255
 
210
256
  files.each do |file|
211
257
  save_working_dir(File.expand_path(file))
212
- content = IO.read(file)
258
+ content = file.read_file
213
259
  indent_level = 0
214
260
  parent = []
215
261
  content.split("\n").each do |line|
@@ -284,7 +330,7 @@ module NA
284
330
  db_file = 'tdlist.txt'
285
331
  file = File.join(db_dir, db_file)
286
332
  if File.exist?(file)
287
- dirs = IO.read(file).split("\n")
333
+ dirs = file.read_file.split("\n")
288
334
  dirs.delete_if { |f| !File.exist?(f) }
289
335
  File.open(file, 'w') { |f| f.puts dirs.join("\n") }
290
336
  end
@@ -295,7 +341,7 @@ module NA
295
341
  dirs = match_working_dir(query)
296
342
  else
297
343
  file = database_path
298
- content = File.exist?(file) ? IO.read(file).strip : ''
344
+ content = File.exist?(file) ? file.read_file.strip : ''
299
345
  notify('{br}Database empty', exit_code: 1) if content.empty?
300
346
 
301
347
  dirs = content.split(/\n/)
@@ -308,6 +354,50 @@ module NA
308
354
  puts NA::Color.template(dirs.join("\n"))
309
355
  end
310
356
 
357
+ def save_search(title, search)
358
+ file = database_path(file: 'saved_searches.yml')
359
+ searches = load_searches
360
+ title = title.gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
361
+
362
+ if searches.key?(title)
363
+ res = yn('Overwrite existing definition?', default: true)
364
+ notify('{r}Cancelled', exit_code: 0) unless res
365
+
366
+ end
367
+
368
+ searches[title] = search
369
+ File.open(file, 'w') { |f| f.puts(YAML.dump(searches)) }
370
+ NA.notify("{y}Search #{title} saved", exit_code: 0)
371
+ end
372
+
373
+ def load_searches
374
+ file = database_path(file: 'saved_searches.yml')
375
+ if File.exist?(file)
376
+ searches = YAML.safe_load(file.read_file)
377
+ else
378
+ searches = {
379
+ 'soon' => 'tagged "due<in 2 days,due>yesterday"',
380
+ 'overdue' => 'tagged "due<now"',
381
+ 'high' => 'tagged "prio>3"',
382
+ 'maybe' => 'tagged "maybe"'
383
+ }
384
+ File.open(file, 'w') { |f| f.puts(YAML.dump(searches)) }
385
+ end
386
+ searches
387
+ end
388
+
389
+ ##
390
+ ## Get path to database of known todo files
391
+ ##
392
+ ## @return [String] File path
393
+ ##
394
+ def database_path(file: 'tdlist.txt')
395
+ db_dir = File.expand_path('~/.local/share/na')
396
+ # Create directory if needed
397
+ FileUtils.mkdir_p(db_dir) unless File.directory?(db_dir)
398
+ File.join(db_dir, file)
399
+ end
400
+
311
401
  private
312
402
 
313
403
  ##
@@ -356,19 +446,6 @@ module NA
356
446
  [optional, required, negated]
357
447
  end
358
448
 
359
- ##
360
- ## Get path to database of known todo files
361
- ##
362
- ## @return [String] File path
363
- ##
364
- def database_path
365
- db_dir = File.expand_path('~/.local/share/na')
366
- # Create directory if needed
367
- FileUtils.mkdir_p(db_dir) unless File.directory?(db_dir)
368
- db_file = 'tdlist.txt'
369
- File.join(db_dir, db_file)
370
- end
371
-
372
449
  ##
373
450
  ## Find a matching path using semi-fuzzy matching.
374
451
  ## Search tokens can include ! and + to negate or make
@@ -382,7 +459,7 @@ module NA
382
459
  file = database_path
383
460
  notify('{r}No na database found', exit_code: 1) unless File.exist?(file)
384
461
 
385
- dirs = IO.read(file).split("\n")
462
+ dirs = file.read_file.split("\n")
386
463
 
387
464
  optional = search.map { |t| t[:token] }
388
465
  required = search.filter { |s| s[:required] }.map { |t| t[:token] }
@@ -401,7 +478,7 @@ module NA
401
478
  ##
402
479
  def save_working_dir(todo_file)
403
480
  file = database_path
404
- content = File.exist?(file) ? IO.read(file) : ''
481
+ content = File.exist?(file) ? file.read_file : ''
405
482
  dirs = content.split(/\n/)
406
483
  dirs.push(File.expand_path(todo_file))
407
484
  dirs.sort!.uniq!
data/lib/na/string.rb CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  # String helpers
4
4
  class ::String
5
+ def read_file
6
+ file = File.expand_path(self)
7
+ raise "Missing file #{file}" unless File.exist?(file)
8
+
9
+ # IO.read(file).force_encoding('ASCII-8BIT').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
10
+ IO.read(file).force_encoding('utf-8')
11
+ end
12
+
5
13
  ##
6
14
  ## Determine indentation level of line
7
15
  ##
@@ -106,7 +114,7 @@ class ::String
106
114
  ## @return [String] Regex string
107
115
  ##
108
116
  def wildcard_to_rx
109
- gsub(/\./, '\\.').gsub(/\*/, '[^ ]*?').gsub(/\?/, '.')
117
+ gsub(/\./, '\\.').gsub(/\?/, '.').gsub(/\*/, '[^ ]*?')
110
118
  end
111
119
 
112
120
  def cap_first!
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.1.22'
2
+ VERSION = '1.1.24'
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.1.21<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.1.23<!--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.22
4
+ version: 1.1.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-07 00:00:00.000000000 Z
11
+ date: 2022-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -90,22 +90,22 @@ dependencies:
90
90
  name: tty-reader
91
91
  requirement: !ruby/object:Gem::Requirement
92
92
  requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '0.9'
96
93
  - - ">="
97
94
  - !ruby/object:Gem::Version
98
95
  version: 0.9.0
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '0.9'
99
99
  type: :runtime
100
100
  prerelease: false
101
101
  version_requirements: !ruby/object:Gem::Requirement
102
102
  requirements:
103
- - - "~>"
104
- - !ruby/object:Gem::Version
105
- version: '0.9'
106
103
  - - ">="
107
104
  - !ruby/object:Gem::Version
108
105
  version: 0.9.0
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '0.9'
109
109
  - !ruby/object:Gem::Dependency
110
110
  name: tty-screen
111
111
  requirement: !ruby/object:Gem::Requirement
@@ -130,22 +130,22 @@ dependencies:
130
130
  name: tty-which
131
131
  requirement: !ruby/object:Gem::Requirement
132
132
  requirements:
133
- - - "~>"
134
- - !ruby/object:Gem::Version
135
- version: '0.5'
136
133
  - - ">="
137
134
  - !ruby/object:Gem::Version
138
135
  version: 0.5.0
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.5'
139
139
  type: :runtime
140
140
  prerelease: false
141
141
  version_requirements: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: '0.5'
146
143
  - - ">="
147
144
  - !ruby/object:Gem::Version
148
145
  version: 0.5.0
146
+ - - "~>"
147
+ - !ruby/object:Gem::Version
148
+ version: '0.5'
149
149
  - !ruby/object:Gem::Dependency
150
150
  name: chronic
151
151
  requirement: !ruby/object:Gem::Requirement
@@ -203,7 +203,7 @@ homepage: https://brettterpstra.com/projects/na/
203
203
  licenses:
204
204
  - MIT
205
205
  metadata: {}
206
- post_install_message:
206
+ post_install_message:
207
207
  rdoc_options:
208
208
  - "--title"
209
209
  - na
@@ -225,8 +225,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
225
225
  - !ruby/object:Gem::Version
226
226
  version: '0'
227
227
  requirements: []
228
- rubygems_version: 3.2.16
229
- signing_key:
228
+ rubygems_version: 3.0.3.1
229
+ signing_key:
230
230
  specification_version: 4
231
231
  summary: A command line tool for adding and listing project todos
232
232
  test_files: []