scriptty 0.5.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitattributes +1 -0
- data/.gitignore +3 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/README.rdoc +31 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/scriptty-capture +5 -0
- data/bin/scriptty-dump-screens +4 -0
- data/bin/scriptty-replay +5 -0
- data/bin/scriptty-term-test +4 -0
- data/bin/scriptty-transcript-parse +4 -0
- data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
- data/examples/captures/xterm-vim-session.bin +262 -0
- data/examples/demo-capture.rb +19 -0
- data/examples/telnet-nego.rb +55 -0
- data/lib/scriptty/apps/capture_app/console.rb +104 -0
- data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
- data/lib/scriptty/apps/capture_app.rb +213 -0
- data/lib/scriptty/apps/dump_screens_app.rb +166 -0
- data/lib/scriptty/apps/replay_app.rb +229 -0
- data/lib/scriptty/apps/term_test_app.rb +124 -0
- data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
- data/lib/scriptty/cursor.rb +39 -0
- data/lib/scriptty/exception.rb +38 -0
- data/lib/scriptty/expect.rb +392 -0
- data/lib/scriptty/multiline_buffer.rb +192 -0
- data/lib/scriptty/net/event_loop.rb +610 -0
- data/lib/scriptty/screen_pattern/generator.rb +398 -0
- data/lib/scriptty/screen_pattern/parser.rb +558 -0
- data/lib/scriptty/screen_pattern.rb +104 -0
- data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
- data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
- data/lib/scriptty/term/dg410/parser.rb +162 -0
- data/lib/scriptty/term/dg410.rb +489 -0
- data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
- data/lib/scriptty/term/xterm.rb +661 -0
- data/lib/scriptty/term.rb +40 -0
- data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
- data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
- data/lib/scriptty/util/fsm.rb +177 -0
- data/lib/scriptty/util/transcript/reader.rb +96 -0
- data/lib/scriptty/util/transcript/writer.rb +111 -0
- data/test/apps/capture_app_test.rb +123 -0
- data/test/apps/transcript_parse_app_test.rb +118 -0
- data/test/cursor_test.rb +51 -0
- data/test/fsm_definition_parser_test.rb +220 -0
- data/test/fsm_test.rb +322 -0
- data/test/multiline_buffer_test.rb +275 -0
- data/test/net/event_loop_test.rb +402 -0
- data/test/screen_pattern/generator_test.rb +408 -0
- data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
- data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
- data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
- data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
- data/test/screen_pattern/parser_test.rb +266 -0
- data/test/term/dg410/parser_test.rb +139 -0
- data/test/term/xterm_test.rb +327 -0
- data/test/test_helper.rb +3 -0
- data/test/util/transcript/reader_test.rb +131 -0
- data/test/util/transcript/writer_test.rb +126 -0
- data/test.watchr +29 -0
- 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
|