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}
),
)}
);
}