lib/hash_delegator.rb in markdown_exec-2.0.6 vs lib/hash_delegator.rb in markdown_exec-2.0.7

- old
+ new

@@ -235,10 +235,26 @@ # Evaluates the given string as Ruby code within a safe context. # If an error occurs, it calls the error_handler method with 'safeval'. # @param str [String] The string to be evaluated. # @return [Object] The result of evaluating the string. def safeval(str) + # # Restricting to evaluate only expressions + # unless str.match?(/\A\s*\w+\s*[\+\-\*\/\=\%\&\|\<\>\!]+\s*\w+\s*\z/) + # error_handler('safeval') # 'Invalid expression' + # return + # end + + # # Whitelisting allowed operations + # allowed_methods = %w[+ - * / == != < > <= >= && || % & |] + # unless allowed_methods.any? { |op| str.include?(op) } + # error_handler('safeval', 'Operation not allowed') + # return + # end + + # # Sanitize input (example: removing potentially harmful characters) + # str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '') + # Evaluate the sanitized string result = nil binding.eval("result = #{str}") result rescue StandardError # catches NameError, StandardError @@ -246,10 +262,20 @@ pp "code: #{str}" error_handler('safeval') exit 1 end + # # Evaluates the given string as Ruby code and rescues any StandardErrors. + # # If an error occurs, it calls the error_handler method with 'safeval'. + # # @param str [String] The string to be evaluated. + # # @return [Object] The result of evaluating the string. + # def safeval(str) + # eval(str) + # rescue StandardError # catches NameError, StandardError + # error_handler('safeval') + # end + def set_file_permissions(file_path, chmod_value) File.chmod(chmod_value, file_path) end # Creates a TTY prompt with custom settings. Specifically, it disables the default 'cross' symbol and @@ -386,10 +412,78 @@ # def oname_for_bash_comment(oname) # oname.gsub("\n", ' ~ ').gsub(/ +/, ' ') # end end +class StringWrapper + attr_reader :width, :left_margin, :right_margin, :indent, :fill_margin + + # Initializes the StringWrapper with the given options. + # + # @param width [Integer] the maximum width of each line + # @param left_margin [Integer] the number of spaces for the left margin + # @param right_margin [Integer] the number of spaces for the right margin + # @param indent [Integer] the number of spaces to indent all but the first line + # @param fill_margin [Boolean] whether to fill the left margin with spaces + def initialize( + width:, + fill_margin: false, + first_indent: '', + indent_space: ' ', + left_margin: 0, + margin_char: ' ', + rest_indent: '', + right_margin: 0 + ) + @fill_margin = fill_margin + @first_indent = first_indent + @indent = indent + @indent_space = indent_space + @rest_indent = rest_indent + @right_margin = right_margin + @width = width + + @margin_space = fill_margin ? (margin_char * left_margin) : '' + @left_margin = @margin_space.length + end + + # Wraps the given text according to the specified options. + # + # @param text [String] the text to wrap + # @return [String] the wrapped text + def wrap(text) + text = text.dup if text.frozen? + max_line_length = width - left_margin - right_margin - @indent_space.length + lines = [] + current_line = String.new + + words = text.split + words.each.with_index do |word, index| + trial_length = word.length + trial_length += @first_indent.length if index.zero? + trial_length += current_line.length + 1 + @rest_indent.length if index != 0 + if trial_length > max_line_length && (words.count != 0) + lines << current_line + current_line = word + current_line = current_line.dup if current_line.frozen? + else + current_line << ' ' unless current_line.empty? + current_line << word + end + end + lines << current_line unless current_line.empty? + + lines.map.with_index do |line, index| + @margin_space + if index.zero? + @first_indent + else + @rest_indent + end + line + end + end +end + module MarkdownExec class DebugHelper # Class-level variable to store history of printed messages @@printed_messages = Set.new @@ -495,10 +589,13 @@ option_name = @delegate_object[:menu_option_load_name] insert_at_top = @delegate_object[:menu_load_at_top] when MenuState::SAVE option_name = @delegate_object[:menu_option_save_name] insert_at_top = @delegate_object[:menu_load_at_top] + when MenuState::SHELL + option_name = @delegate_object[:menu_option_shell_name] + insert_at_top = @delegate_object[:menu_load_at_top] when MenuState::VIEW option_name = @delegate_object[:menu_option_view_name] insert_at_top = @delegate_object[:menu_load_at_top] end @@ -593,24 +690,28 @@ # Iterates through nested files to collect various types of blocks, including dividers, tasks, and others. # The method categorizes blocks based on their type and processes them accordingly. # # @return [Array<FCB>] An array of FCB objects representing the blocks. def blocks_from_nested_files + register_console_attributes(@delegate_object) + blocks = [] iter_blocks_from_nested_files do |btype, fcb| process_block_based_on_type(blocks, btype, fcb) end # &bc 'blocks.count:', blocks.count blocks rescue StandardError HashDelegator.error_handler('blocks_from_nested_files') end + # find a block by its original (undecorated) name or nickname (not visible in menu) + # if matched, the block returned has properties that it is from cli and not ui def block_state_for_name_from_cli(block_name) SelectedBlockMenuState.new( @dml_blocks_in_file.find do |item| - item[:oname] == block_name + block_name == item.pub_name end&.merge( block_name_from_cli: true, block_name_from_ui: false ), MenuState::CONTINUE @@ -661,11 +762,11 @@ # @param mdoc [YourMDocClass] An instance of the MDoc class. # @param selected [Hash] The selected block. # @return [Array<String>] Required code blocks as an array of lines. def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new) required = mdoc.collect_recursively_required_code( - anyname: selected[:nickname] || selected[:oname], + anyname: selected.pub_name, label_format_above: @delegate_object[:shell_code_label_format_above], label_format_below: @delegate_object[:shell_code_label_format_below], block_source: block_source ) dependencies = (link_state&.inherited_dependencies || {}).merge(required[:dependencies] || {}) @@ -699,51 +800,19 @@ @run_state.saved_filespec.present? @run_state.in_own_window = true system( format( @delegate_object[:execute_command_format], - { - batch_index: @run_state.batch_index, - batch_random: @run_state.batch_random, - block_name: @delegate_object[:block_name], - document_filename: File.basename(@delegate_object[:filename]), - document_filespec: @delegate_object[:filename], - home: Dir.pwd, - output_filename: File.basename(@delegate_object[:logged_stdout_filespec]), - output_filespec: @delegate_object[:logged_stdout_filespec], - script_filename: @run_state.saved_filespec, - script_filespec: File.join(Dir.pwd, @run_state.saved_filespec), - started_at: @run_state.started_at.strftime( - @delegate_object[:execute_command_title_time_format] - ) - } + command_execute_in_own_window_format_arguments ) ) else @run_state.in_own_window = false - Open3.popen3(@delegate_object[:shell], - '-c', command, - @delegate_object[:filename], - *args) do |stdin, stdout, stderr, exec_thr| - handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line| - yield nil, line, nil, exec_thr if block_given? - end - handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line| - yield nil, nil, line, exec_thr if block_given? - end - - in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line| - stdin.puts(line) - yield line, nil, nil, exec_thr if block_given? - end - - wait_for_stream_processing - exec_thr.join - sleep 0.1 - in_thr.kill if in_thr&.alive? - end + execute_command_with_streams( + [@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args] + ) end @run_state.completed_at = Time.now.utc rescue Errno::ENOENT => err # Handle ENOENT error @@ -759,10 +828,28 @@ @run_state.error = err @run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message] @fout.fout "Error ENOENT: #{err.inspect}" end + def command_execute_in_own_window_format_arguments(home: Dir.pwd) + { + batch_index: @run_state.batch_index, + batch_random: @run_state.batch_random, + block_name: @delegate_object[:block_name], + document_filename: File.basename(@delegate_object[:filename]), + document_filespec: @delegate_object[:filename], + home: home, + output_filename: File.basename(@delegate_object[:logged_stdout_filespec]), + output_filespec: @delegate_object[:logged_stdout_filespec], + script_filename: @run_state.saved_filespec, + script_filespec: File.join(home, @run_state.saved_filespec), + started_at: @run_state.started_at.strftime( + @delegate_object[:execute_command_title_time_format] + ) + } + end + # This method is responsible for handling the execution of generic blocks in a markdown document. # It collects the required code lines from the document and, depending on the configuration, # may display the code for user approval before execution. It then executes the approved block. # # @param mdoc [Object] The markdown document object containing code blocks. @@ -814,49 +901,106 @@ # Creates and adds a formatted block to the blocks array based on the provided match and format options. # @param blocks [Array] The array of blocks to add the new block to. # @param match_data [MatchData] The match data containing named captures for formatting. # @param format_option [String] The format string to be used for the new block. # @param color_method [Symbol] The color method to apply to the block's display name. - def create_and_add_chrome_block(blocks:, match_data:, format_option:, - color_method:) - oname = format(format_option, - match_data.named_captures.transform_keys(&:to_sym)) - blocks.push FCB.new( - chrome: true, - disabled: '', - dname: oname.send(color_method), - oname: oname - ) + # return number of lines added + def create_and_add_chrome_block(blocks:, match_data:, + format_option:, color_method:, + case_conversion: nil, + center: nil, + wrap: nil) + line_cap = match_data.named_captures.transform_keys(&:to_sym) + + # replace tabs in indent + line_cap[:indent] ||= '' + line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen? + line_cap[:indent].gsub!("\t", ' ') + # replace tabs in text + line_cap[:text] ||= '' + line_cap[:text] = line_cap[:text].dup if line_cap[:text].frozen? + line_cap[:text].gsub!("\t", ' ') + # missing capture + line_cap[:line] ||= '' + + accepted_width = @delegate_object[:console_width] - 2 + line_caps = if wrap + if line_cap[:text].length > accepted_width + wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length) + wrapper.wrap(line_cap[:text]).map do |line| + line_cap.dup.merge(text: line) + end + else + [line_cap] + end + else + [line_cap] + end + if center + line_caps.each do |line_obj| + line_obj[:indent] = if line_obj[:text].length < accepted_width + ' ' * ((accepted_width - line_obj[:text].length) / 2) + else + '' + end + end + end + + line_caps.each do |line_obj| + next if line_obj[:text].nil? + + case case_conversion + when :upcase + line_obj[:text].upcase! + when :downcase + line_obj[:text].downcase! + end + + # format expects :line to be text only + line_obj[:line] = line_obj[:text] + oname = format(format_option, line_obj) + line_obj[:line] = line_obj[:indent] + line_obj[:text] + blocks.push FCB.new( + chrome: true, + disabled: '', + dname: line_obj[:indent] + oname.send(color_method), + oname: line_obj[:text] + ) + end + line_caps.count end ## # Processes lines within the file and converts them into blocks if they match certain criteria. # @param blocks [Array] The array to append new blocks to. # @param fcb [FCB] The file control block being processed. # @param opts [Hash] Options containing configuration for line processing. # @param use_chrome [Boolean] Indicates if the chrome styling should be applied. def create_and_add_chrome_blocks(blocks, fcb) match_criteria = [ - { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match }, - { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match }, - { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match }, + { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, center: true, case_conversion: :upcase, wrap: true }, + { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, center: true, wrap: true }, + { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, center: true, case_conversion: :downcase, wrap: true }, { color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match }, - { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match }, - { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match } + { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true }, + { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true } ] # rubocop:enable Style/UnlessElse match_criteria.each do |criteria| unless @delegate_object[criteria[:match]].present? && (mbody = fcb.body[0].match @delegate_object[criteria[:match]]) next end create_and_add_chrome_block( blocks: blocks, - match_data: mbody, + case_conversion: criteria[:case_conversion], + center: criteria[:center], + color_method: @delegate_object[criteria[:color]].to_sym, format_option: @delegate_object[criteria[:format]], - color_method: @delegate_object[criteria[:color]].to_sym + match_data: mbody, + wrap: criteria[:wrap] ) break end end @@ -948,10 +1092,11 @@ @menu_base_options = @delegate_object @dml_link_state = LinkState.new( block_name: @delegate_object[:block_name], document_filename: @delegate_object[:filename] ) + # @dml_link_state_block_name_from_cli = @dml_link_state.block_name.present? ### @run_state.block_name_from_cli = @dml_link_state.block_name.present? @cli_block_name = @dml_link_state.block_name @dml_now_using_cli = @run_state.block_name_from_cli @dml_menu_default_dname = nil @dml_block_state = SelectedBlockMenuState.new @@ -973,10 +1118,11 @@ item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name])) item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])) item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])) item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])) + item_shell = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_shell_name])) item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])) @run_state.batch_random = Random.new.rand @run_state.batch_index = 0 @@ -997,14 +1143,15 @@ lines_count = @dml_link_state.inherited_lines&.count || 0 # add menu items (glob, load, save) and enable selectively menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive? - menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD) - menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT) - menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), lines_count, 'lines', menu_state: MenuState::SAVE) - menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), lines_count, 'lines', menu_state: MenuState::VIEW) + 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? + 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? + 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? + 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? + 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] end when :display_menu # warn "@ - display menu:" # ii_display_menu @@ -1013,21 +1160,20 @@ when :user_choice if @dml_link_state.block_name.present? # @prior_block_was_link = true @dml_block_state.block = @dml_blocks_in_file.find do |item| - item[:oname] == @dml_link_state.block_name + item.pub_name == @dml_link_state.block_name end @dml_link_state.block_name = nil else # puts "? - Select a block to execute (or type #{$texit} to exit):" break if inpseq_user_choice == :break # into @dml_block_state break if @dml_block_state.block.nil? # no block matched end # puts "! - Executing block: #{data}" - # @dml_block_state.block[:oname] - @dml_block_state.block&.fetch(:oname, nil) + @dml_block_state.block&.pub_name when :execute_block case (block_name = data) when item_back debounce_reset @@ -1043,10 +1189,11 @@ prior_block_was_link: true ) ) when item_edit + debounce_reset edited = edit_text(@dml_link_state.inherited_lines.join("\n")) @dml_link_state.inherited_lines = edited.split("\n") if edited InputSequencer.next_link_state(prior_block_was_link: true) when item_load @@ -1068,14 +1215,32 @@ HashDelegator.join_code_lines(@dml_link_state.inherited_lines) ) return :break end + InputSequencer.next_link_state(prior_block_was_link: true) + when item_shell + debounce_reset + loop do + command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen) + break if !command.present? || command == 'exit' + + exit_status = execute_command_with_streams( + [@delegate_object[:shell], '-c', command] + ) + case exit_status + when 0 + warn "#{'OK'.green} #{exit_status}" + else + warn "#{'ERR'.bred} #{exit_status}" + end + end InputSequencer.next_link_state(prior_block_was_link: true) when item_view + debounce_reset warn @dml_link_state.inherited_lines.join("\n") InputSequencer.next_link_state(prior_block_was_link: true) else @dml_block_state = block_state_for_name_from_cli(block_name) @@ -1101,11 +1266,11 @@ ## order of block name processing: link block, cli, from user # @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \ HashDelegator.next_link_state( block_name: @dml_link_state.block_name, - block_name_from_cli: !@dml_link_state.block_name.present?, + block_name_from_cli: @dml_now_using_cli, block_state: @dml_block_state, was_using_cli: @dml_now_using_cli ) if !@dml_block_state.block[:block_name_from_ui] && cli_break @@ -1242,10 +1407,57 @@ [lfls.link_state, lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]] #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] } end + # Executes a given command and processes its input, output, and error streams. + # + # @param [Array<String>] command the command to execute along with its arguments. + # @yield [stdin, stdout, stderr, thread] if a block is provided, it yields input, output, error lines, and the execution thread. + # @return [Integer] the exit status of the executed command (0 to 255). + # + # @example + # status = execute_command_with_streams(['ls', '-la']) do |stdin, stdout, stderr, thread| + # puts "STDOUT: #{stdout}" if stdout + # puts "STDERR: #{stderr}" if stderr + # end + # puts "Command exited with status: #{status}" + def execute_command_with_streams(command) + exit_status = nil + + Open3.popen3(*command) do |stdin, stdout, stderr, exec_thread| + # Handle stdout stream + handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line| + yield nil, line, nil, exec_thread if block_given? + end + + # Handle stderr stream + handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line| + yield nil, nil, line, exec_thread if block_given? + end + + # Handle stdin stream + input_thread = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line| + stdin.puts(line) + yield line, nil, nil, exec_thread if block_given? + end + + # Wait for all streams to be processed + wait_for_stream_processing + exec_thread.join + + # Ensure the input thread is killed if it's still alive + sleep 0.1 + input_thread.kill if input_thread&.alive? + + # Retrieve the exit status + exit_status = exec_thread.value.exitstatus + end + + exit_status + end + # Executes a block of code that has been approved for execution. # It sets the script block name, writes command files if required, and handles the execution # including output formatting and summarization. # # @param required_lines [Array<String>] The lines of code to be executed. @@ -1292,11 +1504,11 @@ @delegate_object.merge!(options_state.options) ### options_state.load_file_link_state link_state = LinkState.new link_history_push_and_next( - curr_block_name: selected[:oname], + curr_block_name: selected.pub_name, curr_document_filename: @delegate_object[:filename], inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines), next_block_name: '', @@ -1308,11 +1520,11 @@ debounce_reset block_names = [] code_lines = set_environment_variables_for_block(selected) dependencies = {} link_history_push_and_next( - curr_block_name: selected[:oname], + curr_block_name: selected.pub_name, curr_document_filename: @delegate_object[:filename], inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines), next_block_name: '', @@ -1435,20 +1647,20 @@ unless [MenuState::BACK, MenuState::CONTINUE].include?(block_state.state) return end - @delegate_object[:block_name] = block_state.block[:oname] + @delegate_object[:block_name] = block_state.block.pub_name @menu_user_clicked_back_link = block_state.state == MenuState::BACK end def handle_stream(stream:, file_type:, swap: false) @process_mutex.synchronize do Thread.new do stream.each_line do |line| line.strip! - @run_state.files[file_type] << line + @run_state.files[file_type] << line if @run_state.files if @delegate_object[:output_stdout] # print line puts line end @@ -1544,28 +1756,12 @@ file.write(all_code.join("\n")) file.rewind if link_block_data.fetch(LinkKeys::EXEC, false) @run_state.files = Hash.new([]) + execute_command_with_streams([cmd]) - Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr| - handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line| - output_lines.push(line) - end - handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line| - output_lines.push(line) - end - - in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line| - stdin.puts(line) - end - - wait_for_stream_processing - sleep 0.1 - in_thr.kill if in_thr&.alive? - end - ## select output_lines that look like assignment or match other specs # output_lines = process_string_array( output_lines, begin_pattern: @delegate_object.fetch(:output_assignment_begin, nil), @@ -1583,17 +1779,17 @@ label_format_above = @delegate_object[:shell_code_label_format_above] label_format_below = @delegate_object[:shell_code_label_format_below] [label_format_above && format(label_format_above, - block_source.merge({ block_name: selected[:oname] }))] + + block_source.merge({ block_name: selected.pub_name }))] + output_lines.map do |line| re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)')) re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line end.compact + [label_format_below && format(label_format_below, - block_source.merge({ block_name: selected[:oname] }))] + block_source.merge({ block_name: selected.pub_name }))] end def link_history_push_and_next( curr_block_name:, curr_document_filename:, inherited_block_names:, inherited_dependencies:, inherited_lines:, @@ -1653,11 +1849,11 @@ end def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil) if @delegate_object[:block_name].present? block = all_blocks.find do |item| - item[:oname] == @delegate_object[:block_name] + item.pub_name == @delegate_object[:block_name] end&.merge(block_name_from_ui: false) else block_state = wait_for_user_selected_block(all_blocks, menu_blocks, default) block = block_state.block&.merge(block_name_from_ui: true) @@ -1680,10 +1876,48 @@ load_filespec_wildcard_expansion(expanded_expression) else expanded_expression end end + + # private + + # def read_block_name(line) + # bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match]) + # name = bm[:title] + + # if @delegate_object[:block_name_nick_match].present? && line =~ Regexp.new(@delegate_object[:block_name_nick_match]) + # name = $~[0] + # else + # name = bm && bm[1] ? bm[:title] : name + # end + # name + # end + + # # Loads auto link block. + # def load_auto_link_block(all_blocks, link_state, mdoc, block_source:) + # block_name = @delegate_object[:document_load_link_block_name] + # return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] + + # block = HashDelegator.block_find(all_blocks, :oname, block_name) + # return unless block + + # if block.fetch(:shell, '') != BlockType::LINK + # HashDelegator.error_handler('must be Link block type', { abort: true }) + + # else + # # debounce_reset + # push_link_history_and_trigger_load( + # link_block_body: block.fetch(:body, ''), + # mdoc: mdoc, + # selected: block, + # link_state: link_state, + # block_source: block_source + # ) + # end + # end + # Handle expression with wildcard characters def load_filespec_wildcard_expansion(expr, auto_load_single: false) files = find_files(expr) if files.count.zero? HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true }) @@ -1715,10 +1949,11 @@ all_blocks, mdoc = mdoc_and_blocks_from_nested_files # recreate menu with new options # all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks) + # load_auto_link_block(all_blocks, link_state, mdoc, block_source: {}) menu_blocks = mdoc.fcbs_per_options(@delegate_object) add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state) ### compress empty lines HashDelegator.delete_consecutive_blank_lines!(menu_blocks) @@ -1791,11 +2026,11 @@ # update item if it exists # return unless item - item[:dname] = "#{name} (#{count} #{type})" + item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name if count.positive? item.delete(:disabled) else item[:disabled] = '' end @@ -1900,11 +2135,11 @@ next_state.block_name = nil LoadFileLinkState.new(LoadFile::LOAD, next_state) else # no history exists; must have been called independently => retain script link_history_push_and_next( - curr_block_name: selected[:oname], + curr_block_name: selected.pub_name, curr_document_filename: @delegate_object[:filename], inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines), next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || '' @@ -2034,10 +2269,18 @@ true rescue TTY::Reader::InputInterrupt exit 1 end + def prompt_for_command(prompt) + print prompt + + gets.chomp + rescue Interrupt + nil + end + # Prompts the user to enter a path or name to substitute into the wildcard expression. # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil. # # @param filespec [String] the wildcard expression to be substituted # @return [String, nil] the resolved path or substituted expression, or nil if interrupted @@ -2130,12 +2373,11 @@ end # user prompt to exit if the menu will be displayed again # def prompt_user_exit(block_name_from_cli:, selected:) - !block_name_from_cli && - selected[:shell] == BlockType::BASH && + selected[:shell] == BlockType::BASH && @delegate_object[:pause_after_script_execution] && prompt_select_continue == MenuState::EXIT end # Handles the processing of a link block in Markdown Execution. @@ -2152,11 +2394,11 @@ ## collect blocks specified by block # if mdoc code_info = mdoc.collect_recursively_required_code( - anyname: selected[:oname], + anyname: selected.pub_name, label_format_above: @delegate_object[:shell_code_label_format_above], label_format_below: @delegate_object[:shell_code_label_format_below], block_source: block_source ) code_lines = code_info[:code] @@ -2169,11 +2411,11 @@ end # load key and values from link block into current environment # if link_block_data[LinkKeys::VARS] - code_lines.push BashCommentFormatter.format_comment(selected[:oname]) + code_lines.push BashCommentFormatter.format_comment(selected.pub_name) (link_block_data[LinkKeys::VARS] || []).each do |(key, value)| ENV[key] = value.to_s code_lines.push(assign_key_value_in_bash(key, value)) end end @@ -2198,11 +2440,11 @@ pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines, dependencies, selected) else link_history_push_and_next( - curr_block_name: selected[:oname], + curr_block_name: selected.pub_name, curr_document_filename: @delegate_object[:filename], inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines), next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK, @@ -2241,10 +2483,20 @@ load_file_link_state: LoadFileLinkState.new( LoadFile::REUSE, link_state )) end + def register_console_attributes(opts) + unless opts[:console_width] + require 'io/console' + opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize + end + opts[:per_page] = opts[:select_page_height] = [opts[:console_height] - 3, 4].max unless opts[:select_page_height]&.positive? + rescue StandardError + HashDelegator.error_handler('register_console_attributes', { abort: true }) + end + # Check if the delegate object responds to a given method. # @param method_name [Symbol] The name of the method to check. # @param include_private [Boolean] Whether to include private methods in the check. # @return [Boolean] true if the delegate object responds to the method, false otherwise. def respond_to?(method_name, include_private = false) @@ -2321,14 +2573,11 @@ # Presents a TTY prompt to select an option or exit, returns metadata including option and selected def select_option_with_metadata(prompt_text, names, opts = {}) ## configure to environment # - unless opts[:select_page_height].positive? - require 'io/console' - opts[:per_page] = opts[:select_page_height] = [IO.console.winsize[0] - 3, 4].max - end + register_console_attributes(opts) # crashes if all menu options are disabled selection = @prompt.select(prompt_text, names, opts.merge(filter: true)) @@ -2526,10 +2775,12 @@ def wait_for_stream_processing @process_mutex.synchronize do @process_cv.wait(@process_mutex) end + rescue Interrupt + # user interrupts process end def wait_for_user_selected_block(all_blocks, menu_blocks, default) block_state = wait_for_user_selection(all_blocks, menu_blocks, default) handle_back_or_continue(block_state) @@ -2566,11 +2817,11 @@ return unless @delegate_object[:save_executed_script] time_now = Time.now.utc @run_state.saved_script_filename = SavedAsset.script_name( - blockname: selected[:nickname] || selected[:oname], + blockname: selected.pub_name, filename: @delegate_object[:filename], prefix: @delegate_object[:saved_script_filename_prefix], time: time_now ) @run_state.saved_filespec = @@ -2659,10 +2910,25 @@ obj end def self.next_link_state(*args, **kwargs, &block) super + # result = super + + # @logger ||= StdOutErrLogger.new + # @logger.unknown( + # HashDelegator.clean_hash_recursively( + # { "HashDelegator.next_link_state": + # { 'args': args, + # 'at': Time.now.strftime('%FT%TZ'), + # 'for': /[^\/]+:\d+/.match(caller.first)[0], + # 'kwargs': kwargs, + # 'return': result } } + # ) + # ) + + # result end end end return if $PROGRAM_NAME != __FILE__ @@ -2910,10 +3176,9 @@ HashDelegator.stubs(:error_handler) end def test_blocks_from_nested_files result = @hd.blocks_from_nested_files - assert_kind_of Array, result assert_kind_of FCB, result.first end def test_blocks_from_nested_files_with_no_chrome