na 1.2.27 → 1.2.28
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/bin/commands/add.rb +183 -0
- data/bin/commands/archive.rb +56 -0
- data/bin/commands/changes.rb +19 -0
- data/bin/commands/complete.rb +51 -0
- data/bin/commands/edit.rb +45 -0
- data/bin/commands/find.rb +135 -0
- data/bin/commands/init.rb +28 -0
- data/bin/commands/next.rb +143 -0
- data/bin/commands/projects.rb +34 -0
- data/bin/commands/prompt.rb +49 -0
- data/bin/commands/saved.rb +39 -0
- data/bin/commands/tagged.rb +146 -0
- data/bin/commands/todos.rb +28 -0
- data/bin/commands/update.rb +227 -0
- data/lib/na/version.rb +1 -1
- data/src/_README.md +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a331d16e9572f0a63e251c2de46348880b73dbce47838968ac9ac987e6a053f5
|
4
|
+
data.tar.gz: dec5319c89b04f65eb039867a5e01f6e1906c7287af2542bc7feab40f93dbf97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4ac0842dbf415b87ffa200662eea10ac9fd382406d2e953342f7c81777504e05710cf6bf622fc831309d58178c8d1622103713b82f81bfe5df07001366fb5058
|
7
|
+
data.tar.gz: 975091dac71d91659df9c1c9b69722aad42cb74a69d6a030b7a2a335d0273925e72e87cce0afb9cc4507d7c5f376bf304a6f8a806dbeb25b7d03ff259e8031a1
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
_If you're one of the rare people like me who find this useful, feel free to
|
10
10
|
[buy me some coffee][donate]._
|
11
11
|
|
12
|
-
The current version of `na` is 1.2.
|
12
|
+
The current version of `na` is 1.2.28
|
13
13
|
.
|
14
14
|
|
15
15
|
`na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
|
@@ -77,7 +77,7 @@ SYNOPSIS
|
|
77
77
|
na [global options] command [command options] [arguments...]
|
78
78
|
|
79
79
|
VERSION
|
80
|
-
1.2.
|
80
|
+
1.2.28
|
81
81
|
|
82
82
|
GLOBAL OPTIONS
|
83
83
|
-a, --add - Add a next action (deprecated, for backwards compatibility)
|
data/bin/commands/add.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Add a new next action'
|
4
|
+
long_desc 'Provides an easy way to store todos while you work. Add quick
|
5
|
+
reminders and (if you set up Prompt Hooks) they\'ll automatically display
|
6
|
+
next time you enter the directory.
|
7
|
+
|
8
|
+
If multiple todo files are found in the current directory, a menu will
|
9
|
+
allow you to pick to which file the action gets added.'
|
10
|
+
arg_name 'ACTION'
|
11
|
+
command :add do |c|
|
12
|
+
c.example 'na add "A cool feature I thought of @idea"', desc: 'Add a new action to the Inbox, including a tag'
|
13
|
+
c.example 'na add "A bug I need to fix" -p 4 -n',
|
14
|
+
desc: 'Add a new action to the Inbox, set its @priority to 4, and prompt for an additional note.'
|
15
|
+
c.example 'na add "An action item (with a note)"',
|
16
|
+
desc: 'A parenthetical at the end of an action is interpreted as a note'
|
17
|
+
|
18
|
+
c.desc 'Prompt for additional notes. STDIN input (piped) will be treated as a note if present.'
|
19
|
+
c.switch %i[n note], negatable: false
|
20
|
+
|
21
|
+
c.desc 'Add a priority level 1-5'
|
22
|
+
c.arg_name 'PRIO'
|
23
|
+
c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
|
24
|
+
|
25
|
+
c.desc 'Add action to specific project'
|
26
|
+
c.arg_name 'PROJECT'
|
27
|
+
c.default_value 'Inbox'
|
28
|
+
c.flag %i[to project proj]
|
29
|
+
|
30
|
+
c.desc 'Add task at [s]tart or [e]nd of target project'
|
31
|
+
c.arg_name 'POSITION'
|
32
|
+
c.flag %i[at], must_match: /^[sbea].*?$/i
|
33
|
+
|
34
|
+
c.desc 'Add to a known todo file, partial matches allowed'
|
35
|
+
c.arg_name 'TODO_FILE'
|
36
|
+
c.flag %i[in todo]
|
37
|
+
|
38
|
+
c.desc 'Use a tag other than the default next action tag'
|
39
|
+
c.arg_name 'TAG'
|
40
|
+
c.flag %i[t tag]
|
41
|
+
|
42
|
+
c.desc 'Don\'t add next action tag to new entry'
|
43
|
+
c.switch %i[x], negatable: false
|
44
|
+
|
45
|
+
c.desc 'Specify the file to which the task should be added'
|
46
|
+
c.arg_name 'PATH'
|
47
|
+
c.flag %i[f file]
|
48
|
+
|
49
|
+
c.desc 'Mark task as @done with date'
|
50
|
+
c.switch %i[finish done], negatable: false
|
51
|
+
|
52
|
+
c.desc 'Search for files X directories deep'
|
53
|
+
c.arg_name 'DEPTH'
|
54
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
55
|
+
|
56
|
+
c.action do |global_options, options, args|
|
57
|
+
reader = TTY::Reader.new
|
58
|
+
append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/
|
59
|
+
|
60
|
+
if NA.global_file
|
61
|
+
target = File.expand_path(NA.global_file)
|
62
|
+
unless File.exist?(target)
|
63
|
+
res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
|
64
|
+
if res
|
65
|
+
basename = File.basename(target, ".#{NA.extension}")
|
66
|
+
NA.create_todo(target, basename, template: global_options[:template])
|
67
|
+
else
|
68
|
+
puts NA::Color.template('{r}Cancelled{x}')
|
69
|
+
Process.exit 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
elsif options[:file]
|
73
|
+
target = File.expand_path(options[:file])
|
74
|
+
unless File.exist?(target)
|
75
|
+
res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
|
76
|
+
if res
|
77
|
+
basename = File.basename(target, ".#{NA.extension}")
|
78
|
+
NA.create_todo(target, basename, template: global_options[:template])
|
79
|
+
else
|
80
|
+
puts NA::Color.template('{r}Cancelled{x}')
|
81
|
+
Process.exit 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
elsif options[:todo]
|
85
|
+
todo = []
|
86
|
+
all_req = options[:todo] !~ /[+!\-]/
|
87
|
+
options[:todo].split(/ *, */).each do |a|
|
88
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
89
|
+
todo.push({
|
90
|
+
token: m['tok'],
|
91
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
92
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
93
|
+
})
|
94
|
+
end
|
95
|
+
dirs = NA.match_working_dir(todo)
|
96
|
+
if dirs.count.positive?
|
97
|
+
target = dirs[0]
|
98
|
+
else
|
99
|
+
todo = "#{options[:todo].sub(/#{NA.extension}$/, '')}.#{NA.extension}"
|
100
|
+
target = File.expand_path(todo)
|
101
|
+
unless File.exist?(target)
|
102
|
+
|
103
|
+
res = NA.yn(NA::Color.template("{by}Specified file not found, create #{todo}"), default: true)
|
104
|
+
NA.notify('{r}Cancelled{x}', exit_code: 1) unless res
|
105
|
+
|
106
|
+
basename = File.basename(target, ".#{NA.extension}")
|
107
|
+
NA.create_todo(target, basename, template: global_options[:template])
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
else
|
112
|
+
files = NA.find_files(depth: options[:depth])
|
113
|
+
if files.count.zero?
|
114
|
+
res = NA.yn(NA::Color.template('{by}No todo file found, create one'), default: true)
|
115
|
+
if res
|
116
|
+
basename = File.expand_path('.').split('/').last
|
117
|
+
target = "#{basename}.#{NA.extension}"
|
118
|
+
NA.create_todo(target, basename, template: global_options[:template])
|
119
|
+
files = NA.find_files(depth: 1)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
target = files.count > 1 ? NA.select_file(files) : files[0]
|
123
|
+
NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive? && File.exist?(target)
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
action = if args.count.positive?
|
128
|
+
args.join(' ').strip
|
129
|
+
elsif $stdin.isatty && TTY::Which.exist?('gum')
|
130
|
+
`gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
|
131
|
+
elsif $stdin.isatty
|
132
|
+
puts NA::Color.template('{bm}Enter task:{x}')
|
133
|
+
reader.read_line(NA::Color.template('{by}> {bw}')).strip
|
134
|
+
end
|
135
|
+
|
136
|
+
if action.nil? || action.empty?
|
137
|
+
puts 'Empty input, cancelled'
|
138
|
+
Process.exit 1
|
139
|
+
end
|
140
|
+
|
141
|
+
if options[:priority]&.to_i&.positive?
|
142
|
+
action = "#{action.gsub(/@priority\(\d+\)/, '')} @priority(#{options[:priority]})"
|
143
|
+
end
|
144
|
+
|
145
|
+
note_rx = /^(.+) \((.*?)\)$/
|
146
|
+
split_note = if action =~ note_rx
|
147
|
+
n = Regexp.last_match(2)
|
148
|
+
action.sub!(note_rx, '\1').strip!
|
149
|
+
n
|
150
|
+
end
|
151
|
+
|
152
|
+
na_tag = NA.na_tag
|
153
|
+
if options[:x]
|
154
|
+
na_tag = ''
|
155
|
+
else
|
156
|
+
na_tag = options[:tag] unless options[:tag].nil?
|
157
|
+
na_tag = " @#{na_tag}"
|
158
|
+
end
|
159
|
+
|
160
|
+
action = "#{action.gsub(/#{na_tag}\b/, '')}#{na_tag}"
|
161
|
+
|
162
|
+
stdin_note = NA.stdin ? NA.stdin.split("\n") : []
|
163
|
+
|
164
|
+
line_note = if options[:note] && $stdin.isatty
|
165
|
+
puts stdin_note unless stdin_note.nil?
|
166
|
+
if TTY::Which.exist?('gum')
|
167
|
+
args = ['--placeholder "Enter additional note, CTRL-d to save"']
|
168
|
+
args << '--char-limit 0'
|
169
|
+
args << '--width $(tput cols)'
|
170
|
+
`gum write #{args.join(' ')}`.strip.split("\n")
|
171
|
+
else
|
172
|
+
puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
|
173
|
+
reader.read_multiline
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
note = stdin_note.empty? ? [] : stdin_note
|
178
|
+
note.concat(split_note) unless split_note.nil?
|
179
|
+
note.concat(line_note) unless line_note.nil?
|
180
|
+
|
181
|
+
NA.add_action(target, options[:project], action, note, finish: options[:finish], append: append)
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Mark an action as @done and archive'
|
4
|
+
arg_name 'ACTION'
|
5
|
+
command %i[archive] do |c|
|
6
|
+
c.example 'na archive "An existing task"',
|
7
|
+
desc: 'Find "An existing task", mark @done if needed, and move to archive'
|
8
|
+
|
9
|
+
c.desc 'Prompt for additional notes. Input will be appended to any existing note.
|
10
|
+
If STDIN input (piped) is detected, it will be used as a note.'
|
11
|
+
c.switch %i[n note], negatable: false
|
12
|
+
|
13
|
+
c.desc 'Overwrite note instead of appending'
|
14
|
+
c.switch %i[o overwrite], negatable: false
|
15
|
+
|
16
|
+
c.desc 'Archive all done tasks'
|
17
|
+
c.switch %i[done], negatable: false
|
18
|
+
|
19
|
+
c.desc 'Specify the file to search for the task'
|
20
|
+
c.arg_name 'PATH'
|
21
|
+
c.flag %i[file]
|
22
|
+
|
23
|
+
c.desc 'Search for files X directories deep'
|
24
|
+
c.arg_name 'DEPTH'
|
25
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
26
|
+
|
27
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
28
|
+
c.arg_name 'TAG'
|
29
|
+
c.flag %i[tagged], multiple: true
|
30
|
+
|
31
|
+
c.desc 'Act on all matches immediately (no menu)'
|
32
|
+
c.switch %i[all], negatable: false
|
33
|
+
|
34
|
+
c.desc 'Interpret search pattern as regular expression'
|
35
|
+
c.switch %i[e regex], negatable: false
|
36
|
+
|
37
|
+
c.desc 'Match pattern exactly'
|
38
|
+
c.switch %i[x exact], negatable: false
|
39
|
+
|
40
|
+
c.action do |global, options, args|
|
41
|
+
if options[:done]
|
42
|
+
options[:tagged] = ['done']
|
43
|
+
options[:all] = true
|
44
|
+
end
|
45
|
+
|
46
|
+
options[:done] = true
|
47
|
+
options[:finish] = true
|
48
|
+
options[:project] = 'Archive'
|
49
|
+
options[:archive] = true
|
50
|
+
options[:a] = true
|
51
|
+
|
52
|
+
cmd = commands[:update]
|
53
|
+
action = cmd.send(:get_action, nil)
|
54
|
+
action.call(global, options, args)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Display the changelog'
|
4
|
+
command %i[changes changelog] do |c|
|
5
|
+
c.action do |_, _, _|
|
6
|
+
changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
|
7
|
+
pagers = [
|
8
|
+
'mdless',
|
9
|
+
'mdcat',
|
10
|
+
'bat',
|
11
|
+
ENV['PAGER'],
|
12
|
+
'less -FXr',
|
13
|
+
ENV['GIT_PAGER'],
|
14
|
+
'more -r'
|
15
|
+
]
|
16
|
+
pager = pagers.find { |cmd| TTY::Which.exist?(cmd.split.first) }
|
17
|
+
system %(#{pager} "#{changelog}")
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Find and mark an action as @done'
|
4
|
+
arg_name 'ACTION'
|
5
|
+
command %i[complete finish] do |c|
|
6
|
+
c.example 'na complete "An existing task"',
|
7
|
+
desc: 'Find "An existing task" and mark @done'
|
8
|
+
c.example 'na finish "An existing task"',
|
9
|
+
desc: 'Alias for complete'
|
10
|
+
|
11
|
+
c.desc 'Prompt for additional notes. Input will be appended to any existing note.
|
12
|
+
If STDIN input (piped) is detected, it will be used as a note.'
|
13
|
+
c.switch %i[n note], negatable: false
|
14
|
+
|
15
|
+
c.desc 'Overwrite note instead of appending'
|
16
|
+
c.switch %i[o overwrite], negatable: false
|
17
|
+
|
18
|
+
c.desc 'Add a @done tag to action and move to Archive'
|
19
|
+
c.switch %i[a archive], negatable: false
|
20
|
+
|
21
|
+
c.desc 'Specify the file to search for the task'
|
22
|
+
c.arg_name 'PATH'
|
23
|
+
c.flag %i[file]
|
24
|
+
|
25
|
+
c.desc 'Search for files X directories deep'
|
26
|
+
c.arg_name 'DEPTH'
|
27
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
28
|
+
|
29
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
30
|
+
c.arg_name 'TAG'
|
31
|
+
c.flag %i[tagged], multiple: true
|
32
|
+
|
33
|
+
c.desc 'Act on all matches immediately (no menu)'
|
34
|
+
c.switch %i[all], negatable: false
|
35
|
+
|
36
|
+
c.desc 'Interpret search pattern as regular expression'
|
37
|
+
c.switch %i[e regex], negatable: false
|
38
|
+
|
39
|
+
c.desc 'Match pattern exactly'
|
40
|
+
c.switch %i[x exact], negatable: false
|
41
|
+
|
42
|
+
c.action do |global, options, args|
|
43
|
+
options[:finish] = true
|
44
|
+
options[:f] = true
|
45
|
+
options[:project] = 'Archive' if options[:archive]
|
46
|
+
|
47
|
+
cmd = commands[:update]
|
48
|
+
action = cmd.send(:get_action, nil)
|
49
|
+
action.call(global, options, args)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Open a todo file in the default editor'
|
4
|
+
long_desc 'Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim).
|
5
|
+
If more than one todo file is found, a menu is displayed.'
|
6
|
+
command %i[edit] do |c|
|
7
|
+
c.example 'na edit', desc: 'Open the main todo file in the default editor'
|
8
|
+
c.example 'na edit -d 3 -a vim', desc: 'Display a menu of all todo files three levels deep from the
|
9
|
+
current directory, open selection in vim.'
|
10
|
+
|
11
|
+
c.desc 'Recurse to depth'
|
12
|
+
c.arg_name 'DEPTH'
|
13
|
+
c.default_value 1
|
14
|
+
c.flag %i[d depth], type: :integer, must_match: /^\d+$/
|
15
|
+
|
16
|
+
c.desc 'Specify an editor CLI'
|
17
|
+
c.arg_name 'EDITOR'
|
18
|
+
c.flag %i[e editor]
|
19
|
+
|
20
|
+
c.desc 'Specify a Mac app'
|
21
|
+
c.arg_name 'EDITOR'
|
22
|
+
c.flag %i[a app]
|
23
|
+
|
24
|
+
c.action do |global_options, options, args|
|
25
|
+
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
26
|
+
3
|
27
|
+
else
|
28
|
+
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
29
|
+
end
|
30
|
+
files = NA.find_files(depth: depth)
|
31
|
+
files.delete_if { |f| f !~ /.*?(#{args.join('|')}).*?.#{NA.extension}/ } if args.count.positive?
|
32
|
+
|
33
|
+
file = if files.count > 1
|
34
|
+
NA.select_file(files)
|
35
|
+
else
|
36
|
+
files[0]
|
37
|
+
end
|
38
|
+
|
39
|
+
if options[:editor]
|
40
|
+
system options[:editor], file
|
41
|
+
else
|
42
|
+
NA.edit_file(file: file, app: options[:app])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Find actions matching a search pattern'
|
4
|
+
long_desc 'Search tokens are separated by spaces. Actions matching all tokens in the pattern will be shown
|
5
|
+
(partial matches allowed). Add a + before a token to make it required, e.g. `na find +feature +maybe`,
|
6
|
+
add a - or ! to ignore matches containing that token.'
|
7
|
+
arg_name 'PATTERN'
|
8
|
+
command %i[find grep] do |c|
|
9
|
+
c.example 'na find feature idea swift', desc: 'Find all actions containing feature, idea, and swift'
|
10
|
+
c.example 'na find feature idea -swift', desc: 'Find all actions containing feature and idea but NOT swift'
|
11
|
+
c.example 'na find -x feature idea', desc: 'Find all actions containing the exact text "feature idea"'
|
12
|
+
|
13
|
+
c.desc 'Interpret search pattern as regular expression'
|
14
|
+
c.switch %i[e regex], negatable: false
|
15
|
+
|
16
|
+
c.desc 'Match pattern exactly'
|
17
|
+
c.switch %i[x exact], negatable: false
|
18
|
+
|
19
|
+
c.desc 'Recurse to depth'
|
20
|
+
c.arg_name 'DEPTH'
|
21
|
+
c.flag %i[d depth], type: :integer, must_match: /^\d+$/
|
22
|
+
|
23
|
+
c.desc 'Show actions from a specific todo file in history. May use wildcards (* and ?)'
|
24
|
+
c.arg_name 'TODO_PATH'
|
25
|
+
c.flag %i[in]
|
26
|
+
|
27
|
+
c.desc 'Include notes in output'
|
28
|
+
c.switch %i[notes], negatable: true, default_value: false
|
29
|
+
|
30
|
+
c.desc 'Combine search tokens with OR, displaying actions matching ANY of the terms'
|
31
|
+
c.switch %i[o or], negatable: false
|
32
|
+
|
33
|
+
c.desc 'Show actions from a specific project'
|
34
|
+
c.arg_name 'PROJECT[/SUBPROJECT]'
|
35
|
+
c.flag %i[proj project]
|
36
|
+
|
37
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
38
|
+
c.arg_name 'TAG'
|
39
|
+
c.flag %i[tagged], multiple: true
|
40
|
+
|
41
|
+
c.desc 'Include @done actions'
|
42
|
+
c.switch %i[done]
|
43
|
+
|
44
|
+
c.desc 'Show actions not matching search pattern'
|
45
|
+
c.switch %i[v invert], negatable: false
|
46
|
+
|
47
|
+
c.desc 'Save this search for future use'
|
48
|
+
c.arg_name 'TITLE'
|
49
|
+
c.flag %i[save]
|
50
|
+
|
51
|
+
c.desc 'Output actions nested by file'
|
52
|
+
c.switch %[nest], negatable: false
|
53
|
+
|
54
|
+
c.desc 'Output actions nested by file and project'
|
55
|
+
c.switch %[omnifocus], negatable: false
|
56
|
+
|
57
|
+
c.action do |global_options, options, args|
|
58
|
+
options[:nest] = true if options[:omnifocus]
|
59
|
+
|
60
|
+
if options[:save]
|
61
|
+
title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
|
62
|
+
NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
67
|
+
3
|
68
|
+
else
|
69
|
+
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
73
|
+
tags = []
|
74
|
+
options[:tagged].join(',').split(/ *, */).each do |arg|
|
75
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
76
|
+
|
77
|
+
tags.push({
|
78
|
+
tag: m['tag'].wildcard_to_rx,
|
79
|
+
comp: m['op'],
|
80
|
+
value: m['val'],
|
81
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
82
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
83
|
+
})
|
84
|
+
end
|
85
|
+
|
86
|
+
tokens = nil
|
87
|
+
if options[:exact]
|
88
|
+
tokens = args.join(' ')
|
89
|
+
elsif options[:regex]
|
90
|
+
tokens = Regexp.new(args.join(' '), Regexp::IGNORECASE)
|
91
|
+
else
|
92
|
+
tokens = []
|
93
|
+
all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
|
94
|
+
|
95
|
+
args.join(' ').split(/ /).each do |arg|
|
96
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
97
|
+
tokens.push({
|
98
|
+
token: m['tok'],
|
99
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
100
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
101
|
+
})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
todo = nil
|
106
|
+
if options[:in]
|
107
|
+
todo = []
|
108
|
+
options[:in].split(/ *, */).each do |a|
|
109
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
110
|
+
todo.push({
|
111
|
+
token: m['tok'],
|
112
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
113
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
114
|
+
})
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
files, actions, = NA.parse_actions(depth: depth,
|
119
|
+
done: options[:done],
|
120
|
+
query: todo,
|
121
|
+
search: tokens,
|
122
|
+
tag: tags,
|
123
|
+
negate: options[:invert],
|
124
|
+
regex: options[:regex],
|
125
|
+
project: options[:project],
|
126
|
+
require_na: false)
|
127
|
+
regexes = if tokens.is_a?(Array)
|
128
|
+
tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
129
|
+
else
|
130
|
+
[tokens]
|
131
|
+
end
|
132
|
+
|
133
|
+
NA.output_actions(actions, depth, files: files, regexes: regexes, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Create a new todo file in the current directory'
|
4
|
+
arg_name 'PROJECT', optional: true
|
5
|
+
command %i[init create] do |c|
|
6
|
+
c.example 'na init', desc: 'Generate a new todo file, prompting for project name'
|
7
|
+
c.example 'na init warpspeed', desc: 'Generate a new todo for a project called warpspeed'
|
8
|
+
|
9
|
+
c.action do |global_options, _options, args|
|
10
|
+
reader = TTY::Reader.new
|
11
|
+
if args.count.positive?
|
12
|
+
project = args.join(' ')
|
13
|
+
elsif
|
14
|
+
project = File.expand_path('.').split('/').last
|
15
|
+
project = reader.read_line(NA::Color.template('{y}Project name {bw}> {x}'), value: project).strip if $stdin.isatty
|
16
|
+
end
|
17
|
+
|
18
|
+
target = "#{project}.#{NA.extension}"
|
19
|
+
|
20
|
+
if File.exist?(target)
|
21
|
+
res = NA.yn(NA::Color.template("{r}File {bw}#{target}{r} already exists, overwrite it"), default: false)
|
22
|
+
Process.exit 1 unless res
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
NA.create_todo(target, project, template: global_options[:template])
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Show next actions'
|
4
|
+
long_desc 'Next actions are actions which contain the next action tag (default @na),
|
5
|
+
do not contain @done, and are not in the Archive project.
|
6
|
+
|
7
|
+
Arguments will target a todo file from history, whether it\'s in the current
|
8
|
+
directory or not. Todo file queries can include path components separated by /
|
9
|
+
or :, and may use wildcards (`*` to match any text, `?` to match a single character). Multiple queries allowed (separate arguments or separated by comma).'
|
10
|
+
arg_name 'QUERY', optional: true
|
11
|
+
command %i[next show] do |c|
|
12
|
+
c.example 'na next', desc: 'display the next actions from any todo files in the current directory'
|
13
|
+
c.example 'na next -d 3', desc: 'display the next actions from the current directory, traversing 3 levels deep'
|
14
|
+
c.example 'na next marked', desc: 'display next actions for a project you visited in the past'
|
15
|
+
|
16
|
+
c.desc 'Recurse to depth'
|
17
|
+
c.arg_name 'DEPTH'
|
18
|
+
c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
|
19
|
+
|
20
|
+
c.desc 'Display matches from a known todo file'
|
21
|
+
c.arg_name 'TODO_FILE'
|
22
|
+
c.flag %i[in todo], multiple: true
|
23
|
+
|
24
|
+
c.desc 'Alternate tag to search for'
|
25
|
+
c.arg_name 'TAG'
|
26
|
+
c.flag %i[t tag]
|
27
|
+
|
28
|
+
c.desc 'Show actions from a specific project'
|
29
|
+
c.arg_name 'PROJECT[/SUBPROJECT]'
|
30
|
+
c.flag %i[proj project]
|
31
|
+
|
32
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
33
|
+
c.arg_name 'TAG'
|
34
|
+
c.flag %i[tagged], multiple: true
|
35
|
+
|
36
|
+
c.desc 'Filter results using search terms'
|
37
|
+
c.arg_name 'QUERY'
|
38
|
+
c.flag %i[search], multiple: true
|
39
|
+
|
40
|
+
c.desc 'Search query is regular expression'
|
41
|
+
c.switch %i[regex], negatable: false
|
42
|
+
|
43
|
+
c.desc 'Search query is exact text match (not tokens)'
|
44
|
+
c.switch %i[exact], negatable: false
|
45
|
+
|
46
|
+
c.desc 'Include notes in output'
|
47
|
+
c.switch %i[notes], negatable: true, default_value: false
|
48
|
+
|
49
|
+
c.desc 'Include @done actions'
|
50
|
+
c.switch %i[done]
|
51
|
+
|
52
|
+
c.desc 'Output actions nested by file'
|
53
|
+
c.switch %[nest], negatable: false
|
54
|
+
|
55
|
+
c.desc 'Output actions nested by file and project'
|
56
|
+
c.switch %[omnifocus], negatable: false
|
57
|
+
|
58
|
+
c.action do |global_options, options, args|
|
59
|
+
if global_options[:add]
|
60
|
+
cmd = ['add']
|
61
|
+
cmd.push('--note') if global_options[:note]
|
62
|
+
cmd.concat(['--priority', global_options[:priority]]) if global_options[:priority]
|
63
|
+
cmd.push(NA.command_line) if NA.command_line.count > 1
|
64
|
+
cmd.unshift(*NA.globals)
|
65
|
+
exit run(cmd)
|
66
|
+
end
|
67
|
+
|
68
|
+
options[:nest] = true if options[:omnifocus]
|
69
|
+
|
70
|
+
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
71
|
+
3
|
72
|
+
else
|
73
|
+
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
77
|
+
tags = []
|
78
|
+
options[:tagged].join(',').split(/ *, */).each do |arg|
|
79
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
80
|
+
|
81
|
+
tags.push({
|
82
|
+
tag: m['tag'].wildcard_to_rx,
|
83
|
+
comp: m['op'],
|
84
|
+
value: m['val'],
|
85
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
86
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
87
|
+
})
|
88
|
+
end
|
89
|
+
|
90
|
+
args.concat(options[:in])
|
91
|
+
if args.count.positive?
|
92
|
+
all_req = args.join(' ') !~ /[+!\-]/
|
93
|
+
|
94
|
+
tokens = []
|
95
|
+
args.each do |arg|
|
96
|
+
arg.split(/ *, */).each do |a|
|
97
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
98
|
+
tokens.push({
|
99
|
+
token: m['tok'],
|
100
|
+
required: !m['req'].nil? && m['req'] == '+',
|
101
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
102
|
+
})
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
search = nil
|
108
|
+
if options[:search]
|
109
|
+
if options[:exact]
|
110
|
+
search = options[:search].join(' ')
|
111
|
+
elsif options[:regex]
|
112
|
+
search = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
|
113
|
+
else
|
114
|
+
search = []
|
115
|
+
all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
|
116
|
+
|
117
|
+
options[:search].join(' ').split(/ /).each do |arg|
|
118
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
119
|
+
search.push({
|
120
|
+
token: m['tok'],
|
121
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
122
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
123
|
+
})
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
NA.na_tag = options[:tag] unless options[:tag].nil?
|
129
|
+
require_na = true
|
130
|
+
|
131
|
+
tag = [{ tag: NA.na_tag, value: nil }, { tag: 'done', value: nil, negate: true }]
|
132
|
+
tag.concat(tags)
|
133
|
+
files, actions, = NA.parse_actions(depth: depth,
|
134
|
+
done: options[:done],
|
135
|
+
query: tokens,
|
136
|
+
tag: tag,
|
137
|
+
search: search,
|
138
|
+
project: options[:project],
|
139
|
+
require_na: require_na)
|
140
|
+
|
141
|
+
NA.output_actions(actions, depth, files: files, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Show list of projects for a file'
|
4
|
+
long_desc 'Arguments will be interpreted as a query for a known todo file,
|
5
|
+
fuzzy matched. Separate directories with /, :, or a space, e.g. `na projects code/marked`'
|
6
|
+
arg_name 'QUERY', optional: true
|
7
|
+
command %i[projects] do |c|
|
8
|
+
c.desc 'Search for files X directories deep'
|
9
|
+
c.arg_name 'DEPTH'
|
10
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
11
|
+
|
12
|
+
c.desc 'Output projects as paths instead of hierarchy'
|
13
|
+
c.switch %i[p paths], negatable: false
|
14
|
+
|
15
|
+
c.action do |_global_options, options, args|
|
16
|
+
if args.count.positive?
|
17
|
+
all_req = args.join(' ') !~ /[+!-]/
|
18
|
+
|
19
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
20
|
+
args.each do |arg|
|
21
|
+
arg.split(/ *, */).each do |a|
|
22
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
23
|
+
tokens.push({
|
24
|
+
token: m['tok'],
|
25
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
26
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/
|
27
|
+
})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
NA.list_projects(query: tokens, depth: options[:depth], paths: options[:paths])
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Show or install prompt hooks for the current shell'
|
4
|
+
long_desc 'Installing the prompt hook allows you to automatically
|
5
|
+
list next actions when you cd into a directory'
|
6
|
+
command %i[prompt] do |c|
|
7
|
+
c.desc 'Output the prompt hook for the current shell to STDOUT. Pass an argument to
|
8
|
+
specify a shell (zsh, bash, fish)'
|
9
|
+
c.arg_name 'SHELL', optional: true
|
10
|
+
c.command %i[show] do |s|
|
11
|
+
s.action do |_global_options, _options, args|
|
12
|
+
shell = if args.count.positive?
|
13
|
+
args[0]
|
14
|
+
else
|
15
|
+
File.basename(ENV['SHELL'])
|
16
|
+
end
|
17
|
+
|
18
|
+
case shell
|
19
|
+
when /^f/i
|
20
|
+
NA::Prompt.show_prompt_hook(:fish)
|
21
|
+
when /^z/i
|
22
|
+
NA::Prompt.show_prompt_hook(:zsh)
|
23
|
+
when /^b/i
|
24
|
+
NA::Prompt.show_prompt_hook(:bash)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
c.desc 'Install the hook for the current shell to the appropriate startup file.'
|
30
|
+
c.arg_name 'SHELL', optional: true
|
31
|
+
c.command %i[install] do |s|
|
32
|
+
s.action do |_global_options, _options, args|
|
33
|
+
shell = if args.count.positive?
|
34
|
+
args[0]
|
35
|
+
else
|
36
|
+
File.basename(ENV['SHELL'])
|
37
|
+
end
|
38
|
+
|
39
|
+
case shell
|
40
|
+
when /^f/i
|
41
|
+
NA::Prompt.install_prompt_hook(:fish)
|
42
|
+
when /^z/i
|
43
|
+
NA::Prompt.install_prompt_hook(:zsh)
|
44
|
+
when /^b/i
|
45
|
+
NA::Prompt.install_prompt_hook(:bash)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Execute a saved search'
|
4
|
+
long_desc 'Run without argument to list saved searches'
|
5
|
+
arg_name 'SEARCH_TITLE', optional: true
|
6
|
+
command %i[saved] do |c|
|
7
|
+
c.example 'na tagged "+maybe,+priority<=3" --save maybelater', description: 'save a search called "maybelater"'
|
8
|
+
c.example 'na saved maybelater', description: 'perform the search named "maybelater"'
|
9
|
+
c.example 'na saved maybe',
|
10
|
+
description: 'perform the search named "maybelater", assuming no other searches match "maybe"'
|
11
|
+
c.example 'na maybe',
|
12
|
+
description: 'na run with no command and a single argument automatically performs a matching saved search'
|
13
|
+
c.example 'na saved', description: 'list available searches'
|
14
|
+
|
15
|
+
c.desc 'Open the saved search file in $EDITOR'
|
16
|
+
c.switch %i[e edit], negatable: false
|
17
|
+
|
18
|
+
c.desc 'Delete the specified search definition'
|
19
|
+
c.switch %i[d delete], negatable: false
|
20
|
+
|
21
|
+
c.action do |_global_options, options, args|
|
22
|
+
NA.edit_searches if options[:edit]
|
23
|
+
|
24
|
+
searches = NA.load_searches
|
25
|
+
if args.empty?
|
26
|
+
NA.notify("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
|
27
|
+
NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
|
28
|
+
else
|
29
|
+
NA.delete_search(args) if options[:delete]
|
30
|
+
|
31
|
+
keys = searches.keys.delete_if { |k| k !~ /#{args[0]}/ }
|
32
|
+
NA.notify("{r}Search #{args[0]} not found", exit_code: 1) if keys.empty?
|
33
|
+
|
34
|
+
key = keys[0]
|
35
|
+
cmd = Shellwords.shellsplit(searches[key])
|
36
|
+
exit run(cmd)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Find actions matching a tag'
|
4
|
+
long_desc 'Finds actions with tags matching the arguments. An action is shown if it
|
5
|
+
contains all of the tags listed. Add a + before a tag to make it required
|
6
|
+
and others optional. You can specify values using TAG=VALUE pairs.
|
7
|
+
Use <, >, and = for numeric comparisons, and *=, ^=, and $= for text comparisons.
|
8
|
+
Date comparisons use natural language (`na tagged "due<=today"`) and
|
9
|
+
are detected automatically.'
|
10
|
+
arg_name 'TAG[=VALUE]'
|
11
|
+
command %i[tagged] do |c|
|
12
|
+
c.example 'na tagged maybe', desc: 'Show all actions tagged @maybe'
|
13
|
+
c.example 'na tagged -d 3 "feature, idea"', desc: 'Show all actions tagged @feature AND @idea, recurse 3 levels'
|
14
|
+
c.example 'na tagged --or "feature, idea"', desc: 'Show all actions tagged @feature OR @idea'
|
15
|
+
c.example 'na tagged "priority>=4"', desc: 'Show actions with @priority(4) or @priority(5)'
|
16
|
+
c.example 'na tagged "due<in 2 days"', desc: 'Show actions with a due date coming up in the next 2 days'
|
17
|
+
|
18
|
+
c.desc 'Recurse to depth'
|
19
|
+
c.arg_name 'DEPTH'
|
20
|
+
c.default_value 1
|
21
|
+
c.flag %i[d depth], type: :integer, must_match: /^\d+$/
|
22
|
+
|
23
|
+
c.desc 'Show actions from a specific todo file in history. May use wildcards (* and ?)'
|
24
|
+
c.arg_name 'TODO_PATH'
|
25
|
+
c.flag %i[in]
|
26
|
+
|
27
|
+
c.desc 'Include notes in output'
|
28
|
+
c.switch %i[notes], negatable: true, default_value: false
|
29
|
+
|
30
|
+
c.desc 'Combine tags with OR, displaying actions matching ANY of the tags'
|
31
|
+
c.switch %i[o or], negatable: false
|
32
|
+
|
33
|
+
c.desc 'Show actions from a specific project'
|
34
|
+
c.arg_name 'PROJECT[/SUBPROJECT]'
|
35
|
+
c.flag %i[proj project]
|
36
|
+
|
37
|
+
c.desc 'Filter results using search terms'
|
38
|
+
c.arg_name 'QUERY'
|
39
|
+
c.flag %i[search], multiple: true
|
40
|
+
|
41
|
+
c.desc 'Search query is regular expression'
|
42
|
+
c.switch %i[regex], negatable: false
|
43
|
+
|
44
|
+
c.desc 'Search query is exact text match (not tokens)'
|
45
|
+
c.switch %i[exact], negatable: false
|
46
|
+
|
47
|
+
c.desc 'Include @done actions'
|
48
|
+
c.switch %i[done]
|
49
|
+
|
50
|
+
c.desc 'Show actions not matching tags'
|
51
|
+
c.switch %i[v invert], negatable: false
|
52
|
+
|
53
|
+
c.desc 'Save this search for future use'
|
54
|
+
c.arg_name 'TITLE'
|
55
|
+
c.flag %i[save]
|
56
|
+
|
57
|
+
c.desc 'Output actions nested by file'
|
58
|
+
c.switch %[nest], negatable: false
|
59
|
+
|
60
|
+
c.desc 'Output actions nested by file and project'
|
61
|
+
c.switch %[omnifocus], negatable: false
|
62
|
+
|
63
|
+
c.action do |global_options, options, args|
|
64
|
+
options[:nest] = true if options[:omnifocus]
|
65
|
+
|
66
|
+
if options[:save]
|
67
|
+
title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
|
68
|
+
NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
|
69
|
+
end
|
70
|
+
|
71
|
+
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
72
|
+
3
|
73
|
+
else
|
74
|
+
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
tags = []
|
78
|
+
|
79
|
+
all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
|
80
|
+
args.join(',').split(/ *, */).each do |arg|
|
81
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
82
|
+
|
83
|
+
tags.push({
|
84
|
+
tag: m['tag'].wildcard_to_rx,
|
85
|
+
comp: m['op'],
|
86
|
+
value: m['val'],
|
87
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
88
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
89
|
+
})
|
90
|
+
end
|
91
|
+
|
92
|
+
search_for_done = false
|
93
|
+
tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
|
94
|
+
tags.push({ tag: 'done', value: nil, negate: true}) unless search_for_done
|
95
|
+
|
96
|
+
tokens = nil
|
97
|
+
if options[:search]
|
98
|
+
if options[:exact]
|
99
|
+
tokens = options[:search].join(' ')
|
100
|
+
elsif options[:regex]
|
101
|
+
tokens = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
|
102
|
+
else
|
103
|
+
tokens = []
|
104
|
+
all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
|
105
|
+
|
106
|
+
options[:search].join(' ').split(/ /).each do |arg|
|
107
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
108
|
+
tokens.push({
|
109
|
+
token: m['tok'],
|
110
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
111
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
112
|
+
})
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
todo = nil
|
118
|
+
if options[:in]
|
119
|
+
todo = []
|
120
|
+
options[:in].split(/ *, */).each do |a|
|
121
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
122
|
+
todo.push({
|
123
|
+
token: m['tok'],
|
124
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
125
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
126
|
+
})
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
files, actions, = NA.parse_actions(depth: depth,
|
131
|
+
done: options[:done],
|
132
|
+
query: todo,
|
133
|
+
search: tokens,
|
134
|
+
tag: tags,
|
135
|
+
negate: options[:invert],
|
136
|
+
project: options[:project],
|
137
|
+
require_na: false)
|
138
|
+
# regexes = tags.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
139
|
+
regexes = if tokens.is_a?(Array)
|
140
|
+
tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
|
141
|
+
else
|
142
|
+
[tokens]
|
143
|
+
end
|
144
|
+
NA.output_actions(actions, depth, files: files, regexes: regexes, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Show list of known todo files'
|
4
|
+
long_desc 'Arguments will be interpreted as a query against which the
|
5
|
+
list of todos will be fuzzy matched. Separate directories with
|
6
|
+
/, :, or a space, e.g. `na todos code/marked`'
|
7
|
+
arg_name 'QUERY', optional: true
|
8
|
+
command %i[todos] do |c|
|
9
|
+
c.action do |_global_options, _options, args|
|
10
|
+
if args.count.positive?
|
11
|
+
all_req = args.join(' ') !~ /[+!\-]/
|
12
|
+
|
13
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
14
|
+
args.each do |arg|
|
15
|
+
arg.split(/ *, */).each do |a|
|
16
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
17
|
+
tokens.push({
|
18
|
+
token: m['tok'],
|
19
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
20
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
21
|
+
})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
NA.list_todos(query: tokens)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc 'Update an existing action'
|
4
|
+
long_desc 'Provides an easy way to complete, prioritize, and tag existing actions.
|
5
|
+
|
6
|
+
If multiple todo files are found in the current directory, a menu will
|
7
|
+
allow you to pick which file to act on.'
|
8
|
+
arg_name 'ACTION'
|
9
|
+
command %i[update] do |c|
|
10
|
+
c.example 'na update --remove na "An existing task"',
|
11
|
+
desc: 'Find "An existing task" action and remove the @na tag from it'
|
12
|
+
c.example 'na update --tag waiting "A bug I need to fix" -p 4 -n',
|
13
|
+
desc: 'Find "A bug..." action, add @waiting, add/update @priority(4), and prompt for an additional note'
|
14
|
+
c.example 'na update --archive My cool action',
|
15
|
+
desc: 'Add @done to "My cool action" and immediately move to Archive'
|
16
|
+
|
17
|
+
c.desc 'Prompt for additional notes. Input will be appended to any existing note.
|
18
|
+
If STDIN input (piped) is detected, it will be used as a note.'
|
19
|
+
c.switch %i[n note], negatable: false
|
20
|
+
|
21
|
+
c.desc 'Overwrite note instead of appending'
|
22
|
+
c.switch %i[o overwrite], negatable: false
|
23
|
+
|
24
|
+
c.desc 'Add/change a priority level 1-5'
|
25
|
+
c.arg_name 'PRIO'
|
26
|
+
c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
|
27
|
+
|
28
|
+
c.desc 'When moving task, add at [s]tart or [e]nd of target project'
|
29
|
+
c.arg_name 'POSITION'
|
30
|
+
c.flag %i[at], must_match: /^[sbea].*?$/i
|
31
|
+
|
32
|
+
c.desc 'Move action to specific project'
|
33
|
+
c.arg_name 'PROJECT'
|
34
|
+
c.flag %i[to project proj]
|
35
|
+
|
36
|
+
c.desc 'Use a known todo file, partial matches allowed'
|
37
|
+
c.arg_name 'TODO_FILE'
|
38
|
+
c.flag %i[in todo]
|
39
|
+
|
40
|
+
c.desc 'Include @done actions'
|
41
|
+
c.switch %i[done]
|
42
|
+
|
43
|
+
c.desc 'Add a tag to the action, @tag(values) allowed'
|
44
|
+
c.arg_name 'TAG'
|
45
|
+
c.flag %i[t tag], multiple: true
|
46
|
+
|
47
|
+
c.desc 'Remove a tag to the action'
|
48
|
+
c.arg_name 'TAG'
|
49
|
+
c.flag %i[r remove], multiple: true
|
50
|
+
|
51
|
+
c.desc 'Add a @done tag to action'
|
52
|
+
c.switch %i[f finish], negatable: false
|
53
|
+
|
54
|
+
c.desc 'Add a @done tag to action and move to Archive'
|
55
|
+
c.switch %i[a archive], negatable: false
|
56
|
+
|
57
|
+
c.desc 'Delete an action'
|
58
|
+
c.switch %i[delete], negatable: false
|
59
|
+
|
60
|
+
c.desc 'Specify the file to search for the task'
|
61
|
+
c.arg_name 'PATH'
|
62
|
+
c.flag %i[file]
|
63
|
+
|
64
|
+
c.desc 'Search for files X directories deep'
|
65
|
+
c.arg_name 'DEPTH'
|
66
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
67
|
+
|
68
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
69
|
+
c.arg_name 'TAG'
|
70
|
+
c.flag %i[tagged], multiple: true
|
71
|
+
|
72
|
+
c.desc 'Act on all matches immediately (no menu)'
|
73
|
+
c.switch %i[all], negatable: false
|
74
|
+
|
75
|
+
c.desc 'Interpret search pattern as regular expression'
|
76
|
+
c.switch %i[e regex], negatable: false
|
77
|
+
|
78
|
+
c.desc 'Match pattern exactly'
|
79
|
+
c.switch %i[x exact], negatable: false
|
80
|
+
|
81
|
+
c.action do |global_options, options, args|
|
82
|
+
reader = TTY::Reader.new
|
83
|
+
append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/i
|
84
|
+
|
85
|
+
action = if args.count.positive?
|
86
|
+
args.join(' ').strip
|
87
|
+
elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
|
88
|
+
options = [
|
89
|
+
%(--placeholder "Enter a task to search for"),
|
90
|
+
'--char-limit=500',
|
91
|
+
"--width=#{TTY::Screen.columns}"
|
92
|
+
]
|
93
|
+
`gum input #{options.join(' ')}`.strip
|
94
|
+
elsif $stdin.isatty && options[:tagged].empty?
|
95
|
+
puts NA::Color.template('{bm}Enter search string:{x}')
|
96
|
+
reader.read_line(NA::Color.template('{by}> {bw}')).strip
|
97
|
+
end
|
98
|
+
|
99
|
+
if action
|
100
|
+
tokens = nil
|
101
|
+
if options[:exact]
|
102
|
+
tokens = action
|
103
|
+
elsif options[:regex]
|
104
|
+
tokens = Regexp.new(action, Regexp::IGNORECASE)
|
105
|
+
else
|
106
|
+
tokens = []
|
107
|
+
all_req = action !~ /[+!\-]/ && !options[:or]
|
108
|
+
|
109
|
+
action.split(/ /).each do |arg|
|
110
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
111
|
+
tokens.push({
|
112
|
+
token: m['tok'],
|
113
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
114
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
115
|
+
})
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
if (action.nil? || action.empty?) && options[:tagged].empty?
|
121
|
+
puts 'Empty input, cancelled'
|
122
|
+
Process.exit 1
|
123
|
+
end
|
124
|
+
|
125
|
+
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
126
|
+
tags = []
|
127
|
+
options[:tagged].join(',').split(/ *, */).each do |arg|
|
128
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
|
129
|
+
|
130
|
+
tags.push({
|
131
|
+
tag: m['tag'].wildcard_to_rx,
|
132
|
+
comp: m['op'],
|
133
|
+
value: m['val'],
|
134
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
135
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
136
|
+
})
|
137
|
+
end
|
138
|
+
|
139
|
+
priority = options[:priority].to_i if options[:priority]&.to_i&.positive?
|
140
|
+
add_tags = options[:tag] ? options[:tag].map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
|
141
|
+
remove_tags = options[:remove] ? options[:remove].map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
|
142
|
+
|
143
|
+
stdin_note = NA.stdin ? NA.stdin.split("\n") : []
|
144
|
+
|
145
|
+
line_note = if options[:note] && $stdin.isatty
|
146
|
+
puts stdin_note unless stdin_note.nil?
|
147
|
+
if TTY::Which.exist?('gum')
|
148
|
+
args = ['--placeholder "Enter a note, CTRL-d to save"']
|
149
|
+
args << '--char-limit 0'
|
150
|
+
args << '--width $(tput cols)'
|
151
|
+
`gum write #{args.join(' ')}`.strip.split("\n")
|
152
|
+
else
|
153
|
+
puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
|
154
|
+
reader.read_multiline
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
note = stdin_note.empty? ? [] : stdin_note
|
159
|
+
note.concat(line_note) unless line_note.nil? || line_note.empty?
|
160
|
+
|
161
|
+
target_proj = if options[:project]
|
162
|
+
options[:project]
|
163
|
+
elsif NA.cwd_is == :project
|
164
|
+
NA.cwd
|
165
|
+
else
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
|
169
|
+
if options[:file]
|
170
|
+
file = File.expand_path(options[:file])
|
171
|
+
NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
|
172
|
+
|
173
|
+
targets = [file]
|
174
|
+
elsif options[:todo]
|
175
|
+
todo = []
|
176
|
+
options[:todo].split(/ *, */).each do |a|
|
177
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
178
|
+
todo.push({
|
179
|
+
token: m['tok'],
|
180
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
181
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
182
|
+
})
|
183
|
+
end
|
184
|
+
dirs = NA.match_working_dir(todo)
|
185
|
+
|
186
|
+
if dirs.count == 1
|
187
|
+
targets = [dirs[0]]
|
188
|
+
elsif dirs.count.positive?
|
189
|
+
targets = NA.select_file(dirs, multiple: true)
|
190
|
+
NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
|
191
|
+
else
|
192
|
+
NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
|
193
|
+
|
194
|
+
end
|
195
|
+
else
|
196
|
+
files = NA.find_files(depth: options[:depth])
|
197
|
+
NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
|
198
|
+
|
199
|
+
targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
|
200
|
+
NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
if options[:archive]
|
205
|
+
options[:finish] = true
|
206
|
+
options[:project] = 'Archive'
|
207
|
+
end
|
208
|
+
|
209
|
+
NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
210
|
+
|
211
|
+
targets.each do |target|
|
212
|
+
NA.update_action(target, tokens,
|
213
|
+
priority: priority,
|
214
|
+
add_tag: add_tags,
|
215
|
+
remove_tag: remove_tags,
|
216
|
+
finish: options[:finish],
|
217
|
+
project: target_proj,
|
218
|
+
delete: options[:delete],
|
219
|
+
note: note,
|
220
|
+
overwrite: options[:overwrite],
|
221
|
+
tagged: tags,
|
222
|
+
all: options[:all],
|
223
|
+
done: options[:done],
|
224
|
+
append: append)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
data/lib/na/version.rb
CHANGED
data/src/_README.md
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
_If you're one of the rare people like me who find this useful, feel free to
|
10
10
|
[buy me some coffee][donate]._
|
11
11
|
|
12
|
-
The current version of `na` is <!--VER-->1.2.
|
12
|
+
The current version of `na` is <!--VER-->1.2.27<!--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.
|
4
|
+
version: 1.2.28
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brett Terpstra
|
@@ -206,6 +206,20 @@ files:
|
|
206
206
|
- README.md
|
207
207
|
- README.rdoc
|
208
208
|
- Rakefile
|
209
|
+
- bin/commands/add.rb
|
210
|
+
- bin/commands/archive.rb
|
211
|
+
- bin/commands/changes.rb
|
212
|
+
- bin/commands/complete.rb
|
213
|
+
- bin/commands/edit.rb
|
214
|
+
- bin/commands/find.rb
|
215
|
+
- bin/commands/init.rb
|
216
|
+
- bin/commands/next.rb
|
217
|
+
- bin/commands/projects.rb
|
218
|
+
- bin/commands/prompt.rb
|
219
|
+
- bin/commands/saved.rb
|
220
|
+
- bin/commands/tagged.rb
|
221
|
+
- bin/commands/todos.rb
|
222
|
+
- bin/commands/update.rb
|
209
223
|
- bin/na
|
210
224
|
- lib/na.rb
|
211
225
|
- lib/na/action.rb
|