Skip to content

Commit

Permalink
Merge branch 'master' into progress-addon
Browse files Browse the repository at this point in the history
  • Loading branch information
jerch committed Jan 7, 2025
2 parents 94ca05c + 9e20641 commit a1e85a4
Show file tree
Hide file tree
Showing 25 changed files with 250 additions and 114 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ jobs:
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
runs-on: [ubuntu] # macos is flaky
runs-on: [ubuntu-22.04] # macos is flaky
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.runs-on }}-latest
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
Expand Down
10 changes: 7 additions & 3 deletions addons/addon-search/src/SearchAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,12 +499,16 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
const [stringLine, offsets] = cache;

const offset = this._bufferColsToStringOffset(row, col);
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
let searchTerm = term;
let searchStringLine = stringLine;
if (!searchOptions.regex) {
searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
}

let resultIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g');
const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
Expand Down
4 changes: 2 additions & 2 deletions addons/addon-serialize/src/SerializeAddon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ describe('SerializeAddon', () => {

it('empty terminal with default options', async () => {
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
});

it('empty terminal with custom options', async () => {
Expand All @@ -242,7 +242,7 @@ describe('SerializeAddon', () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
});

it('cells with custom color styling', async () => {
Expand Down
64 changes: 46 additions & 18 deletions addons/addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _charAtlasDisposable = this._register(new MutableDisposable());
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
private _deviceMaxTextureSize: number;
private _observerDisposable = this._register(new MutableDisposable());

private _model: RenderModel = new RenderModel();
Expand Down Expand Up @@ -102,6 +103,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
throw new Error('WebGL2 not supported ' + this._gl);
}

this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);

this._register(addDisposableListener(this._canvas, 'webglcontextlost', (e) => {
console.log('webglcontextlost event received');
// Prevent the default behavior in order to enable WebGL context restoration.
Expand Down Expand Up @@ -272,7 +275,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
this.dimensions.device.cell.height,
this.dimensions.device.char.width,
this.dimensions.device.char.height,
this._coreBrowserService.dpr
this._coreBrowserService.dpr,
this._deviceMaxTextureSize
);
if (this._charAtlas !== atlas) {
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
Expand Down Expand Up @@ -354,7 +358,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);
Expand All @@ -377,8 +381,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
let line: IBufferLine;
let joinedRanges: [number, number][];
let isJoined: boolean;
let skipJoinedCheckUntilX: number = 0;
let isValidJoinRange: boolean = true;
let lastCharX: number;
let range: [number, number];
let isCursorRow: boolean;
let chars: string;
let code: number;
let width: number;
Expand All @@ -387,6 +394,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;
Expand All @@ -404,6 +412,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
isCursorRow = cursorY === row;
skipJoinedCheckUntilX = 0;
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
lastBg = this._cellColorResolver.result.bg;
Expand All @@ -415,25 +425,43 @@ export class WebglRenderer extends Disposable implements IRenderer {

// If true, indicates that the current character(s) to draw were joined.
isJoined = false;

// Indicates whether this cell is part of a joined range that should be ignored as it cannot
// be rendered entirely, like the selection state differs across the range.
isValidJoinRange = (x >= skipJoinedCheckUntilX);

lastCharX = x;

// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
if (joinedRanges.length > 0 && x === joinedRanges[0][0] && isValidJoinRange) {
range = joinedRanges.shift()!;

// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly.
cell = new JoinedCellData(
cell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);

// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
// If the ligature's selection state is not consistent, don't join it. This helps the
// selection render correctly regardless whether they should be joined.
const firstSelectionState = this._model.selection.isCellSelected(this._terminal, range[0], row);
for (i = range[0] + 1; i < range[1]; i++) {
isValidJoinRange &&= (firstSelectionState === this._model.selection.isCellSelected(this._terminal, i, row));
}
// Similarly, if the cursor is in the ligature, don't join it.
isValidJoinRange &&= !isCursorRow || cursorX < range[0] || cursorX >= range[1];
if (!isValidJoinRange) {
skipJoinedCheckUntilX = range[1];
} else {
isJoined = true;

// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly.
cell = new JoinedCellData(
cell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);

// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
}
}

chars = cell.getChars();
Expand All @@ -450,18 +478,18 @@ 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
};
lastCursorX = cursorX + cell.getWidth() - 1;
}
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 =
Expand Down
3 changes: 2 additions & 1 deletion addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);

this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr, 2048);
this._charAtlas.warmUp();
}

Expand Down
60 changes: 38 additions & 22 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -136,7 +136,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();
Expand Down Expand Up @@ -246,6 +246,7 @@ if (document.location.pathname === '/test') {
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest);
document.getElementById('ligatures-test').addEventListener('click', ligaturesTest);
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
document.getElementById('bce').addEventListener('click', coloredErase);
addVtButtons();
Expand All @@ -268,7 +269,7 @@ function createTerminal(): void {
backend: 'conpty',
buildNumber: 22621
} : undefined,
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"',
theme: xtermjsTheme
} as ITerminalOptions);

Expand Down Expand Up @@ -1275,9 +1276,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');
Expand All @@ -1290,22 +1291,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];
Expand All @@ -1315,6 +1317,20 @@ function addVtButtons(): void {
document.querySelector('#vt-container').appendChild(vtFragment);
}

function ligaturesTest(): void {
term.write([
'',
'-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
'=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
'<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
'<~~ </ </> /> ~~> == != /= ~= <> === !== !=== =/= =!=',
'<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
'(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <!-- <!---',
'==== ===== ====== ======= ======== =========',
'---- ----- ------ ------- -------- ---------'
].join('\r\n'));
}

function testWeblinks(): void {
const linkExamples = `
aaa http://example.com aaa http://example.com aaa
Expand Down
3 changes: 3 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ <h3>Test</h3>
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
<dd><button id="decoration-stress-test" title="Toggle between adding and removing a decoration to each line">Stress Test</button></dd>

<dt>Ligatures Addon</dt>
<dd><button id="ligatures-test" title="Write common ligatures sequences">Common ligatures</button></dd>

<dt>Weblinks Addon</dt>
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>

Expand Down
6 changes: 4 additions & 2 deletions src/browser/CoreBrowserTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
}

// If the terminal is already opened
if (this.element?.ownerDocument.defaultView && this._coreBrowserService) {
if (this.element?.ownerDocument.defaultView && this._coreBrowserService && this.element?.isConnected) {
// Adjust the window if needed
if (this.element.ownerDocument.defaultView !== this._coreBrowserService.window) {
this._coreBrowserService.window = this.element.ownerDocument.defaultView;
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
Loading

0 comments on commit a1e85a4

Please sign in to comment.