askeroo
Version:
A modern CLI prompt library with flow control, history navigation, and conditional prompts
185 lines • 8.2 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { useEffect, useState } from "react";
import { Box, Text, useInput } from "ink";
import chalk from "chalk";
import { spinnerStore } from "./spinner-store.js";
// Main component for the spinner plugin
export const SpinnerDisplay = ({ node, options, events, }) => {
const [spinnerFrame, setSpinnerFrame] = useState(0);
// Use the spinner ID from options
const spinnerId = options.spinnerId ||
`spinner_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
// Subscribe to the spinner store - auto-updates on any store change
const store = spinnerStore.use();
// Extract data for this spinner
const spinnerState = store.spinners.get(spinnerId) || { status: "idle" };
// Animated spinner frames
const spinnerFrames = ["⠂", "-", "–", "—", "–", "-"];
// Block all input during spinner execution to prevent escape sequences from showing
useInput(() => { }, { isActive: spinnerState.status === "running" });
// Animate spinner only when running
useEffect(() => {
if (spinnerState.status !== "running") {
return;
}
const interval = setInterval(() => {
setSpinnerFrame((prev) => (prev + 1) % spinnerFrames.length);
}, 150);
return () => clearInterval(interval);
}, [spinnerState.status]);
const getLabel = (status) => {
// If currentLabel is set in state, use it
if (spinnerState.currentLabel) {
return spinnerState.currentLabel;
}
// Otherwise use the original label logic
if (typeof options.label === "string")
return options.label;
const labelObj = options.label;
const fallback = labelObj?.idle || "Loading";
return ({
idle: labelObj?.idle || fallback,
running: labelObj?.running || fallback,
paused: labelObj?.paused || fallback,
stopped: labelObj?.stopped || fallback,
}[status] || fallback);
};
const getSymbol = (status) => {
// Check if currentSymbol is set in state
const customSymbol = spinnerState.currentSymbol || options.style?.symbol;
if (customSymbol) {
// If it's a string, use it for all states
if (typeof customSymbol === "string") {
return customSymbol;
}
else {
// It's a SpinnerSymbol object
const symbolObj = customSymbol;
// Handle running symbol (can be string or array)
if (status === "running" && symbolObj.running) {
if (Array.isArray(symbolObj.running)) {
return symbolObj.running[spinnerFrame % symbolObj.running.length];
}
return symbolObj.running;
}
// Handle other statuses
if (status === "idle" && symbolObj.idle)
return symbolObj.idle;
if (status === "paused" && symbolObj.paused)
return symbolObj.paused;
if (status === "stopped" && symbolObj.stopped)
return symbolObj.stopped;
}
}
// Fall back to default symbols
return ({
idle: "□",
running: spinnerFrames[spinnerFrame],
paused: "●",
stopped: "■",
}[status] || " ");
};
const applyStyles = (text) => {
// Get current style from state or fall back to options
const style = spinnerState.currentStyle || options.style;
if (!style ||
(!style.color && !style.bgColor && style.dim === undefined)) {
return text;
}
let styledText = text;
// Apply color
if (style.color && chalk[style.color]) {
const colorFn = chalk[style.color];
if (typeof colorFn === "function") {
styledText = colorFn(styledText);
}
}
// Apply background color
if (style.bgColor) {
const bgKey = `bg${style.bgColor
.charAt(0)
.toUpperCase()}${style.bgColor.slice(1)}`;
const bgFn = chalk[bgKey];
if (typeof bgFn === "function") {
styledText = bgFn(styledText);
}
}
// Apply dim
if (style.dim) {
styledText = chalk.dim(styledText);
}
return styledText;
};
// Auto-submit immediately when active (like streams do)
// This allows multiple spinners to run concurrently without blocking each other
// Use deferCompletion to prevent the node from being marked as completed until stopped
useEffect(() => {
if (node.state === "active" && events.onSubmit) {
// Submit immediately to allow runtime to continue, but defer completion
events.onSubmit({ type: "auto", deferCompletion: true });
}
}, [node.state, events.onSubmit]);
// Mark node as completed when spinner is stopped
useEffect(() => {
if (spinnerState.status === "stopped" && events.onComplete) {
// If there's a submitDelay, wait for it before marking as complete
if (options.submitDelay && options.submitDelay > 0) {
const timer = setTimeout(() => {
events.onComplete(); // PluginWrapper handles the promptId
}, options.submitDelay);
return () => clearTimeout(timer);
}
else {
// Mark as complete immediately
events.onComplete(); // PluginWrapper handles the promptId
}
}
}, [spinnerState.status, events.onComplete, options.submitDelay]);
// Track whether we should hide after delay (for hideOnCompletion with submitDelay)
const [shouldHideAfterDelay, setShouldHideAfterDelay] = useState(false);
// Handle delayed hiding when spinner stops with submitDelay
useEffect(() => {
// Only applies when hideOnCompletion is true and there's a submitDelay
if (!options.hideOnCompletion ||
!options.submitDelay ||
options.submitDelay === 0) {
return;
}
// When spinner stops, wait for submitDelay then trigger hiding
if (spinnerState.status === "stopped") {
const timer = setTimeout(() => {
setShouldHideAfterDelay(true);
}, options.submitDelay);
return () => clearTimeout(timer);
}
}, [spinnerState.status, options.submitDelay, options.hideOnCompletion]);
// Don't render if we're completed and should hide
// Hide when spinner is stopped and hideOnCompletion is true
// This works both in normal flows (when node.state === "completed")
// and in onCancel flows (where node might stay active)
if (options.hideOnCompletion && spinnerState.status === "stopped") {
// No delay: hide immediately
if (!options.submitDelay || options.submitDelay === 0) {
return null;
}
// With delay: hide after delay timer completes
if (shouldHideAfterDelay) {
return null;
}
}
// Don't render if we're inactive (not active, not completed, just disabled)
// AND the spinner is stopped (not running/paused)
// This prevents stopped background spinners from showing after submission
if (node.state === "disabled" && spinnerState.status === "stopped") {
return null;
}
// Build display text with symbol (or blank space during grace period to prevent layout shift)
// Show symbol if: not idle, OR idle but grace period has ended
const shouldShowSymbol = spinnerState.status !== "idle" || !spinnerState.gracePeriodActive;
const symbol = shouldShowSymbol ? getSymbol(spinnerState.status) : " ";
const label = getLabel(spinnerState.status);
const displayText = `${symbol} ${label}`;
const styledText = applyStyles(displayText);
return (_jsx(Box, { children: _jsx(Text, { children: styledText }) }));
};
//# sourceMappingURL=Spinner.js.map