markdown_exec 2.0.6 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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