lib/hash_delegator.rb in markdown_exec-2.1.0 vs lib/hash_delegator.rb in markdown_exec-2.2.0

- old
+ new

@@ -29,16 +29,19 @@ require_relative 'filter' require_relative 'fout' require_relative 'hash' require_relative 'link_history' require_relative 'mdoc' +require_relative 'namer' require_relative 'regexp' require_relative 'resize_terminal' require_relative 'std_out_err_logger' require_relative 'streams_out' require_relative 'string_util' +$pd = false unless defined?($pd) + class String # Checks if the string is not empty. # @return [Boolean] Returns true if the string is not empty, false otherwise. def non_empty? !empty? @@ -52,11 +55,12 @@ # @param string [String] The string to be colored. # @param color_methods [Hash] A hash where keys are color names (String/Symbol) and values are color methods. # @param color_key [String, Symbol] The key representing the desired color method in the color_methods hash. # @param default_method [String] (optional) Default color method to use if color_key is not found in color_methods. Defaults to 'plain'. # @return [String] The colored string. - def apply_color_from_hash(string, color_methods, color_key, default_method: 'plain') + def apply_color_from_hash(string, color_methods, color_key, + default_method: 'plain') color_method = color_methods.fetch(color_key, default_method).to_sym string.to_s.send(color_method) end # # Enhanced `apply_color_from_hash` method to support dynamic color transformations @@ -76,21 +80,21 @@ # } # string = "Hello, World!" # colored_string = apply_color_from_hash(string, color_transformations, :red) # puts colored_string # This will print the string in red - # Searches for the first element in a collection where the specified key matches a given value. + # Searches for the first element in a collection where the specified message sent to an element matches a given value. # This method is particularly useful for finding a specific hash-like object within an enumerable collection. # If no match is found, it returns a specified default value. # # @param blocks [Enumerable] The collection of hash-like objects to search. - # @param key [Object] The key to search for in each element of the collection. - # @param value [Object] The value to match against each element's corresponding key value. + # @param msg [Symbol, String] The message to send to each element of the collection. + # @param value [Object] The value to match against the result of the message sent to each element. # @param default [Object, nil] The default value to return if no match is found (optional). # @return [Object, nil] The first matching element or the default value if no match is found. - def block_find(blocks, key, value, default = nil) - blocks.find { |item| item[key] == value } || default + def block_find(blocks, msg, value, default = nil) + blocks.find { |item| item.send(msg) == value } || default end def code_merge(*bodies) merge_lists(*bodies) end @@ -130,12 +134,14 @@ end # delete the current line if it is empty and the previous is also empty def delete_consecutive_blank_lines!(blocks_menu) blocks_menu.process_and_conditionally_delete! do |prev_item, current_item, _next_item| - prev_item&.fetch(:chrome, nil) && !prev_item&.fetch(:oname).present? && - current_item&.fetch(:chrome, nil) && !current_item&.fetch(:oname).present? + prev_item&.fetch(:chrome, nil) && + !(prev_item && prev_item.oname.present?) && + current_item&.fetch(:chrome, nil) && + !(current_item && current_item.oname.present?) end end # # Deletes a temporary file specified by an environment variable. # # Checks if the file exists before attempting to delete it and clears the environment variable afterward. @@ -186,17 +192,18 @@ # Filters out nil values, flattens the arrays, and ensures an empty list is returned if no valid lists are provided merged = args.compact.flatten merged.empty? ? [] : merged end - def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, block_name: nil) + def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, + block_name: nil) # Set block_name based on block_name_from_cli block_name = @cli_block_name if block_name_from_cli # Determine the state of breaker based on was_using_cli and the block type - # true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block[:shell] equals BlockType::BASH. In all other scenarios, breaker is false. - breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH + # true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block.shell equals BlockType::BASH. In all other scenarios, breaker is false. + breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.shell == BlockType::BASH # Reset block_name_from_cli if the conditions are not met block_name_from_cli ||= false [block_name, block_name_from_cli, breaker] @@ -280,11 +287,12 @@ # If the fcb has a body and meets certain conditions, it yields to the given block. # # @param fcb [Object] The fcb object whose attributes are to be updated. # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable. # @param block [Block] An optional block to yield to if conditions are met. - def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, &block) + def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, + &block) initialize_fcb_names(fcb) return unless fcb.body default_block_title_from_body(fcb) MarkdownExec::Filter.yield_to_block_if_applicable(fcb, messages, configuration, @@ -427,11 +435,13 @@ 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 index != 0 + trial_length += current_line.length + 1 + @rest_indent.length + end 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 @@ -478,11 +488,12 @@ @prompt = HashDelegator.tty_prompt_without_disabled_symbol @most_recent_loaded_filename = nil @pass_args = [] @run_state = OpenStruct.new( - link_history: [] + link_history: [], + source: OpenStruct.new ) @link_history = LinkHistory.new @fout = FOut.new(@delegate_object) ### slice only relevant keys @process_mutex = Mutex.new @@ -504,17 +515,22 @@ # # @param menu_blocks [Array] The array of menu block elements to be modified. def add_menu_chrome_blocks!(menu_blocks:, link_state:) return unless @delegate_object[:menu_link_format].present? - add_inherited_lines(menu_blocks: menu_blocks, link_state: link_state) if @delegate_object[:menu_with_inherited_lines] + if @delegate_object[:menu_with_inherited_lines] + add_inherited_lines(menu_blocks: menu_blocks, + link_state: link_state) + end # back before exit add_back_option(menu_blocks: menu_blocks) if should_add_back_option? # exit after other options - add_exit_option(menu_blocks: menu_blocks) if @delegate_object[:menu_with_exit] + if @delegate_object[:menu_with_exit] + add_exit_option(menu_blocks: menu_blocks) + end add_dividers(menu_blocks: menu_blocks) end private @@ -586,22 +602,36 @@ if insert_at_top menu_blocks.unshift(chrome_block) else menu_blocks.push(chrome_block) end + + chrome_block end # Appends a formatted divider to the specified position in a menu block array. # The method checks for the presence of formatting options before appending. # # @param menu_blocks [Array] The array of menu block elements. # @param position [Symbol] The position to insert the divider (:initial or :final). + def append_divider(menu_blocks:, position:) + return unless divider_formatting_present?(position) + + divider = create_divider(position) + position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider) + end + + # Appends a formatted divider to the specified position in a menu block array. + # The method checks for the presence of formatting options before appending. + # + # @param menu_blocks [Array] The array of menu block elements. + # @param position [Symbol] The position to insert the divider (:initial or :final). def append_inherited_lines(menu_blocks:, link_state:, position: top) - return unless link_state.inherited_lines.present? + return unless link_state.inherited_lines_present? insert_at_top = @delegate_object[:menu_inherited_lines_at_top] - chrome_blocks = link_state.inherited_lines.map do |line| + chrome_blocks = link_state.inherited_lines_map do |line| formatted = format(@delegate_object[:menu_inherited_lines_format], { line: line }) FCB.new( chrome: true, disabled: '', @@ -621,22 +651,10 @@ end rescue StandardError HashDelegator.error_handler('append_inherited_lines') end - # Appends a formatted divider to the specified position in a menu block array. - # The method checks for the presence of formatting options before appending. - # - # @param menu_blocks [Array] The array of menu block elements. - # @param position [Symbol] The position to insert the divider (:initial or :final). - def append_divider(menu_blocks:, position:) - return unless divider_formatting_present?(position) - - divider = create_divider(position) - position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider) - end - # private # Applies shell color options to the given string if applicable. # # @param name [String] The name to potentially colorize. @@ -670,11 +688,11 @@ blocks = [] iter_blocks_from_nested_files do |btype, fcb| process_block_based_on_type(blocks, btype, fcb) end - # &bc 'blocks.count:', blocks.count + # &bt blocks.count blocks rescue StandardError HashDelegator.error_handler('blocks_from_nested_files') end @@ -682,11 +700,12 @@ # 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| block_name == item.pub_name - end&.merge( + end, + OpenStruct.new( block_name_from_cli: true, block_name_from_ui: false ), MenuState::CONTINUE ) @@ -696,16 +715,18 @@ def calc_logged_stdout_filename(block_name:) return unless @delegate_object[:saved_stdout_folder] @delegate_object[:logged_stdout_filename] = - SavedAsset.new(blockname: block_name, - filename: @delegate_object[:filename], - prefix: @delegate_object[:logged_stdout_filename_prefix], - time: Time.now.utc, - exts: '.out.txt', - saved_asset_format: @delegate_object[:saved_asset_format]).generate_name + SavedAsset.new( + blockname: block_name, + filename: @delegate_object[:filename], + prefix: @delegate_object[:logged_stdout_filename_prefix], + time: Time.now.utc, + exts: '.out.txt', + saved_asset_format: shell_escape_asset_format(@dml_link_state) + ).generate_name @logged_stdout_filespec = @delegate_object[:logged_stdout_filespec] = File.join @delegate_object[:saved_stdout_folder], @delegate_object[:logged_stdout_filename] @@ -735,17 +756,18 @@ # If the block type is VARS, it also sets environment variables based on the block's content. # # @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) + def collect_required_code_lines(mdoc:, selected:, block_source:, + link_state: LinkState.new) required = mdoc.collect_recursively_required_code( 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 - ) + ) # &bt 'required' dependencies = (link_state&.inherited_dependencies || {}).merge(required[:dependencies] || {}) required[:unmet_dependencies] = (required[:unmet_dependencies] || []) - (link_state&.inherited_block_names || []) if required[:unmet_dependencies].present? ### filter against link_state.inherited_block_names @@ -753,18 +775,23 @@ warn format_and_highlight_dependencies(dependencies, highlight: required[:unmet_dependencies]) runtime_exception(:runtime_exception_error_level, 'unmet_dependencies, flag: runtime_exception_error_level', required[:unmet_dependencies]) - else + elsif false ### use option 2024-08-02 warn format_and_highlight_dependencies(dependencies, highlight: [@delegate_object[:block_name]]) end - code_lines = selected[:shell] == BlockType::VARS ? set_environment_variables_for_block(selected) : [] - - HashDelegator.code_merge(link_state&.inherited_lines, required[:code] + code_lines) + if selected[:shell] == BlockType::OPTS + # body of blocks is returned as a list of lines to be read an YAML + HashDelegator.code_merge(required[:blocks].map(&:body).flatten(1)) + else + code_lines = selected.shell == BlockType::VARS ? set_environment_variables_for_block(selected) : [] + HashDelegator.code_merge(link_state&.inherited_lines, + required[:code] + code_lines) + end end def command_execute(command, args: []) @run_state.files = StreamsOut.new @run_state.options = @delegate_object @@ -781,28 +808,31 @@ ) ) else @run_state.in_own_window = false execute_command_with_streams( - [@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args] + [@delegate_object[:shell], '-c', command, + @delegate_object[:filename], *args] ) end @run_state.completed_at = Time.now.utc rescue Errno::ENOENT => err # Handle ENOENT error @run_state.aborted_at = Time.now.utc @run_state.error_message = err.message @run_state.error = err - @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, @run_state.error_message) + @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, + @run_state.error_message) @fout.fout "Error ENOENT: #{err.inspect}" rescue SignalException => err # Handle SignalException @run_state.aborted_at = Time.now.utc @run_state.error_message = 'SIGTERM' @run_state.error = err - @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, @run_state.error_message) + @run_state.files.append_stream_line(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, rest: '') { @@ -828,22 +858,29 @@ # 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. # @param selected [Hash] The selected item from the menu to be executed. # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one. - def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:, link_state: nil) + def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:, + link_state: nil) required_lines = collect_required_code_lines(mdoc: mdoc, selected: selected, link_state: link_state, block_source: block_source) output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve] - display_required_code(required_lines: required_lines) if output_or_approval + if output_or_approval + display_required_code(required_lines: required_lines) + end allow_execution = if @delegate_object[:user_must_approve] - prompt_for_user_approval(required_lines: required_lines, selected: selected) + prompt_for_user_approval(required_lines: required_lines, + selected: selected) else true end - execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution + if allow_execution + execute_required_lines(required_lines: required_lines, + selected: selected) + end link_state.block_name = nil LoadFileLinkState.new(LoadFile::REUSE, link_state) end @@ -997,14 +1034,16 @@ # @return [Boolean] Execute the named block. def debounce_allows return true unless @delegate_object[:debounce_execution] # filter block if selected in menu - return true if @run_state.block_name_from_cli + return true if @run_state.source.block_name_from_cli # return false if @prior_execution_block == @delegate_object[:block_name] - return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat if @prior_execution_block == @delegate_object[:block_name] + if @prior_execution_block == @delegate_object[:block_name] + return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat + end @prior_execution_block = @delegate_object[:block_name] @allowed_execution_block = nil true end @@ -1017,21 +1056,25 @@ # It categorizes the selected option into either EXIT, BACK, or CONTINUE state. # # @param selected_option [Hash] The selected menu option. # @return [SelectedBlockMenuState] An object representing the state of the selected block. def determine_block_state(selected_option) - option_name = selected_option.fetch(:oname, nil) + option_name = selected_option[:oname] if option_name == menu_chrome_formatted_option(:menu_option_exit_name) return SelectedBlockMenuState.new(nil, + OpenStruct.new, MenuState::EXIT) end if option_name == menu_chrome_formatted_option(:menu_option_back_name) return SelectedBlockMenuState.new(selected_option, + OpenStruct.new, MenuState::BACK) end - SelectedBlockMenuState.new(selected_option, MenuState::CONTINUE) + SelectedBlockMenuState.new(selected_option, + OpenStruct.new, + MenuState::CONTINUE) end # Displays the required lines of code with color formatting for the preview section. # It wraps the code lines between a formatted header and tail. # @@ -1066,32 +1109,42 @@ @menu_base_options = @delegate_object @dml_link_state = LinkState.new( block_name: @delegate_object[:block_name], document_filename: @delegate_object[:filename] ) - @run_state.block_name_from_cli = @dml_link_state.block_name.present? + @run_state.source.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_now_using_cli = @run_state.source.block_name_from_cli @dml_menu_default_dname = nil @dml_block_state = SelectedBlockMenuState.new @doc_saved_lines_files = [] ## load file with code lines per options # if @menu_base_options[:load_code].present? - @dml_link_state.inherited_lines = [] - @menu_base_options[:load_code].split(':').map do |path| - @dml_link_state.inherited_lines += File.readlines(path, chomp: true) - end + @dml_link_state.inherited_lines = + @menu_base_options[:load_code].split(':').map do |path| + File.readlines(path, chomp: true) + end.flatten(1) inherited_block_names = [] inherited_dependencies = {} - selected = { oname: 'load_code' } - pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected) + selected = FCB.new(oname: 'load_code') + pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, + code_lines, inherited_dependencies, selected) end - fdo = ->(mo) { format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[mo])) } + fdo = ->(option) { + name = format(@delegate_object[:menu_link_format], + HashDelegator.safeval(@delegate_object[option])) + OpenStruct.new( + dname: name, + oname: name, + name: name, + pub_name: name.pub_name + ) + } item_back = fdo.call(:menu_option_back_name) item_edit = fdo.call(:menu_option_edit_name) item_history = fdo.call(:menu_option_history_name) item_load = fdo.call(:menu_option_load_name) item_save = fdo.call(:menu_option_save_name) @@ -1099,40 +1152,69 @@ item_view = fdo.call(:menu_option_view_name) @run_state.batch_random = Random.new.rand @run_state.batch_index = 0 + @run_state.files = StreamsOut.new + InputSequencer.new( @delegate_object[:filename], @delegate_object[:input_cli_rest] ).run do |msg, data| + # &bt msg case msg when :parse_document # once for each menu # puts "@ - parse document #{data}" inpseq_parse_document(data) if @delegate_object[:menu_for_history] - history_files.tap do |files| - menu_enable_option(item_history, files.count, 'files', menu_state: MenuState::HISTORY) if files.count.positive? + history_files(@dml_link_state).tap do |files| + if files.count.positive? + menu_enable_option(item_history.oname, files.count, 'files', + menu_state: MenuState::HISTORY) + end end end if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present? - sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob]) + sf = document_name_in_glob_as_file_name( + @dml_link_state.document_filename, + @delegate_object[:document_saved_lines_glob] + ) files = sf ? Dir.glob(sf) : [] @doc_saved_lines_files = files.count.positive? ? files : [] - lines_count = @dml_link_state.inherited_lines&.count || 0 + lines_count = @dml_link_state.inherited_lines_count # add menu items (glob, load, save) and enable selectively - menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive? - menu_enable_option(item_load, files.count, 'files', menu_state: MenuState::LOAD) if files.count.positive? - menu_enable_option(item_edit, lines_count, 'lines', menu_state: MenuState::EDIT) if lines_count.positive? - menu_enable_option(item_save, 1, '', menu_state: MenuState::SAVE) if lines_count.positive? - menu_enable_option(item_view, 1, '', menu_state: MenuState::VIEW) if lines_count.positive? - menu_enable_option(item_shell, 1, '', menu_state: MenuState::SHELL) if @delegate_object[:menu_with_shell] + if files.count.positive? || lines_count.positive? + menu_add_disabled_option(sf) + end + if files.count.positive? + menu_enable_option(item_load.dname, files.count, 'files', + menu_state: MenuState::LOAD) + end + if lines_count.positive? + menu_enable_option(item_edit.dname, lines_count, 'lines', + menu_state: MenuState::EDIT) + end + if lines_count.positive? + menu_enable_option(item_save.dname, 1, '', + menu_state: MenuState::SAVE) + end + if lines_count.positive? + menu_enable_option(item_view.dname, 1, '', + menu_state: MenuState::VIEW) + end + if @delegate_object[:menu_with_shell] + menu_enable_option(item_shell.dname, 1, '', + menu_state: MenuState::SHELL) + end + + # # reflect new menu items + # @dml_mdoc = MDoc.new(@dml_menu_blocks) end when :display_menu # warn "@ - display menu:" # ii_display_menu @@ -1141,11 +1223,11 @@ 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.pub_name == @dml_link_state.block_name + item.pub_name == @dml_link_state.block_name || item.oname == @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 @@ -1154,11 +1236,11 @@ # puts "! - Executing block: #{data}" @dml_block_state.block&.pub_name when :execute_block case (block_name = data) - when item_back + when item_back.pub_name debounce_reset @menu_user_clicked_back_link = true load_file_link_state = pop_link_history_and_trigger_load @dml_link_state = load_file_link_state.link_state @@ -1169,68 +1251,98 @@ document_filename: @dml_link_state.document_filename, prior_block_was_link: true ) ) - when item_edit + when item_edit.pub_name debounce_reset - edited = edit_text(@dml_link_state.inherited_lines.join("\n")) + edited = edit_text(@dml_link_state.inherited_lines_block) @dml_link_state.inherited_lines = edited.split("\n") if edited return :break if pause_user_exit InputSequencer.next_link_state(prior_block_was_link: true) - when item_history + when item_history.pub_name debounce_reset - files = history_files + files = history_files(@dml_link_state) files_table_rows = files.map do |file| if Regexp.new(@delegate_object[:saved_asset_match]) =~ file - OpenStruct.new(file: file, row: [$~[:time], $~[:blockname], $~[:exts]].join(' ')) + begin + OpenStruct.new( + file: file, + row: format( + @delegate_object[:saved_history_format], + # create with default '*' so unknown parameters are given a wildcard + $~.names.each_with_object(Hash.new('*')) do |name, hash| + hash[name.to_sym] = $~[name] + end + ) + ) + rescue KeyError + # pp $!, $@ + warn "Cannot format with: #{@delegate_object[:saved_history_format]}" + error_handler('saved_history_format') + break + end else warn "Cannot parse name: #{file}" next end - end.compact + end&.compact - case (name = prompt_select_code_filename( - [@delegate_object[:prompt_filespec_back]] + - files_table_rows.map(&:row), - string: @delegate_object[:prompt_select_history_file], - color_sym: :prompt_color_after_script_execution - )) - when @delegate_object[:prompt_filespec_back] - # do nothing - else - file = files_table_rows.select { |ftr| ftr.row == name }&.first - info = file_info(file.file) - warn "#{file.file} - #{info[:lines]} lines / #{info[:size]} bytes" - warn(File.readlines(file.file, chomp: false).map.with_index do |line, ind| - format(' %s. %s', format('% 4d', ind).violet, line) - end) + return :break unless files_table_rows + + # repeat select+display until user exits + row_attrib = :row + loop do + # menu with Back and Facet options at top + case (name = prompt_select_code_filename( + [@delegate_object[:prompt_filespec_back], + @delegate_object[:prompt_filespec_facet]] + + files_table_rows.map(&row_attrib), + string: @delegate_object[:prompt_select_history_file], + color_sym: :prompt_color_after_script_execution + )) + when @delegate_object[:prompt_filespec_back] + break + when @delegate_object[:prompt_filespec_facet] + row_attrib = row_attrib == :row ? :file : :row + else + file = files_table_rows.select { |ftr| ftr.row == name }&.first + info = file_info(file.file) + warn "#{file.file} - #{info[:lines]} lines / #{info[:size]} bytes" + warn(File.readlines(file.file, + chomp: false).map.with_index do |line, ind| + format(' %s. %s', format('% 4d', ind + 1).violet, line) + end) + end end return :break if pause_user_exit InputSequencer.next_link_state(prior_block_was_link: true) - when item_load + when item_load.pub_name debounce_reset - sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob]) + sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, + @delegate_object[:document_saved_lines_glob]) load_filespec = load_filespec_from_expression(sf) if load_filespec - @dml_link_state.inherited_lines ||= [] - @dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true) + @dml_link_state.inherited_lines_append( + File.readlines(load_filespec, chomp: true) + ) end return :break if pause_user_exit InputSequencer.next_link_state(prior_block_was_link: true) - when item_save + when item_save.pub_name debounce_reset - sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob]) + sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, + @delegate_object[:document_saved_lines_glob]) save_filespec = save_filespec_from_expression(sf) if save_filespec && !write_file_with_directory_creation( save_filespec, HashDelegator.join_code_lines(@dml_link_state.inherited_lines) ) @@ -1238,11 +1350,11 @@ end InputSequencer.next_link_state(prior_block_was_link: true) - when item_shell + when item_shell.pub_name debounce_reset loop do command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen) break if !command.present? || command == 'exit' @@ -1259,57 +1371,57 @@ return :break if pause_user_exit InputSequencer.next_link_state(prior_block_was_link: true) - when item_view + when item_view.pub_name debounce_reset - warn @dml_link_state.inherited_lines.join("\n") + warn @dml_link_state.inherited_lines_block return :break if pause_user_exit InputSequencer.next_link_state(prior_block_was_link: true) else @dml_block_state = block_state_for_name_from_cli(block_name) - if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS + if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS debounce_reset link_state = LinkState.new options_state = read_show_options_and_trigger_reuse( - selected: @dml_block_state.block, - link_state: link_state + link_state: link_state, + mdoc: @dml_mdoc, + selected: @dml_block_state.block ) - @menu_base_options.merge!(options_state.options) - @delegate_object.merge!(options_state.options) + update_menu_base(options_state.options) options_state.load_file_link_state.link_state else inpseq_execute_block(block_name) - if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli, + if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli, selected: @dml_block_state.block) return :break end ## order of block name processing: link block, cli, from user # - @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \ + @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break = HashDelegator.next_link_state( block_name: @dml_link_state.block_name, 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 + if !@dml_block_state.source.block_name_from_ui && cli_break # &bsp '!block_name_from_ui + cli_break -> break' return :break end InputSequencer.next_link_state( block_name: @dml_link_state.block_name, - prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH + prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH ) end end when :exit? @@ -1326,13 +1438,17 @@ end # remove leading "./" # replace characters: / : . * (space) with: (underscore) def document_name_in_glob_as_file_name(document_filename, glob) - return document_filename if document_filename.nil? || document_filename.empty? + if document_filename.nil? || document_filename.empty? + return document_filename + end - format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') }) + format(glob, + { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, + '_') }) end def dump_and_warn_block_state(selected:) if selected.nil? Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}", @@ -1349,11 +1465,14 @@ # @param delegate_object [Hash] The delegate object containing configuration flags. # @param blocks_in_file [Hash] Hash of blocks present in the file. # @param menu_blocks [Hash] Hash of menu blocks. # @param link_state [LinkState] Current state of the link. def dump_delobj(blocks_in_file, menu_blocks, link_state) - warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object] + if @delegate_object[:dump_delegate_object] + warn format_and_highlight_hash(@delegate_object, + label: '@delegate_object') + end if @delegate_object[:dump_blocks_in_file] warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file), label: 'blocks_in_file') end @@ -1361,15 +1480,22 @@ if @delegate_object[:dump_menu_blocks] warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks), label: 'menu_blocks') end - warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names] - warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies] + if @delegate_object[:dump_inherited_block_names] + warn format_and_highlight_lines(link_state.inherited_block_names, + label: 'inherited_block_names') + end + if @delegate_object[:dump_inherited_dependencies] + warn format_and_highlight_lines(link_state.inherited_dependencies, + label: 'inherited_dependencies') + end return unless @delegate_object[:dump_inherited_lines] - warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines') + warn format_and_highlight_lines(link_state.inherited_lines, + label: 'inherited_lines') end # Opens text in an editor for user modification and returns the modified text. # # This method reads the provided text, opens it in the default editor, @@ -1430,11 +1556,11 @@ block_source: block_source ) # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item [lfls.link_state, - lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]] + 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. # @@ -1451,21 +1577,24 @@ 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| + 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| + 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| + 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 @@ -1488,12 +1617,17 @@ # including output formatting and summarization. # # @param required_lines [Array<String>] The lines of code to be executed. # @param selected [FCB] The selected functional code block object. def execute_required_lines(required_lines: [], selected: FCB.new) - write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script] - calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state + if @delegate_object[:save_executed_script] + write_command_file(required_lines: required_lines, + selected: selected) + end + if @dml_block_state + calc_logged_stdout_filename(block_name: @dml_block_state.block.oname) + end format_and_execute_command(code_lines: required_lines) post_execution_process end # Execute a code block after approval and provide user interaction options. @@ -1503,64 +1637,53 @@ # code to the clipboard or save it to a file. # # @param opts [Hash] Options hash containing configuration settings. # @param mdoc [YourMDocClass] An instance of the MDoc class. # - def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new) - if selected.fetch(:shell, '') == BlockType::LINK + def execute_shell_type(selected:, mdoc:, block_source:, + link_state: LinkState.new) + if selected.shell == BlockType::LINK debounce_reset - push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''), + push_link_history_and_trigger_load(link_block_body: selected.body, mdoc: mdoc, selected: selected, link_state: link_state, block_source: block_source) elsif @menu_user_clicked_back_link debounce_reset pop_link_history_and_trigger_load - elsif selected[:shell] == BlockType::OPTS + elsif selected.shell == BlockType::OPTS debounce_reset - block_names = [] code_lines = [] - dependencies = {} - options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state) + options_state = read_show_options_and_trigger_reuse( + link_state: link_state, + mdoc: @dml_mdoc, + selected: selected + ) + update_menu_base(options_state.options) - ## apply options to current state - # - @menu_base_options.merge!(options_state.options) - @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.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: '', - next_document_filename: @delegate_object[:filename], - next_load_file: LoadFile::REUSE - ) + next_state_append_code(selected, link_state, code_lines) - elsif selected[:shell] == BlockType::VARS + elsif selected.shell == BlockType::PORT debounce_reset - block_names = [] - code_lines = set_environment_variables_for_block(selected) - dependencies = {} - link_history_push_and_next( - 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: '', - next_document_filename: @delegate_object[:filename], - next_load_file: LoadFile::REUSE + required_lines = collect_required_code_lines( + mdoc: @dml_mdoc, + selected: selected, + link_state: link_state, + block_source: block_source ) + next_state_set_code(selected, link_state, required_lines) + elsif selected.shell == BlockType::VARS + debounce_reset + next_state_append_code(selected, link_state, + set_environment_variables_for_block(selected)) + elsif debounce_allows compile_execute_and_trigger_reuse(mdoc: mdoc, selected: selected, link_state: link_state, block_source: block_source) @@ -1644,10 +1767,20 @@ # Expand expression if it contains format specifiers def formatted_expression(expr) expr.include?('%{') ? format_expression(expr) : expr end + def generate_temp_filename(ext = '.sh') + filename = begin + Dir::Tmpname.make_tmpname(['x', ext], nil) + rescue NoMethodError + require 'securerandom' + "#{SecureRandom.urlsafe_base64}#{ext}" + end + File.join(Dir.tmpdir, filename) + end + # Processes a block to generate its summary, modifying its attributes based on various matching criteria. # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname. # # @param fcb [Object] An object representing a functional code block. # @return [Object] The modified functional code block with updated summary attributes. @@ -1657,24 +1790,25 @@ fcb.call = fcb.title.match(Regexp.new(@delegate_object[:block_calls_scan]))&.fetch(1, nil) titlexcall = fcb.call ? fcb.title.sub("%#{fcb.call}", '') : fcb.title bm = extract_named_captures_from_option(titlexcall, @delegate_object[:block_name_match]) - shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]] + shell_color_option = SHELL_COLOR_OPTIONS[fcb.shell] if @delegate_object[:block_name_nick_match].present? && fcb.oname =~ Regexp.new(@delegate_object[:block_name_nick_match]) fcb.nickname = $~[0] fcb.title = fcb.oname = format_multiline_body_as_title(fcb.body) else fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall end fcb.dname = HashDelegator.indent_all_lines( apply_shell_color_option(fcb.oname, shell_color_option), - fcb.fetch(:indent, nil) + fcb.indent ) - fcb + + fcb # &br end # Updates the delegate object's state based on the provided block state. # It sets the block name and determines if the user clicked the back link in the menu. # @@ -1693,56 +1827,71 @@ 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.append_stream_line(file_type, line) if @run_state.files.streams - - if @delegate_object[:output_stdout] - # print line - puts line + if @run_state.files.streams + @run_state.files.append_stream_line(file_type, + line) end + puts line if @delegate_object[:output_stdout] + yield line if block_given? end rescue IOError # Handle IOError ensure @process_cv.signal end end end - def history_files - Dir.glob( + def history_files(link_state, order: :chronological, direction: :reverse) + files = Dir.glob( File.join( @delegate_object[:saved_script_folder], - SavedAsset.new(filename: @delegate_object[:filename], - saved_asset_format: @delegate_object[:saved_asset_format]).generate_name + SavedAsset.new( + filename: @delegate_object[:filename], + saved_asset_format: shell_escape_asset_format(link_state) + ).generate_name ) ) + + sorted_files = case order + when :alphabetical + files.sort + when :chronological + files.sort_by { |file| File.mtime(file) } + else + raise ArgumentError, "Invalid order: #{order}" + end + + direction == :reverse ? sorted_files.reverse : sorted_files end # Initializes variables for regex and other states def initial_state { - fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch( - :fenced_start_and_end_regex, '^(?<indent> *)`{3,}' - )), - fenced_start_extended_regex: Regexp.new(@delegate_object.fetch( - :fenced_start_and_end_regex, '^(?<indent> *)`{3,}' - )), + fenced_start_and_end_regex: + Regexp.new(@delegate_object.fetch( + :fenced_start_and_end_regex, '^(?<indent> *)`{3,}' + )), + fenced_start_extended_regex: + Regexp.new(@delegate_object.fetch( + :fenced_start_and_end_regex, '^(?<indent> *)`{3,}' + )), fcb: MarkdownExec::FCB.new, in_fenced_block: false, headings: [] } end def inpseq_execute_block(block_name) @dml_block_state = block_state_for_name_from_cli(block_name) dump_and_warn_block_state(selected: @dml_block_state.block) - @dml_link_state, @dml_menu_default_dname = \ + @dml_link_state, @dml_menu_default_dname = exec_bash_next_state( selected: @dml_block_state.block, mdoc: @dml_mdoc, link_state: @dml_link_state, block_source: { @@ -1755,21 +1904,21 @@ def inpseq_parse_document(_document_filename) @run_state.batch_index += 1 @run_state.in_own_window = false # &bsp 'loop', block_name_from_cli, @cli_block_name - @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \ - set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli, + @run_state.source.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = + set_delobj_menu_loop_vars(block_name_from_cli: @run_state.source.block_name_from_cli, now_using_cli: @dml_now_using_cli, link_state: @dml_link_state) end def inpseq_user_choice @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file, menu_blocks: @dml_menu_blocks, default: @dml_menu_default_dname) - # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli + # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli if !@dml_block_state HashDelegator.error_handler('block_state missing', { abort: true }) elsif @dml_block_state.state == MenuState::EXIT # &bsp 'load_cli_or_user_selected_block -> break' :break @@ -1791,12 +1940,14 @@ &block) end end end - def link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source:) - all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines) + def link_block_data_eval(link_state, code_lines, selected, link_block_data, + block_source:) + all_code = HashDelegator.code_merge(link_state&.inherited_lines, + code_lines) output_lines = [] Tempfile.open do |file| cmd = "#{@delegate_object[:shell]} #{file.path}" file.write(all_code.join("\n")) @@ -1811,31 +1962,39 @@ ## 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), + begin_pattern: @delegate_object.fetch(:output_assignment_begin, + nil), end_pattern: @delegate_object.fetch(:output_assignment_end, nil), scan1: @delegate_object.fetch(:output_assignment_match, nil), format1: @delegate_object.fetch(:output_assignment_format, nil) ) else output_lines = `#{cmd}`.split("\n") end end - HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true }) unless output_lines + unless output_lines + HashDelegator.error_handler('all_code eval output_lines is nil', + { abort: true }) + end 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.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 + next unless re =~ line + + re.gsub_format(line, + link_block_data.fetch('format', + '%{line}')) end.compact + [label_format_below && format(label_format_below, block_source.merge({ block_name: selected.pub_name }))] end @@ -1880,38 +2039,45 @@ # Loads auto blocks based on delegate object settings and updates if new filename is detected. # Executes a specified block once per filename. # @param all_blocks [Array] Array of all block elements. # @return [Boolean, nil] True if values were modified, nil otherwise. - def load_auto_opts_block(all_blocks) + def load_auto_opts_block(all_blocks, mdoc:) block_name = @delegate_object[:document_load_opts_block_name] - return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] + unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] + return + end block = HashDelegator.block_find(all_blocks, :oname, block_name) return unless block - options_state = read_show_options_and_trigger_reuse(selected: block) - @menu_base_options.merge!(options_state.options) - @delegate_object.merge!(options_state.options) + options_state = read_show_options_and_trigger_reuse( + mdoc: mdoc, + selected: block + ) + update_menu_base(options_state.options) @most_recent_loaded_filename = @delegate_object[:filename] true end - def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil) + 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.pub_name == @delegate_object[:block_name] - end&.merge(block_name_from_ui: false) + end + source = OpenStruct.new(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) + block = block_state.block + source = OpenStruct.new(block_name_from_ui: true) state = block_state.state end - SelectedBlockMenuState.new(block, state) + SelectedBlockMenuState.new(block, source, state) rescue StandardError HashDelegator.error_handler('load_cli_or_user_selected_block') end # format + glob + select for file in load block @@ -1930,11 +2096,12 @@ # 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 }) + HashDelegator.error_handler("no files found with '#{expr}' ", + { abort: true }) elsif auto_load_single && files.count == 1 files.first else ## user selects from existing files or other # @@ -1964,24 +2131,26 @@ def mdoc_menu_and_blocks_from_nested_files(link_state) 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) + all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block( + all_blocks, mdoc: mdoc + ) 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) - [all_blocks, menu_blocks, mdoc] + [all_blocks, menu_blocks, mdoc] # &br end def menu_add_disabled_option(name) raise unless name.present? raise if @dml_menu_blocks.nil? - block = @dml_menu_blocks.find { |item| item[:oname] == name } + block = @dml_menu_blocks.find { |item| item.oname == name } # create menu item when it is needed (count > 0) # return unless block.nil? @@ -2015,11 +2184,13 @@ # Formats a menu option based on the delegate object's configuration. # It safely evaluates the value of the option and optionally formats it. # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object. # @return [String] The formatted or original value of the menu option. def menu_chrome_formatted_option(option_symbol = :menu_option_back_name) - option_value = HashDelegator.safeval(@delegate_object.fetch(option_symbol, '')) + option_value = HashDelegator.safeval(@delegate_object.fetch( + option_symbol, '' + )) if @delegate_object[:menu_chrome_format] format(@delegate_object[:menu_chrome_format], option_value) else option_value @@ -2028,39 +2199,40 @@ def menu_enable_option(name, count, type, menu_state: MenuState::LOAD) raise unless name.present? raise if @dml_menu_blocks.nil? - item = @dml_menu_blocks.find { |block| block[:oname] == name } + item = @dml_menu_blocks.find { |block| block.oname == name } # create menu item when it is needed (count > 0) # if item.nil? && count.positive? - append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state) - item = @dml_menu_blocks.find { |block| block[:oname] == name } + item = append_chrome_block(menu_blocks: @dml_menu_blocks, + menu_state: menu_state) end # update item if it exists # return unless item - item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name + item.dname = type.present? ? "#{name} (#{count} #{type})" : name if count.positive? item.delete(:disabled) else item[:disabled] = '' end end - def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:) + def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, + link_state:) if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name] # &bsp 'pause cli control, allow user to select block' block_name_from_cli = false now_using_cli = false - @menu_base_options[:block_name] = \ + @menu_base_options[:block_name] = @delegate_object[:block_name] = \ - link_state.block_name = \ + link_state.block_name = @cli_block_name = nil end @delegate_object = @menu_base_options.dup @menu_user_clicked_back_link = false @@ -2077,10 +2249,31 @@ @delegate_object[method_name] # super end end + def next_state_append_code(selected, link_state, code_lines) + next_state_set_code(selected, link_state, HashDelegator.code_merge( + link_state&.inherited_lines, code_lines + )) + end + + def next_state_set_code(selected, link_state, code_lines) + block_names = [] + dependencies = {} + link_history_push_and_next( + 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(code_lines), + next_block_name: '', + next_document_filename: @delegate_object[:filename], + next_load_file: LoadFile::REUSE + ) + end + def output_color_formatted(data_sym, color_sym) formatted_string = string_send_color(@delegate_object[data_sym], color_sym) @fout.fout formatted_string end @@ -2157,13 +2350,16 @@ else # no history exists; must have been called independently => retain script link_history_push_and_next( 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), + 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: next_block_name, next_document_filename: @delegate_object[:filename], # not next_document_filename next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD ) # LoadFileLinkState.new(LoadFile::REUSE, link_state) @@ -2175,16 +2371,19 @@ # # @return [LoadFileLinkState] An object indicating the action to load the next block. def pop_link_history_and_trigger_load pop = @link_history.pop peek = @link_history.peek - LoadFileLinkState.new(LoadFile::LOAD, LinkState.new( - document_filename: pop.document_filename, - inherited_block_names: peek.inherited_block_names, - inherited_dependencies: peek.inherited_dependencies, - inherited_lines: peek.inherited_lines - )) + LoadFileLinkState.new( + LoadFile::LOAD, + LinkState.new( + document_filename: pop.document_filename, + inherited_block_names: peek.inherited_block_names, + inherited_dependencies: peek.inherited_dependencies, + inherited_lines: peek.inherited_lines + ) + ) end def post_execution_process do_save_execution_output output_execution_summary @@ -2196,24 +2395,25 @@ # @param all_blocks [Array<Hash>] The list of blocks from the file. # @param opts [Hash] The options hash. # @return [Array<Hash>] The updated blocks menu. def prepare_blocks_menu(menu_blocks) menu_blocks.map do |fcb| - next if Filter.prepared_not_in_menu?(@delegate_object, fcb, - %i[block_name_include_match block_name_wrapper_match]) + next if Filter.prepared_not_in_menu?( + @delegate_object, + fcb, + %i[block_name_include_match block_name_wrapper_match] + ) - fcb.merge!( - name: fcb.dname, - label: BlockLabel.make( - body: fcb[:body], - filename: @delegate_object[:filename], - headings: fcb.fetch(:headings, []), - menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname], - menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings], - text: fcb[:text], - title: fcb[:title] - ) + fcb.name = fcb.dname + fcb.label = BlockLabel.make( + body: fcb.body, + filename: @delegate_object[:filename], + headings: fcb.headings, + menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname], + menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings], + text: fcb.text, + title: fcb.title ) fcb.to_h end.compact end @@ -2230,11 +2430,14 @@ when :blocks blocks.push(get_block_summary(fcb)) when :filter %i[blocks line] when :line - create_and_add_chrome_blocks(blocks, fcb) unless @delegate_object[:no_chrome] + unless @delegate_object[:no_chrome] + create_and_add_chrome_blocks(blocks, + fcb) + end end end def process_string_array(arr, begin_pattern: nil, end_pattern: nil, scan1: nil, format1: nil) @@ -2303,11 +2506,12 @@ # 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 def prompt_for_filespec_with_wildcard(filespec) - puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec }) + puts format(@delegate_object[:prompt_show_expr_format], + { expr: filespec }) puts @delegate_object[:prompt_enter_filespec] begin input = gets.chomp PathUtils.resolve_path_or_substitute(input, filespec) @@ -2360,30 +2564,44 @@ exit 1 end # public - def prompt_select_code_filename(filenames, string: @delegate_object[:prompt_select_code_file], color_sym: :prompt_color_after_script_execution) + def prompt_select_code_filename( + filenames, + color_sym: :prompt_color_after_script_execution, + cycle: true, + enum: false, + quiet: true, + string: @delegate_object[:prompt_select_code_file] + ) @prompt.select( string_send_color(string, color_sym), - filter: true, - quiet: true + cycle: cycle, + filter: !enum, + per_page: @delegate_object[:select_page_height], + quiet: quiet ) do |menu| - filenames.each do |filename| - menu.choice filename + menu.enum '.' if enum + filenames.each.with_index do |filename, ind| + if enum + menu.choice filename, ind + 1 + else + menu.choice filename + end end end rescue TTY::Reader::InputInterrupt exit 1 end - def prompt_select_continue + def prompt_select_continue(filter: true, quiet: true) sel = @prompt.select( string_send_color(@delegate_object[:prompt_after_script_execution], :prompt_color_after_script_execution), - filter: true, - quiet: true + filter: filter, + quiet: quiet ) do |menu| menu.choice @delegate_object[:prompt_yes] menu.choice @delegate_object[:prompt_exit] end sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE @@ -2392,11 +2610,11 @@ end # user prompt to exit if the menu will be displayed again # def prompt_user_exit(block_name_from_cli:, selected:) - 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. @@ -2441,22 +2659,30 @@ ## append blocks loaded # if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present? load_filespec = load_filespec_from_expression(load_expr) - code_lines += File.readlines(load_filespec, chomp: true) if load_filespec + if load_filespec + code_lines += File.readlines(load_filespec, + chomp: true) + end end # if an eval link block, evaluate code_lines and return its standard output # if link_block_data.fetch(LinkKeys::EVAL, - false) || link_block_data.fetch(LinkKeys::EXEC, false) - code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source) + false) || link_block_data.fetch(LinkKeys::EXEC, + false) + code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, + block_source: block_source) end - next_document_filename = write_inherited_lines_to_file(link_state, link_block_data) - next_block_name = link_block_data.fetch(LinkKeys::NEXT_BLOCK, nil) || link_block_data.fetch(LinkKeys::BLOCK, nil) || '' + next_document_filename = write_inherited_lines_to_file(link_state, + link_block_data) + next_block_name = link_block_data.fetch(LinkKeys::NEXT_BLOCK, + nil) || link_block_data.fetch(LinkKeys::BLOCK, + nil) || '' if link_block_data[LinkKeys::RETURN] pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines, dependencies, selected, next_block_name: next_block_name) @@ -2464,11 +2690,13 @@ link_history_push_and_next( 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), + inherited_lines: HashDelegator.code_merge( + link_state&.inherited_lines, code_lines + ), next_block_name: next_block_name, next_document_filename: next_document_filename, next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD ) end @@ -2485,18 +2713,33 @@ # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output. # @param selected [Hash] Selected item from the menu containing a YAML body. # @param tgt2 [Hash, nil] An optional target hash to update with YAML data. # @return [LoadFileLinkState] An instance indicating the next action for loading files. - def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new) + def read_show_options_and_trigger_reuse(selected:, + mdoc:, link_state: LinkState.new) obj = {} - data = YAML.load(selected[:body].join("\n")) - (data || []).each do |key, value| - sym_key = key.to_sym - obj[sym_key] = value - print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present? + # concatenated body of all required blocks loaded a YAML + data = YAML.load( + collect_required_code_lines( + mdoc: mdoc, selected: selected, + link_state: link_state, block_source: {} + ).join("\n") + ).transform_keys(&:to_sym) + + if selected.shell == BlockType::OPTS + obj = data + else + (data || []).each do |key, value| + sym_key = key.to_sym + obj[sym_key] = value + + if @delegate_object[:menu_opts_set_format].present? + print_formatted_option(key, value) + end + end end link_state.block_name = nil OpenStruct.new(options: obj, load_file_link_state: LoadFileLinkState.new( @@ -2524,15 +2767,23 @@ def register_console_attributes(opts) if (resized = @delegate_object[:menu_resize_terminal]) resize_terminal end - opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize if resized || !opts[:console_width] + if resized || !opts[:console_width] + 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? + unless opts[:select_page_height]&.positive? + opts[:per_page] = + opts[:select_page_height] = + [opts[:console_height] - 3, 4].max + end rescue StandardError - HashDelegator.error_handler('register_console_attributes', { abort: true }) + 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. @@ -2540,11 +2791,12 @@ def respond_to?(method_name, include_private = false) if super true elsif @delegate_object.respond_to?(method_name, include_private) true - elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private) + elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, + include_private) true else @delegate_object.respond_to?(method_name, include_private) end end @@ -2591,11 +2843,12 @@ else ## user selects from existing files or other # input into path with wildcard for easy entry # case (name = prompt_select_code_filename( - [@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files, + [@delegate_object[:prompt_filespec_back], + @delegate_object[:prompt_filespec_other]] + files, string: @delegate_object[:prompt_select_code_file], color_sym: :prompt_color_after_script_execution )) when @delegate_object[:prompt_filespec_back] # do nothing @@ -2611,41 +2864,50 @@ write_command_file(required_lines: required_lines, selected: selected) @fout.fout "File saved: #{@run_state.saved_filespec}" end # 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 = {}) + def select_option_with_metadata(prompt_text, menu_items, opts = {}) ## configure to environment # register_console_attributes(opts) # crashes if all menu options are disabled selection = @prompt.select(prompt_text, - names, + menu_items, opts.merge(filter: true)) - selected_name = names.find do |item| + + selected = menu_items.find do |item| if item.instance_of?(Hash) - item[:dname] == selection + (item[:name] || item[:dname]) == selection + elsif item.instance_of?(MarkdownExec::FCB) + item.dname == selection else item == selection end end - selected_name = { dname: selected_name } if selected_name.instance_of?(String) - unless selected_name - HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found') + if selected.instance_of?(String) + selected = FCB.new(dname: selected) + elsif selected.instance_of?(Hash) + selected = FCB.new(selected) + end + unless selected + HashDelegator.error_handler('select_option_with_metadata', + error: 'menu item not found') exit 1 end - selected_name.merge( - if selection == menu_chrome_colored_option(:menu_option_back_name) - { option: selection, shell: BlockType::LINK } - elsif selection == menu_chrome_colored_option(:menu_option_exit_name) - { option: selection } - else - { selected: selection } - end - ) + if selection == menu_chrome_colored_option(:menu_option_back_name) + selected.option = selection + selected.shell = BlockType::LINK + elsif selection == menu_chrome_colored_option(:menu_option_exit_name) + selected.option = selection + else + selected.selected = selection + end + + selected rescue TTY::Reader::InputInterrupt exit 1 rescue StandardError HashDelegator.error_handler('select_option_with_metadata') end @@ -2661,12 +2923,13 @@ @delegate_object[:filename] = link_state.document_filename link_state.block_name = @delegate_object[:block_name] = block_name_from_cli ? @cli_block_name : link_state.block_name end - def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:) - block_name_from_cli, now_using_cli = \ + def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, + link_state:) + block_name_from_cli, now_using_cli = manage_cli_selection_state(block_name_from_cli: block_name_from_cli, now_using_cli: now_using_cli, link_state: link_state) set_delob_filename_block_name(link_state: link_state, block_name_from_cli: block_name_from_cli) @@ -2679,11 +2942,11 @@ [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc] end def set_environment_variables_for_block(selected) code_lines = [] - YAML.load(selected[:body].join("\n"))&.each do |key, value| + YAML.load(selected.body.join("\n"))&.each do |key, value| ENV[key] = value.to_s require 'shellwords' code_lines.push "#{key}=\"#{Shellwords.escape(value)}\"" @@ -2694,10 +2957,33 @@ print string_send_color(formatted_string, :menu_vars_set_color) end code_lines end + def shell_escape_asset_format(link_state) + raw = @delegate_object[:saved_asset_format] + + return raw unless @delegate_object[:shell_parameter_expansion] + + # unchanged if no parameter expansion takes place + return raw unless /$/ =~ raw + + filespec = generate_temp_filename + cmd = [@delegate_object[:shell], '-c', filespec].join(' ') + + marker = Random.new.rand.to_s + + code = (link_state&.inherited_lines || []) + ["echo -n \"#{marker}#{raw}\""] + # &bt code + File.write filespec, HashDelegator.join_code_lines(code) + File.chmod 0o755, filespec + + out = `#{cmd}`.sub(/.*?#{marker}/m, '') + File.delete filespec + out # &br + end + def should_add_back_option? @delegate_object[:menu_with_back] && @link_history.prior_state_exist? end # Initializes a new fenced code block (FCB) object based on the provided line and heading information. @@ -2816,10 +3102,17 @@ # &bsp 'line is not recognized for block state' end end + ## apply options to current state + # + def update_menu_base(options) + @menu_base_options.merge!(options) + @delegate_object.merge!(options) + end + def wait_for_stream_processing @process_mutex.synchronize do @process_cv.wait(@process_mutex) end rescue Interrupt @@ -2837,12 +3130,15 @@ def wait_for_user_selection(_all_blocks, menu_blocks, default) prompt_title = string_send_color( @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution ) - block_menu = prepare_blocks_menu(menu_blocks) - return SelectedBlockMenuState.new(nil, MenuState::EXIT) if block_menu.empty? + menu_items = prepare_blocks_menu(menu_blocks) + if menu_items.empty? + return SelectedBlockMenuState.new(nil, OpenStruct.new, + MenuState::EXIT) + end # default value may not match if color is different from originating menu (opts changed while processing) selection_opts = if default && menu_blocks.map(&:dname).include?(default) @delegate_object.merge(default: default) else @@ -2850,11 +3146,11 @@ end sph = @delegate_object[:select_page_height] selection_opts.merge!(per_page: sph) - selected_option = select_option_with_metadata(prompt_title, block_menu, + selected_option = select_option_with_metadata(prompt_title, menu_items, selection_opts) determine_block_state(selected_option) end # Handles the core logic for generating the command file's metadata and content. @@ -2865,11 +3161,11 @@ @run_state.saved_script_filename = SavedAsset.new(blockname: selected.pub_name, exts: '.sh', filename: @delegate_object[:filename], prefix: @delegate_object[:saved_script_filename_prefix], - saved_asset_format: @delegate_object[:saved_asset_format], + saved_asset_format: shell_escape_asset_format(@dml_link_state), time: time_now).generate_name @run_state.saved_filespec = File.join(@delegate_object[:saved_script_folder], @run_state.saved_script_filename) @@ -2916,11 +3212,12 @@ # return next document file name def write_inherited_lines_to_file(link_state, link_block_data) save_expr = link_block_data.fetch(LinkKeys::SAVE, '') if save_expr.present? save_filespec = save_filespec_from_expression(save_expr) - File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines)) + File.write(save_filespec, + HashDelegator.join_code_lines(link_state&.inherited_lines)) @delegate_object[:filename] else link_block_data[LinkKeys::FILE] || @delegate_object[:filename] end end @@ -2948,11 +3245,15 @@ obj.each do |key, value| cleaned_value = clean_value(value) # Clean and possibly convert value obj[key] = cleaned_value if value.is_a?(Hash) || value.is_a?(Struct) end - obj.reject! { |_key, value| [nil, '', [], {}, nil].include?(value) } if obj.is_a?(Hash) + if obj.is_a?(Hash) + obj.reject! do |_key, value| + [nil, '', [], {}, nil].include?(value) + end + end obj end def self.next_link_state(*args, **kwargs, &block) @@ -3012,11 +3313,12 @@ @hd = HashDelegator.new end # Test case for empty body def test_next_link_state - @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil, block_name: nil) + @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil, + block_name: nil) end end class TestHashDelegator < Minitest::Test def setup @@ -3059,19 +3361,22 @@ end # Test case for non-empty body with 'file' key def test_push_link_history_and_trigger_load_with_file_key body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"] - expected_result = LoadFileLinkState.new(LoadFile::LOAD, - LinkState.new(block_name: 'sample_block', - document_filename: 'sample_file', - inherited_dependencies: {}, - inherited_lines: ['# ', 'KEY="VALUE"'])) + expected_result = LoadFileLinkState.new( + LoadFile::LOAD, + LinkState.new(block_name: 'sample_block', + document_filename: 'sample_file', + inherited_dependencies: {}, + inherited_lines: ['# ', 'KEY="VALUE"']) + ) assert_equal expected_result, @hd.push_link_history_and_trigger_load( link_block_body: body, - selected: FCB.new(block_name: 'sample_block', filename: 'sample_file') + selected: FCB.new(block_name: 'sample_block', + filename: 'sample_file') ) end def test_indent_all_lines_with_indent body = "Line 1\nLine 2" @@ -3176,24 +3481,24 @@ def setup @hd = HashDelegator.new end def test_block_find_with_match - blocks = [{ key: 'value1' }, { key: 'value2' }] - result = HashDelegator.block_find(blocks, :key, 'value1') - assert_equal({ key: 'value1' }, result) + blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] + result = HashDelegator.block_find(blocks, :text, 'value1') + assert_equal('value1', result.text) end def test_block_find_without_match - blocks = [{ key: 'value1' }, { key: 'value2' }] - result = HashDelegator.block_find(blocks, :key, 'value3') + blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] + result = HashDelegator.block_find(blocks, :text, 'missing_value') assert_nil result end def test_block_find_with_default - blocks = [{ key: 'value1' }, { key: 'value2' }] - result = HashDelegator.block_find(blocks, :key, 'value3', 'default') + blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] + result = HashDelegator.block_find(blocks, :text, 'missing_value', 'default') assert_equal 'default', result end end class TestHashDelegatorBlocksFromNestedFiles < Minitest::Test @@ -3225,20 +3530,21 @@ class TestHashDelegatorCollectRequiredCodeLines < Minitest::Test def setup @hd = HashDelegator.new @hd.instance_variable_set(:@delegate_object, {}) @mdoc = mock('YourMDocClass') - @selected = { shell: BlockType::VARS, body: ['key: value'] } + @selected = FCB.new(shell: BlockType::VARS, body: ['key: value']) HashDelegator.stubs(:read_required_blocks_from_temp_file).returns([]) @hd.stubs(:string_send_color) @hd.stubs(:print) end def test_collect_required_code_lines_with_vars YAML.stubs(:load).returns({ 'key' => 'value' }) @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] }) - result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, block_source: {}) + result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, + block_source: {}) assert_equal ['code line', 'key="value"'], result end end @@ -3255,22 +3561,28 @@ @hd.instance_variable_set(:@delegate_object, { block_name: 'block1' }) result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks) - assert_equal all_blocks.first.merge(block_name_from_ui: false), result.block + assert_equal all_blocks.first, + result.block + assert_equal OpenStruct.new(block_name_from_ui: false), + result.source assert_nil result.state end def test_user_selected_block - block_state = SelectedBlockMenuState.new({ oname: 'block2' }, + block_state = SelectedBlockMenuState.new({ oname: 'block2' }, OpenStruct.new, :some_state) @hd.stubs(:wait_for_user_selected_block).returns(block_state) result = @hd.load_cli_or_user_selected_block - assert_equal block_state.block.merge(block_name_from_ui: true), result.block + assert_equal block_state.block, + result.block + assert_equal OpenStruct.new(block_name_from_ui: true), + result.source assert_equal :some_state, result.state end end class TestHashDelegatorCountBlockInFilename < Minitest::Test @@ -3349,30 +3661,30 @@ @hd = HashDelegator.new @hd.stubs(:menu_chrome_formatted_option).returns('Formatted Option') end def test_determine_block_state_exit - selected_option = { oname: 'Formatted Option' } + selected_option = FCB.new(oname: 'Formatted Option') @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_exit_name).returns('Formatted Option') result = @hd.determine_block_state(selected_option) assert_equal MenuState::EXIT, result.state assert_nil result.block end def test_determine_block_state_back - selected_option = { oname: 'Formatted Back Option' } + selected_option = FCB.new(oname: 'Formatted Back Option') @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('Formatted Back Option') result = @hd.determine_block_state(selected_option) assert_equal MenuState::BACK, result.state assert_equal selected_option, result.block end def test_determine_block_state_continue - selected_option = { oname: 'Other Option' } + selected_option = FCB.new(oname: 'Other Option') result = @hd.determine_block_state(selected_option) assert_equal MenuState::CONTINUE, result.state assert_equal selected_option, result.block @@ -3478,11 +3790,12 @@ end def test_format_execution_stream_with_empty_key @hd.instance_variable_get(:@run_state).stubs(:files).returns({}) - result = HashDelegator.format_execution_stream(nil, ExecutionStreams::STD_ERR) + result = HashDelegator.format_execution_stream(nil, + ExecutionStreams::STD_ERR) assert_equal '', result end def test_format_execution_stream_with_nil_files @@ -3637,11 +3950,12 @@ @hd.stubs(:cfile).returns(Minitest::Mock.new) @hd.stubs(:update_line_and_block_state) end def test_iter_blocks_from_nested_files - @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'], import_paths: nil) + @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'], + import_paths: nil) selected_messages = ['filtered message'] result = @hd.iter_blocks_from_nested_files { selected_messages } assert_equal ['line 1', 'line 2'], result @@ -3743,11 +4057,12 @@ end end def test_yield_line_if_selected_with_line block_called = false - HashDelegator.yield_line_if_selected('Test line', [:line]) do |type, content| + HashDelegator.yield_line_if_selected('Test line', + [:line]) do |type, content| block_called = true assert_equal :line, type assert_equal 'Test line', content.body[0] end assert block_called @@ -3778,19 +4093,22 @@ end def test_update_menu_attrib_yield_selected_with_body HashDelegator.expects(:initialize_fcb_names).with(@fcb) HashDelegator.expects(:default_block_title_from_body).with(@fcb) - Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], {}) + Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], + {}) - HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message]) + HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, + messages: [:some_message]) end def test_update_menu_attrib_yield_selected_without_body @fcb.stubs(:body).returns(nil) HashDelegator.expects(:initialize_fcb_names).with(@fcb) - HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message]) + HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, + messages: [:some_message]) end end class TestHashDelegatorWaitForUserSelectedBlock < Minitest::Test def setup @@ -3862,32 +4180,39 @@ class PathUtilsTest < Minitest::Test def test_absolute_path_returns_unchanged absolute_path = '/usr/local/bin' expression = 'path/to/*/directory' - assert_equal absolute_path, PathUtils.resolve_path_or_substitute(absolute_path, expression) + assert_equal absolute_path, + PathUtils.resolve_path_or_substitute(absolute_path, + expression) end def test_relative_path_gets_substituted relative_path = 'my_folder' expression = 'path/to/*/directory' expected_output = 'path/to/my_folder/directory' - assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression) + assert_equal expected_output, + PathUtils.resolve_path_or_substitute(relative_path, + expression) end def test_path_with_no_slash_substitutes_correctly relative_path = 'data' expression = 'path/to/*/directory' expected_output = 'path/to/data/directory' - assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression) + assert_equal expected_output, + PathUtils.resolve_path_or_substitute(relative_path, + expression) end def test_empty_path_substitution empty_path = '' expression = 'path/to/*/directory' expected_output = 'path/to//directory' - assert_equal expected_output, PathUtils.resolve_path_or_substitute(empty_path, expression) + assert_equal expected_output, + PathUtils.resolve_path_or_substitute(empty_path, expression) end # Test formatting a string containing UTF-8 characters def test_format_utf8_characters input = 'Unicode test: ā, ΓΆ, πŸ’», and πŸš€ are fun!' @@ -3932,10 +4257,11 @@ end private def prompt_for_filespec_with_wildcard(filespec) - puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec }) + puts format(@delegate_object[:prompt_show_expr_format], + { expr: filespec }) puts @delegate_object[:prompt_enter_filespec] begin input = gets.chomp PathUtils.resolve_path_or_substitute(input, filespec)