na 1.2.37 → 1.2.38

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: 35e6d784b55e397b8843663130837bcebb9041640a7833d650cedc9b783289e2
4
- data.tar.gz: a1ba45d7ba0d77e122667bc5c93629832dc379ee5d2275076182849055c8d72e
3
+ metadata.gz: 13c74e296d8e9d6e252949394c5471a27b1c7d3619026383fc75590dcb5d6046
4
+ data.tar.gz: c67deb62ee0ad5c2b3cf35b5ca39cebe2f6780e675cb48fbaec260068111a33a
5
5
  SHA512:
6
- metadata.gz: 1ee63a7b2bfd58496bf2047cd80a16f30bf255ca1edc8b0aa5a84a9abc8e4d1ccdd8082cd8f01fb8db60d5b73c7e2014f4f847369ac0d39274b5929ef8173f07
7
- data.tar.gz: d0321cddedf590c96a18cbd1096c7982240cb8c172c9f3491e0569ecfdf8882955898056ff3fd56d9e529d91be2a9ca56f3003bdbf6f1517a9658d08a302ca30
6
+ metadata.gz: 4fd8a183c2fcadf9d128bab65267b841bdd253db40071cb87c3fa0dbfd1be0517d667ba64f21b28f0a24adfbc08cbefe79c3638ac63736698de34a21c81d0658
7
+ data.tar.gz: 8cb24b17379576cf4f86f709a277c2e075fb41c905d67750f09500471f5948f0d3cd2e0d66d9f29a073172e6808c8d4f2b2dd0afe3b6b223ff04ec493ccd8289
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ### 1.2.38
2
+
3
+ 2023-09-03 11:25
4
+
5
+ #### NEW
6
+
7
+ - Open the todos database in an editor with `na todos --edit`
8
+ - A theme file is written to ~/.local/share/na/theme.yaml where you can modify the colors used for all displays
9
+ - Allow tag=~PATTERN comparison for regex matching
10
+
11
+ #### IMPROVED
12
+
13
+ - Better error message for `na next` when no todo is matched
14
+ - If STDOUT isn't a TTY, don't enable pagination, regardless of global setting
15
+ - Allow --find or --grep as synonyms for --search
16
+
17
+ #### FIXED
18
+
19
+ - Date tags containing hyphens triggered OR searches because they were initially interpreted as negative tag searches
20
+ - Templating irregularities
21
+ - Error thrown when running without $EDITOR variable defined in environment
22
+
1
23
  ### 1.2.37
2
24
 
3
25
  2023-09-01 12:42
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.36)
4
+ na (1.2.38)
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.37
12
+ The current version of `na` is 1.2.38
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.37
80
+ 1.2.38
81
81
 
82
82
  GLOBAL OPTIONS
83
83
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -114,6 +114,7 @@ COMMANDS
114
114
  prompt - Show or install prompt hooks for the current shell
115
115
  restore, unfinish - Find and remove @done tag from an action
116
116
  saved - Execute a saved search
117
+ tag - Add tags to matching action(s)
117
118
  tagged - Find actions matching a tag
118
119
  todos - Show list of known todo files
119
120
  undo - Undo the last change
@@ -291,7 +292,7 @@ COMMAND OPTIONS
291
292
  --omnifocus - Output actions nested by file and project
292
293
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
293
294
  --regex - Search query is regular expression
294
- --search=QUERY - Filter results using search terms (may be used more than once, default: none)
295
+ --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
295
296
  -t, --tag=TAG - Alternate tag to search for (default: none)
296
297
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
297
298
 
@@ -402,7 +403,7 @@ COMMAND OPTIONS
402
403
  --omnifocus - Output actions nested by file and project
403
404
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
404
405
  --regex - Search query is regular expression
405
- --search=QUERY - Filter results using search terms (may be used more than once, default: none)
406
+ --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
406
407
  -t, --tag=TAG - Alternate tag to search for (default: none)
407
408
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
408
409
 
@@ -428,10 +429,13 @@ NAME
428
429
 
429
430
  SYNOPSIS
430
431
 
431
- na [global options] todos [QUERY]
432
+ na [global options] todos [command options] [QUERY]
432
433
 
433
434
  DESCRIPTION
