singularity-forge/packages/tui/src/utils.ts
Mikael Hugo 6725a55591 feat(web): add error boundaries, expand test coverage, add README
- Add class-based ErrorBoundary component wrapping all 7 main views
  inside WorkspaceChrome; fallback shows view name, error, reload button
- Add 30 new unit tests (boot null-project path × 9, onboarding
  pure-function logic × 21); all 43 web/lib tests pass
- Add web/README.md: architecture, auth flow, 7 views, dev setup,
  API route pattern, test instructions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-10 11:24:40 +02:00

158 lines
4.4 KiB
TypeScript

import {
EllipsisKind,
extractSegments as nativeExtractSegments,
sliceWithWidth as nativeSliceWithWidth,
truncateToWidth as nativeTruncateToWidth,
visibleWidth as nativeVisibleWidth,
wrapTextWithAnsi as nativeWrapTextWithAnsi,
} from "@singularity-forge/native/text";
// Grapheme segmenter (shared instance)
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
/**
* Get the shared grapheme segmenter instance.
*/
export function getSegmenter(): Intl.Segmenter {
return segmenter;
}
const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
/**
* Check if a character is whitespace.
*/
export function isWhitespaceChar(char: string): boolean {
return /\s/.test(char);
}
/**
* Check if a character is punctuation.
*/
export function isPunctuationChar(char: string): boolean {
return PUNCTUATION_REGEX.test(char);
}
// ---------------------------------------------------------------------------
// Native text module wrappers
// ---------------------------------------------------------------------------
/**
* Calculate the visible width of a string in terminal columns.
* Delegates to the native Rust implementation.
*/
export function visibleWidth(str: string): number {
return nativeVisibleWidth(str);
}
/**
* Wrap text with ANSI codes preserved.
* Delegates to the native Rust implementation.
*
* @param text - Text to wrap (may contain ANSI codes and newlines)
* @param width - Maximum visible width per line
* @returns Array of wrapped lines (NOT padded to width)
*/
export function wrapTextWithAnsi(text: string, width: number): string[] {
return nativeWrapTextWithAnsi(text, width);
}
/**
* Map an ellipsis string to the native EllipsisKind enum value.
*/
function ellipsisStringToKind(ellipsis: string): number {
if (ellipsis === "\u2026") return EllipsisKind.Unicode;
if (ellipsis === "..." || ellipsis === undefined) return EllipsisKind.Ascii;
if (ellipsis === "") return EllipsisKind.None;
// Default: "..." maps to Ascii
return EllipsisKind.Ascii;
}
/**
* Truncate text to fit within a maximum visible width, adding ellipsis if needed.
* Optionally pad with spaces to reach exactly maxWidth.
* Delegates to the native Rust implementation.
*
* @param text - Text to truncate (may contain ANSI codes)
* @param maxWidth - Maximum visible width
* @param ellipsis - Ellipsis string to append when truncating (default: "...")
* @param pad - If true, pad result with spaces to exactly maxWidth (default: false)
* @returns Truncated text, optionally padded to exactly maxWidth
*/
export function truncateToWidth(
text: string,
maxWidth: number,
ellipsis: string = "...",
pad: boolean = false,
): string {
return nativeTruncateToWidth(
text,
maxWidth,
ellipsisStringToKind(ellipsis),
pad,
);
}
/**
* Extract a range of visible columns from a line. Handles ANSI codes and wide chars.
* @param strict - If true, exclude wide chars at boundary that would extend past the range
*/
export function sliceByColumn(
line: string,
startCol: number,
length: number,
strict = false,
): string {
return sliceWithWidth(line, startCol, length, strict).text;
}
/** Like sliceByColumn but also returns the actual visible width of the result. */
export function sliceWithWidth(
line: string,
startCol: number,
length: number,
strict = false,
): { text: string; width: number } {
return nativeSliceWithWidth(line, startCol, length, strict);
}
/**
* Extract "before" and "after" segments from a line in a single pass.
* Delegates to the native Rust implementation.
*/
export function extractSegments(
line: string,
beforeEnd: number,
afterStart: number,
afterLen: number,
strictAfter = false,
): { before: string; beforeWidth: number; after: string; afterWidth: number } {
return nativeExtractSegments(
line,
beforeEnd,
afterStart,
afterLen,
strictAfter,
);
}
/**
* Apply background color to a line, padding to full width.
*
* @param line - Line of text (may contain ANSI codes)
* @param width - Total width to pad to
* @param bgFn - Background color function
* @returns Line with background applied and padded to width
*/
export function applyBackgroundToLine(
line: string,
width: number,
bgFn: (text: string) => string,
): string {
const visibleLen = visibleWidth(line);
const paddingNeeded = Math.max(0, width - visibleLen);
const padding = " ".repeat(paddingNeeded);
const withPadding = line + padding;
return bgFn(withPadding);
}