howzit 1.2.15 → 1.2.18

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
@@ -673,7 +673,6 @@ module Howzit
673
673
  topics
674
674
  end
675
675
 
676
-
677
676
  def match_topic(search)
678
677
  matches = []
679
678
 
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
 
@@ -0,0 +1,128 @@
1
+ module Howzit
2
+ # Config Class
3
+ class Config
4
+ attr_reader :options
5
+
6
+ DEFAULTS = {
7
+ color: true,
8
+ config_editor: ENV['EDITOR'] || nil,
9
+ editor: ENV['EDITOR'] || nil,
10
+ header_format: 'border',
11
+ highlight: true,
12
+ highlighter: 'auto',
13
+ include_upstream: false,
14
+ log_level: 1, # 0: debug, 1: info, 2: warn, 3: error
15
+ matching: 'partial', # exact, partial, fuzzy, beginswith
16
+ multiple_matches: 'choose',
17
+ output_title: false,
18
+ pager: 'auto',
19
+ paginate: true,
20
+ show_all_code: false,
21
+ show_all_on_error: false,
22
+ wrap: 0
23
+ }.deep_freeze
24
+
25
+ def initialize
26
+ load_options
27
+ end
28
+
29
+ def write_config(config)
30
+ File.open(config_file, 'w') { |f| f.puts config.to_yaml }
31
+ end
32
+
33
+ def should_ignore(filename)
34
+ return false unless File.exist?(ignore_file)
35
+
36
+ @ignore_patterns ||= YAML.safe_load(IO.read(ignore_file))
37
+
38
+ ignore = false
39
+
40
+ @ignore_patterns.each do |pat|
41
+ if filename =~ /#{pat}/
42
+ ignore = true
43
+ break
44
+ end
45
+ end
46
+
47
+ ignore
48
+ end
49
+
50
+ def template_folder
51
+ File.join(config_dir, 'templates')
52
+ end
53
+
54
+ def editor
55
+ edit_config(DEFAULTS)
56
+ end
57
+
58
+ private
59
+
60
+ def load_options
61
+ Color.coloring = $stdout.isatty
62
+ flags = {
63
+ choose: false,
64
+ default: false,
65
+ grep: nil,
66
+ list_runnable: false,
67
+ list_runnable_titles: false,
68
+ list_topic_titles: false,
69
+ list_topics: false,
70
+ quiet: false,
71
+ run: false,
72
+ title_only: false,
73
+ verbose: false
74
+ }
75
+
76
+ config = load_config
77
+ @options = flags.merge(config)
78
+ end
79
+
80
+ def config_dir
81
+ File.expand_path(CONFIG_DIR)
82
+ end
83
+
84
+ def config_file
85
+ File.join(config_dir, CONFIG_FILE)
86
+ end
87
+
88
+ def ignore_file
89
+ File.join(config_dir, IGNORE_FILE)
90
+ end
91
+
92
+ def create_config(d)
93
+ unless File.directory?(config_dir)
94
+ warn "Creating config directory at #{config_dir}"
95
+ FileUtils.mkdir_p(config_dir)
96
+ end
97
+
98
+ unless File.exist?(config_file)
99
+ warn "Writing fresh config file to #{config_file}"
100
+ write_config(d)
101
+ end
102
+ config_file
103
+ end
104
+
105
+ def load_config
106
+ file = create_config(DEFAULTS)
107
+ config = YAML.load(IO.read(file))
108
+ newconfig = config ? DEFAULTS.merge(config) : DEFAULTS
109
+ write_config(newconfig)
110
+ newconfig.dup
111
+ end
112
+
113
+ def edit_config(d)
114
+ editor = Howzit.options.fetch(:config_editor, ENV['EDITOR'])
115
+
116
+ raise 'No config_editor defined' if editor.nil?
117
+
118
+ # raise "Invalid editor (#{editor})" unless Util.valid_command?(editor)
119
+
120
+ load_config
121
+ if Util.valid_command?(editor.split(/ /).first)
122
+ system %(#{editor} "#{config_file}")
123
+ else
124
+ `open -a "#{editor}" "#{config_file}"`
125
+ end
126
+ end
127
+ end
128
+ end