@@ -9,7 +9,7 @@ import { performance } from "node:perf_hooks";
|
||||
import { isKeyRelease, matchesKey } from "./keys.js";
|
||||
import type { Terminal } from "./terminal.js";
|
||||
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
||||
import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
||||
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Component interface - all components must implement this
|
||||
@@ -800,7 +800,7 @@ export class TUI extends Container {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!isImageLine(line)) {
|
||||
lines[i] = line + reset;
|
||||
lines[i] = normalizeTerminalOutput(line) + reset;
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
|
||||
@@ -178,16 +178,6 @@ function graphemeWidth(segment: string): number {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Thai SARA AM (U+0E33) and Lao VOWEL SIGN AM (U+0EB3) are encoded as
|
||||
// spacing letters, but terminals commonly render isolated occurrences as
|
||||
// mark-like clusters (compat decomposition: nonspacing niggahita + vowel).
|
||||
// Count isolated clusters conservatively to avoid cursor drift and stale
|
||||
// cells during differential rendering. When attached to a base consonant,
|
||||
// the base code point is first and normal per-codepoint width below applies.
|
||||
if (cp === 0x0e33 || cp === 0x0eb3) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
let width = eastAsianWidth(cp);
|
||||
|
||||
// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.
|
||||
@@ -265,6 +255,16 @@ export function visibleWidth(str: string): number {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize text for terminal output without changing logical editor content.
|
||||
* Some terminals render precomposed Thai/Lao AM vowels inconsistently during
|
||||
* differential repaint. Their compatibility decompositions have the same cell
|
||||
* width but avoid stale-cell artifacts in terminal renderers.
|
||||
*/
|
||||
export function normalizeTerminalOutput(str: string): string {
|
||||
return str.replace(/\u0e33/g, "\u0e4d\u0e32").replace(/\u0eb3/g, "\u0ecd\u0eb2");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ANSI escape sequences from a string at the given position.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { truncateToWidth, visibleWidth } from "../src/utils.js";
|
||||
import { normalizeTerminalOutput, truncateToWidth, visibleWidth } from "../src/utils.js";
|
||||
|
||||
describe("truncateToWidth", () => {
|
||||
it("keeps output within width for very large unicode input", () => {
|
||||
@@ -60,10 +60,17 @@ describe("visibleWidth", () => {
|
||||
assert.strictEqual(visibleWidth("\t\x1b[31m界\x1b[0m"), 5);
|
||||
});
|
||||
|
||||
it("counts isolated Thai and Lao AM clusters conservatively", () => {
|
||||
assert.strictEqual(visibleWidth("ำ"), 2);
|
||||
assert.strictEqual(visibleWidth("ຳ"), 2);
|
||||
it("keeps Thai and Lao AM clusters at their normal cell width", () => {
|
||||
assert.strictEqual(visibleWidth("ำ"), 1);
|
||||
assert.strictEqual(visibleWidth("ຳ"), 1);
|
||||
assert.strictEqual(visibleWidth("กำ"), 2);
|
||||
assert.strictEqual(visibleWidth("ກຳ"), 2);
|
||||
});
|
||||
|
||||
it("normalizes Thai and Lao AM vowels only for terminal output", () => {
|
||||
assert.strictEqual(normalizeTerminalOutput("ำ"), "ํา");
|
||||
assert.strictEqual(normalizeTerminalOutput("ຳ"), "ໍາ");
|
||||
assert.strictEqual(visibleWidth(normalizeTerminalOutput("ำabc")), visibleWidth("ำabc"));
|
||||
assert.strictEqual(visibleWidth(normalizeTerminalOutput("ຳabc")), visibleWidth("ຳabc"));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user