na 1.2.1 → 1.2.2

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: e7e57d75babca99bccbea082d51f20180c8089b677d2ae6a7088931b0af43fd5
4
- data.tar.gz: 813a861742e460996b8074ce1d7603e9d3b9856a9fd2f5ed4e85e5c04fffbea0
3
+ metadata.gz: ba10397406ce9b754a63fcd7787eb5cc22923d5564de8eca50cb8bb6000115e7
4
+ data.tar.gz: c7c47d9531177787b1615857135d8bc69ab3b3a3595aad30d357fa39f9b41fe5
5
5
  SHA512:
6
- metadata.gz: f80bf3d000c6fe458c807e202a9da1aa09cf6d702b896abf93c494b876597d4efe352ed9132ce9ef3a820008794e795885c67cf41ebce374f10c675c1a5c77c2
7
- data.tar.gz: 584e5f7da19d8c5d877897b2038e14164c6d5278201da89cc6cff8d21cc0aeb29b5be560f45dffc8a65f0756d1e3ee1e9f45760fe838485d21c8fe93bb3eefdf
6
+ metadata.gz: 6861532283cf20d55f9fddff1979209024301863f01dfbe1d79d83096e397ab94b59a45715f1d1486939afce8e2fbb2b90196bbca5cb8a93ae5d02636804442d
7
+ data.tar.gz: 9ebceba70f481a722c616d1ad819c23bd9dab13abebf68371fd966f0834585826596084e27bf27cc632515a4a7c7b162abedc4f997672cb7439ab22bf5ace8f1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ### 1.2.2
2
+
3
+ 2022-10-25 14:30
4
+
5
+ #### CHANGED
6
+
7
+ - `na update --done` now means "include @done actions in search"
8
+ - `na next --in QUERY` now searches for a known todo file (formerly required arguments, now both work)
9
+
10
+ #### NEW
11
+
12
+ - `--at [start|end]` switch for `add` and `update` to determine
13
+ - Global `--file PATH` flag to specify a single global todo file,
14
+ - `--add_at [start|end]` global flag that can be added to config to make permanent
15
+ - `--finish` switch for `na add` to immediately mark an action as @done
16
+ - `--cwd_as [project|tag]` global flag when using a global `--file` to determine if the current working directory (last element) is added as an @tag or parent project
17
+
18
+ #### IMPROVED
19
+
20
+ - Refactor `na add` to use improved task update code
21
+ - Confirm target file before requesting task when running `na
22
+
1
23
  ### 1.2.1
2
24
 
3
25
  2022-10-22 10:18
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.1)
4
+ na (1.2.2)
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.1
12
+ The current version of `na` is 1.2.2
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.
@@ -49,6 +49,10 @@ You can also quickly add todo items from the command line with the `add` subcomm
49
49
 
50
50
  If found, it will try to locate an `Inbox:` project, or create one if it doesn't exist. Any arguments after `add` will be combined to create a new task in TaskPaper format. They will automatically be assigned as next actions (tagged `@na`) and will show up when `na` lists the tasks for the project.
51
51
 
52
+ #### Updating todos
53
+
54
+ You can mark todos as complete, delete them, add and remove tags, change priority, and even move them between projects with the `na update` command.
55
+
52
56
  ### Usage
53
57
 
54
58
  ```
@@ -59,13 +63,16 @@ SYNOPSIS
59
63
  na [global options] command [command options] [arguments...]
60
64
 
61
65
  VERSION
62
- 1.2.1
66
+ 1.2.2
63
67
 
64
68
  GLOBAL OPTIONS
65
69
  -a, --[no-]add - Add a next action (deprecated, for backwards compatibility)
70
+ --add_at=POSITION - Add all new/moved entries at [s]tart or [e]nd of target project (default: end)
71
+ --cwd_as=TYPE - Use current working directory as [p]roject, [t]ag, or [n]one (default: none)
66
72
  -d, --depth=DEPTH - Recurse to depth (default: 1)
67
- --[no-]debug - Display verbose output
73
+ --[no-]debug - Display verbose output (default: enabled)
68
74
  --ext=EXT - File extension to consider a todo file (default: taskpaper)
75
+ -f, --file=PATH - Use a single file as global todo, use --initconfig to make permanent (default: none)
69
76
  --help - Show this message
70
77
  -n, --note - Prompt for additional notes (deprecated, for backwards compatibility)
71
78
  -p, --priority=PRIORITY - Set a priority 0-5 (deprecated, for backwards compatibility) (default: none)
