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