markdown_exec 2.0.6 → 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.
@@ -237,6 +237,22 @@ module HashDelegatorSelf
237
237
  # @param str [String] The string to be evaluated.
238
238
  # @return [Object] The result of evaluating the string.
239
239
  def safeval(str)
240
+ # # Restricting to evaluate only expressions
241
+ # unless str.match?(/\A\s*\w+\s*[\+\-\*\/\=\%\&\|\<\>\!]+\s*\w+\s*\z/)
242
+ # error_handler('safeval') # 'Invalid expression'
243
+ # return
244
+ # end
245
+
246
+ # # Whitelisting allowed operations
247
+ # allowed_methods = %w[+ - * / == != < > <= >= && || % & |]
248
+ # unless allowed_methods.any? { |op| str.include?(op) }
249
+ # error_handler('safeval', 'Operation not allowed')
250
+ # return
251
+ # end
252
+
253
+ # # Sanitize input (example: removing potentially harmful characters)
254
+ # str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
255
+ # Evaluate the sanitized string
240
256
  result = nil
241
257
  binding.eval("result = #{str}")
242
258
 
@@ -248,6 +264,16 @@ module HashDelegatorSelf
248
264
  exit 1
249
265
  end
250
266
 
267
+ # # Evaluates the given string as Ruby code and rescues any StandardErrors.
268
+ # # If an error occurs, it calls the error_handler method with 'safeval'.
269
+ # # @param str [String] The string to be evaluated.
270
+ # # @return [Object] The result of evaluating the string.
271
+ # def safeval(str)
272
+ # eval(str)
273
+ # rescue StandardError # catches NameError, StandardError
274
+ # error_handler('safeval')
275
+ # end
276
+
251
277
  def set_file_permissions(file_path, chmod_value)
252
278
  File.chmod(chmod_value, file_path)
253
279
  end
@@ -388,6 +414,74 @@ class BashCommentFormatter
388
414
  # end
389
415
  end
390
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
+
391
485
  module MarkdownExec
392
486
  class DebugHelper
393
487
  # Class-level variable to store history of printed messages
@@ -497,6 +591,9 @@ module MarkdownExec
497
591
  when MenuState::SAVE
498
592
  option_name = @delegate_object[:menu_option_save_name]
499
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]
500
597
  when MenuState::VIEW
501
598
  option_name = @delegate_object[:menu_option_view_name]
502
599
  insert_at_top = @delegate_object[:menu_load_at_top]
@@ -595,6 +692,8 @@ module MarkdownExec
595
692
  #
596
693
  # @return [Array<FCB>] An array of FCB objects representing the blocks.
597
694
  def blocks_from_nested_files
695
+ register_console_attributes(@delegate_object)
696
+
598
697
  blocks = []
599
698
  iter_blocks_from_nested_files do |btype, fcb|
600
699
  process_block_based_on_type(blocks, btype, fcb)
@@ -605,10 +704,12 @@ module MarkdownExec
605
704
  HashDelegator.error_handler('blocks_from_nested_files')
606
705
  end
607
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
608
709
  def block_state_for_name_from_cli(block_name)
609
710
  SelectedBlockMenuState.new(
610
711
  @dml_blocks_in_file.find do |item|
611
- item[:oname] == block_name
712
+ block_name == item.pub_name
612
713
  end&.merge(
613
714
  block_name_from_cli: true,
614
715
  block_name_from_ui: false
@@ -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::STD_OUT) do |line|
729
- yield nil, line, nil, exec_thr if block_given?
730
- end
731
- handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) 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::STD_IN) 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
@@ -761,6 +830,24 @@ module MarkdownExec
761
830
  @fout.fout "Error ENOENT: #{err.inspect}"
762
831
  end
763
832
 
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
+ }
849
+ end
850
+
764
851
  # This method is responsible for handling the execution of generic blocks in a markdown document.
765
852
  # It collects the required code lines from the document and, depending on the configuration,
766
853
  # may display the code for user approval before execution. It then executes the approved block.
@@ -816,16 +903,70 @@ module MarkdownExec
816
903
  # @param match_data [MatchData] The match data containing named captures for formatting.
817
904
  # @param format_option [String] The format string to be used for the new block.
818
905
  # @param color_method [Symbol] The color method to apply to the block's display name.
