Skip to content

Commit 380e680

Browse files
authored
Merge pull request #4539 from Tyriar/tyriar/windowsPty
Add more granular windowsPty option
2 parents 9f42483 + f6fbf63 commit 380e680

File tree

8 files changed

+151
-22
lines changed

8 files changed

+151
-22
lines changed

demo/client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,11 @@ function createTerminal(): void {
240240
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
241241
term = new Terminal({
242242
allowProposedApi: true,
243-
windowsMode: isWindows,
243+
windowsPty: isWindows ? {
244+
// In a real scenario, these values should be verified on the backend
245+
backend: 'conpty',
246+
buildNumber: 22621
247+
} : undefined,
244248
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
245249
theme: xtermjsTheme
246250
} as ITerminalOptions);

src/browser/Terminal.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,47 @@ describe('Terminal', () => {
10411041
});
10421042
});
10431043

1044+
describe('Windows Pty', () => {
1045+
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
1046+
const data = [
1047+
'aaaaaaaaaa\n\r', // cannot wrap as it's the first
1048+
'aaaaaaaaa\n\r', // wrapped (windows mode only)
1049+
'aaaaaaaaa' // not wrapped
1050+
];
1051+
1052+
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: {} });
1053+
await normalTerminal.writeP(data.join(''));
1054+
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
1055+
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
1056+
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
1057+
1058+
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: { backend: 'conpty', buildNumber: 19000 } });
1059+
await windowsModeTerminal.writeP(data.join(''));
1060+
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
1061+
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
1062+
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
1063+
});
1064+
1065+
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', async () => {
1066+
const data = [
1067+
'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first
1068+
'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only)
1069+
'aaaaaaaaa' // not wrapped
1070+
];
1071+
1072+
const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: {} });
1073+
await normalTerminal.writeP(data.join(''));
1074+
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
1075+
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
1076+
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
1077+
1078+
const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsPty: { backend: 'conpty', buildNumber: 19000 } });
1079+
await windowsModeTerminal.writeP(data.join(''));
1080+
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
1081+
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
1082+
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
1083+
});
1084+
});
10441085
describe('Windows Mode', () => {
10451086
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
10461087
const data = [

src/common/CoreTerminal.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
5757

5858
protected _inputHandler: InputHandler;
5959
private _writeBuffer: WriteBuffer;
60-
private _windowsMode: IDisposable | undefined;
60+
private _windowsWrappingHeuristics: IDisposable | undefined;
6161

6262
private readonly _onBinary = this.register(new EventEmitter<string>());
6363
public readonly onBinary = this._onBinary.event;
@@ -131,7 +131,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
131131
this.register(forwardEvent(this.coreService.onBinary, this._onBinary));
132132
this.register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom()));
133133
this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));
134-
this.register(this.optionsService.onSpecificOptionChange('windowsMode', e => this._handleWindowsModeOptionChange(e)));
134+
this.register(this.optionsService.onMultipleOptionChange(['windowsMode', 'windowsPty'], () => this._handleWindowsPtyOptionChange()));
135135
this.register(this._bufferService.onScroll(event => {
136136
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
137137
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
@@ -146,8 +146,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
146146
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
147147

148148
this.register(toDisposable(() => {
149-
this._windowsMode?.dispose();
150-
this._windowsMode = undefined;
149+
this._windowsWrappingHeuristics?.dispose();
150+
this._windowsWrappingHeuristics = undefined;
151151
}));
152152
}
153153

@@ -250,9 +250,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
250250
}
251251

252252
protected _setup(): void {
253-
if (this.optionsService.rawOptions.windowsMode) {
254-
this._enableWindowsMode();
255-
}
253+
this._handleWindowsPtyOptionChange();
256254
}
257255

258256
public reset(): void {
@@ -263,30 +261,36 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
263261
this.coreMouseService.reset();
264262
}
265263

266-
private _handleWindowsModeOptionChange(value: boolean): void {
264+
265+
private _handleWindowsPtyOptionChange(): void {
266+
let value = false;
267+
const windowsPty = this.optionsService.rawOptions.windowsPty;
268+
if (windowsPty && windowsPty.buildNumber !== undefined && windowsPty.buildNumber !== undefined) {
269+
value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);
270+
} else if (this.optionsService.rawOptions.windowsMode) {
271+
value = true;
272+
}
267273
if (value) {
268-
this._enableWindowsMode();
274+
this._enableWindowsWrappingHeuristics();
269275
} else {
270-
this._windowsMode?.dispose();
271-
this._windowsMode = undefined;
276+
this._windowsWrappingHeuristics?.dispose();
277+
this._windowsWrappingHeuristics = undefined;
272278
}
273279
}
274280

275-
protected _enableWindowsMode(): void {
276-
if (!this._windowsMode) {
281+
protected _enableWindowsWrappingHeuristics(): void {
282+
if (!this._windowsWrappingHeuristics) {
277283
const disposables: IDisposable[] = [];
278284
disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));
279285
disposables.push(this.registerCsiHandler({ final: 'H' }, () => {
280286
updateWindowsModeWrappedState(this._bufferService);
281287
return false;
282288
}));
283-
this._windowsMode = {
284-
dispose: () => {
285-
for (const d of disposables) {
286-
d.dispose();
287-
}
289+
this._windowsWrappingHeuristics = toDisposable(() => {
290+
for (const d of disposables) {
291+
d.dispose();
288292
}
289-
};
293+
});
290294
}
291295
}
292296
}

src/common/buffer/Buffer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export class Buffer implements IBuffer {
177177
if (this._rows < newRows) {
178178
for (let y = this._rows; y < newRows; y++) {
179179
if (this.lines.length < newRows + this.ybase) {
180-
if (this._optionsService.rawOptions.windowsMode) {
180+
if (this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {
181181
// Just add the new missing rows on Windows as conpty reprints the screen with it's
182182
// view of the world. Once a line enters scrollback for conpty it remains there
183183
this.lines.push(new BufferLine(newCols, nullCell));
@@ -290,6 +290,10 @@ export class Buffer implements IBuffer {
290290
}
291291

292292
private get _isReflowEnabled(): boolean {
293+
const windowsPty = this._optionsService.rawOptions.windowsPty;
294+
if (windowsPty && windowsPty.buildNumber) {
295+
return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376;
296+
}
293297
return this._hasScrollback && !this._optionsService.rawOptions.windowsMode;
294298
}
295299

src/common/services/OptionsService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
4343
rightClickSelectsWord: isMac,
4444
windowOptions: {},
4545
windowsMode: false,
46+
windowsPty: {},
4647
wordSeparator: ' ()[]{}\',"`',
4748
altClickMovesCursor: true,
4849
convertEol: false,

src/common/services/Services.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter';
77
import { IBuffer, IBufferSet } from 'common/buffer/Types';
88
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, IOscLinkData } from 'common/Types';
99
import { createDecorator } from 'common/services/ServiceRegistry';
10-
import { IDecorationOptions, IDecoration, ILinkHandler } from 'xterm';
10+
import { IDecorationOptions, IDecoration, ILinkHandler, IWindowsPty } from 'xterm';
1111

1212
export const IBufferService = createDecorator<IBufferService>('BufferService');
1313
export interface IBufferService {
@@ -242,6 +242,7 @@ export interface ITerminalOptions {
242242
tabStopWidth?: number;
243243
theme?: ITheme;
244244
windowsMode?: boolean;
245+
windowsPty?: IWindowsPty;
245246
windowOptions?: IWindowOptions;
246247
wordSeparator?: string;
247248
overviewRulerWidth?: number;

typings/xterm-headless.d.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,34 @@ declare module 'xterm-headless' {
183183
* - Reflow is disabled.
184184
* - Lines are assumed to be wrapped if the last character of the line is
185185
* not whitespace.
186+
*
187+
* When using conpty on Windows 11 version >= 21376, it is recommended to
188+
* disable this because native text wrapping sequences are output correctly
189+
* thanks to https://github.com/microsoft/terminal/issues/405
190+
*
191+
* @deprecated Use {@link windowsPty}. This value will be ignored if
192+
* windowsPty is set.
186193
*/
187194
windowsMode?: boolean;
188195

196+
/**
197+
* Compatibility information when the pty is known to be hosted on Windows.
198+
* Setting this will turn on certain heuristics/workarounds depending on the
199+
* values:
200+
*
201+
* - `if (!!windowsCompat)`
202+
* - When increasing the rows in the terminal, the amount increased into
203+
* the scrollback. This is done because ConPTY does not behave like
204+
* expect scrollback to come back into the viewport, instead it makes
205+
* empty rows at of the viewport. Not having this behavior can result in
206+
* missing data as the rows get replaced.
207+
* - `if !(backend === 'conpty' && buildNumber >= 21376)`
208+
* - Reflow is disabled
209+
* - Lines are assumed to be wrapped if the last character of the line is
210+
* not whitespace.
211+
*/
212+
windowsPty?: IWindowsPty;
213+
189214
/**
190215
* A string containing all characters that are considered word separated by the
191216
* double click to select work logic.
@@ -265,6 +290,20 @@ declare module 'xterm-headless' {
265290
extendedAnsi?: string[];
266291
}
267292

293+
/**
294+
* Pty information for Windows.
295+
*/
296+
export interface IWindowsPty {
297+
/**
298+
* What pty emulation backend is being used.
299+
*/
300+
backend?: 'conpty' | 'winpty';
301+
/**
302+
* The Windows build version (eg. 19045)
303+
*/
304+
buildNumber?: number;
305+
}
306+
268307
/**
269308
* An object that can be disposed via a dispose function.
270309
*/

typings/xterm.d.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,30 @@ declare module 'xterm' {
238238
* When using conpty on Windows 11 version >= 21376, it is recommended to
239239
* disable this because native text wrapping sequences are output correctly
240240
* thanks to https://github.com/microsoft/terminal/issues/405
241+
*
242+
* @deprecated Use {@link windowsPty}. This value will be ignored if
243+
* windowsPty is set.
241244
*/
242245
windowsMode?: boolean;
243246

247+
/**
248+
* Compatibility information when the pty is known to be hosted on Windows.
249+
* Setting this will turn on certain heuristics/workarounds depending on the
250+
* values:
251+
*
252+
* - `if (backend !== undefined || buildNumber !== undefined)`
253+
* - When increasing the rows in the terminal, the amount increased into
254+
* the scrollback. This is done because ConPTY does not behave like
255+
* expect scrollback to come back into the viewport, instead it makes
256+
* empty rows at of the viewport. Not having this behavior can result in
257+
* missing data as the rows get replaced.
258+
* - `if !(backend === 'conpty' && buildNumber >= 21376)`
259+
* - Reflow is disabled
260+
* - Lines are assumed to be wrapped if the last character of the line is
261+
* not whitespace.
262+
*/
263+
windowsPty?: IWindowsPty;
264+
244265
/**
245266
* A string containing all characters that are considered word separated by the
246267
* double click to select work logic.
@@ -330,6 +351,20 @@ declare module 'xterm' {
330351
extendedAnsi?: string[];
331352
}
332353

354+
/**
355+
* Pty information for Windows.
356+
*/
357+
export interface IWindowsPty {
358+
/**
359+
* What pty emulation backend is being used.
360+
*/
361+
backend?: 'conpty' | 'winpty';
362+
/**
363+
* The Windows build version (eg. 19045)
364+
*/
365+
buildNumber?: number;
366+
}
367+
333368
/**
334369
* An object that can be disposed via a dispose function.
335370
*/

0 commit comments

Comments
 (0)