diff --git a/fixtures/escape_sequence_files/run_tests.py b/fixtures/escape_sequence_files/run_tests.py new file mode 100644 index 00000000..08efbf3f --- /dev/null +++ b/fixtures/escape_sequence_files/run_tests.py @@ -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) diff --git a/fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.text b/fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.text index 948e5d43..02caf57d 100644 --- a/fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.text +++ b/fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.text @@ -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 + diff --git a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text index 92c10331..f89893ba 100644 --- a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text +++ b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text @@ -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 \ No newline at end of file diff --git a/fixtures/escape_sequence_files/t0091-alt_screen_ED3.text b/fixtures/escape_sequence_files/t0091-alt_screen_ED3.text index 4a326a40..e481aec1 100644 --- a/fixtures/escape_sequence_files/t0091-alt_screen_ED3.text +++ b/fixtures/escape_sequence_files/t0091-alt_screen_ED3.text @@ -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 diff --git a/fixtures/escape_sequence_files/t0102-DECAWM.text b/fixtures/escape_sequence_files/t0102-DECAWM.text index c1f22de2..6621f2b8 100644 --- a/fixtures/escape_sequence_files/t0102-DECAWM.text +++ b/fixtures/escape_sequence_files/t0102-DECAWM.text @@ -2,7 +2,7 @@ efgh -------- set: wraparound ----------------------------------------------abcd efgh --------- unset: no wraparound -------------------------------------------abcd +-------- unset: no wraparound -------------------------------------------abch this should be immediately below "no wraparound" diff --git a/fixtures/escape_sequence_files/t0300-vttest1.in b/fixtures/escape_sequence_files/t0300-vttest1.in new file mode 100644 index 00000000..dff924d4 --- /dev/null +++ b/fixtures/escape_sequence_files/t0300-vttest1.in @@ -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 + + diff --git a/fixtures/escape_sequence_files/t0300-vttest1.text b/fixtures/escape_sequence_files/t0300-vttest1.text new file mode 100644 index 00000000..d4dd93d7 --- /dev/null +++ b/fixtures/escape_sequence_files/t0300-vttest1.text @@ -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 + + + diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index b8f94d5b..d5f0ecd6 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -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']); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 577029f8..150ed8bb 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -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); + } } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index f50b32a7..dad8d46a 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -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(); } diff --git a/src/Terminal.ts b/src/Terminal.ts index c625114b..54ae3322 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -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) { diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index c8164c54..cbd790d5 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -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 = (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 = { - 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]); - } - }); -} diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 62ae609e..e9a76f7f 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -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); diff --git a/src/Types.d.ts b/src/Types.d.ts index ee6815da..0614d626 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -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 { diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 9c36a5a4..1b3e5d86 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -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]); diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index 2606df2f..532230ec 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -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[]; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 364ba803..1d2b7e1e 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -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;