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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb2c991e3e916c35ac9820504725f1e8b41f0030c0f87a0672ec4cfcc5190c09
4
- data.tar.gz: 97de5c78cf2ce7f53a6268030b9c0fb5d4881c7f40e312d289207353fc53801a
3
+ metadata.gz: e1ae4b30284b89cff9f9e5469ec923668c2b314e907b9f8c398b6b6cc46dee37
4
+ data.tar.gz: 4b4b84c3b995e38a1d02e487129324316a3949d36003418b09602a214a2d560e
5
5
  SHA512:
6
- metadata.gz: 74a6012339df8e359f39942c481905de44c066a1d0c464ce942de7590d23a80da3fe68b79f86215faeaa5efc831e3b88f96630b51ca2285967afbd9b24956e07
7
- data.tar.gz: 134d9db7430525d873025c23b00bd57c2494038a8af9e4bb48d1544f39e804b065313ad4f542ebe871129fe09eda3fc4330ce796e870514e2370616e51291c22
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.25)
4
+ na (1.2.27)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
  gli (~> 2.21.0)
7
7
  mdless (~> 1.0, >= 1.0.32)
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is 1.2.25
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.25
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 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.
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). If more than one todo file is found, a menu is displayed.
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
- current directory, open selection in vim.
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 (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.
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), 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).
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, fuzzy matched. Separate directories with /, :, or a space, e.g. `na projects code/marked`
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), 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).
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 list of todos will be fuzzy matched. Separate directories with /, :, or a space, e.g. `na todos code/marked`
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. If multiple todo files are found in the current directory, a menu will allow you to pick which file to act on.
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. If STDIN input (piped) is detected, it will be used as a 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
- class App
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
- desc 'Show next actions'
77
- long_desc 'Next actions are actions which contain the next action tag (default @na),
78
- do not contain @done, and are not in the Archive project.
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
- Arguments will target a todo file from history, whether it\'s in the current
81
- directory or not. Todo file queries can include path components separated by /
82
- or :, and may use wildcards (`*` to match any text, `?` to match a single character). Multiple queries allowed (separate arguments or separated by comma).'
83
- arg_name 'QUERY', optional: true
84
- command %i[next show] do |c|
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 App.run(ARGV)
148
+ exit run(ARGV)
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.25'
2
+ VERSION = '1.2.27'
3
3
  end
data/src/_README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is <!--VER-->1.2.24<!--END VER-->.
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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: na
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.25
4
+ version: 1.2.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra