markdown_exec 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -2
  3. data/CHANGELOG.md +19 -0
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +32 -8
  6. data/bats/bats.bats +33 -0
  7. data/bats/block-types.bats +56 -0
  8. data/bats/cli.bats +74 -0
  9. data/bats/fail.bats +11 -0
  10. data/bats/history.bats +34 -0
  11. data/bats/markup.bats +66 -0
  12. data/bats/mde.bats +29 -0
  13. data/bats/options.bats +92 -0
  14. data/bats/test_helper.bash +152 -0
  15. data/bin/tab_completion.sh +44 -20
  16. data/docs/dev/block-type-opts.md +10 -0
  17. data/docs/dev/block-type-port.md +24 -0
  18. data/docs/dev/block-type-vars.md +7 -0
  19. data/docs/dev/pass-through-arguments.md +8 -0
  20. data/docs/dev/specs-import.md +9 -0
  21. data/docs/dev/specs.md +83 -0
  22. data/docs/dev/text-decoration.md +7 -0
  23. data/examples/bash-blocks.md +4 -4
  24. data/examples/block-names.md +2 -2
  25. data/examples/import0.md +23 -0
  26. data/examples/import1.md +13 -0
  27. data/examples/link-blocks-vars.md +3 -3
  28. data/examples/opts-blocks-require.md +6 -6
  29. data/examples/table-markup.md +31 -0
  30. data/examples/text-markup.md +58 -0
  31. data/examples/vars-blocks.md +2 -2
  32. data/examples/wrap.md +87 -9
  33. data/lib/ansi_formatter.rb +12 -6
  34. data/lib/ansi_string.rb +153 -0
  35. data/lib/argument_processor.rb +160 -0
  36. data/lib/cached_nested_file_reader.rb +4 -2
  37. data/lib/ce_get_cost_and_usage.rb +4 -3
  38. data/lib/cli.rb +1 -1
  39. data/lib/colorize.rb +39 -11
  40. data/lib/constants.rb +17 -0
  41. data/lib/directory_searcher.rb +4 -2
  42. data/lib/doh.rb +190 -0
  43. data/lib/env.rb +1 -1
  44. data/lib/exceptions.rb +9 -6
  45. data/lib/fcb.rb +0 -199
  46. data/lib/filter.rb +18 -5
  47. data/lib/find_files.rb +8 -3
  48. data/lib/format_table.rb +406 -0
  49. data/lib/hash_delegator.rb +888 -603
  50. data/lib/hierarchy_string.rb +113 -25
  51. data/lib/input_sequencer.rb +16 -10
  52. data/lib/instance_method_wrapper.rb +2 -1
  53. data/lib/layered_hash.rb +143 -0
  54. data/lib/link_history.rb +22 -8
  55. data/lib/markdown_exec/version.rb +1 -1
  56. data/lib/markdown_exec.rb +413 -165
  57. data/lib/mdoc.rb +27 -34
  58. data/lib/menu.src.yml +825 -710
  59. data/lib/menu.yml +799 -703
  60. data/lib/namer.rb +6 -12
  61. data/lib/object_present.rb +1 -1
  62. data/lib/option_value.rb +7 -3
  63. data/lib/poly.rb +33 -14
  64. data/lib/resize_terminal.rb +60 -52
  65. data/lib/saved_assets.rb +45 -34
  66. data/lib/saved_files_matcher.rb +6 -3
  67. data/lib/streams_out.rb +7 -1
  68. data/lib/table_extractor.rb +166 -0
  69. data/lib/tap.rb +5 -6
  70. data/lib/text_analyzer.rb +144 -8
  71. metadata +26 -3
  72. data/lib/std_out_err_logger.rb +0 -119
@@ -17,6 +17,7 @@ require 'tmpdir'
17
17
  require 'tty-prompt'
18
18
  require 'yaml'
19
19
 
20
+ require_relative 'ansi_string'
20
21
  require_relative 'array'
21
22
  require_relative 'array_util'
22
23
  require_relative 'block_label'
@@ -27,6 +28,7 @@ require_relative 'directory_searcher'
27
28
  require_relative 'exceptions'
28
29
  require_relative 'fcb'
29
30
  require_relative 'filter'
31
+ require_relative 'format_table'
30
32
  require_relative 'fout'
31
33
  require_relative 'hash'
32
34
  require_relative 'hierarchy_string'
@@ -35,11 +37,13 @@ require_relative 'mdoc'
35
37
  require_relative 'namer'
36
38
  require_relative 'regexp'
37
39
  require_relative 'resize_terminal'
38
- require_relative 'std_out_err_logger'
39
40
  require_relative 'streams_out'
40
41
  require_relative 'string_util'
42
+ require_relative 'table_extractor'
41
43
  require_relative 'text_analyzer'
42
44
 
45
+ require_relative 'argument_processor'
46
+
43
47
  $pd = false unless defined?($pd)
44
48
 
45
49
  class String
@@ -52,17 +56,20 @@ end
52
56
 
53
57
  module HashDelegatorSelf
54
58
  # Applies an ANSI color method to a string using a specified color key.
55
- # The method retrieves the color method from the provided hash. If the color key
56
- # is not present in the hash, it uses a default color method.
59
+ # The method retrieves the color method from the provided hash. If the
60
+ # color key is not present in the hash, it uses a default color method.
57
61
  # @param string [String] The string to be colored.
58
- # @param color_methods [Hash] A hash where keys are color names (String/Symbol) and values are color methods.
59
- # @param color_key [String, Symbol] The key representing the desired color method in the color_methods hash.
60
- # @param default_method [String] (optional) Default color method to use if color_key is not found in color_methods. Defaults to 'plain'.
62
+ # @param color_methods [Hash] A hash where keys are color names
63
+ # (String/Symbol) and values are color methods.
64
+ # @param color_key [String, Symbol] The key representing the desired
65
+ # color method in the color_methods hash.
66
+ # @param default_method [String] (optional) Default color method to
67
+ # use if color_key is not found in color_methods. Defaults to 'plain'.
61
68
  # @return [String] The colored string.
62
69
  def apply_color_from_hash(string, color_methods, color_key,
63
70
  default_method: 'plain')
64
71
  color_method = color_methods.fetch(color_key, default_method).to_sym
65
- string.to_s.send(color_method)
72
+ AnsiString.new(string.to_s).send(color_method)
66
73
  end
67
74
 
68
75
  # # Enhanced `apply_color_from_hash` method to support dynamic color transformations
@@ -84,15 +91,21 @@ module HashDelegatorSelf
84
91
  # colored_string = apply_color_from_hash(string, color_transformations, :red)
85
92
  # puts colored_string # This will print the string in red
86
93
 
87
- # Searches for the first element in a collection where the specified message sent to an element matches a given value.
88
- # This method is particularly useful for finding a specific hash-like object within an enumerable collection.
94
+ # Searches for the first element in a collection where the specified
95
+ # message sent to an element matches a given value.
96
+ # This method is particularly useful for finding a specific hash-like
97
+ # object within an enumerable collection.
89
98
  # If no match is found, it returns a specified default value.
90
99
  #
91
100
  # @param blocks [Enumerable] The collection of hash-like objects to search.
92
- # @param msg [Symbol, String] The message to send to each element of the collection.
93
- # @param value [Object] The value to match against the result of the message sent to each element.
94
- # @param default [Object, nil] The default value to return if no match is found (optional).
95
- # @return [Object, nil] The first matching element or the default value if no match is found.
101
+ # @param msg [Symbol, String] The message to send to each element of
102
+ # the collection.
103
+ # @param value [Object] The value to match against the result of the message
104
+ # sent to each element.
105
+ # @param default [Object, nil] The default value to return if no match is
106
+ # found (optional).
107
+ # @return [Object, nil] The first matching element or the default value if
108
+ # no match is found.
96
109
  def block_find(blocks, msg, value, default = nil)
97
110
  blocks.find { |item| item.send(msg) == value } || default
98
111
  end
@@ -110,11 +123,13 @@ module HashDelegatorSelf
110
123
  end
111
124
 
112
125
  # Creates a file at the specified path, writes the given content to it,
113
- # and sets file permissions if required. Handles any errors encountered during the process.
126
+ # and sets file permissions if required. Handles any errors encountered
127
+ # during the process.
114
128
  #
115
129
  # @param file_path [String] The path where the file will be created.
116
130
  # @param content [String] The content to write into the file.
117
- # @param chmod_value [Integer] The file permission value to set; skips if zero.
131
+ # @param chmod_value [Integer] The file permission value to set;
132
+ # skips if zero.
118
133
  def create_file_and_write_string_with_permissions(file_path, content,
119
134
  chmod_value)
120
135
  create_directory_for_file(file_path)
@@ -128,7 +143,8 @@ module HashDelegatorSelf
128
143
  # Dir::Tmpname.create(self.class.to_s) { |path| path }
129
144
  # end
130
145
 
131
- # Updates the title of an FCB object from its body content if the title is nil or empty.
146
+ # Updates the title of an FCB object from its body content if the title
147
+ # is nil or empty.
132
148
  def default_block_title_from_body(fcb)
133
149
  return unless fcb.title.nil? || fcb.title.empty?
134
150
 
@@ -174,7 +190,8 @@ module HashDelegatorSelf
174
190
 
175
191
  # Indents all lines in a given string with a specified indentation string.
176
192
  # @param body [String] A multi-line string to be indented.
177
- # @param indent [String] The string used for indentation (default is an empty string).
193
+ # @param indent [String] The string used for indentation
194
+ # (default is an empty string).
178
195
  # @return [String] A single string with each line indented as specified.
179
196
  def indent_all_lines(body, indent = nil)
180
197
  return body unless indent&.non_empty?
@@ -271,13 +288,51 @@ module HashDelegatorSelf
271
288
  File.chmod(chmod_value, file_path)
272
289
  end
273
290
 
291
+ # find tables in multiple lines and format horizontally
292
+ def tables_into_columns!(blocks_menu, delegate_object)
293
+ return unless delegate_object[:tables_into_columns]
294
+
295
+ lines = blocks_menu.map(&:oname)
296
+ text_tables = TableExtractor.extract_tables(lines)
297
+ return unless text_tables.count.positive?
298
+
299
+ text_tables.each do |match|
300
+ range = match[:start_index]..(match[:start_index] + match[:rows] - 1)
301
+ lines = blocks_menu[range].map(&:oname)
302
+ formatted = MarkdownTableFormatter.format_table(
303
+ lines,
304
+ match[:columns],
305
+ decorate: {
306
+ border: delegate_object[:table_border_color],
307
+ header_row: delegate_object[:table_header_row_color],
308
+ row: delegate_object[:table_row_color],
309
+ separator_line: delegate_object[:table_separator_line_color]
310
+ }
311
+ )
312
+
313
+ if formatted.count == range.size
314
+ # read indentation from first line
315
+ indent = blocks_menu[range.first].oname.split('|', 2).first
316
+
317
+ # replace text in each block
318
+ range.each.with_index do |block_ind, ind|
319
+ ### format oname to dname
320
+ blocks_menu[block_ind].dname = indent + formatted[ind]
321
+ end
322
+ else
323
+ warn [__LINE__, range, lines, formatted].inspect
324
+ raise 'Invalid result from MarkdownTableFormatter.format_table()'
325
+ end
326
+ end
327
+ end
328
+
274
329
  # Creates a TTY prompt with custom settings. Specifically, it disables the default 'cross' symbol and
