na 1.2.0 → 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: e91f1eeef1caa7f05d1dab98bd221d88d58ad1a0d928c88a399f8b1a491a200c
4
- data.tar.gz: b08a5c6a2cfe97f0749e784e3a5d9a06ef458ce06162f0f62321d775069bce87
3
+ metadata.gz: ba10397406ce9b754a63fcd7787eb5cc22923d5564de8eca50cb8bb6000115e7
4
+ data.tar.gz: c7c47d9531177787b1615857135d8bc69ab3b3a3595aad30d357fa39f9b41fe5
5
5
  SHA512:
6
- metadata.gz: ea45a8a95a5f78fe551459ca4fe8302e96cfdc5d9de59bf1f145693c900baeee128275efb84823af5ea2827f3ecdb6f0d05c9a233b283cee03d09836cad4fb46
7
- data.tar.gz: af359ef24ea273d166fb8b0dd91ddccaaa9d13c5c42ef00b65da8a78939d59936b9df8ca37dc778b46fa2a9a5b542584c339819e86bd3a9ade904ffd28ce791e
6
+ metadata.gz: 6861532283cf20d55f9fddff1979209024301863f01dfbe1d79d83096e397ab94b59a45715f1d1486939afce8e2fbb2b90196bbca5cb8a93ae5d02636804442d
7
+ data.tar.gz: 9ebceba70f481a722c616d1ad819c23bd9dab13abebf68371fd966f0834585826596084e27bf27cc632515a4a7c7b162abedc4f997672cb7439ab22bf5ace8f1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
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
+
23
+ ### 1.2.1
24
+
25
+ 2022-10-22 10:18
26
+
27
+ #### NEW
28
+
29
+ - Added `--done` tag to next/find/tagged to include @done actions in the output
30
+ - Use `na changes` to view the changelog and see recent changes
31
+
32
+ #### IMPROVED
33
+
34
+ - You can run `na SAVED_SEARCH` using any saved search (same as running `na saved SAVED_SEARCH` but niftier)
35
+
1
36
  ### 1.2.0
2
37
 
3
38
  2022-10-22 01:32
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.0)
4
+ na (1.2.2)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
  gli (~> 2.21.0)
7
+ mdless (~> 1.0, >= 1.0.32)
7
8
  tty-reader (~> 0.9, >= 0.9.0)
8
9
  tty-screen (~> 0.8, >= 0.8.1)
9
10
  tty-which (~> 0.5, >= 0.5.0)
@@ -13,6 +14,7 @@ GEM
13
14
  specs:
14
15
  chronic (0.10.2)
15
16
  gli (2.21.0)
17
+ mdless (1.0.32)
16
18
  minitest (5.16.3)
17
19
  rake (0.9.6)
18
20
  rdoc (4.3.0)
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.0
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.0
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)
@@ -74,19 +81,20 @@ GLOBAL OPTIONS
74
81
  --version - Display the program version
75
82
 
76
83
  COMMANDS
