markdown_exec 2.3.0 → 2.4.0

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.
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