275
330
  # defines a lambda function to handle interrupts.
276
331
  # @return [TTY::Prompt] A new TTY::Prompt instance with specified configurations.
277
332
  def tty_prompt_without_disabled_symbol
278
333
  TTY::Prompt.new(
279
334
  interrupt: lambda {
280
- puts
335
+ puts # next line in case not at start
281
336
  raise TTY::Reader::InputInterrupt
282
337
  },
283
338
  symbols: { cross: ' ' }
@@ -289,7 +344,7 @@ module HashDelegatorSelf
289
344
  # If the fcb has a body and meets certain conditions, it yields to the given block.
290
345
  #
291
346
  # @param fcb [Object] The fcb object whose attributes are to be updated.
292
- # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable.
347
+ # @param selected_types [Array<Symbol>] A list of message types to determine if yielding is applicable.
293
348
  # @param block [Block] An optional block to yield to if conditions are met.
294
349
  def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {},
295
350
  &block)
@@ -303,10 +358,10 @@ module HashDelegatorSelf
303
358
 
304
359
  # Yields a line as a new block if the selected message type includes :line.
305
360
  # @param [String] line The line to be processed.
306
- # @param [Array<Symbol>] selected_messages A list of message types to check.
361
+ # @param [Array<Symbol>] selected_types A list of message types to check.
307
362
  # @param [Proc] block The block to be called with the line data.
308
- def yield_line_if_selected(line, selected_messages, &block)
309
- return unless block && selected_messages.include?(:line)
363
+ def yield_line_if_selected(line, selected_types, &block)
364
+ return unless block && block_type_selected?(selected_types, :line)
310
365
 
311
366
  block.call(:line, MarkdownExec::FCB.new(body: [line]))
312
367
  end
@@ -480,7 +535,8 @@ module MarkdownExec
480
535
  end
481
536
 
482
537
  class HashDelegatorParent
483
- attr_accessor :most_recent_loaded_filename, :pass_args, :run_state
538
+ attr_accessor :most_recent_loaded_filename, :pass_args, :run_state,
539
+ :p_all_arguments, :p_options_parsed, :p_params, :p_rest
484
540
 
485
541
  extend HashDelegatorSelf
486
542
  include CompactionHelpers
@@ -501,6 +557,11 @@ module MarkdownExec
501
557
 
502
558
  @process_mutex = Mutex.new
503
559
  @process_cv = ConditionVariable.new
560
+
561
+ @p_all_arguments = []
562
+ @p_options_parsed = []
563
+ @p_params = {}
564
+ @p_rest = []
504
565
  end
505
566
 
506
567
  # private
@@ -513,6 +574,26 @@ module MarkdownExec
513
574
  # @delegate_object[key] = value
514
575
  # end
515
576
 
577
+ ##
578
+ # Returns the absolute path of the given file path.
579
+ # If the provided path is already absolute, it returns it as is.
580
+ # Otherwise, it prefixes the path with the current working directory.
581
+ #
582
+ # @param file_path [String] The file path to process
583
+ # @return [String] The absolute path
584
+ #
585
+ # Example usage:
586
+ # absolute_path('/absolute/path/to/file.txt') # => '/absolute/path/to/file.txt'
587
+ # absolute_path('relative/path/to/file.txt') # => '/current/working/directory/relative/path/to/file.txt'
588
+ #
589
+ def absolute_path(file_path)
590
+ if File.absolute_path?(file_path)
591
+ file_path
592
+ else
593
+ File.join(Dir.getwd, file_path)
594
+ end
595
+ end
596
+
516
597
  # Modifies the provided menu blocks array by adding 'Back' and 'Exit' options,
517
598
  # along with initial and final dividers, based on the delegate object's configuration.
518
599
  #
@@ -533,7 +614,8 @@ module MarkdownExec
533
614
  add_exit_option(menu_blocks: menu_blocks)
534
615
  end
535
616
 
536
- add_dividers(menu_blocks: menu_blocks)
617
+ append_divider(menu_blocks: menu_blocks, position: :initial)
618
+ append_divider(menu_blocks: menu_blocks, position: :final)
537
619
  end
538
620
 
539
621
  private
@@ -542,11 +624,6 @@ module MarkdownExec
542
624
  append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::BACK)
543
625
  end
544
626
 
545
- def add_dividers(menu_blocks:)
546
- append_divider(menu_blocks: menu_blocks, position: :initial)
547
- append_divider(menu_blocks: menu_blocks, position: :final)
548
- end
549
-
550
627
  def add_exit_option(menu_blocks:)
551
628
  append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::EXIT)
552
629
  end
@@ -599,6 +676,7 @@ module MarkdownExec
599
676
  dname: HashDelegator.new(@delegate_object).string_send_color(
600
677
  formatted_name, :menu_chrome_color
601
678
  ),
679
+ nickname: formatted_name,
602
680
  oname: formatted_name
603
681
  )
604
682
 
@@ -728,6 +806,9 @@ module MarkdownExec
728
806
  # 2024-08-04 match oname for long block names
729
807
  # 2024-08-04 match nickname for long block names
730
808
  block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
809
+ end || @dml_menu_blocks.find do |item|
810
+ # 2024-08-22 search in menu blocks to allow matching of automatic chrome with nickname
811
+ block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
731
812
  end
732
813
  end
733
814
 
@@ -814,7 +895,7 @@ module MarkdownExec
814
895
  runtime_exception(:runtime_exception_error_level,
815
896
  'unmet_dependencies, flag: runtime_exception_error_level',
816
897
  required[:unmet_dependencies])
817
- elsif false ### use option 2024-08-02
898
+ elsif @delegate_object[:dump_dependencies]
818
899
  warn format_and_highlight_dependencies(dependencies,
819
900
  highlight: [@delegate_object[:block_name]])
820
901
  end
@@ -898,16 +979,20 @@ module MarkdownExec
898
979
  # @param selected [Hash] The selected item from the menu to be executed.
899
980
  # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one.
900
981
  def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:,
901
- link_state: nil)
902
- required_lines = collect_required_code_lines(mdoc: mdoc, selected: selected, link_state: link_state,
903
- block_source: block_source)
982
+ link_state:)
983
+ required_lines = collect_required_code_lines(
984
+ mdoc: mdoc, selected: selected,
985
+ link_state: link_state, block_source: block_source
986
+ )
904
987
  output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
905
988
  if output_or_approval
906
989
  display_required_code(required_lines: required_lines)
907
990
  end
908
991
  allow_execution = if @delegate_object[:user_must_approve]
909
- prompt_for_user_approval(required_lines: required_lines,
910
- selected: selected)
992
+ prompt_for_user_approval(
993
+ required_lines: required_lines,
994
+ selected: selected
995
+ )
911
996
  else
912
997
  true
913
998
  end
@@ -918,7 +1003,6 @@ module MarkdownExec
918
1003
  end
919
1004
 
920
1005
  link_state.block_name = nil
921
- LoadFileLinkState.new(LoadFile::REUSE, link_state)
922
1006
  end
923
1007
 
924
1008
  # Check if the expression contains wildcard characters
@@ -1136,348 +1220,39 @@ module MarkdownExec
1136
1220
  @delegate_object[:menu_divider_format].present? && @delegate_object[divider_key].present?
1137
1221
  end
1138
1222
 
1139
- def do_save_execution_output
1140
- return unless @delegate_object[:save_execution_output]
1141
- return if @run_state.in_own_window
1142
-
1143
- @run_state.files.write_execution_output_to_file(@delegate_object[:logged_stdout_filespec])
1144
- end
1223
+ def dml_menu_append_chrome_item(
1224
+ name, count, type, menu_state: MenuState::LOAD,
1225
+ always_create: true, always_enable: true
1226
+ )
1227
+ raise unless name.present?
1228
+ raise if @dml_menu_blocks.nil?
1145
1229
 
1146
- # Select and execute a code block from a Markdown document.
1147
- #
1148
- # This method allows the user to interactively select a code block from a
1149
- # Markdown document, obtain approval, and execute the chosen block of code.
1150
- #
1151
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1152
- def document_inpseq
1153
- @menu_base_options = @delegate_object
1154
- @dml_link_state = LinkState.new(
1155
- block_name: @delegate_object[:block_name],
1156
- document_filename: @delegate_object[:filename]
1157
- )
1158
- @run_state.source.block_name_from_cli = @dml_link_state.block_name.present?
1159
- @cli_block_name = @dml_link_state.block_name
1160
- @dml_now_using_cli = @run_state.source.block_name_from_cli
1161
- @dml_menu_default_dname = nil
1162
- @dml_block_state = SelectedBlockMenuState.new
1163
- @doc_saved_lines_files = []
1230
+ item = @dml_menu_blocks.find { |block| block.oname == name }
1164
1231
 
1165
- ## load file with code lines per options
1232
+ # create menu item when it is needed (count > 0)
1166
1233
  #
