vtparser 0.1.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: d622edd5fbb0b4f47cb9831699c466aeabc3da9c6bed99370cc7d99d169c8c02
4
- data.tar.gz: 33eccc5b843d8c5d734a06d5b2b3d389462c23ea568bf9456ed55a38d6f1a50a
3
+ metadata.gz: cbb3fedd9971980bdb4389680f7ad7f52a82ec573b9bdecc3a30ecd1fb22f102
4
+ data.tar.gz: 5f12efd5ffe0f23656dcd2520f835b64815a423f9cbb7353b98eac372b0a2303
5
5
  SHA512:
6
- metadata.gz: 0e5c6dd30582e24e0ad63b142446280b1be31d51cbae543b78c0019bf626ce2d477ea7bf002ab3707c7dd8056df886d33304eaa2a9509c30a9cac7ac6148e4b9
7
- data.tar.gz: ab1f60a0cdd8e079274bb8a7d6a285287b81a3fe1304462086ccdb71910fe09451faadaa4f3ce8935b6a63fdc579a856e069558e7183c8a7c659924e7ad32417
6
+ metadata.gz: 60c450670c31b4fee417acdefab3f6ed5287351929a6afcbdda46d68d4ba72e5db76731184136d014b9c656b026649fbed0fe5811740532b5402ce7961946fc2
7
+ data.tar.gz: 2d7ec6bdb03a5ef18df04e461cc199a9fe60d4b838d39f772db14d9aec70b95cbc90f5fc2b2cf42314a9a91d0aff70e5ca48f7185de754ead3e70061acdb501d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
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
+
14
+ ## [0.2.0] - 2024-10-03
15
+
16
+ - Add keyevent handling
17
+
3
18
  ## [0.1.0] - 2024-10-01
4
19
 
5
20
  - Initial release
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # VT 100 Parser Gem
2
2
 
3
- This gem is a parser for VT100 terminal escape sequences. It is based on the C code from https://github.com/haberman/vtparse/.
3
+ This gem is a parser for VT100 terminal escape sequences. It is based on the C code from https://github.com/haberman/vtparse/ and implements the statemachine from https://www.vt100.net/emu/dec_ansi_parser.
4
+
5
+ The purpose of this Gem is to have a relatively easy way to filter/modify the output of child/sub-processes (for instance launched via `PTY::spawn`) which use animation or colors.
6
+
7
+ Uses keyboard mapping logic from https://github.com/vidarh/keyboard_map/
4
8
 
5
9
  ## Background on VT100 Escape Sequences
6
10
 
@@ -22,26 +26,46 @@ gem install vtparser
22
26
 
23
27
  ## Basic Usage
24
28
 