819
- def create_and_add_chrome_block(blocks:, match_data:, format_option:,
820
- color_method:)
821
- oname = format(format_option,
822
- match_data.named_captures.transform_keys(&:to_sym))
823
- blocks.push FCB.new(
824
- chrome: true,
825
- disabled: '',
826
- dname: oname.send(color_method),
827
- oname: oname
828
- )
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
829
970
  end
830
971
 
831
972
  ##
@@ -836,12 +977,12 @@ module MarkdownExec
836
977
  # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
837
978
  def create_and_add_chrome_blocks(blocks, fcb)
838
979
  match_criteria = [
839
- { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match },
840
- { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match },
841
- { 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 },
842
983
  { color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match },
843
- { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match },
844
- { 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 }
845
986
  ]
846
987
  # rubocop:enable Style/UnlessElse
847
988
  match_criteria.each do |criteria|
@@ -852,9 +993,12 @@ module MarkdownExec
852
993
 
853
994
  create_and_add_chrome_block(
854
995
  blocks: blocks,
855
- match_data: mbody,
996
+ case_conversion: criteria[:case_conversion],
997
+ center: criteria[:center],
998
+ color_method: @delegate_object[criteria[:color]].to_sym,
856
999
  format_option: @delegate_object[criteria[:format]],
857
- color_method: @delegate_object[criteria[:color]].to_sym
1000
+ match_data: mbody,
1001
+ wrap: criteria[:wrap]
858
1002
  )
859
1003
  break
860
1004
  end
@@ -950,6 +1094,7 @@ module MarkdownExec
950
1094
  block_name: @delegate_object[:block_name],
951
1095
  document_filename: @delegate_object[:filename]
952
1096
  )
1097
+ # @dml_link_state_block_name_from_cli = @dml_link_state.block_name.present? ###
953
1098
  @run_state.block_name_from_cli = @dml_link_state.block_name.present?
954
1099
  @cli_block_name = @dml_link_state.block_name
955
1100
  @dml_now_using_cli = @run_state.block_name_from_cli
@@ -975,6 +1120,7 @@ module MarkdownExec
975
1120
  item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
976
1121
  item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
977
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]))
978
1124
  item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
979
1125
 
980
1126
  @run_state.batch_random = Random.new.rand
@@ -999,10 +1145,11 @@ module MarkdownExec
999
1145
 
1000
1146
  # add menu items (glob, load, save) and enable selectively
1001
1147
  menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
1002
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD)
1003
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT)
1004
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), lines_count, 'lines', menu_state: MenuState::SAVE)
1005
- menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), lines_count, 'lines', menu_state: MenuState::VIEW)
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]
1006
1153
  end
1007
1154
 
1008
1155
  when :display_menu
@@ -1015,7 +1162,7 @@ module MarkdownExec
1015
1162
  if @dml_link_state.block_name.present?
1016
1163
  # @prior_block_was_link = true
1017
1164
  @dml_block_state.block = @dml_blocks_in_file.find do |item|
1018
- item[:oname] == @dml_link_state.block_name
1165
+ item.pub_name == @dml_link_state.block_name
1019
1166
  end
1020
1167
  @dml_link_state.block_name = nil
1021
1168
  else
@@ -1024,8 +1171,7 @@ module MarkdownExec
1024
1171
  break if @dml_block_state.block.nil? # no block matched
1025
1172
  end
1026
1173
  # puts "! - Executing block: #{data}"
1027
- # @dml_block_state.block[:oname]
1028
- @dml_block_state.block&.fetch(:oname, nil)
1174
+ @dml_block_state.block&.pub_name
1029
1175
 
1030
1176
  when :execute_block
1031
1177
  case (block_name = data)
@@ -1045,6 +1191,7 @@ module MarkdownExec
1045
1191
  )
1046
1192
 
1047
1193
  when item_edit
1194
+ debounce_reset
1048
1195
  edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
1049
1196
  @dml_link_state.inherited_lines = edited.split("\n") if edited
1050
1197
  InputSequencer.next_link_state(prior_block_was_link: true)
@@ -1070,10 +1217,28 @@ module MarkdownExec
1070
1217
  return :break
1071
1218
 
1072
1219
  end
1220
+ InputSequencer.next_link_state(prior_block_was_link: true)
1073
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
1074
1238
  InputSequencer.next_link_state(prior_block_was_link: true)
1075
1239
 
1076
1240
  when item_view
1241
+ debounce_reset
1077
1242
  warn @dml_link_state.inherited_lines.join("\n")
1078
1243
  InputSequencer.next_link_state(prior_block_was_link: true)
1079
1244
 
@@ -1103,7 +1268,7 @@ module MarkdownExec
1103
1268
  @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1104
1269
  HashDelegator.next_link_state(
1105
1270
  block_name: @dml_link_state.block_name,
1106
- block_name_from_cli: !@dml_link_state.block_name.present?,
1271
+ block_name_from_cli: @dml_now_using_cli,
1107
1272
  block_state: @dml_block_state,
1108
1273
  was_using_cli: @dml_now_using_cli
1109
1274
  )
@@ -1244,6 +1409,53 @@ module MarkdownExec
1244
1409
  #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1245
1410
  end
1246
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
+
1247
1459
  # Executes a block of code that has been approved for execution.
1248
1460
  # It sets the script block name, writes command files if required, and handles the execution
1249
1461
  # including output formatting and summarization.
@@ -1294,7 +1506,7 @@ module MarkdownExec
1294
1506
  ### options_state.load_file_link_state
1295
1507
  link_state = LinkState.new
1296
1508
  link_history_push_and_next(
1297
- curr_block_name: selected[:oname],
1509
+ curr_block_name: selected.pub_name,
1298
1510
  curr_document_filename: @delegate_object[:filename],
1299
1511
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1300
1512
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -1310,7 +1522,7 @@ module MarkdownExec
1310
1522
  code_lines = set_environment_variables_for_block(selected)
1311
1523
  dependencies = {}
1312
1524
  link_history_push_and_next(
1313
- curr_block_name: selected[:oname],
1525
+ curr_block_name: selected.pub_name,
1314
1526
  curr_document_filename: @delegate_object[:filename],
1315
1527
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1316
1528
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -1437,7 +1649,7 @@ module MarkdownExec
1437
1649
  return
1438
1650
  end
1439
1651
 
1440
- @delegate_object[:block_name] = block_state.block[:oname]
1652
+ @delegate_object[:block_name] = block_state.block.pub_name
1441
1653
  @menu_user_clicked_back_link = block_state.state == MenuState::BACK
1442
1654
  end
1443
1655
 
@@ -1446,7 +1658,7 @@ module MarkdownExec
1446
1658
  Thread.new do
1447
1659
  stream.each_line do |line|
1448
1660
  line.strip!
1449
- @run_state.files[file_type] << line
1661
+ @run_state.files[file_type] << line if @run_state.files
1450
1662
 
1451
1663
  if @delegate_object[:output_stdout]
1452
1664
  # print line
@@ -1546,23 +1758,7 @@ module MarkdownExec
1546
1758
 
1547
1759
  if link_block_data.fetch(LinkKeys::EXEC, false)
1548
1760
  @run_state.files = Hash.new([])
1549
-
1550
- Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
1551
- handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
1552
- output_lines.push(line)
1553
- end
1554
- handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
1555
- output_lines.push(line)
1556
- end
1557
-
1558
- in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
1559
- stdin.puts(line)
1560
- end
1561
-
1562
- wait_for_stream_processing
1563
- sleep 0.1
1564
- in_thr.kill if in_thr&.alive?
1565
- end
1761
+ execute_command_with_streams([cmd])
1566
1762
 
1567
1763
  ## select output_lines that look like assignment or match other specs
1568
1764
  #
@@ -1585,13 +1781,13 @@ module MarkdownExec
1585
1781
  label_format_below = @delegate_object[:shell_code_label_format_below]
1586
1782
 
1587
1783
  [label_format_above && format(label_format_above,
1588
- block_source.merge({ block_name: selected[:oname] }))] +
1784
+ block_source.merge({ block_name: selected.pub_name }))] +
1589
1785
  output_lines.map do |line|
1590
1786
  re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
1591
1787
  re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line
1592
1788
  end.compact +
1593
1789
  [label_format_below && format(label_format_below,
1594
- block_source.merge({ block_name: selected[:oname] }))]
1790
+ block_source.merge({ block_name: selected.pub_name }))]
1595
1791
  end
1596
1792
 