1167
- if @menu_base_options[:load_code].present?
1168
- @dml_link_state.inherited_lines =
1169
- @menu_base_options[:load_code].split(':').map do |path|
1170
- File.readlines(path, chomp: true)
1171
- end.flatten(1)
1172
-
1173
- inherited_block_names = []
1174
- inherited_dependencies = {}
1175
- selected = FCB.new(oname: 'load_code')
1176
- pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names,
1177
- code_lines, inherited_dependencies, selected)
1178
- end
1179
-
1180
- fdo = ->(option) {
1181
- name = format(@delegate_object[:menu_link_format],
1182
- HashDelegator.safeval(@delegate_object[option]))
1183
- OpenStruct.new(
1184
- dname: name,
1185
- oname: name,
1186
- name: name,
1187
- pub_name: name.pub_name
1188
- )
1189
- }
1190
- item_back = fdo.call(:menu_option_back_name)
1191
- item_edit = fdo.call(:menu_option_edit_name)
1192
- item_history = fdo.call(:menu_option_history_name)
1193
- item_load = fdo.call(:menu_option_load_name)
1194
- item_save = fdo.call(:menu_option_save_name)
1195
- item_shell = fdo.call(:menu_option_shell_name)
1196
- item_view = fdo.call(:menu_option_view_name)
1197
-
1198
- @run_state.batch_random = Random.new.rand
1199
- @run_state.batch_index = 0
1200
-
1201
- @run_state.files = StreamsOut.new
1202
-
1203
- InputSequencer.new(
1204
- @delegate_object[:filename],
1205
- @delegate_object[:input_cli_rest]
1206
- ).run do |msg, data|
1207
- # &bt msg
1208
- case msg
1209
- when :parse_document # once for each menu
1210
- # puts "@ - parse document #{data}"
1211
- inpseq_parse_document(data)
1212
-
1213
- if @delegate_object[:menu_for_history]
1214
- history_files(@dml_link_state).tap do |files|
1215
- if files.count.positive?
1216
- menu_enable_option(item_history.oname, files.count, 'files',
1217
- menu_state: MenuState::HISTORY)
1218
- end
1219
- end
1220
- end
1221
-
1222
- if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
1223
-
1224
- sf = document_name_in_glob_as_file_name(
1225
- @dml_link_state.document_filename,
1226
- @delegate_object[:document_saved_lines_glob]
1227
- )
1228
- files = sf ? Dir.glob(sf) : []
1229
- @doc_saved_lines_files = files.count.positive? ? files : []
1230
-
1231
- lines_count = @dml_link_state.inherited_lines_count
1232
-
1233
- # add menu items (glob, load, save) and enable selectively
1234
- if files.count.positive? || lines_count.positive?
1235
- menu_add_disabled_option(sf)
1236
- end
1237
- if files.count.positive?
1238
- menu_enable_option(item_load.dname, files.count, 'files',
1239
- menu_state: MenuState::LOAD)
1240
- end
1241
- if lines_count.positive?
1242
- menu_enable_option(item_edit.dname, lines_count, 'lines',
1243
- menu_state: MenuState::EDIT)
1244
- end
1245
- if lines_count.positive?
1246
- menu_enable_option(item_save.dname, 1, '',
1247
- menu_state: MenuState::SAVE)
1248
- end
1249
- if lines_count.positive?
1250
- menu_enable_option(item_view.dname, 1, '',
1251
- menu_state: MenuState::VIEW)
1252
- end
1253
- if @delegate_object[:menu_with_shell]
1254
- menu_enable_option(item_shell.dname, 1, '',
1255
- menu_state: MenuState::SHELL)
1256
- end
1257
-
1258
- # # reflect new menu items
1259
- # @dml_mdoc = MDoc.new(@dml_menu_blocks)
1260
- end
1261
-
1262
- when :display_menu
1263
- # warn "@ - display menu:"
1264
- # ii_display_menu
1265
- @dml_block_state = SelectedBlockMenuState.new
1266
- @delegate_object[:block_name] = nil
1267
-
1268
- when :user_choice
1269
- if @dml_link_state.block_name.present?
1270
- # @prior_block_was_link = true
1271
- @dml_block_state.block = blocks_find_by_block_name(@dml_blocks_in_file,
1272
- @dml_link_state.block_name)
1273
- @dml_link_state.block_name = nil
1274
- else
1275
- # puts "? - Select a block to execute (or type #{$texit} to exit):"
1276
- break if inpseq_user_choice == :break # into @dml_block_state
1277
- break if @dml_block_state.block.nil? # no block matched
1278
- end
1279
- # puts "! - Executing block: #{data}"
1280
- @dml_block_state.block&.pub_name
1281
-
1282
- when :execute_block
1283
- case (block_name = data)
1284
- when item_back.pub_name
1285
- debounce_reset
1286
- @menu_user_clicked_back_link = true
1287
- load_file_link_state = pop_link_history_and_trigger_load
1288
- @dml_link_state = load_file_link_state.link_state
1289
-
1290
- InputSequencer.merge_link_state(
1291
- @dml_link_state,
1292
- InputSequencer.next_link_state(
1293
- block_name: @dml_link_state.block_name,
1294
- document_filename: @dml_link_state.document_filename,
1295
- prior_block_was_link: true
1296
- )
1297
- )
1298
-
1299
- when item_edit.pub_name
1300
- debounce_reset
1301
- edited = edit_text(@dml_link_state.inherited_lines_block)
1302
- @dml_link_state.inherited_lines = edited.split("\n") if edited
1303
-
1304
- return :break if pause_user_exit
1305
-
1306
- InputSequencer.next_link_state(prior_block_was_link: true)
1307
-
1308
- when item_history.pub_name
1309
- debounce_reset
1310
- files = history_files(@dml_link_state)
1311
- files_table_rows = files.map do |file|
1312
- if Regexp.new(@delegate_object[:saved_asset_match]) =~ file
1313
- begin
1314
- OpenStruct.new(
1315
- file: file,
1316
- row: format(
1317
- @delegate_object[:saved_history_format],
1318
- # create with default '*' so unknown parameters are given a wildcard
1319
- $~.names.each_with_object(Hash.new('*')) do |name, hash|
1320
- hash[name.to_sym] = $~[name]
1321
- end
1322
- )
1323
- )
1324
- rescue KeyError
1325
- # pp $!, $@
1326
- warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
1327
- error_handler('saved_history_format')
1328
- break
1329
- end
1330
- else
1331
- warn "Cannot parse name: #{file}"
1332
- next
1333
- end
1334
- end&.compact
1335
-
1336
- return :break unless files_table_rows
1337
-
1338
- # repeat select+display until user exits
1339
- row_attrib = :row
1340
- loop do
1341
- # menu with Back and Facet options at top
1342
- case (name = prompt_select_code_filename(
1343
- [@delegate_object[:prompt_filespec_back],
1344
- @delegate_object[:prompt_filespec_facet]] +
1345
- files_table_rows.map(&row_attrib),
1346
- string: @delegate_object[:prompt_select_history_file],
1347
- color_sym: :prompt_color_after_script_execution
1348
- ))
1349
- when @delegate_object[:prompt_filespec_back]
1350
- break
1351
- when @delegate_object[:prompt_filespec_facet]
1352
- row_attrib = row_attrib == :row ? :file : :row
1353
- else
1354
- file = files_table_rows.select { |ftr| ftr.row == name }&.first
1355
- info = file_info(file.file)
1356
- warn "#{file.file} - #{info[:lines]} lines / #{info[:size]} bytes"
1357
- warn(File.readlines(file.file,
1358
- chomp: false).map.with_index do |line, ind|
1359
- format(' %s. %s', format('% 4d', ind + 1).violet, line)
1360
- end)
1361
- end
1362
- end
1363
-
1364
- return :break if pause_user_exit
1365
-
1366
- InputSequencer.next_link_state(prior_block_was_link: true)
1367
-
1368
- when item_load.pub_name
1369
- debounce_reset
1370
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
1371
- @delegate_object[:document_saved_lines_glob])
1372
- load_filespec = load_filespec_from_expression(sf)
1373
- if load_filespec
1374
- @dml_link_state.inherited_lines_append(
1375
- File.readlines(load_filespec, chomp: true)
1376
- )
1377
- end
1378
-
1379
- return :break if pause_user_exit
1380
-
1381
- InputSequencer.next_link_state(prior_block_was_link: true)
1382
-
1383
- when item_save.pub_name
1384
- debounce_reset
1385
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
1386
- @delegate_object[:document_saved_lines_glob])
1387
- save_filespec = save_filespec_from_expression(sf)
1388
- if save_filespec && !write_file_with_directory_creation(
1389
- save_filespec,
1390
- HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
1391
- )
1392
- return :break
1393
-
1394
- end
1395
-
1396
- InputSequencer.next_link_state(prior_block_was_link: true)
1397
-
1398
- when item_shell.pub_name
1399
- debounce_reset
1400
- loop do
1401
- command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen)
1402
- break if !command.present? || command == 'exit'
1403
-
1404
- exit_status = execute_command_with_streams(
1405
- [@delegate_object[:shell], '-c', command]
1406
- )
1407
- case exit_status
1408
- when 0
1409
- warn "#{'OK'.green} #{exit_status}"
1410
- else
1411
- warn "#{'ERR'.bred} #{exit_status}"
1412
- end
1413
- end
1414
-
1415
- return :break if pause_user_exit
1416
-
1417
- InputSequencer.next_link_state(prior_block_was_link: true)
1418
-
1419
- when item_view.pub_name
1420
- debounce_reset
1421
- warn @dml_link_state.inherited_lines_block
1422
-
1423
- return :break if pause_user_exit
1424
-
1425
- InputSequencer.next_link_state(prior_block_was_link: true)
1426
-
1427
- else
1428
- @dml_block_state = block_state_for_name_from_cli(block_name)
1429
- if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS
1430
- debounce_reset
1431
- link_state = LinkState.new
1432
- options_state = read_show_options_and_trigger_reuse(
1433
- link_state: link_state,
1434
- mdoc: @dml_mdoc,
1435
- selected: @dml_block_state.block
1436
- )
1437
-
1438
- update_menu_base(options_state.options)
1439
- options_state.load_file_link_state.link_state
1440
- else
1441
- inpseq_execute_block(block_name)
1234
+ if item.nil? && (always_create || count.positive?)
1235
+ item = append_chrome_block(menu_blocks: @dml_menu_blocks,
1236
+ menu_state: menu_state)
1237
+ end
1442
1238
 
1443
- if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli,
1444
- selected: @dml_block_state.block)
1445
- return :break
1446
- end
1239
+ # update item if it exists
1240
+ #
1241
+ return unless item
1447
1242
 
1448
- ## order of block name processing: link block, cli, from user
1449
- #
1450
- @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break =
1451
- HashDelegator.next_link_state(
1452
- block_name: @dml_link_state.block_name,
1453
- block_name_from_cli: @dml_now_using_cli,
1454
- block_state: @dml_block_state,
1455
- was_using_cli: @dml_now_using_cli
1456
- )
1457
-
1458
- if !@dml_block_state.source.block_name_from_ui && cli_break
1459
- # &bsp '!block_name_from_ui + cli_break -> break'
1460
- return :break
1461
- end
1243
+ item.dname = type.present? ? "#{name} (#{count} #{type})" : name
1244
+ if always_enable || count.positive?
1245
+ item.delete(:disabled)
1246
+ else
1247
+ item[:disabled] = ''
1248
+ end
1249
+ end
1462
1250
 
1463
- InputSequencer.next_link_state(
1464
- block_name: @dml_link_state.block_name,
1465
- prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH
1466
- )
1467
- end
1468
- end
1251
+ def do_save_execution_output
1252
+ return unless @delegate_object[:save_execution_output]
1253
+ return if @run_state.in_own_window
1469
1254
 
1470
- when :exit?
1471
- data == $texit
1472
- when :stay?
1473
- data == $stay
1474
- else
1475
- raise "Invalid message: #{msg}"
1476
- end
1477
- end
1478
- rescue StandardError
1479
- HashDelegator.error_handler('document_inpseq',
1480
- { abort: true })
1255
+ @run_state.files.write_execution_output_to_file(@delegate_object[:logged_stdout_filespec])
1481
1256
  end
1482
1257
 
1483
1258
  # remove leading "./"
@@ -1492,9 +1267,9 @@ module MarkdownExec
1492
1267
  '_') })
1493
1268
  end
1494
1269
 
1495
- def dump_and_warn_block_state(selected:)
1270
+ def dump_and_warn_block_state(name:, selected:)
1496
1271
  if selected.nil?
1497
- Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
1272
+ Exceptions.warn_format("Block not found -- name: #{name}",
1498
1273
  { abort: true })
1499
1274
  end
1500
1275
 
@@ -1591,8 +1366,9 @@ module MarkdownExec
1591
1366
  result_text
1592
1367
  end
1593
1368
 
