/** * Terminal cell metrics, wrapping, markdown, and input folding. * * Isolated so the launch session picker can clip labels without loading * the interactive TUI class. */ import { type ColorDepth } from './color-depth.js'; /** * Codex-style compact elapsed: `0s`, `1m 05s`, `1h 01m 01s`. * Used by the workspace wait card while the model has not streamed yet. */ export declare function fmtElapsedCompact(elapsedSecs: number): string; /** * Sweep highlight across `text` (Codex `shimmer.rs`). Truecolor blends a * highlight band; otherwise DIM / default / BOLD. Process-start based so * every paint of the same frame stays in phase. */ export declare function shimmerText(text: string, nowMs: number, color: boolean): string; /** * Codex `extract_first_bold`: the first **closed** `**bold**` in the thinking * stream, else the first markdown heading. An unclosed `**` means the title * has not arrived yet, so return undefined and keep the default header — * never fall back to hard-truncated reasoning, reply, or prompt text. */ export declare function waitSummaryFromReasoning(text: string): string | undefined; /** Wait-card header + optional detail. Header tracks model work when known. */ export declare function waitCardCopy(input: { toolTitle?: string; toolSummary?: string; reasoning?: string; }): { header: string; detail?: string; }; /** * Codex `wrapped_details_lines`: word-wrap the wait-card detail under the * ` └ ` prefix, continue wrapped rows at the prefix width, cap at 3 rows and * end the last one with an ellipsis when the text does not fit. */ export declare function wrapWaitDetails(detail: string, width: number, maxLines?: number): string[]; /** Forget the cached decision, so the next read re-consults the environment. */ export declare function resetAsciiChrome(): void; /** * Whether chrome is being drawn in ASCII, for renderers that pick the glyph run * themselves. * * `pinEmojiCells` translates them anyway; asking is for the case where the * *shape* differs rather than just the character — a metre whose filled cell is * `#` reads as `##..`, not as `#..#`, so the renderer wants to know before it * counts and pads. */ export declare function asciiChromeEnabled(): boolean; /** * Rewrite one string's chrome into ASCII when the terminal cannot decode UTF-8. * * Only the glyphs in {@link ASCII_CHROME} move; CJK text is left byte for byte, * because there is no ASCII for it and a Chinese locale on a non-UTF-8 console * needs `/language en`, not a mangled translation. Widths are preserved for the * two-cell marks, so a row measured either side of this call occupies the same * number of cells. * @param text - one row, possibly with ANSI sequences in it. * @returns the row unchanged when UTF-8 chrome is safe. */ export declare function mapAsciiChrome(text: string): string; /** * Resolve the ambiguous-glyph policy for this process. * * Cheap enough to call once per line (it compares one string) and far too * expensive to call per character, which is the distinction that matters. * @param env - the environment to read. * @param onTerminal - whether the output is a terminal at all. * @returns whether the glyphs are two cells wide, and whether the second cell is * ours to reserve. */ export declare function ambiguousPolicy(env?: NodeJS.ProcessEnv, onTerminal?: boolean): { wide: boolean; reserve: boolean; }; /** * Record what the terminal's own answer said about these glyphs. * * Called by the launcher after measuring; also the escape hatch for a caller * that knows better than the locale (a relay that has already measured for its * own accounting, or a test). * @param wide - true when the terminal advances two cells, false for one, or * undefined to fall back to the locale. */ export declare function setAmbiguousWidthMeasured(wide: boolean | undefined): void; /** * Force the reserve behaviour, for a caller that measured the same terminal for * its own accounting (a relay) or a test. * @param reserve - whether to spend a space after each ambiguous glyph. */ export declare function setAmbiguousWidthReserve(reserve: boolean): void; /** Whether the second cell is currently reserved with a space. */ export declare function ambiguousWidthReserved(): boolean; /** * Cells one character costs in this TUI's layout. * * The reserve case still costs two: the glyph advances one cell and the reserving * space takes the next, so the layout must budget both or every row holding one * comes up short. Non-ambiguous characters are unchanged. * @param cp - the code point. * @returns 0, 1 or 2 cells. */ export declare function ambiguousCellCost(cp: number): number; /** What the last measurement decided, for diagnostics and tests. */ export declare function ambiguousWidthMeasured(): boolean | undefined; /** * Whether ambiguous glyphs in {@link AMBIGUOUS_WIDE_RANGES} are drawn two cells * wide here. * * `DSH_TUI_AMBIGUOUS_WIDTH=1|2` answers outright. Otherwise the locale decides, * and only when there really is a terminal: a zh/ja/ko locale means the terminal * is very likely using a CJK font, where these glyphs are full width. The UI * language is deliberately *not* consulted — a Chinese reader on a Western * terminal has narrow glyphs, and typing in Chinese does not change the font * metrics. * @param env - the environment to read (tests pass their own). * @param onTerminal - whether the output is a terminal at all; a pipe or a test * harness has no font metrics, so there the narrow default applies. * @returns true when those glyphs should be budgeted two cells. */ export declare function ambiguousWidthIsTwo(env?: NodeJS.ProcessEnv, onTerminal?: boolean): boolean; /** * Cells one *code unit* costs at its position, given a resolved policy. * * The per-character core of {@link displayWidth}, split out for the * callers that walk a string themselves (clipping, truncation, wrapping): they * resolve {@link ambiguousPolicy} once per string and then ask this per * character, instead of paying `displayWidth`'s policy lookup and ASCII mapping * for every character of every row. A profile of a window drag put 56% of the * samples in that lookup, because the clipping loops are O(n²) in a way nothing * about their shape advertises: `clipAnsiToWidth` calls this for each character, * and `truncateToWidth` re-measures the whole remainder inside its loop. * * ASCII is answered from its code point before any table lookup: it is the * overwhelming majority of every row, and it can never be ambiguous or wide. * @param cp - the code point. * @param policy - the resolved policy, as returned by {@link ambiguousPolicy}. * @returns 0, 1 or 2 cells. */ export declare function charCellWidth(cp: number, policy: { wide: boolean; reserve: boolean; }): number; export declare function displayWidth(text: string): number; /** * Make a painted row spend the two cells {@link displayWidth} budgets for every * BMP emoji symbol. * * The monospace font of the terminal this was measured on covers `▶` but not * `✖` or `ℹ`, so the two fall back to a colour emoji glyph about 1.6 cells wide * while still advancing one cell: the `|` and `^` beside them were drawn under * the glyph, and the row ended short because the second budgeted cell was never * spent. The fix has two halves: * * - VS15 asks for the narrow text form (`emoji-variation-sequences.txt`, * Unicode 17.0, lists `2139 FE0E` and `2716 FE0E` among 371 such sequences). * Terminals that ignore it fall back to the colour glyph, which the next * bullet still contains. * - A reserving space supplies the second cell, always emitted even when the run * already has one. That keeps the budget and the terminal in step for every * shape: the table counts the symbol as two cells plus every space the text * carries, and the terminal spends one cell of glyph advance, one for the * reserving space, and one for each of those spaces. * * A symbol whose Unicode default presentation is emoji already advances two * cells, so it is left untouched. * @param text - one already-padded, already-clipped row. * @returns the row with the text request and the second cell filled in. */ export declare function pinEmojiCells(text: string): string; /** Pad or clip one already-sanitized line so it occupies exactly `width` cells. */ export declare function padToWidth(text: string, width: number): string; /** * Pad an already-styled ANSI line to `width` cells without resetting SGR. * Diff add/del rows keep their background across the whole terminal row * instead of only the glyphs. */ export declare function padAnsiToWidth(text: string, width: number): string; /** Visible width of an ANSI-styled line, ignoring CSI / OSC sequences. */ export declare function visibleWidth(text: string): number; /** Advance past one ESC sequence starting at `index`. */ export declare function skipAnsiSequence(text: string, index: number): number; /** Repeat a glyph until it occupies exactly `width` cells. */ export declare function repeatToWidth(glyph: string, width: number): string; /** Strip terminal control sequences and expand tabs for display output. */ export declare function sanitizeTerminalText(text: string): string; /** UTF-16 length of the first code point, so fallback cuts never split a surrogate pair. */ export declare function firstCodePointLength(text: string): number; export declare function wrap(text: string, width: number): string[]; /** One colored span inside a tool-card header line. Offsets are UTF-16 char indices. */ export interface TextSegment { start: number; end: number; sgr: string; } /** Wrap plain text and report each output line's char range in the source. */ export declare function wrapTracked(text: string, width: number): { line: string; start: number; end: number; }[]; /** Paint one already-wrapped output line by the segments overlapping its range. */ export declare function paintSegmentedLine(line: string, start: number, end: number, segments: readonly TextSegment[], depth?: ColorDepth): string; /** Wrap `text` and color each output line by overlapping `segments`. */ export declare function wrapSegmented(text: string, width: number, segments: readonly TextSegment[], depth?: ColorDepth): string[]; export declare function truncate(text: string, maxLines: number): string; /** One OSC-8 hyperlink span in a painted row, in display columns. */ export interface PaintedLinkHit { href: string; startCol: number; endCol: number; } /** True when `haystack` contains `needle` the way `/find` compares them. */ export declare function searchContains(haystack: string, needle: string): boolean; /** * Wrap every occurrence of `needle` in a painted line with reverse video. * * The comparison is the search's own — both sides lowercased — so a hit that * `/find` reported is always a hit the reader can see marked. The line carries * escape sequences, so the scan runs over its visible text and the markers are * inserted between them; a long line is highlighted per screen line, which is * why the highlight cannot drift the way source offsets would after a wrap. * * When lowercasing would change the text's length (a handful of exotic code * points), the mapping back to the painted line cannot be trusted, so nothing * is marked rather than marking the wrong cells. */ export declare function highlightAnsiNeedle(line: string, needle: string): string; export declare function osc8Open(href: string): string; export declare function osc8Close(): string; /** True when OSC 8 hyperlinks should be painted. Off when DSH_TUI_OSC8=0/false. */ export declare function osc8Enabled(env?: NodeJS.ProcessEnv): boolean; /** OSC 52 clipboard write. Empty payload clears. ST is ESC \\ so a following CSI cannot be eaten as OSC payload. */ export declare function osc52Clipboard(text: string): string; export declare function stripAnsi(text: string): string; /** * Locate OSC 8 hyperlinks in a painted ANSI line. Columns are 0-based * display cells of the visible glyphs (not counting the sequences). */ export declare function paintedLinkHits(line: string): PaintedLinkHit[]; export declare function hrefAtColumn(hits: readonly PaintedLinkHit[], col: number): string | undefined; /** * Render workspace markdown into width-bounded terminal rows. Assistant * replies use a normal-white base so inline bold can contrast; code blocks, * headings, quotes, lists, rules, links and inline spans keep their own ANSI. */ export declare function renderMarkdownLines(text: string, width: number, color: boolean, hyperlinks?: boolean): string[]; /** * Cut **one line** to fit a width, appending an ellipsis when truncated. * * A line break is not a character in a row: it moves the terminal to the next row, * and a frame that addresses its rows absolutely (`ESC[row;1H`) then writes every * following row one line off — content lands on the composer, on the footer, and on * top of whatever was already there. `sanitizeTerminalText` keeps LF/CR on purpose * (it also sanitizes multi-line bodies), so the collapse happens here, where the * contract is "one row": each break becomes a single space, which keeps the string * the same *length* — callers that pass this result into `paintSegmentedLine` rely * on character offsets into the text they built, and a collapsing replacement would * silently move every colour segment. */ export declare function truncateToWidth(text: string, width: number): string; /** * Clip an already-styled ANSI line to `width` terminal cells without dropping * the reset/SGR sequences. Used by the incremental painter so a leftover wide * glyph cannot wrap into the next row. */ export declare function clipAnsiToWidth(text: string, width: number): string; /** * Cut a line that may already carry SGR accents to fit `width` cells, keeping * the escapes of the part that survives and closing what stays open. * * The plain-text {@link truncateToWidth} cannot be used on a styled line: it * sanitises its input, which strips the `ESC` of a colour sequence and leaves * its `[32m` body on the grid as four literal characters. The footer's chips * carry their own colour (the link pips, the ⚠), so that leak showed up as * `[33m⚠[0m` in the status strip. */ export declare function truncateAnsiToWidth(text: string, width: number): string; /** One renderable view of the input line: text plus the cursor's visual offset. */ export interface InputView { text: string; cursorOffset: number; folded: boolean; } export declare function foldInputView(input: string, cursor: number, maxWidth: number): InputView; /** * Map a character index in the input text to its visual (row, col) after the * same width wrapping `wrap()` applies to the rendered input. `row` is the * 0-based input display line, `col` the 0-based column within that line * (before any prompt prefix). This keeps the cursor on the correct line/column * when the input contains literal newlines from multi-line pastes. */ export declare function cursorVisualPosition(text: string, cursor: number, width: number): { row: number; col: number; }; /** Take the first `max` code points of a string without splitting surrogates. */ export declare function sliceCodePoints(text: string, max: number): string; /** Take the last `max` code points of a string without splitting surrogates. */ export declare function lastCodePoints(text: string, max: number): string;