import React, { useState } from "react"; import { Box, Text, useInput } from "ink"; import { useTerminalTheme } from "../hooks/useTerminalTheme"; function ActiveLine(props: { line: string; cursorCol: number; placeholder?: string; mutedColor?: string }) { if (props.line.length === 0) { return ( {props.placeholder ? {props.placeholder} : null} ); } const before = props.line.slice(0, props.cursorCol); const atCursor = props.cursorCol < props.line.length ? props.line[props.cursorCol] : " "; const after = props.cursorCol < props.line.length ? props.line.slice(props.cursorCol + 1) : ""; return ( {before} {atCursor} {after} ); } /** * The one persistent composer. Enter submits; Alt+Enter (and Shift+Enter where * the terminal reports it distinctly — not universally reliable in raw TTY * mode, see USER_GUIDE) inserts a newline for multiline input. Ctrl-chords are * intentionally ignored here — they're owned by useGlobalShortcuts so both * hooks can stay active without conflicting. */ export function CommandInput(props: { history: string[]; placeholder?: string; onSubmit: (value: string) => void; /** Typing "/" as the very first character opens the palette immediately, without waiting for Enter. */ onSlashTrigger?: () => void; isActive?: boolean; }) { const theme = useTerminalTheme(); const [lines, setLines] = useState([""]); const [cursorRow, setCursorRow] = useState(0); const [cursorCol, setCursorCol] = useState(0); const [historyIndex, setHistoryIndex] = useState(undefined); function replaceLine(row: number, text: string): void { setLines((current) => current.map((line, index) => (index === row ? text : line))); } function resetBuffer(text: string): void { const nextLines = text.length ? text.split("\n") : [""]; setLines(nextLines); setCursorRow(nextLines.length - 1); setCursorCol(nextLines[nextLines.length - 1].length); } useInput( (input, key) => { if (key.ctrl || key.escape) { return; } const line = lines[cursorRow] ?? ""; if (key.return && (key.meta || key.shift)) { const before = line.slice(0, cursorCol); const after = line.slice(cursorCol); const nextLines = [...lines.slice(0, cursorRow), before, after, ...lines.slice(cursorRow + 1)]; setLines(nextLines); setCursorRow(cursorRow + 1); setCursorCol(0); return; } if (key.return) { const value = lines.join("\n"); if (value.trim()) { props.onSubmit(value); } resetBuffer(""); setHistoryIndex(undefined); return; } if (key.upArrow) { if (lines.length > 1 && cursorRow > 0) { setCursorRow(cursorRow - 1); return; } if (props.history.length === 0) return; const nextIndex = historyIndex === undefined ? props.history.length - 1 : Math.max(0, historyIndex - 1); setHistoryIndex(nextIndex); resetBuffer(props.history[nextIndex] ?? ""); return; } if (key.downArrow) { if (lines.length > 1 && cursorRow < lines.length - 1) { setCursorRow(cursorRow + 1); return; } if (historyIndex === undefined) return; const nextIndex = historyIndex + 1; if (nextIndex >= props.history.length) { setHistoryIndex(undefined); resetBuffer(""); return; } setHistoryIndex(nextIndex); resetBuffer(props.history[nextIndex] ?? ""); return; } if (key.leftArrow) { if (cursorCol > 0) { setCursorCol(cursorCol - 1); } else if (cursorRow > 0) { setCursorRow(cursorRow - 1); setCursorCol(lines[cursorRow - 1].length); } return; } if (key.rightArrow) { if (cursorCol < line.length) { setCursorCol(cursorCol + 1); } else if (cursorRow < lines.length - 1) { setCursorRow(cursorRow + 1); setCursorCol(0); } return; } // Ink names the physical Backspace key "delete" (it sends \x7f, not \x08 — // literal "backspace" per Ink's parser is Ctrl+H, which real keyboards don't // send). Treating them as the same action matches TextInputPrompt.tsx and is // what every terminal's actual Backspace key needs to do here. if (key.backspace || key.delete) { if (cursorCol > 0) { replaceLine(cursorRow, line.slice(0, cursorCol - 1) + line.slice(cursorCol)); setCursorCol(cursorCol - 1); } else if (cursorRow > 0) { const prevLine = lines[cursorRow - 1]; const merged = prevLine + line; const nextLines = [...lines.slice(0, cursorRow - 1), merged, ...lines.slice(cursorRow + 1)]; setLines(nextLines); setCursorRow(cursorRow - 1); setCursorCol(prevLine.length); } return; } if (input === "/" && lines.length === 1 && lines[0] === "" && props.onSlashTrigger) { props.onSlashTrigger(); return; } if (input && !key.tab) { replaceLine(cursorRow, line.slice(0, cursorCol) + input + line.slice(cursorCol)); setCursorCol(cursorCol + input.length); setHistoryIndex(undefined); } }, { isActive: props.isActive ?? true }, ); return ( {"❯ "} {lines.map((line, index) => index === cursorRow ? ( ) : ( {line} ), )} ); }