1594
- def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
1595
- lfls = execute_shell_type(
1369
+ def execute_block_for_state_and_name(selected:, mdoc:, link_state:,
1370
+ block_source: {})
1371
+ lfls = execute_block_by_type_for_lfls(
1596
1372
  selected: selected,
1597
1373
  mdoc: mdoc,
1598
1374
  link_state: link_state,
@@ -1601,8 +1377,26 @@ module MarkdownExec
1601
1377
 
1602
1378
  # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1603
1379
  [lfls.link_state,
1604
- lfls.load_file == LoadFile::LOAD ? nil : selected.dname]
1605
- #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1380
+ lfls.load_file == LoadFile::LOAD ? nil : selected.dname,
1381
+ # 2024-08-22 true to quit
1382
+ lfls.load_file == LoadFile::EXIT]
1383
+ end
1384
+
1385
+ def execute_block_in_state(block_name)
1386
+ @dml_block_state = block_state_for_name_from_cli(block_name)
1387
+ dump_and_warn_block_state(name: block_name,
1388
+ selected: @dml_block_state.block)
1389
+ @dml_link_state, @dml_menu_default_dname, quit =
1390
+ execute_block_for_state_and_name(
1391
+ selected: @dml_block_state.block,
1392
+ mdoc: @dml_mdoc,
1393
+ link_state: @dml_link_state,
1394
+ block_source: {
1395
+ document_filename: @delegate_object[:filename],
1396
+ time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1397
+ }
1398
+ )
1399
+ :break if quit
1606
1400
  end
1607
1401
 
1608
1402
  # Executes a given command and processes its input, output, and error streams.
@@ -1655,6 +1449,82 @@ module MarkdownExec
1655
1449
  exit_status
1656
1450
  end
1657
1451
 
1452
+ def execute_history_select(
1453
+ files_table_rows,
1454
+ exit_prompt: @delegate_object[:prompt_filespec_back],
1455
+ pause_refresh: false,
1456
+ stream:
1457
+ )
1458
+ # repeat select+display until user exits
1459
+
1460
+ pause_now = false
1461
+ row_attrib = :row
1462
+ loop do
1463
+ if pause_now
1464
+ break if prompt_select_continue == MenuState::EXIT
1465
+ end
1466
+
1467
+ # menu with Back and Facet options at top
1468
+ case (name = prompt_select_code_filename(
1469
+ [exit_prompt,
1470
+ @delegate_object[:prompt_filespec_facet]] +
1471
+ files_table_rows.map(&row_attrib),
1472
+ string: @delegate_object[:prompt_select_history_file],
1473
+ color_sym: :prompt_color_after_script_execution
1474
+ ))
1475
+ when exit_prompt
1476
+ break
1477
+ when @delegate_object[:prompt_filespec_facet]
1478
+ row_attrib = row_attrib == :row ? :file : :row
1479
+ pause_now = false
1480
+ else
1481
+ file = files_table_rows.select { |ftr| ftr.row == name }&.first
1482
+ info = file_info(file.file)
1483
+ stream.puts "#{file.file} - #{info[:lines]} lines / " \
1484
+ "#{info[:size]} bytes"
1485
+ stream.puts(
1486
+ File.readlines(file.file,
1487
+ chomp: false).map.with_index do |line, ind|
1488
+ format(' %s. %s',
1489
+ AnsiString.new(format('% 4d', ind + 1)).send(:violet), line)
1490
+ end
1491
+ )
1492
+ pause_now = pause_refresh
1493
+ end
1494
+ end
1495
+ end
1496
+
1497
+ def execute_inherited_save
1498
+ save_filespec = save_filespec_from_expression(
1499
+ document_name_in_glob_as_file_name(
1500
+ @dml_link_state.document_filename,
1501
+ @delegate_object[:document_saved_lines_glob]
1502
+ )
1503
+ )
1504
+ if save_filespec && !write_file_with_directory_creation(
1505
+ save_filespec,
1506
+ HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
1507
+ )
1508
+ :break
1509
+ end
1510
+ end
1511
+
1512
+ def execute_navigate_back
1513
+ @menu_user_clicked_back_link = true
1514
+
1515
+ keep_code = @dml_link_state.keep_code
1516
+ inherited_lines = keep_code ? @dml_link_state.inherited_lines_block : nil
1517
+
1518
+ @dml_link_state = pop_link_history_new_state
1519
+
1520
+ {
1521
+ block_name: @dml_link_state.block_name,
1522
+ document_filename: @dml_link_state.document_filename,
1523
+ inherited_lines: inherited_lines,
1524
+ keep_code: keep_code
1525
+ }
1526
+ end
1527
+
1658
1528
  # Executes a block of code that has been approved for execution.
1659
1529
  # It sets the script block name, writes command files if required, and handles the execution
1660
1530
  # including output formatting and summarization.
@@ -1682,8 +1552,8 @@ module MarkdownExec
1682
1552
  # @param opts [Hash] Options hash containing configuration settings.
1683
1553
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
1684
1554
  #
1685
- def execute_shell_type(selected:, mdoc:, block_source:,
1686
- link_state: LinkState.new)
1555
+ def execute_block_by_type_for_lfls(selected:, mdoc:, block_source:,
1556
+ link_state: LinkState.new)
1687
1557
  if selected.shell == BlockType::LINK
1688
1558
  debounce_reset
1689
1559
  push_link_history_and_trigger_load(link_block_body: selected.body,
@@ -1692,9 +1562,18 @@ module MarkdownExec
1692
1562
  link_state: link_state,
1693
1563
  block_source: block_source)
1694
1564
 
1565
+ # from CLI
1566
+ elsif selected.nickname == @delegate_object[:menu_option_exit_name][:line]
1567
+ debounce_reset
1568
+ LoadFileLinkState.new(LoadFile::EXIT, link_state)
1569
+
1695
1570
  elsif @menu_user_clicked_back_link
1696
1571
  debounce_reset
1697
- pop_link_history_and_trigger_load
1572
+ # pop_link_history_new_state
1573
+ LoadFileLinkState.new(
1574
+ LoadFile::LOAD,
1575
+ pop_link_history_new_state
1576
+ )
1698
1577
 
1699
1578
  elsif selected.shell == BlockType::OPTS
1700
1579
  debounce_reset
@@ -1730,6 +1609,7 @@ module MarkdownExec
1730
1609
  selected: selected,
1731
1610
  link_state: link_state,
1732
1611
  block_source: block_source)
1612
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
1733
1613
 
1734
1614
  else
1735
1615
  LoadFileLinkState.new(LoadFile::REUSE, link_state)
@@ -1812,6 +1692,27 @@ module MarkdownExec
1812
1692
  expr.include?('%{') ? format_expression(expr) : expr
1813
1693
  end
1814
1694
 
1695
+ def fout_execution_report
1696
+ @fout.fout fetch_color(data_sym: :execution_report_preview_head,
1697
+ color_sym: :execution_report_preview_frame_color)
1698
+ [
1699
+ ['Block', @run_state.script_block_name],
1700
+ ['Command', ([MarkdownExec::BIN_NAME, @delegate_object[:filename]] +
1701
+ (@run_state.link_history.map { |item|
1702
+ item[:block_name]
1703
+ }) +
1704
+ [@run_state.script_block_name]).join(' ')],
1705
+ ['Script', @run_state.saved_filespec],
1706
+ ['StdOut', @delegate_object[:logged_stdout_filespec]]
1707
+ ].each do |label, value|
1708
+ next unless value
1709
+
1710
+ output_labeled_value(label, value, DISPLAY_LEVEL_ADMIN)
1711
+ end
1712
+ @fout.fout fetch_color(data_sym: :execution_report_preview_tail,
1713
+ color_sym: :execution_report_preview_frame_color)
1714
+ end
1715
+
1815
1716
  def generate_temp_filename(ext = '.sh')
1816
1717
  filename = begin
1817
1718
  Dir::Tmpname.make_tmpname(['x', ext], nil)
@@ -1929,45 +1830,6 @@ module MarkdownExec
1929
1830
  }
1930
1831
  end
1931
1832
 
1932
- def inpseq_execute_block(block_name)
1933
- @dml_block_state = block_state_for_name_from_cli(block_name)
1934
- dump_and_warn_block_state(selected: @dml_block_state.block)
1935
- @dml_link_state, @dml_menu_default_dname =
1936
- exec_bash_next_state(
1937
- selected: @dml_block_state.block,
1938
- mdoc: @dml_mdoc,
1939
- link_state: @dml_link_state,
1940
- block_source: {
1941
- document_filename: @delegate_object[:filename],
1942
- time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
1943
- }
1944
- )
1945
- end
1946
-
1947
- def inpseq_parse_document(_document_filename)
1948
- @run_state.batch_index += 1
1949
- @run_state.in_own_window = false
1950
-
1951
- # &bsp 'loop', block_name_from_cli, @cli_block_name
1952
- @run_state.source.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
1953
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.source.block_name_from_cli,
1954
- now_using_cli: @dml_now_using_cli,
1955
- link_state: @dml_link_state)
1956
- end
1957
-
1958
- def inpseq_user_choice
1959
- @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1960
- menu_blocks: @dml_menu_blocks,
1961
- default: @dml_menu_default_dname)
1962
- # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
1963
- if !@dml_block_state
1964
- HashDelegator.error_handler('block_state missing', { abort: true })
1965
- elsif @dml_block_state.state == MenuState::EXIT
1966
- # &bsp 'load_cli_or_user_selected_block -> break'
1967
- :break
1968
- end
1969
- end
1970
-
1971
1833
  # Iterates through blocks in a file, applying the provided block to each line.
1972
1834
  # The iteration only occurs if the file exists.
1973
1835
  # @yield [Symbol] :filter Yields to obtain selected messages for processing.
@@ -1975,11 +1837,11 @@ module MarkdownExec
1975
1837
  return unless check_file_existence(@delegate_object[:filename])
1976
1838
 
1977
1839
  state = initial_state
1978
- selected_messages = yield :filter
1840
+ selected_types = yield :filter
1979
1841
  cfile.readlines(@delegate_object[:filename],
1980
1842
  import_paths: @delegate_object[:import_paths]&.split(':')).each do |nested_line|
1981
1843
  if nested_line
1982
- update_line_and_block_state(nested_line, state, selected_messages,
1844
+ update_line_and_block_state(nested_line, state, selected_types,
1983
1845
  &block)
1984
1846
  end
1985
1847
  end
@@ -2027,8 +1889,9 @@ module MarkdownExec
2027
1889
  label_format_above = @delegate_object[:shell_code_label_format_above]
2028
1890
  label_format_below = @delegate_object[:shell_code_label_format_below]
2029
1891
 
2030
- [label_format_above && format(label_format_above,
2031
- block_source.merge({ block_name: selected.pub_name }))] +
1892
+ [label_format_above.present? &&
1893
+ format(label_format_above,
1894
+ block_source.merge({ block_name: selected.pub_name }))] +
2032
1895
  output_lines.map do |line|
2033
1896
  re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
2034
1897
  next unless re =~ line
@@ -2037,14 +1900,17 @@ module MarkdownExec
2037
1900
  link_block_data.fetch('format',
2038
1901
  '%{line}'))
2039
1902
  end.compact +
2040
- [label_format_below && format(label_format_below,
2041
- block_source.merge({ block_name: selected.pub_name }))]
1903
+ [label_format_below.present? &&
1904
+ format(label_format_below,
1905
+ block_source.merge({ block_name: selected.pub_name }))]
2042
1906
  end
2043
1907
 
2044
1908
  def link_history_push_and_next(
2045
1909
  curr_block_name:, curr_document_filename:,
2046
1910
  inherited_block_names:, inherited_dependencies:, inherited_lines:,
1911
+ keep_code:,
2047
1912
  next_block_name:, next_document_filename:,
1913
+ next_keep_code:,
2048
1914
  next_load_file:
2049
1915
  )
