tuil
ReferencePackages@mwillbanks/tuil-cellAPI

CellBuffer

class exported by @mwillbanks/tuil-cell.

View rawEdit

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

MemberTypeRequiredDescriptionRelated types
widthnumberYesThe width member uses the number contract.
heightnumberYesThe height member uses the number contract.
#cellsCell[]YesThe #cells member uses the Cell[] contract.Cell
#cursorCursorState | undefinedNoThe #cursor member uses the CursorState | undefined contract.CursorState
__constructoranyYesThe __constructor member uses the any contract.
get(x: number, y: number) => Cell | undefinedYesThe get member uses the (x: number, y: number) => Cell | undefined contract.Cell
set(x: number, y: number, cell: Cell, clip?: TerminalBounds) => voidYesThe 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) => numberYesThe 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) => voidYesThe fill member uses the (rect: TerminalBounds, cell: Cell, clip?: TerminalBounds) => void contract.Cell, TerminalBounds
clear(cell?: Cell) => voidYesThe clear member uses the (cell?: Cell) => void contract.Cell
erase(rect: TerminalBounds) => voidYesThe erase member uses the (rect: TerminalBounds) => void contract.TerminalBounds
border(rect: TerminalBounds, style?: Partial<Omit<Cell, "grapheme" | "continuation">>, glyphs?: &#123; top: string; bottom: string; left: string; right: string; topLeft: string; topRight: string; bottomLeft: string; bottomRight: string; &#125;, clip?: TerminalBounds) => voidYesThe border member uses the (rect: TerminalBounds, style?: Partial<Omit<Cell, "grapheme" | "continuation">>, glyphs?: &#123; top: string; bottom: string; left: string; right: string; topLeft: string; topRight: string; bottomLeft: string; bottomRight: string; &#125;, clip?: TerminalBounds) => void contract.Cell, TerminalBounds
composite(source: CellFrame, xOffset?: number, yOffset?: number, clip?: TerminalBounds) => voidYesThe composite member uses the (source: CellFrame, xOffset?: number, yOffset?: number, clip?: TerminalBounds) => void contract.CellFrame, TerminalBounds
setCursor(cursor: CursorState | undefined) => voidYesThe setCursor member uses the (cursor: CursorState | undefined) => void contract.CursorState
frame() => CellFrameYesThe frame member uses the () => CellFrame contract.CellFrame
#contains(x: number, y: number) => booleanYesThe #contains member uses the (x: number, y: number) => boolean contract.
#setRaw(x: number, y: number, cell: Cell) => voidYesThe #setRaw member uses the (x: number, y: number, cell: Cell) => void contract.Cell
#appendZeroWidth(cursor: number, y: number, grapheme: string, clip: TerminalBounds) => voidYesThe #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) => voidYesThe #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) => voidYesThe #clearWideCellAt member uses the (x: number, y: number) => void contract.
#wideCellStart(x: number, y: number) => numberYesThe #wideCellStart member uses the (x: number, y: number) => number contract.
#clearContinuations(start: number, y: number) => voidYesThe #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.

Source

View the secondary source reference

Package

@mwillbanks/tuil-cell

On this page