Merge remote-tracking branch 'origin/farm/f80f626f/tui-fullwidth-cursor-drift'

This commit is contained in:
can1357
2026-06-25 06:46:07 +02:00
3 changed files with 37 additions and 7 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed bordered `Editor` rendering 1–2 cells past the terminal width when the end-of-line cursor glyph landed past a wide trailing grapheme (CJK comma `,`, emoji, etc.), wrapping the bottom-right corner (`╯`) to its own row. The right chrome (padding + `─` + corner) now shrinks by the exact cursor overflow cell count instead of a 1-cell boolean, so the box stays inside `width` for any `paddingX` ([#3431](https://github.com/can1357/oh-my-pi/issues/3431)).
## [16.1.17] - 2026-06-24
### Added
+11 -7
View File
@@ -837,7 +837,7 @@ export class Editor implements Component, Focusable {
const layoutLine = visibleLayoutLines[visibleIndex]!;
let displayText = layoutLine.text;
let displayWidth = visibleWidth(layoutLine.text);
let cursorInPadding = false;
let cursorPaddingOverflow = 0;
let decorated = false;
const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
const gutterText =
@@ -963,7 +963,7 @@ export class Editor implements Component, Focusable {
displayWidth += cursorWidth;
}
if (displayWidth > lineContentWidth && paddingX > 0) {
cursorInPadding = true;
cursorPaddingOverflow = displayWidth - lineContentWidth;
}
}
}
@@ -982,18 +982,22 @@ export class Editor implements Component, Focusable {
continue;
}
// All lines have consistent borders based on padding
// All lines have consistent borders based on padding. When the end-of-line cursor
// glyph (or a wide trailing grapheme) extends past `lineContentWidth`, shrink the
// right chrome by the exact overflow count: drop padding spaces first, then the
// trailing `─`, but never the corner/vertical bar itself.
const isLastLine = visibleIndex === visibleLayoutLines.length - 1;
const rightPaddingWidth = Math.max(0, paddingX - (cursorInPadding ? 1 : 0));
const rightChromeCells = Math.max(1, paddingX + 1 - cursorPaddingOverflow);
if (isLastLine) {
const bottomRightPadding = Math.max(0, paddingX - 1 - (cursorInPadding ? 1 : 0));
const rightPad = Math.max(0, rightChromeCells - 2);
const includeHorizontal = rightChromeCells >= 2;
const bottomRightAdjusted = this.borderColor(
`${padding(bottomRightPadding)}${box.horizontal}${box.bottomRight}`,
`${padding(rightPad)}${includeHorizontal ? box.horizontal : ""}${box.bottomRight}`,
);
result.push(`${bottomLeft}${displayText}${linePad}${bottomRightAdjusted}`);
} else {
const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`);
const rightBorder = this.borderColor(`${padding(rightPaddingWidth)}${box.vertical}`);
const rightBorder = this.borderColor(`${padding(Math.max(0, rightChromeCells - 1))}${box.vertical}`);
result.push(leftBorder + displayText + linePad + rightBorder);
}
}
+22
View File
@@ -754,6 +754,28 @@ describe("Editor component", () => {
expect(visibleWidth(contentLine)).toBeLessThanOrEqual(width);
});
it("keeps the bordered editor inside `width` when the cursor lands past a wide trailing grapheme (#3431)", () => {
// Regression: typing a fullwidth char (e.g. CJK comma `,`, U+FF0C) at the end
// of the input used to push the bottom-right `─╯` 1–2 cells past the terminal
// edge, wrapping `╯` to its own row. The end-of-line cursor glyph + wide grapheme
// extends into the right padding zone; the right chrome must shrink by the exact
// overflow cell count.
for (const paddingX of [1, 2]) {
const theme = { ...defaultEditorTheme, editorPaddingX: paddingX };
const minContentWidth = 2 * (paddingX + 1) + 3; // chrome + "," (2) + cursor (1)
for (let width = minContentWidth; width <= minContentWidth + 6; width++) {
const editor = new Editor(theme);
editor.focused = true;
for (const c of "asd,") editor.handleInput(c);
const lines = editor.render(width);
for (const line of lines) {
const stripped = line.replaceAll(CURSOR_MARKER, "");
expect(visibleWidth(stripped)).toBeLessThanOrEqual(width);
}
}
}
});
it("shows cursor at end before wrap and wraps on next char", () => {
for (const paddingX of [0, 1]) {
const editor = new Editor({ ...defaultEditorTheme, editorPaddingX: paddingX });