askeroo
Version:
A modern CLI prompt library with flow control, history navigation, and conditional prompts
248 lines • 12.7 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import React, { useState, useEffect } from "react";
import { Text, Box, useInput, Newline } from "ink";
import { createPrompt } from "../../core/registry.js";
import { useFieldReset } from "../../hooks/use-auto-submit.js";
import { isMarkdownString, parseMarkdown } from "../../utils/markdown.js";
// Enhanced confirm input plugin with custom options support
export const confirm = createPrompt({
type: "confirm",
// autoSubmit: false (default) - Requires user interaction
component: ({ node, options, events }) => {
// Use label if provided, fallback to message for compatibility
const displayMessage = options.label || options.message || "Confirm?";
// Default options if none provided (memoized to prevent re-creation)
const confirmOptions = React.useMemo(() => {
let opts = options.options || [
{ value: true, label: "Yes" },
{ value: false, label: "No" },
];
// Reorder options so the one matching initialValue is last
if (options.initialValue !== undefined) {
const initialIndex = opts.findIndex((opt) => opt.value === options.initialValue);
if (initialIndex >= 0 && initialIndex !== opts.length - 1) {
// Move the initial value option to the end
opts = [
...opts.slice(0, initialIndex),
...opts.slice(initialIndex + 1),
opts[initialIndex],
];
}
}
return opts;
}, [options.options, options.initialValue]);
const [selectedIndex, setSelectedIndex] = useState(() => {
if (options.initialValue !== undefined) {
// After reordering, the initial value is always at the last position
const index = confirmOptions.findIndex((option) => option.value === options.initialValue);
return index >= 0 ? index : 0;
}
return 0;
});
const [submitted, setSubmitted] = useState(false);
const [validationError, setValidationError] = useState(null);
const disabled = node.state === "disabled";
useFieldReset(disabled, submitted, setSubmitted);
useEffect(() => {
if (options.initialValue === undefined)
return;
const index = confirmOptions.findIndex((opt) => opt.value === options.initialValue);
if (index >= 0)
setSelectedIndex(index);
}, [options.initialValue, confirmOptions]);
const runValidation = async (val) => {
if (!events.onValidate || node.state !== "active") {
setValidationError(null);
return true;
}
try {
const result = await events.onValidate(val);
setValidationError(result);
return result === null;
}
catch {
setValidationError("Validation error occurred");
return false;
}
};
useEffect(() => {
if (!events.onHintChange)
return;
// Only show hint if there's back navigation available
const hasHint = node.state === "active" &&
!node.isFirstRootPrompt &&
node.allowBack;
events.onHintChange(hasHint ? (_jsxs(_Fragment, { children: [_jsx(Newline, {}), _jsx(Text, { color: "yellow", children: "escape" }), " go back"] })) : null);
}, [
node.state,
node.isFirstRootPrompt,
node.allowBack,
events.onHintChange,
]);
useInput(async (input, key) => {
if (submitted || node.state !== "active")
return;
// Static group navigation
if (node.flow === "static" && node.enableArrowNavigation) {
const val = confirmOptions[selectedIndex].value;
if ((key.downArrow && !node.isLastInGroup) ||
(key.upArrow && !node.isFirstInGroup)) {
if (!(await runValidation(val)))
return;
setSubmitted(true);
// Call user's onSubmit callback if provided and use return value if any
let finalValue = val;
if (options.onSubmit) {
const result = options.onSubmit(val);
if (result !== undefined) {
finalValue = result;
}
}
events.onSubmit?.(key.downArrow
? finalValue
: { __preserveAndBack: true, value: finalValue });
return;
}
if (key.downArrow || key.upArrow)
return;
}
// Escape for static groups
if (key.escape && node.flow === "static") {
if (node.enableArrowNavigation) {
const val = confirmOptions[selectedIndex].value;
if (!node.isFirstInGroup) {
if (!(await runValidation(val)))
return;
setSubmitted(true);
// Call user's onSubmit callback if provided and use return value if any
let finalValue = val;
if (options.onSubmit) {
const result = options.onSubmit(val);
if (result !== undefined) {
finalValue = result;
}
}
events.onSubmit?.({
__preserveAndBack: true,
value: finalValue,
});
}
else {
setSubmitted(true);
// Note: For __clearGroupAndBack, we don't call user's onSubmit
// as this is a special navigation case
events.onSubmit?.({ __clearGroupAndBack: true });
}
}
else if (node.allowBack && events.onBack) {
events.onBack();
}
return;
}
if (key.return) {
const val = confirmOptions[selectedIndex].value;
if (!(await runValidation(val)))
return;
setSubmitted(true);
// Call user's onSubmit callback if provided and use return value if any
let finalValue = val;
if (options.onSubmit) {
const result = options.onSubmit(val);
if (result !== undefined) {
finalValue = result;
}
}
events.onSubmit?.(finalValue);
return;
}
if (key.escape &&
node.flow !== "static" &&
node.allowBack &&
events.onBack) {
events.onBack();
return;
}
// Y/N shortcuts for default options
if (!options.options &&
(input.toLowerCase() === "y" || input.toLowerCase() === "n")) {
const val = input.toLowerCase() === "y";
const idx = confirmOptions.findIndex((opt) => opt.value === val);
setSelectedIndex(idx);
if (!(await runValidation(val)))
return;
setSubmitted(true);
// Call user's onSubmit callback if provided and use return value if any
let finalValue = val;
if (options.onSubmit) {
const result = options.onSubmit(val);
if (result !== undefined) {
finalValue = result;
}
}
events.onSubmit?.(finalValue);
return;
}
// Ctrl+A to jump to first option
if (key.ctrl && input === "a") {
setSelectedIndex(0);
return;
}
// Ctrl+E to jump to last option
if (key.ctrl && input === "e") {
setSelectedIndex(confirmOptions.length - 1);
return;
}
// Arrow navigation
const allowLoop = options.allowLoop ?? false;
if (key.leftArrow || key.upArrow) {
setSelectedIndex(allowLoop && selectedIndex === 0
? confirmOptions.length - 1
: Math.max(0, selectedIndex - 1));
return;
}
if (key.rightArrow || key.downArrow) {
setSelectedIndex(allowLoop && selectedIndex === confirmOptions.length - 1
? 0
: Math.min(confirmOptions.length - 1, selectedIndex + 1));
return;
}
}, { isActive: node.state === "active" && !submitted });
const renderMessage = () => {
if (!displayMessage)
return null;
if (isMarkdownString(displayMessage)) {
return (_jsx(Box, { flexDirection: "column", children: parseMarkdown(displayMessage.content, displayMessage.theme).map((el, i) => (_jsx(Box, { children: el }, i))) }));
}
return _jsx(Text, { children: displayMessage });
};
if (node.state === "completed") {
const val = node.completedValue !== undefined
? node.completedValue
: confirmOptions[selectedIndex]?.value;
const opt = confirmOptions.find((o) => o.value === val);
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: options.shortLabel || options.label }), _jsx(Text, { color: "blue", children: opt ? opt.label : String(val) })] }));
}
if (node.state === "disabled") {
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: options.label }), _jsx(Text, { dimColor: true, color: "gray", children: "..." })] }));
}
// Active state - show options
return (_jsxs(Box, { flexDirection: "column", children: [renderMessage(), options.hintPosition === "side" ? (
// Side layout - two columns, hint only for selected option
_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", children: confirmOptions.map((option, index) => (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: index === selectedIndex
? "cyan"
: option.color || "gray", children: [index === selectedIndex ? "●" : "○", " ", option.label] }) }, String(option.value)))) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: confirmOptions.map((option, index) => (_jsx(Text, { color: "gray", children: index === selectedIndex && option.hint
? option.hint
: "" }, String(option.value)))) })] })) : (
// Original horizontal layout for inline and bottom
_jsx(Box, { flexDirection: "row", gap: 2, children: confirmOptions.map((option, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: index === selectedIndex
? "cyan"
: option.color || "gray", children: [index === selectedIndex ? "●" : "○", " ", option.label] }), options.hintPosition === "inline" &&
index === selectedIndex &&
option.hint && (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", option.hint] }))] }, String(option.value)))) })), options.hintPosition === "bottom" &&
(() => {
const selectedOption = confirmOptions[selectedIndex];
return selectedOption?.hint ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: selectedOption.hint }) })) : null;
})(), validationError && (_jsx(Box, { children: _jsx(Text, { color: "red", children: validationError }) }))] }));
},
});
//# sourceMappingURL=index.js.map