na 1.2.79 → 1.2.81
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/.rubocop.yml +8 -2
- data/.rubocop_todo.yml +33 -538
- data/CHANGELOG.md +55 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +27 -10
- data/README.md +47 -6
- data/Rakefile +6 -0
- data/bin/commands/next.rb +4 -0
- data/bin/commands/scan.rb +84 -0
- data/bin/commands/update.rb +1 -1
- data/bin/na +18 -12
- data/lib/na/action.rb +181 -83
- data/lib/na/actions.rb +91 -66
- data/lib/na/array.rb +11 -7
- data/lib/na/benchmark.rb +57 -0
- data/lib/na/colors.rb +113 -92
- data/lib/na/editor.rb +22 -22
- data/lib/na/hash.rb +32 -9
- data/lib/na/help_monkey_patch.rb +9 -1
- data/lib/na/next_action.rb +327 -248
- data/lib/na/pager.rb +60 -32
- data/lib/na/project.rb +14 -1
- data/lib/na/prompt.rb +25 -3
- data/lib/na/string.rb +91 -130
- data/lib/na/theme.rb +47 -39
- data/lib/na/todo.rb +182 -145
- data/lib/na/version.rb +3 -1
- data/lib/na.rb +4 -1
- data/na.gemspec +4 -2
- data/scripts/generate-fish-completions.rb +18 -21
- data/src/_README.md +14 -4
- data/test_performance.rb +78 -0
- metadata +55 -24
data/lib/na/theme.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module NA
|
|
4
4
|
module Theme
|
|
5
5
|
class << self
|
|
6
|
+
# Returns a help string describing available color placeholders for themes.
|
|
7
|
+
# @return [String] Help text for theme placeholders
|
|
6
8
|
def template_help
|
|
7
9
|
<<~EOHELP
|
|
8
10
|
Use {X} placeholders to apply colors. Available colors are:
|
|
@@ -22,50 +24,56 @@ module NA
|
|
|
22
24
|
EOHELP
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
# Loads the theme configuration, merging defaults with any custom theme file and the provided template.
|
|
28
|
+
# Writes the help text and theme YAML to the theme file.
|
|
29
|
+
# @param template [Hash] Additional theme settings to merge
|
|
30
|
+
# @return [Hash] The merged theme configuration
|
|
25
31
|
def load_theme(template: {})
|
|
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
|
-
|
|
32
|
+
NA::Benchmark.measure('Theme.load_theme') do
|
|
33
|
+
# Default colorization, can be overridden with full or partial template variable
|
|
34
|
+
default_template = {
|
|
35
|
+
parent: '{c}',
|
|
36
|
+
bracket: '{dc}',
|
|
37
|
+
parent_divider: '{xw}/',
|
|
38
|
+
action: '{bg}',
|
|
39
|
+
project: '{xbk}',
|
|
40
|
+
tags: '{m}',
|
|
41
|
+
value_parens: '{m}',
|
|
42
|
+
values: '{c}',
|
|
43
|
+
search_highlight: '{y}',
|
|
44
|
+
note: '{dw}',
|
|
45
|
+
dirname: '{xdw}',
|
|
46
|
+
filename: '{xb}{#eccc87}',
|
|
47
|
+
prompt: '{m}',
|
|
48
|
+
success: '{bg}',
|
|
49
|
+
error: '{b}{#b61d2a}',
|
|
50
|
+
warning: '{by}',
|
|
51
|
+
debug: '{dw}',
|
|
52
|
+
templates: {
|
|
53
|
+
output: '%filename%parents| %action',
|
|
54
|
+
default: '%parent%action',
|
|
55
|
+
single_file: '%parent%action',
|
|
56
|
+
multi_file: '%filename%parent%action',
|
|
57
|
+
no_file: '%parent%action'
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
|
-
}
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
# Load custom theme
|
|
62
|
+
theme_file = NA.database_path(file: 'theme.yaml')
|
|
63
|
+
theme = if File.exist?(theme_file)
|
|
64
|
+
YAML.load(File.read(theme_file)) || {}
|
|
65
|
+
else
|
|
66
|
+
{}
|
|
67
|
+
end
|
|
68
|
+
theme = default_template.deep_merge(theme)
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
File.open(theme_file, 'w') do |f|
|
|
71
|
+
f.puts template_help.comment
|
|
72
|
+
f.puts YAML.dump(theme)
|
|
73
|
+
end
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
theme.merge(template)
|
|
76
|
+
end
|
|
69
77
|
end
|
|
70
78
|
end
|
|
71
79
|
end
|
data/lib/na/todo.rb
CHANGED
|
@@ -4,172 +4,209 @@ module NA
|
|
|
4
4
|
class Todo
|
|
5
5
|
attr_accessor :actions, :projects, :files
|
|
6
6
|
|
|
7
|
+
# Initialize a Todo object and parse actions/projects/files
|
|
8
|
+
#
|
|
9
|
+
# @param options [Hash] Options for parsing todo files
|
|
10
|
+
# @return [void]
|
|
7
11
|
def initialize(options = {})
|
|
8
12
|
@files, @actions, @projects = parse(options)
|
|
9
13
|
end
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
## @option project [String] The project
|
|
26
|
-
## @option require_na [Boolean] Require @na tag
|
|
27
|
-
## @option file_path [String] file path to parse
|
|
28
|
-
##
|
|
15
|
+
# Read a todo file and create a list of actions
|
|
16
|
+
#
|
|
17
|
+
# @param options [Hash] The options
|
|
18
|
+
# @option options [Number] :depth The directory depth to search for files
|
|
19
|
+
# @option options [Boolean] :done include @done actions
|
|
20
|
+
# @option options [Hash] :query The todo file query
|
|
21
|
+
# @option options [Array] :tag Tags to search for
|
|
22
|
+
# @option options [String] :search A search string
|
|
23
|
+
# @option options [Boolean] :negate Invert results
|
|
24
|
+
# @option options [Boolean] :regex Interpret as regular expression
|
|
25
|
+
# @option options [String] :project The project
|
|
26
|
+
# @option options [Boolean] :require_na Require @na tag
|
|
27
|
+
# @option options [String] :file_path file path to parse
|
|
28
|
+
# @return [Array] files, actions, projects
|
|
29
29
|
def parse(options)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
negated_tag = []
|
|
53
|
-
projects = []
|
|
54
|
-
|
|
55
|
-
NA.notify("Tags: #{settings[:tag]}", debug:true)
|
|
56
|
-
NA.notify("Search: #{settings[:search]}", debug:true)
|
|
57
|
-
|
|
58
|
-
settings[:tag]&.each do |t|
|
|
59
|
-
unless t[:tag].nil?
|
|
60
|
-
if settings[:negate]
|
|
61
|
-
optional_tag.push(t) if t[:negate]
|
|
62
|
-
required_tag.push(t) if t[:required] && t[:negate]
|
|
63
|
-
negated_tag.push(t) unless t[:negate]
|
|
64
|
-
else
|
|
65
|
-
optional_tag.push(t) unless t[:negate] || t[:required]
|
|
66
|
-
required_tag.push(t) if t[:required] && !t[:negate]
|
|
67
|
-
negated_tag.push(t) if t[:negate]
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
unless settings[:search].nil? || settings[:search].empty?
|
|
73
|
-
if settings[:regex] || settings[:search].is_a?(String)
|
|
74
|
-
if settings[:negate]
|
|
75
|
-
negated.push(settings[:search])
|
|
76
|
-
else
|
|
77
|
-
optional.push(settings[:search])
|
|
78
|
-
required.push(settings[:search])
|
|
79
|
-
end
|
|
80
|
-
else
|
|
81
|
-
settings[:search].each do |t|
|
|
82
|
-
opt, req, neg = parse_search(t, settings[:negate])
|
|
83
|
-
optional.concat(opt)
|
|
84
|
-
required.concat(req)
|
|
85
|
-
negated.concat(neg)
|
|
86
|
-
end
|
|
30
|
+
NA::Benchmark.measure('Todo.parse') do
|
|
31
|
+
defaults = {
|
|
32
|
+
depth: 1,
|
|
33
|
+
done: false,
|
|
34
|
+
file_path: nil,
|
|
35
|
+
negate: false,
|
|
36
|
+
hidden: false,
|
|
37
|
+
project: nil,
|
|
38
|
+
query: nil,
|
|
39
|
+
regex: false,
|
|
40
|
+
require_na: true,
|
|
41
|
+
search: nil,
|
|
42
|
+
search_note: true,
|
|
43
|
+
tag: nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
settings = defaults.merge(options)
|
|
47
|
+
# Ensure tag is always an Array
|
|
48
|
+
if settings[:tag].nil?
|
|
49
|
+
settings[:tag] = []
|
|
50
|
+
elsif !settings[:tag].is_a?(Array)
|
|
51
|
+
settings[:tag] = [settings[:tag]]
|
|
87
52
|
end
|
|
88
|
-
end
|
|
89
53
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
54
|
+
actions = NA::Actions.new
|
|
55
|
+
required = []
|
|
56
|
+
optional = []
|
|
57
|
+
negated = []
|
|
58
|
+
required_tag = []
|
|
59
|
+
optional_tag = []
|
|
60
|
+
negated_tag = []
|
|
61
|
+
projects = []
|
|
62
|
+
|
|
63
|
+
NA.notify("Tags: #{settings[:tag]}", debug: true)
|
|
64
|
+
NA.notify("Search: #{settings[:search]}", debug: true)
|
|
65
|
+
|
|
66
|
+
settings[:tag]&.each do |t|
|
|
67
|
+
# If t is a Hash, use its keys; if String, treat as a tag string
|
|
68
|
+
if t.is_a?(Hash)
|
|
69
|
+
unless t[:tag].nil?
|
|
70
|
+
if settings[:negate]
|
|
71
|
+
optional_tag.push(t) if t[:negate]
|
|
72
|
+
required_tag.push(t) if t[:required] && t[:negate]
|
|
73
|
+
negated_tag.push(t) unless t[:negate]
|
|
94
74
|
else
|
|
95
|
-
|
|
75
|
+
optional_tag.push(t) unless t[:negate] || t[:required]
|
|
76
|
+
required_tag.push(t) if t[:required] && !t[:negate]
|
|
77
|
+
negated_tag.push(t) if t[:negate]
|
|
96
78
|
end
|
|
97
|
-
|
|
98
|
-
NA.notify("Files: #{files.join(', ')}", debug: true)
|
|
99
|
-
files.each do |file|
|
|
100
|
-
NA.save_working_dir(File.expand_path(file))
|
|
101
|
-
content = file.read_file
|
|
102
|
-
indent_level = 0
|
|
103
|
-
parent = []
|
|
104
|
-
in_yaml = false
|
|
105
|
-
in_action = false
|
|
106
|
-
content.split(/\n/).each.with_index do |line, idx|
|
|
107
|
-
if in_yaml && line !~ /^(---|~~~)\s*$/
|
|
108
|
-
NA.notify("YAML: #{line}", debug: true)
|
|
109
|
-
elsif line =~ /^(---|~~~)\s*$/
|
|
110
|
-
in_yaml = !in_yaml
|
|
111
|
-
elsif line.project? && !in_yaml
|
|
112
|
-
in_action = false
|
|
113
|
-
proj = line.project
|
|
114
|
-
indent = line.indent_level
|
|
115
|
-
|
|
116
|
-
if indent.zero? # top level project
|
|
117
|
-
parent = [proj]
|
|
118
|
-
elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
|
|
119
|
-
parent.slice!(indent, parent.count - indent)
|
|
120
|
-
parent.push(proj)
|
|
121
|
-
else # if indent level is greater, append project to parent
|
|
122
|
-
parent.push(proj)
|
|
123
79
|
end
|
|
80
|
+
elsif t.is_a?(String)
|
|
81
|
+
# Treat string as a simple tag
|
|
82
|
+
optional_tag.push({ tag: t })
|
|
83
|
+
end
|
|
84
|
+
end
|
|
124
85
|
|
|
125
|
-
|
|
86
|
+
unless settings[:search].nil? || settings[:search].empty?
|
|
87
|
+
if settings[:regex] || settings[:search].is_a?(String)
|
|
88
|
+
if settings[:negate]
|
|
89
|
+
negated.push(settings[:search])
|
|
90
|
+
else
|
|
91
|
+
optional.push(settings[:search])
|
|
92
|
+
required.push(settings[:search])
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
settings[:search].each do |t|
|
|
96
|
+
opt, req, neg = parse_search(t, settings[:negate])
|
|
97
|
+
optional.concat(opt)
|
|
98
|
+
required.concat(req)
|
|
99
|
+
negated.concat(neg)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
126
103
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
104
|
+
# Pre-compile regexes for better performance
|
|
105
|
+
optional = optional.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
|
|
106
|
+
required = required.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
|
|
107
|
+
negated = negated.map { |rx| rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE) }
|
|
108
|
+
|
|
109
|
+
files = if !settings[:file_path].nil?
|
|
110
|
+
[settings[:file_path]]
|
|
111
|
+
elsif settings[:query].nil?
|
|
112
|
+
NA.find_files(depth: settings[:depth], include_hidden: settings[:hidden])
|
|
113
|
+
else
|
|
114
|
+
NA.match_working_dir(settings[:query])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
NA.notify("Files: #{files.join(', ')}", debug: true)
|
|
118
|
+
# Cache project regex compilation outside the line loop for better performance
|
|
119
|
+
project_regex = if settings[:project]
|
|
120
|
+
rx = settings[:project].split(%r{[/:]}).join('.*?/')
|
|
121
|
+
Regexp.new("#{rx}.*?", Regexp::IGNORECASE)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
files.each do |file|
|
|
125
|
+
next if File.directory?(file)
|
|
126
|
+
|
|
127
|
+
NA::Benchmark.measure("Parse file: #{File.basename(file)}") do
|
|
128
|
+
NA.save_working_dir(File.expand_path(file))
|
|
129
|
+
content = file.read_file
|
|
130
|
+
indent_level = 0
|
|
131
|
+
parent = []
|
|
132
|
+
in_yaml = false
|
|
131
133
|
in_action = false
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
134
|
+
content.split("\n").each.with_index do |line, idx|
|
|
135
|
+
if in_yaml && line !~ /^(---|~~~)\s*$/
|
|
136
|
+
NA.notify("YAML: #{line}", debug: true)
|
|
137
|
+
elsif line =~ /^(---|~~~)\s*$/
|
|
138
|
+
in_yaml = !in_yaml
|
|
139
|
+
elsif line.project? && !in_yaml
|
|
140
|
+
in_action = false
|
|
141
|
+
proj = line.project
|
|
142
|
+
indent = line.indent_level
|
|
143
|
+
|
|
144
|
+
if indent.zero? # top level project
|
|
145
|
+
parent = [proj]
|
|
146
|
+
elsif indent <= indent_level # if indent level is same or less, split parent before indent level and append
|
|
147
|
+
parent.slice!(indent, parent.count - indent)
|
|
148
|
+
parent.push(proj)
|
|
149
|
+
else # if indent level is greater, append project to parent
|
|
150
|
+
parent.push(proj)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
projects.push(NA::Project.new(parent.join(':'), indent, idx, idx))
|
|
154
|
+
|
|
155
|
+
indent_level = indent
|
|
156
|
+
elsif line.blank?
|
|
157
|
+
in_action = false # Comment out to allow line breaks in of notes, which isn't TaskPaper-compatible
|
|
158
|
+
elsif line.action?
|
|
159
|
+
in_action = false
|
|
160
|
+
|
|
161
|
+
# Early exits before creating Action object
|
|
162
|
+
next if line.done? && !settings[:done]
|
|
163
|
+
|
|
164
|
+
next if settings[:require_na] && !line.na?
|
|
165
|
+
|
|
166
|
+
next if project_regex && parent.join('/') !~ project_regex
|
|
167
|
+
|
|
168
|
+
# Only create Action if we passed basic filters
|
|
169
|
+
action = line.action
|
|
170
|
+
new_action = NA::Action.new(file, File.basename(file, ".#{NA.extension}"), parent.dup, action, idx)
|
|
171
|
+
|
|
172
|
+
projects[-1].last_line = idx if projects.count.positive?
|
|
173
|
+
|
|
174
|
+
# Tag matching
|
|
175
|
+
has_tag = !optional_tag.empty? || !required_tag.empty? || !negated_tag.empty?
|
|
176
|
+
next if has_tag && !new_action.tags_match?(any: optional_tag,
|
|
177
|
+
all: required_tag,
|
|
178
|
+
none: negated_tag)
|
|
179
|
+
|
|
180
|
+
actions.push(new_action)
|
|
181
|
+
in_action = true
|
|
182
|
+
elsif in_action
|
|
183
|
+
actions[-1].note.push(line.strip) if actions.count.positive?
|
|
184
|
+
projects[-1].last_line = idx if projects.count.positive?
|
|
185
|
+
end
|
|
145
186
|
end
|
|
187
|
+
projects = projects.dup
|
|
188
|
+
end
|
|
189
|
+
end
|
|
146
190
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
elsif in_action
|
|
155
|
-
actions[-1].note.push(line.strip) if actions.count.positive?
|
|
156
|
-
projects[-1].last_line = idx if projects.count.positive?
|
|
191
|
+
NA::Benchmark.measure('Filter actions by search') do
|
|
192
|
+
actions.delete_if do |new_action|
|
|
193
|
+
has_search = !optional.empty? || !required.empty? || !negated.empty?
|
|
194
|
+
has_search && !new_action.search_match?(any: optional,
|
|
195
|
+
all: required,
|
|
196
|
+
none: negated,
|
|
197
|
+
include_note: settings[:search_note])
|
|
157
198
|
end
|
|
158
199
|
end
|
|
159
|
-
projects = projects.dup
|
|
160
|
-
end
|
|
161
200
|
|
|
162
|
-
|
|
163
|
-
has_search = !optional.empty? || !required.empty? || !negated.empty?
|
|
164
|
-
has_search && !new_action.search_match?(any: optional,
|
|
165
|
-
all: required,
|
|
166
|
-
none: negated,
|
|
167
|
-
include_note: settings[:search_note])
|
|
201
|
+
[files, actions, projects]
|
|
168
202
|
end
|
|
169
|
-
|
|
170
|
-
[files, actions, projects]
|
|
171
203
|
end
|
|
172
204
|
|
|
205
|
+
# Parse a search tag and categorize as optional, required, or negated
|
|
206
|
+
#
|
|
207
|
+
# @param tag [Hash] Search tag with :token, :negate, :required
|
|
208
|
+
# @param negate [Boolean] Invert results
|
|
209
|
+
# @return [Array<Array>] Arrays of optional, required, and negated regexes
|
|
173
210
|
def parse_search(tag, negate)
|
|
174
211
|
required = []
|
|
175
212
|
optional = []
|
data/lib/na/version.rb
CHANGED
data/lib/na.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'na/benchmark'
|
|
3
4
|
require 'na/version'
|
|
4
5
|
require 'na/pager'
|
|
5
6
|
require 'time'
|
|
6
7
|
require 'fileutils'
|
|
7
8
|
require 'shellwords'
|
|
8
|
-
|
|
9
|
+
# Lazy load heavy gems - only load when needed
|
|
10
|
+
# require 'chronic' # Loaded in action.rb and string.rb when needed
|
|
9
11
|
require 'tty-screen'
|
|
10
12
|
require 'tty-reader'
|
|
11
13
|
require 'tty-which'
|
|
@@ -13,6 +15,7 @@ require 'na/hash'
|
|
|
13
15
|
require 'na/colors'
|
|
14
16
|
require 'na/string'
|
|
15
17
|
require 'na/array'
|
|
18
|
+
require 'yaml'
|
|
16
19
|
require 'na/theme'
|
|
17
20
|
require 'na/todo'
|
|
18
21
|
require 'na/actions'
|
data/na.gemspec
CHANGED
|
@@ -24,12 +24,14 @@ spec = Gem::Specification.new do |s|
|
|
|
24
24
|
s.add_development_dependency('minitest', '~> 5.14')
|
|
25
25
|
s.add_development_dependency('rdoc', '~> 4.3')
|
|
26
26
|
s.add_runtime_dependency('chronic', '~> 0.10', '>= 0.10.2')
|
|
27
|
+
s.add_runtime_dependency('git', '~> 3.0.0')
|
|
27
28
|
s.add_runtime_dependency('gli','~> 2.21.0')
|
|
28
29
|
s.add_runtime_dependency('mdless', '~> 1.0', '>= 1.0.32')
|
|
30
|
+
s.add_runtime_dependency('ostruct', '~> 0.6', '>= 0.6.1')
|
|
29
31
|
s.add_runtime_dependency('tty-reader', '~> 0.9', '>= 0.9.0')
|
|
30
32
|
s.add_runtime_dependency('tty-screen', '~> 0.8', '>= 0.8.1')
|
|
31
33
|
s.add_runtime_dependency('tty-which', '~> 0.5', '>= 0.5.0')
|
|
32
|
-
s.add_runtime_dependency('git', '~> 3.0.0')
|
|
33
|
-
s.add_runtime_dependency('ostruct', '~> 0.6', '>= 0.6.1')
|
|
34
34
|
s.add_development_dependency('tty-spinner', '~> 0.9', '>= 0.9.0')
|
|
35
|
+
s.add_development_dependency 'rspec', '~> 3.0'
|
|
36
|
+
s.add_development_dependency 'bump', '~> 0.6.0'
|
|
35
37
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
2
4
|
require 'tty-progressbar'
|
|
3
5
|
require 'shellwords'
|
|
4
6
|
|
|
@@ -21,7 +23,6 @@ class ::String
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
class FishCompletions
|
|
24
|
-
|
|
25
26
|
attr_accessor :commands, :global_options
|
|
26
27
|
|
|
27
28
|
def generate_helpers
|
|
@@ -68,7 +69,7 @@ class FishCompletions
|
|
|
68
69
|
sections = {}
|
|
69
70
|
scanned.each do |sect|
|
|
70
71
|
title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
|
|
71
|
-
content = sect[1].split(
|
|
72
|
+
content = sect[1].split("\n").map(&:strip).delete_if(&:empty?)
|
|
72
73
|
sections[title] = content
|
|
73
74
|
end
|
|
74
75
|
sections
|
|
@@ -77,6 +78,7 @@ class FishCompletions
|
|
|
77
78
|
def parse_option(option)
|
|
78
79
|
res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
|
|
79
80
|
return nil unless res
|
|
81
|
+
|
|
80
82
|
{
|
|
81
83
|
short: res['short'],
|
|
82
84
|
long: res['long'],
|
|
@@ -92,7 +94,7 @@ class FishCompletions
|
|
|
92
94
|
def parse_command(command)
|
|
93
95
|
res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
|
|
94
96
|
commands = [res['cmd']]
|
|
95
|
-
commands.concat(res['alias'].split(
|
|
97
|
+
commands.concat(res['alias'].split(', ').delete_if(&:empty?)) if res['alias']
|
|
96
98
|
|
|
97
99
|
{
|
|
98
100
|
commands: commands,
|
|
@@ -106,7 +108,7 @@ class FishCompletions
|
|
|
106
108
|
|
|
107
109
|
def generate_subcommand_completions
|
|
108
110
|
out = []
|
|
109
|
-
@commands.each_with_index do |cmd,
|
|
111
|
+
@commands.each_with_index do |cmd, _i|
|
|
110
112
|
out << "complete -xc na -n '__fish_na_needs_command' -a '#{cmd[:commands].join(' ')}' -d #{Shellwords.escape(cmd[:description])}"
|
|
111
113
|
end
|
|
112
114
|
|
|
@@ -114,35 +116,30 @@ class FishCompletions
|
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
def generate_subcommand_option_completions
|
|
117
|
-
|
|
118
119
|
out = []
|
|
119
120
|
need_export = []
|
|
120
121
|
|
|
121
|
-
@commands.each_with_index do |cmd,
|
|
122
|
+
@commands.each_with_index do |cmd, _i|
|
|
122
123
|
@bar.advance
|
|
123
124
|
data = get_help_sections(cmd[:commands].first)
|
|
124
125
|
|
|
125
|
-
if data[:synopsis].join(' ').strip.split(/ /).last =~ /(path|file)/i
|
|
126
|
-
out << "complete -c na -F -n '__fish_na_using_command #{cmd[:commands].join(" ")}'"
|
|
127
|
-
end
|
|
126
|
+
out << "complete -c na -F -n '__fish_na_using_command #{cmd[:commands].join(' ')}'" if data[:synopsis].join(' ').strip.split(/ /).last =~ /(path|file)/i
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
parse_options(data[:command_options]).each do |option|
|
|
131
|
-
next if option.nil?
|
|
128
|
+
next unless data[:command_options]
|
|
132
129
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
long = option[:long] ? "-l #{option[:long]}" : ''
|
|
136
|
-
out << "complete -c na #{long} #{short} -f #{arg} -n '__fish_na_using_command #{cmd[:commands].join(' ')}' -d #{Shellwords.escape(option[:description])}"
|
|
130
|
+
parse_options(data[:command_options]).each do |option|
|
|
131
|
+
next if option.nil?
|
|
137
132
|
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
arg = option[:arg] ? '-r' : ''
|
|
134
|
+
short = option[:short] ? "-s #{option[:short]}" : ''
|
|
135
|
+
long = option[:long] ? "-l #{option[:long]}" : ''
|
|
136
|
+
out << "complete -c na #{long} #{short} -f #{arg} -n '__fish_na_using_command #{cmd[:commands].join(' ')}' -d #{Shellwords.escape(option[:description])}"
|
|
137
|
+
|
|
138
|
+
need_export.concat(cmd[:commands]) if option[:long] == 'output'
|
|
140
139
|
end
|
|
141
140
|
end
|
|
142
141
|
|
|
143
|
-
unless need_export.empty?
|
|
144
|
-
out << "complete -f -c na -s o -l output -x -n '__fish_na_using_command #{need_export.join(' ')}' -a '(__fish_na_export_plugins)'"
|
|
145
|
-
end
|
|
142
|
+
out << "complete -f -c na -s o -l output -x -n '__fish_na_using_command #{need_export.join(' ')}' -a '(__fish_na_export_plugins)'" unless need_export.empty?
|
|
146
143
|
|
|
147
144
|
# clear
|
|
148
145
|
out.join("\n")
|
data/src/_README.md
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
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.80<!--END VER-->.
|
|
13
13
|
|
|
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.
|
|
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
|
|
|
16
16
|
Used with Taskpaper files, it can add new action items quickly from the command line, automatically tagging them as next actions. It can also mark actions as completed, delete them, archive them, and move them between projects.
|
|
17
17
|
|
|
@@ -48,7 +48,7 @@ You can list next actions in files in the current directory by typing `na`. By d
|
|
|
48
48
|
|
|
49
49
|
#### Adding todos
|
|
50
50
|
|
|
51
|
-
You can also quickly add todo items from the command line with the `add` subcommand. The script will look for a file in the current directory with a `.taskpaper` extension (configurable).
|
|
51
|
+
You can also quickly add todo items from the command line with the `add` subcommand. The script will look for a file in the current directory with a `.taskpaper` extension (configurable).
|
|
52
52
|
|
|
53
53
|
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.
|
|
54
54
|
|
|
@@ -82,7 +82,7 @@ If you run the `add` command with no arguments, you'll be asked for input on the
|
|
|
82
82
|
|
|
83
83
|
###### Adding notes
|
|
84
84
|
|
|
85
|
-
Use the `--note` switch to add a note. If STDIN (piped) input is present when this switch is used, it will be included in the note. A prompt will be displayed for adding additional notes, which will be appended to any STDIN note passed. Press CTRL-d to end editing and save the note.
|
|
85
|
+
Use the `--note` switch to add a note. If STDIN (piped) input is present when this switch is used, it will be included in the note. A prompt will be displayed for adding additional notes, which will be appended to any STDIN note passed. Press CTRL-d to end editing and save the note.
|
|
86
86
|
|
|
87
87
|
Notes are not displayed by the `next/tagged/find` commands unless `--notes` is specified.
|
|
88
88
|
|
|
@@ -163,6 +163,16 @@ Run `na saved` without an argument to list your saved searches.
|
|
|
163
163
|
@cli(bundle exec bin/na help saved)
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
##### scan
|
|
167
|
+
|
|
168
|
+
Scan a directory tree for todo files and cache them in tdlist.txt. Avoids duplicates and can optionally prune non-existent entries.
|
|
169
|
+
|
|
170
|
+
Scan reports how many files were added and, if --prune is used, how many were pruned. With --dry-run, it lists the full file paths that would be added and/or pruned.
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
@cli(bundle exec bin/na help scan)
|
|
174
|
+
```
|
|
175
|
+
|
|
166
176
|
##### tagged
|
|
167
177
|
|
|
168
178
|
Example: `na tagged feature +maybe`.
|