mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge remote-tracking branch 'origin/master' into lint
This commit is contained in:
@@ -5,7 +5,6 @@ indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
|
||||
[*.{j,t}s]
|
||||
max_line_length = 100
|
||||
|
||||
@@ -32,9 +32,10 @@ export function acquireTextureAtlas(
|
||||
deviceCharWidth: number,
|
||||
deviceCharHeight: number,
|
||||
devicePixelRatio: number,
|
||||
deviceMaxTextureSize: number
|
||||
deviceMaxTextureSize: number,
|
||||
customGlyphs: boolean = true
|
||||
): ITextureAtlas {
|
||||
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize);
|
||||
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize, customGlyphs);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ITerminalOptions } from '@xterm/xterm';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { NULL_COLOR } from 'common/Color';
|
||||
|
||||
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig {
|
||||
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number, customGlyphs: boolean = true): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors: IColorSet = {
|
||||
foreground: colors.foreground,
|
||||
@@ -32,7 +32,7 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
|
||||
halfContrastCache: colors.halfContrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: options.customGlyphs,
|
||||
customGlyphs,
|
||||
devicePixelRatio,
|
||||
deviceMaxTextureSize,
|
||||
letterSpacing: options.letterSpacing,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||
import type { WebglAddon as IWebglApi } from '@xterm/addon-webgl';
|
||||
import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
@@ -28,9 +28,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
private readonly _onContextLoss = this._register(new Emitter<void>());
|
||||
public readonly onContextLoss = this._onContextLoss.event;
|
||||
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
private readonly _customGlyphs: boolean;
|
||||
private readonly _preserveDrawingBuffer?: boolean;
|
||||
|
||||
constructor(options?: IWebglAddonOptions) {
|
||||
if (isSafari && getSafariVersion() < 16) {
|
||||
// Perform an extra check to determine if Webgl2 is manually enabled in developer settings
|
||||
const contextAttributes = {
|
||||
@@ -44,6 +45,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
}
|
||||
}
|
||||
super();
|
||||
this._customGlyphs = options?.customGlyphs ?? true;
|
||||
this._preserveDrawingBuffer = options?.preserveDrawingBuffer;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -79,6 +82,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
decorationService,
|
||||
optionsService,
|
||||
themeService,
|
||||
this._customGlyphs,
|
||||
this._preserveDrawingBuffer
|
||||
));
|
||||
this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss));
|
||||
|
||||
@@ -72,6 +72,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private readonly _decorationService: IDecorationService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _themeService: IThemeService,
|
||||
private readonly _customGlyphs: boolean = true,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
@@ -278,7 +279,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.device.char.width,
|
||||
this.dimensions.device.char.height,
|
||||
this._coreBrowserService.dpr,
|
||||
this._deviceMaxTextureSize
|
||||
this._deviceMaxTextureSize,
|
||||
this._customGlyphs
|
||||
);
|
||||
if (this._charAtlas !== atlas) {
|
||||
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
||||
import { CustomGlyphDefinitionType, CustomGlyphVectorType, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type CustomGlyphRegionDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||
import { CustomGlyphDefinitionType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||
|
||||
/**
|
||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||
@@ -24,42 +24,63 @@ export function tryDrawCustomGlyph(
|
||||
): boolean {
|
||||
const unifiedCharDefinition = customGlyphDefinitions[c];
|
||||
if (unifiedCharDefinition) {
|
||||
switch (unifiedCharDefinition.type) {
|
||||
case CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR:
|
||||
drawBlockVectorChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN:
|
||||
drawPatternChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION:
|
||||
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR:
|
||||
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data.pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
drawBlockVectorChar(ctx, unifiedCharDefinition.data.vectors, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_CLIP_PATH:
|
||||
drawBlockPatternWithClipPath(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
||||
case CustomGlyphDefinitionType.PATH:
|
||||
drawPathDefinitionCharacter(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.PATH_NEGATIVE:
|
||||
drawPathNegativeDefinitionCharacter(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio, backgroundColor);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.VECTOR_SHAPE:
|
||||
drawVectorShape(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.PATH_FUNCTION_WITH_WEIGHT:
|
||||
drawPathDefinitionCharacterWithWeight(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio);
|
||||
return true;
|
||||
// Normalize to array for uniform handling
|
||||
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
|
||||
for (const part of parts) {
|
||||
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio, backgroundColor);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawDefinitionPart(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
part: CustomGlyphDefinitionPart,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
fontSize: number,
|
||||
devicePixelRatio: number,
|
||||
backgroundColor?: string
|
||||
): void {
|
||||
// Handle clipPath generically for any definition type
|
||||
if (part.clipPath) {
|
||||
ctx.save();
|
||||
applyClipPath(ctx, part.clipPath, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
}
|
||||
|
||||
switch (part.type) {
|
||||
case CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR:
|
||||
drawBlockVectorChar(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN:
|
||||
drawPatternChar(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
||||
drawPathFunctionCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio, part.strokeWidth);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH:
|
||||
drawPathDefinitionCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH_NEGATIVE:
|
||||
drawPathNegativeDefinitionCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio, backgroundColor);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.VECTOR_SHAPE:
|
||||
drawVectorShape(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.BRAILLE:
|
||||
drawBrailleCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
}
|
||||
|
||||
if (part.clipPath) {
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawBlockVectorChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: ICustomGlyphSolidOctantBlockVector[],
|
||||
@@ -81,6 +102,52 @@ function drawBlockVectorChar(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Braille dot positions in octant coordinates (x, y for center of each dot area)
|
||||
* Columns: left=1-2, right=5-6 (leaving 0 and 7 as margins, 3-4 as gap)
|
||||
* Rows: 0-1, 2-3, 4-5, 6-7 for the 4 rows
|
||||
*/
|
||||
const brailleDotPositions = new Uint8Array([
|
||||
1, 0, // dot 1 - bit 0
|
||||
1, 2, // dot 2 - bit 1
|
||||
1, 4, // dot 3 - bit 2
|
||||
5, 0, // dot 4 - bit 3
|
||||
5, 2, // dot 5 - bit 4
|
||||
5, 4, // dot 6 - bit 5
|
||||
1, 6, // dot 7 - bit 6
|
||||
5, 6, // dot 8 - bit 7
|
||||
]);
|
||||
|
||||
/**
|
||||
* Draws a braille pattern
|
||||
*/
|
||||
function drawBrailleCharacter(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
pattern: number,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
const xEighth = deviceCellWidth / 8;
|
||||
const paddingY = deviceCellHeight * 0.1;
|
||||
const usableHeight = deviceCellHeight * 0.8;
|
||||
const yEighth = usableHeight / 8;
|
||||
const radius = Math.min(xEighth, yEighth);
|
||||
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
if (pattern & (1 << bit)) {
|
||||
const x = brailleDotPositions[bit * 2];
|
||||
const y = brailleDotPositions[bit * 2 + 1];
|
||||
const cx = xOffset + (x + 1) * xEighth;
|
||||
const cy = yOffset + paddingY + (y + 1) * yEighth;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawPathDefinitionCharacter(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: CustomGlyphPathDrawFunctionDefinition | string,
|
||||
@@ -398,139 +465,66 @@ function drawPatternChar(
|
||||
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws rectangular shade characters - medium shade pattern clipped to a region.
|
||||
* Uses a checkerboard pattern that shifts 1px each row (same as medium shade U+2592).
|
||||
*/
|
||||
function drawBlockPatternWithRegion(
|
||||
function drawPathFunctionCharacter(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
definition: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition],
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
const [pattern, region] = definition;
|
||||
const [rx, ry, rw, rh] = region;
|
||||
const regionX = Math.round(xOffset + rx * deviceCellWidth);
|
||||
const regionY = Math.round(yOffset + ry * deviceCellHeight);
|
||||
const regionW = Math.round(rw * deviceCellWidth);
|
||||
const regionH = Math.round(rh * deviceCellHeight);
|
||||
|
||||
// Save context state
|
||||
ctx.save();
|
||||
|
||||
// Clip to the region
|
||||
ctx.beginPath();
|
||||
ctx.rect(regionX, regionY, regionW, regionH);
|
||||
ctx.clip();
|
||||
|
||||
// Draw the pattern
|
||||
drawPatternChar(ctx, pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
|
||||
// Restore context state
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to
|
||||
* canvas draw calls.
|
||||
*
|
||||
* Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐
|
||||
* ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤
|
||||
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘
|
||||
* ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐
|
||||
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤
|
||||
* └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘
|
||||
*
|
||||
* Other:
|
||||
* ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈
|
||||
* │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉
|
||||
* ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋
|
||||
*
|
||||
* All box drawing characters:
|
||||
* ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
|
||||
* ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
|
||||
* ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
|
||||
* ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
|
||||
* ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
|
||||
* ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
|
||||
* ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
|
||||
* ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Box drawing alignment tests: █
|
||||
* ▉
|
||||
* ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳
|
||||
* ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳
|
||||
* ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳
|
||||
* ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
|
||||
* ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎
|
||||
* ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏
|
||||
* ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█
|
||||
*
|
||||
* Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
|
||||
*/
|
||||
function drawPathDefinitionCharacterWithWeight(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
|
||||
charDefinition: string | ((xp: number, yp: number) => string),
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
devicePixelRatio: number
|
||||
devicePixelRatio: number,
|
||||
strokeWidth?: number
|
||||
): void {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
for (const [fontWeight, instructions] of Object.entries(charDefinition)) {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = devicePixelRatio * Number.parseInt(fontWeight);
|
||||
let actualInstructions: string;
|
||||
if (typeof instructions === 'function') {
|
||||
const xp = .15;
|
||||
const yp = .15 / deviceCellHeight * deviceCellWidth;
|
||||
actualInstructions = instructions(xp, yp);
|
||||
} else {
|
||||
actualInstructions = instructions;
|
||||
}
|
||||
for (const instruction of actualInstructions.split(' ')) {
|
||||
const type = instruction[0];
|
||||
if (type === 'Z') {
|
||||
ctx.closePath();
|
||||
continue;
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio));
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
ctx.beginPath();
|
||||
let actualInstructions: string;
|
||||
if (typeof charDefinition === 'function') {
|
||||
const xp = .15;
|
||||
const yp = .15 / deviceCellHeight * deviceCellWidth;
|
||||
actualInstructions = charDefinition(xp, yp);
|
||||
} else {
|
||||
actualInstructions = charDefinition;
|
||||
}
|
||||
const state: ISvgPathState = { currentX: 0, currentY: 0, lastControlX: 0, lastControlY: 0, lastCommand: '' };
|
||||
for (const instruction of actualInstructions.split(' ')) {
|
||||
const type = instruction[0];
|
||||
if (type === 'Z') {
|
||||
ctx.closePath();
|
||||
state.lastCommand = type;
|
||||
continue;
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio), state);
|
||||
state.lastCommand = type;
|
||||
}
|
||||
if (strokeWidth !== undefined) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
ctx.lineWidth = devicePixelRatio * strokeWidth;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a pattern clipped to an arbitrary path (for triangular shades, etc.)
|
||||
* Applies a clip path to the canvas context from SVG-like path instructions.
|
||||
*/
|
||||
function drawBlockPatternWithClipPath(
|
||||
function applyClipPath(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
definition: [pattern: CustomGlyphPatternDefinition, clipPath: string],
|
||||
clipPath: string,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
const [pattern, clipPath] = definition;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Build clip path from SVG-like instructions
|
||||
ctx.beginPath();
|
||||
for (const instruction of clipPath.split(' ')) {
|
||||
const type = instruction[0];
|
||||
@@ -551,11 +545,6 @@ function drawBlockPatternWithClipPath(
|
||||
}
|
||||
}
|
||||
ctx.clip();
|
||||
|
||||
// Draw the pattern
|
||||
drawPatternChar(ctx, pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawVectorShape(
|
||||
@@ -577,10 +566,12 @@ function drawVectorShape(
|
||||
// Scale the stroke with DPR and font size
|
||||
const cssLineWidth = fontSize / 12;
|
||||
ctx.lineWidth = devicePixelRatio * cssLineWidth;
|
||||
const state: ISvgPathState = { currentX: 0, currentY: 0, lastControlX: 0, lastControlY: 0, lastCommand: '' };
|
||||
for (const instruction of charDefinition.d.split(' ')) {
|
||||
const type = instruction[0];
|
||||
if (type === 'Z') {
|
||||
ctx.closePath();
|
||||
state.lastCommand = type;
|
||||
continue;
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
@@ -602,7 +593,8 @@ function drawVectorShape(
|
||||
devicePixelRatio,
|
||||
(charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
|
||||
(charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
|
||||
));
|
||||
), state);
|
||||
state.lastCommand = type;
|
||||
}
|
||||
if (charDefinition.type === CustomGlyphVectorType.STROKE) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
@@ -617,11 +609,55 @@ function clamp(value: number, max: number, min: number = 0): number {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
|
||||
const svgToCanvasInstructionMap: { [index: string]: any } = {
|
||||
'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]),
|
||||
'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]),
|
||||
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]),
|
||||
'Q': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.quadraticCurveTo(args[0], args[1], args[2], args[3])
|
||||
interface ISvgPathState {
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
lastControlX: number;
|
||||
lastControlY: number;
|
||||
lastCommand: string;
|
||||
}
|
||||
|
||||
const svgToCanvasInstructionMap: { [index: string]: (ctx: CanvasRenderingContext2D, args: number[], state: ISvgPathState) => void } = {
|
||||
'C': (ctx, args, state) => {
|
||||
ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]);
|
||||
state.lastControlX = args[2];
|
||||
state.lastControlY = args[3];
|
||||
state.currentX = args[4];
|
||||
state.currentY = args[5];
|
||||
},
|
||||
'L': (ctx, args, state) => {
|
||||
ctx.lineTo(args[0], args[1]);
|
||||
state.lastControlX = state.currentX = args[0];
|
||||
state.lastControlY = state.currentY = args[1];
|
||||
},
|
||||
'M': (ctx, args, state) => {
|
||||
ctx.moveTo(args[0], args[1]);
|
||||
state.lastControlX = state.currentX = args[0];
|
||||
state.lastControlY = state.currentY = args[1];
|
||||
},
|
||||
'Q': (ctx, args, state) => {
|
||||
ctx.quadraticCurveTo(args[0], args[1], args[2], args[3]);
|
||||
state.lastControlX = args[0];
|
||||
state.lastControlY = args[1];
|
||||
state.currentX = args[2];
|
||||
state.currentY = args[3];
|
||||
},
|
||||
'T': (ctx, args, state) => {
|
||||
let cpX: number;
|
||||
let cpY: number;
|
||||
if (state.lastCommand === 'Q' || state.lastCommand === 'T') {
|
||||
cpX = 2 * state.currentX - state.lastControlX;
|
||||
cpY = 2 * state.currentY - state.lastControlY;
|
||||
} else {
|
||||
cpX = state.currentX;
|
||||
cpY = state.currentY;
|
||||
}
|
||||
ctx.quadraticCurveTo(cpX, cpY, args[0], args[1]);
|
||||
state.lastControlX = cpX;
|
||||
state.lastControlY = cpY;
|
||||
state.currentX = args[0];
|
||||
state.currentY = args[1];
|
||||
}
|
||||
};
|
||||
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
||||
|
||||
@@ -33,29 +33,37 @@ export type CustomGlyphPatternDefinition = number[][];
|
||||
export const enum CustomGlyphDefinitionType {
|
||||
SOLID_OCTANT_BLOCK_VECTOR,
|
||||
BLOCK_PATTERN,
|
||||
BLOCK_PATTERN_WITH_REGION,
|
||||
BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR,
|
||||
BLOCK_PATTERN_WITH_CLIP_PATH,
|
||||
PATH_FUNCTION,
|
||||
PATH_FUNCTION_WITH_WEIGHT,
|
||||
PATH,
|
||||
PATH_NEGATIVE,
|
||||
VECTOR_SHAPE,
|
||||
BRAILLE,
|
||||
}
|
||||
|
||||
export type CustomGlyphRegionDefinition = [x: number, y: number, w: number, h: number];
|
||||
|
||||
export type CustomGlyphCharacterDefinition = (
|
||||
export type CustomGlyphDefinitionPartRaw = (
|
||||
{ type: CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR, data: ICustomGlyphSolidOctantBlockVector[] } |
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN, data: CustomGlyphPatternDefinition } |
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION, data: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition] } |
|
||||
// TODO: Consolidate these, draws should be possible via regions/clipping instead of special
|
||||
// casing
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR, data: { pattern: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition], vectors: ICustomGlyphSolidOctantBlockVector[] } } |
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_CLIP_PATH, data: [pattern: CustomGlyphPatternDefinition, clipPath: string] } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_FUNCTION, data: CustomGlyphPathDrawFunctionDefinition } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_FUNCTION_WITH_WEIGHT, data: { [fontWeight: number]: string | CustomGlyphPathDrawFunctionDefinition } } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_FUNCTION, data: CustomGlyphPathDrawFunctionDefinition | string } |
|
||||
{ type: CustomGlyphDefinitionType.PATH, data: string } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_NEGATIVE, data: ICustomGlyphVectorShape } |
|
||||
{ type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: ICustomGlyphVectorShape }
|
||||
{ type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: ICustomGlyphVectorShape} |
|
||||
{ type: CustomGlyphDefinitionType.BRAILLE, data: number }
|
||||
);
|
||||
|
||||
export interface ICustomGlyphDefinitionCommon {
|
||||
/**
|
||||
* A custom clip path for the draw definition, restricting the area it can draw to.
|
||||
*/
|
||||
clipPath?: string;
|
||||
/**
|
||||
* The stroke width to use when drawing the path. Defaults to 1.
|
||||
*/
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
export type CustomGlyphDefinitionPart = CustomGlyphDefinitionPartRaw & ICustomGlyphDefinitionCommon;
|
||||
|
||||
/**
|
||||
* A character definition that can be a single part or an array of parts drawn in sequence.
|
||||
*/
|
||||
export type CustomGlyphCharacterDefinition = CustomGlyphDefinitionPart | CustomGlyphDefinitionPart[];
|
||||
|
||||
+26
-1
@@ -32,7 +32,7 @@ declare module '@xterm/addon-webgl' {
|
||||
*/
|
||||
public readonly onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
constructor(preserveDrawingBuffer?: boolean);
|
||||
constructor(options?: IWebglAddonOptions);
|
||||
|
||||
/**
|
||||
* Activates the addon.
|
||||
@@ -50,4 +50,29 @@ declare module '@xterm/addon-webgl' {
|
||||
*/
|
||||
public clearTextureAtlas(): void;
|
||||
}
|
||||
|
||||
export interface IWebglAddonOptions {
|
||||
/**
|
||||
* Whether to draw custom glyphs instead of using the font for the following
|
||||
* unicode ranges:
|
||||
*
|
||||
* - Box Drawing (U+2500-U+257F)
|
||||
* - Box Elements (U+2580-U+259F)
|
||||
* - Braille Patterns (U+2800-U+28FF)
|
||||
* - Powerline Symbols (U+E0A0–U+E0D4)
|
||||
* - Symbols for Legacy Computing (U+1FB00–U+1FBFF)
|
||||
*
|
||||
* This will typically result in better rendering with continuous lines,
|
||||
* even when line height and letter spacing is used. Note that this doesn't
|
||||
* work with the DOM renderer which renders all characters using the font.
|
||||
* The default is true.
|
||||
*/
|
||||
customGlyphs?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to enable the preserveDrawingBuffer flag when creating the WebGL
|
||||
* context. This may be useful in tests. This defaults to false.
|
||||
*/
|
||||
preserveDrawingBuffer?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* Converts an SVG file as exported by fontforge into the SVG-like format as expected by the custom
|
||||
* glyph rasterizer.
|
||||
*
|
||||
* Usage: node convert_svg_to_custom_glyph.js <svg-file-or-folder>
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const input = process.argv[2];
|
||||
if (!input) {
|
||||
console.error('Usage: node convert_svg_to_custom_glyph.js <svg-file-or-folder>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const inputPath = path.resolve(process.cwd(), input);
|
||||
const stat = fs.statSync(inputPath);
|
||||
const files = stat.isDirectory()
|
||||
? fs.readdirSync(inputPath).filter(f => f.endsWith('.svg')).map(f => path.join(inputPath, f))
|
||||
: [inputPath];
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error('No SVG files found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
console.log(`\n${'='.repeat(60)}\nProcessing: ${path.basename(file)}\n${'='.repeat(60)}`);
|
||||
processFile(file);
|
||||
}
|
||||
|
||||
function processFile(filePath) {
|
||||
// Get file content
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Get viewBox
|
||||
const viewBoxMatch = content.match(/viewBox="([^"]+)"/);
|
||||
if (!viewBoxMatch) {
|
||||
console.error('No viewBox found in SVG');
|
||||
return;
|
||||
}
|
||||
const [minX, minY, width, height] = viewBoxMatch[1].split(/\s+/).map(Number);
|
||||
console.log(`ViewBox: ${minX} ${minY} ${width} ${height}`);
|
||||
|
||||
// Get path `d` property
|
||||
const pathMatch = content.match(/<path[^>]*\sd="([^"]+)"/);
|
||||
if (!pathMatch) {
|
||||
console.error('No path d attribute found in SVG');
|
||||
return;
|
||||
}
|
||||
const originalPath = pathMatch[1].replace(/\s+/g, ' ').trim();
|
||||
console.log(`\nOriginal path length: ${originalPath.length} chars`);
|
||||
|
||||
// Parse path into commands
|
||||
function parsePath(d) {
|
||||
const commands = [];
|
||||
const regex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
||||
let match;
|
||||
while ((match = regex.exec(d)) !== null) {
|
||||
const cmd = match[1];
|
||||
const argsStr = match[2].trim();
|
||||
const args = argsStr ? argsStr.split(/[\s,]+/).map(Number) : [];
|
||||
commands.push({ cmd, args });
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
// Convert relative commands to absolute and expand T/S to Q/C
|
||||
function toAbsolute(commands) {
|
||||
const result = [];
|
||||
let x = 0, y = 0; // Current position
|
||||
let startX = 0, startY = 0; // Start of current subpath
|
||||
let lastControlX = 0, lastControlY = 0; // Last control point for T/S
|
||||
let lastCmd = '';
|
||||
|
||||
for (const { cmd, args } of commands) {
|
||||
const isRelative = cmd === cmd.toLowerCase();
|
||||
const absCmd = cmd.toUpperCase();
|
||||
|
||||
switch (absCmd) {
|
||||
case 'M': {
|
||||
// MoveTo: M x y (or m dx dy)
|
||||
const absArgs = [];
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
const newX = isRelative ? x + args[i] : args[i];
|
||||
const newY = isRelative ? y + args[i + 1] : args[i + 1];
|
||||
absArgs.push(newX, newY);
|
||||
x = newX;
|
||||
y = newY;
|
||||
if (i === 0) {
|
||||
startX = x;
|
||||
startY = y;
|
||||
}
|
||||
}
|
||||
lastControlX = x;
|
||||
lastControlY = y;
|
||||
result.push({ cmd: 'M', args: absArgs });
|
||||
break;
|
||||
}
|
||||
case 'L': {
|
||||
// LineTo: L x y (or l dx dy)
|
||||
const absArgs = [];
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
const newX = isRelative ? x + args[i] : args[i];
|
||||
const newY = isRelative ? y + args[i + 1] : args[i + 1];
|
||||
absArgs.push(newX, newY);
|
||||
x = newX;
|
||||
y = newY;
|
||||
}
|
||||
lastControlX = x;
|
||||
lastControlY = y;
|
||||
result.push({ cmd: 'L', args: absArgs });
|
||||
break;
|
||||
}
|
||||
case 'H': {
|
||||
// Horizontal LineTo - convert to L
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const newX = isRelative ? x + args[i] : args[i];
|
||||
result.push({ cmd: 'L', args: [newX, y] });
|
||||
x = newX;
|
||||
}
|
||||
lastControlX = x;
|
||||
lastControlY = y;
|
||||
break;
|
||||
}
|
||||
case 'V': {
|
||||
// Vertical LineTo - convert to L
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const newY = isRelative ? y + args[i] : args[i];
|
||||
result.push({ cmd: 'L', args: [x, newY] });
|
||||
y = newY;
|
||||
}
|
||||
lastControlX = x;
|
||||
lastControlY = y;
|
||||
break;
|
||||
}
|
||||
case 'C': {
|
||||
// CurveTo: C x1 y1 x2 y2 x y (or c dx1 dy1 dx2 dy2 dx dy)
|
||||
const absArgs = [];
|
||||
for (let i = 0; i < args.length; i += 6) {
|
||||
const x1 = isRelative ? x + args[i] : args[i];
|
||||
const y1 = isRelative ? y + args[i + 1] : args[i + 1];
|
||||
const x2 = isRelative ? x + args[i + 2] : args[i + 2];
|
||||
const y2 = isRelative ? y + args[i + 3] : args[i + 3];
|
||||
const newX = isRelative ? x + args[i + 4] : args[i + 4];
|
||||
const newY = isRelative ? y + args[i + 5] : args[i + 5];
|
||||
absArgs.push(x1, y1, x2, y2, newX, newY);
|
||||
lastControlX = x2;
|
||||
lastControlY = y2;
|
||||
x = newX;
|
||||
y = newY;
|
||||
}
|
||||
result.push({ cmd: 'C', args: absArgs });
|
||||
break;
|
||||
}
|
||||
case 'S': {
|
||||
// Smooth CurveTo - expand to C
|
||||
for (let i = 0; i < args.length; i += 4) {
|
||||
// Reflect last control point
|
||||
let x1, y1;
|
||||
if (lastCmd === 'C' || lastCmd === 'S') {
|
||||
x1 = 2 * x - lastControlX;
|
||||
y1 = 2 * y - lastControlY;
|
||||
} else {
|
||||
x1 = x;
|
||||
y1 = y;
|
||||
}
|
||||
const x2 = isRelative ? x + args[i] : args[i];
|
||||
const y2 = isRelative ? y + args[i + 1] : args[i + 1];
|
||||
const newX = isRelative ? x + args[i + 2] : args[i + 2];
|
||||
const newY = isRelative ? y + args[i + 3] : args[i + 3];
|
||||
result.push({ cmd: 'C', args: [x1, y1, x2, y2, newX, newY] });
|
||||
lastControlX = x2;
|
||||
lastControlY = y2;
|
||||
x = newX;
|
||||
y = newY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Q': {
|
||||
// Quadratic CurveTo: Q x1 y1 x y (or q dx1 dy1 dx dy)
|
||||
const absArgs = [];
|
||||
for (let i = 0; i < args.length; i += 4) {
|
||||
const x1 = isRelative ? x + args[i] : args[i];
|
||||
const y1 = isRelative ? y + args[i + 1] : args[i + 1];
|
||||
const newX = isRelative ? x + args[i + 2] : args[i + 2];
|
||||
const newY = isRelative ? y + args[i + 3] : args[i + 3];
|
||||
absArgs.push(x1, y1, newX, newY);
|
||||
lastControlX = x1;
|
||||
lastControlY = y1;
|
||||
x = newX;
|
||||
y = newY;
|
||||
}
|
||||
result.push({ cmd: 'Q', args: absArgs });
|
||||
break;
|
||||
}
|
||||
case 'T': {
|
||||
// Smooth Quadratic CurveTo - keep as T
|
||||
const absArgs = [];
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
// Reflect last control point for tracking
|
||||
let cpX, cpY;
|
||||
if (lastCmd === 'Q' || lastCmd === 'T') {
|
||||
cpX = 2 * x - lastControlX;
|
||||
cpY = 2 * y - lastControlY;
|
||||
} else {
|
||||
cpX = x;
|
||||
cpY = y;
|
||||
}
|
||||
const newX = isRelative ? x + args[i] : args[i];
|
||||
const newY = isRelative ? y + args[i + 1] : args[i + 1];
|
||||
absArgs.push(newX, newY);
|
||||
lastControlX = cpX;
|
||||
lastControlY = cpY;
|
||||
x = newX;
|
||||
y = newY;
|
||||
lastCmd = 'T'; // For chained T commands
|
||||
}
|
||||
result.push({ cmd: 'T', args: absArgs });
|
||||
break;
|
||||
}
|
||||
case 'A': {
|
||||
// Arc: A rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
||||
const absArgs = [];
|
||||
for (let i = 0; i < args.length; i += 7) {
|
||||
const rx = args[i];
|
||||
const ry = args[i + 1];
|
||||
const rotation = args[i + 2];
|
||||
const largeArc = args[i + 3];
|
||||
const sweep = args[i + 4];
|
||||
const newX = isRelative ? x + args[i + 5] : args[i + 5];
|
||||
const newY = isRelative ? y + args[i + 6] : args[i + 6];
|
||||
absArgs.push(rx, ry, rotation, largeArc, sweep, newX, newY);
|
||||
x = newX;
|
||||
y = newY;
|
||||
}
|
||||
lastControlX = x;
|
||||
lastControlY = y;
|
||||
result.push({ cmd: 'A', args: absArgs });
|
||||
break;
|
||||
}
|
||||
case 'Z': {
|
||||
// ClosePath
|
||||
x = startX;
|
||||
y = startY;
|
||||
lastControlX = x;
|
||||
lastControlY = y;
|
||||
result.push({ cmd: 'Z', args: [] });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (absCmd !== 'T') {
|
||||
lastCmd = absCmd;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scale coordinates to 0-1 range
|
||||
function scaleToNormalized(commands, minX, minY, width, height) {
|
||||
function scaleX(val) {
|
||||
return (val - minX) / width;
|
||||
}
|
||||
function scaleY(val) {
|
||||
return (val - minY) / height;
|
||||
}
|
||||
function scaleRx(val) {
|
||||
return val / width;
|
||||
}
|
||||
function scaleRy(val) {
|
||||
return val / height;
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (const { cmd, args } of commands) {
|
||||
const scaledArgs = [];
|
||||
|
||||
switch (cmd) {
|
||||
case 'M':
|
||||
case 'L':
|
||||
case 'T': {
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
scaledArgs.push(scaleX(args[i]), scaleY(args[i + 1]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'H': {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
scaledArgs.push(scaleX(args[i]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'V': {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
scaledArgs.push(scaleY(args[i]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'C': {
|
||||
for (let i = 0; i < args.length; i += 6) {
|
||||
scaledArgs.push(
|
||||
scaleX(args[i]), scaleY(args[i + 1]),
|
||||
scaleX(args[i + 2]), scaleY(args[i + 3]),
|
||||
scaleX(args[i + 4]), scaleY(args[i + 5])
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'S':
|
||||
case 'Q': {
|
||||
for (let i = 0; i < args.length; i += 4) {
|
||||
scaledArgs.push(
|
||||
scaleX(args[i]), scaleY(args[i + 1]),
|
||||
scaleX(args[i + 2]), scaleY(args[i + 3])
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'A': {
|
||||
for (let i = 0; i < args.length; i += 7) {
|
||||
// rx, ry need to be scaled; rotation and flags stay the same
|
||||
scaledArgs.push(
|
||||
scaleRx(args[i]), // rx
|
||||
scaleRy(args[i + 1]), // ry
|
||||
args[i + 2], // rotation
|
||||
args[i + 3], // large-arc
|
||||
args[i + 4], // sweep
|
||||
scaleX(args[i + 5]), // x
|
||||
scaleY(args[i + 6]) // y
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Z': {
|
||||
// No args
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.push({ cmd, args: scaledArgs });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Format number to reasonable precision
|
||||
function formatNum(n, precision = 4) {
|
||||
const rounded = Number(n.toFixed(precision));
|
||||
return String(rounded);
|
||||
}
|
||||
|
||||
// Convert commands back to path string
|
||||
function commandsToPath(commands) {
|
||||
return commands.map(({ cmd, args }, i) => {
|
||||
const prefix = i === 0 ? '' : ' ';
|
||||
if (args.length === 0) return prefix + cmd;
|
||||
return prefix + cmd + args.map(a => formatNum(a)).join(',');
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Main conversion
|
||||
const parsed = parsePath(originalPath);
|
||||
const absolute = toAbsolute(parsed);
|
||||
|
||||
// Calculate actual bounding box from path data
|
||||
function getBoundingBox(commands) {
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
for (const { cmd, args } of commands) {
|
||||
switch (cmd) {
|
||||
case 'M':
|
||||
case 'L':
|
||||
case 'T': {
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
minX = Math.min(minX, args[i]);
|
||||
maxX = Math.max(maxX, args[i]);
|
||||
minY = Math.min(minY, args[i + 1]);
|
||||
maxY = Math.max(maxY, args[i + 1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'H': {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
minX = Math.min(minX, args[i]);
|
||||
maxX = Math.max(maxX, args[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'V': {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
minY = Math.min(minY, args[i]);
|
||||
maxY = Math.max(maxY, args[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'C': {
|
||||
for (let i = 0; i < args.length; i += 6) {
|
||||
// Include control points and endpoint
|
||||
minX = Math.min(minX, args[i], args[i + 2], args[i + 4]);
|
||||
maxX = Math.max(maxX, args[i], args[i + 2], args[i + 4]);
|
||||
minY = Math.min(minY, args[i + 1], args[i + 3], args[i + 5]);
|
||||
maxY = Math.max(maxY, args[i + 1], args[i + 3], args[i + 5]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'S':
|
||||
case 'Q': {
|
||||
for (let i = 0; i < args.length; i += 4) {
|
||||
minX = Math.min(minX, args[i], args[i + 2]);
|
||||
maxX = Math.max(maxX, args[i], args[i + 2]);
|
||||
minY = Math.min(minY, args[i + 1], args[i + 3]);
|
||||
maxY = Math.max(maxY, args[i + 1], args[i + 3]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'A': {
|
||||
for (let i = 0; i < args.length; i += 7) {
|
||||
minX = Math.min(minX, args[i + 5]);
|
||||
maxX = Math.max(maxX, args[i + 5]);
|
||||
minY = Math.min(minY, args[i + 6]);
|
||||
maxY = Math.max(maxY, args[i + 6]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { minX, minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
|
||||
const bbox = getBoundingBox(absolute);
|
||||
console.log(`Path bounding box: x=${bbox.minX}, y=${bbox.minY}, w=${bbox.width}, h=${bbox.height}`);
|
||||
|
||||
// Use path bounding box for normalization
|
||||
const normalized = scaleToNormalized(absolute, bbox.minX, bbox.minY, bbox.width, bbox.height);
|
||||
const result = commandsToPath(normalized);
|
||||
|
||||
console.log(`\nConverted path (${result.length} chars):\n`);
|
||||
console.log(result);
|
||||
|
||||
console.log(`\n\nFor CustomGlyphDefinitions.ts:\n`);
|
||||
console.log(`'\\u{E0C0}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: '${result}', type: CustomGlyphVectorType.FILL } },`);
|
||||
|
||||
// Write output file
|
||||
const ext = path.extname(filePath);
|
||||
const outputPath = filePath.replace(ext, `_output${ext}`);
|
||||
const svgOutput = `<?xml version="1.0" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1 1">
|
||||
<path fill="currentColor" d="${result}" />
|
||||
</svg>
|
||||
`;
|
||||
fs.writeFileSync(outputPath, svgOutput, 'utf8');
|
||||
console.log(`\nOutput written to: ${outputPath}`);
|
||||
}
|
||||
+70
-18
@@ -453,8 +453,6 @@ function initOptions(term: Terminal): void {
|
||||
'theme',
|
||||
'windowOptions',
|
||||
'windowsPty',
|
||||
// Deprecated
|
||||
'fastScrollModifier'
|
||||
];
|
||||
const stringOptions = {
|
||||
cursorStyle: ['block', 'underline', 'bar'],
|
||||
@@ -619,6 +617,20 @@ function initOptions(term: Terminal): void {
|
||||
|
||||
function initAddons(term: Terminal): void {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
function postInitWebgl(): void {
|
||||
setTimeout(() => {
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 500);
|
||||
}
|
||||
function preDisposeWebgl(): void {
|
||||
if (addons.webgl.instance.textureAtlas) {
|
||||
addons.webgl.instance.textureAtlas.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(addons).forEach((name: AddonType) => {
|
||||
const addon = addons[name];
|
||||
const checkbox = document.createElement('input') as HTMLInputElement;
|
||||
@@ -650,18 +662,6 @@ function initAddons(term: Terminal): void {
|
||||
}
|
||||
return;
|
||||
}
|
||||
function postInitWebgl(): void {
|
||||
setTimeout(() => {
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 500);
|
||||
}
|
||||
function preDisposeWebgl(): void {
|
||||
if (addons.webgl.instance.textureAtlas) {
|
||||
addons.webgl.instance.textureAtlas.remove();
|
||||
}
|
||||
}
|
||||
if (checkbox.checked) {
|
||||
// HACK: Manually remove addons that cannot be changes
|
||||
addon.instance = new (addon as IDemoAddon<Exclude<AddonType, 'attach'>>).ctor();
|
||||
@@ -697,7 +697,8 @@ function initAddons(term: Terminal): void {
|
||||
if (addons.webgl.instance) {
|
||||
preDisposeWebgl();
|
||||
addons.webgl.instance.dispose();
|
||||
addons.webgl.instance = new addons.webgl.ctor();
|
||||
const customGlyphsCheckbox = document.getElementById('webgl-custom-glyphs') as HTMLInputElement;
|
||||
addons.webgl.instance = new addons.webgl.ctor({ customGlyphs: customGlyphsCheckbox?.checked ?? true });
|
||||
term.loadAddon(addons.webgl.instance);
|
||||
postInitWebgl();
|
||||
}
|
||||
@@ -713,6 +714,31 @@ function initAddons(term: Terminal): void {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('addon');
|
||||
wrapper.appendChild(label);
|
||||
|
||||
// Add customGlyphs sub-checkbox for webgl addon
|
||||
if (name === 'webgl') {
|
||||
const customGlyphsCheckbox = document.createElement('input') as HTMLInputElement;
|
||||
customGlyphsCheckbox.type = 'checkbox';
|
||||
customGlyphsCheckbox.checked = true; // Default to enabled
|
||||
customGlyphsCheckbox.id = 'webgl-custom-glyphs';
|
||||
addDomListener(customGlyphsCheckbox, 'change', () => {
|
||||
if (addons.webgl.instance) {
|
||||
preDisposeWebgl();
|
||||
addons.webgl.instance.dispose();
|
||||
addons.webgl.instance = new addons.webgl.ctor({ customGlyphs: customGlyphsCheckbox.checked });
|
||||
term.loadAddon(addons.webgl.instance);
|
||||
postInitWebgl();
|
||||
}
|
||||
});
|
||||
const customGlyphsLabel = document.createElement('label');
|
||||
customGlyphsLabel.classList.add('addon');
|
||||
customGlyphsLabel.style.display = 'block';
|
||||
customGlyphsLabel.style.marginLeft = '20px';
|
||||
customGlyphsLabel.appendChild(customGlyphsCheckbox);
|
||||
customGlyphsLabel.appendChild(document.createTextNode('customGlyphs'));
|
||||
wrapper.appendChild(customGlyphsLabel);
|
||||
}
|
||||
|
||||
fragment.appendChild(wrapper);
|
||||
});
|
||||
const container = document.getElementById('addons-container');
|
||||
@@ -797,11 +823,13 @@ function customGlyphAlignmentHandler(): void {
|
||||
term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r');
|
||||
term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r');
|
||||
term.write('\n\r');
|
||||
|
||||
term.write('Other:\n\r');
|
||||
term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r');
|
||||
term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r');
|
||||
term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r');
|
||||
term.write('\n\r');
|
||||
|
||||
term.write('All box drawing characters:\n\r');
|
||||
term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r');
|
||||
term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r');
|
||||
@@ -811,6 +839,7 @@ function customGlyphAlignmentHandler(): void {
|
||||
term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r');
|
||||
term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r');
|
||||
term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r');
|
||||
|
||||
term.write('Box drawing alignment tests:\x1b[31m █\n\r');
|
||||
term.write(' ▉\n\r');
|
||||
term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r');
|
||||
@@ -829,6 +858,7 @@ function customGlyphAlignmentHandler(): void {
|
||||
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
|
||||
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
|
||||
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
|
||||
|
||||
term.write('\x1b[0mSmooth mosaic terminal graphic characters alignment tests:\x1b[33m\n\r');
|
||||
term.write(' 🭇🬼 🭈🬽 🭉🬾 🭊🬿 🭋🭀 🭁🭌 🭂🭍 🭃🭎 🭄🭏 🭅🭐 🭆🭑 🭨🭪 🭩 🭯 🭮🭬\n\r');
|
||||
term.write(' 🭢🭗 🭣🭘 🭤🭙 🭥🭚 🭦🭛 🭒🭝 🭓🭞 🭔🭟 🭕🭠 🭖🭡 🭧🭜 🭫 🭭\n\r');
|
||||
@@ -836,20 +866,24 @@ function customGlyphAlignmentHandler(): void {
|
||||
term.write(' 🭊🭁🭌🬿 🭈🭆🭂🭍🭑🬽 🭇🭄🭏🬼 🭃🭎 🭅🭐 🭨🭪\n\r');
|
||||
term.write(' 🭥🭒🭝🭚 🭣🭧🭓🭞🭜🭘 🭢🭕🭠🭗 🭔🭟 🭖🭡 🭪🭨\n\r');
|
||||
term.write(' 🭢🭗 🭤🭙 🭦🭛\n\r');
|
||||
|
||||
term.write('\x1b[0mCharacter cell diagonals (1FBA0-1FBAE) alignment tests:\x1b[34m\n\r');
|
||||
term.write(' \u{1FBA3}\u{1FBA7}\u{1FBA2} \u{1FBA3}\u{1FBA8}\u{1FBA0} \u{1FBAD}\u{1FBA2} \u{1FBA3}\u{1FBAC} \u{1FBAE}\n\r');
|
||||
term.write(' \u{1FBA3}\u{1FBA0} \u{1FBA1}\u{1FBA2} \u{1FBA1}\u{1FBA9}\u{1FBA2} \u{1FBA1}\u{1FBAA} \u{1FBAB}\u{1FBA0}\n\r');
|
||||
term.write(' \u{1FBA4} \u{1FBA5}\n\r');
|
||||
term.write(' \u{1FBA1}\u{1FBA2} \u{1FBA3}\u{1FBA0}\n\r');
|
||||
term.write(' \u{1FBA1}\u{1FBA6}\u{1FBA0}\n\r');
|
||||
|
||||
term.write('\x1b[0mCharacter cell diagonals (1FBD0-1FBDF) alignment tests:\x1b[34m\n\r');
|
||||
term.write(' \u{1FBD6}\u{1FBD4} \u{1FBD0}\u{1FBD1}\u{1FBD2}\u{1FBD3} \u{1FBDA} \u{1FBD9}\u{1FBDB} \u{1FBDE} \u{1FBDD}\u{1FBDF}\n\r');
|
||||
term.write(' \u{1FBD7}\u{1FBD5} \u{1FBD2}\u{1FBD3}\u{1FBD0}\u{1FBD1} \u{1FBD8} \u{1FBDC}\n\r');
|
||||
term.write(' \u{1FBD4}\u{1FBD6}\n\r');
|
||||
term.write(' \u{1FBD5}\u{1FBD7}\n\r');
|
||||
term.write('');
|
||||
|
||||
term.write('\x1b[0mComposite terminal graphics characters:\x1b[35m\n\r');
|
||||
term.write('\u{1FBB2}\u{1FBB3} \u{1FBB9}\u{1FBBA} \u{1FBC1}\u{1FBC2}\u{1FBC3}\n\r');
|
||||
|
||||
term.write('\x1b[0mFill tests:\x1b[36m\n\r');
|
||||
const fillChars = ['\u{2591}', '\u{2592}', '\u{2593}', '\u{1FB8C}', '\u{1FB8D}', '\u{1FB8E}', '\u{1FB8F}', '\u{1FB90}', '\u{1FB91}', '\u{1FB92}', '\u{1FB94}', '\u{1FB95}', '\u{1FB96}', '\u{1FB97}', '\u{1FB98}', '\u{1FB99}'];
|
||||
while (fillChars.length > 0) {
|
||||
@@ -867,7 +901,19 @@ function customGlyphAlignmentHandler(): void {
|
||||
}
|
||||
}
|
||||
|
||||
term.write('\x1b[0mPowerline alignment tests:\n\r');
|
||||
const powerlineLeftChars = ['\u{E0B2}', '\u{E0B3}', '\u{E0B6}', '\u{E0B7}', '\u{E0BA}', '\u{E0BB}', '\u{E0BE}', '\u{E0BF}', '\u{E0C2}', '\u{E0C3}', '\u{E0C5}', '\u{E0C7}', '\u{E0CA}', '\u{E0D4}'];
|
||||
const powerlineRightChars = ['\u{E0B0}', '\u{E0B1}', '\u{E0B4}', '\u{E0B5}', '\u{E0B8}', '\u{E0B9}', '\u{E0BC}', '\u{E0BD}', '\u{E0C0}', '\u{E0C1}', '\u{E0C4}', '\u{E0C6}', '\u{E0C8}', '\u{E0D2}', '\u{E0CC}', '\u{E0CD}', '\u{E0CE}', '\u{E0CF}', '\u{E0D0}', '\u{E0D1}'];
|
||||
for (const char of powerlineLeftChars) {
|
||||
term.write(`\x1b[31m${char}\x1b[0;41m \x1b[0m `);
|
||||
}
|
||||
term.write('\n\r');
|
||||
for (const char of powerlineRightChars) {
|
||||
term.write(`\x1b[41m \x1b[0;31m${char}\x1b[0m `);
|
||||
}
|
||||
|
||||
term.write('\x1b[0m');
|
||||
term.write('\n\r');
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
@@ -896,12 +942,18 @@ function customGlyphRangesHandler(): void {
|
||||
['Block elements', 0x2594, 0x2595],
|
||||
['Terminal graphic characters', 0x2596, 0x259F],
|
||||
]);
|
||||
// Braille Patterns
|
||||
// 2800-28FF
|
||||
// https://www.unicode.org/charts/PDF/U2800.pdf
|
||||
writeUnicodeTable(term, 'Braille patterns', 0x2800, 0x28FF, [
|
||||
['Braille patterns', 0x2800, 0x28FF],
|
||||
]);
|
||||
// Powerline Symbols
|
||||
// Range: E0A0–E0BF
|
||||
// Range: E0A0–E0D4
|
||||
// https://github.com/ryanoasis/nerd-fonts
|
||||
writeUnicodeTable(term, 'Powerline Symbols', 0xE0A0, 0xE0BF, [
|
||||
writeUnicodeTable(term, 'Powerline Symbols', 0xE0A0, 0xE0D4, [
|
||||
['Powerline Symbols', 0xE0A0, 0xE0B3, [0xE0A4, 0xE0A5, 0xE0A6, 0xE0A7, 0xE0A8, 0xE0A9, 0xE0AA, 0xE0AB, 0xE0AC, 0xE0AD, 0xE0AE, 0xE0AF]],
|
||||
['Powerline Extra Symbols', 0xE0B4, 0xE0BF],
|
||||
['Powerline Extra Symbols', 0xE0B4, 0xE0D4, [0xE0C9, 0xE0CB, 0xE0D3]],
|
||||
]);
|
||||
// Symbols for Legacy Computing
|
||||
// Range: 1FB00–1FBFF
|
||||
|
||||
+39
-11
@@ -218,22 +218,50 @@ export function writeUnicodeTable(term: Terminal, name: string, start: number, e
|
||||
term.write('\n\r');
|
||||
|
||||
// Render reserved labels that appear after the first label (below the row)
|
||||
// Only show one label pointing to the first reserved item when there are multiple
|
||||
// Show one label per non-contiguous reserved range
|
||||
const lateReserved = rowLabels.length > 0
|
||||
? rowReserved.filter(r => r.col >= rowLabels[0].col)
|
||||
: rowReserved;
|
||||
if (lateReserved.length > 0) {
|
||||
const prefix = ' '.repeat(8);
|
||||
const firstReserved = lateReserved[0];
|
||||
const colPos = firstReserved.col * 2 + 1;
|
||||
const padding = ' '.repeat(colPos);
|
||||
let line: string;
|
||||
if (firstReserved.colorIndex >= 0) {
|
||||
line = padding + color('└<reserved>', firstReserved.colorIndex);
|
||||
} else {
|
||||
line = padding + '└<reserved>';
|
||||
// Group contiguous reserved ranges
|
||||
const reservedGroups: { startCol: number, colorIndex: number }[] = [];
|
||||
for (let i = 0; i < lateReserved.length; i++) {
|
||||
const curr = lateReserved[i];
|
||||
const prev = lateReserved[i - 1];
|
||||
// Start a new group if not contiguous (gap of more than 1 column)
|
||||
if (i === 0 || curr.col > prev.col + 1) {
|
||||
reservedGroups.push({ startCol: curr.col, colorIndex: curr.colorIndex });
|
||||
}
|
||||
}
|
||||
|
||||
// Render from bottom to top (last group at bottom with └, earlier groups with │)
|
||||
for (let i = reservedGroups.length - 1; i >= 0; i--) {
|
||||
const prefix = ' '.repeat(8);
|
||||
let line = '';
|
||||
let visualLen = 0;
|
||||
for (let g = 0; g <= i; g++) {
|
||||
const group = reservedGroups[g];
|
||||
const colPos = group.startCol * 2 + 1;
|
||||
const padding = ' '.repeat(colPos - visualLen);
|
||||
if (g === i) {
|
||||
// This is the label for this line
|
||||
if (group.colorIndex >= 0) {
|
||||
line += padding + color('└<reserved>', group.colorIndex);
|
||||
} else {
|
||||
line += padding + '└<reserved>';
|
||||
}
|
||||
} else {
|
||||
// Vertical connector for groups below
|
||||
if (group.colorIndex >= 0) {
|
||||
line += padding + color('│', group.colorIndex);
|
||||
} else {
|
||||
line += padding + '│';
|
||||
}
|
||||
visualLen = colPos + 1;
|
||||
}
|
||||
}
|
||||
term.write(faint(prefix + line) + '\n\r');
|
||||
}
|
||||
term.write(faint(prefix + line) + '\n\r');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
// Clear the renderer when the a change that could affect glyphs occurs
|
||||
this._register(this._optionsService.onMultipleOptionChange([
|
||||
'customGlyphs',
|
||||
'drawBoldTextInBrightColors',
|
||||
'letterSpacing',
|
||||
'lineHeight',
|
||||
|
||||
@@ -132,7 +132,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this._register(Event.forward(this.coreService.onBinary, this._onBinary));
|
||||
this._register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom(true)));
|
||||
this._register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));
|
||||
this._register(this.optionsService.onMultipleOptionChange(['windowsMode', 'windowsPty'], () => this._handleWindowsPtyOptionChange()));
|
||||
this._register(this.optionsService.onMultipleOptionChange(['windowsPty'], () => this._handleWindowsPtyOptionChange()));
|
||||
this._register(this._bufferService.onScroll(() => {
|
||||
this._onScroll.fire({ position: this._bufferService.buffer.ydisp });
|
||||
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
@@ -255,8 +255,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
const windowsPty = this.optionsService.rawOptions.windowsPty;
|
||||
if (windowsPty && windowsPty.buildNumber !== undefined && windowsPty.buildNumber !== undefined) {
|
||||
value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);
|
||||
} else if (this.optionsService.rawOptions.windowsMode) {
|
||||
value = true;
|
||||
}
|
||||
if (value) {
|
||||
this._enableWindowsWrappingHeuristics();
|
||||
|
||||
@@ -181,7 +181,7 @@ export class Buffer implements IBuffer {
|
||||
if (this._rows < newRows) {
|
||||
for (let y = this._rows; y < newRows; y++) {
|
||||
if (this.lines.length < newRows + this.ybase) {
|
||||
if (this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {
|
||||
if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {
|
||||
// Just add the new missing rows on Windows as conpty reprints the screen with it's
|
||||
// view of the world. Once a line enters scrollback for conpty it remains there
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
@@ -298,7 +298,7 @@ export class Buffer implements IBuffer {
|
||||
if (windowsPty && windowsPty.buildNumber) {
|
||||
return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376;
|
||||
}
|
||||
return this._hasScrollback && !this._optionsService.rawOptions.windowsMode;
|
||||
return this._hasScrollback;
|
||||
}
|
||||
|
||||
private _reflow(newCols: number, newRows: number): void {
|
||||
|
||||
@@ -16,10 +16,8 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
|
||||
cursorStyle: 'block',
|
||||
cursorWidth: 1,
|
||||
cursorInactiveStyle: 'outline',
|
||||
customGlyphs: true,
|
||||
drawBoldTextInBrightColors: true,
|
||||
documentOverride: null,
|
||||
fastScrollModifier: 'alt',
|
||||
fastScrollSensitivity: 5,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 15,
|
||||
@@ -49,7 +47,6 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
|
||||
rescaleOverlappingGlyphs: false,
|
||||
rightClickSelectsWord: isMac,
|
||||
windowOptions: {},
|
||||
windowsMode: false,
|
||||
windowsPty: {},
|
||||
wordSeparator: ' ()[]{}\',"`',
|
||||
altClickMovesCursor: true,
|
||||
|
||||
@@ -230,12 +230,9 @@ export interface ITerminalOptions {
|
||||
cursorStyle?: CursorStyle;
|
||||
cursorWidth?: number;
|
||||
cursorInactiveStyle?: CursorInactiveStyle;
|
||||
customGlyphs?: boolean;
|
||||
disableStdin?: boolean;
|
||||
documentOverride?: any | null;
|
||||
drawBoldTextInBrightColors?: boolean;
|
||||
/** @deprecated No longer supported */
|
||||
fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift';
|
||||
fastScrollSensitivity?: number;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
@@ -261,7 +258,6 @@ export interface ITerminalOptions {
|
||||
smoothScrollDuration?: number;
|
||||
tabStopWidth?: number;
|
||||
theme?: ITheme;
|
||||
windowsMode?: boolean;
|
||||
windowsPty?: IWindowsPty;
|
||||
windowOptions?: IWindowOptions;
|
||||
wordSeparator?: string;
|
||||
|
||||
Vendored
-42
@@ -63,22 +63,6 @@ declare module '@xterm/headless' {
|
||||
*/
|
||||
cursorWidth?: number;
|
||||
|
||||
/**
|
||||
* Whether to draw custom glyphs instead of using the font for the following
|
||||
* unicode ranges:
|
||||
*
|
||||
* - Box Drawing (U+2500-U+257F)
|
||||
* - Box Elements (U+2580-U+259F)
|
||||
* - Powerline Symbols (U+E0A0–U+E0BF)
|
||||
* - Symbols for Legacy Computing (U+1FB00–U+1FBFF)
|
||||
*
|
||||
* This will typically result in better rendering with continuous lines,
|
||||
* even when line height and letter spacing is used. Note that this doesn't
|
||||
* work with the DOM renderer which renders all characters using the font.
|
||||
* The default is true.
|
||||
*/
|
||||
customGlyphs?: boolean;
|
||||
|
||||
/**
|
||||
* Whether input should be disabled.
|
||||
*/
|
||||
@@ -89,13 +73,6 @@ declare module '@xterm/headless' {
|
||||
*/
|
||||
drawBoldTextInBrightColors?: boolean;
|
||||
|
||||
/**
|
||||
* The modifier key hold to multiply scroll speed.
|
||||
* @deprecated This option is no longer available and will always use alt.
|
||||
* Setting this will be ignored.
|
||||
*/
|
||||
fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift';
|
||||
|
||||
/**
|
||||
* The spacing in whole pixels between characters.
|
||||
*/
|
||||
@@ -222,25 +199,6 @@ declare module '@xterm/headless' {
|
||||
*/
|
||||
theme?: ITheme;
|
||||
|
||||
/**
|
||||
* Whether "Windows mode" is enabled. Because Windows backends winpty and
|
||||
* conpty operate by doing line wrapping on their side, xterm.js does not
|
||||
* have access to wrapped lines. When Windows mode is enabled the following
|
||||
* changes will be in effect:
|
||||
*
|
||||
* - Reflow is disabled.
|
||||
* - Lines are assumed to be wrapped if the last character of the line is
|
||||
* not whitespace.
|
||||
*
|
||||
* When using conpty on Windows 11 version >= 21376, it is recommended to
|
||||
* disable this because native text wrapping sequences are output correctly
|
||||
* thanks to https://github.com/microsoft/terminal/issues/405
|
||||
*
|
||||
* @deprecated Use {@link windowsPty}. This value will be ignored if
|
||||
* windowsPty is set.
|
||||
*/
|
||||
windowsMode?: boolean;
|
||||
|
||||
/**
|
||||
* Compatibility information when the pty is known to be hosted on Windows.
|
||||
* Setting this will turn on certain heuristics/workarounds depending on the
|
||||
|
||||
Vendored
-16
@@ -77,22 +77,6 @@ declare module '@xterm/xterm' {
|
||||
*/
|
||||
cursorInactiveStyle?: 'outline' | 'block' | 'bar' | 'underline' | 'none';
|
||||
|
||||
/**
|
||||
* Whether to draw custom glyphs instead of using the font for the following
|
||||
* unicode ranges:
|
||||
*
|
||||
* - Box Drawing (U+2500-U+257F)
|
||||
* - Box Elements (U+2580-U+259F)
|
||||
* - Powerline Symbols (U+E0A0–U+E0BF)
|
||||
* - Symbols for Legacy Computing (U+1FB00–U+1FBFF)
|
||||
*
|
||||
* This will typically result in better rendering with continuous lines,
|
||||
* even when line height and letter spacing is used. Note that this doesn't
|
||||
* work with the DOM renderer which renders all characters using the font.
|
||||
* The default is true.
|
||||
*/
|
||||
customGlyphs?: boolean;
|
||||
|
||||
/**
|
||||
* Whether input should be disabled.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user