Skip to content

Commit

Permalink
Merge pull request #2724 from jerch/reverse_wraparound
Browse files Browse the repository at this point in the history
Reverse wraparound
  • Loading branch information
Tyriar authored May 3, 2020
2 parents fbdb01d + 076d51f commit 3ea9a75
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 7 deletions.
48 changes: 48 additions & 0 deletions src/browser/Terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/browser/Terminal2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
95 changes: 94 additions & 1 deletion src/common/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down
72 changes: 67 additions & 5 deletions src/common/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/common/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class MockCoreService implements ICoreService {
applicationKeypad: false,
bracketedPasteMode: false,
origin: false,
reverseWraparound: false,
sendFocus: false,
wraparound: true
};
Expand Down
1 change: 1 addition & 0 deletions src/common/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export interface IDecPrivateModes {
applicationKeypad: boolean;
bracketedPasteMode: boolean;
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
wraparound: boolean; // defaults: xterm - true, vt100 - false
}
Expand Down
1 change: 1 addition & 0 deletions src/common/services/CoreService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down

0 comments on commit 3ea9a75

Please sign in to comment.