434
- Arguments will be interpreted as a query against which the list of todos will be fuzzy matched. Separate directories with /, :, or a space, e.g. `na todos code/marked`
435
+ Arguments will be interpreted as a query against which the list of todos will be fuzzy matched. Separate directories with /, :, or a space, e.g. `na todos code/marked`
436
+
437
+ COMMAND OPTIONS
438
+ -e, --[no-]edit - Open the todo database in an editor for manual modification
435
439
  ```
436
440
 
437
441
  ##### update
@@ -589,6 +593,75 @@ EXAMPLE
589
593
  na archive "An existing task"
590
594
  ```
591
595
 
596
+ ##### tag
597
+
598
+ Add, remove, or modify tags.
599
+
600
+ Use `na tag TAGNAME --[search|tagged] SEARCH_STRING` to add a tag to matching action (use `--all` to apply to all matching actions). If you use `!TAGNAME` it will remove that tag (regardless of value). To change the value of an existing tag (or add it if it doesn't exist), use `~TAGNAME(NEW VALUE)`.
601
+
602
+ ```
603
+ NAME
604
+ tag - Add tags to matching action(s)
605
+
606
+ SYNOPSIS
607
+
608
+ na [global options] tag [command options] TAG
609
+
610
+ DESCRIPTION
611
+ Provides an easy way to tag existing actions. Use !tag to remove a tag, use ~tag(new value) to change a tag or add a value. If multiple todo files are found in the current directory, a menu will allow you to pick which file to act on, or use --all to apply to all matches.
612
+
613
+ COMMAND OPTIONS
614
+ --all - Act on all matches immediately (no menu)
615
+ -d, --depth=DEPTH - Search for files X directories deep (default: 1)
616
+ --[no-]done - Include @done actions
617
+ -e, --regex - Interpret search pattern as regular expression
618
+ --file=PATH - Specify the file to search for the task (default: none)
619
+ --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
620
+ --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
621
+ --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
622
+ -x, --exact - Match pattern exactly
623
+
624
+ EXAMPLES
625
+
626
+ # Find "An existing task" action and add @project(warpspeed) to it
627
+ na tag "project(warpspeed)" --search "An existing task"
628
+
629
+ # Find all actions tagged @project2 and remove @project1 from them
630
+ na tag "!project1" --tagged project2 --all
631
+
632
+ # Remove @project2 from all actions
633
+ na tag "!project2" --all
634
+
635
+ # Find "An existing task" and change (or add) its @project tag value to "dirt nap"
636
+ na tag "~project(dirt nap)" --search "An existing task"
637
+ ```
638
+
639
+ ##### undo
640
+
641
+ Undoes the last file change resulting from an add or update command. If no argument is given, it undoes whatever the last change in history was. If an argument is provided, it's used to match against the change history, finding a specific file to restore from backup.
642
+
643
+ Only the most recent change can be undone.
644
+
645
+ ```
646
+ NAME
647
+ undo - Undo the last change
648
+
649
+ SYNOPSIS
650
+
651
+ na [global options] undo [FILE]...
652
+
653
+ DESCRIPTION
654
+ Run without argument to undo most recent change
655
+
656
+ EXAMPLES
657
+
658
+ # Undo the last change
659
+ na undo
660
+
661
+ # Undo the last change to a file matching "myproject"
662
+ na undo myproject
663
+ ```
664
+
592
665
  ### Configuration
593
666
 
594
667
  Global options such as todo extension and default next action tag can be stored permanently by using the `na initconfig` command. Run na with the global options you'd like to set, and add `initconfig` at the end of the command. A file will be written to `~/.na.rc`. You can edit this manually, or just update it using the `initconfig --force` command to overwrite it with new settings.
data/bin/commands/add.rb CHANGED
@@ -62,25 +62,23 @@ class App
62
62
  if NA.global_file
63
63
  target = File.expand_path(NA.global_file)
64
64
  unless File.exist?(target)
65
- res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
65
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Specified file not found, create it"), default: true)
66
66
  if res
67
67
  basename = File.basename(target, ".#{NA.extension}")
68
68
  NA.create_todo(target, basename, template: global_options[:template])
69
69
  else
70
- puts NA::Color.template('{r}Cancelled{x}')
71
- Process.exit 1
70
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
72
71
  end
73
72
  end
74
73
  elsif options[:file]
75
74
  target = File.expand_path(options[:file])
76
75
  unless File.exist?(target)
77
- res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
76
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Specified file not found, create it"), default: true)
78
77
  if res
79
78
  basename = File.basename(target, ".#{NA.extension}")
80
79
  NA.create_todo(target, basename, template: global_options[:template])
81
80
  else
82
- puts NA::Color.template('{r}Cancelled{x}')
83
- Process.exit 1
81
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
84
82
  end
85
83
  end
86
84
  elsif options[:todo]
@@ -102,8 +100,8 @@ class App
102
100
  target = File.expand_path(todo)
103
101
  unless File.exist?(target)
104
102
 
105
- res = NA.yn(NA::Color.template("{by}Specified file not found, create #{todo}"), default: true)
106
- NA.notify('{r}Cancelled{x}', exit_code: 1) unless res
103
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Specified file not found, create #{todo}"), default: true)
104
+ NA.notify("#{NA.theme[:error]}Cancelled{x}", exit_code: 1) unless res
107
105
 
108
106
  basename = File.basename(target, ".#{NA.extension}")
109
107
  NA.create_todo(target, basename, template: global_options[:template])
@@ -113,7 +111,7 @@ class App
113
111
  else
114
112
  files = NA.find_files(depth: options[:depth])
115
113
  if files.count.zero?
116
- res = NA.yn(NA::Color.template('{by}No todo file found, create one'), default: true)
114
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}No todo file found, create one"), default: true)
117
115
  if res
