Skip to content

feat(terminal) 01 - Create src/render/terminal.ts - Bun 1.4 native API facade #141

Description

@shouze

EPIC: #140
Branch: feat/bun14-terminal
Depends on: nothing (infrastructure, start here)
Blocks: #142, #143


Context / Problem

Visible-width measurement and ANSI-aware truncation are scattered across src/render.ts (helpers stripAnsi, clipAnsi) and src/render/team-pick.ts (direct .length calls). These in-house implementations do not correctly handle emoji, CJK characters, multi-code-point grapheme clusters, or OSC 8 terminal hyperlinks.

There is no single testable entry point for terminal column operations. Bun 1.4 provides native SIMD-backed implementations for all of them.

Solution and Architecture Principles

Create src/render/terminal.ts as a pure module (no I/O, no side effects) exposing four functions:

// Returns the number of terminal columns the string occupies (ANSI codes excluded from count by default)
export function visibleWidth(str: string): number

// Strips all ANSI escape sequences from a string
export function stripAnsi(str: string): string

// Truncates str to maxCols visible columns, preserving open ANSI styles.
// Appends \x1b[22;39m after the cut to reset bold and foreground color without
// affecting any background color applied by the caller (e.g. renderActiveLine).
export function clipToWidth(str: string, maxCols: number): string

// Returns true if str contains at least one ANSI escape sequence
export function hasAnsi(str: string): boolean

Delegations:

  • visibleWidth -> Bun.stringWidth(str)
  • stripAnsi -> Bun.stripANSI(str)
  • clipToWidth -> Bun.sliceAnsi(str, 0, maxCols) + append \x1b[22;39m
  • hasAnsi -> compare Bun.stringWidth(str, { countAnsiEscapeCodes: true }) against Bun.stringWidth(str)

Module position in the render layer

graph TD
    facade["src/render.ts (existing facade)"]
    teampick["src/render/team-pick.ts"]
    terminal["src/render/terminal.ts (NEW - pure module)"]
    bun_sw["Bun.stringWidth()"]
    bun_sa["Bun.stripANSI()"]
    bun_sl["Bun.sliceAnsi()"]

    facade --> terminal
    teampick --> terminal
    terminal --> bun_sw
    terminal --> bun_sa
    terminal --> bun_sl
Loading

Architectural rule: no other source file imports Bun.stringWidth, Bun.sliceAnsi, or Bun.stripANSI directly. All visible-width calculations and ANSI-aware truncation go through this module.

The module is re-exported from src/render.ts (the existing facade) for external consumers.

Note on clipToWidth and background preservation

The current clipAnsi appends \x1b[22;39m (partial reset: bold off + foreground default, background untouched) rather than a full \x1b[0m reset. This is intentional: renderActiveLine applies a dark purple background (\x1b[48;5;53m) around the clipped content; a full reset mid-line would strip that background for the remainder of the row (see issue #105).

Bun.sliceAnsi closes open styles at the cut point without resetting sequences that were not opened in the input string. The additional \x1b[22;39m appended by clipToWidth is a defensive measure to ensure consistent behavior. Tests must verify this explicitly.

Acceptance Criteria

  • src/render/terminal.ts exists and exports visibleWidth, stripAnsi, clipToWidth, and hasAnsi.
  • src/render/terminal.test.ts covers:
    • Plain ASCII (baseline)
    • SGR color and bold codes
    • OSC 8 terminal hyperlinks (ignored in width count)
    • Single wide character (e.g. \u{1F50D} - magnifying glass) - visibleWidth returns 2
    • ZWJ emoji sequence (e.g. \u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}) - visibleWidth returns 2
    • Regional indicator pair / flag (e.g. \u{1F1EB}\u{1F1F7}) - visibleWidth returns 2
    • CJK characters (e.g. "\u524D\u7AEF" - two characters) - visibleWidth returns 4
    • Grapheme with skin tone modifier
    • Mixed colored text, wide characters, and CJK in a single string
    • clipToWidth output ends with \x1b[22;39m when a cut occurs
    • clipToWidth returns the original string unchanged when it fits within maxCols
    • stripAnsi removes SGR, OSC 8, and cursor movement sequences
  • bun run knip reports no unused exports from the new module.
  • No other source file calls Bun.stringWidth, Bun.sliceAnsi, or Bun.stripANSI directly.
  • bun test src/render/terminal.test.ts passes.
  • bun run lint and bun run format:check are clean.

Definition of Done

  • PR reviewed and merged into feat/bun14-terminal.
  • Zero lint errors, zero format diff, test suite green.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions