feat(tui): hardware cursor positioning for IME support

- Add Focusable interface for components that need hardware cursor positioning
- Add CURSOR_MARKER (APC escape sequence) for marking cursor position in render output
- Editor and Input components implement Focusable and emit marker when focused
- TUI extracts cursor position from rendered output and positions hardware cursor
- Track hardwareCursorRow separately from cursorRow for differential rendering
- visibleWidth() and extractAnsiCode() now handle APC sequences
- Update overlay-test.ts example to demonstrate Focusable usage
- Add documentation for Focusable interface in docs/tui.md

Closes #719, closes #525
This commit is contained in:
Mario Zechner 2026-01-16 04:30:07 +01:00
parent d9464383ec
commit 07fad1362c
8 changed files with 193 additions and 17 deletions

View file

@ -9,7 +9,7 @@
*/
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
import { matchesKey, visibleWidth } from "@mariozechner/pi-tui";
import { CURSOR_MARKER, type Focusable, matchesKey, visibleWidth } from "@mariozechner/pi-tui";
export default function (pi: ExtensionAPI) {
pi.registerCommand("overlay-test", {
@ -28,9 +28,12 @@ export default function (pi: ExtensionAPI) {
});
}
class OverlayTestComponent {
class OverlayTestComponent implements Focusable {
readonly width = 70;
/** Focusable interface - set by TUI when focus changes */
focused = false;
private selected = 0;
private items = [
{ label: "Search", hasInput: true, text: "", cursor: 0 },
@ -123,7 +126,9 @@ class OverlayTestComponent {
const before = inputDisplay.slice(0, item.cursor);
const cursorChar = item.cursor < inputDisplay.length ? inputDisplay[item.cursor] : " ";
const after = inputDisplay.slice(item.cursor + 1);
inputDisplay = `${before}\x1b[7m${cursorChar}\x1b[27m${after}`;
// Emit hardware cursor marker for IME support when focused
const marker = this.focused ? CURSOR_MARKER : "";
inputDisplay = `${before}${marker}\x1b[7m${cursorChar}\x1b[27m${after}`;
}
content = `${prefix + label} ${inputDisplay}`;
} else {