mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #2724 from jerch/reverse_wraparound
Reverse wraparound
This commit is contained in:
@@ -1484,6 +1484,54 @@ describe('Terminal', () => {
|
||||
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BS with reverseWraparound set/unset', () => {
|
||||
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
|
||||
|
||||
beforeEach(() => {
|
||||
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
|
||||
});
|
||||
|
||||
describe('reverseWraparound set', () => {
|
||||
it('should not reverse outside of scroll margins', () => {
|
||||
// prepare buffer content
|
||||
term.writeSync('#####abcdefghijklmnopqrstuvwxy');
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
|
||||
assert.equal(term.buffer.ydisp, 1);
|
||||
assert.equal(term.buffer.x, 5);
|
||||
assert.equal(term.buffer.y, 4);
|
||||
term.writeSync(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
|
||||
|
||||
term.writeSync('\x1b[?45h');
|
||||
term.writeSync('uvwxy');
|
||||
|
||||
// set top/bottom to 1/3 (0-based)
|
||||
term.writeSync('\x1b[2;4r');
|
||||
// place cursor below scroll bottom
|
||||
term.buffer.x = 5;
|
||||
term.buffer.y = 4;
|
||||
term.writeSync(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
|
||||
|
||||
term.writeSync('uvwxy');
|
||||
// place cursor within scroll margins
|
||||
term.buffer.x = 5;
|
||||
term.buffer.y = 3;
|
||||
term.writeSync(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
|
||||
assert.equal(term.buffer.x, 0);
|
||||
assert.equal(term.buffer.y, term.buffer.scrollTop); // stops at 0, scrollTop
|
||||
|
||||
term.writeSync('fghijklmnopqrst');
|
||||
// place cursor above scroll top
|
||||
term.buffer.x = 5;
|
||||
term.buffer.y = 0;
|
||||
term.writeSync(ttyBS.repeat(100));
|
||||
assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__
|
||||
const SKIP_FILES = [
|
||||
't0084-CBT.in',
|
||||
't0101-NLM.in',
|
||||
't0103-reverse_wrap.in',
|
||||
't0103-reverse_wrap.in', // not comparable, we deviate from xterm reverse wrap on purpose
|
||||
't0504-vim.in'
|
||||
];
|
||||
if (os.platform() === 'darwin') {
|
||||
|
||||
@@ -28,7 +28,10 @@ function getCursor(bufferService: IBufferService): number[] {
|
||||
function getLines(bufferService: IBufferService, limit: number = bufferService.rows): string[] {
|
||||
const res: string[] = [];
|
||||
for (let i = 0; i < limit; ++i) {
|
||||
res.push(bufferService.buffer.lines.get(i)!.translateToString(true));
|
||||
const line = bufferService.buffer.lines.get(i);
|
||||
if (line) {
|
||||
res.push(line.translateToString(true));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -359,6 +362,14 @@ describe('InputHandler', () => {
|
||||
container[0] = 0x200B;
|
||||
inputHandler.print(container, 0, 1);
|
||||
});
|
||||
it('should clear cells to the right on early wrap-around', () => {
|
||||
bufferService.resize(5, 5);
|
||||
optionsService.options.scrollback = 1;
|
||||
inputHandler.parse('12345');
|
||||
bufferService.buffer.x = 0;
|
||||
inputHandler.parse('¥¥¥');
|
||||
assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alt screen', () => {
|
||||
@@ -1375,6 +1386,88 @@ describe('InputHandler', () => {
|
||||
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BS with reverseWraparound set/unset', () => {
|
||||
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
|
||||
beforeEach(() => {
|
||||
bufferService.resize(5, 5);
|
||||
optionsService.options.scrollback = 1;
|
||||
});
|
||||
describe('reverseWraparound unset (default)', () => {
|
||||
it('cannot delete last cell', () => {
|
||||
inputHandler.parse('12345');
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 1), ['123 5']);
|
||||
inputHandler.parse(ttyBS.repeat(10));
|
||||
assert.deepEqual(getLines(bufferService, 1), [' 5']);
|
||||
});
|
||||
it('cannot access prev line', () => {
|
||||
inputHandler.parse('12345'.repeat(2));
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), ['12345', '123 5']);
|
||||
inputHandler.parse(ttyBS.repeat(10));
|
||||
assert.deepEqual(getLines(bufferService, 2), ['12345', ' 5']);
|
||||
});
|
||||
});
|
||||
describe('reverseWraparound set', () => {
|
||||
it('can delete last cell', () => {
|
||||
inputHandler.parse('\x1b[?45h');
|
||||
inputHandler.parse('12345');
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 1), ['1234 ']);
|
||||
inputHandler.parse(ttyBS.repeat(7));
|
||||
assert.deepEqual(getLines(bufferService, 1), [' ']);
|
||||
});
|
||||
it('can access prev line if wrapped', () => {
|
||||
inputHandler.parse('\x1b[?45h');
|
||||
inputHandler.parse('12345'.repeat(2));
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), ['12345', '1234 ']);
|
||||
inputHandler.parse(ttyBS.repeat(7));
|
||||
assert.deepEqual(getLines(bufferService, 2), ['12 ', ' ']);
|
||||
});
|
||||
it('should lift isWrapped', () => {
|
||||
inputHandler.parse('\x1b[?45h');
|
||||
inputHandler.parse('12345'.repeat(2));
|
||||
assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, true);
|
||||
inputHandler.parse(ttyBS.repeat(7));
|
||||
assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, false);
|
||||
});
|
||||
it('stops at hard NLs', () => {
|
||||
inputHandler.parse('\x1b[?45h');
|
||||
inputHandler.parse('12345\r\n');
|
||||
inputHandler.parse('12345'.repeat(2));
|
||||
inputHandler.parse(ttyBS.repeat(50));
|
||||
assert.deepEqual(getLines(bufferService, 3), ['12345', ' ', ' ']);
|
||||
assert.equal(bufferService.buffer.x, 0);
|
||||
assert.equal(bufferService.buffer.y, 1);
|
||||
});
|
||||
it('handles wide chars correctly', () => {
|
||||
inputHandler.parse('\x1b[?45h');
|
||||
inputHandler.parse('¥¥¥');
|
||||
assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']);
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']);
|
||||
assert.equal(bufferService.buffer.x, 1);
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']);
|
||||
assert.equal(bufferService.buffer.x, 0);
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']);
|
||||
assert.equal(bufferService.buffer.x, 3); // x=4 skipped due to early wrap-around
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']);
|
||||
assert.equal(bufferService.buffer.x, 2);
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), [' ', ' ']);
|
||||
assert.equal(bufferService.buffer.x, 1);
|
||||
inputHandler.parse(ttyBS);
|
||||
assert.deepEqual(getLines(bufferService, 2), [' ', ' ']);
|
||||
assert.equal(bufferService.buffer.x, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DECSTR', () => {
|
||||
beforeEach(() => {
|
||||
bufferService.resize(10, 5);
|
||||
|
||||
@@ -556,6 +556,10 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// autowrap - DECAWM
|
||||
// automatically wraps to the beginning of the next line
|
||||
if (wraparoundMode) {
|
||||
// clear left over cells to the right
|
||||
while (buffer.x < cols) {
|
||||
bufferRow.setCellFromCodePoint(buffer.x++, 0, 1, curAttr.fg, curAttr.bg);
|
||||
}
|
||||
buffer.x = 0;
|
||||
buffer.y++;
|
||||
if (buffer.y === buffer.scrollBottom + 1) {
|
||||
@@ -727,12 +731,62 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Backspace (Ctrl-H).
|
||||
*
|
||||
* @vt: #Y C0 BS "Backspace" "\b, \x08" "Move the cursor one position to the left."
|
||||
* By default it is not possible to move the cursor past the leftmost position.
|
||||
* If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM)
|
||||
* can be undone with BS within the scroll margins. In that case the cursor will wrap back
|
||||
* to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer
|
||||
* with the cursor, thus at the home position (top-leftmost cell) this has no effect.
|
||||
*/
|
||||
public backspace(): void {
|
||||
this._restrictCursor();
|
||||
if (this._bufferService.buffer.x > 0) {
|
||||
this._bufferService.buffer.x--;
|
||||
const buffer = this._bufferService.buffer;
|
||||
|
||||
// reverse wrap-around is disabled
|
||||
if (!this._coreService.decPrivateModes.reverseWraparound) {
|
||||
this._restrictCursor();
|
||||
if (buffer.x > 0) {
|
||||
buffer.x--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// reverse wrap-around is enabled
|
||||
// other than for normal operation mode, reverse wrap-around allows the cursor
|
||||
// to be at x=cols to be able to address the last cell of a row by BS
|
||||
this._restrictCursor(this._bufferService.cols);
|
||||
|
||||
if (buffer.x > 0) {
|
||||
buffer.x--;
|
||||
} else {
|
||||
/**
|
||||
* reverse wrap-around handling:
|
||||
* Our implementation deviates from xterm on purpose. Details:
|
||||
* - only previous soft NLs can be reversed (isWrapped=true)
|
||||
* - only works within scrollborders (top/bottom, left/right not yet supported)
|
||||
* - cannot peek into scrollbuffer
|
||||
* - any cursor movement sequence keeps working as expected
|
||||
*/
|
||||
if (buffer.x === 0
|
||||
&& buffer.y > buffer.scrollTop
|
||||
&& buffer.y <= buffer.scrollBottom
|
||||
&& buffer.lines.get(buffer.ybase + buffer.y)?.isWrapped)
|
||||
{
|
||||
buffer.lines.get(buffer.ybase + buffer.y)!.isWrapped = false;
|
||||
buffer.y--;
|
||||
buffer.x = this._bufferService.cols - 1;
|
||||
// find last taken cell - last cell can have 3 different states:
|
||||
// - hasContent(true) + hasWidth(1): narrow char - we are done
|
||||
// - hasWidth(0): second part of wide char - we are done
|
||||
// - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one cell further back
|
||||
const line = buffer.lines.get(buffer.ybase + buffer.y)!;
|
||||
if (line.hasWidth(buffer.x) && !line.hasContent(buffer.x)) {
|
||||
buffer.x--;
|
||||
// We do this only once, since width=1 + hasContent=false currently happens only once before
|
||||
// early wrapping of a wide char.
|
||||
// This needs to be fixed once we support graphemes taking more than 2 cells.
|
||||
}
|
||||
}
|
||||
}
|
||||
this._restrictCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -777,8 +831,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
/**
|
||||
* Restrict cursor to viewport size / scroll margin (origin mode).
|
||||
*/
|
||||
private _restrictCursor(): void {
|
||||
this._bufferService.buffer.x = Math.min(this._bufferService.cols - 1, Math.max(0, this._bufferService.buffer.x));
|
||||
private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void {
|
||||
this._bufferService.buffer.x = Math.min(maxCol, Math.max(0, this._bufferService.buffer.x));
|
||||
this._bufferService.buffer.y = this._coreService.decPrivateModes.origin
|
||||
? Math.min(this._bufferService.buffer.scrollBottom, Math.max(this._bufferService.buffer.scrollTop, this._bufferService.buffer.y))
|
||||
: Math.min(this._bufferService.rows - 1, Math.max(0, this._bufferService.buffer.y));
|
||||
@@ -1716,6 +1770,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* | 9 | X10 xterm mouse protocol. | #Y |
|
||||
* | 12 | Start Blinking Cursor. | #Y |
|
||||
* | 25 | Show Cursor (DECTCEM). | #Y |
|
||||
* | 45 | Reverse wrap-around. | #Y |
|
||||
* | 47 | Use Alternate Screen Buffer. | #Y |
|
||||
* | 66 | Application keypad (DECNKM). | #Y |
|
||||
* | 1000 | X11 xterm mouse protocol. | #Y |
|
||||
@@ -1767,6 +1822,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 12:
|
||||
// this.cursorBlink = true;
|
||||
break;
|
||||
case 45:
|
||||
this._coreService.decPrivateModes.reverseWraparound = true;
|
||||
break;
|
||||
case 66:
|
||||
this._logService.debug('Serial port requested application keypad.');
|
||||
this._coreService.decPrivateModes.applicationKeypad = true;
|
||||
@@ -1950,6 +2008,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* | 9 | Don't send Mouse X & Y on button press. | #Y |
|
||||
* | 12 | Stop Blinking Cursor. | #Y |
|
||||
* | 25 | Hide Cursor (DECTCEM). | #Y |
|
||||
* | 45 | No reverse wrap-around. | #Y |
|
||||
* | 47 | Use Normal Screen Buffer. | #Y |
|
||||
* | 66 | Numeric keypad (DECNKM). | #Y |
|
||||
* | 1000 | Don't send Mouse reports. | #Y |
|
||||
@@ -1994,6 +2053,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 12:
|
||||
// this.cursorBlink = false;
|
||||
break;
|
||||
case 45:
|
||||
this._coreService.decPrivateModes.reverseWraparound = false;
|
||||
break;
|
||||
case 66:
|
||||
this._logService.debug('Switching back to normal keypad.');
|
||||
this._coreService.decPrivateModes.applicationKeypad = false;
|
||||
|
||||
@@ -68,6 +68,7 @@ export class MockCoreService implements ICoreService {
|
||||
applicationKeypad: false,
|
||||
bracketedPasteMode: false,
|
||||
origin: false,
|
||||
reverseWraparound: false,
|
||||
sendFocus: false,
|
||||
wraparound: true
|
||||
};
|
||||
|
||||
Vendored
+1
@@ -175,6 +175,7 @@ export interface IDecPrivateModes {
|
||||
applicationKeypad: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
origin: boolean;
|
||||
reverseWraparound: boolean;
|
||||
sendFocus: boolean;
|
||||
wraparound: boolean; // defaults: xterm - true, vt100 - false
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
|
||||
applicationKeypad: false,
|
||||
bracketedPasteMode: false,
|
||||
origin: false,
|
||||
reverseWraparound: false,
|
||||
sendFocus: false,
|
||||
wraparound: true // defaults: xterm - true, vt100 - false
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user