UNPKG

askeroo

Version:

A modern CLI prompt library with flow control, history navigation, and conditional prompts

248 lines 12.7 kB
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