2050
1916
  @link_history.push(
@@ -2053,7 +1919,8 @@ module MarkdownExec
2053
1919
  document_filename: curr_document_filename,
2054
1920
  inherited_block_names: inherited_block_names,
2055
1921
  inherited_dependencies: inherited_dependencies,
2056
- inherited_lines: inherited_lines
1922
+ inherited_lines: inherited_lines,
1923
+ keep_code: keep_code
2057
1924
  )
2058
1925
  )
2059
1926
  LoadFileLinkState.new(
@@ -2063,7 +1930,8 @@ module MarkdownExec
2063
1930
  document_filename: next_document_filename,
2064
1931
  inherited_block_names: inherited_block_names,
2065
1932
  inherited_dependencies: inherited_dependencies,
2066
- inherited_lines: inherited_lines
1933
+ inherited_lines: inherited_lines,
1934
+ keep_code: next_keep_code
2067
1935
  )
2068
1936
  )
2069
1937
  end
@@ -2119,8 +1987,6 @@ module MarkdownExec
2119
1987
  end
2120
1988
 
2121
1989
  SelectedBlockMenuState.new(block, source, state)
2122
- rescue StandardError
2123
- HashDelegator.error_handler('load_cli_or_user_selected_block')
2124
1990
  end
2125
1991
 
2126
1992
  # format + glob + select for file in load block
@@ -2161,6 +2027,23 @@ module MarkdownExec
2161
2027
  end
2162
2028
  end
2163
2029
 
2030
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:,
2031
+ link_state:)
2032
+ if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2033
+ # &bsp 'pause cli control, allow user to select block'
2034
+ block_name_from_cli = false
2035
+ now_using_cli = false
2036
+ @menu_base_options[:block_name] =
2037
+ @delegate_object[:block_name] = \
2038
+ link_state.block_name =
2039
+ @cli_block_name = nil
2040
+ end
2041
+
2042
+ @delegate_object = @menu_base_options.dup
2043
+ @menu_user_clicked_back_link = false
2044
+ [block_name_from_cli, now_using_cli]
2045
+ end
2046
+
2164
2047
  def mdoc_and_blocks_from_nested_files
2165
2048
  menu_blocks = blocks_from_nested_files
2166
2049
  mdoc = MDoc.new(menu_blocks) do |nopts|
@@ -2184,6 +2067,7 @@ module MarkdownExec
2184
2067
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
2185
2068
  ### compress empty lines
2186
2069
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
2070
+ HashDelegator.tables_into_columns!(menu_blocks, @delegate_object)
2187
2071
  [all_blocks, menu_blocks, mdoc] # &br
2188
2072
  end
2189
2073
 
@@ -2240,48 +2124,6 @@ module MarkdownExec
2240
2124
  end
2241
2125
  end
2242
2126
 
2243
- def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
2244
- raise unless name.present?
2245
- raise if @dml_menu_blocks.nil?
2246
-
2247
- item = @dml_menu_blocks.find { |block| block.oname == name }
2248
-
2249
- # create menu item when it is needed (count > 0)
2250
- #
2251
- if item.nil? && count.positive?
2252
- item = append_chrome_block(menu_blocks: @dml_menu_blocks,
2253
- menu_state: menu_state)
2254
- end
2255
-
2256
- # update item if it exists
2257
- #
2258
- return unless item
2259
-
2260
- item.dname = type.present? ? "#{name} (#{count} #{type})" : name
2261
- if count.positive?
2262
- item.delete(:disabled)
2263
- else
2264
- item[:disabled] = ''
2265
- end
2266
- end
2267
-
2268
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:,
2269
- link_state:)
2270
- if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2271
- # &bsp 'pause cli control, allow user to select block'
2272
- block_name_from_cli = false
2273
- now_using_cli = false
2274
- @menu_base_options[:block_name] =
2275
- @delegate_object[:block_name] = \
2276
- link_state.block_name =
2277
- @cli_block_name = nil
2278
- end
2279
-
2280
- @delegate_object = @menu_base_options.dup
2281
- @menu_user_clicked_back_link = false
2282
- [block_name_from_cli, now_using_cli]
2283
- end
2284
-
2285
2127
  # If a method is missing, treat it as a key for the @delegate_object.
2286
2128
  def method_missing(method_name, *args, &block)
2287
2129
  if @delegate_object.respond_to?(method_name)
@@ -2309,8 +2151,10 @@ module MarkdownExec
2309
2151
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2310
2152
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2311
2153
  inherited_lines: HashDelegator.code_merge(code_lines),
2154
+ keep_code: link_state&.keep_code,
2312
2155
  next_block_name: '',
2313
2156
  next_document_filename: @delegate_object[:filename],
2157
+ next_keep_code: false,
2314
2158
  next_load_file: LoadFile::REUSE
2315
2159
  )
2316
2160
  end
@@ -2321,27 +2165,6 @@ module MarkdownExec
2321
2165
  @fout.fout formatted_string
2322
2166
  end
2323
2167
 
2324
- def fout_execution_report
2325
- @fout.fout fetch_color(data_sym: :execution_report_preview_head,
2326
- color_sym: :execution_report_preview_frame_color)
2327
- [
2328
- ['Block', @run_state.script_block_name],
2329
- ['Command', ([MarkdownExec::BIN_NAME, @delegate_object[:filename]] +
2330
- (@run_state.link_history.map { |item|
2331
- item[:block_name]
2332
- }) +
2333
- [@run_state.script_block_name]).join(' ')],
2334
- ['Script', @run_state.saved_filespec],
2335
- ['StdOut', @delegate_object[:logged_stdout_filespec]]
2336
- ].each do |label, value|
2337
- next unless value
2338
-
2339
- output_labeled_value(label, value, DISPLAY_LEVEL_ADMIN)
2340
- end
2341
- @fout.fout fetch_color(data_sym: :execution_report_preview_tail,
2342
- color_sym: :execution_report_preview_frame_color)
2343
- end
2344
-
2345
2168
  def output_execution_summary
2346
2169
  return unless @delegate_object[:output_execution_summary]
2347
2170
 
@@ -2401,8 +2224,10 @@ module MarkdownExec
2401
2224
  (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2402
2225
  inherited_lines:
2403
2226
  HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2227
+ keep_code: link_state&.keep_code,
2404
2228
  next_block_name: next_block_name,
2405
2229
  next_document_filename: @delegate_object[:filename], # not next_document_filename
2230
+ next_keep_code: false,
2406
2231
  next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
2407
2232
  )
2408
2233
  # LoadFileLinkState.new(LoadFile::REUSE, link_state)
@@ -2410,20 +2235,17 @@ module MarkdownExec
2410
2235
  end
2411
2236
 
2412
2237
  # This method handles the back-link operation in the Markdown execution context.
2413
- # It updates the history state and prepares to load the next block.
2238
+ # It updates the history state for the next block.
2414
2239
  #
2415
- # @return [LoadFileLinkState] An object indicating the action to load the next block.
2416
- def pop_link_history_and_trigger_load
2240
+ # @return [LinkState] An object indicating the state for the next block.
2241
+ def pop_link_history_new_state
2417
2242
  pop = @link_history.pop
2418
2243
  peek = @link_history.peek
2419
- LoadFileLinkState.new(
2420
- LoadFile::LOAD,
2421
- LinkState.new(
2422
- document_filename: pop.document_filename,
2423
- inherited_block_names: peek.inherited_block_names,
2424
- inherited_dependencies: peek.inherited_dependencies,
2425
- inherited_lines: peek.inherited_lines
2426
- )
2244
+ LinkState.new(
2245
+ document_filename: pop.document_filename,
2246
+ inherited_block_names: peek.inherited_block_names,
2247
+ inherited_dependencies: peek.inherited_dependencies,
2248
+ inherited_lines: peek.inherited_lines
2427
2249
  )
2428
2250
  end
2429
2251
 
@@ -2532,8 +2354,6 @@ module MarkdownExec
2532
2354
 
2533
2355
  @allowed_execution_block = @prior_execution_block
2534
2356
  true
2535
- rescue TTY::Reader::InputInterrupt
2536
- exit 1
2537
2357
  end
2538
2358
 
2539
2359
  def prompt_for_command(prompt)
@@ -2602,8 +2422,6 @@ module MarkdownExec
2602
2422
  end
2603
2423
 
2604
2424
  sel == MenuOptions::YES
2605
- rescue TTY::Reader::InputInterrupt
2606
- exit 1
2607
2425
  end
2608
2426
 
2609
2427
  # public
@@ -2632,8 +2450,6 @@ module MarkdownExec
2632
2450
  end
2633
2451
  end
2634
2452
  end
2635
- rescue TTY::Reader::InputInterrupt
2636
- exit 1
2637
2453
  end
2638
2454
 
2639
2455
  def prompt_select_continue(filter: true, quiet: true)
@@ -2647,8 +2463,6 @@ module MarkdownExec
2647
2463
  menu.choice @delegate_object[:prompt_exit]
2648
2464
  end
2649
2465
  sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
2650
- rescue TTY::Reader::InputInterrupt
2651
- exit 1
2652
2466
  end
2653
2467
 
2654
2468
  # user prompt to exit if the menu will be displayed again
@@ -2729,6 +2543,7 @@ module MarkdownExec
2729
2543
  dependencies, selected, next_block_name: next_block_name)
2730
2544
 
2731
2545
  else
2546
+ next_keep_code = link_state&.keep_code || link_block_data.fetch('keep', false) #/*LinkKeys::KEEP*/
2732
2547
  link_history_push_and_next(
2733
2548
  curr_block_name: selected.pub_name,
2734
2549
  curr_document_filename: @delegate_object[:filename],
@@ -2737,8 +2552,10 @@ module MarkdownExec
2737
2552
  inherited_lines: HashDelegator.code_merge(
2738
2553
  link_state&.inherited_lines, code_lines
2739
2554
  ),
2555
+ keep_code: link_state&.keep_code,
2740
2556
  next_block_name: next_block_name,
2741
2557
  next_document_filename: next_document_filename,
2558
+ next_keep_code: next_keep_code,
2742
2559
  next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
2743
2560
  )
2744
2561
  end
@@ -2753,6 +2570,34 @@ module MarkdownExec
2753
2570
  gets.chomp
2754
2571
  end
2755
2572
 
2573
+ def read_saved_assets_for_history_table
2574
+ files = history_files(@dml_link_state).sort
2575
+ files.map do |file|
2576
+ if Regexp.new(@delegate_object[:saved_asset_match]) =~ file
2577
+ begin
2578
+ OpenStruct.new(
2579
+ file: file,
2580
+ row: format(
2581
+ @delegate_object[:saved_history_format],
2582
+ # create with default '*' so unknown parameters are given a wildcard
2583
+ $~.names.each_with_object(Hash.new('*')) do |name, hash|
2584
+ hash[name.to_sym] = $~[name]
2585
+ end
2586
+ )
2587
+ )
2588
+ rescue KeyError
2589
+ # pp $!, $@
2590
+ warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
2591
+ error_handler('saved_history_format')
2592
+ return nil
2593
+ end
2594
+ else
2595
+ warn "Cannot parse name: #{file}"
2596
+ next
2597
+ end
2598
+ end&.compact
2599
+ end
2600
+
2756
2601
  # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
