markdown_exec 2.0.6 → 2.0.8

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.
@@ -6,6 +6,7 @@
6
6
  require 'clipboard'
7
7
  require 'English'
8
8
  require 'fileutils'
9
+ require 'io/console'
9
10
  require 'open3'
10
11
  require 'optparse'
11
12
  require 'ostruct'
@@ -31,6 +32,7 @@ require_relative 'hash'
31
32
  require_relative 'link_history'
32
33
  require_relative 'mdoc'
33
34
  require_relative 'regexp'
35
+ require_relative 'resize_terminal'
34
36
  require_relative 'std_out_err_logger'
35
37
  require_relative 'string_util'
36
38
 
@@ -237,6 +239,22 @@ module HashDelegatorSelf
237
239
  # @param str [String] The string to be evaluated.
238
240
  # @return [Object] The result of evaluating the string.
239
241
  def safeval(str)
242
+ # # Restricting to evaluate only expressions
243
+ # unless str.match?(/\A\s*\w+\s*[\+\-\*\/\=\%\&\|\<\>\!]+\s*\w+\s*\z/)
244
+ # error_handler('safeval') # 'Invalid expression'
245
+ # return
246
+ # end
247
+
248
+ # # Whitelisting allowed operations
249
+ # allowed_methods = %w[+ - * / == != < > <= >= && || % & |]
250
+ # unless allowed_methods.any? { |op| str.include?(op) }
251
+ # error_handler('safeval', 'Operation not allowed')
252
+ # return
253
+ # end
254
+
255
+ # # Sanitize input (example: removing potentially harmful characters)
256
+ # str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
257
+ # Evaluate the sanitized string
240
258
  result = nil
241
259
  binding.eval("result = #{str}")
242
260
 
@@ -248,6 +266,16 @@ module HashDelegatorSelf
248
266
  exit 1
249
267
  end
250
268
 
269
+ # # Evaluates the given string as Ruby code and rescues any StandardErrors.
270
+ # # If an error occurs, it calls the error_handler method with 'safeval'.
271
+ # # @param str [String] The string to be evaluated.
272
+ # # @return [Object] The result of evaluating the string.
273
+ # def safeval(str)
274
+ # eval(str)
275
+ # rescue StandardError # catches NameError, StandardError
276
+ # error_handler('safeval')
277
+ # end
278
+
251
279
  def set_file_permissions(file_path, chmod_value)
252
280
  File.chmod(chmod_value, file_path)
253
281
  end
@@ -388,6 +416,74 @@ class BashCommentFormatter
388
416
  # end
389
417
  end
390
418
 
419
+ class StringWrapper
420
+ attr_reader :width, :left_margin, :right_margin, :indent, :fill_margin
421
+
422
+ # Initializes the StringWrapper with the given options.
423
+ #
424
+ # @param width [Integer] the maximum width of each line
425
+ # @param left_margin [Integer] the number of spaces for the left margin
426
+ # @param right_margin [Integer] the number of spaces for the right margin
427
+ # @param indent [Integer] the number of spaces to indent all but the first line
428
+ # @param fill_margin [Boolean] whether to fill the left margin with spaces
429
+ def initialize(
430
+ width:,
431
+ fill_margin: false,
432
+ first_indent: '',
433
+ indent_space: ' ',
434
+ left_margin: 0,
435
+ margin_char: ' ',
436
+ rest_indent: '',
437
+ right_margin: 0
438
+ )
439
+ @fill_margin = fill_margin
440
+ @first_indent = first_indent
441
+ @indent = indent
442
+ @indent_space = indent_space
443
+ @rest_indent = rest_indent
444
+ @right_margin = right_margin
445
+ @width = width
446
+
447
+ @margin_space = fill_margin ? (margin_char * left_margin) : ''
448
+ @left_margin = @margin_space.length
449
+ end
450
+
451
+ # Wraps the given text according to the specified options.
452
+ #
453
+ # @param text [String] the text to wrap
454
+ # @return [String] the wrapped text
455
+ def wrap(text)
456
+ text = text.dup if text.frozen?
457
+ max_line_length = width - left_margin - right_margin - @indent_space.length
458
+ lines = []
459
+ current_line = String.new
460
+
461
+ words = text.split
462
+ words.each.with_index do |word, index|
463
+ trial_length = word.length
464
+ trial_length += @first_indent.length if index.zero?
465
+ trial_length += current_line.length + 1 + @rest_indent.length if index != 0
466
+ if trial_length > max_line_length && (words.count != 0)
467
+ lines << current_line
468
+ current_line = word
469
+ current_line = current_line.dup if current_line.frozen?
470
+ else
471
+ current_line << ' ' unless current_line.empty?
472
+ current_line << word
473
+ end
474
+ end
475
+ lines << current_line unless current_line.empty?
476
+
477
+ lines.map.with_index do |line, index|
478
+ @margin_space + if index.zero?
479
+ @first_indent
480
+ else
481
+ @rest_indent
482
+ end + line
483
+ end
484
+ end
485
+ end
486
+
391
487
  module MarkdownExec
