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.
Files changed (69) hide show
  1. data/.gitattributes +1 -0
  2. data/.gitignore +3 -0
  3. data/COPYING +674 -0
  4. data/COPYING.LESSER +165 -0
  5. data/README.rdoc +31 -0
  6. data/Rakefile +49 -0
  7. data/VERSION +1 -0
  8. data/bin/scriptty-capture +5 -0
  9. data/bin/scriptty-dump-screens +4 -0
  10. data/bin/scriptty-replay +5 -0
  11. data/bin/scriptty-term-test +4 -0
  12. data/bin/scriptty-transcript-parse +4 -0
  13. data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
  14. data/examples/captures/xterm-vim-session.bin +262 -0
  15. data/examples/demo-capture.rb +19 -0
  16. data/examples/telnet-nego.rb +55 -0
  17. data/lib/scriptty/apps/capture_app/console.rb +104 -0
  18. data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
  19. data/lib/scriptty/apps/capture_app.rb +213 -0
  20. data/lib/scriptty/apps/dump_screens_app.rb +166 -0
  21. data/lib/scriptty/apps/replay_app.rb +229 -0
  22. data/lib/scriptty/apps/term_test_app.rb +124 -0
  23. data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
  24. data/lib/scriptty/cursor.rb +39 -0
  25. data/lib/scriptty/exception.rb +38 -0
  26. data/lib/scriptty/expect.rb +392 -0
  27. data/lib/scriptty/multiline_buffer.rb +192 -0
  28. data/lib/scriptty/net/event_loop.rb +610 -0
  29. data/lib/scriptty/screen_pattern/generator.rb +398 -0
  30. data/lib/scriptty/screen_pattern/parser.rb +558 -0
  31. data/lib/scriptty/screen_pattern.rb +104 -0
  32. data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
  33. data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
  34. data/lib/scriptty/term/dg410/parser.rb +162 -0
  35. data/lib/scriptty/term/dg410.rb +489 -0
  36. data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
  37. data/lib/scriptty/term/xterm.rb +661 -0
  38. data/lib/scriptty/term.rb +40 -0
  39. data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
  40. data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
  41. data/lib/scriptty/util/fsm.rb +177 -0
  42. data/lib/scriptty/util/transcript/reader.rb +96 -0
  43. data/lib/scriptty/util/transcript/writer.rb +111 -0
  44. data/test/apps/capture_app_test.rb +123 -0
  45. data/test/apps/transcript_parse_app_test.rb +118 -0
  46. data/test/cursor_test.rb +51 -0
  47. data/test/fsm_definition_parser_test.rb +220 -0
  48. data/test/fsm_test.rb +322 -0
  49. data/test/multiline_buffer_test.rb +275 -0
  50. data/test/net/event_loop_test.rb +402 -0
  51. data/test/screen_pattern/generator_test.rb +408 -0
  52. data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
  53. data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
  54. data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
  55. data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
  56. data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
  57. data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
  58. data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
  59. data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
  60. data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
  61. data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
  62. data/test/screen_pattern/parser_test.rb +266 -0
  63. data/test/term/dg410/parser_test.rb +139 -0
  64. data/test/term/xterm_test.rb +327 -0
  65. data/test/test_helper.rb +3 -0
  66. data/test/util/transcript/reader_test.rb +131 -0
  67. data/test/util/transcript/writer_test.rb +126 -0
  68. data/test.watchr +29 -0
  69. 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
+