markdown_exec 2.0.5 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -200,7 +200,7 @@ module HashDelegatorSelf
200
200
 
201
201
  # Determine the state of breaker based on was_using_cli and the block type
202
202
  # true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block[:shell] equals BlockType::BASH. In all other scenarios, breaker is false.
203
- breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH
203
+ breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
204
204
 
205
205
  # Reset block_name_from_cli if the conditions are not met
206
206
  block_name_from_cli ||= false
@@ -252,14 +252,16 @@ module HashDelegatorSelf
252
252
 
253
253
  # # Sanitize input (example: removing potentially harmful characters)
254
254
  # str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
255
-
256
255
  # Evaluate the sanitized string
257
256
  result = nil
258
257
  binding.eval("result = #{str}")
259
258
 
260
259
  result
261
260
  rescue StandardError # catches NameError, StandardError
261
+ pp $!, $@
262
+ pp "code: #{str}"
262
263
  error_handler('safeval')
264
+ exit 1
263
265
  end
264
266
 
265
267
  # # Evaluates the given string as Ruby code and rescues any StandardErrors.
@@ -311,11 +313,11 @@ module HashDelegatorSelf
311
313
  File.write(
312
314
  filespec,
313
315
  ["-STDOUT-\n",
314
- format_execution_streams(ExecutionStreams::StdOut, files),
316
+ format_execution_streams(ExecutionStreams::STD_OUT, files),
315
317
  "-STDERR-\n",
316
- format_execution_streams(ExecutionStreams::StdErr, files),
318
+ format_execution_streams(ExecutionStreams::STD_ERR, files),
317
319
  "-STDIN-\n",
318
- format_execution_streams(ExecutionStreams::StdIn, files),
320
+ format_execution_streams(ExecutionStreams::STD_IN, files),
319
321
  "\n"].join
320
322
  )
321
323
  end
@@ -387,7 +389,7 @@ module PathUtils
387
389
  # @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
388
390
  # @return [String] The absolute path or the expression with the wildcard replaced by the path.
389
391
  def self.resolve_path_or_substitute(path, expression)
390
- if path.include?('/')
392
+ if path.start_with?('/')
391
393
  path
392
394
  else
393
395
  expression.gsub('*', path)
@@ -412,6 +414,74 @@ class BashCommentFormatter
412
414
  # end
413
415
  end
414
416
 
417
+ class StringWrapper
418
+ attr_reader :width, :left_margin, :right_margin, :indent, :fill_margin
419
+
420
+ # Initializes the StringWrapper with the given options.
421
+ #
422
+ # @param width [Integer] the maximum width of each line
423
+ # @param left_margin [Integer] the number of spaces for the left margin
424
+ # @param right_margin [Integer] the number of spaces for the right margin
425
+ # @param indent [Integer] the number of spaces to indent all but the first line
426
+ # @param fill_margin [Boolean] whether to fill the left margin with spaces
427
+ def initialize(
428
+ width:,
429
+ fill_margin: false,
430
+ first_indent: '',
431
+ indent_space: ' ',
432
+ left_margin: 0,
433
+ margin_char: ' ',
434
+ rest_indent: '',
435
+ right_margin: 0
436
+ )
437
+ @fill_margin = fill_margin
438
+ @first_indent = first_indent
439
+ @indent = indent
440
+ @indent_space = indent_space
441
+ @rest_indent = rest_indent
442
+ @right_margin = right_margin
443
+ @width = width
444
+
445
+ @margin_space = fill_margin ? (margin_char * left_margin) : ''
446
+ @left_margin = @margin_space.length
447
+ end
448
+
449
+ # Wraps the given text according to the specified options.
450
+ #
451
+ # @param text [String] the text to wrap
452
+ # @return [String] the wrapped text
453
+ def wrap(text)
454
+ text = text.dup if text.frozen?
455
+ max_line_length = width - left_margin - right_margin - @indent_space.length
456
+ lines = []
457
+ current_line = String.new
458
+
459
+ words = text.split
460
+ words.each.with_index do |word, index|
461
+ trial_length = word.length
462
+ trial_length += @first_indent.length if index.zero?
463
+ trial_length += current_line.length + 1 + @rest_indent.length if index != 0
464
+ if trial_length > max_line_length && (words.count != 0)
465
+ lines << current_line
466
+ current_line = word
467
+ current_line = current_line.dup if current_line.frozen?
468
+ else
469
+ current_line << ' ' unless current_line.empty?
470
+ current_line << word
471
+ end
472
+ end
473
+ lines << current_line unless current_line.empty?
474
+
475
+ lines.map.with_index do |line, index|
476
+ @margin_space + if index.zero?
477
+ @first_indent
478
+ else
479
+ @rest_indent
480
+ end + line
481
+ end
482
+ end
483
+ end
484
+
415
485
  module MarkdownExec
416
486
  class DebugHelper
417
487
  # Class-level variable to store history of printed messages
@@ -509,9 +579,24 @@ module MarkdownExec
509
579
  history_state_partition
510
580
  option_name = @delegate_object[:menu_option_back_name]
511
581
  insert_at_top = @delegate_object[:menu_back_at_top]
582
+ when MenuState::EDIT
583
+ option_name = @delegate_object[:menu_option_edit_name]
584
+ insert_at_top = @delegate_object[:menu_load_at_top]
512
585
  when MenuState::EXIT
513
586
  option_name = @delegate_object[:menu_option_exit_name]
514
587
  insert_at_top = @delegate_object[:menu_exit_at_top]
588
+ when MenuState::LOAD
589
+ option_name = @delegate_object[:menu_option_load_name]
590
+ insert_at_top = @delegate_object[:menu_load_at_top]
591
+ when MenuState::SAVE
592
+ option_name = @delegate_object[:menu_option_save_name]
593
+ insert_at_top = @delegate_object[:menu_load_at_top]
594
+ when MenuState::SHELL
595
+ option_name = @delegate_object[:menu_option_shell_name]
596
+ insert_at_top = @delegate_object[:menu_load_at_top]
597
+ when MenuState::VIEW
598
+ option_name = @delegate_object[:menu_option_view_name]
599
+ insert_at_top = @delegate_object[:menu_load_at_top]
515
600
  end
516
601
 
517
602
  formatted_name = format(@delegate_object[:menu_link_format],
@@ -519,7 +604,7 @@ module MarkdownExec
519
604
  chrome_block = FCB.new(
520
605
  chrome: true,
521
606
  dname: HashDelegator.new(@delegate_object).string_send_color(
522
- formatted_name, :menu_link_color
607
+ formatted_name, :menu_chrome_color
523
608
  ),
524
609
  oname: formatted_name
525
610
  )
@@ -607,6 +692,8 @@ module MarkdownExec
607
692
  #
608
693
  # @return [Array<FCB>] An array of FCB objects representing the blocks.
609
694
  def blocks_from_nested_files
695
+ register_console_attributes(@delegate_object)
696
+
610
697
  blocks = []
611
698
  iter_blocks_from_nested_files do |btype, fcb|
612
699
  process_block_based_on_type(blocks, btype, fcb)
@@ -617,6 +704,20 @@ module MarkdownExec
617
704
  HashDelegator.error_handler('blocks_from_nested_files')
618
705
  end
619
706
 
707
+ # find a block by its original (undecorated) name or nickname (not visible in menu)
708
+ # if matched, the block returned has properties that it is from cli and not ui
709
+ def block_state_for_name_from_cli(block_name)
710
+ SelectedBlockMenuState.new(
711
+ @dml_blocks_in_file.find do |item|
712
+ block_name == item.pub_name
713
+ end&.merge(
714
+ block_name_from_cli: true,
715
+ block_name_from_ui: false
716
+ ),
717
+ MenuState::CONTINUE
718
+ )
719
+ end
720
+
620
721
  # private
621
722
 
622
723
  def calc_logged_stdout_filename(block_name:)
@@ -663,7 +764,7 @@ module MarkdownExec
663
764
  # @return [Array<String>] Required code blocks as an array of lines.
664
765
  def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new)
665
766
  required = mdoc.collect_recursively_required_code(
666
- anyname: selected[:nickname] || selected[:oname],
767
+ anyname: selected.pub_name,
667
768
  label_format_above: @delegate_object[:shell_code_label_format_above],
668
769
  label_format_below: @delegate_object[:shell_code_label_format_below],
669
770
  block_source: block_source
@@ -701,47 +802,15 @@ module MarkdownExec
701
802
  system(
702
803
  format(
703
804
  @delegate_object[:execute_command_format],
704
- {
705
- batch_index: @run_state.batch_index,
706
- batch_random: @run_state.batch_random,
707
- block_name: @delegate_object[:block_name],
708
- document_filename: File.basename(@delegate_object[:filename]),
709
- document_filespec: @delegate_object[:filename],
710
- home: Dir.pwd,
711
- output_filename: File.basename(@delegate_object[:logged_stdout_filespec]),
712
- output_filespec: @delegate_object[:logged_stdout_filespec],
713
- script_filename: @run_state.saved_filespec,
714
- script_filespec: File.join(Dir.pwd, @run_state.saved_filespec),
715
- started_at: @run_state.started_at.strftime(
716
- @delegate_object[:execute_command_title_time_format]
717
- )
718
- }
805
+ command_execute_in_own_window_format_arguments
719
806
  )
720
807
  )
721
808
 
722
809
  else
723
810
  @run_state.in_own_window = false
724
- Open3.popen3(@delegate_object[:shell],
725
- '-c', command,
726
- @delegate_object[:filename],
727
- *args) do |stdin, stdout, stderr, exec_thr|
728
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
729
- yield nil, line, nil, exec_thr if block_given?
730
- end
731
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
732
- yield nil, nil, line, exec_thr if block_given?
733
- end
734
-
735
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
736
- stdin.puts(line)
737
- yield line, nil, nil, exec_thr if block_given?
738
- end
739
-
740
- wait_for_stream_processing
741
- exec_thr.join
742
- sleep 0.1
743
- in_thr.kill if in_thr&.alive?
744
- end
811
+ execute_command_with_streams(
812
+ [@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args]
813
+ )
745
814
  end
746
815
 
747
816
  @run_state.completed_at = Time.now.utc
@@ -750,32 +819,33 @@ module MarkdownExec
750
819
  @run_state.aborted_at = Time.now.utc
751
820
  @run_state.error_message = err.message
752
821
  @run_state.error = err
753
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
822
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
754
823
  @fout.fout "Error ENOENT: #{err.inspect}"
755
824
  rescue SignalException => err
756
825
  # Handle SignalException
757
826
  @run_state.aborted_at = Time.now.utc
758
827
  @run_state.error_message = 'SIGTERM'
759
828
  @run_state.error = err
760
- @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
829
+ @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
761
830
  @fout.fout "Error ENOENT: #{err.inspect}"
762
831
  end
763
832
 
764
- def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
765
- if @delegate_object[:block_name].present?
766
- block = all_blocks.find do |item|
767
- item[:oname] == @delegate_object[:block_name]
768
- end&.merge(block_name_from_ui: false)
769
- else
770
- block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
771
- default)
772
- block = block_state.block&.merge(block_name_from_ui: true)
773
- state = block_state.state
774
- end
775
-
776
- SelectedBlockMenuState.new(block, state)
777
- rescue StandardError
778
- HashDelegator.error_handler('load_cli_or_user_selected_block')
833
+ def command_execute_in_own_window_format_arguments(home: Dir.pwd)
834
+ {
835
+ batch_index: @run_state.batch_index,
836
+ batch_random: @run_state.batch_random,
837
+ block_name: @delegate_object[:block_name],
838
+ document_filename: File.basename(@delegate_object[:filename]),
839
+ document_filespec: @delegate_object[:filename],
840
+ home: home,
841
+ output_filename: File.basename(@delegate_object[:logged_stdout_filespec]),
842
+ output_filespec: @delegate_object[:logged_stdout_filespec],
843
+ script_filename: @run_state.saved_filespec,
844
+ script_filespec: File.join(home, @run_state.saved_filespec),
845
+ started_at: @run_state.started_at.strftime(
846
+ @delegate_object[:execute_command_title_time_format]
847
+ )
848
+ }
779
849
  end