392
488
  class DebugHelper
393
489
  # Class-level variable to store history of printed messages
@@ -497,6 +593,9 @@ module MarkdownExec
497
593
  when MenuState::SAVE
498
594
  option_name = @delegate_object[:menu_option_save_name]
499
595
  insert_at_top = @delegate_object[:menu_load_at_top]
596
+ when MenuState::SHELL
597
+ option_name = @delegate_object[:menu_option_shell_name]
598
+ insert_at_top = @delegate_object[:menu_load_at_top]
500
599
  when MenuState::VIEW
501
600
  option_name = @delegate_object[:menu_option_view_name]
502
601
  insert_at_top = @delegate_object[:menu_load_at_top]
@@ -595,6 +694,8 @@ module MarkdownExec
595
694
  #
596
695
  # @return [Array<FCB>] An array of FCB objects representing the blocks.
597
696
  def blocks_from_nested_files
697
+ register_console_attributes(@delegate_object)
698
+
598
699
  blocks = []
599
700
  iter_blocks_from_nested_files do |btype, fcb|
600
701
  process_block_based_on_type(blocks, btype, fcb)
@@ -605,10 +706,12 @@ module MarkdownExec
605
706
  HashDelegator.error_handler('blocks_from_nested_files')
606
707
  end
607
708
 
709
+ # find a block by its original (undecorated) name or nickname (not visible in menu)
710
+ # if matched, the block returned has properties that it is from cli and not ui
608
711
  def block_state_for_name_from_cli(block_name)