118
116
  basename = File.expand_path('.').split('/').last
119
117
  target = "#{basename}.#{NA.extension}"
@@ -122,7 +120,7 @@ class App
122
120
  end
123
121
  end
124
122
  target = files.count > 1 ? NA.select_file(files) : files[0]
125
- NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive? && File.exist?(target)
123
+ NA.notify("#{NA.theme[:error]}Cancelled{x}", exit_code: 1) unless files.count.positive? && File.exist?(target)
126
124
 
127
125
  end
128
126
 
@@ -131,8 +129,8 @@ class App
131
129
  elsif $stdin.isatty && TTY::Which.exist?('gum')
132
130
  `gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
133
131
  elsif $stdin.isatty
134
- puts NA::Color.template('{bm}Enter task:{x}')
135
- reader.read_line(NA::Color.template('{by}> {bw}')).strip
132
+ NA.notify("#{NA.theme[:prompt]}Enter task:")
133
+ reader.read_line(NA::Color.template("#{NA.theme[:warning]}> #{NA.theme[:action]}")).strip
136
134
  end
137
135
 
138
136
  if action.nil? || action.empty?
@@ -171,7 +169,7 @@ class App
171
169
  args << '--width $(tput cols)'
172
170
  `gum write #{args.join(' ')}`.strip.split("\n")
173
171
  else
174
- puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
172
+ NA.notify("#{NA.theme[:prompt]}Enter a note, {bw}CTRL-d#{NA.theme[:prompt]} to end editing#{NA.theme[:action]}")
175
173
  reader.read_multiline
176
174
  end
177
175
  end
data/bin/commands/edit.rb CHANGED
@@ -51,11 +51,11 @@ class App
51
51
  ]
52
52
  `gum input #{opts.join(' ')}`.strip
53
53
  elsif $stdin.isatty && options[:tagged].empty?
54
- puts NA::Color.template('{bm}Enter search string:{x}')
55
- reader.read_line(NA::Color.template('{by}> {bw}')).strip
54
+ NA.notify("#{NA.theme[:prompt]}Enter search string:")
55
+ reader.read_line(NA::Color.template("#{NA.theme[:warning]}> #{NA.theme[:action]}")).strip
56
56
  end
57
57
 
58
- NA.notify('{br}Empty input{x}', exit_code: 1) unless action
58
+ NA.notify("#{NA.theme[:error]}Empty input", exit_code: 1) if (action.nil? || action.empty?) && options[:tagged].empty?
59
59
 
60
60
  if action
61
61
  tokens = nil
@@ -79,32 +79,28 @@ class App
79
79
  end
80
80
 
81
81
  if (action.nil? || action.empty?) && options[:tagged].empty?
82
- NA.notify('{br}Empty input, cancelled{x}', exit_code: 1)
82
+ NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
83
83
  end
84
84
 
85
- all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
85
+ all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
86
86
  tags = []
87
87
  options[:tagged].join(',').split(/ *, */).each do |arg|
88
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
88
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>*$\^~]+?)(?:(?<op>[=<>~]{1,2}|[*$\^]=)(?<val>.*?))?$/)
89
89
 
90
90
  tags.push({
91
91
  tag: m['tag'].wildcard_to_rx,
92
92
  comp: m['op'],
93
93
  value: m['val'],
94
94
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
95
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
95
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/
96
96
  })
97
97
  end
98
98
 