25
- See the minimal example below and the [`examples`](https://github.com/coezbek/vtparser/examples) directory for more examples.
29
+ See the minimal example below:
26
30
 
27
31
  ```ruby
28
- require 'vtparser'
32
+ require_relative '../lib/vtparser'
29
33
 
30
34
  # Instantiate the parser with a block to handle actions
31
- parser = VTParser.new do |action, ch, intermediate_chars, params|
35
+ parser = VTParser.new do |action|
32
36
 
33
37
  # For this minimal example, we'll just turn everything back strings to print
34
- print VtParser::to_ansi(action, ch, intermediate_chars, params)
38
+ print action.to_ansi
35
39
 
36
40
  end
37
41
 
38
- # Sample input containing ANSI escape sequences
39
- input = "\e[31mHello, \e[1mWorld!\e[0m"
42
+ # Sample input containing ANSI escape sequences (red text, bold text)
43
+ input = "\e[31mHello, \e[1mWorld!\e[0m\n"
40
44
 
41
45
  # Parse the input
42
46
  parser.parse(input)
43
47
  ```
44
48
 
49
+ Further samples in the [`examples directory`](https://github.com/coezbek/vtparser/tree/main/examples):
50
+
51
+ - [`echo_keys.rb`](https://github.com/coezbek/vtparser/tree/main/examples/echo_keys.rb): Echoes the keys pressed by the user
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.
56
+
57
+ ## Limitations
58
+
59
+ - The parser is based on the implementation https://github.com/haberman/vtparse/ and based on a state machine which precedes Unicode. As such it does not have state transitions for Unicode characters. Rather, it will output them as `:ignore` actions. In case unicode characters are used inside escape sequences, the parser will likely not be able to handle them correctly.
60
+
61
+ - The state machine does not expose all input characters to the implementation in relationship to the `DSC` (Device Control String) sequences. In particular the "Final Character" is swallowed by the statemachine from https://www.vt100.net/emu/dec_ansi_parser. To circumvent this limitation, I have modified the parser to expose the final character as intermediate_chars to the `:hook` action.
62
+
63
+ - The parser only outputs full `actions`. So triggering an event for the `ESC` key doesn't work (as expected).
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
+
45
69
  ## Development
46
70
 
47
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)
@@ -0,0 +1,27 @@
1
+ require 'io/console'
2
+ require_relative '../lib/vtparser'
3
+
4
+ #
5
+ # Example for how to switch to raw mode to output infos about each keypress
6
+ #
7
+ STDIN.raw do |io|
8
+
9
+ parser = VTParser.new do |action, ch, intermediate_chars, params|
10
+
11
+ puts " New VTParser action: #{action}, ch: #{ch.inspect}, ch0x: #{ch.ord.to_s(16)}, intermediate_chars: #{intermediate_chars}, params: #{params}\r\n"
12
+
13
+ parser.to_key(action, ch, intermediate_chars, params) do |event|
14
+
15
+ puts " Keyevent: #{event.to_sym.inspect} #{event.inspect}\r\n"
16
+ exit(1) if event.to_sym == :ctrl_c
17
+
18
+ end
19
+ end
20
+
21
+ loop do
22
+ ch = $stdin.getch
23
+
24
+ puts "Getch: #{ch.inspect}\r\n"
25
+ parser.parse ch
26
+ end
27
+ end
@@ -4,6 +4,19 @@ require 'rainbow/refinement' # for colorizing output
4
4
  using Rainbow
5
5
  require_relative '../lib/vtparser'
6
6
 
7
+ #
8
+ # 'indent_cli.rb' - Example for vtparser
9
+ #
10
+ # This example demonstrates how to use the VTParser to indent the output of simple (!) tty programs
11
+ # with colorized or animated output.
12
+ #
13
+ # Run with `ruby indent_cli.rb <command>`` where <command> is the command you want to run.
14
+ #
15
+ # Two simple examples are included:
16
+ # - A simple spinner animation is included in `examples/spinner.rb`: `ruby indent_cli.rb 'ruby spinner.rb'`
17
+ # - A simple progress bar animation is included in `examples/progress.rb`: `ruby indent_cli.rb 'ruby progress.rb'`
18
+ #
19
+
7
20
  # Get the command from ARGV
8
21
  command = ARGV.join(' ')
9
22
  if command.empty?
@@ -12,56 +25,103 @@ if command.empty?
12
25
  end
13
26
 
14
27
  line_indent = ' ▐ '.yellow
28
+ line_indent_length = 6
29
+
30
+ #
31
+ # Use VTParser to process the VT100 escape sequences outputted by nested program and prepend the line_indent text.
32
+ #
15
33
  first_line = true
16
- 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
+
17
42
  print line_indent if first_line
18
43
  first_line = false
19
44
 
20
- to_output = VTParser::to_ansi(action, ch, intermediate_chars, params)
45
+ if $DEBUG && (action_type != :print || !(ch =~ /\P{Cc}/))
46
+ puts action.inspect
47
+ end
21
48
 
22
- case action
49
+ # Handle newlines, carriage returns, and cursor movement
50
+ case action_type
23
51
  when :print, :execute, :put, :osc_put
24
- if ch == "\n" || ch == "\r"
52
+ if ch == "\r" # || ch == "\n"
25
53
  print ch
26
54
  print line_indent
27
55
  next
28
56
  end
29
57
  when :csi_dispatch
30
- if to_output == "\e[2K"
58
+ if to_output == "\e[2K" # Clear line
31
59
  print "\e[2K"
32
60
  print line_indent
33
61
  next
34
62
  else
35
- if ch == 'G'
36
- # puts "to_output: #{to_output.inspect} action: #{action} ch: #{ch.inspect}"
37
- # && parser.params.size == 1
38
- print "\e[#{parser.params[0] + 6}G"
39
-
63
+ if ch == 'G' # Cursor movement to column
64
+ print "\e[#{parser.params[0] + line_indent_length}G"
40
65
  next
41
66
  end
42
67
  end
43
68
  end
44
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
+
45
77
  print to_output
46
78
  end
47
79
 
48
80
  begin
49
81
  PTY.spawn(command) do |stdout_and_stderr, stdin, pid|
50
82
 
51
- Thread.new do
52
- while pid != nil
53
- 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
54
102
  end
55
- rescue => e
56
- puts "Error: #{e}"
57
- exit(0)
58
103
  end
59
104
 
105
+ # Pipe stdout and stderr to the parser
60
106
  begin
61
- stdout_and_stderr.winsize = $stdout.winsize
62
107
 
108
+ begin
109
+ winsize = $stdout.winsize
110
+ rescue Errno::ENOTTY
111
+ winsize = [0, 120] # Default to 120 columns
112
+ end
113
+ # Ensure the child process has the proper window size, because
114
+ # - tools such as yarn use it to identify tty mode
115
+ # - some tools use it to determine the width of the terminal for formatting
116
+ stdout_and_stderr.winsize = [winsize.first, winsize.last - line_indent_length]
117
+
63
118
  stdout_and_stderr.each_char do |char|
64
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
65
125
  parser.parse(char)
66
126
 
67
127
  end
@@ -72,13 +132,12 @@ begin
72
132
  # Wait for the child process to exit
73
133
  Process.wait(pid)
74
134
  pid = nil
75
- exit_status = $?.exitstatus
76
- result = exit_status == 0
77
-
78
- # Clear the line, reset the cursor to the start of the line
79
- print "\e[2K\e[1G"
135
+ input_thread.join
80
136
  end
81
137
 
82
138
  rescue PTY::ChildExited => e
83
139
  puts "The child process exited: #{e}"
84
- end
140
+ end
141
+
142
+ # Clear and reset the cursor to the start of the line
143
+ puts "\e[2K\e[1G"
@@ -0,0 +1,15 @@
1
+ require_relative '../lib/vtparser'
2
+
3
+ # Instantiate the parser with a block to handle actions
4
+ parser = VTParser.new do |action, ch, intermediate_chars, params|
5
+
6
+ # For this minimal example, we'll just turn everything back strings to print
7
+ print VTParser::to_ansi(action, ch, intermediate_chars, params)
8
+
9
+ end
10
+
11
+ # Sample input containing ANSI escape sequences (red text, bold text)
12
+ input = "\e[31mHello, \e[1mWorld!\e[0m\n"
13
+
14
+ # Parse the input
15
+ parser.parse(input)
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # Helper script to be used to demonstrate `indent_cli.rb`. It displays a full-width animated progress bar.
5
+ #
6
+
7
+ require 'ruby-progressbar'
8
+
9
+ # Hide the cursor
10
+ print "\e[?25l"
11
+
12
+ loop do
13
+ progressbar = ProgressBar.create
14
+ 99.times {
15
+ progressbar.increment
16
+ sleep 0.02
17
+ }
18
+ print "\r"
19
+ end
@@ -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
+
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # Helper script to be used to demonstrate `indent_cli.rb`
5
+ #
6
+
7
+ # See: https://github.com/sindresorhus/cli-spinners/blob/main/spinners.json
8
+ frames = [
9
+ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"
10
+ ]
11
+ interval = 0.08 # 80 milliseconds
12
+
13
+ # Infinite loop to display the animation until interrupted
14
+ loop do
15
+ frames.each do |frame|
16
+ print "\r #{frame} " # "\r" moves the cursor back to the start of the line
17
+ sleep(interval)
18
+ end
19
+ end
@@ -0,0 +1,177 @@
1
+ require 'set'
2
+
3
+ #
4
+ # Logic taken from vidarh/keyboard_map gem
5
+ #
6
+ # https://github.com/vidarh/keyboard_map
7
+ #
8
+ # See examples/keymap.rb for usage
9
+ #
10
+
11
+ class KeyEvent
12
+ attr_reader :modifiers, :key, :args
13
+
14
+ def initialize(key, *modifiers, args: nil)
15
+ @key = key
16
+ @args = args
17
+ @modifiers = modifiers.map(&:to_sym).to_set
18
+ end
19
+
20
+ def to_s
21
+ (modifiers.to_a.sort << key).join('_')
22
+ end
23
+
24
+ def to_sym
25
+ to_s.to_sym
26
+ end
27
+
28
+ def ==(other)
29
+ case other
30
+ when KeyEvent
31
+ self.modifiers == other.modifiers && self.key == other.key
32
+ when Symbol
33
+ self.to_sym == other
34
+ else
35
+ self.to_s == other
36
+ end
37
+ end
38
+ end
39
+
40
+ module Keymap
41
+
42
+ SINGLE_KEY_EVENT = {
43
+ "\t" => :tab,
44
+ "\r" => :enter,
45
+ "\n" => :enter,
46
+ "\u007F" => :backspace
47
+ }.freeze
48
+
49
+ CSI_BASIC_MAP = {
50
+ "A" => :up,
51
+ "B" => :down,
52
+ "C" => :right,
53
+ "D" => :left,
54
+ "E" => :keypad_5,
55
+ "F" => :end,
56
+ "H" => :home,
57
+ }.freeze
58
+
59
+ CSI_TILDE_MAP = {
60
+ "1" => :home,
61
+ "2" => :insert,
62
+ "3" => :delete,
63
+ "4" => :end,
64
+ "5" => :page_up,
65
+ "6" => :page_down,
66
+ "15" => :f5,
67
+ "17" => :f6,
68
+ "18" => :f7,
69
+ "19" => :f8,
70
+ "20" => :f9,
71
+ "21" => :f10,
72
+ "23" => :f11,
73
+ "24" => :f12,
74
+ }.freeze
75
+
76
+ SS3_KEY_MAP = {
77
+ "P" => :f1,
78
+ "Q" => :f2,
79
+ "R" => :f3,
80
+ "S" => :f4,
81
+ }.freeze
82
+
83
+ def map_modifiers(mod)
84
+ return [] if mod.nil? || mod < 2
85
+ modifiers = []
86
+ mod = mod - 1 # Subtract 1 to align with modifier bits
87
+ modifiers << :shift if mod & 1 != 0
88
+ modifiers << :alt if mod & 2 != 0
89
+ modifiers << :ctrl if mod & 4 != 0
90
+ modifiers
91
+ end
92
+
93
+ def to_key(action, ch, intermediate_chars, params, &block)
94
+
95
+ case action
96
+ when :execute, :print, :ignore
97
+ # Control characters (e.g., Ctrl+C)
98
+ key = map_control_character(ch)
99
+ yield key if key
100
+ #when :print, :ignore
101
+ # Regular printable characters
102
+ #yield KeyEvent.new(ch)
103
+ when :esc_dispatch
104
+ # ESC sequences without intermediates
105
+ if intermediate_chars == ''
106
+ key = process_esc_sequence(ch)
107
+ yield key if key
108
+ else
109
+ # Handle other ESC sequences if necessary
110
+ end
111
+ when :csi_dispatch
112
+ key = process_csi_sequence(params, intermediate_chars, ch)
113
+ yield key if key
114
+ when :collect, :param, :clear
115
+ # Handled internally; no action needed here
116
+ else
117
+ # Handle other actions if necessary
118
+ end
119
+ end
120
+
121
+ def map_control_character(ch)
122
+ if SINGLE_KEY_EVENT.key?(ch)
123
+ return KeyEvent.new(SINGLE_KEY_EVENT[ch])
124
+ elsif ch.ord.between?(0x01, 0x1A)
125
+ # Ctrl+A to Ctrl+Z
126
+ key = (ch.ord + 96).chr
127
+ return KeyEvent.new(key, :ctrl)
128
+ else
129
+ return KeyEvent.new(ch)
130
+ end
131
+ end
132
+
133
+ def process_esc_sequence(final_char)
134
+ case final_char
135
+ when 'Z'
136
+ # Shift+Tab
137
+ return KeyEvent.new(:tab, :shift)
138
+ when "\e"
139
+ # Double ESC
140
+ return KeyEvent.new(:esc)
141
+ else
142
+ # Meta key (Alt) combinations
143
+ if final_char.ord.between?(0x20, 0x7E)
144
+ return KeyEvent.new(final_char, :meta)
145
+ else
146
+ # Handle other ESC sequences if necessary
147
+ end
148
+ end
149
+ end
150
+
151
+ def process_csi_sequence(params, intermediate_chars, final_char)
152
+ key = nil
153
+ modifiers = []
154
+ params = params.map(&:to_i)
155
+
156
+ if intermediate_chars == ''
157
+ if final_char == '~'
158
+ # Sequences like ESC [ 1 ~
159
+ key = CSI_TILDE_MAP[params[0].to_s]
160
+ modifiers = map_modifiers(params[1]) if params.size > 1
161
+ else
162
+ # Sequences like ESC [ A
163
+ key = CSI_BASIC_MAP[final_char]
164
+ modifiers = map_modifiers(params[0]) if params.size > 0
165
+ end
166
+ else
167
+ # Handle intermediates if necessary
168
+ end
169
+
170
+ if key
171
+ return KeyEvent.new(key, *modifiers)
172
+ else
173
+ # Handle unrecognized sequences
174
+ end
175
+ end
176
+
177
+ end
@@ -1,10 +1,76 @@
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
+
62
+
1
63
  class VTParser
2
- attr_reader :intermediate_chars, :params
64
+ attr_reader :private_mode_intermediate_char, :intermediate_chars, :params
65
+
66
+ include Keymap
67
+ include PtySupport
3
68
 
4
69
  def initialize(&block)
5
70
  @callback = block
6
71
  @state = :GROUND
7
72
  @intermediate_chars = ''
73
+ @private_mode_intermediate_char = ''
8
74
  @params = []
9
75
  @ignore_flagged = false
10
76
  initialize_states
@@ -64,7 +130,7 @@ class VTParser
64
130
  0x3a => :CSI_IGNORE,
65
131
  (0x30..0x39) => [:param, :CSI_PARAM],
66
132
  0x3b => [:param, :CSI_PARAM],
67
- (0x3c..0x3f) => [:collect, :CSI_PARAM],
133
+ (0x3c..0x3f) => [:private_mode_collect, :CSI_PARAM],
68
134
  (0x40..0x7e) => [:csi_dispatch, :GROUND],
69
135
  },
70
136
  :CSI_PARAM => {
@@ -145,7 +211,9 @@ class VTParser
145
211
  },
146
212
  :OSC_STRING => {
147
213
  :on_entry => :osc_start,
148
- (0x00..0x17) => :ignore,
214
+ (0x00..0x06) => :ignore,
215
+ (0x07) => [:osc_put, :GROUND], # BEL character for xterm compatibility
216
+ (0x08..0x17) => :ignore,
149
217
  0x19 => :ignore,
150
218
  (0x1c..0x1f) => :ignore,
151
219
  (0x20..0x7f) => :osc_put,
@@ -249,15 +317,15 @@ class VTParser
249
317
 
250
318
  def handle_action(action, ch)
251
319
  case action
252
- when :execute, :print, :esc_dispatch, :csi_dispatch, :hook, :put, :unhook, :osc_start, :osc_put, :osc_end
253
- @callback.call(action, ch, intermediate_chars, params) if @callback
254
- when :ignore
255
- # Do nothing
256
- @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
257
324
  when :collect
258
325
  unless @ignore_flagged
259
326
  @intermediate_chars << ch
260
327
  end
328
+ return
261
329
  when :param
262
330
  if ch == ';'
263
331
  @params << 0
@@ -267,39 +335,25 @@ class VTParser
267
335
  end
268
336
  @params[-1] = @params[-1] * 10 + (ch.ord - '0'.ord)
269
337
  end
338
+ return
270
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
+
271
343
  @intermediate_chars = ''
344
+ @private_mode_intermediate_char = ''
272
345
  @params = []
273
346
  @ignore_flagged = false
274
- else
275
- @callback.call(:error, ch, intermediate_chars, params) if @callback
276
- end
277
- end
278
347
 
279
- def self.to_ansi(action, ch, intermediate_chars, params)
280
-
281
- case action
282
- when :print, :execute, :put, :osc_put, :ignore
283
- # Output the character
284
- return ch if ch
285
- when :hook
286
- return "\eP#{intermediate_chars}"
287
- when :esc_dispatch
288
- return "\e#{intermediate_chars}#{ch}"
289
- when :csi_dispatch
290
- # Output ESC [ followed by parameters, intermediates, and final character
291
- return "\e[#{params.join(';')}#{intermediate_chars}#{ch}"
292
- when :osc_start
293
- return "\e]"
294
- when :osc_end
295
- return "\x07" # BEL character to end OSC
296
- when :unhook
297
- return "" # \e must come from ESCAPE state
298
348
  else
299
- raise "Unknown action: #{action}"
300
- 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
301
351
 
302
- raise
352
+ @intermediate_chars = ''
353
+ @private_mode_intermediate_char = ''
354
+ @params = []
355
+ @ignore_flagged = false
356
+ end
303
357
  end
304
358
 
305
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.1.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.1.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-01 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
@@ -58,14 +58,28 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.40.0
61
+ version: '0.40'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.40.0
68
+ version: '0.40'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ruby-progressbar
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  description: A pure Ruby VT100 parser that can be used to parse ANSI escape sequences.
70
84
  email:
71
85
  - c.oezbek@gmail.com
@@ -73,13 +87,22 @@ executables: []
73
87
  extensions: []
74
88
  extra_rdoc_files: []
75
89
  files:
76
- - ".bash_history"
77
90
  - CHANGELOG.md
78
91
  - README.md
79
92
  - Rakefile
93
+ - examples/analyze.rb
94
+ - examples/colors.rb
95
+ - examples/colorswap.rb
96
+ - examples/echo_keys.rb
80
97
  - examples/indent_cli.rb
98
+ - examples/minimal.rb
99
+ - examples/progress.rb
100
+ - examples/roundtrip.rb
101
+ - examples/spinner.rb
81
102
  - lib/vtparser.rb
103
+ - lib/vtparser/keymap.rb
82
104
  - lib/vtparser/parser.rb
105
+ - lib/vtparser/pty_support.rb
83
106
  - lib/vtparser/version.rb
84
107
  homepage: https://github.com/coezbek/vtparser
85
108
  licenses: []
@@ -103,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
126
  - !ruby/object:Gem::Version
104
127
  version: '0'
105
128
  requirements: []
106
- rubygems_version: 3.4.19
129
+ rubygems_version: 3.5.20
107
130
  signing_key:
108
131
  specification_version: 4
109
132
  summary: Pure Ruby VT100 parser
data/.bash_history DELETED
@@ -1,117 +0,0 @@
1
- rspec
2
- rspec
3
- rspec
4
- rspec
5
- rspec
6
- rspec
7
- rspec
8
- rspec
9
- rspec
10
- rspec
11
- rspec ./spec/vtparser_spec.rb:153
12
- rspec ./spec/vtparser_spec.rb:153
13
- rspec ./spec/vtparser_spec.rb:153
14
- rspec ./spec/vtparser_spec.rb:153
15
- rspec ./spec/vtparser_spec.rb:153
16
- rspec ./spec/vtparser_spec.rb:153
17
- rspec
18
- rspec
19
- rspec
20
- rspec ./spec/vtparser_spec.rb:37
21
- rspec ./spec/vtparser_spec.rb:37
22
- rspec
23
- rspec
24
- rspec expect(output).to eq(input)
25
- rspec ./spec/vtparser_spec.rb:156
26
- rspec ./spec/vtparser_spec.rb:156
27
- rspec ./spec/vtparser_spec.rb:156
28
- rspec ./spec/vtparser_spec.rb:156
29
- rspec
30
- rspec
31
- D
32
- rspec ./spec/vtparser_spec.rb
33
- rspec ./spec/vtparser_spec.rb:137
34
- rspec ./spec/vtparser_spec.rb:137
35
- rspec ./spec/vtparser_spec.rb:137
36
- rspec ./spec/vtparser_spec.rb:137
37
- rspec ./spec/vtparser_spec.rb:137
38
- rspec ./spec/vtparser_spec.rb:137
39
- rspec ./spec/vtparser_spec.rb:137
40
- rspec ./spec/vtparser_spec.rb:137
41
- rspec ./spec/vtparser_spec.rb
42
- rspec ./spec/vtparser_spec.rb
43
- rspec ./spec/vtparser_spec.rb
44
- bundle
45
- rspec ./spec/vtparser_spec.rb
46
- rspec ./spec/vtparser_spec.rb
47
- ruby lib/vtparser.rb 'yarn add --dev esbuild from "."'
48
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
49
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
50
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
51
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
52
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
53
- bundle
54
- bundle
55
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
56
- irb
57
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
58
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
59
- ruby lib/vtparser/parser.rb 'yarn add --dev esbuild from "."'
60
- ruby lib/vtparser/parser.rb 'joe'
61
- joe
62
- ruby lib/vtparser/parser.rb 'vim'
63
- vim
64
- ruby example/indent_cli.rb 'vim'
65
- ruby examples/indent_cli.rb 'vim'
66
- ruby examples/indent_cli.rb 'vim'
67
- ruby examples/indent_cli.rb 'vim'
68
- ruby examples/indent_cli.rb 'joe'
69
- ruby examples/indent_cli.rb 'joe'
70
- ruby examples/indent_cli.rb 'joe'
71
- lsss
72
- less
73
- less README.md
74
- ls
75
- exit
76
- ls
77
- ruby examples/indent_cli.rb 'less'
78
- ruby examples/indent_cli.rb 'less README.md'
79
- ruby examples/indent_cli.rb 'less README.md'
80
- ruby examples/indent_cli.rb 'less README.md'
81
- ruby examples/indent_cli.rb 'less README.md'
82
- ruby examples/indent_cli.rb 'less README.md'
83
- ruby examples/indent_cli.rb 'less README.md'
84
- ruby examples/indent_cli.rb 'less README.md'
85
- ruby examples/indent_cli.rb 'less README.md'
86
- ruby examples/indent_cli.rb 'less README.md'
87
- ruby examples/indent_cli.rb 'less README.md'
88
- ruby examples/indent_cli.rb 'less README.md'
89
- ruby examples/indent_cli.rb 'less README.md'
90
- ruby examples/indent_cli.rb 'less README.md'
91
- ruby examples/indent_cli.rb 'less README.md'
92
- less README.md
93
- ruby examples/indent_cli.rb 'less README.md'
94
- ruby examples/indent_cli.rb 'less README.md'
95
- ruby examples/indent_cli.rb 'less README.md'
96
- ruby examples/indent_cli.rb 'less README.md'
97
- ruby examples/indent_cli.rb 'less README.md'
98
- ruby examples/indent_cli.rb 'less README.md'
99
- ruby examples/indent_cli.rb 'less README.md'
100
- ruby examples/indent_cli.rb 'less README.md'
101
- ruby examples/indent_cli.rb 'less README.md'
102
- ruby examples/indent_cli.rb 'less README.md'
103
- ruby examples/test.rb
104
- kkllllijjaaa
105
- ruby examples/test.rb
106
- ruby examples/test.rb
107
- ruby examples/test.rb
108
- ruby examples/test.rb
109
- ruby examples/test.rb
110
- ruby examples/test.rb
111
- ruby examples/test.rb
112
- ruby examples/test.rb
113
- ruby examples/test.rb
114
- ruby examples/test.rb
115
- ruby examples/indent_cli.rb
116
- ruby examples/indent_cli.rb 'bundle gem test17'
117
- ruby examples/indent_cli.rb 'bundle gem test18'