@@ -110,8 +117,10 @@ DESCRIPTION
110
117
  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.
111
118
 
112
119
  COMMAND OPTIONS
120
+ --at=POSITION - Add task at [s]tart or [e]nd of target project (default: none)
113
121
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
114
122
  -f, --file=PATH - Specify the file to which the task should be added (default: none)
123
+ --finish, --done - Mark task as @done with date
115
124
  --in, --todo=TODO_FILE - Add to a known todo file, partial matches allowed (default: none)
116
125
  -n, --note - Prompt for additional notes
117
126
  -p, --priority=PRIO - Add a priority level 1-5 (default: 0)
@@ -235,8 +244,9 @@ DESCRIPTION
235
244
  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).
236
245
 
237
246
  COMMAND OPTIONS
238
- -d, --depth=DEPTH - Recurse to depth (default: 2)
247
+ -d, --depth=DEPTH - Recurse to depth (default: none)
239
248
  --[no-]done - Include @done actions
249
+ --in, --todo=TODO_FILE - Display matches from a known todo file (may be used more than once, default: none)
240
250
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
241
251
  -t, --tag=TAG - Alternate tag to search for (default: none)
242
252
 
@@ -326,8 +336,9 @@ DESCRIPTION
326
336
  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).
327
337
 
328
338
  COMMAND OPTIONS
329
- -d, --depth=DEPTH - Recurse to depth (default: 2)
339
+ -d, --depth=DEPTH - Recurse to depth (default: none)
330
340
  --[no-]done - Include @done actions
341
+ --in, --todo=TODO_FILE - Display matches from a known todo file (may be used more than once, default: none)
331
342
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
332
343
  -t, --tag=TAG - Alternate tag to search for (default: none)
333
344
 
@@ -391,10 +402,12 @@ DESCRIPTION
391
402
  COMMAND OPTIONS
392
403
  -a, --archive - Add a @done tag to action and move to Archive
393
404
  --all - Act on all matches immediately (no menu)
405
+ --at=POSITION - When moving task, add at [s]tart or [e]nd of target project (default: none)
394
406
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
395
407
  --delete - Delete an action
408
+ --[no-]done - Include @done actions
396
409
  -e, --regex - Interpret search pattern as regular expression
397
- -f, --finish, --done - Add a @done tag to action
410
+ -f, --finish - Add a @done tag to action
398
411
  --file=PATH - Specify the file to search for the task (default: none)
399
412
  --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
400
413
  -n, --note - Prompt for additional notes. Input will be appended to any existing note.
@@ -444,6 +457,16 @@ Note that I created a new YAML dictionary inside of the `:next:` command, and ad
444
457
  > **WARNING** Don't touch most of the settings at the top of the auto-generated file. Setting any of them to true will alter the way na interprets the commands you're running. Most of those options are there for backwards compatibility with the bash version of this tool and will eventually be removed.
445
458
 
446
459
 