780
850
 
781
851
  # This method is responsible for handling the execution of generic blocks in a markdown document.
@@ -799,7 +869,12 @@ module MarkdownExec
799
869
  execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
800
870
 
801
871
  link_state.block_name = nil
802
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
872
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
873
+ end
874
+
875
+ # Check if the expression contains wildcard characters
876
+ def contains_wildcards?(expr)
877
+ expr.match(%r{\*|\?|\[})
803
878
  end
804
879
 
805
880
  def copy_to_clipboard(required_lines)
@@ -828,16 +903,70 @@ module MarkdownExec
828
903
  # @param match_data [MatchData] The match data containing named captures for formatting.
829
904
  # @param format_option [String] The format string to be used for the new block.
830
905
  # @param color_method [Symbol] The color method to apply to the block's display name.
831
- def create_and_add_chrome_block(blocks:, match_data:, format_option:,
832
- color_method:)
833
- oname = format(format_option,
834
- match_data.named_captures.transform_keys(&:to_sym))
835
- blocks.push FCB.new(
836
- chrome: true,
837
- disabled: '',
838
- dname: oname.send(color_method),
839
- oname: oname
840
- )
906
+ # return number of lines added
907
+ def create_and_add_chrome_block(blocks:, match_data:,
908
+ format_option:, color_method:,
909
+ case_conversion: nil,
910
+ center: nil,
911
+ wrap: nil)
912
+ line_cap = match_data.named_captures.transform_keys(&:to_sym)
913
+
914
+ # replace tabs in indent
915
+ line_cap[:indent] ||= ''
916
+ line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen?
917
+ line_cap[:indent].gsub!("\t", ' ')
918
+ # replace tabs in text
919
+ line_cap[:text] ||= ''
920
+ line_cap[:text] = line_cap[:text].dup if line_cap[:text].frozen?
921
+ line_cap[:text].gsub!("\t", ' ')
922
+ # missing capture
923
+ line_cap[:line] ||= ''
924
+
925
+ accepted_width = @delegate_object[:console_width] - 2
926
+ line_caps = if wrap
927
+ if line_cap[:text].length > accepted_width
928
+ wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length)
929
+ wrapper.wrap(line_cap[:text]).map do |line|
930
+ line_cap.dup.merge(text: line)
931
+ end
932
+ else
933
+ [line_cap]
934
+ end
935
+ else
936
+ [line_cap]
937
+ end
938
+ if center
939
+ line_caps.each do |line_obj|
940
+ line_obj[:indent] = if line_obj[:text].length < accepted_width
941
+ ' ' * ((accepted_width - line_obj[:text].length) / 2)
942
+ else
943
+ ''
944
+ end
945
+ end
946
+ end
947
+
948
+ line_caps.each do |line_obj|
949
+ next if line_obj[:text].nil?
950
+
951
+ case case_conversion
952
+ when :upcase
953
+ line_obj[:text].upcase!
954
+ when :downcase
955
+ line_obj[:text].downcase!
956
+ end
957
+
958
+ # format expects :line to be text only
959
+ line_obj[:line] = line_obj[:text]
960
+ oname = format(format_option, line_obj)
961
+ line_obj[:line] = line_obj[:indent] + line_obj[:text]
962
+ blocks.push FCB.new(
963
+ chrome: true,
964
+ disabled: '',
965
+ dname: line_obj[:indent] + oname.send(color_method),
966
+ oname: line_obj[:text]
967
+ )
968
+ end
969
+ line_caps.count
841
970
  end
842
971
 
843
972
  ##
@@ -848,12 +977,12 @@ module MarkdownExec
848
977
  # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
849
978
  def create_and_add_chrome_blocks(blocks, fcb)
850
979
  match_criteria = [
851
- { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match },
852
- { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match },
853
- { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match },
980
+ { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, center: true, case_conversion: :upcase, wrap: true },
981
+ { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, center: true, wrap: true },
982
+ { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, center: true, case_conversion: :downcase, wrap: true },
854
983
  { color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match },
855
- { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match },
856
- { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match }
984
+ { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true },
985
+ { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true }
857
986
  ]
858
987
  # rubocop:enable Style/UnlessElse
859
988
  match_criteria.each do |criteria|
@@ -864,9 +993,12 @@ module MarkdownExec
864
993
 
865
994
  create_and_add_chrome_block(
866
995
  blocks: blocks,
867
- match_data: mbody,
996
+ case_conversion: criteria[:case_conversion],
997
+ center: criteria[:center],
998
+ color_method: @delegate_object[criteria[:color]].to_sym,
868
999
  format_option: @delegate_object[criteria[:format]],
869
- color_method: @delegate_object[criteria[:color]].to_sym
1000
+ match_data: mbody,
1001
+ wrap: criteria[:wrap]
870
1002
  )
871
1003
  break
872
1004
  end
@@ -950,164 +1082,495 @@ module MarkdownExec
950
1082
  @delegate_object[:logged_stdout_filespec])
951
1083
  end
952
1084
 
953
- # Executes a block of code that has been approved for execution.
954
- # It sets the script block name, writes command files if required, and handles the execution
955
- # including output formatting and summarization.
956
- #
957
- # @param required_lines [Array<String>] The lines of code to be executed.
958
- # @param selected [FCB] The selected functional code block object.
959
- def execute_required_lines(required_lines: [], selected: FCB.new)
960
- write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script]
961
- calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state
962
- format_and_execute_command(code_lines: required_lines)
963
- post_execution_process
964
- end
965
-
966
- # Execute a code block after approval and provide user interaction options.
967
- #
968
- # This method displays required code blocks, asks for user approval, and
969
- # executes the code block if approved. It also allows users to copy the
970
- # code to the clipboard or save it to a file.
1085
+ # Select and execute a code block from a Markdown document.
971
1086
  #
972
- # @param opts [Hash] Options hash containing configuration settings.
973
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
1087
+ # This method allows the user to interactively select a code block from a
1088
+ # Markdown document, obtain approval, and execute the chosen block of code.
974
1089
  #
975
- def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new)
976
- if selected.fetch(:shell, '') == BlockType::LINK
977
- debounce_reset
978
- push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''),
979
- mdoc: mdoc,
980
- selected: selected,
981
- link_state: link_state,
982
- block_source: block_source)
1090
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
1091
+ def document_inpseq
1092
+ @menu_base_options = @delegate_object
1093
+ @dml_link_state = LinkState.new(
1094
+ block_name: @delegate_object[:block_name],
1095
+ document_filename: @delegate_object[:filename]
1096
+ )
1097
+ # @dml_link_state_block_name_from_cli = @dml_link_state.block_name.present? ###
1098
+ @run_state.block_name_from_cli = @dml_link_state.block_name.present?
1099
+ @cli_block_name = @dml_link_state.block_name
1100
+ @dml_now_using_cli = @run_state.block_name_from_cli
1101
+ @dml_menu_default_dname = nil
1102
+ @dml_block_state = SelectedBlockMenuState.new
1103
+ @doc_saved_lines_files = []
983
1104
 
984
- elsif @menu_user_clicked_back_link
985
- debounce_reset
986
- pop_link_history_and_trigger_load
1105
+ ## load file with code lines per options
1106
+ #
1107
+ if @menu_base_options[:load_code].present?
1108
+ @dml_link_state.inherited_lines = []
1109
+ @menu_base_options[:load_code].split(':').map do |path|
1110
+ @dml_link_state.inherited_lines += File.readlines(path, chomp: true)
1111
+ end
987
1112
 
988
- elsif selected[:shell] == BlockType::OPTS
989
- debounce_reset
990
- block_names = []
991
- code_lines = []
992
- dependencies = {}
993
- options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state)
1113
+ inherited_block_names = []
1114
+ inherited_dependencies = {}
1115
+ selected = { oname: 'load_code' }
1116
+ pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
1117
+ end
994
1118
 
995
- ## apply options to current state
996
- #
997
- @menu_base_options.merge!(options_state.options)
998
- @delegate_object.merge!(options_state.options)
1119
+ item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
1120
+ item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
1121
+ item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
1122
+ item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
1123
+ item_shell = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name]))
1124
+ item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
999
1125
 
1000
- ### options_state.load_file_link_state
1001
- link_state = LinkState.new
1002
- link_history_push_and_next(
1003
- curr_block_name: selected[:oname],
1004
- curr_document_filename: @delegate_object[:filename],
1005
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1006
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1007
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1008
- next_block_name: '',
1009
- next_document_filename: @delegate_object[:filename],
1010
- next_load_file: LoadFile::Reuse
1011
- )
1126
+ @run_state.batch_random = Random.new.rand
1127
+ @run_state.batch_index = 0
1012
1128
 
1013
- elsif selected[:shell] == BlockType::VARS
1014
- debounce_reset
1015
- block_names = []
1016
- code_lines = set_environment_variables_for_block(selected)
1017
- dependencies = {}
1018
- link_history_push_and_next(
1019
- curr_block_name: selected[:oname],
1020
- curr_document_filename: @delegate_object[:filename],
1021
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1022
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1023
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1024
- next_block_name: '',
1025
- next_document_filename: @delegate_object[:filename],
1026
- next_load_file: LoadFile::Reuse
1027
- )
1129
+ InputSequencer.new(
1130
+ @delegate_object[:filename],
1131
+ @delegate_object[:input_cli_rest]
1132
+ ).run do |msg, data|
1133
+ case msg
1134
+ when :parse_document # once for each menu
1135
+ # puts "@ - parse document #{data}"
1136
+ inpseq_parse_document(data)
1028
1137
 
1029
- elsif debounce_allows
1030
- compile_execute_and_trigger_reuse(mdoc: mdoc,
1031
- selected: selected,
1032
- link_state: link_state,
1033
- block_source: block_source)
1138
+ if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
1034
1139
 
1035
- else
1036
- LoadFileLinkState.new(LoadFile::Reuse, link_state)
1037
- end
1038
- end
1140
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1141
+ files = sf ? Dir.glob(sf) : []
1142
+ @doc_saved_lines_files = files.count.positive? ? files : []
1039
1143
 
1040
- # Retrieves a specific data symbol from the delegate object, converts it to a string,
1041
- # and applies a color style based on the specified color symbol.
1042
- #
1043
- # @param default [String] The default value if the data symbol is not found.
1044
- # @param data_sym [Symbol] The symbol key to fetch data from the delegate object.
1045
- # @param color_sym [Symbol] The symbol key to fetch the color option for styling.
1046
- # @return [String] The color-styled string.
1047
- def fetch_color(default: '',
1048
- data_sym: :execution_report_preview_head,
1049
- color_sym: :execution_report_preview_frame_color)
1050
- data_string = @delegate_object.fetch(data_sym, default).to_s
1051
- string_send_color(data_string, color_sym)
1052
- end
1144
+ lines_count = @dml_link_state.inherited_lines&.count || 0
1053
1145
 
1054
- def format_and_execute_command(code_lines:)
1055
- formatted_command = code_lines.flatten.join("\n")
1056
- @fout.fout fetch_color(data_sym: :script_execution_head,
1057
- color_sym: :script_execution_frame_color)
1058
- command_execute(formatted_command, args: @pass_args)
1059
- @fout.fout fetch_color(data_sym: :script_execution_tail,
1060
- color_sym: :script_execution_frame_color)
1061
- end
1146
+ # add menu items (glob, load, save) and enable selectively
1147
+ menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
1148
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD) if files.count.positive?
1149
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT) if lines_count.positive?
1150
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), 1, '', menu_state: MenuState::SAVE) if lines_count.positive?
1151
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), 1, '', menu_state: MenuState::VIEW) if lines_count.positive?
1152
+ menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name])), 1, '', menu_state: MenuState::SHELL) if @delegate_object[:menu_with_shell]
1153
+ end
1062
1154
 
