diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index d8f5cbd5..0c284a34 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -52,6 +52,36 @@ When a `Focusable` component has focus, TUI: This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface. +### Container Components with Embedded Inputs + +When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input. + +```typescript +import { Container, type Focusable, Input } from "@mariozechner/pi-tui"; + +class SearchDialog extends Container implements Focusable { + private searchInput: Input; + + // Focusable implementation - propagate to child input for IME cursor positioning + private _focused = false; + get focused(): boolean { + return this._focused; + } + set focused(value: boolean) { + this._focused = value; + this.searchInput.focused = value; + } + + constructor() { + super(); + this.searchInput = new Input(); + this.addChild(this.searchInput); + } +} +``` + +Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen. + ## Using Components **In hooks** via `ctx.ui.custom()`: diff --git a/packages/tui/README.md b/packages/tui/README.md index f9432501..e3b0e89e 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -136,6 +136,58 @@ interface Component { The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line. +### Focusable Interface (IME Support) + +Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface: + +```typescript +import { CURSOR_MARKER, type Component, type Focusable } from "@mariozechner/pi-tui"; + +class MyInput implements Component, Focusable { + focused: boolean = false; // Set by TUI when focus changes + + render(width: number): string[] { + const marker = this.focused ? CURSOR_MARKER : ""; + // Emit marker right before the fake cursor + return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`]; + } +} +``` + +When a `Focusable` component has focus, TUI: +1. Sets `focused = true` on the component +2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence) +3. Positions the hardware terminal cursor at that location +4. Shows the hardware cursor + +This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface. + +**Container components with embedded inputs:** When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child: + +```typescript +import { Container, type Focusable, Input } from "@mariozechner/pi-tui"; + +class SearchDialog extends Container implements Focusable { + private searchInput: Input; + + // Propagate focus to child input for IME cursor positioning + private _focused = false; + get focused(): boolean { return this._focused; } + set focused(value: boolean) { + this._focused = value; + this.searchInput.focused = value; + } + + constructor() { + super(); + this.searchInput = new Input(); + this.addChild(this.searchInput); + } +} +``` + +Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position. + ## Built-in Components ### Container