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

- old
+ new

@@ -32,10 +32,11 @@ require_relative 'link_history' require_relative 'mdoc' require_relative 'regexp' require_relative 'resize_terminal' require_relative 'std_out_err_logger' +require_relative 'streams_out' require_relative 'string_util' class String # Checks if the string is not empty. # @return [Boolean] Returns true if the string is not empty, false otherwise. @@ -161,19 +162,10 @@ # call_stack.take(n + 1)[1..].map do |line| # " . #{line.sub(/^#{Regexp.escape(base_path)}\//, '')}" # end.join("\n") # end - # Formats and returns the execution streams (like stdin, stdout, stderr) for a given key. - # It concatenates the array of strings found under the specified key in the run_state's files. - # - # @param key [Symbol] The key corresponding to the desired execution stream. - # @return [String] A concatenated string of the execution stream's contents. - def format_execution_streams(key, files = {}) - (files || {}).fetch(key, []).join - end - # Indents all lines in a given string with a specified indentation string. # @param body [String] A multi-line string to be indented. # @param indent [String] The string used for indentation (default is an empty string). # @return [String] A single string with each line indented as specified. def indent_all_lines(body, indent = nil) @@ -264,20 +256,10 @@ 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 @@ -307,25 +289,10 @@ default_block_title_from_body(fcb) MarkdownExec::Filter.yield_to_block_if_applicable(fcb, messages, configuration, &block) end - def write_execution_output_to_file(files, filespec) - FileUtils.mkdir_p File.dirname(filespec) - - File.write( - filespec, - ["-STDOUT-\n", - format_execution_streams(ExecutionStreams::STD_OUT, files), - "-STDERR-\n", - format_execution_streams(ExecutionStreams::STD_ERR, files), - "-STDIN-\n", - format_execution_streams(ExecutionStreams::STD_IN, files), - "\n"].join - ) - end - # Yields a line as a new block if the selected message type includes :line. # @param [String] line The line to be processed. # @param [Array<Symbol>] selected_messages A list of message types to check. # @param [Proc] block The block to be called with the line data. def yield_line_if_selected(line, selected_messages, &block) @@ -585,10 +552,13 @@ option_name = @delegate_object[:menu_option_edit_name] insert_at_top = @delegate_object[:menu_load_at_top] when MenuState::EXIT option_name = @delegate_object[:menu_option_exit_name] insert_at_top = @delegate_object[:menu_exit_at_top] + when MenuState::HISTORY + option_name = @delegate_object[:menu_option_history_name] + insert_at_top = @delegate_object[:menu_load_at_top] when MenuState::LOAD 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] @@ -597,10 +567,12 @@ 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] + else + raise "Missing MenuState: #{menu_state}" end formatted_name = format(@delegate_object[:menu_link_format], HashDelegator.safeval(option_name)) chrome_block = FCB.new( @@ -724,15 +696,16 @@ def calc_logged_stdout_filename(block_name:) return unless @delegate_object[:saved_stdout_folder] @delegate_object[:logged_stdout_filename] = - SavedAsset.stdout_name(blockname: block_name, - filename: File.basename(@delegate_object[:filename], - '.*'), - prefix: @delegate_object[:logged_stdout_filename_prefix], - time: Time.now.utc) + 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 @logged_stdout_filespec = @delegate_object[:logged_stdout_filespec] = File.join @delegate_object[:saved_stdout_folder], @delegate_object[:logged_stdout_filename] @@ -791,11 +764,11 @@ HashDelegator.code_merge(link_state&.inherited_lines, required[:code] + code_lines) end def command_execute(command, args: []) - run_state_reset_stream_logs + @run_state.files = StreamsOut.new @run_state.options = @delegate_object @run_state.started_at = Time.now.utc if @delegate_object[:execute_in_own_window] && @delegate_object[:execute_command_format].present? && @@ -818,18 +791,18 @@ 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[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[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: '') { @@ -1078,12 +1051,11 @@ def do_save_execution_output return unless @delegate_object[:save_execution_output] return if @run_state.in_own_window - HashDelegator.write_execution_output_to_file(@run_state.files, - @delegate_object[:logged_stdout_filespec]) + @run_state.files.write_execution_output_to_file(@delegate_object[:logged_stdout_filespec]) end # Select and execute a code block from a Markdown document. # # This method allows the user to interactively select a code block from a @@ -1115,16 +1087,18 @@ 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) end - 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])) + fdo = ->(mo) { format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[mo])) } + 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) + item_shell = fdo.call(:menu_option_shell_name) + item_view = fdo.call(:menu_option_view_name) @run_state.batch_random = Random.new.rand @run_state.batch_index = 0 InputSequencer.new( @@ -1134,25 +1108,31 @@ 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? + 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]) files = sf ? Dir.glob(sf) : [] @doc_saved_lines_files = files.count.positive? ? files : [] 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) 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] + 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] end when :display_menu # warn "@ - display menu:" # ii_display_menu @@ -1193,20 +1173,59 @@ 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 + + return :break if pause_user_exit + InputSequencer.next_link_state(prior_block_was_link: true) + when item_history + debounce_reset + files = history_files + 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(' ')) + else + warn "Cannot parse name: #{file}" + next + end + 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) + end + + return :break if pause_user_exit + + InputSequencer.next_link_state(prior_block_was_link: true) + when item_load debounce_reset 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) end + + return :break if pause_user_exit + InputSequencer.next_link_state(prior_block_was_link: true) when item_save debounce_reset sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob]) @@ -1216,10 +1235,11 @@ 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 @@ -1234,15 +1254,21 @@ warn "#{'OK'.green} #{exit_status}" else warn "#{'ERR'.bred} #{exit_status}" end end + + return :break if pause_user_exit + InputSequencer.next_link_state(prior_block_was_link: true) when item_view debounce_reset warn @dml_link_state.inherited_lines.join("\n") + + 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 @@ -1556,10 +1582,25 @@ color_sym: :execution_report_preview_frame_color) data_string = @delegate_object.fetch(data_sym, default).to_s string_send_color(data_string, color_sym) end + # size of a file in bytes and the number of lines + def file_info(file_path) + file_size = 0 + line_count = 0 + + File.open(file_path, 'r') do |file| + file.each_line do |_line| + line_count += 1 + end + file_size = file.size + end + + { size: file_size, lines: line_count } + end + def format_and_execute_command(code_lines:) formatted_command = code_lines.flatten.join("\n") @fout.fout fetch_color(data_sym: :script_execution_head, color_sym: :script_execution_frame_color) command_execute(formatted_command, args: @pass_args) @@ -1652,11 +1693,11 @@ 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 if @run_state.files + @run_state.files.append_stream_line(file_type, line) if @run_state.files.streams if @delegate_object[:output_stdout] # print line puts line end @@ -1669,10 +1710,20 @@ @process_cv.signal end end end + def history_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 + ) + ) + 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,}' @@ -1750,11 +1801,11 @@ cmd = "#{@delegate_object[:shell]} #{file.path}" file.write(all_code.join("\n")) file.rewind if link_block_data.fetch(LinkKeys::EXEC, false) - run_state_reset_stream_logs + @run_state.files = StreamsOut.new execute_command_with_streams([cmd]) do |_stdin, stdout, stderr, _thread| line = stdout || stderr output_lines.push(line) if line end @@ -1874,21 +1925,26 @@ load_filespec_wildcard_expansion(expanded_expression) else expanded_expression 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 }) elsif auto_load_single && files.count == 1 files.first else ## user selects from existing files or other # - case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files)) + case (name = prompt_select_code_filename( + [@delegate_object[:prompt_filespec_back]] + files, + string: @delegate_object[:prompt_select_code_file], + color_sym: :prompt_color_after_script_execution + )) when @delegate_object[:prompt_filespec_back] # do nothing else name end @@ -2051,20 +2107,20 @@ end def output_execution_summary return unless @delegate_object[:output_execution_summary] - fout_section 'summary', { + @fout.fout_section 'summary', { execute_aborted_at: @run_state.aborted_at, execute_completed_at: @run_state.completed_at, execute_error: @run_state.error, execute_error_message: @run_state.error_message, - execute_files: @run_state.files, execute_options: @run_state.options, execute_started_at: @run_state.started_at, + saved_filespec: @run_state.saved_filespec, script_block_name: @run_state.script_block_name, - saved_filespec: @run_state.saved_filespec + streamed_lines: @run_state.files.streams } end def output_labeled_value(label, value, level) @fout.lout format_references_send_color( @@ -2073,10 +2129,15 @@ :output_execution_label_value_color) }, format_sym: :output_execution_label_format ), level: level end + def pause_user_exit + @delegate_object[:pause_after_script_execution] && + prompt_select_continue == MenuState::EXIT + end + def pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines, dependencies, selected, next_block_name: nil) pop = @link_history.pop # updatable if pop.document_filename next_state = LinkState.new( @@ -2299,14 +2360,13 @@ exit 1 end # public - def prompt_select_code_filename(filenames) + def prompt_select_code_filename(filenames, string: @delegate_object[:prompt_select_code_file], color_sym: :prompt_color_after_script_execution) @prompt.select( - string_send_color(@delegate_object[:prompt_select_code_file], - :prompt_color_after_script_execution), + string_send_color(string, color_sym), filter: true, quiet: true ) do |menu| filenames.each do |filename| menu.choice filename @@ -2443,11 +2503,11 @@ LoadFile::REUSE, link_state )) end # Registers console attributes by modifying the options hash. - # This method handles terminal resizing and adjusts the console dimensions + # This method handles terminal resizing and adjusts the console dimensions # and pagination settings based on the current terminal size. # # @param opts [Hash] a hash containing various options for the console settings. # - :console_width [Integer, nil] The width of the console. If not provided or if the terminal is resized, it will be set to the current console width. # - :console_height [Integer, nil] The height of the console. If not provided or if the terminal is resized, it will be set to the current console height. @@ -2460,23 +2520,19 @@ # @example # opts = { console_width: nil, console_height: nil, select_page_height: nil } # register_console_attributes(opts) # # opts will be updated with the current console dimensions and pagination settings. def register_console_attributes(opts) - begin - if (resized = @delegate_object[:menu_resize_terminal]) - resize_terminal - end + if (resized = @delegate_object[:menu_resize_terminal]) + resize_terminal + end - if resized || !opts[:console_width] - opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize - end + opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize if resized || !opts[:console_width] - 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 + 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. @@ -2491,17 +2547,10 @@ else @delegate_object.respond_to?(method_name, include_private) end end - def run_state_reset_stream_logs - @run_state.files = Hash.new() - @run_state.files[ExecutionStreams::STD_ERR] = [] - @run_state.files[ExecutionStreams::STD_IN] = [] - @run_state.files[ExecutionStreams::STD_OUT] = [] - end - def runtime_exception(exception_sym, name, items) if @delegate_object[exception_sym] != 0 data = { name: name, detail: items.join(', ') } warn( format( @@ -2541,12 +2590,15 @@ prompt_for_filespec_with_wildcard(filespec) else ## user selects from existing files or other # input into path with wildcard for easy entry # - name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files) - case name + case (name = prompt_select_code_filename( + [@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 when @delegate_object[:prompt_filespec_other] prompt_for_filespec_with_wildcard(filespec) else @@ -2809,16 +2861,16 @@ def write_command_file(required_lines:, selected:) return unless @delegate_object[:save_executed_script] time_now = Time.now.utc @run_state.saved_script_filename = - SavedAsset.script_name( - blockname: selected.pub_name, - filename: @delegate_object[:filename], - prefix: @delegate_object[:saved_script_filename_prefix], - time: time_now - ) + 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], + time: time_now).generate_name @run_state.saved_filespec = File.join(@delegate_object[:saved_script_folder], @run_state.saved_script_filename) shebang = if @delegate_object[:shebang]&.present? @@ -3414,28 +3466,30 @@ def setup @hd = HashDelegator.new @hd.instance_variable_set(:@run_state, mock('run_state')) end - def test_format_execution_streams_with_valid_key - result = HashDelegator.format_execution_streams(ExecutionStreams::STD_OUT, - { stdout: %w[output1 output2] }) + def test_format_execution_stream_with_valid_key + result = HashDelegator.format_execution_stream( + { stdout: %w[output1 output2] }, + ExecutionStreams::STD_OUT + ) - assert_equal 'output1output2', result + assert_equal "output1\noutput2", result end - def test_format_execution_streams_with_empty_key + def test_format_execution_stream_with_empty_key @hd.instance_variable_get(:@run_state).stubs(:files).returns({}) - result = HashDelegator.format_execution_streams(ExecutionStreams::STD_ERR) + result = HashDelegator.format_execution_stream(nil, ExecutionStreams::STD_ERR) assert_equal '', result end - def test_format_execution_streams_with_nil_files + def test_format_execution_stream_with_nil_files @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil) - result = HashDelegator.format_execution_streams(:stdin) + result = HashDelegator.format_execution_stream(nil, :stdin) assert_equal '', result end end