fix(tui): protected editor border from IME preedit

- Kept the focused hardware-cursor row empty to its right so terminal-local marked text cannot shift box chrome.

- Rendered a dedicated bottom border and covered terminal insert-mode preedit with a regression test.

Fixes #5563
This commit is contained in:
roboomp
2026-07-15 10:39:34 +00:00
parent 2134526b7d
commit d1c98e9607
3 changed files with 50 additions and 1 deletions
+4
View File
@@ -7,6 +7,10 @@
- Display LaTeX now renders `\underbrace`/`\overbrace` (and the bracket/paren variants) as drawn horizontal braces with centered labels, and stacks `\overset`/`\underset`/`\stackrel` annotations above/below the base instead of falling back to flat inline glyphs.
- Display LaTeX renders multi-letter script words (`N_{turns}`) as raised/lowered blocks instead of ragged per-character Unicode sub/superscript glyphs; single letters and digits keep the compact Unicode forms.
### Fixed
- Fixed macOS IME marked text displacing the prompt editor border when entering fullwidth CJK punctuation ([#5563](https://github.com/can1357/oh-my-pi/issues/5563)).
## [16.5.2] - 2026-07-14
### Fixed
+14
View File
@@ -866,6 +866,7 @@ export class Editor implements Component, Focusable {
let displayWidth = visibleWidth(layoutLine.text);
let cursorPaddingOverflow = 0;
let decorated = false;
let imeSafeCursorTail = false;
const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
const gutterText =
promptGutter === undefined ? "" : showPromptGutter ? promptGutter.firstLine : promptGutter.continuation;
@@ -931,6 +932,10 @@ export class Editor implements Component, Focusable {
displayText = this.#renderTerminalCursorMarker(before, marker, lineContentWidth);
} else {
displayText = before + marker + after;
// Terminal frontends render IME marked text locally before committed bytes
// reach the application. Keep the end-of-input cursor row empty to its
// right so that insertion cannot shift box chrome onto the next row.
imeSafeCursorTail = after.length === 0 && borderVisible;
}
}
} else if (hasCursor && !this.#useTerminalCursor) {
@@ -1022,6 +1027,15 @@ export class Editor implements Component, Focusable {
// trailing `─`, but never the corner/vertical bar itself.
const isLastLine = visibleIndex === visibleLayoutLines.length - 1;
const rightChromeCells = Math.max(1, paddingX + 1 - cursorPaddingOverflow);
if (isLastLine && imeSafeCursorTail) {
const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`);
const bottomBorder = this.borderColor(
`${box.bottomLeft}${box.horizontal.repeat(Math.max(0, width - 2))}${box.bottomRight}`,
);
result.push(leftBorder + displayText);
result.push(bottomBorder);
continue;
}
if (isLastLine) {
const rightPad = Math.max(0, rightChromeCells - 2);
const includeHorizontal = rightChromeCells >= 2;
+32 -1
View File
@@ -3,13 +3,14 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { stripVTControlCharacters } from "node:util";
import { CURSOR_MARKER } from "@oh-my-pi/pi-tui";
import { CURSOR_MARKER, TUI } from "@oh-my-pi/pi-tui";
import { CombinedAutocompleteProvider } from "@oh-my-pi/pi-tui/autocomplete";
import { Editor } from "@oh-my-pi/pi-tui/components/editor";
import { KeybindingsManager, setKeybindings, TUI_KEYBINDINGS } from "@oh-my-pi/pi-tui/keybindings";
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
import { visibleWidth } from "@oh-my-pi/pi-tui/utils";
import { defaultEditorTheme } from "./test-themes";
import { VirtualTerminal } from "./virtual-terminal";
describe("Editor component", () => {
afterEach(() => {
@@ -852,6 +853,36 @@ describe("Editor component", () => {
}
});
it("keeps terminal-local IME preedit from displacing the editor border (#5563)", async () => {
const width = 20;
const terminal = new VirtualTerminal(width, 6, 1_000);
const tui = new TUI(terminal, true);
const editor = new Editor(defaultEditorTheme);
tui.addChild(editor);
tui.setFocus(editor);
try {
tui.start();
await terminal.waitForRender();
for (const char of "ast") editor.handleInput(char);
tui.requestRender();
await terminal.waitForRender();
const beforePreedit = terminal.getViewport().map(row => row.trimEnd());
expect(beforePreedit.slice(0, 3)).toEqual(["+------------------+", "| ast", "+------------------+"]);
// macOS Terminal renders marked text locally in insertion mode before
// committed bytes reach OMP. The open cursor row must not carry right
// chrome that the marked text can shift onto another row.
terminal.write("\x1b[4hast,\x1b[4l");
const afterPreedit = terminal.getViewport().map(row => row.trimEnd());
expect(afterPreedit[1]).toBe("| astast,");
expect(afterPreedit[2]).toBe(beforePreedit[2]);
} finally {
tui.stop();
}
});
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 });