na 1.2.25 → 1.2.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +1 -1
- data/README.md +14 -12
- data/bin/na +11 -1064
- data/lib/na/version.rb +1 -1
- data/src/_README.md +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1ae4b30284b89cff9f9e5469ec923668c2b314e907b9f8c398b6b6cc46dee37
|
4
|
+
data.tar.gz: 4b4b84c3b995e38a1d02e487129324316a3949d36003418b09602a214a2d560e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1d145af86c822a521d991ff1dcc2e759a96a88d86b317249ed48eba1e60d1a5425f91c09d5a452418e75162b249fc1c55a74f3dc9ba2d6736dbe2dd3489d23a
|
7
|
+
data.tar.gz: 0aae383f45f4f73e96d15dd24d1e0002b2dcaca77ebf5e6dd7a0a6d956f3233c01e959497b8f60cd328c6d0a690f2575583f7760cb433a9ea5a60d33268425f6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
### 1.2.27
|
2
|
+
|
3
|
+
2023-08-21 10:58
|
4
|
+
|
5
|
+
#### NEW
|
6
|
+
|
7
|
+
- `na archive --done` to archive all @done actions
|
8
|
+
|
9
|
+
#### IMPROVED
|
10
|
+
|
11
|
+
- Split commands into separate files for easier maintenance
|
12
|
+
|
13
|
+
### 1.2.26
|
14
|
+
|
15
|
+
2023-08-21 10:26
|
16
|
+
|
17
|
+
#### NEW
|
18
|
+
|
19
|
+
- `na finish` and `na archive` subcommands as aliases for equivalant `update` commands
|
20
|
+
|
21
|
+
#### FIXED
|
22
|
+
|
23
|
+
- --archive flag for `na finish`
|
24
|
+
|
1
25
|
### 1.2.25
|
2
26
|
|
3
27
|
2023-08-21 07:23
|
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.27
|
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.27
|
81
81
|
|
82
82
|
GLOBAL OPTIONS
|
83
83
|
-a, --add - Add a next action (deprecated, for backwards compatibility)
|
@@ -98,7 +98,9 @@ GLOBAL OPTIONS
|
|
98
98
|
|
99
99
|
COMMANDS
|
100
100
|
add - Add a new next action
|
101
|
+
archive - Mark an action as @done and archive
|
101
102
|
changes, changelog - Display the changelog
|
103
|
+
complete, finish - Find and mark an action as @done
|
102
104
|
edit - Open a todo file in the default editor
|
103
105
|
find, grep - Find actions matching a search pattern
|
104
106
|
help - Shows a list of commands or help for one command
|
@@ -136,7 +138,7 @@ SYNOPSIS
|
|
136
138
|
na [global options] add [command options] ACTION
|
137
139
|
|
138
140
|
DESCRIPTION
|
139
|
-
Provides an easy way to store todos while you work. Add quick
|
141
|
+
Provides an easy way to store todos while you work. Add quick reminders and (if you set up Prompt Hooks) they'll automatically display next time you enter the directory. If multiple todo files are found in the current directory, a menu will allow you to pick to which file the action gets added.
|
140
142
|
|
141
143
|
COMMAND OPTIONS
|
142
144
|
--at=POSITION - Add task at [s]tart or [e]nd of target project (default: none)
|
@@ -173,7 +175,7 @@ SYNOPSIS
|
|
173
175
|
na [global options] edit [command options]
|
174
176
|
|
175
177
|
DESCRIPTION
|
176
|
-
Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim).
|
178
|
+
Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim). If more than one todo file is found, a menu is displayed.
|
177
179
|
|
178
180
|
COMMAND OPTIONS
|
179
181
|
-a, --app=EDITOR - Specify a Mac app (default: none)
|
@@ -186,7 +188,7 @@ EXAMPLES
|
|
186
188
|
na edit
|
187
189
|
|
188
190
|
# Display a menu of all todo files three levels deep from the
|
189
|
-
|
191
|
+
current directory, open selection in vim.
|
190
192
|
na edit -d 3 -a vim
|
191
193
|
```
|
192
194
|
|
@@ -205,7 +207,7 @@ SYNOPSIS
|
|
205
207
|
na [global options] find [command options] PATTERN
|
206
208
|
|
207
209
|
DESCRIPTION
|
208
|
-
Search tokens are separated by spaces. Actions matching all tokens in the pattern will be shown
|
210
|
+
Search tokens are separated by spaces. Actions matching all tokens in the pattern will be shown (partial matches allowed). Add a + before a token to make it required, e.g. `na find +feature +maybe`, add a - or ! to ignore matches containing that token.
|
209
211
|
|
210
212
|
COMMAND OPTIONS
|
211
213
|
-d, --depth=DEPTH - Recurse to depth (default: none)
|
@@ -272,7 +274,7 @@ SYNOPSIS
|
|
272
274
|
na [global options] next [command options] [QUERY]
|
273
275
|
|
274
276
|
DESCRIPTION
|
275
|
-
Next actions are actions which contain the next action tag (default @na),
|
277
|
+
Next actions are actions which contain the next action tag (default @na), do not contain @done, and are not in the Archive project. Arguments will target a todo file from history, whether it's in the current directory or not. Todo file queries can include path components separated by / or :, and may use wildcards (`*` to match any text, `?` to match a single character). Multiple queries allowed (separate arguments or separated by comma).
|
276
278
|
|
277
279
|
COMMAND OPTIONS
|
278
280
|
-d, --depth=DEPTH - Recurse to depth (default: none)
|
@@ -313,7 +315,7 @@ SYNOPSIS
|
|
313
315
|
na [global options] projects [command options] [QUERY]
|
314
316
|
|
315
317
|
DESCRIPTION
|
316
|
-
Arguments will be interpreted as a query for a known todo file,
|
318
|
+
Arguments will be interpreted as a query for a known todo file, fuzzy matched. Separate directories with /, :, or a space, e.g. `na projects code/marked`
|
317
319
|
|
318
320
|
COMMAND OPTIONS
|
319
321
|
-d, --depth=DEPTH - Search for files X directories deep (default: 1)
|
@@ -378,7 +380,7 @@ SYNOPSIS
|
|
378
380
|
na [global options] next [command options] [QUERY]
|
379
381
|
|
380
382
|
DESCRIPTION
|
381
|
-
Next actions are actions which contain the next action tag (default @na),
|
383
|
+
Next actions are actions which contain the next action tag (default @na), do not contain @done, and are not in the Archive project. Arguments will target a todo file from history, whether it's in the current directory or not. Todo file queries can include path components separated by / or :, and may use wildcards (`*` to match any text, `?` to match a single character). Multiple queries allowed (separate arguments or separated by comma).
|
382
384
|
|
383
385
|
COMMAND OPTIONS
|
384
386
|
-d, --depth=DEPTH - Recurse to depth (default: none)
|
@@ -419,7 +421,7 @@ SYNOPSIS
|
|
419
421
|
na [global options] todos [QUERY]
|
420
422
|
|
421
423
|
DESCRIPTION
|
422
|
-
Arguments will be interpreted as a query against which the
|
424
|
+
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`
|
423
425
|
```
|
424
426
|
|
425
427
|
##### update
|
@@ -465,7 +467,7 @@ SYNOPSIS
|
|
465
467
|
na [global options] update [command options] ACTION
|
466
468
|
|
467
469
|
DESCRIPTION
|
468
|
-
Provides an easy way to complete, prioritize, and tag existing actions.
|
470
|
+
Provides an easy way to complete, prioritize, and tag existing actions. If multiple todo files are found in the current directory, a menu will allow you to pick which file to act on.
|
469
471
|
|
470
472
|
COMMAND OPTIONS
|
471
473
|
-a, --archive - Add a @done tag to action and move to Archive
|
@@ -478,7 +480,7 @@ COMMAND OPTIONS
|
|
478
480
|
-f, --finish - Add a @done tag to action
|
479
481
|
--file=PATH - Specify the file to search for the task (default: none)
|
480
482
|
--in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
|
481
|
-
-n, --note - Prompt for additional notes. Input will be appended to any existing note.
|
483
|
+
-n, --note - Prompt for additional notes. Input will be appended to any existing note. If STDIN input (piped) is detected, it will be used as a note.
|
482
484
|
-o, --overwrite - Overwrite note instead of appending
|
483
485
|
-p, --priority=PRIO - Add/change a priority level 1-5 (default: 0)
|
484
486
|
-r, --remove=TAG - Remove a tag to the action (may be used more than once, default: none)
|
data/bin/na
CHANGED
@@ -7,8 +7,7 @@ require 'na'
|
|
7
7
|
require 'fcntl'
|
8
8
|
|
9
9
|
# Main application
|
10
|
-
|
11
|
-
extend GLI::App
|
10
|
+
include GLI::App
|
12
11
|
|
13
12
|
program_desc 'Add and list next actions for the current project'
|
14
13
|
|
@@ -73,1067 +72,16 @@ class App
|
|
73
72
|
desc 'Display verbose output'
|
74
73
|
switch %i[debug], default_value: false
|
75
74
|
|
76
|
-
|
77
|
-
|
78
|
-
|
75
|
+
# def add_commands(commands)
|
76
|
+
# commands = [commands] unless commands.is_a?(Array)
|
77
|
+
# commands.each { |cmd| require_relative "commands/#{cmd}" }
|
78
|
+
# end
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
c.example 'na next', desc: 'display the next actions from any todo files in the current directory'
|
86
|
-
c.example 'na next -d 3', desc: 'display the next actions from the current directory, traversing 3 levels deep'
|
87
|
-
c.example 'na next marked', desc: 'display next actions for a project you visited in the past'
|
88
|
-
|
89
|
-
c.desc 'Recurse to depth'
|
90
|
-
c.arg_name 'DEPTH'
|
91
|
-
c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
|
92
|
-
|
93
|
-
c.desc 'Display matches from a known todo file'
|
94
|
-
c.arg_name 'TODO_FILE'
|
95
|
-
c.flag %i[in todo], multiple: true
|
96
|
-
|
97
|
-
c.desc 'Alternate tag to search for'
|
98
|
-
c.arg_name 'TAG'
|
99
|
-
c.flag %i[t tag]
|
100
|
-
|
101
|
-
c.desc 'Show actions from a specific project'
|
102
|
-
c.arg_name 'PROJECT[/SUBPROJECT]'
|
103
|
-
c.flag %i[proj project]
|
104
|
-
|
105
|
-
c.desc 'Match actions containing tag. Allows value comparisons'
|
106
|
-
c.arg_name 'TAG'
|
107
|
-
c.flag %i[tagged], multiple: true
|
108
|
-
|
109
|
-
c.desc 'Filter results using search terms'
|
110
|
-
c.arg_name 'QUERY'
|
111
|
-
c.flag %i[search], multiple: true
|
112
|
-
|
113
|
-
c.desc 'Search query is regular expression'
|
114
|
-
c.switch %i[regex], negatable: false
|
115
|
-
|
116
|
-
c.desc 'Search query is exact text match (not tokens)'
|
117
|
-
c.switch %i[exact], negatable: false
|
118
|
-
|
119
|
-
c.desc 'Include notes in output'
|
120
|
-
c.switch %i[notes], negatable: true, default_value: false
|
121
|
-
|
122
|
-
c.desc 'Include @done actions'
|
123
|
-
c.switch %i[done]
|
124
|
-
|
125
|
-
c.desc 'Output actions nested by file'
|
126
|
-
c.switch %[nest], negatable: false
|
127
|
-
|
128
|
-
c.desc 'Output actions nested by file and project'
|
129
|
-
c.switch %[omnifocus], negatable: false
|
130
|
-
|
131
|
-
c.action do |global_options, options, args|
|
132
|
-
if global_options[:add]
|
133
|
-
cmd = ['add']
|
134
|
-
cmd.push('--note') if global_options[:note]
|
135
|
-
cmd.concat(['--priority', global_options[:priority]]) if global_options[:priority]
|
136
|
-
cmd.push(NA.command_line) if NA.command_line.count > 1
|
137
|
-
cmd.unshift(*NA.globals)
|
138
|
-
exit run(cmd)
|
139
|
-
end
|
140
|
-
|
141
|
-
options[:nest] = true if options[:omnifocus]
|
142
|
-
|
143
|
-
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
144
|
-
3
|
145
|
-
else
|
146
|
-
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
147
|
-
end
|
148
|
-
|
149
|
-
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
150
|
-
tags = []
|
151
|
-
options[:tagged].join(',').split(/ *, */).each do |arg|
|
152
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
153
|
-
|
154
|
-
tags.push({
|
155
|
-
tag: m['tag'].wildcard_to_rx,
|
156
|
-
comp: m['op'],
|
157
|
-
value: m['val'],
|
158
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
159
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
160
|
-
})
|
161
|
-
end
|
162
|
-
|
163
|
-
args.concat(options[:in])
|
164
|
-
if args.count.positive?
|
165
|
-
all_req = args.join(' ') !~ /[+!\-]/
|
166
|
-
|
167
|
-
tokens = []
|
168
|
-
args.each do |arg|
|
169
|
-
arg.split(/ *, */).each do |a|
|
170
|
-
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
171
|
-
tokens.push({
|
172
|
-
token: m['tok'],
|
173
|
-
required: !m['req'].nil? && m['req'] == '+',
|
174
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
175
|
-
})
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
search = nil
|
181
|
-
if options[:search]
|
182
|
-
if options[:exact]
|
183
|
-
search = options[:search].join(' ')
|
184
|
-
elsif options[:regex]
|
185
|
-
search = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
|
186
|
-
else
|
187
|
-
search = []
|
188
|
-
all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
|
189
|
-
|
190
|
-
options[:search].join(' ').split(/ /).each do |arg|
|
191
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
192
|
-
search.push({
|
193
|
-
token: m['tok'],
|
194
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
195
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
196
|
-
})
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
NA.na_tag = options[:tag] unless options[:tag].nil?
|
202
|
-
require_na = true
|
203
|
-
|
204
|
-
tag = [{ tag: NA.na_tag, value: nil }, { tag: 'done', value: nil, negate: true }]
|
205
|
-
tag.concat(tags)
|
206
|
-
files, actions, = NA.parse_actions(depth: depth,
|
207
|
-
done: options[:done],
|
208
|
-
query: tokens,
|
209
|
-
tag: tag,
|
210
|
-
search: search,
|
211
|
-
project: options[:project],
|
212
|
-
require_na: require_na)
|
213
|
-
|
214
|
-
NA.output_actions(actions, depth, files: files, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
desc 'Add a new next action'
|
219
|
-
long_desc 'Provides an easy way to store todos while you work. Add quick
|
220
|
-
reminders and (if you set up Prompt Hooks) they\'ll automatically display
|
221
|
-
next time you enter the directory.
|
222
|
-
|
223
|
-
If multiple todo files are found in the current directory, a menu will
|
224
|
-
allow you to pick to which file the action gets added.'
|
225
|
-
arg_name 'ACTION'
|
226
|
-
command :add do |c|
|
227
|
-
c.example 'na add "A cool feature I thought of @idea"', desc: 'Add a new action to the Inbox, including a tag'
|
228
|
-
c.example 'na add "A bug I need to fix" -p 4 -n',
|
229
|
-
desc: 'Add a new action to the Inbox, set its @priority to 4, and prompt for an additional note.'
|
230
|
-
c.example 'na add "An action item (with a note)"',
|
231
|
-
desc: 'A parenthetical at the end of an action is interpreted as a note'
|
232
|
-
|
233
|
-
c.desc 'Prompt for additional notes. STDIN input (piped) will be treated as a note if present.'
|
234
|
-
c.switch %i[n note], negatable: false
|
235
|
-
|
236
|
-
c.desc 'Add a priority level 1-5'
|
237
|
-
c.arg_name 'PRIO'
|
238
|
-
c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
|
239
|
-
|
240
|
-
c.desc 'Add action to specific project'
|
241
|
-
c.arg_name 'PROJECT'
|
242
|
-
c.default_value 'Inbox'
|
243
|
-
c.flag %i[to project proj]
|
244
|
-
|
245
|
-
c.desc 'Add task at [s]tart or [e]nd of target project'
|
246
|
-
c.arg_name 'POSITION'
|
247
|
-
c.flag %i[at], must_match: /^[sbea].*?$/i
|
248
|
-
|
249
|
-
c.desc 'Add to a known todo file, partial matches allowed'
|
250
|
-
c.arg_name 'TODO_FILE'
|
251
|
-
c.flag %i[in todo]
|
252
|
-
|
253
|
-
c.desc 'Use a tag other than the default next action tag'
|
254
|
-
c.arg_name 'TAG'
|
255
|
-
c.flag %i[t tag]
|
256
|
-
|
257
|
-
c.desc 'Don\'t add next action tag to new entry'
|
258
|
-
c.switch %i[x], negatable: false
|
259
|
-
|
260
|
-
c.desc 'Specify the file to which the task should be added'
|
261
|
-
c.arg_name 'PATH'
|
262
|
-
c.flag %i[f file]
|
263
|
-
|
264
|
-
c.desc 'Mark task as @done with date'
|
265
|
-
c.switch %i[finish done], negatable: false
|
266
|
-
|
267
|
-
c.desc 'Search for files X directories deep'
|
268
|
-
c.arg_name 'DEPTH'
|
269
|
-
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
270
|
-
|
271
|
-
c.action do |global_options, options, args|
|
272
|
-
reader = TTY::Reader.new
|
273
|
-
append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/
|
274
|
-
|
275
|
-
if NA.global_file
|
276
|
-
target = File.expand_path(NA.global_file)
|
277
|
-
unless File.exist?(target)
|
278
|
-
res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
|
279
|
-
if res
|
280
|
-
basename = File.basename(target, ".#{NA.extension}")
|
281
|
-
NA.create_todo(target, basename, template: global_options[:template])
|
282
|
-
else
|
283
|
-
puts NA::Color.template('{r}Cancelled{x}')
|
284
|
-
Process.exit 1
|
285
|
-
end
|
286
|
-
end
|
287
|
-
elsif options[:file]
|
288
|
-
target = File.expand_path(options[:file])
|
289
|
-
unless File.exist?(target)
|
290
|
-
res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
|
291
|
-
if res
|
292
|
-
basename = File.basename(target, ".#{NA.extension}")
|
293
|
-
NA.create_todo(target, basename, template: global_options[:template])
|
294
|
-
else
|
295
|
-
puts NA::Color.template('{r}Cancelled{x}')
|
296
|
-
Process.exit 1
|
297
|
-
end
|
298
|
-
end
|
299
|
-
elsif options[:todo]
|
300
|
-
todo = []
|
301
|
-
all_req = options[:todo] !~ /[+!\-]/
|
302
|
-
options[:todo].split(/ *, */).each do |a|
|
303
|
-
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
304
|
-
todo.push({
|
305
|
-
token: m['tok'],
|
306
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
307
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
308
|
-
})
|
309
|
-
end
|
310
|
-
dirs = NA.match_working_dir(todo)
|
311
|
-
if dirs.count.positive?
|
312
|
-
target = dirs[0]
|
313
|
-
else
|
314
|
-
todo = "#{options[:todo].sub(/#{NA.extension}$/, '')}.#{NA.extension}"
|
315
|
-
target = File.expand_path(todo)
|
316
|
-
unless File.exist?(target)
|
317
|
-
|
318
|
-
res = NA.yn(NA::Color.template("{by}Specified file not found, create #{todo}"), default: true)
|
319
|
-
NA.notify('{r}Cancelled{x}', exit_code: 1) unless res
|
320
|
-
|
321
|
-
basename = File.basename(target, ".#{NA.extension}")
|
322
|
-
NA.create_todo(target, basename, template: global_options[:template])
|
323
|
-
end
|
324
|
-
|
325
|
-
end
|
326
|
-
else
|
327
|
-
files = NA.find_files(depth: options[:depth])
|
328
|
-
if files.count.zero?
|
329
|
-
res = NA.yn(NA::Color.template('{by}No todo file found, create one'), default: true)
|
330
|
-
if res
|
331
|
-
basename = File.expand_path('.').split('/').last
|
332
|
-
target = "#{basename}.#{NA.extension}"
|
333
|
-
NA.create_todo(target, basename, template: global_options[:template])
|
334
|
-
files = NA.find_files(depth: 1)
|
335
|
-
end
|
336
|
-
end
|
337
|
-
target = files.count > 1 ? NA.select_file(files) : files[0]
|
338
|
-
NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive? && File.exist?(target)
|
339
|
-
|
340
|
-
end
|
341
|
-
|
342
|
-
action = if args.count.positive?
|
343
|
-
args.join(' ').strip
|
344
|
-
elsif $stdin.isatty && TTY::Which.exist?('gum')
|
345
|
-
`gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
|
346
|
-
elsif $stdin.isatty
|
347
|
-
puts NA::Color.template('{bm}Enter task:{x}')
|
348
|
-
reader.read_line(NA::Color.template('{by}> {bw}')).strip
|
349
|
-
end
|
350
|
-
|
351
|
-
if action.nil? || action.empty?
|
352
|
-
puts 'Empty input, cancelled'
|
353
|
-
Process.exit 1
|
354
|
-
end
|
355
|
-
|
356
|
-
if options[:priority]&.to_i&.positive?
|
357
|
-
action = "#{action.gsub(/@priority\(\d+\)/, '')} @priority(#{options[:priority]})"
|
358
|
-
end
|
359
|
-
|
360
|
-
note_rx = /^(.+) \((.*?)\)$/
|
361
|
-
split_note = if action =~ note_rx
|
362
|
-
n = Regexp.last_match(2)
|
363
|
-
action.sub!(note_rx, '\1').strip!
|
364
|
-
n
|
365
|
-
end
|
366
|
-
|
367
|
-
na_tag = NA.na_tag
|
368
|
-
if options[:x]
|
369
|
-
na_tag = ''
|
370
|
-
else
|
371
|
-
na_tag = options[:tag] unless options[:tag].nil?
|
372
|
-
na_tag = " @#{na_tag}"
|
373
|
-
end
|
374
|
-
|
375
|
-
action = "#{action.gsub(/#{na_tag}\b/, '')}#{na_tag}"
|
376
|
-
|
377
|
-
stdin_note = NA.stdin ? NA.stdin.split("\n") : []
|
378
|
-
|
379
|
-
line_note = if options[:note] && $stdin.isatty
|
380
|
-
puts stdin_note unless stdin_note.nil?
|
381
|
-
if TTY::Which.exist?('gum')
|
382
|
-
args = ['--placeholder "Enter additional note, CTRL-d to save"']
|
383
|
-
args << '--char-limit 0'
|
384
|
-
args << '--width $(tput cols)'
|
385
|
-
`gum write #{args.join(' ')}`.strip.split("\n")
|
386
|
-
else
|
387
|
-
puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
|
388
|
-
reader.read_multiline
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
note = stdin_note.empty? ? [] : stdin_note
|
393
|
-
note.concat(split_note) unless split_note.nil?
|
394
|
-
note.concat(line_note) unless line_note.nil?
|
395
|
-
|
396
|
-
NA.add_action(target, options[:project], action, note, finish: options[:finish], append: append)
|
397
|
-
end
|
398
|
-
end
|
399
|
-
|
400
|
-
desc 'Update an existing action'
|
401
|
-
long_desc 'Provides an easy way to complete, prioritize, and tag existing actions.
|
402
|
-
|
403
|
-
If multiple todo files are found in the current directory, a menu will
|
404
|
-
allow you to pick which file to act on.'
|
405
|
-
arg_name 'ACTION'
|
406
|
-
command %i[update] do |c|
|
407
|
-
c.example 'na update --remove na "An existing task"',
|
408
|
-
desc: 'Find "An existing task" action and remove the @na tag from it'
|
409
|
-
c.example 'na update --tag waiting "A bug I need to fix" -p 4 -n',
|
410
|
-
desc: 'Find "A bug..." action, add @waiting, add/update @priority(4), and prompt for an additional note'
|
411
|
-
c.example 'na update --archive My cool action',
|
412
|
-
desc: 'Add @done to "My cool action" and immediately move to Archive'
|
413
|
-
|
414
|
-
c.desc 'Prompt for additional notes. Input will be appended to any existing note.
|
415
|
-
If STDIN input (piped) is detected, it will be used as a note.'
|
416
|
-
c.switch %i[n note], negatable: false
|
417
|
-
|
418
|
-
c.desc 'Overwrite note instead of appending'
|
419
|
-
c.switch %i[o overwrite], negatable: false
|
420
|
-
|
421
|
-
c.desc 'Add/change a priority level 1-5'
|
422
|
-
c.arg_name 'PRIO'
|
423
|
-
c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
|
424
|
-
|
425
|
-
c.desc 'When moving task, add at [s]tart or [e]nd of target project'
|
426
|
-
c.arg_name 'POSITION'
|
427
|
-
c.flag %i[at], must_match: /^[sbea].*?$/i
|
428
|
-
|
429
|
-
c.desc 'Move action to specific project'
|
430
|
-
c.arg_name 'PROJECT'
|
431
|
-
c.flag %i[to project proj]
|
432
|
-
|
433
|
-
c.desc 'Use a known todo file, partial matches allowed'
|
434
|
-
c.arg_name 'TODO_FILE'
|
435
|
-
c.flag %i[in todo]
|
436
|
-
|
437
|
-
c.desc 'Include @done actions'
|
438
|
-
c.switch %i[done]
|
439
|
-
|
440
|
-
c.desc 'Add a tag to the action, @tag(values) allowed'
|
441
|
-
c.arg_name 'TAG'
|
442
|
-
c.flag %i[t tag], multiple: true
|
443
|
-
|
444
|
-
c.desc 'Remove a tag to the action'
|
445
|
-
c.arg_name 'TAG'
|
446
|
-
c.flag %i[r remove], multiple: true
|
447
|
-
|
448
|
-
c.desc 'Add a @done tag to action'
|
449
|
-
c.switch %i[f finish], negatable: false
|
450
|
-
|
451
|
-
c.desc 'Add a @done tag to action and move to Archive'
|
452
|
-
c.switch %i[a archive], negatable: false
|
453
|
-
|
454
|
-
c.desc 'Delete an action'
|
455
|
-
c.switch %i[delete], negatable: false
|
456
|
-
|
457
|
-
c.desc 'Specify the file to search for the task'
|
458
|
-
c.arg_name 'PATH'
|
459
|
-
c.flag %i[file]
|
460
|
-
|
461
|
-
c.desc 'Search for files X directories deep'
|
462
|
-
c.arg_name 'DEPTH'
|
463
|
-
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
464
|
-
|
465
|
-
c.desc 'Match actions containing tag. Allows value comparisons'
|
466
|
-
c.arg_name 'TAG'
|
467
|
-
c.flag %i[tagged], multiple: true
|
468
|
-
|
469
|
-
c.desc 'Act on all matches immediately (no menu)'
|
470
|
-
c.switch %i[all], negatable: false
|
471
|
-
|
472
|
-
c.desc 'Interpret search pattern as regular expression'
|
473
|
-
c.switch %i[e regex], negatable: false
|
474
|
-
|
475
|
-
c.desc 'Match pattern exactly'
|
476
|
-
c.switch %i[x exact], negatable: false
|
477
|
-
|
478
|
-
c.action do |global_options, options, args|
|
479
|
-
reader = TTY::Reader.new
|
480
|
-
append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/i
|
481
|
-
|
482
|
-
action = if args.count.positive?
|
483
|
-
args.join(' ').strip
|
484
|
-
elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
|
485
|
-
options = [
|
486
|
-
%(--placeholder "Enter a task to search for"),
|
487
|
-
'--char-limit=500',
|
488
|
-
"--width=#{TTY::Screen.columns}"
|
489
|
-
]
|
490
|
-
`gum input #{options.join(' ')}`.strip
|
491
|
-
elsif $stdin.isatty && options[:tagged].empty?
|
492
|
-
puts NA::Color.template('{bm}Enter search string:{x}')
|
493
|
-
reader.read_line(NA::Color.template('{by}> {bw}')).strip
|
494
|
-
end
|
495
|
-
|
496
|
-
if action
|
497
|
-
tokens = nil
|
498
|
-
if options[:exact]
|
499
|
-
tokens = action
|
500
|
-
elsif options[:regex]
|
501
|
-
tokens = Regexp.new(action, Regexp::IGNORECASE)
|
502
|
-
else
|
503
|
-
tokens = []
|
504
|
-
all_req = action !~ /[+!\-]/ && !options[:or]
|
505
|
-
|
506
|
-
action.split(/ /).each do |arg|
|
507
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
508
|
-
tokens.push({
|
509
|
-
token: m['tok'],
|
510
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
511
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
512
|
-
})
|
513
|
-
end
|
514
|
-
end
|
515
|
-
end
|
516
|
-
|
517
|
-
if (action.nil? || action.empty?) && options[:tagged].empty?
|
518
|
-
puts 'Empty input, cancelled'
|
519
|
-
Process.exit 1
|
520
|
-
end
|
521
|
-
|
522
|
-
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
523
|
-
tags = []
|
524
|
-
options[:tagged].join(',').split(/ *, */).each do |arg|
|
525
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
526
|
-
|
527
|
-
tags.push({
|
528
|
-
tag: m['tag'].wildcard_to_rx,
|
529
|
-
comp: m['op'],
|
530
|
-
value: m['val'],
|
531
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
532
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
533
|
-
})
|
534
|
-
end
|
535
|
-
|
536
|
-
priority = options[:priority].to_i if options[:priority]&.to_i&.positive?
|
537
|
-
add_tags = options[:tag].map { |t| t.sub(/^@/, '').wildcard_to_rx }
|
538
|
-
remove_tags = options[:remove].map { |t| t.sub(/^@/, '').wildcard_to_rx }
|
539
|
-
|
540
|
-
stdin_note = NA.stdin ? NA.stdin.split("\n") : []
|
541
|
-
|
542
|
-
line_note = if options[:note] && $stdin.isatty
|
543
|
-
puts stdin_note unless stdin_note.nil?
|
544
|
-
if TTY::Which.exist?('gum')
|
545
|
-
args = ['--placeholder "Enter a note, CTRL-d to save"']
|
546
|
-
args << '--char-limit 0'
|
547
|
-
args << '--width $(tput cols)'
|
548
|
-
`gum write #{args.join(' ')}`.strip.split("\n")
|
549
|
-
else
|
550
|
-
puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
|
551
|
-
reader.read_multiline
|
552
|
-
end
|
553
|
-
end
|
554
|
-
|
555
|
-
note = stdin_note.empty? ? [] : stdin_note
|
556
|
-
note.concat(line_note) unless line_note.nil? || line_note.empty?
|
557
|
-
|
558
|
-
target_proj = if options[:project]
|
559
|
-
options[:project]
|
560
|
-
elsif NA.cwd_is == :project
|
561
|
-
NA.cwd
|
562
|
-
else
|
563
|
-
nil
|
564
|
-
end
|
565
|
-
|
566
|
-
if options[:file]
|
567
|
-
file = File.expand_path(options[:file])
|
568
|
-
NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
|
569
|
-
|
570
|
-
targets = [file]
|
571
|
-
elsif options[:todo]
|
572
|
-
todo = []
|
573
|
-
options[:todo].split(/ *, */).each do |a|
|
574
|
-
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
575
|
-
todo.push({
|
576
|
-
token: m['tok'],
|
577
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
578
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
579
|
-
})
|
580
|
-
end
|
581
|
-
dirs = NA.match_working_dir(todo)
|
582
|
-
|
583
|
-
if dirs.count == 1
|
584
|
-
targets = [dirs[0]]
|
585
|
-
elsif dirs.count.positive?
|
586
|
-
targets = NA.select_file(dirs, multiple: true)
|
587
|
-
NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
|
588
|
-
else
|
589
|
-
NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
|
590
|
-
|
591
|
-
end
|
592
|
-
else
|
593
|
-
files = NA.find_files(depth: options[:depth])
|
594
|
-
NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
|
595
|
-
|
596
|
-
targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
|
597
|
-
NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
|
598
|
-
|
599
|
-
end
|
600
|
-
|
601
|
-
options[:finish] = true if options[:archive]
|
602
|
-
options[:project] = 'Archive' if options[:archive]
|
603
|
-
|
604
|
-
NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
605
|
-
|
606
|
-
targets.each do |target|
|
607
|
-
NA.update_action(target, tokens,
|
608
|
-
priority: priority,
|
609
|
-
add_tag: add_tags,
|
610
|
-
remove_tag: remove_tags,
|
611
|
-
finish: options[:finish],
|
612
|
-
project: target_proj,
|
613
|
-
delete: options[:delete],
|
614
|
-
note: note,
|
615
|
-
overwrite: options[:overwrite],
|
616
|
-
tagged: tags,
|
617
|
-
all: options[:all],
|
618
|
-
done: options[:done],
|
619
|
-
append: append)
|
620
|
-
end
|
621
|
-
end
|
622
|
-
end
|
623
|
-
|
624
|
-
desc 'Find actions matching a search pattern'
|
625
|
-
long_desc 'Search tokens are separated by spaces. Actions matching all tokens in the pattern will be shown
|
626
|
-
(partial matches allowed). Add a + before a token to make it required, e.g. `na find +feature +maybe`,
|
627
|
-
add a - or ! to ignore matches containing that token.'
|
628
|
-
arg_name 'PATTERN'
|
629
|
-
command %i[find grep] do |c|
|
630
|
-
c.example 'na find feature idea swift', desc: 'Find all actions containing feature, idea, and swift'
|
631
|
-
c.example 'na find feature idea -swift', desc: 'Find all actions containing feature and idea but NOT swift'
|
632
|
-
c.example 'na find -x feature idea', desc: 'Find all actions containing the exact text "feature idea"'
|
633
|
-
|
634
|
-
c.desc 'Interpret search pattern as regular expression'
|
635
|
-
c.switch %i[e regex], negatable: false
|
636
|
-
|
637
|
-
c.desc 'Match pattern exactly'
|
638
|
-
c.switch %i[x exact], negatable: false
|
639
|
-
|
640
|
-
c.desc 'Recurse to depth'
|
641
|
-
c.arg_name 'DEPTH'
|
642
|
-
c.flag %i[d depth], type: :integer, must_match: /^\d+$/
|
643
|
-
|
644
|
-
c.desc 'Show actions from a specific todo file in history. May use wildcards (* and ?)'
|
645
|
-
c.arg_name 'TODO_PATH'
|
646
|
-
c.flag %i[in]
|
647
|
-
|
648
|
-
c.desc 'Include notes in output'
|
649
|
-
c.switch %i[notes], negatable: true, default_value: false
|
650
|
-
|
651
|
-
c.desc 'Combine search tokens with OR, displaying actions matching ANY of the terms'
|
652
|
-
c.switch %i[o or], negatable: false
|
653
|
-
|
654
|
-
c.desc 'Show actions from a specific project'
|
655
|
-
c.arg_name 'PROJECT[/SUBPROJECT]'
|
656
|
-
c.flag %i[proj project]
|
657
|
-
|
658
|
-
c.desc 'Match actions containing tag. Allows value comparisons'
|
659
|
-
c.arg_name 'TAG'
|
660
|
-
c.flag %i[tagged], multiple: true
|
661
|
-
|
662
|
-
c.desc 'Include @done actions'
|
663
|
-
c.switch %i[done]
|
664
|
-
|
665
|
-
c.desc 'Show actions not matching search pattern'
|
666
|
-
c.switch %i[v invert], negatable: false
|
667
|
-
|
668
|
-
c.desc 'Save this search for future use'
|
669
|
-
c.arg_name 'TITLE'
|
670
|
-
c.flag %i[save]
|
671
|
-
|
672
|
-
c.desc 'Output actions nested by file'
|
673
|
-
c.switch %[nest], negatable: false
|
674
|
-
|
675
|
-
c.desc 'Output actions nested by file and project'
|
676
|
-
c.switch %[omnifocus], negatable: false
|
677
|
-
|
678
|
-
c.action do |global_options, options, args|
|
679
|
-
options[:nest] = true if options[:omnifocus]
|
680
|
-
|
681
|
-
if options[:save]
|
682
|
-
title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
|
683
|
-
NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
|
684
|
-
end
|
685
|
-
|
686
|
-
|
687
|
-
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
688
|
-
3
|
689
|
-
else
|
690
|
-
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
691
|
-
end
|
692
|
-
|
693
|
-
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
694
|
-
tags = []
|
695
|
-
options[:tagged].join(',').split(/ *, */).each do |arg|
|
696
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
697
|
-
|
698
|
-
tags.push({
|
699
|
-
tag: m['tag'].wildcard_to_rx,
|
700
|
-
comp: m['op'],
|
701
|
-
value: m['val'],
|
702
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
703
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
704
|
-
})
|
705
|
-
end
|
706
|
-
|
707
|
-
tokens = nil
|
708
|
-
if options[:exact]
|
709
|
-
tokens = args.join(' ')
|
710
|
-
elsif options[:regex]
|
711
|
-
tokens = Regexp.new(args.join(' '), Regexp::IGNORECASE)
|
712
|
-
else
|
713
|
-
tokens = []
|
714
|
-
all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
|
715
|
-
|
716
|
-
args.join(' ').split(/ /).each do |arg|
|
717
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
718
|
-
tokens.push({
|
719
|
-
token: m['tok'],
|
720
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
721
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
722
|
-
})
|
723
|
-
end
|
724
|
-
end
|
725
|
-
|
726
|
-
todo = nil
|
727
|
-
if options[:in]
|
728
|
-
todo = []
|
729
|
-
options[:in].split(/ *, */).each do |a|
|
730
|
-
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
731
|
-
todo.push({
|
732
|
-
token: m['tok'],
|
733
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
734
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
735
|
-
})
|
736
|
-
end
|
737
|
-
end
|
738
|
-
|
739
|
-
files, actions, = NA.parse_actions(depth: depth,
|
740
|
-
done: options[:done],
|
741
|
-
query: todo,
|
742
|
-
search: tokens,
|
743
|
-
tag: tags,
|
744
|
-
negate: options[:invert],
|
745
|
-
regex: options[:regex],
|
746
|
-
project: options[:project],
|
747
|
-
require_na: false)
|
748
|
-
regexes = if tokens.is_a?(Array)
|
749
|
-
tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
750
|
-
else
|
751
|
-
[tokens]
|
752
|
-
end
|
753
|
-
|
754
|
-
NA.output_actions(actions, depth, files: files, regexes: regexes, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
|
755
|
-
end
|
756
|
-
end
|
757
|
-
|
758
|
-
desc 'Find actions matching a tag'
|
759
|
-
long_desc 'Finds actions with tags matching the arguments. An action is shown if it
|
760
|
-
contains all of the tags listed. Add a + before a tag to make it required
|
761
|
-
and others optional. You can specify values using TAG=VALUE pairs.
|
762
|
-
Use <, >, and = for numeric comparisons, and *=, ^=, and $= for text comparisons.
|
763
|
-
Date comparisons use natural language (`na tagged "due<=today"`) and
|
764
|
-
are detected automatically.'
|
765
|
-
arg_name 'TAG[=VALUE]'
|
766
|
-
command %i[tagged] do |c|
|
767
|
-
c.example 'na tagged maybe', desc: 'Show all actions tagged @maybe'
|
768
|
-
c.example 'na tagged -d 3 "feature, idea"', desc: 'Show all actions tagged @feature AND @idea, recurse 3 levels'
|
769
|
-
c.example 'na tagged --or "feature, idea"', desc: 'Show all actions tagged @feature OR @idea'
|
770
|
-
c.example 'na tagged "priority>=4"', desc: 'Show actions with @priority(4) or @priority(5)'
|
771
|
-
c.example 'na tagged "due<in 2 days"', desc: 'Show actions with a due date coming up in the next 2 days'
|
772
|
-
|
773
|
-
c.desc 'Recurse to depth'
|
774
|
-
c.arg_name 'DEPTH'
|
775
|
-
c.default_value 1
|
776
|
-
c.flag %i[d depth], type: :integer, must_match: /^\d+$/
|
777
|
-
|
778
|
-
c.desc 'Show actions from a specific todo file in history. May use wildcards (* and ?)'
|
779
|
-
c.arg_name 'TODO_PATH'
|
780
|
-
c.flag %i[in]
|
781
|
-
|
782
|
-
c.desc 'Include notes in output'
|
783
|
-
c.switch %i[notes], negatable: true, default_value: false
|
784
|
-
|
785
|
-
c.desc 'Combine tags with OR, displaying actions matching ANY of the tags'
|
786
|
-
c.switch %i[o or], negatable: false
|
787
|
-
|
788
|
-
c.desc 'Show actions from a specific project'
|
789
|
-
c.arg_name 'PROJECT[/SUBPROJECT]'
|
790
|
-
c.flag %i[proj project]
|
791
|
-
|
792
|
-
c.desc 'Filter results using search terms'
|
793
|
-
c.arg_name 'QUERY'
|
794
|
-
c.flag %i[search], multiple: true
|
795
|
-
|
796
|
-
c.desc 'Search query is regular expression'
|
797
|
-
c.switch %i[regex], negatable: false
|
798
|
-
|
799
|
-
c.desc 'Search query is exact text match (not tokens)'
|
800
|
-
c.switch %i[exact], negatable: false
|
801
|
-
|
802
|
-
c.desc 'Include @done actions'
|
803
|
-
c.switch %i[done]
|
804
|
-
|
805
|
-
c.desc 'Show actions not matching tags'
|
806
|
-
c.switch %i[v invert], negatable: false
|
807
|
-
|
808
|
-
c.desc 'Save this search for future use'
|
809
|
-
c.arg_name 'TITLE'
|
810
|
-
c.flag %i[save]
|
811
|
-
|
812
|
-
c.desc 'Output actions nested by file'
|
813
|
-
c.switch %[nest], negatable: false
|
814
|
-
|
815
|
-
c.desc 'Output actions nested by file and project'
|
816
|
-
c.switch %[omnifocus], negatable: false
|
817
|
-
|
818
|
-
c.action do |global_options, options, args|
|
819
|
-
options[:nest] = true if options[:omnifocus]
|
820
|
-
|
821
|
-
if options[:save]
|
822
|
-
title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
|
823
|
-
NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
|
824
|
-
end
|
825
|
-
|
826
|
-
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
827
|
-
3
|
828
|
-
else
|
829
|
-
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
830
|
-
end
|
831
|
-
|
832
|
-
tags = []
|
833
|
-
|
834
|
-
all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
|
835
|
-
args.join(',').split(/ *, */).each do |arg|
|
836
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
837
|
-
|
838
|
-
tags.push({
|
839
|
-
tag: m['tag'].wildcard_to_rx,
|
840
|
-
comp: m['op'],
|
841
|
-
value: m['val'],
|
842
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
843
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
844
|
-
})
|
845
|
-
end
|
846
|
-
|
847
|
-
search_for_done = false
|
848
|
-
tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
|
849
|
-
tags.push({ tag: 'done', value: nil, negate: true}) unless search_for_done
|
850
|
-
|
851
|
-
tokens = nil
|
852
|
-
if options[:search]
|
853
|
-
if options[:exact]
|
854
|
-
tokens = options[:search].join(' ')
|
855
|
-
elsif options[:regex]
|
856
|
-
tokens = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
|
857
|
-
else
|
858
|
-
tokens = []
|
859
|
-
all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
|
860
|
-
|
861
|
-
options[:search].join(' ').split(/ /).each do |arg|
|
862
|
-
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
863
|
-
tokens.push({
|
864
|
-
token: m['tok'],
|
865
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
866
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
867
|
-
})
|
868
|
-
end
|
869
|
-
end
|
870
|
-
end
|
871
|
-
|
872
|
-
todo = nil
|
873
|
-
if options[:in]
|
874
|
-
todo = []
|
875
|
-
options[:in].split(/ *, */).each do |a|
|
876
|
-
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
877
|
-
todo.push({
|
878
|
-
token: m['tok'],
|
879
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
880
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
881
|
-
})
|
882
|
-
end
|
883
|
-
end
|
884
|
-
|
885
|
-
files, actions, = NA.parse_actions(depth: depth,
|
886
|
-
done: options[:done],
|
887
|
-
query: todo,
|
888
|
-
search: tokens,
|
889
|
-
tag: tags,
|
890
|
-
negate: options[:invert],
|
891
|
-
project: options[:project],
|
892
|
-
require_na: false)
|
893
|
-
# regexes = tags.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
894
|
-
regexes = if tokens.is_a?(Array)
|
895
|
-
tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
896
|
-
else
|
897
|
-
[tokens]
|
898
|
-
end
|
899
|
-
NA.output_actions(actions, depth, files: files, regexes: regexes, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
|
900
|
-
end
|
901
|
-
end
|
902
|
-
|
903
|
-
desc 'Create a new todo file in the current directory'
|
904
|
-
arg_name 'PROJECT', optional: true
|
905
|
-
command %i[init create] do |c|
|
906
|
-
c.example 'na init', desc: 'Generate a new todo file, prompting for project name'
|
907
|
-
c.example 'na init warpspeed', desc: 'Generate a new todo for a project called warpspeed'
|
908
|
-
|
909
|
-
c.action do |global_options, _options, args|
|
910
|
-
reader = TTY::Reader.new
|
911
|
-
if args.count.positive?
|
912
|
-
project = args.join(' ')
|
913
|
-
elsif
|
914
|
-
project = File.expand_path('.').split('/').last
|
915
|
-
project = reader.read_line(NA::Color.template('{y}Project name {bw}> {x}'), value: project).strip if $stdin.isatty
|
916
|
-
end
|
917
|
-
|
918
|
-
target = "#{project}.#{NA.extension}"
|
919
|
-
|
920
|
-
if File.exist?(target)
|
921
|
-
res = NA.yn(NA::Color.template("{r}File {bw}#{target}{r} already exists, overwrite it"), default: false)
|
922
|
-
Process.exit 1 unless res
|
923
|
-
|
924
|
-
end
|
925
|
-
|
926
|
-
NA.create_todo(target, project, template: global_options[:template])
|
927
|
-
end
|
928
|
-
end
|
929
|
-
|
930
|
-
desc 'Open a todo file in the default editor'
|
931
|
-
long_desc 'Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim).
|
932
|
-
If more than one todo file is found, a menu is displayed.'
|
933
|
-
command %i[edit] do |c|
|
934
|
-
c.example 'na edit', desc: 'Open the main todo file in the default editor'
|
935
|
-
c.example 'na edit -d 3 -a vim', desc: 'Display a menu of all todo files three levels deep from the
|
936
|
-
current directory, open selection in vim.'
|
937
|
-
|
938
|
-
c.desc 'Recurse to depth'
|
939
|
-
c.arg_name 'DEPTH'
|
940
|
-
c.default_value 1
|
941
|
-
c.flag %i[d depth], type: :integer, must_match: /^\d+$/
|
942
|
-
|
943
|
-
c.desc 'Specify an editor CLI'
|
944
|
-
c.arg_name 'EDITOR'
|
945
|
-
c.flag %i[e editor]
|
946
|
-
|
947
|
-
c.desc 'Specify a Mac app'
|
948
|
-
c.arg_name 'EDITOR'
|
949
|
-
c.flag %i[a app]
|
950
|
-
|
951
|
-
c.action do |global_options, options, args|
|
952
|
-
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
953
|
-
3
|
954
|
-
else
|
955
|
-
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
956
|
-
end
|
957
|
-
files = NA.find_files(depth: depth)
|
958
|
-
files.delete_if { |f| f !~ /.*?(#{args.join('|')}).*?.#{NA.extension}/ } if args.count.positive?
|
959
|
-
|
960
|
-
file = if files.count > 1
|
961
|
-
NA.select_file(files)
|
962
|
-
else
|
963
|
-
files[0]
|
964
|
-
end
|
965
|
-
|
966
|
-
if options[:editor]
|
967
|
-
system options[:editor], file
|
968
|
-
else
|
969
|
-
NA.edit_file(file: file, app: options[:app])
|
970
|
-
end
|
971
|
-
end
|
972
|
-
end
|
973
|
-
|
974
|
-
desc 'Show list of known todo files'
|
975
|
-
long_desc 'Arguments will be interpreted as a query against which the
|
976
|
-
list of todos will be fuzzy matched. Separate directories with
|
977
|
-
/, :, or a space, e.g. `na todos code/marked`'
|
978
|
-
arg_name 'QUERY', optional: true
|
979
|
-
command %i[todos] do |c|
|
980
|
-
c.action do |_global_options, _options, args|
|
981
|
-
if args.count.positive?
|
982
|
-
all_req = args.join(' ') !~ /[+!\-]/
|
983
|
-
|
984
|
-
tokens = [{ token: '*', required: all_req, negate: false }]
|
985
|
-
args.each do |arg|
|
986
|
-
arg.split(/ *, */).each do |a|
|
987
|
-
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
988
|
-
tokens.push({
|
989
|
-
token: m['tok'],
|
990
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
991
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
992
|
-
})
|
993
|
-
end
|
994
|
-
end
|
995
|
-
end
|
996
|
-
|
997
|
-
NA.list_todos(query: tokens)
|
998
|
-
end
|
999
|
-
end
|
1000
|
-
|
1001
|
-
desc 'Show list of projects for a file'
|
1002
|
-
long_desc 'Arguments will be interpreted as a query for a known todo file,
|
1003
|
-
fuzzy matched. Separate directories with /, :, or a space, e.g. `na projects code/marked`'
|
1004
|
-
arg_name 'QUERY', optional: true
|
1005
|
-
command %i[projects] do |c|
|
1006
|
-
c.desc 'Search for files X directories deep'
|
1007
|
-
c.arg_name 'DEPTH'
|
1008
|
-
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
1009
|
-
|
1010
|
-
c.desc 'Output projects as paths instead of hierarchy'
|
1011
|
-
c.switch %i[p paths], negatable: false
|
1012
|
-
|
1013
|
-
c.action do |_global_options, options, args|
|
1014
|
-
if args.count.positive?
|
1015
|
-
all_req = args.join(' ') !~ /[+!\-]/
|
1016
|
-
|
1017
|
-
tokens = [{ token: '*', required: all_req, negate: false }]
|
1018
|
-
args.each do |arg|
|
1019
|
-
arg.split(/ *, */).each do |a|
|
1020
|
-
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
1021
|
-
tokens.push({
|
1022
|
-
token: m['tok'],
|
1023
|
-
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
1024
|
-
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
1025
|
-
})
|
1026
|
-
end
|
1027
|
-
end
|
1028
|
-
end
|
1029
|
-
|
1030
|
-
NA.list_projects(query: tokens, depth: options[:depth], paths: options[:paths])
|
1031
|
-
end
|
1032
|
-
end
|
1033
|
-
|
1034
|
-
desc 'Show or install prompt hooks for the current shell'
|
1035
|
-
long_desc 'Installing the prompt hook allows you to automatically
|
1036
|
-
list next actions when you cd into a directory'
|
1037
|
-
command %i[prompt] do |c|
|
1038
|
-
c.desc 'Output the prompt hook for the current shell to STDOUT. Pass an argument to
|
1039
|
-
specify a shell (zsh, bash, fish)'
|
1040
|
-
c.arg_name 'SHELL', optional: true
|
1041
|
-
c.command %i[show] do |s|
|
1042
|
-
s.action do |_global_options, _options, args|
|
1043
|
-
shell = if args.count.positive?
|
1044
|
-
args[0]
|
1045
|
-
else
|
1046
|
-
File.basename(ENV['SHELL'])
|
1047
|
-
end
|
1048
|
-
|
1049
|
-
case shell
|
1050
|
-
when /^f/i
|
1051
|
-
NA::Prompt.show_prompt_hook(:fish)
|
1052
|
-
when /^z/i
|
1053
|
-
NA::Prompt.show_prompt_hook(:zsh)
|
1054
|
-
when /^b/i
|
1055
|
-
NA::Prompt.show_prompt_hook(:bash)
|
1056
|
-
end
|
1057
|
-
end
|
1058
|
-
end
|
1059
|
-
|
1060
|
-
c.desc 'Install the hook for the current shell to the appropriate startup file.'
|
1061
|
-
c.arg_name 'SHELL', optional: true
|
1062
|
-
c.command %i[install] do |s|
|
1063
|
-
s.action do |_global_options, _options, args|
|
1064
|
-
shell = if args.count.positive?
|
1065
|
-
args[0]
|
1066
|
-
else
|
1067
|
-
File.basename(ENV['SHELL'])
|
1068
|
-
end
|
1069
|
-
|
1070
|
-
case shell
|
1071
|
-
when /^f/i
|
1072
|
-
NA::Prompt.install_prompt_hook(:fish)
|
1073
|
-
when /^z/i
|
1074
|
-
NA::Prompt.install_prompt_hook(:zsh)
|
1075
|
-
when /^b/i
|
1076
|
-
NA::Prompt.install_prompt_hook(:bash)
|
1077
|
-
end
|
1078
|
-
end
|
1079
|
-
end
|
1080
|
-
end
|
1081
|
-
|
1082
|
-
desc 'Display the changelog'
|
1083
|
-
command %i[changes changelog] do |c|
|
1084
|
-
c.action do |_, _, _|
|
1085
|
-
changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
|
1086
|
-
pagers = [
|
1087
|
-
'mdless',
|
1088
|
-
'mdcat',
|
1089
|
-
'bat',
|
1090
|
-
ENV['PAGER'],
|
1091
|
-
'less -FXr',
|
1092
|
-
ENV['GIT_PAGER'],
|
1093
|
-
'more -r'
|
1094
|
-
]
|
1095
|
-
pager = pagers.find { |cmd| TTY::Which.exist?(cmd.split.first) }
|
1096
|
-
system %(#{pager} "#{changelog}")
|
1097
|
-
end
|
1098
|
-
end
|
1099
|
-
|
1100
|
-
desc 'Execute a saved search'
|
1101
|
-
long_desc 'Run without argument to list saved searches'
|
1102
|
-
arg_name 'SEARCH_TITLE', optional: true
|
1103
|
-
command %i[saved] do |c|
|
1104
|
-
c.example 'na tagged "+maybe,+priority<=3" --save maybelater', description: 'save a search called "maybelater"'
|
1105
|
-
c.example 'na saved maybelater', description: 'perform the search named "maybelater"'
|
1106
|
-
c.example 'na saved maybe',
|
1107
|
-
description: 'perform the search named "maybelater", assuming no other searches match "maybe"'
|
1108
|
-
c.example 'na maybe',
|
1109
|
-
description: 'na run with no command and a single argument automatically performs a matching saved search'
|
1110
|
-
c.example 'na saved', description: 'list available searches'
|
1111
|
-
|
1112
|
-
c.desc 'Open the saved search file in $EDITOR'
|
1113
|
-
c.switch %i[e edit], negatable: false
|
1114
|
-
|
1115
|
-
c.desc 'Delete the specified search definition'
|
1116
|
-
c.switch %i[d delete], negatable: false
|
1117
|
-
|
1118
|
-
c.action do |_global_options, options, args|
|
1119
|
-
NA.edit_searches if options[:edit]
|
1120
|
-
|
1121
|
-
searches = NA.load_searches
|
1122
|
-
if args.empty?
|
1123
|
-
NA.notify("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
|
1124
|
-
NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
|
1125
|
-
else
|
1126
|
-
NA.delete_search(args) if options[:delete]
|
1127
|
-
|
1128
|
-
keys = searches.keys.delete_if { |k| k !~ /#{args[0]}/ }
|
1129
|
-
NA.notify("{r}Search #{args[0]} not found", exit_code: 1) if keys.empty?
|
1130
|
-
|
1131
|
-
key = keys[0]
|
1132
|
-
cmd = Shellwords.shellsplit(searches[key])
|
1133
|
-
exit run(cmd)
|
1134
|
-
end
|
1135
|
-
end
|
1136
|
-
end
|
80
|
+
# ## Add/modify commands
|
81
|
+
# add_commands(%w[next add complete archive update])
|
82
|
+
# add_commands(%w[find tagged])
|
83
|
+
# add_commands(%w[init edit todos projects prompt changes saved])
|
84
|
+
commands_from File.expand_path('bin/commands')
|
1137
85
|
|
1138
86
|
pre do |global, _command, _options, _args|
|
1139
87
|
NA.verbose = global[:debug]
|
@@ -1180,7 +128,6 @@ class App
|
|
1180
128
|
true
|
1181
129
|
end
|
1182
130
|
end
|
1183
|
-
end
|
1184
131
|
|
1185
132
|
NA.stdin = $stdin.read.strip if $stdin.stat.size.positive? || $stdin.fcntl(Fcntl::F_GETFL, 0).zero?
|
1186
133
|
NA.stdin = nil unless NA.stdin && NA.stdin.length.positive?
|
@@ -1198,4 +145,4 @@ ARGV.each do |arg|
|
|
1198
145
|
end
|
1199
146
|
NA.command = NA.command_line[0]
|
1200
147
|
|
1201
|
-
exit
|
148
|
+
exit run(ARGV)
|
data/lib/na/version.rb
CHANGED
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.2.
|
12
|
+
The current version of `na` is <!--VER-->1.2.26<!--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
|
|