howzit 1.2.14 → 1.2.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howzit
4
+ # BuildNote Class
5
+ class BuildNote
6
+ attr_accessor :topics
7
+
8
+ attr_reader :metadata, :title
9
+
10
+ def initialize(file: nil, args: [])
11
+ @topics = []
12
+ @metadata = IO.read(note_file).split(/^#/)[0].strip.get_metadata
13
+
14
+ read_help(file)
15
+ end
16
+
17
+ def inspect
18
+ puts "#<Howzit::BuildNote @topics=[#{@topics.count}]>"
19
+ end
20
+
21
+ def run
22
+ process
23
+ end
24
+
25
+ def edit
26
+ edit_note
27
+ end
28
+
29
+ def find_topic(term)
30
+ @topics.filter do |topic|
31
+ rx = term.to_rx
32
+ topic.title.downcase =~ rx
33
+ end
34
+ end
35
+
36
+ def grep(term)
37
+ @topics.filter { |topic| topic.grep(term) }
38
+ end
39
+
40
+ # Output a list of topic titles
41
+ def list
42
+ output = []
43
+ output.push(Color.template("{bg}Topics:{x}\n"))
44
+ @topics.each do |topic|
45
+ output.push(Color.template("- {bw}#{topic.title}{x}"))
46
+ end
47
+ output.join("\n")
48
+ end
49
+
50
+ def list_topics
51
+ @topics.map { |topic| topic.title }
52
+ end
53
+
54
+ def list_completions
55
+ list_topics.join("\n")
56
+ end
57
+
58
+ def list_runnable_completions
59
+ output = []
60
+ @topics.each do |topic|
61
+ output.push(topic.title) if topic.tasks.count.positive?
62
+ end
63
+ output.join("\n")
64
+ end
65
+
66
+ def list_runnable
67
+ output = []
68
+ output.push(Color.template(%({bg}"Runnable" Topics:{x}\n)))
69
+ @topics.each do |topic|
70
+ s_out = []
71
+
72
+ topic.tasks.each do |task|
73
+ s_out.push(task.to_list)
74
+ end
75
+
76
+ unless s_out.empty?
77
+ output.push(Color.template("- {bw}#{topic.title}{x}"))
78
+ output.push(s_out.join("\n"))
79
+ end
80
+ end
81
+ output.join("\n")
82
+ end
83
+
84
+ def read_file(file)
85
+ read_help_file(file)
86
+ end
87
+
88
+ # Create a buildnotes skeleton
89
+ def create_note
90
+ trap('SIGINT') do
91
+ warn "\nCanceled"
92
+ exit!
93
+ end
94
+ default = !$stdout.isatty || Howzit.options[:default]
95
+ # First make sure there isn't already a buildnotes file
96
+ if note_file
97
+ fname = Color.template("{by}#{note_file}{bw}")
98
+ unless default
99
+ res = Prompt.yn("#{fname} exists and appears to be a build note, continue anyway?", false)
100
+ unless res
101
+ puts 'Canceled'
102
+ Process.exit 0
103
+ end
104
+ end
105
+ end
106
+
107
+ title = File.basename(Dir.pwd)
108
+ if default
109
+ input = title
110
+ else
111
+ printf Color.template("{bw}Project name {xg}[#{title}]{bw}: {x}")
112
+ input = $stdin.gets.chomp
113
+ title = input unless input.empty?
114
+ end
115
+ summary = ''
116
+ unless default
117
+ printf Color.template('{bw}Project summary: {x}')
118
+ input = $stdin.gets.chomp
119
+ summary = input unless input.empty?
120
+ end
121
+
122
+ fname = 'buildnotes.md'
123
+ unless default
124
+ printf Color.template("{bw}Build notes filename (must begin with 'howzit' or 'build')\n{xg}[#{fname}]{bw}: {x}")
125
+ input = $stdin.gets.chomp
126
+ fname = input unless input.empty?
127
+ end
128
+
129
+ note = <<~EOBUILDNOTES
130
+ # #{title}
131
+
132
+ #{summary}
133
+
134
+ ## File Structure
135
+
136
+ Where are the main editable files? Is there a dist/build folder that should be ignored?
137
+
138
+ ## Build
139
+
140
+ What build system/parameters does this use?
141
+
142
+ @run(./build command)
143
+
144
+ ## Deploy
145
+
146
+ What are the procedures/commands to deploy this project?
147
+
148
+ ## Other
149
+
150
+ Version control notes, additional gulp/rake/make/etc tasks...
151
+
152
+ EOBUILDNOTES
153
+
154
+ if File.exist?(fname) && !default
155
+ file = Color.template("{by}#{fname}")
156
+ res = Prompt.yn("Are you absolutely sure you want to overwrite #{file}", false)
157
+
158
+ unless res
159
+ puts 'Canceled'
160
+ Process.exit 0
161
+ end
162
+ end
163
+
164
+ File.open(fname, 'w') do |f|
165
+ f.puts note
166
+ puts Color.template("{by}Build notes for #{title} written to #{fname}")
167
+ end
168
+ end
169
+
170
+ def note_file
171
+ @note_file ||= find_note_file
172
+ end
173
+
174
+ private
175
+
176
+ ##
177
+ ## Traverse up directory tree looking for build notes
178
+ ##
179
+ ## @return topics dictionary
180
+ ##
181
+ def glob_upstream
182
+ home = Dir.pwd
183
+ dir = File.dirname(home)
184
+ buildnotes = []
185
+ filename = nil
186
+
187
+ while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
188
+ Dir.chdir(dir)
189
+ filename = glob_note
190
+ unless filename.nil?
191
+ note = File.join(dir, filename)
192
+ buildnotes.push(note) unless note == note_file
193
+ end
194
+ dir = File.dirname(dir)
195
+ end
196
+
197
+ Dir.chdir(home)
198
+
199
+ buildnotes.reverse
200
+ end
201
+
202
+ def is_build_notes(filename)
203
+ return false if filename.downcase !~ /(^howzit[^.]*|build[^.]+)/
204
+ return false if Howzit.config.should_ignore(filename)
205
+ true
206
+ end
207
+
208
+ def glob_note
209
+ filename = nil
210
+ # Check for a build note file in the current folder. Filename must start
211
+ # with "build" and have an extension of txt, md, or markdown.
212
+
213
+ Dir.glob('*.{txt,md,markdown}').each do |f|
214
+ if is_build_notes(f)
215
+ filename = f
216
+ break
217
+ end
218
+ end
219
+ filename
220
+ end
221
+
222
+ def find_note_file
223
+ filename = glob_note
224
+
225
+ if filename.nil? && 'git'.available?
226
+ proj_dir = `git rev-parse --show-toplevel 2>/dev/null`.strip
227
+ unless proj_dir == ''
228
+ Dir.chdir(proj_dir)
229
+ filename = glob_note
230
+ end
231
+ end
232
+
233
+ if filename.nil? && Howzit.options[:include_upstream]
234
+ upstream_notes = glob_upstream
235
+ filename = upstream_notes[-1] unless upstream_notes.empty?
236
+ end
237
+
238
+ return nil if filename.nil?
239
+
240
+ File.expand_path(filename)
241
+ end
242
+
243
+ def read_upstream
244
+ buildnotes = glob_upstream
245
+
246
+ topics_dict = []
247
+ buildnotes.each do |path|
248
+ topics_dict.concat(read_help_file(path))
249
+ end
250
+ topics_dict
251
+ end
252
+
253
+ def ensure_requirements(template)
254
+ t_leader = IO.read(template).split(/^#/)[0].strip
255
+ if t_leader.length > 0
256
+ t_meta = t_leader.get_metadata
257
+ if t_meta.key?('required')
258
+ required = t_meta['required'].strip.split(/\s*,\s*/)
259
+ required.each do |req|
260
+ unless @metadata.keys.include?(req.downcase)
261
+ warn Color.template(%({xr}ERROR: Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}))
262
+ warn Color.template(%({xr}Please define {by}#{req.downcase}{xr} in build notes{x}))
263
+ Process.exit 1
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ def get_template_topics(content)
271
+ leader = content.split(/^#/)[0].strip
272
+
273
+ template_topics = []
274
+
275
+ if leader.length > 0
276
+ data = leader.get_metadata
277
+
278
+ if data.key?('template')
279
+ templates = data['template'].strip.split(/\s*,\s*/)
280
+ templates.each do |t|
281
+ tasks = nil
282
+ if t =~ /\[(.*?)\]$/
283
+ tasks = Regexp.last_match[1].split(/\s*,\s*/).map {|t| t.gsub(/\*/, '.*?')}
284
+ t = t.sub(/\[.*?\]$/, '').strip
285
+ end
286
+
287
+ t_file = t.sub(/(\.md)?$/, '.md')
288
+ template = File.join(Howzit.config.template_folder, t_file)
289
+ if File.exist?(template)
290
+ ensure_requirements(template)
291
+
292
+ t_topics = BuildNote.new(file: template)
293
+ if tasks
294
+ tasks.each do |task|
295
+ t_topics.topics.each do |topic|
296
+ if topic.title =~ /^(.*?:)?#{task}$/i
297
+ topic.parent = t
298
+ template_topics.push(topic)
299
+ end
300
+ end
301
+ end
302
+ else
303
+ t_topics.topics.map! do |topic|
304
+ topic.parent = t
305
+ topic
306
+ end
307
+
308
+ template_topics.concat(t_topics.topics)
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+ template_topics
315
+ end
316
+
317
+ def include_file(m)
318
+ file = File.expand_path(m[1])
319
+
320
+ return m[0] unless File.exist?(file)
321
+
322
+ content = IO.read(file)
323
+ home = ENV['HOME']
324
+ short_path = File.dirname(file.sub(/^#{home}/, '~'))
325
+ prefix = "#{short_path}/#{File.basename(file)}:"
326
+ parts = content.split(/^##+/)
327
+ parts.shift
328
+ if parts.empty?
329
+ content
330
+ else
331
+ "## #{parts.join('## ')}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}")
332
+ end
333
+ end
334
+
335
+ def note_title(truncate = 0)
336
+ help = IO.read(note_file).strip
337
+ title = help.match(/(?:^(\S.*?)(?=\n==)|^# ?(.*?)$)/)
338
+ title = if title
339
+ title[1].nil? ? title[2] : title[1]
340
+ else
341
+ note_file.sub(/(\.\w+)?$/, '')
342
+ end
343
+
344
+ title && truncate.positive? ? title.trunc(truncate) : title
345
+ end
346
+
347
+ # Read in the build notes file and output a hash of "Title" => contents
348
+ def read_help_file(path = nil)
349
+ topics = []
350
+
351
+ filename = path.nil? ? note_file : path
352
+
353
+ help = IO.read(filename)
354
+
355
+ @title = note_title
356
+
357
+ help.gsub!(/@include\((.*?)\)/) do
358
+ include_file(Regexp.last_match)
359
+ end
360
+
361
+ template_topics = get_template_topics(help)
362
+
363
+ split = help.split(/^##+/)
364
+ split.slice!(0)
365
+ split.each do |sect|
366
+ next if sect.strip.empty?
367
+
368
+ lines = sect.split(/\n/)
369
+ title = lines.slice!(0).strip
370
+ prefix = ''
371
+ if path
372
+ if path =~ /#{Howzit.config.template_folder}/
373
+ short_path = File.basename(path, '.md')
374
+ else
375
+ home = ENV['HOME']
376
+ short_path = File.dirname(path.sub(/^#{home}/, '~'))
377
+ prefix = "_from #{short_path}_\n\n"
378
+ end
379
+ title = "#{short_path}:#{title}"
380
+ end
381
+ topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata))
382
+
383
+ topics.push(topic)
384
+ end
385
+
386
+ template_topics.each do |topic|
387
+ topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
388
+ end
389
+
390
+ topics
391
+ end
392
+
393
+ def read_help(path = nil)
394
+ @topics = read_help_file(path)
395
+ return unless path.nil? && Howzit.options[:include_upstream]
396
+
397
+ upstream_topics = read_upstream
398
+
399
+ upstream_topics.each do |topic|
400
+ @topics.push(topic) unless find_topic(title.sub(/^.+:/, '')).count.positive?
401
+ end
402
+ end
403
+
404
+ def edit_note
405
+ editor = Howzit.options.fetch(:editor, ENV['EDITOR'])
406
+
407
+ raise 'No editor defined' if editor.nil?
408
+
409
+ raise "Invalid editor (#{editor})" unless Util.valid_command?(editor)
410
+
411
+ if note_file.nil?
412
+ res = Prompt.yn('No build notes file found, create one?', true)
413
+
414
+ create_note if res
415
+ edit_note
416
+ else
417
+ `#{editor} "#{note_file}"`
418
+ end
419
+ end
420
+
421
+ def process_topic(topic, run, single = false)
422
+ new_topic = topic.dup
423
+
424
+ # Handle variable replacement
425
+
426
+ unless Howzit.arguments.empty?
427
+ new_topic.content = new_topic.content.gsub(/\$(\d+)/) do |m|
428
+ idx = m[1].to_i - 1
429
+ Howzit.arguments.length > idx ? Howzit.arguments[idx] : m
430
+ end
431
+ new_topic.content = new_topic.content.gsub(/\$[@*]/, Shellwords.join(Howzit.arguments))
432
+ end
433
+
434
+ output = if run
435
+ new_topic.run
436
+ else
437
+ new_topic.print_out({ single: single })
438
+ end
439
+ output.nil? ? '' : output.join("\n")
440
+ end
441
+
442
+ def process
443
+ output = []
444
+
445
+ unless note_file
446
+ Process.exit 0 if Howzit.options[:list_runnable_titles] || Howzit.options[:list_topic_titles]
447
+
448
+ # clear the buffer
449
+ ARGV.length.times do
450
+ ARGV.shift
451
+ end
452
+ res = yn("No build notes file found, create one?", true)
453
+ create_note if res
454
+ Process.exit 1
455
+ end
456
+
457
+ if Howzit.options[:title_only]
458
+ out = note_title(20)
459
+ $stdout.print(out.strip)
460
+ Process.exit(0)
461
+ elsif Howzit.options[:output_title] && !Howzit.options[:run]
462
+ if @title && !@title.empty?
463
+ header = @title.format_header({ hr: "\u{2550}", color: '{bwK}' })
464
+ output.push("#{header}\n")
465
+ end
466
+ end
467
+
468
+ if Howzit.options[:list_runnable]
469
+ if Howzit.options[:list_runnable_titles]
470
+ out = list_runnable_completions
471
+ $stdout.print(out.strip)
472
+ else
473
+ out = list_runnable
474
+ Util.show(out, { color: Howzit.options[:color], paginate: false, highlight: false })
475
+ end
476
+ Process.exit(0)
477
+ end
478
+
479
+ if Howzit.options[:list_topics]
480
+ if Howzit.options[:list_topic_titles]
481
+ $stdout.print(list_completions)
482
+ else
483
+ out = list
484
+ Util.show(out, { color: Howzit.options[:color], paginate: false, highlight: false })
485
+ end
486
+ Process.exit(0)
487
+ end
488
+
489
+ topic_matches = []
490
+ if Howzit.options[:grep]
491
+ matches = grep_topics(Howzit.options[:grep])
492
+ case Howzit.options[:multiple_matches]
493
+ when :all
494
+ topic_matches.concat(matches.sort)
495
+ else
496
+ topic_matches.concat(Prompt.choose(matches))
497
+ end
498
+ elsif Howzit.options[:choose]
499
+ titles = Prompt.choose(list_topics)
500
+ titles.each { |title| topic_matches.push(find_topic(title)[0]) }
501
+ # If there are arguments use those to search for a matching topic
502
+ elsif !Howzit.cli_args.empty?
503
+ search = Howzit.cli_args.join(' ').strip.downcase.split(/ *, */).map(&:strip)
504
+
505
+ search.each do |s|
506
+ matches = find_topic(s)
507
+
508
+ if matches.empty?
509
+ output.push(Color.template(%({bR}ERROR:{xr} No topic match found for {bw}#{s}{x}\n)))
510
+ else
511
+ case Howzit.options[:multiple_matches]
512
+ when :first
513
+ topic_matches.push(matches[0])
514
+ when :best
515
+ topic_matches.push(matches.sort.min_by { |t| t.title.length })
516
+ when :all
517
+ topic_matches.concat(matches)
518
+ else
519
+ titles = matches.map { |topic| topic.title }
520
+ res = Prompt.choose(titles)
521
+ res.each { |title| topic_matches.concat(find_topic(title)) }
522
+ end
523
+ end
524
+ end
525
+
526
+ if topic_matches.empty? && !Howzit.options[:show_all_on_error]
527
+ Util.show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
528
+ Process.exit 1
529
+ end
530
+ end
531
+
532
+ if !topic_matches.empty?
533
+ # If we found a match
534
+ topic_matches.each { |topic_match| output.push(process_topic(topic_match, Howzit.options[:run], true)) }
535
+ else
536
+ # If there's no argument or no match found, output all
537
+ topics.each { |k| output.push(process_topic(k, false, false)) }
538
+ end
539
+ Howzit.options[:paginate] = false if Howzit.options[:run]
540
+ Util.show(output.join("\n").strip, Howzit.options)
541
+ end
542
+ end
543
+ end
@@ -247,17 +247,22 @@ module Howzit
247
247
 
248
248
  options.merge!(opts)
249
249
 
250
- cols = TTY::Screen.columns
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
251
255
 
252
- cols = @options[:wrap] if (@options[:wrap]).positive? && cols > @options[:wrap]
253
- title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")
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]} )")
254
258
 
255
- tail = if should_mark_iterm?
256
- "#{options[:hr] * (cols - title.uncolor.length - 15)}#{options[:mark] ? iterm_marker : ''}"
257
- else
258
- options[:hr] * (cols - title.uncolor.length)
259
- end
260
- Color.template("#{title}#{tail}{x}")
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
261
266
  end
262
267
 
263
268
  def os_open(command)
@@ -451,7 +456,7 @@ module Howzit
451
456
  else
452
457
  output_topic(key, {single: single})
453
458
  end
454
- output.nil? ? '' : output.join("\n").strip
459
+ output.nil? ? '' : output.join("\n")
455
460
  end
456
461
 
457
462
  # Output a list of topic titles
@@ -668,7 +673,6 @@ module Howzit
668
673
  topics
669
674
  end
670
675
 
671
-
672
676
  def match_topic(search)
673
677
  matches = []
674
678
 
@@ -719,6 +723,7 @@ module Howzit
719
723
  include_upstream: false,
720
724
  show_all_code: false,
721
725
  multiple_matches: 'choose',
726
+ header_format: 'border',
722
727
  log_level: 1 # 0: debug, 1: info, 2: warn, 3: error
723
728
  }
724
729
 
@@ -882,6 +887,11 @@ module Howzit
882
887
  Process.exit 0
883
888
  end
884
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
+
885
895
  opts.on('--[no-]color', 'Colorize output (default on)') do |c|
886
896
  @options[:color] = c
887
897
  @options[:highlight] = false unless c
@@ -911,6 +921,7 @@ module Howzit
911
921
  end.parse!(args)
912
922
 
913
923
  @options[:multiple_matches] = @options[:multiple_matches].to_sym
924
+ @options[:header_format] = @options[:header_format].to_sym
914
925
 
915
926
  @cli_args = args
916
927
  end
data/lib/howzit/colors.rb CHANGED
@@ -223,8 +223,8 @@ module Howzit
223
223
  ##
224
224
  def template(input)
225
225
  input = input.join(' ') if input.is_a? Array
226
- input.gsub!(/%/, '%%')
227
- fmt = input.gsub(/\{(\w+)\}/) do
226
+ fmt = input.gsub(/%/, '%%')
227
+ fmt = fmt.gsub(/\{(\w+)\}/) do
228
228
  Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
229
229
  end
230
230