scriptty 0.5.0-java
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.
- data/.gitattributes +1 -0
- data/.gitignore +3 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/README.rdoc +31 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/scriptty-capture +5 -0
- data/bin/scriptty-dump-screens +4 -0
- data/bin/scriptty-replay +5 -0
- data/bin/scriptty-term-test +4 -0
- data/bin/scriptty-transcript-parse +4 -0
- data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
- data/examples/captures/xterm-vim-session.bin +262 -0
- data/examples/demo-capture.rb +19 -0
- data/examples/telnet-nego.rb +55 -0
- data/lib/scriptty/apps/capture_app/console.rb +104 -0
- data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
- data/lib/scriptty/apps/capture_app.rb +213 -0
- data/lib/scriptty/apps/dump_screens_app.rb +166 -0
- data/lib/scriptty/apps/replay_app.rb +229 -0
- data/lib/scriptty/apps/term_test_app.rb +124 -0
- data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
- data/lib/scriptty/cursor.rb +39 -0
- data/lib/scriptty/exception.rb +38 -0
- data/lib/scriptty/expect.rb +392 -0
- data/lib/scriptty/multiline_buffer.rb +192 -0
- data/lib/scriptty/net/event_loop.rb +610 -0
- data/lib/scriptty/screen_pattern/generator.rb +398 -0
- data/lib/scriptty/screen_pattern/parser.rb +558 -0
- data/lib/scriptty/screen_pattern.rb +104 -0
- data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
- data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
- data/lib/scriptty/term/dg410/parser.rb +162 -0
- data/lib/scriptty/term/dg410.rb +489 -0
- data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
- data/lib/scriptty/term/xterm.rb +661 -0
- data/lib/scriptty/term.rb +40 -0
- data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
- data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
- data/lib/scriptty/util/fsm.rb +177 -0
- data/lib/scriptty/util/transcript/reader.rb +96 -0
- data/lib/scriptty/util/transcript/writer.rb +111 -0
- data/test/apps/capture_app_test.rb +123 -0
- data/test/apps/transcript_parse_app_test.rb +118 -0
- data/test/cursor_test.rb +51 -0
- data/test/fsm_definition_parser_test.rb +220 -0
- data/test/fsm_test.rb +322 -0
- data/test/multiline_buffer_test.rb +275 -0
- data/test/net/event_loop_test.rb +402 -0
- data/test/screen_pattern/generator_test.rb +408 -0
- data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
- data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
- data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
- data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
- data/test/screen_pattern/parser_test.rb +266 -0
- data/test/term/dg410/parser_test.rb +139 -0
- data/test/term/xterm_test.rb +327 -0
- data/test/test_helper.rb +3 -0
- data/test/util/transcript/reader_test.rb +131 -0
- data/test/util/transcript/writer_test.rb +126 -0
- data/test.watchr +29 -0
- metadata +175 -0
@@ -0,0 +1,661 @@
|
|
1
|
+
# = XTerm terminal emulation
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
# TODO - This is incomplete
|
20
|
+
|
21
|
+
require 'scriptty/multiline_buffer'
|
22
|
+
require 'scriptty/cursor'
|
23
|
+
require 'scriptty/util/fsm'
|
24
|
+
require 'set'
|
25
|
+
|
26
|
+
module ScripTTY # :nodoc:
|
27
|
+
module Term
|
28
|
+
class XTerm
|
29
|
+
|
30
|
+
PARSER_DEFINITION = File.read(File.join(File.dirname(__FILE__), "xterm/xterm-escapes.txt"))
|
31
|
+
DEFAULT_FLAGS = {
|
32
|
+
:insert_mode => false,
|
33
|
+
:wraparound_mode => false,
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
# width and height of the display buffer
|
37
|
+
attr_reader :width, :height
|
38
|
+
|
39
|
+
def initialize(height=24, width=80)
|
40
|
+
@parser_fsm = Util::FSM.new(:definition => PARSER_DEFINITION,
|
41
|
+
:callback => self, :callback_method => :send)
|
42
|
+
|
43
|
+
@height = height
|
44
|
+
@width = width
|
45
|
+
|
46
|
+
on_unknown_sequence :error
|
47
|
+
reset_to_initial_state!
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set the behaviour of the terminal when an unknown escape sequence is
|
51
|
+
# found.
|
52
|
+
#
|
53
|
+
# This method takes either a symbol or a block.
|
54
|
+
#
|
55
|
+
# When a block is given, it is executed whenever an unknown escape
|
56
|
+
# sequence is received. The block is passed the escape sequence as a
|
57
|
+
# single string.
|
58
|
+
#
|
59
|
+
# When a symbol is given, it may be one of the following:
|
60
|
+
# [:error]
|
61
|
+
# (default) Raise a ScripTTY::Util::FSM::NoMatch exception.
|
62
|
+
# [:ignore]
|
63
|
+
# Ignore the unknown escape sequence.
|
64
|
+
def on_unknown_sequence(mode=nil, &block)
|
65
|
+
if !block and !mode
|
66
|
+
raise ArgumentError.new("No mode specified and no block given")
|
67
|
+
elsif block and mode
|
68
|
+
raise ArgumentError.new("Block and mode are mutually exclusive, but both were given")
|
69
|
+
elsif block
|
70
|
+
@on_unknown_sequence = block
|
71
|
+
elsif [:error, :ignore].include?(mode)
|
72
|
+
@on_unknown_sequence = mode
|
73
|
+
else
|
74
|
+
raise ArgumentError.new("Invalid mode #{mode.inspect}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def inspect # :nodoc:
|
79
|
+
# The default inspect method shows way too much information. Simplify it.
|
80
|
+
"#<#{self.class.name}:#{sprintf('0x%0x', object_id)} h=#{@height.inspect} w=#{@width.inspect} cursor=#{cursor_pos.inspect}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Feed the specified byte to the terminal. Returns a string of
|
84
|
+
# bytes that should be transmitted (e.g. for TELNET negotiation).
|
85
|
+
def feed_byte(byte)
|
86
|
+
raise ArgumentError.new("input should be single byte") unless byte.is_a?(String) and byte.length == 1
|
87
|
+
begin
|
88
|
+
@parser_fsm.process(byte)
|
89
|
+
rescue Util::FSM::NoMatch => e
|
90
|
+
@parser_fsm.reset!
|
91
|
+
if @on_unknown_sequence == :error
|
92
|
+
raise
|
93
|
+
elsif @on_unknown_sequence == :ignore
|
94
|
+
# do nothing
|
95
|
+
elsif !@on_unknown_sequence.is_a?(Symbol) # @on_unknown_sequence is a Proc
|
96
|
+
@on_unknown_sequence.call(e.input_sequence.join)
|
97
|
+
else
|
98
|
+
raise "BUG"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
""
|
102
|
+
end
|
103
|
+
|
104
|
+
# Convenience method: Feeds several bytes to the terminal. Returns a
|
105
|
+
# string of bytes that should be transmitted (e.g. for TELNET
|
106
|
+
# negotiation).
|
107
|
+
def feed_bytes(bytes)
|
108
|
+
retvals = []
|
109
|
+
bytes.split(//n).each do |byte|
|
110
|
+
retvals << feed_byte(byte)
|
111
|
+
end
|
112
|
+
retvals.join
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
# Return an array of strings representing the lines of text on the screen
|
117
|
+
#
|
118
|
+
# NOTE: If passing copy=false, do not modify the return value or the strings inside it.
|
119
|
+
def text(copy=true)
|
120
|
+
if copy
|
121
|
+
@glyphs.content.map{|line| line.dup}
|
122
|
+
else
|
123
|
+
@glyphs.content
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Return the cursor position, as an array of [row, column].
|
128
|
+
#
|
129
|
+
# [0,0] represents the topmost, leftmost position.
|
130
|
+
def cursor_pos
|
131
|
+
[@cursor.row, @cursor.column]
|
132
|
+
end
|
133
|
+
|
134
|
+
# Set the cursor position to [row, column].
|
135
|
+
#
|
136
|
+
# [0,0] represents the topmost, leftmost position.
|
137
|
+
def cursor_pos=(v)
|
138
|
+
@cursor.pos = v
|
139
|
+
end
|
140
|
+
|
141
|
+
# Replace the text on the screen with the specified text.
|
142
|
+
#
|
143
|
+
# NOTE: This is API is very likely to change in the future.
|
144
|
+
def text=(a)
|
145
|
+
@glyphs.clear!
|
146
|
+
@glyphs.replace_at(0, 0, a)
|
147
|
+
a
|
148
|
+
end
|
149
|
+
|
150
|
+
protected
|
151
|
+
|
152
|
+
# Reset to the initial state. Return true.
|
153
|
+
def reset_to_initial_state!
|
154
|
+
@flags = DEFAULT_FLAGS.dup
|
155
|
+
|
156
|
+
# current cursor position
|
157
|
+
@cursor = Cursor.new
|
158
|
+
@cursor.row = @cursor.column = 0
|
159
|
+
@saved_cursor_position = [0,0]
|
160
|
+
|
161
|
+
# Screen buffer
|
162
|
+
@glyphs = MultilineBuffer.new(@height, @width) # the displayable characters (as bytes)
|
163
|
+
@attrs = MultilineBuffer.new(@height, @width) # character attributes (as bytes)
|
164
|
+
|
165
|
+
# Vertical scrolling region. An array of [start_row, end_row]. Defaults to [0, height-1].
|
166
|
+
@scrolling_region = [0, @height-1]
|
167
|
+
true
|
168
|
+
end
|
169
|
+
|
170
|
+
# Replace the character under the cursor with the specified character.
|
171
|
+
#
|
172
|
+
# If curfwd is true, the cursor is also moved forward.
|
173
|
+
#
|
174
|
+
# Returns true.
|
175
|
+
def put_char!(input, curfwd=false)
|
176
|
+
raise TypeError.new("input must be single-character string") unless input.is_a?(String) and input.length == 1
|
177
|
+
@glyphs.replace_at(@cursor.row, @cursor.column, input)
|
178
|
+
@attrs.replace_at(@cursor.row, @cursor.column, " ")
|
179
|
+
cursor_forward! if curfwd
|
180
|
+
true
|
181
|
+
end
|
182
|
+
|
183
|
+
# Move the cursor to the leftmost column in the current row, then return true.
|
184
|
+
def carriage_return!
|
185
|
+
@cursor.column = 0
|
186
|
+
true
|
187
|
+
end
|
188
|
+
|
189
|
+
# Move the cursor down one row and return true.
|
190
|
+
#
|
191
|
+
# If the cursor is on the bottom row of the vertical scrolling region,
|
192
|
+
# the region is scrolled. If bot, but the cursor is on the bottom of
|
193
|
+
# the screen, this command has no effect.
|
194
|
+
def line_feed!
|
195
|
+
if @cursor.row == @scrolling_region[1] # cursor is on the bottom row of the scrolling region
|
196
|
+
scroll_up!
|
197
|
+
elsif @cursor.row >= @height-1
|
198
|
+
# do nothing
|
199
|
+
else
|
200
|
+
cursor_down!
|
201
|
+
end
|
202
|
+
true
|
203
|
+
end
|
204
|
+
|
205
|
+
# Save the cursor position. Return true.
|
206
|
+
def save_cursor!
|
207
|
+
@saved_cursor_position = [@cursor.row, @cursor.column]
|
208
|
+
true
|
209
|
+
end
|
210
|
+
|
211
|
+
# Restore the saved cursor position. If nothing has been saved, then go to the home position. Return true.
|
212
|
+
def restore_cursor!
|
213
|
+
@cursor.row, @cursor.column = @saved_cursor_position
|
214
|
+
true
|
215
|
+
end
|
216
|
+
|
217
|
+
# Move the cursor down one row and return true.
|
218
|
+
# If the cursor is on the bottom row, return false without moving the cursor.
|
219
|
+
def cursor_down!
|
220
|
+
if @cursor.row >= @height-1
|
221
|
+
false
|
222
|
+
else
|
223
|
+
@cursor.row += 1
|
224
|
+
true
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Move the cursor up one row and return true.
|
229
|
+
# If the cursor is on the top row, return false without moving the cursor.
|
230
|
+
def cursor_up!
|
231
|
+
if @cursor.row <= 0
|
232
|
+
false
|
233
|
+
else
|
234
|
+
@cursor.row -= 1
|
235
|
+
true
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Move the cursor right one column and return true.
|
240
|
+
# If the cursor is on the right-most column, return false without moving the cursor.
|
241
|
+
def cursor_right!
|
242
|
+
if @cursor.column >= @width-1
|
243
|
+
false
|
244
|
+
else
|
245
|
+
@cursor.column += 1
|
246
|
+
true
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Move the cursor to the right. Wrap around if we reach the end of the screen.
|
251
|
+
#
|
252
|
+
# Return true.
|
253
|
+
def cursor_forward!(options={})
|
254
|
+
if @cursor.column >= @width-1
|
255
|
+
line_feed!
|
256
|
+
carriage_return!
|
257
|
+
else
|
258
|
+
cursor_right!
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Move the cursor left one column and return true.
|
263
|
+
# If the cursor is on the left-most column, return false without moving the cursor.
|
264
|
+
def cursor_left!
|
265
|
+
if @cursor.column <= 0
|
266
|
+
false
|
267
|
+
else
|
268
|
+
@cursor.column -= 1
|
269
|
+
true
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
alias cursor_back! cursor_left! # In the future, this might not be an alias
|
274
|
+
|
275
|
+
# Scroll the contents of the screen up by one row and return true.
|
276
|
+
# The position of the cursor does not change.
|
277
|
+
def scroll_up!
|
278
|
+
@glyphs.scroll_up_region(@scrolling_region[0], 0, @scrolling_region[1], @width-1, 1)
|
279
|
+
@attrs.scroll_up_region(@scrolling_region[0], 0, @scrolling_region[1], @width-1, 1)
|
280
|
+
true
|
281
|
+
end
|
282
|
+
|
283
|
+
# Scroll the contents of the screen down by one row and return true.
|
284
|
+
# The position of the cursor does not change.
|
285
|
+
def scroll_down!
|
286
|
+
@glyphs.scroll_down_region(@scrolling_region[0], 0, @scrolling_region[1], @width-1, 1)
|
287
|
+
@attrs.scroll_down_region(@scrolling_region[0], 0, @scrolling_region[1], @width-1, 1)
|
288
|
+
true
|
289
|
+
end
|
290
|
+
|
291
|
+
# Erase, starting with the character under the cursor and extending to the end of the line.
|
292
|
+
# Return true.
|
293
|
+
def erase_to_end_of_line!
|
294
|
+
@glyphs.replace_at(@cursor.row, @cursor.column, " "*(@width-@cursor.column))
|
295
|
+
@attrs.replace_at(@cursor.row, @cursor.column, " "*(@width-@cursor.column))
|
296
|
+
true
|
297
|
+
end
|
298
|
+
|
299
|
+
# Erase, starting with the beginning of the line and extending to the character under the cursor.
|
300
|
+
# Return true.
|
301
|
+
def erase_to_start_of_line!
|
302
|
+
@glyphs.replace_at(@cursor.row, 0, " "*(@cursor.column+1))
|
303
|
+
@attrs.replace_at(@cursor.row, 0, " "*(@cursor.column+1))
|
304
|
+
true
|
305
|
+
end
|
306
|
+
|
307
|
+
# Erase the current line. The cursor position is unchanged.
|
308
|
+
# Return true.
|
309
|
+
def erase_line!
|
310
|
+
@glyphs.replace_at(@cursor.row, 0, " "*@width)
|
311
|
+
@attrs.replace_at(@cursor.row, 0, " "*@width)
|
312
|
+
true
|
313
|
+
end
|
314
|
+
|
315
|
+
# Erase the window. Return true.
|
316
|
+
def erase_window!
|
317
|
+
empty_line = " "*@width
|
318
|
+
@height.times do |row|
|
319
|
+
@glyphs.replace_at(row, 0, empty_line)
|
320
|
+
@attrs.replace_at(row, 0, empty_line)
|
321
|
+
end
|
322
|
+
true
|
323
|
+
end
|
324
|
+
|
325
|
+
# Delete the specified number of lines, starting at the cursor position
|
326
|
+
# extending downwards. The lines below the deleted lines are scrolled up,
|
327
|
+
# and blank lines are inserted below them.
|
328
|
+
# Return true.
|
329
|
+
def delete_lines!(count=1)
|
330
|
+
@glyphs.scroll_up_region(@cursor.row, 0, @height-1, @width-1, count)
|
331
|
+
@attrs.scroll_up_region(@cursor.row, 0, @height-1, @width-1, count)
|
332
|
+
true
|
333
|
+
end
|
334
|
+
|
335
|
+
# Delete the specified number of characters, starting at the cursor position
|
336
|
+
# extending to the end of the line. The characters to the right of the
|
337
|
+
# cursor are scrolled left, and blanks are inserted after them.
|
338
|
+
# Return true.
|
339
|
+
def delete_characters!(count=1)
|
340
|
+
@glyphs.scroll_left_region(@cursor.row, @cursor.column, @cursor.row, @width-1, count)
|
341
|
+
@attrs.scroll_left_region(@cursor.row, @cursor.column, @cursor.row, @width-1, count)
|
342
|
+
true
|
343
|
+
end
|
344
|
+
|
345
|
+
# Insert the specified number of blank characters at the cursor position.
|
346
|
+
# The characters to the right of the cursor are scrolled right, and blanks
|
347
|
+
# are inserted in their place.
|
348
|
+
# Return true.
|
349
|
+
def insert_blank_characters!(count=1)
|
350
|
+
@glyphs.scroll_right_region(@cursor.row, @cursor.column, @cursor.row, @width-1, count)
|
351
|
+
@attrs.scroll_right_region(@cursor.row, @cursor.column, @cursor.row, @width-1, count)
|
352
|
+
true
|
353
|
+
end
|
354
|
+
|
355
|
+
# Insert the specified number of lines characters at the cursor position.
|
356
|
+
# The characters to the below the cursor are scrolled down, and blank
|
357
|
+
# lines are inserted in their place.
|
358
|
+
# Return true.
|
359
|
+
def insert_blank_lines!(count=1)
|
360
|
+
@glyphs.scroll_down_region(@cursor.row, 0, @height-1, @width-1, count)
|
361
|
+
@attrs.scroll_down_region(@cursor.row, 0, @height-1, @width-1, count)
|
362
|
+
true
|
363
|
+
end
|
364
|
+
|
365
|
+
private
|
366
|
+
|
367
|
+
# Set the vertical scrolling region.
|
368
|
+
#
|
369
|
+
# Values will be clipped.
|
370
|
+
def set_scrolling_region!(top, bottom)
|
371
|
+
@scrolling_region[0] = [0, [@height-1, top].min].max
|
372
|
+
@scrolling_region[1] = [0, [@width-1, bottom].min].max
|
373
|
+
nil
|
374
|
+
end
|
375
|
+
|
376
|
+
def error(message) # XXX - This sucks
|
377
|
+
raise ArgumentError.new(message)
|
378
|
+
#puts message # DEBUG FIXME
|
379
|
+
end
|
380
|
+
|
381
|
+
def t_reset(fsm)
|
382
|
+
reset_to_initial_state!
|
383
|
+
end
|
384
|
+
|
385
|
+
# Printable character
|
386
|
+
def t_printable(fsm) # :nodoc:
|
387
|
+
insert_blank_characters! if @flags[:insert_mode] # TODO
|
388
|
+
put_char!(fsm.input)
|
389
|
+
cursor_forward!
|
390
|
+
end
|
391
|
+
|
392
|
+
# NUL character
|
393
|
+
def t_nul(fsm) end # TODO
|
394
|
+
|
395
|
+
# Beep
|
396
|
+
def t_bell(fsm) end # TODO
|
397
|
+
|
398
|
+
# Backspace
|
399
|
+
def t_bs(fsm)
|
400
|
+
cursor_back!
|
401
|
+
put_char!(" ")
|
402
|
+
end
|
403
|
+
|
404
|
+
def t_carriage_return(fsm)
|
405
|
+
carriage_return!
|
406
|
+
end
|
407
|
+
|
408
|
+
def t_new_line(fsm)
|
409
|
+
carriage_return!
|
410
|
+
line_feed!
|
411
|
+
end
|
412
|
+
|
413
|
+
def t_save_cursor(fsm)
|
414
|
+
save_cursor!
|
415
|
+
end
|
416
|
+
|
417
|
+
def t_restore_cursor(fsm)
|
418
|
+
restore_cursor!
|
419
|
+
end
|
420
|
+
|
421
|
+
# ESC [
|
422
|
+
def t_parse_csi(fsm)
|
423
|
+
fsm.redirect = lambda {|fsm| fsm.input =~ /[\d;]/n}
|
424
|
+
end
|
425
|
+
|
426
|
+
# Operating system controls
|
427
|
+
# ESC ] Ps ; Pt BEL
|
428
|
+
# "ESC ]", followed by a number and a semicolon, followed by printable text, followed by a non-printable character
|
429
|
+
def t_parse_osc(fsm)
|
430
|
+
fsm.redirect = lambda {|fsm| fsm.input =~ /[\x20-\x7e]/n}
|
431
|
+
end
|
432
|
+
|
433
|
+
# IAC SB ... SE
|
434
|
+
def t_parse_telnet_sb(fsm)
|
435
|
+
# limit subnegotiation to 100 chars
|
436
|
+
count = 0
|
437
|
+
fsm.redirect = lambda {|fsm| count += 1; count < 100 && fsm.input_sequence[-2..-1] != ["\377", "\360"]}
|
438
|
+
end
|
439
|
+
|
440
|
+
# ESC [ Ps J
|
441
|
+
def t_erase_in_display(fsm)
|
442
|
+
(mode,) = parse_csi_params(fsm.input_sequence)
|
443
|
+
mode ||= 0 # default is mode 0
|
444
|
+
case mode
|
445
|
+
when 0
|
446
|
+
# Erase from the cursor to the end of the window. Cursor position is unaffected.
|
447
|
+
erase_to_end_of_line!
|
448
|
+
when 1
|
449
|
+
# Erase the window. Cursor position is unaffected.
|
450
|
+
erase_window!
|
451
|
+
when 2
|
452
|
+
# Erase the window. Cursor moves to the home position.
|
453
|
+
erase_window!
|
454
|
+
@cursor.pos = [0,0]
|
455
|
+
else
|
456
|
+
# ignored
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# ESC [ ? ... h
|
461
|
+
def t_dec_private_mode_set(fsm)
|
462
|
+
parse_csi_params(fsm.input_sequence).each do |mode|
|
463
|
+
case mode
|
464
|
+
when 1 # Application cursor keys
|
465
|
+
when 7 # Wraparound mode
|
466
|
+
@flags[:wraparound_mode] = true
|
467
|
+
when 47 # Use alternate screen buffer
|
468
|
+
else
|
469
|
+
return error("unknown DEC private mode set (escape sequence: #{fsm.input_sequence.inspect})")
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# ESC [ ? ... l
|
475
|
+
def t_dec_private_mode_reset(fsm)
|
476
|
+
parse_csi_params(fsm.input_sequence).each do |mode|
|
477
|
+
case mode
|
478
|
+
when 1 # Normal cursor keys
|
479
|
+
when 7 # No wraparound mode
|
480
|
+
@flags[:wraparound_mode] = false
|
481
|
+
when 47 # Use normal screen buffer
|
482
|
+
else
|
483
|
+
return error("unknown DEC private mode reset (escape sequence: #{fsm.input_sequence.inspect})")
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# ESC [ Ps; Ps r
|
489
|
+
def t_set_scrolling_region(fsm)
|
490
|
+
top, bottom = parse_csi_params(fsm.input_sequence)
|
491
|
+
top ||= 1
|
492
|
+
bottom ||= @height
|
493
|
+
@scrolling_region = [top-1, bottom-1]
|
494
|
+
end
|
495
|
+
|
496
|
+
# ESC [ Ps K
|
497
|
+
def t_erase_in_line(fsm)
|
498
|
+
(mode,) = parse_csi_params(fsm.input_sequence)
|
499
|
+
mode ||= 0
|
500
|
+
case mode
|
501
|
+
when 0 # Erase to right
|
502
|
+
erase_to_end_of_line!
|
503
|
+
when 1 # Erase to left
|
504
|
+
erase_to_start_of_line!
|
505
|
+
when 2 # Erase all
|
506
|
+
erase_line!
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
# ESC [ Ps A
|
511
|
+
def t_cursor_up(fsm)
|
512
|
+
count = parse_csi_params(fsm.input_sequence)[0] || 0
|
513
|
+
count = 1 if count < 1
|
514
|
+
count.times { cursor_up! }
|
515
|
+
end
|
516
|
+
|
517
|
+
# ESC [ Ps B
|
518
|
+
def t_cursor_down(fsm)
|
519
|
+
count = parse_csi_params(fsm.input_sequence)[0] || 0
|
520
|
+
count = 1 if count < 1
|
521
|
+
count.times { cursor_down! }
|
522
|
+
end
|
523
|
+
|
524
|
+
# ESC [ Ps C
|
525
|
+
def t_cursor_right(fsm)
|
526
|
+
count = parse_csi_params(fsm.input_sequence)[0] || 0
|
527
|
+
count = 1 if count < 1
|
528
|
+
count.times { cursor_right! }
|
529
|
+
end
|
530
|
+
|
531
|
+
# ESC [ Ps D
|
532
|
+
def t_cursor_left(fsm)
|
533
|
+
count = parse_csi_params(fsm.input_sequence)[0] || 0
|
534
|
+
count = 1 if count < 1
|
535
|
+
count.times { cursor_left! }
|
536
|
+
end
|
537
|
+
|
538
|
+
# ESC [ Ps ; Ps H
|
539
|
+
def t_cursor_position(fsm)
|
540
|
+
row, column = parse_csi_params(fsm.input_sequence)
|
541
|
+
row ||= 0; column ||= 0 # missing params set to 0
|
542
|
+
row -= 1; column -= 1
|
543
|
+
row = 0 if row < 0
|
544
|
+
column = 0 if column < 0
|
545
|
+
row = @height-1 if row >= @height
|
546
|
+
column = @width-1 if column >= @width
|
547
|
+
@cursor.pos = [row, column]
|
548
|
+
end
|
549
|
+
|
550
|
+
# Select graphic rendition
|
551
|
+
# ESC [ Pm m
|
552
|
+
def t_sgr(fsm)
|
553
|
+
params = parse_csi_params(fsm.input_sequence)
|
554
|
+
params.each do |param|
|
555
|
+
if param.nil?
|
556
|
+
# ignore
|
557
|
+
elsif param >= 30 and param <= 39
|
558
|
+
# TODO - Set foreground colour
|
559
|
+
elsif param >= 40 and param <= 49
|
560
|
+
# TODO - Set background colour
|
561
|
+
else
|
562
|
+
# ignore
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
# ESC [ Ps c
|
568
|
+
def t_send_device_attributes_primary(fsm) end # XXX TODO - respond with ESC [ ? ...
|
569
|
+
|
570
|
+
# ESC [ > Ps c
|
571
|
+
def t_send_device_attributes_secondary(fsm) end # XXX TODO - respond with ESC [ ? ...
|
572
|
+
|
573
|
+
# ESC [ Ps L
|
574
|
+
def t_insert_lines(fsm)
|
575
|
+
count = parse_csi_params(fsm.input_sequence)[0] || 1
|
576
|
+
insert_blank_lines!(count)
|
577
|
+
end
|
578
|
+
|
579
|
+
# ESC [ Ps M
|
580
|
+
def t_delete_lines(fsm)
|
581
|
+
count = parse_csi_params(fsm.input_sequence)[0] || 1
|
582
|
+
delete_lines!(count)
|
583
|
+
end
|
584
|
+
|
585
|
+
# ESC [ Ps P
|
586
|
+
def t_delete_characters(fsm)
|
587
|
+
count = parse_csi_params(fsm.input_sequence)[0] || 1
|
588
|
+
delete_characters!(count)
|
589
|
+
end
|
590
|
+
|
591
|
+
# ESC [ Ps g
|
592
|
+
def t_tab_clear(fsm) end # TODO
|
593
|
+
|
594
|
+
# ESC H
|
595
|
+
def t_tab_set(fsm) end # TODO
|
596
|
+
|
597
|
+
# ESC =
|
598
|
+
def t_application_keypad(fsm) end # TODO
|
599
|
+
|
600
|
+
# ESC >
|
601
|
+
def t_normal_keypad(fsm) end # TODO
|
602
|
+
|
603
|
+
def t_telnet_will(fsm) end # TODO
|
604
|
+
def t_telnet_wont(fsm) end # TODO
|
605
|
+
def t_telnet_do(fsm) end # TODO
|
606
|
+
def t_telnet_dont(fsm) end # TODO
|
607
|
+
def t_telnet_subnegotiation(fsm) end # TODO
|
608
|
+
|
609
|
+
def t_osc_set_text_params(fsm) end # TODO - used for setting window title, etc.
|
610
|
+
|
611
|
+
# ESC [ ... h
|
612
|
+
def t_set_mode(fsm)
|
613
|
+
parse_csi_params(fsm.input_sequence).each do |mode|
|
614
|
+
case mode
|
615
|
+
when 4 # Insert mode
|
616
|
+
@flags[:insert_mode] = true
|
617
|
+
else
|
618
|
+
return error("unknown set mode (escape sequence: #{fsm.input_sequence.inspect})")
|
619
|
+
end
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# ESC >
|
624
|
+
def t_reset_mode(fsm)
|
625
|
+
parse_csi_params(fsm.input_sequence).each do |mode|
|
626
|
+
case mode
|
627
|
+
when 4 # Replace mode
|
628
|
+
@flags[:insert_mode] = false
|
629
|
+
else
|
630
|
+
return error("unknown reset mode (escape sequence: #{fsm.input_sequence.inspect})")
|
631
|
+
end
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
# Parse ANSI/DEC CSI escape sequence parameters. Pass in fsm.input_sequence
|
636
|
+
#
|
637
|
+
# Example:
|
638
|
+
# parse_csi_params("\e[H") # returns []
|
639
|
+
# parse_csi_params("\e[;H") # returns []
|
640
|
+
# parse_csi_params("\e[2J") # returns [2]
|
641
|
+
# parse_csi_params("\e[33;42;0m") # returns [33, 42, 0]
|
642
|
+
# parse_csi_params(["\e", "[", "3", "3", ";", "4" "2", ";" "0", "m"]) # same as above, but takes an array
|
643
|
+
#
|
644
|
+
# This also works with DEC escape sequences:
|
645
|
+
# parse_csi_params("\e[?1;2J") # returns [1,2]
|
646
|
+
def parse_csi_params(input_seq) # TODO - test this
|
647
|
+
seq = input_seq.join if input_seq.respond_to?(:join) # Convert array to string
|
648
|
+
unless seq =~ /\A\e\[\??([\d;]*)[^\d]\Z/n
|
649
|
+
raise "BUG"
|
650
|
+
end
|
651
|
+
$1.split(/;/n).map{|p|
|
652
|
+
if p.empty?
|
653
|
+
nil
|
654
|
+
else
|
655
|
+
p.to_i
|
656
|
+
end
|
657
|
+
}
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# = Generic interface to terminal emulators
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
module ScripTTY
|
20
|
+
module Term
|
21
|
+
TERMINAL_TYPES = {
|
22
|
+
"dg410" => {:require => "scriptty/term/dg410", :class_name => "::ScripTTY::Term::DG410"},
|
23
|
+
"xterm" => {:require => "scriptty/term/xterm", :class_name => "::ScripTTY::Term::XTerm"},
|
24
|
+
}
|
25
|
+
|
26
|
+
# Load and instantiate the specified terminal by name
|
27
|
+
def self.new(name, *args, &block)
|
28
|
+
self.class_by_name(name).new(*args, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Load the specified terminal class by name
|
32
|
+
def self.class_by_name(name)
|
33
|
+
tt = TERMINAL_TYPES[name]
|
34
|
+
return nil unless tt
|
35
|
+
require tt[:require]
|
36
|
+
eval(tt[:class_name])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|