From 040846336d765caf37a084cfa5dc7a20df124661 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 25 Jun 2026 03:34:24 +0000 Subject: [PATCH] fix(tui): shrunk bordered Editor right chrome by cursor overflow cells, not a 1-cell boolean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the end-of-line cursor glyph lands past a wide trailing grapheme (CJK comma ',', emoji, etc.), the inverse-video cursor extends past 'lineContentWidth' by the full overflow. Prior cursorInPadding logic dropped exactly one space of right padding but always re-emitted '─' before '╯', so 'paddingX=1' overflow=1 stayed 1 cell too wide and 'paddingX=2' overflow=2 stayed 2 cells too wide. In the user's terminal the '╯' wrapped to a new row, looking like the editor box collapsed onto two lines. Replaced the boolean with the actual overflow cell count and recomputed the right chrome budget (padding + optional '─' + corner) from 'paddingX + 1 - overflow', floored at 1 so the corner/vertical bar survives. The else branch (non-last lines) follows the same budget for symmetry. Fixes #3431 --- packages/tui/CHANGELOG.md | 4 ++++ packages/tui/src/components/editor.ts | 18 +++++++++++------- packages/tui/test/editor.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index eb30b7f9c..5ed21f2a3 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -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 diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 0cc229b8b..34187d9b9 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -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); } } diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 6ab2b78ca..d451c758e 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -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 });