609
712
  SelectedBlockMenuState.new(
610
713
  @dml_blocks_in_file.find do |item|
611
- item[:oname] == block_name
714
+ block_name == item.pub_name
612
715
  end&.merge(
613
716
  block_name_from_cli: true,
614
717
  block_name_from_ui: false
@@ -663,7 +766,7 @@ module MarkdownExec
663
766
  # @return [Array<String>] Required code blocks as an array of lines.
664
767
  def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new)
665
768
  required = mdoc.collect_recursively_required_code(
666
- anyname: selected[:nickname] || selected[:oname],
769
+ anyname: selected.pub_name,
667
770
  label_format_above: @delegate_object[:shell_code_label_format_above],
668
771
  label_format_below: @delegate_object[:shell_code_label_format_below],
669
772
  block_source: block_source
@@ -701,47 +804,15 @@ module MarkdownExec
701
804
  system(
702
805
  format(
703
806
  @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
- }
807
+ command_execute_in_own_window_format_arguments
719
808
  )
720
809
  )
721
810
 
722
811
  else
723
812
  @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
813
+ execute_command_with_streams(
814
+ [@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args]
815
+ )
745
816
  end
746
817
 
747
818
  @run_state.completed_at = Time.now.utc
@@ -761,6 +832,24 @@ module MarkdownExec
761
832
  @fout.fout "Error ENOENT: #{err.inspect}"
762
833
  end
763
834
 
835
+ def command_execute_in_own_window_format_arguments(home: Dir.pwd)
836
+ {
837
+ batch_index: @run_state.batch_index,
838
+ batch_random: @run_state.batch_random,
839
+ block_name: @delegate_object[:block_name],
840
+ document_filename: File.basename(@delegate_object[:filename]),
841
+ document_filespec: @delegate_object[:filename],
842
+ home: home,
843
+ output_filename: File.basename(@delegate_object[:logged_stdout_filespec]),
844
+ output_filespec: @delegate_object[:logged_stdout_filespec],
845
+ script_filename: @run_state.saved_filespec,
846
+ script_filespec: File.join(home, @run_state.saved_filespec),
847
+ started_at: @run_state.started_at.strftime(
848
+ @delegate_object[:execute_command_title_time_format]
849
+ )
850
+ }
851
+ end
852
+
764
853
  # This method is responsible for handling the execution of generic blocks in a markdown document.
765
854
  # It collects the required code lines from the document and, depending on the configuration,
766
855
  # may display the code for user approval before execution. It then executes the approved block.
@@ -816,16 +905,70 @@ module MarkdownExec
816
905
  # @param match_data [MatchData] The match data containing named captures for formatting.
817
906
  # @param format_option [String] The format string to be used for the new block.
818
907
  # @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
- )
908
+ # return number of lines added
909
+ def create_and_add_chrome_block(blocks:, match_data:,
910
+ format_option:, color_method:,
911
+ case_conversion: nil,
912
+ center: nil,
913
+ wrap: nil)
914
+ line_cap = match_data.named_captures.transform_keys(&:to_sym)
915
+
916
+ # replace tabs in indent
917
+ line_cap[:indent] ||= ''
918
+ line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen?
919
+ line_cap[:indent].gsub!("\t", ' ')
920
+ # replace tabs in text
921
+ line_cap[:text] ||= ''
922
+ line_cap[:text] = line_cap[:text].dup if line_cap[:text].frozen?
923
+ line_cap[:text].gsub!("\t", ' ')
924
+ # missing capture
925
+ line_cap[:line] ||= ''
926
+
927
+ accepted_width = @delegate_object[:console_width] - 2
928
+ line_caps = if wrap
929
+ if line_cap[:text].length > accepted_width
930
+ wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length)
931
+ wrapper.wrap(line_cap[:text]).map do |line|
932
+ line_cap.dup.merge(text: line)
933
+ end
934
+ else
935
+ [line_cap]
936
+ end
937
+ else
938
+ [line_cap]
939
+ end
940
+ if center
941
+ line_caps.each do |line_obj|
942
+ line_obj[:indent] = if line_obj[:text].length < accepted_width
943
+ ' ' * ((accepted_width - line_obj[:text].length) / 2)
944
+ else
945
+ ''
946
+ end
947
+ end
948
+ end
949
+
950
+ line_caps.each do |line_obj|
951
+ next if line_obj[:text].nil?
952
+
953
+ case case_conversion
954
+ when :upcase
955
+ line_obj[:text].upcase!
956
+ when :downcase
957
+ line_obj[:text].downcase!
958
+ end
959
+
960
+ # format expects :line to be text only
961
+ line_obj[:line] = line_obj[:text]
962
+ oname = format(format_option, line_obj)
963
+ line_obj[:line] = line_obj[:indent] + line_obj[:text]
964
+ blocks.push FCB.new(
965
+ chrome: true,
966
+ disabled: '',
967
+ dname: line_obj[:indent] + oname.send(color_method),
968
+ oname: line_obj[:text]
969
+ )
970
+ end
971
+ line_caps.count
829
972
  end
830
973
 
831
974
  ##
@@ -836,12 +979,12 @@ module MarkdownExec
836
979
  # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
837
980
  def create_and_add_chrome_blocks(blocks, fcb)
838
981
  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 },
982
+ { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, center: true, case_conversion: :upcase, wrap: true },
983
+ { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, center: true, wrap: true },
984
+ { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, center: true, case_conversion: :downcase, wrap: true },
842
985
  { 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 }
986
+ { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true },
987
+ { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true }
845
988
  ]
