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,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