460
+ #### Working with a single global file
461
+
462
+ na is designed to work with one or more TaskPaper files in each project directory, but if you prefer to use a single global TaskPaper file, you can add `--file PATH` as a global option and specify a single file. This will bypass the detection of any files in the current directory. Make it permanent by including the `--file` flag when running `initconfig`.
463
+
464
+ When using a global file, you can additionally include `--cwd_as TYPE` to determine whether the current working directory is used as a tag or a project (default is neither). If you add `--cwd_as tag` to the global options (before the command), the last element of the current working directory will be appended as an @tag (e.g. if you're in ~/Code/project/doing, the action would be tagged @doing). If you use `--cwd_as project` the action will be put into a project with the same name as the current directory (e.g. `Doing:` from the previous example).
465
+
466
+ #### Add tasks at the end of a project
467
+
468
+ By default, tasks are added at the top of the target project (Inbox, etc.). If you prefer new tasks to go at the bottom by default, include `--add_at end` as a global option when running `initconfig`.
469
+
447
470
  ### Prompt Hooks
448
471
 
449
472
  You can add a prompt command to your shell to have na automatically list your next actions when you `cd` into a directory. To install a prompt command for your current shell, just run `na prompt install`. It works with Zsh, Bash, and Fish. If you'd rather make the changes to your startup file yourself, run `na prompt show` to get the hook and instructions printed out for copying.
data/bin/na CHANGED
@@ -39,6 +39,18 @@ class App
39
39
  arg_name 'PRIORITY'
40
40
  flag %i[p priority]
41
41
 
42
+ desc 'Use a single file as global todo, use --initconfig to make permanent'
43
+ arg_name 'PATH'
44
+ flag %i[f file]
45
+
46
+ desc 'Use current working directory as [p]roject, [t]ag, or [n]one'
47
+ arg_name 'TYPE'
48
+ flag %i[cwd_as], must_match: /^[ptn].*?$/i, default_value: 'none'
49
+
50
+ desc 'Add all new/moved entries at [s]tart or [e]nd of target project'
51
+ arg_name 'POSITION'
52
+ flag %i[add_at], default_value: 'start'
53
+
42
54
  desc 'Prompt for additional notes (deprecated, for backwards compatibility)'
43
55
  switch %i[n note], negatable: false
44
56
 
@@ -70,6 +82,10 @@ class App
70
82
  c.arg_name 'DEPTH'
71
83
  c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
72
84
 
85
+ c.desc 'Display matches from a known todo file'
86
+ c.arg_name 'TODO_FILE'
87
+ c.flag %i[in todo], multiple: true
88
+
73
89
  c.desc 'Alternate tag to search for'
74
90
  c.arg_name 'TAG'
75
91
  c.flag %i[t tag]
@@ -86,7 +102,7 @@ class App
86
102
  cmd = ['add']
87
103
  cmd.push('--note') if global_options[:note]
88
104
  cmd.concat(['--priority', global_options[:priority]]) if global_options[:priority]
89
- cmd.push(ARGV.unshift($first_arg)) if ARGV.count > 2
105
+ cmd.push(ARGV.unshift(NA.command_line[0])) if NA.command_line.count > 1
90
106
 
91
107
  exit run(cmd)
92
108
  end
@@ -97,6 +113,7 @@ class App
97
113
  options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
98
114
  end
99
115
 
116
+ args.concat(options[:in])
100
117
  if args.count.positive?
101
118
  all_req = false
102
119
 
@@ -155,6 +172,10 @@ class App
155
172
  c.default_value 'Inbox'
156
173
  c.flag %i[to project proj]
157
174
 
175
+ c.desc 'Add task at [s]tart or [e]nd of target project'
176
+ c.arg_name 'POSITION'
177
+ c.flag %i[at], must_match: /^[sbea].*?$/i
178
+
158
179
  c.desc 'Add to a known todo file, partial matches allowed'
159
180
  c.arg_name 'TODO_FILE'
160
181
  c.flag %i[in todo]
@@ -170,64 +191,31 @@ class App
170
191
  c.arg_name 'PATH'
171
192
  c.flag %i[f file]
172
193
 
194
+ c.desc 'Mark task as @done with date'
195
+ c.switch %i[finish done], negatable: false
196
+
173
197
  c.desc 'Search for files X directories deep'
174
198
  c.arg_name 'DEPTH'
175
199
  c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
176
200
 
177
- c.action do |_global_options, options, args|
201
+ c.action do |global_options, options, args|
178
202
  reader = TTY::Reader.new
179
- action = if args.count.positive?
180
- args.join(' ').strip
181
- elsif TTY::Which.exist?('gum')
182
- `gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
183
- else
184
- puts NA::Color.template('{bm}Enter task:{x}')
185
- reader.read_line(NA::Color.template('{by}> {bw}')).strip
186
- end
187
-
188
- if action.nil? || action.empty?
189
- puts 'Empty input, cancelled'
190
- Process.exit 1
191
- end
192
-
193
- if options[:priority]&.to_i&.positive?
194
- action = "#{action.gsub(/@priority\(\d+\)/, '')} @priority(#{options[:priority]})"
195
- end
196
-
197
- note_rx = /^(.+) \((.*?)\)$/
198
- split_note = if action =~ note_rx
199
- n = Regexp.last_match(2)
200
- action.sub!(note_rx, '\1').strip!
201
- n
202
- end
203
+ append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/
203
204
 
204
- na_tag = NA.na_tag
205
- if options[:x]
206
- na_tag = ''
207
- else
208
- na_tag = options[:tag] unless options[:tag].nil?
209
- na_tag = " @#{na_tag}"
210
- end
211
-
212
- action = "#{action.gsub(/#{na_tag}\b/, '')}#{na_tag}"
213
-
214
- line_note = if options[:note]
215
- if TTY::Which.exist?('gum')
216
- args = ['--placeholder "Enter a note, CTRL-d to save"']
217
- args << '--char-limit 0'
218
- args << '--width $(tput cols)'
219
- `gum write #{args.join(' ')}`.strip.split("\n")
220
- else
221
- puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
222
- reader.read_multiline
223
- end
224
- end
225
-
226
- note = split_note.nil? ? [] : [split_note]
227
- note.concat(line_note) unless line_note.nil?
228
- note = nil if note.empty?
229
-
230
- if options[:file]
205
+ if NA.global_file
206
+ target = File.expand_path(NA.global_file)
207
+ unless File.exist?(target)
208
+ print NA::Color.template('{by}Specified file not found, create it? {w}(y/{g}N{w}){x} ')
209
+ res = reader.read_char
210
+ if res =~ /y/i
211
+ basename = File.basename(target, ".#{NA.extension}")
212
+ NA.create_todo(target, basename)
213
+ else
214
+ puts NA::Color.template('{r}Cancelled{x}')
215
+ Process.exit 1
216
+ end
217
+ end
218
+ elsif options[:file]
231
219
  target = File.expand_path(options[:file])
232
220
  unless File.exist?(target)
233
221
  print NA::Color.template('{by}Specified file not found, create it? {w}(y/{g}N{w}){x} ')
@@ -287,7 +275,58 @@ class App
287
275
  end
288
276
  end
289
277
 
290
- NA.add_action(target, options[:project], action, note)
278
+ action = if args.count.positive?
279
+ args.join(' ').strip
280
+ elsif TTY::Which.exist?('gum')
281
+ `gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
282
+ else
283
+ puts NA::Color.template('{bm}Enter task:{x}')
284
+ reader.read_line(NA::Color.template('{by}> {bw}')).strip
285
+ end
286
+
287
+ if action.nil? || action.empty?
288
+ puts 'Empty input, cancelled'
289
+ Process.exit 1
290
+ end
291
+
292
+ if options[:priority]&.to_i&.positive?
293
+ action = "#{action.gsub(/@priority\(\d+\)/, '')} @priority(#{options[:priority]})"
294
+ end
295
+
296
+ note_rx = /^(.+) \((.*?)\)$/
297
+ split_note = if action =~ note_rx
298
+ n = Regexp.last_match(2)
299
+ action.sub!(note_rx, '\1').strip!
300
+ n
301
+ end
302
+
303
+ na_tag = NA.na_tag
304
+ if options[:x]
305
+ na_tag = ''
306
+ else
307
+ na_tag = options[:tag] unless options[:tag].nil?
308
+ na_tag = " @#{na_tag}"
309
+ end
310
+
311
+ action = "#{action.gsub(/#{na_tag}\b/, '')}#{na_tag}"
312
+
313
+ line_note = if options[:note]
314
+ if TTY::Which.exist?('gum')
315
+ args = ['--placeholder "Enter a note, CTRL-d to save"']
316
+ args << '--char-limit 0'
317
+ args << '--width $(tput cols)'
318
+ `gum write #{args.join(' ')}`.strip.split("\n")
319
+ else
320
+ puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
321
+ reader.read_multiline
322
+ end
323
+ end
324
+
325
+ note = split_note.nil? ? [] : [split_note]
326
+ note.concat(line_note) unless line_note.nil?
327
+ note = [] if note.empty?
328
+
329
+ NA.add_action(target, options[:project], action, note, finish: options[:finish], append: append)
291
330
  end
292
331
  end
293
332
 
@@ -313,6 +352,10 @@ class App
313
352
  c.arg_name 'PRIO'
314
353
  c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
315
354
 
355
+ c.desc 'When moving task, add at [s]tart or [e]nd of target project'
356
+ c.arg_name 'POSITION'
357
+ c.flag %i[at], must_match: /^[sbea].*?$/i
358
+
316
359
  c.desc 'Move action to specific project'
317
360
  c.arg_name 'PROJECT'
318
361
  c.flag %i[to project proj]
@@ -321,6 +364,9 @@ class App
321
364
  c.arg_name 'TODO_FILE'
322
365
  c.flag %i[in todo]
323
366
 
367
+ c.desc 'Include @done actions'
368
+ c.switch %i[done]
369
+
324
370
  c.desc 'Add a tag to the action, @tag(values) allowed'
325
371
  c.arg_name 'TAG'
326
372
  c.flag %i[t tag], multiple: true
@@ -330,7 +376,7 @@ class App
330
376
  c.flag %i[r remove], multiple: true
331
377
 
332
378
  c.desc 'Add a @done tag to action'
333
- c.switch %i[f finish done], negatable: false
379
+ c.switch %i[f finish], negatable: false
334
380
 
335
381
  c.desc 'Add a @done tag to action and move to Archive'
336
382
  c.switch %i[a archive], negatable: false
@@ -359,8 +405,10 @@ class App
359
405
  c.desc 'Match pattern exactly'
360
406
  c.switch %i[x exact], negatable: false
361
407
 
362
- c.action do |_global_options, options, args|
408
+ c.action do |global_options, options, args|
363
409
  reader = TTY::Reader.new
410
+ append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/i
411
+
364
412
  action = if args.count.positive?
365
413
  args.join(' ').strip
366
414
  elsif TTY::Which.exist?('gum') && options[:tagged].empty?
@@ -433,6 +481,14 @@ class App
433
481
 
434
482
  note = line_note.nil? || line_note.empty? ? [] : line_note
435
483
 
484
+ target_proj = if options[:project]
485
+ options[:project]
486
+ elsif NA.cwd_is == :project
487
+ NA.cwd
488
+ else
489
+ nil
490
+ end
491
+
436
492
  if options[:file]
437
493
  file = File.expand_path(options[:file])
438
494
  NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
@@ -478,12 +534,14 @@ class App
478
534
  add_tag: add_tags,
479
535
  remove_tag: remove_tags,
480
536
  finish: options[:finish],
481
- project: options[:project],
537
+ project: target_proj,
482
538
  delete: options[:delete],
483
539
  note: note,
484
540
  overwrite: options[:overwrite],
485
541
  tagged: tags,
486
- all: options[:all])
542
+ all: options[:all],
543
+ done: options[:done],
544
+ append: append)
487
545
  end
