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 +4 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +1 -1
- data/README.md +79 -6
- data/bin/commands/add.rb +11 -13
- data/bin/commands/edit.rb +15 -19
- data/bin/commands/find.rb +11 -6
- data/bin/commands/init.rb +1 -1
- data/bin/commands/next.rb +56 -27
- data/bin/commands/projects.rb +1 -1
- data/bin/commands/saved.rb +3 -4
- data/bin/commands/tagged.rb +7 -7
- data/bin/commands/todos.rb +24 -14
- data/bin/commands/update.rb +24 -27
- data/bin/na +2 -1
- data/lib/na/action.rb +16 -25
- data/lib/na/actions.rb +8 -8
- data/lib/na/colors.rb +23 -1
- data/lib/na/editor.rb +13 -11
- data/lib/na/hash.rb +31 -0
- data/lib/na/next_action.rb +45 -38
- data/lib/na/pager.rb +1 -1
- data/lib/na/prompt.rb +6 -6
- data/lib/na/string.rb +23 -3
- data/lib/na/theme.rb +71 -0
- data/lib/na/todo.rb +2 -2
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/src/_README.md +35 -15
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13c74e296d8e9d6e252949394c5471a27b1c7d3619026383fc75590dcb5d6046
|
4
|
+
data.tar.gz: c67deb62ee0ad5c2b3cf35b5ca39cebe2f6780e675cb48fbaec260068111a33a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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.
|
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.
|
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
|
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
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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("{
|
106
|
-
NA.notify(
|
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(
|
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(
|
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
|
-
|
135
|
-
reader.read_line(NA::Color.template(
|
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
|
-
|
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
|
-
|
55
|
-
reader.read_line(NA::Color.template(
|
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(
|
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(
|
82
|
+
NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
|
83
83
|
end
|
84
84
|
|
85
|
-
all_req = options[:tagged].join(' ') !~ /[
|
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>[^
|
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 =
|
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(
|
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(
|
122
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
|
127
123
|
else
|
128
|
-
NA.notify(
|
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(
|
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(
|
132
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
|
137
133
|
|
138
134
|
end
|
139
135
|
|
140
|
-
NA.notify(
|
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
|
-
|
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(' ') !~ /[
|
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>[
|
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(
|
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
|
-
|
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>[^
|
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>[
|
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
|
-
|
110
|
-
if
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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:
|
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,
|
data/bin/commands/projects.rb
CHANGED
@@ -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|
|
data/bin/commands/saved.rb
CHANGED
@@ -25,8 +25,8 @@ class App
|
|
25
25
|
|
26
26
|
if args.empty?
|
27
27
|
searches = NA.load_searches
|
28
|
-
NA.notify("{
|
29
|
-
NA.notify(searches.map { |k, v| "{
|
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("{
|
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
|
data/bin/commands/tagged.rb
CHANGED
@@ -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 *=, ^=,
|
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>[
|
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(
|
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],
|