markdown_exec 2.0.6 → 2.0.8

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