488
546
  end
489
547
  end
@@ -922,21 +980,26 @@ class App
922
980
  NA.verbose = global[:debug]
923
981
  NA.extension = global[:ext]
924
982
  NA.na_tag = global[:na_tag]
983
+ NA.global_file = global[:file]
984
+ NA.cwd = File.basename(ENV['PWD'])
985
+ NA.cwd_is = if global[:cwd_as] =~ /^n/
986
+ :none
987
+ else
988
+ global[:cwd_as] =~ /^p/ ? :project : :tag
989
+ end
925
990
  NA.weed_cache_file
926
991
  true
927
992
  end
928
993
 
929
994
  post do |global, command, options, args|
930
- # Post logic here
931
- # Use skips_post before a command to skip this
932
- # block on that command only
995
+ # post actions
933
996
  end
934
997
 
935
998
  on_error do |exception|
936
999
  case exception
937
1000
  when GLI::UnknownCommand
938
1001
  cmd = ['saved']
939
- cmd.concat(ARGV.unshift($first_arg))
1002
+ cmd.concat(ARGV.unshift(NA.command_line[0]))
940
1003
 
941
1004
  exit run(cmd)
942
1005
  when SystemExit
@@ -947,8 +1010,9 @@ class App
947
1010
  end
948
1011
  end