1597
1793
  def link_history_push_and_next(
@@ -1655,7 +1851,7 @@ module MarkdownExec
1655
1851
  def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
1656
1852
  if @delegate_object[:block_name].present?
1657
1853
  block = all_blocks.find do |item|
1658
- item[:oname] == @delegate_object[:block_name]
1854
+ item.pub_name == @delegate_object[:block_name]
1659
1855
  end&.merge(block_name_from_ui: false)
1660
1856
  else
1661
1857
  block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
@@ -1682,6 +1878,44 @@ module MarkdownExec
1682
1878
  expanded_expression
1683
1879
  end
1684
1880
  end
1881
+
1882
+ # private
1883
+
1884
+ # def read_block_name(line)
1885
+ # bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
1886
+ # name = bm[:title]
1887
+
1888
+ # if @delegate_object[:block_name_nick_match].present? && line =~ Regexp.new(@delegate_object[:block_name_nick_match])
1889
+ # name = $~[0]
1890
+ # else
1891
+ # name = bm && bm[1] ? bm[:title] : name
1892
+ # end
1893
+ # name
1894
+ # end
1895
+
1896
+ # # Loads auto link block.
1897
+ # def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
1898
+ # block_name = @delegate_object[:document_load_link_block_name]
1899
+ # return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1900
+
1901
+ # block = HashDelegator.block_find(all_blocks, :oname, block_name)
1902
+ # return unless block
1903
+
1904
+ # if block.fetch(:shell, '') != BlockType::LINK
1905
+ # HashDelegator.error_handler('must be Link block type', { abort: true })
1906
+
1907
+ # else
1908
+ # # debounce_reset
1909
+ # push_link_history_and_trigger_load(
1910
+ # link_block_body: block.fetch(:body, ''),
1911
+ # mdoc: mdoc,
1912
+ # selected: block,
1913
+ # link_state: link_state,
1914
+ # block_source: block_source
1915
+ # )
1916
+ # end
1917
+ # end
1918
+
1685
1919
  # Handle expression with wildcard characters
1686
1920
  def load_filespec_wildcard_expansion(expr, auto_load_single: false)
1687
1921
  files = find_files(expr)
@@ -1717,6 +1951,7 @@ module MarkdownExec
1717
1951
  # recreate menu with new options
1718
1952
  #
1719
1953
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
1954
+ # load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
1720
1955
 
1721
1956
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1722
1957
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
@@ -1793,7 +2028,7 @@ module MarkdownExec
1793
2028
  #
1794
2029
  return unless item
1795
2030
 
1796
- item[:dname] = "#{name} (#{count} #{type})"
2031
+ item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name
1797
2032
  if count.positive?
1798
2033
  item.delete(:disabled)
1799
2034
  else
@@ -1902,7 +2137,7 @@ module MarkdownExec
1902
2137
  else
1903
2138
  # no history exists; must have been called independently => retain script
1904
2139
  link_history_push_and_next(
1905
- curr_block_name: selected[:oname],
2140
+ curr_block_name: selected.pub_name,
1906
2141
  curr_document_filename: @delegate_object[:filename],
1907
2142
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1908
2143
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -2036,6 +2271,14 @@ module MarkdownExec
2036
2271
  exit 1
2037
2272
  end
2038
2273
 
2274
+ def prompt_for_command(prompt)
2275
+ print prompt
2276
+
2277
+ gets.chomp
2278
+ rescue Interrupt
2279
+ nil
2280
+ end
2281
+
2039
2282
  # Prompts the user to enter a path or name to substitute into the wildcard expression.
2040
2283
  # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
2041
2284
  #
@@ -2132,8 +2375,7 @@ module MarkdownExec
2132
2375
  # user prompt to exit if the menu will be displayed again
2133
2376
  #
2134
2377
  def prompt_user_exit(block_name_from_cli:, selected:)
2135
- !block_name_from_cli &&
2136
- selected[:shell] == BlockType::BASH &&
2378
+ selected[:shell] == BlockType::BASH &&
2137
2379
  @delegate_object[:pause_after_script_execution] &&
2138
2380
  prompt_select_continue == MenuState::EXIT
2139
2381
  end
@@ -2154,7 +2396,7 @@ module MarkdownExec
2154
2396
  #
2155
2397
  if mdoc
2156
2398
  code_info = mdoc.collect_recursively_required_code(
2157
- anyname: selected[:oname],
2399
+ anyname: selected.pub_name,
2158
2400
  label_format_above: @delegate_object[:shell_code_label_format_above],
2159
2401
  label_format_below: @delegate_object[:shell_code_label_format_below],
2160
2402
  block_source: block_source
@@ -2171,7 +2413,7 @@ module MarkdownExec
2171
2413
  # load key and values from link block into current environment
2172
2414
  #
2173
2415
  if link_block_data[LinkKeys::VARS]
2174
- code_lines.push BashCommentFormatter.format_comment(selected[:oname])
2416
+ code_lines.push BashCommentFormatter.format_comment(selected.pub_name)
2175
2417
  (link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
2176
2418
  ENV[key] = value.to_s
2177
2419
  code_lines.push(assign_key_value_in_bash(key, value))
@@ -2200,7 +2442,7 @@ module MarkdownExec
2200
2442
 
2201
2443
  else
2202
2444
  link_history_push_and_next(
2203
- curr_block_name: selected[:oname],
2445
+ curr_block_name: selected.pub_name,
2204
2446
  curr_document_filename: @delegate_object[:filename],
2205
2447
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2206
2448
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -2243,6 +2485,16 @@ module MarkdownExec
2243
2485
  ))
2244
2486
  end
2245
2487
 
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 })
2496
+ end
2497
+
2246
2498
  # Check if the delegate object responds to a given method.
2247
2499
  # @param method_name [Symbol] The name of the method to check.
2248
2500
  # @param include_private [Boolean] Whether to include private methods in the check.
@@ -2323,10 +2575,7 @@ module MarkdownExec
2323
2575
  def select_option_with_metadata(prompt_text, names, opts = {})
2324
2576
  ## configure to environment
2325
2577
  #
2326
- unless opts[:select_page_height].positive?
2327
- require 'io/console'
2328
- opts[:per_page] = opts[:select_page_height] = [IO.console.winsize[0] - 3, 4].max
2329
- end
2578
+ register_console_attributes(opts)
2330
2579
 
2331
2580
  # crashes if all menu options are disabled
2332
2581
  selection = @prompt.select(prompt_text,
@@ -2528,6 +2777,8 @@ module MarkdownExec
2528
2777
  @process_mutex.synchronize do
2529
2778
  @process_cv.wait(@process_mutex)
2530
2779
  end
2780
+ rescue Interrupt
2781
+ # user interrupts process
2531
2782
  end
2532
2783
 
2533
2784
  def wait_for_user_selected_block(all_blocks, menu_blocks, default)
@@ -2568,7 +2819,7 @@ module MarkdownExec
2568
2819
  time_now = Time.now.utc
2569
2820
  @run_state.saved_script_filename =
2570
2821
  SavedAsset.script_name(
2571
- blockname: selected[:nickname] || selected[:oname],
2822
+ blockname: selected.pub_name,
2572
2823
  filename: @delegate_object[:filename],
2573
2824
  prefix: @delegate_object[:saved_script_filename_prefix],
2574
2825
  time: time_now
@@ -2661,6 +2912,21 @@ module MarkdownExec
2661
2912
 
2662
2913
  def self.next_link_state(*args, **kwargs, &block)
2663
2914
  super
2915
+ # result = super
2916
+
2917
+ # @logger ||= StdOutErrLogger.new
2918
+ # @logger.unknown(
2919
+ # HashDelegator.clean_hash_recursively(
2920
+ # { "HashDelegator.next_link_state":
2921
+ # { 'args': args,
2922
+ # 'at': Time.now.strftime('%FT%TZ'),
2923
+ # 'for': /[^\/]+:\d+/.match(caller.first)[0],
2924
+ # 'kwargs': kwargs,
2925
+ # 'return': result } }
2926
+ # )
2927
+ # )
2928
+
2929
+ # result
2664
2930
  end
2665
2931
  end
2666
2932
  end
@@ -2912,7 +3178,6 @@ module MarkdownExec
2912
3178
 
2913
3179
  def test_blocks_from_nested_files
2914
3180
  result = @hd.blocks_from_nested_files
2915
-
2916
3181
  assert_kind_of Array, result
2917
3182
  assert_kind_of FCB, result.first
2918
3183
  end