846
989
  # rubocop:enable Style/UnlessElse
847
990
  match_criteria.each do |criteria|
@@ -852,9 +995,12 @@ module MarkdownExec
852
995
 
853
996
  create_and_add_chrome_block(
854
997
  blocks: blocks,
855
- match_data: mbody,
998
+ case_conversion: criteria[:case_conversion],
999
+ center: criteria[:center],
1000
+ color_method: @delegate_object[criteria[:color]].to_sym,
856
1001
  format_option: @delegate_object[criteria[:format]],
857
- color_method: @delegate_object[criteria[:color]].to_sym
1002
+ match_data: mbody,
1003
+ wrap: criteria[:wrap]
858
1004
  )
859
1005
  break
860
1006
  end
@@ -950,6 +1096,7 @@ module MarkdownExec
950
1096
  block_name: @delegate_object[:block_name],
951
1097
  document_filename: @delegate_object[:filename]
952
1098
  )
1099
+ # @dml_link_state_block_name_from_cli = @dml_link_state.block_name.present? ###
953
1100
  @run_state.block_name_from_cli = @dml_link_state.block_name.present?
954
1101
  @cli_block_name = @dml_link_state.block_name
955
1102
  @dml_now_using_cli = @run_state.block_name_from_cli
@@ -975,6 +1122,7 @@ module MarkdownExec
975
1122
  item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
976
1123
  item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
977
1124
  item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
1125
+ item_shell = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name]))
978
1126
  item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
979
1127
 
980
1128
  @run_state.batch_random = Random.new.rand
@@ -999,10 +1147,11 @@ module MarkdownExec
999
1147
 
1000
1148
  # add menu items (glob, load, save) and enable selectively
1001
1149
  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)
1150
+ 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?
1151
+ 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?
1152
+ 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?
1153
+ 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?
1154
+ 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
1155
  end
1007
1156
 
1008
1157
  when :display_menu
@@ -1015,7 +1164,7 @@ module MarkdownExec
1015
1164
  if @dml_link_state.block_name.present?
1016
1165
  # @prior_block_was_link = true
1017
1166
  @dml_block_state.block = @dml_blocks_in_file.find do |item|
1018
- item[:oname] == @dml_link_state.block_name
1167
+ item.pub_name == @dml_link_state.block_name
1019
1168
  end
1020
1169
  @dml_link_state.block_name = nil
1021
1170
  else
@@ -1024,8 +1173,7 @@ module MarkdownExec
1024
1173
  break if @dml_block_state.block.nil? # no block matched
1025
1174
  end
1026
1175
  # puts "! - Executing block: #{data}"
1027
- # @dml_block_state.block[:oname]
1028
- @dml_block_state.block&.fetch(:oname, nil)
1176
+ @dml_block_state.block&.pub_name
1029
1177
 
1030
1178
  when :execute_block
1031
1179
  case (block_name = data)
@@ -1045,6 +1193,7 @@ module MarkdownExec
1045
1193
  )
1046
1194
 
1047
1195
  when item_edit
1196
+ debounce_reset
1048
1197
  edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
1049
1198
  @dml_link_state.inherited_lines = edited.split("\n") if edited
1050
1199
  InputSequencer.next_link_state(prior_block_was_link: true)
@@ -1070,10 +1219,28 @@ module MarkdownExec
1070
1219
  return :break
1071
1220
 
1072
1221
  end
1222
+ InputSequencer.next_link_state(prior_block_was_link: true)
1223
+
1224
+ when item_shell
1225
+ debounce_reset
1226
+ loop do
1227
+ command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen)
1228
+ break if !command.present? || command == 'exit'
1073
1229
 
1230
+ exit_status = execute_command_with_streams(
1231
+ [@delegate_object[:shell], '-c', command]
1232
+ )
1233
+ case exit_status
1234
+ when 0
1235
+ warn "#{'OK'.green} #{exit_status}"
1236
+ else
1237
+ warn "#{'ERR'.bred} #{exit_status}"
1238
+ end
1239
+ end
1074
1240
  InputSequencer.next_link_state(prior_block_was_link: true)
