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