2757
2602
  # @param selected [Hash] Selected item from the menu containing a YAML body.
2758
2603
  # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
@@ -2762,12 +2607,12 @@ module MarkdownExec
2762
2607
  obj = {}
2763
2608
 
2764
2609
  # concatenated body of all required blocks loaded a YAML
2765
- data = YAML.load(
2610
+ data = (YAML.load(
2766
2611
  collect_required_code_lines(
2767
2612
  mdoc: mdoc, selected: selected,
2768
2613
  link_state: link_state, block_source: {}
2769
2614
  ).join("\n")
2770
- ).transform_keys(&:to_sym)
2615
+ ) || {}).transform_keys(&:to_sym)
2771
2616
 
2772
2617
  if selected.shell == BlockType::OPTS
2773
2618
  obj = data
@@ -2847,14 +2692,19 @@ module MarkdownExec
2847
2692
  if @delegate_object[exception_sym] != 0
2848
2693
  data = { name: name, detail: items.join(', ') }
2849
2694
  warn(
2850
- format(
2851
- @delegate_object.fetch(:exception_format_name, "\n%{name}"),
2852
- data
2853
- ).send(@delegate_object.fetch(:exception_color_name, :red)) +
2854
- format(
2855
- @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
2856
- data
2857
- ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
2695
+ AnsiString.new(format(
2696
+ @delegate_object.fetch(:exception_format_name,
2697
+ "\n%{name}"),
2698
+ data
2699
+ )).send(@delegate_object.fetch(:exception_color_name,
2700
+ :red)) +
2701
+ AnsiString.new(format(
2702
+ @delegate_object.fetch(:exception_format_detail,
2703
+ " - %{detail}\n"),
2704
+ data
2705
+ )).send(@delegate_object.fetch(
2706
+ :exception_color_detail, :yellow
2707
+ ))
2858
2708
  )
2859
2709
  end
2860
2710
  return unless (@delegate_object[exception_sym]).positive?
@@ -2907,6 +2757,24 @@ module MarkdownExec
2907
2757
  @fout.fout "File saved: #{@run_state.saved_filespec}"
2908
2758
  end
2909
2759
 
2760
+ def select_document_if_multiple(options, files, prompt:)
2761
+ # binding.irb
2762
+ return files if files.class == String ###
2763
+ return files[0] if (count = files.count) == 1
2764
+
2765
+ return unless count >= 2
2766
+
2767
+ opts = options.dup
2768
+ select_option_or_exit(
2769
+ string_send_color(
2770
+ prompt,
2771
+ :prompt_color_after_script_execution
2772
+ ),
2773
+ files,
2774
+ opts.merge(per_page: opts[:select_page_height])
2775
+ )
2776
+ end
2777
+
2910
2778
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
2911
2779
  def select_option_with_metadata(prompt_text, menu_items, opts = {})
2912
2780
  ## configure to environment
@@ -2948,40 +2816,6 @@ module MarkdownExec
2948
2816
  end
2949
2817
 
2950
2818
  selected
2951
- rescue TTY::Reader::InputInterrupt
2952
- exit 1
2953
- rescue StandardError
2954
- HashDelegator.error_handler('select_option_with_metadata')
2955
- end
2956
-
2957
- # Update the block name in the link state and delegate object.
2958
- #
2959
- # This method updates the block name based on whether it was specified
2960
- # through the CLI or derived from the link state.
2961
- #
2962
- # @param link_state [LinkState] The current link state object.
2963
- # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
2964
- def set_delob_filename_block_name(link_state:, block_name_from_cli:)
2965
- @delegate_object[:filename] = link_state.document_filename
2966
- link_state.block_name = @delegate_object[:block_name] =
2967
- block_name_from_cli ? @cli_block_name : link_state.block_name
2968
- end
2969
-
2970
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:,
2971
- link_state:)
2972
- block_name_from_cli, now_using_cli =
2973
- manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2974
- now_using_cli: now_using_cli,
2975
- link_state: link_state)
2976
- set_delob_filename_block_name(link_state: link_state,
2977
- block_name_from_cli: block_name_from_cli)
2978
-
2979
- # update @delegate_object and @menu_base_options in auto_load
2980
- #
2981
- blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
2982
- dump_delobj(blocks_in_file, menu_blocks, link_state)
2983
-
2984
- [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
2985
2819
  end
2986
2820
 
2987
2821
  def set_environment_variables_for_block(selected)
@@ -3068,7 +2902,7 @@ module MarkdownExec
3068
2902
  stdin: if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
3069
2903
  tn.named_captures.sym_keys
3070
2904
  end,
3071
- stdout: if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
2905
+ stdout: if (tn = rest.match(/>(?<type>\$)?(?<name>[\w.\-]+)/))
3072
2906
  tn.named_captures.sym_keys
3073
2907
  end,
3074
2908
  title: title,
@@ -3093,7 +2927,7 @@ module MarkdownExec
3093
2927
  # @param line [String] The current line being processed.
3094
2928
  # @param state [Hash] The current state of the parser, including flags and data related to the processing.
3095
2929
  # @param opts [Hash] A hash containing various options for line and block processing.
3096
- # @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
2930
+ # @param selected_types [Array<String>] Accumulator for lines or messages that are subject to further processing.
3097
2931
  # @param block [Proc] An optional block for further processing or transformation of lines.
3098
2932
  #
3099
2933
  # @option state [Array<String>] :headings Current headings to be updated based on the line.
@@ -3103,9 +2937,9 @@ module MarkdownExec
3103
2937
  #
3104
2938
  # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
3105
2939
  #
3106
- # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
2940
+ # @return [Void] The function modifies the `state` and `selected_types` arguments in place.
3107
2941
  ##
3108
- def update_line_and_block_state(nested_line, state, selected_messages,
2942
+ def update_line_and_block_state(nested_line, state, selected_types,
3109
2943
  &block)
3110
2944
  line = nested_line.to_s
3111
2945
  if line.match(@delegate_object[:fenced_start_and_end_regex])
@@ -3114,9 +2948,9 @@ module MarkdownExec
3114
2948
  #
3115
2949
  HashDelegator.update_menu_attrib_yield_selected(
3116
2950
  fcb: state[:fcb],
3117
- messages: selected_messages,
2951
+ messages: selected_types,
3118
2952
  configuration: @delegate_object,
3119
- &block
2953
+ &block
3120
2954
  )
3121
2955
  state[:in_fenced_block] = false
3122
2956
  else
@@ -3138,7 +2972,7 @@ module MarkdownExec
3138
2972
  elsif nested_line[:depth].zero? || @delegate_object[:menu_include_imported_notes]
3139
2973
  # add line if it is depth 0 or option allows it
3140
2974
  #
3141
- HashDelegator.yield_line_if_selected(line, selected_messages, &block)
2975
+ HashDelegator.yield_line_if_selected(line, selected_types, &block)
3142
2976
 
3143
2977
  else
3144
2978
  # &bsp 'line is not recognized for block state'
@@ -3149,10 +2983,464 @@ module MarkdownExec
3149
2983
  ## apply options to current state
3150
2984
  #
3151
2985
  def update_menu_base(options)
3152
- @menu_base_options.merge!(options)
2986
+ # under simple uses, @menu_base_options may be nil
2987
+ @menu_base_options&.merge!(options)
3153
2988
  @delegate_object.merge!(options)
3154
2989
  end
3155
2990
 
2991
+ def vux_await_user_selection
2992
+ @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
2993
+ menu_blocks: @dml_menu_blocks,
2994
+ default: @dml_menu_default_dname)
2995
+ # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
2996
+ if !@dml_block_state
2997
+ HashDelegator.error_handler('block_state missing', { abort: true })
2998
+ elsif @dml_block_state.state == MenuState::EXIT
2999
+ # &bsp 'load_cli_or_user_selected_block -> break'
3000
+ :break
3001
+ end
3002
+ end
3003
+
3004
+ def vux_clear_menu_state
3005
+ @dml_block_state = SelectedBlockMenuState.new
3006
+ @delegate_object[:block_name] = nil
3007
+ end
3008
+
3009
+ def vux_edit_inherited
3010
+ edited = edit_text(@dml_link_state.inherited_lines_block)
3011
+ @dml_link_state.inherited_lines = edited.split("\n") if edited
3012
+ end
3013
+
3014
+ def vux_execute_and_prompt(block_name)
3015
+ @dml_block_state = block_state_for_name_from_cli(block_name)
3016
+ if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS
3017
+ debounce_reset
3018
+ link_state = LinkState.new
3019
+ options_state = read_show_options_and_trigger_reuse(
3020
+ link_state: link_state,
3021
+ mdoc: @dml_mdoc,
3022
+ selected: @dml_block_state.block
3023
+ )
3024
+
3025
+ update_menu_base(options_state.options)
3026
+ options_state.load_file_link_state.link_state
3027
+ return
3028
+ end
3029
+
3030
+ return :break if execute_block_in_state(block_name) == :break
3031
+
3032
+ if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli,
3033
+ selected: @dml_block_state.block)
3034
+ return :break
3035
+ end
3036
+
3037
+ ## order of block name processing: link block, cli, from user
3038
+ #
3039
+ @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break =
3040
+ HashDelegator.next_link_state(
3041
+ block_name: @dml_link_state.block_name,
3042
+ block_name_from_cli: @dml_now_using_cli,
3043
+ block_state: @dml_block_state,
3044
+ was_using_cli: @dml_now_using_cli
3045
+ )
3046
+
3047
+ # &bsp '!block_name_from_ui + cli_break -> break'
3048
+ !@dml_block_state.source.block_name_from_ui && cli_break && :break
3049
+ end
3050
+
3051
+ def vux_execute_block_per_type(block_name, formatted_choice_ostructs)
3052
+ case block_name
3053
+ when formatted_choice_ostructs[:back].pub_name
3054
+ debounce_reset
3055
+ vux_navigate_back_for_ls
3056
+
3057
+ when formatted_choice_ostructs[:edit].pub_name
3058
+ debounce_reset
3059
+ vux_edit_inherited
3060
+ return :break if pause_user_exit
3061
+
3062
+ InputSequencer.next_link_state(prior_block_was_link: true)
3063
+
3064
+ when formatted_choice_ostructs[:history].pub_name
3065
+ debounce_reset
3066
+ files_table_rows = read_saved_assets_for_history_table
3067
+ return :break unless files_table_rows
3068
+
3069
+ execute_history_select(files_table_rows, stream: $stderr)
3070
+ return :break if pause_user_exit
3071
+
3072
+ InputSequencer.next_link_state(prior_block_was_link: true)
3073
+
3074
+ when formatted_choice_ostructs[:load].pub_name
3075
+ debounce_reset
3076
+ vux_load_inherited
3077
+ return :break if pause_user_exit
3078
+
3079
+ InputSequencer.next_link_state(prior_block_was_link: true)
3080
+
3081
+ when formatted_choice_ostructs[:save].pub_name
3082
+ debounce_reset
3083
+ return :break if execute_inherited_save == :break
3084
+
3085
+ InputSequencer.next_link_state(prior_block_was_link: true)
3086
+
3087
+ when formatted_choice_ostructs[:shell].pub_name
3088
+ debounce_reset
3089
+ vux_input_and_execute_shell_commands(stream: $stderr)
3090
+ return :break if pause_user_exit
3091
+
3092
+ InputSequencer.next_link_state(prior_block_was_link: true)
3093
+
3094
+ when formatted_choice_ostructs[:view].pub_name
3095
+ debounce_reset
3096
+ vux_view_inherited(stream: $stderr)
3097
+ return :break if pause_user_exit
3098
+
3099
+ InputSequencer.next_link_state(prior_block_was_link: true)
3100
+
3101
+ else
3102
+ return :break if vux_execute_and_prompt(block_name) == :break
3103
+
3104
+ InputSequencer.next_link_state(
3105
+ block_name: @dml_link_state.block_name,
3106
+ prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH
3107
+ )
3108
+ end
3109
+ end
3110
+
3111
+ def vux_formatted_names_for_state_chrome_blocks(
3112
+ names: %w[back edit history load save shell view]
3113
+ )
3114
+ names.each_with_object({}) do |name, result|
3115
+ do_key = :"menu_option_#{name}_name"
3116
+ oname = HashDelegator.safeval(@delegate_object[do_key])
3117
+ dname = format(@delegate_object[:menu_link_format], oname)
3118
+ result[name.to_sym] = OpenStruct.new(
3119
+ dname: dname,
3120
+ name: dname,
3121
+ oname: dname,
3122
+ pub_name: dname.pub_name
3123
+ )
3124
+ end
3125
+ end
3126
+
3127
+ def vux_init
3128
+ @menu_base_options = @delegate_object
3129
+ @dml_link_state = LinkState.new(
3130
+ block_name: @delegate_object[:block_name],
3131
+ document_filename: @delegate_object[:filename]
3132
+ )
3133
+ @run_state.source.block_name_from_cli = @dml_link_state.block_name.present?
3134
+ @cli_block_name = @dml_link_state.block_name
3135
+ @dml_now_using_cli = @run_state.source.block_name_from_cli
3136
+ @dml_menu_default_dname = nil
3137
+ @dml_block_state = SelectedBlockMenuState.new
3138
+ @doc_saved_lines_files = []
3139
+
3140
+ @run_state.batch_random = Random.new.rand
3141
+ @run_state.batch_index = 0
3142
+
3143
+ @run_state.files = StreamsOut.new
3144
+ end
3145
+
3146
+ def vux_input_and_execute_shell_commands(stream:)
3147
+ loop do
3148
+ command = prompt_for_command(AnsiString.new(":MDE #{Time.now.strftime('%FT%TZ')}> ").send(:bgreen))
3149
+ break if !command.present? || command == 'exit'
3150
+
3151
+ exit_status = execute_command_with_streams(
3152
+ [@delegate_object[:shell], '-c', command]
3153
+ )
3154
+ case exit_status
3155
+ when 0
3156
+ stream.puts "#{'OK'.green} #{exit_status}"
3157
+ else
3158
+ stream.puts "#{'ERR'.bred} #{exit_status}"
3159
+ end
3160
+ end
3161
+ end
3162
+
3163
+ ## load file with code lines per options
3164
+ #
3165
+ def vux_load_code_files_into_state
3166
+ return unless @menu_base_options[:load_code].present?
3167
+
3168
+ @dml_link_state.inherited_lines =
3169
+ @menu_base_options[:load_code].split(':').map do |path|
3170
+ File.readlines(path, chomp: true)
3171
+ end.flatten(1)
3172
+
3173
+ inherited_block_names = []
3174
+ inherited_dependencies = {}
3175
+ selected = FCB.new(oname: 'load_code')
3176
+ pop_add_current_code_to_head_and_trigger_load(
3177
+ @dml_link_state, inherited_block_names,
3178
+ code_lines, inherited_dependencies, selected
3179
+ )
3180
+ end
3181
+
3182
+ def vux_load_inherited
3183
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
3184
+ @delegate_object[:document_saved_lines_glob])
3185
+ load_filespec = load_filespec_from_expression(sf)
3186
+ return unless load_filespec
3187
+
3188
+ @dml_link_state.inherited_lines_append(
3189
+ File.readlines(load_filespec, chomp: true)
3190
+ )
3191
+ end
3192
+
3193
+ # Select and execute a code block from a Markdown document.
3194
+ #
3195
+ # This method allows the user to interactively select a code block from a
3196
+ # Markdown document, obtain approval, and execute the chosen block of code.
3197
+ #
3198
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
3199
+ def vux_main_loop
3200
+ vux_init
3201
+ vux_load_code_files_into_state
3202
+ formatted_choice_ostructs = vux_formatted_names_for_state_chrome_blocks
3203
+
3204
+ block_list = [@delegate_object[:block_name]].select(&:present?).compact + @delegate_object[:input_cli_rest]
3205
+ @delegate_object[:block_name] = nil
3206
+
3207
+ process_commands(
3208
+ arguments: @p_all_arguments,
3209
+ named_procs: yield(:command_names, @delegate_object),
3210
+ options_parsed: @p_options_parsed,
3211
+ rest: @p_rest,
3212
+ enable_search: @delegate_object[:default_find_select_open]
3213
+ ) do |type, data|
3214
+ case type
3215
+ when ArgPro::ActSetBlockName
3216
+ @delegate_object[:block_name] = data
3217
+ @delegate_object[:input_cli_rest] = ''
3218
+ when ArgPro::ConvertValue
3219
+ # call for side effects, output, or exit
3220
+ data[0].call(data[1])
3221
+ when ArgPro::ActFileIsMissing
3222
+ raise FileMissingError, data, caller
3223
+ when ArgPro::ActFind
3224
+ find_value(data, execute_chosen_found: true)
3225
+ when ArgPro::ActSetFileName
3226
+ @delegate_object[:filename] = data
3227
+ when ArgPro::ActSetPath
3228
+ @delegate_object[:path] = data
3229
+ when ArgPro::CallProcess
3230
+ yield :call_proc, [@delegate_object, data]
3231
+ when ArgPro::ActSetOption
3232
+ @delegate_object[data[0]] = data[1]
3233
+ else
3234
+ raise
3235
+ end
3236
+ end
3237
+
3238
+ InputSequencer.new(
3239
+ @delegate_object[:filename],
3240
+ block_list
3241
+ ).run do |msg, data|
3242
+ # &bt msg
3243
+ case msg
3244
+ when :parse_document # once for each menu
3245
+ vux_parse_document
3246
+ vux_menu_append_history_files(formatted_choice_ostructs)
3247
+ vux_publish_document_file_name_for_external_automation
3248
+
3249
+ when :display_menu
3250
+ vux_clear_menu_state
3251
+
3252
+ when :user_choice
3253
+ vux_user_selected_block_name
3254
+
3255
+ when :execute_block
3256
+ ret = vux_execute_block_per_type(data, formatted_choice_ostructs)
3257
+ vux_publish_block_name_for_external_automation(data)
3258
+ ret
3259
+
3260
+ when :close_ux
3261
+ if @vux_pipe_open.present? && File.exist?(@vux_pipe_open)
3262
+ @vux_pipe_open.close
3263
+ @vux_pipe_open = nil
3264
+ end
3265
+ if @vux_pipe_created.present? && File.exist?(@vux_pipe_created)
3266
+ File.delete(@vux_pipe_created)
3267
+ @vux_pipe_created = nil
3268
+ end
3269
+
3270
+ when :exit?
3271
+ data == $texit
3272
+
3273
+ when :stay?
3274
+ data == $stay
3275
+
3276
+ else
3277
+ raise "Invalid message: #{msg}"
3278
+
3279
+ end
3280
+ end
3281
+ end
3282
+
3283
+ def vux_menu_append_history_files(formatted_choice_ostructs)
3284
+ if @delegate_object[:menu_for_history]
3285
+ history_files(@dml_link_state).tap do |files|
3286
+ if files.count.positive?
3287
+ dml_menu_append_chrome_item(
3288
+ formatted_choice_ostructs[:history].oname, files.count,
3289
+ 'files', menu_state: MenuState::HISTORY
3290
+ )
3291
+ end
3292
+ end
3293
+ end
3294
+
3295
+ return unless @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
3296
+
3297
+ sf = document_name_in_glob_as_file_name(
3298
+ @dml_link_state.document_filename,
3299
+ @delegate_object[:document_saved_lines_glob]
3300
+ )
3301
+ files = sf ? Dir.glob(sf) : []
3302
+ @doc_saved_lines_files = files.count.positive? ? files : []
3303
+
3304
+ lines_count = @dml_link_state.inherited_lines_count
3305
+
3306
+ # add menu items (glob, load, save) and enable selectively
3307
+ if files.count.positive? || lines_count.positive?
3308
+ menu_add_disabled_option(sf)
3309
+ end
3310
+ if files.count.positive?
3311
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:load].dname, files.count, 'files',
3312
+ menu_state: MenuState::LOAD)
3313
+ end
3314
+ if @delegate_object[:menu_inherited_lines_edit_always] || lines_count.positive?
3315
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:edit].dname, lines_count, 'lines',
3316
+ menu_state: MenuState::EDIT)
3317
+ end
3318
+ if lines_count.positive?
3319
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:save].dname, 1, '',
3320
+ menu_state: MenuState::SAVE)
3321
+ end
3322
+ if lines_count.positive?
3323
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:view].dname, 1, '',
3324
+ menu_state: MenuState::VIEW)
3325
+ end
3326
+ # rubocop:disable Style/GuardClause
3327
+ if @delegate_object[:menu_with_shell]
3328
+ dml_menu_append_chrome_item(formatted_choice_ostructs[:shell].dname, 1, '',
3329
+ menu_state: MenuState::SHELL)
3330
+ end
3331
+ # rubocop:enable Style/GuardClause
3332
+
3333
+ # # reflect new menu items
3334
+ # @dml_mdoc = MDoc.new(@dml_menu_blocks)
3335
+ end
3336
+
3337
+ def vux_navigate_back_for_ls
3338
+ InputSequencer.merge_link_state(
3339
+ @dml_link_state,
3340
+ InputSequencer.next_link_state(
3341
+ **execute_navigate_back.merge(prior_block_was_link: true)
3342
+ )
3343
+ )
3344
+ end
3345
+
3346
+ def vux_parse_document
3347
+ @run_state.batch_index += 1
3348
+ @run_state.in_own_window = false
3349
+
3350
+ @run_state.source.block_name_from_cli, @dml_now_using_cli =
3351
+ manage_cli_selection_state(
3352
+ block_name_from_cli: @run_state.source.block_name_from_cli,
3353
+ now_using_cli: @dml_now_using_cli,
3354
+ link_state: @dml_link_state
3355
+ )
3356
+
3357
+ @delegate_object[:filename] = @dml_link_state.document_filename
3358
+ @dml_link_state.block_name = @delegate_object[:block_name] =
3359
+ @run_state.source.block_name_from_cli ?
3360
+ @cli_block_name :
3361
+ @dml_link_state.block_name
3362
+
3363
+ # update @delegate_object and @menu_base_options in auto_load
3364
+ #
3365
+ @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
3366
+ mdoc_menu_and_blocks_from_nested_files(@dml_link_state)
3367
+ dump_delobj(@dml_blocks_in_file, @dml_menu_blocks, @dml_link_state)
3368
+ # &bsp 'loop', @run_state.source.block_name_from_cli, @cli_block_name
3369
+ end
3370
+
3371
+ def publish_for_external_automation(message:)
3372
+ return if @delegate_object[:publish_document_file_name].empty?
3373
+
3374
+ pipe_path = absolute_path(@delegate_object[:publish_document_file_name])
3375
+
3376
+ case @delegate_object[:publish_document_file_mode]
3377
+ when 'append'
3378
+ File.write(pipe_path, message + "\n", mode: 'a')
3379
+ when 'fifo'
3380
+ unless @vux_pipe_open
3381
+ unless File.exist?(pipe_path)
3382
+ FileUtils.mkfifo(pipe_path)
3383
+ @vux_pipe_created = pipe_path
3384
+ end
3385
+ @vux_pipe_open = File.open(pipe_path, 'w')
3386
+ end
3387
+ @vux_pipe_open.puts(message + "\n")
3388
+ @vux_pipe_open.flush
3389
+ when 'write'
3390
+ File.write(pipe_path, message)
3391
+ else
3392
+ raise 'Invalid publish_document_file_mode:' \
3393
+ " #{@delegate_object[:publish_document_file_mode]}"
3394
+ end
3395
+ end
3396
+
3397
+ def vux_publish_block_name_for_external_automation(block_name)
3398
+ publish_for_external_automation(
3399
+ message: format(
3400
+ @delegate_object[:publish_block_name_format],
3401
+ { block: block_name,
3402
+ document: @delegate_object[:filename],
3403
+ time: Time.now.utc.strftime(
3404
+ @delegate_object[:publish_time_format]
3405
+ ) }
3406
+ )
3407
+ )
3408
+ end
3409
+
3410
+ def vux_publish_document_file_name_for_external_automation
3411
+ return unless @delegate_object[:publish_document_file_name].present?
3412
+
3413
+ publish_for_external_automation(
3414
+ message: format(
3415
+ @delegate_object[:publish_document_name_format],
3416
+ { document: @delegate_object[:filename],
3417
+ time: Time.now.utc.strftime(
3418
+ @delegate_object[:publish_time_format]
3419
+ ) }
3420
+ )
3421
+ )
3422
+ end
3423
+
3424
+ # return :break to break from loop
3425
+ def vux_user_selected_block_name
3426
+ if @dml_link_state.block_name.present?
3427
+ # @prior_block_was_link = true
3428
+ @dml_block_state.block = blocks_find_by_block_name(@dml_blocks_in_file,
3429
+ @dml_link_state.block_name)
3430
+ @dml_link_state.block_name = nil
3431
+ else
3432
+ # puts "? - Select a block to execute (or type #{$texit} to exit):"
3433
+ return :break if vux_await_user_selection == :break # into @dml_block_state
3434
+ return :break if @dml_block_state.block.nil? # no block matched
3435
+ end
3436
+ # puts "! - Executing block: #{data}"
3437
+ @dml_block_state.block&.pub_name
3438
+ end
3439
+
3440
+ def vux_view_inherited(stream:)
3441
+ stream.puts @dml_link_state.inherited_lines_block
3442
+ end
3443
+
3156
3444
  def wait_for_stream_processing