1075
1241
 
1076
1242
  when item_view
1243
+ debounce_reset
1077
1244
  warn @dml_link_state.inherited_lines.join("\n")
1078
1245
  InputSequencer.next_link_state(prior_block_was_link: true)
1079
1246
 
@@ -1103,7 +1270,7 @@ module MarkdownExec
1103
1270
  @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1104
1271
  HashDelegator.next_link_state(
1105
1272
  block_name: @dml_link_state.block_name,
1106
- block_name_from_cli: !@dml_link_state.block_name.present?,
1273
+ block_name_from_cli: @dml_now_using_cli,
1107
1274
  block_state: @dml_block_state,
1108
1275
  was_using_cli: @dml_now_using_cli
1109
1276
  )
@@ -1244,6 +1411,53 @@ module MarkdownExec
1244
1411
  #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1245
1412
  end
1246
1413
 
1414
+ # Executes a given command and processes its input, output, and error streams.
1415
+ #
1416
+ # @param [Array<String>] command the command to execute along with its arguments.
1417
+ # @yield [stdin, stdout, stderr, thread] if a block is provided, it yields input, output, error lines, and the execution thread.
1418
+ # @return [Integer] the exit status of the executed command (0 to 255).
1419
+ #
1420
+ # @example
1421
+ # status = execute_command_with_streams(['ls', '-la']) do |stdin, stdout, stderr, thread|
1422
+ # puts "STDOUT: #{stdout}" if stdout
1423
+ # puts "STDERR: #{stderr}" if stderr
1424
+ # end
1425
+ # puts "Command exited with status: #{status}"
1426
+ def execute_command_with_streams(command)
1427
+ exit_status = nil
1428
+
1429
+ Open3.popen3(*command) do |stdin, stdout, stderr, exec_thread|
1430
+ # Handle stdout stream
1431
+ handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
1432
+ yield nil, line, nil, exec_thread if block_given?
1433
+ end
1434
+
1435
+ # Handle stderr stream
1436
+ handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
1437
+ yield nil, nil, line, exec_thread if block_given?
1438
+ end
1439
+
1440
+ # Handle stdin stream
1441
+ input_thread = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
1442
+ stdin.puts(line)
1443
+ yield line, nil, nil, exec_thread if block_given?
1444
+ end
1445
+
1446
+ # Wait for all streams to be processed
1447
+ wait_for_stream_processing
1448
+ exec_thread.join
1449
+
1450
+ # Ensure the input thread is killed if it's still alive
1451
+ sleep 0.1
1452
+ input_thread.kill if input_thread&.alive?
1453
+
1454
+ # Retrieve the exit status
1455
+ exit_status = exec_thread.value.exitstatus
1456
+ end
1457
+
1458
+ exit_status
1459
+ end
1460
+
1247
1461
  # Executes a block of code that has been approved for execution.
1248
1462
  # It sets the script block name, writes command files if required, and handles the execution
1249
1463
  # including output formatting and summarization.
@@ -1294,7 +1508,7 @@ module MarkdownExec
1294
1508
  ### options_state.load_file_link_state
1295
1509
  link_state = LinkState.new
