na 1.2.37 → 1.2.39
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/Gemfile.lock +1 -1
- data/README.md +82 -6
- data/bin/commands/add.rb +11 -16
- data/bin/commands/edit.rb +15 -27
- data/bin/commands/find.rb +16 -9
- data/bin/commands/init.rb +1 -1
- data/bin/commands/next.rb +65 -27
- data/bin/commands/projects.rb +1 -1
- data/bin/commands/saved.rb +22 -11
- data/bin/commands/tagged.rb +7 -7
- data/bin/commands/todos.rb +24 -14
- data/bin/commands/update.rb +24 -36
- data/bin/na +2 -1
- data/lib/na/action.rb +24 -26
- data/lib/na/actions.rb +8 -8
- data/lib/na/colors.rb +24 -2
- data/lib/na/editor.rb +13 -11
- data/lib/na/hash.rb +31 -0
- data/lib/na/next_action.rb +90 -49
- data/lib/na/pager.rb +1 -1
- data/lib/na/prompt.rb +6 -6
- data/lib/na/string.rb +53 -7
- 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: e55b998c33447bd876e7c26c1fcf7f136a5f3de847715f8565882b0df4d078f2
|
4
|
+
data.tar.gz: 8ce2a06c64b3e45291c185ffe9a38f9a6cb0fa3dc4b0eaf9f2de9a38bc295701
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cca3d6c0f98d5c30fdc5c22f6a10958b6888e08f2c22b92d13d698152465f756e491258fd4290f42f85062b7718068deabc43a07e4ea92891370f0ba35024a00
|
7
|
+
data.tar.gz: e20114aef91f0c9aa2edb15b95d335d86afa5aee03b6a4e214b2fdca7fa6acbc7b178e6f00d4348dd3329d6f88122f044591271f2886b732f8d046673e628403
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,42 @@
|
|
1
|
+
### 1.2.39
|
2
|
+
|
3
|
+
2023-09-06 04:25
|
4
|
+
|
5
|
+
#### NEW
|
6
|
+
|
7
|
+
- Add `--save NAME` to `na next` to save more complex queries and run with `na saved NAME` (or just `na NAME`)
|
8
|
+
- `na saved --select` flag to allow interactive selection of search(es)
|
9
|
+
|
10
|
+
#### IMPROVED
|
11
|
+
|
12
|
+
- Allow `na saved --delete` to handle multiple arguments
|
13
|
+
- Allow wildcards when deleting saved searches
|
14
|
+
- Refactor request for input, no change to user experience
|
15
|
+
- Refined wildcard (?*) handling
|
16
|
+
- When displaying actions wider than the screen, wrap at words and indent 2 spaces from start of action (after prefix)
|
17
|
+
|
18
|
+
### 1.2.38
|
19
|
+
|
20
|
+
2023-09-03 11:25
|
21
|
+
|
22
|
+
#### NEW
|
23
|
+
|
24
|
+
- Open the todos database in an editor with `na todos --edit`
|
25
|
+
- A theme file is written to ~/.local/share/na/theme.yaml where you can modify the colors used for all displays
|
26
|
+
- Allow tag=~PATTERN comparison for regex matching
|
27
|
+
|
28
|
+
#### IMPROVED
|
29
|
+
|
30
|
+
- Better error message for `na next` when no todo is matched
|
31
|
+
- If STDOUT isn't a TTY, don't enable pagination, regardless of global setting
|
32
|
+
- Allow --find or --grep as synonyms for --search
|
33
|
+
|
34
|
+
#### FIXED
|
35
|
+
|
36
|
+
- Date tags containing hyphens triggered OR searches because they were initially interpreted as negative tag searches
|
37
|
+
- Templating irregularities
|
38
|
+
- Error thrown when running without $EDITOR variable defined in environment
|
39
|
+
|
1
40
|
### 1.2.37
|
2
41
|
|
3
42
|
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.39
|
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.39
|
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,8 @@ 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
|
-
--
|
295
|
+
--save=TITLE - Save this search for future use (default: none)
|
296
|
+
--search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
|
295
297
|
-t, --tag=TAG - Alternate tag to search for (default: none)
|
296
298
|
--tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
|
297
299
|
|
@@ -352,6 +354,7 @@ DESCRIPTION
|
|
352
354
|
COMMAND OPTIONS
|
353
355
|
-d, --delete - Delete the specified search definition
|
354
356
|
-e, --edit - Open the saved search file in $EDITOR
|
357
|
+
-s, --select - Interactively select a saved search to run
|
355
358
|
|
356
359
|
EXAMPLES
|
357
360
|
|
@@ -402,7 +405,8 @@ COMMAND OPTIONS
|
|
402
405
|
--omnifocus - Output actions nested by file and project
|
403
406
|
--proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
|
404
407
|
--regex - Search query is regular expression
|
405
|
-
--
|
408
|
+
--save=TITLE - Save this search for future use (default: none)
|
409
|
+
--search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
|
406
410
|
-t, --tag=TAG - Alternate tag to search for (default: none)
|
407
411
|
--tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
|
408
412
|
|
@@ -428,10 +432,13 @@ NAME
|
|
428
432
|
|
429
433
|
SYNOPSIS
|
430
434
|
|
431
|
-
na [global options] todos [QUERY]
|
435
|
+
na [global options] todos [command options] [QUERY]
|
432
436
|
|
433
437
|
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`
|
438
|
+
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`
|
439
|
+
|
440
|
+
COMMAND OPTIONS
|
441
|
+
-e, --[no-]edit - Open the todo database in an editor for manual modification
|
435
442
|
```
|
436
443
|
|
437
444
|
##### update
|
@@ -589,6 +596,75 @@ EXAMPLE
|
|
589
596
|
na archive "An existing task"
|
590
597
|
```
|
591
598
|
|
599
|
+
##### tag
|
600
|
+
|
601
|
+
Add, remove, or modify tags.
|
602
|
+
|
603
|
+
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)`.
|
604
|
+
|
605
|
+
```
|
606
|
+
NAME
|
607
|
+
tag - Add tags to matching action(s)
|
608
|
+
|
609
|
+
SYNOPSIS
|
610
|
+
|
611
|
+
na [global options] tag [command options] TAG
|
612
|
+
|
613
|
+
DESCRIPTION
|
614
|
+
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.
|
615
|
+
|
616
|
+
COMMAND OPTIONS
|
617
|
+
--all - Act on all matches immediately (no menu)
|
618
|
+
-d, --depth=DEPTH - Search for files X directories deep (default: 1)
|
619
|
+
--[no-]done - Include @done actions
|
620
|
+
-e, --regex - Interpret search pattern as regular expression
|
621
|
+
--file=PATH - Specify the file to search for the task (default: none)
|
622
|
+
--in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
|
623
|
+
--search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
|
624
|
+
--tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
|
625
|
+
-x, --exact - Match pattern exactly
|
626
|
+
|
627
|
+
EXAMPLES
|
628
|
+
|
629
|
+
# Find "An existing task" action and add @project(warpspeed) to it
|
630
|
+
na tag "project(warpspeed)" --search "An existing task"
|
631
|
+
|
632
|
+
# Find all actions tagged @project2 and remove @project1 from them
|
633
|
+
na tag "!project1" --tagged project2 --all
|
634
|
+
|
635
|
+
# Remove @project2 from all actions
|
636
|
+
na tag "!project2" --all
|
637
|
+
|
638
|
+
# Find "An existing task" and change (or add) its @project tag value to "dirt nap"
|
639
|
+
na tag "~project(dirt nap)" --search "An existing task"
|
640
|
+
```
|
641
|
+
|
642
|
+
##### undo
|
643
|
+
|
644
|
+
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.
|
645
|
+
|
646
|
+
Only the most recent change can be undone.
|
647
|
+
|
648
|
+
```
|
649
|
+
NAME
|
650
|
+
undo - Undo the last change
|
651
|
+
|
652
|
+
SYNOPSIS
|
653
|
+
|
654
|
+
na [global options] undo [FILE]...
|
655
|
+
|
656
|
+
DESCRIPTION
|
657
|
+
Run without argument to undo most recent change
|
658
|
+
|
659
|
+
EXAMPLES
|
660
|
+
|
661
|
+
# Undo the last change
|
662
|
+
na undo
|
663
|
+
|
664
|
+
# Undo the last change to a file matching "myproject"
|
665
|
+
na undo myproject
|
666
|
+
```
|
667
|
+
|
592
668
|
### Configuration
|
593
669
|
|
594
670
|
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,17 +120,14 @@ 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
|
|
129
127
|
action = if args.count.positive?
|
130
128
|
args.join(' ').strip
|
131
|
-
|
132
|
-
|
133
|
-
elsif $stdin.isatty
|
134
|
-
puts NA::Color.template('{bm}Enter task:{x}')
|
135
|
-
reader.read_line(NA::Color.template('{by}> {bw}')).strip
|
129
|
+
else
|
130
|
+
NA.request_input(options, prompt: 'Enter a task')
|
136
131
|
end
|
137
132
|
|
138
133
|
if action.nil? || action.empty?
|
@@ -171,7 +166,7 @@ class App
|
|
171
166
|
args << '--width $(tput cols)'
|
172
167
|
`gum write #{args.join(' ')}`.strip.split("\n")
|
173
168
|
else
|
174
|
-
|
169
|
+
NA.notify("#{NA.theme[:prompt]}Enter a note, {bw}CTRL-d#{NA.theme[:prompt]} to end editing#{NA.theme[:action]}")
|
175
170
|
reader.read_multiline
|
176
171
|
end
|
177
172
|
end
|
data/bin/commands/edit.rb
CHANGED
@@ -43,19 +43,11 @@ class App
|
|
43
43
|
options[:edit] = true
|
44
44
|
action = if args.count.positive?
|
45
45
|
args.join(' ').strip
|
46
|
-
|
47
|
-
|
48
|
-
%(--placeholder "Enter a task to search for"),
|
49
|
-
'--char-limit=500',
|
50
|
-
"--width=#{TTY::Screen.columns}"
|
51
|
-
]
|
52
|
-
`gum input #{opts.join(' ')}`.strip
|
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
|
46
|
+
else
|
47
|
+
NA.request_input(options, prompt: 'Enter a task to search for')
|
56
48
|
end
|
57
49
|
|
58
|
-
NA.notify(
|
50
|
+
NA.notify("#{NA.theme[:error]}Empty input", exit_code: 1) if (action.nil? || action.empty?) && options[:tagged].empty?
|
59
51
|
|
60
52
|
if action
|
61
53
|
tokens = nil
|
@@ -79,32 +71,28 @@ class App
|
|
79
71
|
end
|
80
72
|
|
81
73
|
if (action.nil? || action.empty?) && options[:tagged].empty?
|
82
|
-
NA.notify(
|
74
|
+
NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
|
83
75
|
end
|
84
76
|
|
85
|
-
all_req = options[:tagged].join(' ') !~ /[
|
77
|
+
all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
|
86
78
|
tags = []
|
87
79
|
options[:tagged].join(',').split(/ *, */).each do |arg|
|
88
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^
|
80
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^~]+?)(?:(?<op>[=<>~]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
89
81
|
|
90
82
|
tags.push({
|
91
83
|
tag: m['tag'].wildcard_to_rx,
|
92
84
|
comp: m['op'],
|
93
85
|
value: m['val'],
|
94
86
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
95
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
87
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/
|
96
88
|
})
|
97
89
|
end
|
98
90
|
|
99
|
-
target_proj =
|
100
|
-
NA.cwd
|
101
|
-
else
|
102
|
-
nil
|
103
|
-
end
|
91
|
+
target_proj = NA.cwd_is == :project ? NA.cwd : nil
|
104
92
|
|
105
93
|
if options[:file]
|
106
94
|
file = File.expand_path(options[:file])
|
107
|
-
NA.notify(
|
95
|
+
NA.notify("#{NA.theme[:error]}File not found", exit_code: 1) unless File.exist?(file)
|
108
96
|
|
109
97
|
targets = [file]
|
110
98
|
elsif options[:todo]
|
@@ -114,7 +102,7 @@ class App
|
|
114
102
|
todo.push({
|
115
103
|
token: m['tok'],
|
116
104
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
117
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
105
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/
|
118
106
|
})
|
119
107
|
end
|
120
108
|
dirs = NA.match_working_dir(todo)
|
@@ -123,21 +111,21 @@ class App
|
|
123
111
|
targets = [dirs[0]]
|
124
112
|
elsif dirs.count.positive?
|
125
113
|
targets = NA.select_file(dirs, multiple: true)
|
126
|
-
NA.notify(
|
114
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
|
127
115
|
else
|
128
|
-
NA.notify(
|
116
|
+
NA.notify("#{NA.theme[:error]}Todo not found", exit_code: 1) unless targets && targets.count.positive?
|
129
117
|
|
130
118
|
end
|
131
119
|
else
|
132
120
|
files = NA.find_files(depth: options[:depth])
|
133
|
-
NA.notify(
|
121
|
+
NA.notify("#{NA.theme[:error]}No todo file found", exit_code: 1) if files.count.zero?
|
134
122
|
|
135
123
|
targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
|
136
|
-
NA.notify(
|
124
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
|
137
125
|
|
138
126
|
end
|
139
127
|
|
140
|
-
NA.notify(
|
128
|
+
NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
141
129
|
|
142
130
|
targets.each do |target|
|
143
131
|
NA.update_action(target,
|
data/bin/commands/find.rb
CHANGED
@@ -61,7 +61,8 @@ class App
|
|
61
61
|
|
62
62
|
if options[:save]
|
63
63
|
title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
|
64
|
-
|
64
|
+
cmd = NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')
|
65
|
+
NA.save_search(title, cmd)
|
65
66
|
end
|
66
67
|
|
67
68
|
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
@@ -73,7 +74,12 @@ class App
|
|
73
74
|
if options[:exact] || options[:regex]
|
74
75
|
search = args.join(' ')
|
75
76
|
else
|
76
|
-
|
77
|
+
rx = [
|
78
|
+
'(?<=\A|[ ,])(?<req>[+!-])?@(?<tag>[^ *=<>$*\^,@(]+)',
|
79
|
+
'(?:\((?<value>.*?)\)| *(?<op>[=<>~]{1,2}|[*$\^]=) *',
|
80
|
+
'(?<val>.*?(?=\Z|[,@])))?'
|
81
|
+
].join('')
|
82
|
+
search = args.join(' ').gsub(Regexp.new(rx)) do
|
77
83
|
m = Regexp.last_match
|
78
84
|
string = if m['value']
|
79
85
|
"#{m['req']}#{m['tag']}=#{m['value']}"
|
@@ -87,17 +93,17 @@ class App
|
|
87
93
|
|
88
94
|
search = search.gsub(/ +/, ' ').strip
|
89
95
|
|
90
|
-
all_req = options[:tagged].join(' ') !~ /[
|
96
|
+
all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
|
91
97
|
tags = []
|
92
98
|
options[:tagged].join(',').split(/ *, */).each do |arg|
|
93
|
-
m = arg.match(/^(?<req>[
|
99
|
+
m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
|
94
100
|
|
95
101
|
tags.push({
|
96
102
|
tag: m['tag'].wildcard_to_rx,
|
97
103
|
comp: m['op'],
|
98
104
|
value: m['val'],
|
99
105
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
100
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
106
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
101
107
|
})
|
102
108
|
end
|
103
109
|
|
@@ -112,14 +118,15 @@ class App
|
|
112
118
|
tokens = Regexp.new(search, Regexp::IGNORECASE)
|
113
119
|
else
|
114
120
|
tokens = []
|
115
|
-
all_req = search !~ /[+!-]/ && !options[:or]
|
121
|
+
all_req = search !~ /(?<=[, ])[+!-]/ && !options[:or]
|
116
122
|
|
117
123
|
search.split(/ /).each do |arg|
|
118
124
|
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
125
|
+
|
119
126
|
tokens.push({
|
120
|
-
token:
|
127
|
+
token: m['tok'],
|
121
128
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
122
|
-
negate: !m['req'].nil? && m['req'] =~ /[!-]/
|
129
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
123
130
|
})
|
124
131
|
end
|
125
132
|
end
|
@@ -150,7 +157,7 @@ class App
|
|
150
157
|
})
|
151
158
|
|
152
159
|
regexes = if tokens.is_a?(Array)
|
153
|
-
tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
160
|
+
tokens.delete_if { |token| token[:negate] }.map { |token| token[:token].wildcard_to_rx }
|
154
161
|
else
|
155
162
|
[tokens]
|
156
163
|
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
|
@@ -57,6 +57,10 @@ class App
|
|
57
57
|
c.desc 'Output actions nested by file and project'
|
58
58
|
c.switch %i[omnifocus], negatable: false
|
59
59
|
|
60
|
+
c.desc 'Save this search for future use'
|
61
|
+
c.arg_name 'TITLE'
|
62
|
+
c.flag %i[save]
|
63
|
+
|
60
64
|
c.action do |global_options, options, args|
|
61
65
|
if global_options[:add]
|
62
66
|
cmd = ['add']
|
@@ -67,6 +71,11 @@ class App
|
|
67
71
|
exit run(cmd)
|
68
72
|
end
|
69
73
|
|
74
|
+
if options[:save]
|
75
|
+
title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
|
76
|
+
NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
|
77
|
+
end
|
78
|
+
|
70
79
|
options[:nest] = true if options[:omnifocus]
|
71
80
|
|
72
81
|
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
@@ -75,71 +84,100 @@ class App
|
|
75
84
|
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
76
85
|
end
|
77
86
|
|
78
|
-
|
87
|
+
if options[:exact] || options[:regex]
|
88
|
+
search = options[:search].join(' ')
|
89
|
+
else
|
90
|
+
rx = [
|
91
|
+
'(?<=\A|[ ,])(?<req>[+!-])?@(?<tag>[^ *=<>$~\^,@(]+)',
|
92
|
+
'(?:\((?<value>.*?)\)| *(?<op>=~|[=<>~]{1,2}|[*$\^]=) *',
|
93
|
+
'(?<val>.*?(?=\Z|[,@])))?'
|
94
|
+
].join('')
|
95
|
+
search = options[:search].join(' ').gsub(Regexp.new(rx)) do
|
96
|
+
m = Regexp.last_match
|
97
|
+
string = if m['value']
|
98
|
+
"#{m['req']}#{m['tag']}=#{m['value']}"
|
99
|
+
else
|
100
|
+
m[0]
|
101
|
+
end
|
102
|
+
options[:tagged] << string.sub(/@/, '')
|
103
|
+
''
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
search = search.gsub(/,/, '').gsub(/ +/, ' ') unless search.nil?
|
108
|
+
|
109
|
+
all_req = options[:tagged].join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
|
79
110
|
tags = []
|
80
111
|
options[:tagged].join(',').split(/ *, */).each do |arg|
|
81
|
-
m = arg.match(/^(?<req>[+!-])?(?<tag>[^
|
112
|
+
m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
|
82
113
|
|
83
114
|
tags.push({
|
84
115
|
tag: m['tag'].wildcard_to_rx,
|
85
116
|
comp: m['op'],
|
86
117
|
value: m['val'],
|
87
118
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
88
|
-
negate: !m['req'].nil? && m['req'] =~ /[!-]/
|
119
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
89
120
|
})
|
90
121
|
end
|
91
122
|
|
92
123
|
args.concat(options[:in])
|
93
124
|
if args.count.positive?
|
94
|
-
all_req = args.join(' ') !~ /[+!-]/
|
125
|
+
all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
|
95
126
|
|
96
127
|
tokens = []
|
97
128
|
args.each do |arg|
|
98
129
|
arg.split(/ *, */).each do |a|
|
99
|
-
m = a.match(/^(?<req>[
|
130
|
+
m = a.match(/^(?<req>[+!-])?(?<tok>.*?)$/)
|
100
131
|
tokens.push({
|
101
132
|
token: m['tok'],
|
102
133
|
required: !m['req'].nil? && m['req'] == '+',
|
103
|
-
negate: !m['req'].nil? && m['req'] =~ /[!-]/
|
134
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
104
135
|
})
|
105
136
|
end
|
106
137
|
end
|
107
138
|
end
|
108
139
|
|
109
|
-
|
110
|
-
if
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
140
|
+
search_for_done = false
|
141
|
+
tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
|
142
|
+
options[:done] = true if search_for_done
|
143
|
+
|
144
|
+
search_tokens = nil
|
145
|
+
if options[:exact]
|
146
|
+
search_tokens = search
|
147
|
+
elsif options[:regex]
|
148
|
+
search_tokens = Regexp.new(search, Regexp::IGNORECASE)
|
149
|
+
else
|
150
|
+
search_tokens = []
|
151
|
+
all_req = search !~ /(?<=[, ])[+!-]/ && !options[:or]
|
152
|
+
|
153
|
+
search.split(/ /).each do |arg|
|
154
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
155
|
+
search_tokens.push({
|
156
|
+
token: m['tok'],
|
157
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
158
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
159
|
+
})
|
127
160
|
end
|
128
161
|
end
|
129
162
|
|
130
163
|
NA.na_tag = options[:tag] unless options[:tag].nil?
|
131
164
|
require_na = true
|
132
165
|
|
133
|
-
tag = [{ tag: NA.na_tag, value: nil }]
|
166
|
+
tag = [{ tag: NA.na_tag, value: nil, required: true, negate: false }]
|
134
167
|
tag << { tag: 'done', value: nil, negate: true } unless options[:done]
|
135
168
|
tag.concat(tags)
|
169
|
+
|
136
170
|
todo = NA::Todo.new({ depth: depth,
|
137
171
|
done: options[:done],
|
138
172
|
query: tokens,
|
139
173
|
tag: tag,
|
140
|
-
search:
|
174
|
+
search: search_tokens,
|
141
175
|
project: options[:project],
|
142
176
|
require_na: require_na })
|
177
|
+
if todo.files.empty?
|
178
|
+
NA.notify("#{NA.theme[:error]}No matches found for #{tokens[0][:token]}.
|
179
|
+
Run `na todos` to see available todo files.")
|
180
|
+
end
|
143
181
|
NA::Pager.paginate = false if options[:omnifocus]
|
144
182
|
todo.actions.output(depth,
|
145
183
|
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|
|