3157
3445
  @process_mutex.synchronize do
3158
3446
  @process_cv.wait(@process_mutex)
@@ -3165,11 +3453,13 @@ module MarkdownExec
3165
3453
  block_state = wait_for_user_selection(all_blocks, menu_blocks, default)
3166
3454
  handle_back_or_continue(block_state)
3167
3455
  block_state
3168
- rescue StandardError
3169
- HashDelegator.error_handler('wait_for_user_selected_block')
3170
3456
  end
3171
3457
 
3172
3458
  def wait_for_user_selection(_all_blocks, menu_blocks, default)
3459
+ if @delegate_object[:clear_screen_for_select_block]
3460
+ printf("\e[1;1H\e[2J")
3461
+ end
3462
+
3173
3463
  prompt_title = string_send_color(
3174
3464
  @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution
3175
3465
  )
@@ -3823,31 +4113,31 @@ module MarkdownExec
3823
4113
  @hd.instance_variable_set(:@run_state, mock('run_state'))
3824
4114
  end
3825
4115
 
3826
- def test_format_execution_stream_with_valid_key
3827
- result = HashDelegator.format_execution_stream(
3828
- { stdout: %w[output1 output2] },
3829
- ExecutionStreams::STD_OUT
3830
- )
4116
+ # def test_format_execution_stream_with_valid_key
4117
+ # result = HashDelegator.format_execution_stream(
4118
+ # { stdout: %w[output1 output2] },
4119
+ # ExecutionStreams::STD_OUT
4120
+ # )
3831
4121
 
