@@ -9,7 +9,7 @@ import { performance } from "node:perf_hooks";
|
|||||||
import { isKeyRelease, matchesKey } from "./keys.js";
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
||||||
import type { Terminal } from "./terminal.js";
|
import type { Terminal } from "./terminal.js";
|
||||||
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.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
|
* Component interface - all components must implement this
|
||||||
@@ -800,7 +800,7 @@ export class TUI extends Container {
|
|||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
if (!isImageLine(line)) {
|
if (!isImageLine(line)) {
|
||||||
lines[i] = line + reset;
|
lines[i] = normalizeTerminalOutput(line) + reset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
|
|||||||
@@ -178,16 +178,6 @@ function graphemeWidth(segment: string): number {
|
|||||||
return 2;
|
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);
|
let width = eastAsianWidth(cp);
|
||||||
|
|
||||||
// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.
|
// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.
|
||||||
@@ -265,6 +255,16 @@ export function visibleWidth(str: string): number {
|
|||||||
return width;
|
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.
|
* Extract ANSI escape sequences from a string at the given position.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import { truncateToWidth, visibleWidth } from "../src/utils.js";
|
import { normalizeTerminalOutput, truncateToWidth, visibleWidth } from "../src/utils.js";
|
||||||
|
|
||||||
describe("truncateToWidth", () => {
|
describe("truncateToWidth", () => {
|
||||||
it("keeps output within width for very large unicode input", () => {
|
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);
|
assert.strictEqual(visibleWidth("\t\x1b[31m界\x1b[0m"), 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("counts isolated Thai and Lao AM clusters conservatively", () => {
|
it("keeps Thai and Lao AM clusters at their normal cell width", () => {
|
||||||
assert.strictEqual(visibleWidth("ำ"), 2);
|
assert.strictEqual(visibleWidth("ำ"), 1);
|
||||||
assert.strictEqual(visibleWidth("ຳ"), 2);
|
assert.strictEqual(visibleWidth("ຳ"), 1);
|
||||||
assert.strictEqual(visibleWidth("กำ"), 2);
|
assert.strictEqual(visibleWidth("กำ"), 2);
|
||||||
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