77
- add - Add a new next action
78
- edit - Open a todo file in the default editor
79
- find, grep - Find actions matching a search pattern
80
- help - Shows a list of commands or help for one command
81
- init, create - Create a new todo file in the current directory
82
- initconfig - Initialize the config file using current global options
83
- next, show - Show next actions
84
- projects - Show list of projects for a file
85
- prompt - Show or install prompt hooks for the current shell
86
- saved - Execute a saved search
87
- tagged - Find actions matching a tag
88
- todos - Show list of known todo files
89
- update - Update an existing action
84
+ add - Add a new next action
85
+ changes, changelog - Display the changelog
86
+ edit - Open a todo file in the default editor
87
+ find, grep - Find actions matching a search pattern
88
+ help - Shows a list of commands or help for one command
89
+ init, create - Create a new todo file in the current directory
90
+ initconfig - Initialize the config file using current global options
91
+ next, show - Show next actions
92
+ projects - Show list of projects for a file
93
+ prompt - Show or install prompt hooks for the current shell
94
+ saved - Execute a saved search
95
+ tagged - Find actions matching a tag
96
+ todos - Show list of known todo files
97
+ update - Update an existing action
90
98
  ```
91
99
 
92
100
  #### Commands
@@ -109,8 +117,10 @@ DESCRIPTION
109
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.
110
118
 
111
119
  COMMAND OPTIONS
120
+ --at=POSITION - Add task at [s]tart or [e]nd of target project (default: none)
112
121
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
113
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
114
124
  --in, --todo=TODO_FILE - Add to a known todo file, partial matches allowed (default: none)
115
125
  -n, --note - Prompt for additional notes
116
126
  -p, --priority=PRIO - Add a priority level 1-5 (default: 0)
@@ -177,6 +187,7 @@ DESCRIPTION
177
187
 
178
188
  COMMAND OPTIONS
179
189
  -d, --depth=DEPTH - Recurse to depth (default: none)
190
+ --[no-]done - Include @done actions
180
191
  -e, --regex - Interpret search pattern as regular expression
181
192
  --in=TODO_PATH - Show actions from a specific todo file in history. May use wildcards (* and ?) (default: none)
182
193
  -o, --or - Combine search tokens with OR, displaying actions matching ANY of the terms
@@ -233,7 +244,9 @@ DESCRIPTION
233
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).
234
245
 
235
246
  COMMAND OPTIONS
236
- -d, --depth=DEPTH - Recurse to depth (default: 2)
247
+ -d, --depth=DEPTH - Recurse to depth (default: none)
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)
237
250
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
238
251
  -t, --tag=TAG - Alternate tag to search for (default: none)
239
252
 
@@ -323,7 +336,9 @@ DESCRIPTION
323
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).
324
337
 
325
338
  COMMAND OPTIONS
326
- -d, --depth=DEPTH - Recurse to depth (default: 2)
339
+ -d, --depth=DEPTH - Recurse to depth (default: none)
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)
327
342
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
328
343
  -t, --tag=TAG - Alternate tag to search for (default: none)
329
344
 
@@ -387,10 +402,12 @@ DESCRIPTION
387
402
  COMMAND OPTIONS
388
403
  -a, --archive - Add a @done tag to action and move to Archive
389
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)
390
406
  -d, --depth=DEPTH - Search for files X directories deep (default: 1)
391
407
  --delete - Delete an action
408
+ --[no-]done - Include @done actions
392
409
  -e, --regex - Interpret search pattern as regular expression
393
- -f, --finish, --done - Add a @done tag to action
410
+ -f, --finish - Add a @done tag to action
394
411
  --file=PATH - Specify the file to search for the task (default: none)
395
412
  --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
396
413
  -n, --note - Prompt for additional notes. Input will be appended to any existing note.
@@ -440,6 +457,16 @@ Note that I created a new YAML dictionary inside of the `:next:` command, and ad
440
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.
441
458
 
442
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
+
443
470
  ### Prompt Hooks
444
471
 
445
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]
@@ -78,12 +94,15 @@ class App
78
94
  c.arg_name 'PROJECT[/SUBPROJECT]'
79
95
  c.flag %i[proj project]
80
96
 
97
+ c.desc 'Include @done actions'
98
+ c.switch %i[done]
99
+
81
100
  c.action do |global_options, options, args|
82
101
  if global_options[:add]
83
102
  cmd = ['add']
84
103
  cmd.push('--note') if global_options[:note]
85
104
  cmd.concat(['--priority', global_options[:priority]]) if global_options[:priority]
86
- 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
87
106
 
88
107
  exit run(cmd)
89
108
  end
@@ -94,6 +113,7 @@ class App
94
113
  options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
95
114
  end
96
115
 
116
+ args.concat(options[:in])
97
117
  if args.count.positive?
98
118
  all_req = false
99
119
 
@@ -115,6 +135,7 @@ class App
115
135
 
116
136
  tag = [{ tag: tag, value: nil }, { tag: 'done', value: nil, negate: true}]
117
137
  files, actions, = NA.parse_actions(depth: depth,
138
+ done: options[:done],
118
139
  query: tokens,
119
140
  tag: tag,
120
141
  project: options[:project],
@@ -151,6 +172,10 @@ class App
151
172
  c.default_value 'Inbox'
152
173
  c.flag %i[to project proj]
153
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
+
154
179
  c.desc 'Add to a known todo file, partial matches allowed'
155
180
  c.arg_name 'TODO_FILE'
156
181
  c.flag %i[in todo]
@@ -166,64 +191,31 @@ class App
166
191
  c.arg_name 'PATH'
167
192
  c.flag %i[f file]
168
193
 
194
+ c.desc 'Mark task as @done with date'
195
+ c.switch %i[finish done], negatable: false
196
+
169
197
  c.desc 'Search for files X directories deep'
170
198
  c.arg_name 'DEPTH'
171
199
  c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
172
200
 
173
- c.action do |_global_options, options, args|
201
+ c.action do |global_options, options, args|
174
202
  reader = TTY::Reader.new
175
- action = if args.count.positive?
176
- args.join(' ').strip
177
- elsif TTY::Which.exist?('gum')
178
- `gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
179
- else
180
- puts NA::Color.template('{bm}Enter task:{x}')
181
- reader.read_line(NA::Color.template('{by}> {bw}')).strip
182
- end
183
-
184
- if action.nil? || action.empty?
185
- puts 'Empty input, cancelled'
186
- Process.exit 1
187
- end
188
-
189
- if options[:priority]&.to_i&.positive?
190
- action = "#{action.gsub(/@priority\(\d+\)/, '')} @priority(#{options[:priority]})"
191
- end
192
-
193
- note_rx = /^(.+) \((.*?)\)$/
194
- split_note = if action =~ note_rx
195
- n = Regexp.last_match(2)
196
- action.sub!(note_rx, '\1').strip!
197
- n
198
- end
203
+ append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/
199
204
 
