na 1.2.29 → 1.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|