949
1012
 
950
- NA.command_line = ARGV
1013
+ NA.command_line = ARGV.dup
1014
+ NA.globals = NA.command_line.filter { |arg| arg =~ /^-/ }
1015
+ NA.command_line.delete_if { |arg| arg =~ /^-/ }
951
1016
  @command = ARGV[0]
952
- $first_arg = ARGV[1]
953
1017
 
954
1018
  exit App.run(ARGV)
data/lib/na/action.rb CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  module NA
4
4
  class Action < Hash
5
- attr_reader :file, :project, :parent, :action, :tags, :line, :note
5
+ attr_reader :file, :project, :parent, :tags, :line, :note
6
+
7
+ attr_accessor :action
6
8
 
7
9
  def initialize(file, project, parent, action, idx, note = [])
8
10
  super()
@@ -3,21 +3,22 @@
3
3
  # Next Action methods
4
4
  module NA
5
5
  class << self
6
- attr_accessor :verbose, :extension, :na_tag, :command_line
6
+ attr_accessor :verbose, :extension, :na_tag, :command_line, :globals, :global_file, :cwd_is, :cwd
7
7
 
8
8
  ##
9
9
  ## Output to STDERR
10
10
  ##
11
11
  ## @param msg [String] The message
12
- ## @param exit_code [Number] The exit code, no exit if false
12
+ ## @param exit_code [Number] The exit code, no
13
+ ## exit if false
14
+ ## @param debug [Boolean] only display message if running :verbose
13
15
  ##
14
16
  def notify(msg, exit_code: false, debug: false)
15
17
  return if debug && !@verbose
