scriptty 0.5.0-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|