@opentui/core
Version:
OpenTUI is a TypeScript library on a native Zig core for building terminal user interfaces (TUIs)
15,612 lines • 525 kB
JavaScript
import {
BaseRenderable,
BoxRenderable,
CliRenderEvents,
CliRenderer,
CodeRenderable,
ConsolePosition,
EditBuffer,
EditBufferRenderable,
EditBufferRenderableEvents,
EditorView,
LayoutEvents,
MouseButton,
MouseEvent,
NativeSpanFeed,
Renderable,
RenderableEvents,
RendererControlState,
RootRenderable,
RootTextNodeRenderable,
SyntaxStyle,
TerminalConsole,
TextBufferRenderable,
TextBufferView,
TextNodeRenderable,
TextRenderable,
buildKeyBindingsMap,
buildKittyKeyboardFlags,
capture,
convertThemeToStyles,
createCliRenderer,
defaultKeyAliases,
delegate,
getKeyBindingAction,
getObjectsInViewport,
h,
instantiate,
isEditBufferRenderable,
isRenderable,
isTextNodeRenderable,
isVNode,
maybeMakeRenderable,
mergeKeyAliases,
mergeKeyBindings,
wrapWithDelegates
} from "./chunk-node-dcyj0dm5.js";
import {
ASCIIFontSelectionHelper,
ATTRIBUTE_BASE_BITS,
ATTRIBUTE_BASE_MASK,
BorderCharArrays,
BorderChars,
Clipboard,
ClipboardTarget,
DEFAULT_BACKGROUND_RGB,
DEFAULT_FOREGROUND_RGB,
DataPathsManager,
DebugOverlayCorner,
ExtmarksController,
InternalKeyHandler,
KeyEvent,
KeyHandler,
LinearScrollAccel,
LogLevel,
MacOSScrollAccel,
MouseParser,
NativeAudioStreamCloseReason,
NativeAudioStreamCloseReason1 as NativeAudioStreamCloseReason2,
NativeAudioStreamFormat,
NativeAudioStreamFormat1 as NativeAudioStreamFormat2,
NativeAudioStreamState,
NativeAudioStreamState1 as NativeAudioStreamState2,
NativeAudioStreamStateNames,
NativeClipboardCancelStatus,
NativeClipboardCopyStatus,
NativeClipboardDestroyStatus,
NativeClipboardOperationStatus,
NativeClipboardShutdownStatus,
NativeClipboardStartStatus,
NativeMeasureTargetKind,
OptimizedBuffer,
PasteEvent,
RGBA,
Selection,
StdinParser,
StyledText,
SystemClock,
TargetChannel,
TerminalPalette,
TextAttributes,
TextBuffer,
TreeSitterClient,
addDefaultParsers,
ansi256IndexToRgb,
attributesWithLink,
basenameToFiletype,
bg,
bgBlack,
bgBlue,
bgCyan,
bgGreen,
bgMagenta,
bgRed,
bgWhite,
bgYellow,
black,
blink,
blue,
bold,
borderCharsToArray,
brightBlack,
brightBlue,
brightCyan,
brightGreen,
brightMagenta,
brightRed,
brightWhite,
brightYellow,
buildTerminalPaletteSignature,
clearEnvCache,
convertGlobalToLocalSelection,
coordinateToCharacterIndex,
createClipboard,
createExtmarksController,
createHostClipboard,
createRendererClipboardAdapter,
createTerminalPalette,
createTextAttributes,
cyan,
decodePasteBytes,
destroyTreeSitterClient,
detectLinks,
dim,
env,
envRegistry,
exports_yoga,
extToFiletype,
extensionToFiletype,
fg,
fonts,
generateEnvColored,
generateEnvMarkdown,
getBaseAttributes,
getBorderFromSides,
getBorderSides,
getCharacterPositions,
getDataPaths,
getLinkId,
getTreeSitterClient,
green,
hastToStyledText,
hexToRgb,
hsvToRgb,
infoStringToFiletype,
isStyledText,
isValidBorderStyle,
italic,
link,
magenta,
measureText,
nonAlphanumericKeys,
normalizeColorValue,
normalizeIndexedColorIndex,
normalizeTerminalPalette,
parseAlign,
parseAlignItems,
parseBorderStyle,
parseBoxSizing,
parseColor,
parseDimension,
parseDirection,
parseDisplay,
parseEdge,
parseFlexDirection,
parseGutter,
parseJustify,
parseKeypress,
parseLogLevel,
parseMeasureMode,
parseOverflow,
parsePositionType,
parseUnit,
parseWrap,
pathToFiletype,
red,
registerEnvVar,
renderFontToFrameBuffer,
resolveBundledFilePath,
resolveRenderLib,
reverse,
rgbToHex,
setRenderLibPath,
strikethrough,
stringToStyledText,
stringWidth,
stripAnsiSequences,
t,
terminalNamedSingleStrokeKeys,
toArrayBuffer,
treeSitterToStyledText,
treeSitterToTextChunks,
underline,
visualizeRenderableTree,
white,
yellow
} from "./chunk-node-zcz10bhr.js";
// src/post/effects.ts
function toU8(value) {
return Math.round(Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0)) * 255);
}
function channel(buffer, index) {
return (buffer[index] & 255) / 255;
}
function setRgb(buffer, base, r, g, b) {
const a = buffer[base + 3] & 255;
buffer[base] = toU8(r);
buffer[base + 1] = toU8(g);
buffer[base + 2] = toU8(b);
buffer[base + 3] = a;
}
function isGraphemeCell(char) {
return char >>> 30 >= 2;
}
class DistortionEffect {
glitchChancePerSecond = 0.5;
maxGlitchLines = 3;
minGlitchDuration = 0.05;
maxGlitchDuration = 0.2;
maxShiftAmount = 10;
shiftFlipRatio = 0.6;
colorGlitchChance = 0.2;
lastGlitchTime = 0;
glitchDuration = 0;
activeGlitches = [];
constructor(options) {
if (options) {
Object.assign(this, options);
}
}
apply(buffer, deltaTime) {
const width = buffer.width;
const height = buffer.height;
const buf = buffer.buffers;
this.lastGlitchTime += deltaTime;
if (this.activeGlitches.length > 0 && this.lastGlitchTime >= this.glitchDuration) {
this.activeGlitches = [];
this.glitchDuration = 0;
}
if (this.activeGlitches.length === 0 && Math.random() < this.glitchChancePerSecond * deltaTime) {
this.lastGlitchTime = 0;
this.glitchDuration = this.minGlitchDuration + Math.random() * (this.maxGlitchDuration - this.minGlitchDuration);
const numGlitches = 1 + Math.floor(Math.random() * this.maxGlitchLines);
for (let i = 0;i < numGlitches; i++) {
const y = Math.floor(Math.random() * height);
let type;
let amount = 0;
const typeRoll = Math.random();
if (typeRoll < this.colorGlitchChance) {
type = "color";
} else {
const shiftRoll = (typeRoll - this.colorGlitchChance) / (1 - this.colorGlitchChance);
if (shiftRoll < this.shiftFlipRatio) {
type = "shift";
amount = Math.floor((Math.random() - 0.5) * 2 * this.maxShiftAmount);
} else {
type = "flip";
}
}
if (!this.activeGlitches.some((g) => g.y === y)) {
this.activeGlitches.push({ y, type, amount });
}
}
}
if (this.activeGlitches.length > 0) {
let tempChar = null;
let tempFg = null;
let tempBg = null;
let tempAttr = null;
for (const glitch of this.activeGlitches) {
const y = glitch.y;
if (y < 0 || y >= height)
continue;
const baseIndex = y * width;
if (glitch.type === "shift" || glitch.type === "flip") {
if (!tempChar) {
tempChar = new Uint32Array(width);
tempFg = new Uint16Array(width * 4);
tempBg = new Uint16Array(width * 4);
tempAttr = new Uint8Array(width);
}
try {
tempChar.set(buf.char.subarray(baseIndex, baseIndex + width));
tempFg.set(buf.fg.subarray(baseIndex * 4, (baseIndex + width) * 4));
tempBg.set(buf.bg.subarray(baseIndex * 4, (baseIndex + width) * 4));
tempAttr.set(buf.attributes.subarray(baseIndex, baseIndex + width));
} catch (e) {
console.error(`Error copying row ${y} for distortion:`, e);
continue;
}
if (glitch.type === "shift") {
const shift = glitch.amount;
for (let x = 0;x < width; x++) {
const srcX = (x - shift + width) % width;
const destIndex = baseIndex + x;
const srcTempIndex = srcX;
buf.char[destIndex] = tempChar[srcTempIndex];
buf.attributes[destIndex] = tempAttr[srcTempIndex];
const destColorIndex = destIndex * 4;
const srcTempColorIndex = srcTempIndex * 4;
buf.fg.set(tempFg.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex);
buf.bg.set(tempBg.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex);
}
} else {
for (let x = 0;x < width; x++) {
const srcX = width - 1 - x;
const destIndex = baseIndex + x;
const srcTempIndex = srcX;
buf.char[destIndex] = tempChar[srcTempIndex];
buf.attributes[destIndex] = tempAttr[srcTempIndex];
const destColorIndex = destIndex * 4;
const srcTempColorIndex = srcTempIndex * 4;
buf.fg.set(tempFg.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex);
buf.bg.set(tempBg.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex);
}
}
} else if (glitch.type === "color") {
const glitchStart = Math.floor(Math.random() * width);
const maxPossibleLength = width - glitchStart;
let glitchLength = Math.floor(Math.random() * maxPossibleLength) + 1;
if (Math.random() < 0.2) {
glitchLength = Math.floor(Math.random() * (width / 4)) + 1;
}
glitchLength = Math.min(glitchLength, maxPossibleLength);
for (let x = glitchStart;x < glitchStart + glitchLength; x++) {
if (x >= width)
break;
const destIndex = baseIndex + x;
const destColorIndex = destIndex * 4;
let rFg, gFg, bFg, rBg, gBg, bBg;
const colorMode = Math.random();
if (colorMode < 0.33) {
rFg = Math.random();
gFg = Math.random();
bFg = Math.random();
rBg = Math.random();
gBg = Math.random();
bBg = Math.random();
} else if (colorMode < 0.66) {
const emphasis = Math.random();
if (emphasis < 0.25) {
rFg = Math.random();
gFg = 0;
bFg = 0;
} else if (emphasis < 0.5) {
rFg = 0;
gFg = Math.random();
bFg = 0;
} else if (emphasis < 0.75) {
rFg = 0;
gFg = 0;
bFg = Math.random();
} else {
const glitchColorRoll = Math.random();
if (glitchColorRoll < 0.33) {
rFg = 1;
gFg = 0;
bFg = 1;
} else if (glitchColorRoll < 0.66) {
rFg = 0;
gFg = 1;
bFg = 1;
} else {
rFg = 1;
gFg = 1;
bFg = 0;
}
}
if (Math.random() < 0.5) {
rBg = 1 - rFg;
gBg = 1 - gFg;
bBg = 1 - bFg;
} else {
rBg = rFg * (Math.random() * 0.5 + 0.2);
gBg = gFg * (Math.random() * 0.5 + 0.2);
bBg = bFg * (Math.random() * 0.5 + 0.2);
}
} else {
rFg = Math.random() > 0.5 ? 1 : 0;
gFg = Math.random() > 0.5 ? 1 : 0;
bFg = Math.random() > 0.5 ? 1 : 0;
rBg = 1 - rFg;
gBg = 1 - gFg;
bBg = 1 - bFg;
}
setRgb(buf.fg, destColorIndex, rFg, gFg, bFg);
setRgb(buf.bg, destColorIndex, rBg, gBg, bBg);
}
}
}
}
}
}
class VignetteEffect {
_strength;
precomputedAttenuationCellMask = null;
precomputedAttenuation = null;
cachedWidth = -1;
cachedHeight = -1;
static zeroMatrix = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
constructor(strength = 0.5) {
this._strength = strength;
}
set strength(newStrength) {
this._strength = Math.max(0, newStrength);
this.cachedWidth = -1;
this.cachedHeight = -1;
this.precomputedAttenuationCellMask = null;
this.precomputedAttenuation = null;
}
get strength() {
return this._strength;
}
_computeFactors(width, height) {
this.precomputedAttenuationCellMask = new Float32Array(width * height * 3);
this.precomputedAttenuation = new Float32Array(width * height);
const centerX = width / 2;
const centerY = height / 2;
const maxDistSq = centerX * centerX + centerY * centerY;
const safeMaxDistSq = maxDistSq === 0 ? 1 : maxDistSq;
const strength = this._strength;
let i = 0;
for (let y = 0;y < height; y++) {
const dy = y - centerY;
const dySq = dy * dy;
for (let x = 0;x < width; x++) {
const dx = x - centerX;
const distSq = dx * dx + dySq;
const baseAttenuation = Math.min(1, distSq / safeMaxDistSq);
const attenuation = baseAttenuation * strength;
this.precomputedAttenuationCellMask[i++] = x;
this.precomputedAttenuationCellMask[i++] = y;
this.precomputedAttenuationCellMask[i++] = attenuation;
this.precomputedAttenuation[y * width + x] = attenuation;
}
}
this.cachedWidth = width;
this.cachedHeight = height;
}
_preserveGraphemeRuns(buffer) {
const baseAttenuation = this.precomputedAttenuation;
const cellMask = this.precomputedAttenuationCellMask;
const chars = buffer.buffers.char;
const width = buffer.width;
for (let y = 0;y < buffer.height; y++) {
let x = 0;
while (x < width) {
const cell = y * width + x;
if (!isGraphemeCell(chars[cell])) {
cellMask[cell * 3 + 2] = baseAttenuation[cell];
x++;
continue;
}
const start = x;
while (x < width && isGraphemeCell(chars[y * width + x]))
x++;
const attenuation = baseAttenuation[y * width + start + Math.floor((x - start) / 2)];
for (let runX = start;runX < x; runX++)
cellMask[(y * width + runX) * 3 + 2] = attenuation;
}
}
}
apply(buffer) {
const width = buffer.width;
const height = buffer.height;
if (width !== this.cachedWidth || height !== this.cachedHeight || !this.precomputedAttenuationCellMask) {
this._computeFactors(width, height);
}
this._preserveGraphemeRuns(buffer);
buffer.colorMatrix(VignetteEffect.zeroMatrix, this.precomputedAttenuationCellMask, 1, 3);
}
}
class PerlinNoise {
perm;
grad3 = [
[1, 1, 0],
[-1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[1, 0, 1],
[-1, 0, 1],
[1, 0, -1],
[-1, 0, -1],
[0, 1, 1],
[0, -1, 1],
[0, 1, -1],
[0, -1, -1]
];
constructor() {
this.perm = new Uint8Array(512);
const p = new Uint8Array(256);
for (let i = 0;i < 256; i++) {
p[i] = i;
}
for (let i = 255;i > 0; i--) {
const r = Math.floor(Math.random() * (i + 1));
[p[i], p[r]] = [p[r], p[i]];
}
for (let i = 0;i < 512; i++) {
this.perm[i] = p[i & 255];
}
}
dot(g, x, y, z) {
return g[0] * x + g[1] * y + g[2] * z;
}
mix(a, b, t2) {
return a + (b - a) * t2;
}
fade(t2) {
return t2 * t2 * t2 * (t2 * (t2 * 6 - 15) + 10);
}
noise3d(x, y, z) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
const Z = Math.floor(z) & 255;
const xf = x - Math.floor(x);
const yf = y - Math.floor(y);
const zf = z - Math.floor(z);
const u = this.fade(xf);
const v = this.fade(yf);
const w = this.fade(zf);
const A = this.perm[X] + Y;
const AA = this.perm[A] + Z;
const AB = this.perm[A + 1] + Z;
const B = this.perm[X + 1] + Y;
const BA = this.perm[B] + Z;
const BB = this.perm[B + 1] + Z;
let res = this.mix(this.mix(this.mix(this.dot(this.grad3[this.perm[AA] % 12], xf, yf, zf), this.dot(this.grad3[this.perm[BA] % 12], xf - 1, yf, zf), u), this.mix(this.dot(this.grad3[this.perm[AB] % 12], xf, yf - 1, zf), this.dot(this.grad3[this.perm[BB] % 12], xf - 1, yf - 1, zf), u), v), this.mix(this.mix(this.dot(this.grad3[this.perm[AA + 1] % 12], xf, yf, zf - 1), this.dot(this.grad3[this.perm[BA + 1] % 12], xf - 1, yf, zf - 1), u), this.mix(this.dot(this.grad3[this.perm[AB + 1] % 12], xf, yf - 1, zf - 1), this.dot(this.grad3[this.perm[BB + 1] % 12], xf - 1, yf - 1, zf - 1), u), v), w);
return res;
}
}
class CloudsEffect {
noise;
_scale;
_speed;
_density;
_darkness;
time = 0;
constructor(scale = 0.02, speed = 0.5, density = 0.6, darkness = 0.7) {
this.noise = new PerlinNoise;
this._scale = scale;
this._speed = speed;
this._density = density;
this._darkness = darkness;
}
set scale(newScale) {
this._scale = Math.max(0.001, newScale);
}
get scale() {
return this._scale;
}
set speed(newSpeed) {
this._speed = Math.max(0, newSpeed);
}
get speed() {
return this._speed;
}
set density(newDensity) {
this._density = Math.max(0, Math.min(1, newDensity));
}
get density() {
return this._density;
}
set darkness(newDarkness) {
this._darkness = Math.max(0, Math.min(1, newDarkness));
}
get darkness() {
return this._darkness;
}
apply(buffer, deltaTime) {
const width = buffer.width;
const height = buffer.height;
this.time += deltaTime * this._speed;
const scale = this._scale;
const timeOffset = this.time;
const cellMask = new Float32Array(width * height * 3);
let maskIdx = 0;
for (let y = 0;y < height; y++) {
for (let x = 0;x < width; x++) {
let noiseValue = 0;
let amplitude = 1;
let frequency = 1;
let maxValue = 0;
for (let i = 0;i < 4; i++) {
const nx = (x * scale * frequency + timeOffset) * 0.5;
const ny = y * scale * frequency * 0.5;
const nz = timeOffset * 0.3;
noiseValue += this.noise.noise3d(nx, ny, nz) * amplitude;
maxValue += amplitude;
amplitude *= 0.5;
frequency *= 2;
}
noiseValue = (noiseValue / maxValue + 1) * 0.5;
const cloudDensity = Math.max(0, noiseValue - (1 - this._density));
const attenuation = cloudDensity * this._darkness;
cellMask[maskIdx++] = x;
cellMask[maskIdx++] = y;
cellMask[maskIdx++] = attenuation;
}
}
const zeroMatrix = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
buffer.colorMatrix(zeroMatrix, cellMask, 1, 2);
}
}
class FlamesEffect {
noise;
_scale;
_speed;
_intensity;
time = 0;
constructor(scale = 0.03, speed = 0.02, intensity = 0.8) {
this.noise = new PerlinNoise;
this._scale = scale;
this._speed = speed;
this._intensity = intensity;
}
set scale(newScale) {
this._scale = Math.max(0.001, newScale);
}
get scale() {
return this._scale;
}
set speed(newSpeed) {
this._speed = Math.max(0, newSpeed);
}
get speed() {
return this._speed;
}
set intensity(newIntensity) {
this._intensity = Math.max(0, Math.min(1, newIntensity));
}
get intensity() {
return this._intensity;
}
apply(buffer, deltaTime) {
const width = buffer.width;
const height = buffer.height;
const bg2 = buffer.buffers.bg;
this.time += deltaTime * this._speed;
const scale = this._scale;
const timeOffset = this.time;
for (let y = 0;y < height; y++) {
const heightFactor = 1 - y / height;
for (let x = 0;x < width; x++) {
let noiseValue = 0;
let amplitude = 1;
let frequency = 1;
let maxValue = 0;
for (let i = 0;i < 3; i++) {
const nx = (x * scale * frequency + timeOffset) * 0.5;
const ny = (height - y) * scale * frequency * 2 * 0.5;
const nz = timeOffset * 2;
noiseValue += this.noise.noise3d(nx, ny, nz) * amplitude;
maxValue += amplitude;
amplitude *= 0.5;
frequency *= 2;
}
noiseValue = (noiseValue / maxValue + 1) * 0.5;
const flameIntensity = noiseValue * heightFactor * this._intensity;
if (flameIntensity > 0) {
const colorIndex = (y * width + x) * 4;
let r, g, b;
if (flameIntensity > 0.7) {
r = 1;
g = 1;
b = 0.3 + (flameIntensity - 0.7) * 2.3;
} else if (flameIntensity > 0.4) {
r = 1;
g = 0.5 + (flameIntensity - 0.4) * 1.67;
b = 0;
} else {
r = 0.3 + flameIntensity * 1.75;
g = flameIntensity * 0.5;
b = 0;
}
setRgb(bg2, colorIndex, Math.max(channel(bg2, colorIndex), r * flameIntensity), Math.max(channel(bg2, colorIndex + 1), g * flameIntensity), Math.max(channel(bg2, colorIndex + 2), b * flameIntensity));
}
}
}
}
}
class CRTRollingBarEffect {
_speed;
_height;
_intensity;
_fadeDistance;
position = 0;
constructor(speed = 0.5, height = 0.15, intensity = 0.3, fadeDistance = 0.3) {
this._speed = speed;
this._height = Math.max(0.01, Math.min(0.5, height));
this._intensity = Math.max(0, Math.min(1, intensity));
this._fadeDistance = Math.max(0, Math.min(1, fadeDistance));
}
set speed(newSpeed) {
this._speed = newSpeed;
}
get speed() {
return this._speed;
}
set height(newHeight) {
this._height = Math.max(0.01, Math.min(0.5, newHeight));
}
get height() {
return this._height;
}
set intensity(newIntensity) {
this._intensity = Math.max(0, Math.min(1, newIntensity));
}
get intensity() {
return this._intensity;
}
set fadeDistance(newFadeDistance) {
this._fadeDistance = Math.max(0, Math.min(1, newFadeDistance));
}
get fadeDistance() {
return this._fadeDistance;
}
apply(buffer, deltaTime) {
const width = buffer.width;
const height = buffer.height;
const fg2 = buffer.buffers.fg;
const bg2 = buffer.buffers.bg;
this.position += deltaTime / 1000 * this._speed;
const cycleHeight = height + this._height * height * 2;
this.position = this.position % cycleHeight;
const barPixelHeight = this._height * height;
const fadePixelDistance = this._fadeDistance * barPixelHeight;
const totalEffectHeight = barPixelHeight + fadePixelDistance * 2;
const effectCenter = this.position - totalEffectHeight / 2 + barPixelHeight / 2;
for (let y = 0;y < height; y++) {
const distFromCenter = Math.abs(y - effectCenter);
let barFactor = 0;
if (distFromCenter <= totalEffectHeight / 2) {
const normalizedDist = distFromCenter / (totalEffectHeight / 2);
barFactor = Math.cos(normalizedDist * Math.PI / 2);
}
if (barFactor > 0.001) {
const rowMultiplier = 1 + this._intensity * barFactor;
for (let x = 0;x < width; x++) {
const colorIndex = (y * width + x) * 4;
setRgb(fg2, colorIndex, Math.min(1, channel(fg2, colorIndex) * rowMultiplier), Math.min(1, channel(fg2, colorIndex + 1) * rowMultiplier), Math.min(1, channel(fg2, colorIndex + 2) * rowMultiplier));
setRgb(bg2, colorIndex, Math.min(1, channel(bg2, colorIndex) * rowMultiplier), Math.min(1, channel(bg2, colorIndex + 1) * rowMultiplier), Math.min(1, channel(bg2, colorIndex + 2) * rowMultiplier));
}
}
}
}
}
class RainbowTextEffect {
_speed;
_saturation;
_value;
_repeats;
time = 0;
constructor(speed = 0.01, saturation = 1, value = 1, repeats = 3) {
this._speed = speed;
this._saturation = saturation;
this._value = value;
this._repeats = repeats;
}
set speed(newSpeed) {
this._speed = Math.max(0, newSpeed);
}
get speed() {
return this._speed;
}
set saturation(newSaturation) {
this._saturation = Math.max(0, Math.min(1, newSaturation));
}
get saturation() {
return this._saturation;
}
set value(newValue) {
this._value = Math.max(0, Math.min(1, newValue));
}
get value() {
return this._value;
}
set repeats(newRepeats) {
this._repeats = Math.max(0.1, newRepeats);
}
get repeats() {
return this._repeats;
}
hsvToRgb(h2, s, v) {
let r = 0, g = 0, b = 0;
const i = Math.floor(h2 * 6);
const f = h2 * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t2 = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t2;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t2;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t2;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
return [r, g, b];
}
apply(buffer, deltaTime) {
const width = buffer.width;
const height = buffer.height;
const fg2 = buffer.buffers.fg;
this.time += deltaTime * this._speed;
const saturation = this._saturation;
const value = this._value;
const repeats = this._repeats;
const angleRad = 25 * Math.PI / 180;
const cosAngle = Math.cos(angleRad);
const sinAngle = Math.sin(angleRad);
const whiteThreshold = 0.9;
for (let y = 0;y < height; y++) {
for (let x = 0;x < width; x++) {
const colorIndex = (y * width + x) * 4;
const r = channel(fg2, colorIndex);
const g = channel(fg2, colorIndex + 1);
const b = channel(fg2, colorIndex + 2);
if (r >= whiteThreshold && g >= whiteThreshold && b >= whiteThreshold) {
const projection = x * cosAngle + y * sinAngle;
const maxProjection = width * cosAngle + height * sinAngle;
const hue = (projection / maxProjection * repeats + this.time * 0.1) % 1;
const [newR, newG, newB] = this.hsvToRgb(hue, saturation, value);
setRgb(fg2, colorIndex, newR, newG, newB);
}
}
}
}
}
// src/post/filters.ts
function toU82(value) {
return Math.round(Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0)) * 255);
}
function channel2(buffer, index) {
return (buffer[index] & 255) / 255;
}
function setRgb2(buffer, base, r, g, b) {
const a = buffer[base + 3] & 255;
buffer[base] = toU82(r);
buffer[base + 1] = toU82(g);
buffer[base + 2] = toU82(b);
buffer[base + 3] = a;
}
function applyScanlines(buffer, strength = 0.8, step = 2) {
if (strength === 1 || step < 1)
return;
const width = buffer.width;
const height = buffer.height;
const affectedRows = Math.ceil(height / step);
const cellCount = width * affectedRows;
const cellMask = new Float32Array(cellCount * 3);
let maskIdx = 0;
for (let y = 0;y < height; y += step) {
for (let x = 0;x < width; x++) {
cellMask[maskIdx++] = x;
cellMask[maskIdx++] = y;
cellMask[maskIdx++] = 1;
}
}
const s = strength;
const matrix = new Float32Array([
s,
0,
0,
0,
0,
s,
0,
0,
0,
0,
s,
0,
0,
0,
0,
1
]);
buffer.colorMatrix(matrix, cellMask, 1, 2);
}
function applyInvert(buffer, strength = 1) {
if (strength === 0)
return;
const matrix = new Float32Array([
-1,
0,
0,
1,
0,
-1,
0,
1,
0,
0,
-1,
1,
0,
0,
0,
1
]);
buffer.colorMatrixUniform(matrix, strength, 3);
}
function applyNoise(buffer, strength = 0.1) {
const width = buffer.width;
const height = buffer.height;
const size = width * height;
if (strength === 0)
return;
const cellMask = new Float32Array(size * 3);
let cellMaskIndex = 0;
for (let y = 0;y < height; y++) {
for (let x = 0;x < width; x++) {
cellMask[cellMaskIndex++] = x;
cellMask[cellMaskIndex++] = y;
cellMask[cellMaskIndex++] = (Math.random() - 0.5) * 2;
}
}
const b = 1 + strength;
const matrix = new Float32Array([
b,
0,
0,
0,
0,
b,
0,
0,
0,
0,
b,
0,
0,
0,
0,
1
]);
buffer.colorMatrix(matrix, cellMask, 1, 3);
}
function applyChromaticAberration(buffer, strength = 1) {
const width = buffer.width;
const height = buffer.height;
const srcFg = Uint16Array.from(buffer.buffers.fg);
const destFg = buffer.buffers.fg;
const centerX = width / 2;
const centerY = height / 2;
for (let y = 0;y < height; y++) {
for (let x = 0;x < width; x++) {
const dx = x - centerX;
const dy = y - centerY;
const offset = Math.round(Math.sqrt(dx * dx + dy * dy) / Math.max(centerX, centerY) * strength);
const rX = Math.max(0, Math.min(width - 1, x - offset));
const bX = Math.max(0, Math.min(width - 1, x + offset));
const rIndex = (y * width + rX) * 4;
const gIndex = (y * width + x) * 4;
const bIndex = (y * width + bX) * 4;
const destIndex = (y * width + x) * 4;
setRgb2(destFg, destIndex, channel2(srcFg, rIndex), channel2(srcFg, gIndex + 1), channel2(srcFg, bIndex + 2));
}
}
}
function applyAsciiArt(buffer, ramp = ' .\'`^"",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$', fgColor = { r: 1, g: 1, b: 1 }, bgColor = { r: 0, g: 0, b: 0 }) {
const width = buffer.width;
const height = buffer.height;
const chars = buffer.buffers.char;
const bg2 = buffer.buffers.bg;
const rampLength = ramp.length;
for (let y = 0;y < height; y++) {
for (let x = 0;x < width; x++) {
const index = y * width + x;
const colorIndex = index * 4;
const bgR = channel2(bg2, colorIndex);
const bgG = channel2(bg2, colorIndex + 1);
const bgB = channel2(bg2, colorIndex + 2);
const lum = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB;
const rampIndex = Math.min(rampLength - 1, Math.floor(lum * rampLength));
chars[index] = ramp[rampIndex].charCodeAt(0);
}
}
const fgMatrix = new Float32Array([
0,
0,
0,
fgColor.r,
0,
0,
0,
fgColor.g,
0,
0,
0,
fgColor.b,
0,
0,
0,
1
]);
const bgMatrix = new Float32Array([
0,
0,
0,
bgColor.r,
0,
0,
0,
bgColor.g,
0,
0,
0,
bgColor.b,
0,
0,
0,
1
]);
buffer.colorMatrixUniform(fgMatrix, 1, 1);
buffer.colorMatrixUniform(bgMatrix, 1, 2);
}
function applyBrightness(buffer, brightness = 0, cellMask) {
if (brightness === 0)
return;
const b = brightness;
const matrix = new Float32Array([
1,
0,
0,
b,
0,
1,
0,
b,
0,
0,
1,
b,
0,
0,
0,
1
]);
if (!cellMask || cellMask.length === 0) {
buffer.colorMatrixUniform(matrix, 1, 3);
} else {
buffer.colorMatrix(matrix, cellMask, 1, 3);
}
}
function applyGain(buffer, gain = 1, cellMask) {
if (gain === 1)
return;
const g = Math.max(0, gain);
const matrix = new Float32Array([
g,
0,
0,
0,
0,
g,
0,
0,
0,
0,
g,
0,
0,
0,
0,
1
]);
if (!cellMask || cellMask.length === 0) {
buffer.colorMatrixUniform(matrix, 1, 3);
} else {
buffer.colorMatrix(matrix, cellMask, 1, 3);
}
}
function createSaturationMatrix(saturation) {
const s = Math.max(0, saturation);
const sr = 0.299 * (1 - s);
const sg = 0.587 * (1 - s);
const sb = 0.114 * (1 - s);
const m00 = sr + s;
const m01 = sg;
const m02 = sb;
const m10 = sr;
const m11 = sg + s;
const m12 = sb;
const m20 = sr;
const m21 = sg;
const m22 = sb + s;
return new Float32Array([
m00,
m01,
m02,
0,
m10,
m11,
m12,
0,
m20,
m21,
m22,
0,
0,
0,
0,
1
]);
}
function applySaturation(buffer, cellMask, strength = 1) {
if (strength === 1 || strength === 0) {
return;
}
const matrix = createSaturationMatrix(strength);
if (!cellMask || cellMask.length === 0) {
buffer.colorMatrixUniform(matrix, 1, 3);
} else {
buffer.colorMatrix(matrix, cellMask, 1, 3);
}
}
class BloomEffect {
_threshold;
_strength;
_radius;
constructor(threshold = 0.8, strength = 0.2, radius = 2) {
this._threshold = Math.max(0, Math.min(1, threshold));
this._strength = Math.max(0, strength);
this._radius = Math.max(0, Math.round(radius));
}
set threshold(newThreshold) {
this._threshold = Math.max(0, Math.min(1, newThreshold));
}
get threshold() {
return this._threshold;
}
set strength(newStrength) {
this._strength = Math.max(0, newStrength);
}
get strength() {
return this._strength;
}
set radius(newRadius) {
this._radius = Math.max(0, Math.round(newRadius));
}
get radius() {
return this._radius;
}
apply(buffer) {
const threshold = this._threshold;
const strength = this._strength;
const radius = this._radius;
if (strength <= 0 || radius <= 0)
return;
const width = buffer.width;
const height = buffer.height;
const srcFg = Uint16Array.from(buffer.buffers.fg);
const srcBg = Uint16Array.from(buffer.buffers.bg);
const destFg = buffer.buffers.fg;
const destBg = buffer.buffers.bg;
const brightPixels = [];
for (let y = 0;y < height; y++) {
for (let x = 0;x < width; x++) {
const index = (y * width + x) * 4;
const fgLum = 0.299 * channel2(srcFg, index) + 0.587 * channel2(srcFg, index + 1) + 0.114 * channel2(srcFg, index + 2);
const bgLum = 0.299 * channel2(srcBg, index) + 0.587 * channel2(srcBg, index + 1) + 0.114 * channel2(srcBg, index + 2);
const lum = Math.max(fgLum, bgLum);
if (lum > threshold) {
const intensity = (lum - threshold) / (1 - threshold + 0.000001);
brightPixels.push({ x, y, intensity: Math.max(0, intensity) });
}
}
}
if (brightPixels.length === 0)
return;
destFg.set(srcFg);
destBg.set(srcBg);
for (const bright of brightPixels) {
for (let ky = -radius;ky <= radius; ky++) {
for (let kx = -radius;kx <= radius; kx++) {
if (kx === 0 && ky === 0)
continue;
const sampleX = bright.x + kx;
const sampleY = bright.y + ky;
if (sampleX >= 0 && sampleX < width && sampleY >= 0 && sampleY < height) {
const distSq = kx * kx + ky * ky;
const radiusSq = radius * radius;
if (distSq <= radiusSq) {
const falloff = 1 - distSq / radiusSq;
const bloomAmount = bright.intensity * strength * falloff;
const destIndex = (sampleY * width + sampleX) * 4;
setRgb2(destFg, destIndex, Math.min(1, channel2(destFg, destIndex) + bloomAmount), Math.min(1, channel2(destFg, destIndex + 1) + bloomAmount), Math.min(1, channel2(destFg, destIndex + 2) + bloomAmount));
setRgb2(destBg, destIndex, Math.min(1, channel2(destBg, destIndex) + bloomAmount), Math.min(1, channel2(destBg, destIndex + 1) + bloomAmount), Math.min(1, channel2(destBg, destIndex + 2) + bloomAmount));
}
}
}
}
}
}
}
// src/post/matrices.ts
var SEPIA_MATRIX = new Float32Array([
0.393,
0.769,
0.189,
0,
0.349,
0.686,
0.168,
0,
0.272,
0.534,
0.131,
0,
0,
0,
0,
1
]);
var PROTANOPIA_SIM_MATRIX = new Float32Array([
0.567,
0.433,
0,
0,
0.558,
0.442,
0,
0,
0,
0.242,
0.758,
0,
0,
0,
0,
1
]);
var DEUTERANOPIA_SIM_MATRIX = new Float32Array([
0.625,
0.375,
0,
0,
0.7,
0.3,
0,
0,
0,
0.3,
0.7,
0,
0,
0,
0,
1
]);
var TRITANOPIA_SIM_MATRIX = new Float32Array([
0.95,
0.05,
0,
0,
0,
0.433,
0.567,
0,
0,
0.475,
0.525,
0,
0,
0,
0,
1
]);
var ACHROMATOPSIA_MATRIX = new Float32Array([
0.299,
0.587,
0.114,
0,
0.299,
0.587,
0.114,
0,
0.299,
0.587,
0.114,
0,
0,
0,
0,
1
]);
var PROTANOPIA_COMP_MATRIX = new Float32Array([
1,
0.2,
0,
0,
0,
0.9,
0.1,
0,
0,
0.1,
0.9,
0,
0,
0,
0,
1
]);
var DEUTERANOPIA_COMP_MATRIX = new Float32Array([
0.9,
0.1,
0,
0,
0.2,
0.8,
0,
0,
0,
0,
1,
0,
0,
0,
0,
1
]);
var TRITANOPIA_COMP_MATRIX = new Float32Array([
1,
0,
0,
0,
0,
0.9,
0.1,
0,
0.1,
0,
0.9,
0,
0,
0,
0,
1
]);
var TECHNICOLOR_MATRIX = new Float32Array([
1.5,
-0.2,
-0.3,
0,
-0.3,
1.4,
-0.1,
0,
-0.2,
-0.2,
1.4,
0,
0,
0,
0,
1
]);
var SOLARIZATION_MATRIX = new Float32Array([
-0.5,
0.5,
0.5,
0,
0.5,
-0.5,
0.5,
0,
0.5,
0.5,
-0.5,
0,
0,
0,
0,
1
]);
var SYNTHWAVE_MATRIX = new Float32Array([
1,
0,
0.25,
0,
0.1,
0.1,
0.1,
0,
0.25,
0,
1,
0,
0,
0,
0,
1
]);
var GREENSCALE_MATRIX = new Float32Array([
0,
0,
0,
0,
0.299,
0.587,
0.114,
0,
0,
0,
0,
0,
0,
0,
0,
1
]);
var GRAYSCALE_MATRIX = new Float32Array([
0.299,
0.587,
0.114,
0,
0.299,
0.587,
0.114,
0,
0.299,
0.587,
0.114,
0,
0,
0,
0,
1
]);
var INVERT_MATRIX = new Float32Array([
-1,
0,
0,
1,
0,
-1,
0,
1,
0,
0,
-1,
1,
0,
0,
0,
1
]);
// src/animation/Timeline.ts
var easingFunctions = {
linear: (t2) => t2,
inQuad: (t2) => t2 * t2,
outQuad: (t2) => t2 * (2 - t2),
inOutQuad: (t2) => t2 < 0.5 ? 2 * t2 * t2 : -1 + (4 - 2 * t2) * t2,
inExpo: (t2) => t2 === 0 ? 0 : Math.pow(2, 10 * (t2 - 1)),
outExpo: (t2) => t2 === 1 ? 1 : 1 - Math.pow(2, -10 * t2),
inOutSine: (t2) => -(Math.cos(Math.PI * t2) - 1) / 2,
outBounce: (t2) => {
const n1 = 7.5625;
const d1 = 2.75;
if (t2 < 1 / d1) {
return n1 * t2 * t2;
} else if (t2 < 2 / d1) {
return n1 * (t2 -= 1.5 / d1) * t2 + 0.75;
} else if (t2 < 2.5 / d1) {
return n1 * (t2 -= 2.25 / d1) * t2 + 0.9375;
} else {
return n1 * (t2 -= 2.625 / d1) * t2 + 0.984375;
}
},
outElastic: (t2) => {
const c4 = 2 * Math.PI / 3;
return t2 === 0 ? 0 : t2 === 1 ? 1 : Math.pow(2, -10 * t2) * Math.sin((t2 * 10 - 0.75) * c4) + 1;
},
inBounce: (t2) => 1 - easingFunctions.outBounce(1 - t2),
inCirc: (t2) => 1 - Math.sqrt(1 - t2 * t2),
outCirc: (t2) => Math.sqrt(1 - Math.pow(t2 - 1, 2)),
inOutCirc: (t2) => {
if ((t2 *= 2) < 1)
return -0.5 * (Math.sqrt(1 - t2 * t2) - 1);
return 0.5 * (Math.sqrt(1 - (t2 -= 2) * t2) + 1);
},
inBack: (t2, s = 1.70158) => t2 * t2 * ((s + 1) * t2 - s),
outBack: (t2, s = 1.70158) => --t2 * t2 * ((s + 1) * t2 + s) + 1,
inOutBack: (t2, s = 1.70158) => {
s *= 1.525;
if ((t2 *= 2) < 1)
return 0.5 * (t2 * t2 * ((s + 1) * t2 - s));
return 0.5 * ((t2 -= 2) * t2 * ((s + 1) * t2 + s) + 2);
}
};
function captureInitialValues(item) {
if (!item.properties)
return;
if (!item.initialValues || item.initialValues.length === 0) {
const initialValues = [];
for (let i = 0;i < item.target.length; i++) {
const target = item.target[i];
const targetInitialValues = {};
for (const key of Object.keys(item.properties)) {
if (typeof target[key] === "number") {
targetInitialValues[key] = target[key];
}
}
initialValues.push(targetInitialValues);
}
item.initialValues = initialValues;
}
}
function applyAnimationAtProgress(item, progress, reversed, timelineTime, deltaTime = 0) {
if (!item.properties || !item.initialValues)
return;
const easingFn = easingFunctions[item.ease || "linear"] || easingFunctions.linear;
const easedProgress = easingFn(Math.max(0, Math.min(1, progress)));
const finalProgress = reversed ? 1 - easedProgress : easedProgress;
for (let i = 0;i < item.target.length; i++) {
const target = item.target[i];
const targetInitialValues = item.initialValues[i];
if (!targetInitialValues)
continue;
for (const [key, endValue] of Object.entries(item.properties)) {
const startValue = targetInitialValues[key];
const newValue = startValue + (endValue - startValue) * finalProgress;
target[key] = newValue;
}
}
if (item.onUpdate) {
const animation = {
targets: item.target,
progress: easedProgress,
currentTime: timelineTime,
deltaTime
};
item.onUpdate(animation);
}
}
function evaluateAnimation(item, timelineTime, deltaTime = 0) {
if (timelineTime < item.startTime) {
return;
}
const animationTime = timelineTime - item.startTime;
const duration = item.duration || 0;
if (timelineTime >= item.startTime && !item.started) {
captureInitialValues(item);
if (item.onStart) {
item.onStart();
}
item.started = true;
}
if (duration === 0) {
if (!item.completed) {
applyAnimationAtProgress(item, 1, false, timelineTime, deltaTime);
if (item.onComplete) {
item.onComplete();
}
item.completed = true;
}
return;
}
const maxLoops = !item.loop || item.loop === 1 ? 1 : typeof item.loop === "number" ? item.loop : Infinity;
const loopDelay = item.loopDelay || 0;
const cycleTime = duration + loopDelay;
let currentCycle = Math.floor(animationTime / cycleTime);
let timeInCycle = animationTime % cycleTime;
if (item.onLoop && item.currentLoop !== undefined && currentCycle > item.currentLoop && currentCycle < maxLoops) {
item.onLoop();
}
item.currentLoop = currentCycle;
if (item.onComplete && !item.completed && currentCycle === maxLoops - 1 && timeInCycle >= duration) {
const finalLoopReversed = (item.alternate || false) && currentCycle % 2 === 1;
applyAnimationAtProgress(item, 1, finalLoopReversed, timelineTime, deltaTime);
item.onComplete();
item.completed = true;
return;
}
if (currentCycle >= maxLoops) {
if (!item.completed) {
const finalReversed = (item.alternate || false) && (maxLoops - 1) % 2 === 1;
applyAnimationAtProgress(item, 1, finalReversed, timelineTime, deltaTime);
if (item.onComplete) {
item.onComplete();
}
item.completed = true;
}
return;
}
if (timeInCycle === 0 && animationTime > 0 && currentCycle < maxLoops) {
currentCycle = currentCycle - 1;
timeInCycle = cycleTime;
}
if (timeInCycle >= duration) {
const isReversed2 = (item.alternate || false) && currentCycle % 2 === 1;
applyAnimationAtProgress(item, 1, isReversed2, timelineTime, deltaTime);
return;
}
const progress = timeInCycle / duration;
const isReversed = (item.alternate || false) && currentCycle % 2 === 1;
applyAnimationAtProgress(item, progress, isReversed, timelineTime, deltaTime);
}
function evaluateCallback(item, timelineTime) {
if (!item.executed && timelineTime >= item.startTime && item.callback) {
item.callback();
item.executed = true;
}
}
function evaluateTimelineSync(item, timelineTime, deltaTime = 0) {
if (!item.timeline)
return;
if (timelineTime < item.startTime) {
return;
}
if (!item.timelineStarted) {
item.timelineStarted = true;
item.timeline.play();
const overshoot = timelineTime - item.startTime;
item.timeline.update(overshoot);
return;
}
item.timeline.update(deltaTime);
}
function evaluateItem(item, timelineTime, deltaTime = 0) {
if (item.type === "animation") {
evaluateAnimation(item, timelineTime, deltaTime);
} else if (item.type === "callback") {
evaluateCallback(item, timelineTime);
}
}
class Timeline {
items = [];
subTimelines = [];
currentTime = 0;
isPlaying = false;
isComplete = false;
duration;
loop;
synced = false;
autoplay;
onComplete;
onPause;
stateChangeListeners = [];
constructor(options = {}) {
this.duration = options.duration || 1000;
this.loop = options.loop === true;
this.autoplay = options.autoplay !== false;
this.onComplete = options.onComplete;
this.onPause = options.onPause;
}
addStateChangeListener(listener) {
this.stateChangeListeners.push(listener);
}
removeStateChangeListener(listener) {
this.stateChangeListeners = this.stateChangeListeners.filter((l) => l !== listener);
}
notifyStateChange() {
for (const listener of this.stateChangeListeners) {
listener(this);
}
}
add(target, properties, startTime = 0) {
const resolvedStartTime = typeof startTime === "string" ? 0 : startTime;
const animationProperties = {};
for (const key in properties) {
if (!["duration", "ease", "onUpdate", "onComplete", "onStart", "onLoop", "loop", "loopDelay", "alternate"].includes(key)) {
if (typeof properties[key] === "number") {
animationProperties[key] = properties[key];
}
}
}
this.items.push({
type: "animation",
startTime: resolvedStartTime,
target: Array.isArray(target) ? target : [target],
properties: animationProperties,
initialValues: [],
duration: properties.duration !== undefined ? properties.duration : 1000,
ease: properties.ease || "linear",
loop: properties.loop,
loopDelay: properties.loopDelay || 0,
alternate: properties.alternate || false,
onUpdate: properties.onUpdate,
onComplete: properties.onComplete,
onStart: properties.onStart,
onLoop: properties.onLoop,
completed: false,
started: false,
currentLoop: 0,
once: properties.once ?? false
});
return this;
}
once(target, properties) {
this.add(target, {
...properties,
once: true
}, this.currentTime);
return this;
}
call(callback, startTime = 0) {
const resolvedStartTime = typeof startTime === "string" ? 0 : startTime;
this.items.push({
type: "callback",
startTime: resolvedStartTime,
callback,
executed: false
});
return this;
}
sync(timeline, startTime = 0) {
if (timeline.synced) {
throw new Error("Timeline already synced");
}
this.subTimelines.push({
type: "timeline",
startTime,
timeline
});
timeline.synced = true;
return this;
}
play() {
if (this.isComplete) {
return this.restart();
}
this.subTimelines.forEach((subTimeline) => {
if (subTimeline.timelineStarted) {
subTimeline.timeline.play();
}
});
this.isPlaying = true;
this.notifyStateChange();
return this;
}
pause() {
this.subTimelines.forEach((subTimeline) => {
subTimeline.timeline.pause();
});
this.isPlaying = false;
if (this.onPause) {
this.onPause();
}
this.notifyStateChange();
return this;
}
resetItems() {
this.items.forEach((item) => {
if (item.type === "callback") {
item.executed = false;
} else if (item.type === "animation") {
item.completed = false;
item.started = false;
item.currentLoop = 0;
}
});
this.subTimelines.forEach((subTimeline) => {
subTimeline.timelineStarted = false;
if (subTimeline.timeline) {
subTimeline.timeline.restart();
subTimeline.timeline.pause();
}
});
}
restart() {
this.isComplete = false;
this.currentTime = 0;
this.isPlaying = true;
this.resetItems();
this.notifyStateChange();
return this;
}
update(deltaTime) {
for (const subTimeline of this.subTimelines) {
evaluateTimelineSync(subTimeline, this.currentTime + deltaTime, deltaTime);
}
if (!this.isPlaying)
return;
this.currentTime += deltaTime;
for (const item of this.items) {
evaluateItem(item, this.currentTime, deltaTime);
}
for (let i = this.items.length - 1;i >= 0; i--) {
const item = this.items[i];
if (item.type === "animation" && item.once && item.completed) {
this.items.splice(i, 1);
}
}
if (this.loop && this.currentTime >= this.duration) {
const overshoot = this.currentTime % this.duration;
this.resetItems();
this.currentTime = 0;
if (overshoot > 0) {
this.update(overshoot);
}
} else if (!this.loop && this.currentTime >= this.duration) {
this.currentTime = this.duration;
this.isPlaying = false;
this.isComplete = true;
if (this.onComplete) {
this.onComplete();
}
this.notifyStateChange();
}
}
}
class TimelineEngine {
timelines = new Set;
renderer = null;
frameCallback = null;
isLive = false;
defaults = {
frameRate: 60
};
attach(renderer) {
if (this.renderer) {
this.detach();
}
this.renderer = renderer;
this.frameCallback = async (deltaTime) => {
this.update(deltaTime);
};
renderer.setFrameCallback(this.frameCallback);
}
detach() {
if (this.renderer && this.frameCallback) {
this.renderer.removeFrameCallback(this.frameCallback);
if (this.isLive) {
this.renderer.dropLive();
this.isLive = false;
}
}
this.renderer = null;
this.frameCallback = null;
}
updateLiveState() {
if (!this.renderer)
return;
const hasRunningTimelines = Array.from(this.timelines).some((timeline) => !timeline.synced && timeline.isPlaying && !timeline.isComplete);
if (hasRunningTimelines && !this.isLive) {
this.renderer.requestLive();
this.isLive = true;
} else if (!hasRunningTimelines && this.isLive) {
this.renderer.dropLive();
this.isLive = false;
}
}
onTimelineStateChange = (timeline) => {
this.updateLiveState();
};
register(timeline) {
if (!this.timelines.has(timeline)) {
this.timelines.add(timeline);
timeline.addStateChangeListener(this.onTimelineStateChange);
this.updateLiveState();
}
}
unregister(timeline) {
if (this.timelines.has(timeline)) {
this.timelines.delete(timeline);
timeline.removeStateChangeListener(this.onTimelineStateChange);
this.updateLiveState();
}
}
clear() {
for (const timeline of this.timelines) {
timeline.removeStateChangeListener(this.onTimelineStateChange);
}
this.timelines.clear();
this.updateLiveState();
}
update(deltaTime) {
for (const timeline of this.timelines) {
if (!timeline.synced) {
timeline.update(deltaTime);
}
}
}
}
var engine = new TimelineEngine;
function createTimeline(options = {}) {
const timeline = new Timeline(options);
if (options.autoplay !== false) {
timeline.play();
}
engine.register(timeline);
return timeline;
}
// src/plugins/registry.ts
var noop = () => {};
var DEFAULT_DEBUG_PLUGIN_ERRORS = false;
var DEFAULT_MAX_PLUGIN_ERRORS = 100;
function normalizeError(error) {
if (error instanceof Error) {
return error;
}
if (typeof error === "string") {
return new Error(error);
}
return new Error(`Unknown plugin error: ${String(error)}`);
}
class SlotRegistry {
plugins = [];
sortedPluginsCache = null;
listeners = new Set;
errorListeners = new Set;
pluginErrors = [];
registrationOrder = 0;
batchDepth = 0;
batchedNotify = false;
rendererInstance;
hostContext;
options;
constructor(renderer, context, options = {}) {
this.rendererInstance = renderer;
this.hostContext = context;
this.options = {
debugPluginErrors: options.debugPluginErrors ?? DEFAULT_DEBUG_PLUGIN_ERRORS,
maxPluginErrors: options.maxPluginErrors ?? DEFAULT_MAX_PLUGIN_ERRORS,
onPluginError: options.onPluginError
};
}
get renderer() {
return this.rendererInstance;
}
get context() {
return this.hostContext;
}
configure(options) {
if ("debugPluginErrors" in options) {
this.options.debugPluginErrors = options.debugPluginErrors ?? DEFAULT_DEBUG_PLUGIN_ERRORS;
}
if ("maxPluginErrors" in options) {
this.options.maxPluginErrors = options.maxPluginErrors ?? DEFAULT_MAX_PLUGIN_ERRORS;
}
if ("onPluginError" in options) {
this.options.onPluginError = options.onPluginError;
}
}
register(plugin) {
if (this.plugins.some((entry) => entry.plugin.id === plugin.id)) {
throw new Error(`Plugin with id "${plugin.id}" is already registered`);
}
try {
plugin.setup?.(this.hostContext, this.rendererInstance);
} catch (error) {
this.reportPluginError({
pluginId: plugin.id,
phase: "setup",
source: "registry",
error
});
return noop;
}
this.plugins.push({
plugin,
registrationOrder: this.registrationOrder++,
cachedOrder: plugin.order ?? 0,
cachedId: plugin.id
});
this.invalidateSortedPluginsCache();
this.notifyListeners();
return () => {
this.unregister(plugin.id);
};
}
unregister(id) {
const index = this.plugins.findIndex((entry2) => entry2.plugin.id === id);
if (index === -1) {
return false;
}
const [entry] = this.plugins.splice(index, 1);
this.invalidateSortedPluginsCache();
try {
entry?.plugin.dispose?.();
} catch (error) {
this.reportPluginError({
pluginId: id,
phase: "dispose",
source: "registry",
error
});
}
this.notifyListeners();
return true;
}
updateOrder(id, order) {
const entry = this.plugins.find((pluginEntry) => pluginEntry.plugin.id === id);
if (!entry) {
return false;
}
if ((entry.plugin.order ?? 0) === order) {
return true;
}
entry.plugin.order = order;
entry.cachedOrder = order;
this.invalidateSortedPluginsCache();
this.notifyListeners();
return true;
}
clear() {
if (this.plugins.length === 0) {
return;
}
const plugins = [...this.plugins];
this.plugins = [];
this.invalidateSortedPluginsCache();
for (const entry of plugins) {
try {
entry.plugin.dispose?.();
} catch (error) {
this.reportPluginError({
pluginId: entry.plugin.id,
phase: "dispose",
source: "registry",
error
});
}
}
this.notifyListeners();
}
subscribe(listener) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
onPluginError(listener) {
this.errorListeners.add(listener);
return () => {
this.errorListeners.delete(listener);
};
}
batch(run) {
this.batchDepth += 1;
try {
return run();
} finally {
this.batchDepth -= 1;
if (this.batchDepth === 0 && this.batchedNotify) {
this.batchedNotify = false;
this.flushListeners();
}
}
}
getPluginErrors() {
return this.pluginErrors;
}
clearPluginErrors() {
this.pluginErrors = [];
}
reportPluginError(report) {
const event = {
pluginId: report.pluginId,
slot: report.slot,
phase: report.phase,
source: report.source ?? "registry",
error: normalizeError(report.error),
timestamp: Date.now()
};
this.pluginErrors.push(event);
if (this.pluginErrors.length > this.options.maxPluginErrors) {
this.pluginErrors.splice(0, this.pluginErrors.length - this.options.maxPluginErrors);
}
if (this.options.debugPluginErrors) {
const slotLabel = event.slot ? ` slot="${event.slot}"` : "";
console.debug(`[SlotRegistry][PluginError] plugin="${event.pluginId}" phase="${event.phase}" source="${event.source}"${slotLabel}`);
console.debug(event.error);
}
for (const listener of this.errorListeners) {
try {
listener(event);
} catch (error) {
console.error("Error in plugin error listener:", error);
}
}
try {
this.options.onPluginError?.(event);
} catch (error) {
console.error("Error in plugin error callback:", error);
}
return event;
}
resolve(slot) {
return this.resolveEntries(slot).map((entry) => entry.renderer);
}
resolveEntries(slot) {
const slotRenderers = [];
for (const entry of this.getSortedPlugins()) {
const renderer = entry.plugin.slots[slot];
if (renderer) {
slotRenderers.push({
id: entry.plugin.id,
renderer
});
}
}
return slotRenderers;
}
getSortedPlugins() {
this.syncPluginSortMetadata();
if (this.sortedPluginsCache) {
return this.sortedPluginsCache;
}
this.sortedPluginsCache = [...this.plugins].sort((left, right) => {
const leftOrder = left.cachedOrder;
const rightOrder = right.cachedOrder;
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
if (left.registrationOrder !== right.registrationOrder) {
return left.registrationOrder - right.registrationOrder;
}
return left.cachedId.localeCompare(right.cachedId);
});
return this.sortedPluginsCache;
}
syncPluginSortMetadata() {
let hasChanges = false;
for (const entry of this.plugins) {
const nextOrder = entry.plugin.order ?? 0;
const nextId = entry.plugin.id;
if (entry.cachedOrder !== nextOrder || entry.cachedId !== nextId) {
entry.cachedOrder = nextOrder;
entry.cachedId = nextId;
hasChanges = true;
}
}
if (hasChanges) {
this.invalidateSortedPluginsCache();
}
}
invalidateSortedPluginsCache() {
this.sortedPluginsCache = null;
}
notifyListeners() {
if (this.batchDepth > 0) {
this.batchedNotify = true;
return;
}
this.flushListeners();
}
flushListeners() {
for (const listener of this.listeners) {
try {
listener();
} catch (error) {
console.error("Error in slot registry listener:", error);
}
}
}
}
var slotRegistriesByRenderer = new WeakMap;
function getSlotRegistryStore(renderer) {
const existingStore = slotRegistriesByRenderer.get(renderer);
if (existingStore) {
return existingStore;
}
const createdStore = new Map;
slotRegistriesByRenderer.set(renderer, createdStore);
renderer.once("destroy", () => {
for (const registry of createdStore.values()) {
try {
registry.clear();
} catch (error) {
console.error("Error disposing slot registry:", error);
}
}
createdStore.clear();
slotRegistriesByRenderer.delete(renderer);
});
return createdStore;
}
function createSlotRegistry(renderer, key, context, options = {}) {
const store = getSlotRegistryStore(renderer);
const existing = store.get(key);
if (existing) {
if (existing.context !== context) {
throw new Error(`createSlotRegistry called with a different context for renderer key "${key}". Reuse the original context object.`);
}
const typedExisting = existing;
typedExisting.configure(options);
return typedExisting;
}
const created = new SlotRegistry(renderer, context, options);
store.set(key, created);
return created;
}
// src/plugins/core-slot.ts
function isCoreManagedSlot(contribution) {
return typeof contribution === "object" && contribution !== null && "render" in contribution;
}
function toCorePlugin(plugin) {
const slots = {};
for (const [slotName, contribution] of Object.entries(plugin.slots)) {
const wrappedRenderer = (ctx, data) => {
if (isCoreManagedSlot(contribution)) {
return contribution.render(ctx, data);
}
return contribution(ctx, data);
};
if (isCoreManagedSlot(contribution)) {
wrappedRenderer.__coreSlotOwnership = "plugin";
wrappedRenderer.__coreManagedSlot = contribution;
} else {
wrappedRenderer.__coreSlotOwnership = "host";
}
slots[slotName] = wrappedRenderer;
}
return {
id: plugin.id,
order: plugin.order,
setup: plugin.setup,
dispose: plugin.dispose,
slots
};
}
function asArray(value) {
if (!value) {
return [];
}
return Array.isArray(value) ? [...value] : [value];
}
function ensureValidNode(node, pluginId, mount) {
if (!node) {
throw new Error(`Plugin "${pluginId}" did not return a renderable node`);
}
if (typeof node.then === "function") {
throw new Error(`Plugin "${pluginId}" returned an async value. Core slots require synchronous renderers.`);
}
if (!(node instanceof BaseRenderable)) {
throw new Error(`Plugin "${pluginId}" must return a BaseRenderable`);
}
if (node === mount) {
throw new Error(`Plugin "${pluginId}" returned the slot mount container as its node`);
}
if (node.parent && node.parent !== mount) {
throw new Error(`Plugin "${pluginId}" returned a renderable already attached to another parent`);
}
}
function createCoreSlotRegistry(renderer, context, options = {}) {
return createSlotRegistry(renderer, "core:slot-registry", context, options);
}
function registerCorePlugin(registry, plugin) {
return registry.register(toCorePlugin(plugin));
}
function resolveCoreSlot(registry, slot) {
return resolveCoreSlotEntries(registry, slot).map((entry) => {
return {
id: entry.id,
renderer: entry.renderer
};
});
}
function resolveCoreSlotEntries(registry, slot) {
return registry.resolveEntries(slot).map((entry) => {
const wrappedRenderer = entry.renderer;
return {
id: entry.id,
renderer: (ctx, data) => wrappedRenderer(ctx, data),
ownership: wrappedRenderer.__coreSlotOwnership ?? "host",
managedSlot: wrappedRenderer.__coreManagedSlot
};
});
}
class SlotRenderable extends Renderable {
_mode;
_slotRegistry;
_slotName;
_data;
_fallbackOption;
_pluginFailurePlaceholder;
_disposed = false;
_mountedNodes = [];
_pluginNodes = new Map;
_activePluginIds = new Set;
_fallbackNodes = null;
_unsubscribe = null;
constructor(ctx, options) {
super(ctx, options);
this._slotRegistry = options.registry;
this._slotName = options.name;
this._data = options.data ?? {};
this._mode = options.mode ?? "append";
this._fallbackOption = options.fallback;
this._pluginFailurePlaceholder = options.pluginFailurePlaceholder;
this._unsubscribe = this._slotRegistry.subscribe(() => this.refresh());
try {
this.refresh();
} catch (error) {
this._cleanupAll();
throw error;
}
}
get mode() {
return this._mode;
}
set mode(value) {
this._mode = value;
this.refresh();
}
get data() {
return this._data;
}
set data(value) {
this._data = value;
this.refresh();
}
refresh() {
if (this._disposed) {
return;
}
const allEntries = resolveCoreSlotEntries(this._slotRegistry, this._slotName);
const activeEntries = this._mode === "single_winner" && allEntries.length > 0 ? [allEntries[0]] : allEntries;
const nextActivePluginIds = new Set(activeEntries.map((entry) => entry.id));
const registeredPluginIds = new Set(allEntries.map((entry) => entry.id));
this._cleanupInactivePluginNodes(nextActivePluginIds, registeredPluginIds);
for (const entry of activeEntries) {
let state = this._pluginNodes.get(entry.id);
const shouldRender = !state || state.ownership === "plugin" && state.nodes.length === 0 || state.dataRef !== this._data;
if (shouldRender) {
const previousState = state;
try {
const node = entry.renderer(this._slotRegistry.context, this._data);
ensureValidNode(node, entry.id, this);
state = {
nodes: [node],
ownership: entry.ownership,
managedSlot: entry.managedSlot ?? state?.managedSlot,
dataRef: this._data
};
} catch (error) {
const failure = this._slotRegistry.reportPluginError({
pluginId: entry.id,
slot: String(this._slotName),
phase: "render",
source: "core",
error
});
state = {
nodes: this._resolvePluginFailurePlaceholder(failure),
ownership: "host",
managedSlot: entry.managedSlot ?? state?.managedSlot,
dataRef: this._data
};
}
if (previousState) {
this._cleanupReplacedPluginNodes(previousState, state.nodes);
}
this._pluginNodes.set(entry.id, state);
}
if (!this._activePluginIds.has(entry.id)) {
const activeState = this._pluginNodes.get(entry.id);
if (activeState) {
this._callManagedHook(entry.id, activeState.managedSlot, "onActivate", "setup");
}
}
}
const desiredNodes = [];
if (this._mode === "append" || activeEntries.length === 0) {
desiredNodes.push(...this._ensureFallbackNodes());
}
for (const entry of activeEntries) {
const state = this._pluginNodes.get(entry.id);
if (state) {
desiredNodes.push(...state.nodes);
}
}
if (this._mode !== "append" && desiredNodes.length === 0) {
desiredNodes.push(...this._ensureFallbackNodes());
}
this._reconcileMountedNodes(desiredNodes);
this._activePluginIds = nextActivePluginIds;
}
destroySelf() {
this._cleanupAll();
}
_cleanupAll() {
if (this._disposed) {
return;
}
this._disposed = true;
this._unsubscribe?.();
this._unsubscribe = null;
for (const [pluginId, state] of this._pluginNodes) {
if (this._activePluginIds.has(pluginId)) {
this._callManagedHook(pluginId, state.managedSlot, "onDeactivate", "dispose");
}
this._callManagedHook(pluginId, state.managedSlot, "onDispose", "dispose");
for (const node of state.nodes) {
this._detachNodeFromMount(node);
}
if (state.ownership === "host") {
for (const node of state.nodes) {
node.destroyRecursively();
}
}
}
this._pluginNodes.clear();
this._activePluginIds = new Set;
if (this._fallbackNodes) {
for (const node of this._fallbackNodes) {
node.destroyRecursively();
}
this._fallbackNodes = null;
}
this._mountedNodes = [];
}
_ensureFallbackNodes() {
if (this._fallbackNodes !== null) {
return this._fallbackNodes;
}
const source = typeof this._fallbackOption === "function" ? this._fallbackOption() : this._fallbackOption;
const nodes = asArray(source);
for (const node of nodes) {
ensureValidNode(node, "fallback", this);
}
this._fallbackNodes = nodes;
return this._fallbackNodes;
}
_callManagedHook(pluginId, managedSlot, hook, phase) {
const callback = managedSlot?.[hook];
if (!callback) {
return;
}
try {
callback(this._slotRegistry.context);
} catch (error) {
this._slotRegistry.reportPluginError({
pluginId,
slot: String(this._slotName),
phase,
source: "core",
error
});
}
}
_detachNodeFromMount(node) {
if (node.parent === this) {
this.remove(node);
}
}
_cleanupInactivePluginNodes(nextActivePluginIds, registeredPluginIds) {
for (const [pluginId, state] of this._pluginNodes) {
if (nextActivePluginIds.has(pluginId)) {
continue;
}
if (this._activePluginIds.has(pluginId)) {
this._callManagedHook(pluginId, state.managedSlot, "onDeactivate", "dispose");
}
for (const node of state.nodes) {
this._detachNodeFromMount(node);
}
if (!registeredPluginIds.has(pluginId)) {
this._callManagedHook(pluginId, state.managedSlot, "onDispose", "dispose");
if (state.ownership === "host") {
for (const node of state.nodes) {
node.destroyRecursively();
}
}
this._pluginNodes.delete(pluginId);
continue;
}
if (state.ownership === "host") {
for (const node of state.nodes) {
node.destroyRecursively();
}
this._pluginNodes.delete(pluginId);
continue;
}
state.nodes = [];
}
}
_cleanupReplacedPluginNodes(previousState, nextNodes) {
const retainedNodes = new Set(nextNodes);
for (const node of previousState.nodes) {
if (retainedNodes.has(node)) {
continue;
}
this._detachNodeFromMount(node);
if (previousState.ownership === "host") {
node.destroyRecursively();
}
}
}
_resolvePluginFailurePlaceholder(failure) {
if (!this._pluginFailurePlaceholder) {
return [];
}
try {
const placeholderSource = this._pluginFailurePlaceholder(failure, this._slotRegistry.context);
const placeholderNodes = asArray(placeholderSource);
for (const node of placeholderNodes) {
ensureValidNode(node, `${failure.pluginId}:error-placeholder`, this);
}
return placeholderNodes;
} catch (placeholderError) {
this._slotRegistry.reportPluginError({
pluginId: failure.pluginId,
slot: String(this._slotName),
phase: "error_placeholder",
source: "core",
error: placeholderError
});
return [];
}
}
_reconcileMountedNodes(desiredNodes) {
const desiredNodeSet = new Set(desiredNodes);
for (const node of this._mountedNodes) {
if (!desiredNodeSet.has(node)) {
if (node.parent === this) {
this.remove(node);
}
}
}
for (let index = 0;index < desiredNodes.length; index++) {
const node = desiredNodes[index];
if (node.parent !== this) {
this.add(node, index);
continue;
}
const childAtIndex = this.getChildren()[index];
if (childAtIndex !== node) {
this.add(node, index);
}
}
this._mountedNodes = [...desiredNodes];
}
}
// src/audio.ts
import { EventEmitter } from "events";
import { randomBytes } from "node:crypto";
import { open as openFile, readFile, rename, unlink } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
// src/audio-stream/icy/metadata.ts
function parseIcyMetadata(bytes, decoder) {
const decoded = decoder.decode(bytes);
const nul = decoded.indexOf("\x00");
const text = nul === -1 ? decoded : decoded.slice(0, nul);
if (text.length === 0)
return null;
const fields = Object.create(null);
let found = false;
let offset = 0;
while (offset < text.length) {
const separator = text.indexOf("='", offset);
if (separator === -1)
break;
const key = text.slice(offset, separator);
if (key.length === 0 || /[';=]/.test(key))
break;
const end = text.indexOf("';", separator + 2);
if (end === -1)
break;
Object.defineProperty(fields, key, {
configurable: true,
enumerable: true,
writable: true,
value: text.slice(separator + 2, end)
});
found = true;
offset = end + 2;
}
return found ? Object.freeze(fields) : null;
}
// src/audio-stream/icy/demuxer.ts
var EMPTY_FIELDS = Object.freeze(Object.create(null));
function copyHeaders(headers) {
const copy = Object.create(null);
for (const [name, value] of Object.entries(headers ?? {}))
copy[name.toLowerCase()] = value;
return Object.freeze(copy);
}
function fieldsEqual(left, right) {
const keys = Object.keys(left);
return keys.length === Object.keys(right).length && keys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && left[key] === right[key]);
}
class IcyStreamDemuxer {
initialMetadata;
audioRemaining;
metadata = null;
metadataOffset = 0;
fields = EMPTY_FIELDS;
interval;
decoder;
headers;
constructor(options) {
if (!Number.isSafeInteger(options.metadataInterval) || options.metadataInterval < 0) {
throw new TypeError("metadataInterval must be a non-negative safe integer");
}
this.interval = options.metadataInterval;
try {
this.decoder = new TextDecoder(options.metadataEncoding ?? "iso-8859-1");
} catch {
throw new TypeError(`Unsupported metadataEncoding: ${options.metadataEncoding}`);
}
this.headers = copyHeaders(options.headers);
this.audioRemaining = this.interval;
this.initialMetadata = Object.freeze({ format: "icy", headers: this.headers, fields: this.fields });
}
*push(chunk) {
if (this.interval === 0) {
if (chunk.byteLength > 0)
yield { type: "audio", data: chunk };
return;
}
let offset = 0;
while (offset < chunk.byteLength) {
if (this.audioRemaining > 0) {
const length2 = Math.min(this.audioRemaining, chunk.byteLength - offset);
yield { type: "audio", data: chunk.subarray(offset, offset + length2) };
offset += length2;
this.audioRemaining -= length2;
continue;
}
if (this.metadata == null) {
const metadataLength = (chunk[offset] ?? 0) * 16;
offset += 1;
if (metadataLength === 0) {
this.audioRemaining = this.interval;
continue;
}
this.metadata = new Uint8Array(metadataLength);
this.metadataOffset = 0;
}
const metadata = this.metadata;
const length = Math.min(metadata.byteLength - this.metadataOffset, chunk.byteLength - offset);
metadata.set(chunk.subarray(offset, offset + length), this.metadataOffset);
offset += length;
this.metadataOffset += length;
if (this.metadataOffset !== metadata.byteLength)
continue;
const fields = parseIcyMetadata(metadata, this.decoder);
this.metadata = null;
this.metadataOffset = 0;
this.audioRemaining = this.interval;
if (fields != null && !fieldsEqual(this.fields, fields)) {
this.fields = fields;
yield {
type: "metadata",
metadata: Object.freeze({ format: "icy", headers: this.headers, fields })
};
}
}
}
*flush() {
if (this.interval === 0)
return;
if (this.audioRemaining === 0 || this.metadata != null) {
throw new Error("ICY stream ended inside a metadata block");
}
}
}
function createIcyStreamDemuxer(options) {
return new IcyStreamDemuxer(options);
}
// src/audio-stream/demuxer.ts
function selectAudioStreamDemuxer(options) {
const icyHeaders = Object.create(null);
options.headers.forEach((value2, name) => {
if (name.toLowerCase().startsWith("icy-"))
icyHeaders[name.toLowerCase()] = value2;
});
const headers = Object.freeze(icyHeaders);
const rawInterval = options.headers.get("icy-metaint");
if (rawInterval == null) {
return Object.keys(headers).length === 0 ? null : createIcyStreamDemuxer({ metadataInterval: 0, metadataEncoding: options.metadataEncoding, headers });
}
const value = rawInterval.trim();
if (!/^\d+$/.test(value))
throw new Error(`Invalid icy-metaint response header: ${rawInterval}`);
const interval = Number(value);
if (!Number.isSafeInteger(interval))
throw new Error(`Invalid icy-metaint response header: ${rawInterval}`);
return createIcyStreamDemuxer({ metadataInterval: interval, metadataEncoding: options.metadataEncoding, headers });
}
// src/audio.ts
class AudioInitializationError extends Error {
action;
status;
constructor(action, message, status, cause) {
super(message);
this.name = "AudioInitializationError";
this.action = action;
this.status = status;
if (cause !== undefined)
this.cause = cause;
}
}
class AudioCaptureStreamError extends Error {
context;
constructor(message, context, cause) {
super(message);
this.name = "AudioCaptureStreamError";
this.context = context;
if (cause !== undefined)
this.cause = cause;
}
}
class AudioRecorderError extends Error {
context;
constructor(message, context, cause) {
super(message);
this.name = "AudioRecorderError";
this.context = context;
if (cause !== undefined)
this.cause = cause;
}
}
function statusToError(action, status) {
return new Error(`Audio ${action} failed: ${status}`);
}
function toBytes(data) {
return data instanceof Uint8Array ? data : new Uint8Array(data);
}
var DEFAULT_AUDIO_SAMPLE_RATE = 48000;
var DEFAULT_STREAM_PROBE_BYTES = 1024 * 1024;
var STREAM_POLL_INTERVAL_MS = 5;
var MAX_TIMER_DELAY_MS = 2147483647;
var MAX_U32 = 4294967295;
var DEFAULT_CAPTURE_CHUNK_FRAMES = 2048;
var CAPTURE_DISCARD_BATCH_CHUNKS = 32;
var WAV_HEADER_BYTES = 44;
var MAX_WAV_DATA_BYTES = BigInt(MAX_U32 - 36);
var INVALID_STREAM_CHUNK_MESSAGE = "Audio stream chunks must be Uint8Array instances";
class AudioStreamError extends Error {
context;
constructor(message, context, cause) {
super(message);
this.name = "AudioStreamError";
this.context = context;
if (cause !== undefined)
this.cause = cause;
}
}
class ClassifiedAudioStreamError extends AudioStreamError {
retryable;
retryAfterMs;
constructor(message, context, retryable, retryAfterMs, cause) {
super(message, context, cause);
this.retryable = retryable;
this.retryAfterMs = retryAfterMs;
}
}
function createAbortError() {
return new DOMException("The operation was aborted", "AbortError");
}
var isU32 = (value) => Number.isInteger(value) && value >= 0 && value <= MAX_U32;
function resolvePositiveU32(value, fallback, name) {
const resolved = value ?? fallback;
if (!Number.isFinite(resolved) || !Number.isInteger(resolved) || resolved <= 0) {
throw new TypeError(`${name} must be a finite positive integer`);
}
if (resolved > MAX_U32)
throw new RangeError(`${name} exceeds the supported limit`);
return resolved;
}
function resolveAudioCaptureStreamOptions(options, sampleRate) {
const channels = resolvePositiveU32(options.channels, 1, "channels");
const capacityFrames = resolvePositiveU32(options.capacityFrames, sampleRate, "capacityFrames");
const chunkFrames = resolvePositiveU32(options.chunkFrames, DEFAULT_CAPTURE_CHUNK_FRAMES, "chunkFrames");
if (chunkFrames > capacityFrames)
throw new RangeError("chunkFrames must not exceed capacityFrames");
if (chunkFrames > Math.floor(MAX_U32 / channels)) {
throw new RangeError("chunkFrames * channels exceeds the supported limit");
}
return {
sampleRate,
channels,
capacityFrames,
chunkFrames,
startOptions: options.startOptions,
signal: options.signal
};
}
function resolveU32Index(value, name) {
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
throw new TypeError(`${name} must be a finite non-negative integer`);
}
if (value > MAX_U32)
throw new RangeError(`${name} exceeds the supported limit`);
return value;
}
function resolveReconnectOptions(options) {
const maxRetries = options.maxRetries ?? Number.POSITIVE_INFINITY;
const initialDelayMs = options.initialDelayMs ?? 1000;
const maxDelayMs = options.maxDelayMs ?? 15000;
const backoffFactor = options.backoffFactor ?? 2;
const retryOnEnd = options.retryOnEnd ?? false;
const retry = options.retry;
if (maxRetries !== Number.POSITIVE_INFINITY && (!Number.isInteger(maxRetries) || maxRetries < 0)) {
throw new TypeError("reconnect.maxRetries must be a non-negative integer or Infinity");
}
if (!Number.isFinite(initialDelayMs) || !Number.isInteger(initialDelayMs) || initialDelayMs < 0) {
throw new TypeError("reconnect.initialDelayMs must be a finite non-negative integer");
}
if (!Number.isFinite(maxDelayMs) || !Number.isInteger(maxDelayMs) || maxDelayMs < 0) {
throw new TypeError("reconnect.maxDelayMs must be a finite non-negative integer");
}
if (!Number.isFinite(backoffFactor) || backoffFactor < 1) {
throw new TypeError("reconnect.backoffFactor must be a finite number greater than or equal to 1");
}
if (typeof retryOnEnd !== "boolean") {
throw new TypeError("reconnect.retryOnEnd must be a boolean");
}
if (retry !== undefined && typeof retry !== "function") {
throw new TypeError("reconnect.retry must be a function");
}
return {
maxRetries,
initialDelayMs,
maxDelayMs,
backoffFactor,
retryOnEnd,
retry: retry == null ? undefined : (error, context) => retry.call(options, error, context)
};
}
function resolveAudioStreamOptions(options) {
const format = resolveAudioStreamFormat(options.format);
const capacityMs = resolvePositiveU32(options.buffer?.capacityMs, 2000, "buffer.capacityMs");
const startupMs = resolvePositiveU32(options.buffer?.startupMs, 1000, "buffer.startupMs");
const resumeMs = resolvePositiveU32(options.buffer?.resumeMs, 1000, "buffer.resumeMs");
const maxProbeBytes = resolvePositiveU32(options.maxProbeBytes, DEFAULT_STREAM_PROBE_BYTES, "maxProbeBytes");
if (startupMs > capacityMs)
throw new RangeError("buffer.startupMs must not exceed buffer.capacityMs");
if (resumeMs > capacityMs)
throw new RangeError("buffer.resumeMs must not exceed buffer.capacityMs");
return {
format,
capacityMs,
startupMs,
resumeMs,
volume: options.volume ?? 1,
pan: options.pan ?? 0,
groupId: options.groupId ?? 0,
maxProbeBytes,
signal: options.signal,
reconnect: options.reconnect === undefined ? undefined : resolveReconnectOptions(options.reconnect)
};
}
function resolveAudioStreamConnector(connector) {
const connect = connector?.connect;
if (typeof connect !== "function")
throw new TypeError("Audio stream connector must define connect()");
return { connect: (context) => connect.call(connector, context) };
}
function runBoundedCleanup(cleanup, timeoutMs = 50) {
let result;
try {
result = Promise.resolve(cleanup()).catch(() => {
return;
});
} catch {
return Promise.resolve();
}
return new Promise((resolve) => {
const timer = setTimeout(resolve, timeoutMs);
result.then(() => {
clearTimeout(timer);
resolve();
});
});
}
function waitForDelay(delayMs, signal) {
if (signal.aborted)
return Promise.reject(createAbortError());
return new Promise((resolve, reject) => {
let remainingMs = delayMs;
let timer;
const schedule = () => {
const currentDelayMs = Math.min(remainingMs, MAX_TIMER_DELAY_MS);
timer = setTimeout(() => {
remainingMs -= currentDelayMs;
if (remainingMs > 0)
schedule();
else {
signal.removeEventListener("abort", onAbort);
resolve();
}
}, currentDelayMs);
};
const onAbort = () => {
clearTimeout(timer);
reject(createAbortError());
};
signal.addEventListener("abort", onAbort, { once: true });
schedule();
});
}
function waitForPoll(signal) {
return waitForDelay(STREAM_POLL_INTERVAL_MS, signal).then(() => true).catch(() => false);
}
function parseRetryAfter(value, maxDelayMs) {
if (value == null)
return;
const seconds = Number(value.trim());
if (Number.isFinite(seconds) && seconds >= 0)
return Math.min(maxDelayMs, Math.ceil(seconds * 1000));
const date = Date.parse(value);
if (!Number.isFinite(date))
return;
return Math.min(maxDelayMs, Math.max(0, date - Date.now()));
}
function resolveAudioStreamFormat(value) {
const format = value ?? "mp3";
if (format !== "mp3" && format !== "flac")
throw new TypeError(`Unsupported audio stream format: ${format}`);
return format;
}
function toNativeAudioStreamFormat(format) {
switch (format) {
case "mp3":
return NativeAudioStreamFormat.Mp3;
case "flac":
return NativeAudioStreamFormat.Flac;
}
}
function resolveContentTypePolicy(value, receiver) {
const policy = value ?? "validate";
if (policy !== "validate" && policy !== "ignore" && typeof policy !== "function") {
throw new TypeError("contentTypePolicy must be 'validate', 'ignore', or a function");
}
return typeof policy === "function" ? (context) => policy.call(receiver, context) : policy;
}
function isAllowedContentType(format, value) {
const contentType = value.split(";", 1)[0]?.trim().toLowerCase();
switch (format) {
case "mp3":
return ["audio/mpeg", "audio/mp3", "application/octet-stream", "application/mp3"].includes(contentType ?? "");
case "flac":
return ["audio/flac", "audio/x-flac", "application/octet-stream"].includes(contentType ?? "");
}
}
function createAudioStreamUrlConnector(source, request, format, contentTypePolicy) {
return {
async connect({ signal, attempt }) {
let response;
try {
const headers = new Headers(request?.headers);
if (!headers.has("icy-metadata"))
headers.set("Icy-MetaData", "1");
response = await globalThis.fetch(source, { ...request, headers, signal });
} catch (cause) {
throw new ClassifiedAudioStreamError("Audio stream fetch failed", { action: "fetch", attempt }, true, undefined, cause);
}
if (!response.ok) {
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"), Number.POSITIVE_INFINITY);
await runBoundedCleanup(() => response.body?.cancel());
const retryable = [408, 425, 429].includes(response.status) || response.status >= 500 && response.status <= 599;
throw new ClassifiedAudioStreamError(`Audio stream request failed with HTTP ${response.status}`, { action: "response", status: response.status, attempt }, retryable, retryAfterMs);
}
const contentType = response.headers.get("content-type");
let contentTypeAccepted = contentType == null || contentTypePolicy === "ignore";
if (!contentTypeAccepted && contentTypePolicy === "validate") {
contentTypeAccepted = isAllowedContentType(format, contentType);
} else if (typeof contentTypePolicy === "function") {
try {
const result = contentTypePolicy(Object.freeze({
format,
contentType,
status: response.status,
url: response.url || String(source)
}));
if (typeof result !== "boolean")
throw new TypeError("contentTypePolicy must return a boolean");
contentTypeAccepted = result;
} catch (cause) {
await runBoundedCleanup(() => response.body?.cancel());
throw new ClassifiedAudioStreamError(cause instanceof Error ? cause.message : "Audio stream content type policy failed", { action: "response", status: response.status, attempt }, false, undefined, cause);
}
}
if (!contentTypeAccepted) {
await runBoundedCleanup(() => response.body?.cancel());
throw new ClassifiedAudioStreamError(`Unsupported audio stream Content-Type: ${contentType}`, { action: "response", status: response.status, attempt }, false);
}
if (response.body == null) {
throw new ClassifiedAudioStreamError("Audio stream response has no body", { action: "response", status: response.status, attempt }, true);
}
return {
body: response.body,
info: { headers: response.headers, status: response.status }
};
}
};
}
function resolveMetadataEncoding(value) {
try {
return new TextDecoder(value ?? "iso-8859-1").encoding;
} catch {
throw new TypeError(`Unsupported metadataEncoding: ${value}`);
}
}
function resolveAudioStreamRequest(request) {
if (request === undefined)
return;
const { body: _body, signal: _signal, ...safeRequest } = request;
return safeRequest;
}
function isReadableStreamSource(source) {
try {
return typeof source?.getReader === "function";
} catch {
return false;
}
}
function isAsyncIterableSource(source) {
try {
return typeof source?.[Symbol.asyncIterator] === "function";
} catch {
return false;
}
}
function isUint8Array(value) {
return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === "[object Uint8Array]";
}
var createAudioStream;
var openAudioStream;
class AudioStream extends EventEmitter {
closed;
format;
lib;
engine;
connector;
demuxerFactory;
readAction;
options;
removeFromOwner;
lifecycleController = new AbortController;
nativeStreamId = null;
nativeStats = null;
activeAttempt = null;
pendingCleanup = null;
reconnectAttempts = 0;
consecutiveReconnectAttempts = 0;
disposed = false;
exposed = false;
terminalError = null;
metadata = null;
pendingMetadataEvent = false;
metadataEventScheduled = false;
terminalEventScheduled = false;
setupResolve;
setupReject;
closedResolve;
setupPromise;
overallAbortListener = () => this.dispose();
static {
createAudioStream = (init) => new AudioStream(init);
openAudioStream = (stream) => stream.open();
}
constructor(init) {
super();
this.lib = init.lib;
this.engine = init.engine;
this.connector = init.connector;
this.demuxerFactory = init.demuxer;
this.readAction = init.readAction;
this.options = resolveAudioStreamOptions(init.options);
this.format = this.options.format;
this.removeFromOwner = init.removeFromOwner;
this.setupPromise = new Promise((resolve, reject) => (this.setupResolve = resolve, this.setupReject = reject));
this.closed = new Promise((resolve) => this.closedResolve = resolve);
this.options.signal?.addEventListener("abort", this.overallAbortListener, { once: true });
}
get state() {
if (this.disposed)
return "disposed";
if (this.terminalError != null)
return "errored";
return this.nativeStats == null ? "initializing" : NativeAudioStreamStateNames[this.nativeStats.state] ?? "errored";
}
async open() {
if (this.options.signal?.aborted)
this.dispose();
else
this.runLifecycle();
await this.setupPromise;
if (this.lifecycleController.signal.aborted && this.state !== "ended")
throw this.terminalError ?? createAbortError();
this.exposed = true;
if (this.pendingMetadataEvent && this.metadata != null)
this.emitMetadata();
this.pendingMetadataEvent = false;
if (this.state === "ended")
this.emitTerminal("ended");
}
getStats() {
const stats = this.readNativeStats();
if (!this.lifecycleController.signal.aborted && this.nativeStreamId != null) {
const error = this.snapshotError(stats);
const ended = stats?.state === NativeAudioStreamState.Ended && !this.options.reconnect?.retryOnEnd;
if (error != null || ended) {
queueMicrotask(() => {
if (this.lifecycleController.signal.aborted)
return;
const reason = error == null || error.context.action === "decoder" ? NativeAudioStreamCloseReason.PreserveNativeTerminal : NativeAudioStreamCloseReason.TransportError;
this.finish(reason, error ?? undefined);
});
}
}
return this.toPublicStats();
}
getMetadata() {
return this.metadata;
}
setVolume(volume) {
return this.control("setVolume", (streamId) => this.lib.audioSetStreamVolume(this.engine, streamId, volume));
}
setPan(pan) {
return this.control("setPan", (streamId) => this.lib.audioSetStreamPan(this.engine, streamId, pan));
}
setGroup(groupId) {
if (!isU32(groupId)) {
const context = { action: "setGroup" };
if (this.exposed) {
this.emitAsync("error", new AudioStreamError("Invalid audio stream group", context), context);
}
return false;
}
return this.control("setGroup", (streamId) => this.lib.audioSetStreamGroup(this.engine, streamId, groupId));
}
control(action, call) {
const streamId = this.nativeStreamId;
if (this.disposed || this.lifecycleController.signal.aborted || streamId == null)
return false;
const status = call(streamId);
if (status !== 0) {
const context = { action, status };
if (this.exposed) {
this.emitAsync("error", new AudioStreamError(`Audio stream ${action} failed: ${status}`, context), context);
}
return false;
}
return true;
}
dispose() {
const wasExposed = this.exposed;
if (!this.disposed) {
this.disposed = true;
this.lifecycleController.abort();
this.setupReject(createAbortError());
}
const cleanup = this.stopSource();
const closeStatus = this.closeNativeStream(NativeAudioStreamCloseReason.Disposed);
if (closeStatus !== 0) {
throw new AudioStreamError(`Audio stream destroy failed: ${closeStatus}`, {
action: "destroy",
status: closeStatus
});
}
this.removeOwner();
cleanup.finally(() => {
if (wasExposed && !this.terminalEventScheduled)
this.emitTerminal("disposed");
else if (!this.terminalEventScheduled)
this.closedResolve();
});
}
async runLifecycle() {
while (!this.lifecycleController.signal.aborted) {
const attempt = {
controller: new AbortController,
body: null,
closeConnection: null,
reader: null,
iterator: null,
demuxer: null,
demuxerFinished: false,
sourceReleased: false,
sourceAcquisitionAttempted: false,
connectionClosed: false,
demuxerAborted: false,
resourceAcquisition: null,
cleanupPromise: null
};
this.activeAttempt = attempt;
let connection;
try {
let returnedConnection;
const finishConnectionAcquisition = this.beginResourceAcquisition(attempt);
try {
returnedConnection = await this.connector.connect({
signal: attempt.controller.signal,
attempt: this.consecutiveReconnectAttempts
});
} finally {
finishConnectionAcquisition();
}
connection = this.resolveConnection(returnedConnection, attempt);
} catch (failure) {
const active = this.isAttemptActive(attempt);
await this.stopSource(attempt);
if (!active)
return;
const context = {
action: this.readAction,
attempt: this.consecutiveReconnectAttempts
};
const error2 = failure instanceof AudioStreamError ? failure : new AudioStreamError(failure instanceof Error ? failure.message : "Audio stream connection failed", context, failure);
if (await this.retry(error2, attempt, "connect"))
continue;
return;
}
if (!this.isAttemptActive(attempt)) {
await this.stopSource(attempt);
return;
}
if (!isReadableStreamSource(connection.body) && !isAsyncIterableSource(connection.body)) {
await this.stopSource(attempt);
const context = { action: "source" };
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream connection body must be a ReadableStream or AsyncIterable", context));
return;
}
if (!this.isAttemptActive(attempt)) {
await this.stopSource(attempt);
return;
}
let initialMetadata = null;
let demuxerFailed = false;
let demuxerFailure;
const finishDemuxerAcquisition = this.beginResourceAcquisition(attempt);
try {
attempt.demuxer = this.demuxerFactory?.(connection.info) ?? null;
initialMetadata = attempt.demuxer?.initialMetadata ?? null;
} catch (cause) {
demuxerFailed = true;
demuxerFailure = cause;
} finally {
finishDemuxerAcquisition();
}
if (demuxerFailed) {
await this.stopSource(attempt);
const error2 = demuxerFailure instanceof AudioStreamError ? demuxerFailure : new AudioStreamError(demuxerFailure instanceof Error ? demuxerFailure.message : "Audio stream demuxer creation failed", { action: "demuxer" }, demuxerFailure);
await this.finish(NativeAudioStreamCloseReason.TransportError, error2);
return;
}
if (!this.isAttemptActive(attempt)) {
await this.stopSource(attempt);
return;
}
this.publishMetadata(initialMetadata, attempt);
try {
this.createNativeStream();
} catch (cause) {
await this.stopSource(attempt);
const error2 = cause instanceof AudioStreamError ? cause : new AudioStreamError("Audio stream create failed", { action: "create" }, cause);
await this.finish(NativeAudioStreamCloseReason.TransportError, error2);
return;
}
try {
if (!await this.consumeSource(connection, attempt))
return;
} catch (cause) {
if (this.lifecycleController.signal.aborted)
return;
const error2 = cause instanceof AudioStreamError ? cause : cause instanceof TypeError && cause.message === INVALID_STREAM_CHUNK_MESSAGE ? cause : new AudioStreamError("Audio stream source failed", { action: "source" }, cause);
if (error2 instanceof ClassifiedAudioStreamError) {
if (await this.retry(error2, attempt, "read"))
continue;
return;
}
const context = error2 instanceof AudioStreamError ? error2.context : { action: "source" };
await this.finish(NativeAudioStreamCloseReason.TransportError, error2, context);
return;
}
if (!this.options.reconnect?.retryOnEnd) {
await this.finish(NativeAudioStreamCloseReason.PreserveNativeTerminal);
return;
}
const error = new AudioStreamError("Audio stream source ended", { action: "source" });
if (await this.retry(error, attempt, undefined, true))
continue;
return;
}
}
createNativeStream() {
if (this.nativeStreamId != null)
return;
const created = this.lib.audioCreateStream(this.engine, {
capacityMs: this.options.capacityMs,
startupMs: this.options.startupMs,
resumeMs: this.options.resumeMs,
maxProbeBytes: this.options.maxProbeBytes,
volume: this.options.volume,
pan: this.options.pan,
groupId: this.options.groupId,
format: toNativeAudioStreamFormat(this.options.format)
});
if (created.status !== 0 || created.streamId == null) {
const context = { action: "create", status: created.status };
throw new AudioStreamError(`Audio stream create failed: ${created.status}`, context);
}
this.nativeStreamId = created.streamId;
}
async consumeSource(connection, attempt) {
const initial = await this.pollNativeSnapshot(attempt);
if (initial == null || !this.isAttemptActive(attempt))
return false;
const decoderReady = this.awaitReady(attempt, initial.readyGeneration);
try {
await this.pumpSource(connection, attempt);
} catch (cause) {
this.observeReady(this.readNativeStats(), initial.readyGeneration);
await this.stopSource(attempt);
await decoderReady;
if (!this.lifecycleController.signal.aborted)
throw cause;
return false;
}
if (this.lifecycleController.signal.aborted)
return false;
const status = this.lib.audioEndStream(this.engine, this.nativeStreamId);
if (status !== 0) {
const nativeError = this.snapshotError(this.readNativeStats());
if (nativeError?.context.action === "decoder")
throw nativeError;
const context = { action: "end", status };
throw new AudioStreamError(`Audio stream end failed: ${status}`, context);
}
if (!await decoderReady || this.lifecycleController.signal.aborted)
return false;
if (!await this.awaitEnded(attempt))
return false;
await this.stopSource(attempt);
return !this.lifecycleController.signal.aborted;
}
async pumpSource(connection, attempt) {
const source = connection.body;
if (!this.isAttemptActive(attempt)) {
await this.runBoundedAttemptCleanup(attempt);
return;
}
const release = () => {
if (attempt.sourceReleased)
return;
if (attempt.reader == null) {
attempt.sourceReleased = true;
return;
}
try {
attempt.reader.releaseLock();
attempt.sourceReleased = true;
} catch {}
};
const next = () => attempt.reader == null ? attempt.iterator.next() : attempt.reader.read();
attempt.sourceAcquisitionAttempted = true;
const finishSourceAcquisition = this.beginResourceAcquisition(attempt);
try {
attempt.reader = isReadableStreamSource(source) ? source.getReader() : null;
attempt.iterator = attempt.reader == null ? source[Symbol.asyncIterator]() : null;
} catch (cause) {
const context = { action: this.readAction };
throw new ClassifiedAudioStreamError("Audio stream source failed", context, true, undefined, cause);
} finally {
finishSourceAcquisition();
}
if (!this.isAttemptActive(attempt)) {
await this.runBoundedAttemptCleanup(attempt);
return;
}
while (this.isAttemptActive(attempt)) {
let result;
try {
result = await next();
} catch (cause) {
const context = { action: this.readAction };
throw new ClassifiedAudioStreamError("Audio stream source failed", context, true, undefined, cause);
}
if (!this.isAttemptActive(attempt))
return;
if (result.done) {
release();
if (attempt.demuxer != null) {
try {
await this.processDemuxOutput(attempt.demuxer.flush(), attempt);
} catch (cause) {
if (cause instanceof AudioStreamError)
throw cause;
const context = { action: this.readAction };
throw new ClassifiedAudioStreamError(cause instanceof Error ? cause.message : "Audio stream demuxer flush failed", context, true, undefined, cause);
}
}
attempt.demuxerFinished = true;
await this.runBoundedAttemptCleanup(attempt);
return;
}
const chunk = result.value;
if (!isUint8Array(chunk))
throw new TypeError(INVALID_STREAM_CHUNK_MESSAGE);
if (chunk.byteLength === 0) {
await waitForDelay(0, attempt.controller.signal);
continue;
}
if (attempt.demuxer == null) {
await this.writeStreamChunk(chunk, attempt);
continue;
}
try {
await this.processDemuxOutput(attempt.demuxer.push(chunk), attempt);
} catch (cause) {
if (cause instanceof AudioStreamError)
throw cause;
throw new AudioStreamError(cause instanceof Error ? cause.message : "Audio stream demuxer failed", { action: "demuxer" }, cause);
}
}
}
async processDemuxOutput(outputs, attempt) {
for (const output of outputs) {
if (!this.isAttemptActive(attempt))
return;
if (output.type === "audio") {
if (!isUint8Array(output.data)) {
throw new AudioStreamError("Audio stream demuxer audio output must be a Uint8Array", {
action: "demuxer"
});
}
await this.writeStreamChunk(output.data, attempt);
} else if (output.type === "metadata") {
this.publishMetadata(output.metadata, attempt);
} else {
throw new AudioStreamError("Audio stream demuxer returned an invalid output", { action: "demuxer" });
}
}
}
async writeStreamChunk(chunk, attempt) {
let offset = 0;
while (offset < chunk.byteLength && this.isAttemptActive(attempt)) {
const streamId = this.nativeStreamId;
if (streamId == null)
return;
let accepted;
try {
accepted = this.lib.audioWriteStream(this.engine, streamId, chunk.subarray(offset));
} catch (cause) {
const context = { action: "write" };
throw new AudioStreamError("Audio stream write failed", context, cause);
}
if (accepted < 0) {
const context = { action: "write", status: accepted };
throw new AudioStreamError(`Audio stream write failed: ${accepted}`, context);
}
if (accepted === 0) {
if (await this.pollNativeSnapshot(attempt) == null)
return;
await waitForDelay(STREAM_POLL_INTERVAL_MS, attempt.controller.signal);
continue;
}
offset += accepted;
}
}
resolveConnection(connection, attempt) {
const finishAcquisition = this.beginResourceAcquisition(attempt);
try {
const close = connection.close;
if (close !== undefined && typeof close !== "function") {
throw new TypeError("Audio stream connection close must be a function");
}
attempt.closeConnection = close == null ? null : () => close.call(connection);
const info = connection.info;
const body = connection.body;
attempt.body = body;
return { body, info };
} finally {
finishAcquisition();
}
}
beginResourceAcquisition(attempt) {
let resolve;
const acquisition = new Promise((done) => {
resolve = done;
});
attempt.resourceAcquisition = acquisition;
return () => {
if (attempt.resourceAcquisition === acquisition)
attempt.resourceAcquisition = null;
resolve();
};
}
cleanupAttempt(attempt) {
if (attempt.cleanupPromise != null)
return attempt.cleanupPromise;
let resolveCleanup;
let rejectCleanup;
const cleanup = new Promise((resolve, reject) => {
resolveCleanup = resolve;
rejectCleanup = reject;
});
attempt.cleanupPromise = cleanup;
this.performAttemptCleanup(attempt).then(resolveCleanup, rejectCleanup);
const clearCleanup = () => {
if (attempt.cleanupPromise === cleanup)
attempt.cleanupPromise = null;
};
cleanup.then(clearCleanup, clearCleanup);
return cleanup;
}
async performAttemptCleanup(attempt) {
if (attempt.resourceAcquisition != null)
await attempt.resourceAcquisition;
const pending = [];
const reason = createAbortError();
if (!attempt.demuxerFinished && !attempt.demuxerAborted && attempt.demuxer != null) {
attempt.demuxerAborted = true;
try {
attempt.demuxer.abort?.(reason);
} catch {}
}
if (!attempt.sourceReleased) {
if (attempt.reader != null) {
attempt.sourceReleased = true;
try {
const result = attempt.reader.cancel(reason);
try {
attempt.reader.releaseLock();
} catch {}
pending.push(result);
} catch {}
} else if (attempt.iterator != null) {
attempt.sourceReleased = true;
try {
pending.push(Promise.resolve(attempt.iterator.return?.()));
} catch {}
} else if (attempt.body != null && !attempt.sourceAcquisitionAttempted) {
attempt.sourceReleased = true;
try {
if (isReadableStreamSource(attempt.body)) {
pending.push(attempt.body.cancel(reason));
} else if (isAsyncIterableSource(attempt.body)) {
attempt.iterator = attempt.body[Symbol.asyncIterator]();
pending.push(Promise.resolve(attempt.iterator.return?.()));
}
} catch {}
}
}
if (!attempt.connectionClosed && attempt.closeConnection != null) {
attempt.connectionClosed = true;
try {
pending.push(Promise.resolve(attempt.closeConnection()));
} catch {}
}
await Promise.allSettled(pending);
}
runBoundedAttemptCleanup(attempt) {
if (this.pendingCleanup != null)
return this.pendingCleanup;
let resolveCleanup;
let rejectCleanup;
const pendingCleanup = new Promise((resolve, reject) => {
resolveCleanup = resolve;
rejectCleanup = reject;
});
this.pendingCleanup = pendingCleanup;
const cleanup = runBoundedCleanup(() => this.cleanupAttempt(attempt));
cleanup.then(resolveCleanup, rejectCleanup);
const clearCleanup = () => {
if (this.pendingCleanup === pendingCleanup)
this.pendingCleanup = null;
};
pendingCleanup.then(clearCleanup, clearCleanup);
return pendingCleanup;
}
async retry(error, attempt, phase, cleanEnd = false) {
if (this.lifecycleController.signal.aborted)
return false;
const reconnect = this.options.reconnect;
const retryable = phase == null || !(error instanceof ClassifiedAudioStreamError) || error.retryable;
if (reconnect == null || this.consecutiveReconnectAttempts >= reconnect.maxRetries) {
if (cleanEnd)
await this.finish(NativeAudioStreamCloseReason.PreserveNativeTerminal);
else
await this.finish(NativeAudioStreamCloseReason.TransportError, error);
return false;
}
let retryDelayMs = error instanceof ClassifiedAudioStreamError ? error.retryAfterMs : undefined;
if (phase != null && reconnect.retry != null) {
let decision;
try {
decision = reconnect.retry(error, {
attempt: this.consecutiveReconnectAttempts + 1,
maxRetries: reconnect.maxRetries,
phase
});
if (decision !== false && (decision == null || typeof decision !== "object")) {
throw new TypeError("Audio stream retry policy must return false or a retry decision");
}
} catch (cause) {
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream retry policy failed", { action: "source" }, cause));
return false;
}
if (decision === false) {
await this.finish(NativeAudioStreamCloseReason.TransportError, error);
return false;
}
if (this.lifecycleController.signal.aborted)
return false;
let policyDelayMs;
try {
policyDelayMs = decision.delayMs;
} catch (cause) {
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream retry policy failed", { action: "source" }, cause));
return false;
}
if (policyDelayMs !== undefined && (!Number.isFinite(policyDelayMs) || !Number.isInteger(policyDelayMs) || policyDelayMs < 0)) {
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream retry delay must be a finite non-negative integer", {
action: "source"
}));
return false;
}
if (policyDelayMs !== undefined)
retryDelayMs = Math.min(reconnect.maxDelayMs, policyDelayMs);
} else if (!retryable) {
await this.finish(NativeAudioStreamCloseReason.TransportError, error);
return false;
}
if (retryDelayMs !== undefined)
retryDelayMs = Math.min(reconnect.maxDelayMs, retryDelayMs);
await this.cleanupAttempt(attempt);
if (this.lifecycleController.signal.aborted)
return false;
if (this.nativeStreamId != null) {
const nativeError = this.snapshotError(this.readNativeStats());
if (nativeError != null) {
const reason = nativeError.context.action === "decoder" ? NativeAudioStreamCloseReason.PreserveNativeTerminal : NativeAudioStreamCloseReason.TransportError;
await this.finish(reason, nativeError);
return false;
}
const restartStatus = this.lib.audioRestartStream(this.engine, this.nativeStreamId);
if (restartStatus !== 0) {
const restartContext = { action: "restart", status: restartStatus };
await this.finish(NativeAudioStreamCloseReason.TransportError, new AudioStreamError("Audio stream restart failed during reconnect", restartContext));
return false;
}
this.readNativeStats();
}
this.reconnectAttempts += 1;
this.consecutiveReconnectAttempts += 1;
const delayMs = retryDelayMs ?? Math.min(reconnect.maxDelayMs, reconnect.initialDelayMs * reconnect.backoffFactor ** (this.consecutiveReconnectAttempts - 1));
if (this.exposed) {
this.emitAsync("reconnecting", {
attempt: this.consecutiveReconnectAttempts,
delayMs,
maxRetries: reconnect.maxRetries,
error
});
}
return waitForDelay(delayMs, this.lifecycleController.signal).then(() => true).catch(() => false);
}
async awaitReady(attempt, previousGeneration) {
while (this.isAttemptActive(attempt)) {
const stats = await this.pollNativeSnapshot(attempt);
if (stats == null)
return false;
if (this.observeReady(stats, previousGeneration))
return true;
if (!await waitForPoll(attempt.controller.signal))
return false;
}
return false;
}
observeReady(stats, previousGeneration) {
if (stats == null || stats.readyGeneration === previousGeneration)
return false;
this.consecutiveReconnectAttempts = 0;
this.setupResolve();
return true;
}
async awaitEnded(attempt) {
while (this.isAttemptActive(attempt)) {
const stats = await this.pollNativeSnapshot(attempt);
if (stats == null)
return false;
if (stats.state === NativeAudioStreamState.Ended)
return true;
if (!await waitForPoll(attempt.controller.signal))
return false;
}
return false;
}
async pollNativeSnapshot(attempt) {
if (!this.isAttemptActive(attempt))
return null;
const stats = this.readNativeStats();
const error = this.snapshotError(stats);
if (error != null) {
const reason = error.context.action === "decoder" ? NativeAudioStreamCloseReason.PreserveNativeTerminal : NativeAudioStreamCloseReason.TransportError;
await this.finish(reason, error);
return null;
}
return stats;
}
snapshotError(stats) {
if (stats == null)
return new AudioStreamError("Audio stream stats failed", { action: "stats" });
if (NativeAudioStreamStateNames[stats.state] == null) {
return new AudioStreamError(`Unknown native audio stream state: ${stats.state}`, { action: "stats" });
}
if (stats.state !== NativeAudioStreamState.Failed && stats.state !== NativeAudioStreamState.Cancelled)
return null;
const context = { action: "decoder", errorCode: stats.errorCode };
return new AudioStreamError(stats.state === NativeAudioStreamState.Failed ? `Audio stream decoder failed: ${stats.errorCode}` : "Audio stream was cancelled by the decoder", context);
}
async finish(reason, error, context) {
if (this.lifecycleController.signal.aborted)
return;
if (error instanceof AudioStreamError)
context = error.context;
this.lifecycleController.abort();
this.terminalError = error ?? null;
const cleanup = this.stopSource();
const closeStatus = this.closeNativeStream(reason);
if (error == null && closeStatus !== 0) {
context = { action: "destroy", status: closeStatus };
error = new AudioStreamError("Audio stream destroy failed after end", context);
this.terminalError = error;
}
await cleanup;
if (error != null)
this.setupReject(error);
else
this.setupResolve();
if (closeStatus === 0)
this.removeOwner();
if (!this.disposed && this.exposed) {
if (error != null)
this.emitTerminal("error", error, context);
else
this.emitTerminal("ended");
} else if (error != null && !this.disposed)
this.closedResolve();
}
publishMetadata(metadata, attempt) {
if (!this.isAttemptActive(attempt) || Object.is(this.metadata, metadata))
return;
this.metadata = metadata;
if (!this.exposed) {
this.pendingMetadataEvent = true;
return;
}
this.emitMetadata();
}
emitMetadata() {
if (this.metadataEventScheduled)
return;
this.metadataEventScheduled = true;
setTimeout(() => {
this.metadataEventScheduled = false;
if (!this.disposed)
EventEmitter.prototype.emit.call(this, "metadata", this.metadata);
}, 0);
}
emitAsync(event, ...args) {
setTimeout(() => EventEmitter.prototype.emit.call(this, event, ...args), 0);
}
emitTerminal(event, ...args) {
if (this.terminalEventScheduled)
return;
this.terminalEventScheduled = true;
setTimeout(() => {
try {
EventEmitter.prototype.emit.call(this, event, ...args);
} finally {
this.closedResolve();
}
}, 0);
}
isAttemptActive(attempt) {
return !this.lifecycleController.signal.aborted && this.activeAttempt === attempt;
}
stopSource(attempt = this.activeAttempt) {
if (attempt == null)
return this.pendingCleanup ?? Promise.resolve();
if (this.activeAttempt === attempt)
this.activeAttempt = null;
attempt.controller.abort();
return this.runBoundedAttemptCleanup(attempt);
}
closeNativeStream(reason) {
const streamId = this.nativeStreamId;
if (streamId == null)
return 0;
const result = this.lib.audioCloseStream(this.engine, streamId, reason);
if (result.status !== 0 || result.stats == null)
return result.status === 0 ? -1 : result.status;
this.nativeStats = result.stats;
this.nativeStreamId = null;
return 0;
}
readNativeStats() {
if (this.nativeStreamId == null)
return this.nativeStats;
const stats = this.lib.audioGetStreamStats(this.engine, this.nativeStreamId);
if (stats != null)
this.nativeStats = stats;
return stats;
}
toPublicStats() {
const stats = this.nativeStats;
const sampleRate = stats?.sampleRate ?? 0;
const bufferedFrames = stats?.bufferedFrames ?? 0;
return {
state: this.state,
sampleRate,
channels: stats?.channels ?? 0,
bufferedFrames,
capacityFrames: stats?.capacityFrames ?? 0,
bufferedDurationMs: sampleRate === 0 ? 0 : bufferedFrames * 1000 / sampleRate,
bytesReceived: stats?.bytesReceived ?? 0n,
framesDecoded: stats?.framesDecoded ?? 0n,
framesPlayed: stats?.framesPlayed ?? 0n,
underruns: stats?.underruns ?? 0,
reconnectAttempts: this.reconnectAttempts
};
}
removeOwner() {
this.options.signal?.removeEventListener("abort", this.overallAbortListener);
this.removeFromOwner();
}
}
var createAudioCaptureStream;
var openAudioCaptureStream;
var refreshAudioCaptureStreamFinalStats;
class AudioCaptureStream extends EventEmitter {
readable;
sampleRate;
channels;
chunkFrames;
closed;
init;
lifecycleController = new AbortController;
streamController = null;
nativeStats;
currentState = "initializing";
pendingFrames = 0;
pendingSamples;
producerStopAttempted = false;
producerStopped = false;
producerMayBeRunning = false;
ownerRemoved = false;
exposed = false;
terminal = false;
discardRequested = false;
discardDecisionScheduled = false;
pumpPromise = null;
producerCleanupPromise = null;
lastCleanupFailure = null;
terminalCompletionPromise = null;
closedResolve;
signalAbortListener = () => this.dispose();
static {
createAudioCaptureStream = (init) => new AudioCaptureStream(init);
openAudioCaptureStream = (stream) => stream.open();
refreshAudioCaptureStreamFinalStats = (stream) => stream.refreshFinalStats();
}
constructor(init) {
super();
this.init = init;
this.sampleRate = init.options.sampleRate;
this.channels = init.options.channels;
this.chunkFrames = init.options.chunkFrames;
this.pendingSamples = new Float32Array(this.chunkFrames * this.channels);
this.nativeStats = {
sampleRate: this.sampleRate,
channels: this.channels,
capacityFrames: init.options.capacityFrames,
bufferedFrames: 0,
framesReceived: 0n,
framesRead: 0n,
framesDropped: 0n
};
this.closed = new Promise((resolve) => this.closedResolve = resolve);
this.readable = new ReadableStream({
start: (controller) => {
this.streamController = controller;
},
pull: () => this.pull(),
cancel: () => this.disposeInternal(true)
}, { highWaterMark: 0 });
this.init.options.signal?.addEventListener("abort", this.signalAbortListener, { once: true });
}
get state() {
return this.currentState;
}
async open() {
if (this.init.options.signal?.aborted) {
await this.disposeInternal(false);
throw createAbortError();
}
let result;
this.producerMayBeRunning = true;
try {
result = this.init.start();
} catch (cause) {
this.currentState = "errored";
await this.cleanupProducer();
this.closedResolve();
throw new AudioCaptureStreamError("Audio capture stream start failed", { action: "start" }, cause);
}
if (result.status !== 0) {
this.producerMayBeRunning = false;
this.currentState = "errored";
this.removeOwner();
this.closedResolve();
throw this.operationError("start", result);
}
if (this.terminal || this.init.options.signal?.aborted) {
await (this.terminalCompletionPromise ?? this.disposeInternal(false));
throw createAbortError();
}
let stats;
try {
stats = this.init.stats();
} catch (cause) {
stats = { status: -1, stats: null, cause };
}
if (stats.status !== 0 || stats.stats == null) {
this.currentState = "errored";
await this.cleanupProducer();
this.closedResolve();
throw this.operationError("stats", stats);
}
this.nativeStats = stats.stats;
if (this.terminal || this.init.options.signal?.aborted) {
await (this.terminalCompletionPromise ?? this.disposeInternal(false));
throw createAbortError();
}
this.currentState = "capturing";
this.exposed = true;
}
getStats() {
if (!this.terminal) {
const stats = this.refreshStats();
if (stats != null && !this.observeProducer())
this.scheduleDiscardIfIdle();
}
return this.publicStats();
}
stop() {
if (this.terminal || this.currentState === "stopping")
return;
this.currentState = "stopping";
const result = this.stopProducer();
if (result != null && result.status !== 0)
this.fail(this.operationError("stop", result));
else
this.scheduleDiscardIfIdle();
}
dispose() {
this.disposeInternal(false);
}
disposeInternal(fromCancel) {
if (this.terminal) {
if (this.ownerRemoved)
return this.terminalCompletionPromise ?? Promise.resolve();
return this.retryTerminalCleanup();
}
this.terminal = true;
this.currentState = "disposed";
this.refreshFinalStats();
this.lifecycleController.abort();
const immediateCleanup = !this.producerMayBeRunning || this.producerStopped ? { status: 0 } : this.producerStopAttempted ? null : this.stopProducer();
if (immediateCleanup?.status === 0)
this.refreshFinalStats();
this.terminalCompletionPromise = (async () => {
const cleanup = immediateCleanup?.status === 0 ? immediateCleanup : await this.cleanupProducer();
this.refreshFinalStats();
if (cleanup.status === 0) {
this.removeOwner();
if (!fromCancel) {
try {
this.streamController?.close();
} catch {}
}
if (this.exposed)
this.emitTerminal("disposed");
else
this.closedResolve();
return;
}
this.currentState = "errored";
const error = this.operationError("destroy", cleanup);
try {
this.streamController?.error(error);
} catch {}
if (this.exposed)
this.emitTerminal("error", error, error.context);
else
this.closedResolve();
})();
return this.terminalCompletionPromise;
}
pull() {
return this.pump();
}
pump() {
if (this.pumpPromise != null)
return this.pumpPromise;
const pump = Promise.resolve().then(async () => {
do {
await this.pumpSource();
} while (this.discardRequested && !this.terminal);
});
this.pumpPromise = pump;
const clear = () => {
if (this.pumpPromise === pump)
this.pumpPromise = null;
};
pump.then(clear, clear);
return pump;
}
async pumpSource() {
if (this.discardRequested) {
await this.discardNativeRing();
return;
}
const controller = this.streamController;
while (!this.terminal) {
if (this.discardRequested) {
await this.discardNativeRing();
return;
}
const stats = this.refreshStats();
if (stats == null || this.terminal)
return;
const running = this.observeProducer();
if (this.terminal)
return;
const neededFrames = this.chunkFrames - this.pendingFrames;
const readableFrames = running ? stats.bufferedFrames >= neededFrames ? neededFrames : 0 : Math.min(neededFrames, stats.bufferedFrames);
let framesRead = 0;
if (readableFrames > 0) {
let result;
try {
result = this.init.read(readableFrames);
} catch (cause) {
this.fail(new AudioCaptureStreamError("Audio capture stream read failed", { action: "read" }, cause));
return;
}
if (result.status !== 0) {
this.fail(this.operationError("read", result));
return;
}
framesRead = Math.min(readableFrames, result.framesRead);
if (framesRead > 0) {
const sampleCount = framesRead * this.channels;
this.pendingSamples.set(result.frames.subarray(0, sampleCount), this.pendingFrames * this.channels);
this.pendingFrames += framesRead;
if (this.pendingFrames === this.chunkFrames) {
controller.enqueue(this.pendingSamples.slice());
this.pendingFrames = 0;
if (!running && stats.bufferedFrames <= framesRead)
this.finishStopped();
return;
}
}
}
if (!running && stats.bufferedFrames <= framesRead) {
if (this.pendingFrames > 0) {
controller.enqueue(this.pendingSamples.slice(0, this.pendingFrames * this.channels));
this.pendingFrames = 0;
}
this.finishStopped();
return;
}
if (!await waitForPoll(this.lifecycleController.signal))
return;
}
}
async discardNativeRing() {
this.pendingFrames = 0;
let chunksThisTurn = 0;
while (!this.terminal) {
const stats = this.refreshStats();
if (stats == null || this.terminal)
return;
if (stats.bufferedFrames === 0) {
this.finishStopped();
return;
}
const frameCount = Math.min(this.chunkFrames, stats.bufferedFrames);
let result;
try {
result = this.init.read(frameCount);
} catch (cause) {
this.fail(new AudioCaptureStreamError("Audio capture stream read failed", { action: "read" }, cause));
return;
}
if (result.status !== 0) {
this.fail(this.operationError("read", result));
return;
}
chunksThisTurn += 1;
if (result.framesRead === 0) {
if (!await waitForPoll(this.lifecycleController.signal))
return;
} else if (chunksThisTurn >= CAPTURE_DISCARD_BATCH_CHUNKS) {
chunksThisTurn = 0;
await waitForDelay(0, this.lifecycleController.signal).catch(() => {
return;
});
}
}
}
requestDiscardDrain() {
if (this.terminal || this.discardRequested)
return;
this.discardRequested = true;
this.pump();
}
scheduleDiscardIfIdle() {
if (this.terminal || this.discardRequested || this.discardDecisionScheduled)
return;
this.discardDecisionScheduled = true;
setTimeout(() => {
this.discardDecisionScheduled = false;
if (this.terminal)
return;
if (this.readable.locked)
this.scheduleDiscardIfIdle();
else
this.requestDiscardDrain();
}, STREAM_POLL_INTERVAL_MS);
}
refreshStats() {
let result;
try {
result = this.init.stats();
} catch (cause) {
this.fail(new AudioCaptureStreamError("Audio capture stream stats failed", { action: "stats" }, cause));
return null;
}
if (result.status !== 0 || result.stats == null) {
this.fail(this.operationError("stats", result));
return null;
}
this.nativeStats = result.stats;
return result.stats;
}
observeProducer() {
if (this.producerStopAttempted)
return false;
let running;
try {
running = this.init.isRunning();
} catch (cause) {
this.fail(new AudioCaptureStreamError("Audio capture stream stats failed", { action: "stats" }, cause));
return false;
}
if (this.terminal)
return false;
if (running)
return true;
this.currentState = "stopping";
const result = this.stopProducer();
if (result != null && result.status !== 0)
this.fail(this.operationError("stop", result));
return false;
}
stopProducer() {
if (this.producerStopAttempted)
return null;
this.producerStopAttempted = true;
try {
const result = this.init.stop();
if (result.status === 0) {
this.producerStopped = true;
this.producerMayBeRunning = false;
}
return result;
} catch (cause) {
return { status: -1, cause };
}
}
finishStopped() {
if (this.terminal)
return;
if (this.refreshStats() == null || this.terminal)
return;
this.terminal = true;
this.currentState = "stopped";
this.lifecycleController.abort();
this.removeOwner();
this.streamController?.close();
this.emitTerminal("stopped");
}
fail(error) {
if (this.terminal)
return;
this.terminal = true;
this.currentState = "errored";
this.lifecycleController.abort();
try {
this.streamController?.error(error);
} catch {}
this.terminalCompletionPromise = (async () => {
if ((await this.cleanupProducer()).status === 0)
this.removeOwner();
this.refreshFinalStats();
if (this.exposed)
this.emitTerminal("error", error, error.context);
else
this.closedResolve();
})();
}
operationError(action, result) {
const context = { action };
if (result.status !== 0)
context.status = result.status;
return new AudioCaptureStreamError(`Audio capture stream ${action} failed${result.status ? `: ${result.status}` : ""}`, context, result.cause);
}
cleanupProducer() {
if (!this.producerMayBeRunning || this.producerStopped)
return Promise.resolve({ status: 0 });
if (this.producerCleanupPromise != null)
return this.producerCleanupPromise;
const cleanup = (async () => {
let lastFailure = this.lastCleanupFailure ?? { status: -1 };
for (let attempt = 0;attempt < 3; attempt += 1) {
this.producerStopAttempted = false;
const result = this.stopProducer();
if (result?.status === 0) {
this.lastCleanupFailure = null;
return result;
}
if (result != null) {
lastFailure = result;
this.lastCleanupFailure = result;
}
if (attempt < 2)
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_INTERVAL_MS));
}
return lastFailure;
})();
this.producerCleanupPromise = cleanup;
cleanup.finally(() => {
if (this.producerCleanupPromise === cleanup)
this.producerCleanupPromise = null;
});
return cleanup;
}
retryTerminalCleanup() {
return this.cleanupProducer().then((result) => {
if (result.status === 0)
this.removeOwner();
});
}
publicStats() {
return {
...this.nativeStats,
state: this.currentState,
bufferedDurationMs: this.nativeStats.sampleRate === 0 ? 0 : this.nativeStats.bufferedFrames * 1000 / this.nativeStats.sampleRate
};
}
refreshFinalStats() {
try {
const result = this.init.stats();
if (result.status === 0 && result.stats != null)
this.nativeStats = result.stats;
} catch {}
}
removeOwner() {
if (this.ownerRemoved)
return;
this.ownerRemoved = true;
this.init.options.signal?.removeEventListener("abort", this.signalAbortListener);
this.init.removeFromOwner();
}
emitTerminal(event, ...args) {
setTimeout(() => {
try {
EventEmitter.prototype.emit.call(this, event, ...args);
} finally {
this.closedResolve();
}
}, 0);
}
}
var createAudioRecorder;
var openAudioRecorder;
function createWavHeader(sampleRate, channels, dataBytes) {
const header = new Uint8Array(WAV_HEADER_BYTES);
const view = new DataView(header.buffer);
header.set([82, 73, 70, 70], 0);
view.setUint32(4, dataBytes + 36, true);
header.set([87, 65, 86, 69], 8);
header.set([102, 109, 116, 32], 12);
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, channels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * channels * 2, true);
view.setUint16(32, channels * 2, true);
view.setUint16(34, 16, true);
header.set([100, 97, 116, 97], 36);
view.setUint32(40, dataBytes, true);
return header;
}
class AudioRecorder extends EventEmitter {
static fileSystem = { open: openFile, rename, unlink };
filePath;
format = "wav";
sampleRate;
channels;
closed;
init;
currentState = "initializing";
capture = null;
reader = null;
fileHandle = null;
tempPath = null;
captureStats;
framesWritten = 0n;
dataBytesWritten = 0n;
stopRequested = false;
terminal = null;
terminationRequest = null;
publicationStarted = false;
exposed = false;
ownerRemoved = false;
cleanupPromise = null;
resourceCleanupPromise = null;
retainedCleanupScheduled = false;
lifecyclePromise = null;
closedResolve;
signalAbortListener = () => this.dispose();
captureErrorListener = (error) => {
this.fail(this.fromCaptureError(error));
};
static {
createAudioRecorder = (init) => new AudioRecorder(init);
openAudioRecorder = (recorder) => recorder.open();
}
constructor(init) {
super();
this.init = init;
this.filePath = init.filePath;
this.sampleRate = init.captureOptions.sampleRate;
this.channels = init.captureOptions.channels;
this.captureStats = {
state: "initializing",
sampleRate: this.sampleRate,
channels: this.channels,
capacityFrames: init.captureOptions.capacityFrames,
bufferedFrames: 0,
bufferedDurationMs: 0,
framesReceived: 0n,
framesRead: 0n,
framesDropped: 0n
};
this.closed = new Promise((resolve) => this.closedResolve = resolve);
this.init.signal?.addEventListener("abort", this.signalAbortListener, { once: true });
}
get state() {
return this.currentState;
}
async open() {
try {
this.fileHandle = await this.openTemporaryFile();
this.ensureOpening();
await this.writeFully(new Uint8Array(WAV_HEADER_BYTES), 0, "write");
this.ensureOpening();
try {
this.capture = await this.init.openCapture({
channels: this.init.captureOptions.channels,
capacityFrames: this.init.captureOptions.capacityFrames,
chunkFrames: this.init.captureOptions.chunkFrames,
startOptions: this.init.captureOptions.startOptions
});
this.capture.on("error", this.captureErrorListener);
} catch (cause) {
if (cause instanceof DOMException && cause.name === "AbortError")
throw cause;
throw new AudioRecorderError("Audio recorder capture start failed", { action: "start" }, cause);
}
this.ensureOpening();
this.captureStats = this.capture.getStats();
if (this.captureStats.framesDropped > 0n) {
throw new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" });
}
this.currentState = "recording";
this.exposed = true;
this.reader = this.capture.readable.getReader();
const lifecycle = this.consume();
this.lifecyclePromise = lifecycle;
lifecycle.finally(() => {
if (this.lifecyclePromise === lifecycle)
this.lifecyclePromise = null;
const request = this.terminationRequest;
if (request != null)
this.finishCleanup(request.kind, request.error);
});
} catch (cause) {
if (this.terminationRequest == null) {
if (cause instanceof DOMException && cause.name === "AbortError")
this.requestTermination("disposed");
else {
const error = cause instanceof AudioRecorderError ? cause : new AudioRecorderError("Audio recorder open failed", { action: "open" }, cause);
this.requestTermination("error", error);
}
}
const request = this.terminationRequest;
const cleanupError = await this.finishCleanup(request.kind, request.error);
if (cleanupError != null) {
throw new AudioRecorderError("Audio recorder setup cleanup failed", { action: "destroy" }, new AggregateError([cause, cleanupError], "Audio recorder setup and cleanup failed"));
}
if (request.kind === "disposed")
throw createAbortError();
if (cause instanceof AudioRecorderError)
throw cause;
throw new AudioRecorderError("Audio recorder open failed", { action: "open" }, cause);
}
}
getStats() {
if (this.capture != null && this.terminal == null && this.terminationRequest == null) {
this.captureStats = this.capture.getStats();
if (this.captureStats.framesDropped > 0n) {
this.fail(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" }));
}
}
return {
sampleRate: this.captureStats.sampleRate,
channels: this.captureStats.channels,
capacityFrames: this.captureStats.capacityFrames,
bufferedFrames: this.captureStats.bufferedFrames,
bufferedDurationMs: this.captureStats.bufferedDurationMs,
framesReceived: this.captureStats.framesReceived,
framesRead: this.captureStats.framesRead,
framesDropped: this.captureStats.framesDropped,
state: this.currentState,
framesWritten: this.framesWritten,
dataBytesWritten: this.dataBytesWritten,
durationMs: this.sampleRate === 0 ? 0 : Number(this.framesWritten) * 1000 / this.sampleRate
};
}
stop() {
if (this.terminal != null || this.terminationRequest != null || this.stopRequested)
return;
this.stopRequested = true;
this.currentState = "stopping";
try {
this.capture?.stop();
} catch (cause) {
this.fail(new AudioRecorderError("Audio recorder capture stop failed", { action: "stop" }, cause));
}
}
dispose() {
if (this.terminal != null) {
this.retryRetainedCleanup();
return;
}
if (this.publicationStarted)
return;
this.requestTermination("disposed");
}
async consume() {
const reader = this.reader;
let pendingRead = this.readWithStats(reader);
try {
while (this.terminal == null && this.terminationRequest == null) {
const result = await pendingRead;
pendingRead = null;
if (this.terminal != null || this.terminationRequest != null)
return;
if (result.done) {
if (!this.stopRequested) {
this.fail(new AudioRecorderError("Audio capture stopped unexpectedly", { action: "stop" }));
} else {
await this.complete();
}
return;
}
pendingRead = this.readWithStats(reader);
this.captureStats = this.capture.getStats();
if (this.captureStats.framesDropped > 0n) {
this.fail(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" }));
return;
}
await this.writeSamples(result.value);
}
} catch (cause) {
if (this.terminal == null && this.terminationRequest == null) {
const error = cause instanceof AudioRecorderError ? cause : cause instanceof AudioCaptureStreamError ? this.fromCaptureError(cause) : new AudioRecorderError("Audio recorder read failed", { action: "read" }, cause);
this.fail(error);
}
} finally {
if (pendingRead != null)
pendingRead.catch(() => {
return;
});
try {
reader.releaseLock();
} catch {}
}
}
async writeSamples(samples) {
if (samples.length % this.channels !== 0) {
throw new AudioRecorderError("Audio recorder received a partial frame", { action: "read" });
}
const bytes = new Uint8Array(samples.length * 2);
const view = new DataView(bytes.buffer);
for (let index = 0;index < samples.length; index += 1) {
const sample = Math.max(-1, Math.min(1, samples[index]));
view.setInt16(index * 2, Math.round(sample * 32767), true);
}
const nextDataBytes = this.dataBytesWritten + BigInt(bytes.byteLength);
if (nextDataBytes > MAX_WAV_DATA_BYTES) {
throw new AudioRecorderError("Audio recorder exceeded the classic RIFF size limit", { action: "write" });
}
await this.writeFully(bytes, WAV_HEADER_BYTES + Number(this.dataBytesWritten), "write");
this.dataBytesWritten = nextDataBytes;
this.framesWritten += BigInt(samples.length / this.channels);
}
readWithStats(reader) {
return new Promise((resolve, reject) => {
let settled = false;
let timer;
const finish = (callback) => {
if (settled)
return;
settled = true;
if (timer !== undefined)
clearTimeout(timer);
callback();
};
const poll = () => {
if (settled)
return;
if (this.terminal != null || this.terminationRequest != null) {
finish(() => resolve({ done: true, value: undefined }));
return;
}
this.captureStats = this.capture.getStats();
if (this.captureStats.framesDropped > 0n) {
finish(() => reject(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" })));
return;
}
timer = setTimeout(poll, STREAM_POLL_INTERVAL_MS);
};
reader.read().then((result) => finish(() => resolve(result)), (cause) => finish(() => reject(cause)));
timer = setTimeout(poll, STREAM_POLL_INTERVAL_MS);
});
}
async complete() {
if (this.terminal != null || this.terminationRequest != null)
return;
this.captureStats = this.capture.getStats();
if (this.captureStats.framesDropped > 0n) {
this.fail(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" }));
return;
}
try {
await this.writeFully(createWavHeader(this.sampleRate, this.channels, Number(this.dataBytesWritten)), 0, "finalize");
if (this.terminationRequest != null)
return;
await this.fileHandle.sync();
if (this.terminationRequest != null)
return;
await this.fileHandle.close();
this.fileHandle = null;
} catch (cause) {
if (this.terminationRequest == null) {
this.fail(cause instanceof AudioRecorderError ? cause : new AudioRecorderError("Audio recorder finalize failed", { action: "finalize" }, cause));
}
return;
}
if (this.terminationRequest != null)
return;
this.publicationStarted = true;
try {
await this.publish();
} catch (cause) {
const error = new AudioRecorderError("Audio recorder publish failed", { action: "publish" }, cause);
this.terminal = "error";
this.currentState = "errored";
await this.finishCleanup("error", error);
return;
}
this.terminal = "stopped";
this.currentState = "stopped";
this.init.signal?.removeEventListener("abort", this.signalAbortListener);
this.capture?.removeListener("error", this.captureErrorListener);
if (this.hasRetainedResources())
this.scheduleRetainedCleanup();
else
this.removeOwner();
this.emitTerminal("stopped");
}
async publish() {
const tempPath = this.tempPath;
await AudioRecorder.fileSystem.rename(tempPath, this.filePath);
this.tempPath = null;
}
fail(error) {
if (this.terminal != null || this.publicationStarted)
return;
this.requestTermination("error", error);
}
requestTermination(kind, error) {
if (this.terminal != null || this.publicationStarted || this.terminationRequest != null)
return;
this.terminationRequest = { kind, error };
this.currentState = kind === "disposed" ? "disposed" : "errored";
this.capture?.dispose();
if (this.exposed && this.lifecyclePromise == null)
this.finishCleanup(kind, error);
}
finishCleanup(kind, error) {
if (this.cleanupPromise != null)
return this.cleanupPromise;
this.cleanupPromise = (async () => {
this.init.signal?.removeEventListener("abort", this.signalAbortListener);
const capture2 = this.capture;
capture2?.dispose();
if (capture2 != null) {
await capture2.closed;
this.captureStats = capture2.getStats();
}
capture2?.removeListener("error", this.captureErrorListener);
const cleanupError = await this.cleanupOwnedResources();
const captureCleanupError = capture2?.state === "errored" ? new AudioRecorderError("Audio recorder capture cleanup failed", { action: "destroy" }) : null;
const terminalKind = cleanupError == null && captureCleanupError == null ? kind : "error";
const terminalError = cleanupError ?? error ?? captureCleanupError;
this.terminal = terminalKind;
this.currentState = terminalKind === "disposed" ? "disposed" : "errored";
if (this.hasRetainedResources())
this.scheduleRetainedCleanup();
else
this.removeOwner();
if (!this.exposed) {
this.closedResolve();
} else if (terminalKind === "error") {
this.emitTerminal("error", terminalError, terminalError.context);
} else {
this.emitTerminal("disposed");
}
return cleanupError ?? captureCleanupError;
})();
return this.cleanupPromise;
}
cleanupOwnedResources() {
if (!this.hasRetainedResources())
return Promise.resolve(null);
if (this.resourceCleanupPromise != null)
return this.resourceCleanupPromise;
const cleanup = (async () => {
let lastError = null;
for (let attempt = 0;attempt < 3; attempt += 1) {
const handle = this.fileHandle;
if (handle != null) {
try {
await handle.close();
if (this.fileHandle === handle)
this.fileHandle = null;
} catch (cause) {
lastError = new AudioRecorderError("Audio recorder file close failed", { action: "destroy" }, cause);
}
}
if (this.fileHandle == null) {
const tempPath = this.tempPath;
if (tempPath != null) {
try {
await AudioRecorder.fileSystem.unlink(tempPath);
if (this.tempPath === tempPath)
this.tempPath = null;
} catch (cause) {
lastError = new AudioRecorderError("Audio recorder temp file cleanup failed", { action: "destroy" }, cause);
}
}
}
if (!this.hasRetainedResources())
return null;
if (attempt < 2)
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_INTERVAL_MS));
}
return lastError ?? new AudioRecorderError("Audio recorder resource cleanup failed", { action: "destroy" });
})();
this.resourceCleanupPromise = cleanup;
const clear = () => {
if (this.resourceCleanupPromise === cleanup)
this.resourceCleanupPromise = null;
};
cleanup.then(clear, clear);
return cleanup;
}
retryRetainedCleanup() {
this.capture?.dispose();
if (!this.hasRetainedResources()) {
this.removeOwner();
return Promise.resolve();
}
return this.cleanupOwnedResources().then(() => {
if (!this.hasRetainedResources())
this.removeOwner();
});
}
scheduleRetainedCleanup() {
if (this.retainedCleanupScheduled || !this.hasRetainedResources())
return;
this.retainedCleanupScheduled = true;
setTimeout(() => {
this.retainedCleanupScheduled = false;
this.retryRetainedCleanup();
}, STREAM_POLL_INTERVAL_MS);
}
hasRetainedResources() {
return this.fileHandle != null || this.tempPath != null;
}
async openTemporaryFile() {
const directory = dirname(this.filePath);
const destinationName = basename(this.filePath);
const tempNameLength = Math.max(1, Math.min(24, destinationName.length));
const singleCharacterNames = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
for (let attempt = 0;attempt < singleCharacterNames.length; attempt += 1) {
const tempName = tempNameLength === 1 ? singleCharacterNames[attempt] : randomBytes(16).toString("hex").slice(0, tempNameLength);
if (tempName === destinationName)
continue;
const tempPath = join(directory, tempName);
try {
const handle = await AudioRecorder.fileSystem.open(tempPath, "wx");
this.tempPath = tempPath;
return handle;
} catch (cause) {
if (cause.code !== "EEXIST") {
throw new AudioRecorderError("Audio recorder temp file open failed", { action: "open" }, cause);
}
}
}
throw new AudioRecorderError("Audio recorder could not allocate a temporary file", { action: "open" });
}
async writeFully(bytes, position, action) {
let offset = 0;
while (offset < bytes.byteLength) {
let bytesWritten;
try {
({ bytesWritten } = await this.fileHandle.write(bytes, offset, bytes.byteLength - offset, position + offset));
} catch (cause) {
throw new AudioRecorderError(`Audio recorder ${action} write failed`, { action }, cause);
}
if (bytesWritten <= 0) {
throw new AudioRecorderError(`Audio recorder ${action} write made no progress`, { action });
}
offset += Math.min(bytesWritten, bytes.byteLength - offset);
}
}
ensureOpening() {
if (this.terminationRequest?.kind === "disposed" || this.init.signal?.aborted)
throw createAbortError();
if (this.terminationRequest?.kind === "error")
throw this.terminationRequest.error;
}
fromCaptureError(error) {
const action = error.context.action === "stop" ? "stop" : error.context.action === "stats" ? "stats" : error.context.action === "destroy" ? "destroy" : "read";
const context = { action };
if (error.context.status !== undefined)
context.status = error.context.status;
return new AudioRecorderError(`Audio recorder capture ${action} failed`, context, error);
}
removeOwner() {
if (this.ownerRemoved)
return;
this.ownerRemoved = true;
this.init.removeFromOwner();
}
emitTerminal(event, ...args) {
setTimeout(() => {
try {
EventEmitter.prototype.emit.call(this, event, ...args);
} finally {
this.closedResolve();
}
}, 0);
}
}
class Audio extends EventEmitter {
static create(options = {}) {
let lib;
try {
lib = resolveRenderLib();
} catch (cause) {
throw new AudioInitializationError("resolveRenderLib", "Failed to resolve the native audio library", undefined, cause);
}
return new Audio(lib, options);
}
sampleRate;
lib;
defaultStartOptions;
engine = null;
groups = new Map;
streams = new Set;
playbackStarted = false;
mixerStarted = false;
captureStarted = false;
captureDeviceOpen = false;
captureBufferAvailable = false;
captureChannels = 1;
captureCapacityFrames = 0;
captureOwner = null;
captureStream = null;
disposing = false;
constructor(lib, options) {
super();
this.lib = lib;
this.defaultStartOptions = options.startOptions;
const normalizedSampleRate = options.sampleRate == null || !Number.isFinite(options.sampleRate) ? 0 : Math.min(MAX_U32, Math.max(0, Math.trunc(options.sampleRate)));
this.sampleRate = normalizedSampleRate || DEFAULT_AUDIO_SAMPLE_RATE;
const createOptions = options.sampleRate == null && options.playbackChannels == null ? undefined : {
sampleRate: options.sampleRate == null ? undefined : normalizedSampleRate,
playbackChannels: options.playbackChannels == null ? undefined : Math.max(0, Math.trunc(options.playbackChannels))
};
this.engine = this.lib.createAudioEngine(createOptions);
if (!this.engine) {
throw new AudioInitializationError("createAudioEngine", "Audio createAudioEngine returned null");
}
if (options.autoStart ?? false) {
const status = this.lib.audioStart(this.engine, this.defaultStartOptions);
if (status !== 0) {
this.throwAfterInitializationCleanup(new AudioInitializationError("start", `Audio auto-start failed: ${status}`, status));
}
this.playbackStarted = true;
this.mixerStarted = true;
}
}
throwAfterInitializationCleanup(error) {
const engine2 = this.engine;
this.engine = null;
if (engine2)
this.lib.destroyAudioEngine(engine2);
throw error;
}
emitError(action, status, message, cause) {
const error = message ? new Error(message) : statusToError(action, status ?? -1);
if (cause)
error.cause = cause;
this.emit("error", error, { action, status });
}
start(options) {
if (this.playbackStarted)
return true;
const engine2 = this.engine;
if (!engine2) {
this.emitError("start", undefined, "Audio engine unavailable during start");
return false;
}
const startOptions = options ?? this.defaultStartOptions;
const status = this.lib.audioStart(engine2, startOptions);
if (status !== 0) {
this.emitError("start", status);
return false;
}
this.playbackStarted = true;
this.mixerStarted = true;
this.emit("started");
return true;
}
startMixer() {
if (this.mixerStarted)
return true;
const engine2 = this.engine;
if (!engine2) {
this.emitError("startMixer", undefined, "Audio engine unavailable during startMixer");
return false;
}
const status = this.lib.audioStartMixer(engine2);
if (status !== 0) {
this.emitError("startMixer", status);
return false;
}
this.mixerStarted = true;
this.emit("mixerStarted");
return true;
}
stop() {
if (!this.mixerStarted)
return true;
const engine2 = this.engine;
if (!engine2) {
this.emitError("stop", undefined, "Audio engine unavailable during stop");
return false;
}
const status = this.lib.audioStop(engine2);
if (status !== 0) {
this.emitError("stop", status);
return false;
}
this.playbackStarted = false;
this.mixerStarted = false;
this.emit("stopped");
return true;
}
isStarted() {
return this.playbackStarted;
}
isMixerStarted() {
return this.mixerStarted;
}
loadSound(data) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("loadSound", undefined, "Audio engine unavailable during loadSound");
return null;
}
const result = this.lib.audioLoad(engine2, toBytes(data));
if (result.status !== 0 || result.soundId == null) {
this.emitError("loadSound", result.status);
return null;
}
return result.soundId;
}
async loadSoundFile(filePath) {
const bytes = await readFile(filePath).catch((err) => {
this.emitError("loadSoundFile", undefined, `Failed to read file '${filePath}': ${err.message}`, err);
return null;
});
if (bytes == null)
return null;
return this.loadSound(bytes);
}
unloadSound(sound) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("unloadSound", undefined, "Audio engine unavailable during unloadSound");
return false;
}
const status = this.lib.audioUnload(engine2, sound);
if (status !== 0) {
this.emitError("unloadSound", status);
return false;
}
return true;
}
group(name) {
const existing = this.groups.get(name);
if (existing != null) {
return existing;
}
const engine2 = this.engine;
if (!engine2) {
this.emitError("group", undefined, "Audio engine unavailable during group");
return null;
}
const result = this.lib.audioCreateGroup(engine2, name);
if (result.status !== 0 || result.groupId == null) {
this.emitError("group", result.status);
return null;
}
this.groups.set(name, result.groupId);
return result.groupId;
}
play(sound, options) {
const rawOptions = options ? {
volume: options.volume,
pan: options.pan,
loop: options.loop,
groupId: options.groupId ?? 0
} : undefined;
const engine2 = this.engine;
if (!engine2) {
this.emitError("play", undefined, "Audio engine unavailable during play");
return null;
}
const result = this.lib.audioPlay(engine2, sound, rawOptions);
if (result.status !== 0 || result.voiceId == null) {
this.emitError("play", result.status);
return null;
}
return result.voiceId;
}
async playStream(source, options = {}) {
const urlOptions = options;
if (urlOptions.request !== undefined || urlOptions.reconnect !== undefined || urlOptions.metadataEncoding !== undefined || urlOptions.contentTypePolicy !== undefined) {
return Promise.reject(new TypeError("request, reconnect, metadataEncoding, and contentTypePolicy options are only supported by playStreamUrl()"));
}
if (!isReadableStreamSource(source) && !isAsyncIterableSource(source)) {
return Promise.reject(new TypeError("Audio stream source must be a ReadableStream or AsyncIterable"));
}
const { demuxer, ...streamOptions } = options;
const connector = {
async connect() {
return { body: source, info: undefined };
}
};
return this.openStream(connector, demuxer == null ? undefined : () => demuxer(), streamOptions, "source");
}
async playStreamUrl(source, options = {}) {
if (options.demuxer !== undefined) {
throw new TypeError("demuxer is only supported by playStream() and playStreamSource()");
}
if (typeof source !== "string" && Object.prototype.toString.call(source) !== "[object URL]") {
return Promise.reject(new TypeError("Audio stream URL source must be a string or URL"));
}
const { request, metadataEncoding, reconnect, format, contentTypePolicy, ...streamOptions } = options;
const resolvedFormat = resolveAudioStreamFormat(format);
const resolvedContentTypePolicy = resolveContentTypePolicy(contentTypePolicy, options);
const encoding = resolveMetadataEncoding(metadataEncoding);
const connector = createAudioStreamUrlConnector(source, resolveAudioStreamRequest(request), resolvedFormat, resolvedContentTypePolicy);
return this.openStream(connector, (info) => {
try {
return selectAudioStreamDemuxer({ headers: info.headers, metadataEncoding: encoding });
} catch (cause) {
throw new AudioStreamError(cause instanceof Error ? cause.message : "Invalid audio stream metadata response", { action: "response", status: info.status }, cause);
}
}, { ...streamOptions, reconnect, format: resolvedFormat }, "fetch");
}
async playStreamSource(connector, options = {}) {
const urlOptions = options;
if (urlOptions.request !== undefined || urlOptions.metadataEncoding !== undefined || urlOptions.contentTypePolicy !== undefined) {
return Promise.reject(new TypeError("request, metadataEncoding, and contentTypePolicy are only supported by playStreamUrl()"));
}
const { demuxer, ...streamOptions } = options;
return this.openStream(connector, demuxer, streamOptions, "source");
}
async openStream(connector, demuxer, options, readAction) {
const engine2 = this.engine;
if (!engine2)
throw new Error("Audio engine unavailable during stream playback");
const resolvedConnector = resolveAudioStreamConnector(connector);
let stream;
stream = createAudioStream({
lib: this.lib,
engine: engine2,
connector: resolvedConnector,
demuxer,
options,
readAction,
removeFromOwner: () => this.streams.delete(stream)
});
this.streams.add(stream);
try {
await openAudioStream(stream);
return stream;
} catch (error) {
stream.dispose();
throw error;
}
}
stopVoice(voice) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("stopVoice", undefined, "Audio engine unavailable during stopVoice");
return false;
}
const status = this.lib.audioStopVoice(engine2, voice);
if (status !== 0) {
this.emitError("stopVoice", status);
return false;
}
return true;
}
setVoiceGroup(voice, group) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("setVoiceGroup", undefined, "Audio engine unavailable during setVoiceGroup");
return false;
}
const status = this.lib.audioSetVoiceGroup(engine2, voice, group);
if (status !== 0) {
this.emitError("setVoiceGroup", status);
return false;
}
return true;
}
setGroupVolume(group, volume) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("setGroupVolume", undefined, "Audio engine unavailable during setGroupVolume");
return false;
}
const status = this.lib.audioSetGroupVolume(engine2, group, volume);
if (status !== 0) {
this.emitError("setGroupVolume", status);
return false;
}
return true;
}
setMasterVolume(volume) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("setMasterVolume", undefined, "Audio engine unavailable during setMasterVolume");
return false;
}
const status = this.lib.audioSetMasterVolume(engine2, volume);
if (status !== 0) {
this.emitError("setMasterVolume", status);
return false;
}
return true;
}
mixFrames(frameCount, channels = 2) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("mixFrames", undefined, "Audio engine unavailable during mixFrames");
return null;
}
const output = new Float32Array(frameCount * channels);
const status = this.lib.audioMixToBuffer(engine2, output, frameCount, channels);
if (status !== 0) {
this.emitError("mixFrames", status);
return null;
}
return output;
}
enableTap(capacityFrames = 8192) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("enableTap", undefined, "Audio engine unavailable during enableTap");
return false;
}
const status = this.lib.audioEnableTap(engine2, true, capacityFrames);
if (status !== 0) {
this.emitError("enableTap", status);
return false;
}
return true;
}
disableTap() {
const engine2 = this.engine;
if (!engine2) {
this.emitError("enableTap", undefined, "Audio engine unavailable during disableTap");
return false;
}
const status = this.lib.audioEnableTap(engine2, false, 0);
if (status !== 0) {
this.emitError("enableTap", status);
return false;
}
return true;
}
readTapFrames(frameCount, channels = 2) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("readTapFrames", undefined, "Audio engine unavailable during readTapFrames");
return null;
}
const output = new Float32Array(frameCount * channels);
const result = this.lib.audioReadTap(engine2, output, frameCount, channels);
if (result.status !== 0) {
this.emitError("readTapFrames", result.status);
return null;
}
return { frames: output, framesRead: result.framesRead };
}
listPlaybackDevices() {
const engine2 = this.engine;
if (!engine2) {
this.emitError("listPlaybackDevices", undefined, "Audio engine unavailable during listPlaybackDevices");
return null;
}
const refreshStatus = this.lib.audioRefreshPlaybackDevices(engine2);
if (refreshStatus !== 0) {
this.emitError("listPlaybackDevices", refreshStatus);
return null;
}
const count = this.lib.audioGetPlaybackDeviceCount(engine2);
const devices = [];
for (let index = 0;index < count; index += 1) {
devices.push({
index,
name: this.lib.audioGetPlaybackDeviceName(engine2, index),
isDefault: this.lib.audioIsPlaybackDeviceDefault(engine2, index)
});
}
return devices;
}
selectPlaybackDevice(index) {
const engine2 = this.engine;
if (!engine2) {
this.emitError("selectPlaybackDevice", undefined, "Audio engine unavailable during selectPlaybackDevice");
return false;
}
const refreshStatus = this.lib.audioRefreshPlaybackDevices(engine2);
if (refreshStatus !== 0) {
this.emitError("selectPlaybackDevice", refreshStatus);
return false;
}
const status = this.lib.audioSelectPlaybackDevice(engine2, index);
if (status !== 0) {
this.emitError("selectPlaybackDevice", status);
return false;
}
return true;
}
clearPlaybackDeviceSelection() {
const engine2 = this.engine;
if (!engine2) {
this.emitError("clearPlaybackDeviceSelection", undefined, "Audio engine unavailable during clearPlaybackDeviceSelection");
return;
}
this.lib.audioClearPlaybackDeviceSelection(engine2);
}
async openCapture(options = {}) {
const resolved = resolveAudioCaptureStreamOptions(options, this.sampleRate);
if (resolved.signal?.aborted)
throw createAbortError();
if (!this.engine) {
throw new AudioCaptureStreamError("Audio engine unavailable during capture stream start", { action: "start" });
}
if (this.captureOwner != null || this.isCapturing()) {
throw new AudioCaptureStreamError("Audio capture ring is already in use", { action: "start" });
}
const owner = {};
this.captureOwner = owner;
let stream = null;
try {
stream = createAudioCaptureStream({
options: resolved,
start: () => this.startCaptureInternal(resolved, owner),
read: (frameCount) => this.readCaptureInternal(frameCount, owner),
stats: () => this.getCaptureStatsInternal(owner),
stop: () => this.stopCaptureInternal(owner),
isRunning: () => this.isCapturingInternal(owner),
removeFromOwner: () => {
if (this.captureOwner === owner)
this.captureOwner = null;
if (this.captureStream === stream)
this.captureStream = null;
if (stream != null)
this.streams.delete(stream);
}
});
this.captureStream = stream;
this.streams.add(stream);
await openAudioCaptureStream(stream);
return stream;
} catch (error) {
if (stream == null) {
if (this.captureOwner === owner)
this.captureOwner = null;
} else {
stream.dispose();
if (this.captureOwner !== owner)
this.streams.delete(stream);
}
throw error;
}
}
async recordToFile(filePath, options = {}) {
if (typeof filePath !== "string" || filePath.length === 0)
throw new TypeError("filePath must be a nonempty string");
if (filePath.includes("\x00"))
throw new TypeError("filePath must not contain NUL bytes");
const resolved = resolveAudioCaptureStreamOptions(options, this.sampleRate);
if (resolved.channels !== 1 && resolved.channels !== 2) {
throw new RangeError("WAV recording supports only 1 or 2 channels");
}
if (this.sampleRate * resolved.channels * 2 > MAX_U32) {
throw new RangeError("WAV byte rate exceeds the supported limit");
}
if (resolved.signal?.aborted)
throw createAbortError();
if (!this.engine)
throw new AudioRecorderError("Audio engine unavailable during recorder open", { action: "open" });
let recorder;
recorder = createAudioRecorder({
filePath,
signal: resolved.signal,
captureOptions: resolved,
openCapture: (captureOptions) => this.openCapture(captureOptions),
removeFromOwner: () => this.streams.delete(recorder)
});
this.streams.add(recorder);
try {
await openAudioRecorder(recorder);
return recorder;
} catch (error) {
throw error;
}
}
listCaptureDevices() {
const engine2 = this.engine;
if (!engine2) {
this.emitError("listCaptureDevices", undefined, "Audio engine unavailable during listCaptureDevices");
return null;
}
const refreshStatus = this.lib.audioRefreshCaptureDevices(engine2);
if (refreshStatus !== 0) {
this.emitError("listCaptureDevices", refreshStatus);
return null;
}
const count = this.lib.audioGetCaptureDeviceCount(engine2);
const devices = [];
for (let index = 0;index < count; index += 1) {
devices.push({
index,
name: this.lib.audioGetCaptureDeviceName(engine2, index),
isDefault: this.lib.audioIsCaptureDeviceDefault(engine2, index)
});
}
return devices;
}
selectCaptureDevice(index) {
const resolvedIndex = resolveU32Index(index, "index");
if (this.captureOwner != null) {
this.emitCaptureOwnershipError("selectCaptureDevice");
return false;
}
const engine2 = this.engine;
if (!engine2) {
this.emitError("selectCaptureDevice", undefined, "Audio engine unavailable during selectCaptureDevice");
return false;
}
const status = this.lib.audioSelectCaptureDevice(engine2, resolvedIndex);
if (status !== 0) {
this.emitError("selectCaptureDevice", status);
return false;
}
return true;
}
clearCaptureDeviceSelection() {
if (this.captureOwner != null) {
this.emitCaptureOwnershipError("clearCaptureDeviceSelection");
return;
}
const engine2 = this.engine;
if (!engine2) {
this.emitError("clearCaptureDeviceSelection", undefined, "Audio engine unavailable during clearCaptureDeviceSelection");
return;
}
this.lib.audioClearCaptureDeviceSelection(engine2);
}
startCapture(options = {}) {
if (this.disposing)
return false;
const channels = resolvePositiveU32(options.channels, 1, "channels");
const capacityFrames = resolvePositiveU32(options.capacityFrames, this.sampleRate, "capacityFrames");
if (this.captureOwner != null) {
this.emitCaptureOwnershipError("startCapture");
return false;
}
if (this.isCapturing()) {
const configurationMatches = (options.channels === undefined || channels === this.captureChannels) && (options.capacityFrames === undefined || capacityFrames === this.captureCapacityFrames) && options.startOptions === undefined;
if (configurationMatches)
return true;
this.emitError("startCapture", undefined, "Audio capture is already running with a different configuration");
return false;
}
const engine2 = this.engine;
if (!engine2) {
this.emitError("startCapture", undefined, "Audio engine unavailable during startCapture");
return false;
}
const result = this.startCaptureInternal({ sampleRate: this.sampleRate, channels, capacityFrames, chunkFrames: 1, startOptions: options.startOptions }, null);
if (result.status !== 0) {
this.emitError("startCapture", result.status, undefined, result.cause);
return false;
}
return true;
}
isCapturing() {
return this.isCapturingInternal(null);
}
isCapturingInternal(_owner) {
if (!this.captureStarted)
return false;
const engine2 = this.engine;
if (engine2 && this.lib.audioIsCaptureRunning(engine2))
return true;
this.captureStarted = false;
this.emit("captureStopped");
return this.captureStarted;
}
readCaptureFrames(frameCount) {
const resolvedFrameCount = resolvePositiveU32(frameCount, frameCount, "frameCount");
if (this.captureOwner != null) {
this.emitCaptureOwnershipError("readCaptureFrames");
return null;
}
if (!this.captureBufferAvailable) {
this.emitError("readCaptureFrames", -4);
return null;
}
if (resolvedFrameCount > this.captureCapacityFrames) {
throw new RangeError("frameCount exceeds the capture buffer capacity");
}
if (resolvedFrameCount > Math.floor(MAX_U32 / this.captureChannels)) {
throw new RangeError("frameCount * channels exceeds the supported limit");
}
const engine2 = this.engine;
if (!engine2) {
this.emitError("readCaptureFrames", undefined, "Audio engine unavailable during readCaptureFrames");
return null;
}
const result = this.readCaptureInternal(resolvedFrameCount, null);
if (result.status !== 0) {
this.emitError("readCaptureFrames", result.status, undefined, result.cause);
return null;
}
return { frames: result.frames, framesRead: result.framesRead };
}
getCaptureStats() {
const engine2 = this.engine;
if (!engine2) {
this.emitError("getCaptureStats", undefined, "Audio engine unavailable during getCaptureStats");
return null;
}
const result = this.getCaptureStatsInternal(null);
if (result.status !== 0 || result.stats == null) {
this.emitError("getCaptureStats", result.status, undefined, result.cause);
return null;
}
return result.stats;
}
stopCapture() {
if (this.captureOwner != null) {
this.emitCaptureOwnershipError("stopCapture");
return false;
}
if (!this.captureDeviceOpen)
return true;
const engine2 = this.engine;
if (!engine2) {
this.emitError("stopCapture", undefined, "Audio engine unavailable during stopCapture");
return false;
}
const result = this.stopCaptureInternal(null);
if (result.status !== 0) {
this.emitError("stopCapture", result.status, undefined, result.cause);
return false;
}
return true;
}
emitCaptureOwnershipError(action) {
this.emitError(action, undefined, `Audio ${action} failed: capture is owned by a stream`);
}
startCaptureInternal(options, owner) {
if (this.captureOwner != null && this.captureOwner !== owner)
return { status: -1 };
const engine2 = this.engine;
if (!engine2)
return { status: -1 };
let status;
try {
status = this.lib.audioStartCapture(engine2, options.startOptions, options.channels, options.capacityFrames);
} catch (cause) {
return { status: -1, cause };
}
if (status !== 0)
return { status };
this.captureChannels = options.channels;
this.captureCapacityFrames = options.capacityFrames;
this.captureBufferAvailable = true;
this.captureDeviceOpen = true;
this.captureStarted = true;
this.emit("captureStarted");
return { status: 0 };
}
readCaptureInternal(frameCount, owner) {
const output = new Float32Array(frameCount * this.captureChannels);
if (this.captureOwner != null && this.captureOwner !== owner)
return { status: -1, frames: output, framesRead: 0 };
const engine2 = this.engine;
if (!engine2 || !this.captureBufferAvailable)
return { status: -4, frames: output, framesRead: 0 };
try {
const result = this.lib.audioReadCapture(engine2, output, frameCount);
return { status: result.status, frames: output, framesRead: Math.min(frameCount, result.framesRead) };
} catch (cause) {
return { status: -1, frames: output, framesRead: 0, cause };
}
}
getCaptureStatsInternal(_owner) {
const engine2 = this.engine;
if (!engine2)
return { status: -1, stats: null };
try {
const result = this.lib.audioGetCaptureStats(engine2);
if (result.status !== 0 || result.stats == null)
return { status: result.status, stats: null };
return {
status: 0,
stats: {
sampleRate: result.stats.sampleRate,
channels: result.stats.channels,
capacityFrames: result.stats.capacityFrames,
bufferedFrames: result.stats.bufferedFrames,
framesReceived: result.stats.framesReceived,
framesRead: result.stats.framesRead,
framesDropped: result.stats.framesDropped
}
};
} catch (cause) {
return { status: -1, stats: null, cause };
}
}
stopCaptureInternal(owner) {
if (this.captureOwner != null && this.captureOwner !== owner)
return { status: -1 };
if (!this.captureDeviceOpen)
return { status: 0 };
const engine2 = this.engine;
if (!engine2)
return { status: -1 };
let status;
try {
status = this.lib.audioStopCapture(engine2);
} catch (cause) {
return { status: -1, cause };
}
if (status !== 0)
return { status };
const wasStarted = this.captureStarted;
this.captureDeviceOpen = false;
this.captureStarted = false;
if (wasStarted)
this.emit("captureStopped");
return { status: 0 };
}
getStats() {
const engine2 = this.engine;
if (!engine2) {
this.emitError("getStats", undefined, "Audio engine unavailable during getStats");
return null;
}
const stats = this.lib.audioGetStats(engine2);
if (stats == null) {
this.emitError("getStats", undefined, "Failed to retrieve audio stats");
}
return stats;
}
dispose() {
if (!this.engine || this.disposing)
return;
this.disposing = true;
let firstError;
let hasError = false;
let childCleanupFailed = false;
const runCleanup = (operation) => {
try {
operation();
} catch (error) {
if (!hasError) {
firstError = error;
hasError = true;
}
}
};
try {
for (const stream of [...this.streams]) {
try {
stream.dispose();
} catch (error) {
childCleanupFailed = true;
if (!hasError) {
firstError = error;
hasError = true;
}
}
}
if (this.captureDeviceOpen) {
let result;
runCleanup(() => {
result = this.stopCaptureInternal(this.captureOwner);
});
if (result && result.status !== 0) {
const wasStarted = this.captureStarted;
this.captureDeviceOpen = false;
this.captureStarted = false;
runCleanup(() => this.emitError("stopCapture", result.status, undefined, result.cause));
if (wasStarted)
runCleanup(() => void this.emit("captureStopped"));
}
}
if (this.captureStream != null) {
runCleanup(() => refreshAudioCaptureStreamFinalStats(this.captureStream));
}
if (this.mixerStarted) {
runCleanup(() => void this.stop());
}
this.groups.clear();
const engine2 = this.engine;
let engineDestroyed = false;
if (!childCleanupFailed) {
runCleanup(() => {
this.lib.destroyAudioEngine(engine2);
engineDestroyed = true;
});
}
if (engineDestroyed) {
this.engine = null;
this.captureStarted = false;
this.captureDeviceOpen = false;
this.captureBufferAvailable = false;
this.captureCapacityFrames = 0;
this.captureOwner = null;
this.captureStream = null;
runCleanup(() => void this.emit("disposed"));
}
} finally {
this.disposing = false;
}
if (hasError)
throw firstError;
}
}
function setupAudio(options = {}) {
return Audio.create(options);
}
// src/image.ts
import { open, stat } from "node:fs/promises";
class ImageLoadError extends Error {
code;
source;
status;
constructor(code, source, message, options) {
super(message, { cause: options?.cause });
this.name = "ImageLoadError";
this.code = code;
this.source = source;
this.status = options?.status;
}
}
class OwnedRawImageImpl {
data;
width;
height;
stride;
lib;
handle;
format = "rgba8";
colorSpace = "srgb";
alpha = "straight";
constructor(data, width, height, stride, lib, handle) {
this.data = data;
this.width = width;
this.height = height;
this.stride = stride;
this.lib = lib;
this.handle = handle;
}
dispose() {
if (!this.handle)
return;
this.lib.imageDestroy(this.handle);
this.handle = null;
}
}
var STATUS_MESSAGES = [
"ok",
"invalid image handle",
"unsupported image format",
"unsupported image color space",
"malformed image data",
"image dimensions exceed limits",
"image memory limit exceeded",
"invalid image argument",
"out of memory",
"image output buffer is too small",
"internal image error",
"unsupported image feature"
];
var STATUS_CODES = [
"internal-error",
"invalid-handle",
"unsupported-format",
"unsupported-color-space",
"malformed-data",
"dimension-limit",
"memory-limit",
"invalid-argument",
"out-of-memory",
"output-too-small",
"internal-error",
"unsupported-feature"
];
var INVALID_ARGUMENT_STATUS = 7;
class ImageError extends Error {
code;
status;
constructor(status) {
super(`Native image operation failed: ${STATUS_MESSAGES[status] ?? `unknown status ${status}`}`);
this.name = "ImageError";
this.status = status;
this.code = STATUS_CODES[status] ?? "internal-error";
}
}
var FILTER_IDS = {
default: 0,
area: 1,
triangle: 2,
"cubic-bspline": 3,
"catmull-rom": 4,
mitchell: 5,
nearest: 6
};
var BLEND_IDS = {
"source-over": 0,
source: 1,
"destination-over": 2
};
var PIXEL_FORMAT_BGRA = {
rgba8: false,
bgra8: true
};
var MAX_ENCODED_BYTES = 64 * 1024 * 1024;
function imageError(status) {
return new ImageError(status);
}
function checkStatus(status) {
if (status !== 0)
throw imageError(status);
}
function requireMappedOption(mapping, value, name) {
if (!Object.prototype.hasOwnProperty.call(mapping, value))
throw new TypeError(`Unsupported ${name}: ${String(value)}`);
return mapping[value];
}
function requireU32(value, name, allowZero = false) {
if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1) || value > 4294967295) {
throw new RangeError(`${name} must be ${allowZero ? "a non-negative" : "a positive"} u32 integer`);
}
return value;
}
function requireI32(value, name) {
if (!Number.isSafeInteger(value) || value < -2147483648 || value > 2147483647) {
throw new RangeError(`${name} must be an i32 integer`);
}
return value;
}
function requireByte(value, name) {
if (!Number.isInteger(value) || value < 0 || value > 255)
throw new RangeError(`${name} must be an integer from 0 to 255`);
return value;
}
function unpackInfo(info) {
const format = ["unknown", "png", "raw-rgba", "jpeg", "webp", "gif"][info.format];
if (!format || format === "unknown")
throw new Error(`Unknown native image format ${info.format}`);
return {
width: info.width,
height: info.height,
sourceWidth: info.sourceWidth,
sourceHeight: info.sourceHeight,
format,
colorStatus: info.colorStatus === 1 ? "explicit-srgb" : "assumed-srgb",
orientation: info.orientation,
hasAlpha: info.hasAlpha !== 0
};
}
function encodedBytes(data) {
if (data instanceof Uint8Array)
return data;
if (data instanceof ArrayBuffer)
return new Uint8Array(data);
throw new TypeError("image data must be a Uint8Array or ArrayBuffer");
}
async function readResponseBytes(response, signal) {
const contentLength = response.headers.get("content-length");
if (contentLength !== null) {
const declaredLength = Number(contentLength);
if (Number.isFinite(declaredLength) && declaredLength > MAX_ENCODED_BYTES) {
response.body?.cancel().catch(() => {});
throw imageError(6);
}
}
if (!response.body)
return new Uint8Array;
const reader = response.body.getReader();
const abort = () => void reader.cancel(signal?.reason).catch(() => {});
signal?.addEventListener("abort", abort, { once: true });
let data = new Uint8Array;
let total = 0;
try {
while (true) {
signal?.throwIfAborted();
const { done, value } = await reader.read();
if (done)
break;
if (value.byteLength > MAX_ENCODED_BYTES - total) {
throw imageError(6);
}
if (value.byteLength === 0)
continue;
const required = total + value.byteLength;
if (required > data.byteLength) {
const capacity = Math.min(MAX_ENCODED_BYTES, Math.max(required, data.byteLength * 2));
const grown = new Uint8Array(capacity);
grown.set(data.subarray(0, total));
data = grown;
}
data.set(value, total);
total = required;
}
} catch (error) {
reader.cancel().catch(() => {});
throw error;
} finally {
signal?.removeEventListener("abort", abort);
reader.releaseLock();
}
return data.byteLength === total ? data : data.slice(0, total);
}
async function readFileBytes(path, signal) {
signal?.throwIfAborted();
if ((await stat(path)).size > MAX_ENCODED_BYTES)
throw imageError(6);
const file = await open(path, "r");
const chunks = [];
let total = 0;
try {
while (true) {
signal?.throwIfAborted();
const chunk = new Uint8Array(Math.min(64 * 1024, MAX_ENCODED_BYTES - total + 1));
const { bytesRead } = await file.read(chunk, 0, chunk.byteLength, null);
if (bytesRead === 0)
break;
total += bytesRead;
if (total > MAX_ENCODED_BYTES)
throw imageError(6);
chunks.push(chunk.subarray(0, bytesRead));
}
} finally {
await file.close();
}
const data = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
data.set(chunk, offset);
offset += chunk.byteLength;
}
return data;
}
async function loadResponseBytes(response, source, signal) {
try {
signal?.throwIfAborted();
} catch (error) {
response.body?.cancel().catch(() => {});
throw error;
}
if (!response.ok) {
response.body?.cancel().catch(() => {});
throw new ImageLoadError("http-status", source, `Failed to fetch image: HTTP ${response.status}`, {
status: response.status
});
}
try {
const data = await readResponseBytes(response, signal);
signal?.throwIfAborted();
return data;
} catch (error) {
if (signal?.aborted)
throw signal.reason;
if (error instanceof ImageError)
throw error;
throw new ImageLoadError("network", source, `Failed to read image response: ${source}`, { cause: error });
}
}
function imageInfo(data) {
const bytes = encodedBytes(data);
if (bytes.byteLength === 0)
throw new TypeError("image data must not be empty");
const result = resolveRenderLib().imageInfo(bytes);
checkStatus(result.status);
return unpackInfo(result.info);
}
class NativeImage {
lib;
handle;
imageInfo;
constructor(lib, handle, info) {
this.lib = lib;
this.handle = handle;
this.imageInfo = info;
}
static decode(data) {
const bytes = encodedBytes(data);
if (bytes.byteLength === 0)
throw new TypeError("image data must not be empty");
const lib = resolveRenderLib();
const result = lib.imageDecode(bytes);
checkStatus(result.status);
if (!result.handle)
throw imageError(10);
return NativeImage.fromHandle(lib, result.handle);
}
static async load(source, options = {}) {
if (source instanceof Response) {
return NativeImage.decode(await loadResponseBytes(source, source.url || "Response", options.signal));
}
options.signal?.throwIfAborted();
if (source instanceof Uint8Array || source instanceof ArrayBuffer)
return NativeImage.decode(source);
if (source instanceof Blob) {
if (source.size > MAX_ENCODED_BYTES)
throw imageError(6);
return NativeImage.decode(await loadResponseBytes(new Response(source), "Blob", options.signal));
}
const url = source instanceof URL ? source : (/^(?:https?|file|blob|data):/i.test(source) || /^[a-z][a-z0-9+.-]*:\/\//i.test(source)) && !/^[a-z]:[\\/]/i.test(source) ? new URL(source) : null;
if (!url || url.protocol === "file:") {
const path = url ?? source;
let data;
try {
data = await readFileBytes(path, options.signal);
} catch (error) {
if (options.signal?.aborted)
throw options.signal.reason;
if (error instanceof ImageError)
throw error;
throw new ImageLoadError("file-read", String(source), `Failed to read image: ${String(source)}`, {
cause: error
});
}
options.signal?.throwIfAborted();
return NativeImage.decode(data);
}
if (url.protocol !== "http:" && url.protocol !== "https:" && url.protocol !== "blob:" && url.protocol !== "data:") {
throw new ImageLoadError("unsupported-url-scheme", url.href, `Unsupported image URL scheme: ${url.protocol}`);
}
let response;
try {
response = await (options.fetch ?? globalThis.fetch)(url, { signal: options.signal });
} catch (error) {
if (options.signal?.aborted)
throw options.signal.reason;
throw new ImageLoadError("network", url.href, `Failed to fetch image: ${url.href}`, { cause: error });
}
return NativeImage.decode(await loadResponseBytes(response, url.href, options.signal));
}
static fromRgba(pixels, width, height, stride = width * 4) {
if (!(pixels instanceof Uint8Array))
throw new TypeError("pixels must be a Uint8Array");
requireU32(width, "width");
requireU32(height, "height");
requireU32(stride, "stride");
const lib = resolveRenderLib();
const result = lib.imageCreateFromRgba(pixels, width, height, stride);
checkStatus(result.status);
if (!result.handle)
throw imageError(10);
return NativeImage.fromHandle(lib, result.handle);
}
static fromHandle(lib, handle) {
const result = lib.imageGetInfo(handle);
if (result.status !== 0) {
lib.imageDestroy(handle);
throw imageError(result.status);
}
return new NativeImage(lib, handle, unpackInfo(result.info));
}
guard() {
if (!this.handle)
throw new Error("NativeImage is disposed");
return this.handle;
}
get ptr() {
return this.guard();
}
wrap(result) {
checkStatus(result.status);
if (!result.handle)
throw imageError(10);
return NativeImage.fromHandle(this.lib, result.handle);
}
info() {
this.guard();
return { ...this.imageInfo };
}
get width() {
this.guard();
return this.imageInfo.width;
}
get height() {
this.guard();
return this.imageInfo.height;
}
clone() {
return this.wrap(this.lib.imageClone(this.guard()));
}
retain() {
return this.wrap(this.lib.imageRetain(this.guard()));
}
resize(options) {
if (!options || options.width === undefined && options.height === undefined) {
throw new TypeError("resize requires width, height, or both");
}
let width = options.width;
let height = options.height;
if (width !== undefined)
requireU32(width, "width");
if (height !== undefined)
requireU32(height, "height");
if (width === undefined)
width = Math.max(1, Math.round(this.width * height / this.height));
if (height === undefined)
height = Math.max(1, Math.round(this.height * width / this.width));
requireU32(width, "width");
requireU32(height, "height");
const filter = requireMappedOption(FILTER_IDS, options.kernel ?? "area", "resize kernel");
return this.wrap(this.lib.imageResize(this.guard(), width, height, filter));
}
extract(options) {
return this.wrap(this.lib.imageExtract(this.guard(), requireU32(options.left, "left", true), requireU32(options.top, "top", true), requireU32(options.width, "width"), requireU32(options.height, "height")));
}
extend(options = {}) {
const background = options.background ?? [0, 0, 0, 0];
if (background.length !== 4)
throw new TypeError("background must contain four RGBA channels");
const color = Uint8Array.from(background.map((value, index) => requireByte(value, `background[${index}]`)));
return this.wrap(this.lib.imageExtend(this.guard(), requireU32(options.top ?? 0, "top", true), requireU32(options.right ?? 0, "right", true), requireU32(options.bottom ?? 0, "bottom", true), requireU32(options.left ?? 0, "left", true), color));
}
rotate(angle) {
const operation = angle === 90 ? 0 : angle === 180 ? 1 : angle === 270 ? 2 : -1;
if (operation < 0)
throw new RangeError("angle must be 90, 180, or 270");
return this.wrap(this.lib.imageTransform(this.guard(), operation));
}
flip() {
return this.wrap(this.lib.imageTransform(this.guard(), 3));
}
flop() {
return this.wrap(this.lib.imageTransform(this.guard(), 4));
}
composite(overlay, options = {}) {
if (!(overlay instanceof NativeImage))
throw new TypeError("overlay must be a NativeImage");
const opacity = options.opacity ?? 1;
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1)
throw new RangeError("opacity must be between 0 and 1");
return this.wrap(this.lib.imageComposite(this.guard(), overlay.guard(), requireI32(options.left ?? 0, "left"), requireI32(options.top ?? 0, "top"), requireMappedOption(BLEND_IDS, options.blend ?? "source-over", "blend mode"), Math.round(opacity * 255)));
}
ensureEncodedPng() {
checkStatus(this.lib.imageEnsureEncodedPng(this.guard()));
}
raw(format = "rgba8") {
const stride = this.width * 4;
const data = new Uint8Array(stride * this.height);
checkStatus(this.lib.imageCopyPixels(this.guard(), data, stride, requireMappedOption(PIXEL_FORMAT_BGRA, format, "pixel format")));
return { data, width: this.width, height: this.height, stride, format, colorSpace: "srgb", alpha: "straight" };
}
takeRaw() {
const handle = this.guard();
const materializeStatus = this.lib.imageMaterialize(handle);
if (materializeStatus === INVALID_ARGUMENT_STATUS) {
throw new Error("Cannot transfer image pixels while native buffers retain the image");
}
checkStatus(materializeStatus);
const pointer = this.lib.imageGetPixelsPtr(handle);
if (!pointer)
throw new Error("Cannot transfer image pixels while native buffers retain the image");
const width = this.imageInfo.width;
const height = this.imageInfo.height;
const stride = width * 4;
const data = new Uint8Array(toArrayBuffer(pointer, 0, stride * height));
const raw = new OwnedRawImageImpl(data, width, height, stride, this.lib, handle);
this.handle = null;
return raw;
}
copyTo(destination, options = {}) {
if (!(destination instanceof Uint8Array))
throw new TypeError("destination must be a Uint8Array");
const stride = options.stride ?? this.width * 4;
requireU32(stride, "stride");
const bgra = requireMappedOption(PIXEL_FORMAT_BGRA, options.format ?? "rgba8", "pixel format");
checkStatus(this.lib.imageCopyPixels(this.guard(), destination, stride, bgra));
}
dispose() {
if (!this.handle)
return;
this.lib.imageDestroy(this.handle);
this.handle = null;
}
}
// src/renderables/FrameBuffer.ts
class FrameBufferRenderable extends Renderable {
frameBuffer;
respectAlpha;
constructor(ctx, options) {
super(ctx, options);
this.respectAlpha = options.respectAlpha || false;
this.frameBuffer = OptimizedBuffer.create(options.width, options.height, this._ctx.widthMethod, {
respectAlpha: this.respectAlpha,
id: options.id || `framebufferrenderable-${this.id}`
});
}
onResize(width, height) {
if (width <= 0 || height <= 0) {
throw new Error(`Invalid resize dimensions for FrameBufferRenderable ${this.id}: ${width}x${height}`);
}
this.frameBuffer.resize(width, height);
super.onResize(width, height);
this.requestRender();
}
renderSelf(buffer) {
if (!this.visible || this.isDestroyed)
return;
buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer);
}
destroySelf() {
this.frameBuffer?.destroy();
super.destroySelf();
}
}
// src/renderables/ASCIIFont.ts
class ASCIIFontRenderable extends FrameBufferRenderable {
selectable = true;
static _defaultOptions = {
text: "",
font: "tiny",
color: "#FFFFFF",
backgroundColor: "transparent",
selectionBg: undefined,
selectionFg: undefined,
selectable: true
};
_text;
_font;
_color;
_backgroundColor;
_selectionBg;
_selectionFg;
lastLocalSelection = null;
selectionHelper;
constructor(ctx, options) {
const defaultOptions = ASCIIFontRenderable._defaultOptions;
const font = options.font || defaultOptions.font;
const text = options.text || defaultOptions.text;
const measurements = measureText({ text, font });
super(ctx, {
flexShrink: 0,
...options,
width: measurements.width || 1,
height: measurements.height || 1,
respectAlpha: true
});
this._text = text;
this._font = font;
this._color = options.color || defaultOptions.color;
this._backgroundColor = options.backgroundColor || defaultOptions.backgroundColor;
this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined;
this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined;
this.selectable = options.selectable ?? true;
this.selectionHelper = new ASCIIFontSelectionHelper(() => this._text, () => this._font);
this.renderFontToBuffer();
}
get text() {
return this._text;
}
set text(value) {
this._text = value;
this.updateDimensions();
if (this.lastLocalSelection) {
this.selectionHelper.onLocalSelectionChanged(this.lastLocalSelection, this.width, this.height);
}
this.renderFontToBuffer();
this.requestRender();
}
get font() {
return this._font;
}
set font(value) {
this._font = value;
this.updateDimensions();
if (this.lastLocalSelection) {
this.selectionHelper.onLocalSelectionChanged(this.lastLocalSelection, this.width, this.height);
}
this.renderFontToBuffer();
this.requestRender();
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.renderFontToBuffer();
this.requestRender();
}
get backgroundColor() {
return this._backgroundColor;
}
set backgroundColor(value) {
this._backgroundColor = value;
this.renderFontToBuffer();
this.requestRender();
}
updateDimensions() {
const measurements = measureText({ text: this._text, font: this._font });
this.width = measurements.width;
this.height = measurements.height;
}
shouldStartSelection(x, y) {
const localX = x - this.x;
const localY = y - this.y;
return this.selectionHelper.shouldStartSelection(localX, localY, this.width, this.height);
}
onSelectionChanged(selection) {
const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y);
this.lastLocalSelection = localSelection;
const changed = this.selectionHelper.onLocalSelectionChanged(localSelection, this.width, this.height);
if (changed) {
this.renderFontToBuffer();
this.requestRender();
}
return this.selectionHelper.hasSelection();
}
getSelectedText() {
const selection = this.selectionHelper.getSelection();
if (!selection)
return "";
return this._text.slice(selection.start, selection.end);
}
hasSelection() {
return this.selectionHelper.hasSelection();
}
onResize(width, height) {
super.onResize(width, height);
this.renderFontToBuffer();
}
renderFontToBuffer() {
if (this.isDestroyed)
return;
this.frameBuffer.clear(parseColor(this._backgroundColor));
renderFontToFrameBuffer(this.frameBuffer, {
text: this._text,
x: 0,
y: 0,
color: this.color,
backgroundColor: this._backgroundColor,
font: this._font
});
const selection = this.selectionHelper.getSelection();
if (selection && (this._selectionBg || this._selectionFg)) {
this.renderSelectionHighlight(selection);
}
}
renderSelectionHighlight(selection) {
if (!this._selectionBg && !this._selectionFg)
return;
const selectedText = this._text.slice(selection.start, selection.end);
if (!selectedText)
return;
const positions = getCharacterPositions(this._text, this._font);
const startX = positions[selection.start] || 0;
const endX = selection.end < positions.length ? positions[selection.end] : measureText({ text: this._text, font: this._font }).width;
if (this._selectionBg) {
this.frameBuffer.fillRect(startX, 0, endX - startX, this.height, parseColor(this._selectionBg));
}
if (this._selectionFg || this._selectionBg) {
renderFontToFrameBuffer(this.frameBuffer, {
text: selectedText,
x: startX,
y: 0,
color: this._selectionFg ? this._selectionFg : this._color,
backgroundColor: this._selectionBg ? this._selectionBg : this._backgroundColor,
font: this._font
});
}
}
}
// src/renderables/composition/constructs.ts
function Generic(props, ...children) {
return h(VRenderable, props || {}, ...children);
}
function Box(props, ...children) {
return h(BoxRenderable, props || {}, ...children);
}
function Text(props, ...children) {
return h(TextRenderable, props || {}, ...children);
}
function ASCIIFont(props, ...children) {
return h(ASCIIFontRenderable, props || {}, ...children);
}
function Input(props, ...children) {
return h(InputRenderable, props || {}, ...children);
}
function Select(props, ...children) {
return h(SelectRenderable, props || {}, ...children);
}
function TabSelect(props, ...children) {
return h(TabSelectRenderable, props || {}, ...children);
}
function FrameBuffer(props, ...children) {
return h(FrameBufferRenderable, props, ...children);
}
function Code(props, ...children) {
return h(CodeRenderable, props, ...children);
}
function ScrollBox(props, ...children) {
return h(ScrollBoxRenderable, props || {}, ...children);
}
function StyledText2(props, ...children) {
const styledProps = props;
const textNodeOptions = {
...styledProps,
attributes: styledProps?.attributes ?? 0
};
const textNode = new TextNodeRenderable(textNodeOptions);
for (const child of children) {
textNode.add(child);
}
return textNode;
}
var vstyles = {
bold: (...children) => StyledText2({ attributes: TextAttributes.BOLD }, ...children),
italic: (...children) => StyledText2({ attributes: TextAttributes.ITALIC }, ...children),
underline: (...children) => StyledText2({ attributes: TextAttributes.UNDERLINE }, ...children),
dim: (...children) => StyledText2({ attributes: TextAttributes.DIM }, ...children),
blink: (...children) => StyledText2({ attributes: TextAttributes.BLINK }, ...children),
inverse: (...children) => StyledText2({ attributes: TextAttributes.INVERSE }, ...children),
hidden: (...children) => StyledText2({ attributes: TextAttributes.HIDDEN }, ...children),
strikethrough: (...children) => StyledText2({ attributes: TextAttributes.STRIKETHROUGH }, ...children),
boldItalic: (...children) => StyledText2({ attributes: TextAttributes.BOLD | TextAttributes.ITALIC }, ...children),
boldUnderline: (...children) => StyledText2({ attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE }, ...children),
italicUnderline: (...children) => StyledText2({ attributes: TextAttributes.ITALIC | TextAttributes.UNDERLINE }, ...children),
boldItalicUnderline: (...children) => StyledText2({ attributes: TextAttributes.BOLD | TextAttributes.ITALIC | TextAttributes.UNDERLINE }, ...children),
color: (color, ...children) => StyledText2({ fg: color }, ...children),
bgColor: (bgColor, ...children) => StyledText2({ bg: bgColor }, ...children),
fg: (color, ...children) => StyledText2({ fg: color }, ...children),
bg: (bgColor, ...children) => StyledText2({ bg: bgColor }, ...children),
styled: (attributes = 0, ...children) => StyledText2({ attributes }, ...children)
};
// src/renderables/composition/VRenderable.ts
class VRenderable extends Renderable {
options;
constructor(ctx, options) {
super(ctx, options);
this.options = options;
}
renderSelf(buffer, deltaTime) {
if (this.options.render) {
this.options.render.call(this.options, buffer, deltaTime, this);
}
}
}
// src/renderables/LineNumberRenderable.ts
var DEFAULT_GUTTER_FG = "#888888";
var DEFAULT_GUTTER_BG = "transparent";
class GutterRenderable extends Renderable {
target;
_fg;
_bg;
_minWidth;
_paddingRight;
_lineColorsGutter;
_lineColorsContent;
_lineSigns;
_lineNumberOffset;
_hideLineNumbers;
_lineNumbers;
_maxBeforeWidth = 0;
_maxAfterWidth = 0;
_lastKnownLineCount = 0;
_lastKnownScrollY = 0;
constructor(ctx, target, options) {
super(ctx, {
id: options.id,
width: "auto",
height: "auto",
flexGrow: 0,
flexShrink: 0,
buffered: options.buffered
});
this.target = target;
this._fg = options.fg;
this._bg = options.bg;
this._minWidth = options.minWidth;
this._paddingRight = options.paddingRight;
this._lineColorsGutter = options.lineColorsGutter;
this._lineColorsContent = options.lineColorsContent;
this._lineSigns = options.lineSigns;
this._lineNumberOffset = options.lineNumberOffset;
this._hideLineNumbers = options.hideLineNumbers;
this._lineNumbers = options.lineNumbers ?? new Map;
this._lastKnownLineCount = this.target.virtualLineCount;
this._lastKnownScrollY = this.target.scrollY;
this.calculateSignWidths();
this.setupMeasureFunc();
this.onLifecyclePass = () => {
const currentLineCount = this.target.virtualLineCount;
if (currentLineCount !== this._lastKnownLineCount) {
this._lastKnownLineCount = currentLineCount;
this.yogaNode.markDirty();
this.requestRender();
}
};
}
setupMeasureFunc() {
const measureFunc = (width, widthMode, height, heightMode) => {
const gutterWidth = this.calculateWidth();
const gutterHeight = this.target.virtualLineCount;
return {
width: gutterWidth,
height: gutterHeight
};
};
this.yogaNode.setMeasureFunc(measureFunc);
}
remeasure() {
this.yogaNode.markDirty();
}
setLineNumberOffset(offset) {
if (this._lineNumberOffset !== offset) {
this._lineNumberOffset = offset;
this.yogaNode.markDirty();
this.requestRender();
}
}
setHideLineNumbers(hideLineNumbers) {
this._hideLineNumbers = hideLineNumbers;
this.yogaNode.markDirty();
this.requestRender();
}
setLineNumbers(lineNumbers) {
this._lineNumbers = lineNumbers;
this.yogaNode.markDirty();
this.requestRender();
}
calculateSignWidths() {
this._maxBeforeWidth = 0;
this._maxAfterWidth = 0;
for (const sign of this._lineSigns.values()) {
if (sign.before) {
const width = stringWidth(sign.before);
this._maxBeforeWidth = Math.max(this._maxBeforeWidth, width);
}
if (sign.after) {
const width = stringWidth(sign.after);
this._maxAfterWidth = Math.max(this._maxAfterWidth, width);
}
}
}
calculateWidth() {
const totalLines = this.target.virtualLineCount;
let maxLineNumber = totalLines + this._lineNumberOffset;
if (this._lineNumbers.size > 0) {
for (const customLineNum of this._lineNumbers.values()) {
maxLineNumber = Math.max(maxLineNumber, customLineNum);
}
}
const digits = maxLineNumber > 0 ? Math.floor(Math.log10(maxLineNumber)) + 1 : 1;
const baseWidth = Math.max(this._minWidth, digits + this._paddingRight + 1);
return baseWidth + this._maxBeforeWidth + this._maxAfterWidth;
}
setLineColors(lineColorsGutter, lineColorsContent) {
this._lineColorsGutter = lineColorsGutter;
this._lineColorsContent = lineColorsContent;
this.requestRender();
}
get fg() {
return this._fg;
}
setFg(fg2) {
if (this._fg !== fg2) {
this._fg = fg2;
this.requestRender();
}
}
get bg() {
return this._bg;
}
setBg(bg2) {
if (this._bg !== bg2) {
this._bg = bg2;
this.requestRender();
}
}
getLineColors() {
return {
gutter: this._lineColorsGutter,
content: this._lineColorsContent
};
}
setLineSigns(lineSigns) {
const oldMaxBefore = this._maxBeforeWidth;
const oldMaxAfter = this._maxAfterWidth;
this._lineSigns = lineSigns;
this.calculateSignWidths();
if (this._maxBeforeWidth !== oldMaxBefore || this._maxAfterWidth !== oldMaxAfter) {
this.yogaNode.markDirty();
}
this.requestRender();
}
getLineSigns() {
return this._lineSigns;
}
renderSelf(buffer) {
const currentScrollY = this.target.scrollY;
const scrollChanged = currentScrollY !== this._lastKnownScrollY;
if (this.buffered && !this.isDirty && !scrollChanged) {
return;
}
this._lastKnownScrollY = currentScrollY;
this.refreshFrameBuffer(buffer);
}
refreshFrameBuffer(buffer) {
const startX = this.buffered ? 0 : this.x;
const startY = this.buffered ? 0 : this.y;
if (this.buffered) {
buffer.clear(this._bg);
} else if (this._bg.a > 0) {
buffer.fillRect(startX, startY, this.width, this.height, this._bg);
}
const lineInfo = this.target.lineInfo;
if (!lineInfo || !lineInfo.lineSources)
return;
const sources = lineInfo.lineSources;
let lastSource = -1;
const startLine = this.target.scrollY;
if (startLine >= sources.length)
return;
lastSource = startLine > 0 ? sources[startLine - 1] : -1;
for (let i = 0;i < this.height; i++) {
const visualLineIndex = startLine + i;
if (visualLineIndex >= sources.length)
break;
const logicalLine = sources[visualLineIndex];
const lineBg = this._lineColorsGutter.get(logicalLine) ?? this._bg;
if (lineBg !== this._bg) {
buffer.fillRect(startX, startY + i, this.width, 1, lineBg);
}
if (logicalLine === lastSource) {} else {
let currentX = startX;
const sign = this._lineSigns.get(logicalLine);
if (sign?.before) {
const beforeWidth = stringWidth(sign.before);
const padding = this._maxBeforeWidth - beforeWidth;
currentX += padding;
const beforeColor = sign.beforeColor ? parseColor(sign.beforeColor) : this._fg;
buffer.drawText(sign.before, currentX, startY + i, beforeColor, lineBg);
currentX += beforeWidth;
} else if (this._maxBeforeWidth > 0) {
currentX += this._maxBeforeWidth;
}
if (!this._hideLineNumbers.has(logicalLine)) {
const customLineNum = this._lineNumbers.get(logicalLine);
const lineNum = customLineNum !== undefined ? customLineNum : logicalLine + 1 + this._lineNumberOffset;
const lineNumStr = lineNum.toString();
const lineNumWidth = lineNumStr.length;
const availableSpace = this.width - this._maxBeforeWidth - this._maxAfterWidth - this._paddingRight;
const lineNumX = startX + this._maxBeforeWidth + 1 + availableSpace - lineNumWidth - 1;
if (lineNumX >= startX + this._maxBeforeWidth + 1) {
buffer.drawText(lineNumStr, lineNumX, startY + i, this._fg, lineBg);
}
}
if (sign?.after) {
const afterX = startX + this.width - this._paddingRight - this._maxAfterWidth;
const afterColor = sign.afterColor ? parseColor(sign.afterColor) : this._fg;
buffer.drawText(sign.after, afterX, startY + i, afterColor, lineBg);
}
}
lastSource = logicalLine;
}
}
}
function darkenColor(color) {
return RGBA.fromValues(color.r * 0.8, color.g * 0.8, color.b * 0.8, color.a);
}
class LineNumberRenderable extends Renderable {
gutter = null;
target = null;
_lineColorsGutter;
_lineColorsContent;
_lineSigns;
_fg;
_bg;
_minWidth;
_paddingRight;
_lineNumberOffset;
_hideLineNumbers;
_lineNumbers;
_isDestroying = false;
handleLineInfoChange = () => {
this.gutter?.remeasure();
this.requestRender();
};
parseLineColor(line, color) {
if (typeof color === "object" && "gutter" in color) {
const config = color;
if (config.gutter) {
this._lineColorsGutter.set(line, parseColor(config.gutter));
}
if (config.content) {
this._lineColorsContent.set(line, parseColor(config.content));
} else if (config.gutter) {
this._lineColorsContent.set(line, darkenColor(parseColor(config.gutter)));
}
} else {
const parsedColor = parseColor(color);
this._lineColorsGutter.set(line, parsedColor);
this._lineColorsContent.set(line, darkenColor(parsedColor));
}
}
constructor(ctx, options) {
super(ctx, {
...options,
flexDirection: "row",
height: "auto"
});
this._fg = parseColor(options.fg ?? DEFAULT_GUTTER_FG);
this._bg = parseColor(options.bg ?? DEFAULT_GUTTER_BG);
this._minWidth = options.minWidth ?? 3;
this._paddingRight = options.paddingRight ?? 1;
this._lineNumberOffset = options.lineNumberOffset ?? 0;
this._hideLineNumbers = options.hideLineNumbers ?? new Set;
this._lineNumbers = options.lineNumbers ?? new Map;
this._lineColorsGutter = new Map;
this._lineColorsContent = new Map;
if (options.lineColors) {
for (const [line, color] of options.lineColors) {
this.parseLineColor(line, color);
}
}
this._lineSigns = new Map;
if (options.lineSigns) {
for (const [line, sign] of options.lineSigns) {
this._lineSigns.set(line, sign);
}
}
if (options.target) {
this.setTarget(options.target);
}
}
setTarget(target) {
if (this.target === target)
return;
if (this.target) {
this.target.off("line-info-change", this.handleLineInfoChange);
super.remove(this.target);
}
if (this.gutter) {
super.remove(this.gutter);
this.gutter = null;
}
this.target = target;
this.target.on("line-info-change", this.handleLineInfoChange);
this.gutter = new GutterRenderable(this.ctx, this.target, {
fg: this._fg,
bg: this._bg,
minWidth: this._minWidth,
paddingRight: this._paddingRight,
lineColorsGutter: this._lineColorsGutter,
lineColorsContent: this._lineColorsContent,
lineSigns: this._lineSigns,
lineNumberOffset: this._lineNumberOffset,
hideLineNumbers: this._hideLineNumbers,
lineNumbers: this._lineNumbers,
id: this.id ? `${this.id}-gutter` : undefined,
buffered: true
});
super.add(this.gutter);
super.add(this.target);
}
add(child) {
if (!this.target && "lineInfo" in child && "lineCount" in child && "virtualLineCount" in child && "scrollY" in child) {
this.setTarget(child);
return this.getChildrenCount() - 1;
}
return -1;
}
remove(child) {
if (this._isDestroying) {
super.remove(child);
return;
}
if (this.gutter && child === this.gutter) {
throw new Error("LineNumberRenderable: Cannot remove gutter directly.");
}
if (this.target && child === this.target) {
throw new Error("LineNumberRenderable: Cannot remove target directly. Use clearTarget() instead.");
}
super.remove(child);
}
destroyRecursively() {
this._isDestroying = true;
if (this.target) {
this.target.off("line-info-change", this.handleLineInfoChange);
}
super.destroyRecursively();
this.gutter = null;
this.target = null;
}
clearTarget() {
if (this.target) {
this.target.off("line-info-change", this.handleLineInfoChange);
super.remove(this.target);
this.target = null;
}
if (this.gutter) {
super.remove(this.gutter);
this.gutter = null;
}
}
renderSelf(buffer) {
if (!this.target || !this.gutter)
return;
const lineInfo = this.target.lineInfo;
if (!lineInfo || !lineInfo.lineSources)
return;
const sources = lineInfo.lineSources;
const startLine = this.target.scrollY;
if (startLine >= sources.length)
return;
const gutterWidth = this.gutter.visible ? this.gutter.width : 0;
const contentWidth = this.width - gutterWidth;
for (let i = 0;i < this.height; i++) {
const visualLineIndex = startLine + i;
if (visualLineIndex >= sources.length)
break;
const logicalLine = sources[visualLineIndex];
const lineBg = this._lineColorsContent.get(logicalLine);
if (lineBg) {
buffer.fillRect(this.x + gutterWidth, this.y + i, contentWidth, 1, lineBg);
}
}
}
set showLineNumbers(value) {
if (this.gutter) {
this.gutter.visible = value;
}
}
get showLineNumbers() {
return this.gutter?.visible ?? false;
}
get fg() {
return this._fg;
}
set fg(value) {
const parsed = parseColor(value ?? DEFAULT_GUTTER_FG);
if (this._fg !== parsed) {
this._fg = parsed;
this.gutter?.setFg(parsed);
}
}
get bg() {
return this._bg;
}
set bg(value) {
const parsed = parseColor(value ?? DEFAULT_GUTTER_BG);
if (this._bg !== parsed) {
this._bg = parsed;
this.gutter?.setBg(parsed);
}
}
setLineColor(line, color) {
this.parseLineColor(line, color);
if (this.gutter) {
this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent);
}
}
clearLineColor(line) {
this._lineColorsGutter.delete(line);
this._lineColorsContent.delete(line);
if (this.gutter) {
this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent);
}
}
clearAllLineColors() {
this._lineColorsGutter.clear();
this._lineColorsContent.clear();
if (this.gutter) {
this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent);
}
}
setLineColors(lineColors) {
this._lineColorsGutter.clear();
this._lineColorsContent.clear();
for (const [line, color] of lineColors) {
this.parseLineColor(line, color);
}
if (this.gutter) {
this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent);
}
}
getLineColors() {
return {
gutter: this._lineColorsGutter,
content: this._lineColorsContent
};
}
setLineSign(line, sign) {
this._lineSigns.set(line, sign);
if (this.gutter) {
this.gutter.setLineSigns(this._lineSigns);
}
}
clearLineSign(line) {
this._lineSigns.delete(line);
if (this.gutter) {
this.gutter.setLineSigns(this._lineSigns);
}
}
clearAllLineSigns() {
this._lineSigns.clear();
if (this.gutter) {
this.gutter.setLineSigns(this._lineSigns);
}
}
setLineSigns(lineSigns) {
this._lineSigns.clear();
for (const [line, sign] of lineSigns) {
this._lineSigns.set(line, sign);
}
if (this.gutter) {
this.gutter.setLineSigns(this._lineSigns);
}
}
getLineSigns() {
return this._lineSigns;
}
set lineNumberOffset(value) {
if (this._lineNumberOffset !== value) {
this._lineNumberOffset = value;
if (this.gutter) {
this.gutter.setLineNumberOffset(value);
}
}
}
get lineNumberOffset() {
return this._lineNumberOffset;
}
setHideLineNumbers(hideLineNumbers) {
this._hideLineNumbers = hideLineNumbers;
if (this.gutter) {
this.gutter.setHideLineNumbers(hideLineNumbers);
}
}
getHideLineNumbers() {
return this._hideLineNumbers;
}
setLineNumbers(lineNumbers) {
this._lineNumbers = lineNumbers;
if (this.gutter) {
this.gutter.setLineNumbers(lineNumbers);
}
}
getLineNumbers() {
return this._lineNumbers;
}
highlightLines(startLine, endLine, color) {
for (let i = startLine;i <= endLine; i++) {
this.parseLineColor(i, color);
}
if (this.gutter) {
this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent);
}
}
clearHighlightLines(startLine, endLine) {
for (let i = startLine;i <= endLine; i++) {
this._lineColorsGutter.delete(i);
this._lineColorsContent.delete(i);
}
if (this.gutter) {
this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent);
}
}
}
// ../../node_modules/.bun/diff@9.0.0/node_modules/diff/libesm/patch/parse.js
function parsePatch(uniDiff) {
const diffstr = uniDiff.split(/\n/), list = [];
let i = 0;
function isGitDiffHeader(line) {
return /^diff --git /.test(line);
}
function isDiffHeader(line) {
return isGitDiffHeader(line) || /^Index:\s/.test(line) || /^diff(?: -r \w+)+\s/.test(line);
}
function isFileHeader(line) {
return /^(---|\+\+\+)\s/.test(line);
}
function isHunkHeader(line) {
return /^@@\s/.test(line);
}
function parseIndex() {
var _a;
const index = {};
index.hunks = [];
list.push(index);
let seenDiffHeader = false;
while (i < diffstr.length) {
const line = diffstr[i];
if (isFileHeader(line) || isHunkHeader(line)) {
break;
}
if (isGitDiffHeader(line)) {
if (seenDiffHeader) {
return;
}
seenDiffHeader = true;
index.isGit = true;
const paths = parseGitDiffHeader(line);
if (paths) {
index.oldFileName = paths.oldFileName;
index.newFileName = paths.newFileName;
}
i++;
while (i < diffstr.length) {
const extLine = diffstr[i];
if (isFileHeader(extLine) || isHunkHeader(extLine) || isDiffHeader(extLine)) {
break;
}
const renameFromMatch = /^rename from (.*)/.exec(extLine);
if (renameFromMatch) {
index.oldFileName = "a/" + unquoteIfQuoted(renameFromMatch[1]);
index.isRename = true;
}
const renameToMatch = /^rename to (.*)/.exec(extLine);
if (renameToMatch) {
index.newFileName = "b/" + unquoteIfQuoted(renameToMatch[1]);
index.isRename = true;
}
const copyFromMatch = /^copy from (.*)/.exec(extLine);
if (copyFromMatch) {
index.oldFileName = "a/" + unquoteIfQuoted(copyFromMatch[1]);
index.isCopy = true;
}
const copyToMatch = /^copy to (.*)/.exec(extLine);
if (copyToMatch) {
index.newFileName = "b/" + unquoteIfQuoted(copyToMatch[1]);
index.isCopy = true;
}
const newFileModeMatch = /^new file mode (\d+)/.exec(extLine);
if (newFileModeMatch) {
index.isCreate = true;
index.newMode = newFileModeMatch[1];
}
const deletedFileModeMatch = /^deleted file mode (\d+)/.exec(extLine);
if (deletedFileModeMatch) {
index.isDelete = true;
index.oldMode = deletedFileModeMatch[1];
}
const oldModeMatch = /^old mode (\d+)/.exec(extLine);
if (oldModeMatch) {
index.oldMode = oldModeMatch[1];
}
const newModeMatch = /^new mode (\d+)/.exec(extLine);
if (newModeMatch) {
index.newMode = newModeMatch[1];
}
if (/^Binary files /.test(extLine)) {
index.isBinary = true;
}
i++;
}
continue;
} else if (isDiffHeader(line)) {
if (seenDiffHeader) {
return;
}
seenDiffHeader = true;
const headerMatch = /^(?:Index:|diff(?: -r \w+)+)\s+/.exec(line);
if (headerMatch) {
index.index = line.substring(headerMatch[0].length).trim();
}
}
i++;
}
parseFileHeader(index);
parseFileHeader(index);
if (index.oldFileName === undefined !== (index.newFileName === undefined)) {
throw new Error("Missing " + (index.oldFileName !== undefined ? '"+++ ..."' : '"--- ..."') + " file header for " + ((_a = index.oldFileName) !== null && _a !== undefined ? _a : index.newFileName));
}
while (i < diffstr.length) {
const line = diffstr[i];
if (isDiffHeader(line) || isFileHeader(line) || /^===================================================================/.test(line)) {
break;
} else if (isHunkHeader(line)) {
index.hunks.push(parseHunk());
} else {
i++;
}
}
}
function parseGitDiffHeader(line) {
const rest = line.substring("diff --git ".length);
if (rest.startsWith('"')) {
const oldPath = parseQuotedFileName(rest);
if (oldPath === null) {
return null;
}
const afterOld = rest.substring(oldPath.rawLength + 1);
let newFileName;
if (afterOld.startsWith('"')) {
const newPath = parseQuotedFileName(afterOld);
if (newPath === null) {
return null;
}
newFileName = newPath.fileName;
} else {
newFileName = afterOld;
}
return {
oldFileName: oldPath.fileName,
newFileName
};
}
const quoteIdx = rest.indexOf('"');
if (quoteIdx > 0) {
const oldFileName = rest.substring(0, quoteIdx - 1);
const newPath = parseQuotedFileName(rest.substring(quoteIdx));
if (newPath === null) {
return null;
}
return {
oldFileName,
newFileName: newPath.fileName
};
}
if (rest.startsWith("a/")) {
const splits = [];
let idx = 0;
while (true) {
idx = rest.indexOf(" b/", idx + 1);
if (idx === -1) {
break;
}
splits.push(idx);
}
if (splits.length > 0) {
const mid = splits[Math.floor(splits.length / 2)];
return {
oldFileName: rest.substring(0, mid),
newFileName: rest.substring(mid + 1)
};
}
}
return null;
}
function unquoteIfQuoted(s) {
if (s.startsWith('"')) {
const parsed = parseQuotedFileName(s);
if (parsed) {
return parsed.fileName;
}
}
return s;
}
function parseQuotedFileName(s) {
if (!s.startsWith('"')) {
return null;
}
let result = "";
let j = 1;
while (j < s.length) {
if (s[j] === '"') {
return { fileName: result, rawLength: j + 1 };
}
if (s[j] === "\\" && j + 1 < s.length) {
j++;
switch (s[j]) {
case "a":
result += "\x07";
break;
case "b":
result += "\b";
break;
case "f":
result += "\f";
break;
case "n":
result += `
`;
break;
case "r":
result += "\r";
break;
case "t":
result += "\t";
break;
case "v":
result += "\v";
break;
case "\\":
result += "\\";
break;
case '"':
result += '"';
break;
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7": {
if (j + 2 >= s.length || s[j + 1] < "0" || s[j + 1] > "7" || s[j + 2] < "0" || s[j + 2] > "7") {
return null;
}
const bytes = [parseInt(s.substring(j, j + 3), 8)];
j += 3;
while (s[j] === "\\" && s[j + 1] >= "0" && s[j + 1] <= "7") {
if (j + 3 >= s.length || s[j + 2] < "0" || s[j + 2] > "7" || s[j + 3] < "0" || s[j + 3] > "7") {
return null;
}
bytes.push(parseInt(s.substring(j + 1, j + 4), 8));
j += 4;
}
result += new TextDecoder("utf-8").decode(new Uint8Array(bytes));
continue;
}
default:
return null;
}
} else {
result += s[j];
}
j++;
}
return null;
}
function parseFileHeader(index) {
const fileHeaderMatch = /^(---|\+\+\+)\s+/.exec(diffstr[i]);
if (fileHeaderMatch) {
const prefix = fileHeaderMatch[1], data = diffstr[i].substring(3).trim().split("\t", 2), header = (data[1] || "").trim();
let fileName = data[0];
if (fileName.startsWith('"')) {
fileName = unquoteIfQuoted(fileName);
} else {
fileName = fileName.replace(/\\\\/g, "\\");
}
if (prefix === "---") {
index.oldFileName = fileName;
index.oldHeader = header;
} else {
index.newFileName = fileName;
index.newHeader = header;
}
i++;
}
}
function parseHunk() {
var _a;
const chunkHeaderIndex = i, chunkHeaderLine = diffstr[i++], chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
const hunk = {
oldStart: +chunkHeader[1],
oldLines: typeof chunkHeader[2] === "undefined" ? 1 : +chunkHeader[2],
newStart: +chunkHeader[3],
newLines: typeof chunkHeader[4] === "undefined" ? 1 : +chunkHeader[4],
lines: []
};
if (hunk.oldLines === 0) {
hunk.oldStart += 1;
}
if (hunk.newLines === 0) {
hunk.newStart += 1;
}
let addCount = 0, removeCount = 0;
for (;i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || ((_a = diffstr[i]) === null || _a === undefined ? undefined : _a.startsWith("\\"))); i++) {
const operation = diffstr[i].length == 0 && i != diffstr.length - 1 ? " " : diffstr[i][0];
if (operation === "+" || operation === "-" || operation === " " || operation === "\\") {
hunk.lines.push(diffstr[i]);
if (operation === "+") {
addCount++;
} else if (operation === "-") {
removeCount++;
} else if (operation === " ") {
addCount++;
removeCount++;
}
} else {
throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
}
}
if (!addCount && hunk.newLines === 1) {
hunk.newLines = 0;
}
if (!removeCount && hunk.oldLines === 1) {
hunk.oldLines = 0;
}
if (addCount !== hunk.newLines) {
throw new Error("Added line count did not match for hunk at line " + (chunkHeaderIndex + 1));
}
if (removeCount !== hunk.oldLines) {
throw new Error("Removed line count did not match for hunk at line " + (chunkHeaderIndex + 1));
}
if (i < diffstr.length && diffstr[i] && /^[+ -]/.test(diffstr[i]) && !isFileHeader(diffstr[i])) {
throw new Error("Hunk at line " + (chunkHeaderIndex + 1) + " has more lines than expected (expected " + hunk.oldLines + " old lines and " + hunk.newLines + " new lines)");
}
return hunk;
}
while (i < diffstr.length) {
parseIndex();
}
return list;
}
// src/renderables/Diff.ts
class DiffRenderable extends Renderable {
_diff;
_syncScroll = false;
_view;
_parsedDiff = null;
_parseError = null;
_hunkStartLines = [];
_hunkRowOffsets = null;
_fg;
_filetype;
_syntaxStyle;
_wrapMode;
_conceal;
_selectionBg;
_selectionFg;
_treeSitterClient;
_showLineNumbers;
_lineNumberFg;
_lineNumberBg;
_addedBg;
_removedBg;
_contextBg;
_addedContentBg;
_removedContentBg;
_contextContentBg;
_addedSignColor;
_removedSignColor;
_addedLineNumberBg;
_removedLineNumberBg;
leftSide = null;
rightSide = null;
leftSideAdded = false;
rightSideAdded = false;
leftCodeRenderable = null;
rightCodeRenderable = null;
pendingRebuild = false;
_lastWidth = 0;
errorTextRenderable = null;
errorCodeRenderable = null;
_waitingForHighlight = false;
_lineInfoChangeHandler = null;
constructor(ctx, options) {
super(ctx, {
...options,
flexDirection: options.view === "split" ? "row" : "column"
});
this._diff = options.diff ?? "";
this._syncScroll = options.syncScroll ?? false;
this._view = options.view ?? "unified";
this._fg = options.fg ? parseColor(options.fg) : undefined;
this._filetype = options.filetype;
this._syntaxStyle = options.syntaxStyle;
this._wrapMode = options.wrapMode;
this._conceal = options.conceal ?? false;
this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined;
this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined;
this._treeSitterClient = options.treeSitterClient;
this._showLineNumbers = options.showLineNumbers ?? true;
this._lineNumberFg = parseColor(options.lineNumberFg ?? "#888888");
this._lineNumberBg = parseColor(options.lineNumberBg ?? "transparent");
this._addedBg = parseColor(options.addedBg ?? "#1a4d1a");
this._removedBg = parseColor(options.removedBg ?? "#4d1a1a");
this._contextBg = parseColor(options.contextBg ?? "transparent");
this._addedContentBg = options.addedContentBg ? parseColor(options.addedContentBg) : null;
this._removedContentBg = options.removedContentBg ? parseColor(options.removedContentBg) : null;
this._contextContentBg = options.contextContentBg ? parseColor(options.contextContentBg) : null;
this._addedSignColor = parseColor(options.addedSignColor ?? "#22c55e");
this._removedSignColor = parseColor(options.removedSignColor ?? "#ef4444");
this._addedLineNumberBg = parseColor(options.addedLineNumberBg ?? "transparent");
this._removedLineNumberBg = parseColor(options.removedLineNumberBg ?? "transparent");
if (this._diff) {
this.parseDiff();
this.buildView();
}
}
parseDiff() {
if (!this._diff) {
this._parsedDiff = null;
this._parseError = null;
return;
}
try {
const patches = parsePatch(this._diff);
if (patches.length === 0) {
this._parsedDiff = null;
this._parseError = null;
return;
}
this._parsedDiff = patches[0];
this._parseError = null;
} catch (error) {
this._parsedDiff = null;
this._parseError = error instanceof Error ? error : new Error(String(error));
}
}
buildView() {
this._hunkStartLines = [];
this.invalidateHunkRowOffsets();
if (this._parseError) {
this.buildErrorView();
return;
}
if (!this._parsedDiff || this._parsedDiff.hunks.length === 0) {
return;
}
if (this._view === "unified") {
this.buildUnifiedView();
} else {
this.buildSplitView();
}
}
onMouseEvent(event) {
if (event.type !== "scroll" || this._view !== "split" || !this._syncScroll)
return;
if (!this.leftCodeRenderable || !this.rightCodeRenderable)
return;
if (!event.target)
return;
if (this.isInsideSide(event.target, "left")) {
this.rightCodeRenderable.scrollY = this.leftCodeRenderable.scrollY;
this.rightCodeRenderable.scrollX = this.leftCodeRenderable.scrollX;
} else if (this.isInsideSide(event.target, "right")) {
this.leftCodeRenderable.scrollY = this.rightCodeRenderable.scrollY;
this.leftCodeRenderable.scrollX = this.rightCodeRenderable.scrollX;
}
}
isInsideSide(target, side) {
const container = side === "left" ? this.leftCodeRenderable : this.rightCodeRenderable;
let current = target;
while (current) {
if (current === container)
return true;
current = current.parent;
}
return false;
}
onResize(width, height) {
super.onResize(width, height);
if (this._view === "split" && this._wrapMode !== "none" && this._wrapMode !== undefined) {
if (this._lastWidth !== width) {
this._lastWidth = width;
this.requestRebuild();
}
}
}
requestRebuild() {
if (this.pendingRebuild) {
return;
}
this.pendingRebuild = true;
queueMicrotask(() => {
if (!this.isDestroyed && this.pendingRebuild) {
this.pendingRebuild = false;
this.buildView();
this.requestRender();
}
});
}
invalidateHunkRowOffsets() {
this._hunkRowOffsets = null;
}
rebuildView() {
if (this._view === "split") {
this.requestRebuild();
} else {
this.buildView();
}
}
handleLineInfoChange = () => {
this.invalidateHunkRowOffsets();
if (!this._waitingForHighlight)
return;
if (!this.leftCodeRenderable || !this.rightCodeRenderable)
return;
const leftIsHighlighting = this.leftCodeRenderable.isHighlighting;
const rightIsHighlighting = this.rightCodeRenderable.isHighlighting;
if (!leftIsHighlighting && !rightIsHighlighting) {
this._waitingForHighlight = false;
this.requestRebuild();
}
};
attachLineInfoListeners() {
if (!this.leftCodeRenderable && !this.rightCodeRenderable)
return;
this._lineInfoChangeHandler ??= this.handleLineInfoChange;
if (this.leftCodeRenderable) {
this.leftCodeRenderable.off("line-info-change", this._lineInfoChangeHandler);
this.leftCodeRenderable.on("line-info-change", this._lineInfoChangeHandler);
}
if (this.rightCodeRenderable) {
this.rightCodeRenderable.off("line-info-change", this._lineInfoChangeHandler);
this.rightCodeRenderable.on("line-info-change", this._lineInfoChangeHandler);
}
}
detachLineInfoListeners() {
if (!this._lineInfoChangeHandler)
return;
if (this.leftCodeRenderable) {
this.leftCodeRenderable.off("line-info-change", this._lineInfoChangeHandler);
}
if (this.rightCodeRenderable) {
this.rightCodeRenderable.off("line-info-change", this._lineInfoChangeHandler);
}
this._lineInfoChangeHandler = null;
}
destroyRecursively() {
this.detachLineInfoListeners();
this.pendingRebuild = false;
this.leftSideAdded = false;
this.rightSideAdded = false;
super.destroyRecursively();
}
buildErrorView() {
this.flexDirection = "column";
if (this.leftSide && this.leftSideAdded) {
super.remove(this.leftSide);
this.leftSideAdded = false;
}
if (this.rightSide && this.rightSideAdded) {
super.remove(this.rightSide);
this.rightSideAdded = false;
}
const errorMessage = `Error parsing diff: ${this._parseError?.message || "Unknown error"}
`;
if (!this.errorTextRenderable) {
this.errorTextRenderable = new TextRenderable(this.ctx, {
id: this.id ? `${this.id}-error-text` : undefined,
content: errorMessage,
fg: "#ef4444",
width: "100%",
flexShrink: 0
});
super.add(this.errorTextRenderable);
} else {
this.errorTextRenderable.content = errorMessage;
const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable);
if (errorTextIndex === -1) {
super.add(this.errorTextRenderable);
}
}
if (!this.errorCodeRenderable) {
this.errorCodeRenderable = new CodeRenderable(this.ctx, {
id: this.id ? `${this.id}-error-code` : undefined,
content: this._diff,
filetype: "diff",
syntaxStyle: this._syntaxStyle ?? SyntaxStyle.create(),
wrapMode: this._wrapMode,
conceal: this._conceal,
width: "100%",
flexGrow: 1,
flexShrink: 1,
...this._treeSitterClient !== undefined && { treeSitterClient: this._treeSitterClient }
});
super.add(this.errorCodeRenderable);
} else {
this.errorCodeRenderable.content = this._diff;
this.errorCodeRenderable.wrapMode = this._wrapMode ?? "none";
if (this._syntaxStyle) {
this.errorCodeRenderable.syntaxStyle = this._syntaxStyle;
}
const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable);
if (errorCodeIndex === -1) {
super.add(this.errorCodeRenderable);
}
}
}
createOrUpdateCodeRenderable(side, content, wrapMode, drawUnstyledText) {
const existingRenderable = side === "left" ? this.leftCodeRenderable : this.rightCodeRenderable;
if (!existingRenderable) {
const codeOptions = {
id: this.id ? `${this.id}-${side}-code` : undefined,
content,
filetype: this._filetype,
wrapMode,
conceal: this._conceal,
syntaxStyle: this._syntaxStyle ?? SyntaxStyle.create(),
width: "100%",
height: "100%",
...this._fg !== undefined && { fg: this._fg },
...drawUnstyledText !== undefined && { drawUnstyledText },
...this._selectionBg !== undefined && { selectionBg: this._selectionBg },
...this._selectionFg !== undefined && { selectionFg: this._selectionFg },
...this._treeSitterClient !== undefined && { treeSitterClient: this._treeSitterClient }
};
const newRenderable = new CodeRenderable(this.ctx, codeOptions);
if (side === "left") {
this.leftCodeRenderable = newRenderable;
} else {
this.rightCodeRenderable = newRenderable;
}
return newRenderable;
} else {
existingRenderable.content = content;
existingRenderable.wrapMode = wrapMode ?? "none";
existingRenderable.conceal = this._conceal;
if (drawUnstyledText !== undefined) {
existingRenderable.drawUnstyledText = drawUnstyledText;
}
if (this._filetype !== undefined) {
existingRenderable.filetype = this._filetype;
}
if (this._syntaxStyle !== undefined) {
existingRenderable.syntaxStyle = this._syntaxStyle;
}
if (this._selectionBg !== undefined) {
existingRenderable.selectionBg = this._selectionBg;
}
if (this._selectionFg !== undefined) {
existingRenderable.selectionFg = this._selectionFg;
}
if (this._fg !== undefined) {
existingRenderable.fg = this._fg;
}
return existingRenderable;
}
}
createOrUpdateSide(side, target, lineColors, lineSigns, lineNumbers, hideLineNumbers, width) {
const sideRef = side === "left" ? this.leftSide : this.rightSide;
const addedFlag = side === "left" ? this.leftSideAdded : this.rightSideAdded;
if (!sideRef) {
const newSide = new LineNumberRenderable(this.ctx, {
id: this.id ? `${this.id}-${side}` : undefined,
target,
fg: this._lineNumberFg,
bg: this._lineNumberBg,
lineColors,
lineSigns,
lineNumbers,
lineNumberOffset: 0,
hideLineNumbers,
width,
height: "100%"
});
newSide.showLineNumbers = this._showLineNumbers;
super.add(newSide);
if (side === "left") {
this.leftSide = newSide;
this.leftSideAdded = true;
} else {
this.rightSide = newSide;
this.rightSideAdded = true;
}
} else {
sideRef.width = width;
sideRef.fg = this._lineNumberFg;
sideRef.bg = this._lineNumberBg;
sideRef.setLineColors(lineColors);
sideRef.setLineSigns(lineSigns);
sideRef.setLineNumbers(lineNumbers);
sideRef.setHideLineNumbers(hideLineNumbers);
if (!addedFlag) {
super.add(sideRef);
if (side === "left") {
this.leftSideAdded = true;
} else {
this.rightSideAdded = true;
}
}
}
}
buildUnifiedView() {
if (!this._parsedDiff)
return;
this.flexDirection = "column";
if (this.errorTextRenderable) {
const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable);
if (errorTextIndex !== -1) {
super.remove(this.errorTextRenderable);
}
}
if (this.errorCodeRenderable) {
const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable);
if (errorCodeIndex !== -1) {
super.remove(this.errorCodeRenderable);
}
}
const contentLines = [];
const lineColors = new Map;
const lineSigns = new Map;
const lineNumbers = new Map;
let lineIndex = 0;
for (const hunk of this._parsedDiff.hunks) {
this._hunkStartLines.push(lineIndex);
let oldLineNum = hunk.oldStart;
let newLineNum = hunk.newStart;
for (const line of hunk.lines) {
const firstChar = line[0];
const content2 = line.slice(1);
if (firstChar === "+") {
contentLines.push(content2);
const config = {
gutter: this._addedLineNumberBg
};
if (this._addedContentBg) {
config.content = this._addedContentBg;
} else {
config.content = this._addedBg;
}
lineColors.set(lineIndex, config);
lineSigns.set(lineIndex, {
after: " +",
afterColor: this._addedSignColor
});
lineNumbers.set(lineIndex, newLineNum);
newLineNum++;
lineIndex++;
} else if (firstChar === "-") {
contentLines.push(content2);
const config = {
gutter: this._removedLineNumberBg
};
if (this._removedContentBg) {
config.content = this._removedContentBg;
} else {
config.content = this._removedBg;
}
lineColors.set(lineIndex, config);
lineSigns.set(lineIndex, {
after: " -",
afterColor: this._removedSignColor
});
lineNumbers.set(lineIndex, oldLineNum);
oldLineNum++;
lineIndex++;
} else if (firstChar === " ") {
contentLines.push(content2);
const config = {
gutter: this._lineNumberBg
};
if (this._contextContentBg) {
config.content = this._contextContentBg;
} else {
config.content = this._contextBg;
}
lineColors.set(lineIndex, config);
lineNumbers.set(lineIndex, newLineNum);
oldLineNum++;
newLineNum++;
lineIndex++;
}
}
}
const content = contentLines.join(`
`);
const codeRenderable = this.createOrUpdateCodeRenderable("left", content, this._wrapMode);
this.attachLineInfoListeners();
this.createOrUpdateSide("left", codeRenderable, lineColors, lineSigns, lineNumbers, new Set, "100%");
if (this.rightSide && this.rightSideAdded) {
super.remove(this.rightSide);
this.rightSideAdded = false;
}
}
buildSplitView() {
if (!this._parsedDiff)
return;
this.flexDirection = "row";
if (this.errorTextRenderable) {
const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable);
if (errorTextIndex !== -1) {
super.remove(this.errorTextRenderable);
}
}
if (this.errorCodeRenderable) {
const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable);
if (errorCodeIndex !== -1) {
super.remove(this.errorCodeRenderable);
}
}
const leftLogicalLines = [];
const rightLogicalLines = [];
const hunkFirstLeftLine = [];
for (const hunk of this._parsedDiff.hunks) {
hunkFirstLeftLine.push(leftLogicalLines.length);
let oldLineNum = hunk.oldStart;
let newLineNum = hunk.newStart;
let i = 0;
while (i < hunk.lines.length) {
const line = hunk.lines[i];
const firstChar = line[0];
if (firstChar === " ") {
const content = line.slice(1);
leftLogicalLines.push({
content,
lineNum: oldLineNum,
color: this._contextBg,
type: "context"
});
rightLogicalLines.push({
content,
lineNum: newLineNum,
color: this._contextBg,
type: "context"
});
oldLineNum++;
newLineNum++;
i++;
} else if (firstChar === "\\") {
i++;
} else {
const removes = [];
const adds = [];
while (i < hunk.lines.length) {
const currentLine = hunk.lines[i];
const currentChar = currentLine[0];
if (currentChar === " " || currentChar === "\\") {
break;
}
const content = currentLine.slice(1);
if (currentChar === "-") {
removes.push({ content, lineNum: oldLineNum });
oldLineNum++;
} else if (currentChar === "+") {
adds.push({ content, lineNum: newLineNum });
newLineNum++;
}
i++;
}
const maxLength = Math.max(removes.length, adds.length);
for (let j = 0;j < maxLength; j++) {
if (j < removes.length) {
leftLogicalLines.push({
content: removes[j].content,
lineNum: removes[j].lineNum,
color: this._removedBg,
sign: {
after: " -",
afterColor: this._removedSignColor
},
type: "remove"
});
} else {
leftLogicalLines.push({
content: "",
hideLineNumber: true,
type: "empty"
});
}
if (j < adds.length) {
rightLogicalLines.push({
content: adds[j].content,
lineNum: adds[j].lineNum,
color: this._addedBg,
sign: {
after: " +",
afterColor: this._addedSignColor
},
type: "add"
});
} else {
rightLogicalLines.push({
content: "",
hideLineNumber: true,
type: "empty"
});
}
}
}
}
}
for (const startIndex of hunkFirstLeftLine) {
const firstLine = leftLogicalLines[startIndex];
if (firstLine)
firstLine.hunkStart = true;
}
const canDoWrapAlignment = this.width > 0 && (this._wrapMode === "word" || this._wrapMode === "char");
const preLeftContent = leftLogicalLines.map((l) => l.content).join(`
`);
const preRightContent = rightLogicalLines.map((l) => l.content).join(`
`);
const needsConsistentConcealing = (this._wrapMode === "word" || this._wrapMode === "char") && this._conceal && this._filetype;
const drawUnstyledText = !needsConsistentConcealing;
const leftCodeRenderable = this.createOrUpdateCodeRenderable("left", preLeftContent, this._wrapMode, drawUnstyledText);
const rightCodeRenderable = this.createOrUpdateCodeRenderable("right", preRightContent, this._wrapMode, drawUnstyledText);
this.attachLineInfoListeners();
let finalLeftLines;
let finalRightLines;
const leftIsHighlighting = leftCodeRenderable.isHighlighting;
const rightIsHighlighting = rightCodeRenderable.isHighlighting;
const highlightingInProgress = needsConsistentConcealing && (leftIsHighlighting || rightIsHighlighting);
if (highlightingInProgress) {
this._waitingForHighlight = true;
this.attachLineInfoListeners();
}
const shouldDoAlignment = canDoWrapAlignment && !highlightingInProgress;
if (shouldDoAlignment) {
const leftLineInfo = leftCodeRenderable.lineInfo;
const rightLineInfo = rightCodeRenderable.lineInfo;
const leftSources = leftLineInfo.lineSources || [];
const rightSources = rightLineInfo.lineSources || [];
const leftVisualCounts = new Map;
const rightVisualCounts = new Map;
for (const logicalLine of leftSources) {
leftVisualCounts.set(logicalLine, (leftVisualCounts.get(logicalLine) || 0) + 1);
}
for (const logicalLine of rightSources) {
rightVisualCounts.set(logicalLine, (rightVisualCounts.get(logicalLine) || 0) + 1);
}
finalLeftLines = [];
finalRightLines = [];
let leftVisualPos = 0;
let rightVisualPos = 0;
for (let i = 0;i < leftLogicalLines.length; i++) {
const leftLine = leftLogicalLines[i];
const rightLine = rightLogicalLines[i];
const leftVisualCount = leftVisualCounts.get(i) ?? 0;
const rightVisualCount = rightVisualCounts.get(i) ?? 0;
if (leftVisualPos < rightVisualPos) {
const pad = rightVisualPos - leftVisualPos;
for (let p = 0;p < pad; p++) {
finalLeftLines.push({ content: "", hideLineNumber: true, type: "empty" });
}
leftVisualPos += pad;
} else if (rightVisualPos < leftVisualPos) {
const pad = leftVisualPos - rightVisualPos;
for (let p = 0;p < pad; p++) {
finalRightLines.push({ content: "", hideLineNumber: true, type: "empty" });
}
rightVisualPos += pad;
}
finalLeftLines.push(leftLine);
finalRightLines.push(rightLine);
leftVisualPos += leftVisualCount;
rightVisualPos += rightVisualCount;
}
if (leftVisualPos < rightVisualPos) {
const pad = rightVisualPos - leftVisualPos;
for (let p = 0;p < pad; p++) {
finalLeftLines.push({ content: "", hideLineNumber: true, type: "empty" });
}
} else if (rightVisualPos < leftVisualPos) {
const pad = leftVisualPos - rightVisualPos;
for (let p = 0;p < pad; p++) {
finalRightLines.push({ content: "", hideLineNumber: true, type: "empty" });
}
}
} else {
finalLeftLines = leftLogicalLines;
finalRightLines = rightLogicalLines;
}
const leftLineColors = new Map;
const rightLineColors = new Map;
const leftLineSigns = new Map;
const rightLineSigns = new Map;
const leftHideLineNumbers = new Set;
const rightHideLineNumbers = new Set;
const leftLineNumbers = new Map;
const rightLineNumbers = new Map;
finalLeftLines.forEach((line, index) => {
if (line.hunkStart) {
this._hunkStartLines.push(index);
}
if (line.lineNum !== undefined) {
leftLineNumbers.set(index, line.lineNum);
}
if (line.hideLineNumber) {
leftHideLineNumbers.add(index);
}
if (line.type === "remove") {
const config = {
gutter: this._removedLineNumberBg
};
if (this._removedContentBg) {
config.content = this._removedContentBg;
} else {
config.content = this._removedBg;
}
leftLineColors.set(index, config);
} else if (line.type === "context") {
const config = {
gutter: this._lineNumberBg
};
if (this._contextContentBg) {
config.content = this._contextContentBg;
} else {
config.content = this._contextBg;
}
leftLineColors.set(index, config);
}
if (line.sign) {
leftLineSigns.set(index, line.sign);
}
});
finalRightLines.forEach((line, index) => {
if (line.lineNum !== undefined) {
rightLineNumbers.set(index, line.lineNum);
}
if (line.hideLineNumber) {
rightHideLineNumbers.add(index);
}
if (line.type === "add") {
const config = {
gutter: this._addedLineNumberBg
};
if (this._addedContentBg) {
config.content = this._addedContentBg;
} else {
config.content = this._addedBg;
}
rightLineColors.set(index, config);
} else if (line.type === "context") {
const config = {
gutter: this._lineNumberBg
};
if (this._contextContentBg) {
config.content = this._contextContentBg;
} else {
config.content = this._contextBg;
}
rightLineColors.set(index, config);
}
if (line.sign) {
rightLineSigns.set(index, line.sign);
}
});
const leftContentFinal = finalLeftLines.map((l) => l.content).join(`
`);
const rightContentFinal = finalRightLines.map((l) => l.content).join(`
`);
leftCodeRenderable.content = leftContentFinal;
rightCodeRenderable.content = rightContentFinal;
this.createOrUpdateSide("left", leftCodeRenderable, leftLineColors, leftLineSigns, leftLineNumbers, leftHideLineNumbers, "50%");
this.createOrUpdateSide("right", rightCodeRenderable, rightLineColors, rightLineSigns, rightLineNumbers, rightHideLineNumbers, "50%");
}
get diff() {
return this._diff;
}
set diff(value) {
if (this._diff !== value) {
this._diff = value;
this._waitingForHighlight = false;
this.parseDiff();
this.rebuildView();
}
}
get syncScroll() {
return this._syncScroll;
}
set syncScroll(value) {
if (this._syncScroll !== value) {
this._syncScroll = value;
}
}
get view() {
return this._view;
}
set view(value) {
if (this._view !== value) {
this._view = value;
this.flexDirection = value === "split" ? "row" : "column";
this.buildView();
}
}
get filetype() {
return this._filetype;
}
set filetype(value) {
if (this._filetype !== value) {
this._filetype = value;
this.rebuildView();
}
}
get syntaxStyle() {
return this._syntaxStyle;
}
set syntaxStyle(value) {
if (this._syntaxStyle !== value) {
this._syntaxStyle = value;
this.rebuildView();
}
}
get wrapMode() {
return this._wrapMode;
}
set wrapMode(value) {
if (this._wrapMode !== value) {
this._wrapMode = value;
this.invalidateHunkRowOffsets();
if (this._view === "unified" && this.leftCodeRenderable) {
this.leftCodeRenderable.wrapMode = value ?? "none";
} else if (this._view === "split") {
this.requestRebuild();
}
}
}
get showLineNumbers() {
return this._showLineNumbers;
}
set showLineNumbers(value) {
if (this._showLineNumbers !== value) {
this._showLineNumbers = value;
if (this.leftSide) {
this.leftSide.showLineNumbers = value;
}
if (this.rightSide) {
this.rightSide.showLineNumbers = value;
}
}
}
get addedBg() {
return this._addedBg;
}
set addedBg(value) {
const parsed = parseColor(value);
if (this._addedBg !== parsed) {
this._addedBg = parsed;
this.rebuildView();
}
}
get removedBg() {
return this._removedBg;
}
set removedBg(value) {
const parsed = parseColor(value);
if (this._removedBg !== parsed) {
this._removedBg = parsed;
this.rebuildView();
}
}
get contextBg() {
return this._contextBg;
}
set contextBg(value) {
const parsed = parseColor(value);
if (this._contextBg !== parsed) {
this._contextBg = parsed;
this.rebuildView();
}
}
get addedSignColor() {
return this._addedSignColor;
}
set addedSignColor(value) {
const parsed = parseColor(value);
if (this._addedSignColor !== parsed) {
this._addedSignColor = parsed;
this.rebuildView();
}
}
get removedSignColor() {
return this._removedSignColor;
}
set removedSignColor(value) {
const parsed = parseColor(value);
if (this._removedSignColor !== parsed) {
this._removedSignColor = parsed;
this.rebuildView();
}
}
get addedLineNumberBg() {
return this._addedLineNumberBg;
}
set addedLineNumberBg(value) {
const parsed = parseColor(value);
if (this._addedLineNumberBg !== parsed) {
this._addedLineNumberBg = parsed;
this.rebuildView();
}
}
get removedLineNumberBg() {
return this._removedLineNumberBg;
}
set removedLineNumberBg(value) {
const parsed = parseColor(value);
if (this._removedLineNumberBg !== parsed) {
this._removedLineNumberBg = parsed;
this.rebuildView();
}
}
get lineNumberFg() {
return this._lineNumberFg;
}
set lineNumberFg(value) {
const parsed = parseColor(value);
if (this._lineNumberFg !== parsed) {
this._lineNumberFg = parsed;
this.rebuildView();
}
}
get lineNumberBg() {
return this._lineNumberBg;
}
set lineNumberBg(value) {
const parsed = parseColor(value);
if (this._lineNumberBg !== parsed) {
this._lineNumberBg = parsed;
this.rebuildView();
}
}
get addedContentBg() {
return this._addedContentBg;
}
set addedContentBg(value) {
const parsed = value ? parseColor(value) : null;
if (this._addedContentBg !== parsed) {
this._addedContentBg = parsed;
this.rebuildView();
}
}
get removedContentBg() {
return this._removedContentBg;
}
set removedContentBg(value) {
const parsed = value ? parseColor(value) : null;
if (this._removedContentBg !== parsed) {
this._removedContentBg = parsed;
this.rebuildView();
}
}
get contextContentBg() {
return this._contextContentBg;
}
set contextContentBg(value) {
const parsed = value ? parseColor(value) : null;
if (this._contextContentBg !== parsed) {
this._contextContentBg = parsed;
this.rebuildView();
}
}
get selectionBg() {
return this._selectionBg;
}
set selectionBg(value) {
const parsed = value ? parseColor(value) : undefined;
if (this._selectionBg !== parsed) {
this._selectionBg = parsed;
if (this.leftCodeRenderable) {
this.leftCodeRenderable.selectionBg = parsed;
}
if (this.rightCodeRenderable) {
this.rightCodeRenderable.selectionBg = parsed;
}
}
}
get selectionFg() {
return this._selectionFg;
}
set selectionFg(value) {
const parsed = value ? parseColor(value) : undefined;
if (this._selectionFg !== parsed) {
this._selectionFg = parsed;
if (this.leftCodeRenderable) {
this.leftCodeRenderable.selectionFg = parsed;
}
if (this.rightCodeRenderable) {
this.rightCodeRenderable.selectionFg = parsed;
}
}
}
get conceal() {
return this._conceal;
}
set conceal(value) {
if (this._conceal !== value) {
this._conceal = value;
this.rebuildView();
}
}
get fg() {
return this._fg;
}
set fg(value) {
const parsed = value ? parseColor(value) : undefined;
if (this._fg !== parsed) {
this._fg = parsed;
if (this.leftCodeRenderable) {
this.leftCodeRenderable.fg = parsed;
}
if (this.rightCodeRenderable) {
this.rightCodeRenderable.fg = parsed;
}
}
}
setLineColor(line, color) {
this.leftSide?.setLineColor(line, color);
this.rightSide?.setLineColor(line, color);
}
clearLineColor(line) {
this.leftSide?.clearLineColor(line);
this.rightSide?.clearLineColor(line);
}
setLineColors(lineColors) {
this.leftSide?.setLineColors(lineColors);
this.rightSide?.setLineColors(lineColors);
}
clearAllLineColors() {
this.leftSide?.clearAllLineColors();
this.rightSide?.clearAllLineColors();
}
highlightLines(startLine, endLine, color) {
this.leftSide?.highlightLines(startLine, endLine, color);
this.rightSide?.highlightLines(startLine, endLine, color);
}
clearHighlightLines(startLine, endLine) {
this.leftSide?.clearHighlightLines(startLine, endLine);
this.rightSide?.clearHighlightLines(startLine, endLine);
}
getHunkRowOffsets() {
if (this._hunkRowOffsets)
return [...this._hunkRowOffsets];
this._hunkRowOffsets = this.computeHunkRowOffsets();
return [...this._hunkRowOffsets];
}
computeHunkRowOffsets() {
if (this._hunkStartLines.length === 0)
return [];
const sources = this.leftCodeRenderable?.lineInfo.lineSources;
if (!sources || sources.length === 0)
return [...this._hunkStartLines];
const offsets = [];
let visualRow = 0;
for (const hunkStartLine of this._hunkStartLines) {
while (visualRow < sources.length && sources[visualRow] < hunkStartLine) {
visualRow++;
}
offsets.push(visualRow < sources.length ? visualRow : hunkStartLine);
}
return offsets;
}
}
// src/renderables/EmbeddedTerminal.ts
var MOD_SHIFT = 1 << 0;
var MOD_CTRL = 1 << 1;
var MOD_ALT = 1 << 2;
var MOD_SUPER = 1 << 3;
var MOD_CAPS_LOCK = 1 << 4;
var MOD_NUM_LOCK = 1 << 5;
class EmbeddedTerminalRenderable extends Renderable {
selectable = true;
lib;
handle = null;
_onData;
_onTerminalResize;
_onScreenChange;
keyreleaseHandler = null;
hadRenderHooks = false;
selection = false;
constructor(ctx, options) {
const cols = options.cols ?? (typeof options.width === "number" ? options.width : 80);
const rows = options.rows ?? (typeof options.height === "number" ? options.height : 24);
super(ctx, {
...options,
width: options.width ?? cols,
height: options.height ?? rows,
buffered: true
});
this._focusable = true;
this._onData = options.onData;
this._onTerminalResize = options.onTerminalResize;
this._onScreenChange = options.onScreenChange;
this.selectable = options.selectable ?? true;
this.lib = resolveRenderLib();
try {
this.handle = this.lib.createEmbeddedTerminal({ cols, rows, maxScrollback: options.maxScrollback });
this.setupMouse(options);
} catch (error) {
this.destroy();
throw error;
}
}
get onData() {
return this._onData;
}
set onData(value) {
this._onData = value;
}
get onTerminalResize() {
return this._onTerminalResize;
}
set onTerminalResize(value) {
this._onTerminalResize = value;
}
get onScreenChange() {
return this._onScreenChange;
}
set onScreenChange(value) {
this._onScreenChange = value;
}
screen() {
const cursor = this.handle ? this.lib.embeddedTerminalCursor(this.handle) : { x: 0, y: 0, visible: false, hasValue: false };
const lines = this.frameBuffer ? new TextDecoder().decode(this.frameBuffer.getRealCharBytes(true)).split(`
`).slice(0, this.height).map((line) => line.trimEnd()) : [];
while (lines.at(-1) === "")
lines.pop();
return {
text: lines.join(`
`),
lines,
columns: this.width,
rows: this.height,
cursor: {
x: cursor.x,
y: cursor.y,
visible: cursor.hasValue && cursor.visible
}
};
}
write(data) {
if (!this.handle)
return;
this.lib.embeddedTerminalWrite(this.handle, data);
try {
this.flushResponses();
} finally {
this.requestRender();
}
}
invalidate() {
if (!this.handle)
return;
this.lib.embeddedTerminalInvalidate(this.handle);
this.requestRender();
}
encodeKey(key) {
if (!this.handle)
return new Uint8Array;
const text = textualKey(key);
const physical = physicalKey(key);
return this.lib.embeddedTerminalEncodeKey(this.handle, {
action: key.eventType === "release" ? "release" : key.repeated ? "repeat" : "press",
key: physical,
mods: modifiers(key),
text,
unshiftedCodepoint: key.baseCode ?? physicalUnshiftedCodepoint(physical)
});
}
encodePaste(bytes) {
if (!this.handle)
return new Uint8Array;
return this.lib.embeddedTerminalEncodePaste(this.handle, bytes);
}
shouldStartSelection(x, y) {
if (!this.selectable)
return false;
const localX = x - this.x;
const localY = y - this.y;
return localX >= 0 && localX < this.width && localY >= 0 && localY < this.height;
}
onSelectionChanged(selection) {
if (!this.handle)
return false;
const local = convertGlobalToLocalSelection(selection, this.x, this.y);
if (!local?.isActive) {
if (!this.selection)
return false;
this.lib.embeddedTerminalClearSelection(this.handle);
this.selection = false;
this.requestRender();
return false;
}
if (!this.selection && local.behavior === "cell" && local.anchorX === local.focusX && local.anchorY === local.focusY)
return false;
const point = (x, y) => {
if (y < 0)
return { x: 0, y: 0 };
if (y >= this.height)
return { x: this.width - 1, y: this.height - 1 };
return { x: Math.max(0, Math.min(this.width - 1, x)), y };
};
this.lib.embeddedTerminalSetSelection(this.handle, point(local.anchorX, local.anchorY), point(local.focusX, local.focusY));
this.selection = true;
this.requestRender();
return true;
}
hasSelection() {
return this.selection;
}
getSelectedText() {
if (!this.handle || !this.selection)
return "";
return new TextDecoder().decode(this.lib.embeddedTerminalGetSelectedText(this.handle));
}
focus() {
if (this.focused)
return;
super.focus();
if (!this.focused)
return;
this.keyreleaseHandler = (key) => this.handleKeyPress(key);
this.ctx._internalKeyInput.onInternal("keyrelease", this.keyreleaseHandler);
try {
this.send(this.handle ? this.lib.embeddedTerminalEncodeFocus(this.handle, true) : new Uint8Array, "input");
} catch (error) {
this.removeKeyreleaseHandler();
super.blur();
throw error;
}
}
blur() {
if (!this.focused)
return;
try {
this.send(this.handle ? this.lib.embeddedTerminalEncodeFocus(this.handle, false) : new Uint8Array, "input");
} catch {} finally {
this.removeKeyreleaseHandler();
super.blur();
this._ctx.setCursorPosition(0, 0, false);
}
}
handleKeyPress(key) {
const output = this.encodeKey(key);
this.send(output, "input");
return output.byteLength > 0;
}
handlePaste(event) {
this.send(this.encodePaste(event.bytes), "input");
}
render(buffer, deltaTime) {
const hasRenderHooks = Boolean(this.renderBefore || this.renderAfter);
if (this.handle && (hasRenderHooks || this.hadRenderHooks))
this.lib.embeddedTerminalInvalidate(this.handle);
this.hadRenderHooks = hasRenderHooks;
super.render(buffer, deltaTime);
}
onResize(width, height) {
super.onResize(width, height);
if (!this.handle || !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
return;
const cols = Math.min(Math.floor(width), 65535);
const rows = Math.min(Math.floor(height), 65535);
this.lib.embeddedTerminalResize(this.handle, cols, rows);
this.lib.embeddedTerminalInvalidate(this.handle);
this.flushResponses();
this._onTerminalResize?.(cols, rows);
}
renderSelf(buffer) {
if (!this.handle || !this.frameBuffer || !this.visible || this.isDestroyed)
return;
this.lib.embeddedTerminalCompose(this.handle, buffer.ptr, 0, 0);
this._onScreenChange?.();
if (!this.focused)
return;
const cursor = this.lib.embeddedTerminalCursor(this.handle);
const visible = cursor.visible && cursor.hasValue;
const cursorX = cursor.wideTail && cursor.x > 0 ? cursor.x - 1 : cursor.x;
this._ctx.setCursorPosition(this._screenX + cursorX + 1, this._screenY + cursor.y + 1, visible);
if (!visible)
return;
this._ctx.setCursorStyle({
style: cursor.style === "bar" ? "line" : cursor.style === "underline" ? "underline" : "block",
blinking: cursor.blinking
});
if (cursor.color)
this._ctx.setCursorColor(RGBA.fromInts(cursor.color.r, cursor.color.g, cursor.color.b, 255));
}
destroySelf() {
if (this.handle) {
this.lib.destroyEmbeddedTerminal(this.handle);
this.handle = null;
}
this._ctx.setCursorPosition(0, 0, false);
super.destroySelf();
}
onRemove() {
if (this.focused)
this.blur();
}
setupMouse(options) {
const { onMouseDown, onMouseUp, onMouseMove, onMouseDrag, onMouseScroll } = options;
this.onMouseDown = (event) => {
this.forwardMouse(event, "press");
onMouseDown?.call(this, event);
};
this.onMouseUp = (event) => {
this.forwardMouse(event, "release");
onMouseUp?.call(this, event);
};
this.onMouseMove = (event) => {
this.forwardMouse(event, "motion");
onMouseMove?.call(this, event);
};
this.onMouseDrag = (event) => {
this.forwardMouse(event, "motion");
onMouseDrag?.call(this, event);
};
this.onMouseScroll = (event) => {
this.forwardMouse(event, "press");
onMouseScroll?.call(this, event);
};
}
forwardMouse(event, action) {
if (!this.handle)
return;
if (event.type === "down" && event.button === 0)
this.focus();
const output = this.lib.embeddedTerminalEncodeMouse(this.handle, {
action,
button: event.type === "move" && !event.isDragging ? undefined : mouseButton(event),
mods: modifiers(event.modifiers),
x: event.x - this._screenX,
y: event.y - this._screenY,
anyButtonPressed: event.isDragging === true || event.type === "down"
});
if (event.type === "scroll" && output.byteLength === 0) {
const direction = event.scroll?.direction;
if (direction !== "up" && direction !== "down")
return;
this.lib.embeddedTerminalScroll(this.handle, direction === "up" ? -3 : 3);
this.requestRender();
event.preventDefault();
event.stopPropagation();
return;
}
if (output.byteLength === 0)
return;
event.preventDefault();
event.stopPropagation();
this.send(output, "input");
}
flushResponses() {
if (!this.handle)
return;
this.send(this.lib.embeddedTerminalDrainResponses(this.handle), "response");
}
removeKeyreleaseHandler() {
if (!this.keyreleaseHandler)
return;
this.ctx._internalKeyInput.offInternal("keyrelease", this.keyreleaseHandler);
this.keyreleaseHandler = null;
}
send(data, source) {
if (data.byteLength > 0)
this._onData?.(data, source);
}
}
function modifiers(input) {
let value = 0;
if (input.shift)
value |= MOD_SHIFT;
if (input.ctrl)
value |= MOD_CTRL;
if (input.alt || input.option)
value |= MOD_ALT;
if (input.meta || input.super)
value |= MOD_SUPER;
if (input.capsLock)
value |= MOD_CAPS_LOCK;
if (input.numLock)
value |= MOD_NUM_LOCK;
return value;
}
function physicalKey(key) {
if (key.code && !key.code.startsWith("["))
return key.code;
if (/^[a-z]$/i.test(key.name))
return `Key${key.name.toUpperCase()}`;
if (/^[0-9]$/.test(key.name))
return `Digit${key.name}`;
return {
backspace: "Backspace",
enter: "Enter",
return: "Enter",
space: "Space",
tab: "Tab",
delete: "Delete",
end: "End",
home: "Home",
insert: "Insert",
pagedown: "PageDown",
pageup: "PageUp",
down: "ArrowDown",
left: "ArrowLeft",
right: "ArrowRight",
up: "ArrowUp",
escape: "Escape"
}[key.name.toLowerCase()] ?? "";
}
function textualKey(key) {
if (key.sequence.length > 0 && !/[\p{Cc}]/u.test(key.sequence))
return key.sequence;
if (key.name === "space")
return " ";
if (key.name.length === 0 || /[\p{Cc}]/u.test(key.name))
return;
if ([...key.name].length === 1 || /[^\x00-\x7f]/.test(key.name))
return key.name;
}
function physicalUnshiftedCodepoint(code) {
if (code?.startsWith("Key") && code.length === 4)
return code.charCodeAt(3) + 32;
if (code?.startsWith("Digit") && code.length === 6)
return code.charCodeAt(5);
return 0;
}
function mouseButton(event) {
if (event.type === "scroll") {
if (event.scroll?.direction === "up")
return "four";
if (event.scroll?.direction === "down")
return "five";
if (event.scroll?.direction === "left")
return "six";
if (event.scroll?.direction === "right")
return "seven";
return;
}
return { 0: "left", 1: "middle", 2: "right", 4: "four", 5: "five" }[event.button];
}
// src/renderables/Textarea.ts
var defaultTextareaKeyBindings = [
{ name: "left", action: "move-left" },
{ name: "right", action: "move-right" },
{ name: "up", action: "move-up" },
{ name: "down", action: "move-down" },
{ name: "left", shift: true, action: "select-left" },
{ name: "right", shift: true, action: "select-right" },
{ name: "up", shift: true, action: "select-up" },
{ name: "down", shift: true, action: "select-down" },
{ name: "home", action: "buffer-home" },
{ name: "end", action: "buffer-end" },
{ name: "home", shift: true, action: "select-buffer-home" },
{ name: "end", shift: true, action: "select-buffer-end" },
{ name: "a", ctrl: true, action: "line-home" },
{ name: "e", ctrl: true, action: "line-end" },
{ name: "a", ctrl: true, shift: true, action: "select-line-home" },
{ name: "e", ctrl: true, shift: true, action: "select-line-end" },
{ name: "a", meta: true, action: "visual-line-home" },
{ name: "e", meta: true, action: "visual-line-end" },
{ name: "a", meta: true, shift: true, action: "select-visual-line-home" },
{ name: "e", meta: true, shift: true, action: "select-visual-line-end" },
{ name: "f", ctrl: true, action: "move-right" },
{ name: "b", ctrl: true, action: "move-left" },
{ name: "w", ctrl: true, action: "delete-word-backward" },
{ name: "backspace", ctrl: true, action: "delete-word-backward" },
{ name: "d", meta: true, action: "delete-word-forward" },
{ name: "delete", meta: true, action: "delete-word-forward" },
{ name: "delete", ctrl: true, action: "delete-word-forward" },
{ name: "d", ctrl: true, shift: true, action: "delete-line" },
{ name: "k", ctrl: true, action: "delete-to-line-end" },
{ name: "u", ctrl: true, action: "delete-to-line-start" },
{ name: "backspace", action: "backspace" },
{ name: "backspace", shift: true, action: "backspace" },
{ name: "d", ctrl: true, action: "delete" },
{ name: "delete", action: "delete" },
{ name: "delete", shift: true, action: "delete" },
{ name: "return", action: "newline" },
{ name: "kpenter", action: "newline" },
{ name: "linefeed", action: "newline" },
{ name: "return", meta: true, action: "submit" },
{ name: "kpenter", meta: true, action: "submit" },
{ name: "-", ctrl: true, action: "undo" },
{ name: ".", ctrl: true, action: "redo" },
{ name: "z", super: true, action: "undo" },
{ name: "z", super: true, shift: true, action: "redo" },
{ name: "f", meta: true, action: "word-forward" },
{ name: "b", meta: true, action: "word-backward" },
{ name: "right", meta: true, action: "word-forward" },
{ name: "left", meta: true, action: "word-backward" },
{ name: "right", ctrl: true, action: "word-forward" },
{ name: "left", ctrl: true, action: "word-backward" },
{ name: "f", meta: true, shift: true, action: "select-word-forward" },
{ name: "b", meta: true, shift: true, action: "select-word-backward" },
{ name: "right", meta: true, shift: true, action: "select-word-forward" },
{ name: "left", meta: true, shift: true, action: "select-word-backward" },
{ name: "backspace", meta: true, action: "delete-word-backward" },
{ name: "left", super: true, action: "visual-line-home" },
{ name: "right", super: true, action: "visual-line-end" },
{ name: "up", super: true, action: "buffer-home" },
{ name: "down", super: true, action: "buffer-end" },
{ name: "left", super: true, shift: true, action: "select-visual-line-home" },
{ name: "right", super: true, shift: true, action: "select-visual-line-end" },
{ name: "up", super: true, shift: true, action: "select-buffer-home" },
{ name: "down", super: true, shift: true, action: "select-buffer-end" },
{ name: "a", super: true, action: "select-all" }
];
class TextareaRenderable extends EditBufferRenderable {
_placeholder;
_placeholderColor;
_unfocusedBackgroundColor;
_unfocusedTextColor;
_focusedBackgroundColor;
_focusedTextColor;
_keyBindingsMap;
_keyAliasMap;
_keyBindings;
_actionHandlers;
_initialValueSet = false;
_submitListener = undefined;
static defaults = {
backgroundColor: "transparent",
textColor: "#FFFFFF",
focusedBackgroundColor: "transparent",
focusedTextColor: "#FFFFFF",
placeholder: null,
placeholderColor: "#666666"
};
constructor(ctx, options) {
const defaults = TextareaRenderable.defaults;
const baseOptions = {
...options,
backgroundColor: options.backgroundColor || defaults.backgroundColor,
textColor: options.textColor || defaults.textColor
};
super(ctx, baseOptions);
this._unfocusedBackgroundColor = parseColor(options.backgroundColor || defaults.backgroundColor);
this._unfocusedTextColor = parseColor(options.textColor || defaults.textColor);
this._focusedBackgroundColor = parseColor(options.focusedBackgroundColor || options.backgroundColor || defaults.focusedBackgroundColor);
this._focusedTextColor = parseColor(options.focusedTextColor || options.textColor || defaults.focusedTextColor);
this._placeholder = options.placeholder ?? defaults.placeholder;
this._placeholderColor = parseColor(options.placeholderColor ?? defaults.placeholderColor);
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {});
this._keyBindings = options.keyBindings || [];
const mergedBindings = mergeKeyBindings(defaultTextareaKeyBindings, this._keyBindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
this._actionHandlers = this.buildActionHandlers();
this._submitListener = options.onSubmit;
if (options.initialValue) {
this.setText(options.initialValue);
this._initialValueSet = true;
}
this.updateColors();
this.applyPlaceholder(this._placeholder);
}
applyPlaceholder(placeholder) {
if (placeholder === null) {
this.editorView.setPlaceholderStyledText([]);
return;
}
if (typeof placeholder === "string") {
const colorStyle = fg(this._placeholderColor);
const chunks = [colorStyle(placeholder)];
this.editorView.setPlaceholderStyledText(chunks);
} else {
this.editorView.setPlaceholderStyledText(placeholder.chunks);
}
}
buildActionHandlers() {
return new Map([
["move-left", () => this.moveCursorLeft()],
["move-right", () => this.moveCursorRight()],
["move-up", () => this.moveCursorUp()],
["move-down", () => this.moveCursorDown()],
["select-left", () => this.moveCursorLeft({ select: true })],
["select-right", () => this.moveCursorRight({ select: true })],
["select-up", () => this.moveCursorUp({ select: true })],
["select-down", () => this.moveCursorDown({ select: true })],
["line-home", () => this.gotoLineHome()],
["line-end", () => this.gotoLineEnd()],
["select-line-home", () => this.gotoLineHome({ select: true })],
["select-line-end", () => this.gotoLineEnd({ select: true })],
["visual-line-home", () => this.gotoVisualLineHome()],
["visual-line-end", () => this.gotoVisualLineEnd()],
["select-visual-line-home", () => this.gotoVisualLineHome({ select: true })],
["select-visual-line-end", () => this.gotoVisualLineEnd({ select: true })],
["select-buffer-home", () => this.gotoBufferHome({ select: true })],
["select-buffer-end", () => this.gotoBufferEnd({ select: true })],
["buffer-home", () => this.gotoBufferHome()],
["buffer-end", () => this.gotoBufferEnd()],
["delete-line", () => this.deleteLine()],
["delete-to-line-end", () => this.deleteToLineEnd()],
["delete-to-line-start", () => this.deleteToLineStart()],
["backspace", () => this.deleteCharBackward()],
["delete", () => this.deleteChar()],
["newline", () => this.newLine()],
["undo", () => this.undo()],
["redo", () => this.redo()],
["word-forward", () => this.moveWordForward()],
["word-backward", () => this.moveWordBackward()],
["select-word-forward", () => this.moveWordForward({ select: true })],
["select-word-backward", () => this.moveWordBackward({ select: true })],
["delete-word-forward", () => this.deleteWordForward()],
["delete-word-backward", () => this.deleteWordBackward()],
["select-all", () => this.selectAll()],
["submit", () => this.submit()]
]);
}
handlePaste(event) {
this.insertText(stripAnsiSequences(decodePasteBytes(event.bytes)));
}
handleKeyPress(key) {
if (this.traits.suspend !== true) {
const action = getKeyBindingAction(this._keyBindingsMap, key);
if (action) {
const handler = this._actionHandlers.get(action);
if (handler) {
return handler();
}
}
}
if (!key.ctrl && !key.meta && !key.super && !key.hyper) {
if (key.name === "space") {
this.insertText(" ");
return true;
}
if (key.sequence) {
const firstCharCode = key.sequence.charCodeAt(0);
if (firstCharCode < 32) {
return false;
}
if (firstCharCode === 127) {
return false;
}
this.insertText(key.sequence);
return true;
}
}
return false;
}
updateColors() {
const effectiveBg = this._focused ? this._focusedBackgroundColor : this._unfocusedBackgroundColor;
const effectiveFg = this._focused ? this._focusedTextColor : this._unfocusedTextColor;
super.backgroundColor = effectiveBg;
super.textColor = effectiveFg;
}
focus() {
super.focus();
this.updateColors();
}
blur() {
super.blur();
if (!this.isDestroyed) {
this.updateColors();
}
}
get placeholder() {
return this._placeholder;
}
set placeholder(value) {
const normalizedValue = value ?? null;
if (this._placeholder !== normalizedValue) {
this._placeholder = normalizedValue;
this.applyPlaceholder(normalizedValue);
this.requestRender();
}
}
get placeholderColor() {
return this._placeholderColor;
}
set placeholderColor(value) {
const newColor = parseColor(value ?? TextareaRenderable.defaults.placeholderColor);
if (this._placeholderColor !== newColor) {
this._placeholderColor = newColor;
this.applyPlaceholder(this._placeholder);
this.requestRender();
}
}
get backgroundColor() {
return this._unfocusedBackgroundColor;
}
set backgroundColor(value) {
const newColor = parseColor(value ?? TextareaRenderable.defaults.backgroundColor);
if (this._unfocusedBackgroundColor !== newColor) {
this._unfocusedBackgroundColor = newColor;
this.updateColors();
}
}
get textColor() {
return this._unfocusedTextColor;
}
set textColor(value) {
const newColor = parseColor(value ?? TextareaRenderable.defaults.textColor);
if (this._unfocusedTextColor !== newColor) {
this._unfocusedTextColor = newColor;
this.updateColors();
}
}
set focusedBackgroundColor(value) {
const newColor = parseColor(value ?? TextareaRenderable.defaults.focusedBackgroundColor);
if (this._focusedBackgroundColor !== newColor) {
this._focusedBackgroundColor = newColor;
this.updateColors();
}
}
set focusedTextColor(value) {
const newColor = parseColor(value ?? TextareaRenderable.defaults.focusedTextColor);
if (this._focusedTextColor !== newColor) {
this._focusedTextColor = newColor;
this.updateColors();
}
}
set initialValue(value) {
if (!this._initialValueSet) {
this.setText(value);
this._initialValueSet = true;
}
}
submit() {
if (this._submitListener) {
this._submitListener({});
}
return true;
}
set onSubmit(handler) {
this._submitListener = handler;
}
get onSubmit() {
return this._submitListener;
}
set keyBindings(bindings) {
this._keyBindings = bindings;
const mergedBindings = mergeKeyBindings(defaultTextareaKeyBindings, bindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
}
set keyAliasMap(aliases) {
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, aliases);
const mergedBindings = mergeKeyBindings(defaultTextareaKeyBindings, this._keyBindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
}
get extmarks() {
return this.editorView.extmarks;
}
}
// src/renderables/Input.ts
var InputRenderableEvents;
((InputRenderableEvents2) => {
InputRenderableEvents2["INPUT"] = "input";
InputRenderableEvents2["CHANGE"] = "change";
InputRenderableEvents2["ENTER"] = "enter";
})(InputRenderableEvents ||= {});
class InputRenderable extends TextareaRenderable {
_maxLength;
_minLength;
_lastCommittedValue = "";
static defaultOptions = {
placeholder: "",
maxLength: 1000,
minLength: 0,
value: ""
};
constructor(ctx, options) {
const defaults = InputRenderable.defaultOptions;
const maxLength = options.maxLength ?? defaults.maxLength;
const minLength = options.minLength ?? defaults.minLength;
const rawValue = options.value ?? defaults.value;
const initialValue = rawValue.replace(/[\n\r]/g, "").substring(0, maxLength);
if (minLength > maxLength) {
throw new Error(`InputRenderable: minLength (${minLength}) cannot be greater than maxLength (${maxLength})`);
}
super(ctx, {
...options,
placeholder: options.placeholder ?? defaults.placeholder,
initialValue,
height: 1,
wrapMode: "none",
keyBindings: [
{ name: "return", action: "submit" },
{ name: "kpenter", action: "submit" },
{ name: "linefeed", action: "submit" },
...options.keyBindings || []
]
});
this._maxLength = maxLength;
this._minLength = minLength;
this._lastCommittedValue = this.plainText;
if (initialValue) {
this.cursorOffset = initialValue.length;
}
}
newLine() {
return false;
}
handlePaste(event) {
const sanitized = stripAnsiSequences(decodePasteBytes(event.bytes)).replace(/[\n\r]/g, "");
if (sanitized) {
this.insertText(sanitized);
}
}
insertText(text) {
const sanitized = text.replace(/[\n\r]/g, "");
if (!sanitized)
return;
const currentLength = this.plainText.length;
const remaining = this._maxLength - currentLength;
if (remaining <= 0)
return;
const toInsert = sanitized.substring(0, remaining);
super.insertText(toInsert);
this.emit("input" /* INPUT */, this.plainText);
}
get value() {
return this.plainText;
}
set value(value) {
const newValue = value.substring(0, this._maxLength).replace(/[\n\r]/g, "");
const currentValue = this.plainText;
if (currentValue !== newValue) {
this.setText(newValue);
this.cursorOffset = newValue.length;
this.emit("input" /* INPUT */, newValue);
}
}
focus() {
super.focus();
this._lastCommittedValue = this.plainText;
}
blur() {
if (!this.isDestroyed) {
const currentValue = this.plainText;
if (currentValue !== this._lastCommittedValue) {
this._lastCommittedValue = currentValue;
this.emit("change" /* CHANGE */, currentValue);
}
}
super.blur();
}
submit() {
const currentValue = this.plainText;
if (currentValue.length < this._minLength) {
return false;
}
if (currentValue !== this._lastCommittedValue) {
this._lastCommittedValue = currentValue;
this.emit("change" /* CHANGE */, currentValue);
}
this.emit("enter" /* ENTER */, currentValue);
return true;
}
deleteCharBackward() {
const result = super.deleteCharBackward();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
deleteChar() {
const result = super.deleteChar();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
deleteLine() {
const result = super.deleteLine();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
deleteWordBackward() {
const result = super.deleteWordBackward();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
deleteWordForward() {
const result = super.deleteWordForward();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
deleteToLineStart() {
const result = super.deleteToLineStart();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
deleteToLineEnd() {
const result = super.deleteToLineEnd();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
undo() {
const result = super.undo();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
redo() {
const result = super.redo();
this.emit("input" /* INPUT */, this.plainText);
return result;
}
deleteCharacter(direction) {
if (direction === "backward") {
this.deleteCharBackward();
} else {
this.deleteChar();
}
}
set maxLength(maxLength) {
this._maxLength = maxLength;
const currentValue = this.plainText;
if (currentValue.length > maxLength) {
this.setText(currentValue.substring(0, maxLength));
}
}
get maxLength() {
return this._maxLength;
}
set minLength(minLength) {
if (minLength > this._maxLength) {
throw new Error(`InputRenderable: minLength (${minLength}) cannot be greater than maxLength (${this._maxLength})`);
}
this._minLength = minLength;
}
get minLength() {
return this._minLength;
}
set placeholder(placeholder) {
super.placeholder = placeholder;
}
get placeholder() {
const p = super.placeholder;
return typeof p === "string" ? p : "";
}
set initialValue(value) {}
}
// src/renderables/Image.ts
var TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
function resolveImageRenderProtocol(requested, capabilities, hasResolution) {
if (requested !== "auto")
return requested === "sixel" && !hasResolution ? "blocks" : requested;
const configured = capabilities?.image_protocol ?? "auto";
if (configured !== "auto")
return configured === "sixel" && !hasResolution ? "blocks" : configured;
if (!capabilities || capabilities.multiplexer === "tmux")
return "blocks";
if (capabilities.kitty_graphics)
return "kitty";
if (capabilities.sixel && hasResolution)
return "sixel";
return "blocks";
}
function pixelResolution(ctx) {
const terminalWidth = ctx.terminalWidth ?? 0;
const terminalHeight = ctx.terminalHeight ?? 0;
const resolution = terminalWidth > 0 && terminalHeight > 0 ? ctx.resolution : null;
return resolution && resolution.width > 0 && resolution.height > 0 ? resolution : null;
}
class ImageRenderable extends Renderable {
_source;
_image = null;
_pendingImage = null;
_loadError = null;
_loadController = null;
onLoad;
onError;
_fit;
_protocol;
loadPromise = null;
constructor(ctx, options) {
super(ctx, options);
this._fit = options.fit ?? "fit";
this._protocol = options.protocol ?? "auto";
this.onLoad = options.onLoad;
this.onError = options.onError;
if (options.source !== undefined)
this.source = options.source;
}
get source() {
return this._source;
}
set source(source) {
source ??= undefined;
if (source === this._source)
return;
this._source = source;
this._loadController?.abort();
this._loadController = null;
this._pendingImage?.dispose();
this._pendingImage = null;
if (source === undefined) {
this._loadError = null;
this._image?.dispose();
this._image = null;
this.loadPromise = null;
this.requestRender();
return;
}
const controller = new AbortController;
this._loadController = controller;
this._loadError = null;
let imagePromise;
if (source instanceof NativeImage) {
try {
const image = source.retain();
this._pendingImage = image;
imagePromise = Promise.resolve(image);
} catch (error) {
imagePromise = Promise.reject(error);
}
} else {
imagePromise = NativeImage.load(source, { signal: controller.signal });
}
this.loadPromise = this.load(imagePromise, controller);
}
get image() {
return this._image;
}
get fit() {
return this._fit;
}
set fit(value) {
const next = value ?? "fit";
if (this._fit === next)
return;
this._fit = next;
this.requestRender();
}
get protocol() {
return this._protocol;
}
set protocol(value) {
const next = value ?? "auto";
if (this._protocol === next)
return;
this._protocol = next;
this.requestRender();
}
get effectiveProtocol() {
return resolveImageRenderProtocol(this._protocol, this._ctx.capabilities, pixelResolution(this._ctx) !== null);
}
get cellAspectRatio() {
const resolution = pixelResolution(this._ctx);
if (!resolution)
return 2;
const cellWidth = resolution.width / this._ctx.terminalWidth;
const cellHeight = resolution.height / this._ctx.terminalHeight;
return cellWidth > 0 && cellHeight > 0 ? cellHeight / cellWidth : 2;
}
getFittedSize(targetWidth, targetHeight, cellAspectRatio = this.cellAspectRatio, sourceWidth = this._image?.width ?? 0, sourceHeight = this._image?.height ?? 0) {
if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0)
return { width: 0, height: 0 };
if (this._fit === "fill")
return { width: targetWidth, height: targetHeight };
const displayAspect = sourceWidth / sourceHeight * cellAspectRatio;
const scale = this._fit === "fit" ? Math.min(targetWidth / displayAspect, targetHeight) : Math.max(targetWidth / displayAspect, targetHeight);
return {
width: Math.max(1, Math.round(displayAspect * scale)),
height: Math.max(1, Math.round(scale))
};
}
get loading() {
return this._loadController !== null;
}
get loadError() {
return this._loadError;
}
render(buffer, deltaTime) {
if (this.buffered)
this.frameBuffer?.clear(TRANSPARENT);
super.render(buffer, deltaTime);
}
renderSelf(buffer) {
if (!this._image || this.width <= 0 || this.height <= 0)
return;
const fitted = this._fit === "cover" ? { width: this.width, height: this.height } : this.getFittedSize(this.width, this.height);
if (fitted.width <= 0 || fitted.height <= 0)
return;
const originX = this.buffered ? 0 : this._screenX;
const originY = this.buffered ? 0 : this._screenY;
const x = originX + Math.floor((this.width - fitted.width) / 2);
const y = originY + Math.floor((this.height - fitted.height) / 2);
const resolution = pixelResolution(this._ctx);
const pixelWidth = resolution ? Math.max(1, Math.round(fitted.width * resolution.width / this._ctx.terminalWidth)) : 0;
const pixelHeight = resolution ? Math.max(1, Math.round(fitted.height * resolution.height / this._ctx.terminalHeight)) : 0;
let sourceX = 0;
let sourceY = 0;
let sourceWidth = this._image.width;
let sourceHeight = this._image.height;
if (this._fit === "cover") {
const targetAspect = this.width / (this.height * this.cellAspectRatio);
const sourceAspect = sourceWidth / sourceHeight;
if (sourceAspect > targetAspect) {
sourceWidth = Math.max(1, Math.round(sourceHeight * targetAspect));
sourceX = Math.floor((this._image.width - sourceWidth) / 2);
} else {
sourceHeight = Math.max(1, Math.round(sourceWidth / targetAspect));
sourceY = Math.floor((this._image.height - sourceHeight) / 2);
}
}
buffer.drawImage(this._image, x, y, fitted.width, fitted.height, pixelWidth, pixelHeight, sourceX, sourceY, sourceWidth, sourceHeight, this._protocol);
}
async load(imagePromise, controller) {
let image;
try {
image = await imagePromise;
} catch (error) {
if (controller.signal.aborted || this.isDestroyed || this._loadController !== controller)
return;
this._loadController = null;
this._loadError = error;
this.onError?.(error);
return;
}
if (this.isDestroyed || this._loadController !== controller) {
image.dispose();
return;
}
if (this._pendingImage === image)
this._pendingImage = null;
const previous = this._image;
this._image = image;
this._loadController = null;
previous?.dispose();
this.requestRender();
this.onLoad?.(image);
}
destroySelf() {
this._loadController?.abort();
this._loadController = null;
this._pendingImage?.dispose();
this._pendingImage = null;
this._image?.dispose();
this._image = null;
super.destroySelf();
}
}
// ../../node_modules/.bun/marked@17.0.1/node_modules/marked/lib/marked.esm.js
function L() {
return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
}
var T = L();
function Z(u) {
T = u;
}
var C = { exec: () => null };
function k(u, e = "") {
let t2 = typeof u == "string" ? u : u.source, n = { replace: (r, i) => {
let s = typeof i == "string" ? i : i.source;
return s = s.replace(m.caret, "$1"), t2 = t2.replace(r, s), n;
}, getRegex: () => new RegExp(t2, e) };
return n;
}
var me = (() => {
try {
return !!new RegExp("(?<=1)(?<!1)");
} catch {
return false;
}
})();
var m = { codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, outputLinkReplace: /\\([\[\]])/g, indentCodeCompensation: /^(\s+)(?:```)/, beginningSpace: /^\s+/, endingHash: /#$/, startingSpaceChar: /^ /, endingSpaceChar: / $/, nonSpaceChar: /[^ ]/, newLineCharGlobal: /\n/g, tabCharGlobal: /\t/g, multipleSpaceGlobal: /\s+/g, blankLine: /^[ \t]*$/, doubleBlankLine: /\n[ \t]*\n[ \t]*$/, blockquoteStart: /^ {0,3}>/, blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, listReplaceTabs: /^\t+/, listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, listIsTask: /^\[[ xX]\] +\S/, listReplaceTask: /^\[[ xX]\] +/, listTaskCheckbox: /\[[ xX]\]/, anyLine: /\n.*\n/, hrefBrackets: /^<(.*)>$/, tableDelimiter: /[:|]/, tableAlignChars: /^\||\| *$/g, tableRowBlankLine: /\n[ \t]*$/, tableAlignRight: /^ *-+: *$/, tableAlignCenter: /^ *:-+: *$/, tableAlignLeft: /^ *:-+ *$/, startATag: /^<a /i, endATag: /^<\/a>/i, startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, startAngleBracket: /^</, endAngleBracket: />$/, pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, unicodeAlphaNumeric: /[\p{L}\p{N}]/u, escapeTest: /[&<>"']/, escapeReplace: /[&<>"']/g, escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, unescapeTest: /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, caret: /(^|[^\[])\^/g, percentDecode: /%25/g, findPipe: /\|/g, splitPipe: / \|/, slashPipe: /\\\|/g, carriageReturn: /\r\n|\r/g, spaceLine: /^ +$/gm, notSpaceStart: /^\S*/, endingNewline: /\n$/, listItemRegex: (u) => new RegExp(`^( {0,3}${u})((?:[ ][^\\n]*)?(?:\\n|$))`), nextBulletRegex: (u) => new RegExp(`^ {0,${Math.min(3, u - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`), hrRegex: (u) => new RegExp(`^ {0,${Math.min(3, u - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`), fencesBeginRegex: (u) => new RegExp(`^ {0,${Math.min(3, u - 1)}}(?:\`\`\`|~~~)`), headingBeginRegex: (u) => new RegExp(`^ {0,${Math.min(3, u - 1)}}#`), htmlBeginRegex: (u) => new RegExp(`^ {0,${Math.min(3, u - 1)}}<(?:[a-z].*>|!--)`, "i") };
var xe = /^(?:[ \t]*(?:\n|$))+/;
var be = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/;
var Re = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/;
var I = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/;
var Te = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/;
var N = /(?:[*+-]|\d{1,9}[.)])/;
var re = /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/;
var se = k(re).replace(/bull/g, N).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/\|table/g, "").getRegex();
var Oe = k(re).replace(/bull/g, N).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex();
var Q = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/;
var we = /^[^\n]+/;
var F = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/;
var ye = k(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label", F).replace("title", /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex();
var Pe = k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g, N).getRegex();
var v = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul";
var j = /<!--(?:-?>|[\s\S]*?(?:-->|$))/;
var Se = k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))", "i").replace("comment", j).replace("tag", v).replace("attribute", / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex();
var ie = k(Q).replace("hr", I).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("|table", "").replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", v).getRegex();
var $e = k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph", ie).getRegex();
var U = { blockquote: $e, code: be, def: ye, fences: Re, heading: Te, hr: I, html: Se, lheading: se, list: Pe, newline: xe, paragraph: ie, table: C, text: we };
var te = k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr", I).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("blockquote", " {0,3}>").replace("code", "(?: {4}| {0,3}\t)[^\\n]").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", v).getRegex();
var _e = { ...U, lheading: Oe, table: te, paragraph: k(Q).replace("hr", I).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("table", te).replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)]) ").replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag", v).getRegex() };
var Le = { ...U, html: k(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment", j).replace(/tag/g, "(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(), def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, heading: /^(#{1,6})(.*)(?:\n+|$)/, fences: C, lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, paragraph: k(Q).replace("hr", I).replace("heading", ` *#{1,6} *[^
]`).replace("lheading", se).replace("|table", "").replace("blockquote", " {0,3}>").replace("|fences", "").replace("|list", "").replace("|html", "").replace("|tag", "").getRegex() };
var Me = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/;
var ze = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/;
var oe = /^( {2,}|\\)\n(?!\s*$)/;
var Ae = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/;
var D = /[\p{P}\p{S}]/u;
var K = /[\s\p{P}\p{S}]/u;
var ae = /[^\s\p{P}\p{S}]/u;
var Ce = k(/^((?![*_])punctSpace)/, "u").replace(/punctSpace/g, K).getRegex();
var le = /(?!~)[\p{P}\p{S}]/u;
var Ie = /(?!~)[\s\p{P}\p{S}]/u;
var Ee = /(?:[^\s\p{P}\p{S}]|~)/u;
var Be = k(/link|precode-code|html/, "g").replace("link", /\[(?:[^\[\]`]|(?<a>`+)[^`]+\k<a>(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-", me ? "(?<!`)()" : "(^^|[^`])").replace("code", /(?<b>`+)[^`]+\k<b>(?!`)/).replace("html", /<(?! )[^<>]*?>/).getRegex();
var ue = /^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/;
var qe = k(ue, "u").replace(/punct/g, D).getRegex();
var ve = k(ue, "u").replace(/punct/g, le).getRegex();
var pe = "^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)";
var De = k(pe, "gu").replace(/notPunctSpace/g, ae).replace(/punctSpace/g, K).replace(/punct/g, D).getRegex();
var He = k(pe, "gu").replace(/notPunctSpace/g, Ee).replace(/punctSpace/g, Ie).replace(/punct/g, le).getRegex();
var Ze = k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)", "gu").replace(/notPunctSpace/g, ae).replace(/punctSpace/g, K).replace(/punct/g, D).getRegex();
var Ge = k(/\\(punct)/, "gu").replace(/punct/g, D).getRegex();
var Ne = k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme", /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email", /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex();
var Qe = k(j).replace("(?:-->|$)", "-->").getRegex();
var Fe = k("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment", Qe).replace("attribute", /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex();
var q = /(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+[^`]*?`+(?!`)|[^\[\]\\`])*?/;
var je = k(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label", q).replace("href", /<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title", /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex();
var ce = k(/^!?\[(label)\]\[(ref)\]/).replace("label", q).replace("ref", F).getRegex();
var he = k(/^!?\[(ref)\](?:\[\])?/).replace("ref", F).getRegex();
var Ue = k("reflink|nolink(?!\\()", "g").replace("reflink", ce).replace("nolink", he).getRegex();
var ne = /[hH][tT][tT][pP][sS]?|[fF][tT][pP]/;
var W = { _backpedal: C, anyPunctuation: Ge, autolink: Ne, blockSkip: Be, br: oe, code: ze, del: C, emStrongLDelim: qe, emStrongRDelimAst: De, emStrongRDelimUnd: Ze, escape: Me, link: je, nolink: he, punctuation: Ce, reflink: ce, reflinkSearch: Ue, tag: Fe, text: Ae, url: C };
var Ke = { ...W, link: k(/^!?\[(label)\]\((.*?)\)/).replace("label", q).getRegex(), reflink: k(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label", q).getRegex() };
var G = { ...W, emStrongRDelimAst: He, emStrongLDelim: ve, url: k(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol", ne).replace("email", /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(), _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, del: /^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/, text: k(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/).replace("protocol", ne).getRegex() };
var We = { ...G, br: k(oe).replace("{2,}", "*").getRegex(), text: k(G.text).replace("\\b_", "\\b_| {2,}\\n").replace(/\{2,\}/g, "*").getRegex() };
var E = { normal: U, gfm: _e, pedantic: Le };
var M = { normal: W, gfm: G, breaks: We, pedantic: Ke };
var Xe = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
var ke = (u) => Xe[u];
function w(u, e) {
if (e) {
if (m.escapeTest.test(u))
return u.replace(m.escapeReplace, ke);
} else if (m.escapeTestNoEncode.test(u))
return u.replace(m.escapeReplaceNoEncode, ke);
return u;
}
function X(u) {
try {
u = encodeURI(u).replace(m.percentDecode, "%");
} catch {
return null;
}
return u;
}
function J(u, e) {
let t2 = u.replace(m.findPipe, (i, s, a) => {
let o = false, l = s;
for (;--l >= 0 && a[l] === "\\"; )
o = !o;
return o ? "|" : " |";
}), n = t2.split(m.splitPipe), r = 0;
if (n[0].trim() || n.shift(), n.length > 0 && !n.at(-1)?.trim() && n.pop(), e)
if (n.length > e)
n.splice(e);
else
for (;n.length < e; )
n.push("");
for (;r < n.length; r++)
n[r] = n[r].trim().replace(m.slashPipe, "|");
return n;
}
function z(u, e, t2) {
let n = u.length;
if (n === 0)
return "";
let r = 0;
for (;r < n; ) {
let i = u.charAt(n - r - 1);
if (i === e && !t2)
r++;
else if (i !== e && t2)
r++;
else
break;
}
return u.slice(0, n - r);
}
function de(u, e) {
if (u.indexOf(e[1]) === -1)
return -1;
let t2 = 0;
for (let n = 0;n < u.length; n++)
if (u[n] === "\\")
n++;
else if (u[n] === e[0])
t2++;
else if (u[n] === e[1] && (t2--, t2 < 0))
return n;
return t2 > 0 ? -2 : -1;
}
function ge(u, e, t2, n, r) {
let i = e.href, s = e.title || null, a = u[1].replace(r.other.outputLinkReplace, "$1");
n.state.inLink = true;
let o = { type: u[0].charAt(0) === "!" ? "image" : "link", raw: t2, href: i, title: s, text: a, tokens: n.inlineTokens(a) };
return n.state.inLink = false, o;
}
function Je(u, e, t2) {
let n = u.match(t2.other.indentCodeCompensation);
if (n === null)
return e;
let r = n[1];
return e.split(`
`).map((i) => {
let s = i.match(t2.other.beginningSpace);
if (s === null)
return i;
let [a] = s;
return a.length >= r.length ? i.slice(r.length) : i;
}).join(`
`);
}
var y = class {
options;
rules;
lexer;
constructor(e) {
this.options = e || T;
}
space(e) {
let t2 = this.rules.block.newline.exec(e);
if (t2 && t2[0].length > 0)
return { type: "space", raw: t2[0] };
}
code(e) {
let t2 = this.rules.block.code.exec(e);
if (t2) {
let n = t2[0].replace(this.rules.other.codeRemoveIndent, "");
return { type: "code", raw: t2[0], codeBlockStyle: "indented", text: this.options.pedantic ? n : z(n, `
`) };
}
}
fences(e) {
let t2 = this.rules.block.fences.exec(e);
if (t2) {
let n = t2[0], r = Je(n, t2[3] || "", this.rules);
return { type: "code", raw: n, lang: t2[2] ? t2[2].trim().replace(this.rules.inline.anyPunctuation, "$1") : t2[2], text: r };
}
}
heading(e) {
let t2 = this.rules.block.heading.exec(e);
if (t2) {
let n = t2[2].trim();
if (this.rules.other.endingHash.test(n)) {
let r = z(n, "#");
(this.options.pedantic || !r || this.rules.other.endingSpaceChar.test(r)) && (n = r.trim());
}
return { type: "heading", raw: t2[0], depth: t2[1].length, text: n, tokens: this.lexer.inline(n) };
}
}
hr(e) {
let t2 = this.rules.block.hr.exec(e);
if (t2)
return { type: "hr", raw: z(t2[0], `
`) };
}
blockquote(e) {
let t2 = this.rules.block.blockquote.exec(e);
if (t2) {
let n = z(t2[0], `
`).split(`
`), r = "", i = "", s = [];
for (;n.length > 0; ) {
let a = false, o = [], l;
for (l = 0;l < n.length; l++)
if (this.rules.other.blockquoteStart.test(n[l]))
o.push(n[l]), a = true;
else if (!a)
o.push(n[l]);
else
break;
n = n.slice(l);
let p = o.join(`
`), c = p.replace(this.rules.other.blockquoteSetextReplace, `
$1`).replace(this.rules.other.blockquoteSetextReplace2, "");
r = r ? `${r}
${p}` : p, i = i ? `${i}
${c}` : c;
let g = this.lexer.state.top;
if (this.lexer.state.top = true, this.lexer.blockTokens(c, s, true), this.lexer.state.top = g, n.length === 0)
break;
let h2 = s.at(-1);
if (h2?.type === "code")
break;
if (h2?.type === "blockquote") {
let R = h2, f = R.raw + `
` + n.join(`
`), O = this.blockquote(f);
s[s.length - 1] = O, r = r.substring(0, r.length - R.raw.length) + O.raw, i = i.substring(0, i.length - R.text.length) + O.text;
break;
} else if (h2?.type === "list") {
let R = h2, f = R.raw + `
` + n.join(`
`), O = this.list(f);
s[s.length - 1] = O, r = r.substring(0, r.length - h2.raw.length) + O.raw, i = i.substring(0, i.length - R.raw.length) + O.raw, n = f.substring(s.at(-1).raw.length).split(`
`);
continue;
}
}
return { type: "blockquote", raw: r, tokens: s, text: i };
}
}
list(e) {
let t2 = this.rules.block.list.exec(e);
if (t2) {
let n = t2[1].trim(), r = n.length > 1, i = { type: "list", raw: "", ordered: r, start: r ? +n.slice(0, -1) : "", loose: false, items: [] };
n = r ? `\\d{1,9}\\${n.slice(-1)}` : `\\${n}`, this.options.pedantic && (n = r ? n : "[*+-]");
let s = this.rules.other.listItemRegex(n), a = false;
for (;e; ) {
let l = false, p = "", c = "";
if (!(t2 = s.exec(e)) || this.rules.block.hr.test(e))
break;
p = t2[0], e = e.substring(p.length);
let g = t2[2].split(`
`, 1)[0].replace(this.rules.other.listReplaceTabs, (O) => " ".repeat(3 * O.length)), h2 = e.split(`
`, 1)[0], R = !g.trim(), f = 0;
if (this.options.pedantic ? (f = 2, c = g.trimStart()) : R ? f = t2[1].length + 1 : (f = t2[2].search(this.rules.other.nonSpaceChar), f = f > 4 ? 1 : f, c = g.slice(f), f += t2[1].length), R && this.rules.other.blankLine.test(h2) && (p += h2 + `
`, e = e.substring(h2.length + 1), l = true), !l) {
let O = this.rules.other.nextBulletRegex(f), V = this.rules.other.hrRegex(f), Y = this.rules.other.fencesBeginRegex(f), ee = this.rules.other.headingBeginRegex(f), fe = this.rules.other.htmlBeginRegex(f);
for (;e; ) {
let H = e.split(`
`, 1)[0], A;
if (h2 = H, this.options.pedantic ? (h2 = h2.replace(this.rules.other.listReplaceNesting, " "), A = h2) : A = h2.replace(this.rules.other.tabCharGlobal, " "), Y.test(h2) || ee.test(h2) || fe.test(h2) || O.test(h2) || V.test(h2))
break;
if (A.search(this.rules.other.nonSpaceChar) >= f || !h2.trim())
c += `
` + A.slice(f);
else {
if (R || g.replace(this.rules.other.tabCharGlobal, " ").search(this.rules.other.nonSpaceChar) >= 4 || Y.test(g) || ee.test(g) || V.test(g))
break;
c += `
` + h2;
}
!R && !h2.trim() && (R = true), p += H + `
`, e = e.substring(H.length + 1), g = A.slice(f);
}
}
i.loose || (a ? i.loose = true : this.rules.other.doubleBlankLine.test(p) && (a = true)), i.items.push({ type: "list_item", raw: p, task: !!this.options.gfm && this.rules.other.listIsTask.test(c), loose: false, text: c, tokens: [] }), i.raw += p;
}
let o = i.items.at(-1);
if (o)
o.raw = o.raw.trimEnd(), o.text = o.text.trimEnd();
else
return;
i.raw = i.raw.trimEnd();
for (let l of i.items) {
if (this.lexer.state.top = false, l.tokens = this.lexer.blockTokens(l.text, []), l.task) {
if (l.text = l.text.replace(this.rules.other.listReplaceTask, ""), l.tokens[0]?.type === "text" || l.tokens[0]?.type === "paragraph") {
l.tokens[0].raw = l.tokens[0].raw.replace(this.rules.other.listReplaceTask, ""), l.tokens[0].text = l.tokens[0].text.replace(this.rules.other.listReplaceTask, "");
for (let c = this.lexer.inlineQueue.length - 1;c >= 0; c--)
if (this.rules.other.listIsTask.test(this.lexer.inlineQueue[c].src)) {
this.lexer.inlineQueue[c].src = this.lexer.inlineQueue[c].src.replace(this.rules.other.listReplaceTask, "");
break;
}
}
let p = this.rules.other.listTaskCheckbox.exec(l.raw);
if (p) {
let c = { type: "checkbox", raw: p[0] + " ", checked: p[0] !== "[ ]" };
l.checked = c.checked, i.loose ? l.tokens[0] && ["paragraph", "text"].includes(l.tokens[0].type) && "tokens" in l.tokens[0] && l.tokens[0].tokens ? (l.tokens[0].raw = c.raw + l.tokens[0].raw, l.tokens[0].text = c.raw + l.tokens[0].text, l.tokens[0].tokens.unshift(c)) : l.tokens.unshift({ type: "paragraph", raw: c.raw, text: c.raw, tokens: [c] }) : l.tokens.unshift(c);
}
}
if (!i.loose) {
let p = l.tokens.filter((g) => g.type === "space"), c = p.length > 0 && p.some((g) => this.rules.other.anyLine.test(g.raw));
i.loose = c;
}
}
if (i.loose)
for (let l of i.items) {
l.loose = true;
for (let p of l.tokens)
p.type === "text" && (p.type = "paragraph");
}
return i;
}
}
html(e) {
let t2 = this.rules.block.html.exec(e);
if (t2)
return { type: "html", block: true, raw: t2[0], pre: t2[1] === "pre" || t2[1] === "script" || t2[1] === "style", text: t2[0] };
}
def(e) {
let t2 = this.rules.block.def.exec(e);
if (t2) {
let n = t2[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, " "), r = t2[2] ? t2[2].replace(this.rules.other.hrefBrackets, "$1").replace(this.rules.inline.anyPunctuation, "$1") : "", i = t2[3] ? t2[3].substring(1, t2[3].length - 1).replace(this.rules.inline.anyPunctuation, "$1") : t2[3];
return { type: "def", tag: n, raw: t2[0], href: r, title: i };
}
}
table(e) {
let t2 = this.rules.block.table.exec(e);
if (!t2 || !this.rules.other.tableDelimiter.test(t2[2]))
return;
let n = J(t2[1]), r = t2[2].replace(this.rules.other.tableAlignChars, "").split("|"), i = t2[3]?.trim() ? t2[3].replace(this.rules.other.tableRowBlankLine, "").split(`
`) : [], s = { type: "table", raw: t2[0], header: [], align: [], rows: [] };
if (n.length === r.length) {
for (let a of r)
this.rules.other.tableAlignRight.test(a) ? s.align.push("right") : this.rules.other.tableAlignCenter.test(a) ? s.align.push("center") : this.rules.other.tableAlignLeft.test(a) ? s.align.push("left") : s.align.push(null);
for (let a = 0;a < n.length; a++)
s.header.push({ text: n[a], tokens: this.lexer.inline(n[a]), header: true, align: s.align[a] });
for (let a of i)
s.rows.push(J(a, s.header.length).map((o, l) => ({ text: o, tokens: this.lexer.inline(o), header: false, align: s.align[l] })));
return s;
}
}
lheading(e) {
let t2 = this.rules.block.lheading.exec(e);
if (t2)
return { type: "heading", raw: t2[0], depth: t2[2].charAt(0) === "=" ? 1 : 2, text: t2[1], tokens: this.lexer.inline(t2[1]) };
}
paragraph(e) {
let t2 = this.rules.block.paragraph.exec(e);
if (t2) {
let n = t2[1].charAt(t2[1].length - 1) === `
` ? t2[1].slice(0, -1) : t2[1];
return { type: "paragraph", raw: t2[0], text: n, tokens: this.lexer.inline(n) };
}
}
text(e) {
let t2 = this.rules.block.text.exec(e);
if (t2)
return { type: "text", raw: t2[0], text: t2[0], tokens: this.lexer.inline(t2[0]) };
}
escape(e) {
let t2 = this.rules.inline.escape.exec(e);
if (t2)
return { type: "escape", raw: t2[0], text: t2[1] };
}
tag(e) {
let t2 = this.rules.inline.tag.exec(e);
if (t2)
return !this.lexer.state.inLink && this.rules.other.startATag.test(t2[0]) ? this.lexer.state.inLink = true : this.lexer.state.inLink && this.rules.other.endATag.test(t2[0]) && (this.lexer.state.inLink = false), !this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(t2[0]) ? this.lexer.state.inRawBlock = true : this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(t2[0]) && (this.lexer.state.inRawBlock = false), { type: "html", raw: t2[0], inLink: this.lexer.state.inLink, inRawBlock: this.lexer.state.inRawBlock, block: false, text: t2[0] };
}
link(e) {
let t2 = this.rules.inline.link.exec(e);
if (t2) {
let n = t2[2].trim();
if (!this.options.pedantic && this.rules.other.startAngleBracket.test(n)) {
if (!this.rules.other.endAngleBracket.test(n))
return;
let s = z(n.slice(0, -1), "\\");
if ((n.length - s.length) % 2 === 0)
return;
} else {
let s = de(t2[2], "()");
if (s === -2)
return;
if (s > -1) {
let o = (t2[0].indexOf("!") === 0 ? 5 : 4) + t2[1].length + s;
t2[2] = t2[2].substring(0, s), t2[0] = t2[0].substring(0, o).trim(), t2[3] = "";
}
}
let r = t2[2], i = "";
if (this.options.pedantic) {
let s = this.rules.other.pedanticHrefTitle.exec(r);
s && (r = s[1], i = s[3]);
} else
i = t2[3] ? t2[3].slice(1, -1) : "";
return r = r.trim(), this.rules.other.startAngleBracket.test(r) && (this.options.pedantic && !this.rules.other.endAngleBracket.test(n) ? r = r.slice(1) : r = r.slice(1, -1)), ge(t2, { href: r && r.replace(this.rules.inline.anyPunctuation, "$1"), title: i && i.replace(this.rules.inline.anyPunctuation, "$1") }, t2[0], this.lexer, this.rules);
}
}
reflink(e, t2) {
let n;
if ((n = this.rules.inline.reflink.exec(e)) || (n = this.rules.inline.nolink.exec(e))) {
let r = (n[2] || n[1]).replace(this.rules.other.multipleSpaceGlobal, " "), i = t2[r.toLowerCase()];
if (!i) {
let s = n[0].charAt(0);
return { type: "text", raw: s, text: s };
}
return ge(n, i, n[0], this.lexer, this.rules);
}
}
emStrong(e, t2, n = "") {
let r = this.rules.inline.emStrongLDelim.exec(e);
if (!r || r[3] && n.match(this.rules.other.unicodeAlphaNumeric))
return;
if (!(r[1] || r[2] || "") || !n || this.rules.inline.punctuation.exec(n)) {
let s = [...r[0]].length - 1, a, o, l = s, p = 0, c = r[0][0] === "*" ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd;
for (c.lastIndex = 0, t2 = t2.slice(-1 * e.length + s);(r = c.exec(t2)) != null; ) {
if (a = r[1] || r[2] || r[3] || r[4] || r[5] || r[6], !a)
continue;
if (o = [...a].length, r[3] || r[4]) {
l += o;
continue;
} else if ((r[5] || r[6]) && s % 3 && !((s + o) % 3)) {
p += o;
continue;
}
if (l -= o, l > 0)
continue;
o = Math.min(o, o + l + p);
let g = [...r[0]][0].length, h2 = e.slice(0, s + r.index + g + o);
if (Math.min(s, o) % 2) {
let f = h2.slice(1, -1);
return { type: "em", raw: h2, text: f, tokens: this.lexer.inlineTokens(f) };
}
let R = h2.slice(2, -2);
return { type: "strong", raw: h2, text: R, tokens: this.lexer.inlineTokens(R) };
}
}
}
codespan(e) {
let t2 = this.rules.inline.code.exec(e);
if (t2) {
let n = t2[2].replace(this.rules.other.newLineCharGlobal, " "), r = this.rules.other.nonSpaceChar.test(n), i = this.rules.other.startingSpaceChar.test(n) && this.rules.other.endingSpaceChar.test(n);
return r && i && (n = n.substring(1, n.length - 1)), { type: "codespan", raw: t2[0], text: n };
}
}
br(e) {
let t2 = this.rules.inline.br.exec(e);
if (t2)
return { type: "br", raw: t2[0] };
}
del(e) {
let t2 = this.rules.inline.del.exec(e);
if (t2)
return { type: "del", raw: t2[0], text: t2[2], tokens: this.lexer.inlineTokens(t2[2]) };
}
autolink(e) {
let t2 = this.rules.inline.autolink.exec(e);
if (t2) {
let n, r;
return t2[2] === "@" ? (n = t2[1], r = "mailto:" + n) : (n = t2[1], r = n), { type: "link", raw: t2[0], text: n, href: r, tokens: [{ type: "text", raw: n, text: n }] };
}
}
url(e) {
let t2;
if (t2 = this.rules.inline.url.exec(e)) {
let n, r;
if (t2[2] === "@")
n = t2[0], r = "mailto:" + n;
else {
let i;
do
i = t2[0], t2[0] = this.rules.inline._backpedal.exec(t2[0])?.[0] ?? "";
while (i !== t2[0]);
n = t2[0], t2[1] === "www." ? r = "http://" + t2[0] : r = t2[0];
}
return { type: "link", raw: t2[0], text: n, href: r, tokens: [{ type: "text", raw: n, text: n }] };
}
}
inlineText(e) {
let t2 = this.rules.inline.text.exec(e);
if (t2) {
let n = this.lexer.state.inRawBlock;
return { type: "text", raw: t2[0], text: t2[0], escaped: n };
}
}
};
var x = class u {
tokens;
options;
state;
inlineQueue;
tokenizer;
constructor(e) {
this.tokens = [], this.tokens.links = Object.create(null), this.options = e || T, this.options.tokenizer = this.options.tokenizer || new y, this.tokenizer = this.options.tokenizer, this.tokenizer.options = this.options, this.tokenizer.lexer = this, this.inlineQueue = [], this.state = { inLink: false, inRawBlock: false, top: true };
let t2 = { other: m, block: E.normal, inline: M.normal };
this.options.pedantic ? (t2.block = E.pedantic, t2.inline = M.pedantic) : this.options.gfm && (t2.block = E.gfm, this.options.breaks ? t2.inline = M.breaks : t2.inline = M.gfm), this.tokenizer.rules = t2;
}
static get rules() {
return { block: E, inline: M };
}
static lex(e, t2) {
return new u(t2).lex(e);
}
static lexInline(e, t2) {
return new u(t2).inlineTokens(e);
}
lex(e) {
e = e.replace(m.carriageReturn, `
`), this.blockTokens(e, this.tokens);
for (let t2 = 0;t2 < this.inlineQueue.length; t2++) {
let n = this.inlineQueue[t2];
this.inlineTokens(n.src, n.tokens);
}
return this.inlineQueue = [], this.tokens;
}
blockTokens(e, t2 = [], n = false) {
for (this.options.pedantic && (e = e.replace(m.tabCharGlobal, " ").replace(m.spaceLine, ""));e; ) {
let r;
if (this.options.extensions?.block?.some((s) => (r = s.call({ lexer: this }, e, t2)) ? (e = e.substring(r.raw.length), t2.push(r), true) : false))
continue;
if (r = this.tokenizer.space(e)) {
e = e.substring(r.raw.length);
let s = t2.at(-1);
r.raw.length === 1 && s !== undefined ? s.raw += `
` : t2.push(r);
continue;
}
if (r = this.tokenizer.code(e)) {
e = e.substring(r.raw.length);
let s = t2.at(-1);
s?.type === "paragraph" || s?.type === "text" ? (s.raw += (s.raw.endsWith(`
`) ? "" : `
`) + r.raw, s.text += `
` + r.text, this.inlineQueue.at(-1).src = s.text) : t2.push(r);
continue;
}
if (r = this.tokenizer.fences(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
if (r = this.tokenizer.heading(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
if (r = this.tokenizer.hr(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
if (r = this.tokenizer.blockquote(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
if (r = this.tokenizer.list(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
if (r = this.tokenizer.html(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
if (r = this.tokenizer.def(e)) {
e = e.substring(r.raw.length);
let s = t2.at(-1);
s?.type === "paragraph" || s?.type === "text" ? (s.raw += (s.raw.endsWith(`
`) ? "" : `
`) + r.raw, s.text += `
` + r.raw, this.inlineQueue.at(-1).src = s.text) : this.tokens.links[r.tag] || (this.tokens.links[r.tag] = { href: r.href, title: r.title }, t2.push(r));
continue;
}
if (r = this.tokenizer.table(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
if (r = this.tokenizer.lheading(e)) {
e = e.substring(r.raw.length), t2.push(r);
continue;
}
let i = e;
if (this.options.extensions?.startBlock) {
let s = 1 / 0, a = e.slice(1), o;
this.options.extensions.startBlock.forEach((l) => {
o = l.call({ lexer: this }, a), typeof o == "number" && o >= 0 && (s = Math.min(s, o));
}), s < 1 / 0 && s >= 0 && (i = e.substring(0, s + 1));
}
if (this.state.top && (r = this.tokenizer.paragraph(i))) {
let s = t2.at(-1);
n && s?.type === "paragraph" ? (s.raw += (s.raw.endsWith(`
`) ? "" : `
`) + r.raw, s.text += `
` + r.text, this.inlineQueue.pop(), this.inlineQueue.at(-1).src = s.text) : t2.push(r), n = i.length !== e.length, e = e.substring(r.raw.length);
continue;
}
if (r = this.tokenizer.text(e)) {
e = e.substring(r.raw.length);
let s = t2.at(-1);
s?.type === "text" ? (s.raw += (s.raw.endsWith(`
`) ? "" : `
`) + r.raw, s.text += `
` + r.text, this.inlineQueue.pop(), this.inlineQueue.at(-1).src = s.text) : t2.push(r);
continue;
}
if (e) {
let s = "Infinite loop on byte: " + e.charCodeAt(0);
if (this.options.silent) {
console.error(s);
break;
} else
throw new Error(s);
}
}
return this.state.top = true, t2;
}
inline(e, t2 = []) {
return this.inlineQueue.push({ src: e, tokens: t2 }), t2;
}
inlineTokens(e, t2 = []) {
let n = e, r = null;
if (this.tokens.links) {
let o = Object.keys(this.tokens.links);
if (o.length > 0)
for (;(r = this.tokenizer.rules.inline.reflinkSearch.exec(n)) != null; )
o.includes(r[0].slice(r[0].lastIndexOf("[") + 1, -1)) && (n = n.slice(0, r.index) + "[" + "a".repeat(r[0].length - 2) + "]" + n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex));
}
for (;(r = this.tokenizer.rules.inline.anyPunctuation.exec(n)) != null; )
n = n.slice(0, r.index) + "++" + n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);
let i;
for (;(r = this.tokenizer.rules.inline.blockSkip.exec(n)) != null; )
i = r[2] ? r[2].length : 0, n = n.slice(0, r.index + i) + "[" + "a".repeat(r[0].length - i - 2) + "]" + n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);
n = this.options.hooks?.emStrongMask?.call({ lexer: this }, n) ?? n;
let s = false, a = "";
for (;e; ) {
s || (a = ""), s = false;
let o;
if (this.options.extensions?.inline?.some((p) => (o = p.call({ lexer: this }, e, t2)) ? (e = e.substring(o.raw.length), t2.push(o), true) : false))
continue;
if (o = this.tokenizer.escape(e)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (o = this.tokenizer.tag(e)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (o = this.tokenizer.link(e)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (o = this.tokenizer.reflink(e, this.tokens.links)) {
e = e.substring(o.raw.length);
let p = t2.at(-1);
o.type === "text" && p?.type === "text" ? (p.raw += o.raw, p.text += o.text) : t2.push(o);
continue;
}
if (o = this.tokenizer.emStrong(e, n, a)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (o = this.tokenizer.codespan(e)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (o = this.tokenizer.br(e)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (o = this.tokenizer.del(e)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (o = this.tokenizer.autolink(e)) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
if (!this.state.inLink && (o = this.tokenizer.url(e))) {
e = e.substring(o.raw.length), t2.push(o);
continue;
}
let l = e;
if (this.options.extensions?.startInline) {
let p = 1 / 0, c = e.slice(1), g;
this.options.extensions.startInline.forEach((h2) => {
g = h2.call({ lexer: this }, c), typeof g == "number" && g >= 0 && (p = Math.min(p, g));
}), p < 1 / 0 && p >= 0 && (l = e.substring(0, p + 1));
}
if (o = this.tokenizer.inlineText(l)) {
e = e.substring(o.raw.length), o.raw.slice(-1) !== "_" && (a = o.raw.slice(-1)), s = true;
let p = t2.at(-1);
p?.type === "text" ? (p.raw += o.raw, p.text += o.text) : t2.push(o);
continue;
}
if (e) {
let p = "Infinite loop on byte: " + e.charCodeAt(0);
if (this.options.silent) {
console.error(p);
break;
} else
throw new Error(p);
}
}
return t2;
}
};
var P = class {
options;
parser;
constructor(e) {
this.options = e || T;
}
space(e) {
return "";
}
code({ text: e, lang: t2, escaped: n }) {
let r = (t2 || "").match(m.notSpaceStart)?.[0], i = e.replace(m.endingNewline, "") + `
`;
return r ? '<pre><code class="language-' + w(r) + '">' + (n ? i : w(i, true)) + `</code></pre>
` : "<pre><code>" + (n ? i : w(i, true)) + `</code></pre>
`;
}
blockquote({ tokens: e }) {
return `<blockquote>
${this.parser.parse(e)}</blockquote>
`;
}
html({ text: e }) {
return e;
}
def(e) {
return "";
}
heading({ tokens: e, depth: t2 }) {
return `<h${t2}>${this.parser.parseInline(e)}</h${t2}>
`;
}
hr(e) {
return `<hr>
`;
}
list(e) {
let { ordered: t2, start: n } = e, r = "";
for (let a = 0;a < e.items.length; a++) {
let o = e.items[a];
r += this.listitem(o);
}
let i = t2 ? "ol" : "ul", s = t2 && n !== 1 ? ' start="' + n + '"' : "";
return "<" + i + s + `>
` + r + "</" + i + `>
`;
}
listitem(e) {
return `<li>${this.parser.parse(e.tokens)}</li>
`;
}
checkbox({ checked: e }) {
return "<input " + (e ? 'checked="" ' : "") + 'disabled="" type="checkbox"> ';
}
paragraph({ tokens: e }) {
return `<p>${this.parser.parseInline(e)}</p>
`;
}
table(e) {
let t2 = "", n = "";
for (let i = 0;i < e.header.length; i++)
n += this.tablecell(e.header[i]);
t2 += this.tablerow({ text: n });
let r = "";
for (let i = 0;i < e.rows.length; i++) {
let s = e.rows[i];
n = "";
for (let a = 0;a < s.length; a++)
n += this.tablecell(s[a]);
r += this.tablerow({ text: n });
}
return r && (r = `<tbody>${r}</tbody>`), `<table>
<thead>
` + t2 + `</thead>
` + r + `</table>
`;
}
tablerow({ text: e }) {
return `<tr>
${e}</tr>
`;
}
tablecell(e) {
let t2 = this.parser.parseInline(e.tokens), n = e.header ? "th" : "td";
return (e.align ? `<${n} align="${e.align}">` : `<${n}>`) + t2 + `</${n}>
`;
}
strong({ tokens: e }) {
return `<strong>${this.parser.parseInline(e)}</strong>`;
}
em({ tokens: e }) {
return `<em>${this.parser.parseInline(e)}</em>`;
}
codespan({ text: e }) {
return `<code>${w(e, true)}</code>`;
}
br(e) {
return "<br>";
}
del({ tokens: e }) {
return `<del>${this.parser.parseInline(e)}</del>`;
}
link({ href: e, title: t2, tokens: n }) {
let r = this.parser.parseInline(n), i = X(e);
if (i === null)
return r;
e = i;
let s = '<a href="' + e + '"';
return t2 && (s += ' title="' + w(t2) + '"'), s += ">" + r + "</a>", s;
}
image({ href: e, title: t2, text: n, tokens: r }) {
r && (n = this.parser.parseInline(r, this.parser.textRenderer));
let i = X(e);
if (i === null)
return w(n);
e = i;
let s = `<img src="${e}" alt="${n}"`;
return t2 && (s += ` title="${w(t2)}"`), s += ">", s;
}
text(e) {
return "tokens" in e && e.tokens ? this.parser.parseInline(e.tokens) : ("escaped" in e) && e.escaped ? e.text : w(e.text);
}
};
var $ = class {
strong({ text: e }) {
return e;
}
em({ text: e }) {
return e;
}
codespan({ text: e }) {
return e;
}
del({ text: e }) {
return e;
}
html({ text: e }) {
return e;
}
text({ text: e }) {
return e;
}
link({ text: e }) {
return "" + e;
}
image({ text: e }) {
return "" + e;
}
br() {
return "";
}
checkbox({ raw: e }) {
return e;
}
};
var b = class u2 {
options;
renderer;
textRenderer;
constructor(e) {
this.options = e || T, this.options.renderer = this.options.renderer || new P, this.renderer = this.options.renderer, this.renderer.options = this.options, this.renderer.parser = this, this.textRenderer = new $;
}
static parse(e, t2) {
return new u2(t2).parse(e);
}
static parseInline(e, t2) {
return new u2(t2).parseInline(e);
}
parse(e) {
let t2 = "";
for (let n = 0;n < e.length; n++) {
let r = e[n];
if (this.options.extensions?.renderers?.[r.type]) {
let s = r, a = this.options.extensions.renderers[s.type].call({ parser: this }, s);
if (a !== false || !["space", "hr", "heading", "code", "table", "blockquote", "list", "html", "def", "paragraph", "text"].includes(s.type)) {
t2 += a || "";
continue;
}
}
let i = r;
switch (i.type) {
case "space": {
t2 += this.renderer.space(i);
break;
}
case "hr": {
t2 += this.renderer.hr(i);
break;
}
case "heading": {
t2 += this.renderer.heading(i);
break;
}
case "code": {
t2 += this.renderer.code(i);
break;
}
case "table": {
t2 += this.renderer.table(i);
break;
}
case "blockquote": {
t2 += this.renderer.blockquote(i);
break;
}
case "list": {
t2 += this.renderer.list(i);
break;
}
case "checkbox": {
t2 += this.renderer.checkbox(i);
break;
}
case "html": {
t2 += this.renderer.html(i);
break;
}
case "def": {
t2 += this.renderer.def(i);
break;
}
case "paragraph": {
t2 += this.renderer.paragraph(i);
break;
}
case "text": {
t2 += this.renderer.text(i);
break;
}
default: {
let s = 'Token with "' + i.type + '" type was not found.';
if (this.options.silent)
return console.error(s), "";
throw new Error(s);
}
}
}
return t2;
}
parseInline(e, t2 = this.renderer) {
let n = "";
for (let r = 0;r < e.length; r++) {
let i = e[r];
if (this.options.extensions?.renderers?.[i.type]) {
let a = this.options.extensions.renderers[i.type].call({ parser: this }, i);
if (a !== false || !["escape", "html", "link", "image", "strong", "em", "codespan", "br", "del", "text"].includes(i.type)) {
n += a || "";
continue;
}
}
let s = i;
switch (s.type) {
case "escape": {
n += t2.text(s);
break;
}
case "html": {
n += t2.html(s);
break;
}
case "link": {
n += t2.link(s);
break;
}
case "image": {
n += t2.image(s);
break;
}
case "checkbox": {
n += t2.checkbox(s);
break;
}
case "strong": {
n += t2.strong(s);
break;
}
case "em": {
n += t2.em(s);
break;
}
case "codespan": {
n += t2.codespan(s);
break;
}
case "br": {
n += t2.br(s);
break;
}
case "del": {
n += t2.del(s);
break;
}
case "text": {
n += t2.text(s);
break;
}
default: {
let a = 'Token with "' + s.type + '" type was not found.';
if (this.options.silent)
return console.error(a), "";
throw new Error(a);
}
}
}
return n;
}
};
var S = class {
options;
block;
constructor(e) {
this.options = e || T;
}
static passThroughHooks = new Set(["preprocess", "postprocess", "processAllTokens", "emStrongMask"]);
static passThroughHooksRespectAsync = new Set(["preprocess", "postprocess", "processAllTokens"]);
preprocess(e) {
return e;
}
postprocess(e) {
return e;
}
processAllTokens(e) {
return e;
}
emStrongMask(e) {
return e;
}
provideLexer() {
return this.block ? x.lex : x.lexInline;
}
provideParser() {
return this.block ? b.parse : b.parseInline;
}
};
var B = class {
defaults = L();
options = this.setOptions;
parse = this.parseMarkdown(true);
parseInline = this.parseMarkdown(false);
Parser = b;
Renderer = P;
TextRenderer = $;
Lexer = x;
Tokenizer = y;
Hooks = S;
constructor(...e) {
this.use(...e);
}
walkTokens(e, t2) {
let n = [];
for (let r of e)
switch (n = n.concat(t2.call(this, r)), r.type) {
case "table": {
let i = r;
for (let s of i.header)
n = n.concat(this.walkTokens(s.tokens, t2));
for (let s of i.rows)
for (let a of s)
n = n.concat(this.walkTokens(a.tokens, t2));
break;
}
case "list": {
let i = r;
n = n.concat(this.walkTokens(i.items, t2));
break;
}
default: {
let i = r;
this.defaults.extensions?.childTokens?.[i.type] ? this.defaults.extensions.childTokens[i.type].forEach((s) => {
let a = i[s].flat(1 / 0);
n = n.concat(this.walkTokens(a, t2));
}) : i.tokens && (n = n.concat(this.walkTokens(i.tokens, t2)));
}
}
return n;
}
use(...e) {
let t2 = this.defaults.extensions || { renderers: {}, childTokens: {} };
return e.forEach((n) => {
let r = { ...n };
if (r.async = this.defaults.async || r.async || false, n.extensions && (n.extensions.forEach((i) => {
if (!i.name)
throw new Error("extension name required");
if ("renderer" in i) {
let s = t2.renderers[i.name];
s ? t2.renderers[i.name] = function(...a) {
let o = i.renderer.apply(this, a);
return o === false && (o = s.apply(this, a)), o;
} : t2.renderers[i.name] = i.renderer;
}
if ("tokenizer" in i) {
if (!i.level || i.level !== "block" && i.level !== "inline")
throw new Error("extension level must be 'block' or 'inline'");
let s = t2[i.level];
s ? s.unshift(i.tokenizer) : t2[i.level] = [i.tokenizer], i.start && (i.level === "block" ? t2.startBlock ? t2.startBlock.push(i.start) : t2.startBlock = [i.start] : i.level === "inline" && (t2.startInline ? t2.startInline.push(i.start) : t2.startInline = [i.start]));
}
"childTokens" in i && i.childTokens && (t2.childTokens[i.name] = i.childTokens);
}), r.extensions = t2), n.renderer) {
let i = this.defaults.renderer || new P(this.defaults);
for (let s in n.renderer) {
if (!(s in i))
throw new Error(`renderer '${s}' does not exist`);
if (["options", "parser"].includes(s))
continue;
let a = s, o = n.renderer[a], l = i[a];
i[a] = (...p) => {
let c = o.apply(i, p);
return c === false && (c = l.apply(i, p)), c || "";
};
}
r.renderer = i;
}
if (n.tokenizer) {
let i = this.defaults.tokenizer || new y(this.defaults);
for (let s in n.tokenizer) {
if (!(s in i))
throw new Error(`tokenizer '${s}' does not exist`);
if (["options", "rules", "lexer"].includes(s))
continue;
let a = s, o = n.tokenizer[a], l = i[a];
i[a] = (...p) => {
let c = o.apply(i, p);
return c === false && (c = l.apply(i, p)), c;
};
}
r.tokenizer = i;
}
if (n.hooks) {
let i = this.defaults.hooks || new S;
for (let s in n.hooks) {
if (!(s in i))
throw new Error(`hook '${s}' does not exist`);
if (["options", "block"].includes(s))
continue;
let a = s, o = n.hooks[a], l = i[a];
S.passThroughHooks.has(s) ? i[a] = (p) => {
if (this.defaults.async && S.passThroughHooksRespectAsync.has(s))
return (async () => {
let g = await o.call(i, p);
return l.call(i, g);
})();
let c = o.call(i, p);
return l.call(i, c);
} : i[a] = (...p) => {
if (this.defaults.async)
return (async () => {
let g = await o.apply(i, p);
return g === false && (g = await l.apply(i, p)), g;
})();
let c = o.apply(i, p);
return c === false && (c = l.apply(i, p)), c;
};
}
r.hooks = i;
}
if (n.walkTokens) {
let i = this.defaults.walkTokens, s = n.walkTokens;
r.walkTokens = function(a) {
let o = [];
return o.push(s.call(this, a)), i && (o = o.concat(i.call(this, a))), o;
};
}
this.defaults = { ...this.defaults, ...r };
}), this;
}
setOptions(e) {
return this.defaults = { ...this.defaults, ...e }, this;
}
lexer(e, t2) {
return x.lex(e, t2 ?? this.defaults);
}
parser(e, t2) {
return b.parse(e, t2 ?? this.defaults);
}
parseMarkdown(e) {
return (n, r) => {
let i = { ...r }, s = { ...this.defaults, ...i }, a = this.onError(!!s.silent, !!s.async);
if (this.defaults.async === true && i.async === false)
return a(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));
if (typeof n > "u" || n === null)
return a(new Error("marked(): input parameter is undefined or null"));
if (typeof n != "string")
return a(new Error("marked(): input parameter is of type " + Object.prototype.toString.call(n) + ", string expected"));
if (s.hooks && (s.hooks.options = s, s.hooks.block = e), s.async)
return (async () => {
let o = s.hooks ? await s.hooks.preprocess(n) : n, p = await (s.hooks ? await s.hooks.provideLexer() : e ? x.lex : x.lexInline)(o, s), c = s.hooks ? await s.hooks.processAllTokens(p) : p;
s.walkTokens && await Promise.all(this.walkTokens(c, s.walkTokens));
let h2 = await (s.hooks ? await s.hooks.provideParser() : e ? b.parse : b.parseInline)(c, s);
return s.hooks ? await s.hooks.postprocess(h2) : h2;
})().catch(a);
try {
s.hooks && (n = s.hooks.preprocess(n));
let l = (s.hooks ? s.hooks.provideLexer() : e ? x.lex : x.lexInline)(n, s);
s.hooks && (l = s.hooks.processAllTokens(l)), s.walkTokens && this.walkTokens(l, s.walkTokens);
let c = (s.hooks ? s.hooks.provideParser() : e ? b.parse : b.parseInline)(l, s);
return s.hooks && (c = s.hooks.postprocess(c)), c;
} catch (o) {
return a(o);
}
};
}
onError(e, t2) {
return (n) => {
if (n.message += `
Please report this to https://github.com/markedjs/marked.`, e) {
let r = "<p>An error occurred:</p><pre>" + w(n.message + "", true) + "</pre>";
return t2 ? Promise.resolve(r) : r;
}
if (t2)
return Promise.reject(n);
throw n;
};
}
};
var _ = new B;
function d(u3, e) {
return _.parse(u3, e);
}
d.options = d.setOptions = function(u3) {
return _.setOptions(u3), d.defaults = _.defaults, Z(d.defaults), d;
};
d.getDefaults = L;
d.defaults = T;
d.use = function(...u3) {
return _.use(...u3), d.defaults = _.defaults, Z(d.defaults), d;
};
d.walkTokens = function(u3, e) {
return _.walkTokens(u3, e);
};
d.parseInline = _.parseInline;
d.Parser = b;
d.parser = b.parse;
d.Renderer = P;
d.TextRenderer = $;
d.Lexer = x;
d.lexer = x.lex;
d.Tokenizer = y;
d.Hooks = S;
d.parse = d;
var Dt = d.options;
var Ht = d.setOptions;
var Zt = d.use;
var Gt = d.walkTokens;
var Nt = d.parseInline;
var Ft = b.parse;
var jt = x.lex;
// src/renderables/text-table-width.ts
function comparePriority(leftGrowth, leftCapacity, rightGrowth, rightCapacity) {
const left = leftGrowth * leftGrowth * rightCapacity;
const right = rightGrowth * rightGrowth * leftCapacity;
if (Number.isSafeInteger(left) && Number.isSafeInteger(right)) {
return left < right ? -1 : left > right ? 1 : 0;
}
const exactLeft = BigInt(leftGrowth) * BigInt(leftGrowth) * BigInt(rightCapacity);
const exactRight = BigInt(rightGrowth) * BigInt(rightGrowth) * BigInt(leftCapacity);
return exactLeft < exactRight ? -1 : exactLeft > exactRight ? 1 : 0;
}
function allocateProportionalColumnWidths(widths, targetWidth, minWidth) {
const baseWidths = widths.map((width) => Math.max(minWidth, Math.floor(width)));
const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0);
const capacity = baseWidths.map((width) => width - minWidth);
const growth = new Array(baseWidths.length).fill(0);
const available = Math.min(Math.max(0, targetWidth - minWidth * baseWidths.length), totalBaseWidth - minWidth * baseWidths.length);
if (available === 0)
return growth.map(() => minWidth);
if (available === capacity.reduce((sum, width) => sum + width, 0))
return baseWidths;
const weights = capacity.map(Math.sqrt);
const active = capacity.map((width, idx) => ({ idx, width, weight: weights[idx] })).filter((column) => column.width > 0).sort((a, b2) => a.weight - b2.weight);
if (active.length === capacity.length && capacity.every((width) => width === capacity[0])) {
const sharedGrowth = Math.floor(available / capacity.length);
const remainder = available % capacity.length;
return growth.map((_2, idx) => minWidth + sharedGrowth + (idx < remainder ? 1 : 0));
}
let remaining = available;
let totalWeight = active.reduce((sum, column) => sum + column.weight, 0);
for (const column of active) {
if (remaining / totalWeight <= column.weight)
break;
growth[column.idx] = column.width;
remaining -= column.width;
totalWeight -= column.weight;
}
const level = remaining / totalWeight;
for (const column of active) {
if (growth[column.idx] === column.width)
continue;
growth[column.idx] = Math.min(column.width, Math.floor(level * column.weight));
}
let allocatedGrowth = growth.reduce((sum, width) => sum + width, 0);
while (allocatedGrowth > available) {
let worstIdx = -1;
for (let idx = 0;idx < baseWidths.length; idx++) {
if (growth[idx] === 0)
continue;
const comparison = worstIdx === -1 ? 1 : comparePriority(growth[idx], capacity[idx], growth[worstIdx], capacity[worstIdx]);
if (comparison > 0 || comparison === 0 && idx > worstIdx) {
worstIdx = idx;
}
}
if (worstIdx === -1)
break;
growth[worstIdx] -= 1;
allocatedGrowth -= 1;
}
while (allocatedGrowth < available) {
let bestIdx = -1;
for (let idx = 0;idx < baseWidths.length; idx++) {
if (growth[idx] >= capacity[idx])
continue;
const comparison = bestIdx === -1 ? -1 : comparePriority(growth[idx] + 1, capacity[idx], growth[bestIdx] + 1, capacity[bestIdx]);
if (comparison < 0) {
bestIdx = idx;
}
}
if (bestIdx === -1)
break;
growth[bestIdx] += 1;
allocatedGrowth += 1;
}
return growth.map((width) => width + minWidth);
}
// src/renderables/TextTable.ts
var MEASURE_HEIGHT = 1e4;
class TextTableRenderable extends Renderable {
_content;
_wrapMode;
_columnWidthMode;
_columnFitter;
_cellPaddingX;
_cellPaddingY;
_columnGap;
_showBorders;
_border;
_outerBorder;
_hasExplicitOuterBorder;
_borderStyle;
_borderColor;
_borderBackgroundColor;
_backgroundColor;
_defaultFg;
_defaultBg;
_defaultAttributes;
_selectionBg;
_selectionFg;
_lastLocalSelection = null;
_lastSelectionMode = null;
_cells = [];
_prevCellContent = [];
_rowCount = 0;
_columnCount = 0;
_layout = this.createEmptyLayout();
_layoutDirty = true;
_rasterDirty = true;
_cachedMeasureLayout = null;
_cachedMeasureWidth = undefined;
_defaultOptions = {
content: [],
wrapMode: "word",
columnWidthMode: "full",
columnFitter: "proportional",
cellPadding: 0,
cellPaddingX: undefined,
cellPaddingY: undefined,
columnGap: 0,
showBorders: true,
border: true,
outerBorder: true,
selectable: true,
selectionBg: undefined,
selectionFg: undefined,
borderStyle: "single",
borderColor: "#FFFFFF",
borderBackgroundColor: "transparent",
backgroundColor: "transparent",
fg: "#FFFFFF",
bg: "transparent",
attributes: 0
};
constructor(ctx, options = {}) {
super(ctx, { ...options, flexShrink: options.flexShrink ?? 0, buffered: true });
this._content = options.content ?? this._defaultOptions.content;
this._wrapMode = options.wrapMode ?? this._defaultOptions.wrapMode;
this._columnWidthMode = options.columnWidthMode ?? this._defaultOptions.columnWidthMode;
this._columnFitter = this.resolveColumnFitter(options.columnFitter);
this._cellPaddingX = this.resolveCellPadding(options.cellPaddingX ?? options.cellPadding);
this._cellPaddingY = this.resolveCellPadding(options.cellPaddingY ?? options.cellPadding);
this._columnGap = this.resolveColumnGap(options.columnGap);
this._showBorders = options.showBorders ?? this._defaultOptions.showBorders;
this._border = options.border ?? this._defaultOptions.border;
this._hasExplicitOuterBorder = options.outerBorder !== undefined;
this._outerBorder = options.outerBorder ?? this._border;
this.selectable = options.selectable ?? this._defaultOptions.selectable;
this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined;
this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined;
this._borderStyle = parseBorderStyle(options.borderStyle, this._defaultOptions.borderStyle);
this._borderColor = parseColor(options.borderColor ?? this._defaultOptions.borderColor);
this._borderBackgroundColor = parseColor(options.borderBackgroundColor ?? this._defaultOptions.borderBackgroundColor);
this._backgroundColor = parseColor(options.backgroundColor ?? this._defaultOptions.backgroundColor);
this._defaultFg = parseColor(options.fg ?? this._defaultOptions.fg);
this._defaultBg = parseColor(options.bg ?? this._defaultOptions.bg);
this._defaultAttributes = options.attributes ?? this._defaultOptions.attributes;
this.setupMeasureFunc();
this.rebuildCells();
}
get content() {
return this._content;
}
set content(value) {
this._content = value ?? [];
this.rebuildCells();
}
get wrapMode() {
return this._wrapMode;
}
set wrapMode(value) {
if (this._wrapMode === value)
return;
this._wrapMode = value;
for (const row of this._cells) {
for (const cell of row) {
cell.textBufferView.setWrapMode(value);
}
}
this.invalidateLayoutAndRaster();
}
get columnWidthMode() {
return this._columnWidthMode;
}
set columnWidthMode(value) {
if (this._columnWidthMode === value)
return;
this._columnWidthMode = value;
this.invalidateLayoutAndRaster();
}
get columnFitter() {
return this._columnFitter;
}
set columnFitter(value) {
const next = this.resolveColumnFitter(value);
if (this._columnFitter === next)
return;
this._columnFitter = next;
this.invalidateLayoutAndRaster();
}
get cellPadding() {
return this._cellPaddingX === this._cellPaddingY ? this._cellPaddingX : 0;
}
set cellPadding(value) {
const next = this.resolveCellPadding(value);
if (this._cellPaddingX === next && this._cellPaddingY === next)
return;
this._cellPaddingX = next;
this._cellPaddingY = next;
this.invalidateLayoutAndRaster();
}
get cellPaddingX() {
return this._cellPaddingX;
}
set cellPaddingX(value) {
const next = this.resolveCellPadding(value);
if (this._cellPaddingX === next)
return;
this._cellPaddingX = next;
this.invalidateLayoutAndRaster();
}
get cellPaddingY() {
return this._cellPaddingY;
}
set cellPaddingY(value) {
const next = this.resolveCellPadding(value);
if (this._cellPaddingY === next)
return;
this._cellPaddingY = next;
this.invalidateLayoutAndRaster();
}
get columnGap() {
return this._columnGap;
}
set columnGap(value) {
const next = this.resolveColumnGap(value);
if (this._columnGap === next)
return;
this._columnGap = next;
this.invalidateLayoutAndRaster();
}
get showBorders() {
return this._showBorders;
}
set showBorders(value) {
if (this._showBorders === value)
return;
this._showBorders = value;
this.invalidateRasterOnly();
}
get outerBorder() {
return this._outerBorder;
}
set outerBorder(value) {
if (this._outerBorder === value)
return;
this._hasExplicitOuterBorder = true;
this._outerBorder = value;
this.invalidateLayoutAndRaster();
}
get border() {
return this._border;
}
set border(value) {
if (this._border === value)
return;
this._border = value;
if (!this._hasExplicitOuterBorder) {
this._outerBorder = value;
}
this.invalidateLayoutAndRaster();
}
get borderStyle() {
return this._borderStyle;
}
set borderStyle(value) {
const next = parseBorderStyle(value, this._defaultOptions.borderStyle);
if (this._borderStyle === next)
return;
this._borderStyle = next;
this.invalidateRasterOnly();
}
get borderColor() {
return this._borderColor;
}
set borderColor(value) {
const next = parseColor(value);
if (this._borderColor === next)
return;
this._borderColor = next;
this.invalidateRasterOnly();
}
shouldStartSelection(x2, y2) {
if (!this.selectable)
return false;
this.ensureLayoutReady();
const localX = x2 - this.x;
const localY = y2 - this.y;
return this.getCellAtLocalPosition(localX, localY) !== null;
}
onSelectionChanged(selection) {
this.ensureLayoutReady();
const previousLocalSelection = this._lastLocalSelection;
const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y);
this._lastLocalSelection = localSelection;
const dirtyRows = this.getDirtySelectionRowRange(previousLocalSelection, localSelection);
if (!localSelection?.isActive) {
this.resetCellSelections();
this._lastSelectionMode = null;
} else {
this.applySelectionToCells(localSelection, selection?.isStart ?? false);
}
if (dirtyRows !== null) {
this.redrawSelectionRows(dirtyRows.firstRow, dirtyRows.lastRow);
}
return this.hasSelection();
}
hasSelection() {
for (const row of this._cells) {
for (const cell of row) {
if (cell.textBufferView.hasSelection()) {
return true;
}
}
}
return false;
}
getSelection() {
for (const row of this._cells) {
for (const cell of row) {
const selection = cell.textBufferView.getSelection();
if (selection) {
return selection;
}
}
}
return null;
}
getSelectedText() {
const selectedRows = [];
for (let rowIdx = 0;rowIdx < this._rowCount; rowIdx++) {
const rowSelections = [];
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const cell = this._cells[rowIdx]?.[colIdx];
if (!cell || !cell.textBufferView.hasSelection())
continue;
const selectedText = cell.textBufferView.getSelectedText();
if (selectedText.length > 0) {
rowSelections.push(selectedText);
}
}
if (rowSelections.length > 0) {
selectedRows.push(rowSelections.join("\t"));
}
}
return selectedRows.join(`
`);
}
onResize(width, height) {
this.invalidateLayoutAndRaster(false);
super.onResize(width, height);
}
renderSelf(buffer) {
if (!this.visible || this.isDestroyed)
return;
if (this._layoutDirty) {
this.rebuildLayoutForCurrentWidth();
}
if (!this._rasterDirty)
return;
buffer.clear(this._backgroundColor);
if (this._rowCount === 0 || this._columnCount === 0) {
this._rasterDirty = false;
return;
}
this.drawBorders(buffer);
this.drawCells(buffer);
this._rasterDirty = false;
}
destroySelf() {
this.destroyCells();
super.destroySelf();
}
setupMeasureFunc() {
const measureFunc = (width, widthMode, _height, _heightMode) => {
const hasWidthConstraint = widthMode !== 0 /* Undefined */ && Number.isFinite(width);
const rawWidthConstraint = hasWidthConstraint ? Math.max(1, Math.floor(width)) : undefined;
const widthConstraint = this.resolveLayoutWidthConstraint(rawWidthConstraint);
const measuredLayout = this.computeLayout(widthConstraint);
this._cachedMeasureLayout = measuredLayout;
this._cachedMeasureWidth = widthConstraint;
let measuredWidth = measuredLayout.tableWidth > 0 ? measuredLayout.tableWidth : 1;
let measuredHeight = measuredLayout.tableHeight > 0 ? measuredLayout.tableHeight : 1;
if (widthMode === 2 /* AtMost */ && rawWidthConstraint !== undefined && this._positionType !== "absolute") {
measuredWidth = Math.min(rawWidthConstraint, measuredWidth);
}
return {
width: measuredWidth,
height: measuredHeight
};
};
this.yogaNode.setMeasureFunc(measureFunc);
}
rebuildCells() {
const newRowCount = this._content.length;
const newColumnCount = this._content.reduce((max, row) => Math.max(max, row.length), 0);
if (this._cells.length === 0) {
this._rowCount = newRowCount;
this._columnCount = newColumnCount;
this._cells = [];
this._prevCellContent = [];
for (let rowIdx = 0;rowIdx < newRowCount; rowIdx++) {
const row = this._content[rowIdx] ?? [];
const rowCells = [];
const rowRefs = [];
for (let colIdx = 0;colIdx < newColumnCount; colIdx++) {
const cellContent = row[colIdx];
rowCells.push(this.createCell(cellContent));
rowRefs.push(cellContent);
}
this._cells.push(rowCells);
this._prevCellContent.push(rowRefs);
}
this.invalidateLayoutAndRaster();
return;
}
this.updateCellsDiff(newRowCount, newColumnCount);
this.invalidateLayoutAndRaster();
}
updateCellsDiff(newRowCount, newColumnCount) {
const oldRowCount = this._rowCount;
const oldColumnCount = this._columnCount;
const keepRows = Math.min(oldRowCount, newRowCount);
const keepCols = Math.min(oldColumnCount, newColumnCount);
for (let rowIdx = 0;rowIdx < keepRows; rowIdx++) {
const newRow = this._content[rowIdx] ?? [];
const cellRow = this._cells[rowIdx];
const refRow = this._prevCellContent[rowIdx];
for (let colIdx = 0;colIdx < keepCols; colIdx++) {
const cellContent = newRow[colIdx];
if (cellContent === refRow[colIdx])
continue;
const oldCell = cellRow[colIdx];
oldCell.textBufferView.destroy();
oldCell.textBuffer.destroy();
oldCell.syntaxStyle.destroy();
cellRow[colIdx] = this.createCell(cellContent);
refRow[colIdx] = cellContent;
}
if (newColumnCount > oldColumnCount) {
for (let colIdx = oldColumnCount;colIdx < newColumnCount; colIdx++) {
const cellContent = newRow[colIdx];
cellRow.push(this.createCell(cellContent));
refRow.push(cellContent);
}
} else if (newColumnCount < oldColumnCount) {
for (let colIdx = newColumnCount;colIdx < oldColumnCount; colIdx++) {
const cell = cellRow[colIdx];
cell.textBufferView.destroy();
cell.textBuffer.destroy();
cell.syntaxStyle.destroy();
}
cellRow.length = newColumnCount;
refRow.length = newColumnCount;
}
}
if (newRowCount > oldRowCount) {
for (let rowIdx = oldRowCount;rowIdx < newRowCount; rowIdx++) {
const newRow = this._content[rowIdx] ?? [];
const rowCells = [];
const rowRefs = [];
for (let colIdx = 0;colIdx < newColumnCount; colIdx++) {
const cellContent = newRow[colIdx];
rowCells.push(this.createCell(cellContent));
rowRefs.push(cellContent);
}
this._cells.push(rowCells);
this._prevCellContent.push(rowRefs);
}
} else if (newRowCount < oldRowCount) {
for (let rowIdx = newRowCount;rowIdx < oldRowCount; rowIdx++) {
const row = this._cells[rowIdx];
for (const cell of row) {
cell.textBufferView.destroy();
cell.textBuffer.destroy();
cell.syntaxStyle.destroy();
}
}
this._cells.length = newRowCount;
this._prevCellContent.length = newRowCount;
}
this._rowCount = newRowCount;
this._columnCount = newColumnCount;
}
createCell(content) {
const styledText = this.toStyledText(content);
const textBuffer = TextBuffer.create(this._ctx.widthMethod);
const syntaxStyle = SyntaxStyle.create();
textBuffer.setDefaultFg(this._defaultFg);
textBuffer.setDefaultBg(this._defaultBg);
textBuffer.setDefaultAttributes(this._defaultAttributes);
textBuffer.setSyntaxStyle(syntaxStyle);
textBuffer.setStyledText(styledText);
const textBufferView = TextBufferView.create(textBuffer);
textBufferView.setWrapMode(this._wrapMode);
return { textBuffer, textBufferView, syntaxStyle };
}
toStyledText(content) {
if (Array.isArray(content)) {
return new StyledText(content);
}
if (content === null || content === undefined) {
return stringToStyledText("");
}
return stringToStyledText(String(content));
}
destroyCells() {
for (const row of this._cells) {
for (const cell of row) {
cell.textBufferView.destroy();
cell.textBuffer.destroy();
cell.syntaxStyle.destroy();
}
}
this._cells = [];
this._prevCellContent = [];
this._rowCount = 0;
this._columnCount = 0;
this._layout = this.createEmptyLayout();
}
rebuildLayoutForCurrentWidth() {
const maxTableWidth = this.resolveLayoutWidthConstraint(this.width);
let layout;
if (this._cachedMeasureLayout !== null && this._cachedMeasureWidth === maxTableWidth) {
layout = this._cachedMeasureLayout;
} else {
layout = this.computeLayout(maxTableWidth);
}
this._cachedMeasureLayout = null;
this._cachedMeasureWidth = undefined;
this._layout = layout;
this.applyLayoutToViews(layout);
this._layoutDirty = false;
if (this._lastLocalSelection?.isActive) {
this.applySelectionToCells(this._lastLocalSelection, true);
}
}
computeLayout(maxTableWidth) {
if (this._rowCount === 0 || this._columnCount === 0) {
return this.createEmptyLayout();
}
const borderLayout = this.resolveBorderLayout();
const columnWidths = this.computeColumnWidths(maxTableWidth, borderLayout);
const rowHeights = this.computeRowHeights(columnWidths);
const columnOffsets = this.computeOffsets(columnWidths, borderLayout.left, borderLayout.right, borderLayout.innerVertical, this.getInterColumnGap(borderLayout));
const rowOffsets = this.computeOffsets(rowHeights, borderLayout.top, borderLayout.bottom, borderLayout.innerHorizontal);
return {
columnWidths,
rowHeights,
columnOffsets,
rowOffsets,
columnOffsetsI32: new Int32Array(columnOffsets),
rowOffsetsI32: new Int32Array(rowOffsets),
tableWidth: (columnOffsets[columnOffsets.length - 1] ?? 0) + 1,
tableHeight: (rowOffsets[rowOffsets.length - 1] ?? 0) + 1
};
}
isFullWidthMode() {
return this._columnWidthMode === "full";
}
computeColumnWidths(maxTableWidth, borderLayout) {
const horizontalPadding = this.getHorizontalCellPadding();
const intrinsicWidths = new Array(this._columnCount).fill(1 + horizontalPadding);
for (let rowIdx = 0;rowIdx < this._rowCount; rowIdx++) {
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const cell = this._cells[rowIdx]?.[colIdx];
if (!cell)
continue;
const measure = cell.textBufferView.measureForDimensions(0, MEASURE_HEIGHT);
const measuredWidth = Math.max(1, measure?.widthColsMax ?? 0) + horizontalPadding;
intrinsicWidths[colIdx] = Math.max(intrinsicWidths[colIdx], measuredWidth);
}
}
if (maxTableWidth === undefined || !Number.isFinite(maxTableWidth) || maxTableWidth <= 0) {
return intrinsicWidths;
}
const maxContentWidth = Math.max(1, Math.floor(maxTableWidth) - this.getVerticalBorderCount(borderLayout) - this.getTotalInterColumnGap(borderLayout));
const currentWidth = intrinsicWidths.reduce((sum, width) => sum + width, 0);
if (currentWidth === maxContentWidth) {
return intrinsicWidths;
}
if (currentWidth < maxContentWidth) {
if (this.isFullWidthMode()) {
return this.expandColumnWidths(intrinsicWidths, maxContentWidth);
}
return intrinsicWidths;
}
if (this._wrapMode === "none") {
return intrinsicWidths;
}
return this.fitColumnWidths(intrinsicWidths, maxContentWidth);
}
expandColumnWidths(widths, targetContentWidth) {
const baseWidths = widths.map((width) => Math.max(1, Math.floor(width)));
const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0);
if (totalBaseWidth >= targetContentWidth) {
return baseWidths;
}
const expanded = [...baseWidths];
const columns = expanded.length;
const extraWidth = targetContentWidth - totalBaseWidth;
const sharedWidth = Math.floor(extraWidth / columns);
const remainder = extraWidth % columns;
for (let idx = 0;idx < columns; idx++) {
expanded[idx] += sharedWidth;
if (idx < remainder) {
expanded[idx] += 1;
}
}
return expanded;
}
fitColumnWidths(widths, targetContentWidth) {
if (this._columnFitter === "balanced") {
return this.fitColumnWidthsBalanced(widths, targetContentWidth);
}
return this.fitColumnWidthsProportional(widths, targetContentWidth);
}
fitColumnWidthsProportional(widths, targetContentWidth) {
const minWidth = 1 + this.getHorizontalCellPadding();
return allocateProportionalColumnWidths(widths, targetContentWidth, minWidth);
}
fitColumnWidthsBalanced(widths, targetContentWidth) {
const minWidth = 1 + this.getHorizontalCellPadding();
const hardMinWidths = new Array(widths.length).fill(minWidth);
const baseWidths = widths.map((width) => Math.max(1, Math.floor(width)));
const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0);
const columns = baseWidths.length;
if (columns === 0 || totalBaseWidth <= targetContentWidth) {
return baseWidths;
}
const evenShare = Math.max(minWidth, Math.floor(targetContentWidth / columns));
const preferredMinWidths = baseWidths.map((width) => Math.min(width, evenShare));
const preferredMinTotal = preferredMinWidths.reduce((sum, width) => sum + width, 0);
const floorWidths = preferredMinTotal <= targetContentWidth ? preferredMinWidths : hardMinWidths;
const floorTotal = floorWidths.reduce((sum, width) => sum + width, 0);
const clampedTarget = Math.max(floorTotal, targetContentWidth);
if (totalBaseWidth <= clampedTarget) {
return baseWidths;
}
const shrinkable = baseWidths.map((width, idx) => width - floorWidths[idx]);
const totalShrinkable = shrinkable.reduce((sum, value) => sum + value, 0);
if (totalShrinkable <= 0) {
return [...floorWidths];
}
const targetShrink = totalBaseWidth - clampedTarget;
const shrink = this.allocateShrinkByWeight(shrinkable, targetShrink, "sqrt");
return baseWidths.map((width, idx) => Math.max(floorWidths[idx], width - shrink[idx]));
}
allocateShrinkByWeight(shrinkable, targetShrink, mode) {
const shrink = new Array(shrinkable.length).fill(0);
if (targetShrink <= 0) {
return shrink;
}
const weights = shrinkable.map((value) => {
if (value <= 0) {
return 0;
}
return mode === "sqrt" ? Math.sqrt(value) : value;
});
const totalWeight = weights.reduce((sum, value) => sum + value, 0);
if (totalWeight <= 0) {
return shrink;
}
const fractions = new Array(shrinkable.length).fill(0);
let usedShrink = 0;
for (let idx = 0;idx < shrinkable.length; idx++) {
if (shrinkable[idx] <= 0 || weights[idx] <= 0)
continue;
const exact = weights[idx] / totalWeight * targetShrink;
const whole = Math.min(shrinkable[idx], Math.floor(exact));
shrink[idx] = whole;
fractions[idx] = exact - whole;
usedShrink += whole;
}
let remainingShrink = targetShrink - usedShrink;
while (remainingShrink > 0) {
let bestIdx = -1;
let bestFraction = -1;
for (let idx = 0;idx < shrinkable.length; idx++) {
if (shrinkable[idx] - shrink[idx] <= 0)
continue;
if (bestIdx === -1 || fractions[idx] > bestFraction || fractions[idx] === bestFraction && shrinkable[idx] > shrinkable[bestIdx]) {
bestIdx = idx;
bestFraction = fractions[idx];
}
}
if (bestIdx === -1) {
break;
}
shrink[bestIdx] += 1;
fractions[bestIdx] = 0;
remainingShrink -= 1;
}
return shrink;
}
computeRowHeights(columnWidths) {
const horizontalPadding = this.getHorizontalCellPadding();
const verticalPadding = this.getVerticalCellPadding();
const rowHeights = new Array(this._rowCount).fill(1 + verticalPadding);
for (let rowIdx = 0;rowIdx < this._rowCount; rowIdx++) {
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const cell = this._cells[rowIdx]?.[colIdx];
if (!cell)
continue;
const width = Math.max(1, (columnWidths[colIdx] ?? 1) - horizontalPadding);
const measure = cell.textBufferView.measureForDimensions(width, MEASURE_HEIGHT);
const lineCount = Math.max(1, measure?.lineCount ?? 1);
rowHeights[rowIdx] = Math.max(rowHeights[rowIdx], lineCount + verticalPadding);
}
}
return rowHeights;
}
computeOffsets(parts, startBoundary, endBoundary, includeInnerBoundaries, innerGap = 0) {
const offsets = [startBoundary ? 0 : -1];
let cursor = offsets[0] ?? 0;
for (let idx = 0;idx < parts.length; idx++) {
const size = parts[idx] ?? 1;
const separatorAfter = idx < parts.length - 1 ? includeInnerBoundaries ? 1 : innerGap : endBoundary ? 1 : 0;
cursor += size + separatorAfter;
offsets.push(cursor);
}
return offsets;
}
getInterColumnGap(borderLayout) {
if (borderLayout.innerVertical) {
return 0;
}
return this._columnGap;
}
getTotalInterColumnGap(borderLayout) {
return Math.max(0, this._columnCount - 1) * this.getInterColumnGap(borderLayout);
}
applyLayoutToViews(layout) {
const horizontalPadding = this.getHorizontalCellPadding();
const verticalPadding = this.getVerticalCellPadding();
for (let rowIdx = 0;rowIdx < this._rowCount; rowIdx++) {
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const cell = this._cells[rowIdx]?.[colIdx];
if (!cell)
continue;
const colWidth = layout.columnWidths[colIdx] ?? 1;
const rowHeight = layout.rowHeights[rowIdx] ?? 1;
const contentWidth = Math.max(1, colWidth - horizontalPadding);
const contentHeight = Math.max(1, rowHeight - verticalPadding);
if (this._wrapMode === "none") {
cell.textBufferView.setWrapWidth(null);
} else {
cell.textBufferView.setWrapWidth(contentWidth);
}
cell.textBufferView.setViewport(0, 0, contentWidth, contentHeight);
}
}
}
resolveBorderLayout() {
return {
left: this._outerBorder,
right: this._outerBorder,
top: this._outerBorder,
bottom: this._outerBorder,
innerVertical: this._border && this._columnCount > 1,
innerHorizontal: this._border && this._rowCount > 1
};
}
getVerticalBorderCount(borderLayout) {
return (borderLayout.left ? 1 : 0) + (borderLayout.right ? 1 : 0) + (borderLayout.innerVertical ? Math.max(0, this._columnCount - 1) : 0);
}
getHorizontalBorderCount(borderLayout) {
return (borderLayout.top ? 1 : 0) + (borderLayout.bottom ? 1 : 0) + (borderLayout.innerHorizontal ? Math.max(0, this._rowCount - 1) : 0);
}
drawBorders(buffer) {
if (!this._showBorders) {
return;
}
const borderLayout = this.resolveBorderLayout();
if (this.getVerticalBorderCount(borderLayout) === 0 && this.getHorizontalBorderCount(borderLayout) === 0) {
return;
}
buffer.drawGrid({
borderChars: BorderCharArrays[this._borderStyle],
borderFg: this._borderColor,
borderBg: this._borderBackgroundColor,
columnOffsets: this._layout.columnOffsetsI32,
rowOffsets: this._layout.rowOffsetsI32,
drawInner: this._border,
drawOuter: this._outerBorder
});
}
drawCells(buffer) {
this.drawCellRange(buffer, 0, this._rowCount - 1);
}
drawCellRange(buffer, firstRow, lastRow) {
const colOffsets = this._layout.columnOffsets;
const rowOffsets = this._layout.rowOffsets;
const cellPaddingX = this._cellPaddingX;
const cellPaddingY = this._cellPaddingY;
for (let rowIdx = firstRow;rowIdx <= lastRow; rowIdx++) {
const cellY = (rowOffsets[rowIdx] ?? 0) + 1 + cellPaddingY;
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const cell = this._cells[rowIdx]?.[colIdx];
if (!cell)
continue;
buffer.drawTextBuffer(cell.textBufferView, (colOffsets[colIdx] ?? 0) + 1 + cellPaddingX, cellY);
}
}
}
redrawSelectionRows(firstRow, lastRow) {
if (firstRow > lastRow)
return;
if (this._backgroundColor.a < 1) {
this.invalidateRasterOnly();
return;
}
const buffer = this.frameBuffer;
if (!buffer)
return;
this.clearCellRange(buffer, firstRow, lastRow);
this.drawCellRange(buffer, firstRow, lastRow);
this.requestRender();
}
clearCellRange(buffer, firstRow, lastRow) {
const colWidths = this._layout.columnWidths;
const rowHeights = this._layout.rowHeights;
const colOffsets = this._layout.columnOffsets;
const rowOffsets = this._layout.rowOffsets;
for (let rowIdx = firstRow;rowIdx <= lastRow; rowIdx++) {
const cellY = (rowOffsets[rowIdx] ?? 0) + 1;
const rowHeight = rowHeights[rowIdx] ?? 1;
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const cellX = (colOffsets[colIdx] ?? 0) + 1;
const colWidth = colWidths[colIdx] ?? 1;
if (this._backgroundColor.a < 1) {
for (let y2 = cellY;y2 < cellY + rowHeight; y2++) {
for (let x2 = cellX;x2 < cellX + colWidth; x2++) {
buffer.setCell(x2, y2, " ", this._defaultFg, this._backgroundColor, this._defaultAttributes);
}
}
} else {
buffer.fillRect(cellX, cellY, colWidth, rowHeight, this._backgroundColor);
}
}
}
}
ensureLayoutReady() {
if (!this._layoutDirty)
return;
this.rebuildLayoutForCurrentWidth();
}
getCellAtLocalPosition(localX, localY) {
if (this._rowCount === 0 || this._columnCount === 0)
return null;
if (localX < 0 || localY < 0 || localX >= this._layout.tableWidth || localY >= this._layout.tableHeight) {
return null;
}
let rowIdx = -1;
for (let idx = 0;idx < this._rowCount; idx++) {
const top = (this._layout.rowOffsets[idx] ?? 0) + 1;
const bottom = top + (this._layout.rowHeights[idx] ?? 1) - 1;
if (localY >= top && localY <= bottom) {
rowIdx = idx;
break;
}
}
if (rowIdx < 0)
return null;
let colIdx = -1;
for (let idx = 0;idx < this._columnCount; idx++) {
const left = (this._layout.columnOffsets[idx] ?? 0) + 1;
const right = left + (this._layout.columnWidths[idx] ?? 1) - 1;
if (localX >= left && localX <= right) {
colIdx = idx;
break;
}
}
if (colIdx < 0)
return null;
return { rowIdx, colIdx };
}
applySelectionToCells(localSelection, isStart) {
if (localSelection.anchorX === localSelection.focusX && localSelection.anchorY === localSelection.focusY) {
this.resetCellSelections();
this._lastSelectionMode = null;
return;
}
const minSelY = Math.min(localSelection.anchorY, localSelection.focusY);
const maxSelY = Math.max(localSelection.anchorY, localSelection.focusY);
const firstRow = this.findRowForLocalY(minSelY);
const lastRow = this.findRowForLocalY(maxSelY);
const selection = this.resolveSelectionResolution(localSelection);
const modeChanged = this._lastSelectionMode !== selection.mode;
this._lastSelectionMode = selection.mode;
const lockToAnchorColumn = selection.mode === "column-locked" && selection.anchorColumn !== null;
for (let rowIdx = 0;rowIdx < this._rowCount; rowIdx++) {
if (rowIdx < firstRow || rowIdx > lastRow) {
this.resetRowSelection(rowIdx);
continue;
}
const cellTop = (this._layout.rowOffsets[rowIdx] ?? 0) + 1 + this._cellPaddingY;
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const cell = this._cells[rowIdx]?.[colIdx];
if (!cell)
continue;
if (lockToAnchorColumn && colIdx !== selection.anchorColumn) {
cell.textBufferView.resetLocalSelection();
continue;
}
const cellLeft = (this._layout.columnOffsets[colIdx] ?? 0) + 1 + this._cellPaddingX;
let coords = {
anchorX: localSelection.anchorX - cellLeft,
anchorY: localSelection.anchorY - cellTop,
focusX: localSelection.focusX - cellLeft,
focusY: localSelection.focusY - cellTop
};
const isAnchorCell = selection.anchorCell !== null && selection.anchorCell.rowIdx === rowIdx && selection.anchorCell.colIdx === colIdx;
if (selection.mode === "single-cell" && !isAnchorCell) {
cell.textBufferView.resetLocalSelection();
continue;
}
const forceSet = isAnchorCell && selection.mode !== "single-cell";
if (forceSet) {
coords = this.getFullCellSelectionCoords(rowIdx, colIdx);
}
const shouldUseSet = isStart || modeChanged || forceSet;
if (shouldUseSet) {
cell.textBufferView.setLocalSelection(coords.anchorX, coords.anchorY, coords.focusX, coords.focusY, this._selectionBg, this._selectionFg);
} else {
cell.textBufferView.updateLocalSelection(coords.anchorX, coords.anchorY, coords.focusX, coords.focusY, this._selectionBg, this._selectionFg);
}
}
}
}
resolveSelectionResolution(localSelection) {
const anchorCell = this.getCellAtLocalPosition(localSelection.anchorX, localSelection.anchorY);
const focusCell = this.getCellAtLocalPosition(localSelection.focusX, localSelection.focusY);
const anchorColumn = anchorCell?.colIdx ?? this.getColumnAtLocalX(localSelection.anchorX);
if (anchorCell !== null && focusCell !== null && anchorCell.rowIdx === focusCell.rowIdx && anchorCell.colIdx === focusCell.colIdx) {
return {
mode: "single-cell",
anchorCell,
anchorColumn
};
}
const focusColumn = this.getColumnAtLocalX(localSelection.focusX);
if (anchorColumn !== null && focusColumn === anchorColumn) {
return {
mode: "column-locked",
anchorCell,
anchorColumn
};
}
return {
mode: "grid",
anchorCell,
anchorColumn
};
}
getColumnAtLocalX(localX) {
if (this._columnCount === 0)
return null;
if (localX < 0 || localX >= this._layout.tableWidth)
return null;
for (let colIdx = 0;colIdx < this._columnCount; colIdx++) {
const colStart = (this._layout.columnOffsets[colIdx] ?? 0) + 1;
const colEnd = colStart + (this._layout.columnWidths[colIdx] ?? 1) - 1;
if (localX >= colStart && localX <= colEnd) {
return colIdx;
}
}
return null;
}
getFullCellSelectionCoords(rowIdx, colIdx) {
const colWidth = this._layout.columnWidths[colIdx] ?? 1;
const rowHeight = this._layout.rowHeights[rowIdx] ?? 1;
const contentWidth = Math.max(1, colWidth - this.getHorizontalCellPadding());
const contentHeight = Math.max(1, rowHeight - this.getVerticalCellPadding());
return {
anchorX: -1,
anchorY: 0,
focusX: contentWidth,
focusY: contentHeight
};
}
findRowForLocalY(localY) {
if (this._rowCount === 0)
return 0;
if (localY < 0)
return 0;
for (let rowIdx = 0;rowIdx < this._rowCount; rowIdx++) {
const rowStart = (this._layout.rowOffsets[rowIdx] ?? 0) + 1;
const rowEnd = rowStart + (this._layout.rowHeights[rowIdx] ?? 1) - 1;
if (localY <= rowEnd)
return rowIdx;
}
return this._rowCount - 1;
}
getSelectionRowRange(selection) {
if (!selection?.isActive || this._rowCount === 0)
return null;
const minSelY = Math.min(selection.anchorY, selection.focusY);
const maxSelY = Math.max(selection.anchorY, selection.focusY);
return {
firstRow: this.findRowForLocalY(minSelY),
lastRow: this.findRowForLocalY(maxSelY)
};
}
getDirtySelectionRowRange(previousSelection, currentSelection) {
const previousRange = this.getSelectionRowRange(previousSelection);
const currentRange = this.getSelectionRowRange(currentSelection);
if (previousRange === null)
return currentRange;
if (currentRange === null)
return previousRange;
return {
firstRow: Math.min(previousRange.firstRow, currentRange.firstRow),
lastRow: Math.max(previousRange.lastRow, currentRange.lastRow)
};
}
resetRowSelection(rowIdx) {
const row = this._cells[rowIdx];
if (!row)
return;
for (const cell of row) {
cell.textBufferView.resetLocalSelection();
}
}
resetCellSelections() {
for (let rowIdx = 0;rowIdx < this._rowCount; rowIdx++) {
this.resetRowSelection(rowIdx);
}
}
createEmptyLayout() {
return {
columnWidths: [],
rowHeights: [],
columnOffsets: [0],
rowOffsets: [0],
columnOffsetsI32: new Int32Array([0]),
rowOffsetsI32: new Int32Array([0]),
tableWidth: 0,
tableHeight: 0
};
}
resolveLayoutWidthConstraint(width) {
if (width === undefined || !Number.isFinite(width) || width <= 0) {
return;
}
if (this._wrapMode !== "none" || this.isFullWidthMode()) {
return Math.max(1, Math.floor(width));
}
return;
}
getHorizontalCellPadding() {
return this._cellPaddingX * 2;
}
getVerticalCellPadding() {
return this._cellPaddingY * 2;
}
resolveColumnFitter(value) {
if (value === undefined) {
return this._defaultOptions.columnFitter;
}
return value === "balanced" ? "balanced" : "proportional";
}
resolveCellPadding(value) {
if (value === undefined || !Number.isFinite(value)) {
return this._defaultOptions.cellPadding;
}
return Math.max(0, Math.floor(value));
}
resolveColumnGap(value) {
if (value === undefined || !Number.isFinite(value)) {
return this._defaultOptions.columnGap;
}
return Math.max(0, Math.floor(value));
}
invalidateLayoutAndRaster(markYogaDirty = true) {
this._layoutDirty = true;
this._rasterDirty = true;
this._cachedMeasureLayout = null;
this._cachedMeasureWidth = undefined;
if (markYogaDirty) {
this.yogaNode.markDirty();
}
this.requestRender();
}
invalidateRasterOnly() {
this._rasterDirty = true;
this.requestRender();
}
}
// src/renderables/markdown-parser.ts
function parseMarkdownIncremental(newContent, prevState, trailingUnstable = 2) {
if (!prevState || prevState.tokens.length === 0) {
try {
const tokens = x.lex(newContent, { gfm: true });
return {
content: newContent,
tokens,
stableTokenCount: Math.max(0, tokens.length - trailingUnstable)
};
} catch {
return { content: newContent, tokens: [], stableTokenCount: 0 };
}
}
let offset = 0;
let reuseCount = 0;
for (const token of prevState.tokens) {
const tokenLength = token.raw.length;
if (offset + tokenLength <= newContent.length && newContent.startsWith(token.raw, offset)) {
reuseCount++;
offset += tokenLength;
} else {
break;
}
}
reuseCount = Math.max(0, reuseCount - trailingUnstable);
offset = 0;
for (let i = 0;i < reuseCount; i++) {
offset += prevState.tokens[i].raw.length;
}
const stableTokens = prevState.tokens.slice(0, reuseCount);
const remainingContent = newContent.slice(offset);
if (!remainingContent) {
return {
content: newContent,
tokens: stableTokens,
stableTokenCount: stableTokens.length
};
}
try {
const newTokens = x.lex(remainingContent, { gfm: true });
return {
content: newContent,
tokens: [...stableTokens, ...newTokens],
stableTokenCount: trailingUnstable === 0 ? stableTokens.length + newTokens.length : stableTokens.length
};
} catch {
try {
const fullTokens = x.lex(newContent, { gfm: true });
return { content: newContent, tokens: fullTokens, stableTokenCount: 0 };
} catch {
return { content: newContent, tokens: [], stableTokenCount: 0 };
}
}
}
// src/renderables/Markdown.ts
function normalizeMarkdownCodeBlockRenderers(renderers) {
const rendererMap = new Map;
const maybeMap = renderers;
if (typeof maybeMap.forEach === "function") {
maybeMap.forEach((renderer, language) => {
rendererMap.set(language, renderer);
});
return rendererMap;
}
const rendererRecord = renderers;
for (const [language, renderer] of Object.entries(rendererRecord)) {
rendererMap.set(language, renderer);
}
return rendererMap;
}
function createMarkdownCodeBlockRenderer(renderers) {
const rendererMap = normalizeMarkdownCodeBlockRenderers(renderers);
const renderNode = (token, context) => {
if (token.type !== "code") {
return;
}
const language = infoStringToFiletype(token.lang ?? "");
if (!language)
return;
return rendererMap.get(language)?.(token, context);
};
renderNode.codeBlockOnly = true;
return renderNode;
}
var TRAILING_MARKDOWN_BLOCK_BREAKS_RE = /(?:\r?\n){2,}$/;
var TRAILING_MARKDOWN_BLOCK_NEWLINES_RE = /(?:\r?\n)+$/;
function colorsEqual(left, right) {
if (!left || !right)
return left === right;
return left.equals(right);
}
class MarkdownRenderable extends Renderable {
_content = "";
_syntaxStyle;
_fg;
_bg;
_conceal;
_concealCode;
_treeSitterClient;
_tableOptions;
_renderNode;
_internalBlockMode;
_parseState = null;
_streaming = false;
_blockStates = [];
_stableBlockCount = 0;
_styleDirty = false;
_linkifyMarkdownChunks = (chunks, context) => detectLinks(chunks, {
content: context.content,
highlights: context.highlights
});
_contentDefaultOptions = {
content: "",
conceal: true,
concealCode: false,
streaming: false,
internalBlockMode: "coalesced"
};
constructor(ctx, options) {
super(ctx, {
...options,
flexDirection: "column",
flexShrink: options.flexShrink ?? 0
});
this._syntaxStyle = options.syntaxStyle;
this._fg = options.fg ? parseColor(options.fg) : undefined;
this._bg = options.bg ? parseColor(options.bg) : undefined;
this._conceal = options.conceal ?? this._contentDefaultOptions.conceal;
this._concealCode = options.concealCode ?? this._contentDefaultOptions.concealCode;
this._content = options.content ?? this._contentDefaultOptions.content;
this._treeSitterClient = options.treeSitterClient;
this._tableOptions = options.tableOptions;
this._renderNode = options.renderNode;
this._streaming = options.streaming ?? this._contentDefaultOptions.streaming;
this._internalBlockMode = options.internalBlockMode ?? this._contentDefaultOptions.internalBlockMode;
this.updateBlocks();
}
get content() {
return this._content;
}
set content(value) {
if (this.isDestroyed)
return;
if (this._content !== value) {
this._content = value;
this.updateBlocks();
this.requestRender();
}
}
get syntaxStyle() {
return this._syntaxStyle;
}
set syntaxStyle(value) {
if (this._syntaxStyle !== value) {
this._syntaxStyle = value;
this._styleDirty = true;
}
}
get fg() {
return this._fg;
}
set fg(value) {
const next = value ? parseColor(value) : undefined;
if (!colorsEqual(this._fg, next)) {
this._fg = next;
this._styleDirty = true;
}
}
get bg() {
return this._bg;
}
set bg(value) {
const next = value ? parseColor(value) : undefined;
if (!colorsEqual(this._bg, next)) {
this._bg = next;
this._styleDirty = true;
}
}
get conceal() {
return this._conceal;
}
set conceal(value) {
if (this._conceal !== value) {
this._conceal = value;
this._styleDirty = true;
}
}
get concealCode() {
return this._concealCode;
}
set concealCode(value) {
if (this._concealCode !== value) {
this._concealCode = value;
this._styleDirty = true;
}
}
get streaming() {
return this._streaming;
}
set streaming(value) {
if (this.isDestroyed)
return;
if (this._streaming !== value) {
this._streaming = value;
this.updateBlocks(true);
}
}
get tableOptions() {
return this._tableOptions;
}
set tableOptions(value) {
this._tableOptions = value;
this.applyTableOptionsToBlocks();
}
get renderNode() {
return this._renderNode;
}
set renderNode(value) {
if (this._renderNode === value)
return;
this._renderNode = value;
this.clearBlockStates();
this._parseState = null;
this.updateBlocks(true);
this.requestRender();
}
get internalBlockMode() {
return this._internalBlockMode;
}
set internalBlockMode(value) {
if (this._internalBlockMode === value)
return;
this._internalBlockMode = value;
this.updateBlocks(true);
this.requestRender();
}
getStyle(group) {
if (!this._syntaxStyle)
return;
let style = this._syntaxStyle.getStyle(group);
if (!style && group.includes(".")) {
const baseName = group.split(".")[0];
style = this._syntaxStyle.getStyle(baseName);
}
return style;
}
createChunk(text, group, link2) {
const style = this.getStyle(group) || this.getStyle("default");
return {
__isChunk: true,
text,
fg: style?.fg,
bg: style?.bg,
attributes: style ? createTextAttributes({
bold: style.bold,
italic: style.italic,
underline: style.underline,
dim: style.dim
}) : 0,
link: link2
};
}
createDefaultChunk(text) {
return this.createChunk(text, "default");
}
createInitialStyledText(token) {
if (!this._streaming)
return;
const chunks = [];
if ("tokens" in token && Array.isArray(token.tokens)) {
this.renderInlineContent(token.tokens, chunks);
}
if (chunks.length === 0 && "text" in token && typeof token.text === "string") {
this.renderInlineContent(x.lexInline(token.text), chunks);
}
return chunks.length > 0 ? new StyledText(chunks) : undefined;
}
renderInlineContent(tokens, chunks) {
for (const token of tokens) {
this.renderInlineToken(token, chunks);
}
}
renderInlineToken(token, chunks) {
switch (token.type) {
case "text":
chunks.push(this.createDefaultChunk(token.text));
break;
case "escape":
chunks.push(this.createDefaultChunk(token.text));
break;
case "codespan":
if (this._conceal) {
chunks.push(this.createChunk(token.text, "markup.raw"));
} else {
chunks.push(this.createChunk("`", "markup.raw"));
chunks.push(this.createChunk(token.text, "markup.raw"));
chunks.push(this.createChunk("`", "markup.raw"));
}
break;
case "strong":
if (!this._conceal) {
chunks.push(this.createChunk("**", "markup.strong"));
}
for (const child of token.tokens) {
this.renderInlineTokenWithStyle(child, chunks, "markup.strong");
}
if (!this._conceal) {
chunks.push(this.createChunk("**", "markup.strong"));
}
break;
case "em":
if (!this._conceal) {
chunks.push(this.createChunk("*", "markup.italic"));
}
for (const child of token.tokens) {
this.renderInlineTokenWithStyle(child, chunks, "markup.italic");
}
if (!this._conceal) {
chunks.push(this.createChunk("*", "markup.italic"));
}
break;
case "del":
if (!this._conceal) {
chunks.push(this.createChunk("~~", "markup.strikethrough"));
}
for (const child of token.tokens) {
this.renderInlineTokenWithStyle(child, chunks, "markup.strikethrough");
}
if (!this._conceal) {
chunks.push(this.createChunk("~~", "markup.strikethrough"));
}
break;
case "link": {
const linkHref = { url: token.href };
if (this._conceal) {
for (const child of token.tokens) {
this.renderInlineTokenWithStyle(child, chunks, "markup.link.label", linkHref);
}
chunks.push(this.createChunk(" (", "markup.link", linkHref));
chunks.push(this.createChunk(token.href, "markup.link.url", linkHref));
chunks.push(this.createChunk(")", "markup.link", linkHref));
} else {
chunks.push(this.createChunk("[", "markup.link", linkHref));
for (const child of token.tokens) {
this.renderInlineTokenWithStyle(child, chunks, "markup.link.label", linkHref);
}
chunks.push(this.createChunk("](", "markup.link", linkHref));
chunks.push(this.createChunk(token.href, "markup.link.url", linkHref));
chunks.push(this.createChunk(")", "markup.link", linkHref));
}
break;
}
case "image": {
const imageHref = { url: token.href };
if (this._conceal) {
chunks.push(this.createChunk(token.text || "image", "markup.link.label", imageHref));
} else {
chunks.push(this.createChunk(");
chunks.push(this.createChunk(token.href, "markup.link.url", imageHref));
chunks.push(this.createChunk(")", "markup.link", imageHref));
}
break;
}
case "br":
chunks.push(this.createDefaultChunk(`
`));
break;
default:
if ("tokens" in token && Array.isArray(token.tokens)) {
this.renderInlineContent(token.tokens, chunks);
} else if ("text" in token && typeof token.text === "string") {
chunks.push(this.createDefaultChunk(token.text));
}
break;
}
}
renderInlineTokenWithStyle(token, chunks, styleGroup, link2) {
switch (token.type) {
case "text":
chunks.push(this.createChunk(token.text, styleGroup, link2));
break;
case "escape":
chunks.push(this.createChunk(token.text, styleGroup, link2));
break;
case "codespan":
if (this._conceal) {
chunks.push(this.createChunk(token.text, "markup.raw", link2));
} else {
chunks.push(this.createChunk("`", "markup.raw", link2));
chunks.push(this.createChunk(token.text, "markup.raw", link2));
chunks.push(this.createChunk("`", "markup.raw", link2));
}
break;
default:
this.renderInlineToken(token, chunks);
break;
}
}
applyMargins(renderable, marginTop, marginBottom) {
renderable.marginTop = marginTop;
renderable.marginBottom = marginBottom;
}
createMarkdownCodeRenderable(content, id, marginBottom = 0, onChunks = this._linkifyMarkdownChunks, baseHighlight, initialStyledText) {
return new CodeRenderable(this.ctx, {
id,
content,
filetype: "markdown",
syntaxStyle: this._syntaxStyle,
fg: this._fg,
bg: this._bg,
conceal: this._conceal,
drawUnstyledText: initialStyledText !== undefined,
streaming: true,
initialStyledText,
baseHighlight,
onChunks,
treeSitterClient: this._treeSitterClient,
width: "100%",
marginBottom
});
}
getBlockquoteContent(token) {
return "text" in token && typeof token.text === "string" && token.text ? token.text : " ";
}
getBlockquoteBorderColor() {
return this.getStyle("conceal")?.fg ?? this.getStyle("default")?.fg ?? this._fg ?? "#FFFFFF";
}
createBlockquoteRenderable(token, id, marginBottom = 0) {
const renderable = new BoxRenderable(this.ctx, {
id,
width: "100%",
border: ["left"],
borderColor: this.getBlockquoteBorderColor(),
paddingLeft: 1,
flexShrink: 0,
marginBottom
});
renderable.add(this.createMarkdownCodeRenderable(this.getBlockquoteContent(token), `${id}-content`, 0, this._linkifyMarkdownChunks, "markup.quote"));
return renderable;
}
createListRenderable(token, id, marginBottom = 0) {
const list = new BoxRenderable(this.ctx, {
id,
width: "100%",
flexDirection: "column",
flexShrink: 0,
marginBottom
});
for (const item of this.getListItemInputs(token, id)) {
list.add(this.createListItemRenderable(item));
}
return list;
}
getListItemInputs(token, id) {
const items = token.items ?? [];
const start = token.start === "" || token.start === undefined || token.start === null ? 1 : Number(token.start);
const markerWidth = Math.max(1, ...items.map((_2, index) => (token.ordered ? `${start + index}.` : "-").length));
return items.map((item, index) => ({
item,
marker: token.ordered ? `${start + index}.` : "-",
markerWidth,
id: `${id}-item-${index}`
}));
}
applyListRenderable(renderable, token, previousToken, id, marginBottom = 0) {
if (!(renderable instanceof BoxRenderable))
return false;
renderable.marginBottom = marginBottom;
const inputs = this.getListItemInputs(token, id);
const previousItems = previousToken?.items ?? [];
const rows = renderable.getChildren();
for (let index = 0;index < inputs.length; index += 1) {
const input = inputs[index];
const existing = rows[index];
if (existing instanceof BoxRenderable && this.applyListItemRenderable(existing, input, previousItems[index])) {
continue;
}
existing?.destroyRecursively();
renderable.add(this.createListItemRenderable(input), index);
}
for (let index = rows.length - 1;index >= inputs.length; index -= 1) {
rows[index]?.destroyRecursively();
}
return true;
}
createListItemRenderable(input) {
const row = new BoxRenderable(this.ctx, {
id: input.id,
width: "100%",
flexDirection: "row",
flexShrink: 0,
marginBottom: /\n[ \t]*\n$/.test(input.item.raw) ? 1 : 0
});
row.add(new TextRenderable(this.ctx, {
id: `${input.id}-marker`,
content: new StyledText([this.createChunk(input.marker.padStart(input.markerWidth) + " ", "markup.list")]),
width: input.markerWidth + 1,
flexShrink: 0
}));
const content = new BoxRenderable(this.ctx, {
id: `${input.id}-content`,
flexDirection: "column",
flexGrow: 1,
flexShrink: 1
});
row.add(content);
let pendingMarginTop = 0;
for (let index = 0;index < input.item.tokens.length; index += 1) {
const child = input.item.tokens[index];
if (!child)
continue;
if (child.type === "checkbox")
continue;
if (child.type === "space") {
pendingMarginTop = Math.max(pendingMarginTop, 1);
continue;
}
const renderable = this.createListChildRenderable(child, `${input.id}-child-${index}`);
if (!renderable)
continue;
renderable.marginTop = child.type === "list" ? 0 : pendingMarginTop;
pendingMarginTop = 0;
content.add(renderable);
}
return row;
}
applyListItemRenderable(row, input, previousItem) {
this.applyListItemMarker(row, input);
const content = row.getChildren()[1];
if (!(content instanceof BoxRenderable))
return false;
if (previousItem && previousItem.raw === input.item.raw) {
return true;
}
return this.applyListItemChildren(content, input.item, previousItem, input.id);
}
applyListItemChildren(content, item, previousItem, id) {
const previousTokens = previousItem ? this.getRenderableListItemTokens(previousItem) : [];
const children = content.getChildren();
let childIndex = 0;
let pendingMarginTop = 0;
for (let tokenIndex = 0;tokenIndex < item.tokens.length; tokenIndex += 1) {
const token = item.tokens[tokenIndex];
if (!token)
continue;
if (token.type === "checkbox")
continue;
if (token.type === "space") {
pendingMarginTop = Math.max(pendingMarginTop, 1);
continue;
}
const existing = children[childIndex];
const childId = `${id}-child-${tokenIndex}`;
const marginTop = token.type === "list" ? 0 : pendingMarginTop;
pendingMarginTop = 0;
if (!existing) {
const renderable = this.createListChildRenderable(token, childId);
if (!renderable)
return false;
renderable.marginTop = marginTop;
content.add(renderable, childIndex);
childIndex += 1;
continue;
}
if (!this.applyListChildRenderable(existing, token, previousTokens[childIndex], childId)) {
return false;
}
existing.marginTop = marginTop;
childIndex += 1;
}
this.destroyListItemChildrenAfter(content, childIndex);
return true;
}
getRenderableListItemTokens(item) {
const tokens = [];
for (const token of item.tokens) {
if (token.type === "checkbox" || token.type === "space")
continue;
tokens.push(token);
}
return tokens;
}
applyListChildRenderable(renderable, token, previousToken, id) {
if ((token.type === "text" || token.type === "paragraph") && renderable instanceof CodeRenderable) {
this.applyMarkdownCodeRenderable(renderable, this.normalizeScrollbackMarkdownBlockRaw(token.raw), 0);
return true;
}
if (token.type === "list" && renderable instanceof BoxRenderable) {
return this.applyListRenderable(renderable, token, previousToken, id);
}
if (token.type === "code" && renderable instanceof CodeRenderable) {
this.applyCodeBlockRenderable(renderable, token, 0);
return true;
}
return previousToken?.raw === token.raw;
}
destroyListItemChildrenAfter(content, index) {
const children = content.getChildren();
for (let i = children.length - 1;i >= index; i -= 1) {
children[i]?.destroyRecursively();
}
}
applyListItemMarker(row, input) {
const marker = row.getChildren()[0];
if (!(marker instanceof TextRenderable))
return;
const marginBottom = /\n[ \t]*\n$/.test(input.item.raw) ? 1 : 0;
const markerWidth = input.markerWidth + 1;
const markerText = input.marker.padStart(input.markerWidth) + " ";
if (row.marginBottom !== marginBottom)
row.marginBottom = marginBottom;
if (marker.width !== markerWidth)
marker.width = markerWidth;
if (marker.chunks[0]?.text !== markerText) {
marker.content = new StyledText([this.createChunk(markerText, "markup.list")]);
}
}
createListChildRenderable(token, id) {
if (token.type === "text" || token.type === "paragraph") {
return this.createMarkdownCodeRenderable(this.normalizeScrollbackMarkdownBlockRaw(token.raw), id, 0, this._linkifyMarkdownChunks, undefined, this.createInitialStyledText(token));
}
if (token.type === "list")
return this.createListRenderable(token, id);
if (token.type === "code")
return this.createCodeRenderable(token, id);
if (token.type === "blockquote")
return this.createBlockquoteRenderable(token, id);
if (token.type === "hr")
return this.createHorizontalRuleRenderable(id);
if (token.type === "table")
return this.createTableBlock(token, id).renderable;
return token.raw ? this.createMarkdownCodeRenderable(token.raw, id, 0, this._linkifyMarkdownChunks, undefined, this.createInitialStyledText(token)) : null;
}
createHorizontalRuleRenderable(id, marginBottom = 0) {
return new BoxRenderable(this.ctx, {
id,
width: "100%",
height: 1,
border: ["top"],
borderColor: this.getStyle("conceal")?.fg ?? this._fg ?? "#888888",
flexShrink: 0,
marginBottom
});
}
createCodeRenderable(token, id, marginBottom = 0) {
return new CodeRenderable(this.ctx, {
id,
content: token.text,
filetype: infoStringToFiletype(token.lang ?? ""),
syntaxStyle: this._syntaxStyle,
fg: this._fg,
bg: this._bg,
conceal: this._concealCode,
drawUnstyledText: !this._streaming,
streaming: this._streaming,
treeSitterClient: this._treeSitterClient,
width: "100%",
marginBottom
});
}
applyMarkdownCodeRenderable(renderable, content, marginBottom, baseHighlight, initialStyledText) {
renderable.initialStyledText = initialStyledText;
renderable.filetype = "markdown";
renderable.syntaxStyle = this._syntaxStyle;
renderable.fg = this._fg;
renderable.bg = this._bg;
renderable.conceal = this._conceal;
renderable.drawUnstyledText = initialStyledText !== undefined;
renderable.streaming = true;
renderable.baseHighlight = baseHighlight;
renderable.content = content;
renderable.marginBottom = marginBottom;
}
applyBlockquoteRenderable(renderable, token, marginBottom) {
if (!(renderable instanceof BoxRenderable))
return;
renderable.borderColor = this.getBlockquoteBorderColor();
renderable.marginBottom = marginBottom;
const child = renderable.getChildren()[0];
if (child instanceof CodeRenderable) {
this.applyMarkdownCodeRenderable(child, this.getBlockquoteContent(token), 0, "markup.quote");
return;
}
for (const existing of renderable.getChildren()) {
existing.destroyRecursively();
}
renderable.add(this.createMarkdownCodeRenderable(this.getBlockquoteContent(token), `${renderable.id}-content`, 0, this._linkifyMarkdownChunks, "markup.quote"));
}
applyCodeBlockRenderable(renderable, token, marginBottom) {
if (!(renderable instanceof CodeRenderable))
return;
renderable.filetype = infoStringToFiletype(token.lang ?? "");
renderable.syntaxStyle = this._syntaxStyle;
renderable.fg = this._fg;
renderable.bg = this._bg;
renderable.conceal = this._concealCode;
renderable.drawUnstyledText = !this._streaming;
renderable.streaming = this._streaming;
renderable.content = token.text;
renderable.marginBottom = marginBottom;
}
shouldRenderSeparately(token) {
return token.type === "code" || token.type === "table" || token.type === "blockquote" || token.type === "hr";
}
getInterBlockMargin(token, nextToken) {
if (!nextToken)
return 0;
if (this.shouldRenderSeparately(token))
return 1;
if (!this.shouldRenderSeparately(nextToken))
return 0;
return TRAILING_MARKDOWN_BLOCK_NEWLINES_RE.test(token.raw) ? 0 : 1;
}
applyInterBlockMargin(state, token, nextToken) {
if (state.tracksInterBlockMargin === false)
return;
state.renderable.marginBottom = this.getInterBlockMargin(token, nextToken);
}
createMarkdownBlockToken(raw) {
return {
type: "paragraph",
raw,
text: raw,
tokens: []
};
}
normalizeMarkdownBlockRaw(raw) {
return raw.replace(TRAILING_MARKDOWN_BLOCK_BREAKS_RE, `
`);
}
normalizeScrollbackMarkdownBlockRaw(raw) {
return raw.replace(TRAILING_MARKDOWN_BLOCK_NEWLINES_RE, "");
}
isCodeBlockOnlyRenderer() {
return this._renderNode?.codeBlockOnly === true;
}
buildRenderableTokens(tokens) {
if (this._renderNode && !this.isCodeBlockOnlyRenderer()) {
return tokens.filter((token) => token.type !== "space");
}
const renderTokens = [];
let markdownRaw = "";
const flushMarkdownRaw = () => {
if (markdownRaw.length === 0)
return;
const normalizedRaw = this.normalizeMarkdownBlockRaw(markdownRaw);
if (normalizedRaw.length > 0) {
renderTokens.push(this.createMarkdownBlockToken(normalizedRaw));
}
markdownRaw = "";
};
for (let i = 0;i < tokens.length; i += 1) {
const token = tokens[i];
if (token.type === "space") {
if (markdownRaw.length === 0) {
continue;
}
let nextIndex = i + 1;
while (nextIndex < tokens.length && tokens[nextIndex].type === "space") {
nextIndex += 1;
}
const nextToken = tokens[nextIndex];
if (nextToken && !this.shouldRenderSeparately(nextToken)) {
markdownRaw += token.raw;
}
continue;
}
if (this.shouldRenderSeparately(token)) {
flushMarkdownRaw();
renderTokens.push(token);
continue;
}
markdownRaw += token.raw;
}
flushMarkdownRaw();
return renderTokens;
}
buildTopLevelRenderBlocks(tokens) {
const blocks = [];
let gapBefore = "";
for (let i = 0;i < tokens.length; i += 1) {
const token = tokens[i];
if (token.type === "space") {
gapBefore += token.raw;
continue;
}
const prev = blocks[blocks.length - 1];
const marginTop = prev && this.shouldAddTopLevelMargin(prev.token, token, gapBefore) ? 1 : 0;
blocks.push({
token,
sourceTokenEnd: i + 1,
marginTop
});
gapBefore = "";
}
return blocks;
}
shouldAddTopLevelMargin(prev, current, gapBefore) {
if (this.isSeparatedTopLevelBlock(prev) || this.isSeparatedTopLevelBlock(current))
return true;
if (prev.type !== "paragraph" || current.type !== "paragraph")
return false;
return TRAILING_MARKDOWN_BLOCK_BREAKS_RE.test(prev.raw + gapBefore);
}
isSeparatedTopLevelBlock(token) {
return token.type === "heading" || token.type === "list" || this.shouldRenderSeparately(token);
}
getTableRowsToRender(table) {
return table.rows;
}
hashString(value, seed) {
let hash = seed >>> 0;
for (let i = 0;i < value.length; i += 1) {
hash ^= value.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
return hash >>> 0;
}
hashTableToken(token, seed, depth = 0) {
let hash = this.hashString(token.type, seed);
if ("raw" in token && typeof token.raw === "string") {
return this.hashString(token.raw, hash);
}
if ("text" in token && typeof token.text === "string") {
hash = this.hashString(token.text, hash);
}
if (depth < 2 && "tokens" in token && Array.isArray(token.tokens)) {
for (const child of token.tokens) {
hash = this.hashTableToken(child, hash, depth + 1);
}
}
return hash >>> 0;
}
getTableCellKey(cell, isHeader) {
const seed = isHeader ? 2902232141 : 1371922141;
if (!cell) {
return seed;
}
if (typeof cell.text === "string") {
return this.hashString(cell.text, seed);
}
if (Array.isArray(cell.tokens) && cell.tokens.length > 0) {
let hash = seed ^ cell.tokens.length;
for (const token of cell.tokens) {
hash = this.hashTableToken(token, hash);
}
return hash >>> 0;
}
return (seed ^ 2654435769) >>> 0;
}
createTableDataCellChunks(cell) {
const chunks = [];
if (cell) {
this.renderInlineContent(cell.tokens, chunks);
}
return chunks.length > 0 ? chunks : [this.createDefaultChunk(" ")];
}
createTableHeaderCellChunks(cell) {
const chunks = [];
this.renderInlineContent(cell.tokens, chunks);
const baseChunks = chunks.length > 0 ? chunks : [this.createDefaultChunk(" ")];
const headingStyle = this.getStyle("markup.heading") || this.getStyle("default");
if (!headingStyle) {
return baseChunks;
}
const headingAttributes = createTextAttributes({
bold: headingStyle.bold,
italic: headingStyle.italic,
underline: headingStyle.underline,
dim: headingStyle.dim
});
return baseChunks.map((chunk) => ({
...chunk,
fg: headingStyle.fg ?? chunk.fg,
bg: headingStyle.bg ?? chunk.bg,
attributes: headingAttributes
}));
}
buildTableContentCache(table, previous, forceRegenerate = false) {
const colCount = table.header.length;
const rowsToRender = this.getTableRowsToRender(table);
if (colCount === 0 || rowsToRender.length === 0) {
return { cache: null, changed: previous !== undefined };
}
const content = [];
const cellKeys = [];
const totalRows = rowsToRender.length + 1;
let changed = forceRegenerate || !previous;
for (let rowIndex = 0;rowIndex < totalRows; rowIndex += 1) {
const rowContent = [];
const rowKeys = new Uint32Array(colCount);
for (let colIndex = 0;colIndex < colCount; colIndex += 1) {
const isHeader = rowIndex === 0;
const cell = isHeader ? table.header[colIndex] : rowsToRender[rowIndex - 1]?.[colIndex];
const cellKey = this.getTableCellKey(cell, isHeader);
rowKeys[colIndex] = cellKey;
const previousCellKey = previous?.cellKeys[rowIndex]?.[colIndex];
const previousCellContent = previous?.content[rowIndex]?.[colIndex];
if (!forceRegenerate && previousCellKey === cellKey && Array.isArray(previousCellContent)) {
rowContent.push(previousCellContent);
continue;
}
changed = true;
rowContent.push(isHeader ? this.createTableHeaderCellChunks(table.header[colIndex]) : this.createTableDataCellChunks(cell));
}
content.push(rowContent);
cellKeys.push(rowKeys);
}
if (previous && !changed) {
if (previous.content.length !== content.length) {
changed = true;
} else {
for (let rowIndex = 0;rowIndex < content.length; rowIndex += 1) {
if ((previous.content[rowIndex]?.length ?? 0) !== content[rowIndex].length) {
changed = true;
break;
}
}
}
}
return {
cache: {
content,
cellKeys
},
changed
};
}
resolveTableStyle(options = this._tableOptions) {
if (options?.style === "columns") {
return "columns";
}
if (options?.style === "grid") {
return "grid";
}
return this._internalBlockMode === "top-level" ? "columns" : "grid";
}
usesBorderlessColumnSpacing(options = this._tableOptions) {
const style = this.resolveTableStyle(options);
const borders = options?.borders ?? style === "grid";
return style === "columns" && !borders;
}
resolveTableRenderableOptions() {
const style = this.resolveTableStyle();
const borders = this._tableOptions?.borders ?? style === "grid";
return {
columnWidthMode: this._tableOptions?.widthMode ?? (style === "columns" ? "content" : "full"),
columnFitter: this._tableOptions?.columnFitter ?? "proportional",
wrapMode: this._tableOptions?.wrapMode ?? "word",
cellPadding: this._tableOptions?.cellPadding ?? 0,
cellPaddingX: this._tableOptions?.cellPaddingX ?? this._tableOptions?.cellPadding ?? 0,
cellPaddingY: this._tableOptions?.cellPaddingY ?? this._tableOptions?.cellPadding ?? 0,
columnGap: this.usesBorderlessColumnSpacing() ? 2 : 0,
border: borders,
outerBorder: this._tableOptions?.outerBorder ?? borders,
showBorders: borders,
borderStyle: this._tableOptions?.borderStyle ?? "single",
borderColor: this._tableOptions?.borderColor ?? this.getStyle("conceal")?.fg ?? "#888888",
selectable: this._tableOptions?.selectable ?? true
};
}
applyTableRenderableOptions(tableRenderable, options) {
tableRenderable.columnWidthMode = options.columnWidthMode;
tableRenderable.columnFitter = options.columnFitter;
tableRenderable.wrapMode = options.wrapMode;
tableRenderable.cellPaddingX = options.cellPaddingX;
tableRenderable.cellPaddingY = options.cellPaddingY;
tableRenderable.columnGap = options.columnGap;
tableRenderable.border = options.border;
tableRenderable.outerBorder = options.outerBorder;
tableRenderable.showBorders = options.showBorders;
tableRenderable.borderStyle = options.borderStyle;
tableRenderable.borderColor = options.borderColor;
tableRenderable.selectable = options.selectable;
}
applyTableOptionsToBlocks() {
const options = this.resolveTableRenderableOptions();
let updated = false;
for (const state of this._blockStates) {
if (state.renderable instanceof TextTableRenderable) {
this.applyTableRenderableOptions(state.renderable, options);
updated = true;
}
}
if (updated) {
this.requestRender();
}
}
createTextTableRenderable(content, id, marginBottom = 0) {
const options = this.resolveTableRenderableOptions();
return new TextTableRenderable(this.ctx, {
id,
content,
width: "100%",
marginBottom,
columnWidthMode: options.columnWidthMode,
columnFitter: options.columnFitter,
wrapMode: options.wrapMode,
cellPadding: options.cellPadding,
cellPaddingX: options.cellPaddingX,
cellPaddingY: options.cellPaddingY,
columnGap: options.columnGap,
border: options.border,
outerBorder: options.outerBorder,
showBorders: options.showBorders,
borderStyle: options.borderStyle,
borderColor: options.borderColor,
selectable: options.selectable
});
}
createTableBlock(table, id, marginBottom = 0, previousCache, forceRegenerate = false) {
const { cache } = this.buildTableContentCache(table, previousCache, forceRegenerate);
if (!cache) {
return {
renderable: this.createMarkdownCodeRenderable(table.raw, id, marginBottom)
};
}
return {
renderable: this.createTextTableRenderable(cache.content, id, marginBottom),
tableContentCache: cache
};
}
getStableBlockCount(blocks, stableTokenCount) {
if (this._internalBlockMode !== "top-level") {
return 0;
}
let stableBlockCount = 0;
for (const block of blocks) {
if (block.sourceTokenEnd <= stableTokenCount) {
stableBlockCount += 1;
continue;
}
break;
}
return stableBlockCount;
}
syncTopLevelBlockState(state, block, tableContentCache = state.tableContentCache) {
state.token = block.token;
state.tokenRaw = block.token.raw;
state.marginTop = block.marginTop;
state.tableContentCache = tableContentCache;
}
getTopLevelBlockRaw(token) {
if (!token.raw) {
return;
}
return this.shouldRenderSeparately(token) ? token.raw : this.normalizeScrollbackMarkdownBlockRaw(token.raw);
}
createTopLevelDefaultRenderable(block, index) {
const { token, marginTop } = block;
const id = `${this.id}-block-${index}`;
if (token.type === "code") {
const renderable2 = this.createCodeRenderable(token, id);
renderable2.marginTop = marginTop;
return { renderable: renderable2, canUpdateInPlace: true };
}
if (token.type === "table") {
const next = this.createTableBlock(token, id);
next.renderable.marginTop = marginTop;
return { ...next, canUpdateInPlace: true };
}
if (token.type === "blockquote") {
const renderable2 = this.createBlockquoteRenderable(token, id);
renderable2.marginTop = marginTop;
return { renderable: renderable2, canUpdateInPlace: true };
}
if (token.type === "list") {
const renderable2 = this.createListRenderable(token, id);
renderable2.marginTop = marginTop;
return { renderable: renderable2, canUpdateInPlace: true };
}
if (token.type === "hr") {
const renderable2 = this.createHorizontalRuleRenderable(id);
renderable2.marginTop = marginTop;
return { renderable: renderable2, canUpdateInPlace: true };
}
const markdownRaw = this.getTopLevelBlockRaw(token);
if (!markdownRaw) {
return { renderable: undefined, canUpdateInPlace: true };
}
const renderable = this.createMarkdownCodeRenderable(markdownRaw, id, 0, this._linkifyMarkdownChunks, undefined, this.createInitialStyledText(token));
renderable.marginTop = marginTop;
return { renderable, canUpdateInPlace: true };
}
createTopLevelRenderable(block, index) {
if (!this._renderNode) {
return this.createTopLevelDefaultRenderable(block, index);
}
const custom = this.createTopLevelCustomRenderable(block, index);
if (!custom.renderable)
return this.createTopLevelDefaultRenderable(block, index);
const marginTop = typeof custom.renderable.marginTop === "number" ? Math.max(custom.renderable.marginTop, block.marginTop) : block.marginTop;
this.applyMargins(custom.renderable, marginTop, 0);
return {
renderable: custom.renderable,
tableContentCache: custom.tableContentCache,
canUpdateInPlace: custom.canUpdateInPlace
};
}
createDefaultRenderable(token, index, nextToken) {
const id = `${this.id}-block-${index}`;
const marginBottom = this.getInterBlockMargin(token, nextToken);
if (token.type === "code") {
return this.createCodeRenderable(token, id, marginBottom);
}
if (token.type === "blockquote") {
return this.createBlockquoteRenderable(token, id, marginBottom);
}
if (token.type === "list") {
return this.createListRenderable(token, id, marginBottom);
}
if (token.type === "hr") {
return this.createHorizontalRuleRenderable(id, marginBottom);
}
if (token.type === "table") {
return this.createTableBlock(token, id, marginBottom).renderable;
}
if (token.type === "space") {
return null;
}
if (!token.raw) {
return null;
}
return this.createMarkdownCodeRenderable(token.raw, id, marginBottom, this._linkifyMarkdownChunks, undefined, this.createInitialStyledText(token));
}
createCustomRenderable(token, index, nextToken) {
const custom = this.renderCustomNode(token, () => {
return { renderable: this.createDefaultRenderable(token, index, nextToken) };
});
if (!custom.renderable) {
return { tracksInterBlockMargin: true, canUpdateInPlace: true };
}
const canUpdateInPlace = custom.renderable === custom.defaultResult?.renderable;
return {
renderable: custom.renderable,
tracksInterBlockMargin: canUpdateInPlace,
canUpdateInPlace
};
}
createTopLevelCustomRenderable(block, index) {
const custom = this.renderCustomNode(block.token, () => {
return this.createTopLevelDefaultRenderable(block, index);
});
if (!custom.renderable) {
return { tracksInterBlockMargin: true, canUpdateInPlace: true };
}
const canUpdateInPlace = custom.renderable === custom.defaultResult?.renderable;
return {
renderable: custom.renderable,
tableContentCache: canUpdateInPlace ? custom.defaultResult?.tableContentCache : undefined,
tracksInterBlockMargin: canUpdateInPlace,
canUpdateInPlace
};
}
renderCustomNode(token, createDefault) {
if (!this._renderNode)
return {};
let defaultResult;
const custom = this._renderNode(token, {
syntaxStyle: this._syntaxStyle,
conceal: this._conceal,
concealCode: this._concealCode,
treeSitterClient: this._treeSitterClient,
defaultRender: () => {
defaultResult = createDefault();
return defaultResult.renderable ?? null;
}
});
this.destroyUnusedDefaultRenderable(defaultResult?.renderable, custom ?? undefined);
return custom ? { renderable: custom, defaultResult } : {};
}
destroyUnusedDefaultRenderable(renderable, usedRenderable) {
if (!renderable || renderable === usedRenderable || renderable.parent)
return;
renderable.destroyRecursively();
}
updateBlockRenderable(state, token, index, nextToken, forceListRefresh = false) {
const marginBottom = this.getInterBlockMargin(token, nextToken);
if (token.type === "code") {
this.applyCodeBlockRenderable(state.renderable, token, marginBottom);
return;
}
if (token.type === "blockquote") {
this.applyBlockquoteRenderable(state.renderable, token, marginBottom);
return;
}
if (token.type === "list") {
if (!this.applyListRenderable(state.renderable, token, forceListRefresh ? undefined : state.token, `${this.id}-block-${index}`, marginBottom)) {
state.renderable.destroyRecursively();
state.renderable = this.createListRenderable(token, `${this.id}-block-${index}`, marginBottom);
this.add(state.renderable, index);
}
return;
}
if (token.type === "hr") {
state.renderable.marginBottom = marginBottom;
return;
}
if (token.type === "table") {
const tableToken = token;
const { cache, changed } = this.buildTableContentCache(tableToken, state.tableContentCache);
if (!cache) {
if (state.renderable instanceof CodeRenderable) {
this.applyMarkdownCodeRenderable(state.renderable, tableToken.raw, marginBottom);
state.tableContentCache = undefined;
return;
}
state.renderable.destroyRecursively();
const fallbackRenderable = this.createMarkdownCodeRenderable(tableToken.raw, `${this.id}-block-${index}`, marginBottom);
this.add(fallbackRenderable, index);
state.renderable = fallbackRenderable;
state.tableContentCache = undefined;
return;
}
if (state.renderable instanceof TextTableRenderable) {
if (changed) {
state.renderable.content = cache.content;
}
this.applyTableRenderableOptions(state.renderable, this.resolveTableRenderableOptions());
state.renderable.marginBottom = marginBottom;
state.tableContentCache = cache;
return;
}
state.renderable.destroyRecursively();
const tableRenderable = this.createTextTableRenderable(cache.content, `${this.id}-block-${index}`, marginBottom);
this.add(tableRenderable, index);
state.renderable = tableRenderable;
state.tableContentCache = cache;
return;
}
if (state.renderable instanceof CodeRenderable) {
this.applyMarkdownCodeRenderable(state.renderable, this.getTopLevelBlockRaw(token) ?? token.raw, marginBottom, undefined, this.createInitialStyledText(token));
return;
}
state.renderable.destroyRecursively();
const markdownRenderable = this.createMarkdownCodeRenderable(this.getTopLevelBlockRaw(token) ?? token.raw, `${this.id}-block-${index}`, marginBottom, this._linkifyMarkdownChunks, undefined, this.createInitialStyledText(token));
this.add(markdownRenderable, index);
state.renderable = markdownRenderable;
}
updateTopLevelBlocks(tokens, forceTableRefresh) {
const blocks = this.buildTopLevelRenderBlocks(tokens);
this._stableBlockCount = this.getStableBlockCount(blocks, this._parseState?.stableTokenCount ?? 0);
let blockIndex = 0;
for (let i = 0;i < blocks.length; i += 1) {
const block = blocks[i];
const existing = this._blockStates[blockIndex];
if (existing && existing.token === block.token && !forceTableRefresh) {
if (existing.marginTop !== block.marginTop) {
this.applyMargins(existing.renderable, block.marginTop, 0);
}
this.syncTopLevelBlockState(existing, block);
blockIndex++;
continue;
}
if (existing && existing.tokenRaw === block.token.raw && existing.token.type === block.token.type && !forceTableRefresh) {
if (existing.marginTop !== block.marginTop) {
this.applyMargins(existing.renderable, block.marginTop, 0);
}
this.syncTopLevelBlockState(existing, block);
blockIndex++;
continue;
}
if (existing && !forceTableRefresh && existing.canUpdateInPlace && existing.token.type === block.token.type && this.canUpdateBlockRenderable(existing.renderable, block.token)) {
if (this._renderNode) {
const custom = this.createTopLevelCustomRenderable(block, blockIndex);
if (custom.renderable && !custom.canUpdateInPlace) {
const marginTop = typeof custom.renderable.marginTop === "number" ? Math.max(custom.renderable.marginTop, block.marginTop) : block.marginTop;
this.applyMargins(custom.renderable, marginTop, 0);
if (custom.renderable !== existing.renderable) {
existing.renderable.destroyRecursively();
this.add(custom.renderable, blockIndex);
}
this._blockStates[blockIndex] = {
token: block.token,
tokenRaw: block.token.raw,
marginTop: block.marginTop,
renderable: custom.renderable,
tableContentCache: custom.tableContentCache,
canUpdateInPlace: custom.canUpdateInPlace
};
blockIndex++;
continue;
}
this.destroyUnusedDefaultRenderable(custom.renderable);
}
this.updateBlockRenderable(existing, block.token, blockIndex, blocks[i + 1]?.token);
existing.renderable.marginBottom = 0;
if (existing.marginTop !== block.marginTop) {
this.applyMargins(existing.renderable, block.marginTop, 0);
}
this.syncTopLevelBlockState(existing, block);
blockIndex++;
continue;
}
if (existing) {
existing.renderable.destroyRecursively();
}
const next = this.createTopLevelRenderable(block, blockIndex);
if (next.renderable) {
this.add(next.renderable, blockIndex);
this._blockStates[blockIndex] = {
token: block.token,
tokenRaw: block.token.raw,
marginTop: block.marginTop,
renderable: next.renderable,
tableContentCache: next.tableContentCache,
canUpdateInPlace: next.canUpdateInPlace
};
}
blockIndex++;
}
while (this._blockStates.length > blockIndex) {
const removed = this._blockStates.pop();
removed.renderable.destroyRecursively();
}
}
canUpdateBlockRenderable(renderable, token) {
if (token.type === "code")
return renderable instanceof CodeRenderable;
if (token.type === "table")
return renderable instanceof TextTableRenderable;
if (token.type === "blockquote")
return renderable instanceof BoxRenderable;
if (token.type === "list")
return renderable instanceof BoxRenderable;
if (token.type === "hr")
return renderable instanceof BoxRenderable;
return renderable instanceof CodeRenderable;
}
updateBlocks(forceTableRefresh = false) {
if (this.isDestroyed)
return;
if (!this._content) {
this.clearBlockStates();
this._parseState = null;
this._stableBlockCount = 0;
return;
}
const trailingUnstable = this._streaming ? 2 : 0;
this._parseState = parseMarkdownIncremental(this._content, this._parseState, trailingUnstable);
const tokens = this._parseState.tokens;
if (tokens.length === 0 && this._content.length > 0) {
this.clearBlockStates();
this._stableBlockCount = 0;
const fallback = this.createMarkdownCodeRenderable(this._content, `${this.id}-fallback`);
this.add(fallback);
this._blockStates = [
{
token: { type: "text", raw: this._content, text: this._content },
tokenRaw: this._content,
marginTop: 0,
renderable: fallback,
tracksInterBlockMargin: true,
canUpdateInPlace: true
}
];
return;
}
if (this._internalBlockMode === "top-level") {
this.updateTopLevelBlocks(tokens, forceTableRefresh);
return;
}
this._stableBlockCount = 0;
const blockTokens = this.buildRenderableTokens(tokens);
let blockIndex = 0;
for (let i = 0;i < blockTokens.length; i++) {
const token = blockTokens[i];
const nextToken = blockTokens[i + 1];
const existing = this._blockStates[blockIndex];
const shouldForceRefresh = forceTableRefresh;
if (existing && existing.token === token) {
if (shouldForceRefresh) {
this.updateBlockRenderable(existing, token, blockIndex, nextToken);
existing.tokenRaw = token.raw;
} else {
this.applyInterBlockMargin(existing, token, nextToken);
}
blockIndex++;
continue;
}
if (existing && existing.tokenRaw === token.raw && existing.token.type === token.type) {
existing.token = token;
if (shouldForceRefresh) {
this.updateBlockRenderable(existing, token, blockIndex, nextToken);
existing.tokenRaw = token.raw;
} else {
this.applyInterBlockMargin(existing, token, nextToken);
}
blockIndex++;
continue;
}
if (existing && existing.canUpdateInPlace && existing.token.type === token.type) {
const custom2 = this.createCustomRenderable(token, blockIndex, nextToken);
if (custom2.renderable && !custom2.canUpdateInPlace) {
if (custom2.renderable !== existing.renderable) {
existing.renderable.destroyRecursively();
this.add(custom2.renderable, blockIndex);
}
this._blockStates[blockIndex] = {
token,
tokenRaw: token.raw,
renderable: custom2.renderable,
tracksInterBlockMargin: custom2.tracksInterBlockMargin,
canUpdateInPlace: custom2.canUpdateInPlace
};
blockIndex++;
continue;
}
this.destroyUnusedDefaultRenderable(custom2.renderable);
this.updateBlockRenderable(existing, token, blockIndex, nextToken);
existing.token = token;
existing.tokenRaw = token.raw;
existing.tracksInterBlockMargin = true;
blockIndex++;
continue;
}
if (existing) {
existing.renderable.destroyRecursively();
}
let renderable;
let tableContentCache;
let tracksInterBlockMargin = true;
let canUpdateInPlace = true;
const custom = this.createCustomRenderable(token, blockIndex, nextToken);
if (custom.renderable) {
renderable = custom.renderable;
tracksInterBlockMargin = custom.tracksInterBlockMargin;
canUpdateInPlace = custom.canUpdateInPlace;
}
if (!renderable) {
if (token.type === "table") {
const tableBlock = this.createTableBlock(token, `${this.id}-block-${blockIndex}`, this.getInterBlockMargin(token, nextToken));
renderable = tableBlock.renderable;
tableContentCache = tableBlock.tableContentCache;
} else {
renderable = this.createDefaultRenderable(token, blockIndex, nextToken) ?? undefined;
}
}
if (token.type === "table" && !tableContentCache && renderable instanceof TextTableRenderable) {
const { cache } = this.buildTableContentCache(token);
tableContentCache = cache ?? undefined;
}
if (renderable) {
this.add(renderable, blockIndex);
this._blockStates[blockIndex] = {
token,
tokenRaw: token.raw,
renderable,
tableContentCache,
tracksInterBlockMargin,
canUpdateInPlace
};
}
blockIndex++;
}
while (this._blockStates.length > blockIndex) {
const removed = this._blockStates.pop();
removed.renderable.destroyRecursively();
}
}
clearBlockStates() {
for (const state of this._blockStates) {
state.renderable.destroyRecursively();
}
this._blockStates = [];
this._stableBlockCount = 0;
}
rerenderBlocks() {
if (this._internalBlockMode === "top-level") {
this.updateBlocks(true);
return;
}
for (let i = 0;i < this._blockStates.length; i++) {
const state = this._blockStates[i];
const marginBottom = this.getInterBlockMargin(state.token, this._blockStates[i + 1]?.token);
if (state.token.type === "code") {
this.applyCodeBlockRenderable(state.renderable, state.token, marginBottom);
continue;
}
if (state.token.type === "blockquote") {
this.applyBlockquoteRenderable(state.renderable, state.token, marginBottom);
continue;
}
if (state.token.type === "list") {
this.updateBlockRenderable(state, state.token, i, this._blockStates[i + 1]?.token, true);
continue;
}
if (state.token.type === "hr") {
state.renderable.marginBottom = marginBottom;
continue;
}
if (state.token.type === "table") {
const tableToken = state.token;
const { cache } = this.buildTableContentCache(tableToken, state.tableContentCache, true);
if (!cache) {
if (state.renderable instanceof CodeRenderable) {
this.applyMarkdownCodeRenderable(state.renderable, tableToken.raw, marginBottom);
} else {
state.renderable.destroyRecursively();
const fallbackRenderable = this.createMarkdownCodeRenderable(tableToken.raw, `${this.id}-block-${i}`, marginBottom);
this.add(fallbackRenderable, i);
state.renderable = fallbackRenderable;
}
state.tableContentCache = undefined;
continue;
}
if (state.renderable instanceof TextTableRenderable) {
state.renderable.content = cache.content;
this.applyTableRenderableOptions(state.renderable, this.resolveTableRenderableOptions());
state.renderable.marginBottom = marginBottom;
state.tableContentCache = cache;
continue;
}
state.renderable.destroyRecursively();
const tableRenderable = this.createTextTableRenderable(cache.content, `${this.id}-block-${i}`, marginBottom);
this.add(tableRenderable, i);
state.renderable = tableRenderable;
state.tableContentCache = cache;
continue;
}
if (state.renderable instanceof CodeRenderable) {
this.applyMarkdownCodeRenderable(state.renderable, this.getTopLevelBlockRaw(state.token) ?? state.token.raw, marginBottom, undefined, this.createInitialStyledText(state.token));
continue;
}
state.renderable.destroyRecursively();
const markdownRenderable = this.createMarkdownCodeRenderable(this.getTopLevelBlockRaw(state.token) ?? state.token.raw, `${this.id}-block-${i}`, marginBottom, this._linkifyMarkdownChunks, undefined, this.createInitialStyledText(state.token));
this.add(markdownRenderable, i);
state.renderable = markdownRenderable;
}
}
clearCache() {
this._parseState = null;
this.clearBlockStates();
this.updateBlocks();
this.requestRender();
}
refreshStyles() {
this._styleDirty = false;
this.rerenderBlocks();
this.requestRender();
}
renderSelf(buffer, deltaTime) {
if (this._styleDirty) {
this._styleDirty = false;
this.rerenderBlocks();
}
super.renderSelf(buffer, deltaTime);
}
}
// src/renderables/Slider.ts
var defaultThumbBackgroundColor = RGBA.fromHex("#9a9ea3");
var defaultTrackBackgroundColor = RGBA.fromHex("#252527");
class SliderRenderable extends Renderable {
orientation;
_value;
_min;
_max;
_viewPortSize;
_backgroundColor;
_foregroundColor;
_onChange;
constructor(ctx, options) {
super(ctx, { flexShrink: 0, ...options });
this.orientation = options.orientation;
this._min = options.min ?? 0;
this._max = options.max ?? 100;
this._value = options.value ?? this._min;
this._viewPortSize = options.viewPortSize ?? Math.max(1, (this._max - this._min) * 0.1);
this._onChange = options.onChange;
this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : defaultTrackBackgroundColor;
this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : defaultThumbBackgroundColor;
this.setupMouseHandling();
}
get value() {
return this._value;
}
set value(newValue) {
const clamped = Math.max(this._min, Math.min(this._max, newValue));
if (clamped !== this._value) {
this._value = clamped;
this._onChange?.(clamped);
this.emit("change", { value: clamped });
this.requestRender();
}
}
get min() {
return this._min;
}
set min(newMin) {
if (newMin !== this._min) {
this._min = newMin;
if (this._value < newMin) {
this.value = newMin;
}
this.requestRender();
}
}
get max() {
return this._max;
}
set max(newMax) {
if (newMax !== this._max) {
this._max = newMax;
if (this._value > newMax) {
this.value = newMax;
}
this.requestRender();
}
}
set viewPortSize(size) {
const clampedSize = Math.max(0.01, Math.min(size, this._max - this._min));
if (clampedSize !== this._viewPortSize) {
this._viewPortSize = clampedSize;
this.requestRender();
}
}
get viewPortSize() {
return this._viewPortSize;
}
get backgroundColor() {
return this._backgroundColor;
}
set backgroundColor(value) {
this._backgroundColor = parseColor(value);
this.requestRender();
}
get foregroundColor() {
return this._foregroundColor;
}
set foregroundColor(value) {
this._foregroundColor = parseColor(value);
this.requestRender();
}
calculateDragOffsetVirtual(event) {
const trackStart = this.orientation === "vertical" ? this.y : this.x;
const mousePos = (this.orientation === "vertical" ? event.y : event.x) - trackStart;
const virtualMousePos = Math.max(0, Math.min((this.orientation === "vertical" ? this.height : this.width) * 2, mousePos * 2));
const virtualThumbStart = this.getVirtualThumbStart();
const virtualThumbSize = this.getVirtualThumbSize();
return Math.max(0, Math.min(virtualThumbSize, virtualMousePos - virtualThumbStart));
}
setupMouseHandling() {
let isDragging = false;
let dragOffsetVirtual = 0;
this.onMouseDown = (event) => {
event.stopPropagation();
event.preventDefault();
const thumb = this.getThumbRect();
const inThumb = event.x >= thumb.x && event.x < thumb.x + thumb.width && event.y >= thumb.y && event.y < thumb.y + thumb.height;
if (inThumb) {
isDragging = true;
dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
} else {
this.updateValueFromMouseDirect(event);
isDragging = true;
dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
}
};
this.onMouseDrag = (event) => {
if (!isDragging)
return;
event.stopPropagation();
this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
};
this.onMouseUp = (event) => {
if (isDragging) {
this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
}
isDragging = false;
};
}
updateValueFromMouseDirect(event) {
const trackStart = this.orientation === "vertical" ? this.y : this.x;
const trackSize = this.orientation === "vertical" ? this.height : this.width;
const mousePos = this.orientation === "vertical" ? event.y : event.x;
const relativeMousePos = mousePos - trackStart;
const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
const ratio = trackSize === 0 ? 0 : clampedMousePos / trackSize;
const range = this._max - this._min;
const newValue = this._min + ratio * range;
this.value = newValue;
}
updateValueFromMouseWithOffset(event, offsetVirtual) {
const trackStart = this.orientation === "vertical" ? this.y : this.x;
const trackSize = this.orientation === "vertical" ? this.height : this.width;
const mousePos = this.orientation === "vertical" ? event.y : event.x;
const virtualTrackSize = trackSize * 2;
const relativeMousePos = mousePos - trackStart;
const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
const virtualMousePos = clampedMousePos * 2;
const virtualThumbSize = this.getVirtualThumbSize();
const maxThumbStart = Math.max(0, virtualTrackSize - virtualThumbSize);
let desiredThumbStart = virtualMousePos - offsetVirtual;
desiredThumbStart = Math.max(0, Math.min(maxThumbStart, desiredThumbStart));
const ratio = maxThumbStart === 0 ? 0 : desiredThumbStart / maxThumbStart;
const range = this._max - this._min;
const newValue = this._min + ratio * range;
this.value = newValue;
}
getThumbRect() {
const virtualThumbSize = this.getVirtualThumbSize();
const virtualThumbStart = this.getVirtualThumbStart();
const realThumbStart = Math.floor(virtualThumbStart / 2);
const realThumbSize = Math.ceil((virtualThumbStart + virtualThumbSize) / 2) - realThumbStart;
if (this.orientation === "vertical") {
return {
x: this.x,
y: this.y + realThumbStart,
width: this.width,
height: Math.max(1, realThumbSize)
};
} else {
return {
x: this.x + realThumbStart,
y: this.y,
width: Math.max(1, realThumbSize),
height: this.height
};
}
}
renderSelf(buffer) {
if (this.orientation === "horizontal") {
this.renderHorizontal(buffer);
} else {
this.renderVertical(buffer);
}
}
renderHorizontal(buffer) {
const virtualThumbSize = this.getVirtualThumbSize();
const virtualThumbStart = this.getVirtualThumbStart();
const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
const realStartCell = Math.floor(virtualThumbStart / 2);
const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
const startX = Math.max(0, realStartCell);
const endX = Math.min(this.width - 1, realEndCell);
for (let realX = startX;realX <= endX; realX++) {
const virtualCellStart = realX * 2;
const virtualCellEnd = virtualCellStart + 2;
const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
const coverage = thumbEndInCell - thumbStartInCell;
let char = " ";
if (coverage >= 2) {
char = "█";
} else {
const isLeftHalf = thumbStartInCell === virtualCellStart;
if (isLeftHalf) {
char = "▌";
} else {
char = "▐";
}
}
for (let y2 = 0;y2 < this.height; y2++) {
buffer.setCellWithAlphaBlending(this.x + realX, this.y + y2, char, this._foregroundColor, this._backgroundColor);
}
}
}
renderVertical(buffer) {
const virtualThumbSize = this.getVirtualThumbSize();
const virtualThumbStart = this.getVirtualThumbStart();
const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
const realStartCell = Math.floor(virtualThumbStart / 2);
const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
const startY = Math.max(0, realStartCell);
const endY = Math.min(this.height - 1, realEndCell);
for (let realY = startY;realY <= endY; realY++) {
const virtualCellStart = realY * 2;
const virtualCellEnd = virtualCellStart + 2;
const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
const coverage = thumbEndInCell - thumbStartInCell;
let char = " ";
if (coverage >= 2) {
char = "█";
} else if (coverage > 0) {
const virtualPositionInCell = thumbStartInCell - virtualCellStart;
if (virtualPositionInCell === 0) {
char = "▀";
} else {
char = "▄";
}
}
for (let x2 = 0;x2 < this.width; x2++) {
buffer.setCellWithAlphaBlending(this.x + x2, this.y + realY, char, this._foregroundColor, this._backgroundColor);
}
}
}
getVirtualThumbSize() {
const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
const range = this._max - this._min;
if (range === 0)
return virtualTrackSize;
const viewportSize = Math.max(1, this._viewPortSize);
const contentSize = range + viewportSize;
if (contentSize <= viewportSize)
return virtualTrackSize;
const thumbRatio = viewportSize / contentSize;
const calculatedSize = Math.floor(virtualTrackSize * thumbRatio);
return Math.max(1, Math.min(calculatedSize, virtualTrackSize));
}
getVirtualThumbStart() {
const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
const range = this._max - this._min;
if (range === 0)
return 0;
const valueRatio = (this._value - this._min) / range;
const virtualThumbSize = this.getVirtualThumbSize();
return Math.round(valueRatio * (virtualTrackSize - virtualThumbSize));
}
}
// src/renderables/ScrollBar.ts
class ScrollBarRenderable extends Renderable {
slider;
startArrow;
endArrow;
orientation;
_focusable = true;
_scrollSize = 0;
_scrollPosition = 0;
_viewportSize = 0;
_showArrows = false;
_manualVisibility = false;
_onChange;
scrollStep = null;
get visible() {
return super.visible;
}
set visible(value) {
this._manualVisibility = true;
super.visible = value;
}
resetVisibilityControl() {
this._manualVisibility = false;
this.recalculateVisibility();
}
get scrollSize() {
return this._scrollSize;
}
get scrollPosition() {
return this._scrollPosition;
}
get viewportSize() {
return this._viewportSize;
}
set scrollSize(value) {
if (value === this.scrollSize)
return;
this._scrollSize = value;
this.recalculateVisibility();
this.updateSliderFromScrollState();
this.scrollPosition = this.scrollPosition;
}
set scrollPosition(value) {
const newPosition = Math.round(Math.min(Math.max(0, value), this.scrollSize - this.viewportSize));
if (newPosition !== this._scrollPosition) {
this._scrollPosition = newPosition;
this.updateSliderFromScrollState();
}
}
set viewportSize(value) {
if (value === this.viewportSize)
return;
this._viewportSize = value;
this.slider.viewPortSize = Math.max(1, this._viewportSize);
this.recalculateVisibility();
this.updateSliderFromScrollState();
this.scrollPosition = this.scrollPosition;
}
get showArrows() {
return this._showArrows;
}
set showArrows(value) {
if (value === this._showArrows)
return;
this._showArrows = value;
this.startArrow.visible = value;
this.endArrow.visible = value;
}
constructor(ctx, { trackOptions, arrowOptions, orientation, showArrows = false, ...options }) {
super(ctx, {
flexDirection: orientation === "vertical" ? "column" : "row",
alignSelf: "stretch",
alignItems: "stretch",
...options
});
this._onChange = options.onChange;
this.orientation = orientation;
this._showArrows = showArrows;
const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
const defaultStepSize = Math.max(1, this._viewportSize);
const stepSize = trackOptions?.viewPortSize ?? defaultStepSize;
this.slider = new SliderRenderable(ctx, {
orientation,
min: 0,
max: scrollRange,
value: this._scrollPosition,
viewPortSize: stepSize,
onChange: (value) => {
this._scrollPosition = Math.round(value);
this._onChange?.(this._scrollPosition);
this.emit("change", { position: this._scrollPosition });
},
...orientation === "vertical" ? {
width: Math.max(1, Math.min(2, this.width)),
height: "100%",
marginLeft: "auto"
} : {
width: "100%",
height: 1,
marginTop: "auto"
},
flexGrow: 1,
flexShrink: 1,
...trackOptions
});
this.updateSliderFromScrollState();
const arrowOpts = arrowOptions ? {
foregroundColor: arrowOptions.backgroundColor,
backgroundColor: arrowOptions.backgroundColor,
attributes: arrowOptions.attributes,
...arrowOptions
} : {};
this.startArrow = new ArrowRenderable(ctx, {
alignSelf: "center",
visible: this.showArrows,
direction: this.orientation === "vertical" ? "up" : "left",
height: this.orientation === "vertical" ? 1 : 1,
...arrowOpts
});
this.endArrow = new ArrowRenderable(ctx, {
alignSelf: "center",
visible: this.showArrows,
direction: this.orientation === "vertical" ? "down" : "right",
height: this.orientation === "vertical" ? 1 : 1,
...arrowOpts
});
this.add(this.startArrow);
this.add(this.slider);
this.add(this.endArrow);
let startArrowMouseTimeout = undefined;
let endArrowMouseTimeout = undefined;
this.startArrow.onMouseDown = (event) => {
event.stopPropagation();
event.preventDefault();
this.scrollBy(-0.5, "viewport");
startArrowMouseTimeout = setTimeout(() => {
this.scrollBy(-0.5, "viewport");
startArrowMouseTimeout = setInterval(() => {
this.scrollBy(-0.2, "viewport");
}, 200);
}, 500);
};
this.startArrow.onMouseUp = (event) => {
event.stopPropagation();
clearInterval(startArrowMouseTimeout);
};
this.endArrow.onMouseDown = (event) => {
event.stopPropagation();
event.preventDefault();
this.scrollBy(0.5, "viewport");
endArrowMouseTimeout = setTimeout(() => {
this.scrollBy(0.5, "viewport");
endArrowMouseTimeout = setInterval(() => {
this.scrollBy(0.2, "viewport");
}, 200);
}, 500);
};
this.endArrow.onMouseUp = (event) => {
event.stopPropagation();
clearInterval(endArrowMouseTimeout);
};
}
set arrowOptions(options) {
Object.assign(this.startArrow, options);
Object.assign(this.endArrow, options);
this.requestRender();
}
set trackOptions(options) {
Object.assign(this.slider, options);
this.requestRender();
}
updateSliderFromScrollState() {
const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
this.slider.min = 0;
this.slider.max = scrollRange;
this.slider.value = Math.min(this._scrollPosition, scrollRange);
}
scrollBy(delta, unit = "absolute") {
const multiplier = unit === "viewport" ? this.viewportSize : unit === "content" ? this.scrollSize : unit === "step" ? this.scrollStep ?? 1 : 1;
const resolvedDelta = multiplier * delta;
this.scrollPosition += resolvedDelta;
}
recalculateVisibility() {
if (!this._manualVisibility) {
const sizeRatio = this.scrollSize <= this.viewportSize ? 1 : this.viewportSize / this.scrollSize;
super.visible = sizeRatio < 1;
}
}
handleKeyPress(key) {
switch (key.name) {
case "left":
case "h":
if (this.orientation !== "horizontal")
return false;
this.scrollBy(-1 / 5, "viewport");
return true;
case "right":
case "l":
if (this.orientation !== "horizontal")
return false;
this.scrollBy(1 / 5, "viewport");
return true;
case "up":
case "k":
if (this.orientation !== "vertical")
return false;
this.scrollBy(-1 / 5, "viewport");
return true;
case "down":
case "j":
if (this.orientation !== "vertical")
return false;
this.scrollBy(1 / 5, "viewport");
return true;
case "pageup":
this.scrollBy(-1 / 2, "viewport");
return true;
case "pagedown":
this.scrollBy(1 / 2, "viewport");
return true;
case "home":
this.scrollBy(-1, "content");
return true;
case "end":
this.scrollBy(1, "content");
return true;
}
return false;
}
}
class ArrowRenderable extends Renderable {
_direction;
_foregroundColor;
_backgroundColor;
_attributes;
_arrowChars;
constructor(ctx, options) {
super(ctx, options);
this._direction = options.direction;
this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : RGBA.fromValues(1, 1, 1, 1);
this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : RGBA.fromValues(0, 0, 0, 0);
this._attributes = options.attributes ?? 0;
this._arrowChars = {
up: "▲",
down: "▼",
left: "◀",
right: "▶",
...options.arrowChars
};
if (!options.width) {
this.width = stringWidth(this.getArrowChar());
}
}
get direction() {
return this._direction;
}
set direction(value) {
if (this._direction !== value) {
this._direction = value;
this.requestRender();
}
}
get foregroundColor() {
return this._foregroundColor;
}
set foregroundColor(value) {
if (this._foregroundColor !== value) {
this._foregroundColor = parseColor(value);
this.requestRender();
}
}
get backgroundColor() {
return this._backgroundColor;
}
set backgroundColor(value) {
if (this._backgroundColor !== value) {
this._backgroundColor = parseColor(value);
this.requestRender();
}
}
get attributes() {
return this._attributes;
}
set attributes(value) {
if (this._attributes !== value) {
this._attributes = value;
this.requestRender();
}
}
set arrowChars(value) {
this._arrowChars = {
...this._arrowChars,
...value
};
this.requestRender();
}
renderSelf(buffer) {
const char = this.getArrowChar();
buffer.drawText(char, this.x, this.y, this._foregroundColor, this._backgroundColor, this._attributes);
}
getArrowChar() {
switch (this._direction) {
case "up":
return this._arrowChars.up;
case "down":
return this._arrowChars.down;
case "left":
return this._arrowChars.left;
case "right":
return this._arrowChars.right;
default:
return "?";
}
}
}
// src/renderables/ScrollBox.ts
class ContentRenderable extends BoxRenderable {
viewport;
_viewportCulling;
constructor(ctx, viewport, viewportCulling, options) {
super(ctx, options);
this.viewport = viewport;
this._viewportCulling = viewportCulling;
}
get viewportCulling() {
return this._viewportCulling;
}
set viewportCulling(value) {
this._viewportCulling = value;
}
_hasVisibleChildFilter() {
return this._viewportCulling;
}
_getVisibleChildren() {
if (this._viewportCulling) {
return getObjectsInViewport({
x: this.viewport.screenX,
y: this.viewport.screenY,
width: this.viewport.width,
height: this.viewport.height
}, this.getChildrenSortedByPrimaryAxis(), this.primaryAxis, 0).map((child) => child.num);
}
return super._getVisibleChildren();
}
}
var SCROLLBOX_PADDING_KEYS = [
"padding",
"paddingX",
"paddingY",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft"
];
function pickScrollBoxPadding(options) {
if (!options)
return {};
const picked = {};
for (const key of SCROLLBOX_PADDING_KEYS) {
const value = options[key];
if (value !== undefined) {
picked[key] = value;
}
}
return picked;
}
function stripScrollBoxPadding(options) {
const sanitized = { ...options };
for (const key of SCROLLBOX_PADDING_KEYS) {
delete sanitized[key];
}
return sanitized;
}
class ScrollBoxRenderable extends BoxRenderable {
static idCounter = 0;
internalId = 0;
wrapper;
viewport;
content;
horizontalScrollBar;
verticalScrollBar;
_focusable = true;
selectionListener;
autoScrollMouseX = 0;
autoScrollMouseY = 0;
autoScrollThresholdVertical = 3;
autoScrollThresholdHorizontal = 3;
autoScrollSpeedSlow = 6;
autoScrollSpeedMedium = 36;
autoScrollSpeedFast = 72;
isAutoScrolling = false;
cachedAutoScrollSpeed = 3;
autoScrollAccumulatorX = 0;
autoScrollAccumulatorY = 0;
scrollAccumulatorX = 0;
scrollAccumulatorY = 0;
_stickyScroll;
_stickyScrollTop = false;
_stickyScrollBottom = false;
_stickyScrollLeft = false;
_stickyScrollRight = false;
_stickyStart;
_hasManualScroll = false;
_isApplyingStickyScroll = false;
scrollAccel;
get stickyScroll() {
return this._stickyScroll;
}
set stickyScroll(value) {
this._stickyScroll = value;
this.updateStickyState();
}
get stickyStart() {
return this._stickyStart;
}
set stickyStart(value) {
this._stickyStart = value;
this.updateStickyState();
}
get scrollTop() {
return this.verticalScrollBar.scrollPosition;
}
set scrollTop(value) {
this.verticalScrollBar.scrollPosition = value;
this.updateStickyState();
}
get scrollLeft() {
return this.horizontalScrollBar.scrollPosition;
}
set scrollLeft(value) {
this.horizontalScrollBar.scrollPosition = value;
this.updateStickyState();
}
get scrollWidth() {
return this.horizontalScrollBar.scrollSize;
}
get scrollHeight() {
return this.verticalScrollBar.scrollSize;
}
updateStickyState() {
if (!this._stickyScroll) {
this.syncManualScrollState();
return;
}
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
if (this.scrollTop <= 0) {
this._stickyScrollTop = true;
this._stickyScrollBottom = false;
} else if (this.scrollTop >= maxScrollTop) {
this._stickyScrollTop = false;
this._stickyScrollBottom = true;
} else {
this._stickyScrollTop = false;
this._stickyScrollBottom = false;
}
if (this.scrollLeft <= 0) {
this._stickyScrollLeft = true;
this._stickyScrollRight = false;
} else if (this.scrollLeft >= maxScrollLeft) {
this._stickyScrollLeft = false;
this._stickyScrollRight = true;
} else {
this._stickyScrollLeft = false;
this._stickyScrollRight = false;
}
this.syncManualScrollState();
}
syncManualScrollState() {
if (!this._stickyScroll) {
this._hasManualScroll = false;
return;
}
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
const hasScrollableContent = maxScrollTop > 1 || maxScrollLeft > 1;
if (this._isApplyingStickyScroll) {
if (this._hasManualScroll && hasScrollableContent && this.isAtStickyPosition()) {
this._hasManualScroll = false;
}
return;
}
this._hasManualScroll = hasScrollableContent && !this.isAtStickyPosition();
}
applyStickyStart(stickyStart) {
const wasApplyingStickyScroll = this._isApplyingStickyScroll;
this._isApplyingStickyScroll = true;
try {
switch (stickyStart) {
case "top":
this._stickyScrollTop = true;
this._stickyScrollBottom = false;
this.verticalScrollBar.scrollPosition = 0;
break;
case "bottom":
this._stickyScrollTop = false;
this._stickyScrollBottom = true;
this.verticalScrollBar.scrollPosition = Math.max(0, this.scrollHeight - this.viewport.height);
break;
case "left":
this._stickyScrollLeft = true;
this._stickyScrollRight = false;
this.horizontalScrollBar.scrollPosition = 0;
break;
case "right":
this._stickyScrollLeft = false;
this._stickyScrollRight = true;
this.horizontalScrollBar.scrollPosition = Math.max(0, this.scrollWidth - this.viewport.width);
break;
}
} finally {
this._isApplyingStickyScroll = wasApplyingStickyScroll;
}
}
constructor(ctx, options) {
const {
wrapperOptions,
viewportOptions,
contentOptions,
rootOptions,
scrollbarOptions,
verticalScrollbarOptions,
horizontalScrollbarOptions,
stickyScroll = false,
stickyStart,
scrollX = false,
scrollY = true,
scrollAcceleration,
viewportCulling = true,
...rootBoxOptions
} = options;
const forwardedContentPadding = {
...pickScrollBoxPadding(rootBoxOptions),
...pickScrollBoxPadding(rootOptions)
};
const sanitizedRootBoxOptions = stripScrollBoxPadding(rootBoxOptions);
const sanitizedRootOptions = rootOptions ? stripScrollBoxPadding(rootOptions) : undefined;
const mergedContentOptions = {
...forwardedContentPadding,
...contentOptions
};
super(ctx, {
flexDirection: "row",
alignItems: "stretch",
...sanitizedRootBoxOptions,
...sanitizedRootOptions
});
this.internalId = ScrollBoxRenderable.idCounter++;
this._stickyScroll = stickyScroll;
this._stickyStart = stickyStart;
this.scrollAccel = scrollAcceleration ?? new LinearScrollAccel;
this.wrapper = new BoxRenderable(ctx, {
flexDirection: "column",
flexGrow: 1,
...wrapperOptions,
id: `scroll-box-wrapper-${this.internalId}`
});
super.add(this.wrapper);
this.viewport = new BoxRenderable(ctx, {
flexDirection: "column",
flexGrow: 1,
overflow: "hidden",
onSizeChange: () => {
this.recalculateBarProps();
},
...viewportOptions,
id: `scroll-box-viewport-${this.internalId}`
});
this.wrapper.add(this.viewport);
this.content = new ContentRenderable(ctx, this.viewport, viewportCulling, {
alignSelf: "flex-start",
flexShrink: 0,
...scrollX ? { minWidth: "100%" } : { minWidth: "100%", maxWidth: "100%" },
...scrollY ? { minHeight: "100%" } : { minHeight: "100%", maxHeight: "100%" },
onSizeChange: () => {
this.recalculateBarProps();
},
...mergedContentOptions,
id: `scroll-box-content-${this.internalId}`
});
this.viewport.add(this.content);
this.verticalScrollBar = new ScrollBarRenderable(ctx, {
...scrollbarOptions,
...verticalScrollbarOptions,
arrowOptions: {
...scrollbarOptions?.arrowOptions,
...verticalScrollbarOptions?.arrowOptions
},
id: `scroll-box-vertical-scrollbar-${this.internalId}`,
orientation: "vertical",
onChange: (position) => {
this.content.translateY = -position;
this.updateStickyState();
}
});
super.add(this.verticalScrollBar);
this.horizontalScrollBar = new ScrollBarRenderable(ctx, {
...scrollbarOptions,
...horizontalScrollbarOptions,
arrowOptions: {
...scrollbarOptions?.arrowOptions,
...horizontalScrollbarOptions?.arrowOptions
},
id: `scroll-box-horizontal-scrollbar-${this.internalId}`,
orientation: "horizontal",
onChange: (position) => {
this.content.translateX = -position;
this.updateStickyState();
}
});
this.wrapper.add(this.horizontalScrollBar);
this.recalculateBarProps();
if (stickyStart && stickyScroll) {
this.applyStickyStart(stickyStart);
}
this.selectionListener = () => {
const selection = this._ctx.getSelection();
if (!selection || !selection.isDragging) {
this.stopAutoScroll();
}
};
this._ctx.on("selection", this.selectionListener);
}
onUpdate(deltaTime) {
this.handleAutoScroll(deltaTime);
}
scrollBy(delta, unit = "absolute") {
if (typeof delta === "number") {
this.verticalScrollBar.scrollBy(delta, unit);
} else {
this.verticalScrollBar.scrollBy(delta.y, unit);
this.horizontalScrollBar.scrollBy(delta.x, unit);
}
}
scrollChildIntoView(childId) {
const child = this.content.findDescendantById(childId);
if (!child)
return;
const getNearestDelta = (elementStart, elementEnd, viewportStart, viewportEnd) => {
const elementSize = elementEnd - elementStart;
const viewportSize = viewportEnd - viewportStart;
const elementStartOutside = elementStart < viewportStart;
const elementEndOutside = elementEnd > viewportEnd;
if (elementStartOutside && elementEndOutside) {
return 0;
}
if (elementStartOutside && elementSize < viewportSize || elementEndOutside && elementSize > viewportSize) {
return elementStart - viewportStart;
}
if (elementStartOutside && elementSize > viewportSize || elementEndOutside && elementSize < viewportSize) {
return elementEnd - viewportEnd;
}
return 0;
};
const childTop = child.y;
const childBottom = child.y + child.height;
const viewportTop = this.viewport.y;
const viewportBottom = this.viewport.y + this.viewport.height;
const dy = getNearestDelta(childTop, childBottom, viewportTop, viewportBottom);
const childLeft = child.x;
const childRight = child.x + child.width;
const viewportLeft = this.viewport.x;
const viewportRight = this.viewport.x + this.viewport.width;
const dx = getNearestDelta(childLeft, childRight, viewportLeft, viewportRight);
if (dx !== 0 || dy !== 0) {
this.scrollBy({ x: dx, y: dy });
}
}
scrollTo(position) {
if (typeof position === "number") {
this.scrollTop = position;
} else {
this.scrollTop = position.y;
this.scrollLeft = position.x;
}
}
isAtStickyPosition() {
if (!this._stickyScroll || !this._stickyStart) {
return false;
}
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
switch (this._stickyStart) {
case "top":
return this.scrollTop === 0;
case "bottom":
return this.scrollTop >= maxScrollTop;
case "left":
return this.scrollLeft === 0;
case "right":
return this.scrollLeft >= maxScrollLeft;
default:
return false;
}
}
isAtStickyReengagePoint(stickyStart, maxScrollTop, maxScrollLeft) {
switch (stickyStart) {
case "top":
return maxScrollTop > 0 && this.scrollTop <= 0;
case "bottom":
return maxScrollTop > 0 && this.scrollTop >= maxScrollTop - 1;
case "left":
return maxScrollLeft > 0 && this.scrollLeft <= 0;
case "right":
return maxScrollLeft > 0 && this.scrollLeft >= maxScrollLeft - 1;
}
}
add(obj, index) {
return this.content.add(obj, index);
}
insertBefore(obj, anchor) {
return this.content.insertBefore(obj, anchor);
}
remove(child) {
if (child.parent === this) {
super.remove(child);
return;
}
this.content.remove(child);
}
getChildren() {
return this.content.getChildren();
}
getRenderable(id) {
return this.content.getRenderable(id);
}
onMouseEvent(event) {
if (event.type === "scroll") {
let dir = event.scroll?.direction;
if (event.modifiers.shift)
dir = dir === "up" ? "left" : dir === "down" ? "right" : dir === "right" ? "down" : "up";
const baseDelta = event.scroll?.delta ?? 0;
const now = Date.now();
const multiplier = this.scrollAccel.tick(now);
const scrollAmount = baseDelta * multiplier;
if (dir === "up") {
this.scrollAccumulatorY -= scrollAmount;
const integerScroll = Math.trunc(this.scrollAccumulatorY);
if (integerScroll !== 0) {
this.scrollTop += integerScroll;
this.scrollAccumulatorY -= integerScroll;
}
} else if (dir === "down") {
this.scrollAccumulatorY += scrollAmount;
const integerScroll = Math.trunc(this.scrollAccumulatorY);
if (integerScroll !== 0) {
this.scrollTop += integerScroll;
this.scrollAccumulatorY -= integerScroll;
}
} else if (dir === "left") {
this.scrollAccumulatorX -= scrollAmount;
const integerScroll = Math.trunc(this.scrollAccumulatorX);
if (integerScroll !== 0) {
this.scrollLeft += integerScroll;
this.scrollAccumulatorX -= integerScroll;
}
} else if (dir === "right") {
this.scrollAccumulatorX += scrollAmount;
const integerScroll = Math.trunc(this.scrollAccumulatorX);
if (integerScroll !== 0) {
this.scrollLeft += integerScroll;
this.scrollAccumulatorX -= integerScroll;
}
}
this.syncManualScrollState();
}
if (event.type === "drag" && event.isDragging) {
this.updateAutoScroll(event.x, event.y);
} else if (event.type === "up") {
this.stopAutoScroll();
}
}
handleKeyPress(key) {
if (this.verticalScrollBar.handleKeyPress(key)) {
this.scrollAccel.reset();
this.resetScrollAccumulators();
this.syncManualScrollState();
return true;
}
if (this.horizontalScrollBar.handleKeyPress(key)) {
this.scrollAccel.reset();
this.resetScrollAccumulators();
this.syncManualScrollState();
return true;
}
return false;
}
resetScrollAccumulators() {
this.scrollAccumulatorX = 0;
this.scrollAccumulatorY = 0;
}
startAutoScroll(mouseX, mouseY) {
this.stopAutoScroll();
this.autoScrollMouseX = mouseX;
this.autoScrollMouseY = mouseY;
this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
this.isAutoScrolling = true;
if (!this.live) {
this.live = true;
}
}
updateAutoScroll(mouseX, mouseY) {
this.autoScrollMouseX = mouseX;
this.autoScrollMouseY = mouseY;
this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
const scrollX = this.getAutoScrollDirectionX(mouseX);
const scrollY = this.getAutoScrollDirectionY(mouseY);
if (scrollX === 0 && scrollY === 0) {
this.stopAutoScroll();
} else if (!this.isAutoScrolling) {
this.startAutoScroll(mouseX, mouseY);
}
}
stopAutoScroll() {
const wasAutoScrolling = this.isAutoScrolling;
this.isAutoScrolling = false;
this.autoScrollAccumulatorX = 0;
this.autoScrollAccumulatorY = 0;
if (wasAutoScrolling && !this.hasOtherLiveReasons()) {
this.live = false;
}
}
hasOtherLiveReasons() {
return false;
}
handleAutoScroll(deltaTime) {
if (!this.isAutoScrolling)
return;
const scrollX = this.getAutoScrollDirectionX(this.autoScrollMouseX);
const scrollY = this.getAutoScrollDirectionY(this.autoScrollMouseY);
const scrollAmount = this.cachedAutoScrollSpeed * (deltaTime / 1000);
let scrolled = false;
if (scrollX !== 0) {
this.autoScrollAccumulatorX += scrollX * scrollAmount;
const integerScrollX = Math.trunc(this.autoScrollAccumulatorX);
if (integerScrollX !== 0) {
this.scrollLeft += integerScrollX;
this.autoScrollAccumulatorX -= integerScrollX;
scrolled = true;
}
}
if (scrollY !== 0) {
this.autoScrollAccumulatorY += scrollY * scrollAmount;
const integerScrollY = Math.trunc(this.autoScrollAccumulatorY);
if (integerScrollY !== 0) {
this.scrollTop += integerScrollY;
this.autoScrollAccumulatorY -= integerScrollY;
scrolled = true;
}
}
if (scrolled) {
this._ctx.requestSelectionUpdate();
}
if (scrollX === 0 && scrollY === 0) {
this.stopAutoScroll();
}
}
getAutoScrollDirectionX(mouseX) {
const relativeX = mouseX - this.x;
const distToLeft = relativeX;
const distToRight = this.width - relativeX;
if (distToLeft <= this.autoScrollThresholdHorizontal) {
return this.scrollLeft > 0 ? -1 : 0;
} else if (distToRight <= this.autoScrollThresholdHorizontal) {
const maxScrollLeft = this.scrollWidth - this.viewport.width;
return this.scrollLeft < maxScrollLeft ? 1 : 0;
}
return 0;
}
getAutoScrollDirectionY(mouseY) {
const relativeY = mouseY - this.y;
const distToTop = relativeY;
const distToBottom = this.height - relativeY;
if (distToTop <= this.autoScrollThresholdVertical) {
return this.scrollTop > 0 ? -1 : 0;
} else if (distToBottom <= this.autoScrollThresholdVertical) {
const maxScrollTop = this.scrollHeight - this.viewport.height;
return this.scrollTop < maxScrollTop ? 1 : 0;
}
return 0;
}
getAutoScrollSpeed(mouseX, mouseY) {
const relativeX = mouseX - this.x;
const relativeY = mouseY - this.y;
const distToLeft = relativeX;
const distToRight = this.width - relativeX;
const distToTop = relativeY;
const distToBottom = this.height - relativeY;
const minDistance = Math.min(distToLeft, distToRight, distToTop, distToBottom);
if (minDistance <= 1) {
return this.autoScrollSpeedFast;
} else if (minDistance <= 2) {
return this.autoScrollSpeedMedium;
} else {
return this.autoScrollSpeedSlow;
}
}
recalculateBarProps() {
const wasApplyingStickyScroll = this._isApplyingStickyScroll;
this._isApplyingStickyScroll = true;
try {
this.verticalScrollBar.scrollSize = this.content.height;
this.verticalScrollBar.viewportSize = this.viewport.height;
this.horizontalScrollBar.scrollSize = this.content.width;
this.horizontalScrollBar.viewportSize = this.viewport.width;
if (this._stickyScroll) {
const newMaxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
const newMaxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
const stickyStart = this._stickyStart;
if (stickyStart && !this._hasManualScroll) {
this.applyStickyStart(stickyStart);
} else if (stickyStart && this._hasManualScroll && this.isAtStickyReengagePoint(stickyStart, newMaxScrollTop, newMaxScrollLeft)) {
this._hasManualScroll = false;
this.applyStickyStart(stickyStart);
} else if (!this._hasManualScroll) {
if (this._stickyScrollTop) {
this.scrollTop = 0;
} else if (this._stickyScrollBottom && newMaxScrollTop > 0) {
this.scrollTop = newMaxScrollTop;
}
if (this._stickyScrollLeft) {
this.scrollLeft = 0;
} else if (this._stickyScrollRight && newMaxScrollLeft > 0) {
this.scrollLeft = newMaxScrollLeft;
}
}
}
} finally {
this._isApplyingStickyScroll = wasApplyingStickyScroll;
}
process.nextTick(() => {
this.requestRender();
});
}
set padding(value) {
this.content.padding = value;
this.requestRender();
}
set paddingX(value) {
this.content.paddingX = value;
this.requestRender();
}
set paddingY(value) {
this.content.paddingY = value;
this.requestRender();
}
set paddingTop(value) {
this.content.paddingTop = value;
this.requestRender();
}
set paddingRight(value) {
this.content.paddingRight = value;
this.requestRender();
}
set paddingBottom(value) {
this.content.paddingBottom = value;
this.requestRender();
}
set paddingLeft(value) {
this.content.paddingLeft = value;
this.requestRender();
}
set rootOptions(options) {
Object.assign(this, options);
this.requestRender();
}
set wrapperOptions(options) {
Object.assign(this.wrapper, options);
this.requestRender();
}
set viewportOptions(options) {
Object.assign(this.viewport, options);
this.requestRender();
}
set contentOptions(options) {
Object.assign(this.content, options);
this.requestRender();
}
set scrollbarOptions(options) {
Object.assign(this.verticalScrollBar, options);
Object.assign(this.horizontalScrollBar, options);
this.requestRender();
}
set verticalScrollbarOptions(options) {
Object.assign(this.verticalScrollBar, options);
this.requestRender();
}
set horizontalScrollbarOptions(options) {
Object.assign(this.horizontalScrollBar, options);
this.requestRender();
}
get scrollAcceleration() {
return this.scrollAccel;
}
set scrollAcceleration(value) {
this.scrollAccel = value;
}
get viewportCulling() {
return this.content.viewportCulling;
}
set viewportCulling(value) {
this.content.viewportCulling = value;
this.requestRender();
}
destroySelf() {
if (this.selectionListener) {
this._ctx.off("selection", this.selectionListener);
this.selectionListener = undefined;
}
super.destroySelf();
}
}
// src/renderables/Select.ts
var defaultSelectKeybindings = [
{ name: "up", action: "move-up" },
{ name: "k", action: "move-up" },
{ name: "down", action: "move-down" },
{ name: "j", action: "move-down" },
{ name: "up", shift: true, action: "move-up-fast" },
{ name: "down", shift: true, action: "move-down-fast" },
{ name: "return", action: "select-current" },
{ name: "linefeed", action: "select-current" }
];
var SelectRenderableEvents;
((SelectRenderableEvents2) => {
SelectRenderableEvents2["SELECTION_CHANGED"] = "selectionChanged";
SelectRenderableEvents2["ITEM_SELECTED"] = "itemSelected";
})(SelectRenderableEvents ||= {});
class SelectRenderable extends Renderable {
_focusable = true;
_options = [];
_selectedIndex = 0;
scrollOffset = 0;
maxVisibleItems;
_backgroundColor;
_textColor;
_focusedBackgroundColor;
_focusedTextColor;
_selectedBackgroundColor;
_selectedTextColor;
_descriptionColor;
_selectedDescriptionColor;
_showScrollIndicator;
_wrapSelection;
_showDescription;
_showSelectionIndicator;
_font;
_itemSpacing;
linesPerItem;
fontHeight;
_fastScrollStep;
_keyBindingsMap;
_keyAliasMap;
_keyBindings;
_defaultOptions = {
backgroundColor: "transparent",
textColor: "#FFFFFF",
focusedBackgroundColor: "#1a1a1a",
focusedTextColor: "#FFFFFF",
selectedBackgroundColor: "#334455",
selectedTextColor: "#FFFF00",
selectedIndex: 0,
descriptionColor: "#888888",
selectedDescriptionColor: "#CCCCCC",
showScrollIndicator: false,
wrapSelection: false,
showDescription: true,
showSelectionIndicator: true,
itemSpacing: 0,
fastScrollStep: 5
};
constructor(ctx, options) {
super(ctx, { ...options, buffered: true });
this._options = options.options || [];
const requestedIndex = options.selectedIndex ?? this._defaultOptions.selectedIndex;
this._selectedIndex = this._options.length > 0 ? Math.min(requestedIndex, this._options.length - 1) : 0;
this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor);
this._textColor = parseColor(options.textColor || this._defaultOptions.textColor);
this._focusedBackgroundColor = parseColor(options.focusedBackgroundColor || this._defaultOptions.focusedBackgroundColor);
this._focusedTextColor = parseColor(options.focusedTextColor || this._defaultOptions.focusedTextColor);
this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator;
this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection;
this._showDescription = options.showDescription ?? this._defaultOptions.showDescription;
this._showSelectionIndicator = options.showSelectionIndicator ?? this._defaultOptions.showSelectionIndicator;
this._font = options.font;
this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing;
this.fontHeight = this._font ? measureText({ text: "A", font: this._font }).height : 1;
this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
this.linesPerItem += this._itemSpacing;
this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
this._selectedBackgroundColor = parseColor(options.selectedBackgroundColor || this._defaultOptions.selectedBackgroundColor);
this._selectedTextColor = parseColor(options.selectedTextColor || this._defaultOptions.selectedTextColor);
this._descriptionColor = parseColor(options.descriptionColor || this._defaultOptions.descriptionColor);
this._selectedDescriptionColor = parseColor(options.selectedDescriptionColor || this._defaultOptions.selectedDescriptionColor);
this._fastScrollStep = options.fastScrollStep || this._defaultOptions.fastScrollStep;
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {});
this._keyBindings = options.keyBindings || [];
const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
this.updateScrollOffset();
this.requestRender();
}
renderSelf(buffer, deltaTime) {
if (!this.visible || !this.frameBuffer)
return;
if (this.isDirty) {
this.refreshFrameBuffer();
}
}
refreshFrameBuffer() {
if (!this.frameBuffer)
return;
const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor;
this.frameBuffer.clear(bgColor);
if (this._options.length === 0)
return;
const contentX = 0;
const contentY = 0;
const contentWidth = this.width;
const contentHeight = this.height;
const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleItems);
for (let i = 0;i < visibleOptions.length; i++) {
const actualIndex = this.scrollOffset + i;
const option = visibleOptions[i];
const isSelected = actualIndex === this._selectedIndex;
const itemY = contentY + i * this.linesPerItem;
if (itemY + this.linesPerItem - 1 >= contentY + contentHeight)
break;
if (isSelected) {
const contentHeight2 = this.linesPerItem - this._itemSpacing;
this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight2, this._selectedBackgroundColor);
}
const indicator = this._showSelectionIndicator ? isSelected ? "▶ " : " " : "";
const indicatorWidth = this._showSelectionIndicator ? 2 : 0;
const nameContent = `${indicator}${option.name}`;
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
const nameColor = isSelected ? this._selectedTextColor : baseTextColor;
const textX = contentX + 1 + indicatorWidth;
if (this._font) {
if (indicator) {
this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor);
}
renderFontToFrameBuffer(this.frameBuffer, {
text: option.name,
x: textX,
y: itemY,
color: nameColor,
backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,
font: this._font
});
} else {
this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor);
}
if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {
const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor;
this.frameBuffer.drawText(option.description, textX, itemY + this.fontHeight, descColor);
}
}
if (this._showScrollIndicator && this._options.length > this.maxVisibleItems) {
this.renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight);
}
}
renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight) {
if (!this.frameBuffer)
return;
const maxScrollOffset = this._options.length - this.maxVisibleItems;
const scrollPercent = this.scrollOffset / maxScrollOffset;
const indicatorHeight = Math.max(1, contentHeight - 2);
const indicatorY = contentY + 1 + Math.floor(scrollPercent * indicatorHeight);
const indicatorX = contentX + contentWidth - 1;
this.frameBuffer.drawText("█", indicatorX, indicatorY, parseColor("#666666"));
}
get options() {
return this._options;
}
set options(options) {
this._options = options;
this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, options.length - 1));
this.updateScrollOffset();
this.requestRender();
}
getSelectedOption() {
return this._options[this._selectedIndex] || null;
}
getSelectedIndex() {
return this._selectedIndex;
}
moveUp(steps = 1) {
const newIndex = this._selectedIndex - steps;
if (newIndex >= 0) {
this._selectedIndex = newIndex;
} else if (this._wrapSelection && this._options.length > 0) {
this._selectedIndex = this._options.length - 1;
} else {
this._selectedIndex = 0;
}
this.updateScrollOffset();
this.requestRender();
this.emit("selectionChanged" /* SELECTION_CHANGED */, this._selectedIndex, this.getSelectedOption());
}
moveDown(steps = 1) {
const newIndex = this._selectedIndex + steps;
if (newIndex < this._options.length) {
this._selectedIndex = newIndex;
} else if (this._wrapSelection && this._options.length > 0) {
this._selectedIndex = 0;
} else {
this._selectedIndex = this._options.length - 1;
}
this.updateScrollOffset();
this.requestRender();
this.emit("selectionChanged" /* SELECTION_CHANGED */, this._selectedIndex, this.getSelectedOption());
}
selectCurrent() {
const selected = this.getSelectedOption();
if (selected) {
this.emit("itemSelected" /* ITEM_SELECTED */, this._selectedIndex, selected);
}
}
setSelectedIndex(index) {
if (index >= 0 && index < this._options.length) {
this._selectedIndex = index;
this.updateScrollOffset();
this.requestRender();
this.emit("selectionChanged" /* SELECTION_CHANGED */, this._selectedIndex, this.getSelectedOption());
}
}
updateScrollOffset() {
if (!this._options)
return;
const halfVisible = Math.floor(this.maxVisibleItems / 2);
const newScrollOffset = Math.max(0, Math.min(this._selectedIndex - halfVisible, this._options.length - this.maxVisibleItems));
if (newScrollOffset !== this.scrollOffset) {
this.scrollOffset = newScrollOffset;
this.requestRender();
}
}
onResize(width, height) {
this.maxVisibleItems = Math.max(1, Math.floor(height / this.linesPerItem));
this.updateScrollOffset();
this.requestRender();
}
handleKeyPress(key) {
const action = getKeyBindingAction(this._keyBindingsMap, key);
if (action) {
switch (action) {
case "move-up":
this.moveUp(1);
return true;
case "move-down":
this.moveDown(1);
return true;
case "move-up-fast":
this.moveUp(this._fastScrollStep);
return true;
case "move-down-fast":
this.moveDown(this._fastScrollStep);
return true;
case "select-current":
this.selectCurrent();
return true;
}
}
return false;
}
get showScrollIndicator() {
return this._showScrollIndicator;
}
set showScrollIndicator(show) {
this._showScrollIndicator = show;
this.requestRender();
}
get showDescription() {
return this._showDescription;
}
set showDescription(show) {
if (this._showDescription !== show) {
this._showDescription = show;
this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
this.linesPerItem += this._itemSpacing;
this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
this.updateScrollOffset();
this.requestRender();
}
}
get showSelectionIndicator() {
return this._showSelectionIndicator;
}
set showSelectionIndicator(show) {
const next = show ?? this._defaultOptions.showSelectionIndicator;
if (this._showSelectionIndicator !== next) {
this._showSelectionIndicator = next;
this.requestRender();
}
}
get wrapSelection() {
return this._wrapSelection;
}
set wrapSelection(wrap) {
this._wrapSelection = wrap;
}
set backgroundColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.backgroundColor);
if (this._backgroundColor !== newColor) {
this._backgroundColor = newColor;
this.requestRender();
}
}
set textColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.textColor);
if (this._textColor !== newColor) {
this._textColor = newColor;
this.requestRender();
}
}
set focusedBackgroundColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.focusedBackgroundColor);
if (this._focusedBackgroundColor !== newColor) {
this._focusedBackgroundColor = newColor;
this.requestRender();
}
}
set focusedTextColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.focusedTextColor);
if (this._focusedTextColor !== newColor) {
this._focusedTextColor = newColor;
this.requestRender();
}
}
set selectedBackgroundColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.selectedBackgroundColor);
if (this._selectedBackgroundColor !== newColor) {
this._selectedBackgroundColor = newColor;
this.requestRender();
}
}
set selectedTextColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.selectedTextColor);
if (this._selectedTextColor !== newColor) {
this._selectedTextColor = newColor;
this.requestRender();
}
}
set descriptionColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.descriptionColor);
if (this._descriptionColor !== newColor) {
this._descriptionColor = newColor;
this.requestRender();
}
}
set selectedDescriptionColor(value) {
const newColor = parseColor(value ?? this._defaultOptions.selectedDescriptionColor);
if (this._selectedDescriptionColor !== newColor) {
this._selectedDescriptionColor = newColor;
this.requestRender();
}
}
set font(font) {
this._font = font;
this.fontHeight = measureText({ text: "A", font: this._font }).height;
this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
this.linesPerItem += this._itemSpacing;
this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
this.updateScrollOffset();
this.requestRender();
}
set itemSpacing(spacing) {
this._itemSpacing = spacing;
this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
this.linesPerItem += this._itemSpacing;
this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
this.updateScrollOffset();
this.requestRender();
}
set fastScrollStep(step) {
this._fastScrollStep = step;
}
set keyBindings(bindings) {
this._keyBindings = bindings;
const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, bindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
}
set keyAliasMap(aliases) {
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, aliases);
const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
}
set selectedIndex(value) {
const newIndex = value ?? this._defaultOptions.selectedIndex;
const clampedIndex = this._options.length > 0 ? Math.min(Math.max(0, newIndex), this._options.length - 1) : 0;
if (this._selectedIndex !== clampedIndex) {
this._selectedIndex = clampedIndex;
this.updateScrollOffset();
this.requestRender();
}
}
}
// src/renderables/TabSelect.ts
var defaultTabSelectKeybindings = [
{ name: "left", action: "move-left" },
{ name: "[", action: "move-left" },
{ name: "right", action: "move-right" },
{ name: "]", action: "move-right" },
{ name: "return", action: "select-current" },
{ name: "linefeed", action: "select-current" }
];
var TabSelectRenderableEvents;
((TabSelectRenderableEvents2) => {
TabSelectRenderableEvents2["SELECTION_CHANGED"] = "selectionChanged";
TabSelectRenderableEvents2["ITEM_SELECTED"] = "itemSelected";
})(TabSelectRenderableEvents ||= {});
function calculateDynamicHeight(showUnderline, showDescription) {
let height = 1;
if (showUnderline) {
height += 1;
}
if (showDescription) {
height += 1;
}
return height;
}
class TabSelectRenderable extends Renderable {
_focusable = true;
_options = [];
selectedIndex = 0;
scrollOffset = 0;
_tabWidth;
maxVisibleTabs;
_backgroundColor;
_textColor;
_focusedBackgroundColor;
_focusedTextColor;
_selectedBackgroundColor;
_selectedTextColor;
_selectedDescriptionColor;
_showScrollArrows;
_showDescription;
_showUnderline;
_wrapSelection;
_keyBindingsMap;
_keyAliasMap;
_keyBindings;
constructor(ctx, options) {
const calculatedHeight = calculateDynamicHeight(options.showUnderline ?? true, options.showDescription ?? true);
super(ctx, { ...options, height: calculatedHeight, buffered: true });
this._backgroundColor = parseColor(options.backgroundColor || "transparent");
this._textColor = parseColor(options.textColor || "#FFFFFF");
this._focusedBackgroundColor = parseColor(options.focusedBackgroundColor || options.backgroundColor || "#1a1a1a");
this._focusedTextColor = parseColor(options.focusedTextColor || options.textColor || "#FFFFFF");
this._options = options.options || [];
this._tabWidth = options.tabWidth || 20;
this._showDescription = options.showDescription ?? true;
this._showUnderline = options.showUnderline ?? true;
this._showScrollArrows = options.showScrollArrows ?? true;
this._wrapSelection = options.wrapSelection ?? false;
this.maxVisibleTabs = Math.max(1, Math.floor(this.width / this._tabWidth));
this._selectedBackgroundColor = parseColor(options.selectedBackgroundColor || "#334455");
this._selectedTextColor = parseColor(options.selectedTextColor || "#FFFF00");
this._selectedDescriptionColor = parseColor(options.selectedDescriptionColor || "#CCCCCC");
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {});
this._keyBindings = options.keyBindings || [];
const mergedBindings = mergeKeyBindings(defaultTabSelectKeybindings, this._keyBindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
}
calculateDynamicHeight() {
return calculateDynamicHeight(this._showUnderline, this._showDescription);
}
renderSelf(buffer, deltaTime) {
if (!this.visible || !this.frameBuffer)
return;
if (this.isDirty) {
this.refreshFrameBuffer();
}
}
refreshFrameBuffer() {
if (!this.frameBuffer)
return;
const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor;
this.frameBuffer.clear(bgColor);
if (this._options.length === 0)
return;
const contentX = 0;
const contentY = 0;
const contentWidth = this.width;
const contentHeight = this.height;
const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleTabs);
for (let i = 0;i < visibleOptions.length; i++) {
const actualIndex = this.scrollOffset + i;
const option = visibleOptions[i];
const isSelected = actualIndex === this.selectedIndex;
const tabX = contentX + i * this._tabWidth;
if (tabX >= contentX + contentWidth)
break;
const actualTabWidth = Math.min(this._tabWidth, contentWidth - i * this._tabWidth);
if (isSelected) {
this.frameBuffer.fillRect(tabX, contentY, actualTabWidth, 1, this._selectedBackgroundColor);
}
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
const nameColor = isSelected ? this._selectedTextColor : baseTextColor;
const nameContent = this.truncateText(option.name, actualTabWidth - 2);
this.frameBuffer.drawText(nameContent, tabX + 1, contentY, nameColor);
if (isSelected && this._showUnderline && contentHeight >= 2) {
const underlineY = contentY + 1;
const underlineBg = isSelected ? this._selectedBackgroundColor : bgColor;
this.frameBuffer.drawText("▬".repeat(actualTabWidth), tabX, underlineY, nameColor, underlineBg);
}
}
if (this._showDescription && contentHeight >= (this._showUnderline ? 3 : 2)) {
const selectedOption = this.getSelectedOption();
if (selectedOption) {
const descriptionY = contentY + (this._showUnderline ? 2 : 1);
const descColor = this._selectedDescriptionColor;
const descContent = this.truncateText(selectedOption.description, contentWidth - 2);
this.frameBuffer.drawText(descContent, contentX + 1, descriptionY, descColor);
}
}
if (this._showScrollArrows && this._options.length > this.maxVisibleTabs) {
this.renderScrollArrowsToFrameBuffer(contentX, contentY, contentWidth, contentHeight);
}
}
truncateText(text, maxWidth) {
if (text.length <= maxWidth)
return text;
return text.substring(0, Math.max(0, maxWidth - 1)) + "…";
}
renderScrollArrowsToFrameBuffer(contentX, contentY, contentWidth, contentHeight) {
if (!this.frameBuffer)
return;
const hasMoreLeft = this.scrollOffset > 0;
const hasMoreRight = this.scrollOffset + this.maxVisibleTabs < this._options.length;
if (hasMoreLeft) {
this.frameBuffer.drawText("‹", contentX, contentY, parseColor("#AAAAAA"));
}
if (hasMoreRight) {
this.frameBuffer.drawText("›", contentX + contentWidth - 1, contentY, parseColor("#AAAAAA"));
}
}
setOptions(options) {
this._options = options;
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, options.length - 1));
this.updateScrollOffset();
this.requestRender();
}
getSelectedOption() {
return this._options[this.selectedIndex] || null;
}
getSelectedIndex() {
return this.selectedIndex;
}
moveLeft() {
if (this.selectedIndex > 0) {
this.selectedIndex--;
} else if (this._wrapSelection && this._options.length > 0) {
this.selectedIndex = this._options.length - 1;
} else {
return;
}
this.updateScrollOffset();
this.requestRender();
this.emit("selectionChanged" /* SELECTION_CHANGED */, this.selectedIndex, this.getSelectedOption());
}
moveRight() {
if (this.selectedIndex < this._options.length - 1) {
this.selectedIndex++;
} else if (this._wrapSelection && this._options.length > 0) {
this.selectedIndex = 0;
} else {
return;
}
this.updateScrollOffset();
this.requestRender();
this.emit("selectionChanged" /* SELECTION_CHANGED */, this.selectedIndex, this.getSelectedOption());
}
selectCurrent() {
const selected = this.getSelectedOption();
if (selected) {
this.emit("itemSelected" /* ITEM_SELECTED */, this.selectedIndex, selected);
}
}
setSelectedIndex(index) {
if (index >= 0 && index < this._options.length) {
this.selectedIndex = index;
this.updateScrollOffset();
this.requestRender();
this.emit("selectionChanged" /* SELECTION_CHANGED */, this.selectedIndex, this.getSelectedOption());
}
}
updateScrollOffset() {
const halfVisible = Math.floor(this.maxVisibleTabs / 2);
const newScrollOffset = Math.max(0, Math.min(this.selectedIndex - halfVisible, this._options.length - this.maxVisibleTabs));
if (newScrollOffset !== this.scrollOffset) {
this.scrollOffset = newScrollOffset;
this.requestRender();
}
}
onResize(width, height) {
this.maxVisibleTabs = Math.max(1, Math.floor(width / this._tabWidth));
this.updateScrollOffset();
this.requestRender();
}
setTabWidth(tabWidth) {
if (this._tabWidth === tabWidth)
return;
this._tabWidth = tabWidth;
this.maxVisibleTabs = Math.max(1, Math.floor(this.width / this._tabWidth));
this.updateScrollOffset();
this.requestRender();
}
getTabWidth() {
return this._tabWidth;
}
handleKeyPress(key) {
const action = getKeyBindingAction(this._keyBindingsMap, key);
if (action) {
switch (action) {
case "move-left":
this.moveLeft();
return true;
case "move-right":
this.moveRight();
return true;
case "select-current":
this.selectCurrent();
return true;
}
}
return false;
}
get options() {
return this._options;
}
set options(options) {
this._options = options;
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, options.length - 1));
this.updateScrollOffset();
this.requestRender();
}
set backgroundColor(color) {
this._backgroundColor = parseColor(color);
this.requestRender();
}
set textColor(color) {
this._textColor = parseColor(color);
this.requestRender();
}
set focusedBackgroundColor(color) {
this._focusedBackgroundColor = parseColor(color);
this.requestRender();
}
set focusedTextColor(color) {
this._focusedTextColor = parseColor(color);
this.requestRender();
}
set selectedBackgroundColor(color) {
this._selectedBackgroundColor = parseColor(color);
this.requestRender();
}
set selectedTextColor(color) {
this._selectedTextColor = parseColor(color);
this.requestRender();
}
set selectedDescriptionColor(color) {
this._selectedDescriptionColor = parseColor(color);
this.requestRender();
}
get showDescription() {
return this._showDescription;
}
set showDescription(show) {
if (this._showDescription !== show) {
this._showDescription = show;
const newHeight = this.calculateDynamicHeight();
this.height = newHeight;
this.requestRender();
}
}
get showUnderline() {
return this._showUnderline;
}
set showUnderline(show) {
if (this._showUnderline !== show) {
this._showUnderline = show;
const newHeight = this.calculateDynamicHeight();
this.height = newHeight;
this.requestRender();
}
}
get showScrollArrows() {
return this._showScrollArrows;
}
set showScrollArrows(show) {
if (this._showScrollArrows !== show) {
this._showScrollArrows = show;
this.requestRender();
}
}
get wrapSelection() {
return this._wrapSelection;
}
set wrapSelection(wrap) {
this._wrapSelection = wrap;
}
get tabWidth() {
return this._tabWidth;
}
set tabWidth(tabWidth) {
if (this._tabWidth === tabWidth)
return;
this._tabWidth = tabWidth;
this.maxVisibleTabs = Math.max(1, Math.floor(this.width / this._tabWidth));
this.updateScrollOffset();
this.requestRender();
}
set keyBindings(bindings) {
this._keyBindings = bindings;
const mergedBindings = mergeKeyBindings(defaultTabSelectKeybindings, bindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
}
set keyAliasMap(aliases) {
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, aliases);
const mergedBindings = mergeKeyBindings(defaultTabSelectKeybindings, this._keyBindings);
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
}
}
// src/renderables/TimeToFirstDraw.ts
var graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
function measureCellWidth(buffer, text) {
const encoded = buffer.encodeUnicode(text);
if (!encoded)
return 0;
try {
return encoded.data.reduce((width, glyph) => width + glyph.width, 0);
} finally {
buffer.freeUnicode(encoded);
}
}
function truncateToCellWidth(buffer, text, maxWidth) {
let visibleText = "";
let visibleWidth = 0;
for (const { segment } of graphemeSegmenter.segment(text)) {
const segmentWidth = measureCellWidth(buffer, segment);
if (visibleWidth + segmentWidth > maxWidth)
break;
visibleText += segment;
visibleWidth += segmentWidth;
}
return { text: visibleText, width: visibleWidth };
}
class TimeToFirstDrawRenderable extends Renderable {
_runtimeMs = null;
textColor;
label;
precision;
constructor(ctx, options = {}) {
super(ctx, {
width: "100%",
height: 1,
flexShrink: 0,
alignSelf: "center",
...options
});
this.textColor = parseColor(options.fg ?? "#AAAAAA");
this.label = options.label ?? "Time to first draw";
this.precision = this.normalizePrecision(options.precision ?? 2);
}
get runtimeMs() {
return this._runtimeMs;
}
set fg(value) {
this.textColor = parseColor(value);
this.requestRender();
}
set color(value) {
this.fg = value;
}
set textLabel(value) {
if (value === this.label) {
return;
}
this.label = value;
this.requestRender();
}
set decimals(value) {
const nextPrecision = this.normalizePrecision(value);
if (nextPrecision === this.precision) {
return;
}
this.precision = nextPrecision;
this.requestRender();
}
reset() {
this._runtimeMs = null;
this.requestRender();
}
renderSelf(buffer) {
if (this._runtimeMs === null) {
this._runtimeMs = performance.now();
}
const content = `${this.label}: ${this._runtimeMs.toFixed(this.precision)}ms`;
const maxWidth = Math.max(this.width, 1);
const visibleContent = truncateToCellWidth(buffer, content, maxWidth);
const centeredX = this.x + Math.max(0, Math.floor((maxWidth - visibleContent.width) / 2));
buffer.drawText(visibleContent.text, centeredX, this.y, this.textColor);
}
normalizePrecision(value) {
if (!Number.isFinite(value)) {
return 2;
}
return Math.max(0, Math.floor(value));
}
}
export {
yellow,
wrapWithDelegates,
white,
vstyles,
visualizeRenderableTree,
underline,
treeSitterToTextChunks,
treeSitterToStyledText,
terminalNamedSingleStrokeKeys,
t,
stripAnsiSequences,
stringToStyledText,
strikethrough,
setupAudio,
setRenderLibPath,
rgbToHex,
reverse,
resolveRenderLib,
resolveImageRenderProtocol,
resolveCoreSlot,
resolveBundledFilePath,
renderFontToFrameBuffer,
registerEnvVar,
registerCorePlugin,
red,
pathToFiletype,
parseWrap,
parseUnit,
parsePositionType,
parseOverflow,
parseMeasureMode,
parseLogLevel,
parseKeypress,
parseJustify,
parseGutter,
parseFlexDirection,
parseEdge,
parseDisplay,
parseDirection,
parseDimension,
parseColor,
parseBoxSizing,
parseBorderStyle,
parseAlignItems,
parseAlign,
normalizeTerminalPalette,
normalizeIndexedColorIndex,
normalizeColorValue,
nonAlphanumericKeys,
measureText,
maybeMakeRenderable,
magenta,
link,
italic,
isValidBorderStyle,
isVNode,
isTextNodeRenderable,
isStyledText,
isRenderable,
isEditBufferRenderable,
instantiate,
infoStringToFiletype,
imageInfo,
hsvToRgb,
hexToRgb,
hastToStyledText,
h,
green,
getTreeSitterClient,
getLinkId,
getDataPaths,
getCharacterPositions,
getBorderSides,
getBorderFromSides,
getBaseAttributes,
generateEnvMarkdown,
generateEnvColored,
fonts,
fg,
extensionToFiletype,
extToFiletype,
envRegistry,
env,
engine,
dim,
detectLinks,
destroyTreeSitterClient,
delegate,
defaultTextareaKeyBindings,
decodePasteBytes,
cyan,
createTimeline,
createTextAttributes,
createTerminalPalette,
createSlotRegistry,
createRendererClipboardAdapter,
createMarkdownCodeBlockRenderer,
createIcyStreamDemuxer,
createHostClipboard,
createExtmarksController,
createCoreSlotRegistry,
createClipboard,
createCliRenderer,
coordinateToCharacterIndex,
convertThemeToStyles,
convertGlobalToLocalSelection,
clearEnvCache,
capture,
buildTerminalPaletteSignature,
buildKittyKeyboardFlags,
brightYellow,
brightWhite,
brightRed,
brightMagenta,
brightGreen,
brightCyan,
brightBlue,
brightBlack,
borderCharsToArray,
bold,
blue,
blink,
black,
bgYellow,
bgWhite,
bgRed,
bgMagenta,
bgGreen,
bgCyan,
bgBlue,
bgBlack,
bg,
basenameToFiletype,
attributesWithLink,
applyScanlines,
applySaturation,
applyNoise,
applyInvert,
applyGain,
applyChromaticAberration,
applyBrightness,
applyAsciiArt,
ansi256IndexToRgb,
addDefaultParsers,
exports_yoga as Yoga,
VignetteEffect,
VRenderable,
TreeSitterClient,
Timeline,
TimeToFirstDrawRenderable,
TextareaRenderable,
TextTableRenderable,
TextRenderable,
TextNodeRenderable,
TextBufferView,
TextBufferRenderable,
TextBuffer,
TextAttributes,
Text,
TerminalPalette,
TerminalConsole,
TargetChannel,
TabSelectRenderableEvents,
TabSelectRenderable,
TabSelect,
TRITANOPIA_SIM_MATRIX,
TRITANOPIA_COMP_MATRIX,
TECHNICOLOR_MATRIX,
SystemClock,
SyntaxStyle,
StyledText,
StdinParser,
SlotRenderable,
SlotRegistry,
SliderRenderable,
Selection,
SelectRenderableEvents,
SelectRenderable,
Select,
ScrollBoxRenderable,
ScrollBox,
ScrollBarRenderable,
SYNTHWAVE_MATRIX,
SOLARIZATION_MATRIX,
SEPIA_MATRIX,
RootTextNodeRenderable,
RootRenderable,
RendererControlState,
RenderableEvents,
Renderable,
RainbowTextEffect,
RGBA,
PasteEvent,
PROTANOPIA_SIM_MATRIX,
PROTANOPIA_COMP_MATRIX,
OptimizedBuffer,
NativeSpanFeed,
NativeMeasureTargetKind,
NativeImage,
NativeClipboardStartStatus,
NativeClipboardShutdownStatus,
NativeClipboardOperationStatus,
NativeClipboardDestroyStatus,
NativeClipboardCopyStatus,
NativeClipboardCancelStatus,
NativeAudioStreamState2 as NativeAudioStreamState,
NativeAudioStreamFormat2 as NativeAudioStreamFormat,
NativeAudioStreamCloseReason2 as NativeAudioStreamCloseReason,
MouseParser,
MouseEvent,
MouseButton,
MarkdownRenderable,
MacOSScrollAccel,
LogLevel,
LinearScrollAccel,
LineNumberRenderable,
LayoutEvents,
KeyHandler,
KeyEvent,
InternalKeyHandler,
InputRenderableEvents,
InputRenderable,
Input,
ImageRenderable,
ImageLoadError,
ImageError,
INVERT_MATRIX,
Generic,
GREENSCALE_MATRIX,
GRAYSCALE_MATRIX,
FrameBufferRenderable,
FrameBuffer,
FlamesEffect,
ExtmarksController,
EmbeddedTerminalRenderable,
EditorView,
EditBufferRenderableEvents,
EditBufferRenderable,
EditBuffer,
DistortionEffect,
DiffRenderable,
DebugOverlayCorner,
DataPathsManager,
DEUTERANOPIA_SIM_MATRIX,
DEUTERANOPIA_COMP_MATRIX,
DEFAULT_FOREGROUND_RGB,
DEFAULT_BACKGROUND_RGB,
ConsolePosition,
CodeRenderable,
Code,
CloudsEffect,
ClipboardTarget,
Clipboard,
CliRenderer,
CliRenderEvents,
CRTRollingBarEffect,
BoxRenderable,
Box,
BorderChars,
BorderCharArrays,
BloomEffect,
BaseRenderable,
AudioStreamError,
AudioStream,
AudioRecorderError,
AudioRecorder,
AudioInitializationError,
AudioCaptureStreamError,
AudioCaptureStream,
Audio,
ArrowRenderable,
ATTRIBUTE_BASE_MASK,
ATTRIBUTE_BASE_BITS,
ASCIIFontSelectionHelper,
ASCIIFontRenderable,
ASCIIFont,
ACHROMATOPSIA_MATRIX
};
//# debugId=9F5430F160FB010564756E2164756E21
//# sourceMappingURL=index.node.js.map