vtparser 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b142e3d52113ff288e56ef70f3a948e71233e3dbf6dd52078c168b205c0f5c69
4
- data.tar.gz: 5a76607662cc257ad2b963d2ea8d52d86c3f7e1e5176e9b289966c611d649cb0
3
+ metadata.gz: cbb3fedd9971980bdb4389680f7ad7f52a82ec573b9bdecc3a30ecd1fb22f102
4
+ data.tar.gz: 5f12efd5ffe0f23656dcd2520f835b64815a423f9cbb7353b98eac372b0a2303
5
5
  SHA512:
6
- metadata.gz: e2861798a37ed651916d3bdd2c5a8c5e990ffc66a92c1e2b006bcfd75fe80e38288af7ca1118681a805573d2b9fb87f01bd1842f6e5232d7e599abc61d0c7255
7
- data.tar.gz: 2f1b3fa73f48e839c818c0d433d2d42edc9caa1a9afff6eb4ea6e8e13fd0453405cf364a404b9605346e47a8a625f3c7bd38babe4c9434b629714aca8d331e9b
6
+ metadata.gz: 60c450670c31b4fee417acdefab3f6ed5287351929a6afcbdda46d68d4ba72e5db76731184136d014b9c656b026649fbed0fe5811740532b5402ce7961946fc2
7
+ data.tar.gz: 2d7ec6bdb03a5ef18df04e461cc199a9fe60d4b838d39f772db14d9aec70b95cbc90f5fc2b2cf42314a9a91d0aff70e5ca48f7185de754ead3e70061acdb501d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2024-10-05
4
+
5
+ - Added more examples `colorswap.rb`, `analyze.rb`, `roundtrip.rb`
6
+ - Renamed KeyboardEvent to KeyEvent
7
+ - Added support for BEL instead of ESC to terminate OSC sequences (xterm uses this)
8
+ - Encapsulate action in class Action and added instance methods
9
+ - Fixed serialization issue for private mode CSI commands
10
+ - Added PtySupport::spawn to easily spawn a command and link stdin/out to the parser
11
+ - Switch PTY to `raw` mode to capture keys, disable echo.
12
+ - Added support for piping output to files from examples.
13
+
3
14
  ## [0.2.0] - 2024-10-03
4
15
 
5
16
  - Add keyevent handling
data/README.md CHANGED
@@ -32,10 +32,10 @@ See the minimal example below:
32
32
  require_relative '../lib/vtparser'
33
33
 
34
34
  # Instantiate the parser with a block to handle actions
35
- parser = VTParser.new do |action, ch, intermediate_chars, params|
35
+ parser = VTParser.new do |action|
36
36
 
37
37
  # For this minimal example, we'll just turn everything back strings to print
38
- print VTParser::to_ansi(action, ch, intermediate_chars, params)
38
+ print action.to_ansi
39
39
 
40
40
  end
41
41
 
