howzit 2.1.18 → 2.1.22
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/CHANGELOG.md +81 -0
- data/bin/howzit +2 -2
- data/howzit.gemspec +3 -1
- data/lib/howzit/buildnote.rb +345 -104
- data/lib/howzit/prompt.rb +207 -7
- data/lib/howzit/run_report.rb +51 -0
- data/lib/howzit/stringutils.rb +3 -1
- data/lib/howzit/task.rb +21 -2
- data/lib/howzit/topic.rb +24 -1
- data/lib/howzit/util.rb +5 -1
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +11 -2
- data/spec/buildnote_spec.rb +161 -3
- data/spec/cli_spec.rb +1 -1
- data/spec/run_report_spec.rb +35 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/topic_spec.rb +5 -2
- metadata +6 -15
- data/.editorconfig +0 -9
- data/.github/FUNDING.yml +0 -2
- data/.gitignore +0 -45
- data/.howzit.taskpaper.bak +0 -27
- data/.irbrc +0 -12
- data/.rspec +0 -2
- data/.rubocop.yml +0 -48
- data/.travis.yml +0 -17
- data/.yardopts +0 -6
- data/lib/.rubocop.yml +0 -1
- data/spec/.rubocop.yml +0 -4
data/lib/howzit/buildnote.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Howzit
|
|
|
57
57
|
## @param template [String] The template title
|
|
58
58
|
##
|
|
59
59
|
def edit_template(template)
|
|
60
|
-
file = template.sub(/(\.md)?$/i,
|
|
60
|
+
file = template.sub(/(\.md)?$/i, '.md')
|
|
61
61
|
file = File.join(Howzit.config.template_folder, file)
|
|
62
62
|
edit_template_file(file)
|
|
63
63
|
end
|
|
@@ -70,9 +70,39 @@ module Howzit
|
|
|
70
70
|
def find_topic(term = nil)
|
|
71
71
|
return @topics if term.nil?
|
|
72
72
|
|
|
73
|
+
rx = term.to_rx
|
|
74
|
+
|
|
75
|
+
@topics.filter do |topic|
|
|
76
|
+
title = topic.title.downcase.sub(/ *\(.*?\) *$/, '')
|
|
77
|
+
match = title =~ rx
|
|
78
|
+
|
|
79
|
+
if !match && term =~ /[,:]/
|
|
80
|
+
normalized = title.gsub(/\s*([,:])\s*/, '\1')
|
|
81
|
+
match = normalized =~ rx
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
match
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
## Find a topic with an exact whole-word match
|
|
90
|
+
##
|
|
91
|
+
## @param term [String] The search term
|
|
92
|
+
##
|
|
93
|
+
## @return [Array] Array of topics that exactly match the term
|
|
94
|
+
##
|
|
95
|
+
def find_topic_exact(term = nil)
|
|
96
|
+
return [] if term.nil?
|
|
97
|
+
|
|
73
98
|
@topics.filter do |topic|
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
title = topic.title.downcase.sub(/ *\(.*?\) *$/, '').strip
|
|
100
|
+
# Split both the title and search term into words
|
|
101
|
+
title_words = title.split
|
|
102
|
+
search_words = term.split
|
|
103
|
+
|
|
104
|
+
# Check if all search words match the title words exactly (case-insensitive)
|
|
105
|
+
search_words.map(&:downcase) == title_words.map(&:downcase)
|
|
76
106
|
end
|
|
77
107
|
end
|
|
78
108
|
|
|
@@ -84,7 +114,7 @@ module Howzit
|
|
|
84
114
|
title = "#{title} project notes"
|
|
85
115
|
url = "[#{title}](file://#{note_file})"
|
|
86
116
|
Util.os_copy(url)
|
|
87
|
-
Howzit.console.info(
|
|
117
|
+
Howzit.console.info('Link copied to clipboard.')
|
|
88
118
|
end
|
|
89
119
|
|
|
90
120
|
##
|
|
@@ -117,9 +147,7 @@ module Howzit
|
|
|
117
147
|
def list_topics
|
|
118
148
|
@topics.map do |topic|
|
|
119
149
|
title = topic.title
|
|
120
|
-
unless topic.named_args.empty?
|
|
121
|
-
title += "(#{topic.named_args.keys.join(", ")})"
|
|
122
|
-
end
|
|
150
|
+
title += "(#{topic.named_args.keys.join(', ')})" unless topic.named_args.empty?
|
|
123
151
|
title
|
|
124
152
|
end
|
|
125
153
|
end
|
|
@@ -143,13 +171,11 @@ module Howzit
|
|
|
143
171
|
def list_runnable_completions
|
|
144
172
|
output = []
|
|
145
173
|
@topics.each do |topic|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
output.push(title)
|
|
152
|
-
end
|
|
174
|
+
next unless topic.tasks.count.positive?
|
|
175
|
+
|
|
176
|
+
title = topic.title
|
|
177
|
+
title += "(#{topic.named_args.keys.join(', ')})" unless topic.named_args.empty?
|
|
178
|
+
output.push(title)
|
|
153
179
|
end
|
|
154
180
|
output.join("\n")
|
|
155
181
|
end
|
|
@@ -172,9 +198,7 @@ module Howzit
|
|
|
172
198
|
next if s_out.empty?
|
|
173
199
|
|
|
174
200
|
title = topic.title
|
|
175
|
-
unless topic.named_args.empty?
|
|
176
|
-
title += " {dy}({xy}#{topic.named_args.keys.join(", ")}{dy}){x}"
|
|
177
|
-
end
|
|
201
|
+
title += " {dy}({xy}#{topic.named_args.keys.join(', ')}{dy}){x}" unless topic.named_args.empty?
|
|
178
202
|
|
|
179
203
|
output.push("- {g}#{title}{x}".c)
|
|
180
204
|
output.push(s_out.join("\n"))
|
|
@@ -199,7 +223,7 @@ module Howzit
|
|
|
199
223
|
## @param prompt [Boolean] confirm file creation?
|
|
200
224
|
##
|
|
201
225
|
def create_template_file(file, prompt: false)
|
|
202
|
-
trap(
|
|
226
|
+
trap('SIGINT') do
|
|
203
227
|
Howzit.console.info "\nCancelled"
|
|
204
228
|
exit!
|
|
205
229
|
end
|
|
@@ -211,7 +235,7 @@ module Howzit
|
|
|
211
235
|
Process.exit 0 unless res
|
|
212
236
|
end
|
|
213
237
|
|
|
214
|
-
title = File.basename(file,
|
|
238
|
+
title = File.basename(file, '.md')
|
|
215
239
|
|
|
216
240
|
note = <<~EOBUILDNOTES
|
|
217
241
|
# #{title}
|
|
@@ -223,12 +247,12 @@ module Howzit
|
|
|
223
247
|
if File.exist?(file) && !default
|
|
224
248
|
file = "{by}#{file}".c
|
|
225
249
|
unless Prompt.yn("Are you sure you want to overwrite #{file}", default: false)
|
|
226
|
-
Howzit.console.info(
|
|
250
|
+
Howzit.console.info('Cancelled')
|
|
227
251
|
Process.exit 0
|
|
228
252
|
end
|
|
229
253
|
end
|
|
230
254
|
|
|
231
|
-
File.open(file,
|
|
255
|
+
File.open(file, 'w') do |f|
|
|
232
256
|
f.puts note
|
|
233
257
|
Howzit.console.info("{by}Template {bw}#{title}{by} written to {bw}#{file}{x}".c)
|
|
234
258
|
end
|
|
@@ -243,7 +267,7 @@ module Howzit
|
|
|
243
267
|
|
|
244
268
|
# Create a buildnotes skeleton
|
|
245
269
|
def create_note(prompt: false)
|
|
246
|
-
trap(
|
|
270
|
+
trap('SIGINT') do
|
|
247
271
|
Howzit.console.info "\nCancelled"
|
|
248
272
|
exit!
|
|
249
273
|
end
|
|
@@ -251,7 +275,7 @@ module Howzit
|
|
|
251
275
|
default = !$stdout.isatty || Howzit.options[:default]
|
|
252
276
|
|
|
253
277
|
if prompt && !default
|
|
254
|
-
res = Prompt.yn(
|
|
278
|
+
res = Prompt.yn('No build notes file found, create one?', default: true)
|
|
255
279
|
Process.exit 0 unless res
|
|
256
280
|
end
|
|
257
281
|
|
|
@@ -269,27 +293,37 @@ module Howzit
|
|
|
269
293
|
if default
|
|
270
294
|
input = title
|
|
271
295
|
else
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
296
|
+
title = Prompt.get_line('{bw}Project name{x}'.c, default: title)
|
|
297
|
+
end
|
|
298
|
+
summary = ''
|
|
299
|
+
unless default
|
|
300
|
+
summary = Prompt.get_line('{bw}Project summary{x}'.c)
|
|
276
301
|
end
|
|
277
|
-
|
|
302
|
+
|
|
303
|
+
# Template selection
|
|
304
|
+
selected_templates = []
|
|
305
|
+
template_metadata = {}
|
|
278
306
|
unless default
|
|
279
|
-
|
|
280
|
-
input = $stdin.gets.chomp
|
|
281
|
-
summary = input unless input.empty?
|
|
307
|
+
selected_templates, template_metadata = select_templates_for_note(title)
|
|
282
308
|
end
|
|
283
309
|
|
|
284
|
-
fname =
|
|
310
|
+
fname = 'buildnotes.md'
|
|
285
311
|
unless default
|
|
286
|
-
|
|
287
|
-
input = $stdin.gets.chomp
|
|
288
|
-
fname = input unless input.empty?
|
|
312
|
+
fname = Prompt.get_line("{bw}Build notes filename{x}\n(must begin with 'howzit' or 'build')".c, default: fname)
|
|
289
313
|
end
|
|
290
314
|
|
|
315
|
+
# Build metadata section
|
|
316
|
+
metadata_lines = []
|
|
317
|
+
unless selected_templates.empty?
|
|
318
|
+
metadata_lines << "template: #{selected_templates.join(',')}"
|
|
319
|
+
end
|
|
320
|
+
template_metadata.each do |key, value|
|
|
321
|
+
metadata_lines << "#{key}: #{value}"
|
|
322
|
+
end
|
|
323
|
+
metadata_section = metadata_lines.empty? ? '' : "#{metadata_lines.join("\n")}\n\n"
|
|
324
|
+
|
|
291
325
|
note = <<~EOBUILDNOTES
|
|
292
|
-
# #{title}
|
|
326
|
+
#{metadata_section}# #{title}
|
|
293
327
|
|
|
294
328
|
#{summary}
|
|
295
329
|
|
|
@@ -316,12 +350,12 @@ module Howzit
|
|
|
316
350
|
if File.exist?(fname) && !default
|
|
317
351
|
file = "{by}#{fname}".c
|
|
318
352
|
unless Prompt.yn("Are you absolutely sure you want to overwrite #{file}", default: false)
|
|
319
|
-
Howzit.console.info(
|
|
353
|
+
Howzit.console.info('Canceled')
|
|
320
354
|
Process.exit 0
|
|
321
355
|
end
|
|
322
356
|
end
|
|
323
357
|
|
|
324
|
-
File.open(fname,
|
|
358
|
+
File.open(fname, 'w') do |f|
|
|
325
359
|
f.puts note
|
|
326
360
|
Howzit.console.info("{by}Build notes for {bw}#{title}{by} written to {bw}#{fname}{x}".c)
|
|
327
361
|
end
|
|
@@ -345,6 +379,132 @@ module Howzit
|
|
|
345
379
|
|
|
346
380
|
private
|
|
347
381
|
|
|
382
|
+
##
|
|
383
|
+
## Select templates for a new build note
|
|
384
|
+
##
|
|
385
|
+
## @param project_title [String] The project title for prompts
|
|
386
|
+
##
|
|
387
|
+
## @return [Array<Array, Hash>] Array of [selected_template_names, required_vars_hash]
|
|
388
|
+
##
|
|
389
|
+
def select_templates_for_note(project_title)
|
|
390
|
+
template_dir = Howzit.config.template_folder
|
|
391
|
+
template_glob = File.join(template_dir, '*.md')
|
|
392
|
+
template_files = Dir.glob(template_glob)
|
|
393
|
+
|
|
394
|
+
return [[], {}] if template_files.empty?
|
|
395
|
+
|
|
396
|
+
# Get basenames without extension for menu
|
|
397
|
+
template_names = template_files.map { |f| File.basename(f, '.md') }.sort
|
|
398
|
+
|
|
399
|
+
# Show multi-select menu
|
|
400
|
+
selected = Prompt.choose_templates(template_names, prompt_text: 'Select templates to include')
|
|
401
|
+
return [[], {}] if selected.empty?
|
|
402
|
+
|
|
403
|
+
# Prompt for required variables from each template
|
|
404
|
+
required_vars = {}
|
|
405
|
+
selected.each do |template_name|
|
|
406
|
+
template_path = File.join(template_dir, "#{template_name}.md")
|
|
407
|
+
next unless File.exist?(template_path)
|
|
408
|
+
|
|
409
|
+
vars = parse_template_required_vars(template_path)
|
|
410
|
+
vars.each do |var|
|
|
411
|
+
next if required_vars.key?(var)
|
|
412
|
+
|
|
413
|
+
value = Prompt.get_line("{bw}[#{template_name}] requires {by}#{var}{x}".c)
|
|
414
|
+
required_vars[var] = value unless value.empty?
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
[selected, required_vars]
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
##
|
|
422
|
+
## Parse a template file for required variables
|
|
423
|
+
##
|
|
424
|
+
## @param template_path [String] Path to the template file
|
|
425
|
+
##
|
|
426
|
+
## @return [Array] Array of required variable names
|
|
427
|
+
##
|
|
428
|
+
def parse_template_required_vars(template_path)
|
|
429
|
+
content = File.read(template_path)
|
|
430
|
+
|
|
431
|
+
# Look for required: in the metadata at the top of the file
|
|
432
|
+
# Metadata is before the first # heading
|
|
433
|
+
meta_section = content.split(/^#/)[0]
|
|
434
|
+
return [] if meta_section.nil? || meta_section.strip.empty?
|
|
435
|
+
|
|
436
|
+
# Find the required: line
|
|
437
|
+
match = meta_section.match(/^required:\s*(.+)$/i)
|
|
438
|
+
return [] unless match
|
|
439
|
+
|
|
440
|
+
# Split by comma and strip whitespace
|
|
441
|
+
match[1].split(',').map(&:strip).reject(&:empty?)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def topic_search_terms_from_cli
|
|
445
|
+
args = Howzit.cli_args || []
|
|
446
|
+
raw = args.join(' ').strip
|
|
447
|
+
return [] if raw.empty?
|
|
448
|
+
|
|
449
|
+
smart_split_topics(raw).map { |term| term.strip.downcase }.reject(&:empty?)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def smart_split_topics(raw)
|
|
453
|
+
segments, separators = segments_and_separators_for(raw)
|
|
454
|
+
return segments if separators.empty?
|
|
455
|
+
|
|
456
|
+
combined = []
|
|
457
|
+
current = segments.shift || ''
|
|
458
|
+
|
|
459
|
+
separators.each_with_index do |separator, idx|
|
|
460
|
+
next_segment = segments[idx] || ''
|
|
461
|
+
if keep_separator_with_current?(current, separator, next_segment)
|
|
462
|
+
current = "#{current}#{separator}#{next_segment}"
|
|
463
|
+
else
|
|
464
|
+
combined << current
|
|
465
|
+
current = next_segment
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
combined << current
|
|
470
|
+
combined
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def segments_and_separators_for(raw)
|
|
474
|
+
segments = []
|
|
475
|
+
separators = []
|
|
476
|
+
current = String.new
|
|
477
|
+
|
|
478
|
+
raw.each_char do |char|
|
|
479
|
+
if char =~ /[,:]/
|
|
480
|
+
segments << current
|
|
481
|
+
separators << char
|
|
482
|
+
current = String.new
|
|
483
|
+
else
|
|
484
|
+
current << char
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
segments << current
|
|
489
|
+
[segments, separators]
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def keep_separator_with_current?(current, separator, next_segment)
|
|
493
|
+
candidate = "#{current}#{separator}#{next_segment}"
|
|
494
|
+
normalized_candidate = normalize_separator_string(candidate)
|
|
495
|
+
return false if normalized_candidate.empty?
|
|
496
|
+
|
|
497
|
+
@topics.any? do |topic|
|
|
498
|
+
normalize_separator_string(topic.title).start_with?(normalized_candidate)
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def normalize_separator_string(string)
|
|
503
|
+
return '' if string.nil?
|
|
504
|
+
|
|
505
|
+
string.downcase.gsub(/\s+/, ' ').strip.gsub(/\s*([,:])\s*/, '\1')
|
|
506
|
+
end
|
|
507
|
+
|
|
348
508
|
##
|
|
349
509
|
## Import the contents of a filename as new topics
|
|
350
510
|
##
|
|
@@ -357,15 +517,15 @@ module Howzit
|
|
|
357
517
|
return mtch[0] unless File.exist?(file)
|
|
358
518
|
|
|
359
519
|
content = Util.read_file(file)
|
|
360
|
-
home = ENV[
|
|
361
|
-
short_path = File.dirname(file.sub(/^#{home}/,
|
|
520
|
+
home = ENV['HOME']
|
|
521
|
+
short_path = File.dirname(file.sub(/^#{home}/, '~'))
|
|
362
522
|
prefix = "#{short_path}/#{File.basename(file)}:"
|
|
363
523
|
parts = content.split(/^##+/)
|
|
364
524
|
parts.shift
|
|
365
525
|
if parts.empty?
|
|
366
526
|
content
|
|
367
527
|
else
|
|
368
|
-
"## #{parts.join(
|
|
528
|
+
"## #{parts.join('## ')}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}")
|
|
369
529
|
end
|
|
370
530
|
end
|
|
371
531
|
|
|
@@ -382,15 +542,15 @@ module Howzit
|
|
|
382
542
|
|
|
383
543
|
t_meta = t_leader.metadata
|
|
384
544
|
|
|
385
|
-
return unless t_meta.key?(
|
|
545
|
+
return unless t_meta.key?('required')
|
|
386
546
|
|
|
387
|
-
required = t_meta[
|
|
547
|
+
required = t_meta['required'].strip.split(/\s*,\s*/)
|
|
388
548
|
required.each do |req|
|
|
389
549
|
next if @metadata.keys.include?(req.downcase)
|
|
390
550
|
|
|
391
551
|
Howzit.console.error %({bRw}ERROR:{xbr} Missing required metadata key from template '{bw}#{File.basename(
|
|
392
|
-
|
|
393
|
-
|
|
552
|
+
template, '.md'
|
|
553
|
+
)}{xr}'{x}).c
|
|
394
554
|
Howzit.console.error %({br}Please define {by}#{req.downcase}{xr} in build notes{x}).c
|
|
395
555
|
Process.exit 1
|
|
396
556
|
end
|
|
@@ -408,8 +568,8 @@ module Howzit
|
|
|
408
568
|
subtopics = nil
|
|
409
569
|
|
|
410
570
|
if template =~ /\[(.*?)\]$/
|
|
411
|
-
subtopics = Regexp.last_match[1].split(/\s*\|\s*/).map { |t| t.gsub(/\*/,
|
|
412
|
-
template.sub!(/\[.*?\]$/,
|
|
571
|
+
subtopics = Regexp.last_match[1].split(/\s*\|\s*/).map { |t| t.gsub(/\*/, '.*?') }
|
|
572
|
+
template.sub!(/\[.*?\]$/, '').strip
|
|
413
573
|
end
|
|
414
574
|
|
|
415
575
|
[template, subtopics]
|
|
@@ -429,7 +589,7 @@ module Howzit
|
|
|
429
589
|
templates.each do |template|
|
|
430
590
|
template, subtopics = detect_subtopics(template)
|
|
431
591
|
|
|
432
|
-
file = template.sub(/(\.md)?$/i,
|
|
592
|
+
file = template.sub(/(\.md)?$/i, '.md')
|
|
433
593
|
file = File.join(Howzit.config.template_folder, file)
|
|
434
594
|
|
|
435
595
|
next unless File.exist?(file)
|
|
@@ -493,7 +653,7 @@ module Howzit
|
|
|
493
653
|
buildnotes = []
|
|
494
654
|
filename = nil
|
|
495
655
|
|
|
496
|
-
while dir !=
|
|
656
|
+
while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
|
|
497
657
|
Dir.chdir(dir)
|
|
498
658
|
filename = glob_note
|
|
499
659
|
unless filename.nil?
|
|
@@ -516,7 +676,7 @@ module Howzit
|
|
|
516
676
|
## @return [String] file path
|
|
517
677
|
##
|
|
518
678
|
def glob_note
|
|
519
|
-
Dir.glob(
|
|
679
|
+
Dir.glob('*.{txt,md,markdown}').select(&:build_note?).sort[0]
|
|
520
680
|
end
|
|
521
681
|
|
|
522
682
|
##
|
|
@@ -529,9 +689,9 @@ module Howzit
|
|
|
529
689
|
def find_note_file
|
|
530
690
|
filename = glob_note
|
|
531
691
|
|
|
532
|
-
if filename.nil? &&
|
|
692
|
+
if filename.nil? && 'git'.available?
|
|
533
693
|
proj_dir = `git rev-parse --show-toplevel 2>/dev/null`.strip
|
|
534
|
-
unless proj_dir ==
|
|
694
|
+
unless proj_dir == ''
|
|
535
695
|
Dir.chdir(proj_dir)
|
|
536
696
|
filename = glob_note
|
|
537
697
|
end
|
|
@@ -575,8 +735,8 @@ module Howzit
|
|
|
575
735
|
|
|
576
736
|
data = leader.metadata
|
|
577
737
|
|
|
578
|
-
if data.key?(
|
|
579
|
-
templates = data[
|
|
738
|
+
if data.key?('template')
|
|
739
|
+
templates = data['template'].strip.split(/\s*,\s*/)
|
|
580
740
|
|
|
581
741
|
template_topics.concat(gather_templates(templates))
|
|
582
742
|
end
|
|
@@ -618,13 +778,13 @@ module Howzit
|
|
|
618
778
|
|
|
619
779
|
lines = sect.split(/\n/)
|
|
620
780
|
title = lines.slice!(0).strip
|
|
621
|
-
prefix =
|
|
781
|
+
prefix = ''
|
|
622
782
|
if path && path != note_file
|
|
623
783
|
if path =~ /#{Howzit.config.template_folder}/
|
|
624
|
-
short_path = File.basename(path,
|
|
784
|
+
short_path = File.basename(path, '.md')
|
|
625
785
|
else
|
|
626
|
-
home = ENV[
|
|
627
|
-
short_path = File.dirname(path.sub(/^#{home}/,
|
|
786
|
+
home = ENV['HOME']
|
|
787
|
+
short_path = File.dirname(path.sub(/^#{home}/, '~'))
|
|
628
788
|
prefix = "_from #{short_path}_\n\n"
|
|
629
789
|
end
|
|
630
790
|
title = "#{short_path}:#{title}"
|
|
@@ -636,7 +796,7 @@ module Howzit
|
|
|
636
796
|
end
|
|
637
797
|
|
|
638
798
|
template_topics.each do |topic|
|
|
639
|
-
topics.push(topic) unless find_topic(topic.title.sub(/^.+:/,
|
|
799
|
+
topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
|
|
640
800
|
end
|
|
641
801
|
|
|
642
802
|
topics
|
|
@@ -655,7 +815,7 @@ module Howzit
|
|
|
655
815
|
upstream_topics = read_upstream
|
|
656
816
|
|
|
657
817
|
upstream_topics.each do |topic|
|
|
658
|
-
@topics.push(topic) unless find_topic(topic.title.sub(/^.+:/,
|
|
818
|
+
@topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
|
|
659
819
|
end
|
|
660
820
|
Howzit.has_read_upstream = true
|
|
661
821
|
end
|
|
@@ -670,11 +830,11 @@ module Howzit
|
|
|
670
830
|
## Open build note in editor
|
|
671
831
|
##
|
|
672
832
|
def edit_note
|
|
673
|
-
editor = Howzit.options.fetch(:editor, ENV[
|
|
833
|
+
editor = Howzit.options.fetch(:editor, ENV['EDITOR'])
|
|
674
834
|
|
|
675
835
|
editor = Howzit.config.update_editor if editor.nil?
|
|
676
836
|
|
|
677
|
-
raise
|
|
837
|
+
raise 'No editor defined' if editor.nil?
|
|
678
838
|
|
|
679
839
|
raise "Invalid editor (#{editor})" unless Util.valid_command?(editor)
|
|
680
840
|
|
|
@@ -688,7 +848,7 @@ module Howzit
|
|
|
688
848
|
## @param template [String] The template name
|
|
689
849
|
##
|
|
690
850
|
def create_template(template)
|
|
691
|
-
file = template.sub(/(\.md)?$/i,
|
|
851
|
+
file = template.sub(/(\.md)?$/i, '.md')
|
|
692
852
|
file = File.join(Howzit.config.template_folder, file)
|
|
693
853
|
create_template_file(file, prompt: false)
|
|
694
854
|
end
|
|
@@ -697,11 +857,11 @@ module Howzit
|
|
|
697
857
|
## Open template in editor
|
|
698
858
|
##
|
|
699
859
|
def edit_template_file(file)
|
|
700
|
-
editor = Howzit.options.fetch(:editor, ENV[
|
|
860
|
+
editor = Howzit.options.fetch(:editor, ENV['EDITOR'])
|
|
701
861
|
|
|
702
862
|
editor = Howzit.config.update_editor if editor.nil?
|
|
703
863
|
|
|
704
|
-
raise
|
|
864
|
+
raise 'No editor defined' if editor.nil?
|
|
705
865
|
|
|
706
866
|
raise "Invalid editor (#{editor})" unless Util.valid_command?(editor)
|
|
707
867
|
|
|
@@ -722,12 +882,12 @@ module Howzit
|
|
|
722
882
|
new_topic = topic.is_a?(String) ? find_topic(topic)[0] : topic.dup
|
|
723
883
|
|
|
724
884
|
output = if run
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
885
|
+
new_topic.run
|
|
886
|
+
else
|
|
887
|
+
new_topic.print_out({ single: single })
|
|
888
|
+
end
|
|
729
889
|
|
|
730
|
-
output.nil? ?
|
|
890
|
+
output.nil? ? '' : output.join("\n\n")
|
|
731
891
|
end
|
|
732
892
|
|
|
733
893
|
##
|
|
@@ -735,6 +895,10 @@ module Howzit
|
|
|
735
895
|
##
|
|
736
896
|
def process
|
|
737
897
|
output = []
|
|
898
|
+
if Howzit.options[:run]
|
|
899
|
+
Howzit.run_log = []
|
|
900
|
+
Howzit.multi_topic_run = false
|
|
901
|
+
end
|
|
738
902
|
|
|
739
903
|
unless note_file
|
|
740
904
|
Process.exit 0 if Howzit.options[:list_runnable_titles] || Howzit.options[:list_topic_titles]
|
|
@@ -748,7 +912,7 @@ module Howzit
|
|
|
748
912
|
Process.exit(0)
|
|
749
913
|
elsif Howzit.options[:output_title] && !Howzit.options[:run]
|
|
750
914
|
if @title && !@title.empty?
|
|
751
|
-
header = @title.format_header({ hr: "\u{2550}", color:
|
|
915
|
+
header = @title.format_header({ hr: "\u{2550}", color: '{bwK}' })
|
|
752
916
|
output.push("#{header}\n")
|
|
753
917
|
end
|
|
754
918
|
end
|
|
@@ -774,62 +938,139 @@ module Howzit
|
|
|
774
938
|
Process.exit(0)
|
|
775
939
|
end
|
|
776
940
|
|
|
777
|
-
|
|
778
|
-
|
|
941
|
+
# Handle grep and choose modes (batch all results)
|
|
779
942
|
if Howzit.options[:grep]
|
|
943
|
+
topic_matches = []
|
|
780
944
|
matches = grep(Howzit.options[:grep])
|
|
781
945
|
case Howzit.options[:multiple_matches]
|
|
782
946
|
when :all
|
|
783
947
|
topic_matches.concat(matches.sort_by(&:title))
|
|
784
948
|
else
|
|
785
|
-
topic_matches.concat(Prompt.choose(matches.map(&:title), height: :max))
|
|
949
|
+
topic_matches.concat(Prompt.choose(matches.map(&:title), height: :max, query: Howzit.options[:grep]))
|
|
786
950
|
end
|
|
951
|
+
process_topic_matches(topic_matches, output)
|
|
787
952
|
elsif Howzit.options[:choose]
|
|
953
|
+
topic_matches = []
|
|
788
954
|
titles = Prompt.choose(list_topics, height: :max)
|
|
789
955
|
titles.each { |title| topic_matches.push(find_topic(title)[0]) }
|
|
790
|
-
|
|
956
|
+
process_topic_matches(topic_matches, output)
|
|
791
957
|
elsif !Howzit.cli_args.empty?
|
|
792
|
-
|
|
958
|
+
# Collect all topic matches first (showing menus as needed)
|
|
959
|
+
search = topic_search_terms_from_cli
|
|
960
|
+
topic_matches = collect_topic_matches(search, output)
|
|
961
|
+
process_topic_matches(topic_matches, output)
|
|
962
|
+
else
|
|
963
|
+
# No arguments - show all topics
|
|
964
|
+
if Howzit.options[:run]
|
|
965
|
+
Howzit.run_log = []
|
|
966
|
+
Howzit.multi_topic_run = topics.length > 1
|
|
967
|
+
end
|
|
968
|
+
topics.each { |k| output.push(process_topic(k, false, single: false)) }
|
|
969
|
+
finalize_output(output)
|
|
970
|
+
end
|
|
971
|
+
end
|
|
793
972
|
|
|
794
|
-
|
|
795
|
-
|
|
973
|
+
##
|
|
974
|
+
## Collect all topic matches from search terms, showing menus as needed
|
|
975
|
+
## but not displaying/running until all selections are made
|
|
976
|
+
##
|
|
977
|
+
## @param search_terms [Array] Array of search term strings
|
|
978
|
+
## @param output [Array] Output array for error messages
|
|
979
|
+
##
|
|
980
|
+
## @return [Array] Array of all matched topics
|
|
981
|
+
##
|
|
982
|
+
def collect_topic_matches(search_terms, output)
|
|
983
|
+
all_matches = []
|
|
796
984
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
topic_matches.concat(matches)
|
|
807
|
-
else
|
|
808
|
-
titles = matches.map(&:title)
|
|
809
|
-
res = Prompt.choose(titles)
|
|
810
|
-
old_matching = Howzit.options[:matching]
|
|
811
|
-
Howzit.options[:matching] = "exact"
|
|
812
|
-
res.each { |title| topic_matches.concat(find_topic(title)) }
|
|
813
|
-
Howzit.options[:matching] = old_matching
|
|
814
|
-
end
|
|
815
|
-
end
|
|
816
|
-
end
|
|
985
|
+
search_terms.each do |s|
|
|
986
|
+
# First check for exact whole-word matches
|
|
987
|
+
exact_matches = find_topic_exact(s)
|
|
988
|
+
|
|
989
|
+
topic_matches = if !exact_matches.empty?
|
|
990
|
+
exact_matches
|
|
991
|
+
else
|
|
992
|
+
resolve_fuzzy_matches(s, output)
|
|
993
|
+
end
|
|
817
994
|
|
|
818
|
-
if topic_matches.empty?
|
|
819
|
-
|
|
820
|
-
|
|
995
|
+
if topic_matches.empty?
|
|
996
|
+
output.push(%({bR}ERROR:{xr} No topic match found for {bw}#{s}{x}\n).c)
|
|
997
|
+
else
|
|
998
|
+
all_matches.concat(topic_matches)
|
|
821
999
|
end
|
|
822
1000
|
end
|
|
823
1001
|
|
|
1002
|
+
all_matches
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
##
|
|
1006
|
+
## Resolve fuzzy matches for a search term
|
|
1007
|
+
##
|
|
1008
|
+
## @param search_term [String] The search term
|
|
1009
|
+
## @param output [Array] Output array for errors
|
|
1010
|
+
##
|
|
1011
|
+
## @return [Array] Array of matched topics
|
|
1012
|
+
##
|
|
1013
|
+
def resolve_fuzzy_matches(search_term, output)
|
|
1014
|
+
matches = find_topic(search_term)
|
|
1015
|
+
|
|
1016
|
+
return [] if matches.empty?
|
|
1017
|
+
|
|
1018
|
+
case Howzit.options[:multiple_matches]
|
|
1019
|
+
when :first
|
|
1020
|
+
[matches[0]]
|
|
1021
|
+
when :best
|
|
1022
|
+
[matches.sort_by { |a| [a.title.comp_distance(search_term), a.title.length] }.first]
|
|
1023
|
+
when :all
|
|
1024
|
+
matches
|
|
1025
|
+
else
|
|
1026
|
+
titles = matches.map(&:title)
|
|
1027
|
+
res = Prompt.choose(titles, query: search_term)
|
|
1028
|
+
old_matching = Howzit.options[:matching]
|
|
1029
|
+
Howzit.options[:matching] = 'exact'
|
|
1030
|
+
selected = res.flat_map { |title| find_topic(title) }
|
|
1031
|
+
Howzit.options[:matching] = old_matching
|
|
1032
|
+
selected
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
##
|
|
1037
|
+
## Process collected topic matches and display output
|
|
1038
|
+
##
|
|
1039
|
+
## @param topic_matches [Array] Array of matched topics
|
|
1040
|
+
## @param output [Array] Output array
|
|
1041
|
+
##
|
|
1042
|
+
def process_topic_matches(topic_matches, output)
|
|
1043
|
+
if topic_matches.empty? && !Howzit.options[:show_all_on_error]
|
|
1044
|
+
Util.show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
|
|
1045
|
+
Process.exit 1
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
if Howzit.options[:run]
|
|
1049
|
+
Howzit.run_log = []
|
|
1050
|
+
Howzit.multi_topic_run = topic_matches.length > 1
|
|
1051
|
+
end
|
|
1052
|
+
|
|
824
1053
|
if !topic_matches.empty?
|
|
825
|
-
# If we found a match
|
|
826
1054
|
topic_matches.map! { |topic| topic.is_a?(String) ? find_topic(topic)[0] : topic }
|
|
827
1055
|
topic_matches.each { |topic_match| output.push(process_topic(topic_match, Howzit.options[:run], single: true)) }
|
|
828
1056
|
else
|
|
829
|
-
# If there's no argument or no match found, output all
|
|
830
1057
|
topics.each { |k| output.push(process_topic(k, false, single: false)) }
|
|
831
1058
|
end
|
|
832
|
-
|
|
1059
|
+
|
|
1060
|
+
finalize_output(output)
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
##
|
|
1064
|
+
## Finalize and display output with run summary if applicable
|
|
1065
|
+
##
|
|
1066
|
+
## @param output [Array] Output array
|
|
1067
|
+
##
|
|
1068
|
+
def finalize_output(output)
|
|
1069
|
+
if Howzit.options[:run]
|
|
1070
|
+
Howzit.options[:paginate] = false
|
|
1071
|
+
summary = Howzit::RunReport.format
|
|
1072
|
+
output.push(summary) unless summary.empty?
|
|
1073
|
+
end
|
|
833
1074
|
Util.show(output.join("\n").strip, Howzit.options)
|
|
834
1075
|
end
|
|
835
1076
|
end
|