1296
1510
  link_history_push_and_next(
1297
- curr_block_name: selected[:oname],
1511
+ curr_block_name: selected.pub_name,
1298
1512
  curr_document_filename: @delegate_object[:filename],
1299
1513
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1300
1514
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -1310,7 +1524,7 @@ module MarkdownExec
1310
1524
  code_lines = set_environment_variables_for_block(selected)
1311
1525
  dependencies = {}
1312
1526
  link_history_push_and_next(
1313
- curr_block_name: selected[:oname],
1527
+ curr_block_name: selected.pub_name,
1314
1528
  curr_document_filename: @delegate_object[:filename],
1315
1529
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1316
1530
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -1437,7 +1651,7 @@ module MarkdownExec
1437
1651
  return
1438
1652
  end
1439
1653
 
1440
- @delegate_object[:block_name] = block_state.block[:oname]
1654
+ @delegate_object[:block_name] = block_state.block.pub_name
1441
1655
  @menu_user_clicked_back_link = block_state.state == MenuState::BACK
1442
1656
  end
1443
1657
 
@@ -1446,7 +1660,7 @@ module MarkdownExec
1446
1660
  Thread.new do
1447
1661
  stream.each_line do |line|
1448
1662
  line.strip!
1449
- @run_state.files[file_type] << line
1663
+ @run_state.files[file_type] << line if @run_state.files
1450
1664
 
1451
1665
  if @delegate_object[:output_stdout]
1452
1666
  # print line
@@ -1546,23 +1760,7 @@ module MarkdownExec
1546
1760
 
1547
1761
  if link_block_data.fetch(LinkKeys::EXEC, false)
1548
1762
  @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
1763
+ execute_command_with_streams([cmd])
1566
1764
 
1567
1765
  ## select output_lines that look like assignment or match other specs
1568
1766
  #
@@ -1585,13 +1783,13 @@ module MarkdownExec
1585
1783
  label_format_below = @delegate_object[:shell_code_label_format_below]
1586
1784
 
1587
1785
  [label_format_above && format(label_format_above,
1588
- block_source.merge({ block_name: selected[:oname] }))] +
1786
+ block_source.merge({ block_name: selected.pub_name }))] +
1589
1787
  output_lines.map do |line|
1590
1788
  re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
1591
1789
  re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line
1592
1790
  end.compact +
1593
1791
  [label_format_below && format(label_format_below,
1594
- block_source.merge({ block_name: selected[:oname] }))]
1792
+ block_source.merge({ block_name: selected.pub_name }))]
1595
1793
  end
1596
1794
 
