Use jsdoc/multiline-blocks instead of multiline-comment-style

Replace multiline-comment-style (starred-block), which also forces
multiline // comments into block comments, with jsdoc/multiline-blocks.
That rule only enforces starred JSDoc layout for /** blocks (no text on
the opening line) and leaves multiline // comments unchanged for
internal documentation.

Fix multiline JSDoc in Types.ts.

Fixes #5899

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-05-26 20:32:23 +00:00
co-authored by Daniel Imms
parent 4ec85e67ba
commit df3422fb0b
138 changed files with 1834 additions and 3028 deletions
+12 -20
View File
@@ -26,24 +26,18 @@ test.describe('FitAddon', () => {
`);
});
/*
* test.beforeEach(async function(): Promise<any> {
* await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display=''`);
* await openTerminal(page);
* });
*/
// test.beforeEach(async function(): Promise<any> {
// await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display=''`);
// await openTerminal(page);
// });
/*
* after(async () => {
* await browser.close();
* });
*/
// after(async () => {
// await browser.close();
// });
/*
* afterEach(async function(): Promise<any> {
* await ctx.proxy.dispose();
* });
*/
// afterEach(async function(): Promise<any> {
// await ctx.proxy.dispose();
// });
test('no terminal', async function(): Promise<any> {
await ctx.page.evaluate(`window.fit2 = new FitAddon();`);
@@ -85,10 +79,8 @@ test.describe('FitAddon', () => {
await ctx.page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
await setDimensions();
const dimensions: { cols: number, rows: number } | undefined = await ctx.page.evaluate(`window.fit.proposeDimensions()`);
/*
* The value of dims will be undefined if the char measure strategy falls back to the DOM
* method, so only assert if it's not undefined.
*/
// The value of dims will be undefined if the char measure strategy falls back to the DOM
// method, so only assert if it's not undefined.
if (dimensions) {
ok(dimensions.cols > 85);
ok(dimensions.cols < 88);
+2 -4
View File
@@ -24,10 +24,8 @@ export interface IHeaderFields {
height?: string;
// Optional, defaults to 1 respecting aspect ratio (width takes precedence).
preserveAspectRatio?: number;
/*
* Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
* (download not supported).
*/
// Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
// (download not supported).
inline?: number;
}
+2 -4
View File
@@ -23,10 +23,8 @@ export function imageType(d: Uint8Array): IMetrics {
return UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
/*
* PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
* + first chunk must be IHDR
*/
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
+12 -18
View File
@@ -137,13 +137,11 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
// enable size reports
if (this._opts.enableSizeReports) {
/*
* const windowOptions = terminal.getOption('windowOptions');
* windowOptions.getWinSizePixels = true;
* windowOptions.getCellSizePixels = true;
* windowOptions.getWinSizeChars = true;
* terminal.setOption('windowOptions', windowOptions);
*/
// const windowOptions = terminal.getOption('windowOptions');
// windowOptions.getWinSizePixels = true;
// windowOptions.getCellSizePixels = true;
// windowOptions.getWinSizeChars = true;
// terminal.setOption('windowOptions', windowOptions);
const windowOps = terminal.options.windowOptions ?? {};
windowOps.getWinSizePixels = true;
windowOps.getCellSizePixels = true;
@@ -292,13 +290,11 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
if (params[0]) {
return true;
}
/*
* reported features:
* 62 - VT220
* 4 - SIXEL support
* 9 - charsets
* 22 - ANSI colors
*/
// reported features:
// 62 - VT220
// 4 - SIXEL support
// 9 - charsets
// 22 - ANSI colors
if (this._opts.sixelSupport) {
this._report(`\x1b[?62;4;9;22c`);
return true;
@@ -356,10 +352,8 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
let width = this._renderer?.dimensions?.css.canvas.width;
let height = this._renderer?.dimensions?.css.canvas.height;
if (!width || !height) {
/*
* for some reason we have no working image renderer
* --> fallback to default cell size
*/
// for some reason we have no working image renderer
// --> fallback to default cell size
const cellSize = CELL_SIZE_DEFAULT;
width = (this._terminal?.cols || 80) * cellSize.width;
height = (this._terminal?.rows || 24) * cellSize.height;
+14 -22
View File
@@ -197,12 +197,10 @@ export class ImageRenderer extends Disposable implements IDisposable {
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
/*
* Floor all pixel offsets to get stable tile mapping without any overflows.
* Note: For not pixel perfect aligned cells like in the DOM renderer
* this will move a tile slightly to the top/left (subpixel range, thus ignore it).
* FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
*/
// Floor all pixel offsets to get stable tile mapping without any overflows.
// Note: For not pixel perfect aligned cells like in the DOM renderer
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
@@ -344,27 +342,21 @@ export class ImageRenderer extends Disposable implements IDisposable {
);
canvas.classList.add(`xterm-image-layer-${layer}`);
const screenElement = this._terminal._core.screenElement;
/*
* Use isolation to create a stacking context without overriding z-index,
* which would conflict with integrators (e.g. VS Code) that set their
* own z-index on the screen element.
*/
// Use isolation to create a stacking context without overriding z-index,
// which would conflict with integrators (e.g. VS Code) that set their
// own z-index on the screen element.
screenElement.style.isolation = 'isolate';
if (layer === 'bottom') {
/*
* Use z-index:-1 so it paints behind non-positioned text elements.
* The screen element needs to be a stacking context (via isolation)
* to contain the negative z-index, otherwise it would go behind the
* entire terminal.
*/
// Use z-index:-1 so it paints behind non-positioned text elements.
// The screen element needs to be a stacking context (via isolation)
// to contain the negative z-index, otherwise it would go behind the
// entire terminal.
canvas.style.zIndex = '-1';
screenElement.insertBefore(canvas, screenElement.firstChild);
} else {
/*
* Explicit z-index ensures the image canvas reliably stacks above
* the text layer (DOM renderer rows). z-index: 0 is below the
* selection overlay (z-index: 1).
*/
// Explicit z-index ensures the image canvas reliably stacks above
// the text layer (DOM renderer rows). z-index: 0 is below the
// selection overlay (z-index: 1).
canvas.style.zIndex = '0';
screenElement.appendChild(canvas);
}
+17 -31
View File
@@ -154,10 +154,8 @@ export class ImageStorage implements IDisposable {
for (const spec of this._images.values()) {
spec.marker?.dispose();
}
/*
* NOTE: marker.dispose above already calls ImageBitmap.close
* therefore we can just wipe the map here
*/
// NOTE: marker.dispose above already calls ImageBitmap.close
// therefore we can just wipe the map here
this._images.clear();
this._renderer.clearAll();
}
@@ -308,10 +306,8 @@ export class ImageStorage implements IDisposable {
this._delImg(id);
}
/*
* eviction marker:
* delete the image when the marker gets disposed
*/
// eviction marker:
// delete the image when the marker gets disposed
const endMarker = this._terminal.registerMarker(0);
endMarker?.onDispose(() => {
const spec = this._images.get(imageId);
@@ -320,10 +316,8 @@ export class ImageStorage implements IDisposable {
}
});
/*
* since markers do not work on alternate for some reason,
* we evict images here manually
*/
// since markers do not work on alternate for some reason,
// we evict images here manually
if (this._terminal.buffer.active.type === 'alternate') {
this._evictOnAlternate();
}
@@ -422,12 +416,10 @@ export class ImageStorage implements IDisposable {
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
const placeholderCalls: { col: number, row: number, count: number }[] = [];
/*
* walk all cells in viewport and collect tiles found
* Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
* because text writes clear the BG flag but leave image tile data intact.
* This lets top-layer images survive text overwrites (kitty C=1 behavior).
*/
// walk all cells in viewport and collect tiles found
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
// because text writes clear the BG flag but leave image tile data intact.
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
for (let row = start; row <= end; ++row) {
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
if (!line) return;
@@ -500,10 +492,8 @@ export class ImageStorage implements IDisposable {
return;
}
/*
* handle only viewport width enlargements, exit all other cases
* TODO: needs patch for tile counter
*/
// handle only viewport width enlargements, exit all other cases
// TODO: needs patch for tile counter
if (this._viewportMetrics.cols >= metrics.cols) {
this._viewportMetrics = metrics;
return;
@@ -591,10 +581,8 @@ export class ImageStorage implements IDisposable {
}
}
/*
* TODO: Do we need some blob offloading tricks here to avoid early eviction?
* also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
*/
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
private _evictOldest(room: number): number {
const used = this._getStoredPixels();
let current = used;
@@ -617,11 +605,9 @@ export class ImageStorage implements IDisposable {
const old = line._extendedAttrs[x];
if (old) {
if (old.imageId !== undefined) {
/*
* found an old ExtendedAttrsImage, since we know that
* they are always isolated instances (single cell usage),
* we can re-use it and just update their id entries
*/
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
+4 -8
View File
@@ -113,17 +113,13 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
* Some helpers to extract current terminal colors.
*/
/*
* get currently active background color from terminal
* also respect INVERSE setting
*/
// get currently active background color from terminal
// also respect INVERSE setting
function extractActiveBg(attr: AttributeData, colors: ReadonlyColorSet | undefined): RGBA8888 {
let bg = 0;
if (!colors) {
/*
* FIXME: theme service is prolly not available yet,
* happens if .open() was not called yet (bug in core?)
*/
// FIXME: theme service is prolly not available yet,
// happens if .open() was not called yet (bug in core?)
return bg;
}
if (attr.isInverse()) {
@@ -62,11 +62,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Storage related states
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
/*
* Tracks the pending key of the most recently started chunked upload.
* Per spec, subsequent chunks only need m= (and optionally q=), without i=.
* When a chunk arrives with no i=, this key is used to find the pending upload.
*/
// Tracks the pending key of the most recently started chunked upload.
// Per spec, subsequent chunks only need m= (and optionally q=), without i=.
// When a chunk arrives with no i=, this key is used to find the pending upload.
private _lastPendingKey: number | undefined;
constructor(
@@ -175,11 +173,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
private _streamPayload(data: Uint32Array, start: number, end: number): void {
if (this._aborted) return;
/*
* Check size limit (compare encoded bytes against pre-calculated limit)
* Include cumulative size from pending transmission for multi-chunk images.
* Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
*/
// Check size limit (compare encoded bytes against pre-calculated limit)
// Include cumulative size from pending transmission for multi-chunk images.
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
const pendingKey = this._parsedCommand?.id ?? this._lastPendingKey ?? 0;
const pending = this._pendingTransmissions.get(pendingKey);
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
@@ -289,11 +285,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
}
this._activeDecoder = null;
/*
* Handle command first — handlers create Blob/ImageData from imageBytes,
* which copies the data. Only then is it safe to release the decoder's
* wasm memory that imageBytes points into.
*/
// Handle command first — handlers create Blob/ImageData from imageBytes,
// which copies the data. Only then is it safe to release the decoder's
// wasm memory that imageBytes points into.
const result = this._handleCommandWithBytesAndCmd(finalCmd, imageBytes, decodeError);
if (decoder) {
decoder.release();
@@ -331,12 +325,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
case KittyAction.PLACEMENT:
return this._handlePlacement(cmd);
default:
/*
* TODO: Implement remaining actions when needed:
* - a=f (frame): animation frame operations
* - a=a (animation): animation control
* - a=c (compose): compose images
*/
// TODO: Implement remaining actions when needed:
// - a=f (frame): animation frame operations
// - a=a (animation): animation control
// - a=c (compose): compose images
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
}
@@ -350,10 +342,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
switch (action) {
case KittyAction.TRANSMIT: {
const result = this._handleTransmit(cmd, bytes, decodeError);
/*
* Only send response when _handleTransmit didn't already respond
* (it handles unsupported transmission medium responses internally)
*/
// Only send response when _handleTransmit didn't already respond
// (it handles unsupported transmission medium responses internally)
if ((cmd.transmission ?? 'd') === 'd' && cmd.id !== undefined) {
if (decodeError) {
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
@@ -371,12 +361,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// a=p ignores any payload — image data was already transmitted
return this._handlePlacement(cmd);
default:
/*
* TODO: Implement remaining actions when needed:
* - a=f (frame): animation frame operations
* - a=a (animation): animation control
* - a=c (compose): compose images
*/
// TODO: Implement remaining actions when needed:
// - a=f (frame): animation frame operations
// - a=a (animation): animation control
// - a=c (compose): compose images
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
}
@@ -402,18 +390,16 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
}
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
/*
* TODO: Support file-based transmission modes (t=f, t=t, t=s)
* Currently only supports direct transmission (t=d, the default).
* - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
* - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
* - t=s: Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
* These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
* 1. Check cmd.transmission (t key) before treating bytes as image data
* 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
* 3. For t=d: treat bytes as image data (current behavior)
* When implementing, also update _handleQuery to accept these transmission mediums.
*/
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
// Currently only supports direct transmission (t=d, the default).
// - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
// - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
// - t=s: Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
// These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
// 1. Check cmd.transmission (t key) before treating bytes as image data
// 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
// 3. For t=d: treat bytes as image data (current behavior)
// When implementing, also update _handleQuery to accept these transmission mediums.
const transmission = cmd.transmission ?? 'd';
if (transmission !== 'd') {
if (cmd.id !== undefined) {
@@ -463,11 +449,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const id = cmd.id ?? 0;
const quiet = cmd.quiet ?? 0;
/*
* Per spec: reject unsupported transmission mediums (only t=d is supported atm)
* TODO: When filesystem support is added (Node.js/Electron), update this to accept
* t=f (file), t=t (temp file), and t=s (shared memory) and respond OK for queries.
*/
// Per spec: reject unsupported transmission mediums (only t=d is supported atm)
// TODO: When filesystem support is added (Node.js/Electron), update this to accept
// t=f (file), t=t (temp file), and t=s (shared memory) and respond OK for queries.
const transmission = cmd.transmission ?? 'd';
if (transmission !== 'd') {
this._sendResponse(id, 'EINVAL:unsupported transmission medium', quiet);
@@ -516,11 +500,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Per spec: default delete selector is 'a' (delete all visible placements)
const selector = cmd.deleteSelector ?? 'a';
/*
* TODO: Distinguish lowercase (delete placements only) from uppercase
* (delete placements + free stored image data). Currently both variants
* free everything since we don't separate stored data from placements.
*/
// TODO: Distinguish lowercase (delete placements only) from uppercase
// (delete placements + free stored image data). Currently both variants
// free everything since we don't separate stored data from placements.
switch (selector) {
case 'a':
case 'A':
@@ -529,12 +511,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
break;
case 'i':
case 'I':
/*
* TODO: When placement id tracking is implemented (see TODO in
* KittyImageStorage), d=i with p=<pid> should delete only that
* specific placement, while d=i without p should delete all
* placements for the image.
*/
// TODO: When placement id tracking is implemented (see TODO in
// KittyImageStorage), d=i with p=<pid> should delete only that
// specific placement, while d=i without p should delete all
// placements for the image.
if (cmd.id !== undefined) {
const pending = this._pendingTransmissions.get(cmd.id);
if (pending) {
@@ -596,10 +576,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
/*
* Per spec: c/r default to image's natural cell dimensions.
* If only one of c/r is specified, compute the other from image aspect ratio.
*/
// Per spec: c/r default to image's natural cell dimensions.
// If only one of c/r is specified, compute the other from image aspect ratio.
let imgCols: number;
let imgRows: number;
if (cmd.columns !== undefined && cmd.rows !== undefined) {
@@ -635,12 +613,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const savedY = buffer.y;
const savedYbase = buffer.ybase;
/*
* Determine layer based on z-index: negative = behind text, 0+ = on top.
* When z<0 we always use the bottom layer even without allowTransparency —
* the image will simply be hidden behind the opaque text background, which
* is the correct behavior (client asked for "behind text").
*/
// Determine layer based on z-index: negative = behind text, 0+ = on top.
// When z<0 we always use the bottom layer even without allowTransparency —
// the image will simply be hidden behind the opaque text background, which
// is the correct behavior (client asked for "behind text").
const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0;
const layer: ImageLayer = wantsBottom ? 'bottom' : 'top';
@@ -654,12 +630,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const xOffset = Math.min(Math.max(0, cmd.xOffset ?? 0), cw - 1);
const yOffset = Math.min(Math.max(0, cmd.yOffset ?? 0), ch - 1);
if (xOffset !== 0 || yOffset !== 0) {
/*
* Per spec: X/Y is not added to c/r area. When c/r are explicit, the
* total placement area remains c*cw × r*ch pixels and the offset image
* is clipped to fit. When c/r are unset, the padded canvas determines
* the natural cell dimensions.
*/
// Per spec: X/Y is not added to c/r area. When c/r are explicit, the
// total placement area remains c*cw × r*ch pixels and the offset image
// is clipped to fit. When c/r are unset, the padded canvas determines
// the natural cell dimensions.
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
@@ -690,11 +664,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
this._kittyStorage.addImage(image.id, bitmap, true, layer, zIndex);
bitmap = undefined; // ownership transferred to storage
/*
* Kitty cursor movement
* Per spec: cursor placed at first column after last image column,
* on the last row of the image. C=1 means don't move cursor.
*/
// Kitty cursor movement
// Per spec: cursor placed at first column after last image column,
// on the last row of the image. C=1 means don't move cursor.
if (cmd.cursorMovement === 1) {
// C=1: restore cursor to position before image was placed
const scrolled = buffer.ybase - savedYbase;
@@ -702,10 +674,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Can't restore cursor to scrollback?
buffer.y = Math.max(savedY - scrolled, 0);
} else {
/*
* Default (C=0): advance cursor horizontally past the image
* addImage already positioned cursor on the last row via lineFeeds
*/
// Default (C=0): advance cursor horizontally past the image
// addImage already positioned cursor on the last row via lineFeeds
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
}
} catch (e) {
@@ -766,11 +736,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA), width, height));
}
/*
* RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
* 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
* Assumes little-endian (all modern browsers/Node.js).
*/
// RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
// 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
// Assumes little-endian (all modern browsers/Node.js).
const data = new Uint8ClampedArray(pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA);
const src32 = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
const dst32 = new Uint32Array(data.buffer);
@@ -7,10 +7,8 @@
import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
/*
* Kitty graphics protocol action types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
*/
// Kitty graphics protocol action types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
export const enum KittyAction {
TRANSMIT = 't',
TRANSMIT_DISPLAY = 'T',
@@ -19,29 +17,23 @@ export const enum KittyAction {
DELETE = 'd'
}
/*
* Kitty graphics protocol format types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
*/
// Kitty graphics protocol format types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
export const enum KittyFormat {
RGB = 24,
RGBA = 32,
PNG = 100
}
/*
* Kitty graphics protocol compression types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
*/
// Kitty graphics protocol compression types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
export const enum KittyCompression {
NONE = '',
ZLIB = 'z'
}
/*
* Kitty graphics protocol control data keys.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
*/
// Kitty graphics protocol control data keys.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
export const enum KittyKey {
// Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
ACTION = 'a',
@@ -123,10 +115,8 @@ export interface IKittyCommand {
payload?: string;
}
/*
* Pending chunked transmission state.
* Stores metadata from the first chunk while accumulating decoded payload data.
*/
// Pending chunked transmission state.
// Stores metadata from the first chunk while accumulating decoded payload data.
export interface IPendingTransmission {
// The parsed command from the first chunk (contains action, format, dimensions, etc.)
cmd: IKittyCommand;
@@ -8,31 +8,27 @@ import { ImageStorage } from '../ImageStorage';
import { ImageLayer } from '../Types';
import { IKittyImageData } from './KittyGraphicsTypes';
/*
* Kitty-specific image storage controller.
*
* Wraps shared ImageStorage with kitty protocol semantics:
* - tracks transmitted image payloads by kitty image id
* - tracks kitty image id -> shared ImageStorage id mapping for displayed images
* - mirrors shared-storage evictions into kitty maps
* - applies protocol-level undisplayed-image eviction policy
*/
// Kitty-specific image storage controller.
//
// Wraps shared ImageStorage with kitty protocol semantics:
// - tracks transmitted image payloads by kitty image id
// - tracks kitty image id -> shared ImageStorage id mapping for displayed images
// - mirrors shared-storage evictions into kitty maps
// - applies protocol-level undisplayed-image eviction policy
export class KittyImageStorage implements IDisposable {
private static readonly _maxStoredImages = 256;
private _nextImageId = 1;
private readonly _images: Map<number, IKittyImageData> = new Map();
/*
* TODO: Support multiple placements per image. The kitty spec identifies
* placements by an (image id, placement id) pair — same i + different p
* values should coexist, and same i + same p should replace the prior
* placement. Currently we track only one storage entry per kitty image id,
* so multiple placements of the same image overwrite each other. Fixing
* this requires changing these maps to Map<number, Map<number, number>>
* (kittyId → placementId → storageId) and updating addImage/deleteById
* accordingly. The underlying shared ImageStorage would also need to
* support multiple entries per logical image.
*/
// TODO: Support multiple placements per image. The kitty spec identifies
// placements by an (image id, placement id) pair — same i + different p
// values should coexist, and same i + same p should replace the prior
// placement. Currently we track only one storage entry per kitty image id,
// so multiple placements of the same image overwrite each other. Fixing
// this requires changing these maps to Map<number, Map<number, number>>
// (kittyId → placementId → storageId) and updating addImage/deleteById
// accordingly. The underlying shared ImageStorage would also need to
// support multiple entries per logical image.
private readonly _kittyIdToStorageId: Map<number, number> = new Map();
private readonly _storageIdToKittyId: Map<number, number> = new Map();
@@ -94,12 +90,10 @@ export class KittyImageStorage implements IDisposable {
}
public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
/*
* Clean up stale reverse-mapping from a previous placement of the same
* kitty image. The old shared-storage entry is kept (it may still be
* visible on screen) but its reverse mapping is removed so that eviction
* of the old entry won't incorrectly delete the kitty image data.
*/
// Clean up stale reverse-mapping from a previous placement of the same
// kitty image. The old shared-storage entry is kept (it may still be
// visible on screen) but its reverse mapping is removed so that eviction
// of the old entry won't incorrectly delete the kitty image data.
const oldStorageId = this._kittyIdToStorageId.get(kittyId);
if (oldStorageId !== undefined) {
this._storageIdToKittyId.delete(oldStorageId);
+2 -4
View File
@@ -64,10 +64,8 @@ const TESTDATA: ITestData = (() => {
};
})();
const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER;
/*
* const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
* const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
*/
// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
// NOTE: the data is loaded as string for easier transport through playwright
const TESTDATA_IIP: [string, [number, number]][] = [
+33 -57
View File
@@ -101,14 +101,12 @@ test.beforeAll(async ({ browser }) => {
test.afterAll(async () => await ctx.page.close());
test.describe('Kitty Graphics Protocol', () => {
/*
* TODO: Add tests for larger images with various dimensions
* TODO: Add tests for virtual placement (U=1)
* TODO: Add tests for animation frames
* TODO: Add performance tests for streaming large images
* TODO: Implement cursor movement per Kitty spec - cursor should move by cols/rows after placement (unless C=1)
* TODO: Distinguish lowercase delete selectors (placement only) from uppercase (placement + free data)
*/
// TODO: Add tests for larger images with various dimensions
// TODO: Add tests for virtual placement (U=1)
// TODO: Add tests for animation frames
// TODO: Add performance tests for streaming large images
// TODO: Implement cursor movement per Kitty spec - cursor should move by cols/rows after placement (unless C=1)
// TODO: Distinguish lowercase delete selectors (placement only) from uppercase (placement + free data)
test.beforeEach(async ({}, testInfo) => {
await ctx.page.evaluate(`
@@ -211,10 +209,8 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('enforces size limit across chunked transmissions', async () => {
/*
* Create a custom addon with very small size limit (100 bytes)
* The 1x1 PNG is ~164 bytes base64, so 2 chunks should exceed 100
*/
// Create a custom addon with very small size limit (100 bytes)
// The 1x1 PNG is ~164 bytes base64, so 2 chunks should exceed 100
await ctx.page.evaluate(() => {
(window as any).smallLimitAddon = new ImageAddon({
kittySupport: true,
@@ -1283,10 +1279,8 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('only c specified computes r from aspect ratio', async () => {
/*
* 200x100 image (2:1 aspect) with c=10.
* Per spec: r = ceil((h/w) * c * cw / ch) = ceil(0.5 * 10 * cw / ch)
*/
// 200x100 image (2:1 aspect) with c=10.
// Per spec: r = ceil((h/w) * c * cw / ch) = ceil(0.5 * 10 * cw / ch)
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=225;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(100);
@@ -1303,10 +1297,8 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('only r specified computes c from aspect ratio', async () => {
/*
* 200x100 image (2:1 aspect) with r=5.
* Per spec: c = ceil((w/h) * r * ch / cw) = ceil(2 * 5 * ch / cw)
*/
// 200x100 image (2:1 aspect) with r=5.
// Per spec: c = ceil((w/h) * r * ch / cw) = ceil(2 * 5 * ch / cw)
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=226;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(100);
@@ -1324,11 +1316,9 @@ test.describe('Kitty Graphics Protocol', () => {
});
test.describe('Cursor positioning', () => {
/*
* NOTE: Current tests document ACTUAL behavior (MVP - cursor doesn't move)
* Per Kitty spec: cursor placed at first column after last image column,
* on the last row of the image. C=1 means don't move cursor.
*/
// NOTE: Current tests document ACTUAL behavior (MVP - cursor doesn't move)
// Per Kitty spec: cursor placed at first column after last image column,
// on the last row of the image. C=1 means don't move cursor.
test('cursor advances past 1x1 image', async () => {
const cursorBefore = await getCursor();
@@ -1386,10 +1376,8 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('cursor should move right by cols when c specified', async () => {
/*
* c=5, r computed from 1x1 aspect ratio: r = ceil((1/1) * (5*cw) / ch)
* With 1:1 aspect, image height in pixels = 5*cw, so r = ceil(5*cw/ch)
*/
// c=5, r computed from 1x1 aspect ratio: r = ceil((1/1) * (5*cw) / ch)
// With 1:1 aspect, image height in pixels = 5*cw, so r = ceil(5*cw/ch)
await ctx.proxy.write(`\x1b_Ga=T,f=100,c=5;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
await timeout(100);
@@ -1646,12 +1634,10 @@ test.describe('Kitty Graphics Protocol', () => {
});
test.describe('Pixel verification', () => {
/*
* The 200x100 image has 20 colored rectangles in a 10x2 grid.
* Each rectangle is 20px wide x 50px tall.
* Top row (y=0..49): Red, Orange, Yellow, Lime, Green, Cyan, SkyBlue, Blue, Purple, Magenta
* Bottom row (y=50..99): Pink, Brown, Maroon, Olive, Teal, Navy, Gray, DarkGray, LightGray, White
*/
// The 200x100 image has 20 colored rectangles in a 10x2 grid.
// Each rectangle is 20px wide x 50px tall.
// Top row (y=0..49): Red, Orange, Yellow, Lime, Green, Cyan, SkyBlue, Blue, Purple, Magenta
// Bottom row (y=50..99): Pink, Brown, Maroon, Olive, Teal, Navy, Gray, DarkGray, LightGray, White
test('renders red rectangle at top-left origin (0,0)', async () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
@@ -1664,11 +1650,9 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200);
/*
* Sample center of each top-row rectangle (y=25, x=10,30,50,...,190)
* All within the first cell row, so we read from the canvas at cell (0,0)
* Red at x=10
*/
// Sample center of each top-row rectangle (y=25, x=10,30,50,...,190)
// All within the first cell row, so we read from the canvas at cell (0,0)
// Red at x=10
deepStrictEqual(await getPixel(0, 0, 10, 25), [255, 0, 0, 255]);
// Orange at x=30
deepStrictEqual(await getPixel(0, 0, 30, 25), [255, 128, 0, 255]);
@@ -1684,10 +1668,8 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200);
/*
* Bottom row starts at y=50. Center at y=75.
* Pink at x=10
*/
// Bottom row starts at y=50. Center at y=75.
// Pink at x=10
deepStrictEqual(await getPixel(0, 0, 10, 75), [255, 192, 203, 255]);
// Brown at x=30
deepStrictEqual(await getPixel(0, 0, 30, 75), [165, 42, 42, 255]);
@@ -1743,10 +1725,8 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('scales cropped source region to c/r placement rectangle', async () => {
/*
* Firefox's createImageBitmap uses different resize sampling, producing
* slightly off pixel values compared to Chromium, so skip on Firefox.
*/
// Firefox's createImageBitmap uses different resize sampling, producing
// slightly off pixel values compared to Chromium, so skip on Firefox.
if (ctx.browser.browserType().name() === 'firefox') {
test.skip();
}
@@ -2157,10 +2137,8 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('memory limit eviction cleans Kitty handler maps', async () => {
/*
* Resize terminal to fit 7 non-overlapping 200x100 images without scrolling.
* Each image ≈ 29 cols × 8 rows at default cell size.
*/
// Resize terminal to fit 7 non-overlapping 200x100 images without scrolling.
// Each image ≈ 29 cols × 8 rows at default cell size.
await ctx.page.evaluate(`
window.term.reset();
window.imageAddon?.dispose();
@@ -2169,11 +2147,9 @@ test.describe('Kitty Graphics Protocol', () => {
window.term.loadAddon(window.imageAddon);
`);
/*
* storageLimit 0.5 MB = 125,000 pixels. Each 200x100 image = 20,000 pixels.
* 6 images = 120K pixels (under limit). 7th triggers eviction (140K > 125K).
* Place non-overlapping so tile-count eviction doesn't interfere.
*/
// storageLimit 0.5 MB = 125,000 pixels. Each 200x100 image = 20,000 pixels.
// 6 images = 120K pixels (under limit). 7th triggers eviction (140K > 125K).
// Place non-overlapping so tile-count eviction doesn't interfere.
const positions = [[1, 1], [30, 1], [1, 9], [30, 9], [1, 17], [30, 17]];
for (let n = 0; n < 6; n++) {
const [c, r] = positions[n];
+6 -10
View File
@@ -43,11 +43,9 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
throw new Error('Permission to access local fonts not granted.');
}
} catch (err: unknown) {
/*
* A `TypeError` indicates the 'local-fonts'
* permission is not yet implemented, so
* only `throw` if this is _not_ the problem.
*/
// A `TypeError` indicates the 'local-fonts'
// permission is not yet implemented, so
// only `throw` if this is _not_ the problem.
if (err instanceof Error && err.name !== 'TypeError') {
throw err;
}
@@ -91,11 +89,9 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
const fonts = await fontsPromise;
for (const family of parse(fontFamily)) {
/*
* If we reach one of the generic font families, the font resolution
* will end for the browser and we can't determine the specific font
* used. Throw.
*/
// If we reach one of the generic font families, the font resolution
// will end for the browser and we can't determine the specific font
// used. Throw.
if (genericFontFamilies.includes(family)) {
return undefined;
}
@@ -88,11 +88,9 @@ class FontImpl implements IFont {
glyphIds.push(this._font.charToGlyphIndex(char));
}
/*
* If there are no lookup groups, there's no point looking for
* replacements. This gives us a minor performance boost for fonts with
* no ligatures
*/
// If there are no lookup groups, there's no point looking for
// replacements. This gives us a minor performance boost for fonts with
// no ligatures
if (this._lookupTrees.length === 0) {
return {
inputGlyphs: glyphIds,
@@ -115,10 +113,8 @@ class FontImpl implements IFont {
}
public findLigatureRanges(text: string): [number, number][] {
/*
* Short circuit the process if there are no possible ligatures in the
* font
*/
// Short circuit the process if there are no possible ligatures in the
// font
if (this._lookupTrees.length === 0) {
return [];
}
@@ -174,11 +170,9 @@ class FontImpl implements IFont {
}
}
} else {
/*
* We don't need to do the lastGlyphIndex tracking here because
* reverse processing isn't allowed to replace more than one
* character at a time.
*/
// We don't need to do the lastGlyphIndex tracking here because
// reverse processing isn't allowed to replace more than one
// character at a time.
for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) {
const result = walkTree(lookup.tree, sequence, i, i);
if (result) {
@@ -230,10 +224,8 @@ class FontImpl implements IFont {
for (let j = 0; j < lookups.length; j++) {
const lookupIndex = lookups[j];
if (lookupIndex >= start) {
/*
* Update the lookup information if it's the one we're
* storing or earlier than it.
*/
// Update the lookup information if it's the one we're
// storing or earlier than it.
if (result.index === null || lookupIndex <= result.index) {
result.index = lookupIndex;
@@ -32,11 +32,9 @@ export default function mergeTrees(trees: ILookupTree[]): ILookupTree {
function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
// Need to fix this recursively (and handle lookups)
for (const [glyphId, value] of Object.entries(mergeTree.individual)) {
/*
* The main tree is guaranteed to have no overlaps between the
* individual and range values, so if we match an invididual, there
* must not be a range
*/
// The main tree is guaranteed to have no overlaps between the
// individual and range values, so if we match an invididual, there
// must not be a range
if (mainTree.individual[glyphId]) {
mergeTreeEntry(mainTree.individual[glyphId], value, mergedEntries);
} else {
@@ -51,17 +49,13 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntri
matched = true;
/*
* If they overlap, we have to split the range and then
* merge the overlap
*/
// If they overlap, we have to split the range and then
// merge the overlap
mainTree.individual[glyphId] = value;
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
/*
* When there's an overlap, we also have to fix up the range
* that we had already processed
*/
// When there's an overlap, we also have to fix up the range
// that we had already processed
mainTree.range.splice(index, 1);
for (const glyph of overlap.second) {
if (Array.isArray(glyph)) {
@@ -82,11 +76,9 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntri
}
for (const { range, entry } of mergeTree.range) {
/*
* Ranges are more complicated, because they can overlap with
* multiple things, individual and range alike. We start by
* eliminating ranges that are already present in another range
*/
// Ranges are more complicated, because they can overlap with
// multiple things, individual and range alike. We start by
// eliminating ranges that are already present in another range
let remainingRanges: (number | [number, number])[] = [range];
for (let index = 0; index < mainTree.range.length; index++) {
@@ -131,17 +123,13 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntri
continue;
}
/*
* If they overlap, we have to split the range and then
* merge the overlap
*/
// If they overlap, we have to split the range and then
// merge the overlap
mainTree.individual[remainingRange] = cloneEntry(entry);
mergeTreeEntry(mainTree.individual[remainingRange], cloneEntry(resultEntry), mergedEntries);
/*
* When there's an overlap, we also have to fix up the range
* that we had already processed
*/
// When there's an overlap, we also have to fix up the range
// that we had already processed
mainTree.range.splice(index, 1);
index--;
@@ -18,18 +18,14 @@ export default function mergeRange(ranges: [number, number][], newRangeStart: nu
return ranges;
}
if (newRangeEnd <= range[1]) {
/*
* Case 2: New range is either wholly contained within the
* search range or overlaps with the front of it
*/
// Case 2: New range is either wholly contained within the
// search range or overlaps with the front of it
range[0] = Math.min(newRangeStart, range[0]);
return ranges;
}
if (newRangeStart < range[1]) {
/*
* Case 3: New range either wholly contains the search range
* or overlaps with the end of it
*/
// Case 3: New range either wholly contains the search range
// or overlaps with the end of it
range[0] = Math.min(newRangeStart, range[0]);
inRange = true;
} else {
@@ -38,27 +34,21 @@ export default function mergeRange(ranges: [number, number][], newRangeStart: nu
}
} else {
if (newRangeEnd <= range[0]) {
/*
* Case 5: New range extends from previous range but doesn't
* reach the current one
*/
// Case 5: New range extends from previous range but doesn't
// reach the current one
ranges[i - 1][1] = newRangeEnd;
return ranges;
}
if (newRangeEnd <= range[1]) {
/*
* Case 6: New range extends from prvious range into the
* current range
*/
// Case 6: New range extends from prvious range into the
// current range
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
ranges.splice(i, 1);
inRange = false;
return ranges;
}
/*
* Case 7: New range extends from previous range past the
* end of the current range
*/
// Case 7: New range extends from previous range past the
// end of the current range
ranges.splice(i, 1);
i--;
}
@@ -72,10 +72,8 @@ export default function buildTree(table: ChainingContextualSubstitutionTable.IFo
);
}
/*
* When we get to the end, all of the entries we've accumulated
* should have a lookup defined
*/
// When we get to the end, all of the entries we've accumulated
// should have a lookup defined
for (const { entry, substitutions } of currentEntries) {
entry.lookup = {
substitutions,
@@ -53,10 +53,8 @@ export default function buildTree(table: ChainingContextualSubstitutionTable.IFo
);
}
/*
* When we get to the end, all of the entries we've accumulated
* should have a lookup defined
*/
// When we get to the end, all of the entries we've accumulated
// should have a lookup defined
for (const { entry, substitutions } of currentEntries) {
entry.lookup = {
substitutions,
@@ -57,10 +57,8 @@ export function processLookaheadPosition(
range: []
};
/*
* All glyphs at this position share ONE entry - lookahead just needs to match,
* all paths lead to the same result
*/
// All glyphs at this position share ONE entry - lookahead just needs to match,
// all paths lead to the same result
const sharedEntry: ILookupTreeEntry = {};
for (const glyph of glyphs) {
@@ -102,10 +100,8 @@ export function processBacktrackPosition(
range: []
};
/*
* All glyphs at this position share ONE entry - backtrack just needs to match,
* all paths lead to the same result
*/
// All glyphs at this position share ONE entry - backtrack just needs to match,
// all paths lead to the same result
const sharedEntry: ILookupTreeEntry = {};
for (const glyph of glyphs) {
@@ -49,10 +49,8 @@ export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId
switch (table.substFormat) {
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1
case 1:
/*
* TODO: determine if there's a rhyme or reason to the 16-bit
* wraparound and if it can ever be a different number
*/
// TODO: determine if there's a rhyme or reason to the 16-bit
// wraparound and if it can ever be a different number
return (glyphId + table.deltaGlyphId) % (2 ** 16);
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
case 2:
+8 -16
View File
@@ -15,10 +15,8 @@ const enum LoadingState {
FAILED
}
/*
* Caches 100K characters worth of ligatures. In practice this works out to
* about 650 KB worth of cache, when a moderate number of ligatures are present.
*/
// Caches 100K characters worth of ligatures. In practice this works out to
// about 650 KB worth of cache, when a moderate number of ligatures are present.
const CACHE_SIZE = 100000;
/**
@@ -48,10 +46,8 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
load(currentCallFontName, CACHE_SIZE)
.then(f => {
/*
* Another request may have come in while we were waiting, so make
* sure our font is still vaild.
*/
// Another request may have come in while we were waiting, so make
// sure our font is still vaild.
if (currentCallFontName === term.options.fontFamily) {
loadingState = LoadingState.LOADED;
font = f;
@@ -63,10 +59,8 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
}
})
.catch(e => {
/*
* Another request may have come in while we were waiting, so make
* sure our font is still vaild.
*/
// Another request may have come in while we were waiting, so make
// sure our font is still vaild.
if (currentCallFontName === term.options.fontFamily) {
loadingState = LoadingState.FAILED;
if (term.options.logLevel === 'debug') {
@@ -79,10 +73,8 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
}
if (font && loadingState === LoadingState.LOADED) {
/*
* We clone the entries to avoid the internal cache of the ligature finder
* getting messed up.
*/
// We clone the entries to avoid the internal cache of the ligature finder
// getting messed up.
return font.findLigatureRanges(text).map<[number, number]>(
range => [range[0], range[1]]
);
+6 -12
View File
@@ -27,10 +27,8 @@ export default function parse(family: string): string[] {
const families = [];
let currentFamily = '';
/*
* Work through the input character by character until there are none left.
* This lexing and parsing in one pass.
*/
// Work through the input character by character until there are none left.
// This lexing and parsing in one pass.
while (context.offset < context.input.length) {
const char = context.input[context.offset++];
switch (char) {
@@ -154,17 +152,13 @@ function parseUnicode(context: IParseContext): string {
while (context.offset < context.input.length) {
const char = context.input[context.offset++];
if (/\s/.test(char)) {
/*
* The first whitespace character after a unicode escape indicates the end
* of the escape and is swallowed.
*/
// The first whitespace character after a unicode escape indicates the end
// of the escape and is swallowed.
return unicodeToString(str);
}
if (str.length >= 6 || !/[\dA-Fa-f]/.test(char)) {
/*
* If the next character is not a valid hex digit or we have reached the
* maximum of 6 digits in the escape, terminate the escape.
*/
// If the next character is not a valid hex digit or we have reached the
// maximum of 6 digits in the escape, terminate the escape.
context.offset--;
return unicodeToString(str);
}
+6 -12
View File
@@ -261,10 +261,8 @@ export class SearchEngine {
return;
}
/*
* This will iterate until we find the line start.
* When we find it, we will search using the calculated start column.
*/
// This will iterate until we find the line start.
// When we find it, we will search using the calculated start column.
searchPosition.startRow--;
searchPosition.startCol += this._terminal.cols;
return this._findInLine(term, searchPosition, searchOptions);
@@ -317,10 +315,8 @@ export class SearchEngine {
return;
}
/*
* Adjust the row number and search index if needed since a "line" of text can span multiple
* rows
*/
// Adjust the row number and search index if needed since a "line" of text can span multiple
// rows
let startRowOffset = 0;
while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
startRowOffset++;
@@ -359,10 +355,8 @@ export class SearchEngine {
if (char.length > 1) {
offset -= char.length - 1;
}
/*
* Adjust the searchIndex for empty characters following wide unicode
* chars (eg. CJK)
*/
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const nextCell = line.getCell(i + 1);
if (nextCell && nextCell.getWidth() === 0) {
offset++;
+2 -4
View File
@@ -38,10 +38,8 @@ export class SearchLineCache extends Disposable {
private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeout = this._register(new MutableDisposable());
private _linesCacheDisposables = this._register(new MutableDisposable());
/*
* Track access to avoid recreating a timeout on every init call which occurs once per search
* result (findNext/findPrevious -> _highlightAllMatches -> find loop).
*/
// Track access to avoid recreating a timeout on every init call which occurs once per search
// result (findNext/findPrevious -> _highlightAllMatches -> find loop).
private _lastAccessTimestamp = 0;
constructor(private readonly _terminal: Terminal) {
+36 -68
View File
@@ -84,10 +84,8 @@ function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boole
}
function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
/*
* If neither cell has underline, consider them equal regardless of internal underline color
* values
*/
// If neither cell has underline, consider them equal regardless of internal underline color
// values
if (!cell1.isUnderline() && !cell2.isUnderline()) {
return true;
}
@@ -136,18 +134,14 @@ class StringSerializeHandler extends BaseSerializeHandler {
private _currentRow: string = '';
private _nullCellCount: number = 0;
/*
* we can see a full colored cell and a null cell that only have background the same style
* but the information isn't preserved by null cell itself
* so wee need to record it when required.
*/
// we can see a full colored cell and a null cell that only have background the same style
// but the information isn't preserved by null cell itself
// so wee need to record it when required.
private _cursorStyle: IBufferCell = this._buffer.getNullCell();
/*
* where exact the cursor styles comes from
* because we can't copy the cell directly
* so we remember where the content comes from instead
*/
// where exact the cursor styles comes from
// because we can't copy the cell directly
// so we remember where the content comes from instead
private _cursorStyleRow: number = 0;
private _cursorStyleCol: number = 0;
@@ -178,10 +172,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
protected _rowEnd(row: number, isLastRow: boolean): void {
/*
* if there is colorful empty cell at line end, whe must pad it back, or the the color block
* will missing
*/
// if there is colorful empty cell at line end, whe must pad it back, or the the color block
// will missing
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
// use clear right to set background.
this._currentRow += `\u001b[${this._nullCellCount}X`;
@@ -214,10 +206,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!;
const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1;
/*
* validate whether this line wrap is ever possible
* which mean whether cursor can placed at a overflow position (x === row) naturally
*/
// validate whether this line wrap is ever possible
// which mean whether cursor can placed at a overflow position (x === row) naturally
let isValid = false;
if (
@@ -226,31 +216,23 @@ class StringSerializeHandler extends BaseSerializeHandler {
isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0
) {
if (
/*
* the last character can't be null,
* you can't use control sequence to move cursor to (x === row)
*/
// the last character can't be null,
// you can't use control sequence to move cursor to (x === row)
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
/*
* change background of the first wrapped cell also affects BCE
* so we mark it as invalid to simply the process to determine line separator
*/
// change background of the first wrapped cell also affects BCE
// so we mark it as invalid to simply the process to determine line separator
equalBg(thisRowLastChar, nextRowFirstChar)
) {
isValid = true;
}
if (
/*
* the second to last character can't be null if the next line starts with CJK,
* you can't use control sequence to move cursor to (x === row)
*/
// the second to last character can't be null if the next line starts with CJK,
// you can't use control sequence to move cursor to (x === row)
isNextRowFirstCharDoubleWidth &&
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
/*
* change background of the first wrapped cell also affects BCE
* so we mark it as invalid to simply the process to determine line separator
*/
// change background of the first wrapped cell also affects BCE
// so we mark it as invalid to simply the process to determine line separator
equalBg(thisRowLastChar, nextRowFirstChar) &&
equalBg(thisRowLastSecondChar, nextRowFirstChar)
) {
@@ -259,10 +241,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
if (!isValid) {
/*
* force the wrap with magic
* insert enough character to force the wrap
*/
// force the wrap with magic
// insert enough character to force the wrap
rowSeparator = '-'.repeat(this._nullCellCount + 1);
// move back and erase next line head
rowSeparator += '\u001b[1D\u001b[1X';
@@ -276,10 +256,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
rowSeparator += '\u001b[B';
}
/*
* This is content and need the be serialized even it is invisible.
* without this, wrap will be missing from outputs.
*/
// This is content and need the be serialized even it is invisible.
// without this, wrap will be missing from outputs.
this._lastContentCursorRow = row + 1;
this._lastContentCursorCol = 0;
@@ -380,10 +358,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
const sgrSeq = this._diffStyle(cell, this._cursorStyle);
/*
* the empty cell style is only assumed to be changed when background changed, because
* foreground is always 0.
*/
// the empty cell style is only assumed to be changed when background changed, because
// foreground is always 0.
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;
/**
@@ -422,11 +398,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
this._nullCellCount += cell.getWidth();
} else {
if (this._nullCellCount > 0) {
/*
* we can just assume we have same style with previous one here
* because style change is handled by previous stage
* use move right when background is empty, use clear right when there is background.
*/
// we can just assume we have same style with previous one here
// because style change is handled by previous stage
// use move right when background is empty, use clear right when there is background.
if (equalBg(this._cursorStyle, this._backgroundCell)) {
this._currentRow += `\u001b[${this._nullCellCount}C`;
} else {
@@ -447,10 +421,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
protected _serializeString(excludeFinalCursorPosition: boolean): string {
let rowEnd = this._allRows.length;
/*
* the fixup is only required for data without scrollback
* because it will always be placed at last line otherwise
*/
// the fixup is only required for data without scrollback
// because it will always be placed at last line otherwise
if (this._buffer.length - this._firstRow <= this._terminal.rows) {
rowEnd = this._lastContentCursorRow + 1 - this._firstRow;
this._lastCursorCol = this._lastContentCursorCol;
@@ -494,11 +466,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
}
/*
* Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
* HACK: Internal API access since it's awkward to expose this in the API and serialize will
* likely be the only consumer
*/
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
// HACK: Internal API access since it's awkward to expose this in the API and serialize will
// likely be the only consumer
const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;
const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);
if (sgrSeq.length > 0) {
@@ -611,10 +581,8 @@ export class SerializeAddon implements ITerminalAddon, ISerializeApi {
}
}
/*
* Cursor visibility (DECTCEM)
* Default: visible
*/
// Cursor visibility (DECTCEM)
// Default: visible
if (!modes.showCursor) {
content += '\x1b[?25l';
}
@@ -416,13 +416,11 @@ test.describe('SerializeAddon', () => {
'中文中文',
'12中文',
'中文12',
/*
* This line is going to be wrapped at last character
* because it has line length of 11 (1+2*5).
* We concat it back without the null cell currently.
* But this may be incorrect.
* see also #3097
*/
// This line is going to be wrapped at last character
// because it has line length of 11 (1+2*5).
// We concat it back without the null cell currently.
// But this may be incorrect.
// see also #3097
'1中文中文中'
];
await ctx.proxy.write(lines.join('\r\n'));
@@ -505,11 +503,9 @@ test.describe('SerializeAddon', () => {
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
/*
* A line wrap happened after current line.
* But there is no content.
* so wrap shouldn't even be able to happen.
*/
// A line wrap happened after current line.
// But there is no content.
// so wrap shouldn't even be able to happen.
const segments = [
`123456789012345`,
MOVE_UP(1),
@@ -535,11 +531,9 @@ test.describe('SerializeAddon', () => {
10
);
/*
* A line wrap happened after current line.
* But there is no content.
* so wrap shouldn't even be able to happen.
*/
// A line wrap happened after current line.
// But there is no content.
// so wrap shouldn't even be able to happen.
const lines = [
padLines.join('\r\n'),
'\r\n',
@@ -22,11 +22,9 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
= UnicodeService.createPropertyValue(UC.GRAPHEME_BREAK_Other, 1, false);
public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {
/*
* Optimize the simple ASCII case, under the condition that
* UnicodeService.extractCharKind(preceding) === GRAPHEME_BREAK_Other
* (which also covers the case that preceding === 0).
*/
// Optimize the simple ASCII case, under the condition that
// UnicodeService.extractCharKind(preceding) === GRAPHEME_BREAK_Other
// (which also covers the case that preceding === 0).
if ((codepoint >= 32 && codepoint < 127) && (preceding >> 3) === 0) {
return UnicodeGraphemeProvider._plainNarrowProperties;
}
@@ -21,10 +21,8 @@ test.describe('WebFontsAddon', () => {
deepStrictEqual(empty, []);
});
test.afterEach(async () => {
/*
* for font loading tests to work, we have to remove added rules and fonts
* to work around the quite aggressive font caching done by the browsers
*/
// for font loading tests to work, we have to remove added rules and fonts
// to work around the quite aggressive font caching done by the browsers
await ctx.page.evaluate(`
document.styleSheets[0].deleteRule(1);
document.styleSheets[0].deleteRule(0);
@@ -75,10 +73,8 @@ test.describe('WebFontsAddon', () => {
});
test('autoload & relayout from ctor', async ({ browser }) => {
/*
* to make this test work, we exclude the default measurement char W (x57) by restricting unicode-range
* now the browser will postpone font loading until codepoint is hit --> wrong glyph metrics on first usage
*/
// to make this test work, we exclude the default measurement char W (x57) by restricting unicode-range
// now the browser will postpone font loading until codepoint is hit --> wrong glyph metrics on first usage
const data = await ctx.page.evaluate(`
document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/fonts/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0);
`);
@@ -101,10 +97,8 @@ test.describe('WebFontsAddon', () => {
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]);
}
/*
* good case: addon fixes layout for webfont in ctor
* the relayout happens async, so wait a bit with a promise
*/
// good case: addon fixes layout for webfont in ctor
// the relayout happens async, so wait a bit with a promise
await ctx.page.evaluate(`
window.helperTerm.dispose();
window.helperTerm = new Terminal({fontFamily: '"Kongtext", ' + term.options.fontFamily});
+7 -11
View File
@@ -73,10 +73,8 @@ export class LinkComputer {
continue;
}
/*
* map string positions back to buffer positions
* values are 0-based right side excluding
*/
// map string positions back to buffer positions
// values are 0-based right side excluding
const [startY, startX] = LinkComputer._mapStrIdx(terminal, startLineIndex, 0, match.index);
const [endY, endX] = LinkComputer._mapStrIdx(terminal, startY, startX, text.length);
@@ -174,13 +172,11 @@ export class LinkComputer {
if (width) {
stringIndex -= chars.length || 1;
/*
* correct stringIndex for early wrapped wide chars:
* - currently only happens at last cell
* - cells to the right are reset with chars='' and width=1 in InputHandler.print
* - follow-up line must be wrapped and contain wide char at first cell
* --> if all these conditions are met, correct stringIndex by +1
*/
// correct stringIndex for early wrapped wide chars:
// - currently only happens at last cell
// - cells to the right are reset with chars='' and width=1 in InputHandler.print
// - follow-up line must be wrapped and contain wide char at first cell
// --> if all these conditions are met, correct stringIndex by +1
if (i === line.length - 1 && chars === '') {
const line = buf.getLine(lineIndex + 1);
if (line && line.isWrapped) {
+11 -13
View File
@@ -7,19 +7,17 @@ import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
import type { WebLinksAddon as IWebLinksApi } from '@xterm/addon-web-links';
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
/*
* consider everthing starting with http:// or https://
* up to first whitespace, `"` or `'` as url
* NOTE: The repeated end clause is needed to not match a dangling `:`
* resembling the old (...)*([^:"\'\\s]) final path clause
* additionally exclude early + final:
* - unsafe from rfc3986: !*'()
* - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
* also exclude as finals:
* - final interpunction like ,.!?
* - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
* - unsafe chars from rfc1738: {}|\^~[]`
*/
// consider everthing starting with http:// or https://
// up to first whitespace, `"` or `'` as url
// NOTE: The repeated end clause is needed to not match a dangling `:`
// resembling the old (...)*([^:"\'\\s]) final path clause
// additionally exclude early + final:
// - unsafe from rfc3986: !*'()
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
// also exclude as finals:
// - final interpunction like ,.!?
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
// - unsafe chars from rfc1738: {}|\^~[]`
const strictUrlRegex = /(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;
+6 -12
View File
@@ -47,10 +47,8 @@ export class CellColorResolver {
this.result.bg = cell.bg;
this.result.fg = cell.fg;
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
/*
* Get any foreground/background overrides, this happens on the model to avoid spreading
* override logic throughout the different sub-renderers
*/
// Get any foreground/background overrides, this happens on the model to avoid spreading
// override logic throughout the different sub-renderers
// Reset overrides work variables
$bg = 0;
@@ -188,10 +186,8 @@ export class CellColorResolver {
}
});
/*
* Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
* ahead of time in order to use the correct cache key
*/
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
// ahead of time in order to use the correct cache key
if ($hasBg) {
if ($isSelected) {
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
@@ -206,10 +202,8 @@ export class CellColorResolver {
$fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB;
}
/*
* Handle case where inverse was specified by only one of bg override or fg override was set,
* resolving the other inverse color and setting the inverse flag if needed.
*/
// Handle case where inverse was specified by only one of bg override or fg override was set,
// resolving the other inverse color and setting the inverse flag if needed.
if (this.result.fg & FgFlags.INVERSE) {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
+2 -4
View File
@@ -13,10 +13,8 @@ import type { ILogService } from 'common/services/Services';
interface ITextureAtlasCacheEntry {
atlas: ITextureAtlas;
config: ICharAtlasConfig;
/*
* N.B. This implementation potentially holds onto copies of the terminal forever, so
* this may cause memory leaks.
*/
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: Terminal[];
}
+2 -4
View File
@@ -25,10 +25,8 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
scrollbarSliderBackground: NULL_COLOR,
scrollbarSliderHoverBackground: NULL_COLOR,
scrollbarSliderActiveBackground: NULL_COLOR,
/*
* For the static char atlas, we only use the first 16 colors, but we need all 256 for the
* dynamic character atlas.
*/
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
ansi: colors.ansi.slice(),
contrastCache: colors.contrastCache,
halfContrastCache: colors.halfContrastCache
+3 -5
View File
@@ -6,9 +6,7 @@
import { isFirefox, isLegacyEdge } from 'common/Platform';
export const DIM_OPACITY = 0.5;
/*
* The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge
* would result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
* unaligned Powerline fonts (PR 3356#issuecomment-850928179).
*/
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge
// would result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
// unaligned Powerline fonts (PR 3356#issuecomment-850928179).
export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic';
@@ -87,17 +87,13 @@ export class CursorBlinkStateManager {
this._blinkInterval = undefined;
}
/*
* Setup the initial timeout which will hide the cursor, this is done before
* the regular interval is setup in order to support restarting the blink
* animation in a lightweight way (without thrashing clearInterval and
* setInterval).
*/
// Setup the initial timeout which will hide the cursor, this is done before
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
/*
* Check if another animation restart was requested while this was being
* started
*/
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
const time = Constants.BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
@@ -118,10 +114,8 @@ export class CursorBlinkStateManager {
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
/*
* calc time diff
* Make restart interval do a setTimeout initially?
*/
// calc time diff
// Make restart interval do a setTimeout initially?
const time = Constants.BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
this._restartInterval(time);
@@ -6,12 +6,10 @@
import { toDisposable, IDisposable } from 'common/Lifecycle';
export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
/*
* Observe any resizes to the element and extract the actual pixel size of the element if the
* devicePixelContentBoxSize API is supported. This allows correcting rounding errors when
* converting between CSS pixels and device pixels which causes blurry rendering when device
* pixel ratio is not a round number.
*/
// Observe any resizes to the element and extract the actual pixel size of the element if the
// devicePixelContentBoxSize API is supported. This allows correcting rounding errors when
// converting between CSS pixels and device pixels which causes blurry rendering when device
// pixel ratio is not a round number.
let observer: ResizeObserver | undefined = new parentWindow.ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === element);
if (!entry) {
+23 -37
View File
@@ -148,11 +148,9 @@ export class GlyphRenderer extends Disposable {
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
/*
* Setup the unit quad element array buffer, this points to indices in
* unitQuadVertices to allow is to draw 2 triangles from the vertices via a
* triangle strip
*/
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
// triangle strip
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this._register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
@@ -191,10 +189,8 @@ export class GlyphRenderer extends Disposable {
gl.uniform1iv(this._textureLocation, textureUnits);
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
/*
* Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
* is ever drawn it will show characters as red rectangles.
*/
// Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
// is ever drawn it will show characters as red rectangles.
this._atlasTextures = [];
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
const glTexture = new GLTexture(throwIfFalsy(gl.createTexture()));
@@ -220,22 +216,18 @@ export class GlyphRenderer extends Disposable {
}
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
/*
* Since this function is called for every cell (`rows*cols`), it must be very optimized. It
* should not instantiate any variables unless a new glyph is drawn to the cache where the
* slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
* each glyph.
*/
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph.
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg);
}
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
$i = (y * this._terminal.cols + x) * Constants.INDICES_PER_CELL;
/*
* Exit early if this is a null character, allow space character to continue as it may have
* underline/strikethrough styles
*/
// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
array.fill(0, $i, $i + Constants.INDICES_PER_CELL - 1 - Constants.CELL_POSITION_INDICES);
return;
@@ -287,10 +279,8 @@ export class GlyphRenderer extends Disposable {
}
// a_cellpos only changes on resize
/*
* Reduce scale horizontally for wide glyphs printed in cells that would overlap with the
* following cell (ie. the width is not 2).
*/
// Reduce scale horizontally for wide glyphs printed in cells that would overlap with the
// following cell (ie. the width is not 2).
if (this._optionsService.rawOptions.rescaleOverlappingGlyphs) {
if (allowRescaling(code, width, $glyph.size.x, this._dimensions.device.cell.width)) {
array[$i + 2] = (this._dimensions.device.cell.width - 1) / this._dimensions.device.canvas.width; // - 1 to improve readability
@@ -349,15 +339,13 @@ export class GlyphRenderer extends Disposable {
this._activeBuffer = (this._activeBuffer + 1) % 2;
const activeBuffer = this._vertices.attributesBuffers[this._activeBuffer];
/*
* Copy data for each cell of each line up to its line length (the last non-whitespace cell)
* from the attributes buffer into activeBuffer, which is the one that gets bound to the GPU.
* The reasons for this are as follows:
* - So the active buffer can be alternated so we don't get blocked on rendering finishing
* - To copy either the normal attributes buffer or the selection attributes buffer when there
* is a selection
* - So we don't send vertices for all the line-ending whitespace to the GPU
*/
// Copy data for each cell of each line up to its line length (the last non-whitespace cell)
// from the attributes buffer into activeBuffer, which is the one that gets bound to the GPU.
// The reasons for this are as follows:
// - So the active buffer can be alternated so we don't get blocked on rendering finishing
// - To copy either the normal attributes buffer or the selection attributes buffer when there
// is a selection
// - So we don't send vertices for all the line-ending whitespace to the GPU
let bufferLength = 0;
for (let y = 0; y < renderModel.lineLengths.length; y++) {
const si = y * this._terminal.cols * Constants.INDICES_PER_CELL;
@@ -370,11 +358,9 @@ export class GlyphRenderer extends Disposable {
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
/*
* Bind the atlas page texture if they have changed. AtlasPage.version is globally
* monotonic, so a page object swap at the same index (which happens after a page merge)
* is detected by the same comparison.
*/
// Bind the atlas page texture if they have changed. AtlasPage.version is globally
// monotonic, so a page object swap at the same index (which happens after a page merge)
// is detected by the same comparison.
for (let i = 0; i < this._atlas.pages.length; i++) {
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
this._bindAtlasPageTexture(gl, this._atlas, i);
+3 -5
View File
@@ -114,11 +114,9 @@ export class RectangleRenderer extends Disposable {
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
/*
* Setup the unit quad element array buffer, this points to indices in
* unitQuadVertices to allow is to draw 2 triangles from the vertices via a
* triangle strip
*/
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
// triangle strip
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this._register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
+77 -129
View File
@@ -151,17 +151,13 @@ export class TextureAtlas implements ITextureAtlas {
}
private _createNewPage(): AtlasPage {
/*
* Try merge the set of the 4 most used pages of the largest size. This is is deferred to a
* microtask to ensure it does not interrupt textures that will be rendered in the current
* animation frame which would result in blank rendered areas. This is actually not that
* expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
*/
// Try merge the set of the 4 most used pages of the largest size. This is is deferred to a
// microtask to ensure it does not interrupt textures that will be rendered in the current
// animation frame which would result in blank rendered areas. This is actually not that
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
if (TextureAtlas.maxAtlasPages && this._pages.length >= Math.max(4, TextureAtlas.maxAtlasPages)) {
/*
* Find the set of the largest 4 images, below the maximum size, with the highest
* percentages used
*/
// Find the set of the largest 4 images, below the maximum size, with the highest
// percentages used
const pagesBySize = this._pages.filter(e => {
return e.canvas.width * 2 <= (TextureAtlas.maxTextureSize || Constants.FORCED_MAX_TEXTURE_SIZE);
}).sort((a, b) => {
@@ -184,10 +180,8 @@ export class TextureAtlas implements ITextureAtlas {
// Gather details of the merge
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
/*
* Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
* effectively reduce page count and merging would cause issues.
*/
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
// effectively reduce page count and merging would cause issues.
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
const newPage = new AtlasPage(this._document, this._textureSize);
this._pages.push(newPage);
@@ -300,11 +294,9 @@ export class TextureAtlas implements ITextureAtlas {
private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean, dim: boolean): IColor {
if (this._config.allowTransparency) {
/*
* The background color might have some transparency, so we need to render it as fully
* transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
* around the anti-aliased edges of the glyph, and it would look too dark.
*/
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return NULL_COLOR;
}
@@ -426,10 +418,8 @@ export class TextureAtlas implements ITextureAtlas {
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
/*
* Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from
* non-dim cells
*/
// Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from
// non-dim cells
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio / (dim ? 2 : 1));
if (!result) {
@@ -457,26 +447,20 @@ export class TextureAtlas implements ITextureAtlas {
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
/*
* Uncomment for debugging
* console.log(`draw to cache "${chars}"`, bg, fg, ext);
*/
// Uncomment for debugging
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
/*
* Attach the canvas to the DOM in order to inherit font-feature-settings
* from the parent elements. This is necessary for ligatures and variants to
* work.
*/
// Attach the canvas to the DOM in order to inherit font-feature-settings
// from the parent elements. This is necessary for ligatures and variants to
// work.
if (domContainer && this._tmpCanvas.parentElement !== domContainer) {
this._tmpCanvas.style.display = 'none';
domContainer.append(this._tmpCanvas);
}
/*
* Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
* to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
* giant ligatures (eg. =====>) don't impact overall performance.
*/
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
// giant ligatures (eg. =====>) don't impact overall performance.
const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._config.deviceMaxTextureSize);
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
@@ -519,10 +503,8 @@ export class TextureAtlas implements ITextureAtlas {
// draw the background
const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse, dim);
/*
* Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha,
* regardless of transparency in backgroundColor
*/
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha,
// regardless of transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
@@ -550,11 +532,9 @@ export class TextureAtlas implements ITextureAtlas {
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
}
/*
* Whether to clear pixels based on a threshold difference between the glyph color and the
* background color. This should be disabled when the glyph contains multiple colors such as
* underline colors to prevent important colors could get cleared.
*/
// Whether to clear pixels based on a threshold difference between the glyph color and the
// background color. This should be disabled when the glyph contains multiple colors such as
// underline colors to prevent important colors could get cleared.
let enableClearThresholdCheck = !powerlineGlyph;
let chWidth: number;
@@ -619,10 +599,8 @@ export class TextureAtlas implements ITextureAtlas {
clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, yBot - yTop);
this._tmpCtx.clip(clipRegion);
/*
* Draw a zigzag pattern, this is derived from the SVG used in monaco for the same
* style. The viewbox is 6x3 so scale it using that.
*/
// Draw a zigzag pattern, this is derived from the SVG used in monaco for the same
// style. The viewbox is 6x3 so scale it using that.
const cellW = this._config.deviceCellWidth;
const curlyH = (yBot - yTop);
const scaleX = cellW / 6;
@@ -693,21 +671,15 @@ export class TextureAtlas implements ITextureAtlas {
}
this._tmpCtx.restore();
/*
* Draw stroke in the background color for non custom characters in order to give an outline
* between the text and the underline. Only do this when font size is >= 12 as the underline
* looks odd when the font size is too small
*/
// Draw stroke in the background color for non custom characters in order to give an outline
// between the text and the underline. Only do this when font size is >= 12 as the underline
// looks odd when the font size is too small
if (!customGlyph && this._config.fontSize >= 12) {
/*
* This only works when transparency is disabled because it's not clear how to clear stroked
* text
*/
// This only works when transparency is disabled because it's not clear how to clear stroked
// text
if (!this._config.allowTransparency && chars !== ' ') {
/*
* Measure the text, only draw the stroke if there is a descent beyond an alphabetic text
* baseline
*/
// Measure the text, only draw the stroke if there is a descent beyond an alphabetic text
// baseline
this._tmpCtx.save();
this._tmpCtx.textBaseline = 'alphabetic';
const metrics = this._tmpCtx.measureText(chars);
@@ -715,11 +687,9 @@ export class TextureAtlas implements ITextureAtlas {
if ('actualBoundingBoxDescent' in metrics && metrics.actualBoundingBoxDescent > 0) {
// This translates to 1/2 the line width in either direction
this._tmpCtx.save();
/*
* Clip the region to only draw in valid pixels near the underline to avoid a slight
* outline around the whole glyph, as well as additional pixels in the glyph at the top
* which would increase GPU memory demands
*/
// Clip the region to only draw in valid pixels near the underline to avoid a slight
// outline around the whole glyph, as well as additional pixels in the glyph at the top
// which would increase GPU memory demands
const clipRegion = new Path2D();
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.deviceCellWidth * chWidth, yBot - yTop + Math.ceil(lineWidth / 2));
this._tmpCtx.clip(clipRegion);
@@ -749,10 +719,8 @@ export class TextureAtlas implements ITextureAtlas {
this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight);
}
/*
* If this character is underscore and beyond the cell bounds, shift it up until it is visible
* even on the bottom row, try for a maximum of 5 pixels.
*/
// If this character is underscore and beyond the cell bounds, shift it up until it is visible
// even on the bottom row, try for a maximum of 5 pixels.
if (chars === '_' && !this._config.allowTransparency) {
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
if (isBeyondCellBounds) {
@@ -784,10 +752,8 @@ export class TextureAtlas implements ITextureAtlas {
this._tmpCtx.restore();
/*
* clear the background from the character to avoid issues with drawing over the previous
* character if it extends past it's bounds
*/
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
const imageData = this._tmpCtx.getImageData(
0, 0, this._tmpCanvas.width, this._tmpCanvas.height
);
@@ -830,16 +796,12 @@ export class TextureAtlas implements ITextureAtlas {
}
}
/*
* TODO: This algorithm could be simplified:
* - Search for the page with ROW_PIXEL_THRESHOLD in mind
* - Keep track of current/fixed rows in a Map
*/
// TODO: This algorithm could be simplified:
// - Search for the page with ROW_PIXEL_THRESHOLD in mind
// - Keep track of current/fixed rows in a Map
/*
* Replace the best current row with a fixed row if there is one at least as good as the
* current row. Search in reverse to prioritize filling in older pages.
*/
// Replace the best current row with a fixed row if there is one at least as good as the
// current row. Search in reverse to prioritize filling in older pages.
for (let i = this._activePages.length - 1; i >= 0; i--) {
for (const row of this._activePages[i].fixedRows) {
if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) {
@@ -870,16 +832,12 @@ export class TextureAtlas implements ITextureAtlas {
break;
}
/*
* Create a new page if too much vertical space would be wasted or there is not enough room
* left in the page. The previous active row will become fixed in the process as it now has a
* fixed height
*/
// Create a new page if too much vertical space would be wasted or there is not enough room
// left in the page. The previous active row will become fixed in the process as it now has a
// fixed height
if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) {
/*
* Create the new fixed height row, creating a new page if there isn't enough room on the
* current page
*/
// Create the new fixed height row, creating a new page if there isn't enough room on the
// current page
let wasPageAndRowFound = false;
if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) {
// Find the first page with room to create the new row on
@@ -894,12 +852,10 @@ export class TextureAtlas implements ITextureAtlas {
if (candidatePage) {
activePage = candidatePage;
} else {
/*
* Before creating a new atlas page that would trigger a page merge, check if the
* current active row is sufficient when ignoring the ROW_PIXEL_THRESHOLD. This will
* improve texture utilization by using the available space before the page is merged
* and becomes static.
*/
// Before creating a new atlas page that would trigger a page merge, check if the
// current active row is sufficient when ignoring the ROW_PIXEL_THRESHOLD. This will
// improve texture utilization by using the available space before the page is merged
// and becomes static.
if (
TextureAtlas.maxAtlasPages &&
this._pages.length >= TextureAtlas.maxAtlasPages &&
@@ -967,10 +923,8 @@ export class TextureAtlas implements ITextureAtlas {
rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width;
rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height;
/*
* Update atlas current row, for fixed rows the glyph height will never be larger than the row
* height
*/
// Update atlas current row, for fixed rows the glyph height will never be larger than the row
// height
activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y);
activeRow.x += rasterizedGlyph.size.x;
@@ -1103,18 +1057,16 @@ class AtlasPage {
public static nextVersion: number = 0;
public version = ++AtlasPage.nextVersion;
/*
* Texture atlas current positioning data. The texture packing strategy used is to fill from
* left-to-right and top-to-bottom. When the glyph being written is less than half of the current
* row's height, the following happens:
*
* - The current row becomes the fixed height row A
* - A new fixed height row B the exact size of the glyph is created below the current row
* - A new dynamic height current row is created below B
*
* This strategy does a good job preventing space being wasted for very short glyphs such as
* underscores, hyphens etc. or those with underlines rendered.
*/
// Texture atlas current positioning data. The texture packing strategy used is to fill from
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
// row's height, the following happens:
//
// - The current row becomes the fixed height row A
// - A new fixed height row B the exact size of the glyph is created below the current row
// - A new dynamic height current row is created below B
//
// This strategy does a good job preventing space being wasted for very short glyphs such as
// underscores, hyphens etc. or those with underlines rendered.
public currentRow: ICharAtlasActiveRow = {
x: 0,
y: 0,
@@ -1134,11 +1086,9 @@ class AtlasPage {
}
}
this.canvas = createCanvas(document, size, size);
/*
* The canvas needs alpha because we use clearColor to convert the background color to alpha.
* It might also contain some characters with transparent backgrounds if allowTransparency is
* set.
*/
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
}
@@ -1166,14 +1116,12 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThreshol
const fgG = fg.rgba >>> 16 & 0xFF;
const fgB = fg.rgba >>> 8 & 0xFF;
/*
* Calculate a threshold that when below a color will be treated as transpart when the sum of
* channel value differs. This helps improve rendering when glyphs overlap with others. This
* threshold is calculated relative to the difference between the background and foreground to
* ensure important details of the glyph are always shown, even when the contrast ratio is low.
* The number 12 is largely arbitrary to ensure the pixels that escape the cell in the test case
* were covered (fg=#8ae234, bg=#c4a000).
*/
// Calculate a threshold that when below a color will be treated as transpart when the sum of
// channel value differs. This helps improve rendering when glyphs overlap with others. This
// threshold is calculated relative to the difference between the background and foreground to
// ensure important details of the glyph are always shown, even when the contrast ratio is low.
// The number 12 is largely arbitrary to ensure the pixels that escape the cell in the test case
// were covered (fg=#8ae234, bg=#c4a000).
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);
// Set alpha channel of relevent pixels to 0
+58 -106
View File
@@ -84,11 +84,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
) {
super();
/*
* IMPORTANT: Canvas initialization and fetching of the context must be first in order to
* prevent possible listeners leaking and continuing to operate after the WebglRenderer has been
* discarded.
*/
// IMPORTANT: Canvas initialization and fetching of the context must be first in order to
// prevent possible listeners leaking and continuing to operate after the WebglRenderer has been
// discarded.
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
const contextAttributes = {
antialias: false,
@@ -127,10 +125,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
console.log('webglcontextlost event received');
// Prevent the default behavior in order to enable WebGL context restoration.
e.preventDefault();
/*
* Wait a few seconds to see if the 'webglcontextrestored' event is fired.
* If not, dispatch the onContextLoss notification to observers.
*/
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
// If not, dispatch the onContextLoss notification to observers.
this._contextRestorationTimeout = setTimeout(() => {
this._contextRestorationTimeout = undefined;
console.warn('webgl context not restored; firing onContextLoss');
@@ -141,10 +137,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
console.warn('webglcontextrestored event received');
clearTimeout(this._contextRestorationTimeout);
this._contextRestorationTimeout = undefined;
/*
* The texture atlas and glyph renderer must be fully reinitialized
* because their contents have been lost.
*/
// The texture atlas and glyph renderer must be fully reinitialized
// because their contents have been lost.
removeTerminalFromCache(this._terminal);
this._initializeWebGLState();
this._requestRedrawViewport();
@@ -184,10 +178,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
public handleDevicePixelRatioChange(): void {
/*
* If the device pixel ratio changed, the char atlas needs to be regenerated
* and the terminal needs to refreshed
*/
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
this._devicePixelRatio = this._coreBrowserService.dpr;
this.handleResize(this._terminal.cols, this._terminal.rows);
@@ -223,10 +215,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._refreshCharAtlas();
/*
* Force a full refresh. Resizing `_glyphRenderer` should clear it already,
* so there is no need to clear it again here.
*/
// Force a full refresh. Resizing `_glyphRenderer` should clear it already,
// so there is no need to clear it again here.
this._clearModel(false);
// Render synchronously to avoid flicker when the canvas is cleared
@@ -378,11 +368,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
return;
}
/*
* Tell renderer the frame is beginning
* upon a model clear also refresh the full viewport model
* (also triggered by an atlas page merge, part of #4480)
*/
// Tell renderer the frame is beginning
// upon a model clear also refresh the full viewport model
// (also triggered by an atlas page merge, part of #4480)
if (this._glyphRenderer.value.beginFrame()) {
this._clearModel(true);
this._updateModel(0, this._terminal.rows - 1);
@@ -391,10 +379,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._updateModel(start, end);
}
/*
* A mid-update atlas page merge invalidates vertex data and may not bump the host
* page's version, so re-run the update and force a full texture rebind.
*/
// A mid-update atlas page merge invalidates vertex data and may not bump the host
// page's version, so re-run the update and force a full texture rebind.
let merged = false;
let mergeRetries = 0;
while (this._charAtlas && this._glyphRenderer.value.beginFrame() && mergeRetries++ < Constants.MERGE_RETRY_LIMIT) {
@@ -422,10 +408,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
} else {
this._cursorBlinkStateManager.clear();
}
/*
* Request a refresh from the terminal as management of rendering is being
* moved back to the terminal
*/
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._requestRedrawCursor();
}
@@ -486,26 +470,20 @@ 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.
*/
// 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.
*/
// 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] && isValidJoinRange) {
range = joinedRanges.shift()!;
/*
* 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.
*/
// 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));
@@ -517,10 +495,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
} 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.
*/
// 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]),
@@ -610,10 +586,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
j = ((y * terminal.cols) + x) * RenderModelConstants.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
*/
// Don't re-resolve the cell color since multi-colored ligature backgrounds are not
// supported
this._model.cells[j + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[j + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
@@ -657,64 +631,48 @@ export class WebglRenderer extends Disposable implements IRenderer {
return;
}
/*
* Calculate the device character width. Width is floored as it must be drawn to an integer grid
* in order for the char atlas glyphs to not be blurry.
*/
// Calculate the device character width. Width is floored as it must be drawn to an integer grid
// in order for the char atlas glyphs to not be blurry.
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * this._devicePixelRatio);
/*
* Calculate the device character height. Height is ceiled in case devicePixelRatio is a
* floating point number in order to ensure there is enough space to draw the character to the
* cell.
*/
// Calculate the device character height. Height is ceiled in case devicePixelRatio is a
// floating point number in order to ensure there is enough space to draw the character to the
// cell.
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
/*
* Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
* floored since lineHeight can never be lower then 1, this guarentees the device cell height
* will always be larger than device char height.
*/
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
// will always be larger than device char height.
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
/*
* Calculate the y offset within a cell that glyph should draw at in order for it to be centered
* correctly within the cell.
*/
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
// correctly within the cell.
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
// Calculate the device cell width, taking the letterSpacing into account.
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
/*
* Calculate the x offset with a cell that text should draw from in order for it to be centered
* correctly within the cell.
*/
// Calculate the x offset with a cell that text should draw from in order for it to be centered
// correctly within the cell.
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
/*
* Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in
* the canvas
*/
// Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in
// the canvas
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
/*
* The the size of the canvas on the page. It's important that this rounds to nearest integer
* and not ceils as browsers often have floating point precision issues where
* `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
* it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
* large for the canvas element size.
*/
// The the size of the canvas on the page. It's important that this rounds to nearest integer
// and not ceils as browsers often have floating point precision issues where
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
// large for the canvas element size.
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
/*
* Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
* device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
* page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
* size on the canvas can differ.
*/
// Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
// device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
// size on the canvas can differ.
this.dimensions.css.cell.height = this.dimensions.device.cell.height / this._devicePixelRatio;
this.dimensions.css.cell.width = this.dimensions.device.cell.width / this._devicePixelRatio;
}
@@ -723,10 +681,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
if (this._canvas.width === width && this._canvas.height === height) {
return;
}
/*
* While the actual canvas size has changed, keep device canvas dimensions as the value before
* the change as it's an exact multiple of the cell sizes.
*/
// While the actual canvas size has changed, keep device canvas dimensions as the value before
// the change as it's an exact multiple of the cell sizes.
this._canvas.width = width;
this._canvas.height = height;
// Render synchronously to avoid flicker when the canvas is cleared
@@ -746,10 +702,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
// TODO: Share impl with core
export class JoinedCellData extends AttributeData implements ICellData {
private _width: number;
/*
* .content carries no meaning for joined CellData, simply nullify it
* thus we have to overload all other .content accessors
*/
// .content carries no meaning for joined CellData, simply nullify it
// thus we have to overload all other .content accessors
public content: number = 0;
public fg: number;
public bg: number;
@@ -777,10 +731,8 @@ export class JoinedCellData extends AttributeData implements ICellData {
}
public getCode(): number {
/*
* code always gets the highest possible fake codepoint (read as -1)
* this is needed as code is used by caches as identifier
*/
// code always gets the highest possible fake codepoint (read as -1)
// this is needed as code is used by caches as identifier
return 0x1FFFFF;
}
@@ -274,17 +274,15 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// #region Powerline Symbols (E0A0-E0BF)
/*
* This contains the definitions of the primarily used box drawing characters as vector shapes.
* The reason these characters are defined specially is to avoid common problems if a user's font
* has not been patched with powerline characters and also to get pixel perfect rendering as
* rendering issues can occur around AA/SPAA.
*
* The line variants draw beyond the cell and get clipped to ensure the end of the line is not
* visible.
*
* Original symbols defined in https://github.com/powerline/fontpatcher
*/
// This contains the definitions of the primarily used box drawing characters as vector shapes.
// The reason these characters are defined specially is to avoid common problems if a user's font
// has not been patched with powerline characters and also to get pixel perfect rendering as
// rendering issues can occur around AA/SPAA.
//
// The line variants draw beyond the cell and get clipped to ensure the end of the line is not
// visible.
//
// Original symbols defined in https://github.com/powerline/fontpatcher
// Git branch
'\u{E0A0}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M.3,1 L.03,1 L.03,.88 C.03,.82,.06,.78,.11,.73 C.15,.7,.2,.68,.28,.65 L.43,.6 C.49,.58,.53,.56,.56,.53 C.59,.5,.6,.47,.6,.43 L.6,.27 L.4,.27 L.69,.1 L.98,.27 L.78,.27 L.78,.46 C.78,.52,.76,.56,.72,.61 C.68,.66,.63,.67,.56,.7 L.48,.72 C.42,.74,.38,.76,.35,.78 C.32,.8,.31,.84,.31,.88 L.31,1 M.3,.5 L.03,.59 L.03,.09 L.3,.09 L.3,.655', type: CustomGlyphVectorType.FILL } },
@@ -357,10 +355,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// #region Progress Indicators (EE00-EE0B)
/*
* initially added in Fira Code and later Nerd Fonts
* https://github.com/ryanoasis/nerd-fonts/pull/1733
*/
// initially added in Fira Code and later Nerd Fonts
// https://github.com/ryanoasis/nerd-fonts/pull/1733
// Progress bars (EE00-EE05)
'\u{EE00}': { type: CustomGlyphDefinitionType.PATH, data: 'M0,0 L1,0 L1,.05 L.1,.05 L.1,.95 L1,.95 L1,1 L0,1 Z' }, // PROGRESS BAR EMPTY START
@@ -382,10 +378,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// #region Git Branch Symbols (F5D0-F60D)
/*
* Initially added in Kitty (https://github.com/kovidgoyal/kitty/pull/7681)
* then in Wezterm (https://github.com/wezterm/wezterm/issues/6328).
*/
// Initially added in Kitty (https://github.com/kovidgoyal/kitty/pull/7681)
// then in Wezterm (https://github.com/wezterm/wezterm/issues/6328).
// Straight lines (F5D0-F5D9)
'\u{F5D0}': GitBranchSymbolsParts.LINE_H, // Same as 2500
@@ -439,10 +433,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
'\u{F5FA}': [GitBranchSymbolsParts.NODE_FILL, GitBranchSymbolsParts.NODE_STROKE, GitBranchSymbolsParts.NODE_LINE_DOWN, GitBranchSymbolsParts.NODE_LINE_UP],
'\u{F5FB}': [GitBranchSymbolsParts.NODE_STROKE, GitBranchSymbolsParts.NODE_LINE_DOWN, GitBranchSymbolsParts.NODE_LINE_UP],
/*
* Extended Nodes (F5FC-F60D)
* These were added a little later https://github.com/kovidgoyal/kitty/pull/7805
*/
// Extended Nodes (F5FC-F60D)
// These were added a little later https://github.com/kovidgoyal/kitty/pull/7805
'\u{F5FC}': [GitBranchSymbolsParts.NODE_FILL, GitBranchSymbolsParts.NODE_STROKE, GitBranchSymbolsParts.NODE_LINE_DOWN, GitBranchSymbolsParts.NODE_LINE_RIGHT],
'\u{F5FD}': [GitBranchSymbolsParts.NODE_STROKE, GitBranchSymbolsParts.NODE_LINE_DOWN, GitBranchSymbolsParts.NODE_LINE_RIGHT],
'\u{F5FE}': [GitBranchSymbolsParts.NODE_FILL, GitBranchSymbolsParts.NODE_STROKE, GitBranchSymbolsParts.NODE_LINE_DOWN, GitBranchSymbolsParts.NODE_LINE_LEFT],
@@ -468,10 +460,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
// https://www.unicode.org/charts/PDF/U1FB00.pdf
/*
* Block mosaic terminal graphic characters (1FB00-1FB3B)
* The term "sextant" refers to block mosaics divided into six parts.
*/
// Block mosaic terminal graphic characters (1FB00-1FB3B)
// The term "sextant" refers to block mosaics divided into six parts.
'\u{1FB00}': sextant(0b000001), // BLOCK SEXTANT-1
'\u{1FB01}': sextant(0b000010), // BLOCK SEXTANT-2
'\u{1FB02}': sextant(0b000011), // BLOCK SEXTANT-12 (upper one third block)
@@ -734,10 +724,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
'\u{1FBB2}': { type: CustomGlyphDefinitionType.PATH, data: 'M.29,.27 L0.13,.56 L.22,.59 L.35,0.35 L.67,.35 L.57,.57 L.71,.76 L.22,.76 L.42,1 L.53,.98 L.43,.86 L.9,.86 L.71,.6 L1,.6 L1,.52 L.83,.52 L.92,.36 L1,.36 L1,.27Z M.99,.13 A.12,.12,0,1,1,.75,.13 A.12,.12,0,1,1,.99,.13' }, // LEFT HALF RUNNING MAN
'\u{1FBB3}': { type: CustomGlyphDefinitionType.PATH, data: 'M0,.27 L.3,.27 L.55,.12 L.63,.18 L.33,.36 L0,.36 M0,.52 L.33,.52 L.59,.89 L.73,.89 L.73,.98 L.53,.98 L.28,.6 L0,.6' }, // RIGHT HALF RUNNING MAN
/*
* Arrows (1FBB4-1FBB8)
* TODO: Improve all arrows, use hybrid approach
*/
// Arrows (1FBB4-1FBB8)
// TODO: Improve all arrows, use hybrid approach
'\u{1FBB4}': { type: CustomGlyphDefinitionType.PATH_NEGATIVE, data: { d: 'M.15,.55 L.5,.4 L.5,.45 L.65,.45 L.65,.35 L.85,.35 L.85,.625 L.5,.625 L.5,.7 Z', type: CustomGlyphVectorType.FILL }, scaleType: CustomGlyphScaleType.CHAR }, // INVERSE DOWNWARDS ARROW WITH TIP LEFTWARDS
'\u{1FBB5}': [ // LEFTWARDS ARROW AND UPPER AND LOWER ONE EIGHTH BLOCK
{ type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M0,0 L1,0 L1,.0625 L0,.0625 Z M0,.9375 L1,.9375 L1,1 L0,1 Z', type: CustomGlyphVectorType.FILL } },
@@ -892,11 +880,9 @@ function sextant(pattern: number): { type: CustomGlyphDefinitionType.PATH_FUNCTI
return {
type: CustomGlyphDefinitionType.PATH_FUNCTION,
data: () => {
/*
* Sextant grid: 2 columns, 3 rows
* Row heights in 8ths: top=3, middle=2, bottom=3
* Column widths: left=4, right=4
*/
// Sextant grid: 2 columns, 3 rows
// Row heights in 8ths: top=3, middle=2, bottom=3
// Column widths: left=4, right=4
const rects: string[] = [];
const colW = 0.5; // Each column is half width
const rowH = [3 / 8, 2 / 8, 3 / 8]; // Row heights as fractions
@@ -946,12 +932,10 @@ function sextant(pattern: number): { type: CustomGlyphDefinitionType.PATH_FUNCTI
function segmentedDigit(pattern: number): string {
const paths: string[] = [];
/*
* Each segment should have approximately the same stroke width, this is somewhat difficult to be
* precise since coordinates are 0-1 of the whole cell (percentage-based). To handle this, the
* fact that terminal cells are typically sized at ~2:1 (height:width) is leveraged.
* for horizontal vs vertical to make segments appear the same thickness
*/
// Each segment should have approximately the same stroke width, this is somewhat difficult to be
// precise since coordinates are 0-1 of the whole cell (percentage-based). To handle this, the
// fact that terminal cells are typically sized at ~2:1 (height:width) is leveraged.
// for horizontal vs vertical to make segments appear the same thickness
const segW = 0.15; // Width of vertical segments (fraction of cell width)
const segH = 0.075; // Height of horizontal segments (fraction of cell height, ~half of segW for 2:1 cells)
const padX = 0.05; // Horizontal padding from edge
@@ -489,10 +489,8 @@ function drawPatternChar(
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
patternSet.set(fillStyle, pattern);
}
/*
* Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd.
* variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift.
*/
// Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd.
// variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift.
const dx = (variantOffset >> 1) & 1;
const dy = variantOffset & 1;
if (dx !== 0 || dy !== 0) {
@@ -715,10 +713,8 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let x = 0; x < result.length; x += 2) {
// Translate from 0-1 to 0-cellWidth
result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
/*
* Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
* clamp to the cell bounds.
*/
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[x] !== 0) {
const rounded = Math.round(result[x] + 0.5) - 0.5;
result[x] = clampToCell ? clamp(rounded, cellWidth, 0) : rounded;
@@ -730,10 +726,8 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let y = 1; y < result.length; y += 2) {
// Translate from 0-1 to 0-cellHeight
result[y] *= cellHeight;
/*
* Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
* clamp to the cell bounds.
*/
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
// clamp to the cell bounds.
if (doClamp && result[y] !== 0) {
const rounded = Math.round(result[y] + 0.5) - 0.5;
result[y] = clampToCell ? clamp(rounded, cellHeight, 0) : rounded;
@@ -24,11 +24,9 @@ test.beforeAll(async ({ browser }) => {
test.afterAll(async () => await ctx.page.close());
test.describe('WebGL Renderer Integration Tests', async () => {
/*
* HACK: webgl2 is often not supported in headless firefox on Linux
* https://github.com/microsoft/playwright/issues/11566
* also disable safari due to #5852
*/
// HACK: webgl2 is often not supported in headless firefox on Linux
// https://github.com/microsoft/playwright/issues/11566
// also disable safari due to #5852
if (platform() === 'linux') {
test.skip(({ browserName }) => browserName === 'firefox' || browserName === 'webkit');
}