/**
 * Minimal TUI implementation with differential rendering
 */
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { isKeyRelease, matchesKey } from "./keys.js";
import { applyLineResets, compositeOverlays, extractCursorPosition, isOverlayVisible as isOverlayEntryVisible, } from "./overlay-layout.js";
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
import { truncateToWidth, visibleWidth } from "./utils.js";
/** Type guard to check if a component implements Focusable */
export function isFocusable(component) {
    return component !== null && "focused" in component;
}
/**
 * Cursor position marker - APC (Application Program Command) sequence.
 * This is a zero-width escape sequence that terminals ignore.
 * Components emit this at the cursor position when focused.
 * TUI finds and strips this marker, then positions the hardware cursor there.
 */
export const CURSOR_MARKER = "\x1b_pi:c\x07";
export { visibleWidth };
/**
 * Container - a component that contains other components
 */
export class Container {
    constructor() {
        this.children = [];
    }
    addChild(component) {
        this.children.push(component);
    }
    removeChild(component) {
        const index = this.children.indexOf(component);
        if (index !== -1) {
            this.children.splice(index, 1);
        }
    }
    clear() {
        this.children = [];
    }
    invalidate() {
        for (const child of this.children) {
            child.invalidate?.();
        }
    }
    render(width) {
        const lines = [];
        for (const child of this.children) {
            const rendered = child.render(width);
            for (let i = 0; i < rendered.length; i++)
                lines.push(rendered[i]);
        }
        return lines;
    }
}
/**
 * TUI - Main class for managing terminal UI with differential rendering
 */