200
- na_tag = NA.na_tag
201
- if options[:x]
202
- na_tag = ''
203
- else
204
- na_tag = options[:tag] unless options[:tag].nil?
205
- na_tag = " @#{na_tag}"
206
- end
207
-
208
- action = "#{action.gsub(/#{na_tag}\b/, '')}#{na_tag}"
209
-
210
- line_note = if options[:note]
211
- if TTY::Which.exist?('gum')
212
- args = ['--placeholder "Enter a note, CTRL-d to save"']
213
- args << '--char-limit 0'
214
- args << '--width $(tput cols)'
215
- `gum write #{args.join(' ')}`.strip.split("\n")
216
- else
217
- puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
218
- reader.read_multiline
219
- end
220
- end
221
-
222
- note = split_note.nil? ? [] : [split_note]
223
- note.concat(line_note) unless line_note.nil?
224
- note = nil if note.empty?
225
-
226
- 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]
227
219
  target = File.expand_path(options[:file])
228
220
  unless File.exist?(target)
229
221
  print NA::Color.template('{by}Specified file not found, create it? {w}(y/{g}N{w}){x} ')
@@ -283,7 +275,58 @@ class App
283
275
  end
284
276
  end
285
277
 
286
- 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)
287
330
  end
288
331
  end
289
332
 
@@ -309,6 +352,10 @@ class App
309
352
  c.arg_name 'PRIO'
310
353
  c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
311
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
+
312
359
  c.desc 'Move action to specific project'
313
360
  c.arg_name 'PROJECT'
314
361
  c.flag %i[to project proj]
@@ -317,6 +364,9 @@ class App
317
364
  c.arg_name 'TODO_FILE'
318
365
  c.flag %i[in todo]
319
366
 
367
+ c.desc 'Include @done actions'
368
+ c.switch %i[done]
369
+
320
370
  c.desc 'Add a tag to the action, @tag(values) allowed'
321
371
  c.arg_name 'TAG'
322
372
  c.flag %i[t tag], multiple: true
@@ -326,7 +376,7 @@ class App
326
376
  c.flag %i[r remove], multiple: true
327
377
 
328
378
  c.desc 'Add a @done tag to action'
329
- c.switch %i[f finish done], negatable: false
379
+ c.switch %i[f finish], negatable: false
330
380
 
331
381
  c.desc 'Add a @done tag to action and move to Archive'
332
382
  c.switch %i[a archive], negatable: false
@@ -355,8 +405,10 @@ class App
355
405
  c.desc 'Match pattern exactly'
356
406
  c.switch %i[x exact], negatable: false
357
407
 
358
- c.action do |_global_options, options, args|
408
+ c.action do |global_options, options, args|
359
409
  reader = TTY::Reader.new
410
+ append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/i
411
+
360
412
  action = if args.count.positive?
361
413
  args.join(' ').strip
362
414
  elsif TTY::Which.exist?('gum') && options[:tagged].empty?
@@ -429,6 +481,14 @@ class App
429
481
 
430
482
  note = line_note.nil? || line_note.empty? ? [] : line_note
431
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
+
432
492
  if options[:file]
433
493
  file = File.expand_path(options[:file])
434
494
  NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
@@ -474,12 +534,14 @@ class App
474
534
  add_tag: add_tags,
475
535
  remove_tag: remove_tags,
476
536
  finish: options[:finish],
477
- project: options[:project],
537
+ project: target_proj,
478
538
  delete: options[:delete],
479
539
  note: note,
480
540
  overwrite: options[:overwrite],
481
541
  tagged: tags,
482
- all: options[:all])
542
+ all: options[:all],
543
+ done: options[:done],
544
+ append: append)
483
545
  end
484
546
  end
485
547
  end
@@ -513,6 +575,9 @@ class App
513
575
  c.arg_name 'PROJECT[/SUBPROJECT]'
514
576
  c.flag %i[proj project]
515
577
 
578
+ c.desc 'Include @done actions'
579
+ c.switch %i[done]
580
+
516
581
  c.desc 'Show actions not matching search pattern'
517
582
  c.switch %i[v invert], negatable: false
518
583
 
@@ -563,6 +628,7 @@ class App
563
628
  end
564
629
 
