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,392 @@
1
+ # = Expect object
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 'scriptty/exception'
20
+ require 'scriptty/net/event_loop'
21
+ require 'scriptty/term'
22
+ require 'scriptty/screen_pattern'
23
+ require 'scriptty/util/transcript/writer'
24
+ require 'set'
25
+
26
+ module ScripTTY
27
+ class Expect
28
+
29
+ # Methods to export to Evaluator
30
+ EXPORTED_METHODS = Set.new [:init_term, :term, :connect, :screen, :expect, :on, :wait, :send, :match, :push_patterns, :pop_patterns, :exit, :eval_script_file, :eval_script_inline, :sleep, :set_timeout, :load_screens ]
31
+
32
+ attr_reader :term # The terminal emulation object
33
+
34
+ attr_reader :match # The last non-background expect match. For a ScreenPattern match, this is a Hash of fields. For a String or Regexp match, this is a MatchData object.
35
+
36
+ attr_accessor :transcript_writer # Set this to an instance of ScripTTY::Util::Transcript::Writer
37
+
38
+ # Initialize the Expect object.
39
+ def initialize(options={})
40
+ @net = ScripTTY::Net::EventLoop.new
41
+ @suspended = false
42
+ @effective_patterns = nil
43
+ @term_name = nil
44
+ @effective_patterns = [] # Array of PatternHandle objects
45
+ @pattern_stack = []
46
+ @wait_finished = false
47
+ @evaluator = Evaluator.new(self)
48
+ @match_buffer = ""
49
+ @timeout = nil
50
+ @timeout_timer = nil
51
+ @transcript_writer = options[:transcript_writer]
52
+ @screen_patterns = {}
53
+ end
54
+
55
+ def set_timeout(seconds)
56
+ raise ArgumentError.new("argument to set_timeout must be Numeric or nil") unless seconds.is_a?(Numeric) or seconds.nil?
57
+ if seconds
58
+ @timeout = seconds.to_f
59
+ else
60
+ @timeout = nil
61
+ end
62
+ refresh_timeout
63
+ nil
64
+ end
65
+
66
+ # Load and evaluate a script from a file.
67
+ def eval_script_file(path)
68
+ eval_script_inline(File.read(path), path)
69
+ end
70
+
71
+ # Evaluate a script specified as a string.
72
+ def eval_script_inline(str, filename=nil, lineno=nil)
73
+ @evaluator.instance_eval(str, filename || "(inline)", lineno || 1)
74
+ end
75
+
76
+ # Initialize a terminal emulator.
77
+ #
78
+ # If a name is specified, use that terminal type. Otherwise, use the
79
+ # previous terminal type.
80
+ def init_term(name=nil)
81
+ @transcript_writer.info("Script executing command", "init_term", name || "") if @transcript_writer
82
+ name ||= @term_name
83
+ @term_name = name
84
+ raise ArgumentError.new("No previous terminal specified") unless name
85
+ without_timeout {
86
+ @term = ScripTTY::Term.new(name)
87
+ @term.on_unknown_sequence do |seq|
88
+ @transcript_writer.info("Unknown escape sequence", seq) if @transcript_writer
89
+ end
90
+ }
91
+ nil
92
+ end
93
+
94
+ # Connect to the specified address. Return true if the connection was
95
+ # successful. Otherwise, raise an exception.
96
+ def connect(remote_address)
97
+ @transcript_writer.info("Script executing command", "connect", *remote_address.map{|a| a.inspect}) if @transcript_writer
98
+ connected = false
99
+ connect_error = nil
100
+ @conn = @net.connect(remote_address) do |c|
101
+ c.on_connect { connected = true; handle_connect; @net.suspend }
102
+ c.on_connect_error { |e| connect_error = e; @net.suspend }
103
+ c.on_receive_bytes { |bytes| handle_receive_bytes(bytes) }
104
+ c.on_close { @conn = nil; handle_connection_close }
105
+ end
106
+ dispatch until connected or connect_error or @net.done?
107
+ if connect_error
108
+ transcribe_connect_error(connect_error)
109
+ raise ScripTTY::Exception::ConnectError.new(connect_error)
110
+ end
111
+ refresh_timeout
112
+ connected
113
+ end
114
+
115
+ # Add the specified pattern to the effective pattern list.
116
+ #
117
+ # Return the PatternHandle for the pattern.
118
+ #
119
+ # Options:
120
+ # [:continue]
121
+ # If true, matching this pattern will not cause the wait method to
122
+ # return.
123
+ def on(pattern, opts={}, &block)
124
+ case pattern
125
+ when String
126
+ @transcript_writer.info("Script executing command", "on", "String", pattern.inspect) if @transcript_writer
127
+ ph = PatternHandle.new(/#{Regexp.escape(pattern)}/n, block, opts[:background])
128
+ when Regexp
129
+ @transcript_writer.info("Script executing command", "on", "Regexp", pattern.inspect) if @transcript_writer
130
+ if pattern.kcode == "none"
131
+ ph = PatternHandle.new(pattern, block, opts[:background])
132
+ else
133
+ ph = PatternHandle.new(/#{pattern}/n, block, opts[:background])
134
+ end
135
+ when ScreenPattern
136
+ @transcript_writer.info("Script executing command", "on", "ScreenPattern", pattern.name, opts[:background] ? "BACKGROUND" : "") if @transcript_writer
137
+ ph = PatternHandle.new(pattern, block, opts[:background])
138
+ else
139
+ raise TypeError.new("Unsupported pattern type: #{pattern.class.inspect}")
140
+ end
141
+ @effective_patterns << ph
142
+ ph
143
+ end
144
+
145
+ # Sleep for the specified number of seconds
146
+ def sleep(seconds)
147
+ @transcript_writer.info("Script executing command", "sleep", seconds.inspect) if @transcript_writer
148
+ sleep_done = false
149
+ @net.timer(seconds) { sleep_done = true ; @net.suspend }
150
+ dispatch until sleep_done
151
+ refresh_timeout
152
+ nil
153
+ end
154
+
155
+ # Return the named ScreenPattern (or nil if no such pattern exists)
156
+ def screen(name)
157
+ @screen_patterns[name.to_sym]
158
+ end
159
+
160
+ # Load screens from the specified filenames
161
+ def load_screens(filenames_or_glob)
162
+ if filenames_or_glob.is_a?(String)
163
+ filenames = Dir.glob(filenames_or_glob)
164
+ elsif filenames_or_glob.is_a?(Array)
165
+ filenames = filenames_or_glob
166
+ else
167
+ raise ArgumentError.new("load_screens takes a string(glob) or an array, not #{filenames.class.name}")
168
+ end
169
+ filenames.each do |filename|
170
+ ScreenPattern.parse(File.read(filename)).each do |pattern|
171
+ @screen_patterns[pattern.name.to_sym] = pattern
172
+ end
173
+ end
174
+ nil
175
+ end
176
+
177
+ # Convenience function.
178
+ #
179
+ # == Examples
180
+ # # Wait for a single pattern to match.
181
+ # expect("login: ")
182
+ #
183
+ # # Wait for one of several patterns to match.
184
+ # expect {
185
+ # on("login successful") { ... }
186
+ # on("login incorrect") { ... }
187
+ # }
188
+ def expect(pattern=nil)
189
+ raise ArgumentError.new("no pattern and no block given") if !pattern and !block_given?
190
+ @transcript_writer.info("Script expect block BEGIN") if @transcript_writer and block_given?
191
+ push_patterns
192
+ begin
193
+ on(pattern) if pattern
194
+ yield if block_given?
195
+ wait
196
+ ensure
197
+ pop_patterns
198
+ @transcript_writer.info("Script expect block END") if @transcript_writer and block_given?
199
+ end
200
+ end
201
+
202
+ # Push a copy of the effective pattern list to an internal stack.
203
+ def push_patterns
204
+ @pattern_stack << @effective_patterns.dup
205
+ end
206
+
207
+ # Pop the effective pattern list from the stack.
208
+ def pop_patterns
209
+ raise ArgumentError.new("pattern stack empty") if @pattern_stack.empty?
210
+ @effective_patterns = @pattern_stack.pop
211
+ end
212
+
213
+ # Wait for an effective pattern to match.
214
+ #
215
+ # Clears the character-match buffer on return.
216
+ def wait
217
+ @transcript_writer.info("Script executing command", "wait") if @transcript_writer
218
+ check_expect_match unless @wait_finished
219
+ dispatch until @wait_finished
220
+ refresh_timeout
221
+ @wait_finished = false
222
+ nil
223
+ end
224
+
225
+ # Send bytes to the remote application.
226
+ #
227
+ # NOTE: This method returns immediately, even if not all the bytes are
228
+ # finished being sent. Remaining bytes will be sent during an expect,
229
+ # wait, or sleep call.
230
+ def send(bytes)
231
+ @transcript_writer.from_client(bytes) if @transcript_writer
232
+ @conn.write(bytes)
233
+ true
234
+ end
235
+
236
+ # Close the connection and exit.
237
+ def exit
238
+ @transcript_writer.info("Script executing command", "exit") if @transcript_writer
239
+ @net.exit
240
+ dispatch until @net.done?
241
+ @transcript_writer.close if @transcript_writer
242
+ end
243
+
244
+ private
245
+
246
+ # Kick the watchdog timer
247
+ def refresh_timeout
248
+ disable_timeout
249
+ enable_timeout
250
+ end
251
+
252
+ def without_timeout
253
+ raise ArgumentError.new("no block given") unless block_given?
254
+ disable_timeout
255
+ begin
256
+ yield
257
+ ensure
258
+ enable_timeout
259
+ end
260
+ end
261
+
262
+ # Disable timeout handling
263
+ def disable_timeout
264
+ if @timeout_timer
265
+ @timeout_timer.cancel
266
+ @timeout_timer = nil
267
+ end
268
+ nil
269
+ end
270
+
271
+ # Enable timeout handling (if @timeout is set)
272
+ def enable_timeout
273
+ if @timeout
274
+ @timeout_timer = @net.timer(@timeout, :daemon=>true) { raise ScripTTY::Exception::Timeout.new("Operation timed out") }
275
+ end
276
+ nil
277
+ end
278
+
279
+ # Re-enter the dispatch loop
280
+ def dispatch
281
+ if @suspended
282
+ @suspended = @net.resume
283
+ else
284
+ @suspended = @net.main
285
+ end
286
+ end
287
+
288
+ def handle_connection_close # XXX - we should raise an error when disconnected prematurely
289
+ @transcript_writer.server_close("connection closed") if @transcript_writer
290
+ self.exit
291
+ end
292
+
293
+ def handle_connect
294
+ @transcript_writer.server_open(*@conn.remote_address) if @transcript_writer
295
+ init_term
296
+ end
297
+
298
+ def transcribe_connect_error(e)
299
+ if @transcript_writer
300
+ @transcript_writer.info("Connect error", e.class.name, e.to_s, e.backtrace.join("\n"))
301
+ # Write the backtrace out as separate records, for the convenience of people reading the logs without a parser.
302
+ e.backtrace.each do |line|
303
+ @transcript_writer.info("Connect error backtrace", line)
304
+ end
305
+ end
306
+ end
307
+
308
+ def handle_receive_bytes(bytes)
309
+ @transcript_writer.from_server(bytes) if @transcript_writer
310
+ @match_buffer << bytes
311
+ @term.feed_bytes(bytes)
312
+ check_expect_match
313
+ end
314
+
315
+ # Check for a match.
316
+ #
317
+ # If there is a (non-background) match, set @wait_finished and return true. Otherwise, return false.
318
+ def check_expect_match
319
+ found = true
320
+ while found
321
+ found = false
322
+ @effective_patterns.each { |ph|
323
+ case ph.pattern
324
+ when Regexp
325
+ m = ph.pattern.match(@match_buffer)
326
+ @match_buffer = @match_buffer[m.end(0)..-1] if m # truncate match buffer
327
+ when ScreenPattern
328
+ m = ph.pattern.match_term(@term)
329
+ @match_buffer = "" if m # truncate match buffer if a screen matches
330
+ else
331
+ raise "BUG: pattern is #{ph.pattern.inspect}"
332
+ end
333
+
334
+ next unless m
335
+
336
+ # Matched - Invoke the callback
337
+ ph.callback.call(m) if ph.callback
338
+
339
+ # Make the next wait() call return
340
+ unless ph.background?
341
+ @match = m
342
+ @wait_finished = true
343
+ @net.suspend
344
+ return true
345
+ else
346
+ found = true
347
+ end
348
+ }
349
+ end
350
+ false
351
+ end
352
+
353
+ class Evaluator
354
+ def initialize(expect_object)
355
+ @_expect_object = expect_object
356
+ end
357
+
358
+ # Define proxy methods
359
+ EXPORTED_METHODS.each do |m|
360
+ # We would use define_method, but JRuby 1.4 doesn't support defining
361
+ # a block that takes a block. http://jira.codehaus.org/browse/JRUBY-4180
362
+ class_eval("def #{m}(*args, &block) @_expect_object.__send__(#{m.inspect}, *args, &block); end")
363
+ end
364
+ end
365
+
366
+ class PatternHandle
367
+ attr_reader :pattern
368
+ attr_reader :callback
369
+
370
+ def initialize(pattern, callback, background)
371
+ @pattern = pattern
372
+ @callback = callback
373
+ @background = background
374
+ end
375
+
376
+ def background?
377
+ @background
378
+ end
379
+ end
380
+
381
+ class Match
382
+ attr_reader :pattern_handle
383
+ attr_reader :result
384
+
385
+ def initialize(pattern_handle, result)
386
+ @pattern_handle = pattern_handle
387
+ @result = result
388
+ end
389
+ end
390
+
391
+ end
392
+ end
@@ -0,0 +1,192 @@
1
+ # = Multi-line character buffer
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
+ class MultilineBuffer
21
+ attr_reader :height # buffer height (in lines)
22
+ attr_reader :width # buffer width (in columns)
23
+ attr_reader :content # contents of the screen buffer (mainly for debugging/testing)
24
+ def initialize(height, width)
25
+ @height = height
26
+ @width = width
27
+ clear!
28
+ end
29
+
30
+ # Clear the buffer
31
+ def clear!
32
+ @content = [] # Array of mutable Strings
33
+ @height.times {
34
+ @content << " "*@width
35
+ }
36
+ nil
37
+ end
38
+
39
+ # Write a string (or array of strings) to the specified location.
40
+ #
41
+ # row & column are zero-based
42
+ #
43
+ # Returns the characters that were replaced
44
+ def replace_at(row, column, value)
45
+ if value.is_a?(Array)
46
+ orig = []
47
+ value.each_with_index do |string, i|
48
+ orig << replace_at(row+i, column, string)
49
+ end
50
+ orig
51
+ else
52
+ # value is a string
53
+ return nil if row < 0 or row >= height or column < 0 or column >= width # XXX should we raise an exception here?
54
+ orig = @content[row][column,value.length]
55
+ @content[row][column,value.length] = value
56
+ @content[row] = @content[row][0,width] # truncate to maximum width
57
+ orig
58
+ end
59
+ end
60
+
61
+ # Return characters starting at the specified location.
62
+ #
63
+ # The limit parameter specifies the maximum number of bytes to return.
64
+ # If limit is negative, then everything up to the end of the line is returned.
65
+ def get_at(row, column, limit=1)
66
+ return nil if row < 0 or row >= height or column < 0 or column >= width # XXX should we raise an exception here?
67
+ if limit >= 0
68
+ @content[row][column,limit]
69
+ else
70
+ @content[row][column..-1]
71
+ end
72
+ end
73
+
74
+ # Scroll the specified rectangle up by the specified number of lines.
75
+ # Return true.
76
+ def scroll_up_region(row0, col0, row1, col1, count)
77
+ scroll_region_vertical(:up, row0, col0, row1, col1, count)
78
+ end
79
+
80
+ # Scroll the specified rectangle down by the specified number of lines.
81
+ # Return true.
82
+ def scroll_down_region(row0, col0, row1, col1, count)
83
+ scroll_region_vertical(:down, row0, col0, row1, col1, count)
84
+ end
85
+
86
+ def scroll_left_region(row0, col0, row1, col1, count)
87
+ scroll_region_horizontal(:left, row0, col0, row1, col1, count)
88
+ end
89
+
90
+ def scroll_right_region(row0, col0, row1, col1, count)
91
+ scroll_region_horizontal(:right, row0, col0, row1, col1, count)
92
+ end
93
+
94
+ private
95
+
96
+ def scroll_region_vertical(direction, row0, col0, row1, col1, count) # :nodoc:
97
+ row0, col0, row1, col1, rect_height, rect_width = rect_helper(row0, col0, row1, col1)
98
+
99
+ # Clip the count to the rectangle height
100
+ count = [0, [rect_height, count].min].max
101
+
102
+ # Split the region into source and destination ranges
103
+ if direction == :up
104
+ dst_rows = (row0..row1-count)
105
+ src_rows = (row0+count..row1)
106
+ intermediate_rows = (dst_rows.end+1..src_rows.begin-1)
107
+ elsif direction == :down
108
+ src_rows = (row0..row1-count)
109
+ dst_rows = (row0+count..row1)
110
+ intermediate_rows = (src_rows.end+1..dst_rows.begin-1)
111
+ else
112
+ raise ArgumentError.new("Invalid direction #{direction.inspect}")
113
+ end
114
+
115
+ # Erase any rows that lie between the source and destination rows.
116
+ # If there are no such rows (e.g. if the source and destination rows
117
+ # overlap) this will do nothing.
118
+ intermediate_rows.each do |row|
119
+ replace_at(row, col0, " "*rect_width)
120
+ end
121
+
122
+ # Move the region that is <count> rows below the top of the rectangle to
123
+ # the top of the rectangle. If we're scrolling the entire region off the
124
+ # screen, this will do nothing.
125
+ z = src_rows.zip(dst_rows)
126
+ z.reverse! if direction == :down
127
+ z.each do |src_row, dst_row|
128
+ replace_at dst_row, col0, replace_at(src_row, col0, " "*rect_width)
129
+ end
130
+ nil
131
+ end
132
+
133
+ def scroll_region_horizontal(direction, row0, col0, row1, col1, count) # :nodoc:
134
+ row0, col0, row1, col1, rect_height, rect_width = rect_helper(row0, col0, row1, col1)
135
+
136
+ # Clip the count to the rectangle height
137
+ count = [0, [rect_width, count].min].max
138
+
139
+ # Split the region into source and destination ranges
140
+ if direction == :left
141
+ dst_cols = (col0..col1-count)
142
+ src_cols = (col0+count..col1)
143
+ intermediate_cols = (dst_cols.end+1..src_cols.begin-1)
144
+ elsif direction == :right
145
+ src_cols = (col0..col1-count)
146
+ dst_cols = (col0+count..col1)
147
+ intermediate_cols = (src_cols.end+1..dst_cols.begin-1)
148
+ else
149
+ raise ArgumentError.new("Invalid direction #{direction.inspect}")
150
+ end
151
+
152
+ # Erase any columns that lie between the source and destination columns.
153
+ # If there are no such columns (e.g. if the source and destination columns
154
+ # overlap) this will do nothing.
155
+ intermediate_width = intermediate_cols.end - intermediate_cols.begin + 1
156
+ if intermediate_width > 0
157
+ (row0..row1).each do |row|
158
+ replace_at(row, intermediate_cols.begin, " "*intermediate_width)
159
+ end
160
+ end
161
+
162
+ # Move the region that is <count> rows below the top of the rectangle to
163
+ # the top of the rectangle. If we're scrolling the entire region off the
164
+ # screen, this will do nothing.
165
+ move_width = src_cols.end - src_cols.begin + 1
166
+ if move_width > 0
167
+ (row0..row1).each do |row|
168
+ replace_at row, dst_cols.begin, replace_at(row, src_cols.begin, " "*move_width)
169
+ end
170
+ end
171
+ nil
172
+ end
173
+
174
+ def rect_helper(row0, col0, row1, col1) # :nodoc:
175
+ # Sort coordinates
176
+ row0, row1 = row1, row0 if row0 > row1
177
+ col0, col1 = col1, col0 if col0 > col1
178
+
179
+ # Clip the rectangle to the screen size
180
+ row0 = [0, [@height-1, row0].min].max
181
+ col0 = [0, [@width-1, col0].min].max
182
+ row1 = [0, [@height-1, row1].min].max
183
+ col1 = [0, [@width-1, col1].min].max
184
+
185
+ # Determine the height and width of the rectangle
186
+ rect_height = row1 - row0 + 1
187
+ rect_width = col1 - col0 + 1
188
+
189
+ [row0, col0, row1, col1, rect_height, rect_width]
190
+ end
191
+ end
192
+ end