markdown_exec 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -5
- data/CHANGELOG.md +27 -0
- data/Gemfile.lock +1 -1
- data/bin/tab_completion.sh +15 -31
- data/examples/block_names.md +23 -0
- data/examples/linked.md +8 -0
- data/examples/linked_show.md +7 -0
- data/examples/load_code.md +10 -0
- data/examples/search.md +21 -0
- data/lib/color_scheme.rb +65 -0
- data/lib/constants.rb +18 -19
- data/lib/directory_searcher.rb +4 -2
- data/lib/find_files.rb +36 -42
- data/lib/hash_delegator.rb +842 -571
- data/lib/input_sequencer.rb +16 -9
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +146 -31
- data/lib/menu.src.yml +76 -19
- data/lib/menu.yml +68 -12
- data/lib/std_out_err_logger.rb +12 -14
- metadata +7 -2
data/lib/hash_delegator.rb
CHANGED
@@ -200,7 +200,7 @@ module HashDelegatorSelf
|
|
200
200
|
|
201
201
|
# Determine the state of breaker based on was_using_cli and the block type
|
202
202
|
# 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.
|
203
|
-
breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block
|
203
|
+
breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
|
204
204
|
|
205
205
|
# Reset block_name_from_cli if the conditions are not met
|
206
206
|
block_name_from_cli ||= false
|
@@ -232,14 +232,20 @@ module HashDelegatorSelf
|
|
232
232
|
FileUtils.rm_f(path)
|
233
233
|
end
|
234
234
|
|
235
|
-
# Evaluates the given string as Ruby code
|
235
|
+
# Evaluates the given string as Ruby code within a safe context.
|
236
236
|
# If an error occurs, it calls the error_handler method with 'safeval'.
|
237
237
|
# @param str [String] The string to be evaluated.
|
238
238
|
# @return [Object] The result of evaluating the string.
|
239
239
|
def safeval(str)
|
240
|
-
|
240
|
+
result = nil
|
241
|
+
binding.eval("result = #{str}")
|
242
|
+
|
243
|
+
result
|
241
244
|
rescue StandardError # catches NameError, StandardError
|
245
|
+
pp $!, $@
|
246
|
+
pp "code: #{str}"
|
242
247
|
error_handler('safeval')
|
248
|
+
exit 1
|
243
249
|
end
|
244
250
|
|
245
251
|
def set_file_permissions(file_path, chmod_value)
|
@@ -281,11 +287,11 @@ module HashDelegatorSelf
|
|
281
287
|
File.write(
|
282
288
|
filespec,
|
283
289
|
["-STDOUT-\n",
|
284
|
-
format_execution_streams(ExecutionStreams::
|
290
|
+
format_execution_streams(ExecutionStreams::STD_OUT, files),
|
285
291
|
"-STDERR-\n",
|
286
|
-
format_execution_streams(ExecutionStreams::
|
292
|
+
format_execution_streams(ExecutionStreams::STD_ERR, files),
|
287
293
|
"-STDIN-\n",
|
288
|
-
format_execution_streams(ExecutionStreams::
|
294
|
+
format_execution_streams(ExecutionStreams::STD_IN, files),
|
289
295
|
"\n"].join
|
290
296
|
)
|
291
297
|
end
|
@@ -357,7 +363,7 @@ module PathUtils
|
|
357
363
|
# @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
|
358
364
|
# @return [String] The absolute path or the expression with the wildcard replaced by the path.
|
359
365
|
def self.resolve_path_or_substitute(path, expression)
|
360
|
-
if path.
|
366
|
+
if path.start_with?('/')
|
361
367
|
path
|
362
368
|
else
|
363
369
|
expression.gsub('*', path)
|
@@ -479,9 +485,21 @@ module MarkdownExec
|
|
479
485
|
history_state_partition
|
480
486
|
option_name = @delegate_object[:menu_option_back_name]
|
481
487
|
insert_at_top = @delegate_object[:menu_back_at_top]
|
488
|
+
when MenuState::EDIT
|
489
|
+
option_name = @delegate_object[:menu_option_edit_name]
|
490
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
482
491
|
when MenuState::EXIT
|
483
492
|
option_name = @delegate_object[:menu_option_exit_name]
|
484
493
|
insert_at_top = @delegate_object[:menu_exit_at_top]
|
494
|
+
when MenuState::LOAD
|
495
|
+
option_name = @delegate_object[:menu_option_load_name]
|
496
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
497
|
+
when MenuState::SAVE
|
498
|
+
option_name = @delegate_object[:menu_option_save_name]
|
499
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
500
|
+
when MenuState::VIEW
|
501
|
+
option_name = @delegate_object[:menu_option_view_name]
|
502
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
485
503
|
end
|
486
504
|
|
487
505
|
formatted_name = format(@delegate_object[:menu_link_format],
|
@@ -489,7 +507,7 @@ module MarkdownExec
|
|
489
507
|
chrome_block = FCB.new(
|
490
508
|
chrome: true,
|
491
509
|
dname: HashDelegator.new(@delegate_object).string_send_color(
|
492
|
-
formatted_name, :
|
510
|
+
formatted_name, :menu_chrome_color
|
493
511
|
),
|
494
512
|
oname: formatted_name
|
495
513
|
)
|
@@ -587,6 +605,18 @@ module MarkdownExec
|
|
587
605
|
HashDelegator.error_handler('blocks_from_nested_files')
|
588
606
|
end
|
589
607
|
|
608
|
+
def block_state_for_name_from_cli(block_name)
|
609
|
+
SelectedBlockMenuState.new(
|
610
|
+
@dml_blocks_in_file.find do |item|
|
611
|
+
item[:oname] == block_name
|
612
|
+
end&.merge(
|
613
|
+
block_name_from_cli: true,
|
614
|
+
block_name_from_ui: false
|
615
|
+
),
|
616
|
+
MenuState::CONTINUE
|
617
|
+
)
|
618
|
+
end
|
619
|
+
|
590
620
|
# private
|
591
621
|
|
592
622
|
def calc_logged_stdout_filename(block_name:)
|
@@ -695,14 +725,14 @@ module MarkdownExec
|
|
695
725
|
'-c', command,
|
696
726
|
@delegate_object[:filename],
|
697
727
|
*args) do |stdin, stdout, stderr, exec_thr|
|
698
|
-
handle_stream(stream: stdout, file_type: ExecutionStreams::
|
728
|
+
handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
|
699
729
|
yield nil, line, nil, exec_thr if block_given?
|
700
730
|
end
|
701
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::
|
731
|
+
handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
|
702
732
|
yield nil, nil, line, exec_thr if block_given?
|
703
733
|
end
|
704
734
|
|
705
|
-
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::
|
735
|
+
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
|
706
736
|
stdin.puts(line)
|
707
737
|
yield line, nil, nil, exec_thr if block_given?
|
708
738
|
end
|
@@ -720,34 +750,17 @@ module MarkdownExec
|
|
720
750
|
@run_state.aborted_at = Time.now.utc
|
721
751
|
@run_state.error_message = err.message
|
722
752
|
@run_state.error = err
|
723
|
-
@run_state.files[ExecutionStreams::
|
753
|
+
@run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
|
724
754
|
@fout.fout "Error ENOENT: #{err.inspect}"
|
725
755
|
rescue SignalException => err
|
726
756
|
# Handle SignalException
|
727
757
|
@run_state.aborted_at = Time.now.utc
|
728
758
|
@run_state.error_message = 'SIGTERM'
|
729
759
|
@run_state.error = err
|
730
|
-
@run_state.files[ExecutionStreams::
|
760
|
+
@run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
|
731
761
|
@fout.fout "Error ENOENT: #{err.inspect}"
|
732
762
|
end
|
733
763
|
|
734
|
-
def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
|
735
|
-
if @delegate_object[:block_name].present?
|
736
|
-
block = all_blocks.find do |item|
|
737
|
-
item[:oname] == @delegate_object[:block_name]
|
738
|
-
end&.merge(block_name_from_ui: false)
|
739
|
-
else
|
740
|
-
block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
|
741
|
-
default)
|
742
|
-
block = block_state.block&.merge(block_name_from_ui: true)
|
743
|
-
state = block_state.state
|
744
|
-
end
|
745
|
-
|
746
|
-
SelectedBlockMenuState.new(block, state)
|
747
|
-
rescue StandardError
|
748
|
-
HashDelegator.error_handler('load_cli_or_user_selected_block')
|
749
|
-
end
|
750
|
-
|
751
764
|
# This method is responsible for handling the execution of generic blocks in a markdown document.
|
752
765
|
# It collects the required code lines from the document and, depending on the configuration,
|
753
766
|
# may display the code for user approval before execution. It then executes the approved block.
|
@@ -769,7 +782,12 @@ module MarkdownExec
|
|
769
782
|
execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
|
770
783
|
|
771
784
|
link_state.block_name = nil
|
772
|
-
LoadFileLinkState.new(LoadFile::
|
785
|
+
LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
786
|
+
end
|
787
|
+
|
788
|
+
# Check if the expression contains wildcard characters
|
789
|
+
def contains_wildcards?(expr)
|
790
|
+
expr.match(%r{\*|\?|\[})
|
773
791
|
end
|
774
792
|
|
775
793
|
def copy_to_clipboard(required_lines)
|
@@ -920,6 +938,312 @@ module MarkdownExec
|
|
920
938
|
@delegate_object[:logged_stdout_filespec])
|
921
939
|
end
|
922
940
|
|
941
|
+
# Select and execute a code block from a Markdown document.
|
942
|
+
#
|
943
|
+
# This method allows the user to interactively select a code block from a
|
944
|
+
# Markdown document, obtain approval, and execute the chosen block of code.
|
945
|
+
#
|
946
|
+
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
947
|
+
def document_inpseq
|
948
|
+
@menu_base_options = @delegate_object
|
949
|
+
@dml_link_state = LinkState.new(
|
950
|
+
block_name: @delegate_object[:block_name],
|
951
|
+
document_filename: @delegate_object[:filename]
|
952
|
+
)
|
953
|
+
@run_state.block_name_from_cli = @dml_link_state.block_name.present?
|
954
|
+
@cli_block_name = @dml_link_state.block_name
|
955
|
+
@dml_now_using_cli = @run_state.block_name_from_cli
|
956
|
+
@dml_menu_default_dname = nil
|
957
|
+
@dml_block_state = SelectedBlockMenuState.new
|
958
|
+
@doc_saved_lines_files = []
|
959
|
+
|
960
|
+
## load file with code lines per options
|
961
|
+
#
|
962
|
+
if @menu_base_options[:load_code].present?
|
963
|
+
@dml_link_state.inherited_lines = []
|
964
|
+
@menu_base_options[:load_code].split(':').map do |path|
|
965
|
+
@dml_link_state.inherited_lines += File.readlines(path, chomp: true)
|
966
|
+
end
|
967
|
+
|
968
|
+
inherited_block_names = []
|
969
|
+
inherited_dependencies = {}
|
970
|
+
selected = { oname: 'load_code' }
|
971
|
+
pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
|
972
|
+
end
|
973
|
+
|
974
|
+
item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
|
975
|
+
item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
|
976
|
+
item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
|
977
|
+
item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
|
978
|
+
item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
|
979
|
+
|
980
|
+
@run_state.batch_random = Random.new.rand
|
981
|
+
@run_state.batch_index = 0
|
982
|
+
|
983
|
+
InputSequencer.new(
|
984
|
+
@delegate_object[:filename],
|
985
|
+
@delegate_object[:input_cli_rest]
|
986
|
+
).run do |msg, data|
|
987
|
+
case msg
|
988
|
+
when :parse_document # once for each menu
|
989
|
+
# puts "@ - parse document #{data}"
|
990
|
+
inpseq_parse_document(data)
|
991
|
+
|
992
|
+
if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
|
993
|
+
|
994
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
995
|
+
files = sf ? Dir.glob(sf) : []
|
996
|
+
@doc_saved_lines_files = files.count.positive? ? files : []
|
997
|
+
|
998
|
+
lines_count = @dml_link_state.inherited_lines&.count || 0
|
999
|
+
|
1000
|
+
# add menu items (glob, load, save) and enable selectively
|
1001
|
+
menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
|
1002
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD)
|
1003
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT)
|
1004
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), lines_count, 'lines', menu_state: MenuState::SAVE)
|
1005
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), lines_count, 'lines', menu_state: MenuState::VIEW)
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
when :display_menu
|
1009
|
+
# warn "@ - display menu:"
|
1010
|
+
# ii_display_menu
|
1011
|
+
@dml_block_state = SelectedBlockMenuState.new
|
1012
|
+
@delegate_object[:block_name] = nil
|
1013
|
+
|
1014
|
+
when :user_choice
|
1015
|
+
if @dml_link_state.block_name.present?
|
1016
|
+
# @prior_block_was_link = true
|
1017
|
+
@dml_block_state.block = @dml_blocks_in_file.find do |item|
|
1018
|
+
item[:oname] == @dml_link_state.block_name
|
1019
|
+
end
|
1020
|
+
@dml_link_state.block_name = nil
|
1021
|
+
else
|
1022
|
+
# puts "? - Select a block to execute (or type #{$texit} to exit):"
|
1023
|
+
break if inpseq_user_choice == :break # into @dml_block_state
|
1024
|
+
break if @dml_block_state.block.nil? # no block matched
|
1025
|
+
end
|
1026
|
+
# puts "! - Executing block: #{data}"
|
1027
|
+
# @dml_block_state.block[:oname]
|
1028
|
+
@dml_block_state.block&.fetch(:oname, nil)
|
1029
|
+
|
1030
|
+
when :execute_block
|
1031
|
+
case (block_name = data)
|
1032
|
+
when item_back
|
1033
|
+
debounce_reset
|
1034
|
+
@menu_user_clicked_back_link = true
|
1035
|
+
load_file_link_state = pop_link_history_and_trigger_load
|
1036
|
+
@dml_link_state = load_file_link_state.link_state
|
1037
|
+
|
1038
|
+
InputSequencer.merge_link_state(
|
1039
|
+
@dml_link_state,
|
1040
|
+
InputSequencer.next_link_state(
|
1041
|
+
block_name: @dml_link_state.block_name,
|
1042
|
+
document_filename: @dml_link_state.document_filename,
|
1043
|
+
prior_block_was_link: true
|
1044
|
+
)
|
1045
|
+
)
|
1046
|
+
|
1047
|
+
when item_edit
|
1048
|
+
edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
|
1049
|
+
@dml_link_state.inherited_lines = edited.split("\n") if edited
|
1050
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1051
|
+
|
1052
|
+
when item_load
|
1053
|
+
debounce_reset
|
1054
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
1055
|
+
load_filespec = load_filespec_from_expression(sf)
|
1056
|
+
if load_filespec
|
1057
|
+
@dml_link_state.inherited_lines ||= []
|
1058
|
+
@dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
|
1059
|
+
end
|
1060
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1061
|
+
|
1062
|
+
when item_save
|
1063
|
+
debounce_reset
|
1064
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
1065
|
+
save_filespec = save_filespec_from_expression(sf)
|
1066
|
+
if save_filespec && !write_file_with_directory_creation(
|
1067
|
+
save_filespec,
|
1068
|
+
HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
|
1069
|
+
)
|
1070
|
+
return :break
|
1071
|
+
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1075
|
+
|
1076
|
+
when item_view
|
1077
|
+
warn @dml_link_state.inherited_lines.join("\n")
|
1078
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1079
|
+
|
1080
|
+
else
|
1081
|
+
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1082
|
+
if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
|
1083
|
+
debounce_reset
|
1084
|
+
link_state = LinkState.new
|
1085
|
+
options_state = read_show_options_and_trigger_reuse(
|
1086
|
+
selected: @dml_block_state.block,
|
1087
|
+
link_state: link_state
|
1088
|
+
)
|
1089
|
+
|
1090
|
+
@menu_base_options.merge!(options_state.options)
|
1091
|
+
@delegate_object.merge!(options_state.options)
|
1092
|
+
options_state.load_file_link_state.link_state
|
1093
|
+
else
|
1094
|
+
inpseq_execute_block(block_name)
|
1095
|
+
|
1096
|
+
if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
|
1097
|
+
selected: @dml_block_state.block)
|
1098
|
+
return :break
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
## order of block name processing: link block, cli, from user
|
1102
|
+
#
|
1103
|
+
@dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
|
1104
|
+
HashDelegator.next_link_state(
|
1105
|
+
block_name: @dml_link_state.block_name,
|
1106
|
+
block_name_from_cli: !@dml_link_state.block_name.present?,
|
1107
|
+
block_state: @dml_block_state,
|
1108
|
+
was_using_cli: @dml_now_using_cli
|
1109
|
+
)
|
1110
|
+
|
1111
|
+
if !@dml_block_state.block[:block_name_from_ui] && cli_break
|
1112
|
+
# &bsp '!block_name_from_ui + cli_break -> break'
|
1113
|
+
return :break
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
InputSequencer.next_link_state(
|
1117
|
+
block_name: @dml_link_state.block_name,
|
1118
|
+
prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
|
1119
|
+
)
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
when :exit?
|
1124
|
+
data == $texit
|
1125
|
+
when :stay?
|
1126
|
+
data == $stay
|
1127
|
+
else
|
1128
|
+
raise "Invalid message: #{msg}"
|
1129
|
+
end
|
1130
|
+
end
|
1131
|
+
rescue StandardError
|
1132
|
+
HashDelegator.error_handler('document_inpseq',
|
1133
|
+
{ abort: true })
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
# remove leading "./"
|
1137
|
+
# replace characters: / : . * (space) with: (underscore)
|
1138
|
+
def document_name_in_glob_as_file_name(document_filename, glob)
|
1139
|
+
return document_filename if document_filename.nil? || document_filename.empty?
|
1140
|
+
|
1141
|
+
format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
def dump_and_warn_block_state(selected:)
|
1145
|
+
if selected.nil?
|
1146
|
+
Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
|
1147
|
+
{ abort: true })
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
return unless @delegate_object[:dump_selected_block]
|
1151
|
+
|
1152
|
+
warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
# Outputs warnings based on the delegate object's configuration
|
1156
|
+
#
|
1157
|
+
# @param delegate_object [Hash] The delegate object containing configuration flags.
|
1158
|
+
# @param blocks_in_file [Hash] Hash of blocks present in the file.
|
1159
|
+
# @param menu_blocks [Hash] Hash of menu blocks.
|
1160
|
+
# @param link_state [LinkState] Current state of the link.
|
1161
|
+
def dump_delobj(blocks_in_file, menu_blocks, link_state)
|
1162
|
+
warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
|
1163
|
+
|
1164
|
+
if @delegate_object[:dump_blocks_in_file]
|
1165
|
+
warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
|
1166
|
+
label: 'blocks_in_file')
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
if @delegate_object[:dump_menu_blocks]
|
1170
|
+
warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
|
1171
|
+
label: 'menu_blocks')
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
|
1175
|
+
warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
|
1176
|
+
return unless @delegate_object[:dump_inherited_lines]
|
1177
|
+
|
1178
|
+
warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
# Opens text in an editor for user modification and returns the modified text.
|
1182
|
+
#
|
1183
|
+
# This method reads the provided text, opens it in the default editor,
|
1184
|
+
# and allows the user to modify it. If the user makes changes, the
|
1185
|
+
# modified text is returned. If the user exits the editor without
|
1186
|
+
# making changes or the editor is closed abruptly, appropriate messages
|
1187
|
+
# are displayed.
|
1188
|
+
#
|
1189
|
+
# @param [String] initial_text The initial text to be edited.
|
1190
|
+
# @param [String] temp_name The base name for the temporary file (default: 'edit_text').
|
1191
|
+
# @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly.
|
1192
|
+
def edit_text(initial_text, temp_name: 'edit_text')
|
1193
|
+
# Create a temporary file to store the initial text
|
1194
|
+
temp_file = Tempfile.new(temp_name)
|
1195
|
+
temp_file.write(initial_text)
|
1196
|
+
temp_file.rewind
|
1197
|
+
|
1198
|
+
# Capture the modification time of the temporary file before editing
|
1199
|
+
before_mtime = temp_file.mtime
|
1200
|
+
|
1201
|
+
# Open the temporary file in the default editor
|
1202
|
+
system("#{ENV['EDITOR'] || 'vi'} #{temp_file.path}")
|
1203
|
+
|
1204
|
+
# Capture the exit status of the editor
|
1205
|
+
editor_exit_status = $?.exitstatus
|
1206
|
+
|
1207
|
+
# Reopen the file to ensure the updated modification time is read
|
1208
|
+
temp_file.open
|
1209
|
+
after_mtime = temp_file.mtime
|
1210
|
+
|
1211
|
+
# Check if the editor was exited normally or was interrupted
|
1212
|
+
if editor_exit_status != 0
|
1213
|
+
warn 'The editor was closed abruptly. No changes were made.'
|
1214
|
+
temp_file.close
|
1215
|
+
temp_file.unlink
|
1216
|
+
return
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
result_text = nil
|
1220
|
+
# Read the file if it was modified
|
1221
|
+
if before_mtime != after_mtime
|
1222
|
+
temp_file.rewind
|
1223
|
+
result_text = temp_file.read
|
1224
|
+
end
|
1225
|
+
|
1226
|
+
# Remove the temporary file
|
1227
|
+
temp_file.close
|
1228
|
+
temp_file.unlink
|
1229
|
+
|
1230
|
+
result_text
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
|
1234
|
+
lfls = execute_shell_type(
|
1235
|
+
selected: selected,
|
1236
|
+
mdoc: mdoc,
|
1237
|
+
link_state: link_state,
|
1238
|
+
block_source: block_source
|
1239
|
+
)
|
1240
|
+
|
1241
|
+
# if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
|
1242
|
+
[lfls.link_state,
|
1243
|
+
lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
|
1244
|
+
#.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
|
1245
|
+
end
|
1246
|
+
|
923
1247
|
# Executes a block of code that has been approved for execution.
|
924
1248
|
# It sets the script block name, writes command files if required, and handles the execution
|
925
1249
|
# including output formatting and summarization.
|
@@ -977,7 +1301,7 @@ module MarkdownExec
|
|
977
1301
|
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
978
1302
|
next_block_name: '',
|
979
1303
|
next_document_filename: @delegate_object[:filename],
|
980
|
-
next_load_file: LoadFile::
|
1304
|
+
next_load_file: LoadFile::REUSE
|
981
1305
|
)
|
982
1306
|
|
983
1307
|
elsif selected[:shell] == BlockType::VARS
|
@@ -993,7 +1317,7 @@ module MarkdownExec
|
|
993
1317
|
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
994
1318
|
next_block_name: '',
|
995
1319
|
next_document_filename: @delegate_object[:filename],
|
996
|
-
next_load_file: LoadFile::
|
1320
|
+
next_load_file: LoadFile::REUSE
|
997
1321
|
)
|
998
1322
|
|
999
1323
|
elsif debounce_allows
|
@@ -1003,7 +1327,7 @@ module MarkdownExec
|
|
1003
1327
|
block_source: block_source)
|
1004
1328
|
|
1005
1329
|
else
|
1006
|
-
LoadFileLinkState.new(LoadFile::
|
1330
|
+
LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
1007
1331
|
end
|
1008
1332
|
end
|
1009
1333
|
|
@@ -1030,6 +1354,23 @@ module MarkdownExec
|
|
1030
1354
|
color_sym: :script_execution_frame_color)
|
1031
1355
|
end
|
1032
1356
|
|
1357
|
+
# Format expression using environment variables and run state
|
1358
|
+
def format_expression(expr)
|
1359
|
+
data = link_load_format_data
|
1360
|
+
ENV.each { |key, value| data[key] = value }
|
1361
|
+
format(expr, data)
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
# Formats multiline body content as a title string.
|
1365
|
+
# indents all but first line with two spaces so it displays correctly in menu
|
1366
|
+
# @param body_lines [Array<String>] The lines of body content.
|
1367
|
+
# @return [String] Formatted title.
|
1368
|
+
def format_multiline_body_as_title(body_lines)
|
1369
|
+
body_lines.map.with_index do |line, index|
|
1370
|
+
index.zero? ? line : " #{line}"
|
1371
|
+
end.join("\n") + "\n"
|
1372
|
+
end
|
1373
|
+
|
1033
1374
|
# Formats a string based on a given context and applies color styling to it.
|
1034
1375
|
# It retrieves format and color information from the delegate object and processes accordingly.
|
1035
1376
|
#
|
@@ -1046,10 +1387,15 @@ module MarkdownExec
|
|
1046
1387
|
string_send_color(formatted_string, color_sym)
|
1047
1388
|
end
|
1048
1389
|
|
1049
|
-
#
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1390
|
+
# Expand expression if it contains format specifiers
|
1391
|
+
def formatted_expression(expr)
|
1392
|
+
expr.include?('%{') ? format_expression(expr) : expr
|
1393
|
+
end
|
1394
|
+
|
1395
|
+
# Processes a block to generate its summary, modifying its attributes based on various matching criteria.
|
1396
|
+
# It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
|
1397
|
+
#
|
1398
|
+
# @param fcb [Object] An object representing a functional code block.
|
1053
1399
|
# @return [Object] The modified functional code block with updated summary attributes.
|
1054
1400
|
def get_block_summary(fcb)
|
1055
1401
|
return fcb unless @delegate_object[:bash]
|
@@ -1080,16 +1426,6 @@ module MarkdownExec
|
|
1080
1426
|
fcb
|
1081
1427
|
end
|
1082
1428
|
|
1083
|
-
# Formats multiline body content as a title string.
|
1084
|
-
# indents all but first line with two spaces so it displays correctly in menu
|
1085
|
-
# @param body_lines [Array<String>] The lines of body content.
|
1086
|
-
# @return [String] Formatted title.
|
1087
|
-
def format_multiline_body_as_title(body_lines)
|
1088
|
-
body_lines.map.with_index do |line, index|
|
1089
|
-
index.zero? ? line : " #{line}"
|
1090
|
-
end.join("\n") + "\n"
|
1091
|
-
end
|
1092
|
-
|
1093
1429
|
# Updates the delegate object's state based on the provided block state.
|
1094
1430
|
# It sets the block name and determines if the user clicked the back link in the menu.
|
1095
1431
|
#
|
@@ -1142,6 +1478,46 @@ module MarkdownExec
|
|
1142
1478
|
}
|
1143
1479
|
end
|
1144
1480
|
|
1481
|
+
def inpseq_execute_block(block_name)
|
1482
|
+
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1483
|
+
|
1484
|
+
dump_and_warn_block_state(selected: @dml_block_state.block)
|
1485
|
+
@dml_link_state, @dml_menu_default_dname = \
|
1486
|
+
exec_bash_next_state(
|
1487
|
+
selected: @dml_block_state.block,
|
1488
|
+
mdoc: @dml_mdoc,
|
1489
|
+
link_state: @dml_link_state,
|
1490
|
+
block_source: {
|
1491
|
+
document_filename: @delegate_object[:filename],
|
1492
|
+
time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
|
1493
|
+
}
|
1494
|
+
)
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
def inpseq_parse_document(_document_filename)
|
1498
|
+
@run_state.batch_index += 1
|
1499
|
+
@run_state.in_own_window = false
|
1500
|
+
|
1501
|
+
# &bsp 'loop', block_name_from_cli, @cli_block_name
|
1502
|
+
@run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
|
1503
|
+
set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
|
1504
|
+
now_using_cli: @dml_now_using_cli,
|
1505
|
+
link_state: @dml_link_state)
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
def inpseq_user_choice
|
1509
|
+
@dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
|
1510
|
+
menu_blocks: @dml_menu_blocks,
|
1511
|
+
default: @dml_menu_default_dname)
|
1512
|
+
# &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
|
1513
|
+
if !@dml_block_state
|
1514
|
+
HashDelegator.error_handler('block_state missing', { abort: true })
|
1515
|
+
elsif @dml_block_state.state == MenuState::EXIT
|
1516
|
+
# &bsp 'load_cli_or_user_selected_block -> break'
|
1517
|
+
:break
|
1518
|
+
end
|
1519
|
+
end
|
1520
|
+
|
1145
1521
|
# Iterates through blocks in a file, applying the provided block to each line.
|
1146
1522
|
# The iteration only occurs if the file exists.
|
1147
1523
|
# @yield [Symbol] :filter Yields to obtain selected messages for processing.
|
@@ -1168,18 +1544,18 @@ module MarkdownExec
|
|
1168
1544
|
file.write(all_code.join("\n"))
|
1169
1545
|
file.rewind
|
1170
1546
|
|
1171
|
-
if link_block_data.fetch(LinkKeys::
|
1547
|
+
if link_block_data.fetch(LinkKeys::EXEC, false)
|
1172
1548
|
@run_state.files = Hash.new([])
|
1173
1549
|
|
1174
1550
|
Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
|
1175
|
-
handle_stream(stream: stdout, file_type: ExecutionStreams::
|
1551
|
+
handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
|
1176
1552
|
output_lines.push(line)
|
1177
1553
|
end
|
1178
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::
|
1554
|
+
handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
|
1179
1555
|
output_lines.push(line)
|
1180
1556
|
end
|
1181
1557
|
|
1182
|
-
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::
|
1558
|
+
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
|
1183
1559
|
stdin.puts(line)
|
1184
1560
|
end
|
1185
1561
|
|
@@ -1245,102 +1621,6 @@ module MarkdownExec
|
|
1245
1621
|
)
|
1246
1622
|
end
|
1247
1623
|
|
1248
|
-
# format + glob + select for file in load block
|
1249
|
-
# name has references to ENV vars and doc and batch vars incl. timestamp
|
1250
|
-
def load_filespec_from_expression(expression)
|
1251
|
-
# Process expression with embedded formatting
|
1252
|
-
expanded_expression = formatted_expression(expression)
|
1253
|
-
|
1254
|
-
# Handle wildcards or direct file specification
|
1255
|
-
if contains_wildcards?(expanded_expression)
|
1256
|
-
load_filespec_wildcard_expansion(expanded_expression)
|
1257
|
-
else
|
1258
|
-
expanded_expression
|
1259
|
-
end
|
1260
|
-
end
|
1261
|
-
|
1262
|
-
def save_filespec_from_expression(expression)
|
1263
|
-
# Process expression with embedded formatting
|
1264
|
-
formatted = formatted_expression(expression)
|
1265
|
-
|
1266
|
-
# Handle wildcards or direct file specification
|
1267
|
-
if contains_wildcards?(formatted)
|
1268
|
-
save_filespec_wildcard_expansion(formatted)
|
1269
|
-
else
|
1270
|
-
formatted
|
1271
|
-
end
|
1272
|
-
end
|
1273
|
-
|
1274
|
-
# private
|
1275
|
-
|
1276
|
-
# Expand expression if it contains format specifiers
|
1277
|
-
def formatted_expression(expr)
|
1278
|
-
expr.include?('%{') ? format_expression(expr) : expr
|
1279
|
-
end
|
1280
|
-
|
1281
|
-
# Format expression using environment variables and run state
|
1282
|
-
def format_expression(expr)
|
1283
|
-
data = link_load_format_data
|
1284
|
-
ENV.each { |key, value| data[key] = value }
|
1285
|
-
format(expr, data)
|
1286
|
-
end
|
1287
|
-
|
1288
|
-
# Check if the expression contains wildcard characters
|
1289
|
-
def contains_wildcards?(expr)
|
1290
|
-
expr.match(%r{\*|\?|\[})
|
1291
|
-
end
|
1292
|
-
|
1293
|
-
# Handle expression with wildcard characters
|
1294
|
-
def load_filespec_wildcard_expansion(expr)
|
1295
|
-
files = find_files(expr)
|
1296
|
-
case files.count
|
1297
|
-
when 0
|
1298
|
-
HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
|
1299
|
-
when 1
|
1300
|
-
files.first
|
1301
|
-
else
|
1302
|
-
prompt_select_code_filename(files)
|
1303
|
-
end
|
1304
|
-
end
|
1305
|
-
|
1306
|
-
# Handle expression with wildcard characters
|
1307
|
-
# allow user to select or enter
|
1308
|
-
def puts_gets_oprompt_(filespec)
|
1309
|
-
puts format(@delegate_object[:prompt_show_expr_format],
|
1310
|
-
{ expr: filespec })
|
1311
|
-
puts @delegate_object[:prompt_enter_filespec]
|
1312
|
-
gets.chomp
|
1313
|
-
end
|
1314
|
-
|
1315
|
-
# prompt user to enter a path (i.e. containing a path separator)
|
1316
|
-
# or name to substitute into the wildcard expression
|
1317
|
-
def prompt_for_filespec_with_wildcard(filespec)
|
1318
|
-
puts format(@delegate_object[:prompt_show_expr_format],
|
1319
|
-
{ expr: filespec })
|
1320
|
-
puts @delegate_object[:prompt_enter_filespec]
|
1321
|
-
PathUtils.resolve_path_or_substitute(gets.chomp, filespec)
|
1322
|
-
end
|
1323
|
-
|
1324
|
-
# Handle expression with wildcard characters
|
1325
|
-
# allow user to select or enter
|
1326
|
-
def save_filespec_wildcard_expansion(filespec)
|
1327
|
-
files = find_files(filespec)
|
1328
|
-
case files.count
|
1329
|
-
when 0
|
1330
|
-
prompt_for_filespec_with_wildcard(filespec)
|
1331
|
-
else
|
1332
|
-
## user selects from existing files or other
|
1333
|
-
# input into path with wildcard for easy entry
|
1334
|
-
#
|
1335
|
-
name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
|
1336
|
-
if name == @delegate_object[:prompt_filespec_other]
|
1337
|
-
prompt_for_filespec_with_wildcard(filespec)
|
1338
|
-
else
|
1339
|
-
name
|
1340
|
-
end
|
1341
|
-
end
|
1342
|
-
end
|
1343
|
-
|
1344
1624
|
def link_load_format_data
|
1345
1625
|
{
|
1346
1626
|
batch_index: @run_state.batch_index,
|
@@ -1353,29 +1633,6 @@ module MarkdownExec
|
|
1353
1633
|
}
|
1354
1634
|
end
|
1355
1635
|
|
1356
|
-
# # Loads auto link block.
|
1357
|
-
# def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
|
1358
|
-
# block_name = @delegate_object[:document_load_link_block_name]
|
1359
|
-
# return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
|
1360
|
-
|
1361
|
-
# block = HashDelegator.block_find(all_blocks, :oname, block_name)
|
1362
|
-
# return unless block
|
1363
|
-
|
1364
|
-
# if block.fetch(:shell, '') != BlockType::LINK
|
1365
|
-
# HashDelegator.error_handler('must be Link block type', { abort: true })
|
1366
|
-
|
1367
|
-
# else
|
1368
|
-
# # debounce_reset
|
1369
|
-
# push_link_history_and_trigger_load(
|
1370
|
-
# link_block_body: block.fetch(:body, ''),
|
1371
|
-
# mdoc: mdoc,
|
1372
|
-
# selected: block,
|
1373
|
-
# link_state: link_state,
|
1374
|
-
# block_source: block_source
|
1375
|
-
# )
|
1376
|
-
# end
|
1377
|
-
# end
|
1378
|
-
|
1379
1636
|
# Loads auto blocks based on delegate object settings and updates if new filename is detected.
|
1380
1637
|
# Executes a specified block once per filename.
|
1381
1638
|
# @param all_blocks [Array] Array of all block elements.
|
@@ -1395,6 +1652,55 @@ module MarkdownExec
|
|
1395
1652
|
true
|
1396
1653
|
end
|
1397
1654
|
|
1655
|
+
def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
|
1656
|
+
if @delegate_object[:block_name].present?
|
1657
|
+
block = all_blocks.find do |item|
|
1658
|
+
item[:oname] == @delegate_object[:block_name]
|
1659
|
+
end&.merge(block_name_from_ui: false)
|
1660
|
+
else
|
1661
|
+
block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
|
1662
|
+
default)
|
1663
|
+
block = block_state.block&.merge(block_name_from_ui: true)
|
1664
|
+
state = block_state.state
|
1665
|
+
end
|
1666
|
+
|
1667
|
+
SelectedBlockMenuState.new(block, state)
|
1668
|
+
rescue StandardError
|
1669
|
+
HashDelegator.error_handler('load_cli_or_user_selected_block')
|
1670
|
+
end
|
1671
|
+
|
1672
|
+
# format + glob + select for file in load block
|
1673
|
+
# name has references to ENV vars and doc and batch vars incl. timestamp
|
1674
|
+
def load_filespec_from_expression(expression)
|
1675
|
+
# Process expression with embedded formatting
|
1676
|
+
expanded_expression = formatted_expression(expression)
|
1677
|
+
|
1678
|
+
# Handle wildcards or direct file specification
|
1679
|
+
if contains_wildcards?(expanded_expression)
|
1680
|
+
load_filespec_wildcard_expansion(expanded_expression)
|
1681
|
+
else
|
1682
|
+
expanded_expression
|
1683
|
+
end
|
1684
|
+
end
|
1685
|
+
# Handle expression with wildcard characters
|
1686
|
+
def load_filespec_wildcard_expansion(expr, auto_load_single: false)
|
1687
|
+
files = find_files(expr)
|
1688
|
+
if files.count.zero?
|
1689
|
+
HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
|
1690
|
+
elsif auto_load_single && files.count == 1
|
1691
|
+
files.first
|
1692
|
+
else
|
1693
|
+
## user selects from existing files or other
|
1694
|
+
#
|
1695
|
+
case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
|
1696
|
+
when @delegate_object[:prompt_filespec_back]
|
1697
|
+
# do nothing
|
1698
|
+
else
|
1699
|
+
name
|
1700
|
+
end
|
1701
|
+
end
|
1702
|
+
end
|
1703
|
+
|
1398
1704
|
def mdoc_and_blocks_from_nested_files
|
1399
1705
|
menu_blocks = blocks_from_nested_files
|
1400
1706
|
mdoc = MDoc.new(menu_blocks) do |nopts|
|
@@ -1411,15 +1717,41 @@ module MarkdownExec
|
|
1411
1717
|
# recreate menu with new options
|
1412
1718
|
#
|
1413
1719
|
all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
|
1414
|
-
# load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
|
1415
1720
|
|
1416
1721
|
menu_blocks = mdoc.fcbs_per_options(@delegate_object)
|
1417
1722
|
add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
|
1418
1723
|
### compress empty lines
|
1419
|
-
HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
|
1724
|
+
HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
|
1420
1725
|
[all_blocks, menu_blocks, mdoc]
|
1421
1726
|
end
|
1422
1727
|
|
1728
|
+
def menu_add_disabled_option(name)
|
1729
|
+
raise unless name.present?
|
1730
|
+
raise if @dml_menu_blocks.nil?
|
1731
|
+
|
1732
|
+
block = @dml_menu_blocks.find { |item| item[:oname] == name }
|
1733
|
+
|
1734
|
+
# create menu item when it is needed (count > 0)
|
1735
|
+
#
|
1736
|
+
return unless block.nil?
|
1737
|
+
|
1738
|
+
# append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD)
|
1739
|
+
chrome_block = FCB.new(
|
1740
|
+
chrome: true,
|
1741
|
+
disabled: '',
|
1742
|
+
dname: HashDelegator.new(@delegate_object).string_send_color(
|
1743
|
+
name, :menu_inherited_lines_color
|
1744
|
+
),
|
1745
|
+
oname: formatted_name
|
1746
|
+
)
|
1747
|
+
|
1748
|
+
if insert_at_top
|
1749
|
+
@dml_menu_blocks.unshift(chrome_block)
|
1750
|
+
else
|
1751
|
+
@dml_menu_blocks.push(chrome_block)
|
1752
|
+
end
|
1753
|
+
end
|
1754
|
+
|
1423
1755
|
# Formats and optionally colors a menu option based on delegate object's configuration.
|
1424
1756
|
# @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
|
1425
1757
|
# @return [String] The formatted and possibly colored value of the menu option.
|
@@ -1444,6 +1776,47 @@ module MarkdownExec
|
|
1444
1776
|
end
|
1445
1777
|
end
|
1446
1778
|
|
1779
|
+
def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
|
1780
|
+
raise unless name.present?
|
1781
|
+
raise if @dml_menu_blocks.nil?
|
1782
|
+
|
1783
|
+
item = @dml_menu_blocks.find { |block| block[:oname] == name }
|
1784
|
+
|
1785
|
+
# create menu item when it is needed (count > 0)
|
1786
|
+
#
|
1787
|
+
if item.nil? && count.positive?
|
1788
|
+
append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
|
1789
|
+
item = @dml_menu_blocks.find { |block| block[:oname] == name }
|
1790
|
+
end
|
1791
|
+
|
1792
|
+
# update item if it exists
|
1793
|
+
#
|
1794
|
+
return unless item
|
1795
|
+
|
1796
|
+
item[:dname] = "#{name} (#{count} #{type})"
|
1797
|
+
if count.positive?
|
1798
|
+
item.delete(:disabled)
|
1799
|
+
else
|
1800
|
+
item[:disabled] = ''
|
1801
|
+
end
|
1802
|
+
end
|
1803
|
+
|
1804
|
+
def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
|
1805
|
+
if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
|
1806
|
+
# &bsp 'pause cli control, allow user to select block'
|
1807
|
+
block_name_from_cli = false
|
1808
|
+
now_using_cli = false
|
1809
|
+
@menu_base_options[:block_name] = \
|
1810
|
+
@delegate_object[:block_name] = \
|
1811
|
+
link_state.block_name = \
|
1812
|
+
@cli_block_name = nil
|
1813
|
+
end
|
1814
|
+
|
1815
|
+
@delegate_object = @menu_base_options.dup
|
1816
|
+
@menu_user_clicked_back_link = false
|
1817
|
+
[block_name_from_cli, now_using_cli]
|
1818
|
+
end
|
1819
|
+
|
1447
1820
|
# If a method is missing, treat it as a key for the @delegate_object.
|
1448
1821
|
def method_missing(method_name, *args, &block)
|
1449
1822
|
if @delegate_object.respond_to?(method_name)
|
@@ -1525,7 +1898,7 @@ module MarkdownExec
|
|
1525
1898
|
@link_history.push(next_state)
|
1526
1899
|
|
1527
1900
|
next_state.block_name = nil
|
1528
|
-
LoadFileLinkState.new(LoadFile::
|
1901
|
+
LoadFileLinkState.new(LoadFile::LOAD, next_state)
|
1529
1902
|
else
|
1530
1903
|
# no history exists; must have been called independently => retain script
|
1531
1904
|
link_history_push_and_next(
|
@@ -1534,11 +1907,11 @@ module MarkdownExec
|
|
1534
1907
|
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1535
1908
|
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1536
1909
|
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1537
|
-
next_block_name: '', # not link_block_data[LinkKeys::
|
1910
|
+
next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || ''
|
1538
1911
|
next_document_filename: @delegate_object[:filename], # not next_document_filename
|
1539
|
-
next_load_file: LoadFile::
|
1912
|
+
next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
|
1540
1913
|
)
|
1541
|
-
# LoadFileLinkState.new(LoadFile::
|
1914
|
+
# LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
1542
1915
|
end
|
1543
1916
|
end
|
1544
1917
|
|
@@ -1549,7 +1922,7 @@ module MarkdownExec
|
|
1549
1922
|
def pop_link_history_and_trigger_load
|
1550
1923
|
pop = @link_history.pop
|
1551
1924
|
peek = @link_history.peek
|
1552
|
-
LoadFileLinkState.new(LoadFile::
|
1925
|
+
LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
|
1553
1926
|
document_filename: pop.document_filename,
|
1554
1927
|
inherited_block_names: peek.inherited_block_names,
|
1555
1928
|
inherited_dependencies: peek.inherited_dependencies,
|
@@ -1663,6 +2036,24 @@ module MarkdownExec
|
|
1663
2036
|
exit 1
|
1664
2037
|
end
|
1665
2038
|
|
2039
|
+
# Prompts the user to enter a path or name to substitute into the wildcard expression.
|
2040
|
+
# If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
|
2041
|
+
#
|
2042
|
+
# @param filespec [String] the wildcard expression to be substituted
|
2043
|
+
# @return [String, nil] the resolved path or substituted expression, or nil if interrupted
|
2044
|
+
def prompt_for_filespec_with_wildcard(filespec)
|
2045
|
+
puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
|
2046
|
+
puts @delegate_object[:prompt_enter_filespec]
|
2047
|
+
|
2048
|
+
begin
|
2049
|
+
input = gets.chomp
|
2050
|
+
PathUtils.resolve_path_or_substitute(input, filespec)
|
2051
|
+
rescue Interrupt
|
2052
|
+
puts "\nOperation interrupted. Returning nil."
|
2053
|
+
nil
|
2054
|
+
end
|
2055
|
+
end
|
2056
|
+
|
1666
2057
|
##
|
1667
2058
|
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
1668
2059
|
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
@@ -1706,38 +2097,47 @@ module MarkdownExec
|
|
1706
2097
|
exit 1
|
1707
2098
|
end
|
1708
2099
|
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
2100
|
+
# public
|
2101
|
+
|
2102
|
+
def prompt_select_code_filename(filenames)
|
2103
|
+
@prompt.select(
|
2104
|
+
string_send_color(@delegate_object[:prompt_select_code_file],
|
1712
2105
|
:prompt_color_after_script_execution),
|
1713
2106
|
filter: true,
|
1714
2107
|
quiet: true
|
1715
2108
|
) do |menu|
|
1716
|
-
|
1717
|
-
|
2109
|
+
filenames.each do |filename|
|
2110
|
+
menu.choice filename
|
2111
|
+
end
|
1718
2112
|
end
|
1719
|
-
sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1720
2113
|
rescue TTY::Reader::InputInterrupt
|
1721
2114
|
exit 1
|
1722
2115
|
end
|
1723
2116
|
|
1724
|
-
|
1725
|
-
|
1726
|
-
|
1727
|
-
@prompt.select(
|
1728
|
-
string_send_color(@delegate_object[:prompt_select_code_file],
|
2117
|
+
def prompt_select_continue
|
2118
|
+
sel = @prompt.select(
|
2119
|
+
string_send_color(@delegate_object[:prompt_after_script_execution],
|
1729
2120
|
:prompt_color_after_script_execution),
|
1730
2121
|
filter: true,
|
1731
2122
|
quiet: true
|
1732
2123
|
) do |menu|
|
1733
|
-
|
1734
|
-
|
1735
|
-
end
|
2124
|
+
menu.choice @delegate_object[:prompt_yes]
|
2125
|
+
menu.choice @delegate_object[:prompt_exit]
|
1736
2126
|
end
|
2127
|
+
sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1737
2128
|
rescue TTY::Reader::InputInterrupt
|
1738
2129
|
exit 1
|
1739
2130
|
end
|
1740
2131
|
|
2132
|
+
# user prompt to exit if the menu will be displayed again
|
2133
|
+
#
|
2134
|
+
def prompt_user_exit(block_name_from_cli:, selected:)
|
2135
|
+
!block_name_from_cli &&
|
2136
|
+
selected[:shell] == BlockType::BASH &&
|
2137
|
+
@delegate_object[:pause_after_script_execution] &&
|
2138
|
+
prompt_select_continue == MenuState::EXIT
|
2139
|
+
end
|
2140
|
+
|
1741
2141
|
# Handles the processing of a link block in Markdown Execution.
|
1742
2142
|
# It loads YAML data from the link_block_body content, pushes the state to history,
|
1743
2143
|
# sets environment variables, and decides on the next block to load.
|
@@ -1766,361 +2166,161 @@ module MarkdownExec
|
|
1766
2166
|
block_names = []
|
1767
2167
|
code_lines = []
|
1768
2168
|
dependencies = {}
|
1769
|
-
end
|
1770
|
-
|
1771
|
-
# load key and values from link block into current environment
|
1772
|
-
#
|
1773
|
-
if link_block_data[LinkKeys::Vars]
|
1774
|
-
code_lines.push BashCommentFormatter.format_comment(selected[:oname])
|
1775
|
-
(link_block_data[LinkKeys::Vars] || []).each do |(key, value)|
|
1776
|
-
ENV[key] = value.to_s
|
1777
|
-
code_lines.push(assign_key_value_in_bash(key, value))
|
1778
|
-
end
|
1779
|
-
end
|
1780
|
-
|
1781
|
-
## append blocks loaded, apply LinkKeys::Eval
|
1782
|
-
#
|
1783
|
-
if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
|
1784
|
-
load_filespec = load_filespec_from_expression(load_expr)
|
1785
|
-
code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
|
1786
|
-
end
|
1787
|
-
|
1788
|
-
# if an eval link block, evaluate code_lines and return its standard output
|
1789
|
-
#
|
1790
|
-
if link_block_data.fetch(LinkKeys::Eval,
|
1791
|
-
false) || link_block_data.fetch(LinkKeys::Exec, false)
|
1792
|
-
code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
|
1793
|
-
end
|
1794
|
-
|
1795
|
-
next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
|
1796
|
-
|
1797
|
-
if link_block_data[LinkKeys::Return]
|
1798
|
-
pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
|
1799
|
-
dependencies, selected)
|
1800
|
-
|
1801
|
-
else
|
1802
|
-
link_history_push_and_next(
|
1803
|
-
curr_block_name: selected[:oname],
|
1804
|
-
curr_document_filename: @delegate_object[:filename],
|
1805
|
-
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1806
|
-
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1807
|
-
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1808
|
-
next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
|
1809
|
-
nil) || link_block_data[LinkKeys::Block] || '',
|
1810
|
-
next_document_filename: next_document_filename,
|
1811
|
-
next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
|
1812
|
-
)
|
1813
|
-
end
|
1814
|
-
end
|
1815
|
-
|
1816
|
-
def runtime_exception(exception_sym, name, items)
|
1817
|
-
if @delegate_object[exception_sym] != 0
|
1818
|
-
data = { name: name, detail: items.join(', ') }
|
1819
|
-
warn(
|
1820
|
-
format(
|
1821
|
-
@delegate_object.fetch(:exception_format_name, "\n%{name}"),
|
1822
|
-
data
|
1823
|
-
).send(@delegate_object.fetch(:exception_color_name, :red)) +
|
1824
|
-
format(
|
1825
|
-
@delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
|
1826
|
-
data
|
1827
|
-
).send(@delegate_object.fetch(:exception_color_detail, :yellow))
|
1828
|
-
)
|
1829
|
-
end
|
1830
|
-
return unless (@delegate_object[exception_sym]).positive?
|
1831
|
-
|
1832
|
-
exit @delegate_object[exception_sym]
|
1833
|
-
end
|
1834
|
-
|
1835
|
-
def save_to_file(required_lines:, selected:)
|
1836
|
-
write_command_file(required_lines: required_lines, selected: selected)
|
1837
|
-
@fout.fout "File saved: #{@run_state.saved_filespec}"
|
1838
|
-
end
|
1839
|
-
|
1840
|
-
def block_state_for_name_from_cli(block_name)
|
1841
|
-
SelectedBlockMenuState.new(
|
1842
|
-
@dml_blocks_in_file.find do |item|
|
1843
|
-
item[:oname] == block_name
|
1844
|
-
end&.merge(
|
1845
|
-
block_name_from_cli: true,
|
1846
|
-
block_name_from_ui: false
|
1847
|
-
),
|
1848
|
-
MenuState::CONTINUE
|
1849
|
-
)
|
1850
|
-
end
|
1851
|
-
|
1852
|
-
# Select and execute a code block from a Markdown document.
|
1853
|
-
#
|
1854
|
-
# This method allows the user to interactively select a code block from a
|
1855
|
-
# Markdown document, obtain approval, and execute the chosen block of code.
|
1856
|
-
#
|
1857
|
-
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1858
|
-
def document_menu_loop
|
1859
|
-
@menu_base_options = @delegate_object
|
1860
|
-
@dml_link_state = LinkState.new(
|
1861
|
-
block_name: @delegate_object[:block_name],
|
1862
|
-
document_filename: @delegate_object[:filename]
|
1863
|
-
)
|
1864
|
-
@run_state.block_name_from_cli = @dml_link_state.block_name.present?
|
1865
|
-
@cli_block_name = @dml_link_state.block_name
|
1866
|
-
@dml_now_using_cli = @run_state.block_name_from_cli
|
1867
|
-
@dml_menu_default_dname = nil
|
1868
|
-
@dml_block_state = SelectedBlockMenuState.new
|
1869
|
-
|
1870
|
-
@run_state.batch_random = Random.new.rand
|
1871
|
-
@run_state.batch_index = 0
|
1872
|
-
|
1873
|
-
InputSequencer.new(
|
1874
|
-
@delegate_object[:filename],
|
1875
|
-
@delegate_object[:input_cli_rest]
|
1876
|
-
).run do |msg, data|
|
1877
|
-
case msg
|
1878
|
-
when :parse_document # once for each menu
|
1879
|
-
# puts "@ - parse document #{data}"
|
1880
|
-
ii_parse_document(data)
|
1881
|
-
|
1882
|
-
when :display_menu
|
1883
|
-
# warn "@ - display menu:"
|
1884
|
-
# ii_display_menu
|
1885
|
-
@dml_block_state = SelectedBlockMenuState.new
|
1886
|
-
@delegate_object[:block_name] = nil
|
1887
|
-
|
1888
|
-
when :user_choice
|
1889
|
-
if @dml_link_state.block_name.present?
|
1890
|
-
# @prior_block_was_link = true
|
1891
|
-
@dml_block_state.block = @dml_blocks_in_file.find do |item|
|
1892
|
-
item[:oname] == @dml_link_state.block_name
|
1893
|
-
end
|
1894
|
-
@dml_link_state.block_name = nil
|
1895
|
-
else
|
1896
|
-
# puts "? - Select a block to execute (or type #{$texit} to exit):"
|
1897
|
-
break if ii_user_choice == :break # into @dml_block_state
|
1898
|
-
break if @dml_block_state.block.nil? # no block matched
|
1899
|
-
end
|
1900
|
-
# puts "! - Executing block: #{data}"
|
1901
|
-
# @dml_block_state.block[:oname]
|
1902
|
-
@dml_block_state.block&.fetch(:oname, nil)
|
1903
|
-
|
1904
|
-
when :execute_block
|
1905
|
-
block_name = data
|
1906
|
-
if block_name == '* Back' ####
|
1907
|
-
debounce_reset
|
1908
|
-
@menu_user_clicked_back_link = true
|
1909
|
-
load_file_link_state = pop_link_history_and_trigger_load
|
1910
|
-
@dml_link_state = load_file_link_state.link_state
|
1911
|
-
|
1912
|
-
InputSequencer.merge_link_state(
|
1913
|
-
@dml_link_state,
|
1914
|
-
InputSequencer.next_link_state(
|
1915
|
-
block_name: @dml_link_state.block_name,
|
1916
|
-
document_filename: @dml_link_state.document_filename,
|
1917
|
-
prior_block_was_link: true
|
1918
|
-
)
|
1919
|
-
)
|
1920
|
-
|
1921
|
-
else
|
1922
|
-
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1923
|
-
if @dml_block_state.block[:shell] == BlockType::OPTS
|
1924
|
-
debounce_reset
|
1925
|
-
link_state = LinkState.new
|
1926
|
-
options_state = read_show_options_and_trigger_reuse(
|
1927
|
-
selected: @dml_block_state.block,
|
1928
|
-
link_state: link_state
|
1929
|
-
)
|
1930
|
-
|
1931
|
-
@menu_base_options.merge!(options_state.options)
|
1932
|
-
@delegate_object.merge!(options_state.options)
|
1933
|
-
options_state.load_file_link_state.link_state
|
1934
|
-
else
|
1935
|
-
ii_execute_block(block_name)
|
1936
|
-
|
1937
|
-
if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
|
1938
|
-
selected: @dml_block_state.block)
|
1939
|
-
return :break
|
1940
|
-
end
|
1941
|
-
|
1942
|
-
## order of block name processing: link block, cli, from user
|
1943
|
-
#
|
1944
|
-
@dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
|
1945
|
-
HashDelegator.next_link_state(
|
1946
|
-
block_name: @dml_link_state.block_name,
|
1947
|
-
block_name_from_cli: !@dml_link_state.block_name.present?,
|
1948
|
-
block_state: @dml_block_state,
|
1949
|
-
was_using_cli: @dml_now_using_cli
|
1950
|
-
)
|
1951
|
-
|
1952
|
-
if !@dml_block_state.block[:block_name_from_ui] && cli_break
|
1953
|
-
# &bsp '!block_name_from_ui + cli_break -> break'
|
1954
|
-
return :break
|
1955
|
-
end
|
1956
|
-
|
1957
|
-
InputSequencer.next_link_state(
|
1958
|
-
block_name: @dml_link_state.block_name,
|
1959
|
-
prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
|
1960
|
-
)
|
1961
|
-
end
|
1962
|
-
end
|
1963
|
-
|
1964
|
-
when :exit?
|
1965
|
-
data == $texit
|
1966
|
-
when :stay?
|
1967
|
-
data == $stay
|
1968
|
-
else
|
1969
|
-
raise "Invalid message: #{msg}"
|
1970
|
-
end
|
1971
|
-
end
|
1972
|
-
rescue StandardError
|
1973
|
-
HashDelegator.error_handler('document_menu_loop',
|
1974
|
-
{ abort: true })
|
1975
|
-
end
|
1976
|
-
|
1977
|
-
def ii_parse_document(_document_filename)
|
1978
|
-
@run_state.batch_index += 1
|
1979
|
-
@run_state.in_own_window = false
|
1980
|
-
|
1981
|
-
# &bsp 'loop', block_name_from_cli, @cli_block_name
|
1982
|
-
@run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
|
1983
|
-
set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
|
1984
|
-
now_using_cli: @dml_now_using_cli,
|
1985
|
-
link_state: @dml_link_state)
|
1986
|
-
end
|
1987
|
-
|
1988
|
-
def ii_user_choice
|
1989
|
-
@dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
|
1990
|
-
menu_blocks: @dml_menu_blocks,
|
1991
|
-
default: @dml_menu_default_dname)
|
1992
|
-
# &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
|
1993
|
-
if !@dml_block_state
|
1994
|
-
HashDelegator.error_handler('block_state missing', { abort: true })
|
1995
|
-
elsif @dml_block_state.state == MenuState::EXIT
|
1996
|
-
# &bsp 'load_cli_or_user_selected_block -> break'
|
1997
|
-
:break
|
1998
|
-
end
|
1999
|
-
end
|
2000
|
-
|
2001
|
-
def ii_execute_block(block_name)
|
2002
|
-
@dml_block_state = block_state_for_name_from_cli(block_name)
|
2003
|
-
|
2004
|
-
dump_and_warn_block_state(selected: @dml_block_state.block)
|
2005
|
-
@dml_link_state, @dml_menu_default_dname = \
|
2006
|
-
exec_bash_next_state(
|
2007
|
-
selected: @dml_block_state.block,
|
2008
|
-
mdoc: @dml_mdoc,
|
2009
|
-
link_state: @dml_link_state,
|
2010
|
-
block_source: {
|
2011
|
-
document_filename: @delegate_object[:filename],
|
2012
|
-
time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
|
2013
|
-
}
|
2014
|
-
)
|
2015
|
-
end
|
2016
|
-
|
2017
|
-
def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
|
2018
|
-
lfls = execute_shell_type(
|
2019
|
-
selected: selected,
|
2020
|
-
mdoc: mdoc,
|
2021
|
-
link_state: link_state,
|
2022
|
-
block_source: block_source
|
2023
|
-
)
|
2169
|
+
end
|
2024
2170
|
|
2025
|
-
#
|
2026
|
-
|
2027
|
-
|
2028
|
-
|
2171
|
+
# load key and values from link block into current environment
|
2172
|
+
#
|
2173
|
+
if link_block_data[LinkKeys::VARS]
|
2174
|
+
code_lines.push BashCommentFormatter.format_comment(selected[:oname])
|
2175
|
+
(link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
|
2176
|
+
ENV[key] = value.to_s
|
2177
|
+
code_lines.push(assign_key_value_in_bash(key, value))
|
2178
|
+
end
|
2179
|
+
end
|
2029
2180
|
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
2033
|
-
|
2034
|
-
|
2035
|
-
|
2036
|
-
block_name_from_cli: block_name_from_cli)
|
2181
|
+
## append blocks loaded, apply LinkKeys::EVAL
|
2182
|
+
#
|
2183
|
+
if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
|
2184
|
+
load_filespec = load_filespec_from_expression(load_expr)
|
2185
|
+
code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
|
2186
|
+
end
|
2037
2187
|
|
2038
|
-
#
|
2188
|
+
# if an eval link block, evaluate code_lines and return its standard output
|
2039
2189
|
#
|
2040
|
-
|
2041
|
-
|
2190
|
+
if link_block_data.fetch(LinkKeys::EVAL,
|
2191
|
+
false) || link_block_data.fetch(LinkKeys::EXEC, false)
|
2192
|
+
code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
|
2193
|
+
end
|
2042
2194
|
|
2043
|
-
|
2044
|
-
end
|
2195
|
+
next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
|
2045
2196
|
|
2046
|
-
|
2047
|
-
|
2048
|
-
|
2049
|
-
!block_name_from_cli &&
|
2050
|
-
selected[:shell] == BlockType::BASH &&
|
2051
|
-
@delegate_object[:pause_after_script_execution] &&
|
2052
|
-
prompt_select_continue == MenuState::EXIT
|
2053
|
-
end
|
2197
|
+
if link_block_data[LinkKeys::RETURN]
|
2198
|
+
pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
|
2199
|
+
dependencies, selected)
|
2054
2200
|
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2060
|
-
|
2061
|
-
|
2062
|
-
|
2063
|
-
|
2201
|
+
else
|
2202
|
+
link_history_push_and_next(
|
2203
|
+
curr_block_name: selected[:oname],
|
2204
|
+
curr_document_filename: @delegate_object[:filename],
|
2205
|
+
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
2206
|
+
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
2207
|
+
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
2208
|
+
next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK,
|
2209
|
+
nil) || link_block_data[LinkKeys::BLOCK] || '',
|
2210
|
+
next_document_filename: next_document_filename,
|
2211
|
+
next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
|
2212
|
+
)
|
2064
2213
|
end
|
2065
|
-
|
2066
|
-
@delegate_object = @menu_base_options.dup
|
2067
|
-
@menu_user_clicked_back_link = false
|
2068
|
-
[block_name_from_cli, now_using_cli]
|
2069
2214
|
end
|
2070
2215
|
|
2071
|
-
#
|
2072
|
-
#
|
2073
|
-
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
def set_delob_filename_block_name(link_state:, block_name_from_cli:)
|
2079
|
-
@delegate_object[:filename] = link_state.document_filename
|
2080
|
-
link_state.block_name = @delegate_object[:block_name] =
|
2081
|
-
block_name_from_cli ? @cli_block_name : link_state.block_name
|
2216
|
+
# Handle expression with wildcard characters
|
2217
|
+
# allow user to select or enter
|
2218
|
+
def puts_gets_oprompt_(filespec)
|
2219
|
+
puts format(@delegate_object[:prompt_show_expr_format],
|
2220
|
+
{ expr: filespec })
|
2221
|
+
puts @delegate_object[:prompt_enter_filespec]
|
2222
|
+
gets.chomp
|
2082
2223
|
end
|
2083
2224
|
|
2084
|
-
#
|
2085
|
-
#
|
2086
|
-
# @param
|
2087
|
-
# @
|
2088
|
-
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2225
|
+
# Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
|
2226
|
+
# @param selected [Hash] Selected item from the menu containing a YAML body.
|
2227
|
+
# @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
|
2228
|
+
# @return [LoadFileLinkState] An instance indicating the next action for loading files.
|
2229
|
+
def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
|
2230
|
+
obj = {}
|
2231
|
+
data = YAML.load(selected[:body].join("\n"))
|
2232
|
+
(data || []).each do |key, value|
|
2233
|
+
sym_key = key.to_sym
|
2234
|
+
obj[sym_key] = value
|
2092
2235
|
|
2093
|
-
|
2094
|
-
warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
|
2095
|
-
label: 'blocks_in_file')
|
2236
|
+
print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
|
2096
2237
|
end
|
2097
2238
|
|
2098
|
-
|
2099
|
-
|
2100
|
-
|
2239
|
+
link_state.block_name = nil
|
2240
|
+
OpenStruct.new(options: obj,
|
2241
|
+
load_file_link_state: LoadFileLinkState.new(
|
2242
|
+
LoadFile::REUSE, link_state
|
2243
|
+
))
|
2244
|
+
end
|
2245
|
+
|
2246
|
+
# Check if the delegate object responds to a given method.
|
2247
|
+
# @param method_name [Symbol] The name of the method to check.
|
2248
|
+
# @param include_private [Boolean] Whether to include private methods in the check.
|
2249
|
+
# @return [Boolean] true if the delegate object responds to the method, false otherwise.
|
2250
|
+
def respond_to?(method_name, include_private = false)
|
2251
|
+
if super
|
2252
|
+
true
|
2253
|
+
elsif @delegate_object.respond_to?(method_name, include_private)
|
2254
|
+
true
|
2255
|
+
elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
|
2256
|
+
true
|
2257
|
+
else
|
2258
|
+
@delegate_object.respond_to?(method_name, include_private)
|
2101
2259
|
end
|
2260
|
+
end
|
2102
2261
|
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2262
|
+
def runtime_exception(exception_sym, name, items)
|
2263
|
+
if @delegate_object[exception_sym] != 0
|
2264
|
+
data = { name: name, detail: items.join(', ') }
|
2265
|
+
warn(
|
2266
|
+
format(
|
2267
|
+
@delegate_object.fetch(:exception_format_name, "\n%{name}"),
|
2268
|
+
data
|
2269
|
+
).send(@delegate_object.fetch(:exception_color_name, :red)) +
|
2270
|
+
format(
|
2271
|
+
@delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
|
2272
|
+
data
|
2273
|
+
).send(@delegate_object.fetch(:exception_color_detail, :yellow))
|
2274
|
+
)
|
2275
|
+
end
|
2276
|
+
return unless (@delegate_object[exception_sym]).positive?
|
2106
2277
|
|
2107
|
-
|
2278
|
+
exit @delegate_object[exception_sym]
|
2108
2279
|
end
|
2109
2280
|
|
2110
|
-
|
2111
|
-
|
2112
|
-
|
2113
|
-
|
2281
|
+
# allow user to select or enter
|
2282
|
+
def save_filespec_from_expression(expression)
|
2283
|
+
# Process expression with embedded formatting
|
2284
|
+
formatted = formatted_expression(expression)
|
2285
|
+
|
2286
|
+
# Handle wildcards or direct file specification
|
2287
|
+
if contains_wildcards?(formatted)
|
2288
|
+
save_filespec_wildcard_expansion(formatted)
|
2289
|
+
else
|
2290
|
+
formatted
|
2114
2291
|
end
|
2292
|
+
end
|
2115
2293
|
|
2116
|
-
|
2294
|
+
# Handle expression with wildcard characters
|
2295
|
+
# allow user to select or enter
|
2296
|
+
def save_filespec_wildcard_expansion(filespec)
|
2297
|
+
files = find_files(filespec)
|
2298
|
+
case files.count
|
2299
|
+
when 0
|
2300
|
+
prompt_for_filespec_with_wildcard(filespec)
|
2301
|
+
else
|
2302
|
+
## user selects from existing files or other
|
2303
|
+
# input into path with wildcard for easy entry
|
2304
|
+
#
|
2305
|
+
name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
|
2306
|
+
case name
|
2307
|
+
when @delegate_object[:prompt_filespec_back]
|
2308
|
+
# do nothing
|
2309
|
+
when @delegate_object[:prompt_filespec_other]
|
2310
|
+
prompt_for_filespec_with_wildcard(filespec)
|
2311
|
+
else
|
2312
|
+
name
|
2313
|
+
end
|
2314
|
+
end
|
2315
|
+
end
|
2117
2316
|
|
2118
|
-
|
2317
|
+
def save_to_file(required_lines:, selected:)
|
2318
|
+
write_command_file(required_lines: required_lines, selected: selected)
|
2319
|
+
@fout.fout "File saved: #{@run_state.saved_filespec}"
|
2119
2320
|
end
|
2120
2321
|
|
2121
2322
|
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
2122
2323
|
def select_option_with_metadata(prompt_text, names, opts = {})
|
2123
|
-
|
2124
2324
|
## configure to environment
|
2125
2325
|
#
|
2126
2326
|
unless opts[:select_page_height].positive?
|
@@ -2128,23 +2328,24 @@ module MarkdownExec
|
|
2128
2328
|
opts[:per_page] = opts[:select_page_height] = [IO.console.winsize[0] - 3, 4].max
|
2129
2329
|
end
|
2130
2330
|
|
2331
|
+
# crashes if all menu options are disabled
|
2131
2332
|
selection = @prompt.select(prompt_text,
|
2132
2333
|
names,
|
2133
2334
|
opts.merge(filter: true))
|
2134
|
-
|
2135
|
-
if item.instance_of?(
|
2136
|
-
item == selection
|
2137
|
-
else
|
2335
|
+
selected_name = names.find do |item|
|
2336
|
+
if item.instance_of?(Hash)
|
2138
2337
|
item[:dname] == selection
|
2338
|
+
else
|
2339
|
+
item == selection
|
2139
2340
|
end
|
2140
2341
|
end
|
2141
|
-
|
2142
|
-
unless
|
2342
|
+
selected_name = { dname: selected_name } if selected_name.instance_of?(String)
|
2343
|
+
unless selected_name
|
2143
2344
|
HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
|
2144
2345
|
exit 1
|
2145
2346
|
end
|
2146
2347
|
|
2147
|
-
|
2348
|
+
selected_name.merge(
|
2148
2349
|
if selection == menu_chrome_colored_option(:menu_option_back_name)
|
2149
2350
|
{ option: selection, shell: BlockType::LINK }
|
2150
2351
|
elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
|
@@ -2159,6 +2360,35 @@ module MarkdownExec
|
|
2159
2360
|
HashDelegator.error_handler('select_option_with_metadata')
|
2160
2361
|
end
|
2161
2362
|
|
2363
|
+
# Update the block name in the link state and delegate object.
|
2364
|
+
#
|
2365
|
+
# This method updates the block name based on whether it was specified
|
2366
|
+
# through the CLI or derived from the link state.
|
2367
|
+
#
|
2368
|
+
# @param link_state [LinkState] The current link state object.
|
2369
|
+
# @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
|
2370
|
+
def set_delob_filename_block_name(link_state:, block_name_from_cli:)
|
2371
|
+
@delegate_object[:filename] = link_state.document_filename
|
2372
|
+
link_state.block_name = @delegate_object[:block_name] =
|
2373
|
+
block_name_from_cli ? @cli_block_name : link_state.block_name
|
2374
|
+
end
|
2375
|
+
|
2376
|
+
def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
|
2377
|
+
block_name_from_cli, now_using_cli = \
|
2378
|
+
manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
|
2379
|
+
now_using_cli: now_using_cli,
|
2380
|
+
link_state: link_state)
|
2381
|
+
set_delob_filename_block_name(link_state: link_state,
|
2382
|
+
block_name_from_cli: block_name_from_cli)
|
2383
|
+
|
2384
|
+
# update @delegate_object and @menu_base_options in auto_load
|
2385
|
+
#
|
2386
|
+
blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
|
2387
|
+
dump_delobj(blocks_in_file, menu_blocks, link_state)
|
2388
|
+
|
2389
|
+
[block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
|
2390
|
+
end
|
2391
|
+
|
2162
2392
|
def set_environment_variables_for_block(selected)
|
2163
2393
|
code_lines = []
|
2164
2394
|
YAML.load(selected[:body].join("\n"))&.each do |key, value|
|
@@ -2294,27 +2524,6 @@ module MarkdownExec
|
|
2294
2524
|
end
|
2295
2525
|
end
|
2296
2526
|
|
2297
|
-
# Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
|
2298
|
-
# @param selected [Hash] Selected item from the menu containing a YAML body.
|
2299
|
-
# @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
|
2300
|
-
# @return [LoadFileLinkState] An instance indicating the next action for loading files.
|
2301
|
-
def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
|
2302
|
-
obj = {}
|
2303
|
-
data = YAML.load(selected[:body].join("\n"))
|
2304
|
-
(data || []).each do |key, value|
|
2305
|
-
sym_key = key.to_sym
|
2306
|
-
obj[sym_key] = value
|
2307
|
-
|
2308
|
-
print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
|
2309
|
-
end
|
2310
|
-
|
2311
|
-
link_state.block_name = nil
|
2312
|
-
OpenStruct.new(options: obj,
|
2313
|
-
load_file_link_state: LoadFileLinkState.new(
|
2314
|
-
LoadFile::Reuse, link_state
|
2315
|
-
))
|
2316
|
-
end
|
2317
|
-
|
2318
2527
|
def wait_for_stream_processing
|
2319
2528
|
@process_mutex.synchronize do
|
2320
2529
|
@process_cv.wait(@process_mutex)
|
@@ -2389,14 +2598,34 @@ module MarkdownExec
|
|
2389
2598
|
HashDelegator.error_handler('write_command_file')
|
2390
2599
|
end
|
2391
2600
|
|
2601
|
+
# Ensure the directory exists before writing the file
|
2602
|
+
def write_file_with_directory_creation(save_filespec, content)
|
2603
|
+
directory = File.dirname(save_filespec)
|
2604
|
+
|
2605
|
+
begin
|
2606
|
+
FileUtils.mkdir_p(directory)
|
2607
|
+
File.write(save_filespec, content)
|
2608
|
+
rescue Errno::EACCES
|
2609
|
+
warn "Permission denied: Unable to write to file '#{save_filespec}'"
|
2610
|
+
nil
|
2611
|
+
rescue Errno::EROFS
|
2612
|
+
warn "Read-only file system: Unable to write to file '#{save_filespec}'"
|
2613
|
+
nil
|
2614
|
+
rescue StandardError => err
|
2615
|
+
warn "An error occurred while writing to file '#{save_filespec}': #{err.message}"
|
2616
|
+
nil
|
2617
|
+
end
|
2618
|
+
end
|
2619
|
+
|
2620
|
+
# return next document file name
|
2392
2621
|
def write_inherited_lines_to_file(link_state, link_block_data)
|
2393
|
-
save_expr = link_block_data.fetch(LinkKeys::
|
2622
|
+
save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
|
2394
2623
|
if save_expr.present?
|
2395
2624
|
save_filespec = save_filespec_from_expression(save_expr)
|
2396
2625
|
File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
|
2397
2626
|
@delegate_object[:filename]
|
2398
2627
|
else
|
2399
|
-
link_block_data[LinkKeys::
|
2628
|
+
link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
|
2400
2629
|
end
|
2401
2630
|
end
|
2402
2631
|
end
|
@@ -2432,21 +2661,6 @@ module MarkdownExec
|
|
2432
2661
|
|
2433
2662
|
def self.next_link_state(*args, **kwargs, &block)
|
2434
2663
|
super
|
2435
|
-
# result = super
|
2436
|
-
|
2437
|
-
# @logger ||= StdOutErrLogger.new
|
2438
|
-
# @logger.unknown(
|
2439
|
-
# HashDelegator.clean_hash_recursively(
|
2440
|
-
# { "HashDelegator.next_link_state":
|
2441
|
-
# { 'args': args,
|
2442
|
-
# 'at': Time.now.strftime('%FT%TZ'),
|
2443
|
-
# 'for': /[^\/]+:\d+/.match(caller.first)[0],
|
2444
|
-
# 'kwargs': kwargs,
|
2445
|
-
# 'return': result } }
|
2446
|
-
# )
|
2447
|
-
# )
|
2448
|
-
|
2449
|
-
# result
|
2450
2664
|
end
|
2451
2665
|
end
|
2452
2666
|
end
|
@@ -2537,21 +2751,21 @@ module MarkdownExec
|
|
2537
2751
|
|
2538
2752
|
# Test case for empty body
|
2539
2753
|
def test_push_link_history_and_trigger_load_with_empty_body
|
2540
|
-
assert_equal LoadFile::
|
2754
|
+
assert_equal LoadFile::REUSE,
|
2541
2755
|
@hd.push_link_history_and_trigger_load.load_file
|
2542
2756
|
end
|
2543
2757
|
|
2544
2758
|
# Test case for non-empty body without 'file' key
|
2545
2759
|
def test_push_link_history_and_trigger_load_without_file_key
|
2546
2760
|
body = ["vars:\n KEY: VALUE"]
|
2547
|
-
assert_equal LoadFile::
|
2761
|
+
assert_equal LoadFile::REUSE,
|
2548
2762
|
@hd.push_link_history_and_trigger_load(link_block_body: body).load_file
|
2549
2763
|
end
|
2550
2764
|
|
2551
2765
|
# Test case for non-empty body with 'file' key
|
2552
2766
|
def test_push_link_history_and_trigger_load_with_file_key
|
2553
2767
|
body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
|
2554
|
-
expected_result = LoadFileLinkState.new(LoadFile::
|
2768
|
+
expected_result = LoadFileLinkState.new(LoadFile::LOAD,
|
2555
2769
|
LinkState.new(block_name: 'sample_block',
|
2556
2770
|
document_filename: 'sample_file',
|
2557
2771
|
inherited_dependencies: {},
|
@@ -2589,8 +2803,9 @@ module MarkdownExec
|
|
2589
2803
|
end
|
2590
2804
|
|
2591
2805
|
def test_safeval_rescue_from_error
|
2592
|
-
|
2593
|
-
|
2806
|
+
assert_raises(SystemExit) do
|
2807
|
+
HashDelegator.safeval('invalid_code_raises_exception')
|
2808
|
+
end
|
2594
2809
|
end
|
2595
2810
|
|
2596
2811
|
def test_set_fcb_title
|
@@ -2996,7 +3211,7 @@ module MarkdownExec
|
|
2996
3211
|
|
2997
3212
|
# Asserting the result is an instance of LoadFileLinkState
|
2998
3213
|
assert_instance_of LoadFileLinkState, result
|
2999
|
-
assert_equal LoadFile::
|
3214
|
+
assert_equal LoadFile::LOAD, result.load_file
|
3000
3215
|
assert_nil result.link_state.block_name
|
3001
3216
|
end
|
3002
3217
|
end
|
@@ -3383,4 +3598,60 @@ module MarkdownExec
|
|
3383
3598
|
assert_equal expected, BashCommentFormatter.format_comment(input)
|
3384
3599
|
end
|
3385
3600
|
end
|
3601
|
+
|
3602
|
+
class PromptForFilespecWithWildcardTest < Minitest::Test
|
3603
|
+
def setup
|
3604
|
+
@delegate_object = {
|
3605
|
+
prompt_show_expr_format: 'Current expression: %{expr}',
|
3606
|
+
prompt_enter_filespec: 'Please enter a filespec:'
|
3607
|
+
}
|
3608
|
+
@original_stdin = $stdin
|
3609
|
+
end
|
3610
|
+
|
3611
|
+
def teardown
|
3612
|
+
$stdin = @original_stdin
|
3613
|
+
end
|
3614
|
+
|
3615
|
+
def test_prompt_for_filespec_with_normal_input
|
3616
|
+
$stdin = StringIO.new("test_input\n")
|
3617
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3618
|
+
assert_equal 'resolved_path_or_substituted_value', result
|
3619
|
+
end
|
3620
|
+
|
3621
|
+
def test_prompt_for_filespec_with_interruption
|
3622
|
+
$stdin = StringIO.new
|
3623
|
+
# rubocop disable:Lint/NestedMethodDefinition
|
3624
|
+
def $stdin.gets; raise Interrupt; end
|
3625
|
+
# rubocop enable:Lint/NestedMethodDefinition
|
3626
|
+
|
3627
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3628
|
+
assert_nil result
|
3629
|
+
end
|
3630
|
+
|
3631
|
+
def test_prompt_for_filespec_with_empty_input
|
3632
|
+
$stdin = StringIO.new("\n")
|
3633
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3634
|
+
assert_equal 'resolved_path_or_substituted_value', result
|
3635
|
+
end
|
3636
|
+
|
3637
|
+
private
|
3638
|
+
|
3639
|
+
def prompt_for_filespec_with_wildcard(filespec)
|
3640
|
+
puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
|
3641
|
+
puts @delegate_object[:prompt_enter_filespec]
|
3642
|
+
|
3643
|
+
begin
|
3644
|
+
input = gets.chomp
|
3645
|
+
PathUtils.resolve_path_or_substitute(input, filespec)
|
3646
|
+
rescue Interrupt
|
3647
|
+
nil
|
3648
|
+
end
|
3649
|
+
end
|
3650
|
+
|
3651
|
+
module PathUtils
|
3652
|
+
def self.resolve_path_or_substitute(input, filespec)
|
3653
|
+
'resolved_path_or_substituted_value' # Placeholder implementation
|
3654
|
+
end
|
3655
|
+
end
|
3656
|
+
end
|
3386
3657
|
end # module MarkdownExec
|