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