CellBuffer
class exported by @mwillbanks/tuil-cell.
class
Public class exported by @mwillbanks/tuil-cell.
export class CellBuffer {
readonly width: number;
readonly height: number;
readonly #cells: Cell[];
#cursor?: CursorState;
constructor(width: number, height: number, fill: Cell = emptyCell) {
if (
!Number.isSafeInteger(width) ||
!Number.isSafeInteger(height) ||
width < 1 ||
height < 1
) {
throw new Error("Cell buffer dimensions must be positive integers");
}
this.width = width;
this.height = height;
this.#cells = Array.from({ length: width * height }, () => cloneCell(fill));
}
get(x: number, y: number): Cell | undefined {
if (!this.#contains(x, y)) return undefined;
return this.#cells[y * this.width + x];
}
set(x: number, y: number, cell: Cell, clip?: TerminalBounds): void {
if (!this.#contains(x, y) || (clip && !inRect(x, y, clip))) return;
if (!cell.continuation) this.#clearWideCellAt(x, y);
this.#setRaw(x, y, cell);
}
write(
x: number,
y: number,
value: string,
style: Partial<Omit<Cell, "grapheme" | "continuation">> = {},
clip: TerminalBounds = {
x: 0,
y: 0,
width: this.width,
height: this.height,
},
): number {
let cursor = x;
const safeValue = escapeTerminalControlCharacters(value);
for (const grapheme of graphemeSegmenter.segment(safeValue)) {
const width = stringWidth(grapheme.segment);
if (width === 0) {
this.#appendZeroWidth(cursor, y, grapheme.segment, clip);
continue;
}
if (cursor + width > clip.x + clip.width || cursor + width > this.width)
break;
this.#writeGrapheme(cursor, y, grapheme.segment, width, style, clip);
cursor += width;
}
return cursor;
}
fill(rect: TerminalBounds, cell: Cell, clip?: TerminalBounds): void {
for (let y = rect.y; y < rect.y + rect.height; y += 1) {
for (let x = rect.x; x < rect.x + rect.width; x += 1)
this.set(x, y, cell, clip);
}
}
clear(cell: Cell = emptyCell): void {
this.#cells.fill(cloneCell(cell));
this.#cursor = undefined;
}
erase(rect: TerminalBounds): void {
this.fill(rect, emptyCell);
}
border(
rect: TerminalBounds,
style: Partial<Omit<Cell, "grapheme" | "continuation">> = {},
glyphs = {
top: "─",
bottom: "─",
left: "│",
right: "│",
topLeft: "┌",
topRight: "┐",
bottomLeft: "└",
bottomRight: "┘",
},
clip: TerminalBounds = rect,
): void {
if (rect.width < 2 || rect.height < 2) return;
this.write(
rect.x,
rect.y,
glyphs.topLeft + glyphs.top.repeat(rect.width - 2) + glyphs.topRight,
style,
clip,
);
this.write(
rect.x,
rect.y + rect.height - 1,
glyphs.bottomLeft +
glyphs.bottom.repeat(rect.width - 2) +
glyphs.bottomRight,
style,
clip,
);
for (let y = rect.y + 1; y < rect.y + rect.height - 1; y += 1) {
this.write(rect.x, y, glyphs.left, style, clip);
this.write(rect.x + rect.width - 1, y, glyphs.right, style, clip);
}
}
composite(
source: CellFrame,
xOffset = 0,
yOffset = 0,
clip?: TerminalBounds,
): void {
const actualClip = clip ?? {
x: 0,
y: 0,
width: this.width,
height: this.height,
};
for (let y = 0; y < source.height; y += 1) {
for (let x = 0; x < source.width; x += 1) {
const cell = source.cells[y * source.width + x];
if (cell) this.set(x + xOffset, y + yOffset, cell, actualClip);
}
}
}
setCursor(cursor: CursorState | undefined): void {
validateRendererCursor(cursor, this.width, this.height);
this.#cursor = cursor ? Object.freeze({ ...cursor }) : undefined;
}
frame(): CellFrame {
return Object.freeze({
width: this.width,
height: this.height,
cells: Object.freeze(this.#cells.map(cloneCell)),
cursor: this.#cursor,
});
}
#contains(x: number, y: number): boolean {
return (
Number.isSafeInteger(x) &&
Number.isSafeInteger(y) &&
x >= 0 &&
y >= 0 &&
x < this.width &&
y < this.height
);
}
#setRaw(x: number, y: number, cell: Cell): void {
this.#cells[y * this.width + x] = cloneCell(cell);
}
#appendZeroWidth(
cursor: number,
y: number,
grapheme: string,
clip: TerminalBounds,
): void {
const previous = this.get(cursor - 1, y);
if (!previous) return;
this.set(
cursor - 1,
y,
{ ...previous, grapheme: previous.grapheme + grapheme },
clip,
);
}
#writeGrapheme(
x: number,
y: number,
grapheme: string,
width: number,
style: Partial<Omit<Cell, "grapheme" | "continuation">>,
clip: TerminalBounds,
): void {
const cell: Cell = {
grapheme,
foreground: style.foreground ?? defaultColor,
background: style.background ?? defaultColor,
attributes: style.attributes ?? {},
link: style.link,
};
this.set(x, y, cell, clip);
for (let offset = 1; offset < width; offset += 1) {
const continuationX = x + offset;
if (inRect(continuationX, y, clip)) {
this.#setRaw(continuationX, y, {
...cell,
grapheme: "",
continuation: true,
});
}
}
}
#clearWideCellAt(x: number, y: number): void {
const start = this.#wideCellStart(x, y);
const leading = this.get(start, y);
if (start !== x || (leading && stringWidth(leading.grapheme) > 1)) {
this.#setRaw(start, y, emptyCell);
}
this.#clearContinuations(start, y);
}
#wideCellStart(x: number, y: number): number {
let start = x;
while (start > 0 && this.get(start, y)?.continuation) start -= 1;
return start;
}
#clearContinuations(start: number, y: number): void {
for (
let column = start + 1;
column < this.width && this.get(column, y)?.continuation;
column += 1
) {
this.#setRaw(column, y, emptyCell);
}
}
}Members
| Member | Type | Required | Description | Related types |
|---|---|---|---|---|
width | number | Yes | The width member uses the number contract. | — |
height | number | Yes | The height member uses the number contract. | — |
#cells | Cell[] | Yes | The #cells member uses the Cell[] contract. | Cell |
#cursor | CursorState | undefined | No | The #cursor member uses the CursorState | undefined contract. | CursorState |
__constructor | any | Yes | The __constructor member uses the any contract. | — |
get | (x: number, y: number) => Cell | undefined | Yes | The get member uses the (x: number, y: number) => Cell | undefined contract. | Cell |
set | (x: number, y: number, cell: Cell, clip?: TerminalBounds) => void | Yes | The set member uses the (x: number, y: number, cell: Cell, clip?: TerminalBounds) => void contract. | Cell, TerminalBounds |
write | (x: number, y: number, value: string, style?: Partial<Omit<Cell, "grapheme" | "continuation">>, clip?: TerminalBounds) => number | Yes | The write member uses the (x: number, y: number, value: string, style?: Partial<Omit<Cell, "grapheme" | "continuation">>, clip?: TerminalBounds) => number contract. | Cell, TerminalBounds |
fill | (rect: TerminalBounds, cell: Cell, clip?: TerminalBounds) => void | Yes | The fill member uses the (rect: TerminalBounds, cell: Cell, clip?: TerminalBounds) => void contract. | Cell, TerminalBounds |
clear | (cell?: Cell) => void | Yes | The clear member uses the (cell?: Cell) => void contract. | Cell |
erase | (rect: TerminalBounds) => void | Yes | The erase member uses the (rect: TerminalBounds) => void contract. | TerminalBounds |
border | (rect: TerminalBounds, style?: Partial<Omit<Cell, "grapheme" | "continuation">>, glyphs?: { top: string; bottom: string; left: string; right: string; topLeft: string; topRight: string; bottomLeft: string; bottomRight: string; }, clip?: TerminalBounds) => void | Yes | The border member uses the (rect: TerminalBounds, style?: Partial<Omit<Cell, "grapheme" | "continuation">>, glyphs?: { top: string; bottom: string; left: string; right: string; topLeft: string; topRight: string; bottomLeft: string; bottomRight: string; }, clip?: TerminalBounds) => void contract. | Cell, TerminalBounds |
composite | (source: CellFrame, xOffset?: number, yOffset?: number, clip?: TerminalBounds) => void | Yes | The composite member uses the (source: CellFrame, xOffset?: number, yOffset?: number, clip?: TerminalBounds) => void contract. | CellFrame, TerminalBounds |
setCursor | (cursor: CursorState | undefined) => void | Yes | The setCursor member uses the (cursor: CursorState | undefined) => void contract. | CursorState |
frame | () => CellFrame | Yes | The frame member uses the () => CellFrame contract. | CellFrame |
#contains | (x: number, y: number) => boolean | Yes | The #contains member uses the (x: number, y: number) => boolean contract. | — |
#setRaw | (x: number, y: number, cell: Cell) => void | Yes | The #setRaw member uses the (x: number, y: number, cell: Cell) => void contract. | Cell |
#appendZeroWidth | (cursor: number, y: number, grapheme: string, clip: TerminalBounds) => void | Yes | The #appendZeroWidth member uses the (cursor: number, y: number, grapheme: string, clip: TerminalBounds) => void contract. | TerminalBounds |
#writeGrapheme | (x: number, y: number, grapheme: string, width: number, style: Partial<Omit<Cell, "grapheme" | "continuation">>, clip: TerminalBounds) => void | Yes | The #writeGrapheme member uses the (x: number, y: number, grapheme: string, width: number, style: Partial<Omit<Cell, "grapheme" | "continuation">>, clip: TerminalBounds) => void contract. | Cell, TerminalBounds |
#clearWideCellAt | (x: number, y: number) => void | Yes | The #clearWideCellAt member uses the (x: number, y: number) => void contract. | — |
#wideCellStart | (x: number, y: number) => number | Yes | The #wideCellStart member uses the (x: number, y: number) => number contract. | — |
#clearContinuations | (start: number, y: number) => void | Yes | The #clearContinuations member uses the (start: number, y: number) => void contract. | — |
Parameters
This declaration has no public members.
Returns
This declaration does not return a value.
Throws
No thrown errors are documented for this declaration.
Related types
Source
View the secondary source reference