You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)exportfunctionvisibleWidth(str: string): number// Strips all ANSI escape sequences from a stringexportfunctionstripAnsi(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).exportfunctionclipToWidth(str: string,maxCols: number): string// Returns true if str contains at least one ANSI escape sequenceexportfunctionhasAnsi(str: string): boolean
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
EPIC: #140
Branch:
feat/bun14-terminalDepends on: nothing (infrastructure, start here)
Blocks: #142, #143
Context / Problem
Visible-width measurement and ANSI-aware truncation are scattered across
src/render.ts(helpersstripAnsi,clipAnsi) andsrc/render/team-pick.ts(direct.lengthcalls). 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.tsas a pure module (no I/O, no side effects) exposing four functions:Delegations:
visibleWidth->Bun.stringWidth(str)stripAnsi->Bun.stripANSI(str)clipToWidth->Bun.sliceAnsi(str, 0, maxCols)+ append\x1b[22;39mhasAnsi-> compareBun.stringWidth(str, { countAnsiEscapeCodes: true })againstBun.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_slArchitectural rule: no other source file imports
Bun.stringWidth,Bun.sliceAnsi, orBun.stripANSIdirectly. 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
clipToWidthand background preservationThe current
clipAnsiappends\x1b[22;39m(partial reset: bold off + foreground default, background untouched) rather than a full\x1b[0mreset. This is intentional:renderActiveLineapplies 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.sliceAnsicloses open styles at the cut point without resetting sequences that were not opened in the input string. The additional\x1b[22;39mappended byclipToWidthis a defensive measure to ensure consistent behavior. Tests must verify this explicitly.Acceptance Criteria
src/render/terminal.tsexists and exportsvisibleWidth,stripAnsi,clipToWidth, andhasAnsi.src/render/terminal.test.tscovers:\u{1F50D}- magnifying glass) -visibleWidthreturns 2\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}) -visibleWidthreturns 2\u{1F1EB}\u{1F1F7}) -visibleWidthreturns 2"\u524D\u7AEF"- two characters) -visibleWidthreturns 4clipToWidthoutput ends with\x1b[22;39mwhen a cut occursclipToWidthreturns the original string unchanged when it fits withinmaxColsstripAnsiremoves SGR, OSC 8, and cursor movement sequencesbun run knipreports no unused exports from the new module.Bun.stringWidth,Bun.sliceAnsi, orBun.stripANSIdirectly.bun test src/render/terminal.test.tspasses.bun run lintandbun run format:checkare clean.Definition of Done
feat/bun14-terminal.