Merge pull request #2288 from jerch/cleanup_sequences_files

fix edge cases in inputhandler methods
This commit is contained in:
Daniel Imms
2019-07-11 15:37:42 -07:00
committed by GitHub
17 changed files with 1054 additions and 430 deletions
@@ -0,0 +1,73 @@
from glob import glob
import os
import sys
import termios
import atexit
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def enable_echo(fd, enabled):
(iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(fd)
if enabled:
lflag |= termios.ECHO
else:
lflag &= ~termios.ECHO
new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
termios.tcsetattr(fd, termios.TCSANOW, new_attr)
atexit.register(enable_echo, sys.stdin.fileno(), True)
output = []
def log(append=False, *s):
if append:
output[-1] += ' ' + ' '.join(str(part) for part in s)
else:
output.append(' '.join(str(part) for part in s))
def reset_terminal():
sys.stdout.write('\x1bc\x1b[H')
sys.stdout.flush()
def test():
count = 0
passed = 0
for i, testfile in enumerate(sorted(glob(os.path.join(BASE_DIR, '*.in')))):
count += 1
log(False, os.path.basename(testfile))
reset_terminal()
with open(testfile) as test:
sys.stdout.write('\x1b]0;%s\x07' % os.path.basename(testfile))
sys.stdout.write(test.read()+'\x1bt')
sys.stdout.flush()
with open(os.path.join(os.path.dirname(testfile),
os.path.basename(testfile).split('.')[0]+'.text')) as expected:
terminal_output = sys.stdin.read()
if not terminal_output:
# we are in xterm
continue
if terminal_output != expected.read():
log(True, '\x1b[31merror\x1b[0m')
with open(os.path.join(os.path.dirname(testfile), 'output',
os.path.basename(testfile)), 'w') as t_out:
t_out.write(terminal_output)
else:
passed += 1
log(True, '\x1b[32mpass\x1b[0m')
return count, passed
if __name__ == '__main__':
enable_echo(sys.stdin.fileno(), False)
count, passed = test()
enable_echo(sys.stdin.fileno(), True)
reset_terminal()
for i in range(len(output)/2+1):
if not (i+1) % 25:
sys.stdin.read()
print ''.join(i.ljust(40) for i in output[i*2:i*2+2])
print '\x1b[33mcoverage: %s/%s (%d%%) tests passed.\x1b[0m' % (passed, count, passed*100/count)
@@ -1,24 +1,25 @@
a
b
c
d
f
g
h
i
b
c
d
f
g
h
i
j
k
l
m
j
k
l
m
n
o
p
q
r
s
w
x
n
o
p
q
r
s
w
x
@@ -1,3 +1,4 @@
6 C
8 ^^^^
9 vvvv DL on line 11, expected: ACD_
10 A
@@ -12,14 +13,14 @@
19 vvvv IL on line 21, expected: A_
20 A
22 ^^^^
23 vvvv IL on line 24, expected: _A
24 A
25 B
26 ^^^^
28 A
27 vvvv DL on line 28, expected: B_
28 A
29 B
30 ^^^^
31
32
32
@@ -1,25 +1,25 @@
n
o
p
q
r
s
t
u
v
w
x
y
z
1
2
3
4
5
6
7
8
9
10
o
p
q
r
s
t
u
v
w
x
y
z
1
2
3
4
5
6
7
8
9
10
11
11
@@ -2,7 +2,7 @@
efgh
-------- set: wraparound ----------------------------------------------abcd
efgh
-------- unset: no wraparound -------------------------------------------abcd
-------- unset: no wraparound -------------------------------------------abch
this should be immediately below "no wraparound"
@@ -0,0 +1,35 @@
Test of autowrap, mixing control and print characters.
The left/right margins should have letters in order:
[?6hAa
aBB b
C cC

DdEe
eFF f
G gG

HhIi
iJJ j
K kK

LlMm
mNN n
O oO

PpQq
qRR r
S sS

TtUu
uVV v
W wW

XxYy
yZZ z
[?6lPush <RETURN>
@@ -0,0 +1,25 @@
Test of autowrap, mixing control and print characters.
I i
J j
K k
L l
M m
N n
O o
P p
Q q
R r
S s
T t
U u
V v
W w
X x
Y y
Z z
Push <RETURN>
+488
View File
@@ -15,9 +15,18 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService } from 'common/TestUtils.test';
function getCursor(term: TestTerminal): number[] {
return [
term.buffer.x,
term.buffer.y
];
}
describe('InputHandler', () => {
describe('save and restore cursor', () => {
const terminal = new MockInputHandlingTerminal();
terminal.cols = 80;
terminal.rows = 30;
terminal.buffer.x = 1;
terminal.buffer.y = 2;
terminal.buffer.ybase = 0;
@@ -697,4 +706,483 @@ describe('InputHandler', () => {
});
});
});
describe('cursor positioning', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal({cols: 10, rows: 10});
});
it('cursor forward (CUF)', () => {
term.writeSync('\x1b[C');
assert.deepEqual(getCursor(term), [1, 0]);
term.writeSync('\x1b[1C');
assert.deepEqual(getCursor(term), [2, 0]);
term.writeSync('\x1b[4C');
assert.deepEqual(getCursor(term), [6, 0]);
term.writeSync('\x1b[100C');
assert.deepEqual(getCursor(term), [9, 0]);
// should not change y
term.buffer.x = 8;
term.buffer.y = 4;
term.writeSync('\x1b[C');
assert.deepEqual(getCursor(term), [9, 4]);
});
it('cursor backward (CUB)', () => {
term.writeSync('\x1b[D');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[1D');
assert.deepEqual(getCursor(term), [0, 0]);
// place cursor at end of first line
term.writeSync('\x1b[100C');
term.writeSync('\x1b[D');
assert.deepEqual(getCursor(term), [8, 0]);
term.writeSync('\x1b[1D');
assert.deepEqual(getCursor(term), [7, 0]);
term.writeSync('\x1b[4D');
assert.deepEqual(getCursor(term), [3, 0]);
term.writeSync('\x1b[100D');
assert.deepEqual(getCursor(term), [0, 0]);
// should not change y
term.buffer.x = 4;
term.buffer.y = 4;
term.writeSync('\x1b[D');
assert.deepEqual(getCursor(term), [3, 4]);
});
it('cursor down (CUD)', () => {
term.writeSync('\x1b[B');
assert.deepEqual(getCursor(term), [0, 1]);
term.writeSync('\x1b[1B');
assert.deepEqual(getCursor(term), [0, 2]);
term.writeSync('\x1b[4B');
assert.deepEqual(getCursor(term), [0, 6]);
term.writeSync('\x1b[100B');
assert.deepEqual(getCursor(term), [0, 9]);
// should not change x
term.buffer.x = 8;
term.buffer.y = 0;
term.writeSync('\x1b[B');
assert.deepEqual(getCursor(term), [8, 1]);
});
it('cursor up (CUU)', () => {
term.writeSync('\x1b[A');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[1A');
assert.deepEqual(getCursor(term), [0, 0]);
// place cursor at beginning of last row
term.writeSync('\x1b[100B');
term.writeSync('\x1b[A');
assert.deepEqual(getCursor(term), [0, 8]);
term.writeSync('\x1b[1A');
assert.deepEqual(getCursor(term), [0, 7]);
term.writeSync('\x1b[4A');
assert.deepEqual(getCursor(term), [0, 3]);
term.writeSync('\x1b[100A');
assert.deepEqual(getCursor(term), [0, 0]);
// should not change x
term.buffer.x = 8;
term.buffer.y = 9;
term.writeSync('\x1b[A');
assert.deepEqual(getCursor(term), [8, 8]);
});
it('cursor next line (CNL)', () => {
term.writeSync('\x1b[E');
assert.deepEqual(getCursor(term), [0, 1]);
term.writeSync('\x1b[1E');
assert.deepEqual(getCursor(term), [0, 2]);
term.writeSync('\x1b[4E');
assert.deepEqual(getCursor(term), [0, 6]);
term.writeSync('\x1b[100E');
assert.deepEqual(getCursor(term), [0, 9]);
// should reset x to zero
term.buffer.x = 8;
term.buffer.y = 0;
term.writeSync('\x1b[E');
assert.deepEqual(getCursor(term), [0, 1]);
});
it('cursor previous line (CPL)', () => {
term.writeSync('\x1b[F');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[1F');
assert.deepEqual(getCursor(term), [0, 0]);
// place cursor at beginning of last row
term.writeSync('\x1b[100E');
term.writeSync('\x1b[F');
assert.deepEqual(getCursor(term), [0, 8]);
term.writeSync('\x1b[1F');
assert.deepEqual(getCursor(term), [0, 7]);
term.writeSync('\x1b[4F');
assert.deepEqual(getCursor(term), [0, 3]);
term.writeSync('\x1b[100F');
assert.deepEqual(getCursor(term), [0, 0]);
// should reset x to zero
term.buffer.x = 8;
term.buffer.y = 9;
term.writeSync('\x1b[F');
assert.deepEqual(getCursor(term), [0, 8]);
});
it('cursor character absolute (CHA)', () => {
term.writeSync('\x1b[G');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[1G');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[2G');
assert.deepEqual(getCursor(term), [1, 0]);
term.writeSync('\x1b[5G');
assert.deepEqual(getCursor(term), [4, 0]);
term.writeSync('\x1b[100G');
assert.deepEqual(getCursor(term), [9, 0]);
});
it('cursor position (CUP)', () => {
term.buffer.x = 5;
term.buffer.y = 5;
term.writeSync('\x1b[H');
assert.deepEqual(getCursor(term), [0, 0]);
term.buffer.x = 5;
term.buffer.y = 5;
term.writeSync('\x1b[1H');
assert.deepEqual(getCursor(term), [0, 0]);
term.buffer.x = 5;
term.buffer.y = 5;
term.writeSync('\x1b[1;1H');
assert.deepEqual(getCursor(term), [0, 0]);
term.buffer.x = 5;
term.buffer.y = 5;
term.writeSync('\x1b[8H');
assert.deepEqual(getCursor(term), [0, 7]);
term.buffer.x = 5;
term.buffer.y = 5;
term.writeSync('\x1b[;8H');
assert.deepEqual(getCursor(term), [7, 0]);
term.buffer.x = 5;
term.buffer.y = 5;
term.writeSync('\x1b[100;100H');
assert.deepEqual(getCursor(term), [9, 9]);
});
it('horizontal position absolute (HPA)', () => {
term.writeSync('\x1b[`');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[1`');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[2`');
assert.deepEqual(getCursor(term), [1, 0]);
term.writeSync('\x1b[5`');
assert.deepEqual(getCursor(term), [4, 0]);
term.writeSync('\x1b[100`');
assert.deepEqual(getCursor(term), [9, 0]);
});
it('horizontal position relative (HPR)', () => {
term.writeSync('\x1b[a');
assert.deepEqual(getCursor(term), [1, 0]);
term.writeSync('\x1b[1a');
assert.deepEqual(getCursor(term), [2, 0]);
term.writeSync('\x1b[4a');
assert.deepEqual(getCursor(term), [6, 0]);
term.writeSync('\x1b[100a');
assert.deepEqual(getCursor(term), [9, 0]);
// should not change y
term.buffer.x = 8;
term.buffer.y = 4;
term.writeSync('\x1b[a');
assert.deepEqual(getCursor(term), [9, 4]);
});
it('vertical position absolute (VPA)', () => {
term.writeSync('\x1b[d');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[1d');
assert.deepEqual(getCursor(term), [0, 0]);
term.writeSync('\x1b[2d');
assert.deepEqual(getCursor(term), [0, 1]);
term.writeSync('\x1b[5d');
assert.deepEqual(getCursor(term), [0, 4]);
term.writeSync('\x1b[100d');
assert.deepEqual(getCursor(term), [0, 9]);
// should not change x
term.buffer.x = 8;
term.buffer.y = 4;
term.writeSync('\x1b[d');
assert.deepEqual(getCursor(term), [8, 0]);
});
it('vertical position relative (VPR)', () => {
term.writeSync('\x1b[e');
assert.deepEqual(getCursor(term), [0, 1]);
term.writeSync('\x1b[1e');
assert.deepEqual(getCursor(term), [0, 2]);
term.writeSync('\x1b[4e');
assert.deepEqual(getCursor(term), [0, 6]);
term.writeSync('\x1b[100e');
assert.deepEqual(getCursor(term), [0, 9]);
// should not change x
term.buffer.x = 8;
term.buffer.y = 4;
term.writeSync('\x1b[e');
assert.deepEqual(getCursor(term), [8, 5]);
});
describe('should clamp cursor into addressible range', () => {
it('CUF', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[C');
assert.deepEqual(getCursor(term), [9, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[C');
assert.deepEqual(getCursor(term), [1, 0]);
});
it('CUB', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[D');
assert.deepEqual(getCursor(term), [8, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[D');
assert.deepEqual(getCursor(term), [0, 0]);
});
it('CUD', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[B');
assert.deepEqual(getCursor(term), [9, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[B');
assert.deepEqual(getCursor(term), [0, 1]);
});
it('CUU', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[A');
assert.deepEqual(getCursor(term), [9, 8]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[A');
assert.deepEqual(getCursor(term), [0, 0]);
});
it('CNL', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[E');
assert.deepEqual(getCursor(term), [0, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[E');
assert.deepEqual(getCursor(term), [0, 1]);
});
it('CPL', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[F');
assert.deepEqual(getCursor(term), [0, 8]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[F');
assert.deepEqual(getCursor(term), [0, 0]);
});
it('CHA', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[5G');
assert.deepEqual(getCursor(term), [4, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[5G');
assert.deepEqual(getCursor(term), [4, 0]);
});
it('CUP', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[5;5H');
assert.deepEqual(getCursor(term), [4, 4]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[5;5H');
assert.deepEqual(getCursor(term), [4, 4]);
});
it('HPA', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[5`');
assert.deepEqual(getCursor(term), [4, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[5`');
assert.deepEqual(getCursor(term), [4, 0]);
});
it('HPR', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[a');
assert.deepEqual(getCursor(term), [9, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[a');
assert.deepEqual(getCursor(term), [1, 0]);
});
it('VPA', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[5d');
assert.deepEqual(getCursor(term), [9, 4]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[5d');
assert.deepEqual(getCursor(term), [0, 4]);
});
it('VPR', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[e');
assert.deepEqual(getCursor(term), [9, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[e');
assert.deepEqual(getCursor(term), [0, 1]);
});
it('DCH', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[P');
assert.deepEqual(getCursor(term), [9, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[P');
assert.deepEqual(getCursor(term), [0, 0]);
});
it('DCH - should delete last cell', () => {
term.writeSync('0123456789\x1b[P');
assert.equal(term.buffer.lines.get(0).translateToString(false), '012345678 ');
});
it('ECH', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[X');
assert.deepEqual(getCursor(term), [9, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[X');
assert.deepEqual(getCursor(term), [0, 0]);
});
it('ECH - should delete last cell', () => {
term.writeSync('0123456789\x1b[X');
assert.equal(term.buffer.lines.get(0).translateToString(false), '012345678 ');
});
it('ICH', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[@');
assert.deepEqual(getCursor(term), [9, 9]);
term.buffer.x = -10000;
term.buffer.y = -10000;
term.writeSync('\x1b[@');
assert.deepEqual(getCursor(term), [0, 0]);
});
it('ICH - should delete last cell', () => {
term.writeSync('0123456789\x1b[@');
assert.equal(term.buffer.lines.get(0).translateToString(false), '012345678 ');
});
});
});
describe('DECSTBM - scroll margins', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal({cols: 10, rows: 10});
});
it('should default to whole viewport', () => {
term.writeSync('\x1b[r');
assert.equal(term.buffer.scrollTop, 0);
assert.equal(term.buffer.scrollBottom, 9);
term.writeSync('\x1b[3;7r');
assert.equal(term.buffer.scrollTop, 2);
assert.equal(term.buffer.scrollBottom, 6);
term.writeSync('\x1b[0;0r');
assert.equal(term.buffer.scrollTop, 0);
assert.equal(term.buffer.scrollBottom, 9);
});
it('should clamp bottom', () => {
term.writeSync('\x1b[3;1000r');
assert.equal(term.buffer.scrollTop, 2);
assert.equal(term.buffer.scrollBottom, 9);
});
it('should only apply for top < bottom', () => {
term.writeSync('\x1b[7;2r');
assert.equal(term.buffer.scrollTop, 0);
assert.equal(term.buffer.scrollBottom, 9);
});
it('should home cursor', () => {
term.buffer.x = 10000;
term.buffer.y = 10000;
term.writeSync('\x1b[2;7r');
assert.deepEqual(getCursor(term), [0, 0]);
});
});
describe('scroll margins', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal({cols: 10, rows: 10});
});
function getLines(term: TestTerminal, limit: number = term.rows): string[] {
const res: string[] = [];
for (let i = 0; i < limit; ++i) {
res.push(term.buffer.lines.get(i).translateToString(true));
}
return res;
}
it('scrollUp', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm');
assert.deepEqual(getLines(term), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']);
});
it('scrollDown', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm');
assert.deepEqual(getLines(term), ['m', '', '', '1', '4', '5', '6', '7', '8', '9']);
});
it('insertLines - out of margins', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(term.buffer.scrollTop, 2);
assert.equal(term.buffer.scrollBottom, 5);
term.writeSync('\x1b[2Lm');
assert.deepEqual(getLines(term), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
term.writeSync('\x1b[2H\x1b[2Ln');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']);
// skip below scrollbottom
term.writeSync('\x1b[7H\x1b[2Lo');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']);
term.writeSync('\x1b[8H\x1b[2Lp');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']);
term.writeSync('\x1b[100H\x1b[2Lq');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']);
});
it('insertLines - within margins', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(term.buffer.scrollTop, 2);
assert.equal(term.buffer.scrollBottom, 5);
term.writeSync('\x1b[3H\x1b[2Lm');
assert.deepEqual(getLines(term), ['0', '1', 'm', '', '2', '3', '6', '7', '8', '9']);
term.writeSync('\x1b[6H\x1b[2Ln');
assert.deepEqual(getLines(term), ['0', '1', 'm', '', '2', 'n', '6', '7', '8', '9']);
});
it('deleteLines - out of margins', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(term.buffer.scrollTop, 2);
assert.equal(term.buffer.scrollBottom, 5);
term.writeSync('\x1b[2Mm');
assert.deepEqual(getLines(term), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
term.writeSync('\x1b[2H\x1b[2Mn');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']);
// skip below scrollbottom
term.writeSync('\x1b[7H\x1b[2Mo');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']);
term.writeSync('\x1b[8H\x1b[2Mp');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']);
term.writeSync('\x1b[100H\x1b[2Mq');
assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']);
});
it('deleteLines - within margins', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r');
assert.equal(term.buffer.scrollTop, 2);
assert.equal(term.buffer.scrollBottom, 5);
term.writeSync('\x1b[6H\x1b[2Mm');
assert.deepEqual(getLines(term), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']);
term.writeSync('\x1b[3H\x1b[2Mn');
assert.deepEqual(getLines(term), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
});
});
});
+264 -196
View File
@@ -15,7 +15,7 @@ import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from '
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags } from 'common/buffer/Constants';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { IAttributeData, IDisposable } from 'common/Types';
@@ -280,6 +280,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.setEscHandler('.' + flag, () => this.selectCharset('.' + flag));
this._parser.setEscHandler('/' + flag, () => this.selectCharset('/' + flag)); // TODO: supported?
}
this._parser.setEscHandler('#8', () => this.screenAlignmentPattern());
/**
* error handler
@@ -430,12 +431,12 @@ export class InputHandler extends Disposable implements IInputHandler {
// row changed, get it again
bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
} else {
buffer.x = cols - 1;
if (chWidth === 2) {
// FIXME: check for xterm behavior
// What to do here? We got a wide char that does not fit into last cell
continue;
}
// FIXME: Do we have to set buffer.x to cols - 1, if not wrapping?
}
}
@@ -540,6 +541,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Backspace (Ctrl-H).
*/
public backspace(): void {
this._restrictCursor();
if (this._terminal.buffer.x > 0) {
this._terminal.buffer.x--;
}
@@ -550,6 +552,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* Horizontal Tab (HT) (Ctrl-I).
*/
public tab(): void {
if (this._terminal.buffer.x >= this._terminal.cols) {
return;
}
const originalX = this._terminal.buffer.x;
this._terminal.buffer.x = this._terminal.buffer.nextStop();
if (this._terminal.options.screenReaderMode) {
@@ -576,16 +581,37 @@ export class InputHandler extends Disposable implements IInputHandler {
}
/**
* CSI Ps @
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
* Restrict cursor to viewport size / scroll margin (origin mode).
*/
public insertChars(params: IParams): void {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells(
this._terminal.buffer.x,
params.params[0] || 1,
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
private _restrictCursor(): void {
this._terminal.buffer.x = Math.min(this._terminal.cols - 1, Math.max(0, this._terminal.buffer.x));
this._terminal.buffer.y = this._terminal.originMode
? Math.min(this._terminal.buffer.scrollBottom, Math.max(this._terminal.buffer.scrollTop, this._terminal.buffer.y))
: Math.min(this._terminal.rows - 1, Math.max(0, this._terminal.buffer.y));
}
/**
* Set absolute cursor position.
*/
private _setCursor(x: number, y: number): void {
if (this._terminal.originMode) {
this._terminal.buffer.x = x;
this._terminal.buffer.y = this._terminal.buffer.scrollTop + y;
} else {
this._terminal.buffer.x = x;
this._terminal.buffer.y = y;
}
this._restrictCursor();
}
/**
* Set relative cursor position.
*/
private _moveCursor(x: number, y: number): void {
// for relative changes we have to make sure we are within 0 .. cols/rows - 1
// before calculating the new position
this._restrictCursor();
this._setCursor(this._terminal.buffer.x + x, this._terminal.buffer.y + y);
}
/**
@@ -593,11 +619,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Up Ps Times (default = 1) (CUU).
*/
public cursorUp(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.y -= param;
if (this._terminal.buffer.y < 0) {
this._terminal.buffer.y = 0;
}
this._moveCursor(0, -(params.params[0] || 1));
}
/**
@@ -605,15 +627,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Down Ps Times (default = 1) (CUD).
*/
public cursorDown(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.y += param;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
}
this._moveCursor(0, params.params[0] || 1);
}
/**
@@ -621,11 +635,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Forward Ps Times (default = 1) (CUF).
*/
public cursorForward(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.x += param;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
}
this._moveCursor(params.params[0] || 1, 0);
}
/**
@@ -633,55 +643,35 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Backward Ps Times (default = 1) (CUB).
*/
public cursorBackward(params: IParams): void {
const param = params.params[0] || 1;
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
}
this._terminal.buffer.x -= param;
if (this._terminal.buffer.x < 0) {
this._terminal.buffer.x = 0;
}
this._moveCursor(-(params.params[0] || 1), 0);
}
/**
* CSI Ps E
* Cursor Next Line Ps Times (default = 1) (CNL).
* same as CSI Ps B ?
* Other than cursorDown (CUD) also set the cursor to first column.
*/
public cursorNextLine(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.y += param;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
this._moveCursor(0, params.params[0] || 1);
this._terminal.buffer.x = 0;
}
/**
* CSI Ps F
* Cursor Preceding Line Ps Times (default = 1) (CNL).
* reuse CSI Ps A ?
* Cursor Previous Line Ps Times (default = 1) (CPL).
* Other than cursorUp (CUU) also set the cursor to first column.
*/
public cursorPrecedingLine(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.y -= param;
if (this._terminal.buffer.y < 0) {
this._terminal.buffer.y = 0;
}
this._moveCursor(0, -(params.params[0] || 1));
this._terminal.buffer.x = 0;
}
/**
* CSI Ps G
* Cursor Character Absolute [column] (default = [row,1]) (CHA).
*/
public cursorCharAbsolute(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.x = param - 1;
this._setCursor((params.params[0] || 1) - 1, this._terminal.buffer.y);
}
/**
@@ -689,23 +679,73 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Position [row;column] (default = [1,1]) (CUP).
*/
public cursorPosition(params: IParams): void {
let row: number = params.params[0] - 1;
let col: number = (params.length >= 2) ? params.params[1] - 1 : 0;
this._setCursor(
// col
(params.length >= 2) ? (params.params[1] || 1) - 1 : 0,
// row
(params.params[0] || 1) - 1);
}
if (row < 0) {
row = 0;
} else if (row >= this._terminal.rows) {
row = this._terminal.rows - 1;
/**
* CSI Pm ` Character Position Absolute
* [column] (default = [row,1]) (HPA).
* Currently same functionality as CHA.
*/
public charPosAbsolute(params: IParams): void {
this._setCursor((params.params[0] || 1) - 1, this._terminal.buffer.y);
}
/**
* CSI Pm a Character Position Relative
* [columns] (default = [row,col+1]) (HPR)
* Currently same functionality as CUF.
*/
public hPositionRelative(params: IParams): void {
this._moveCursor(params.params[0] || 1, 0);
}
/**
* CSI Pm d Vertical Position Absolute (VPA)
* [row] (default = [1,column])
*/
public linePosAbsolute(params: IParams): void {
this._setCursor(this._terminal.buffer.x, (params.params[0] || 1) - 1);
}
/**
* CSI Pm e Vertical Position Relative (VPR)
* [rows] (default = [row+1,column])
* reuse CSI Ps B ?
*/
public vPositionRelative(params: IParams): void {
this._moveCursor(0, params.params[0] || 1);
}
/**
* CSI Ps ; Ps f
* Horizontal and Vertical Position [row;column] (default =
* [1,1]) (HVP).
* Same as CUP.
*/
public hVPosition(params: IParams): void {
this.cursorPosition(params);
}
/**
* CSI Ps g Tab Clear (TBC).
* Ps = 0 -> Clear Current Column (default).
* Ps = 3 -> Clear All.
* Potentially:
* Ps = 2 -> Clear Stops on Line.
* http://vt100.net/annarbor/aaa-ug/section6.html
*/
public tabClear(params: IParams): void {
const param = params.params[0];
if (param === 0) {
delete this._terminal.buffer.tabs[this._terminal.buffer.x];
} else if (param === 3) {
this._terminal.buffer.tabs = {};
}
if (col < 0) {
col = 0;
} else if (col >= this._terminal.cols) {
col = this._terminal.cols - 1;
}
this._terminal.buffer.x = col;
this._terminal.buffer.y = row;
}
/**
@@ -713,12 +753,33 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Forward Tabulation Ps tab stops (default = 1) (CHT).
*/
public cursorForwardTab(params: IParams): void {
if (this._terminal.buffer.x >= this._terminal.cols) {
return;
}
let param = params.params[0] || 1;
while (param--) {
this._terminal.buffer.x = this._terminal.buffer.nextStop();
}
}
/**
* CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
*/
public cursorBackwardTab(params: IParams): void {
if (this._terminal.buffer.x >= this._terminal.cols) {
return;
}
let param = params.params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
buffer.x = buffer.prevStop();
}
}
/**
* Helper method to erase cells in a terminal row.
* The cell gets replaced with the eraseChar of the terminal.
@@ -744,7 +805,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* @param y row index
*/
private _resetBufferLine(y: number): void {
this._eraseInBufferLine(y, 0, this._terminal.cols, true);
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
line.fill(this._terminal.buffer.getNullCell(this._terminal.eraseAttrData()));
line.isWrapped = false;
}
/**
@@ -760,6 +823,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Ps = 2 -> Selective Erase All.
*/
public eraseInDisplay(params: IParams): void {
this._restrictCursor();
let j;
switch (params.params[0]) {
case 0:
@@ -819,6 +883,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Ps = 2 -> Selective Erase All.
*/
public eraseInLine(params: IParams): void {
this._restrictCursor();
switch (params.params[0]) {
case 0:
this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols);
@@ -838,11 +903,16 @@ export class InputHandler extends Disposable implements IInputHandler {
* Insert Ps Line(s) (default = 1) (IL).
*/
public insertLines(params: IParams): void {
this._restrictCursor();
let param = params.params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
return;
}
const row: number = buffer.y + buffer.ybase;
const scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom;
@@ -857,6 +927,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// this.maxRange();
this._terminal.updateRange(buffer.y);
this._terminal.updateRange(buffer.scrollBottom);
buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
}
/**
@@ -864,11 +935,16 @@ export class InputHandler extends Disposable implements IInputHandler {
* Delete Ps Line(s) (default = 1) (DL).
*/
public deleteLines(params: IParams): void {
this._restrictCursor();
let param = params.params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
return;
}
const row: number = buffer.y + buffer.ybase;
let j: number;
@@ -884,6 +960,24 @@ export class InputHandler extends Disposable implements IInputHandler {
// this.maxRange();
this._terminal.updateRange(buffer.y);
this._terminal.updateRange(buffer.scrollBottom);
buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
}
/**
* CSI Ps @
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
*/
public insertChars(params: IParams): void {
this._restrictCursor();
const line = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase);
if (line) {
line.insertCells(
this._terminal.buffer.x,
params.params[0] || 1,
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
}
}
/**
@@ -891,12 +985,16 @@ export class InputHandler extends Disposable implements IInputHandler {
* Delete Ps Character(s) (default = 1) (DCH).
*/
public deleteChars(params: IParams): void {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells(
this._terminal.buffer.x,
params.params[0] || 1,
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
this._restrictCursor();
const line = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase);
if (line) {
line.deleteCells(
this._terminal.buffer.x,
params.params[0] || 1,
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
}
}
/**
@@ -942,50 +1040,15 @@ export class InputHandler extends Disposable implements IInputHandler {
* Erase Ps Character(s) (default = 1) (ECH).
*/
public eraseChars(params: IParams): void {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells(
this._terminal.buffer.x,
this._terminal.buffer.x + (params.params[0] || 1),
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
}
/**
* CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT).
*/
public cursorBackwardTab(params: IParams): void {
let param = params.params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
buffer.x = buffer.prevStop();
}
}
/**
* CSI Pm ` Character Position Absolute
* [column] (default = [row,1]) (HPA).
*/
public charPosAbsolute(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.x = param - 1;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
}
}
/**
* CSI Pm a Character Position Relative
* [columns] (default = [row,col+1]) (HPR)
* reuse CSI Ps C ?
*/
public hPositionRelative(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.x += param;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
this._restrictCursor();
const line = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase);
if (line) {
line.replaceCells(
this._terminal.buffer.x,
this._terminal.buffer.x + (params.params[0] || 1),
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
);
this._terminal.updateRange(this._terminal.buffer.y);
}
}
@@ -1090,72 +1153,6 @@ export class InputHandler extends Disposable implements IInputHandler {
}
}
/**
* CSI Pm d Vertical Position Absolute (VPA)
* [row] (default = [1,column])
*/
public linePosAbsolute(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.y = param - 1;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
}
/**
* CSI Pm e Vertical Position Relative (VPR)
* [rows] (default = [row+1,column])
* reuse CSI Ps B ?
*/
public vPositionRelative(params: IParams): void {
const param = params.params[0] || 1;
this._terminal.buffer.y += param;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
}
}
/**
* CSI Ps ; Ps f
* Horizontal and Vertical Position [row;column] (default =
* [1,1]) (HVP).
*/
public hVPosition(params: IParams): void {
const row = params.params[0] || 1;
const col = (params.length > 1) ? params.params[1] || 1 : 1;
this._terminal.buffer.y = row - 1;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
this._terminal.buffer.x = col - 1;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
}
}
/**
* CSI Ps g Tab Clear (TBC).
* Ps = 0 -> Clear Current Column (default).
* Ps = 3 -> Clear All.
* Potentially:
* Ps = 2 -> Clear Stops on Line.
* http://vt100.net/annarbor/aaa-ug/section6.html
*/
public tabClear(params: IParams): void {
const param = params.params[0];
if (param === 0) {
delete this._terminal.buffer.tabs[this._terminal.buffer.x];
} else if (param === 3) {
this._terminal.buffer.tabs = {};
}
}
/**
* CSI Pm h Set Mode (SM).
* Ps = 2 -> Keyboard Action Mode (AM).
@@ -1271,11 +1268,14 @@ export class InputHandler extends Disposable implements IInputHandler {
// set VT100 mode here
break;
case 3: // 132 col mode
// TODO: move DECCOLM into compat addon
this._terminal.savedCols = this._terminal.cols;
this._terminal.resize(132, this._terminal.rows);
this._terminal.reset();
break;
case 6:
this._terminal.originMode = true;
this._setCursor(0, 0);
break;
case 7:
this._terminal.wraparoundMode = true;
@@ -1467,13 +1467,18 @@ export class InputHandler extends Disposable implements IInputHandler {
this._coreService.decPrivateModes.applicationCursorKeys = false;
break;
case 3:
// TODO: move DECCOLM into compat addon
// Note: This impl currently does not enforce col 80, instead reverts
// to previous terminal width before entering DECCOLM 132
if (this._terminal.cols === 132 && this._terminal.savedCols) {
this._terminal.resize(this._terminal.savedCols, this._terminal.rows);
}
delete this._terminal.savedCols;
this._terminal.reset();
break;
case 6:
this._terminal.originMode = false;
this._setCursor(0, 0);
break;
case 7:
this._terminal.wraparoundMode = false;
@@ -1913,10 +1918,19 @@ export class InputHandler extends Disposable implements IInputHandler {
if (collect) {
return;
}
this._terminal.buffer.scrollTop = (params.params[0] || 1) - 1;
this._terminal.buffer.scrollBottom = (params.length > 1 && params.params[1] && params.params[1] <= this._terminal.rows ? params.params[1] : this._terminal.rows) - 1;
this._terminal.buffer.x = 0;
this._terminal.buffer.y = 0;
const top = params.params[0] || 1;
let bottom: number;
if (params.length < 2 || (bottom = params.params[1]) > this._terminal.rows || bottom === 0) {
bottom = this._terminal.rows;
}
if (bottom > top) {
this._terminal.buffer.scrollTop = top - 1;
this._terminal.buffer.scrollBottom = bottom - 1;
this._setCursor(0, 0);
}
}
@@ -1930,6 +1944,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.buffer.savedY = this._terminal.buffer.ybase + this._terminal.buffer.y;
this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg;
this._terminal.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg;
this._terminal.buffer.savedCharset = this._terminal.charset;
}
@@ -1943,6 +1958,11 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.buffer.y = Math.max(this._terminal.buffer.savedY - this._terminal.buffer.ybase, 0);
this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg;
this._terminal.curAttrData.bg = this._terminal.buffer.savedCurAttrData.bg;
this._terminal.charset = (this as any)._savedCharset;
if (this._terminal.buffer.savedCharset) {
this._terminal.charset = this._terminal.buffer.savedCharset;
}
this._restrictCursor();
}
@@ -2038,7 +2058,13 @@ export class InputHandler extends Disposable implements IInputHandler {
* Moves the cursor down one line in the same column.
*/
public index(): void {
this._terminal.index(); // TODO: save to move from terminal?
this._restrictCursor();
this._terminal.buffer.y++;
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
this._terminal.buffer.y--;
this._terminal.scroll();
}
this._restrictCursor();
}
/**
@@ -2049,7 +2075,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* the value of the active column when the terminal receives an HTS.
*/
public tabSet(): void {
this._terminal.tabSet(); // TODO: save to move from terminal?
this._terminal.buffer.tabs[this._terminal.buffer.x] = true;
}
/**
@@ -2060,7 +2086,21 @@ export class InputHandler extends Disposable implements IInputHandler {
* the page scrolls down.
*/
public reverseIndex(): void {
this._terminal.reverseIndex(); // TODO: save to move from terminal?
this._restrictCursor();
const buffer = this._terminal.buffer;
if (buffer.y === buffer.scrollTop) {
// possibly move the code below to term.reverseScroll();
// test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
// blankLine(true) is xterm/linux behavior
const scrollRegionHeight = buffer.scrollBottom - buffer.scrollTop;
buffer.lines.shiftElements(buffer.y + buffer.ybase, scrollRegionHeight, 1);
buffer.lines.set(buffer.y + buffer.ybase, buffer.getBlankLine(this._terminal.eraseAttrData()));
this._terminal.updateRange(buffer.scrollTop);
this._terminal.updateRange(buffer.scrollBottom);
} else {
buffer.y--;
this._restrictCursor(); // quickfix to not run out of bounds
}
}
/**
@@ -2086,4 +2126,32 @@ export class InputHandler extends Disposable implements IInputHandler {
public setgLevel(level: number): void {
this._terminal.setgLevel(level); // TODO: save to move from terminal?
}
/**
* ESC # 8
* DEC mnemonic: DECALN (https://vt100.net/docs/vt510-rm/DECALN.html)
* This control function fills the complete screen area with
* a test pattern (E) used for adjusting screen alignment.
*
* TODO: move DECALN into compat addon
*/
public screenAlignmentPattern(): void {
// prepare cell data
const cell = new CellData();
cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0);
cell.fg = this._terminal.curAttrData.fg;
cell.bg = this._terminal.curAttrData.bg;
const buffer = this._terminal.buffer;
this._setCursor(0, 0);
for (let yOffset = 0; yOffset < this._terminal.rows; ++yOffset) {
const row = buffer.y + buffer.ybase + yOffset;
buffer.lines.get(row).fill(cell);
buffer.lines.get(row).isWrapped = false;
}
this._terminal.updateRange(0);
this._terminal.updateRange(this._terminal.rows);
this._setCursor(0, 0);
}
}
+7 -2
View File
@@ -8,6 +8,7 @@ import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { wcwidth } from 'common/CharWidth';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -750,10 +751,14 @@ describe('Terminal', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = false;
const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000);
if (width !== 1) {
continue;
}
term.write('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1);
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(2);
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
term.reset();
}
+6 -48
View File
@@ -1811,46 +1811,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
/**
* ESC
*/
/**
* ESC D Index (IND is 0x84).
*/
public index(): void {
this.buffer.y++;
if (this.buffer.y > this.buffer.scrollBottom) {
this.buffer.y--;
this.scroll();
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this.buffer.x >= this.cols) {
this.buffer.x--;
}
}
/**
* ESC M Reverse Index (RI is 0x8d).
*
* Move the cursor up one row, inserting a new blank line if necessary.
*/
public reverseIndex(): void {
if (this.buffer.y === this.buffer.scrollTop) {
// possibly move the code below to term.reverseScroll();
// test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
// blankLine(true) is xterm/linux behavior
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttrData()));
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
} else {
this.buffer.y--;
}
}
/**
* ESC c Full Reset (RIS).
* Reset terminal.
* Note: Calling this directly from JS is synchronous but does not clear
* input buffers and does not reset the parser, thus the terminal will
* continue to apply pending input data.
* If you need in band reset (synchronous with input data) consider
* using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).
*/
public reset(): void {
/**
@@ -1892,14 +1858,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
}
/**
* ESC H Tab Set (HTS is 0x88).
*/
public tabSet(): void {
this.buffer.tabs[this.buffer.x] = true;
}
// TODO: Remove cancel function and cancelEvents option
public cancel(ev: Event, force?: boolean): boolean {
if (!this.options.cancelEvents && !force) {
+92 -112
View File
@@ -1,45 +1,106 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file contains integration tests for xterm.js.
*/
import * as glob from 'glob';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import * as pty from 'node-pty';
import { Terminal } from './Terminal';
import { IViewport } from './Types';
import { CellData } from 'common/buffer/CellData';
import { WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
import { IDisposable } from 'xterm';
class TestTerminal extends Terminal {
innerWrite(): void { this._innerWrite(); }
// all test files expect terminal in 80x25
const COLS = 80;
const ROWS = 25;
const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
const SKIP_FILES = [
't0070-DECSTBM_LF.in', // lineFeed not working correctly
't0071-DECSTBM_IND.in',
't0072-DECSTBM_NEL.in',
't0075-DECSTBM_CUU_CUD.in',
't0076-DECSTBM_IL_DL.in', // not working due to lineFeed
't0077-DECSTBM_quirks.in',
't0084-CBT.in',
't0101-NLM.in',
't0103-reverse_wrap.in',
't0504-vim.in'
];
if (os.platform() === 'darwin') {
// These are failing on macOS only (termios related?)
SKIP_FILES.push(
't0003-line_wrap.in',
't0005-CR.in',
't0009-NEL.in',
't0503-zsh_ls_color.in'
);
}
// filter skipFilenames
const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1);
let primitivePty: any;
// fake sychronous pty write - read
// we just pipe the data from slave to master as a child program would do
// pty.js opens pipe fds with O_NONBLOCK
// just wait 10ms instead of setting fds to blocking mode
function ptyWriteRead(data: string, cb: (result: string) => void): void {
fs.writeSync(primitivePty.slave, data);
setTimeout(() => {
const b = new Buffer(64000);
const bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
cb(b.toString('utf8', 0, bytes));
describe('Escape Sequence Files', function(): void {
this.timeout(20000);
let ptyTerm: any;
let slaveEnd: any;
let term: Terminal;
let customHandler: IDisposable | undefined;
before(() => {
ptyTerm = (pty as any).open({cols: COLS, rows: ROWS});
slaveEnd = ptyTerm._slave;
term = new Terminal({cols: COLS, rows: ROWS});
ptyTerm._master.on('data', (data: string) => term.write(data));
});
}
// make sure raw pty is at x=0 and has no pending data
function ptyReset(cb: (result: string) => void): void {
ptyWriteRead('\r\n', cb);
}
after(() => {
ptyTerm._master.end();
ptyTerm._master.destroy();
});
FILES.forEach(filename => {
it(filename.split('/').slice(-1)[0], async () => {
// reset terminal and handler
if (customHandler) {
customHandler.dispose();
}
slaveEnd.write('\r\n');
term.reset();
slaveEnd.write('\x1bc\x1b[H');
// register handler to trigger viewport scraping, wait for it to finish
let content = '';
const OSC_CODE = 12345;
await new Promise(resolve => {
customHandler = term.addOscHandler(OSC_CODE, () => {
// grab terminal viewport content
content = terminalToString(term);
resolve();
return true;
});
// write file to slave
slaveEnd.write(fs.readFileSync(filename, 'utf8'));
// trigger custom sequence
slaveEnd.write(`\x1b]${OSC_CODE};\x07`);
});
// compare with expected output (right trimmed)
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (content !== expectedRightTrimmed) {
throw new Error(formatError(fs.readFileSync(filename, 'utf8'), content, expected));
}
});
});
});
/**
* Helpers
*/
/* debug helpers */
// generate colorful noisy output to compare xterm and emulator cell states
function formatError(input: string, output: string, expected: string): string {
function addLineNumber(start: number, color: string): (s: string) => string {
@@ -51,10 +112,10 @@ function formatError(input: string, output: string, expected: string): string {
}
const line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
let s = '';
s += '\n\x1b[34m' + JSON.stringify(input);
s += '\n\x1b[33m ' + line80 + '\n';
s += `\n\x1b[34m${JSON.stringify(input)}`;
s += `\n\x1b[33m ${line80}\n`;
s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += '\n\x1b[33m ' + line80 + '\n';
s += `\n\x1b[33m ${line80}\n`;
s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
return s;
}
@@ -64,10 +125,7 @@ function terminalToString(term: Terminal): string {
let result = '';
let lineText = '';
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = '';
for (let cell = 0; cell < term.cols; ++cell) {
lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).getChars() || WHITESPACE_CELL_CHAR;
}
lineText = term.buffer.lines.get(line).translateToString(true);
// rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, '');
result += lineText;
@@ -75,81 +133,3 @@ function terminalToString(term: Terminal): string {
}
return result;
}
// Skip tests on Windows since pty.open isn't supported
if (os.platform() !== 'win32') {
const consoleLog = console.log;
// expect files need terminal at 80x25!
const cols = 80;
const rows = 25;
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitivePty = (<any>pty).native.open(cols, rows);
/** tests */
describe('xterm output comparison', function(): void {
this.timeout(10000);
let xterm: TestTerminal;
beforeEach(() => {
xterm = new TestTerminal({ cols: cols, rows: rows });
xterm.refresh = () => {};
xterm.viewport = <IViewport>{
syncScrollArea: () => {}
};
});
// omit stack trace for escape sequence files
Error.stackTraceLimit = 0;
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
// for (let i = 0; i < files.length; ++i) console.debug(i, files[i]);
// only successful tests for now
const skip = [
10, 16, 17, 19, 32, 34, 35, 36, 39,
40, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
63, 68
];
// These are failing on macOS only
if (os.platform() === 'darwin') {
skip.push(3, 7, 11, 67);
}
for (let i = 0; i < files.length; i++) {
if (skip.indexOf(i) >= 0) {
continue;
}
((filename: string) => {
const inFile = fs.readFileSync(filename, 'utf8');
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
ptyWriteRead(inFile, fromPty => {
// uncomment this to get log from terminal
// console.log = function(){};
// Perform a synchronous .write(data)
xterm.writeBuffer.push(fromPty);
xterm.innerWrite();
const fromEmulator = terminalToString(xterm);
console.log = consoleLog;
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least.
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (fromEmulator !== expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(inFile, fromEmulator, expected));
// throw new Error('mismatch');
}
done();
});
});
});
})(files[i]);
}
});
}
+2 -10
View File
@@ -6,7 +6,7 @@
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener } from 'common/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
@@ -295,21 +295,12 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
tabSet(): void {
throw new Error('Method not implemented.');
}
handler(data: string): void {
throw new Error('Method not implemented.');
}
handleTitle(title: string): void {
throw new Error('Method not implemented.');
}
index(): void {
throw new Error('Method not implemented.');
}
reverseIndex(): void {
throw new Error('Method not implemented.');
}
}
export class MockBuffer implements IBuffer {
@@ -329,6 +320,7 @@ export class MockBuffer implements IBuffer {
scrollTop: number;
savedY: number;
savedX: number;
savedCharset: ICharset | null;
savedCurAttrData = new AttributeData();
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
+1 -3
View File
@@ -70,10 +70,7 @@ export interface IInputHandlingTerminal {
showCursor(): void;
refresh(start: number, end: number): void;
error(text: string, data?: any): void;
tabSet(): void;
handleTitle(title: string): void;
index(): void;
reverseIndex(): void;
}
export interface IViewport extends IDisposable {
@@ -169,6 +166,7 @@ export interface IInputHandler {
ESC |
ESC }
ESC ~ */ setgLevel(level: number): void;
/** ESC # 8 */ screenAlignmentPattern(): void;
}
export interface ILinkMatcher {
+3 -1
View File
@@ -5,13 +5,14 @@
import { CircularList, IInsertEvent } from 'common/CircularList';
import { IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData } from 'common/Types';
import { IBufferLine, ICellData, IAttributeData, ICharset } from 'common/Types';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from 'common/buffer/Constants';
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'common/buffer/BufferReflow';
import { Marker } from 'common/buffer/Marker';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { DEFAULT_CHARSET } from 'common/data/Charsets';
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
@@ -35,6 +36,7 @@ export class Buffer implements IBuffer {
public savedY: number = 0;
public savedX: number = 0;
public savedCurAttrData = DEFAULT_ATTR_DATA.clone();
public savedCharset: ICharset | null = DEFAULT_CHARSET;
public markers: Marker[] = [];
private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);
+2 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker } from 'common/Types';
import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker, ICharset } from 'common/Types';
import { IEvent } from 'common/EventEmitter';
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
@@ -31,6 +31,7 @@ export interface IBuffer {
hasScrollback: boolean;
savedY: number;
savedX: number;
savedCharset: ICharset | null;
savedCurAttrData: IAttributeData;
isCursorInViewport: boolean;
markers: IMarker[];
+5 -8
View File
@@ -466,10 +466,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.EXECUTE:
this.precedingCodepoint = 0;
callback = this._executeHandlers[code];
if (callback) callback();
else this._executeHandlerFb(code);
this.precedingCodepoint = 0;
break;
case ParserAction.IGNORE:
break;
@@ -488,10 +488,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// inject values: currently not implemented
break;
case ParserAction.CSI_DISPATCH:
// dont reset preceding codepoint for REP itself
if (code !== 98) { // 'b'
this.precedingCodepoint = 0;
}
// Trigger CSI Handler
const handlers = this._csiHandlers[code];
let j = handlers ? handlers.length - 1 : -1;
@@ -504,6 +500,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
if (j < 0) {
this._csiHandlerFb(collect, params, code);
}
this.precedingCodepoint = 0;
break;
case ParserAction.PARAM:
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
@@ -529,10 +526,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
collect += String.fromCharCode(code);
break;
case ParserAction.ESC_DISPATCH:
this.precedingCodepoint = 0;
callback = this._escHandlers[collect + String.fromCharCode(code)];
if (callback) callback(collect, code);
else this._escHandlerFb(collect, code);
this.precedingCodepoint = 0;
break;
case ParserAction.CLEAR:
osc = '';
@@ -541,7 +538,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
collect = '';
break;
case ParserAction.DCS_HOOK:
this.precedingCodepoint = 0;
dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)];
if (!dcsHandler) dcsHandler = this._dcsHandlerFb;
dcsHandler.hook(collect, params, code);
@@ -569,6 +565,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
params.reset();
params.addParam(0); // ZDM
collect = '';
this.precedingCodepoint = 0;
break;
case ParserAction.OSC_START:
osc = '';
@@ -584,7 +581,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.OSC_END:
this.precedingCodepoint = 0;
if (osc && code !== 0x18 && code !== 0x1a) {
// NOTE: OSC subparsing is not part of the original parser
// we do basic identifier parsing here to offer a jump table for OSC as well
@@ -616,6 +612,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
params.reset();
params.addParam(0); // ZDM
collect = '';
this.precedingCodepoint = 0;
break;
}
currentState = transition & TableAccess.TRANSITION_STATE_MASK;