@@ -50,6 +50,9 @@ Further samples in the [`examples directory`](https://github.com/coezbek/vtparse
50
50
 
51
51
  - [`echo_keys.rb`](https://github.com/coezbek/vtparser/tree/main/examples/echo_keys.rb): Echoes the keys pressed by the user
52
52
  - [`indent_cli.rb`](https://github.com/coezbek/vtparser/tree/main/examples/indent_cli.rb): Indents the output of simple command line tools
53
+ - [`colorswap.rb`](https://github.com/coezbek/vtparser/tree/main/examples/colorswap.rb): Swaps the colors red / green in the input program
54
+ - [`analyze.rb`](https://github.com/coezbek/vtparser/tree/main/examples/analyze.rb): Output all VT100 escape sequences written by the subprocess.
55
+ - [`roundtrip.rb`](https://github.com/coezbek/vtparser/tree/main/examples/roundtrip.rb): Runs the given command and compares characters written by the command to the output of running the characters through the parser and serializing the actions back `to_ansi`. If the parser works correctly the output should be the same.
53
56
 
54
57
  ## Limitations
55
58
 
@@ -59,6 +62,10 @@ Further samples in the [`examples directory`](https://github.com/coezbek/vtparse
59
62
 
60
63
  - The parser only outputs full `actions`. So triggering an event for the `ESC` key doesn't work (as expected).
61
64
 
65
+ - The parser does not emit actions for commands which are 'interrupted' by another command."
66
+
67
+ - The parser does not explain any of the actions.
68
+
62
69
  ## Development
63
70
 
64
71
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,21 @@
1
+ require_relative '../lib/vtparser'
2
+ require 'pty'
3
+ require 'tty-prompt' # for winsize call below
4
+
5
+ #
6
+ # This example demonstrates how to use the VTParser to analyze the VT100 escape sequences outputted by a program.
7
+ #
8
+
9
+ # Get the command from ARGV
10
+ command = ARGV.join(' ')
11
+ if command.empty?
12
+ puts "Usage: ruby indent_cli.rb '<command>'"
13
+ exit 1
14
+ end
15
+
16
+ # Instantiate the parser with a block to handle actions
17
+ parser = VTParser.new do |action|
18
+ puts action.inspect + "\r\n"
19
+ end
20
+
21
+ parser.spawn(command)
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # This script outputs alternating red and green text to demonstrate the color-swapping functionality.
5
+ #
6
+
7
+ # ANSI escape sequences for colors
8
+ COLOR_RED = "\e[31m"
9
+ COLOR_GREEN = "\e[32m"
10
+ RESET_COLOR = "\e[0m"
11
+
12
+ # Infinite loop to print red and green text alternately
13
+ loop do
14
+ print "\r#{COLOR_RED}This is red text#{RESET_COLOR} "
15
+ sleep 1
16
+ print "\r#{COLOR_GREEN}This is green text#{RESET_COLOR}"
17
+ sleep 1
18
+ end
@@ -0,0 +1,40 @@
1
+ require 'pty'
2
+ require 'rainbow/refinement' # for colorizing output
3
+ using Rainbow
4
+ require_relative '../lib/vtparser'
5
+
6
+ #
7
+ # 'swap_colors_cli.rb' - Example for vtparser
8
+ #
9
+ # This example demonstrates how to use VTParser to swap colors (red to green, green to red) of a simple tty program.
10
+ #
11
+ # Run with `ruby swap_colors_cli.rb <command>`, where <command> is the command you want to run.
12
+ #
13
+
14
+ # Get the command from ARGV
15
+ command = ARGV.join(' ')
16
+ if command.empty?
17
+ puts "Usage: ruby swap_colors_cli.rb '<command>'"
18
+ exit 1
19
+ end
20
+
21
+ # VT100 color codes for red and green
22
+ COLOR_RED = "\e[31m"
23
+ COLOR_GREEN = "\e[32m"
24
+ RESET_COLOR = "\e[0m"
25
+
26
+ # VTParser block to swap red and green colors in the output
27
+ parser = VTParser.new do |action, ch, intermediate_chars, params|
28
+ to_output = VTParser::to_ansi(action, ch, intermediate_chars, params)
29
+
30
+ case to_output
31
+ when COLOR_RED
32
+ print COLOR_GREEN # Swap red to green
33
+ when COLOR_GREEN
34
+ print COLOR_RED # Swap green to red
35
+ else
36
+ print to_output # Default behavior for other output
37
+ end
38
+ end
39
+
40
+ parser.spawn(command)
@@ -31,19 +31,25 @@ line_indent_length = 6
31
31
  # Use VTParser to process the VT100 escape sequences outputted by nested program and prepend the line_indent text.
32
32
  #
33
33
  first_line = true
34
- parser = VTParser.new do |action, ch, intermediate_chars, params|
34
+ parser = VTParser.new do |action|
35
+
36
+ ch = action.ch
37
+ intermediate_chars = action.intermediate_chars
38
+ params = action.params
39
+ action_type = action.action_type
40
+ to_output = action.to_ansi
41
+
35
42
  print line_indent if first_line
36
43
  first_line = false
37
44
 
38
- if $DEBUG && (action != :print || !(ch =~ /\P{Cc}/))
39
- puts "action: #{action}, ch: #{ch.inspect}, ch0x: 0x#{ "%02x" % ch.ord}, intermediate_chars: #{intermediate_chars}, params: #{params}"
45
+ if $DEBUG && (action_type != :print || !(ch =~ /\P{Cc}/))
46
+ puts action.inspect
40
47
  end
41
- to_output = VTParser::to_ansi(action, ch, intermediate_chars, params)
42
48
 
43
49
  # Handle newlines, carriage returns, and cursor movement
44
- case action
50
+ case action_type
45
51
  when :print, :execute, :put, :osc_put
46
- if ch == "\n" || ch == "\r"
52
+ if ch == "\r" # || ch == "\n"
47
53
  print ch
48
54
  print line_indent
49
55
  next
@@ -61,34 +67,61 @@ parser = VTParser.new do |action, ch, intermediate_chars, params|
61
67
  end
62
68
  end
63
69
 
70
+ if $DEBUG && (action_type != :print || !(ch =~ /\P{Cc}/))
71
+ puts "\r\n"
72
+ puts action.inspect
73
+ puts "\r\n"
74
+ # sleep 5
75
+ end
76
+
64
77
  print to_output
65
78
  end
66
79
 
67
- #
68
- # Spawn the given command using PTY::spawn, and connect pipes.
69
- #
70
80
  begin
71
81
  PTY.spawn(command) do |stdout_and_stderr, stdin, pid|
72
82
 
73
- # Start separate thread to pipe stdin to the child process
74
- Thread.new do
75
- while pid != nil
76
- stdin.write(STDIN.readpartial(1024)) # Requires user to press enter!
83
+ # Input Thread
84
+ input_thread = Thread.new do
85
+
86
+ STDIN.raw do |io|
87
+ loop do
88
+ break if pid.nil?
89
+ begin
90
+ if io.wait_readable(0.1)
91
+ data = io.read_nonblock(1024)
92
+ stdin.write data
93
+ end
94
+ rescue IO::WaitReadable
95
+ # No input available right now
96
+ rescue EOFError
97
+ break
98
+ rescue Errno::EIO
99
+ break
100
+ end
101
+ end
77
102
  end
78
- rescue => e
79
- puts "Error: #{e}"
80
- exit(0)
81
103
  end
82
104
 
83
105
  # Pipe stdout and stderr to the parser
84
106
  begin
107
+
108
+ begin
109
+ winsize = $stdout.winsize
110
+ rescue Errno::ENOTTY
111
+ winsize = [0, 120] # Default to 120 columns
112
+ end
85
113
  # Ensure the child process has the proper window size, because
86
114
  # - tools such as yarn use it to identify tty mode
87
115
  # - some tools use it to determine the width of the terminal for formatting
88
- stdout_and_stderr.winsize = [$stdout.winsize.first, $stdout.winsize.last - line_indent_length]
89
-
116
+ stdout_and_stderr.winsize = [winsize.first, winsize.last - line_indent_length]
117
+
90
118
  stdout_and_stderr.each_char do |char|
91
119
 
120
+ char = block.call(char) if block_given?
121
+ next if char.nil?
122
+
123
+ # puts Action.inspect_char(char) + "\r\n"
124
+ # Pass to parser
92
125
  parser.parse(char)
93
126
 
94
127
  end
@@ -99,13 +132,12 @@ begin
99
132
  # Wait for the child process to exit
100
133
  Process.wait(pid)
101
134
  pid = nil
102
- exit_status = $?.exitstatus
103
- # result = exit_status == 0
104
-
105
- # Clear the line, reset the cursor to the start of the line
106
- print "\e[2K\e[1G"
135
+ input_thread.join
107
136
  end
108
137
 
109
138
  rescue PTY::ChildExited => e
110
139
  puts "The child process exited: #{e}"
111
- end
140
+ end
141
+
142
+ # Clear and reset the cursor to the start of the line
143
+ puts "\e[2K\e[1G"
data/examples/progress.rb CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  require 'ruby-progressbar'
8
8
 
9
+ # Hide the cursor
10
+ print "\e[?25l"
11
+
9
12
  loop do
10
13
  progressbar = ProgressBar.create
11
14
  99.times {
@@ -0,0 +1,83 @@
1
+ require_relative '../lib/vtparser'
2
+ require 'pty'
3
+ require 'tty-prompt' # for winsize call below
4
+
5
+ COLOR_GREEN = "\e[32m"
6
+ RESET_COLOR = "\e[0m"
7
+
8
+ #
9
+ # This example checks if the output of VTParser is identical to the data fed into parser.
10
+ #
11
+ # Examples you can try:
12
+ #
13
+ # ruby roundtrip.rb 'vim' # Exit with :q
14
+ # ruby roundtrip.rb 'ls -la'
15
+ # ruby roundtrip.rb 'less' # Exit with q
16
+ # ruby roundtrip.rb 'top'
17
+ # Screensavers:
18
+ # ruby roundtrip.rb 'cmatrix' # sudo apt-get install cmatrix
19
+ # ruby roundtrip.rb 'neo' # install from https://github.com/st3w/neo
20
+ # Note: that upon termination a mismatch will be detected, because neo will cancel an on-going CSI sequence by sending an ESC character.
21
+ #
22
+
23
+ # Get the command from ARGV
24
+ command = ARGV.join(' ')
25
+ if command.empty?
26
+ puts "Usage: ruby indent_cli.rb '<command>'"
27
+ exit 1
28
+ end
29
+
30
+ captured = []
31
+ previous_actions = []
32
+ previous_characters = []
33
+
34
+ # Instantiate the parser with a block to handle actions
35
+ parser = VTParser.new do |action|
36
+
37
+ from_parser = action.to_ansi
38
+ from_parser.each_char.with_index do |char, i|
39
+ if captured.empty?
40
+ puts "ERROR: Parser has extra character after parsing: #{char.inspect}\r\n"
41
+ exit(1)
42
+ end
43
+ if char != captured.first
44
+ puts "\r\n"
45
+ puts "ERROR: Parser output does not match input with index #{i}: #{char.inspect} != #{captured.first.inspect}\r\n"
46
+ puts "Current captured characters: #{captured.inspect}\r\n"
47
+ puts "Current parser output: #{from_parser.inspect}\r\n"
48
+ puts "Previous characters: #{previous_characters.join.inspect}\r\n"
49
+
50
+ puts "\r\n"
51
+ previous_actions.each_with_index do |prev_action, i|
52
+ puts "Previous action -#{(previous_actions.length - i).to_s.rjust(2)}: #{prev_action.inspect}\r\n"
53
+ end
54
+ puts "Current action : #{action.inspect}\r\n"
55
+
56
+ exit(1)
57
+ end
58
+ matched_char = captured.shift
59
+ previous_characters << matched_char
60
+ if previous_characters.size > 20
61
+ previous_characters.shift
62
+ end
63
+
64
+ print "#{COLOR_GREEN}.#{RESET_COLOR}"
65
+ end
66
+
67
+ previous_actions << action
68
+ if previous_actions.size > 20
69
+ previous_actions.shift
70
+ end
71
+
72
+ end
73
+
74
+ parser.spawn(command) do |char|
75
+
76
+ captured << char
77
+
78
+ next char
79
+ end
80
+
81
+ puts
82
+ puts "#{COLOR_GREEN}SUCCESS: Parser output matches input.#{RESET_COLOR}"
83
+
@@ -99,7 +99,7 @@ module Keymap
99
99
  yield key if key
100
100
  #when :print, :ignore
101
101
  # Regular printable characters
102
- #yield KeyboardEvent.new(ch)
102
+ #yield KeyEvent.new(ch)
103
103
  when :esc_dispatch
104
104
  # ESC sequences without intermediates
105
105
  if intermediate_chars == ''
@@ -120,13 +120,13 @@ module Keymap
120
120
 
121
121
  def map_control_character(ch)
122
122
  if SINGLE_KEY_EVENT.key?(ch)
123
- return KeyboardEvent.new(SINGLE_KEY_EVENT[ch])
123
+ return KeyEvent.new(SINGLE_KEY_EVENT[ch])
124
124
  elsif ch.ord.between?(0x01, 0x1A)
125
125
  # Ctrl+A to Ctrl+Z
126
126
  key = (ch.ord + 96).chr
127
- return KeyboardEvent.new(key, :ctrl)
127
+ return KeyEvent.new(key, :ctrl)
128
128
  else
129
- return KeyboardEvent.new(ch)
129
+ return KeyEvent.new(ch)
130
130
  end
131
131
  end
132
132
 
@@ -134,14 +134,14 @@ module Keymap
134
134
  case final_char
135
135
  when 'Z'
136
136
  # Shift+Tab
137
- return KeyboardEvent.new(:tab, :shift)
137
+ return KeyEvent.new(:tab, :shift)
138
138
  when "\e"
139
139
  # Double ESC
140
- return KeyboardEvent.new(:esc)
140
+ return KeyEvent.new(:esc)
141
141
  else
142
142
  # Meta key (Alt) combinations
143
143
  if final_char.ord.between?(0x20, 0x7E)
144
- return KeyboardEvent.new(final_char, :meta)
144
+ return KeyEvent.new(final_char, :meta)
145
145
  else
146
146
  # Handle other ESC sequences if necessary
147
147
  end
@@ -168,7 +168,7 @@ module Keymap
168
168
  end
169
169
 
170
170
  if key
171
- return KeyboardEvent.new(key, *modifiers)
171
+ return KeyEvent.new(key, *modifiers)
172
172
  else
173
173
  # Handle unrecognized sequences
174
174
  end
@@ -1,14 +1,76 @@
1
1
  require_relative "keymap"
2
+ require_relative "pty_support"
3
+
4
+ class Action
5
+
6
+ attr_reader :action_type, :ch, :private_mode_intermediate_char, :intermediate_chars, :params
7
+
8
+ def initialize(action_type, ch, private_mode_intermediate_char, intermediate_chars, params)
9
+ @action_type = action_type
10
+ @ch = ch
11
+ @intermediate_chars = intermediate_chars
12
+ @private_mode_intermediate_char = private_mode_intermediate_char
13
+ @params = params
14
+ end
15
+
16
+ def to_s
17
+ to_ansi
18
+ end
19
+
20
+ def inspect
21
+ "ansi: #{to_ansi.inspect.ljust(16)} " +
22
+ "action: #{@action_type.to_s.ljust(12)} #{Action.inspect_char(@ch)} " +
23
+ "private mode: #{"'#{@private_mode_intermediate_char}'".ljust(3)} " +
24
+ "params: '#{@params.inspect.ljust(20)}' " +
25
+ "intermediate_chars: '#{@intermediate_chars}'"
26
+ end
27
+
28
+ def self.inspect_char(ch)
29
+ "ch: #{ch.inspect.ljust(4)} (ord=#{ch ? ch.ord.to_s.rjust(4) : " "}, hex=0x#{ch ? ("%02x" % ch.ord).rjust(6) : " "})"
30
+ end
31
+
32
+ def to_ansi
33
+
34
+ case @action_type
35
+ when :print, :execute, :put, :osc_put, :ignore
36
+ # Output the character
37
+ return @ch if @ch
38
+ when :hook
39
+ return "\eP#{@intermediate_chars}"
40
+ when :esc_dispatch
41
+ return "\e#{@intermediate_chars}#{@ch}"
42
+ when :csi_dispatch
43
+ # Output ESC [ followed by parameters, intermediates, and final character
44
+ return "\e[#{@private_mode_intermediate_char}#{@params.join(';')}#{@intermediate_chars}#{@ch}"
45
+ when :osc_start
46
+ return "\e]"
47
+ when :osc_end
48
+ return '' # "\x07" # BEL character to end OSC
49
+ when :unhook
50
+ return "" # \e must come from ESCAPE state
51
+ when :clear # Clear action is called when a command is interrupted by a new command (there is unfinished content in the parser)
52
+
53
+ else
54
+ raise "Unknown action type: #{@action_type}"
55
+ end
56
+
57
+ raise
58
+ end
59
+
60
+ end
61
+
2
62
 
3
63
  class VTParser
4
- attr_reader :intermediate_chars, :params
64
+ attr_reader :private_mode_intermediate_char, :intermediate_chars, :params
5
65
 
6
66
  include Keymap
67
+ include PtySupport
7
68
 
8
69
  def initialize(&block)
9
70
  @callback = block
10
71
  @state = :GROUND
11
72
  @intermediate_chars = ''
73
+ @private_mode_intermediate_char = ''
12
74
  @params = []
13
75
  @ignore_flagged = false
14
76
  initialize_states
@@ -68,7 +130,7 @@ class VTParser
68
130
  0x3a => :CSI_IGNORE,
69
131
  (0x30..0x39) => [:param, :CSI_PARAM],
70
132
  0x3b => [:param, :CSI_PARAM],
71
- (0x3c..0x3f) => [:collect, :CSI_PARAM],
133
+ (0x3c..0x3f) => [:private_mode_collect, :CSI_PARAM],
72
134
  (0x40..0x7e) => [:csi_dispatch, :GROUND],
73
135
  },
74
136
  :CSI_PARAM => {
@@ -149,7 +211,9 @@ class VTParser
149
211
  },
150
212
  :OSC_STRING => {
151
213
  :on_entry => :osc_start,
152
- (0x00..0x17) => :ignore,
214
+ (0x00..0x06) => :ignore,
215
+ (0x07) => [:osc_put, :GROUND], # BEL character for xterm compatibility
216
+ (0x08..0x17) => :ignore,
153
217
  0x19 => :ignore,
154
218
  (0x1c..0x1f) => :ignore,
155
219
  (0x20..0x7f) => :osc_put,
@@ -253,15 +317,15 @@ class VTParser
253
317
 
254
318
  def handle_action(action, ch)
255
319
  case action
256
- when :execute, :print, :esc_dispatch, :csi_dispatch, :hook, :put, :unhook, :osc_start, :osc_put, :osc_end
257
- @callback.call(action, ch, intermediate_chars, params) if @callback
258
- when :ignore
259
- # Do nothing
260
- @callback.call(action, ch, intermediate_chars, params) if @callback
320
+ when :private_mode_collect
321
+ raise "Private mode intermediate char already set" unless @private_mode_intermediate_char.empty?
322
+ @private_mode_intermediate_char = ch
323
+ return
261
324
  when :collect
262
325
  unless @ignore_flagged
263
326
  @intermediate_chars << ch
264
327
  end
328
+ return
265
329
  when :param
266
330
  if ch == ';'
267
331
  @params << 0
@@ -271,39 +335,25 @@ class VTParser
271
335
  end
272
336
  @params[-1] = @params[-1] * 10 + (ch.ord - '0'.ord)
273
337
  end
338
+ return
274
339
  when :clear
340
+
341
+ # Warning: If ESC is sent in the middle of a command, the command is cleared and there is no callback being called.
342
+
275
343
  @intermediate_chars = ''
344
+ @private_mode_intermediate_char = ''
276
345
  @params = []
277
346
  @ignore_flagged = false
278
- else
279
- @callback.call(:error, ch, intermediate_chars, params) if @callback
280
- end
281
- end
282
347
 
283
- def self.to_ansi(action, ch, intermediate_chars, params)
284
-
285
- case action
286
- when :print, :execute, :put, :osc_put, :ignore
287
- # Output the character
288
- return ch if ch
289
- when :hook
290
- return "\eP#{intermediate_chars}"
291
- when :esc_dispatch
292
- return "\e#{intermediate_chars}#{ch}"
293
- when :csi_dispatch
294
- # Output ESC [ followed by parameters, intermediates, and final character
295
- return "\e[#{params.join(';')}#{intermediate_chars}#{ch}"
296
- when :osc_start
297
- return "\e]"
298
- when :osc_end
299
- return "\x07" # BEL character to end OSC
300
- when :unhook
301
- return "" # \e must come from ESCAPE state
302
348
  else
303
- raise "Unknown action: #{action}"
304
- end
349
+ # when :execute, :print, :esc_dispatch, :csi_dispatch, :hook, :put, :unhook, :osc_start, :osc_put, :osc_end, :ignore
350
+ @callback.call(Action.new(action, ch, private_mode_intermediate_char, intermediate_chars, params)) if @callback
305
351
 
306
- raise
352
+ @intermediate_chars = ''
353
+ @private_mode_intermediate_char = ''
354
+ @params = []
355
+ @ignore_flagged = false
356
+ end
307
357
  end
308
358
 
309
359
  end
@@ -0,0 +1,81 @@
1
+ #
2
+ # This module can only be included in class which has a parse method to receive the next character read.
3
+ #
4
+ module PtySupport
5
+
6
+ #
7
+ # Spawn the given command using PTY::spawn, connect pipes and calls parse for each character read.
8
+ #
9
+ # Caution: While this command is running, STDIN will be in raw mode.
10
+ #
11
+ # If a block is given, it will be called for each character read, and the character
12
+ # will be replaced by the block's return value. If nil is returned by block, the character will be dropped.
13
+ #
14
+ def spawn(command, &block)
15
+
16
+ begin
17
+ PTY.spawn(command) do |stdout_and_stderr, stdin, pid|
18
+
19
+ # Input Thread
20
+ input_thread = Thread.new do
21
+
22
+ STDIN.raw do |io|
23
+ loop do
24
+ break if pid.nil?
25
+ begin
26
+ if io.wait_readable(0.1)
27
+ data = io.read_nonblock(1024)
28
+ stdin.write data
29
+ end
30
+ rescue IO::WaitReadable
31
+ # No input available right now
32
+ rescue EOFError
33
+ break
34
+ rescue Errno::EIO
35
+ break
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # Pipe stdout and stderr to the parser
42
+ begin
43
+
44
+ begin
45
+ winsize = $stdout.winsize
46
+ rescue Errno::ENOTTY
47
+ winsize = [0, 120] # Default to 120 columns
48
+ end
49
+ # Ensure the child process has the proper window size, because
50
+ # - tools such as yarn use it to identify tty mode
51
+ # - some tools use it to determine the width of the terminal for formatting
52
+ stdout_and_stderr.winsize = winsize
53
+
54
+ stdout_and_stderr.each_char do |char|
55
+
56
+ char = block.call(char) if block_given?
57
+ next if char.nil?
58
+
59
+ # puts Action.inspect_char(char) + "\r\n"
60
+ # Pass to parser
61
+ parse(char)
62
+
63
+ end
64
+ rescue Errno::EIO
65
+ # End of output
66
+ end
67
+
68
+ # Wait for the child process to exit
69
+ Process.wait(pid)
70
+ pid = nil
71
+ input_thread.join
72
+ return exit_status = $?.exitstatus
73
+
74
+ end
75
+
76
+ rescue PTY::ChildExited => e
77
+ puts "The child process exited: #{e}"
78
+ end
79
+ end
80
+
81
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vtparser
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vtparser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Oezbek
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-03 00:00:00.000000000 Z
11
+ date: 2024-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-prompt
@@ -90,14 +90,19 @@ files:
90
90
  - CHANGELOG.md
91
91
  - README.md
92
92
  - Rakefile
93
+ - examples/analyze.rb
94
+ - examples/colors.rb
95
+ - examples/colorswap.rb
93
96
  - examples/echo_keys.rb
94
97
  - examples/indent_cli.rb
95
98
  - examples/minimal.rb
96
99
  - examples/progress.rb
100
+ - examples/roundtrip.rb
97
101
  - examples/spinner.rb
98
102
  - lib/vtparser.rb
99
103
  - lib/vtparser/keymap.rb
100
104
  - lib/vtparser/parser.rb
105
+ - lib/vtparser/pty_support.rb
101
106
  - lib/vtparser/version.rb
102
107
  homepage: https://github.com/coezbek/vtparser
103
108
  licenses: []