na 1.2.29 → 1.2.30
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +1 -1
- data/README.md +28 -20
- data/bin/commands/add.rb +167 -164
- data/bin/commands/archive.rb +54 -51
- data/bin/commands/changes.rb +18 -15
- data/bin/commands/complete.rb +50 -47
- data/bin/commands/completed.rb +99 -0
- data/bin/commands/edit.rb +37 -34
- data/bin/commands/find.rb +109 -106
- data/bin/commands/init.rb +22 -19
- data/bin/commands/next.rb +135 -131
- data/bin/commands/projects.rb +27 -24
- data/bin/commands/prompt.rb +41 -38
- data/bin/commands/saved.rb +42 -34
- data/bin/commands/tagged.rb +137 -134
- data/bin/commands/todos.rb +23 -20
- data/bin/commands/update.rb +215 -207
- data/bin/na +7 -1166
- data/lib/na/array.rb +13 -0
- data/lib/na/help_monkey_patch.rb +32 -0
- data/lib/na/next_action.rb +3 -2
- data/lib/na/pager.rb +94 -0
- data/lib/na/string.rb +10 -0
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +2 -0
- metadata +7 -3
data/bin/commands/next.rb
CHANGED
@@ -1,143 +1,147 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
3
|
+
class App
|
4
|
+
extend GLI::App
|
5
|
+
desc 'Show next actions'
|
6
|
+
long_desc 'Next actions are actions which contain the next action tag (default @na),
|
7
|
+
do not contain @done, and are not in the Archive project.
|
8
|
+
|
9
|
+
Arguments will target a todo file from history, whether it\'s in the current
|
10
|
+
directory or not. Todo file queries can include path components separated by /
|
11
|
+
or :, and may use wildcards (`*` to match any text, `?` to match a single character). Multiple queries allowed (separate arguments or separated by comma).'
|
12
|
+
arg_name 'QUERY', optional: true
|
13
|
+
command %i[next show] do |c|
|
14
|
+
c.example 'na next', desc: 'display the next actions from any todo files in the current directory'
|
15
|
+
c.example 'na next -d 3', desc: 'display the next actions from the current directory, traversing 3 levels deep'
|
16
|
+
c.example 'na next marked', desc: 'display next actions for a project you visited in the past'
|
17
|
+
|
18
|
+
c.desc 'Recurse to depth'
|
19
|
+
c.arg_name 'DEPTH'
|
20
|
+
c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
|
21
|
+
|
22
|
+
c.desc 'Display matches from a known todo file'
|
23
|
+
c.arg_name 'TODO_FILE'
|
24
|
+
c.flag %i[in todo], multiple: true
|
25
|
+
|
26
|
+
c.desc 'Alternate tag to search for'
|
27
|
+
c.arg_name 'TAG'
|
28
|
+
c.flag %i[t tag]
|
29
|
+
|
30
|
+
c.desc 'Show actions from a specific project'
|
31
|
+
c.arg_name 'PROJECT[/SUBPROJECT]'
|
32
|
+
c.flag %i[proj project]
|
33
|
+
|
34
|
+
c.desc 'Match actions containing tag. Allows value comparisons'
|
35
|
+
c.arg_name 'TAG'
|
36
|
+
c.flag %i[tagged], multiple: true
|
37
|
+
|
38
|
+
c.desc 'Filter results using search terms'
|
39
|
+
c.arg_name 'QUERY'
|
40
|
+
c.flag %i[search], multiple: true
|
41
|
+
|
42
|
+
c.desc 'Search query is regular expression'
|
43
|
+
c.switch %i[regex], negatable: false
|
44
|
+
|
45
|
+
c.desc 'Search query is exact text match (not tokens)'
|
46
|
+
c.switch %i[exact], negatable: false
|
47
|
+
|
48
|
+
c.desc 'Include notes in output'
|
49
|
+
c.switch %i[notes], negatable: true, default_value: false
|
50
|
+
|
51
|
+
c.desc 'Include @done actions'
|
52
|
+
c.switch %i[done]
|
53
|
+
|
54
|
+
c.desc 'Output actions nested by file'
|
55
|
+
c.switch %[nest], negatable: false
|
56
|
+
|
57
|
+
c.desc 'Output actions nested by file and project'
|
58
|
+
c.switch %[omnifocus], negatable: false
|
59
|
+
|
60
|
+
c.action do |global_options, options, args|
|
61
|
+
if global_options[:add]
|
62
|
+
cmd = ['add']
|
63
|
+
cmd.push('--note') if global_options[:note]
|
64
|
+
cmd.concat(['--priority', global_options[:priority]]) if global_options[:priority]
|
65
|
+
cmd.push(NA.command_line) if NA.command_line.count > 1
|
66
|
+
cmd.unshift(*NA.globals)
|
67
|
+
exit run(cmd)
|
68
|
+
end
|
67
69
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
70
|
+
options[:nest] = true if options[:omnifocus]
|
71
|
+
|
72
|
+
depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
|
73
|
+
3
|
74
|
+
else
|
75
|
+
options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
|
79
|
+
tags = []
|
80
|
+
options[:tagged].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
|
89
91
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
92
|
+
args.concat(options[:in])
|
93
|
+
if args.count.positive?
|
94
|
+
all_req = args.join(' ') !~ /[+!\-]/
|
95
|
+
|
96
|
+
tokens = []
|
97
|
+
args.each do |arg|
|
98
|
+
arg.split(/ *, */).each do |a|
|
99
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
100
|
+
tokens.push({
|
101
|
+
token: m['tok'],
|
102
|
+
required: !m['req'].nil? && m['req'] == '+',
|
103
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
104
|
+
})
|
105
|
+
end
|
103
106
|
end
|
104
107
|
end
|
105
|
-
end
|
106
108
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
109
|
+
search = nil
|
110
|
+
if options[:search]
|
111
|
+
if options[:exact]
|
112
|
+
search = options[:search].join(' ')
|
113
|
+
elsif options[:regex]
|
114
|
+
search = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
|
115
|
+
else
|
116
|
+
search = []
|
117
|
+
all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
|
118
|
+
|
119
|
+
options[:search].join(' ').split(/ /).each do |arg|
|
120
|
+
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
121
|
+
search.push({
|
122
|
+
token: m['tok'],
|
123
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
124
|
+
negate: !m['req'].nil? && m['req'] =~ /[!\-]/
|
125
|
+
})
|
126
|
+
end
|
124
127
|
end
|
125
128
|
end
|
126
|
-
end
|
127
|
-
|
128
|
-
NA.na_tag = options[:tag] unless options[:tag].nil?
|
129
|
-
require_na = true
|
130
129
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
130
|
+
NA.na_tag = options[:tag] unless options[:tag].nil?
|
131
|
+
require_na = true
|
132
|
+
|
133
|
+
tag = [{ tag: NA.na_tag, value: nil }]
|
134
|
+
tag << { tag: 'done', value: nil, negate: true } unless options[:done]
|
135
|
+
tag.concat(tags)
|
136
|
+
files, actions, = NA.parse_actions(depth: depth,
|
137
|
+
done: options[:done],
|
138
|
+
query: tokens,
|
139
|
+
tag: tag,
|
140
|
+
search: search,
|
141
|
+
project: options[:project],
|
142
|
+
require_na: require_na)
|
143
|
+
|
144
|
+
NA.output_actions(actions, depth, files: files, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
|
145
|
+
end
|
142
146
|
end
|
143
147
|
end
|
data/bin/commands/projects.rb
CHANGED
@@ -1,34 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
c
|
10
|
-
|
3
|
+
class App
|
4
|
+
extend GLI::App
|
5
|
+
desc 'Show list of projects for a file'
|
6
|
+
long_desc 'Arguments will be interpreted as a query for a known todo file,
|
7
|
+
fuzzy matched. Separate directories with /, :, or a space, e.g. `na projects code/marked`'
|
8
|
+
arg_name 'QUERY', optional: true
|
9
|
+
command %i[projects] do |c|
|
10
|
+
c.desc 'Search for files X directories deep'
|
11
|
+
c.arg_name 'DEPTH'
|
12
|
+
c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
c.desc 'Output projects as paths instead of hierarchy'
|
15
|
+
c.switch %i[p paths], negatable: false
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
c.action do |_global_options, options, args|
|
18
|
+
if args.count.positive?
|
19
|
+
all_req = args.join(' ') !~ /[+!-]/
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
22
|
+
args.each do |arg|
|
23
|
+
arg.split(/ *, */).each do |a|
|
24
|
+
m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
25
|
+
tokens.push({
|
26
|
+
token: m['tok'],
|
27
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
28
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/
|
29
|
+
})
|
30
|
+
end
|
28
31
|
end
|
29
32
|
end
|
30
|
-
end
|
31
33
|
|
32
|
-
|
34
|
+
NA.list_projects(query: tokens, depth: options[:depth], paths: options[:paths])
|
35
|
+
end
|
33
36
|
end
|
34
37
|
end
|
data/bin/commands/prompt.rb
CHANGED
@@ -1,48 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
3
|
+
class App
|
4
|
+
extend GLI::App
|
5
|
+
desc 'Show or install prompt hooks for the current shell'
|
6
|
+
long_desc 'Installing the prompt hook allows you to automatically
|
7
|
+
list next actions when you cd into a directory'
|
8
|
+
command %i[prompt] do |c|
|
9
|
+
c.desc 'Output the prompt hook for the current shell to STDOUT. Pass an argument to
|
10
|
+
specify a shell (zsh, bash, fish)'
|
11
|
+
c.arg_name 'SHELL', optional: true
|
12
|
+
c.command %i[show] do |s|
|
13
|
+
s.action do |_global_options, _options, args|
|
14
|
+
shell = if args.count.positive?
|
15
|
+
args[0]
|
16
|
+
else
|
17
|
+
File.basename(ENV['SHELL'])
|
18
|
+
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
case shell
|
21
|
+
when /^f/i
|
22
|
+
NA::Prompt.show_prompt_hook(:fish)
|
23
|
+
when /^z/i
|
24
|
+
NA::Prompt.show_prompt_hook(:zsh)
|
25
|
+
when /^b/i
|
26
|
+
NA::Prompt.show_prompt_hook(:bash)
|
27
|
+
end
|
25
28
|
end
|
26
29
|
end
|
27
|
-
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
31
|
+
c.desc 'Install the hook for the current shell to the appropriate startup file.'
|
32
|
+
c.arg_name 'SHELL', optional: true
|
33
|
+
c.command %i[install] do |s|
|
34
|
+
s.action do |_global_options, _options, args|
|
35
|
+
shell = if args.count.positive?
|
36
|
+
args[0]
|
37
|
+
else
|
38
|
+
File.basename(ENV['SHELL'])
|
39
|
+
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
case shell
|
42
|
+
when /^f/i
|
43
|
+
NA::Prompt.install_prompt_hook(:fish)
|
44
|
+
when /^z/i
|
45
|
+
NA::Prompt.install_prompt_hook(:zsh)
|
46
|
+
when /^b/i
|
47
|
+
NA::Prompt.install_prompt_hook(:bash)
|
48
|
+
end
|
46
49
|
end
|
47
50
|
end
|
48
51
|
end
|
data/bin/commands/saved.rb
CHANGED
@@ -1,39 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
3
|
+
class App
|
4
|
+
extend GLI::App
|
5
|
+
desc 'Execute a saved search'
|
6
|
+
long_desc 'Run without argument to list saved searches'
|
7
|
+
arg_name 'SEARCH_TITLE', optional: true, multiple: true
|
8
|
+
command %i[saved] do |c|
|
9
|
+
c.example 'na tagged "+maybe,+priority<=3" --save maybelater', desc: 'save a search called "maybelater"'
|
10
|
+
c.example 'na saved maybelater', desc: 'perform the search named "maybelater"'
|
11
|
+
c.example 'na saved maybe',
|
12
|
+
desc: 'perform the search named "maybelater", assuming no other searches match "maybe"'
|
13
|
+
c.example 'na maybe',
|
14
|
+
desc: 'na run with no command and a single argument automatically performs a matching saved search'
|
15
|
+
c.example 'na saved', desc: 'list available searches'
|
16
|
+
|
17
|
+
c.desc 'Open the saved search file in $EDITOR'
|
18
|
+
c.switch %i[e edit], negatable: false
|
19
|
+
|
20
|
+
c.desc 'Delete the specified search definition'
|
21
|
+
c.switch %i[d delete], negatable: false
|
22
|
+
|
23
|
+
c.action do |_global_options, options, args|
|
24
|
+
NA.edit_searches if options[:edit]
|
25
|
+
|
26
|
+
if args.empty?
|
27
|
+
searches = NA.load_searches
|
28
|
+
NA.notify("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
|
29
|
+
NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
|
30
|
+
else
|
31
|
+
args.each do |arg|
|
32
|
+
searches = NA.load_searches
|
33
|
+
|
34
|
+
NA.delete_search(arg) if options[:delete]
|
35
|
+
|
36
|
+
keys = searches.keys.delete_if { |k| k !~ /#{arg}/ }
|
37
|
+
NA.notify("{r}Search #{arg} not found", exit_code: 1) if keys.empty?
|
38
|
+
|
39
|
+
key = keys[0]
|
40
|
+
cmd = Shellwords.shellsplit(searches[key])
|
41
|
+
run(cmd)
|
42
|
+
end
|
43
|
+
exit
|
44
|
+
end
|
37
45
|
end
|
38
46
|
end
|
39
47
|
end
|