Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
4.3 kB · 139 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140/** * Bidirectional text reordering for terminal rendering. * * Terminals on Windows do not implement the Unicode Bidi Algorithm, * so RTL text (Hebrew, Arabic, etc.) appears reversed. This module * applies the bidi algorithm to reorder ClusteredChar arrays from * logical order to visual order before Ink's LTR cell placement loop. * * On macOS terminals (Terminal.app, iTerm2) bidi works natively. * Windows Terminal (including WSL) does not implement bidi * (https://github.com/microsoft/terminal/issues/538). * * Detection: Windows Terminal sets WT_SESSION; native Windows cmd/conhost * also lacks bidi. We enable bidi reordering when running on Windows or * inside Windows Terminal (covers WSL). */import bidiFactory from 'bidi-js'
type ClusteredChar = { value: string width: number styleId: number hyperlink: string | undefined}
let bidiInstance: ReturnType<typeof bidiFactory> | undefinedlet needsSoftwareBidi: boolean | undefined
function needsBidi(): boolean { if (needsSoftwareBidi === undefined) { needsSoftwareBidi = process.platform === 'win32' || typeof process.env['WT_SESSION'] === 'string' || // WSL in Windows Terminal process.env['TERM_PROGRAM'] === 'vscode' // VS Code integrated terminal (xterm.js) } return needsSoftwareBidi}
function getBidi() { if (!bidiInstance) { bidiInstance = bidiFactory() } return bidiInstance}
/** * Reorder an array of ClusteredChars from logical order to visual order * using the Unicode Bidi Algorithm. Active on terminals that lack native * bidi support (Windows Terminal, conhost, WSL). * * Returns the same array on bidi-capable terminals (no-op). */export function reorderBidi(characters: ClusteredChar[]): ClusteredChar[] { if (!needsBidi() || characters.length === 0) { return characters }
// Build a plain string from the clustered chars to run through bidi const plainText = characters.map(c => c.value).join('')
// Check if there are any RTL characters — skip bidi if pure LTR if (!hasRTLCharacters(plainText)) { return characters }
const bidi = getBidi() const { levels } = bidi.getEmbeddingLevels(plainText, 'auto')
// Map bidi levels back to ClusteredChar indices. // Each ClusteredChar may be multiple code units in the joined string. const charLevels: number[] = [] let offset = 0 for (let i = 0; i < characters.length; i++) { charLevels.push(levels[offset]!) offset += characters[i]!.value.length }
// Get reorder segments from bidi-js, but we need to work at the // ClusteredChar level, not the string level. We'll implement the // standard bidi reordering: find the max level, then for each level // from max down to 1, reverse all contiguous runs >= that level. const reordered = [...characters] const maxLevel = Math.max(...charLevels)
for (let level = maxLevel; level >= 1; level--) { let i = 0 while (i < reordered.length) { if (charLevels[i]! >= level) { // Find the end of this run let j = i + 1 while (j < reordered.length && charLevels[j]! >= level) { j++ } // Reverse the run in both arrays reverseRange(reordered, i, j - 1) reverseRangeNumbers(charLevels, i, j - 1) i = j } else { i++ } } }
return reordered}
function reverseRange<T>(arr: T[], start: number, end: number): void { while (start < end) { const temp = arr[start]! arr[start] = arr[end]! arr[end] = temp start++ end-- }}
function reverseRangeNumbers(arr: number[], start: number, end: number): void { while (start < end) { const temp = arr[start]! arr[start] = arr[end]! arr[end] = temp start++ end-- }}
/** * Quick check for RTL characters (Hebrew, Arabic, and related scripts). * Avoids running the full bidi algorithm on pure-LTR text. */function hasRTLCharacters(text: string): boolean { // Hebrew: U+0590-U+05FF, U+FB1D-U+FB4F // Arabic: U+0600-U+06FF, U+0750-U+077F, U+08A0-U+08FF, U+FB50-U+FDFF, U+FE70-U+FEFF // Thaana: U+0780-U+07BF // Syriac: U+0700-U+074F return /[\u0590-\u05FF\uFB1D-\uFB4F\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u0780-\u07BF\u0700-\u074F]/u.test( text, )}