1597
1795
  def link_history_push_and_next(
@@ -1655,7 +1853,7 @@ module MarkdownExec
1655
1853
  def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
1656
1854
  if @delegate_object[:block_name].present?
1657
1855
  block = all_blocks.find do |item|
1658
- item[:oname] == @delegate_object[:block_name]
1856
+ item.pub_name == @delegate_object[:block_name]
1659
1857
  end&.merge(block_name_from_ui: false)
1660
1858
  else
1661
1859
  block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
@@ -1682,6 +1880,44 @@ module MarkdownExec
1682
1880
  expanded_expression
1683
1881
  end
1684
1882
  end
1883
+
1884
+ # private
1885
+
1886
+ # def read_block_name(line)
1887
+ # bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
1888
+ # name = bm[:title]
1889
+
1890
+ # if @delegate_object[:block_name_nick_match].present? && line =~ Regexp.new(@delegate_object[:block_name_nick_match])
1891
+ # name = $~[0]
1892
+ # else
1893
+ # name = bm && bm[1] ? bm[:title] : name
1894
+ # end
1895
+ # name
1896
+ # end
1897
+
1898
+ # # Loads auto link block.
1899
+ # def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
1900
+ # block_name = @delegate_object[:document_load_link_block_name]
1901
+ # return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
1902
+
1903
+ # block = HashDelegator.block_find(all_blocks, :oname, block_name)
1904
+ # return unless block
1905
+
1906
+ # if block.fetch(:shell, '') != BlockType::LINK
1907
+ # HashDelegator.error_handler('must be Link block type', { abort: true })
1908
+
1909
+ # else
1910
+ # # debounce_reset
1911
+ # push_link_history_and_trigger_load(
1912
+ # link_block_body: block.fetch(:body, ''),
1913
+ # mdoc: mdoc,
1914
+ # selected: block,
1915
+ # link_state: link_state,
1916
+ # block_source: block_source
1917
+ # )
1918
+ # end
1919
+ # end
1920
+
1685
1921
  # Handle expression with wildcard characters
1686
1922
  def load_filespec_wildcard_expansion(expr, auto_load_single: false)
1687
1923
  files = find_files(expr)
@@ -1717,6 +1953,7 @@ module MarkdownExec
1717
1953
  # recreate menu with new options
1718
1954
  #
1719
1955
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
1956
+ # load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
1720
1957
 
1721
1958
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1722
1959
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
@@ -1793,7 +2030,7 @@ module MarkdownExec
1793
2030
  #
1794
2031
  return unless item
1795
2032
 
1796
- item[:dname] = "#{name} (#{count} #{type})"
2033
+ item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name
1797
2034
  if count.positive?
1798
2035
  item.delete(:disabled)
1799
2036
  else
@@ -1902,7 +2139,7 @@ module MarkdownExec
1902
2139
  else
1903
2140
  # no history exists; must have been called independently => retain script
1904
2141
  link_history_push_and_next(
1905
- curr_block_name: selected[:oname],
2142
+ curr_block_name: selected.pub_name,
1906
2143
  curr_document_filename: @delegate_object[:filename],
1907
2144
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1908
2145
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -2036,6 +2273,14 @@ module MarkdownExec
2036
2273
  exit 1
2037
2274
  end
2038
2275
 
2276
+ def prompt_for_command(prompt)
2277
+ print prompt
2278
+
2279
+ gets.chomp
2280
+ rescue Interrupt
2281
+ nil
2282
+ end
2283
+
2039
2284
  # Prompts the user to enter a path or name to substitute into the wildcard expression.
2040
2285
  # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
2041
2286
  #
@@ -2132,8 +2377,7 @@ module MarkdownExec
2132
2377
  # user prompt to exit if the menu will be displayed again
2133
2378
  #
2134
2379
  def prompt_user_exit(block_name_from_cli:, selected:)
2135
- !block_name_from_cli &&
2136
- selected[:shell] == BlockType::BASH &&
2380
+ selected[:shell] == BlockType::BASH &&
2137
2381
  @delegate_object[:pause_after_script_execution] &&
2138
2382
  prompt_select_continue == MenuState::EXIT
2139
2383
  end
@@ -2154,7 +2398,7 @@ module MarkdownExec
2154
2398
  #
2155
2399
  if mdoc
2156
2400
  code_info = mdoc.collect_recursively_required_code(
2157
- anyname: selected[:oname],
2401
+ anyname: selected.pub_name,
2158
2402
  label_format_above: @delegate_object[:shell_code_label_format_above],
2159
2403
  label_format_below: @delegate_object[:shell_code_label_format_below],
2160
2404
  block_source: block_source
@@ -2171,7 +2415,7 @@ module MarkdownExec
2171
2415
  # load key and values from link block into current environment
2172
2416
  #
2173
2417
  if link_block_data[LinkKeys::VARS]
2174
- code_lines.push BashCommentFormatter.format_comment(selected[:oname])
2418
+ code_lines.push BashCommentFormatter.format_comment(selected.pub_name)
2175
2419
  (link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
2176
2420
  ENV[key] = value.to_s
2177
2421
  code_lines.push(assign_key_value_in_bash(key, value))
@@ -2200,7 +2444,7 @@ module MarkdownExec
2200
2444
 
2201
2445
  else
2202
2446
  link_history_push_and_next(
2203
- curr_block_name: selected[:oname],
2447
+ curr_block_name: selected.pub_name,
2204
2448
  curr_document_filename: @delegate_object[:filename],
2205
2449
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2206
2450
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
@@ -2243,6 +2487,39 @@ module MarkdownExec
2243
2487
  ))
2244
2488
  end
2245
2489
 
2490
+ # Registers console attributes by modifying the options hash.
2491
+ # This method handles terminal resizing and adjusts the console dimensions
2492
+ # and pagination settings based on the current terminal size.
2493
+ #
2494
+ # @param opts [Hash] a hash containing various options for the console settings.
2495
+ # - :console_width [Integer, nil] The width of the console. If not provided or if the terminal is resized, it will be set to the current console width.
2496
+ # - :console_height [Integer, nil] The height of the console. If not provided or if the terminal is resized, it will be set to the current console height.
2497
+ # - :console_winsize [Array<Integer>, nil] The dimensions of the console [height, width]. If not provided or if the terminal is resized, it will be set to the current console dimensions.
2498
+ # - :select_page_height [Integer, nil] The height of the page for selection. If not provided or if not positive, it will be set to the maximum of (console height - 3) or 4.
2499
+ # - :per_page [Integer, nil] The number of items per page. If :select_page_height is not provided or if not positive, it will be set to the maximum of (console height - 3) or 4.
2500
+ #
2501
+ # @raise [StandardError] If an error occurs during the process, it will be caught and handled by calling HashDelegator.error_handler with 'register_console_attributes' and { abort: true }.
2502
+ #
2503
+ # @example
2504
+ # opts = { console_width: nil, console_height: nil, select_page_height: nil }
2505
+ # register_console_attributes(opts)
2506
+ # # opts will be updated with the current console dimensions and pagination settings.
2507
+ def register_console_attributes(opts)
2508
+ begin
2509
+ if (resized = @delegate_object[:menu_resize_terminal])
2510
+ resize_terminal
2511
+ end
2512
+
2513
+ if resized || !opts[:console_width]
2514
+ opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize
2515
+ end
2516
+
2517
+ opts[:per_page] = opts[:select_page_height] = [opts[:console_height] - 3, 4].max unless opts[:select_page_height]&.positive?
2518
+ rescue StandardError
2519
+ HashDelegator.error_handler('register_console_attributes', { abort: true })
2520
+ end
2521
+ end
2522
+
2246
2523
  # Check if the delegate object responds to a given method.
2247
2524
  # @param method_name [Symbol] The name of the method to check.
2248
2525
  # @param include_private [Boolean] Whether to include private methods in the check.
@@ -2323,10 +2600,7 @@ module MarkdownExec
2323
2600
  def select_option_with_metadata(prompt_text, names, opts = {})
2324
2601
  ## configure to environment
2325
2602
  #
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
2603
+ register_console_attributes(opts)
2330
2604
 
2331
2605
  # crashes if all menu options are disabled
2332
2606
  selection = @prompt.select(prompt_text,
@@ -2528,6 +2802,8 @@ module MarkdownExec
2528
2802
  @process_mutex.synchronize do
2529
2803
  @process_cv.wait(@process_mutex)
2530
2804
  end
2805
+ rescue Interrupt
2806
+ # user interrupts process
2531
2807
  end
2532
2808
 
2533
2809
  def wait_for_user_selected_block(all_blocks, menu_blocks, default)
@@ -2568,7 +2844,7 @@ module MarkdownExec
2568
2844
  time_now = Time.now.utc
2569
2845
  @run_state.saved_script_filename =
2570
2846
  SavedAsset.script_name(
2571
- blockname: selected[:nickname] || selected[:oname],
2847
+ blockname: selected.pub_name,
2572
2848
  filename: @delegate_object[:filename],
2573
2849
  prefix: @delegate_object[:saved_script_filename_prefix],
2574
2850
  time: time_now
@@ -2661,6 +2937,21 @@ module MarkdownExec
2661
2937
 
2662
2938
  def self.next_link_state(*args, **kwargs, &block)
2663
2939
  super
2940
+ # result = super
2941
+
2942
+ # @logger ||= StdOutErrLogger.new
2943
+ # @logger.unknown(
2944
+ # HashDelegator.clean_hash_recursively(
2945
+ # { "HashDelegator.next_link_state":
2946
+ # { 'args': args,
2947
+ # 'at': Time.now.strftime('%FT%TZ'),
2948
+ # 'for': /[^\/]+:\d+/.match(caller.first)[0],
2949
+ # 'kwargs': kwargs,
2950
+ # 'return': result } }
2951
+ # )
2952
+ # )
2953
+
2954
+ # result
2664
2955
  end
2665
2956
  end
2666
2957
  end
@@ -2912,7 +3203,6 @@ module MarkdownExec
2912
3203
 
2913
3204
  def test_blocks_from_nested_files
2914
3205
  result = @hd.blocks_from_nested_files
2915
-
2916
3206
  assert_kind_of Array, result
2917
3207
  assert_kind_of FCB, result.first
2918
3208
  end