99
- target_proj = if NA.cwd_is == :project
100
- NA.cwd
101
- else
102
- nil
103
- end
99
+ target_proj = NA.cwd_is == :project ? NA.cwd : nil
104
100
 
105
101
  if options[:file]
106
102
  file = File.expand_path(options[:file])
107
- NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
103
+ NA.notify("#{NA.theme[:error]}File not found", exit_code: 1) unless File.exist?(file)
108
104
 
109
105
  targets = [file]
110
106
  elsif options[:todo]
@@ -114,7 +110,7 @@ class App
114
110
  todo.push({
115
111
  token: m['tok'],
116
112
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
117
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
113
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/
118
114
  })
119
115
  end
120
116
  dirs = NA.match_working_dir(todo)
@@ -123,21 +119,21 @@ class App
123
119
  targets = [dirs[0]]
124
120
  elsif dirs.count.positive?
125
121
  targets = NA.select_file(dirs, multiple: true)
126
- NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
122
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
127
123
  else
128
- NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
124
+ NA.notify("#{NA.theme[:error]}Todo not found", exit_code: 1) unless targets && targets.count.positive?
129
125
 
130
126
  end
131
127
  else
132
128
  files = NA.find_files(depth: options[:depth])
133
- NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
129
+ NA.notify("#{NA.theme[:error]}No todo file found", exit_code: 1) if files.count.zero?
134
130
 
135
131
  targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
136
- NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
132
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
137
133
 
138
134
  end
139
135
 
140
- NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
136
+ NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
141
137
 
142
138
  targets.each do |target|
