getCursorPosition(width: number): { row: number; col: number } | null { let rowOffset = 0; for (const child of this.children) { const childCursor = child.getCursorPosition?.(width); if (childCursor) { return { row: rowOffset + childCursor.row, col: childCursor.col }; } rowOffset += child.render(width).length; // Re-renders child! } return null;}class TUI { private focusedComponentStartRow: number = -1; private doRender(): void { // During render, track where focused component starts let currentRow = 0; const focusedLines: string[] = []; for (const child of this.children) { if (child === this.focusedComponent) { this.focusedComponentStartRow = currentRow; } const lines = child.render(width); currentRow += lines.length; } // After render, position hardware cursor if (this.focusedComponent?.getCursorPosition) { const pos = this.focusedComponent.getCursorPosition(width); if (pos) { const absoluteRow = this.focusedComponentStartRow + pos.row; this.positionHardwareCursor(absoluteRow, pos.col); } } }}// Special marker that won't render visuallyconst CURSOR_MARKER = "\x1b[?cursor;r;c\x1b[0m"; // r=row, c=col within component
// In Editor.render():const cursorMarker = `\x1b[?cursor;${layoutLineIndex};${visualCol}\x1b[0m`;interface CursorReporter { getCursorPosition(width: number): { row: number; col: number } | null;}
// Editor implements both Component and CursorReporterclass Editor implements Component, CursorReporter { ... }if (isCursorReporter(this.focusedComponent)) { const pos = this.focusedComponent.getCursorPosition(width); // ...}class Container { render(width: number): string[] { const lines: string[] = []; for (const child of this.children) { lines.push(...child.render(width)); // No tracking of which child produced which lines } return lines; }}// Only the focused component emits this markerconst CURSOR_MARKER = "\x1b]8;;cursor\x07"; // Using OSC 8 (hyperlink) with special ID // Or any zero-width escape sequence
class Editor { render(width: number): string[] { // ... layout text ... // Emit marker at cursor position (only if we have focus) if (this.hasFocus) { // Insert marker right before the fake cursor line = before + CURSOR_MARKER + fakeCursor + after; } return lines; }}class TUI { doRender(): void { let newLines = this.render(width); newLines = this.compositeOverlays(newLines, width, height); // Find cursor marker in final output const cursorPos = this.findAndStripCursorMarker(newLines); newLines = this.applyLineResets(newLines); // ... differential rendering ... // Position hardware cursor at marker location if (cursorPos) { this.positionHardwareCursor(cursorPos.row, cursorPos.col); } } private findAndStripCursorMarker(lines: string[]): { row: number; col: number } | null { for (let row = 0; row < lines.length; row++) { const col = this.findMarkerColumn(lines[row]); if (col !== -1) { lines[row] = lines[row].replace(CURSOR_MARKER, ''); return { row, col }; } } return null; }}// Cursor position marker - APC with unique payloadconst CURSOR_MARKER = "\x1b_pi:cursor\x1b\\";const CURSOR_MARKER = "\x1b_pi:c\x07";/** * Interface for components that can receive focus and display a cursor. * When focused, component should emit CURSOR_MARKER at the cursor position. */export interface Focusable { /** Set by TUI when focus changes */ focused: boolean;}
/** Type guard */export function isFocusable(component: Component): component is Component & Focusable { return 'focused' in component;}class TUI extends Container { setFocus(component: Component | null): void { // Clear old focus if (this.focusedComponent && isFocusable(this.focusedComponent)) { this.focusedComponent.focused = false; } this.focusedComponent = component; // Set new focus if (component && isFocusable(component)) { component.focused = true; } } private doRender(): void { let newLines = this.render(width); newLines = this.compositeOverlays(newLines, width, height); // Find and strip cursor marker, get position const cursorPos = this.extractCursorPosition(newLines); newLines = this.applyLineResets(newLines); // ... existing differential rendering ... // Position hardware cursor if (cursorPos) { this.positionHardwareCursor(cursorPos.row, cursorPos.col); this.terminal.showCursor(); } else { this.terminal.hideCursor(); } } private extractCursorPosition(lines: string[]): { row: number; col: number } | null { const marker = "\x1b_pi:c\x07"; for (let row = 0; row < lines.length; row++) { const markerIndex = lines[row].indexOf(marker); if (markerIndex !== -1) { // Calculate visual column (width of text before marker) const beforeMarker = lines[row].slice(0, markerIndex); const col = visibleWidth(beforeMarker); // Strip marker lines[row] = lines[row].replace(marker, ''); return { row, col }; } } return null; }}class Editor implements Component, Focusable { focused: boolean = false; render(width: number): string[] { // ... existing layout code ... // When building the line with cursor: if (layoutLine.hasCursor && this.focused) { // Emit marker right before the fake cursor character const marker = "\x1b_pi:c\x07"; displayText = before + marker + fakeCursor + after; } else { displayText = before + fakeCursor + after; } // ... }}private doRender(): void { // ... find firstChanged, lastChanged ... // Move cursor to first changed line (relative to current position) const lineDiff = firstChanged - this.cursorRow; if (lineDiff > 0) buffer += `\x1b[${lineDiff}B`; else if (lineDiff < 0) buffer += `\x1b[${-lineDiff}A`; buffer += "\r"; // Column 0 // Write changed lines... // Track where cursor ended up this.cursorRow = finalCursorRow;}// After differential rendering leaves cursor at row 50this.cursorRow = 50;
// Then we move cursor to IME position at row 10this.positionHardwareCursor(10, 15);class TUI { private cursorRow = 0; // Logical: where content rendering ends private hardwareCursorRow = 0; // Actual: where terminal cursor physically is}private doRender(): void { // Movement is relative to actual cursor position const lineDiff = firstChanged - this.hardwareCursorRow; // ... write lines ... // After writing, cursor is at end of last written line this.cursorRow = finalCursorRow; this.hardwareCursorRow = finalCursorRow; // Now position for IME (updates hardwareCursorRow) if (cursorPos) { this.positionHardwareCursor(cursorPos.row, cursorPos.col); this.hardwareCursorRow = cursorPos.row; this.terminal.showCursor(); }}private extractCursorPosition(lines: string[]): { row: number; col: number } | null { // ... find marker, calculate position ...}
private doRender(): void { // ... existing differential rendering using hardwareCursorRow for movement ... this.cursorRow = finalCursorRow; this.hardwareCursorRow = finalCursorRow; // Position for IME const cursorPos = /* extracted earlier */; if (cursorPos) { const rowDelta = cursorPos.row - this.hardwareCursorRow; if (rowDelta > 0) this.terminal.write(`\x1b[${rowDelta}B`); else if (rowDelta < 0) this.terminal.write(`\x1b[${-rowDelta}A`); this.terminal.write(`\x1b[${cursorPos.col + 1}G`); // Absolute column this.hardwareCursorRow = cursorPos.row; this.terminal.showCursor(); } else { this.terminal.hideCursor(); }}cd /Users/badlogic/workspaces/pi-mono/packages/coding-agentnpm run dev# Switch to Japanese input (Ctrl+Space)# Type in the editorcd packages/coding-agentnpm run dev# then /overlay-test and switch to Japanese input