565
630
  files, actions, = NA.parse_actions(depth: depth,
631
+ done: options[:done],
566
632
  query: todo,
567
633
  search: tokens,
568
634
  negate: options[:invert],
@@ -610,6 +676,9 @@ class App
610
676
  c.arg_name 'PROJECT[/SUBPROJECT]'
611
677
  c.flag %i[proj project]
612
678
 
679
+ c.desc 'Include @done actions'
680
+ c.switch %i[done]
681
+
613
682
  c.desc 'Show actions not matching tags'
614
683
  c.switch %i[v invert], negatable: false
615
684
 
@@ -661,6 +730,7 @@ class App
661
730
  end
662
731
 
663
732
  files, actions, = NA.parse_actions(depth: depth,
733
+ done: options[:done],
664
734
  query: todo,
665
735
  tag: tags,
666
736
  negate: options[:invert],
@@ -850,6 +920,24 @@ class App
850
920
  end
851
921
  end
852
922
 
923
+ desc 'Display the changelog'
924
+ command %i[changes changelog] do |c|
925
+ c.action do |_, _, _|
926
+ changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
927
+ pagers = [
928
+ 'mdless',
929
+ 'mdcat',
930
+ 'bat',
931
+ ENV['PAGER'],
932
+ 'less -FXr',
933
+ ENV['GIT_PAGER'],
934
+ 'more -r'
935
+ ]
936
+ pager = pagers.find { |cmd| TTY::Which.exist?(cmd.split.first) }
937
+ system %(#{pager} "#{changelog}")
938
+ end
939
+ end
940
+
853
941
  desc 'Execute a saved search'
854
942
  long_desc 'Run without argument to list saved searches'
855
943
  arg_name 'SEARCH_TITLE', optional: true
@@ -892,21 +980,26 @@ class App
892
980
  NA.verbose = global[:debug]
893
981
  NA.extension = global[:ext]
894
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
895
990
  NA.weed_cache_file
896
991
  true
897
992
  end
898
993
 
899
994
  post do |global, command, options, args|
900
- # Post logic here
901
- # Use skips_post before a command to skip this
902
- # block on that command only
995
+ # post actions
903
996
  end
904
997
 
905
998
  on_error do |exception|
906
999
  case exception
907
1000
  when GLI::UnknownCommand
908
- cmd = ['add']
909
- cmd.concat(ARGV.unshift($first_arg)) if ARGV.count.positive?
1001
+ cmd = ['saved']
1002
+ cmd.concat(ARGV.unshift(NA.command_line[0]))
910
1003
 
911
1004
  exit run(cmd)
912
1005
  when SystemExit
@@ -917,8 +1010,9 @@ class App
917
1010
  end
918
1011
  end
919
1012
 
920
- 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 =~ /^-/ }
921
1016
  @command = ARGV[0]
922
- $first_arg = ARGV[1]
923
1017
 
924
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,9 +475,9 @@ 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
- def parse_actions(depth: 1, query: nil, tag: nil, search: nil, negate: false, regex: false, project: nil, require_na: true, file_path: nil)
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 = []
409
482
  required = []
410
483
  optional = []
@@ -482,7 +555,7 @@ module NA
482
555
  in_action = false
483
556
  # search_for_done = false
484
557
  # optional_tag.each { |t| search_for_done = true if t[:tag] =~ /done/ }
485
- # next if line =~ /@done/ && !search_for_done
558
+ next if line =~ /@done/ && !done
486
559
 
487
560
  next if require_na && line !~ /@#{NA.na_tag}\b/
488
561
 
@@ -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.0'
2
+ VERSION = '1.2.2'
3
3
  end
data/na.gemspec CHANGED
@@ -31,4 +31,5 @@ spec = Gem::Specification.new do |s|
31
31
  s.add_runtime_dependency('tty-screen', '~> 0.8', '>= 0.8.1')
32
32
  s.add_runtime_dependency('tty-which', '~> 0.5', '>= 0.5.0')
33
33
  s.add_runtime_dependency('chronic', '~> 0.10', '>= 0.10.2')
34
+ s.add_runtime_dependency('mdless', '~> 1.0', '>= 1.0.32')
34
35
  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.1.26<!--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.0
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
@@ -166,6 +166,26 @@ dependencies:
166
166
  - - ">="
167
167
  - !ruby/object:Gem::Version
168
168
  version: 0.10.2
169
+ - !ruby/object:Gem::Dependency
170
+ name: mdless
171
+ requirement: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - "~>"
174
+ - !ruby/object:Gem::Version
175
+ version: '1.0'
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: 1.0.32
179
+ type: :runtime
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '1.0'
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: 1.0.32
169
189
  description: A tool for managing a TaskPaper file of project todos for the current
170
190
  directory. Easily create "next actions" to come back to, add tags and priorities,
171
191
  and notes. Add prompt hooks to display your next actions automatically when cd'ing