143
139
  NA.update_action(target,
data/bin/commands/find.rb CHANGED
@@ -73,7 +73,12 @@ class App
73
73
  if options[:exact] || options[:regex]
74
74
  search = args.join(' ')
75
75
  else
76
- search = args.join(' ').gsub(/(?<=\A|[ ,])(?<req>[+\-!])?@(?<tag>[^ *=<>$\^,@(]+)(?:\((?<value>.*?)\)| *(?<op>[=<>]{1,2}|[*$\^]=) *(?<val>.*?(?=\Z|[,@])))?/) do |arg|
76
+ rx = [
77
+ '(?<=\A|[ ,])(?<req>[+!-])?@(?<tag>[^ *=<>$*\^,@(]+)',
78
+ '(?:\((?<value>.*?)\)| *(?<op>[=<>~]{1,2}|[*$\^]=) *',
79
+ '(?<val>.*?(?=\Z|[,@])))?'
80
+ ].join('')
81
+ search = args.join(' ').gsub(Regexp.new(rx)) do
77
82
  m = Regexp.last_match
78
83
  string = if m['value']
79
84
  "#{m['req']}#{m['tag']}=#{m['value']}"
@@ -87,17 +92,17 @@ class App
87
92
 
88
93
  search = search.gsub(/ +/, ' ').strip
89
94
 
90
- all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
95
+ all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
91
96
  tags = []
92
97
  options[:tagged].join(',').split(/ *, */).each do |arg|
93
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?) *(?:(?<op>[=<>]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
98
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
94
99
 
95
100
  tags.push({
96
101
  tag: m['tag'].wildcard_to_rx,
97
102
  comp: m['op'],
98
103
  value: m['val'],
99
104
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
100
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
105
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
101
106
  })
102
107
  end
103
108
 
@@ -112,14 +117,14 @@ class App
112
117
  tokens = Regexp.new(search, Regexp::IGNORECASE)
113
118
  else
114
119
  tokens = []
115
- all_req = search !~ /[+!-]/ && !options[:or]
120
+ all_req = search !~ /(?<=[, ])[+!-]/ && !options[:or]
116
121
 
117
122
  search.split(/ /).each do |arg|
118
123
  m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
119
124
  tokens.push({
120
125
  token: Regexp.escape(m['tok']),
121
126
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
122
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
127
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
123
128
  })
124
129
  end
125
130
  end
data/bin/commands/init.rb CHANGED
@@ -14,7 +14,7 @@ class App
14
14
  project = args.join(' ')
15
15
  elsif
16
16
  project = File.expand_path('.').split('/').last
17
- project = reader.read_line(NA::Color.template('{y}Project name {bw}> {x}'), value: project).strip if $stdin.isatty
17
+ project = reader.read_line(NA::Color.template("#{NA.theme[:prompt]}Project name #{NA.theme[:filename]}> "), value: project).strip if $stdin.isatty
18
18
  end
19
19
 
20
20
  target = "#{project}.#{NA.extension}"
data/bin/commands/next.rb CHANGED
@@ -37,7 +37,7 @@ class App
37
37
 
38
38
  c.desc 'Filter results using search terms'
39
39
  c.arg_name 'QUERY'
40
- c.flag %i[search], multiple: true
40
+ c.flag %i[search find grep], multiple: true
41
41
 
42
42
  c.desc 'Search query is regular expression'
43
43
  c.switch %i[regex], negatable: false
@@ -75,71 +75,100 @@ class App
75
75
  options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
76
76
  end
77
77
 
78
- all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
78
+ if options[:exact] || options[:regex]
79
+ search = options[:search].join(' ')
80
+ else
81
+ rx = [
82
+ '(?<=\A|[ ,])(?<req>[+!-])?@(?<tag>[^ *=<>$~\^,@(]+)',
83
+ '(?:\((?<value>.*?)\)| *(?<op>=~|[=<>~]{1,2}|[*$\^]=) *',
84
+ '(?<val>.*?(?=\Z|[,@])))?'
85
+ ].join('')
86
+ search = options[:search].join(' ').gsub(Regexp.new(rx)) do
87
+ m = Regexp.last_match
88
+ string = if m['value']
89
+ "#{m['req']}#{m['tag']}=#{m['value']}"
90
+ else
91
+ m[0]
92
+ end
93
+ options[:tagged] << string.sub(/@/, '')
94
+ ''
95
+ end
96
+ end
97
+
98
+ search = search.gsub(/,/, '').gsub(/ +/, ' ') unless search.nil?
99
+
100
+ all_req = options[:tagged].join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
79
101
  tags = []
80
102
  options[:tagged].join(',').split(/ *, */).each do |arg|
81
- m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
103
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
82
104
 
83
105
  tags.push({
84
106
  tag: m['tag'].wildcard_to_rx,
85
107
  comp: m['op'],
86
108
  value: m['val'],
87
109
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
88
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
110
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
89
111
  })
90
112
  end
91
113
 
92
114
  args.concat(options[:in])
93
115
  if args.count.positive?
94
- all_req = args.join(' ') !~ /[+!-]/
116
+ all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
95
117
 
96
118
  tokens = []
97
119
  args.each do |arg|
98
120
  arg.split(/ *, */).each do |a|
99
- m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
121
+ m = a.match(/^(?<req>[+!-])?(?<tok>.*?)$/)
100
122
  tokens.push({
101
123
  token: m['tok'],
102
124
  required: !m['req'].nil? && m['req'] == '+',
103
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
125
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
104
126
  })
105
127
  end
106
128
  end
107
129
  end
108
130
 
109
- search = nil
110
- if options[:search]
111
- if options[:exact]
112
- search = options[:search].join(' ')
113
- elsif options[:regex]
114
- search = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
115
- else
116
- search = []
117
- all_req = options[:search].join(' ') !~ /[+!-]/ && !options[:or]
118
-
119
- options[:search].join(' ').split(/ /).each do |arg|
120
- m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
121
- search.push({
122
- token: m['tok'],
123
- required: all_req || (!m['req'].nil? && m['req'] == '+'),
124
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
125
- })
126
- end
131
+ search_for_done = false
132
+ tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
133
+ options[:done] = true if search_for_done
134
+
135
+ search_tokens = nil
136
+ if options[:exact]
137
+ search_tokens = search
138
+ elsif options[:regex]
139
+ search_tokens = Regexp.new(search, Regexp::IGNORECASE)
140
+ else
141
+ search_tokens = []
142
+ all_req = search !~ /(?<=[, ])[+!-]/ && !options[:or]
143
+
144
+ search.split(/ /).each do |arg|
145
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
146
+ search_tokens.push({
147
+ token: m['tok'],
148
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
149
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
150
+ })
127
151
  end
128
152
  end
129
153
 
130
154
  NA.na_tag = options[:tag] unless options[:tag].nil?
131
155
  require_na = true
132
156
 
133
- tag = [{ tag: NA.na_tag, value: nil }]
157
+ tag = [{ tag: NA.na_tag, value: nil, required: true, negate: false }]
134
158
  tag << { tag: 'done', value: nil, negate: true } unless options[:done]
135
159
  tag.concat(tags)
160
+
136
161
  todo = NA::Todo.new({ depth: depth,
137
162
  done: options[:done],
138
163
  query: tokens,
139
164
  tag: tag,
140
- search: search,
165
+ search: search_tokens,
141
166
  project: options[:project],
142
167
  require_na: require_na })
168
+ if todo.files.empty?
169
+ NA.notify("#{NA.theme[:error]}No matches found for #{tokens[0][:token]}.
170
+ Run `na todos` to see available todo files.")
171
+ end
143
172
  NA::Pager.paginate = false if options[:omnifocus]
144
173
  todo.actions.output(depth,
145
174
  files: todo.files,
@@ -16,7 +16,7 @@ class App
16
16
 
17
17
  c.action do |_global_options, options, args|
18
18
  if args.count.positive?
19
- all_req = args.join(' ') !~ /[+!-]/
19
+ all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
20
20
 
21
21
  tokens = [{ token: '*', required: all_req, negate: false }]
22
22
  args.each do |arg|
@@ -25,8 +25,8 @@ class App
25
25
 
26
26
  if args.empty?
27
27
  searches = NA.load_searches
28
- NA.notify("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
29
- NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
28
+ NA.notify("#{NA.theme[:success]}Saved searches stored in #{NA.database_path(file: 'saved_searches.yml').highlight_filename}")
29
+ NA.notify(searches.map { |k, v| "#{NA.theme[:filename]}#{k}: #{NA.theme[:values]}#{v}" }.join("\n"))
30
30
  else
31
31
  args.each do |arg|
32
32
  searches = NA.load_searches
@@ -34,13 +34,12 @@ class App
34
34
  NA.delete_search(arg) if options[:delete]
35
35
 
36
36
  keys = searches.keys.delete_if { |k| k !~ /#{arg}/ }
37
- NA.notify("{r}Search #{arg} not found", exit_code: 1) if keys.empty?
37
+ NA.notify("#{NA.theme[:error]}Search #{arg} not found", exit_code: 1) if keys.empty?
38
38
 
39
39
  key = keys[0]
40
40
  cmd = Shellwords.shellsplit(searches[key])
41
41
  run(cmd)
42
42
  end
43
- exit
44
43
  end
45
44
  end
46
45
  end
@@ -6,7 +6,7 @@ class App
6
6
  long_desc 'Finds actions with tags matching the arguments. An action is shown if it
7
7
  contains all of the tags listed. Add a + before a tag to make it required
8
8
  and others optional. You can specify values using TAG=VALUE pairs.
9
- Use <, >, and = for numeric comparisons, and *=, ^=, and $= for text comparisons.
9
+ Use <, >, and = for numeric comparisons, and *=, ^=, $=, or =~ (regex) for text comparisons.
10
10
  Date comparisons use natural language (`na tagged "due<=today"`) and
11
11
  are detected automatically.'
12
12
  arg_name 'TAG[=VALUE]'
@@ -38,7 +38,7 @@ class App
38
38
 
39
39
  c.desc 'Filter results using search terms'
40
40
  c.arg_name 'QUERY'
41
- c.flag %i[search], multiple: true
41
+ c.flag %i[search find grep], multiple: true
42
42
 
43
43
  c.desc 'Search query is regular expression'
44
44
  c.switch %i[regex], negatable: false
@@ -79,9 +79,9 @@ class App
79
79
 
80
80
  tags = []
81
81
 
82
- all_req = args.join(' ') !~ /[+!-]/ && !options[:or]
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({
@@ -106,7 +106,7 @@ class App
106
106
  tokens = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
107
107
  else
108
108
  tokens = []
109
- all_req = options[:search].join(' ') !~ /[+!-]/ && !options[:or]
109
+ all_req = options[:search].join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
110
110
 
111
111
  options[:search].join(' ').split(/ /).each do |arg|
112
112
  m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
@@ -122,7 +122,7 @@ class App
122
122
  todos = nil
123
123
  if options[:in]
124
124
  todos = []
125
- all_req = options[:in] !~ /[+!-]/ && !options[:or]
125
+ all_req = options[:in] !~ /(?<=[, ])[+!-]/ && !options[:or]
126
126
  options[:in].split(/ *, */).each do |a|
127
127
  m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
128
128
  todos.push({
@@ -133,7 +133,7 @@ class App
133
133
  end
134
134
  end
135
135
 
136
- NA.notify('{br}No actions matched search', exit_code: 1) if tags.empty? && tokens.empty?
136
+ NA.notify("#{NA.theme[:error]}No actions matched search", exit_code: 1) if tags.empty? && tokens.empty?
137
137
 
138
138
  todo = NA::Todo.new({ depth: depth,
139
139
  done: options[:done],