3832
- assert_equal "output1\noutput2", result
3833
- end
4122
+ # assert_equal "output1\noutput2", result
4123
+ # end
3834
4124
 
3835
- def test_format_execution_stream_with_empty_key
3836
- @hd.instance_variable_get(:@run_state).stubs(:files).returns({})
4125
+ # def test_format_execution_stream_with_empty_key
4126
+ # @hd.instance_variable_get(:@run_state).stubs(:files).returns({})
3837
4127
 
3838
- result = HashDelegator.format_execution_stream(nil,
3839
- ExecutionStreams::STD_ERR)
4128
+ # result = HashDelegator.format_execution_stream(nil,
4129
+ # ExecutionStreams::STD_ERR)
3840
4130
 
3841
- assert_equal '', result
3842
- end
4131
+ # assert_equal '', result
4132
+ # end
3843
4133
 
3844
- def test_format_execution_stream_with_nil_files
3845
- @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil)
4134
+ # def test_format_execution_stream_with_nil_files
4135
+ # @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil)
3846
4136
 
3847
- result = HashDelegator.format_execution_stream(nil, :stdin)
4137
+ # result = HashDelegator.format_execution_stream(nil, :stdin)
3848
4138
 
3849
- assert_equal '', result
3850
- end
4139
+ # assert_equal '', result
4140
+ # end
3851
4141
  end
3852
4142
 
3853
4143
  class TestHashDelegatorHandleBackLink < Minitest::Test
@@ -3856,16 +4146,14 @@ module MarkdownExec
3856
4146
  @hd.stubs(:history_state_pop)
3857
4147
  end
3858
4148
 
3859
- def test_pop_link_history_and_trigger_load
4149
+ def test_pop_link_history_new_state
3860
4150
  # Verifying that history_state_pop is called
3861
4151
  # @hd.expects(:history_state_pop).once
3862
4152
 
3863
- result = @hd.pop_link_history_and_trigger_load
4153
+ result = @hd.pop_link_history_new_state
3864
4154
 
3865
- # Asserting the result is an instance of LoadFileLinkState
3866
- assert_instance_of LoadFileLinkState, result
3867
- assert_equal LoadFile::LOAD, result.load_file
3868
- assert_nil result.link_state.block_name
4155
+ # Asserting the result is an instance of LinkState
4156
+ assert_nil result.block_name
3869
4157
  end
3870
4158
  end
3871
4159
 
@@ -3952,7 +4240,7 @@ module MarkdownExec
3952
4240
  def setup
3953
4241
  @hd = HashDelegator.new
3954
4242
  @hd.instance_variable_set(:@run_state,
3955
- OpenStruct.new(files: { stdout: [] }))
4243
+ OpenStruct.new(files: StreamsOut.new))
3956
4244
  @hd.instance_variable_set(:@delegate_object,
3957
4245
  { output_stdout: true })
3958
4246
  end
@@ -3964,9 +4252,8 @@ module MarkdownExec
3964
4252
  Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
3965
4253
 
3966
4254
  @hd.wait_for_stream_processing
3967
-
3968
4255
  assert_equal ['line 1', 'line 2'],
3969
- @hd.instance_variable_get(:@run_state).files[ExecutionStreams::STD_OUT]
4256
+ @hd.instance_variable_get(:@run_state).files.stream_lines(ExecutionStreams::STD_OUT)
3970
4257
  end
3971
4258
 
3972
4259
  def test_handle_stream_with_io_error
@@ -3979,7 +4266,7 @@ module MarkdownExec
3979
4266
  @hd.wait_for_stream_processing
3980
4267
 
3981
4268
  assert_equal [],
3982
- @hd.instance_variable_get(:@run_state).files[ExecutionStreams::STD_OUT]
4269
+ @hd.instance_variable_get(:@run_state).files.stream_lines(ExecutionStreams::STD_OUT)
3983
4270
  end
3984
4271
  end
3985
4272
 
@@ -3997,9 +4284,9 @@ module MarkdownExec
3997
4284
  def test_iter_blocks_from_nested_files
3998
4285
  @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'],
3999
4286
  import_paths: nil)
4000
- selected_messages = ['filtered message']
4287
+ selected_types = ['filtered message']
4001
4288
 
4002
- result = @hd.iter_blocks_from_nested_files { selected_messages }
4289
+ result = @hd.iter_blocks_from_nested_files { selected_types }
4003
4290
  assert_equal ['line 1', 'line 2'], result
4004
4291
 
4005
4292
  @hd.cfile.verify
@@ -4024,11 +4311,11 @@ module MarkdownExec
4024
4311
  })
4025
4312
  @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('-- Back --')
4026
4313
  @hd.stubs(:string_send_color).with('-- Back --',
4027
- :menu_chrome_color).returns('-- Back --'.red)
4314
+ :menu_chrome_color).returns(AnsiString.new('-- Back --').red)
4028
4315
  end
4029
4316
 
4030
4317
  def test_menu_chrome_colored_option_with_color
4031
- assert_equal '-- Back --'.red,
4318
+ assert_equal AnsiString.new('-- Back --').red,
4032
4319
  @hd.menu_chrome_colored_option(:menu_option_back_name)
4033
4320
  end
4034
4321
 
@@ -4092,10 +4379,11 @@ module MarkdownExec
4092
4379
  end
4093
4380
 
4094
4381
  def test_string_send_color
4095
- assert_equal 'Hello'.red, @hd.string_send_color('Hello', :red)
4096
- assert_equal 'World'.green,
4382
+ assert_equal AnsiString.new('Hello').red,
4383
+ @hd.string_send_color('Hello', :red)
4384
+ assert_equal AnsiString.new('World').green,
4097
4385
  @hd.string_send_color('World', :green)
4098
- assert_equal 'Default'.plain,
4386
+ assert_equal AnsiString.new('Default').plain,
4099
4387
  @hd.string_send_color('Default', :blue)
4100
4388
  end
4101
4389
  end
@@ -4285,10 +4573,7 @@ module MarkdownExec
4285
4573
 
4286
4574
  def test_prompt_for_filespec_with_interruption
4287
4575
  $stdin = StringIO.new
4288
- # rubocop disable:Lint/NestedMethodDefinition
4289
4576
  def $stdin.gets; raise Interrupt; end
4290
- # rubocop enable:Lint/NestedMethodDefinition
4291
-
4292
4577
  result = prompt_for_filespec_with_wildcard('*.txt')
4293
4578
  assert_nil result
4294
4579
  end