scriptty 0.5.0-java

Sign up to get free protection for your applications and to get access to all the features.
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,398 @@
1
+ # = Generator for screen pattern files
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
+ require 'multibyte'
20
+ require 'set'
21
+ require 'scriptty/screen_pattern'
22
+ require 'scriptty/screen_pattern/parser'
23
+
24
+ module ScripTTY
25
+ class ScreenPattern # reopen
26
+ class Generator
27
+ class <<self
28
+ # Generate a screen pattern from a specification
29
+ #
30
+ # Options:
31
+ # [:force_fields]
32
+ # If true, the fields will be positioned in-line, even if there is
33
+ # matching text or cursor there. :force_fields takes precedence over :force_cursor.
34
+ # [:force_cursor]
35
+ # If true, the cursor will be positioned in-line, even if there is
36
+ # matching text or fields there. :force_cursor may also be a
37
+ # Regexp, in which case the regexp must match in order for the field
38
+ # to be replaced. :force_fields takes precedence over :force_cursor.
39
+ # [:ignore]
40
+ # If specified, this is an array of [row, col0..col1] ranges.
41
+ def generate(name, properties_and_options={})
42
+ new(name, properties_and_options).generate
43
+ end
44
+ protected :new # Users should not instantiate this object directly
45
+ end
46
+
47
+ IGNORE_CHAR_CHOICES = ['.', '~', "'", "^", "-", "?", " ", "░"]
48
+ CURSOR_CHAR_CHOICES = ["@", "+", "&", "█"]
49
+ FIELD_CHAR_CHOICES = ["#", "*", "%", "=", "_", "◆"]
50
+
51
+ def initialize(name, properties={})
52
+ properties = properties.dup
53
+ @force_cursor = properties.delete(:force_cursor)
54
+ @force_fields = properties.delete(:force_fields)
55
+ @ignore = properties.delete(:ignore)
56
+ load_spec(name, properties)
57
+ make_grid
58
+ end
59
+
60
+ def generate
61
+ @out = []
62
+ @out << "[#{@name}]"
63
+ @out << "position: #{encode_tuple(@position)}" if @position and @position != [0,0]
64
+ @out << "size: #{encode_tuple(@size)}"
65
+ if @char_cursor
66
+ @out << "char_cursor: #{encode_string(@char_cursor)}"
67
+ elsif @cursor_pos
68
+ @out << "cursor_pos: #{encode_tuple(@cursor_pos)}"
69
+ end
70
+ @out << "char_ignore: #{encode_string(@char_ignore)}" if @char_ignore
71
+ @out << "char_field: #{encode_string(@char_field)}" if @char_field
72
+ if @explicit_fields
73
+ @out << "fields: <<END"
74
+ @explicit_fields.each { |f|
75
+ @out << " #{encode_tuple(f)}"
76
+ }
77
+ @out << "END"
78
+ end
79
+ if @text_lines
80
+ @out << "text: <<END"
81
+ @out += @text_lines
82
+ @out << "END"
83
+ end
84
+ @out.map{|line| "#{line}\n"}.join
85
+ end
86
+
87
+ private
88
+
89
+ def make_grid
90
+ # Initialize grid as 2D array of nils
91
+ height, width = @size
92
+ grid = (1..height).map { [nil] * width }
93
+
94
+ # Fill in matches
95
+ if @matches
96
+ @matches.each do |pos, string|
97
+ row, col = pos
98
+ string.chars.to_a.each do |char|
99
+ raise ArgumentError.new("overlapping match: #{[pos, string].inspect}") if grid[row][col]
100
+ grid[row][col] = char
101
+ col += 1
102
+ end
103
+ end
104
+ end
105
+
106
+ # Fill in ignore overrides
107
+ if @ignore
108
+ @ignore.each do |row, col_range|
109
+ row, col_range = rel_pos([row, col_range])
110
+ col_range.each do |col|
111
+ grid[row][col] = nil
112
+ end
113
+ end
114
+ end
115
+
116
+ # Fill in fields, possibly overwriting matches
117
+ if @fields
118
+ @explicit_fields = []
119
+ @implicit_fields_by_row = {}
120
+ @fields.each_with_index do |f, i|
121
+ name, row, col_range = f
122
+ first_col = col_range.first
123
+ explicit = false
124
+ if first_col > 0 and grid[row][first_col-1] == :field # adjacent fields
125
+ explicit = true
126
+ elsif !@force_fields
127
+ col_range.each do |col|
128
+ if grid[row][col]
129
+ explicit = true
130
+ break
131
+ end
132
+ end
133
+ end
134
+ if explicit
135
+ @explicit_fields << [name, [row, first_col], col_range.count] # [name, pos, length]
136
+ else
137
+ @implicit_fields_by_row[row] ||= []
138
+ @implicit_fields_by_row[row] << name
139
+ col_range.each do |col|
140
+ grid[row][col] = :field
141
+ end
142
+ end
143
+ end
144
+ @explicit_fields = nil if @explicit_fields.empty?
145
+ @implicit_fields_by_row = nil if @implicit_fields_by_row.empty?
146
+ end
147
+
148
+ # Fill in the cursor, possibly overwriting matches (but never fields)
149
+ if @cursor_pos
150
+ row, col = @cursor_pos
151
+ if !grid[row][col]
152
+ grid[row][col] = :cursor
153
+ elsif @force_cursor and grid[row][col] != :field and (!@force_cursor.is_a?(Regexp) or @force_cursor =~ grid[row][col])
154
+ grid[row][col] = :cursor
155
+ end
156
+ end
157
+
158
+ # Walk the grid, checking for nil, :field, and :cursor. We won't
159
+ # generate characters for ones that aren't present.
160
+ has_ignore = has_field = has_cursor = false
161
+ height.times do |row|
162
+ width.times do |col|
163
+ if grid[row][col].nil?
164
+ has_ignore = true
165
+ elsif grid[row][col] == :field
166
+ has_field = true
167
+ elsif grid[row][col] == :cursor
168
+ has_cursor = true
169
+ end
170
+ end
171
+ end
172
+ @char_ignore = nil unless has_ignore
173
+ @char_field = nil unless has_field
174
+ @char_cursor = nil unless has_cursor
175
+
176
+ # Determine which characters were already used
177
+ @used_chars = Set.new
178
+ @used_chars << @char_ignore if @char_ignore
179
+ @used_chars << @char_field if @char_field
180
+ @used_chars << @char_cursor if @char_cursor
181
+ height.times do |row|
182
+ width.times do |col|
183
+ if grid[row][col] and grid[row][col].is_a?(String)
184
+ @used_chars << grid[row][col]
185
+ end
186
+ end
187
+ end
188
+
189
+ # Choose a character to represent ignored positions
190
+ if has_ignore and !@char_ignore
191
+ IGNORE_CHAR_CHOICES.each do |char|
192
+ next if @used_chars.include?(char)
193
+ @char_ignore = char
194
+ break
195
+ end
196
+ raise ArgumentError.new("Could not auto-select char_ignore") unless @char_ignore
197
+ @used_chars << @char_ignore
198
+ end
199
+
200
+ # Choose a character to represent the cursor
201
+ if has_cursor and !@char_cursor
202
+ CURSOR_CHAR_CHOICES.each do |char|
203
+ next if @used_chars.include?(char)
204
+ @char_cursor = char
205
+ break
206
+ end
207
+ raise ArgumentError.new("Could not auto-select char_cursor") unless @char_cursor
208
+ @used_chars << @char_cursor
209
+ end
210
+
211
+ # Choose a character to represent fields
212
+ if has_field and !@char_field
213
+ FIELD_CHAR_CHOICES.each do |char|
214
+ next if @used_chars.include?(char)
215
+ @char_field = char
216
+ break
217
+ end
218
+ raise ArgumentError.new("Could not auto-select char_field") unless @char_field
219
+ @used_chars << @char_field
220
+ end
221
+
222
+ # Walk the grid and fill in the placeholders
223
+ height.times do |row|
224
+ width.times do |col|
225
+ if grid[row][col].nil?
226
+ grid[row][col] = @char_ignore
227
+ elsif grid[row][col] == :field
228
+ grid[row][col] = @char_field
229
+ elsif grid[row][col] == :cursor
230
+ grid[row][col] = @char_cursor
231
+ elsif !grid[row][col].is_a?(String)
232
+ raise "BUG: grid[#{row}][#{col}] #{grid[row][col].inspect}"
233
+ elsif [@char_cursor, @char_field].include?(grid[row][col])
234
+ grid[row][col] = @char_ignore
235
+ end
236
+ end
237
+ end
238
+
239
+ @text_lines = []
240
+ @text_lines << "+" + "-"*width + "+"
241
+ height.times do |row|
242
+ grid_row = "|" + grid[row].join + "|"
243
+ if @implicit_fields_by_row and @implicit_fields_by_row[row]
244
+ grid_row += " " + encode_tuple(@implicit_fields_by_row[row])
245
+ end
246
+ @text_lines << grid_row
247
+ end
248
+ @text_lines << "+" + "-"*width + "+"
249
+ end
250
+
251
+ def encode_tuple(t)
252
+ "(" + t.map{|v|
253
+ if v.is_a?(Integer)
254
+ v.to_s
255
+ elsif v.is_a?(String)
256
+ encode_string(v)
257
+ elsif v.is_a?(Array)
258
+ encode_tuple(v)
259
+ else
260
+ raise "BUG: encode_tuple(#{t.inspect}) on #{v.inspect}"
261
+ end
262
+ }.join(", ") + ")"
263
+ end
264
+
265
+ def encode_string(v)
266
+ r = ['"']
267
+ v.split("").each { |c|
268
+ if c =~ /\A#{Parser::STR_UNESCAPED}\Z/no
269
+ r << c
270
+ else
271
+ r << sprintf("\\%03o", c.unpack("C*")[0])
272
+ end
273
+ }
274
+ r << '"'
275
+ r.join
276
+ end
277
+
278
+ def load_spec(name, properties)
279
+ properties = properties.dup
280
+ properties.keys.each do |k| # Replace symbol keys with strings, and remove false/nil properties.
281
+ if properties[k]
282
+ properties[k.to_s] = properties.delete(k)
283
+ else
284
+ properties.delete(k)
285
+ end
286
+ end
287
+
288
+ # Check name
289
+ raise ArgumentError.new("illegal name") unless name =~ /\A#{Parser::IDENTIFIER}\Z/no
290
+ @name = name
291
+
292
+ # position [row, column]
293
+ if properties["position"]
294
+ @position = properties.delete("position").dup
295
+ unless @position.is_a?(Array) and @position.length == 2 and @position.map{|v| v.is_a?(Integer) and v >= 0}.all?
296
+ raise ArgumentError.new("bad 'position' entry: #{@position.inspect}")
297
+ end
298
+ end
299
+
300
+ # size [rows, columns]
301
+ if properties["size"]
302
+ @size = properties.delete("size").dup
303
+ unless @size.is_a?(Array) and @size.length == 2 and @size.map{|v| v.is_a?(Integer) and v >= 0}.all?
304
+ raise ArgumentError.new("bad 'size' entry: #{@size.inspect}")
305
+ end
306
+ else
307
+ raise ArgumentError.new("'size' is required")
308
+ end
309
+
310
+ # cursor_pos [row, column]
311
+ if properties["cursor_pos"]
312
+ @cursor_pos = properties.delete("cursor_pos")
313
+ unless @cursor_pos.is_a?(Array) and @cursor_pos.length == 2 and @cursor_pos.map{|n| n.is_a?(Integer)}.all?
314
+ raise ArgumentError.new("Illegal cursor_pos")
315
+ end
316
+ @cursor_pos = rel_pos(@cursor_pos)
317
+ unless @cursor_pos[0] >= 0 and @cursor_pos[1] >= 0
318
+ raise ArgumentError.new("cursor_pos out of range")
319
+ end
320
+ end
321
+
322
+ # fields {"name" => [row, column_range]}
323
+ if properties["fields"]
324
+ fields = []
325
+ pfields = {}
326
+ properties.delete("fields").each_pair do |name, range|
327
+ pfields[name.to_s] = range # convert symbols to strings
328
+ end
329
+ pfields.each_pair do |name, range|
330
+ unless range.is_a?(Array) and range.length == 2 and range[0].is_a?(Integer) and range[1].is_a?(Range)
331
+ raise ArgumentError.new("field #{name.inspect} should be [row, col0..col1], not #{range.inspect}")
332
+ end
333
+ row, col_range = range
334
+ unless col_range.first >= 0 and col_range.last >= 0
335
+ raise ArgumentError.new("field #{name.inspect} should contain positive column range, not #{col_range.inspect}")
336
+ end
337
+ row, col_range = rel_pos([row, col_range])
338
+ fields << [name, row, col_range]
339
+ end
340
+ @fields = fields.sort{|a,b| [a[1], a[2].first] <=> [b[1], b[2].first]} # sort fields in screen order
341
+ @fields = nil if @fields.empty?
342
+ end
343
+
344
+ # matches [pos, string]
345
+ if properties["matches"]
346
+ @matches = []
347
+ properties.delete("matches").each do |m|
348
+ unless (m.is_a?(Array) and m.length == 2 and m[0].is_a?(Array) and
349
+ m[0].length == 2 and m[0].map{|v| v.is_a?(Integer)}.all? and
350
+ m[1].is_a?(String))
351
+ raise ArgumentError.new("bad 'matches' entry: #{m.inspect}")
352
+ end
353
+ pos, string = m
354
+ pos = rel_pos(pos)
355
+ unless pos[0] >= 0 and pos[1] >= 0
356
+ raise ArgumentError.new("'matches' entry out of range: #{m.inspect}")
357
+ end
358
+ @matches << [pos, string]
359
+ end
360
+ @matches.sort!{|a,b| a[0] <=> b[0]} # sort matches in screen order
361
+ end
362
+
363
+ if properties['char_cursor']
364
+ @char_cursor = properties.delete("char_cursor")
365
+ @char_cursor = Multibyte::Chars.new(@char_cursor).normalize(:c).to_a.join # Unicode Normalization Form C (NFC)
366
+ raise ArgumentError.new("char_cursor must be 1 character") unless @char_cursor.chars.to_a.length == 1
367
+ end
368
+
369
+ if properties['char_field']
370
+ @char_field = properties.delete("char_field")
371
+ @char_field = Multibyte::Chars.new(@char_field).normalize(:c).to_a.join # Unicode Normalization Form C (NFC)
372
+ raise ArgumentError.new("char_field must be 1 character") unless @char_field.chars.to_a.length == 1
373
+ raise ArgumentError.new("char_field conflicts with char_cursor") if @char_field == @char_cursor
374
+ end
375
+
376
+ if properties['char_ignore']
377
+ @char_ignore = properties.delete("char_ignore")
378
+ @char_ignore = Multibyte::Chars.new(@char_ignore).normalize(:c).to_a.join # Unicode Normalization Form C (NFC)
379
+ raise ArgumentError.new("char_ignore must be 1 character") unless @char_ignore.chars.to_a.length == 1
380
+ raise ArgumentError.new("char_ignore conflicts with char_cursor") if @char_ignore == @char_cursor
381
+ raise ArgumentError.new("char_ignore conflicts with char_field") if @char_ignore == @char_field
382
+ end
383
+
384
+ raise ArgumentError.new("extraneous properties: #{properties.keys.inspect}") unless properties.empty?
385
+ end
386
+
387
+ # Convert an absolute [row,column] or [row, col1..col2] into a relative position or range.
388
+ def rel_pos(absolute_pos)
389
+ screen_pos = @position || [0,0]
390
+ if absolute_pos[1].is_a?(Range)
391
+ [absolute_pos[0]-screen_pos[0], absolute_pos[1].first-screen_pos[1]..absolute_pos[1].last-screen_pos[1]]
392
+ else
393
+ [absolute_pos[0]-screen_pos[0], absolute_pos[1]-screen_pos[1]]
394
+ end
395
+ end
396
+ end
397
+ end
398
+ end