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