export class TUI extends Container {
    constructor(terminal, showHardwareCursor) {
        super();
        this.previousLines = [];
        this.previousWidth = 0;
        this.previousHeight = 0;
        this.focusedComponent = null;
        this.inputListeners = new Set();
        this.renderRequested = false;
        this.cursorRow = 0; // Logical cursor row (end of rendered content)
        this.hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
        this.inputBuffer = ""; // Buffer for parsing terminal responses
        this.cellSizeQueryPending = false;
        this.showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1" || process.env.TERM_PROGRAM === "WarpTerminal";
        this.clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off)
        this.maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
        this.previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
        this.fullRedrawCount = 0;
        this.stopped = false;
        // Overlay stack for modal components rendered on top of base content
        this.focusOrderCounter = 0;
        this.overlayStack = [];
        this.terminal = terminal;
        if (showHardwareCursor !== undefined) {
            this.showHardwareCursor = showHardwareCursor;
        }
    }
    get fullRedraws() {
        return this.fullRedrawCount;
    }
    getShowHardwareCursor() {
        return this.showHardwareCursor;
    }
    setShowHardwareCursor(enabled) {
        if (this.showHardwareCursor === enabled)
            return;
        this.showHardwareCursor = enabled;
        if (!enabled) {
            this.terminal.hideCursor();
        }
        this.requestRender();
    }
    getClearOnShrink() {
        return this.clearOnShrink;
    }
    /**
     * Set whether to trigger full re-render when content shrinks.
     * When true (default), empty rows are cleared when content shrinks.
     * When false, empty rows remain (reduces redraws on slower terminals).
     */
    setClearOnShrink(enabled) {
        this.clearOnShrink = enabled;
    }
    setFocus(component) {
        // Clear focused flag on old component
        if (isFocusable(this.focusedComponent)) {
            this.focusedComponent.focused = false;
        }
        this.focusedComponent = component;
        // Set focused flag on new component
        if (isFocusable(component)) {
            component.focused = true;
        }
    }
    /**
     * Show an overlay component with configurable positioning and sizing.
     * Returns a handle to control the overlay's visibility.
     */
    showOverlay(component, options) {
        const entry = {
            component,
            options,
            preFocus: this.focusedComponent,
            hidden: false,
            focusOrder: ++this.focusOrderCounter,
        };
        this.overlayStack.push(entry);
        // Only focus if overlay is actually visible
        if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
            this.setFocus(component);
        }
        this.terminal.hideCursor();
        this.requestRender();
        // Return handle for controlling this overlay
        return {
            hide: () => {
                const index = this.overlayStack.indexOf(entry);
                if (index !== -1) {
                    this.overlayStack.splice(index, 1);
                    // Restore focus if this overlay had focus
                    if (this.focusedComponent === component) {
                        const topVisible = this.getTopmostVisibleOverlay();
                        this.setFocus(topVisible?.component ?? entry.preFocus);
                    }
                    if (this.overlayStack.length === 0)
                        this.terminal.hideCursor();
                    this.requestRender();
                }
            },
            setHidden: (hidden) => {
                if (entry.hidden === hidden)
                    return;
                entry.hidden = hidden;
                // Update focus when hiding/showing
                if (hidden) {
                    // If this overlay had focus, move focus to next visible or preFocus
                    if (this.focusedComponent === component) {
                        const topVisible = this.getTopmostVisibleOverlay();
                        this.setFocus(topVisible?.component ?? entry.preFocus);
                    }
                }
                else {
                    // Restore focus to this overlay when showing (if it's actually visible)
                    if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
                        entry.focusOrder = ++this.focusOrderCounter;
                        this.setFocus(component);
                    }
                }
                this.requestRender();
            },
            isHidden: () => entry.hidden,
            focus: () => {
                if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry))
                    return;
                if (this.focusedComponent !== component) {
                    this.setFocus(component);
                }
                entry.focusOrder = ++this.focusOrderCounter;
                this.requestRender();
            },
            unfocus: () => {
                if (this.focusedComponent !== component)
                    return;
                const topVisible = this.getTopmostVisibleOverlay();
                this.setFocus(topVisible && topVisible !== entry ? topVisible.component : entry.preFocus);
                this.requestRender();
            },
            isFocused: () => this.focusedComponent === component,
        };
    }
    /** Hide the topmost overlay and restore previous focus. */
    hideOverlay() {
        const overlay = this.overlayStack.pop();
        if (!overlay)
            return;
        if (this.focusedComponent === overlay.component) {
            // Find topmost visible overlay, or fall back to preFocus
            const topVisible = this.getTopmostVisibleOverlay();
            this.setFocus(topVisible?.component ?? overlay.preFocus);
        }
        if (this.overlayStack.length === 0)
            this.terminal.hideCursor();
        this.requestRender();
    }
    /** Check if there are any visible overlays */
    hasOverlay() {
        return this.overlayStack.some((o) => this.isOverlayVisible(o));
    }
    /** Check if an overlay entry is currently visible */
    isOverlayVisible(entry) {
        return isOverlayEntryVisible(entry, this.terminal.columns, this.terminal.rows);
    }
    /** Find the topmost visible capturing overlay, if any */
    getTopmostVisibleOverlay() {
        for (let i = this.overlayStack.length - 1; i >= 0; i--) {
            if (this.overlayStack[i].options?.nonCapturing)
                continue;
            if (this.isOverlayVisible(this.overlayStack[i])) {
                return this.overlayStack[i];
            }
        }
        return undefined;
    }
    invalidate() {
        super.invalidate();
        for (const overlay of this.overlayStack)
            overlay.component.invalidate?.();
    }
    start() {
        this.stopped = false;
        this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
        this.terminal.hideCursor();
        this.queryCellSize();
        this.requestRender();
    }
    addInputListener(listener) {
        this.inputListeners.add(listener);
        return () => {
            this.inputListeners.delete(listener);
        };
    }
    removeInputListener(listener) {
        this.inputListeners.delete(listener);
    }
    queryCellSize() {
        // Only query if terminal supports images (cell size is only used for image rendering)
        if (!getCapabilities().images) {
            return;
        }
        // Query terminal for cell size in pixels: CSI 16 t
        // Response format: CSI 6 ; height ; width t
        this.cellSizeQueryPending = true;
        this.terminal.write("\x1b[16t");
    }
    stop() {
        this.stopped = true;
        // Dispose all overlays to stop any running timers
        for (const entry of this.overlayStack) {
            if ("dispose" in entry.component && typeof entry.component.dispose === "function") {
                entry.component.dispose();
            }
        }
        this.overlayStack = [];
        // Move cursor to the end of the content to prevent overwriting/artifacts on exit
        if (this.previousLines.length > 0) {
            const targetRow = this.previousLines.length; // Line after the last content
            const lineDiff = targetRow - this.hardwareCursorRow;
            if (lineDiff > 0) {
                this.terminal.write(`\x1b[${lineDiff}B`);
            }
            else if (lineDiff < 0) {
                this.terminal.write(`\x1b[${-lineDiff}A`);
            }
            this.terminal.write("\r\n");
        }
        this.terminal.showCursor();
        this.terminal.stop();
    }
    requestRender(force = false) {
        if (force) {
            this.previousLines = [];
            this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
            this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
            this.cursorRow = 0;
            this.hardwareCursorRow = 0;
            this.maxLinesRendered = 0;
            this.previousViewportTop = 0;
        }
        if (this.renderRequested)
            return;
        this.renderRequested = true;
        process.nextTick(() => {
            this.renderRequested = false;
            this.doRender();
        });
    }
    handleInput(data) {
        if (this.inputListeners.size > 0) {
            let current = data;
            for (const listener of this.inputListeners) {
                const result = listener(current);
                if (result?.consume) {
                    return;
                }
                if (result?.data !== undefined) {
                    current = result.data;
                }
            }
            if (current.length === 0) {
                return;
            }
            data = current;
        }
        // If we're waiting for cell size response, buffer input and parse
        if (this.cellSizeQueryPending) {
            this.inputBuffer += data;
            const filtered = this.parseCellSizeResponse();
            if (filtered.length === 0)
                return;
            data = filtered;
        }
        // Global debug key handler (Shift+Ctrl+D)
        if (matchesKey(data, "shift+ctrl+d") && this.onDebug) {
            this.onDebug();
            return;
        }
        // If focused component is an overlay, verify it's still visible
        // (visibility can change due to terminal resize or visible() callback)
        const focusedOverlay = this.overlayStack.find((o) => o.component === this.focusedComponent);
        if (focusedOverlay && !this.isOverlayVisible(focusedOverlay)) {
            // Focused overlay is no longer visible, redirect to topmost visible overlay
            const topVisible = this.getTopmostVisibleOverlay();
            if (topVisible) {
                this.setFocus(topVisible.component);
            }
            else {
                // No visible overlays, restore to preFocus
                this.setFocus(focusedOverlay.preFocus);
            }
        }
        // Pass input to focused component (including Ctrl+C)
        // The focused component can decide how to handle Ctrl+C
        if (this.focusedComponent?.handleInput) {
            // Filter out key release events unless component opts in
            if (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) {
                return;
            }
            this.focusedComponent.handleInput(data);
            this.requestRender();
        }
    }
    parseCellSizeResponse() {
        // Response format: ESC [ 6 ; height ; width t
        // Match the response pattern
        const responsePattern = /\x1b\[6;(\d+);(\d+)t/;
        const match = this.inputBuffer.match(responsePattern);
        if (match) {
            const heightPx = parseInt(match[1], 10);
            const widthPx = parseInt(match[2], 10);
            if (heightPx > 0 && widthPx > 0) {
                setCellDimensions({ widthPx, heightPx });
                // Invalidate all components so images re-render with correct dimensions
                this.invalidate();
                this.requestRender();
            }
            // Remove the response from buffer
            this.inputBuffer = this.inputBuffer.replace(responsePattern, "");
            this.cellSizeQueryPending = false;
        }
        // Check if we have a partial cell size response starting (wait for more data)
        // Patterns that could be incomplete cell size response: \x1b, \x1b[, \x1b[6, \x1b[6;...(no t yet)
        const partialCellSizePattern = /\x1b(\[6?;?[\d;]*)?$/;
        if (partialCellSizePattern.test(this.inputBuffer)) {
            // Check if it's actually a complete different escape sequence (ends with a letter)
            // Cell size response ends with 't', Kitty keyboard ends with 'u', arrows end with A-D, etc.
            const lastChar = this.inputBuffer[this.inputBuffer.length - 1];
            if (!/[a-zA-Z~]/.test(lastChar)) {
                // Doesn't end with a terminator, might be incomplete - wait for more
                return "";
            }
        }
        // No cell size response found, return buffered data as user input
        const result = this.inputBuffer;
        this.inputBuffer = "";
        this.cellSizeQueryPending = false; // Give up waiting
        return result;
    }
    doRender() {
        if (this.stopped)
            return;
        const width = this.terminal.columns;
        const height = this.terminal.rows;
        let viewportTop = Math.max(0, this.maxLinesRendered - height);
        let prevViewportTop = this.previousViewportTop;
        let hardwareCursorRow = this.hardwareCursorRow;
        const computeLineDiff = (targetRow) => {
            const currentScreenRow = hardwareCursorRow - prevViewportTop;
            const targetScreenRow = targetRow - viewportTop;
            return targetScreenRow - currentScreenRow;
        };
        // Render all components to get new lines
        let newLines = this.render(width);
        // Composite overlays into the rendered lines (before differential compare)
        if (this.overlayStack.length > 0) {
            newLines = compositeOverlays(newLines, this.overlayStack, width, height, this.maxLinesRendered);
        }
        // Extract cursor position before applying line resets (marker must be found first)
        const cursorPos = extractCursorPosition(newLines, height);
        newLines = applyLineResets(newLines);
        // Width or height changed - need full re-render
        const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
        const heightChanged = this.previousHeight !== 0 && this.previousHeight !== height;
        // Helper to clear scrollback and viewport and render all new lines
        const fullRender = (clear) => {
            this.fullRedrawCount += 1;
            let buffer = "\x1b[?2026h"; // Begin synchronized output
            if (clear)
                buffer += "\x1b[2J\x1b[H"; // Clear screen and home (no scrollback clear — preserves view position)
            for (let i = 0; i < newLines.length; i++) {
                if (i > 0)
                    buffer += "\r\n";
                let line = newLines[i];
                if (!isImageLine(line) && visibleWidth(line) > width) {
                    line = truncateToWidth(line, width);
                }
                buffer += line;
            }
            buffer += "\x1b[?2026l"; // End synchronized output
            this.terminal.write(buffer);
            this.cursorRow = Math.max(0, newLines.length - 1);
            this.hardwareCursorRow = this.cursorRow;
            // Reset max lines when clearing, otherwise track growth
            if (clear) {
                this.maxLinesRendered = newLines.length;
            }
            else {
                this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
            }
            this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
            this.positionHardwareCursor(cursorPos, newLines.length);
            this.previousLines = newLines;
            this.previousWidth = width;
            this.previousHeight = height;
        };
        const debugRedraw = process.env.PI_DEBUG_REDRAW === "1";
        const logRedraw = (reason) => {
            if (!debugRedraw)
                return;
            const logPath = path.join(os.homedir(), ".pi", "agent", "pi-debug.log");
            const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
            fs.appendFileSync(logPath, msg);
        };
        // First render - just output everything without clearing (assumes clean screen)
        if (this.previousLines.length === 0 && !widthChanged && !heightChanged) {
            logRedraw("first render");
            fullRender(false);
            return;
        }
        // Width or height changed - full re-render
        if (widthChanged || heightChanged) {
            logRedraw(`terminal size changed (${this.previousWidth}x${this.previousHeight} -> ${width}x${height})`);
            fullRender(true);
            return;
        }
        // Content shrunk below the working area and no overlays - re-render to clear empty rows
        // (overlays need the padding, so only do this when no overlays are active)
        // Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
        if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
            logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
            fullRender(true);
            return;
        }
        // Find first and last changed lines
        let firstChanged = -1;
        let lastChanged = -1;
        const maxLines = Math.max(newLines.length, this.previousLines.length);
        for (let i = 0; i < maxLines; i++) {
            const oldLine = i < this.previousLines.length ? this.previousLines[i] : "";
            const newLine = i < newLines.length ? newLines[i] : "";
            if (oldLine !== newLine) {
                if (firstChanged === -1) {
                    firstChanged = i;
                }
                lastChanged = i;
            }
        }
        const appendedLines = newLines.length > this.previousLines.length;
        if (appendedLines) {
            if (firstChanged === -1) {
                firstChanged = this.previousLines.length;
            }
            lastChanged = newLines.length - 1;
        }
        const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0;
        // No changes - but still need to update hardware cursor position if it moved
        if (firstChanged === -1) {
            this.positionHardwareCursor(cursorPos, newLines.length);
            this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
            this.previousHeight = height;
            return;
        }
        // All changes are in deleted lines (nothing to render, just clear)
        if (firstChanged >= newLines.length) {
            if (this.previousLines.length > newLines.length) {
                let buffer = "\x1b[?2026h";
                // Move to end of new content (clamp to 0 for empty content)
                const targetRow = Math.max(0, newLines.length - 1);
                const lineDiff = computeLineDiff(targetRow);
                if (lineDiff > 0)
                    buffer += `\x1b[${lineDiff}B`;
                else if (lineDiff < 0)
                    buffer += `\x1b[${-lineDiff}A`;
                buffer += "\r";
                // Clear extra lines without scrolling
                const extraLines = this.previousLines.length - newLines.length;
                if (extraLines > height) {
                    logRedraw(`extraLines > height (${extraLines} > ${height})`);
                    fullRender(true);
                    return;
                }
                if (extraLines > 0) {
                    buffer += "\x1b[1B";
                }
                for (let i = 0; i < extraLines; i++) {
                    buffer += "\r\x1b[2K";
                    if (i < extraLines - 1)
                        buffer += "\x1b[1B";
                }
                if (extraLines > 0) {
                    buffer += `\x1b[${extraLines}A`;
                }
                buffer += "\x1b[?2026l";
                this.terminal.write(buffer);
                this.cursorRow = targetRow;
                this.hardwareCursorRow = targetRow;
            }
            this.positionHardwareCursor(cursorPos, newLines.length);
            this.previousLines = newLines;
            this.previousWidth = width;
            this.previousHeight = height;
            this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
            return;
        }
        // Check if firstChanged is above what was previously visible
        // Use previousLines.length (not maxLinesRendered) to avoid false positives after content shrinks
        const previousContentViewportTop = Math.max(0, this.previousLines.length - height);
        if (firstChanged < previousContentViewportTop) {
            // First change is above previous viewport - need full re-render
            logRedraw(`firstChanged < viewportTop (${firstChanged} < ${previousContentViewportTop})`);
            fullRender(true);
            return;
        }
        // Render from first changed line to end
        // Build buffer with all updates wrapped in synchronized output
        let buffer = "\x1b[?2026h"; // Begin synchronized output
        const prevViewportBottom = prevViewportTop + height - 1;
        const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
        if (moveTargetRow > prevViewportBottom) {
            const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
            const moveToBottom = height - 1 - currentScreenRow;
            if (moveToBottom > 0) {
                buffer += `\x1b[${moveToBottom}B`;
            }
            const scroll = moveTargetRow - prevViewportBottom;
            buffer += "\r\n".repeat(scroll);
            prevViewportTop += scroll;
            viewportTop += scroll;
            hardwareCursorRow = moveTargetRow;
        }
        // Move cursor to first changed line (use hardwareCursorRow for actual position)
        const lineDiff = computeLineDiff(moveTargetRow);
        if (lineDiff > 0) {
            buffer += `\x1b[${lineDiff}B`; // Move down
        }
        else if (lineDiff < 0) {
            buffer += `\x1b[${-lineDiff}A`; // Move up
        }
        buffer += appendStart ? "\r\n" : "\r"; // Move to column 0
        // Only render changed lines (firstChanged to lastChanged), not all lines to end
        // This reduces flicker when only a single line changes (e.g., spinner animation)
        const renderEnd = Math.min(lastChanged, newLines.length - 1);
        for (let i = firstChanged; i <= renderEnd; i++) {
            if (i > firstChanged)
                buffer += "\r\n";
            buffer += "\x1b[2K"; // Clear current line
            let line = newLines[i];
            const isImage = isImageLine(line);
            if (!isImage && visibleWidth(line) > width) {
                line = truncateToWidth(line, width);
            }
            buffer += line;
        }
        // Track where cursor ended up after rendering
        let finalCursorRow = renderEnd;
        // If we had more lines before, clear them and move cursor back
        if (this.previousLines.length > newLines.length) {
            // Move to end of new content first if we stopped before it
            if (renderEnd < newLines.length - 1) {
                const moveDown = newLines.length - 1 - renderEnd;
                buffer += `\x1b[${moveDown}B`;
                finalCursorRow = newLines.length - 1;
            }
            const extraLines = this.previousLines.length - newLines.length;
            for (let i = newLines.length; i < this.previousLines.length; i++) {
                buffer += "\r\n\x1b[2K";
            }
            // Move cursor back to end of new content
            buffer += `\x1b[${extraLines}A`;
        }
        buffer += "\x1b[?2026l"; // End synchronized output
        if (process.env.PI_TUI_DEBUG === "1") {
            const debugDir = path.join(os.tmpdir(), "tui");
            fs.mkdirSync(debugDir, { recursive: true });
            const debugPath = path.join(debugDir, `render-${Date.now()}-${Math.random().toString(36).slice(2)}.log`);
            const debugData = [
                `firstChanged: ${firstChanged}`,
                `viewportTop: ${viewportTop}`,
                `cursorRow: ${this.cursorRow}`,
                `height: ${height}`,
                `lineDiff: ${lineDiff}`,
                `hardwareCursorRow: ${hardwareCursorRow}`,
                `renderEnd: ${renderEnd}`,
                `finalCursorRow: ${finalCursorRow}`,
                `cursorPos: ${JSON.stringify(cursorPos)}`,
                `newLines.length: ${newLines.length}`,
                `previousLines.length: ${this.previousLines.length}`,
                "",
                "=== newLines ===",
                JSON.stringify(newLines, null, 2),
                "",
                "=== previousLines ===",
                JSON.stringify(this.previousLines, null, 2),
                "",
                "=== buffer ===",
                JSON.stringify(buffer),
            ].join("\n");
            fs.writeFileSync(debugPath, debugData);
        }
        // Write entire buffer at once
        this.terminal.write(buffer);
        // Track cursor position for next render
        // cursorRow tracks end of content (for viewport calculation)
        // hardwareCursorRow tracks actual terminal cursor position (for movement)
        this.cursorRow = Math.max(0, newLines.length - 1);
        this.hardwareCursorRow = finalCursorRow;
        // Track terminal's working area (grows but doesn't shrink unless cleared)
        this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
        this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
        // Position hardware cursor for IME
        this.positionHardwareCursor(cursorPos, newLines.length);
        this.previousLines = newLines;
        this.previousWidth = width;
        this.previousHeight = height;
    }
    /**
     * Position the hardware cursor for IME candidate window.
     * @param cursorPos The cursor position extracted from rendered output, or null
     * @param totalLines Total number of rendered lines
     */
    positionHardwareCursor(cursorPos, totalLines) {
        if (!cursorPos || totalLines <= 0) {
            this.terminal.hideCursor();
            return;
        }
        // Clamp cursor position to valid range
        const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
        const targetCol = Math.max(0, cursorPos.col);
        // Move cursor from current position to target
        const rowDelta = targetRow - this.hardwareCursorRow;
        let buffer = "";
        if (rowDelta > 0) {
            buffer += `\x1b[${rowDelta}B`; // Move down
        }
        else if (rowDelta < 0) {
            buffer += `\x1b[${-rowDelta}A`; // Move up
        }
        // Move to absolute column (1-indexed)
        buffer += `\x1b[${targetCol + 1}G`;
        if (buffer) {
            this.terminal.write(buffer);
        }
        this.hardwareCursorRow = targetRow;
        if (this.showHardwareCursor) {
            this.terminal.showCursor();
        }
        else {
            this.terminal.hideCursor();
        }
    }
}
//# sourceMappingURL=tui.js.map