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