diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3bf1c6914..cf683d8a8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,13 +2,8 @@
"files.associations": {
".eslintrc.json.typings": "jsonc"
},
- // Hide output files from the file explorer, comment this out to see the build output
"files.exclude": {
- "**/.nyc_output": true,
- "**/lib": true,
- "**/dist": true,
- "**/out": true,
- "**/out-*": true,
+ "**/.nyc_output": true
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single",
diff --git a/addons/addon-ligatures/src/LigaturesAddon.ts b/addons/addon-ligatures/src/LigaturesAddon.ts
index cb5893035..47fbc3237 100644
--- a/addons/addon-ligatures/src/LigaturesAddon.ts
+++ b/addons/addon-ligatures/src/LigaturesAddon.ts
@@ -31,8 +31,12 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
}
public activate(terminal: Terminal): void {
+ if (!terminal.element) {
+ throw new Error('Cannot activate LigaturesAddon before open is called');
+ }
this._terminal = terminal;
this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures);
+ terminal.element.style.fontFeatureSettings = '"liga" on, "calt" on';
}
public dispose(): void {
@@ -40,5 +44,8 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
this._terminal?.deregisterCharacterJoiner(this._characterJoinerId);
this._characterJoinerId = undefined;
}
+ if (this._terminal?.element) {
+ this._terminal.element.style.fontFeatureSettings = '';
+ }
}
}
diff --git a/addons/addon-ligatures/webpack.config.js b/addons/addon-ligatures/webpack.config.js
index f9e9f3478..c43d0a17c 100644
--- a/addons/addon-ligatures/webpack.config.js
+++ b/addons/addon-ligatures/webpack.config.js
@@ -31,10 +31,10 @@ module.exports = {
},
mode: 'production',
externals: {
- 'fs': 'fs',
- 'path': 'path',
- 'stream': 'stream',
- 'util': 'util'
+ 'fs': 'commonjs fs',
+ 'path': 'commonjs path',
+ 'stream': 'commonjs stream',
+ 'util': 'commonjs util'
},
resolve: {
// The ligature modules contains fallbacks for node environments, we never want to browserify them
diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts
index 48023eaec..6c270d127 100644
--- a/addons/addon-webgl/src/WebglRenderer.ts
+++ b/addons/addon-webgl/src/WebglRenderer.ts
@@ -314,14 +314,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._updateCursorBlink();
}
- public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
- return -1;
- }
-
- public deregisterCharacterJoiner(joinerId: number): boolean {
- return false;
- }
-
public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
@@ -362,7 +354,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
private _updateCursorBlink(): void {
- if (this._terminal.options.cursorBlink) {
+ if (this._coreService.decPrivateModes.cursorBlink ?? this._terminal.options.cursorBlink) {
this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => {
this._requestRedrawCursor();
}, this._coreBrowserService);
@@ -395,6 +387,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
let j: number;
start = clamp(start, terminal.rows - 1, 0);
end = clamp(end, terminal.rows - 1, 0);
+ const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? terminal.options.cursorStyle ?? 'block';
const cursorY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;
const viewportRelativeCursorY = cursorY - terminal.buffer.ydisp;
@@ -458,8 +451,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
x: cursorX,
y: viewportRelativeCursorY,
width: cell.getWidth(),
- style: this._coreBrowserService.isFocused ?
- (terminal.options.cursorStyle || 'block') : terminal.options.cursorInactiveStyle,
+ style: this._coreBrowserService.isFocused ? cursorStyle : terminal.options.cursorInactiveStyle,
cursorWidth: terminal.options.cursorWidth,
dpr: this._devicePixelRatio
};
@@ -467,9 +459,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
if (x >= cursorX && x <= lastCursorX &&
((this._coreBrowserService.isFocused &&
- (terminal.options.cursorStyle || 'block') === 'block') ||
+ cursorStyle === 'block') ||
(this._coreBrowserService.isFocused === false &&
- terminal.options.cursorInactiveStyle === 'block'))) {
+ terminal.options.cursorInactiveStyle === 'block'))
+ ) {
this._cellColorResolver.result.fg =
Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK);
this._cellColorResolver.result.bg =
@@ -510,14 +503,17 @@ export class WebglRenderer extends Disposable implements IRenderer {
cell = this._workCell;
// Null out non-first cells
- for (x++; x < lastCharX; x++) {
+ for (x++; x <= lastCharX; x++) {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
this._model.cells[j] = NULL_CELL_CODE;
+ // Don't re-resolve the cell color since multi-colored ligature backgrounds are not
+ // supported
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
}
+ x--; // Go back to the previous update cell for next iteration
}
}
}
diff --git a/demo/client.ts b/demo/client.ts
index 49ba67438..f75032101 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -16,7 +16,7 @@ if ('WebAssembly' in window) {
ImageAddon = imageAddon.ImageAddon;
}
-import { Terminal, ITerminalOptions, type IDisposable } from '@xterm/xterm';
+import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm';
import { AttachAddon } from '@xterm/addon-attach';
import { ClipboardAddon } from '@xterm/addon-clipboard';
import { FitAddon } from '@xterm/addon-fit';
@@ -131,7 +131,7 @@ const xtermjsTheme = {
brightCyan: '#72F0FF',
white: '#F8F8F8',
brightWhite: '#FFFFFF'
-};
+} satisfies ITheme;
function setPadding(): void {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
addons.fit.instance.fit();
@@ -366,14 +366,19 @@ function createTerminal(): void {
// Set terminal size again to set the specific dimensions on the demo
updateTerminalSize();
- const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
- const processId = await res.text();
- pid = processId;
- socketURL += processId;
- socket = new WebSocket(socketURL);
- socket.onopen = runRealTerminal;
- socket.onclose = runFakeTerminal;
- socket.onerror = runFakeTerminal;
+ const useRealTerminal = document.getElementById('use-real-terminal');
+ if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) {
+ runFakeTerminal();
+ } else {
+ const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
+ const processId = await res.text();
+ pid = processId;
+ socketURL += processId;
+ socket = new WebSocket(socketURL);
+ socket.onopen = runRealTerminal;
+ socket.onclose = runFakeTerminal;
+ socket.onerror = runFakeTerminal;
+ }
}, 0);
}
@@ -1261,9 +1266,9 @@ function addVtButtons(): void {
const element = document.createElement('button');
element.textContent = name;
- writeCsi.split('');
- const prefix = writeCsi.length === 2 ? writeCsi[0] : '';
- const suffix = writeCsi[writeCsi.length - 1];
+ const writeCsiSplit = writeCsi.split('|');
+ const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : '';
+ const suffix = writeCsiSplit[writeCsiSplit.length - 1];
element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
const desc = document.createElement('span');
@@ -1276,22 +1281,23 @@ function addVtButtons(): void {
}
const vtFragment = document.createDocumentFragment();
const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = {
- A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
- B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
- C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
- D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
- E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
- F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
- G: { label: 'CHA', description: 'Cursor Character Absolute' },
- H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
- I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
- J: { label: 'ED', description: 'Erase in Display' },
- '?J': { label: 'DECSED', description: 'Erase in Display' },
- K: { label: 'EL', description: 'Erase in Line' },
- '?K': { label: 'DECSEL', description: 'Erase in Line' },
- L: { label: 'IL', description: 'Insert Ps Line(s)' },
- M: { label: 'DL', description: 'Delete Ps Line(s)' },
- P: { label: 'DCH', description: 'Delete Ps Character(s)' }
+ A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
+ B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
+ C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
+ D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
+ E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
+ F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
+ G: { label: 'CHA', description: 'Cursor Character Absolute' },
+ H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
+ I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
+ J: { label: 'ED', description: 'Erase in Display' },
+ '?|J': { label: 'DECSED', description: 'Erase in Display' },
+ K: { label: 'EL', description: 'Erase in Line' },
+ '?|K': { label: 'DECSEL', description: 'Erase in Line' },
+ L: { label: 'IL', description: 'Insert Ps Line(s)' },
+ M: { label: 'DL', description: 'Delete Ps Line(s)' },
+ P: { label: 'DCH', description: 'Delete Ps Character(s)' },
+ ' q': { label: 'DECSCUSR', description: 'Set Cursor Style', paramCount: 1 }
};
for (const s of Object.keys(buttonSpecs)) {
const spec = buttonSpecs[s];
diff --git a/demo/index.html b/demo/index.html
index 064518475..3d46f61ca 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -81,6 +81,7 @@
- Lifecycle
+
diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts
index 2e15b5f37..58da93445 100644
--- a/src/browser/CoreBrowserTerminal.ts
+++ b/src/browser/CoreBrowserTerminal.ts
@@ -437,7 +437,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this.screenElement.appendChild(this._helperContainer);
fragment.appendChild(this.screenElement);
- this.textarea = this._document.createElement('textarea');
+ const textarea = this.textarea = this._document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
this.textarea.setAttribute('aria-label', Strings.promptLabel.get());
if (!Browser.isChromeOS) {
@@ -449,6 +449,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
+ this._register(this.optionsService.onSpecificOptionChange('disableStdin', () => textarea.readOnly = this.optionsService.rawOptions.disableStdin));
+ this.textarea.readOnly = this.optionsService.rawOptions.disableStdin;
// Register the core browser service before the generic textarea handlers are registered so it
// handles them first. Otherwise the renderers may use the wrong focus state.
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
index f6fda22e6..1c271797a 100644
--- a/src/browser/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -13,7 +13,7 @@ import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/se
import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types';
import { color } from 'common/Color';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
-import { IBufferService, IInstantiationService, IOptionsService } from 'common/services/Services';
+import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'vs/base/common/event';
@@ -59,6 +59,7 @@ export class DomRenderer extends Disposable implements IRenderer {
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService,
+ @ICoreService private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IThemeService private readonly _themeService: IThemeService
) {
@@ -161,6 +162,10 @@ export class DomRenderer extends Disposable implements IRenderer {
// Base CSS
let styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
+ // Disabling pointer events circumvents a browser behavior that prevents `click` events from
+ // being delivered if the target element is replaced during the click. This happened due to
+ // refresh() being called during the mousedown handler to start a selection.
+ ` pointer-events: none;` +
` color: ${colors.foreground.css};` +
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
@@ -437,8 +442,8 @@ export class DomRenderer extends Disposable implements IRenderer {
const buffer = this._bufferService.buffer;
const cursorAbsoluteY = buffer.ybase + buffer.y;
const cursorX = Math.min(buffer.x, this._bufferService.cols - 1);
- const cursorBlink = this._optionsService.rawOptions.cursorBlink;
- const cursorStyle = this._optionsService.rawOptions.cursorStyle;
+ const cursorBlink = this._coreService.decPrivateModes.cursorBlink ?? this._optionsService.rawOptions.cursorBlink;
+ const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? this._optionsService.rawOptions.cursorStyle;
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
for (let y = start; y <= end; y++) {
diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts
index 88ffd99d5..cd85e0fff 100644
--- a/src/browser/services/ThemeService.ts
+++ b/src/browser/services/ThemeService.ts
@@ -82,8 +82,8 @@ export class ThemeService extends Disposable implements IThemeService {
const colors = this._colors;
colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND);
colors.background = parseColor(theme.background, DEFAULT_BACKGROUND);
- colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR);
- colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
+ colors.cursor = color.blend(colors.background, parseColor(theme.cursor, DEFAULT_CURSOR));
+ colors.cursorAccent = color.blend(colors.background, parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT));
colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION);
colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent);
colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent);
diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts
index baae735c9..1280417bd 100644
--- a/src/common/InputHandler.test.ts
+++ b/src/common/InputHandler.test.ts
@@ -202,38 +202,38 @@ describe('InputHandler', () => {
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
inputHandler.setCursorStyle(Params.fromArray([0]));
- assert.equal(optionsService.options['cursorStyle'], 'block');
- assert.equal(optionsService.options['cursorBlink'], true);
+ assert.equal(coreService.decPrivateModes.cursorStyle, undefined);
+ assert.equal(coreService.decPrivateModes.cursorBlink, undefined);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([1]));
- assert.equal(optionsService.options['cursorStyle'], 'block');
- assert.equal(optionsService.options['cursorBlink'], true);
+ assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
+ assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([2]));
- assert.equal(optionsService.options['cursorStyle'], 'block');
- assert.equal(optionsService.options['cursorBlink'], false);
+ assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
+ assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([3]));
- assert.equal(optionsService.options['cursorStyle'], 'underline');
- assert.equal(optionsService.options['cursorBlink'], true);
+ assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
+ assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([4]));
- assert.equal(optionsService.options['cursorStyle'], 'underline');
- assert.equal(optionsService.options['cursorBlink'], false);
+ assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
+ assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([5]));
- assert.equal(optionsService.options['cursorStyle'], 'bar');
- assert.equal(optionsService.options['cursorBlink'], true);
+ assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
+ assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([6]));
- assert.equal(optionsService.options['cursorStyle'], 'bar');
- assert.equal(optionsService.options['cursorBlink'], false);
+ assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
+ assert.equal(coreService.decPrivateModes.cursorBlink, false);
});
});
describe('setMode', () => {
diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index b94d78554..8308e6def 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -2714,7 +2714,7 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* CSI Ps SP q Set cursor style (DECSCUSR, VT520).
- * Ps = 0 -> blinking block.
+ * Ps = 0 -> reset to option.
* Ps = 1 -> blinking block (default).
* Ps = 2 -> steady block.
* Ps = 3 -> blinking underline.
@@ -2724,31 +2724,37 @@ export class InputHandler extends Disposable implements IInputHandler {
*
* @vt: #Y CSI DECSCUSR "Set Cursor Style" "CSI Ps SP q" "Set cursor style."
* Supported cursor styles:
- * - empty, 0 or 1: steady block
- * - 2: blink block
- * - 3: steady underline
- * - 4: blink underline
- * - 5: steady bar
- * - 6: blink bar
+ * - 0: reset to option
+ * - empty, 1: blinking block
+ * - 2: steady block
+ * - 3: blinking underline
+ * - 4: steady underline
+ * - 5: blinking bar
+ * - 6: steady bar
*/
public setCursorStyle(params: IParams): boolean {
- const param = params.params[0] || 1;
- switch (param) {
- case 1:
- case 2:
- this._optionsService.options.cursorStyle = 'block';
- break;
- case 3:
- case 4:
- this._optionsService.options.cursorStyle = 'underline';
- break;
- case 5:
- case 6:
- this._optionsService.options.cursorStyle = 'bar';
- break;
+ const param = params.length === 0 ? 1 : params.params[0];
+ if (param === 0) {
+ this._coreService.decPrivateModes.cursorStyle = undefined;
+ this._coreService.decPrivateModes.cursorBlink = undefined;
+ } else {
+ switch (param) {
+ case 1:
+ case 2:
+ this._coreService.decPrivateModes.cursorStyle = 'block';
+ break;
+ case 3:
+ case 4:
+ this._coreService.decPrivateModes.cursorStyle = 'underline';
+ break;
+ case 5:
+ case 6:
+ this._coreService.decPrivateModes.cursorStyle = 'bar';
+ break;
+ }
+ const isBlinking = param % 2 === 1;
+ this._coreService.decPrivateModes.cursorBlink = isBlinking;
}
- const isBlinking = param % 2 === 1;
- this._optionsService.options.cursorBlink = isBlinking;
return true;
}
diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts
index 8c9634db6..127e1f24b 100644
--- a/src/common/TestUtils.test.ts
+++ b/src/common/TestUtils.test.ts
@@ -89,6 +89,8 @@ export class MockCoreService implements ICoreService {
applicationCursorKeys: false,
applicationKeypad: false,
bracketedPasteMode: false,
+ cursorBlink: undefined,
+ cursorStyle: undefined,
origin: false,
reverseWraparound: false,
sendFocus: false,
diff --git a/src/common/Types.ts b/src/common/Types.ts
index 289aa1f69..c254d330d 100644
--- a/src/common/Types.ts
+++ b/src/common/Types.ts
@@ -268,6 +268,8 @@ export interface IDecPrivateModes {
applicationCursorKeys: boolean;
applicationKeypad: boolean;
bracketedPasteMode: boolean;
+ cursorBlink: boolean | undefined;
+ cursorStyle: CursorStyle | undefined;
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts
index d5e057318..81ab156b5 100644
--- a/src/common/buffer/Buffer.ts
+++ b/src/common/buffer/Buffer.ts
@@ -315,7 +315,8 @@ export class Buffer implements IBuffer {
}
private _reflowLarger(newCols: number, newRows: number): void {
- const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA));
+ const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;
+ const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine);
if (toRemove.length > 0) {
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
@@ -347,6 +348,7 @@ export class Buffer implements IBuffer {
}
private _reflowSmaller(newCols: number, newRows: number): void {
+ const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
// batched up and only committed once
@@ -367,11 +369,13 @@ export class Buffer implements IBuffer {
wrappedLines.unshift(nextLine);
}
- // If these lines contain the cursor don't touch them, the program will handle fixing up
- // wrapped lines with the cursor
- const absoluteY = this.ybase + this.y;
- if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
- continue;
+ if (!reflowCursorLine) {
+ // If these lines contain the cursor don't touch them, the program will handle fixing up
+ // wrapped lines with the cursor
+ const absoluteY = this.ybase + this.y;
+ if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
+ continue;
+ }
}
const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();
diff --git a/src/common/buffer/BufferReflow.ts b/src/common/buffer/BufferReflow.ts
index af1c64738..44aa0976f 100644
--- a/src/common/buffer/BufferReflow.ts
+++ b/src/common/buffer/BufferReflow.ts
@@ -20,8 +20,9 @@ export interface INewLayoutResult {
* @param newCols The columns after resize.
* @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY).
* @param nullCell The cell data to use when filling in empty cells.
+ * @param reflowCursorLine Whether to reflow the line containing the cursor.
*/
-export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
+export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] {
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
// batched up and only committed once
const toRemove: number[] = [];
@@ -41,11 +42,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, o
nextLine = lines.get(++i) as BufferLine;
}
- // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
- // lines with the cursor
- if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
- y += wrappedLines.length - 1;
- continue;
+ if (!reflowCursorLine) {
+ // If these lines contain the cursor don't touch them, the program will handle fixing up
+ // wrapped lines with the cursor
+ if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
+ y += wrappedLines.length - 1;
+ continue;
+ }
}
// Copy buffer data to new locations
diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts
index 9c41fc1a8..5bee63569 100644
--- a/src/common/services/CoreService.ts
+++ b/src/common/services/CoreService.ts
@@ -17,6 +17,8 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
applicationCursorKeys: false,
applicationKeypad: false,
bracketedPasteMode: false,
+ cursorBlink: undefined,
+ cursorStyle: undefined,
origin: false,
reverseWraparound: false,
sendFocus: false,
diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts
index a757c1791..4b8ca8227 100644
--- a/src/common/services/OptionsService.ts
+++ b/src/common/services/OptionsService.ts
@@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly> = {
allowTransparency: false,
tabStopWidth: 8,
theme: {},
+ reflowCursorLine: false,
rescaleOverlappingGlyphs: false,
rightClickSelectsWord: isMac,
windowOptions: {},
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 9d5ca64b2..be1dec064 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -238,6 +238,7 @@ export interface ITerminalOptions {
macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean;
minimumContrastRatio?: number;
+ reflowCursorLine?: boolean;
rescaleOverlappingGlyphs?: boolean;
rightClickSelectsWord?: boolean;
rows?: number;
diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts
index 4a1798ce6..1d37b5c16 100644
--- a/test/playwright/SharedRendererTests.ts
+++ b/test/playwright/SharedRendererTests.ts
@@ -1248,6 +1248,24 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await ctx.value.proxy.scrollLines(-2);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
});
+ test('#5241 cursor with alpha should blend color with background color', async () => {
+ const theme: ITheme = {
+ cursor: '#FF000080'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ await ctx.value.proxy.focus();
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
+ });
+ test('#5241 cursorAccent with alpha should blend color with background color', async () => {
+ const theme: ITheme = {
+ cursorAccent: '#FF000080'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ await ctx.value.proxy.focus();
+ await ctx.value.proxy.write('■');
+ await ctx.value.proxy.write('\x1b[1D');
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
+ });
});
}
diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts
index 3cbde44b2..1085db9b4 100644
--- a/typings/xterm-headless.d.ts
+++ b/typings/xterm-headless.d.ts
@@ -142,6 +142,13 @@ declare module '@xterm/headless' {
*/
minimumContrastRatio?: number;
+ /**
+ * Whether to reflow the line containing the cursor when the terminal is
+ * resized. Defaults to false, because shells usually handle this
+ * themselves.
+ */
+ reflowCursorLine?: boolean;
+
/**
* Whether to rescale glyphs horizontally that are a single cell wide but
* have glyphs that would overlap following cell(s). This typically happens
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index f9cf14f9c..15a033278 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -213,6 +213,13 @@ declare module '@xterm/xterm' {
*/
minimumContrastRatio?: number;
+ /**
+ * Whether to reflow the line containing the cursor when the terminal is
+ * resized. Defaults to false, because shells usually handle this
+ * themselves.
+ */
+ reflowCursorLine?: boolean;
+
/**
* Whether to rescale glyphs horizontally that are a single cell wide but
* have glyphs that would overlap following cell(s). This typically happens