1063
- # Formats a string based on a given context and applies color styling to it.
1064
- # It retrieves format and color information from the delegate object and processes accordingly.
1065
- #
1066
- # @param default [String] The default value if the format symbol is not found (unused in current implementation).
1067
- # @param context [Hash] Contextual data used for string formatting.
1068
- # @param format_sym [Symbol] Symbol key to fetch the format string from the delegate object.
1069
- # @param color_sym [Symbol] Symbol key to fetch the color option for string styling.
1070
- # @return [String] The formatted and color-styled string.
1071
- def format_references_send_color(default: '', context: {},
1072
- format_sym: :output_execution_label_format,
1073
- color_sym: :execution_report_preview_frame_color)
1074
- formatted_string = format(@delegate_object.fetch(format_sym, ''),
1075
- context).to_s
1076
- string_send_color(formatted_string, color_sym)
1077
- end
1155
+ when :display_menu
1156
+ # warn "@ - display menu:"
1157
+ # ii_display_menu
1158
+ @dml_block_state = SelectedBlockMenuState.new
1159
+ @delegate_object[:block_name] = nil
1078
1160
 
1079
- # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1080
- # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1081
- #
1082
- # @param fcb [Object] An object representing a functional code block.
1083
- # @return [Object] The modified functional code block with updated summary attributes.
1084
- def get_block_summary(fcb)
1085
- return fcb unless @delegate_object[:bash]
1161
+ when :user_choice
1162
+ if @dml_link_state.block_name.present?
1163
+ # @prior_block_was_link = true
1164
+ @dml_block_state.block = @dml_blocks_in_file.find do |item|
1165
+ item.pub_name == @dml_link_state.block_name
1166
+ end
1167
+ @dml_link_state.block_name = nil
1168
+ else
1169
+ # puts "? - Select a block to execute (or type #{$texit} to exit):"
1170
+ break if inpseq_user_choice == :break # into @dml_block_state
1171
+ break if @dml_block_state.block.nil? # no block matched
1172
+ end
1173
+ # puts "! - Executing block: #{data}"
1174
+ @dml_block_state.block&.pub_name
1086
1175
 
1087
- fcb.call = fcb.title.match(Regexp.new(@delegate_object[:block_calls_scan]))&.fetch(1, nil)
1088
- titlexcall = fcb.call ? fcb.title.sub("%#{fcb.call}", '') : fcb.title
1089
- bm = extract_named_captures_from_option(titlexcall,
1090
- @delegate_object[:block_name_match])
1176
+ when :execute_block
1177
+ case (block_name = data)
1178
+ when item_back
1179
+ debounce_reset
1180
+ @menu_user_clicked_back_link = true
1181
+ load_file_link_state = pop_link_history_and_trigger_load
1182
+ @dml_link_state = load_file_link_state.link_state
1091
1183
 
1092
- fcb.stdin = extract_named_captures_from_option(titlexcall,
1093
- @delegate_object[:block_stdin_scan])
1094
- fcb.stdout = extract_named_captures_from_option(titlexcall,
1095
- @delegate_object[:block_stdout_scan])
1184
+ InputSequencer.merge_link_state(
1185
+ @dml_link_state,
1186
+ InputSequencer.next_link_state(
1187
+ block_name: @dml_link_state.block_name,
1188
+ document_filename: @dml_link_state.document_filename,
1189
+ prior_block_was_link: true
1190
+ )
1191
+ )
1096
1192
 
1097
- shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
1193
+ when item_edit
1194
+ debounce_reset
1195
+ edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
1196
+ @dml_link_state.inherited_lines = edited.split("\n") if edited
1197
+ InputSequencer.next_link_state(prior_block_was_link: true)
1098
1198
 
1099
- if @delegate_object[:block_name_nick_match].present? && fcb.oname =~ Regexp.new(@delegate_object[:block_name_nick_match])
1100
- fcb.nickname = $~[0]
1101
- fcb.title = fcb.oname = format_multiline_body_as_title(fcb.body)
1102
- else
1103
- fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
1199
+ when item_load
1200
+ debounce_reset
1201
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1202
+ load_filespec = load_filespec_from_expression(sf)
1203
+ if load_filespec
1204
+ @dml_link_state.inherited_lines ||= []
1205
+ @dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
1206
+ end
1207
+ InputSequencer.next_link_state(prior_block_was_link: true)
1208
+
1209
+ when item_save
1210
+ debounce_reset
1211
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1212
+ save_filespec = save_filespec_from_expression(sf)
1213
+ if save_filespec && !write_file_with_directory_creation(
1214
+ save_filespec,
1215
+ HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
1216
+ )
1217
+ return :break
1218
+
1219
+ end
1220
+ InputSequencer.next_link_state(prior_block_was_link: true)
1221
+
1222
+ when item_shell
1223
+ debounce_reset
1224
+ loop do
1225
+ command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen)
1226
+ break if !command.present? || command == 'exit'
1227
+
1228
+ exit_status = execute_command_with_streams(
1229
+ [@delegate_object[:shell], '-c', command]
1230
+ )
1231
+ case exit_status
1232
+ when 0
1233
+ warn "#{'OK'.green} #{exit_status}"
1234
+ else
1235
+ warn "#{'ERR'.bred} #{exit_status}"
1236
+ end
1237
+ end
1238
+ InputSequencer.next_link_state(prior_block_was_link: true)
1239
+
1240
+ when item_view
1241
+ debounce_reset
1242
+ warn @dml_link_state.inherited_lines.join("\n")
1243
+ InputSequencer.next_link_state(prior_block_was_link: true)
1244
+
1245
+ else
1246
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1247
+ if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
1248
+ debounce_reset
1249
+ link_state = LinkState.new
1250
+ options_state = read_show_options_and_trigger_reuse(
1251
+ selected: @dml_block_state.block,
1252
+ link_state: link_state
1253
+ )
1254
+
1255
+ @menu_base_options.merge!(options_state.options)
1256
+ @delegate_object.merge!(options_state.options)
1257
+ options_state.load_file_link_state.link_state
1258
+ else
1259
+ inpseq_execute_block(block_name)
1260
+
1261
+ if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1262
+ selected: @dml_block_state.block)
1263
+ return :break
1264
+ end
1265
+
1266
+ ## order of block name processing: link block, cli, from user
1267
+ #
1268
+ @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1269
+ HashDelegator.next_link_state(
1270
+ block_name: @dml_link_state.block_name,
1271
+ block_name_from_cli: @dml_now_using_cli,
1272
+ block_state: @dml_block_state,
1273
+ was_using_cli: @dml_now_using_cli
1274
+ )
1275
+
1276
+ if !@dml_block_state.block[:block_name_from_ui] && cli_break
1277
+ # &bsp '!block_name_from_ui + cli_break -> break'
1278
+ return :break
1279
+ end
1280
+
1281
+ InputSequencer.next_link_state(
1282
+ block_name: @dml_link_state.block_name,
1283
+ prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
1284
+ )
1285
+ end
1286
+ end
1287
+
1288
+ when :exit?
1289
+ data == $texit
1290
+ when :stay?
1291
+ data == $stay
1292
+ else
1293
+ raise "Invalid message: #{msg}"
1294
+ end
1295
+ end
1296
+ rescue StandardError
1297
+ HashDelegator.error_handler('document_inpseq',
1298
+ { abort: true })
1299
+ end
1300
+
1301
+ # remove leading "./"
1302
+ # replace characters: / : . * (space) with: (underscore)
1303
+ def document_name_in_glob_as_file_name(document_filename, glob)
1304
+ return document_filename if document_filename.nil? || document_filename.empty?
1305
+
1306
+ format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
1307
+ end
1308
+
1309
+ def dump_and_warn_block_state(selected:)
1310
+ if selected.nil?
1311
+ Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
1312
+ { abort: true })
1104
1313
  end
1105
1314
 
1106
- fcb.dname = HashDelegator.indent_all_lines(
1107
- apply_shell_color_option(fcb.oname, shell_color_option),
1108
- fcb.fetch(:indent, nil)
1315
+ return unless @delegate_object[:dump_selected_block]
1316
+
1317
+ warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
1318
+ end
1319
+
1320
+ # Outputs warnings based on the delegate object's configuration
1321
+ #
1322
+ # @param delegate_object [Hash] The delegate object containing configuration flags.
1323
+ # @param blocks_in_file [Hash] Hash of blocks present in the file.
1324
+ # @param menu_blocks [Hash] Hash of menu blocks.
1325
+ # @param link_state [LinkState] Current state of the link.
1326
+ def dump_delobj(blocks_in_file, menu_blocks, link_state)
1327
+ warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
1328
+
1329
+ if @delegate_object[:dump_blocks_in_file]
1330
+ warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
1331
+ label: 'blocks_in_file')
1332
+ end
1333
+
1334
+ if @delegate_object[:dump_menu_blocks]
1335
+ warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
1336
+ label: 'menu_blocks')
1337
+ end
1338
+
1339
+ warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
1340
+ warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
1341
+ return unless @delegate_object[:dump_inherited_lines]
1342
+
1343
+ warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
1344
+ end
1345
+
1346
+ # Opens text in an editor for user modification and returns the modified text.
1347
+ #
1348
+ # This method reads the provided text, opens it in the default editor,
1349
+ # and allows the user to modify it. If the user makes changes, the
1350
+ # modified text is returned. If the user exits the editor without
1351
+ # making changes or the editor is closed abruptly, appropriate messages
1352
+ # are displayed.
1353
+ #
1354
+ # @param [String] initial_text The initial text to be edited.
1355
+ # @param [String] temp_name The base name for the temporary file (default: 'edit_text').
1356
+ # @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly.
1357
+ def edit_text(initial_text, temp_name: 'edit_text')
1358
+ # Create a temporary file to store the initial text
1359
+ temp_file = Tempfile.new(temp_name)
1360
+ temp_file.write(initial_text)
1361
+ temp_file.rewind
1362
+
1363
+ # Capture the modification time of the temporary file before editing
1364
+ before_mtime = temp_file.mtime
1365
+
1366
+ # Open the temporary file in the default editor
1367
+ system("#{ENV['EDITOR'] || 'vi'} #{temp_file.path}")
1368
+
1369
+ # Capture the exit status of the editor
1370
+ editor_exit_status = $?.exitstatus
1371
+
1372
+ # Reopen the file to ensure the updated modification time is read
1373
+ temp_file.open
1374
+ after_mtime = temp_file.mtime
1375
+
1376
+ # Check if the editor was exited normally or was interrupted
1377
+ if editor_exit_status != 0
1378
+ warn 'The editor was closed abruptly. No changes were made.'
1379
+ temp_file.close
1380
+ temp_file.unlink
1381
+ return
1382
+ end
1383
+
1384
+ result_text = nil
1385
+ # Read the file if it was modified
1386
+ if before_mtime != after_mtime
1387
+ temp_file.rewind
1388
+ result_text = temp_file.read
1389
+ end
1390
+
1391
+ # Remove the temporary file
1392
+ temp_file.close
1393
+ temp_file.unlink
1394
+
1395
+ result_text
1396
+ end
1397
+
1398
+ def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
1399
+ lfls = execute_shell_type(
1400
+ selected: selected,
1401
+ mdoc: mdoc,
1402
+ link_state: link_state,
1403
+ block_source: block_source
1109
1404
  )
