markdown_exec 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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