howzit 2.0.8 → 2.0.11
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/.travis.yml +1 -1
- data/CHANGELOG.md +32 -0
- data/README.md +4 -343
- data/bin/howzit +101 -83
- data/fish/completions/howzit.fish +34 -2
- data/howzit.gemspec +2 -0
- data/lib/howzit/buildnote.rb +161 -33
- data/lib/howzit/colors.rb +3 -0
- data/lib/howzit/config.rb +39 -5
- data/lib/howzit/console_logger.rb +38 -0
- data/lib/howzit/hash.rb +25 -4
- data/lib/howzit/prompt.rb +42 -6
- data/lib/howzit/stringutils.rb +131 -4
- data/lib/howzit/task.rb +10 -6
- data/lib/howzit/topic.rb +72 -10
- data/lib/howzit/util.rb +35 -5
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +13 -0
- data/spec/cli_spec.rb +27 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/task_spec.rb +6 -1
- metadata +32 -5
- data/lib/howzit/buildnotes.rb +0 -1252
- data/spec/buildnotes.md.bak +0 -22
data/lib/howzit/buildnotes.rb
DELETED
@@ -1,1252 +0,0 @@
|
|
1
|
-
module Howzit
|
2
|
-
# Primary Class for this module
|
3
|
-
class BuildNotes
|
4
|
-
include Prompt
|
5
|
-
include Color
|
6
|
-
|
7
|
-
attr_accessor :cli_args, :options, :arguments, :metadata
|
8
|
-
|
9
|
-
def topics
|
10
|
-
@topics ||= read_help
|
11
|
-
end
|
12
|
-
|
13
|
-
# If either mdless or mdcat are installed, use that for highlighting
|
14
|
-
# markdown
|
15
|
-
def which_highlighter
|
16
|
-
if @options[:highlighter] =~ /auto/i
|
17
|
-
highlighters = %w[mdcat mdless]
|
18
|
-
highlighters.delete_if(&:nil?).select!(&:available?)
|
19
|
-
return nil if highlighters.empty?
|
20
|
-
|
21
|
-
hl = highlighters.first
|
22
|
-
args = case hl
|
23
|
-
when 'mdless'
|
24
|
-
'--no-pager'
|
25
|
-
end
|
26
|
-
|
27
|
-
[hl, args].join(' ')
|
28
|
-
else
|
29
|
-
hl = @options[:highlighter].split(/ /)[0]
|
30
|
-
if hl.available?
|
31
|
-
@options[:highlighter]
|
32
|
-
else
|
33
|
-
warn 'Specified highlighter not found, switching to auto' if @options[:log_level] < 2
|
34
|
-
@options[:highlighter] = 'auto'
|
35
|
-
which_highlighter
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# When pagination is enabled, find the best (in my opinion) option,
|
41
|
-
# favoring environment settings
|
42
|
-
def which_pager
|
43
|
-
if @options[:pager] =~ /auto/i
|
44
|
-
pagers = [ENV['PAGER'], ENV['GIT_PAGER'],
|
45
|
-
'bat', 'less', 'more', 'pager']
|
46
|
-
pagers.delete_if(&:nil?).select!(&:available?)
|
47
|
-
return nil if pagers.empty?
|
48
|
-
|
49
|
-
pg = pagers.first
|
50
|
-
args = case pg
|
51
|
-
when 'delta'
|
52
|
-
'--pager="less -FXr"'
|
53
|
-
when /^(less|more)$/
|
54
|
-
'-FXr'
|
55
|
-
when 'bat'
|
56
|
-
if @options[:highlight]
|
57
|
-
'--language Markdown --style plain --pager="less -FXr"'
|
58
|
-
else
|
59
|
-
'--style plain --pager="less -FXr"'
|
60
|
-
end
|
61
|
-
else
|
62
|
-
''
|
63
|
-
end
|
64
|
-
|
65
|
-
[pg, args].join(' ')
|
66
|
-
else
|
67
|
-
pg = @options[:pager].split(/ /)[0]
|
68
|
-
if pg.available?
|
69
|
-
@options[:pager]
|
70
|
-
else
|
71
|
-
warn 'Specified pager not found, switching to auto' if @options[:log_level] < 2
|
72
|
-
@options[:pager] = 'auto'
|
73
|
-
which_pager
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Paginate the output
|
79
|
-
def page(text)
|
80
|
-
read_io, write_io = IO.pipe
|
81
|
-
|
82
|
-
input = $stdin
|
83
|
-
|
84
|
-
pid = Kernel.fork do
|
85
|
-
write_io.close
|
86
|
-
input.reopen(read_io)
|
87
|
-
read_io.close
|
88
|
-
|
89
|
-
# Wait until we have input before we start the pager
|
90
|
-
IO.select [input]
|
91
|
-
|
92
|
-
pager = which_pager
|
93
|
-
begin
|
94
|
-
exec(pager)
|
95
|
-
rescue SystemCallError => e
|
96
|
-
@log.error(e)
|
97
|
-
exit 1
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
read_io.close
|
102
|
-
write_io.write(text)
|
103
|
-
write_io.close
|
104
|
-
|
105
|
-
_, status = Process.waitpid2(pid)
|
106
|
-
status.success?
|
107
|
-
end
|
108
|
-
|
109
|
-
# print output to terminal
|
110
|
-
def show(string, opts = {})
|
111
|
-
options = {
|
112
|
-
color: true,
|
113
|
-
highlight: false,
|
114
|
-
wrap: 0
|
115
|
-
}
|
116
|
-
|
117
|
-
options.merge!(opts)
|
118
|
-
|
119
|
-
string = string.uncolor unless options[:color]
|
120
|
-
|
121
|
-
pipes = ''
|
122
|
-
if options[:highlight]
|
123
|
-
hl = which_highlighter
|
124
|
-
pipes = "|#{hl}" if hl
|
125
|
-
end
|
126
|
-
|
127
|
-
output = `echo #{Shellwords.escape(string.strip)}#{pipes}`.strip
|
128
|
-
|
129
|
-
if @options[:paginate]
|
130
|
-
page(output)
|
131
|
-
else
|
132
|
-
puts output
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def should_mark_iterm?
|
137
|
-
ENV['TERM_PROGRAM'] =~ /^iTerm/ && !@options[:run] && !@options[:paginate]
|
138
|
-
end
|
139
|
-
|
140
|
-
def iterm_marker
|
141
|
-
"\e]1337;SetMark\a" if should_mark_iterm?
|
142
|
-
end
|
143
|
-
|
144
|
-
def color_single_options(choices = %w[y n])
|
145
|
-
out = []
|
146
|
-
choices.each do |choice|
|
147
|
-
case choice
|
148
|
-
when /[A-Z]/
|
149
|
-
out.push(Color.template("{bg}#{choice}{xg}"))
|
150
|
-
else
|
151
|
-
out.push(Color.template("{w}#{choice}"))
|
152
|
-
end
|
153
|
-
end
|
154
|
-
Color.template("{g}[#{out.join('/')}{g}]{x}")
|
155
|
-
end
|
156
|
-
|
157
|
-
# Create a buildnotes skeleton
|
158
|
-
def create_note
|
159
|
-
trap('SIGINT') do
|
160
|
-
warn "\nCanceled"
|
161
|
-
exit!
|
162
|
-
end
|
163
|
-
default = !$stdout.isatty || @options[:default]
|
164
|
-
# First make sure there isn't already a buildnotes file
|
165
|
-
if note_file
|
166
|
-
fname = Color.template("{by}#{note_file}{bw}")
|
167
|
-
unless default
|
168
|
-
res = yn("#{fname} exists and appears to be a build note, continue anyway?", false)
|
169
|
-
unless res
|
170
|
-
puts 'Canceled'
|
171
|
-
Process.exit 0
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
title = File.basename(Dir.pwd)
|
177
|
-
if default
|
178
|
-
input = title
|
179
|
-
else
|
180
|
-
printf Color.template("{bw}Project name {xg}[#{title}]{bw}: {x}")
|
181
|
-
input = $stdin.gets.chomp
|
182
|
-
title = input unless input.empty?
|
183
|
-
end
|
184
|
-
summary = ''
|
185
|
-
unless default
|
186
|
-
printf Color.template('{bw}Project summary: {x}')
|
187
|
-
input = $stdin.gets.chomp
|
188
|
-
summary = input unless input.empty?
|
189
|
-
end
|
190
|
-
|
191
|
-
fname = 'buildnotes.md'
|
192
|
-
unless default
|
193
|
-
printf Color.template("{bw}Build notes filename (must begin with 'howzit' or 'build')\n{xg}[#{fname}]{bw}: {x}")
|
194
|
-
input = $stdin.gets.chomp
|
195
|
-
fname = input unless input.empty?
|
196
|
-
end
|
197
|
-
|
198
|
-
note = <<~EOBUILDNOTES
|
199
|
-
# #{title}
|
200
|
-
|
201
|
-
#{summary}
|
202
|
-
|
203
|
-
## File Structure
|
204
|
-
|
205
|
-
Where are the main editable files? Is there a dist/build folder that should be ignored?
|
206
|
-
|
207
|
-
## Build
|
208
|
-
|
209
|
-
What build system/parameters does this use?
|
210
|
-
|
211
|
-
@run(./build command)
|
212
|
-
|
213
|
-
## Deploy
|
214
|
-
|
215
|
-
What are the procedures/commands to deploy this project?
|
216
|
-
|
217
|
-
## Other
|
218
|
-
|
219
|
-
Version control notes, additional gulp/rake/make/etc tasks...
|
220
|
-
|
221
|
-
EOBUILDNOTES
|
222
|
-
|
223
|
-
if File.exist?(fname) && !default
|
224
|
-
file = Color.template("{by}#{fname}")
|
225
|
-
res = yn("Are you absolutely sure you want to overwrite #{file}", false)
|
226
|
-
|
227
|
-
unless res
|
228
|
-
puts 'Canceled'
|
229
|
-
Process.exit 0
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
File.open(fname, 'w') do |f|
|
234
|
-
f.puts note
|
235
|
-
puts Color.template("{by}Build notes for #{title} written to #{fname}")
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
# Make a fancy title line for the topic
|
240
|
-
def format_header(title, opts = {})
|
241
|
-
options = {
|
242
|
-
hr: "\u{254C}",
|
243
|
-
color: '{bg}',
|
244
|
-
border: '{x}',
|
245
|
-
mark: false
|
246
|
-
}
|
247
|
-
|
248
|
-
options.merge!(opts)
|
249
|
-
|
250
|
-
case @options[:header_format]
|
251
|
-
when :block
|
252
|
-
Color.template("#{options[:color]}\u{258C}#{title}#{should_mark_iterm? && options[:mark] ? iterm_marker : ''}{x}")
|
253
|
-
else
|
254
|
-
cols = TTY::Screen.columns
|
255
|
-
|
256
|
-
cols = @options[:wrap] if (@options[:wrap]).positive? && cols > @options[:wrap]
|
257
|
-
title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")
|
258
|
-
|
259
|
-
tail = if should_mark_iterm?
|
260
|
-
"#{options[:hr] * (cols - title.uncolor.length - 15)}#{options[:mark] ? iterm_marker : ''}"
|
261
|
-
else
|
262
|
-
options[:hr] * (cols - title.uncolor.length)
|
263
|
-
end
|
264
|
-
Color.template("#{title}#{tail}{x}")
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
def os_open(command)
|
269
|
-
os = RbConfig::CONFIG['target_os']
|
270
|
-
out = Color.template("{bg}Opening {bw}#{command}")
|
271
|
-
case os
|
272
|
-
when /darwin.*/i
|
273
|
-
warn Color.template("#{out} (macOS){x}") if @options[:log_level] < 2
|
274
|
-
`open #{Shellwords.escape(command)}`
|
275
|
-
when /mingw|mswin/i
|
276
|
-
warn Color.template("#{out} (Windows){x}") if @options[:log_level] < 2
|
277
|
-
`start #{Shellwords.escape(command)}`
|
278
|
-
else
|
279
|
-
if 'xdg-open'.available?
|
280
|
-
warn Color.template("#{out} (Linux){x}") if @options[:log_level] < 2
|
281
|
-
`xdg-open #{Shellwords.escape(command)}`
|
282
|
-
else
|
283
|
-
warn out if @options[:log_level] < 2
|
284
|
-
warn 'Unable to determine executable for `open`.'
|
285
|
-
end
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
def grep_topics(pat)
|
290
|
-
matching_topics = []
|
291
|
-
topics.each do |topic, content|
|
292
|
-
if content =~ /#{pat}/i || topic =~ /#{pat}/i
|
293
|
-
matching_topics.push(topic)
|
294
|
-
end
|
295
|
-
end
|
296
|
-
matching_topics
|
297
|
-
end
|
298
|
-
|
299
|
-
# Handle run command, execute directives
|
300
|
-
def run_topic(key)
|
301
|
-
output = []
|
302
|
-
tasks = 0
|
303
|
-
if topics[key] =~ /(@(include|run|copy|open|url)\((.*?)\)|`{3,}run)/i
|
304
|
-
prereqs = topics[key].scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
|
305
|
-
postreqs = topics[key].scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
|
306
|
-
|
307
|
-
unless prereqs.empty?
|
308
|
-
puts prereqs.join("\n\n")
|
309
|
-
res = yn('This task has prerequisites, have they been met?', true)
|
310
|
-
Process.exit 1 unless res
|
311
|
-
|
312
|
-
end
|
313
|
-
directives = topics[key].scan(/(?:@(include|run|copy|open|url)\((.*?)\)|(`{3,})run(?: +([^\n]+))?(.*?)\3)/mi)
|
314
|
-
|
315
|
-
tasks += directives.length
|
316
|
-
directives.each do |c|
|
317
|
-
if c[0].nil?
|
318
|
-
title = c[3] ? c[3].strip : ''
|
319
|
-
warn Color.template("{bg}Running block {bw}#{title}{x}") if @options[:log_level] < 2
|
320
|
-
block = c[4].strip
|
321
|
-
script = Tempfile.new('howzit_script')
|
322
|
-
begin
|
323
|
-
script.write(block)
|
324
|
-
script.close
|
325
|
-
File.chmod(0777, script.path)
|
326
|
-
system(%(/bin/sh -c "#{script.path}"))
|
327
|
-
ensure
|
328
|
-
script.close
|
329
|
-
script.unlink
|
330
|
-
end
|
331
|
-
else
|
332
|
-
cmd = c[0]
|
333
|
-
obj = c[1]
|
334
|
-
case cmd
|
335
|
-
when /include/i
|
336
|
-
matches = match_topic(obj)
|
337
|
-
if matches.empty?
|
338
|
-
warn "No topic match for @include(#{search})"
|
339
|
-
else
|
340
|
-
if @included.include?(matches[0])
|
341
|
-
warn Color.template("{by}Tasks from {bw}#{matches[0]} already included, skipping{x}") if @options[:log_level] < 2
|
342
|
-
else
|
343
|
-
warn Color.template("{by}Including tasks from {bw}#{matches[0]}{x}") if @options[:log_level] < 2
|
344
|
-
process_topic(matches[0], true)
|
345
|
-
warn Color.template("{by}End include {bw}#{matches[0]}{x}") if @options[:log_level] < 2
|
346
|
-
end
|
347
|
-
end
|
348
|
-
when /run/i
|
349
|
-
warn Color.template("{bg}Running {bw}#{obj}{x}") if @options[:log_level] < 2
|
350
|
-
system(obj)
|
351
|
-
when /copy/i
|
352
|
-
warn Color.template("{bg}Copied {bw}#{obj}{bg} to clipboard{x}") if @options[:log_level] < 2
|
353
|
-
`echo #{Shellwords.escape(obj)}'\\c'|pbcopy`
|
354
|
-
when /open|url/i
|
355
|
-
os_open(obj)
|
356
|
-
end
|
357
|
-
end
|
358
|
-
end
|
359
|
-
else
|
360
|
-
warn Color.template("{r}--run: No {br}@directive{xr} found in {bw}#{key}{x}")
|
361
|
-
end
|
362
|
-
output.push("Ran #{tasks} #{tasks == 1 ? 'task' : 'tasks'}") if @options[:log_level] < 2
|
363
|
-
|
364
|
-
puts postreqs.join("\n\n") unless postreqs.empty?
|
365
|
-
|
366
|
-
output
|
367
|
-
end
|
368
|
-
|
369
|
-
# Output a topic with fancy title and bright white text.
|
370
|
-
def output_topic(key, options = {})
|
371
|
-
defaults = { single: false, header: true }
|
372
|
-
opt = defaults.merge(options)
|
373
|
-
|
374
|
-
output = []
|
375
|
-
if opt[:header]
|
376
|
-
output.push(format_header(key, { mark: should_mark_iterm? }))
|
377
|
-
output.push('')
|
378
|
-
end
|
379
|
-
topic = topics[key].strip
|
380
|
-
topic.gsub!(/(?mi)^(`{3,})run *([^\n]*)[\s\S]*?\n\1\s*$/, '@@@run \2') unless @options[:show_all_code]
|
381
|
-
topic.split(/\n/).each do |l|
|
382
|
-
case l
|
383
|
-
when /@(before|after|prereq|end)/
|
384
|
-
next
|
385
|
-
when /@include\((.*?)\)/
|
386
|
-
|
387
|
-
m = Regexp.last_match
|
388
|
-
matches = match_topic(m[1])
|
389
|
-
unless matches.empty?
|
390
|
-
if opt[:single]
|
391
|
-
title = "From #{matches[0]}:"
|
392
|
-
color = '{Kyd}'
|
393
|
-
rule = '{kKd}'
|
394
|
-
else
|
395
|
-
title = "Include #{matches[0]}"
|
396
|
-
color = '{Kyd}'
|
397
|
-
rule = '{kKd}'
|
398
|
-
end
|
399
|
-
output.push(format_header("#{'> ' * @nest_level}#{title}", { color: color, hr: '.', border: rule })) unless @included.include?(matches[0])
|
400
|
-
|
401
|
-
if opt[:single]
|
402
|
-
if @included.include?(matches[0])
|
403
|
-
output.push(format_header("#{'> ' * @nest_level}#{title} included above", { color: color, hr: '.', border: rule }))
|
404
|
-
else
|
405
|
-
@nest_level += 1
|
406
|
-
output.concat(output_topic(matches[0], {single: true, header: false}))
|
407
|
-
@nest_level -= 1
|
408
|
-
end
|
409
|
-
output.push(format_header("#{'> ' * @nest_level}...", { color: color, hr: '.', border: rule })) unless @included.include?(matches[0])
|
410
|
-
end
|
411
|
-
@included.push(matches[0])
|
412
|
-
end
|
413
|
-
|
414
|
-
when /@(run|copy|open|url|include)\((.*?)\)/
|
415
|
-
m = Regexp.last_match
|
416
|
-
cmd = m[1]
|
417
|
-
obj = m[2]
|
418
|
-
icon = case cmd
|
419
|
-
when 'run'
|
420
|
-
"\u{25B6}"
|
421
|
-
when 'copy'
|
422
|
-
"\u{271A}"
|
423
|
-
when /open|url/
|
424
|
-
"\u{279A}"
|
425
|
-
end
|
426
|
-
output.push(Color.template("{bmK}#{icon} {bwK}#{obj}{x}"))
|
427
|
-
when /(`{3,})run *(.*?)$/i
|
428
|
-
m = Regexp.last_match
|
429
|
-
desc = m[2].length.positive? ? "Block: #{m[2]}" : 'Code Block'
|
430
|
-
output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}\n```"))
|
431
|
-
when /@@@run *(.*?)$/i
|
432
|
-
m = Regexp.last_match
|
433
|
-
desc = m[1].length.positive? ? "Block: #{m[1]}" : 'Code Block'
|
434
|
-
output.push(Color.template("{bmK}\u{25B6} {bwK}#{desc}{x}"))
|
435
|
-
else
|
436
|
-
l.wrap!(@options[:wrap]) if (@options[:wrap]).positive?
|
437
|
-
output.push(l)
|
438
|
-
end
|
439
|
-
end
|
440
|
-
output.push('')
|
441
|
-
end
|
442
|
-
|
443
|
-
def process_topic(key, run, single = false)
|
444
|
-
# Handle variable replacement
|
445
|
-
content = topics[key]
|
446
|
-
unless @arguments.empty?
|
447
|
-
content.gsub!(/\$(\d+)/) do |m|
|
448
|
-
idx = m[1].to_i - 1
|
449
|
-
@arguments.length > idx ? @arguments[idx] : m
|
450
|
-
end
|
451
|
-
content.gsub!(/\$[@*]/, Shellwords.join(@arguments))
|
452
|
-
end
|
453
|
-
|
454
|
-
output = if run
|
455
|
-
run_topic(key)
|
456
|
-
else
|
457
|
-
output_topic(key, {single: single})
|
458
|
-
end
|
459
|
-
output.nil? ? '' : output.join("\n")
|
460
|
-
end
|
461
|
-
|
462
|
-
# Output a list of topic titles
|
463
|
-
def list_topics
|
464
|
-
output = []
|
465
|
-
output.push(Color.template("{bg}Topics:{x}\n"))
|
466
|
-
topics.each_key do |title|
|
467
|
-
output.push(Color.template("- {bw}#{title}{x}"))
|
468
|
-
end
|
469
|
-
output.join("\n")
|
470
|
-
end
|
471
|
-
|
472
|
-
# Output a list of topic titles for shell completion
|
473
|
-
def list_topic_titles
|
474
|
-
topics.keys.join("\n")
|
475
|
-
end
|
476
|
-
|
477
|
-
def get_note_title(truncate = 0)
|
478
|
-
title = nil
|
479
|
-
help = IO.read(note_file).strip
|
480
|
-
title = help.match(/(?:^(\S.*?)(?=\n==)|^# ?(.*?)$)/)
|
481
|
-
title = if title
|
482
|
-
title[1].nil? ? title[2] : title[1]
|
483
|
-
else
|
484
|
-
note_file.sub(/(\.\w+)?$/, '')
|
485
|
-
end
|
486
|
-
|
487
|
-
title && truncate.positive? ? title.trunc(truncate) : title
|
488
|
-
end
|
489
|
-
|
490
|
-
def list_runnable_titles
|
491
|
-
output = []
|
492
|
-
topics.each do |title, sect|
|
493
|
-
runnable = false
|
494
|
-
sect.split(/\n/).each do |l|
|
495
|
-
if l =~ /(@(run|copy|open|url)\((.*?)\)|`{3,}run)/
|
496
|
-
runnable = true
|
497
|
-
break
|
498
|
-
end
|
499
|
-
end
|
500
|
-
output.push(title) if runnable
|
501
|
-
end
|
502
|
-
output.join("\n")
|
503
|
-
end
|
504
|
-
|
505
|
-
def list_runnable
|
506
|
-
output = []
|
507
|
-
output.push(Color.template(%({bg}"Runnable" Topics:{x}\n)))
|
508
|
-
topics.each do |title, sect|
|
509
|
-
s_out = []
|
510
|
-
lines = sect.split(/\n/)
|
511
|
-
lines.each do |l|
|
512
|
-
case l
|
513
|
-
when /@run\((.*?)\)(.*)?/
|
514
|
-
m = Regexp.last_match
|
515
|
-
run = m[2].strip.length.positive? ? m[2].strip : m[1]
|
516
|
-
s_out.push(" * run: #{run.gsub(/\\n/, '\n')}")
|
517
|
-
when /@(copy|open|url)\((.*?)\)/
|
518
|
-
m = Regexp.last_match
|
519
|
-
s_out.push(" * #{m[1]}: #{m[2]}")
|
520
|
-
when /`{3,}run(.*)?/m
|
521
|
-
run = ' * run code block'
|
522
|
-
title = Regexp.last_match(1).strip
|
523
|
-
run += " (#{title})" if title.length.positive?
|
524
|
-
s_out.push(run)
|
525
|
-
end
|
526
|
-
end
|
527
|
-
unless s_out.empty?
|
528
|
-
output.push(Color.template("- {bw}#{title}{x}"))
|
529
|
-
output.push(s_out.join("\n"))
|
530
|
-
end
|
531
|
-
end
|
532
|
-
output.join("\n")
|
533
|
-
end
|
534
|
-
|
535
|
-
def read_upstream
|
536
|
-
buildnotes = glob_upstream
|
537
|
-
topics_dict = {}
|
538
|
-
buildnotes.each do |path|
|
539
|
-
topics_dict = topics_dict.merge(read_help_file(path))
|
540
|
-
end
|
541
|
-
topics_dict
|
542
|
-
end
|
543
|
-
|
544
|
-
def ensure_requirements(template)
|
545
|
-
t_leader = IO.read(template).split(/^#/)[0].strip
|
546
|
-
if t_leader.length > 0
|
547
|
-
t_meta = t_leader.get_metadata
|
548
|
-
if t_meta.key?('required')
|
549
|
-
required = t_meta['required'].strip.split(/\s*,\s*/)
|
550
|
-
required.each do |req|
|
551
|
-
unless @metadata.keys.include?(req.downcase)
|
552
|
-
warn Color.template(%({xr}ERROR: Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}))
|
553
|
-
warn Color.template(%({xr}Please define {by}#{req.downcase}{xr} in build notes{x}))
|
554
|
-
Process.exit 1
|
555
|
-
end
|
556
|
-
end
|
557
|
-
end
|
558
|
-
end
|
559
|
-
end
|
560
|
-
|
561
|
-
def get_template_topics(content)
|
562
|
-
leader = content.split(/^#/)[0].strip
|
563
|
-
|
564
|
-
template_topics = {}
|
565
|
-
if leader.length > 0
|
566
|
-
data = leader.get_metadata
|
567
|
-
@metadata = @metadata.merge(data)
|
568
|
-
|
569
|
-
if data.key?('template')
|
570
|
-
templates = data['template'].strip.split(/\s*,\s*/)
|
571
|
-
templates.each do |t|
|
572
|
-
tasks = nil
|
573
|
-
if t =~ /\[(.*?)\]$/
|
574
|
-
tasks = Regexp.last_match[1].split(/\s*,\s*/).map {|t| t.gsub(/\*/, '.*?')}
|
575
|
-
t = t.sub(/\[.*?\]$/, '').strip
|
576
|
-
end
|
577
|
-
|
578
|
-
t_file = t.sub(/(\.md)?$/, '.md')
|
579
|
-
template = File.join(template_folder, t_file)
|
580
|
-
if File.exist?(template)
|
581
|
-
ensure_requirements(template)
|
582
|
-
|
583
|
-
t_topics = read_help_file(template)
|
584
|
-
if tasks
|
585
|
-
tasks.each do |task|
|
586
|
-
t_topics.keys.each do |topic|
|
587
|
-
if topic =~ /^(.*?:)?#{task}$/i
|
588
|
-
template_topics[topic] = t_topics[topic]
|
589
|
-
end
|
590
|
-
end
|
591
|
-
end
|
592
|
-
else
|
593
|
-
template_topics = template_topics.merge(t_topics)
|
594
|
-
end
|
595
|
-
end
|
596
|
-
end
|
597
|
-
end
|
598
|
-
end
|
599
|
-
template_topics
|
600
|
-
end
|
601
|
-
|
602
|
-
def include_file(m)
|
603
|
-
file = File.expand_path(m[1])
|
604
|
-
|
605
|
-
return m[0] unless File.exist?(file)
|
606
|
-
|
607
|
-
content = IO.read(file)
|
608
|
-
home = ENV['HOME']
|
609
|
-
short_path = File.dirname(file.sub(/^#{home}/, '~'))
|
610
|
-
prefix = "#{short_path}/#{File.basename(file)}:"
|
611
|
-
parts = content.split(/^##+/)
|
612
|
-
parts.shift
|
613
|
-
if parts.empty?
|
614
|
-
content
|
615
|
-
else
|
616
|
-
"## #{parts.join('## ')}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}")
|
617
|
-
end
|
618
|
-
end
|
619
|
-
|
620
|
-
# Read in the build notes file and output a hash of "Title" => contents
|
621
|
-
def read_help_file(path = nil)
|
622
|
-
filename = path.nil? ? note_file : path
|
623
|
-
topics_dict = {}
|
624
|
-
help = IO.read(filename)
|
625
|
-
|
626
|
-
help.gsub!(/@include\((.*?)\)/) do
|
627
|
-
include_file(Regexp.last_match)
|
628
|
-
end
|
629
|
-
|
630
|
-
template_topics = get_template_topics(help)
|
631
|
-
|
632
|
-
split = help.split(/^##+/)
|
633
|
-
split.slice!(0)
|
634
|
-
split.each do |sect|
|
635
|
-
next if sect.strip.empty?
|
636
|
-
|
637
|
-
lines = sect.split(/\n/)
|
638
|
-
title = lines.slice!(0).strip
|
639
|
-
prefix = ''
|
640
|
-
if path
|
641
|
-
if path =~ /#{template_folder}/
|
642
|
-
short_path = File.basename(path, '.md')
|
643
|
-
else
|
644
|
-
home = ENV['HOME']
|
645
|
-
short_path = File.dirname(path.sub(/^#{home}/, '~'))
|
646
|
-
prefix = "_from #{short_path}_\n\n"
|
647
|
-
end
|
648
|
-
title = "#{short_path}:#{title}"
|
649
|
-
end
|
650
|
-
topics_dict[title] = prefix + lines.join("\n").strip.render_template(@metadata)
|
651
|
-
end
|
652
|
-
|
653
|
-
template_topics.each do |title, content|
|
654
|
-
unless topics_dict.key?(title.sub(/^.+:/, ''))
|
655
|
-
topics_dict[title] = content
|
656
|
-
end
|
657
|
-
end
|
658
|
-
|
659
|
-
topics_dict
|
660
|
-
end
|
661
|
-
|
662
|
-
def read_help
|
663
|
-
topics = read_help_file
|
664
|
-
if @options[:include_upstream]
|
665
|
-
upstream_topics = read_upstream
|
666
|
-
upstream_topics.each do |topic, content|
|
667
|
-
unless topics.key?(topic.sub(/^.*?:/, ''))
|
668
|
-
topics[topic] = content
|
669
|
-
end
|
670
|
-
end
|
671
|
-
# topics = upstream_topics.merge(topics)
|
672
|
-
end
|
673
|
-
topics
|
674
|
-
end
|
675
|
-
|
676
|
-
def match_topic(search)
|
677
|
-
matches = []
|
678
|
-
|
679
|
-
rx = case @options[:matching]
|
680
|
-
when 'exact'
|
681
|
-
/^#{search}$/i
|
682
|
-
when 'beginswith'
|
683
|
-
/^#{search}/i
|
684
|
-
when 'fuzzy'
|
685
|
-
search = search.split(//).join('.*?') if @options[:matching] == 'fuzzy'
|
686
|
-
/#{search}/i
|
687
|
-
else
|
688
|
-
/#{search}/i
|
689
|
-
end
|
690
|
-
|
691
|
-
topics.each_key do |k|
|
692
|
-
matches.push(k) if k.downcase =~ rx
|
693
|
-
end
|
694
|
-
matches
|
695
|
-
end
|
696
|
-
|
697
|
-
def initialize(args = [])
|
698
|
-
Color.coloring = $stdout.isatty
|
699
|
-
flags = {
|
700
|
-
run: false,
|
701
|
-
list_topics: false,
|
702
|
-
list_topic_titles: false,
|
703
|
-
list_runnable: false,
|
704
|
-
list_runnable_titles: false,
|
705
|
-
title_only: false,
|
706
|
-
choose: false,
|
707
|
-
quiet: false,
|
708
|
-
verbose: false,
|
709
|
-
default: false,
|
710
|
-
grep: nil
|
711
|
-
}
|
712
|
-
|
713
|
-
defaults = {
|
714
|
-
color: true,
|
715
|
-
highlight: true,
|
716
|
-
paginate: true,
|
717
|
-
wrap: 0,
|
718
|
-
output_title: false,
|
719
|
-
highlighter: 'auto',
|
720
|
-
pager: 'auto',
|
721
|
-
matching: 'partial', # exact, partial, fuzzy, beginswith
|
722
|
-
show_all_on_error: false,
|
723
|
-
include_upstream: false,
|
724
|
-
show_all_code: false,
|
725
|
-
multiple_matches: 'choose',
|
726
|
-
header_format: 'border',
|
727
|
-
log_level: 1 # 0: debug, 1: info, 2: warn, 3: error
|
728
|
-
}
|
729
|
-
|
730
|
-
@metadata = {}
|
731
|
-
@included = []
|
732
|
-
@nest_level = 0
|
733
|
-
|
734
|
-
parts = Shellwords.shelljoin(args).split(/ -- /)
|
735
|
-
args = parts[0] ? Shellwords.shellsplit(parts[0]) : []
|
736
|
-
@arguments = parts[1] ? Shellwords.shellsplit(parts[1]) : []
|
737
|
-
|
738
|
-
config = load_config(defaults)
|
739
|
-
@options = flags.merge(config)
|
740
|
-
|
741
|
-
OptionParser.new do |opts|
|
742
|
-
opts.banner = "Usage: #{__FILE__} [OPTIONS] [TOPIC]"
|
743
|
-
opts.separator ''
|
744
|
-
opts.separator 'Show build notes for the current project (buildnotes.md).
|
745
|
-
Include a topic name to see just that topic, or no argument to display all.'
|
746
|
-
opts.separator ''
|
747
|
-
opts.separator 'Options:'
|
748
|
-
|
749
|
-
opts.on('-c', '--create', 'Create a skeleton build note in the current working directory') do
|
750
|
-
create_note
|
751
|
-
Process.exit 0
|
752
|
-
end
|
753
|
-
|
754
|
-
opts.on('-e', '--edit', "Edit buildnotes file in current working directory
|
755
|
-
using $EDITOR") do
|
756
|
-
edit_note
|
757
|
-
Process.exit 0
|
758
|
-
end
|
759
|
-
|
760
|
-
opts.on('--grep PATTERN', 'Display sections matching a search pattern') do |pat|
|
761
|
-
@options[:grep] = pat
|
762
|
-
end
|
763
|
-
|
764
|
-
opts.on('-L', '--list-completions', 'List topics for completion') do
|
765
|
-
@options[:list_topics] = true
|
766
|
-
@options[:list_topic_titles] = true
|
767
|
-
end
|
768
|
-
|
769
|
-
opts.on('-l', '--list', 'List available topics') do
|
770
|
-
@options[:list_topics] = true
|
771
|
-
end
|
772
|
-
|
773
|
-
opts.on('-m', '--matching TYPE', MATCHING_OPTIONS,
|
774
|
-
'Topics matching type', "(#{MATCHING_OPTIONS.join(', ')})") do |c|
|
775
|
-
@options[:matching] = c
|
776
|
-
end
|
777
|
-
|
778
|
-
opts.on('--multiple TYPE', MULTIPLE_OPTIONS,
|
779
|
-
'Multiple result handling', "(#{MULTIPLE_OPTIONS.join(', ')}, default choose)") do |c|
|
780
|
-
@options[:multiple_matches] = c.to_sym
|
781
|
-
end
|
782
|
-
|
783
|
-
opts.on('-R', '--list-runnable', 'List topics containing @ directives (verbose)') do
|
784
|
-
@options[:list_runnable] = true
|
785
|
-
end
|
786
|
-
|
787
|
-
opts.on('-r', '--run', 'Execute @run, @open, and/or @copy commands for given topic') do
|
788
|
-
@options[:run] = true
|
789
|
-
end
|
790
|
-
|
791
|
-
opts.on('-s', '--select', 'Select topic from menu') do
|
792
|
-
@options[:choose] = true
|
793
|
-
end
|
794
|
-
|
795
|
-
opts.on('-T', '--task-list', 'List topics containing @ directives (completion-compatible)') do
|
796
|
-
@options[:list_runnable] = true
|
797
|
-
@options[:list_runnable_titles] = true
|
798
|
-
end
|
799
|
-
|
800
|
-
opts.on('-t', '--title', 'Output title with build notes') do
|
801
|
-
@options[:output_title] = true
|
802
|
-
end
|
803
|
-
|
804
|
-
opts.on('-q', '--quiet', 'Silence info message') do
|
805
|
-
@options[:log_level] = 3
|
806
|
-
end
|
807
|
-
|
808
|
-
opts.on('-v', '--verbose', 'Show all messages') do
|
809
|
-
@options[:log_level] = 0
|
810
|
-
end
|
811
|
-
|
812
|
-
opts.on('-u', '--[no-]upstream', 'Traverse up parent directories for additional build notes') do |p|
|
813
|
-
@options[:include_upstream] = p
|
814
|
-
end
|
815
|
-
|
816
|
-
opts.on('--show-code', 'Display the content of fenced run blocks') do
|
817
|
-
@options[:show_all_code] = true
|
818
|
-
end
|
819
|
-
|
820
|
-
opts.on('-w', '--wrap COLUMNS', 'Wrap to specified width (default 80, 0 to disable)') do |w|
|
821
|
-
@options[:wrap] = w.to_i
|
822
|
-
end
|
823
|
-
|
824
|
-
opts.on('--config-get [KEY]', 'Display the configuration settings or setting for a specific key') do |k|
|
825
|
-
|
826
|
-
if k.nil?
|
827
|
-
config.sort_by { |key, _| key }.each do |key, val|
|
828
|
-
print "#{key}: "
|
829
|
-
p val
|
830
|
-
end
|
831
|
-
else
|
832
|
-
k.sub!(/^:/, '')
|
833
|
-
if config.key?(k.to_sym)
|
834
|
-
puts config[k.to_sym]
|
835
|
-
else
|
836
|
-
puts "Key #{k} not found"
|
837
|
-
end
|
838
|
-
end
|
839
|
-
Process.exit 0
|
840
|
-
end
|
841
|
-
|
842
|
-
opts.on('--config-set KEY=VALUE', 'Set a config value (must be a valid key)') do |key|
|
843
|
-
raise 'Argument must be KEY=VALUE' unless key =~ /\S=\S/
|
844
|
-
|
845
|
-
k, v = key.split(/=/)
|
846
|
-
k.sub!(/^:/, '')
|
847
|
-
|
848
|
-
if config.key?(k.to_sym)
|
849
|
-
config[k.to_sym] = v.to_config_value(config[k.to_sym])
|
850
|
-
else
|
851
|
-
puts "Key #{k} not found"
|
852
|
-
end
|
853
|
-
write_config(config)
|
854
|
-
Process.exit 0
|
855
|
-
end
|
856
|
-
|
857
|
-
opts.on('--edit-config', "Edit configuration file using default $EDITOR") do
|
858
|
-
edit_config(defaults)
|
859
|
-
Process.exit 0
|
860
|
-
end
|
861
|
-
|
862
|
-
opts.on('--title-only', 'Output title only') do
|
863
|
-
@options[:output_title] = true
|
864
|
-
@options[:title_only] = true
|
865
|
-
end
|
866
|
-
|
867
|
-
opts.on('--templates', 'List available templates') do
|
868
|
-
Dir.chdir(template_folder)
|
869
|
-
Dir.glob('*.md').each do |file|
|
870
|
-
template = File.basename(file, '.md')
|
871
|
-
puts Color.template("{Mk}template:{Yk}#{template}{x}")
|
872
|
-
puts Color.template("{bk}[{bl}tasks{bk}]──────────────────────────────────────┐{x}")
|
873
|
-
metadata = file.extract_metadata
|
874
|
-
topics = read_help_file(file)
|
875
|
-
topics.each_key do |topic|
|
876
|
-
puts Color.template(" {bk}│{bw}-{x} {bcK}#{template}:#{topic.sub(/^.*?:/, '')}{x}")
|
877
|
-
end
|
878
|
-
if metadata.size > 0
|
879
|
-
meta = []
|
880
|
-
meta << metadata['required'].split(/\s*,\s*/).map {|m| "*{bw}#{m}{xw}" } if metadata.key?('required')
|
881
|
-
meta << metadata['optional'].split(/\s*,\s*/).map {|m| "#{m}" } if metadata.key?('optional')
|
882
|
-
puts Color.template("{bk}[{bl}meta{bk}]───────────────────────────────────────┤{x}")
|
883
|
-
puts Color.template(" {bk}│ {xw}#{meta.join(", ")}{x}")
|
884
|
-
end
|
885
|
-
puts Color.template(" {bk}└───────────────────────────────────────────┘{x}")
|
886
|
-
end
|
887
|
-
Process.exit 0
|
888
|
-
end
|
889
|
-
|
890
|
-
opts.on('--header-format TYPE', HEADER_FORMAT_OPTIONS,
|
891
|
-
"Formatting style for topic titles (#{HEADER_FORMAT_OPTIONS.join(', ')})") do |t|
|
892
|
-
@options[:header_format] = t
|
893
|
-
end
|
894
|
-
|
895
|
-
opts.on('--[no-]color', 'Colorize output (default on)') do |c|
|
896
|
-
@options[:color] = c
|
897
|
-
@options[:highlight] = false unless c
|
898
|
-
end
|
899
|
-
|
900
|
-
opts.on('--[no-]md-highlight', 'Highlight Markdown syntax (default on), requires mdless or mdcat') do |m|
|
901
|
-
@options[:highlight] = @options[:color] ? m : false
|
902
|
-
end
|
903
|
-
|
904
|
-
opts.on('--[no-]pager', 'Paginate output (default on)') do |p|
|
905
|
-
@options[:paginate] = p
|
906
|
-
end
|
907
|
-
|
908
|
-
opts.on('-h', '--help', 'Display this screen') do
|
909
|
-
puts opts
|
910
|
-
Process.exit 0
|
911
|
-
end
|
912
|
-
|
913
|
-
opts.on('-v', '--version', 'Display version number') do
|
914
|
-
puts "Howzit v#{VERSION}"
|
915
|
-
Process.exit 0
|
916
|
-
end
|
917
|
-
|
918
|
-
opts.on('--default', 'Answer all prompts with default response') do
|
919
|
-
@options[:default] = true
|
920
|
-
end
|
921
|
-
end.parse!(args)
|
922
|
-
|
923
|
-
@options[:multiple_matches] = @options[:multiple_matches].to_sym
|
924
|
-
@options[:header_format] = @options[:header_format].to_sym
|
925
|
-
|
926
|
-
@cli_args = args
|
927
|
-
end
|
928
|
-
|
929
|
-
def edit_note
|
930
|
-
raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
931
|
-
|
932
|
-
if note_file.nil?
|
933
|
-
res = yn("No build notes file found, create one?", true)
|
934
|
-
|
935
|
-
create_note if res
|
936
|
-
edit_note
|
937
|
-
else
|
938
|
-
`#{ENV['EDITOR']} "#{note_file}"`
|
939
|
-
end
|
940
|
-
end
|
941
|
-
|
942
|
-
##
|
943
|
-
## Traverse up directory tree looking for build notes
|
944
|
-
##
|
945
|
-
## @return topics dictionary
|
946
|
-
##
|
947
|
-
def glob_upstream
|
948
|
-
home = Dir.pwd
|
949
|
-
dir = File.dirname(home)
|
950
|
-
buildnotes = []
|
951
|
-
filename = nil
|
952
|
-
|
953
|
-
while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
|
954
|
-
Dir.chdir(dir)
|
955
|
-
filename = glob_note
|
956
|
-
unless filename.nil?
|
957
|
-
note = File.join(dir, filename)
|
958
|
-
buildnotes.push(note) unless note == note_file
|
959
|
-
end
|
960
|
-
dir = File.dirname(dir)
|
961
|
-
end
|
962
|
-
|
963
|
-
Dir.chdir(home)
|
964
|
-
|
965
|
-
buildnotes.reverse
|
966
|
-
end
|
967
|
-
|
968
|
-
def is_build_notes(filename)
|
969
|
-
return false if filename.downcase !~ /(^howzit[^.]*|build[^.]+)/
|
970
|
-
return false if should_ignore(filename)
|
971
|
-
true
|
972
|
-
end
|
973
|
-
|
974
|
-
def should_ignore(filename)
|
975
|
-
return false unless File.exist?(ignore_file)
|
976
|
-
|
977
|
-
unless @ignore_patterns
|
978
|
-
@ignore_patterns = YAML.load(IO.read(ignore_file))
|
979
|
-
end
|
980
|
-
|
981
|
-
ignore = false
|
982
|
-
|
983
|
-
@ignore_patterns.each do |pat|
|
984
|
-
if filename =~ /#{pat}/
|
985
|
-
ignore = true
|
986
|
-
break
|
987
|
-
end
|
988
|
-
end
|
989
|
-
|
990
|
-
ignore
|
991
|
-
end
|
992
|
-
|
993
|
-
def glob_note
|
994
|
-
filename = nil
|
995
|
-
# Check for a build note file in the current folder. Filename must start
|
996
|
-
# with "build" and have an extension of txt, md, or markdown.
|
997
|
-
|
998
|
-
Dir.glob('*.{txt,md,markdown}').each do |f|
|
999
|
-
if is_build_notes(f)
|
1000
|
-
filename = f
|
1001
|
-
break
|
1002
|
-
end
|
1003
|
-
end
|
1004
|
-
filename
|
1005
|
-
end
|
1006
|
-
|
1007
|
-
def note_file
|
1008
|
-
@note_file ||= find_note_file
|
1009
|
-
end
|
1010
|
-
|
1011
|
-
def find_note_file
|
1012
|
-
filename = glob_note
|
1013
|
-
|
1014
|
-
if filename.nil? && 'git'.available?
|
1015
|
-
proj_dir = `git rev-parse --show-toplevel 2>/dev/null`.strip
|
1016
|
-
unless proj_dir == ''
|
1017
|
-
Dir.chdir(proj_dir)
|
1018
|
-
filename = glob_note
|
1019
|
-
end
|
1020
|
-
end
|
1021
|
-
|
1022
|
-
if filename.nil? && @options[:include_upstream]
|
1023
|
-
upstream_notes = glob_upstream
|
1024
|
-
filename = upstream_notes[-1] unless upstream_notes.empty?
|
1025
|
-
end
|
1026
|
-
|
1027
|
-
return nil if filename.nil?
|
1028
|
-
|
1029
|
-
File.expand_path(filename)
|
1030
|
-
end
|
1031
|
-
|
1032
|
-
def options_list(matches)
|
1033
|
-
counter = 1
|
1034
|
-
puts
|
1035
|
-
matches.each do |match|
|
1036
|
-
printf("%<counter>2d ) %<option>s\n", counter: counter, option: match)
|
1037
|
-
counter += 1
|
1038
|
-
end
|
1039
|
-
puts
|
1040
|
-
end
|
1041
|
-
|
1042
|
-
def command_exist?(command)
|
1043
|
-
exts = ENV.fetch('PATHEXT', '').split(::File::PATH_SEPARATOR)
|
1044
|
-
if Pathname.new(command).absolute?
|
1045
|
-
::File.exist?(command) ||
|
1046
|
-
exts.any? { |ext| ::File.exist?("#{command}#{ext}") }
|
1047
|
-
else
|
1048
|
-
ENV.fetch('PATH', '').split(::File::PATH_SEPARATOR).any? do |dir|
|
1049
|
-
file = ::File.join(dir, command)
|
1050
|
-
::File.exist?(file) ||
|
1051
|
-
exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
|
1052
|
-
end
|
1053
|
-
end
|
1054
|
-
end
|
1055
|
-
|
1056
|
-
def choose(matches)
|
1057
|
-
if command_exist?('fzf')
|
1058
|
-
settings = [
|
1059
|
-
'-0',
|
1060
|
-
'-1',
|
1061
|
-
'-m',
|
1062
|
-
"--height=#{matches.count + 2}",
|
1063
|
-
'--header="Use tab to mark multiple selections, enter to display/run"',
|
1064
|
-
'--prompt="Select a section > "'
|
1065
|
-
]
|
1066
|
-
res = `echo #{Shellwords.escape(matches.join("\n"))} | fzf #{settings.join(' ')}`.strip
|
1067
|
-
if res.nil? || res.empty?
|
1068
|
-
warn 'Cancelled'
|
1069
|
-
Process.exit 0
|
1070
|
-
end
|
1071
|
-
return res.split(/\n/)
|
1072
|
-
end
|
1073
|
-
|
1074
|
-
res = matches[0..9]
|
1075
|
-
stty_save = `stty -g`.chomp
|
1076
|
-
|
1077
|
-
trap('INT') do
|
1078
|
-
system('stty', stty_save)
|
1079
|
-
exit
|
1080
|
-
end
|
1081
|
-
|
1082
|
-
options_list(matches)
|
1083
|
-
|
1084
|
-
begin
|
1085
|
-
printf("Type 'q' to cancel, enter for first item", res.length)
|
1086
|
-
while (line = Readline.readline(': ', true))
|
1087
|
-
if line =~ /^[a-z]/i
|
1088
|
-
system('stty', stty_save) # Restore
|
1089
|
-
exit
|
1090
|
-
end
|
1091
|
-
line = line == '' ? 1 : line.to_i
|
1092
|
-
|
1093
|
-
return matches[line - 1] if line.positive? && line <= matches.length
|
1094
|
-
|
1095
|
-
puts 'Out of range'
|
1096
|
-
options_list(matches)
|
1097
|
-
end
|
1098
|
-
rescue Interrupt
|
1099
|
-
system('stty', stty_save)
|
1100
|
-
exit
|
1101
|
-
end
|
1102
|
-
end
|
1103
|
-
|
1104
|
-
def config_dir
|
1105
|
-
File.expand_path(CONFIG_DIR)
|
1106
|
-
end
|
1107
|
-
|
1108
|
-
def config_file
|
1109
|
-
File.join(config_dir, CONFIG_FILE)
|
1110
|
-
end
|
1111
|
-
|
1112
|
-
def ignore_file
|
1113
|
-
File.join(config_dir, IGNORE_FILE)
|
1114
|
-
end
|
1115
|
-
|
1116
|
-
def template_folder
|
1117
|
-
File.join(config_dir, 'templates')
|
1118
|
-
end
|
1119
|
-
|
1120
|
-
def create_config(defaults)
|
1121
|
-
dir, file = [config_dir, config_file]
|
1122
|
-
unless File.directory?(dir)
|
1123
|
-
warn "Creating config directory at #{dir}"
|
1124
|
-
FileUtils.mkdir_p(dir)
|
1125
|
-
end
|
1126
|
-
|
1127
|
-
unless File.exist?(file)
|
1128
|
-
warn "Writing fresh config file to #{file}"
|
1129
|
-
write_config(defaults)
|
1130
|
-
end
|
1131
|
-
file
|
1132
|
-
end
|
1133
|
-
|
1134
|
-
def load_config(defaults)
|
1135
|
-
file = create_config(defaults)
|
1136
|
-
config = YAML.load(IO.read(file))
|
1137
|
-
newconfig = config ? defaults.merge(config) : defaults
|
1138
|
-
write_config(newconfig)
|
1139
|
-
newconfig
|
1140
|
-
end
|
1141
|
-
|
1142
|
-
def write_config(config)
|
1143
|
-
File.open(config_file, 'w') { |f| f.puts config.to_yaml }
|
1144
|
-
end
|
1145
|
-
|
1146
|
-
def edit_config(defaults)
|
1147
|
-
raise 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil?
|
1148
|
-
|
1149
|
-
load_config(defaults)
|
1150
|
-
`#{ENV['EDITOR']} "#{config_file}"`
|
1151
|
-
end
|
1152
|
-
|
1153
|
-
def process
|
1154
|
-
output = []
|
1155
|
-
|
1156
|
-
unless note_file
|
1157
|
-
Process.exit 0 if @options[:list_runnable_titles] || @options[:list_topic_titles]
|
1158
|
-
|
1159
|
-
# clear the buffer
|
1160
|
-
ARGV.length.times do
|
1161
|
-
ARGV.shift
|
1162
|
-
end
|
1163
|
-
res = yn("No build notes file found, create one?", true)
|
1164
|
-
create_note if res
|
1165
|
-
Process.exit 1
|
1166
|
-
end
|
1167
|
-
|
1168
|
-
if @options[:title_only]
|
1169
|
-
out = get_note_title(20)
|
1170
|
-
$stdout.print(out.strip)
|
1171
|
-
Process.exit(0)
|
1172
|
-
elsif @options[:output_title] && !@options[:run]
|
1173
|
-
title = get_note_title
|
1174
|
-
if title && !title.empty?
|
1175
|
-
header = format_header(title, { hr: "\u{2550}", color: '{bwK}' })
|
1176
|
-
output.push("#{header}\n")
|
1177
|
-
end
|
1178
|
-
end
|
1179
|
-
|
1180
|
-
if @options[:list_runnable]
|
1181
|
-
if @options[:list_runnable_titles]
|
1182
|
-
out = list_runnable_titles
|
1183
|
-
$stdout.print(out.strip)
|
1184
|
-
else
|
1185
|
-
out = list_runnable
|
1186
|
-
show(out, { color: @options[:color], paginate: false, highlight: false })
|
1187
|
-
end
|
1188
|
-
Process.exit(0)
|
1189
|
-
end
|
1190
|
-
|
1191
|
-
if @options[:list_topics]
|
1192
|
-
if @options[:list_topic_titles]
|
1193
|
-
$stdout.print(list_topic_titles)
|
1194
|
-
else
|
1195
|
-
out = list_topics
|
1196
|
-
show(out, { color: @options[:color], paginate: false, highlight: false })
|
1197
|
-
end
|
1198
|
-
Process.exit(0)
|
1199
|
-
end
|
1200
|
-
|
1201
|
-
topic_matches = []
|
1202
|
-
if @options[:grep]
|
1203
|
-
matches = grep_topics(@options[:grep])
|
1204
|
-
case @options[:multiple_matches]
|
1205
|
-
when :all
|
1206
|
-
topic_matches.concat(matches.sort)
|
1207
|
-
else
|
1208
|
-
topic_matches.concat(choose(matches))
|
1209
|
-
end
|
1210
|
-
elsif @options[:choose]
|
1211
|
-
topic_matches.concat(choose(topics.keys))
|
1212
|
-
# If there are arguments use those to search for a matching topic
|
1213
|
-
elsif !@cli_args.empty?
|
1214
|
-
search = @cli_args.join(' ').strip.downcase.split(/ *, */).map(&:strip)
|
1215
|
-
|
1216
|
-
search.each do |s|
|
1217
|
-
matches = match_topic(s)
|
1218
|
-
|
1219
|
-
if matches.empty?
|
1220
|
-
output.push(Color.template(%({bR}ERROR:{xr} No topic match found for {bw}#{s}{x}\n)))
|
1221
|
-
else
|
1222
|
-
case @options[:multiple_matches]
|
1223
|
-
when :first
|
1224
|
-
topic_matches.push(matches[0])
|
1225
|
-
when :best
|
1226
|
-
topic_matches.push(matches.sort.min_by(&:length))
|
1227
|
-
when :all
|
1228
|
-
topic_matches.concat(matches)
|
1229
|
-
else
|
1230
|
-
topic_matches.concat(choose(matches))
|
1231
|
-
end
|
1232
|
-
end
|
1233
|
-
end
|
1234
|
-
|
1235
|
-
if topic_matches.empty? && !@options[:show_all_on_error]
|
1236
|
-
show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
|
1237
|
-
Process.exit 1
|
1238
|
-
end
|
1239
|
-
end
|
1240
|
-
|
1241
|
-
if !topic_matches.empty?
|
1242
|
-
# If we found a match
|
1243
|
-
topic_matches.each { |topic_match| output.push(process_topic(topic_match, @options[:run], true)) }
|
1244
|
-
else
|
1245
|
-
# If there's no argument or no match found, output all
|
1246
|
-
topics.each_key { |k| output.push(process_topic(k, false, false)) }
|
1247
|
-
end
|
1248
|
-
@options[:paginate] = false if @options[:run]
|
1249
|
-
show(output.join("\n").strip, @options)
|
1250
|
-
end
|
1251
|
-
end
|
1252
|
-
end
|