1110
- fcb
1405
+
1406
+ # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1407
+ [lfls.link_state,
1408
+ lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
1409
+ #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1410
+ end
1411
+
1412
+ # Executes a given command and processes its input, output, and error streams.
1413
+ #
1414
+ # @param [Array<String>] command the command to execute along with its arguments.
1415
+ # @yield [stdin, stdout, stderr, thread] if a block is provided, it yields input, output, error lines, and the execution thread.
1416
+ # @return [Integer] the exit status of the executed command (0 to 255).
1417
+ #
1418
+ # @example
1419
+ # status = execute_command_with_streams(['ls', '-la']) do |stdin, stdout, stderr, thread|
1420
+ # puts "STDOUT: #{stdout}" if stdout
1421
+ # puts "STDERR: #{stderr}" if stderr
1422
+ # end
1423
+ # puts "Command exited with status: #{status}"
1424
+ def execute_command_with_streams(command)
1425
+ exit_status = nil
1426
+
1427
+ Open3.popen3(*command) do |stdin, stdout, stderr, exec_thread|
1428
+ # Handle stdout stream
1429
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
1430
+ yield nil, line, nil, exec_thread if block_given?
1431
+ end
1432
+
1433
+ # Handle stderr stream
1434
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
1435
+ yield nil, nil, line, exec_thread if block_given?
1436
+ end
1437
+
1438
+ # Handle stdin stream
1439
+ input_thread = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
1440
+ stdin.puts(line)
1441
+ yield line, nil, nil, exec_thread if block_given?
1442
+ end
1443
+
1444
+ # Wait for all streams to be processed
1445
+ wait_for_stream_processing
1446
+ exec_thread.join
1447
+
1448
+ # Ensure the input thread is killed if it's still alive
1449
+ sleep 0.1
1450
+ input_thread.kill if input_thread&.alive?
1451
+
1452
+ # Retrieve the exit status
1453
+ exit_status = exec_thread.value.exitstatus
1454
+ end
1455
+
1456
+ exit_status
1457
+ end
1458
+
1459
+ # Executes a block of code that has been approved for execution.
1460
+ # It sets the script block name, writes command files if required, and handles the execution
1461
+ # including output formatting and summarization.
1462
+ #
1463
+ # @param required_lines [Array<String>] The lines of code to be executed.
1464
+ # @param selected [FCB] The selected functional code block object.
1465
+ def execute_required_lines(required_lines: [], selected: FCB.new)
1466
+ write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script]
1467
+ calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state
1468
+ format_and_execute_command(code_lines: required_lines)
1469
+ post_execution_process
1470
+ end
1471
+
1472
+ # Execute a code block after approval and provide user interaction options.
1473
+ #
1474
+ # This method displays required code blocks, asks for user approval, and
1475
+ # executes the code block if approved. It also allows users to copy the
1476
+ # code to the clipboard or save it to a file.
1477
+ #
1478
+ # @param opts [Hash] Options hash containing configuration settings.
1479
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
1480
+ #
1481
+ def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new)
1482
+ if selected.fetch(:shell, '') == BlockType::LINK
1483
+ debounce_reset
1484
+ push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''),
1485
+ mdoc: mdoc,
1486
+ selected: selected,
1487
+ link_state: link_state,
1488
+ block_source: block_source)
1489
+
1490
+ elsif @menu_user_clicked_back_link
1491
+ debounce_reset
1492
+ pop_link_history_and_trigger_load
1493
+
1494
+ elsif selected[:shell] == BlockType::OPTS
1495
+ debounce_reset
1496
+ block_names = []
1497
+ code_lines = []
1498
+ dependencies = {}
1499
+ options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state)
1500
+
1501
+ ## apply options to current state
1502
+ #
1503
+ @menu_base_options.merge!(options_state.options)
1504
+ @delegate_object.merge!(options_state.options)
1505
+
1506
+ ### options_state.load_file_link_state
1507
+ link_state = LinkState.new
1508
+ link_history_push_and_next(
1509
+ curr_block_name: selected.pub_name,
1510
+ curr_document_filename: @delegate_object[:filename],
1511
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1512
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1513
+ inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1514
+ next_block_name: '',
1515
+ next_document_filename: @delegate_object[:filename],
1516
+ next_load_file: LoadFile::REUSE
1517
+ )
1518
+
1519
+ elsif selected[:shell] == BlockType::VARS
1520
+ debounce_reset
1521
+ block_names = []
1522
+ code_lines = set_environment_variables_for_block(selected)
1523
+ dependencies = {}
1524
+ link_history_push_and_next(
1525
+ curr_block_name: selected.pub_name,
1526
+ curr_document_filename: @delegate_object[:filename],
1527
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1528
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1529
+ inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1530
+ next_block_name: '',
1531
+ next_document_filename: @delegate_object[:filename],
1532
+ next_load_file: LoadFile::REUSE
1533
+ )
1534
+
1535
+ elsif debounce_allows
1536
+ compile_execute_and_trigger_reuse(mdoc: mdoc,
1537
+ selected: selected,
1538
+ link_state: link_state,
1539
+ block_source: block_source)
1540
+
1541
+ else
1542
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
1543
+ end
1544
+ end
1545
+
1546
+ # Retrieves a specific data symbol from the delegate object, converts it to a string,
1547
+ # and applies a color style based on the specified color symbol.
1548
+ #
1549
+ # @param default [String] The default value if the data symbol is not found.
1550
+ # @param data_sym [Symbol] The symbol key to fetch data from the delegate object.
1551
+ # @param color_sym [Symbol] The symbol key to fetch the color option for styling.
1552
+ # @return [String] The color-styled string.
1553
+ def fetch_color(default: '',
1554
+ data_sym: :execution_report_preview_head,
1555
+ color_sym: :execution_report_preview_frame_color)
1556
+ data_string = @delegate_object.fetch(data_sym, default).to_s
1557
+ string_send_color(data_string, color_sym)
1558
+ end
1559
+
1560
+ def format_and_execute_command(code_lines:)
1561
+ formatted_command = code_lines.flatten.join("\n")
1562
+ @fout.fout fetch_color(data_sym: :script_execution_head,
1563
+ color_sym: :script_execution_frame_color)
1564
+ command_execute(formatted_command, args: @pass_args)
1565
+ @fout.fout fetch_color(data_sym: :script_execution_tail,
1566
+ color_sym: :script_execution_frame_color)
1567
+ end
1568
+
1569
+ # Format expression using environment variables and run state
1570
+ def format_expression(expr)
1571
+ data = link_load_format_data
1572
+ ENV.each { |key, value| data[key] = value }
1573
+ format(expr, data)
1111
1574
  end
1112
1575
 
1113
1576
  # Formats multiline body content as a title string.
@@ -1120,6 +1583,61 @@ module MarkdownExec
1120
1583
  end.join("\n") + "\n"
1121
1584
  end
1122
1585
 
1586
+ # Formats a string based on a given context and applies color styling to it.
1587
+ # It retrieves format and color information from the delegate object and processes accordingly.
1588
+ #
1589
+ # @param default [String] The default value if the format symbol is not found (unused in current implementation).
1590
+ # @param context [Hash] Contextual data used for string formatting.
1591
+ # @param format_sym [Symbol] Symbol key to fetch the format string from the delegate object.
1592
+ # @param color_sym [Symbol] Symbol key to fetch the color option for string styling.
1593
+ # @return [String] The formatted and color-styled string.
1594
+ def format_references_send_color(default: '', context: {},
1595
+ format_sym: :output_execution_label_format,
1596
+ color_sym: :execution_report_preview_frame_color)
1597
+ formatted_string = format(@delegate_object.fetch(format_sym, ''),
1598
+ context).to_s
1599
+ string_send_color(formatted_string, color_sym)
1600
+ end
1601
+
1602
+ # Expand expression if it contains format specifiers
1603
+ def formatted_expression(expr)
1604
+ expr.include?('%{') ? format_expression(expr) : expr
1605
+ end
1606
+
1607
+ # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1608
+ # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1609
+ #
1610
+ # @param fcb [Object] An object representing a functional code block.
1611
+ # @return [Object] The modified functional code block with updated summary attributes.
1612
+ def get_block_summary(fcb)
1613
+ return fcb unless @delegate_object[:bash]
1614
+
1615
+ fcb.call = fcb.title.match(Regexp.new(@delegate_object[:block_calls_scan]))&.fetch(1, nil)
1616
+ titlexcall = fcb.call ? fcb.title.sub("%#{fcb.call}", '') : fcb.title
1617
+ bm = extract_named_captures_from_option(titlexcall,
1618
+ @delegate_object[:block_name_match])
1619
+
1620
+ fcb.stdin = extract_named_captures_from_option(titlexcall,
1621
+ @delegate_object[:block_stdin_scan])
1622
+ fcb.stdout = extract_named_captures_from_option(titlexcall,
1623
+ @delegate_object[:block_stdout_scan])
1624
+
1625
+ shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
1626
+
1627
+ if @delegate_object[:block_name_nick_match].present? && fcb.oname =~ Regexp.new(@delegate_object[:block_name_nick_match])
1628
+ fcb.nickname = $~[0]
1629
+ fcb.title = fcb.oname = format_multiline_body_as_title(fcb.body)
1630
+ else
1631
+ fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
1632
+ end
1633
+
1634
+ fcb.dname = HashDelegator.indent_all_lines(
1635
+ apply_shell_color_option(fcb.oname, shell_color_option),
1636
+ fcb.fetch(:indent, nil)
1637
+ )
1638
+ fcb
1639
+ end
1640
+
1123
1641
  # Updates the delegate object's state based on the provided block state.
1124
1642
  # It sets the block name and determines if the user clicked the back link in the menu.
1125
1643
  #
@@ -1131,7 +1649,7 @@ module MarkdownExec
1131
1649
  return
1132
1650
  end
1133
1651
 
1134
- @delegate_object[:block_name] = block_state.block[:oname]
1652
+ @delegate_object[:block_name] = block_state.block.pub_name
1135
1653
  @menu_user_clicked_back_link = block_state.state == MenuState::BACK
1136
1654
  end
1137
1655
 
@@ -1140,7 +1658,7 @@ module MarkdownExec
1140
1658
  Thread.new do
1141
1659
  stream.each_line do |line|
1142
1660
  line.strip!
1143
- @run_state.files[file_type] << line
1661
+ @run_state.files[file_type] << line if @run_state.files
1144
1662
 
1145
1663
  if @delegate_object[:output_stdout]
1146
1664
  # print line
@@ -1157,21 +1675,61 @@ module MarkdownExec
1157
1675
  end
1158
1676
  end
1159
1677
 
1160
- # Initializes variables for regex and other states
1161
- def initial_state
1162
- {
1163
- fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch(
1164
- :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1165
- )),
1166
- fenced_start_extended_regex: Regexp.new(@delegate_object.fetch(
1167
- :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1168
- )),
1169
- fcb: MarkdownExec::FCB.new,
1170
- in_fenced_block: false,
1171
- headings: []
1172
- }
1173
- end
1174
-
1678
+ # Initializes variables for regex and other states
1679
+ def initial_state
1680
+ {
1681
+ fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch(
1682
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1683
+ )),
1684
+ fenced_start_extended_regex: Regexp.new(@delegate_object.fetch(
1685
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1686
+ )),
1687
+ fcb: MarkdownExec::FCB.new,
1688
+ in_fenced_block: false,
1689
+ headings: []
1690
+ }
1691
+ end
1692
+
1693
+ def inpseq_execute_block(block_name)
1694
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1695
+
1696
+ dump_and_warn_block_state(selected: @dml_block_state.block)
1697
+ @dml_link_state, @dml_menu_default_dname = \
1698
+ exec_bash_next_state(
1699
+ selected: @dml_block_state.block,
1700
+ mdoc: @dml_mdoc,
1701
+ link_state: @dml_link_state,
1702
+ block_source: {
1703
+ document_filename: @delegate_object[:filename],
1704
+ time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1705
+ }
1706
+ )
1707
+ end
1708
+
1709
+ def inpseq_parse_document(_document_filename)
1710
+ @run_state.batch_index += 1
1711
+ @run_state.in_own_window = false
1712
+
1713
+ # &bsp 'loop', block_name_from_cli, @cli_block_name
1714
+ @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
1715
+ set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
1716
+ now_using_cli: @dml_now_using_cli,
1717
+ link_state: @dml_link_state)
1718
+ end
1719
+
1720
+ def inpseq_user_choice
1721
+ @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1722
+ menu_blocks: @dml_menu_blocks,
1723
+ default: @dml_menu_default_dname)
1724
+ # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1725
+ if !@dml_block_state
1726
+ HashDelegator.error_handler('block_state missing', { abort: true })
1727
+ elsif @dml_block_state.state == MenuState::EXIT
1728
+ # &bsp 'load_cli_or_user_selected_block -> break'
1729
+ :break
1730
+ end
1731
+ end
1732
+
1175
1733
  # Iterates through blocks in a file, applying the provided block to each line.
1176
1734
  # The iteration only occurs if the file exists.
1177
1735
  # @yield [Symbol] :filter Yields to obtain selected messages for processing.
@@ -1198,25 +1756,9 @@ module MarkdownExec
1198
1756
  file.write(all_code.join("\n"))
1199
1757
  file.rewind
1200
1758
 
1201
- if link_block_data.fetch(LinkKeys::Exec, false)
1759
+ if link_block_data.fetch(LinkKeys::EXEC, false)
1202
1760
  @run_state.files = Hash.new([])
1203
-
1204
- Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
1205
- handle_stream(stream: stdout, file_type: ExecutionStreams::StdOut) do |line|
1206
- output_lines.push(line)
1207
- end
1208
- handle_stream(stream: stderr, file_type: ExecutionStreams::StdErr) do |line|
1209
- output_lines.push(line)
1210
- end
1211
-
1212
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::StdIn) do |line|
1213
- stdin.puts(line)
1214
- end
1215
-
1216
- wait_for_stream_processing
1217
- sleep 0.1
1218
- in_thr.kill if in_thr&.alive?
1219
- end
1761
+ execute_command_with_streams([cmd])
1220
1762
 
1221
1763
  ## select output_lines that look like assignment or match other specs
1222
1764
  #
@@ -1239,13 +1781,13 @@ module MarkdownExec
1239
1781
  label_format_below = @delegate_object[:shell_code_label_format_below]
1240
1782
 
1241
1783
  [label_format_above && format(label_format_above,
1242
- block_source.merge({ block_name: selected[:oname] }))] +
1784
+ block_source.merge({ block_name: selected.pub_name }))] +
1243
1785
  output_lines.map do |line|
1244
1786
  re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
1245
1787
  re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line
1246
1788
  end.compact +
1247
1789
  [label_format_below && format(label_format_below,
1248
- block_source.merge({ block_name: selected[:oname] }))]
1790
+ block_source.merge({ block_name: selected.pub_name }))]
1249
1791
  end
1250
1792
 
1251
1793
  def link_history_push_and_next(
@@ -1275,6 +1817,54 @@ module MarkdownExec
1275
1817
  )
1276
1818
  end
1277
1819
 
1820
+ def link_load_format_data
1821
+ {
1822
+ batch_index: @run_state.batch_index,
1823
+ batch_random: @run_state.batch_random,
1824
+ block_name: @delegate_object[:block_name],
1825
+ document_filename: File.basename(@delegate_object[:filename]),
1826
+ document_filespec: @delegate_object[:filename],
1827
+ home: Dir.pwd,
1828
+ started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format])
1829
+ }
1830
+ end
1831
+
1832
+ # Loads auto blocks based on delegate object settings and updates if new filename is detected.
1833
+ # Executes a specified block once per filename.
1834
+ # @param all_blocks [Array] Array of all block elements.
1835
+ # @return [Boolean, nil] True if values were modified, nil otherwise.
1836
+ def load_auto_opts_block(all_blocks)
1837
+ block_name = @delegate_object[:document_load_opts_block_name]
1838
+ return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1839
+
1840
+ block = HashDelegator.block_find(all_blocks, :oname, block_name)
1841
+ return unless block
1842
+
1843
+ options_state = read_show_options_and_trigger_reuse(selected: block)
1844
+ @menu_base_options.merge!(options_state.options)
1845
+ @delegate_object.merge!(options_state.options)
1846
+
1847
+ @most_recent_loaded_filename = @delegate_object[:filename]
1848
+ true
1849
+ end
1850
+
1851
+ def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
1852
+ if @delegate_object[:block_name].present?
1853
+ block = all_blocks.find do |item|
1854
+ item.pub_name == @delegate_object[:block_name]
1855
+ end&.merge(block_name_from_ui: false)
1856
+ else
1857
+ block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
1858
+ default)
1859
+ block = block_state.block&.merge(block_name_from_ui: true)
1860
+ state = block_state.state
1861
+ end
1862
+
1863
+ SelectedBlockMenuState.new(block, state)
1864
+ rescue StandardError
1865
+ HashDelegator.error_handler('load_cli_or_user_selected_block')
1866
+ end
1867
+
1278
1868
  # format + glob + select for file in load block
1279
1869
  # name has references to ENV vars and doc and batch vars incl. timestamp
1280
1870
  def load_filespec_from_expression(expression)
@@ -1289,68 +1879,8 @@ module MarkdownExec
1289
1879
  end
1290
1880
  end
1291
1881
 
1292
- def save_filespec_from_expression(expression)
1293
- # Process expression with embedded formatting
1294
- formatted = formatted_expression(expression)
1295
-
1296
- # Handle wildcards or direct file specification
1297
- if contains_wildcards?(formatted)
1298
- save_filespec_wildcard_expansion(formatted)
1299
- else
1300
- formatted
1301
- end
1302
- end
1303
-
1304
1882
  # private
1305
1883
 
1306
- # Expand expression if it contains format specifiers
1307
- def formatted_expression(expr)
1308
- expr.include?('%{') ? format_expression(expr) : expr
1309
- end
1310
-
1311
- # Format expression using environment variables and run state
1312
- def format_expression(expr)
1313
- data = link_load_format_data
1314
- ENV.each { |key, value| data[key] = value }
1315
- format(expr, data)
1316
- end
1317
-
1318
- # Check if the expression contains wildcard characters
1319
- def contains_wildcards?(expr)
1320
- expr.match(%r{\*|\?|\[})
1321
- end
1322
-
1323
- # Handle expression with wildcard characters
1324
- def load_filespec_wildcard_expansion(expr)
1325
- files = find_files(expr)
1326
- case files.count
1327
- when 0
1328
- HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
1329
- when 1
1330
- files.first
1331
- else
1332
- prompt_select_code_filename(files)
1333
- end
1334
- end
1335
-
1336
- # Handle expression with wildcard characters
1337
- # allow user to select or enter
1338
- def puts_gets_oprompt_(filespec)
1339
- puts format(@delegate_object[:prompt_show_expr_format],
1340
- { expr: filespec })
1341
- puts @delegate_object[:prompt_enter_filespec]
1342
- gets.chomp
1343
- end
1344
-
1345
- # prompt user to enter a path (i.e. containing a path separator)
1346
- # or name to substitute into the wildcard expression
1347
- def prompt_for_filespec_with_wildcard(filespec)
1348
- puts format(@delegate_object[:prompt_show_expr_format],
1349
- { expr: filespec })
1350
- puts @delegate_object[:prompt_enter_filespec]
1351
- PathUtils.resolve_path_or_substitute(gets.chomp, filespec)
1352
- end
1353
-
1354
1884
  # def read_block_name(line)
1355
1885
  # bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
1356
1886
  # name = bm[:title]
@@ -1363,38 +1893,6 @@ module MarkdownExec
1363
1893
  # name
1364
1894
  # end
1365
1895
 
1366
- # Handle expression with wildcard characters
1367
- # allow user to select or enter
1368
- def save_filespec_wildcard_expansion(filespec)
1369
- files = find_files(filespec)
1370
- case files.count
1371
- when 0
1372
- prompt_for_filespec_with_wildcard(filespec)
1373
- else
1374
- ## user selects from existing files or other
1375
- # input into path with wildcard for easy entry
1376
- #
1377
- name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
1378
- if name == @delegate_object[:prompt_filespec_other]
1379
- prompt_for_filespec_with_wildcard(filespec)
1380
- else
1381
- name
1382
- end
1383
- end
1384
- end
1385
-
1386
- def link_load_format_data
1387
- {
1388
- batch_index: @run_state.batch_index,
1389
- batch_random: @run_state.batch_random,
1390
- block_name: @delegate_object[:block_name],
1391
- document_filename: File.basename(@delegate_object[:filename]),
1392
- document_filespec: @delegate_object[:filename],
1393
- home: Dir.pwd,
1394
- started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format])
1395
- }
1396
- end
1397
-
1398
1896
  # # Loads auto link block.
1399
1897
  # def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
1400
1898
  # block_name = @delegate_object[:document_load_link_block_name]
@@ -1418,23 +1916,23 @@ module MarkdownExec
1418
1916
  # end
1419
1917
  # end
1420
1918
 
1421
- # Loads auto blocks based on delegate object settings and updates if new filename is detected.
1422
- # Executes a specified block once per filename.
1423
- # @param all_blocks [Array] Array of all block elements.
1424
- # @return [Boolean, nil] True if values were modified, nil otherwise.
1425
- def load_auto_opts_block(all_blocks)
1426
- block_name = @delegate_object[:document_load_opts_block_name]
1427
- return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1428
-
1429
- block = HashDelegator.block_find(all_blocks, :oname, block_name)
1430
- return unless block
1431
-
1432
- options_state = read_show_options_and_trigger_reuse(selected: block)
1433
- @menu_base_options.merge!(options_state.options)
1434
- @delegate_object.merge!(options_state.options)
1435
-
1436
- @most_recent_loaded_filename = @delegate_object[:filename]
1437
- true
1919
+ # Handle expression with wildcard characters
1920
+ def load_filespec_wildcard_expansion(expr, auto_load_single: false)
1921
+ files = find_files(expr)
1922
+ if files.count.zero?
1923
+ HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
1924
+ elsif auto_load_single && files.count == 1
1925
+ files.first
1926
+ else
1927
+ ## user selects from existing files or other
1928
+ #
1929
+ case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
1930
+ when @delegate_object[:prompt_filespec_back]
1931
+ # do nothing
1932
+ else
1933
+ name
1934
+ end
1935
+ end
1438
1936
  end
1439
1937
 
1440
1938
  def mdoc_and_blocks_from_nested_files
@@ -1458,10 +1956,37 @@ module MarkdownExec
1458
1956
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1459
1957
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
1460
1958
  ### compress empty lines
1461
- HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true
1959
+ HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
1462
1960
  [all_blocks, menu_blocks, mdoc]
1463
1961
  end
1464
1962
 
1963
+ def menu_add_disabled_option(name)
1964
+ raise unless name.present?
1965
+ raise if @dml_menu_blocks.nil?
1966
+
1967
+ block = @dml_menu_blocks.find { |item| item[:oname] == name }
1968
+
1969
+ # create menu item when it is needed (count > 0)
1970
+ #
1971
+ return unless block.nil?
1972
+
1973
+ # append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD)
1974
+ chrome_block = FCB.new(
1975
+ chrome: true,
1976
+ disabled: '',
1977
+ dname: HashDelegator.new(@delegate_object).string_send_color(
1978
+ name, :menu_inherited_lines_color
1979
+ ),
1980
+ oname: formatted_name
1981
+ )
1982
+
1983
+ if insert_at_top
1984
+ @dml_menu_blocks.unshift(chrome_block)
1985
+ else
1986
+ @dml_menu_blocks.push(chrome_block)
1987
+ end
1988
+ end
1989
+
1465
1990
  # Formats and optionally colors a menu option based on delegate object's configuration.
1466
1991
  # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
1467
1992
  # @return [String] The formatted and possibly colored value of the menu option.
@@ -1486,6 +2011,47 @@ module MarkdownExec
1486
2011
  end
1487
2012
  end
1488
2013
 
2014
+ def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
2015
+ raise unless name.present?
2016
+ raise if @dml_menu_blocks.nil?
2017
+
2018
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
2019
+
2020
+ # create menu item when it is needed (count > 0)
2021
+ #
2022
+ if item.nil? && count.positive?
2023
+ append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
2024
+ item = @dml_menu_blocks.find { |block| block[:oname] == name }
2025
+ end
2026
+
2027
+ # update item if it exists
2028
+ #
2029
+ return unless item
2030
+
2031
+ item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name
2032
+ if count.positive?
2033
+ item.delete(:disabled)
2034
+ else
2035
+ item[:disabled] = ''
2036
+ end
2037
+ end
2038
+
2039
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
2040
+ if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2041
+ # &bsp 'pause cli control, allow user to select block'
2042
+ block_name_from_cli = false
2043
+ now_using_cli = false
2044
+ @menu_base_options[:block_name] = \
2045
+ @delegate_object[:block_name] = \
2046
+ link_state.block_name = \
2047
+ @cli_block_name = nil
2048
+ end
2049
+
2050
+ @delegate_object = @menu_base_options.dup
2051
+ @menu_user_clicked_back_link = false
2052
+ [block_name_from_cli, now_using_cli]
2053
+ end
2054
+
1489
2055
  # If a method is missing, treat it as a key for the @delegate_object.
1490
2056
  def method_missing(method_name, *args, &block)
1491
2057
  if @delegate_object.respond_to?(method_name)
@@ -1567,20 +2133,20 @@ module MarkdownExec
1567
2133
  @link_history.push(next_state)
1568
2134
 
1569
2135
  next_state.block_name = nil
1570
- LoadFileLinkState.new(LoadFile::Load, next_state)
2136
+ LoadFileLinkState.new(LoadFile::LOAD, next_state)
1571
2137
  else
1572
2138
  # no history exists; must have been called independently => retain script
1573
2139
  link_history_push_and_next(
1574
- curr_block_name: selected[:oname],
2140
+ curr_block_name: selected.pub_name,
1575
2141
  curr_document_filename: @delegate_object[:filename],
1576
2142
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1577
2143
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1578
2144
  inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1579
- next_block_name: '', # not link_block_data[LinkKeys::Block] || ''
2145
+ next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || ''
1580
2146
  next_document_filename: @delegate_object[:filename], # not next_document_filename
1581
- next_load_file: LoadFile::Reuse # not next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
2147
+ next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
1582
2148
  )
1583
- # LoadFileLinkState.new(LoadFile::Reuse, link_state)
2149
+ # LoadFileLinkState.new(LoadFile::REUSE, link_state)
1584
2150
  end
1585
2151
  end
1586
2152
 
@@ -1591,7 +2157,7 @@ module MarkdownExec
1591
2157
  def pop_link_history_and_trigger_load
1592
2158
  pop = @link_history.pop
1593
2159
  peek = @link_history.peek
1594
- LoadFileLinkState.new(LoadFile::Load, LinkState.new(
2160
+ LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
1595
2161
  document_filename: pop.document_filename,
1596
2162
  inherited_block_names: peek.inherited_block_names,
1597
2163
  inherited_dependencies: peek.inherited_dependencies,
@@ -1705,6 +2271,32 @@ module MarkdownExec
1705
2271
  exit 1
1706
2272
  end
1707
2273
 
2274
+ def prompt_for_command(prompt)
2275
+ print prompt
2276
+
2277
+ gets.chomp
2278
+ rescue Interrupt
2279
+ nil
2280
+ end
2281
+
2282
+ # Prompts the user to enter a path or name to substitute into the wildcard expression.
2283
+ # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
2284
+ #
2285
+ # @param filespec [String] the wildcard expression to be substituted
2286
+ # @return [String, nil] the resolved path or substituted expression, or nil if interrupted
2287
+ def prompt_for_filespec_with_wildcard(filespec)
2288
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
2289
+ puts @delegate_object[:prompt_enter_filespec]
2290
+
2291
+ begin
2292
+ input = gets.chomp
2293
+ PathUtils.resolve_path_or_substitute(input, filespec)
2294
+ rescue Interrupt
2295
+ puts "\nOperation interrupted. Returning nil."
2296
+ nil
2297
+ end
2298
+ end
2299
+
1708
2300
  ##
1709
2301
  # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1710
2302
  # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
@@ -1748,38 +2340,46 @@ module MarkdownExec
1748
2340
  exit 1
1749
2341
  end
1750
2342
 
1751
- def prompt_select_continue
1752
- sel = @prompt.select(
1753
- string_send_color(@delegate_object[:prompt_after_script_execution],
2343
+ # public
2344
+
2345
+ def prompt_select_code_filename(filenames)
2346
+ @prompt.select(
2347
+ string_send_color(@delegate_object[:prompt_select_code_file],
1754
2348
  :prompt_color_after_script_execution),
1755
2349
  filter: true,
1756
2350
  quiet: true
1757
2351
  ) do |menu|
1758
- menu.choice @delegate_object[:prompt_yes]
1759
- menu.choice @delegate_object[:prompt_exit]
2352
+ filenames.each do |filename|
2353
+ menu.choice filename
2354
+ end
1760
2355
  end
1761
- sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1762
2356
  rescue TTY::Reader::InputInterrupt
1763
2357
  exit 1
1764
2358
  end
1765
-
1766
- # public
1767
-
1768
- def prompt_select_code_filename(filenames)
1769
- @prompt.select(
1770
- string_send_color(@delegate_object[:prompt_select_code_file],
2359
+
2360
+ def prompt_select_continue
2361
+ sel = @prompt.select(
2362
+ string_send_color(@delegate_object[:prompt_after_script_execution],
1771
2363
  :prompt_color_after_script_execution),
1772
2364
  filter: true,
1773
2365
  quiet: true
1774
2366
  ) do |menu|
1775
- filenames.each do |filename|
1776
- menu.choice filename
1777
- end
2367
+ menu.choice @delegate_object[:prompt_yes]
2368
+ menu.choice @delegate_object[:prompt_exit]
1778
2369
  end
2370
+ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1779
2371
  rescue TTY::Reader::InputInterrupt
1780
2372
  exit 1
1781
2373
  end
1782
2374
 
2375
+ # user prompt to exit if the menu will be displayed again
2376
+ #
2377
+ def prompt_user_exit(block_name_from_cli:, selected:)
2378
+ selected[:shell] == BlockType::BASH &&
2379
+ @delegate_object[:pause_after_script_execution] &&
2380
+ prompt_select_continue == MenuState::EXIT
2381
+ end
2382
+
1783
2383
  # Handles the processing of a link block in Markdown Execution.
1784
2384
  # It loads YAML data from the link_block_body content, pushes the state to history,
1785
2385
  # sets environment variables, and decides on the next block to load.
@@ -1796,7 +2396,7 @@ module MarkdownExec
1796
2396
  #
1797
2397
  if mdoc
1798
2398
  code_info = mdoc.collect_recursively_required_code(
1799
- anyname: selected[:oname],
2399
+ anyname: selected.pub_name,
1800
2400
  label_format_above: @delegate_object[:shell_code_label_format_above],
1801
2401
  label_format_below: @delegate_object[:shell_code_label_format_below],
1802
2402
  block_source: block_source
@@ -1812,397 +2412,189 @@ module MarkdownExec
1812
2412
 
1813
2413
  # load key and values from link block into current environment
1814
2414
  #
1815
- if link_block_data[LinkKeys::Vars]
1816
- code_lines.push BashCommentFormatter.format_comment(selected[:oname])
1817
- (link_block_data[LinkKeys::Vars] || []).each do |(key, value)|
2415
+ if link_block_data[LinkKeys::VARS]
2416
+ code_lines.push BashCommentFormatter.format_comment(selected.pub_name)
2417
+ (link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
1818
2418
  ENV[key] = value.to_s
1819
2419
  code_lines.push(assign_key_value_in_bash(key, value))
1820
2420
  end
1821
- end
1822
-
1823
- ## append blocks loaded, apply LinkKeys::Eval
1824
- #
1825
- if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
1826
- load_filespec = load_filespec_from_expression(load_expr)
1827
- code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
1828
- end
1829
-
1830
- # if an eval link block, evaluate code_lines and return its standard output
1831
- #
1832
- if link_block_data.fetch(LinkKeys::Eval,
1833
- false) || link_block_data.fetch(LinkKeys::Exec, false)
1834
- code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
1835
- end
1836
-
1837
- next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
1838
-
1839
- if link_block_data[LinkKeys::Return]
1840
- pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
1841
- dependencies, selected)
1842
-
1843
- else
1844
- link_history_push_and_next(
1845
- curr_block_name: selected[:oname],
1846
- curr_document_filename: @delegate_object[:filename],
1847
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1848
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1849
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1850
- next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
1851
- nil) || link_block_data[LinkKeys::Block] || '',
1852
- next_document_filename: next_document_filename,
1853
- next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
1854
- )
1855
- end
1856
- end
1857
-
1858
- # Check if the delegate object responds to a given method.
1859
- # @param method_name [Symbol] The name of the method to check.
1860
- # @param include_private [Boolean] Whether to include private methods in the check.
1861
- # @return [Boolean] true if the delegate object responds to the method, false otherwise.
1862
- def respond_to?(method_name, include_private = false)
1863
- if super
1864
- true
1865
- elsif @delegate_object.respond_to?(method_name, include_private)
1866
- true
1867
- elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
1868
- true
1869
- else
1870
- @delegate_object.respond_to?(method_name, include_private)
1871
- end
1872
- end
1873
-
1874
- def runtime_exception(exception_sym, name, items)
1875
- if @delegate_object[exception_sym] != 0
1876
- data = { name: name, detail: items.join(', ') }
1877
- warn(
1878
- format(
1879
- @delegate_object.fetch(:exception_format_name, "\n%{name}"),
1880
- data
1881
- ).send(@delegate_object.fetch(:exception_color_name, :red)) +
1882
- format(
1883
- @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
1884
- data
1885
- ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
1886
- )
1887
- end
1888
- return unless (@delegate_object[exception_sym]).positive?
1889
-
1890
- exit @delegate_object[exception_sym]
1891
- end
1892
-
1893
- def save_to_file(required_lines:, selected:)
1894
- write_command_file(required_lines: required_lines, selected: selected)
1895
- @fout.fout "File saved: #{@run_state.saved_filespec}"
1896
- end
1897
-
1898
- def block_state_for_name_from_cli(block_name)
1899
- SelectedBlockMenuState.new(
1900
- @dml_blocks_in_file.find do |item|
1901
- item[:oname] == block_name
1902
- end&.merge(
1903
- block_name_from_cli: true,
1904
- block_name_from_ui: false
1905
- ),
1906
- MenuState::CONTINUE
1907
- )
1908
- end
1909
-
1910
- # Select and execute a code block from a Markdown document.
1911
- #
1912
- # This method allows the user to interactively select a code block from a
1913
- # Markdown document, obtain approval, and execute the chosen block of code.
1914
- #
1915
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1916
- def document_menu_loop
1917
- @menu_base_options = @delegate_object
1918
- @dml_link_state = LinkState.new(
1919
- block_name: @delegate_object[:block_name],
1920
- document_filename: @delegate_object[:filename]
1921
- )
1922
- @run_state.block_name_from_cli = @dml_link_state.block_name.present?
1923
- @cli_block_name = @dml_link_state.block_name
1924
- @dml_now_using_cli = @run_state.block_name_from_cli
1925
- @dml_menu_default_dname = nil
1926
- @dml_block_state = SelectedBlockMenuState.new
1927
-
1928
- @run_state.batch_random = Random.new.rand
1929
- @run_state.batch_index = 0
1930
-
1931
- InputSequencer.new(
1932
- @delegate_object[:filename],
1933
- @delegate_object[:input_cli_rest]
1934
- ).run do |msg, data|
1935
- case msg
1936
- when :parse_document # once for each menu
1937
- # puts "@ - parse document #{data}"
1938
- ii_parse_document(data)
1939
-
1940
- when :display_menu
1941
- # warn "@ - display menu:"
1942
- # ii_display_menu
1943
- @dml_block_state = SelectedBlockMenuState.new
1944
- @delegate_object[:block_name] = nil
1945
-
1946
- when :user_choice
1947
- if @dml_link_state.block_name.present?
1948
- # @prior_block_was_link = true
1949
- @dml_block_state.block = @dml_blocks_in_file.find do |item|
1950
- item[:oname] == @dml_link_state.block_name
1951
- end
1952
- @dml_link_state.block_name = nil
1953
- else
1954
- # puts "? - Select a block to execute (or type #{$texit} to exit):"
1955
- break if ii_user_choice == :break # into @dml_block_state
1956
- break if @dml_block_state.block.nil? # no block matched
1957
- end
1958
- # puts "! - Executing block: #{data}"
1959
- # @dml_block_state.block[:oname]
1960
- @dml_block_state.block&.fetch(:oname, nil)
1961
-
1962
- when :execute_block
1963
- block_name = data
1964
- if block_name == '* Back' ####
1965
- debounce_reset
1966
- @menu_user_clicked_back_link = true
1967
- load_file_link_state = pop_link_history_and_trigger_load
1968
- @dml_link_state = load_file_link_state.link_state
1969
-
1970
- InputSequencer.merge_link_state(
1971
- @dml_link_state,
1972
- InputSequencer.next_link_state(
1973
- block_name: @dml_link_state.block_name,
1974
- document_filename: @dml_link_state.document_filename,
1975
- prior_block_was_link: true
1976
- )
1977
- )
1978
-
1979
- else
1980
- @dml_block_state = block_state_for_name_from_cli(block_name)
1981
- if @dml_block_state.block[:shell] == BlockType::OPTS
1982
- debounce_reset
1983
- link_state = LinkState.new
1984
- options_state = read_show_options_and_trigger_reuse(
1985
- selected: @dml_block_state.block,
1986
- link_state: link_state
1987
- )
1988
-
1989
- @menu_base_options.merge!(options_state.options)
1990
- @delegate_object.merge!(options_state.options)
1991
- options_state.load_file_link_state.link_state
1992
- else
1993
- ii_execute_block(block_name)
1994
-
1995
- if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1996
- selected: @dml_block_state.block)
1997
- return :break
1998
- end
1999
-
2000
- ## order of block name processing: link block, cli, from user
2001
- #
2002
- @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
2003
- HashDelegator.next_link_state(
2004
- block_name: @dml_link_state.block_name,
2005
- block_name_from_cli: !@dml_link_state.block_name.present?,
2006
- block_state: @dml_block_state,
2007
- was_using_cli: @dml_now_using_cli
2008
- )
2009
-
2010
- if !@dml_block_state.block[:block_name_from_ui] && cli_break
2011
- # &bsp '!block_name_from_ui + cli_break -> break'
2012
- return :break
2013
- end
2014
-
2015
- InputSequencer.next_link_state(
2016
- block_name: @dml_link_state.block_name,
2017
- prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
2018
- )
2019
- end
2020
- end
2021
-
2022
- when :exit?
2023
- data == $texit
2024
- when :stay?
2025
- data == $stay
2026
- else
2027
- raise "Invalid message: #{msg}"
2028
- end
2029
- end
2030
- rescue StandardError
2031
- HashDelegator.error_handler('document_menu_loop',
2032
- { abort: true })
2033
- end
2034
-
2035
- def ii_parse_document(_document_filename)
2036
- @run_state.batch_index += 1
2037
- @run_state.in_own_window = false
2038
-
2039
- # &bsp 'loop', block_name_from_cli, @cli_block_name
2040
- @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
2041
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
2042
- now_using_cli: @dml_now_using_cli,
2043
- link_state: @dml_link_state)
2044
- end
2045
-
2046
- def ii_user_choice
2047
- @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
2048
- menu_blocks: @dml_menu_blocks,
2049
- default: @dml_menu_default_dname)
2050
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
2051
- if !@dml_block_state
2052
- HashDelegator.error_handler('block_state missing', { abort: true })
2053
- elsif @dml_block_state.state == MenuState::EXIT
2054
- # &bsp 'load_cli_or_user_selected_block -> break'
2055
- :break
2056
- end
2057
- end
2058
-
2059
- def ii_execute_block(block_name)
2060
- @dml_block_state = block_state_for_name_from_cli(block_name)
2061
-
2062
- dump_and_warn_block_state(selected: @dml_block_state.block)
2063
- @dml_link_state, @dml_menu_default_dname = \
2064
- exec_bash_next_state(
2065
- selected: @dml_block_state.block,
2066
- mdoc: @dml_mdoc,
2067
- link_state: @dml_link_state,
2068
- block_source: {
2069
- document_filename: @delegate_object[:filename],
2070
- time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
2071
- }
2072
- )
2073
- end
2074
-
2075
- def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
2076
- lfls = execute_shell_type(
2077
- selected: selected,
2078
- mdoc: mdoc,
2079
- link_state: link_state,
2080
- block_source: block_source
2081
- )
2082
-
2083
- # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
2084
- [lfls.link_state,
2085
- lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
2086
- end
2421
+ end
2087
2422
 
2088
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2089
- block_name_from_cli, now_using_cli = \
2090
- manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2091
- now_using_cli: now_using_cli,
2092
- link_state: link_state)
2093
- set_delob_filename_block_name(link_state: link_state,
2094
- block_name_from_cli: block_name_from_cli)
2423
+ ## append blocks loaded, apply LinkKeys::EVAL
2424
+ #
2425
+ if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
2426
+ load_filespec = load_filespec_from_expression(load_expr)
2427
+ code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
2428
+ end
2095
2429
 
2096
- # update @delegate_object and @menu_base_options in auto_load
2430
+ # if an eval link block, evaluate code_lines and return its standard output
2097
2431
  #
2098
- blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2099
- dump_delobj(blocks_in_file, menu_blocks, link_state)
2432
+ if link_block_data.fetch(LinkKeys::EVAL,
2433
+ false) || link_block_data.fetch(LinkKeys::EXEC, false)
2434
+ code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
2435
+ end
2100
2436
 
2101
- [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2437
+ next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
2438
+
2439
+ if link_block_data[LinkKeys::RETURN]
2440
+ pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
2441
+ dependencies, selected)
2442
+
2443
+ else
2444
+ link_history_push_and_next(
2445
+ curr_block_name: selected.pub_name,
2446
+ curr_document_filename: @delegate_object[:filename],
2447
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2448
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2449
+ inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2450
+ next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK,
2451
+ nil) || link_block_data[LinkKeys::BLOCK] || '',
2452
+ next_document_filename: next_document_filename,
2453
+ next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
2454
+ )
2455
+ end
2102
2456
  end
2103
2457
 
2104
- # user prompt to exit if the menu will be displayed again
2105
- #
2106
- def prompt_user_exit(block_name_from_cli:, selected:)
2107
- !block_name_from_cli &&
2108
- selected[:shell] == BlockType::BASH &&
2109
- @delegate_object[:pause_after_script_execution] &&
2110
- prompt_select_continue == MenuState::EXIT
2458
+ # Handle expression with wildcard characters
2459
+ # allow user to select or enter
2460
+ def puts_gets_oprompt_(filespec)
2461
+ puts format(@delegate_object[:prompt_show_expr_format],
2462
+ { expr: filespec })
2463
+ puts @delegate_object[:prompt_enter_filespec]
2464
+ gets.chomp
2111
2465
  end
2112
2466
 
2113
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
2114
- if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2115
- # &bsp 'pause cli control, allow user to select block'
2116
- block_name_from_cli = false
2117
- now_using_cli = false
2118
- @menu_base_options[:block_name] = \
2119
- @delegate_object[:block_name] = \
2120
- link_state.block_name = \
2121
- @cli_block_name = nil
2467
+ # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2468
+ # @param selected [Hash] Selected item from the menu containing a YAML body.
2469
+ # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2470
+ # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2471
+ def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2472
+ obj = {}
2473
+ data = YAML.load(selected[:body].join("\n"))
2474
+ (data || []).each do |key, value|
2475
+ sym_key = key.to_sym
2476
+ obj[sym_key] = value
2477
+
2478
+ print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2122
2479
  end
2123
2480
 
2124
- @delegate_object = @menu_base_options.dup
2125
- @menu_user_clicked_back_link = false
2126
- [block_name_from_cli, now_using_cli]
2481
+ link_state.block_name = nil
2482
+ OpenStruct.new(options: obj,
2483
+ load_file_link_state: LoadFileLinkState.new(
2484
+ LoadFile::REUSE, link_state
2485
+ ))
2127
2486
  end
2128
2487
 
2129
- # Update the block name in the link state and delegate object.
2130
- #
2131
- # This method updates the block name based on whether it was specified
2132
- # through the CLI or derived from the link state.
2133
- #
2134
- # @param link_state [LinkState] The current link state object.
2135
- # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2136
- def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2137
- @delegate_object[:filename] = link_state.document_filename
2138
- link_state.block_name = @delegate_object[:block_name] =
2139
- block_name_from_cli ? @cli_block_name : link_state.block_name
2488
+ def register_console_attributes(opts)
2489
+ unless opts[:console_width]
2490
+ require 'io/console'
2491
+ opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize
2492
+ end
2493
+ opts[:per_page] = opts[:select_page_height] = [opts[:console_height] - 3, 4].max unless opts[:select_page_height]&.positive?
2494
+ rescue StandardError
2495
+ HashDelegator.error_handler('register_console_attributes', { abort: true })
2140
2496
  end
2141
2497
 
2142
- # Outputs warnings based on the delegate object's configuration
2143
- #
2144
- # @param delegate_object [Hash] The delegate object containing configuration flags.
2145
- # @param blocks_in_file [Hash] Hash of blocks present in the file.
2146
- # @param menu_blocks [Hash] Hash of menu blocks.
2147
- # @param link_state [LinkState] Current state of the link.
2148
- def dump_delobj(blocks_in_file, menu_blocks, link_state)
2149
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
2150
-
2151
- if @delegate_object[:dump_blocks_in_file]
2152
- warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
2153
- label: 'blocks_in_file')
2498
+ # Check if the delegate object responds to a given method.
2499
+ # @param method_name [Symbol] The name of the method to check.
2500
+ # @param include_private [Boolean] Whether to include private methods in the check.
2501
+ # @return [Boolean] true if the delegate object responds to the method, false otherwise.
2502
+ def respond_to?(method_name, include_private = false)
2503
+ if super
2504
+ true
2505
+ elsif @delegate_object.respond_to?(method_name, include_private)
2506
+ true
2507
+ elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
2508
+ true
2509
+ else
2510
+ @delegate_object.respond_to?(method_name, include_private)
2154
2511
  end
2512
+ end
2155
2513
 
2156
- if @delegate_object[:dump_menu_blocks]
2157
- warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
2158
- label: 'menu_blocks')
2514
+ def runtime_exception(exception_sym, name, items)
2515
+ if @delegate_object[exception_sym] != 0
2516
+ data = { name: name, detail: items.join(', ') }
2517
+ warn(
2518
+ format(
2519
+ @delegate_object.fetch(:exception_format_name, "\n%{name}"),
2520
+ data
2521
+ ).send(@delegate_object.fetch(:exception_color_name, :red)) +
2522
+ format(
2523
+ @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
2524
+ data
2525
+ ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
2526
+ )
2159
2527
  end
2528
+ return unless (@delegate_object[exception_sym]).positive?
2160
2529
 
2161
- warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
2162
- warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
2163
- return unless @delegate_object[:dump_inherited_lines]
2164
-
2165
- warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
2530
+ exit @delegate_object[exception_sym]
2166
2531
  end
2167
2532
 
2168
- def dump_and_warn_block_state(selected:)
2169
- if selected.nil?
2170
- Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
2171
- { abort: true })
2533
+ # allow user to select or enter
2534
+ def save_filespec_from_expression(expression)
2535
+ # Process expression with embedded formatting
2536
+ formatted = formatted_expression(expression)
2537
+
2538
+ # Handle wildcards or direct file specification
2539
+ if contains_wildcards?(formatted)
2540
+ save_filespec_wildcard_expansion(formatted)
2541
+ else
2542
+ formatted
2172
2543
  end
2544
+ end
2173
2545
 
2174
- return unless @delegate_object[:dump_selected_block]
2546
+ # Handle expression with wildcard characters
2547
+ # allow user to select or enter
2548
+ def save_filespec_wildcard_expansion(filespec)
2549
+ files = find_files(filespec)
2550
+ case files.count
2551
+ when 0
2552
+ prompt_for_filespec_with_wildcard(filespec)
2553
+ else
2554
+ ## user selects from existing files or other
2555
+ # input into path with wildcard for easy entry
2556
+ #
2557
+ name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
2558
+ case name
2559
+ when @delegate_object[:prompt_filespec_back]
2560
+ # do nothing
2561
+ when @delegate_object[:prompt_filespec_other]
2562
+ prompt_for_filespec_with_wildcard(filespec)
2563
+ else
2564
+ name
2565
+ end
2566
+ end
2567
+ end
2175
2568
 
2176
- warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
2569
+ def save_to_file(required_lines:, selected:)
2570
+ write_command_file(required_lines: required_lines, selected: selected)
2571
+ @fout.fout "File saved: #{@run_state.saved_filespec}"
2177
2572
  end
2178
2573
 
2179
2574
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
2180
2575
  def select_option_with_metadata(prompt_text, names, opts = {})
2181
2576
  ## configure to environment
2182
2577
  #
2183
- unless opts[:select_page_height].positive?
2184
- require 'io/console'
2185
- opts[:per_page] = opts[:select_page_height] = [IO.console.winsize[0] - 3, 4].max
2186
- end
2578
+ register_console_attributes(opts)
2187
2579
 
2188
2580
  # crashes if all menu options are disabled
2189
2581
  selection = @prompt.select(prompt_text,
2190
2582
  names,
2191
2583
  opts.merge(filter: true))
2192
- item = names.find do |item|
2584
+ selected_name = names.find do |item|
2193
2585
  if item.instance_of?(Hash)
2194
2586
  item[:dname] == selection
2195
2587
  else
2196
2588
  item == selection
2197
2589
  end
2198
2590
  end
2199
- item = { dname: item } if item.instance_of?(String)
2200
- unless item
2591
+ selected_name = { dname: selected_name } if selected_name.instance_of?(String)
2592
+ unless selected_name
2201
2593
  HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
2202
2594
  exit 1
2203
2595
  end
2204
2596
 
2205
- item.merge(
2597
+ selected_name.merge(
2206
2598
  if selection == menu_chrome_colored_option(:menu_option_back_name)
2207
2599
  { option: selection, shell: BlockType::LINK }
2208
2600
  elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
@@ -2217,6 +2609,35 @@ module MarkdownExec
2217
2609
  HashDelegator.error_handler('select_option_with_metadata')
2218
2610
  end
2219
2611
 
2612
+ # Update the block name in the link state and delegate object.
2613
+ #
2614
+ # This method updates the block name based on whether it was specified
2615
+ # through the CLI or derived from the link state.
2616
+ #
2617
+ # @param link_state [LinkState] The current link state object.
2618
+ # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2619
+ def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2620
+ @delegate_object[:filename] = link_state.document_filename
2621
+ link_state.block_name = @delegate_object[:block_name] =
2622
+ block_name_from_cli ? @cli_block_name : link_state.block_name
2623
+ end
2624
+
2625
+ def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2626
+ block_name_from_cli, now_using_cli = \
2627
+ manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2628
+ now_using_cli: now_using_cli,
2629
+ link_state: link_state)
2630
+ set_delob_filename_block_name(link_state: link_state,
2631
+ block_name_from_cli: block_name_from_cli)
2632
+
2633
+ # update @delegate_object and @menu_base_options in auto_load
2634
+ #
2635
+ blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2636
+ dump_delobj(blocks_in_file, menu_blocks, link_state)
2637
+
2638
+ [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2639
+ end
2640
+
2220
2641
  def set_environment_variables_for_block(selected)
2221
2642
  code_lines = []
2222
2643
  YAML.load(selected[:body].join("\n"))&.each do |key, value|
@@ -2352,31 +2773,12 @@ module MarkdownExec
2352
2773
  end
2353
2774
  end
2354
2775
 
2355
- # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2356
- # @param selected [Hash] Selected item from the menu containing a YAML body.
2357
- # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2358
- # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2359
- def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2360
- obj = {}
2361
- data = YAML.load(selected[:body].join("\n"))
2362
- (data || []).each do |key, value|
2363
- sym_key = key.to_sym
2364
- obj[sym_key] = value
2365
-
2366
- print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2367
- end
2368
-
2369
- link_state.block_name = nil
2370
- OpenStruct.new(options: obj,
2371
- load_file_link_state: LoadFileLinkState.new(
2372
- LoadFile::Reuse, link_state
2373
- ))
2374
- end
2375
-
2376
2776
  def wait_for_stream_processing
2377
2777
  @process_mutex.synchronize do
2378
2778
  @process_cv.wait(@process_mutex)
2379
2779
  end
2780
+ rescue Interrupt
2781
+ # user interrupts process
2380
2782
  end
2381
2783
 
2382
2784
  def wait_for_user_selected_block(all_blocks, menu_blocks, default)
@@ -2417,7 +2819,7 @@ module MarkdownExec
2417
2819
  time_now = Time.now.utc
2418
2820
  @run_state.saved_script_filename =
2419
2821
  SavedAsset.script_name(
2420
- blockname: selected[:nickname] || selected[:oname],
2822
+ blockname: selected.pub_name,
2421
2823
  filename: @delegate_object[:filename],
2422
2824
  prefix: @delegate_object[:saved_script_filename_prefix],
2423
2825
  time: time_now
@@ -2447,14 +2849,34 @@ module MarkdownExec
2447
2849
  HashDelegator.error_handler('write_command_file')
2448
2850
  end
2449
2851
 
2852
+ # Ensure the directory exists before writing the file
2853
+ def write_file_with_directory_creation(save_filespec, content)
2854
+ directory = File.dirname(save_filespec)
2855
+
2856
+ begin
2857
+ FileUtils.mkdir_p(directory)
2858
+ File.write(save_filespec, content)
2859
+ rescue Errno::EACCES
2860
+ warn "Permission denied: Unable to write to file '#{save_filespec}'"
2861
+ nil
2862
+ rescue Errno::EROFS
2863
+ warn "Read-only file system: Unable to write to file '#{save_filespec}'"
2864
+ nil
2865
+ rescue StandardError => err
2866
+ warn "An error occurred while writing to file '#{save_filespec}': #{err.message}"
2867
+ nil
2868
+ end
2869
+ end
2870
+
2871
+ # return next document file name
2450
2872
  def write_inherited_lines_to_file(link_state, link_block_data)
2451
- save_expr = link_block_data.fetch(LinkKeys::Save, '')
2873
+ save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
2452
2874
  if save_expr.present?
2453
2875
  save_filespec = save_filespec_from_expression(save_expr)
2454
2876
  File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
2455
2877
  @delegate_object[:filename]
2456
2878
  else
2457
- link_block_data[LinkKeys::File] || @delegate_object[:filename]
2879
+ link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
2458
2880
  end
2459
2881
  end
2460
2882
  end
@@ -2595,21 +3017,21 @@ module MarkdownExec
2595
3017
 
2596
3018
  # Test case for empty body
2597
3019
  def test_push_link_history_and_trigger_load_with_empty_body
2598
- assert_equal LoadFile::Reuse,
3020
+ assert_equal LoadFile::REUSE,
2599
3021
  @hd.push_link_history_and_trigger_load.load_file
2600
3022
  end
2601
3023
 
2602
3024
  # Test case for non-empty body without 'file' key
2603
3025
  def test_push_link_history_and_trigger_load_without_file_key
2604
3026
  body = ["vars:\n KEY: VALUE"]
2605
- assert_equal LoadFile::Reuse,
3027
+ assert_equal LoadFile::REUSE,
2606
3028
  @hd.push_link_history_and_trigger_load(link_block_body: body).load_file
2607
3029
  end
2608
3030
 
2609
3031
  # Test case for non-empty body with 'file' key
2610
3032
  def test_push_link_history_and_trigger_load_with_file_key
2611
3033
  body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
2612
- expected_result = LoadFileLinkState.new(LoadFile::Load,
3034
+ expected_result = LoadFileLinkState.new(LoadFile::LOAD,
2613
3035
  LinkState.new(block_name: 'sample_block',
2614
3036
  document_filename: 'sample_file',
2615
3037
  inherited_dependencies: {},
@@ -2647,8 +3069,9 @@ module MarkdownExec
2647
3069
  end
2648
3070
 
2649
3071
  def test_safeval_rescue_from_error
2650
- HashDelegator.stubs(:error_handler).with('safeval')
2651
- assert_nil HashDelegator.safeval('invalid code')
3072
+ assert_raises(SystemExit) do
3073
+ HashDelegator.safeval('invalid_code_raises_exception')
3074
+ end
2652
3075
  end
2653
3076
 
2654
3077
  def test_set_fcb_title
@@ -2755,7 +3178,6 @@ module MarkdownExec
2755
3178
 
2756
3179
  def test_blocks_from_nested_files
2757
3180
  result = @hd.blocks_from_nested_files
2758
-
2759
3181
  assert_kind_of Array, result
2760
3182
  assert_kind_of FCB, result.first
2761
3183
  end
@@ -3054,7 +3476,7 @@ module MarkdownExec
3054
3476
 
3055
3477
  # Asserting the result is an instance of LoadFileLinkState
3056
3478
  assert_instance_of LoadFileLinkState, result
3057
- assert_equal LoadFile::Load, result.load_file
3479
+ assert_equal LoadFile::LOAD, result.load_file
3058
3480
  assert_nil result.link_state.block_name
3059
3481
  end
3060
3482
  end
@@ -3441,4 +3863,60 @@ module MarkdownExec
3441
3863
  assert_equal expected, BashCommentFormatter.format_comment(input)
3442
3864
  end
3443
3865
  end
3866
+
3867
+ class PromptForFilespecWithWildcardTest < Minitest::Test
3868
+ def setup
3869
+ @delegate_object = {
3870
+ prompt_show_expr_format: 'Current expression: %{expr}',
3871
+ prompt_enter_filespec: 'Please enter a filespec:'
3872
+ }
3873
+ @original_stdin = $stdin
3874
+ end
3875
+
3876
+ def teardown
3877
+ $stdin = @original_stdin
3878
+ end
3879
+
3880
+ def test_prompt_for_filespec_with_normal_input
3881
+ $stdin = StringIO.new("test_input\n")
3882
+ result = prompt_for_filespec_with_wildcard('*.txt')
3883
+ assert_equal 'resolved_path_or_substituted_value', result
3884
+ end
3885
+
3886
+ def test_prompt_for_filespec_with_interruption
3887
+ $stdin = StringIO.new
3888
+ # rubocop disable:Lint/NestedMethodDefinition
3889
+ def $stdin.gets; raise Interrupt; end
3890
+ # rubocop enable:Lint/NestedMethodDefinition
3891
+
3892
+ result = prompt_for_filespec_with_wildcard('*.txt')
3893
+ assert_nil result
3894
+ end
3895
+
3896
+ def test_prompt_for_filespec_with_empty_input
3897
+ $stdin = StringIO.new("\n")
3898
+ result = prompt_for_filespec_with_wildcard('*.txt')
3899
+ assert_equal 'resolved_path_or_substituted_value', result
3900
+ end
3901
+
3902
+ private
3903
+
3904
+ def prompt_for_filespec_with_wildcard(filespec)
3905
+ puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
3906
+ puts @delegate_object[:prompt_enter_filespec]
3907
+
3908
+ begin
3909
+ input = gets.chomp
3910
+ PathUtils.resolve_path_or_substitute(input, filespec)
3911
+ rescue Interrupt
3912
+ nil
3913
+ end
3914
+ end
3915
+
3916
+ module PathUtils
3917
+ def self.resolve_path_or_substitute(input, filespec)
3918
+ 'resolved_path_or_substituted_value' # Placeholder implementation
3919
+ end
3920
+ end
3921
+ end
3444
3922
  end # module MarkdownExec