16
18
 
17
19
  $stderr.puts NA::Color.template("{x}#{msg}{x}")
18
- if exit_code
19
- Process.exit exit_code
20
- end
20
+ Process.exit exit_code if exit_code
21
+
21
22
  end
22
23
 
23
24
  ##
@@ -99,6 +100,8 @@ module NA
99
100
  ## @param depth [Number] The depth at which to search
100
101
  ##
101
102
  def find_files(depth: 1)
103
+ return [NA.global_file] if NA.global_file
104
+
102
105
  files = `find . -name "*.#{NA.extension}" -maxdepth #{depth}`.strip.split("\n")
103
106
  files.each { |f| save_working_dir(File.expand_path(f)) }
104
107
  files
@@ -152,10 +155,14 @@ module NA
152
155
  projects
153
156
  end
154
157
 
155
- def find_actions(target, search, tagged = nil, all: false)
156
- _, actions, projects = parse_actions(search: search, require_na: false, file_path: target, tag: tagged)
158
+ def find_actions(target, search, tagged = nil, all: false, done: false)
159
+ _, actions, projects = parse_actions(search: search, require_na: false, file_path: target, tag: tagged, done: done)
160
+
161
+ unless actions.count.positive?
162
+ NA.notify("{r}No matching actions found in {bw}#{File.basename(target, ".#{NA.extension}")}")
163
+ return
164
+ end
157
165
 
158
- NA.notify('{r}No matching actions found', exit_code: 1) unless actions.count.positive?
159
166
  return [projects, actions] if actions.count == 1 || all
160
167
 
161
168
  options = actions.map { |action| "#{action.line} % #{action.parent.join('/')} : #{action.action}" }
@@ -248,8 +255,35 @@ module NA
248
255
  new_project
249
256
  end
250
257
 
258
+ def process_action(action, priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
259
+ string = action.action
260
+
261
+ if priority&.positive?
262
+ string.gsub!(/(?<=\A| )@priority\(\d+\)/, '').strip!
263
+ string += " @priority(#{priority})"
264
+ end
265
+
266
+ add_tag.each do |tag|
267
+ string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
268
+ string.strip!
269
+ string += " @#{tag}"
270
+ end
271
+
272
+ remove_tag.each do |tag|
273
+ string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
274
+ string.strip!
275
+ end
276
+
277
+ string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
278
+
279
+ action.action = string
280
+
281
+ action
282
+ end
283
+
251
284
  def update_action(target,
252
285
  search,
286
+ add: nil,
253
287
  priority: 0,
254
288
  add_tag: [],
255
289
  remove_tag: [],
@@ -259,7 +293,9 @@ module NA
259
293
  note: [],
260
294
  overwrite: false,
261
295
  tagged: nil,
262
- all: false)
296
+ all: false,
297
+ done: false,
298
+ append: false)
263
299
 
264
300
  projects = find_projects(target)
265
301
 
@@ -277,35 +313,12 @@ module NA
277
313
  end
278
314
  end
279
315
 
280
- projects, actions = find_actions(target, search, tagged, all: all)
281
-
282
316
  contents = target.read_file.split(/\n/)
283
317
 
284
- actions.sort_by(&:line).reverse.each do |action|
285
- string = action.action
318
+ if add.is_a?(Action)
319
+ action = process_action(add, priority: priority, finish: finish, add_tag: add_tag, remove_tag: remove_tag)
286
320
 
287
- if priority&.positive?
288
- string.gsub!(/@priority\(\d+\)/, '').strip!
289
- string += " @priority(#{priority})"
290
- end
291
-
292
- add_tag.each do |tag|
293
- string.gsub!(/@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
294
- string.strip!
295
- string += " @#{tag}"
296
- end
297
-
298
- remove_tag.each do |tag|
299
- string.gsub!(/@#{tag}(\(.*?\))?/, '')
300
- string.strip!
301
- end
302
-
303
- string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /@done/
304
-
305
- contents.slice!(action.line, action.note.count + 1)
306
- next if delete
307
-
308
- projects = shift_index_after(projects, action.line, action.note.count + 1)
321
+ projects = find_projects(target)
309
322
 
310
323
  target_proj = if target_proj
311
324
  projects.select { |proj| proj.project =~ /^#{target_proj.project}$/ }.first
@@ -320,13 +333,74 @@ module NA
320
333
  else
321
334
  overwrite ? note : action.note.concat(note)
322
335
  end
336
+
323
337
  note = note.empty? ? '' : "\n#{indent}\t\t#{note.join("\n#{indent}\t\t").strip}"
324
- contents.insert(target_proj.line, "#{indent}\t- #{string}#{note}")
338
+
339
+ if append
340
+ this_idx = 0
341
+ projects.each_with_index do |proj, idx|
342
+ if proj.line == target_proj.line
343
+ this_idx = idx
344
+ break
345
+ end
346
+ end
347
+
348
+ target_line = if this_idx == projects.length - 1
349
+ contents.count
350
+ else
351
+ projects[this_idx + 1].line - 1
352
+ end
353
+ else
354
+ target_line = target_proj.line
355
+ end
356
+
357
+ contents.insert(target_line, "#{indent}\t- #{action.action}#{note}")
358
+ else
359
+ projects, actions = find_actions(target, search, tagged, done: done, all: all)
360
+
361
+ return if actions.nil?
362
+
363
+ actions.sort_by(&:line).reverse.each do |action|
364
+ contents.slice!(action.line, action.note.count + 1)
365
+ next if delete
366
+
367
+ projects = shift_index_after(projects, action.line, action.note.count + 1)
368
+
369
+ action = process_action(action, priority: priority, finish: finish, add_tag: add_tag, remove_tag: remove_tag)
370
+
371
+ target_proj = if target_proj
372
+ projects.select { |proj| proj.project =~ /^#{target_proj.project}$/ }.first
373
+ else
374
+ projects.select { |proj| proj.project =~ /^#{action.parent.join(':')}$/ }.first
375
+ end
376
+
377
+ indent = "\t" * target_proj.indent
378
+ note = note.split("\n") unless note.is_a?(Array)
379
+ note = if note.empty?
380
+ action.note
381
+ else
382
+ overwrite ? note : action.note.concat(note)
383
+ end
384
+ note = note.empty? ? '' : "\n#{indent}\t\t#{note.join("\n#{indent}\t\t").strip}"
385
+
386
+ if append
387
+ this_idx = projects.index(target_proj)
388
+ if this_idx == projects.length - 1
389
+ target_line = contents.count
390
+ else
391
+ target_line = projects[this_idx + 1].line - 1
392
+ end
393
+ else
394
+ target_line = target_proj.line
395
+ end
396
+
397
+ contents.insert(target_line, "#{indent}\t- #{action.action}#{note}")
398
+ end
325
399
  end
326
400
  backup_file(target)
327
401
  File.open(target, 'w') { |f| f.puts contents.join("\n") }
328
402
 
329
- notify("{by}Task updated in {bw}#{target}")
403
+ add ? notify("{by}Task added to {bw}#{target}") : notify("{by}Task updated in {bw}#{target}")
330
404
  end
331
405
 
332
406
  ##
@@ -337,24 +411,22 @@ module NA
337
411
  ## @param action [String] The action
338
412
  ## @param note [String] The note
339
413
  ##
340
- def add_action(file, project, action, note = nil)
341
- content = file.read_file
342
- # Insert the target project at the top if it doesn't exist
343
- unless content =~ /^[ \t]*#{project}:/i
344
- content = "#{project.cap_first}:\n#{content}"
345
- end
414
+ def add_action(file, project, action, note = [], priority: 0, finish: false, append: false)
415
+ parent = project.split(%r{[:/]})
346
416
 
347
- # Insert the action at the top of the target project
348
- content.sub!(/^([ \t]*)#{project}:(.*?)$/i) do
349
- m = Regexp.last_match
350
- indent = "\n#{m[1]}\t\t"
351
- note = note.nil? ? '' : "#{indent}#{note.join(indent).strip}"
352
- "#{m[1]}#{project.cap_first}:#{m[2]}\n#{m[1]}\t- #{action}#{note}"
417
+ if NA.global_file
418
+ puts NA.global_file
419
+ if NA.cwd_is == :tag
420
+ add_tag = [NA.cwd]
421
+ else
422
+ project = NA.cwd
423
+ end
424
+ puts [add_tag, project]
353
425
  end
354
426
 
355
- File.open(file, 'w') { |f| f.puts content }
427
+ action = Action.new(file, project, parent, action, nil, note)
356
428
 
357
- notify("{by}Task added to {bw}#{file}")
429
+ update_action(file, nil, add: action, project: project, add_tag: add_tag, priority: priority, finish: finish, append: append)
358
430
  end
359
431
 
360
432
  ##
@@ -394,6 +466,7 @@ module NA
394
466
  ##
395
467
  ## @param depth [Number] The directory depth
396
468
  ## to search for files
469
+ ## @param done [Boolean] include @done actions
397
470
  ## @param query [Hash] The todo file query
398
471
  ## @param tag [Array] Tags to search for
399
472
  ## @param search [String] A search string
@@ -402,7 +475,7 @@ module NA
402
475
  ## regular expression
403
476
  ## @param project [String] The project
404
477
  ## @param require_na [Boolean] Require @na tag
405
- ## @param file [String] file path to parse
478
+ ## @param file_path [String] file path to parse
406
479
  ##
407
480
  def parse_actions(depth: 1, done: false, query: nil, tag: nil, search: nil, negate: false, regex: false, project: nil, require_na: true, file_path: nil)
408
481
  actions = []
@@ -552,7 +625,9 @@ module NA
552
625
  end
553
626
 
554
627
  def list_projects(query: [], file_path: nil, depth: 1, paths: true)
555
- files = if !file_path.nil?
628
+ files = if NA.global_file
629
+ [NA.global_file]
630
+ elsif !file_path.nil?
556
631
  [file_path]
557
632
  elsif query.nil?
558
633
  find_files(depth: depth)
@@ -740,6 +815,7 @@ module NA
740
815
 
741
816
  default_args = [%(--prompt="#{prompt}"), "--height=#{options.count + 2}", '--info=inline']
742
817
  default_args << '--multi' if multiple
818
+ default_args << '--bind ctrl-a:select-all'
743
819
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
744
820
  default_args << %(--header="#{header}")
745
821
  default_args.concat(fzf_args)
data/lib/na/project.rb CHANGED
@@ -20,7 +20,7 @@ module NA
20
20
  "@project: #{@project}",
21
21
  "@indent: #{@indent}",
22
22
  "@line: #{@line}"
23
- ].join("\n")
23
+ ].join(" ")
24
24
  end
25
25
  end
26
26
  end
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.1'
2
+ VERSION = '1.2.2'
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.0<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.1<!--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
 
@@ -48,6 +48,10 @@ You can also quickly add todo items from the command line with the `add` subcomm
48
48
 
49
49
  If found, it will try to locate an `Inbox:` project, or create one if it doesn't exist. Any arguments after `add` will be combined to create a new task in TaskPaper format. They will automatically be assigned as next actions (tagged `@na`) and will show up when `na` lists the tasks for the project.
50
50
 
51
+ #### Updating todos
52
+
53
+ You can mark todos as complete, delete them, add and remove tags, change priority, and even move them between projects with the `na update` command.
54
+
51
55
  ### Usage
52
56
 
53
57
  ```
@@ -190,6 +194,16 @@ Note that I created a new YAML dictionary inside of the `:next:` command, and ad
190
194
  > **WARNING** Don't touch most of the settings at the top of the auto-generated file. Setting any of them to true will alter the way na interprets the commands you're running. Most of those options are there for backwards compatibility with the bash version of this tool and will eventually be removed.
191
195
  <!--JEKYLL{:.warn}-->
192
196
 
197
+ #### Working with a single global file
198
+
199
+ na is designed to work with one or more TaskPaper files in each project directory, but if you prefer to use a single global TaskPaper file, you can add `--file PATH` as a global option and specify a single file. This will bypass the detection of any files in the current directory. Make it permanent by including the `--file` flag when running `initconfig`.
200
+
201
+ When using a global file, you can additionally include `--cwd_as TYPE` to determine whether the current working directory is used as a tag or a project (default is neither). If you add `--cwd_as tag` to the global options (before the command), the last element of the current working directory will be appended as an @tag (e.g. if you're in ~/Code/project/doing, the action would be tagged @doing). If you use `--cwd_as project` the action will be put into a project with the same name as the current directory (e.g. `Doing:` from the previous example).
202
+
203
+ #### Add tasks at the end of a project
204
+
205
+ By default, tasks are added at the top of the target project (Inbox, etc.). If you prefer new tasks to go at the bottom by default, include `--add_at end` as a global option when running `initconfig`.
206
+
193
207
  ### Prompt Hooks
194
208
 
195
209
  You can add a prompt command to your shell to have na automatically list your next actions when you `cd` into a directory. To install a prompt command for your current shell, just run `na prompt install`. It works with Zsh, Bash, and Fish. If you'd rather make the changes to your startup file yourself, run `na prompt show` to get the hook and instructions printed out for copying.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: na
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-22 00:00:00.000000000 Z
11
+ date: 2022-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake