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.
@@ -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, ".md")
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
- rx = term.to_rx
75
- topic.title.downcase.sub(/ *\(.*?\) *$/, "") =~ rx
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("Link copied to clipboard.")
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
- if topic.tasks.count.positive?
147
- title = topic.title
148
- unless topic.named_args.empty?
149
- title += "(#{topic.named_args.keys.join(", ")})"
150
- end
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("SIGINT") do
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, ".md")
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("Cancelled")
250
+ Howzit.console.info('Cancelled')
227
251
  Process.exit 0
228
252
  end
229
253
  end
230
254
 
231
- File.open(file, "w") do |f|
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("SIGINT") do
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("No build notes file found, create one?", default: true)
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
- # title = prompt.ask("{bw}Project name:{x}".c, default: title)
273
- printf "{bw}Project name {xg}[#{title}]{bw}: {x}".c
274
- input = $stdin.gets.chomp
275
- title = input unless input.empty?
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
- summary = ""
302
+
303
+ # Template selection
304
+ selected_templates = []
305
+ template_metadata = {}
278
306
  unless default
279
- printf "{bw}Project summary: {x}".c
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 = "buildnotes.md"
310
+ fname = 'buildnotes.md'
285
311
  unless default
286
- printf "{bw}Build notes filename (must begin with 'howzit' or 'build')\n{xg}[#{fname}]{bw}: {x}".c
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("Canceled")
353
+ Howzit.console.info('Canceled')
320
354
  Process.exit 0
321
355
  end
322
356
  end
323
357
 
324
- File.open(fname, "w") do |f|
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["HOME"]
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("## ")}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}")
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?("required")
545
+ return unless t_meta.key?('required')
386
546
 
387
- required = t_meta["required"].strip.split(/\s*,\s*/)
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
- template, ".md"
393
- )}{xr}'{x}).c
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!(/\[.*?\]$/, "").strip
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, ".md")
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 != "/" && (dir =~ %r{[A-Z]:/}).nil?
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("*.{txt,md,markdown}").select(&:build_note?).sort[0]
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? && "git".available?
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?("template")
579
- templates = data["template"].strip.split(/\s*,\s*/)
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, ".md")
784
+ short_path = File.basename(path, '.md')
625
785
  else
626
- home = ENV["HOME"]
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(/^.+:/, "")).count.positive?
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(/^.+:/, "")).count.positive?
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["EDITOR"])
833
+ editor = Howzit.options.fetch(:editor, ENV['EDITOR'])
674
834
 
675
835
  editor = Howzit.config.update_editor if editor.nil?
676
836
 
677
- raise "No editor defined" if editor.nil?
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, ".md")
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["EDITOR"])
860
+ editor = Howzit.options.fetch(:editor, ENV['EDITOR'])
701
861
 
702
862
  editor = Howzit.config.update_editor if editor.nil?
703
863
 
704
- raise "No editor defined" if editor.nil?
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
- new_topic.run
726
- else
727
- new_topic.print_out({ single: single })
728
- end
885
+ new_topic.run
886
+ else
887
+ new_topic.print_out({ single: single })
888
+ end
729
889
 
730
- output.nil? ? "" : output.join("\n\n")
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: "{bwK}" })
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
- topic_matches = []
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
- # If there are arguments use those to search for a matching topic
956
+ process_topic_matches(topic_matches, output)
791
957
  elsif !Howzit.cli_args.empty?
792
- search = Howzit.cli_args.join(" ").strip.downcase.split(/ *, */).map(&:strip)
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
- search.each do |s|
795
- matches = find_topic(s)
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
- if matches.empty?
798
- output.push(%({bR}ERROR:{xr} No topic match found for {bw}#{s}{x}\n).c)
799
- else
800
- case Howzit.options[:multiple_matches]
801
- when :first
802
- topic_matches.push(matches[0])
803
- when :best
804
- topic_matches.push(matches.sort_by { |a| [a.title.comp_distance(s), a.title.length] })
805
- when :all
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? && !Howzit.options[:show_all_on_error]
819
- Util.show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
820
- Process.exit 1
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
- Howzit.options[:paginate] = false if Howzit.options[:run]
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