feat(builtins): sweep of GNU/BSD compat fixes + str performance opts
Addresses a broad audit of built-in shell utilities against their real counterparts: timeout gains signal delivery, -s/-k/--preserve-status/--foreground/-v, and GNU exit codes; diff defaults to normal format and gains -w/-b/-B/-i/-c/-x/-L/-s/--strip-trailing-cr and proper -r gating; find fixes -newerXY timestamp comparison direction, anchors -regex to whole paths, and gains BSD -perm +mode, -type lists, -size T/P suffixes, -E/-x/-s flags; date gains BSD -r epoch, -v adjustments, -j -f strptime, and non-greedy -I; tail/head accept obsolete -N/+N at any position with any file count; rg resolves case flags by last occurrence and gains --path-separator and clean -0 output; stat prints integer epochs for %X/%Y/%Z and gains BSD -s/-x/-t; cksum is registered as a builtin; truncate implements -o/--io-blocks and b/= size suffixes; sleep/timeout accept infinity; yes/errno/kill accept hyphen-prefixed operands; nohup -- cmd no longer runs --; which gains BSD -s.
This commit is contained in:
+54
-59
@@ -225,9 +225,7 @@ export function getSegmenter(): Intl.Segmenter {
|
||||
// added back so width matches the native truncate/slice/wrap helpers.
|
||||
const OSC66_SPAN_REGEX = /\x1b\]66;([^;]*);([\s\S]*?)(?:\x07|\x1b\\)/g;
|
||||
const OSC66_PREFIX = "\x1b]66;";
|
||||
const ESC = "\x1b";
|
||||
const TAB = "\t";
|
||||
const LONG_WIDTH_FAST_PATH_MIN = 128;
|
||||
const PRINTABLE_ASCII_REGEX = /^[\u0020-\u007e]*$/;
|
||||
|
||||
// Pin Bun.stringWidth semantics to the native width engine and guard against Bun
|
||||
// default drift: strip ANSI/OSC (don't count escape bytes) and treat
|
||||
@@ -243,8 +241,6 @@ const STRING_WIDTH_OPTS = { countAnsiEscapeCodes: false, ambiguousIsNarrow: true
|
||||
// `setHangulCompatibilityJamoWidth`; mirror the same correction here so the TS
|
||||
// width stays in parity with the native truncate/slice/wrap model — and so the
|
||||
// hardware cursor column lands on the actual glyph during Korean IME input.
|
||||
const HANGUL_COMPAT_JAMO_REGEX = /[\u3131-\u318e]/;
|
||||
const HANGUL_COMPAT_JAMO_GLOBAL_REGEX = /[\u3131-\u318e]/g;
|
||||
const HANGUL_FILLER_CODE_POINT = 0x3164;
|
||||
// `Bun.stringWidth` counts every code point in the Compatibility Jamo block as
|
||||
// 2 cells (even the U+3164 filler that `unicode-width` treats as zero-width).
|
||||
@@ -274,19 +270,28 @@ function hangulCompatibilityJamoTargetWidth(): 1 | 2 | null {
|
||||
// crates/pi-natives/src/text.rs, including the rule that the zero-width filler
|
||||
// (U+3164) is never widened past the narrow correction (a wide terminal still
|
||||
// renders it at its Unicode width of 0).
|
||||
function correctHangulCompatibilityJamoWidth(width: number, str: string): number {
|
||||
if (!HANGUL_COMPAT_JAMO_REGEX.test(str)) return width;
|
||||
function correctHangulCompatibilityJamoWidth(
|
||||
width: number,
|
||||
compatibilityJamoCount: number,
|
||||
fillerCount: number,
|
||||
): number {
|
||||
if (compatibilityJamoCount === 0) return width;
|
||||
const target = hangulCompatibilityJamoTargetWidth();
|
||||
let corrected = width;
|
||||
HANGUL_COMPAT_JAMO_GLOBAL_REGEX.lastIndex = 0;
|
||||
for (let m = HANGUL_COMPAT_JAMO_GLOBAL_REGEX.exec(str); m !== null; m = HANGUL_COMPAT_JAMO_GLOBAL_REGEX.exec(str)) {
|
||||
const unicodeWidth = m[0].codePointAt(0) === HANGUL_FILLER_CODE_POINT ? 0 : 2;
|
||||
const finalWidth = target === null || (unicodeWidth === 0 && target > 1) ? unicodeWidth : target;
|
||||
corrected += finalWidth - HANGUL_COMPAT_JAMO_BUN_WIDTH;
|
||||
}
|
||||
return corrected;
|
||||
return target === 1 ? width - compatibilityJamoCount : width - fillerCount * HANGUL_COMPAT_JAMO_BUN_WIDTH;
|
||||
}
|
||||
|
||||
// Terminal redraws re-measure the same visible lines every frame, usually as
|
||||
// the same string objects (JSC caches their hashes, so repeat lookups are
|
||||
// O(1) — cheaper than even the ASCII fast scan). Strings longer than the
|
||||
// length gate skip the cache entirely: hashing them costs as much as measuring
|
||||
// them, and retaining them would pin large render buffers. Worst-case
|
||||
// retention is MAX * MAX_LEN UTF-16 units (~2 MiB); cleared when the width
|
||||
// configuration epoch changes.
|
||||
const VISIBLE_WIDTH_CACHE_MAX = 2048;
|
||||
const VISIBLE_WIDTH_CACHE_MAX_LEN = 512;
|
||||
const visibleWidthCache = new Map<string, number>();
|
||||
let visibleWidthCacheEpoch = widthConfigEpoch;
|
||||
|
||||
/**
|
||||
* Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
|
||||
*
|
||||
@@ -296,65 +301,51 @@ function correctHangulCompatibilityJamoWidth(width: number, str: string): number
|
||||
*/
|
||||
export function visibleWidth(str: string): number {
|
||||
if (!str) return 0;
|
||||
|
||||
// Long non-escape text is faster through Bun's native scanner than through
|
||||
// a JS printable-ASCII prepass. Escape-bearing strings stay on the scanner
|
||||
// below so CSI/OSC-heavy render output can still bail out at the first ESC.
|
||||
if (str.length >= LONG_WIDTH_FAST_PATH_MIN && !str.includes(ESC)) {
|
||||
let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
|
||||
let tabCount = 0;
|
||||
for (let tabIndex = str.indexOf(TAB); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
|
||||
tabCount++;
|
||||
const cacheable = str.length <= VISIBLE_WIDTH_CACHE_MAX_LEN;
|
||||
if (cacheable) {
|
||||
if (visibleWidthCacheEpoch !== widthConfigEpoch) {
|
||||
visibleWidthCache.clear();
|
||||
visibleWidthCacheEpoch = widthConfigEpoch;
|
||||
}
|
||||
if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
|
||||
return correctHangulCompatibilityJamoWidth(width, str);
|
||||
const cached = visibleWidthCache.get(str);
|
||||
if (cached !== undefined) return cached;
|
||||
}
|
||||
|
||||
// This regex compiles to a native ASCII scan, cheaper than Bun's width
|
||||
// scanner for the overwhelmingly common source-code path.
|
||||
if (PRINTABLE_ASCII_REGEX.test(str)) {
|
||||
if (cacheable) {
|
||||
if (visibleWidthCache.size >= VISIBLE_WIDTH_CACHE_MAX) visibleWidthCache.clear();
|
||||
visibleWidthCache.set(str, str.length);
|
||||
}
|
||||
return str.length;
|
||||
}
|
||||
|
||||
let tabCount = 0;
|
||||
let i = 0;
|
||||
for (; i < str.length; i++) {
|
||||
let compatibilityJamoCount = 0;
|
||||
let fillerCount = 0;
|
||||
let hasEsc = false;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
if (code < 0x20 || code > 0x7e) {
|
||||
if (code === 0x09) {
|
||||
tabCount++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i === str.length) {
|
||||
return tabCount === 0 ? str.length : str.length + tabCount * (DEFAULT_TAB_WIDTH - 1);
|
||||
}
|
||||
|
||||
if (tabCount === 0) {
|
||||
let tabIndex = str.indexOf(TAB, i + 1);
|
||||
if (tabIndex !== -1) {
|
||||
tabCount = 1;
|
||||
for (tabIndex = str.indexOf(TAB, tabIndex + 1); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
|
||||
tabCount++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let tabIndex = str.indexOf(TAB, i + 1); tabIndex !== -1; tabIndex = str.indexOf(TAB, tabIndex + 1)) {
|
||||
if (code === 0x09) {
|
||||
tabCount++;
|
||||
} else if (code === 0x1b) {
|
||||
hasEsc = true;
|
||||
} else if (code >= 0x3131 && code <= 0x318e) {
|
||||
compatibilityJamoCount++;
|
||||
if (code === HANGUL_FILLER_CODE_POINT) fillerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// `Bun.stringWidth` is a JSC builtin (no per-call N-API number box, unlike
|
||||
// the native scanner that traps under Bun 1.3.x GC/N-API load). It strips
|
||||
// CSI/OSC to zero cells and shares the native engine's UAX#11 width tables.
|
||||
let width = Bun.stringWidth(str, STRING_WIDTH_OPTS);
|
||||
if (tabCount > 0) width += tabCount * DEFAULT_TAB_WIDTH;
|
||||
|
||||
// OSC 66: add back each stripped span as `scale * (explicit w ?? payload
|
||||
// width)`. Matched rather than replaced to avoid reallocating the string.
|
||||
if (str.includes(OSC66_PREFIX, i)) {
|
||||
if (hasEsc && str.includes(OSC66_PREFIX)) {
|
||||
OSC66_SPAN_REGEX.lastIndex = 0;
|
||||
for (let m = OSC66_SPAN_REGEX.exec(str); m !== null; m = OSC66_SPAN_REGEX.exec(str)) {
|
||||
let scale = 1;
|
||||
let explicit: number | undefined;
|
||||
for (const part of m[1].split(":")) {
|
||||
// metadata keys are single chars, e.g. `s=2`, `w=5`
|
||||
if (part.indexOf("=") !== 1) continue;
|
||||
const value = Number.parseInt(part.slice(2), 10);
|
||||
if (!Number.isFinite(value)) continue;
|
||||
@@ -368,11 +359,15 @@ export function visibleWidth(str: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
return correctHangulCompatibilityJamoWidth(width, str);
|
||||
width = correctHangulCompatibilityJamoWidth(width, compatibilityJamoCount, fillerCount);
|
||||
if (cacheable) {
|
||||
if (visibleWidthCache.size >= VISIBLE_WIDTH_CACHE_MAX) visibleWidthCache.clear();
|
||||
visibleWidthCache.set(str, width);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a row carries a Kitty OSC 66 text-sizing span (`\x1b]66;…`).
|
||||
* Scaled spans must bypass wrapping/padding and, when scaled up, reserve the
|
||||
* terminal rows their multicell glyphs flow into.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user