EdgeAIG's picture
download
raw
93.7 kB
/**
* Builds interactive terminal prompts for CLI applications.
*
* A `Prompt<A>` describes a small terminal UI that renders frames, reads
* keyboard input, validates responses, and eventually produces an `A`. Prompts
* can ask for simple values, selections, lists, files, hidden text, or custom
* interactions. This module includes prompt constructors, tools for combining
* and transforming prompt output, and support for running prompts through the
* `Terminal` service.
*
* @since 4.0.0
*/
import * as Arr from "../../Array.js";
import * as Data from "../../Data.js";
import * as Effect from "../../Effect.js";
import * as Effectable from "../../Effectable.js";
import * as FileSystem from "../../FileSystem.js";
import { dual, pipe } from "../../Function.js";
import * as EffectNumber from "../../Number.js";
import * as Option from "../../Option.js";
import * as Path from "../../Path.js";
import * as Predicate from "../../Predicate.js";
import * as Queue from "../../Queue.js";
import * as Redacted from "../../Redacted.js";
import * as Terminal from "../../Terminal.js";
import * as Ansi from "./internal/ansi.js";
const TypeId = "~effect/cli/Prompt";
/**
* Returns `true` if the provided value is a `Prompt`.
*
* @category guards
* @since 4.0.0
*/
export const isPrompt = u => Predicate.hasProperty(u, TypeId);
const defaultFigures = {
arrowUp: "↑",
arrowDown: "↓",
arrowLeft: "←",
arrowRight: "→",
radioOn: "◉",
radioOff: "◯",
checkboxOn: "☒",
checkboxOff: "☐",
tick: "✔",
cross: "✖",
ellipsis: "…",
pointerSmall: "›",
line: "─",
pointer: "❯"
};
const windowsFigures = {
arrowUp: defaultFigures.arrowUp,
arrowDown: defaultFigures.arrowDown,
arrowLeft: defaultFigures.arrowLeft,
arrowRight: defaultFigures.arrowRight,
radioOn: "(*)",
radioOff: "( )",
checkboxOn: "[*]",
checkboxOff: "[ ]",
tick: "√",
cross: "×",
ellipsis: "...",
pointerSmall: "»",
line: "─",
pointer: ">"
};
/** @internal */
export const platformFigures = /*#__PURE__*/Effect.map(/*#__PURE__*/Effect.sync(() => process.platform === "win32"), isWindows => isWindows ? windowsFigures : defaultFigures);
/**
* Runs all the provided prompts in sequence respecting the structure provided
* in input.
*
* **Details**
*
* Supports either a tuple / iterable of prompts or a record / struct of prompts
* as an argument.
*
* **Example** (Collecting prompt results)
*
* ```ts
* import { Effect } from "effect"
* import { Prompt } from "effect/unstable/cli"
*
* const username = Prompt.text({
* message: "Enter your username: "
* })
*
* const password = Prompt.password({
* message: "Enter your password: ",
* validate: (value) =>
* value.length === 0
* ? Effect.fail("Password cannot be empty")
* : Effect.succeed(value)
* })
*
* const allWithTuple = Prompt.all([username, password])
*
* const allWithRecord = Prompt.all({ username, password })
* ```
*
* @category collecting & elements
* @since 4.0.0
*/
export const all = function () {
if (arguments.length === 1) {
if (isPrompt(arguments[0])) {
return map(arguments[0], x => [x]);
} else if (Array.isArray(arguments[0])) {
return allTupled(arguments[0]);
} else {
const entries = Object.entries(arguments[0]);
let result = map(entries[0][1], value => ({
[entries[0][0]]: value
}));
if (entries.length === 1) {
return result;
}
const rest = entries.slice(1);
for (const [key, prompt] of rest) {
result = result.pipe(flatMap(record => prompt.pipe(map(value => ({
...record,
[key]: value
})))));
}
return result;
}
}
return allTupled(arguments[0]);
};
const annotateLine = line => Ansi.annotate(line, Ansi.bold);
const annotateErrorLine = line => Ansi.annotate(line, Ansi.combine(Ansi.italicized, Ansi.red));
/**
* Creates a confirmation prompt that asks the user to choose a boolean yes/no
* value.
*
* **When to use**
*
* Use to ask for a yes/no answer that can be submitted directly.
*
* **Details**
*
* `initial` defaults to `false`. Enter submits the current default, yes-style
* input submits `true`, no-style input submits `false`, and other input beeps.
*
* @see {@link toggle} for an interactive switch-before-submit boolean prompt
*
* @category constructors
* @since 4.0.0
*/
export const confirm = options => {
const opts = {
initial: false,
...options,
label: {
confirm: "yes",
deny: "no",
...options.label
},
placeholder: {
defaultConfirm: "(Y/n)",
defaultDeny: "(y/N)",
...options.placeholder
}
};
const initialState = {
value: opts.initial
};
return custom(initialState, {
render: handleConfirmRender(opts),
process: input => handleConfirmProcess(input, opts.initial),
clear: handleConfirmClear(opts)
});
};
/**
* Creates a custom `Prompt` from the specified initial state and handlers.
*
* **Details**
*
* The initial state can either be a pure value or an `Effect`. This is
* particularly useful when the initial state of the `Prompt` must be computed
* by performing an effectful computation, such as reading data from the file
* system. A `Prompt` runs as a render loop: `render` returns ANSI output for
* the current frame, the `Terminal` obtains user input, `process` returns the
* next prompt action, and `clear` returns ANSI output used to clear the previous
* frame.
*
* Optionally, an external `events` dequeue can be provided as the third
* argument. When present, the render loop will race user input against events
* from the dequeue, allowing background events to trigger re-renders without
* waiting for a keypress. When an event is received from the dequeue, the
* `receive` handler is called instead of `process`.
*
* @category constructors
* @since 4.0.0
*/
export const custom = (initialState, ...args) => {
const [events, handlers] = args.length === 1 ? [undefined, args[0]] : [args[0], args[1]];
const op = Object.create(proto);
op._tag = "Loop";
op.initialState = initialState;
op.render = handlers.render;
op.process = handlers.process;
op.clear = handlers.clear;
op.events = events;
return op;
};
/**
* Creates a date prompt that lets the user edit a formatted date value and
* validates the final `Date` before submission.
*
* **Details**
*
* `initial` defaults to the current `Date`, `dateMask` defaults to
* `YYYY-MM-DD HH:mm:ss`, mask parsing creates editable date parts plus literal
* tokens, `locales` customizes month and weekday labels, and `validate` runs on
* submission.
*
* **Gotchas**
*
* A supplied `initial` `Date` is edited in place during prompt interaction.
* Date edits use JavaScript `Date` setters, so out-of-range typed values can
* normalize before validation. If the prompt is meant to be editable,
* `dateMask` should contain at least one editable date token.
*
* @category constructors
* @since 4.0.0
*/
export const date = options => {
const opts = {
initial: new Date(),
dateMask: "YYYY-MM-DD HH:mm:ss",
validate: Effect.succeed,
...options,
locales: {
...defaultLocales,
...options.locales
}
};
const dateParts = makeDateParts(opts.dateMask, opts.initial, opts.locales);
const initialCursorPosition = dateParts.findIndex(part => !part.isToken());
const initialState = {
dateParts,
typed: "",
cursor: initialCursorPosition,
value: opts.initial,
error: Option.none()
};
return custom(initialState, {
render: handleDateRender(opts),
process: handleDateProcess(opts),
clear: handleDateClear(opts)
});
};
/**
* Creates a file-system selection prompt and returns the selected path.
*
* **Details**
*
* The prompt can be configured to select files, directories, or either path
* type.
*
* @category constructors
* @since 4.0.0
*/
export const file = (options = {}) => {
const opts = {
type: options.type ?? "file",
message: options.message ?? `Choose a file`,
startingPath: Option.fromUndefinedOr(options.startingPath),
default: Option.fromUndefinedOr(options.default),
maxPerPage: options.maxPerPage ?? 10,
filter: options.filter ?? (() => Effect.succeed(true))
};
const initialState = Effect.gen(function* () {
const currentPath = yield* resolveCurrentPath(Option.none(), opts);
const path = yield* Path.Path;
const defaultPath = Option.map(opts.default, defaultValue => path.resolve(currentPath, defaultValue));
const initialPath = Option.match(defaultPath, {
onNone: () => currentPath,
onSome: defaultPath => path.dirname(defaultPath)
});
const files = yield* getFileList(initialPath, opts);
const cursor = Option.match(defaultPath, {
onNone: () => 0,
onSome: defaultPath => {
const index = files.indexOf(path.basename(defaultPath));
return index === -1 ? 0 : index;
}
});
const confirm = Confirm.Hide();
return {
cursor,
files,
allFiles: files,
query: "",
path: Option.map(defaultPath, path.dirname),
confirm
};
});
return custom(initialState, {
render: handleFileRender(opts),
process: handleFileProcess(opts),
clear: handleFileClear(opts)
});
};
/**
* Composes prompts by using the output of this prompt to create the next prompt.
*
* @category combinators
* @since 4.0.0
*/
export const flatMap = /*#__PURE__*/dual(2, (self, f) => {
const op = Object.create(proto);
op._tag = "OnSuccess";
op.prompt = self;
op.onSuccess = f;
return op;
});
/**
* Creates a floating-point number prompt.
*
* **Details**
*
* The prompt supports minimum and maximum bounds, keyboard step sizes, display
* precision, and additional validation before submission.
*
* @category constructors
* @since 4.0.0
*/
export const float = options => {
const opts = {
default: 0,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
incrementBy: 1,
decrementBy: 1,
precision: 2,
validate: n => {
if (n < opts.min) {
return Effect.fail(`${n} must be greater than or equal to ${opts.min}`);
}
if (n > opts.max) {
return Effect.fail(`${n} must be less than or equal to ${opts.max}`);
}
return Effect.succeed(n);
},
...options
};
const initialValue = options.default === undefined ? "" : `${opts.default}`;
const initialState = {
cursor: initialValue.length,
value: initialValue,
error: Option.none()
};
return custom(initialState, {
render: handleRenderFloat(opts),
process: handleProcessFloat(opts),
clear: handleNumberClear(opts)
});
};
/**
* Creates a text prompt that does not echo typed input and returns the
* submitted value wrapped in `Redacted`.
*
* @category constructors
* @since 4.0.0
*/
export const hidden = options => basePrompt(options, "hidden").pipe(map(Redacted.make));
/**
* Creates an integer prompt.
*
* **Details**
*
* The prompt supports minimum and maximum bounds, keyboard step sizes, and
* additional validation before submission.
*
* @category constructors
* @since 4.0.0
*/
export const integer = options => {
const opts = {
default: 0,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
incrementBy: 1,
decrementBy: 1,
validate: n => {
if (n < opts.min) {
return Effect.fail(`${n} must be greater than or equal to ${opts.min}`);
}
if (n > opts.max) {
return Effect.fail(`${n} must be less than or equal to ${opts.max}`);
}
return Effect.succeed(n);
},
...options
};
const initialValue = options.default === undefined ? "" : `${opts.default}`;
const initialState = {
cursor: initialValue.length,
value: initialValue,
error: Option.none()
};
return custom(initialState, {
render: handleRenderInteger(opts),
process: handleProcessInteger(opts),
clear: handleNumberClear(opts)
});
};
/**
* Creates a text prompt that returns an array of strings by splitting the
* submitted input on the configured delimiter.
*
* @category constructors
* @since 4.0.0
*/
export const list = options => text(options).pipe(map(output => output.split(options.delimiter || ",")));
/**
* Transforms the output value produced by a prompt.
*
* @category combinators
* @since 4.0.0
*/
export const map = /*#__PURE__*/dual(2, (self, f) => flatMap(self, a => succeed(f(a))));
/**
* Creates a password prompt that masks typed input and returns the submitted
* value wrapped in `Redacted`.
*
* @category constructors
* @since 4.0.0
*/
export const password = options => basePrompt(options, "password").pipe(map(Redacted.make));
/**
* Runs a prompt by reading terminal input and rendering prompt frames until the
* prompt submits a value.
*
* **Gotchas**
*
* The returned effect may fail with `Terminal.QuitError` if terminal input ends
* or the prompt is quit.
*
* @category execution
* @since 4.0.0
*/
export const run = /*#__PURE__*/Effect.fnUntraced(function* (self) {
const terminal = yield* Terminal.Terminal;
const input = yield* terminal.readInput;
return yield* runWithInput(self, terminal, input);
}, /*#__PURE__*/Effect.mapError(() => new Terminal.QuitError({})), Effect.scoped);
const getSelectInitialIndex = choices => {
let initialIndex = 0;
let seenSelected = -1;
for (let i = 0; i < choices.length; i++) {
const choice = choices[i];
if (choice.selected === true) {
if (seenSelected !== -1) {
throw new Error("InvalidArgumentException: only a single choice can be selected by default for Prompt.select");
}
seenSelected = i;
}
}
if (seenSelected !== -1) {
initialIndex = seenSelected;
}
return initialIndex;
};
/**
* Creates a prompt that lets the user select a single value from a list of
* choices.
*
* **Gotchas**
*
* At most one choice may be marked as selected by default.
*
* @category constructors
* @since 4.0.0
*/
export const select = options => {
const opts = {
maxPerPage: 10,
...options
};
const initialIndex = getSelectInitialIndex(opts.choices);
return custom(initialIndex, {
render: handleSelectRender(opts),
process: handleSelectProcess(opts),
clear: handleSelectClear(opts)
});
};
/**
* Creates a prompt that lets users filter select choices by typing.
*
* **Example** (Filtering choices with autocomplete)
*
* ```ts
* import { Prompt } from "effect/unstable/cli"
*
* const language = Prompt.autoComplete({
* message: "Choose a language",
* choices: [
* { title: "TypeScript", value: "ts" },
* { title: "Rust", value: "rs" },
* { title: "Kotlin", value: "kt" }
* ]
* })
* ```
*
* @category constructors
* @since 4.0.0
*/
export const autoComplete = options => {
const opts = {
maxPerPage: 10,
filterLabel: "filter",
filterPlaceholder: "type to filter",
emptyMessage: "No matches",
...options
};
const initialIndex = getSelectInitialIndex(opts.choices);
const filtered = filterAutoCompleteChoices(opts.choices, "");
const index = filtered.length === 0 ? 0 : filtered.includes(initialIndex) ? initialIndex : filtered[0];
const initialState = {
query: "",
index,
filtered
};
return custom(initialState, {
render: handleAutoCompleteRender(opts),
process: handleAutoCompleteProcess(opts),
clear: handleAutoCompleteClear(opts)
});
};
/**
* Creates a prompt that lets the user select multiple choices and returns their
* values as an array.
*
* **Details**
*
* The prompt supports default selected choices, bulk-selection commands, and
* minimum or maximum selection counts.
*
* @category constructors
* @since 4.0.0
*/
export const multiSelect = options => {
const opts = {
maxPerPage: 10,
...options
};
// Seed initial selection from choices marked as selected: true
const initialSelected = new Set();
for (let i = 0; i < opts.choices.length; i++) {
const choice = opts.choices[i];
if (choice.selected === true) {
initialSelected.add(i);
}
}
const initialState = {
index: 0,
selectedIndices: initialSelected,
error: Option.none()
};
return custom(initialState, {
render: handleMultiSelectRender(opts),
process: handleMultiSelectProcess(opts),
clear: handleMultiSelectClear(opts)
});
};
/**
* Creates a `Prompt` which immediately succeeds with the specified value.
*
* **Details**
*
* This prompt does not attempt to obtain user input or render anything to the
* screen.
*
* @category constructors
* @since 4.0.0
*/
export const succeed = value => {
const op = Object.create(proto);
op._tag = "Succeed";
op.value = value;
return op;
};
/**
* Creates a text-entry prompt that echoes input and returns the submitted
* string after validation.
*
* @category constructors
* @since 4.0.0
*/
export const text = options => basePrompt(options, "text");
/**
* Creates a toggle prompt that lets the user switch between active and inactive
* states and returns the selected boolean value.
*
* @category constructors
* @since 4.0.0
*/
export const toggle = options => {
const opts = {
initial: false,
active: "on",
inactive: "off",
...options
};
return custom(opts.initial, {
render: handleToggleRender(opts),
process: handleToggleProcess,
clear: () => handleToggleClear(opts)
});
};
const proto = {
... /*#__PURE__*/Effectable.Prototype({
label: "Prompt",
evaluate() {
return run(this);
}
}),
[TypeId]: {
_Output: _ => _
}
};
const allTupled = arg => {
if (arg.length === 0) {
return succeed([]);
}
if (arg.length === 1) {
return map(arg[0], x => [x]);
}
let result = map(arg[0], x => [x]);
for (let i = 1; i < arg.length; i++) {
const curr = arg[i];
result = flatMap(result, tuple => map(curr, a => [...tuple, a]));
}
return result;
};
const runWithInput = (prompt, terminal, input) => Effect.suspend(() => {
const op = prompt;
switch (op._tag) {
case "Loop":
{
return runLoop(op, terminal, input);
}
case "OnSuccess":
{
return Effect.flatMap(runWithInput(op.prompt, terminal, input), a => runWithInput(op.onSuccess(a), terminal, input));
}
case "Succeed":
{
return Effect.succeed(op.value);
}
}
});
const runLoop = /*#__PURE__*/Effect.fnUntraced(function* (loop, terminal, input) {
let state = Effect.isEffect(loop.initialState) ? yield* loop.initialState : loop.initialState;
let action = Action.NextFrame({
state
});
while (true) {
const msg = yield* loop.render(state, action);
yield* Effect.orDie(terminal.display(msg));
if (loop.events) {
const takeInput = Queue.take(input).pipe(Effect.map(event => ({
_tag: "Input",
event
})));
const result = yield* Effect.raceFirst(takeInput, Queue.take(loop.events).pipe(Effect.map(value => ({
_tag: "Event",
value
}))));
action = yield* loop.process(result, state);
} else {
const result = yield* Queue.take(input);
action = yield* loop.process(result, state);
}
switch (action._tag) {
case "Beep":
continue;
case "NextFrame":
{
yield* Effect.orDie(terminal.display(yield* loop.clear(state, action)));
state = action.state;
continue;
}
case "Submit":
{
yield* Effect.orDie(terminal.display(yield* loop.clear(state, action)));
const msg = yield* loop.render(state, action);
yield* Effect.orDie(terminal.display(msg));
return action.value;
}
}
}
}, (effect, _, terminal) => Effect.ensuring(effect, Effect.orDie(terminal.display(Ansi.cursorShow))));
const Action = /*#__PURE__*/Data.taggedEnum();
/**
* Clears all lines taken up by the specified `text`.
*/
const eraseText = (text, columns) => {
if (columns === 0) {
return Ansi.eraseLine + Ansi.cursorTo(0);
}
let rows = 0;
const lines = text.split(NEWLINE_REGEXP);
for (const line of lines) {
rows += 1 + Math.floor(Math.max(line.length - 1, 0) / columns);
}
return Ansi.eraseLines(rows);
};
const lines = (prompt, columns) => {
const lines = prompt.split(NEWLINE_REGEXP);
return columns === 0 ? lines.length : pipe(Arr.map(lines, line => Math.ceil(line.length / columns)), Arr.reduce(0, (left, right) => left + right));
};
const clearOutputWithError = (outputText, columns, errorText) => {
if (errorText !== undefined && errorText.length > 0) {
return Ansi.cursorDown(lines(errorText, columns)) + eraseText(`\n${errorText}`, columns) + eraseText(outputText, columns);
}
return eraseText(outputText, columns);
};
const renderBeep = Ansi.beep;
const NEWLINE_REGEXP = /\r?\n/;
const handleConfirmClear = options => {
return Effect.fnUntraced(function* (state, _) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const confirmMessage = state.value ? options.placeholder.defaultConfirm : options.placeholder.defaultDeny;
const promptText = renderConfirmOutput(confirmMessage, "?", figures.pointerSmall, options, {
plain: true
});
const clearOutput = eraseText(promptText, columns);
const resetCurrentLine = Ansi.eraseLine + Ansi.cursorLeft;
return clearOutput + resetCurrentLine;
});
};
const renderConfirmOutput = (confirm, leadingSymbol, trailingSymbol, options, renderOptions) => renderPrompt(confirm, options.message, leadingSymbol, trailingSymbol, renderOptions);
const renderConfirmNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
// Marking these explicitly as present with `!` because they always will be
// and there is really no value in adding a `DeepRequired` type helper just
// for these internal cases
const confirmMessage = state.value ? options.placeholder.defaultConfirm : options.placeholder.defaultDeny;
const confirm = Ansi.annotate(confirmMessage, Ansi.blackBright);
const promptMsg = renderConfirmOutput(confirm, leadingSymbol, trailingSymbol, options);
return Ansi.cursorHide + promptMsg;
});
const renderConfirmSubmission = /*#__PURE__*/Effect.fnUntraced(function* (value, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const confirmMessage = value ? options.label.confirm : options.label.deny;
const promptMsg = renderConfirmOutput(confirmMessage, leadingSymbol, trailingSymbol, options);
return promptMsg + "\n";
});
const handleConfirmRender = options => {
return (_, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderConfirmNextFrame(state, options),
Submit: ({
value
}) => renderConfirmSubmission(value, options)
});
};
};
const TRUE_VALUE_REGEXP = /^y|t$/;
const FALSE_VALUE_REGEXP = /^n|f$/;
const handleConfirmProcess = (input, defaultValue) => {
const value = Option.getOrElse(input.input, () => "");
if (input.key.name === "enter" || input.key.name === "return") {
return Effect.succeed(Action.Submit({
value: defaultValue
}));
}
if (TRUE_VALUE_REGEXP.test(value.toLowerCase())) {
return Effect.succeed(Action.Submit({
value: true
}));
}
if (FALSE_VALUE_REGEXP.test(value.toLowerCase())) {
return Effect.succeed(Action.Submit({
value: false
}));
}
return Effect.succeed(Action.Beep());
};
const handleDateClear = options => {
return Effect.fnUntraced(function* (state, _) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const resetCurrentLine = Ansi.eraseLine + Ansi.cursorLeft;
const parts = Arr.reduce(state.dateParts, "", (doc, part) => doc + part.toString());
const promptText = renderDateOutput("?", figures.pointerSmall, parts, options, {
plain: true
});
const errorText = Option.isSome(state.error) ? Arr.match(state.error.value.split(NEWLINE_REGEXP), {
onEmpty: () => "",
onNonEmpty: errorLines => `${figures.pointerSmall} ${errorLines.join("\n")}`
}) : "";
const clearOutput = clearOutputWithError(promptText, columns, errorText);
return clearOutput + resetCurrentLine;
});
};
const renderDateError = (state, pointer) => {
if (Option.isSome(state.error)) {
const errorLines = state.error.value.split(NEWLINE_REGEXP);
if (Arr.isReadonlyArrayNonEmpty(errorLines)) {
const prefix = Ansi.annotate(pointer, Ansi.red) + " ";
const lines = Arr.map(errorLines, str => annotateErrorLine(str));
return Ansi.cursorSavePosition + "\n" + prefix + lines.join("\n") + Ansi.cursorRestorePosition;
}
}
return "";
};
const renderParts = (state, submitted = false) => {
return Arr.reduce(state.dateParts, "", (doc, part, currentIndex) => {
const partDoc = part.toString();
if (currentIndex === state.cursor && !submitted) {
const annotation = Ansi.combine(Ansi.underlined, Ansi.cyanBright);
return doc + Ansi.annotate(partDoc, annotation);
}
return doc + partDoc;
});
};
const renderDateOutput = (leadingSymbol, trailingSymbol, parts, options, renderOptions) => renderPrompt(parts, options.message, leadingSymbol, trailingSymbol, renderOptions);
const renderDateNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const parts = renderParts(state);
const promptMsg = renderDateOutput(leadingSymbol, trailingSymbol, parts, options);
const errorMsg = renderDateError(state, figures.pointerSmall);
return Ansi.cursorHide + promptMsg + errorMsg;
});
const renderDateSubmission = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const parts = renderParts(state, true);
const promptMsg = renderDateOutput(leadingSymbol, trailingSymbol, parts, options);
return promptMsg + "\n";
});
const processUp = state => {
state.dateParts[state.cursor].increment();
return Action.NextFrame({
state: {
...state,
typed: ""
}
});
};
const processDown = state => {
state.dateParts[state.cursor].decrement();
return Action.NextFrame({
state: {
...state,
typed: ""
}
});
};
const processDateCursorLeft = state => {
const previous = state.dateParts[state.cursor].previousPart();
if (Option.isSome(previous)) {
return Action.NextFrame({
state: {
...state,
typed: "",
cursor: state.dateParts.indexOf(previous.value)
}
});
}
return Action.Beep();
};
const processDateCursorRight = state => {
const next = state.dateParts[state.cursor].nextPart();
if (Option.isSome(next)) {
return Action.NextFrame({
state: {
...state,
typed: "",
cursor: state.dateParts.indexOf(next.value)
}
});
}
return Action.Beep();
};
const processDateNext = state => {
const next = state.dateParts[state.cursor].nextPart();
const cursor = Option.match(next, {
onNone: () => state.dateParts.findIndex(part => !part.isToken()),
onSome: next => state.dateParts.indexOf(next)
});
return Action.NextFrame({
state: {
...state,
cursor
}
});
};
const defaultDateProcessor = (value, state) => {
if (/\d/.test(value)) {
const typed = state.typed + value;
state.dateParts[state.cursor].setValue(typed);
return Action.NextFrame({
state: {
...state,
typed
}
});
}
return Action.Beep();
};
const defaultLocales = {
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
weekdays: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
};
const handleDateRender = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderDateNextFrame(state, options),
Submit: () => renderDateSubmission(state, options)
});
};
};
const handleDateProcess = options => {
return (input, state) => {
switch (input.key.name) {
case "left":
{
return Effect.succeed(processDateCursorLeft(state));
}
case "right":
{
return Effect.succeed(processDateCursorRight(state));
}
case "k":
case "up":
{
return Effect.succeed(processUp(state));
}
case "j":
case "down":
{
return Effect.succeed(processDown(state));
}
case "tab":
{
return Effect.succeed(processDateNext(state));
}
case "enter":
case "return":
{
return Effect.match(options.validate(state.value), {
onFailure: error => Action.NextFrame({
state: {
...state,
error: Option.some(error)
}
}),
onSuccess: value => Action.Submit({
value
})
});
}
default:
{
return Effect.succeed(defaultDateProcessor(Option.getOrElse(input.input, () => ""), state));
}
}
};
};
const DATE_PART_REGEXP = /\\(.)|"((?:\\["\\]|[^"])+)"|(D[Do]?|d{3,4}|d)|(M{1,4})|(YY(?:YY)?)|([aA])|([Hh]{1,2})|(m{1,2})|(s{1,2})|(S{1,4})|./g;
const regExpGroups = {
1: ({
token,
...opts
}) => new Token({
token: token.replace(/\\(.)/g, "$1"),
...opts
}),
2: opts => new Day(opts),
3: opts => new Month(opts),
4: opts => new Year(opts),
5: opts => new Meridiem(opts),
6: opts => new Hours(opts),
7: opts => new Minutes(opts),
8: opts => new Seconds(opts),
9: opts => new Milliseconds(opts)
};
const makeDateParts = (dateMask, date, locales) => {
const parts = [];
let result = null;
// oxlint-disable-next-line no-cond-assign
while (result = DATE_PART_REGEXP.exec(dateMask)) {
const match = result.shift();
const index = result.findIndex(group => group !== undefined);
if (index in regExpGroups) {
const token = result[index] || match;
parts.push(regExpGroups[index]({
token,
date,
parts,
locales
}));
} else {
parts.push(new Token({
token: result[index] || match,
date,
parts,
locales
}));
}
}
const orderedParts = parts.reduce((array, element) => {
const lastElement = array[array.length - 1];
if (element.isToken() && lastElement !== undefined && lastElement.isToken()) {
lastElement.setValue(element.token);
} else {
array.push(element);
}
return array;
}, Arr.empty());
parts.splice(0, parts.length, ...orderedParts);
return parts;
};
class DatePart {
token;
date;
parts;
locales;
constructor(params) {
this.token = params.token;
this.locales = params.locales;
this.date = params.date || new Date();
this.parts = params.parts || [this];
}
/**
* Returns `true` if this `DatePart` is a `Token`, `false` otherwise.
*/
isToken() {
return false;
}
/**
* Retrieves the next date part in the list of parts.
*/
nextPart() {
const currentPartIndex = Option.getOrElse(Arr.findFirstIndex(this.parts, part => part === this), () => 0);
return Arr.findFirst(this.parts.slice(currentPartIndex + 1), part => !part.isToken());
}
/**
* Retrieves the previous date part in the list of parts.
*/
previousPart() {
const currentPartIndex = Arr.findFirstIndex(this.parts, part => part === this);
if (Option.isSome(currentPartIndex)) {
return Arr.findLast(this.parts.slice(0, currentPartIndex.value), part => !part.isToken());
}
return Option.none();
}
toString() {
return String(this.date);
}
}
class Token extends DatePart {
increment() {}
decrement() {}
setValue(value) {
this.token = this.token + value;
}
isToken() {
return true;
}
toString() {
return this.token;
}
}
class Milliseconds extends DatePart {
increment() {
this.date.setMilliseconds(this.date.getMilliseconds() + 1);
}
decrement() {
this.date.setMilliseconds(this.date.getMilliseconds() - 1);
}
setValue(value) {
this.date.setMilliseconds(Number.parseInt(value.slice(-this.token.length)));
}
toString() {
const millis = `${this.date.getMilliseconds()}`;
return millis.padStart(4, "0").substring(0, this.token.length);
}
}
class Seconds extends DatePart {
increment() {
this.date.setSeconds(this.date.getSeconds() + 1);
}
decrement() {
this.date.setSeconds(this.date.getSeconds() - 1);
}
setValue(value) {
this.date.setSeconds(Number.parseInt(value.slice(-2)));
}
toString() {
const seconds = `${this.date.getSeconds()}`;
return this.token.length > 1 ? seconds.padStart(2, "0") : seconds;
}
}
class Minutes extends DatePart {
increment() {
this.date.setMinutes(this.date.getMinutes() + 1);
}
decrement() {
this.date.setMinutes(this.date.getMinutes() - 1);
}
setValue(value) {
this.date.setMinutes(Number.parseInt(value.slice(-2)));
}
toString() {
const minutes = `${this.date.getMinutes()}`;
return this.token.length > 1 ? minutes.padStart(2, "0") : minutes;
}
}
class Hours extends DatePart {
increment() {
this.date.setHours(this.date.getHours() + 1);
}
decrement() {
this.date.setHours(this.date.getHours() - 1);
}
setValue(value) {
this.date.setHours(Number.parseInt(value.slice(-2)));
}
toString() {
const hours = /h/.test(this.token) ? this.date.getHours() % 12 || 12 : this.date.getHours();
return this.token.length > 1 ? `${hours}`.padStart(2, "0") : `${hours}`;
}
}
class Day extends DatePart {
increment() {
this.date.setDate(this.date.getDate() + 1);
}
decrement() {
this.date.setDate(this.date.getDate() - 1);
}
setValue(value) {
this.date.setDate(Number.parseInt(value.slice(-2)));
}
toString() {
const date = this.date.getDate();
const day = this.date.getDay();
switch (this.token) {
case "DD":
return `${date}`.padStart(2, "0");
case "Do":
return `${date}${this.ordinalIndicator(date)}`;
case "d":
return `${day + 1}`;
case "ddd":
return this.locales.weekdaysShort[day];
case "dddd":
return this.locales.weekdays[day];
default:
return `${date}`;
}
}
ordinalIndicator(day) {
switch (day % 10) {
case 1:
return "st";
case 2:
return "nd";
case 3:
return "rd";
default:
return "th";
}
}
}
class Month extends DatePart {
increment() {
this.date.setMonth(this.date.getMonth() + 1);
}
decrement() {
this.date.setMonth(this.date.getMonth() - 1);
}
setValue(value) {
const month = Number.parseInt(value.slice(-2)) - 1;
this.date.setMonth(month < 0 ? 0 : month);
}
toString() {
const month = this.date.getMonth();
switch (this.token.length) {
case 2:
return `${month + 1}`.padStart(2, "0");
case 3:
return this.locales.monthsShort[month];
case 4:
return this.locales.months[month];
default:
return `${month + 1}`;
}
}
}
class Year extends DatePart {
increment() {
this.date.setFullYear(this.date.getFullYear() + 1);
}
decrement() {
this.date.setFullYear(this.date.getFullYear() - 1);
}
setValue(value) {
this.date.setFullYear(Number.parseInt(value.slice(-4)));
}
toString() {
const year = `${this.date.getFullYear()}`.padStart(4, "0");
return this.token.length === 2 ? year.substring(-2) : year;
}
}
class Meridiem extends DatePart {
increment() {
this.date.setHours((this.date.getHours() + 12) % 24);
}
decrement() {
this.increment();
}
setValue(_value) {}
toString() {
const meridiem = this.date.getHours() > 12 ? "pm" : "am";
return /A/.test(this.token) ? meridiem.toUpperCase() : meridiem;
}
}
const CONFIRM_MESSAGE = "The selected directory contains files. Would you like to traverse the selected directory?";
const FILE_FILTER_LABEL = "filter";
const FILE_FILTER_PLACEHOLDER = "type to filter";
const FILE_EMPTY_MESSAGE = "No matches";
const Confirm = /*#__PURE__*/Data.taggedEnum();
const showConfirmation = /*#__PURE__*/Confirm.$is("Show");
const resolveCurrentPath = (path, options) => {
if (Option.isSome(path)) {
return Effect.succeed(path.value);
}
if (Option.isSome(options.startingPath)) {
const startingPath = options.startingPath.value;
return Effect.flatMap(FileSystem.FileSystem, fs =>
// Ensure the user provided starting path exists
Effect.orDie(fs.exists(startingPath)).pipe(Effect.flatMap(exists => exists ? Effect.void : Effect.die(`The provided starting path '${startingPath}' does not exist`)), Effect.as(startingPath)));
}
return Effect.sync(() => process.cwd());
};
const getFileList = /*#__PURE__*/Effect.fnUntraced(function* (directory, options) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const files = yield* Effect.orDie(fs.readDirectory(directory)).pipe(
// Always prepend the `".."` option to the file list but allow it
// to be filtered out if the user so desires
Effect.map(files => ["..", ...files]));
return yield* Effect.filter(files, file => {
const result = options.filter(file);
const userDefinedFilter = Effect.isEffect(result) ? result : Effect.succeed(result);
const directoryFilter = options.type === "directory" ? Effect.map(Effect.orDie(fs.stat(path.join(directory, file))), info => info.type === "Directory") : Effect.succeed(true);
return Effect.zipWith(userDefinedFilter, directoryFilter, (a, b) => a && b);
}, {
concurrency: files.length
});
});
const filterFiles = (files, query) => {
if (query.length === 0) {
return files;
}
const normalizedQuery = query.toLowerCase();
const filtered = [];
for (let index = 0; index < files.length; index++) {
if (files[index].toLowerCase().includes(normalizedQuery)) {
filtered.push(files[index]);
}
}
return filtered;
};
const updateFileState = (state, query, allFiles = state.allFiles) => {
const files = filterFiles(allFiles, query);
if (files.length === 0) {
return {
...state,
query,
allFiles,
files,
cursor: 0
};
}
const selected = state.files[state.cursor];
const cursor = selected === undefined ? 0 : files.indexOf(selected);
return {
...state,
query,
allFiles,
files,
cursor: cursor === -1 ? 0 : cursor
};
};
const handleFileClear = options => {
return Effect.fnUntraced(function* (state, _) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const path = yield* Path.Path;
const figures = yield* platformFigures;
const currentPath = yield* resolveCurrentPath(state.path, options);
const selectedPath = state.files[state.cursor];
const resolvedPath = selectedPath === undefined ? currentPath : path.resolve(currentPath, selectedPath);
const resolvedPathText = `${figures.pointerSmall} ${resolvedPath}`;
const isConfirming = showConfirmation(state.confirm);
const promptText = isConfirming ? renderPrompt("(Y/n)", CONFIRM_MESSAGE, "?", figures.pointerSmall, {
plain: true
}) : renderPrompt(renderFileFilter(state, {
plain: true
}), options.message, figures.tick, figures.ellipsis, {
plain: true
});
const filesText = isConfirming ? "" : renderFiles(state, state.files, figures, options, {
plain: true
});
const outputText = isConfirming ? `${promptText}\n${resolvedPathText}` : `${promptText}\n${resolvedPathText}\n${filesText}`;
const clearOutput = eraseText(outputText, columns);
const resetCurrentLine = Ansi.eraseLine + Ansi.cursorLeft;
return clearOutput + resetCurrentLine;
});
};
const renderPrompt = (confirm, message, leadingSymbol, trailingSymbol, options) => {
const prefix = leadingSymbol + " ";
const annotate = options?.plain === true ? line => line : annotateLine;
return Arr.match(message.split(NEWLINE_REGEXP), {
onEmpty: () => prefix + " " + trailingSymbol + " " + confirm,
onNonEmpty: promptLines => {
const lines = Arr.map(promptLines, line => annotate(line));
return prefix + lines.join("\n") + " " + trailingSymbol + " " + confirm;
}
});
};
const renderPrefix = (state, toDisplay, currentIndex, length, figures, renderOptions) => {
let prefix = " ";
if (currentIndex === toDisplay.startIndex && toDisplay.startIndex > 0) {
prefix = figures.arrowUp;
} else if (currentIndex === toDisplay.endIndex - 1 && toDisplay.endIndex < length) {
prefix = figures.arrowDown;
}
if (state.cursor === currentIndex) {
return renderOptions?.plain === true ? figures.pointer + prefix : Ansi.annotate(figures.pointer, Ansi.cyanBright) + prefix;
}
return prefix + " ";
};
const renderFileName = (file, isSelected, renderOptions) => {
if (renderOptions?.plain === true) {
return file;
}
return isSelected ? Ansi.annotate(file, Ansi.combine(Ansi.underlined, Ansi.cyanBright)) : file;
};
const renderFileFilter = (state, renderOptions) => {
const filterValue = state.query.length === 0 ? renderOptions?.plain === true ? FILE_FILTER_PLACEHOLDER : Ansi.annotate(FILE_FILTER_PLACEHOLDER, Ansi.blackBright) : renderOptions?.plain === true ? state.query : Ansi.annotate(state.query, Ansi.combine(Ansi.underlined, Ansi.cyanBright));
return `[${FILE_FILTER_LABEL}: ${filterValue}]`;
};
const renderFiles = (state, files, figures, options, renderOptions) => {
const length = files.length;
if (length === 0) {
return renderOptions?.plain === true ? FILE_EMPTY_MESSAGE : Ansi.annotate(FILE_EMPTY_MESSAGE, Ansi.blackBright);
}
const toDisplay = entriesToDisplay(state.cursor, length, options.maxPerPage);
const documents = [];
for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) {
const isSelected = state.cursor === index;
const prefix = renderPrefix(state, toDisplay, index, length, figures, renderOptions);
const fileName = renderFileName(files[index], isSelected, renderOptions);
documents.push(prefix + fileName);
}
return documents.join("\n");
};
const renderFileNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const path = yield* Path.Path;
const figures = yield* platformFigures;
const currentPath = yield* resolveCurrentPath(state.path, options);
const selectedPath = state.files[state.cursor];
const resolvedPath = selectedPath === undefined ? currentPath : path.resolve(currentPath, selectedPath);
const resolvedPathMsg = Ansi.annotate(figures.pointerSmall + " " + resolvedPath, Ansi.blackBright);
if (showConfirmation(state.confirm)) {
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const confirm = Ansi.annotate("(Y/n)", Ansi.blackBright);
const promptMsg = renderPrompt(confirm, CONFIRM_MESSAGE, leadingSymbol, trailingSymbol);
return Ansi.cursorHide + promptMsg + "\n" + resolvedPathMsg;
}
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const promptMsg = renderPrompt(renderFileFilter(state), options.message, leadingSymbol, trailingSymbol);
const files = renderFiles(state, state.files, figures, options);
return Ansi.cursorHide + promptMsg + "\n" + resolvedPathMsg + "\n" + files;
});
const renderFileSubmission = /*#__PURE__*/Effect.fnUntraced(function* (state, value, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const promptMsg = renderPrompt(renderFileFilter(state), options.message, leadingSymbol, trailingSymbol);
return promptMsg + " " + Ansi.annotate(value, Ansi.white) + "\n";
});
const handleFileRender = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderFileNextFrame(state, options),
Submit: ({
value
}) => renderFileSubmission(state, value, options)
});
};
};
const processFileCursorUp = state => {
if (state.files.length === 0) {
return Effect.succeed(Action.Beep());
}
const cursor = state.cursor - 1;
return Effect.succeed(Action.NextFrame({
state: {
...state,
cursor: cursor < 0 ? state.files.length - 1 : cursor
}
}));
};
const processFileCursorDown = state => {
if (state.files.length === 0) {
return Effect.succeed(Action.Beep());
}
return Effect.succeed(Action.NextFrame({
state: {
...state,
cursor: (state.cursor + 1) % state.files.length
}
}));
};
const processFileBackspace = state => {
if (state.query.length === 0) {
return Effect.succeed(Action.Beep());
}
const query = state.query.slice(0, state.query.length - 1);
return Effect.succeed(Action.NextFrame({
state: updateFileState(state, query)
}));
};
const processFileClear = state => Effect.succeed(Action.NextFrame({
state: updateFileState(state, "")
}));
const processFileInput = (input, state) => {
if (input.length === 0) {
return Effect.succeed(Action.Beep());
}
const query = state.query + input;
return Effect.succeed(Action.NextFrame({
state: updateFileState(state, query)
}));
};
const processSelection = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
if (state.files.length === 0) {
return Action.Beep();
}
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const currentPath = yield* resolveCurrentPath(state.path, options);
const selectedPath = state.files[state.cursor];
const resolvedPath = path.resolve(currentPath, selectedPath);
const info = yield* Effect.orDie(fs.stat(resolvedPath));
if (info.type === "Directory") {
const files = yield* getFileList(resolvedPath, options);
const filesWithoutParent = files.filter(file => file !== "..");
// If the user selected a directory AND the prompt type can result with
// a directory, we must confirm:
// - If the selected directory has any files
// - Confirm whether or not the user wants to traverse those files
if (options.type === "directory" || options.type === "either") {
return filesWithoutParent.length === 0
// Directory is empty so it's safe to select it
? Action.Submit({
value: resolvedPath
})
// Directory has contents - show confirmation to user
: Action.NextFrame({
state: {
...state,
confirm: Confirm.Show()
}
});
}
return Action.NextFrame({
state: {
cursor: 0,
files,
allFiles: files,
query: "",
path: Option.some(resolvedPath),
confirm: Confirm.Hide()
}
});
}
return Action.Submit({
value: resolvedPath
});
});
const handleFileProcess = options => {
return Effect.fnUntraced(function* (input, state) {
if (input.key.ctrl) {
if (input.key.name === "u") {
if (showConfirmation(state.confirm)) {
return Action.Beep();
}
return yield* processFileClear(state);
}
return Action.Beep();
}
switch (input.key.name) {
case "k":
case "up":
{
return yield* processFileCursorUp(state);
}
case "j":
case "down":
case "tab":
{
return yield* processFileCursorDown(state);
}
case "backspace":
{
if (showConfirmation(state.confirm)) {
return Action.Beep();
}
return yield* processFileBackspace(state);
}
case "enter":
case "return":
{
return yield* processSelection(state, options);
}
case "y":
case "t":
{
if (showConfirmation(state.confirm)) {
const path = yield* Path.Path;
const currentPath = yield* resolveCurrentPath(state.path, options);
const selectedPath = state.files[state.cursor];
const resolvedPath = path.resolve(currentPath, selectedPath);
const files = yield* getFileList(resolvedPath, options);
return Action.NextFrame({
state: {
cursor: 0,
files,
allFiles: files,
query: "",
path: Option.some(resolvedPath),
confirm: Confirm.Hide()
}
});
}
return yield* processFileInput(Option.getOrElse(input.input, () => ""), state);
}
case "n":
case "f":
{
if (showConfirmation(state.confirm)) {
const path = yield* Path.Path;
const currentPath = yield* resolveCurrentPath(state.path, options);
const selectedPath = state.files[state.cursor];
const resolvedPath = path.resolve(currentPath, selectedPath);
return Action.Submit({
value: resolvedPath
});
}
return yield* processFileInput(Option.getOrElse(input.input, () => ""), state);
}
default:
{
if (showConfirmation(state.confirm)) {
return Action.Beep();
}
return yield* processFileInput(Option.getOrElse(input.input, () => ""), state);
}
}
});
};
const renderMultiSelectError = (state, pointer, renderOptions) => {
if (Option.isSome(state.error)) {
return Arr.match(state.error.value.split(NEWLINE_REGEXP), {
onEmpty: () => "",
onNonEmpty: errorLines => {
if (renderOptions?.plain === true) {
return `${pointer} ${errorLines.join("\n")}`;
}
const prefix = Ansi.annotate(pointer, Ansi.red) + " ";
const lines = Arr.map(errorLines, str => annotateErrorLine(str));
return Ansi.cursorSavePosition + "\n" + prefix + lines.join("\n") + Ansi.cursorRestorePosition;
}
});
}
return "";
};
const renderChoiceDescription = (choice, isActive, renderOptions) => {
if (!choice.disabled && choice.description && isActive) {
return renderOptions?.plain === true ? "- " + choice.description : Ansi.annotate("- " + choice.description, Ansi.blackBright);
}
return "";
};
const metaOptionsCount = 2;
const renderMultiSelectTitle = (title, isHighlighted, renderOptions) => {
if (renderOptions?.plain === true || !isHighlighted) {
return title;
}
return Ansi.annotate(title, Ansi.combine(Ansi.underlined, Ansi.cyanBright));
};
const renderMultiSelectChoices = (state, options, figures, renderOptions) => {
const choices = options.choices;
const totalChoices = choices.length;
const selectedCount = state.selectedIndices.size;
const allSelected = selectedCount === totalChoices;
const selectAllText = allSelected ? options?.selectNone ?? "Select None" : options?.selectAll ?? "Select All";
const inverseSelectionText = options?.inverseSelection ?? "Inverse Selection";
const metaOptions = [{
title: selectAllText
}, {
title: inverseSelectionText
}];
const allChoices = [...metaOptions, ...choices];
const toDisplay = entriesToDisplay(state.index, allChoices.length, options.maxPerPage);
const documents = [];
for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) {
const choice = allChoices[index];
const isHighlighted = state.index === index;
let prefix = " ";
if (index === toDisplay.startIndex && toDisplay.startIndex > 0) {
prefix = figures.arrowUp;
} else if (index === toDisplay.endIndex - 1 && toDisplay.endIndex < allChoices.length) {
prefix = figures.arrowDown;
}
if (index < metaOptions.length) {
// Meta options
const title = renderMultiSelectTitle(choice.title, isHighlighted, renderOptions);
documents.push(prefix + " " + title);
} else {
// Regular choices
const choiceIndex = index - metaOptions.length;
const isSelected = state.selectedIndices.has(choiceIndex);
const checkbox = isSelected ? figures.checkboxOn : figures.checkboxOff;
const annotatedCheckbox = isHighlighted && renderOptions?.plain !== true ? Ansi.annotate(checkbox, Ansi.cyanBright) : checkbox;
const title = renderMultiSelectTitle(choice.title, isHighlighted, renderOptions);
const description = renderChoiceDescription(choice, isHighlighted, renderOptions);
documents.push(prefix + " " + annotatedCheckbox + " " + title + " " + description);
}
}
return documents.join("\n");
};
const renderMultiSelectNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const choices = renderMultiSelectChoices(state, options, figures);
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const promptMsg = renderSelectOutput(leadingSymbol, trailingSymbol, options);
const error = renderMultiSelectError(state, figures.pointer);
return Ansi.cursorHide + promptMsg + "\n" + choices + error;
});
const renderMultiSelectSubmission = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const selectedChoices = Array.from(state.selectedIndices).sort(EffectNumber.Order).map(index => options.choices[index].title);
const selectedText = selectedChoices.join(", ");
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const promptMsg = renderSelectOutput(leadingSymbol, trailingSymbol, options);
return promptMsg + " " + Ansi.annotate(selectedText, Ansi.white) + "\n";
});
const processMultiSelectCursorUp = (state, totalChoices) => {
const newIndex = state.index === 0 ? totalChoices - 1 : state.index - 1;
return Effect.succeed(Action.NextFrame({
state: {
...state,
index: newIndex
}
}));
};
const processMultiSelectCursorDown = (state, totalChoices) => {
const newIndex = (state.index + 1) % totalChoices;
return Effect.succeed(Action.NextFrame({
state: {
...state,
index: newIndex
}
}));
};
const processSpace = (state, options) => {
const selectedIndices = new Set(state.selectedIndices);
if (state.index === 0) {
if (state.selectedIndices.size === options.choices.length) {
selectedIndices.clear();
} else {
for (let i = 0; i < options.choices.length; i++) {
selectedIndices.add(i);
}
}
} else if (state.index === 1) {
for (let i = 0; i < options.choices.length; i++) {
if (state.selectedIndices.has(i)) {
selectedIndices.delete(i);
} else {
selectedIndices.add(i);
}
}
} else {
const choiceIndex = state.index - metaOptionsCount;
if (selectedIndices.has(choiceIndex)) {
selectedIndices.delete(choiceIndex);
} else {
selectedIndices.add(choiceIndex);
}
}
return Effect.succeed(Action.NextFrame({
state: {
...state,
selectedIndices
}
}));
};
const handleMultiSelectClear = options => Effect.fnUntraced(function* (state, _) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const clearPrompt = Ansi.eraseLine + Ansi.cursorLeft;
const promptText = renderSelectOutput("?", figures.pointerSmall, options, {
plain: true
});
const choicesText = renderMultiSelectChoices(state, options, figures, {
plain: true
});
const errorText = renderMultiSelectError(state, figures.pointer, {
plain: true
});
const clearOutput = clearOutputWithError(`${promptText}\n${choicesText}`, columns, errorText);
return clearOutput + clearPrompt;
});
const handleMultiSelectProcess = options => {
return (input, state) => {
const totalChoices = options.choices.length + metaOptionsCount;
switch (input.key.name) {
case "k":
case "up":
{
return processMultiSelectCursorUp({
...state,
error: Option.none()
}, totalChoices);
}
case "j":
case "down":
case "tab":
{
return processMultiSelectCursorDown({
...state,
error: Option.none()
}, totalChoices);
}
case "space":
{
return processSpace(state, options);
}
case "enter":
case "return":
{
const selectedCount = state.selectedIndices.size;
if (options.min !== undefined && selectedCount < options.min) {
return Effect.succeed(Action.NextFrame({
state: {
...state,
error: Option.some(`At least ${options.min} are required`)
}
}));
}
if (options.max !== undefined && selectedCount > options.max) {
return Effect.succeed(Action.NextFrame({
state: {
...state,
error: Option.some(`At most ${options.max} choices are allowed`)
}
}));
}
const selectedValues = Array.from(state.selectedIndices).sort(EffectNumber.Order).map(index => options.choices[index].value);
return Effect.succeed(Action.Submit({
value: selectedValues
}));
}
default:
{
return Effect.succeed(Action.Beep());
}
}
};
};
const handleMultiSelectRender = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderMultiSelectNextFrame(state, options),
Submit: () => renderMultiSelectSubmission(state, options)
});
};
};
const handleNumberClear = options => {
return Effect.fnUntraced(function* (state, _) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const resetCurrentLine = Ansi.eraseLine + Ansi.cursorLeft;
const errorText = renderNumberError(state, figures.pointerSmall, {
plain: true
});
const promptText = renderNumberOutput(state, "?", figures.pointerSmall, options, {
plain: true
});
const clearOutput = clearOutputWithError(promptText, columns, errorText);
return clearOutput + resetCurrentLine;
});
};
const renderNumberInput = (state, submitted, renderOptions) => {
const value = state.value === "" ? "" : `${state.value}`;
if (submitted || renderOptions?.plain === true) {
return value;
}
const annotation = Option.isSome(state.error) ? Ansi.red : Ansi.combine(Ansi.underlined, Ansi.cyanBright);
return Ansi.annotate(value, annotation);
};
const renderNumberError = (state, pointer, renderOptions) => {
if (Option.isSome(state.error)) {
return Arr.match(state.error.value.split(NEWLINE_REGEXP), {
onEmpty: () => "",
onNonEmpty: errorLines => {
if (renderOptions?.plain === true) {
return `${pointer} ${errorLines.join("\n")}`;
}
const prefix = Ansi.annotate(pointer, Ansi.red) + " ";
const lines = Arr.map(errorLines, str => annotateErrorLine(str));
return Ansi.cursorSavePosition + "\n" + prefix + lines.join("\n") + Ansi.cursorRestorePosition;
}
});
}
return "";
};
const renderNumberOutput = (state, leadingSymbol, trailingSymbol, options, renderOptions, submitted = false) => {
const value = renderNumberInput(state, submitted, renderOptions);
return renderPrompt(value, options.message, leadingSymbol, trailingSymbol, renderOptions);
};
const renderNumberNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const errorMsg = renderNumberError(state, figures.pointerSmall);
const promptMsg = renderNumberOutput(state, leadingSymbol, trailingSymbol, options);
return promptMsg + errorMsg;
});
const renderNumberSubmission = /*#__PURE__*/Effect.fnUntraced(function* (nextState, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const promptMsg = renderNumberOutput(nextState, leadingSymbol, trailingSymbol, options, undefined, true);
return promptMsg + "\n";
});
const processNumberBackspace = state => {
if (state.value.length <= 0) {
return Effect.succeed(Action.Beep());
}
const value = state.value.slice(0, state.value.length - 1);
return Effect.succeed(Action.NextFrame({
state: {
...state,
value,
error: Option.none()
}
}));
};
const processNumberClear = state => Effect.succeed(Action.NextFrame({
state: {
...state,
cursor: 0,
value: "",
error: Option.none()
}
}));
const defaultIntProcessor = (input, state) => {
if (state.value.length === 0 && input === "-") {
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: "-",
error: Option.none()
}
}));
}
const parsed = Number.parseInt(state.value + input);
if (Number.isNaN(parsed)) {
return Effect.succeed(Action.Beep());
} else {
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: `${parsed}`,
error: Option.none()
}
}));
}
};
const defaultFloatProcessor = (input, state) => {
if (input === "." && state.value.includes(".")) {
return Effect.succeed(Action.Beep());
}
if (state.value.length === 0 && input === "-") {
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: "-",
error: Option.none()
}
}));
}
const parsed = Number.parseFloat(state.value + input);
if (Number.isNaN(parsed)) {
return Effect.succeed(Action.Beep());
} else {
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: input === "." ? `${parsed}.` : `${parsed}`,
error: Option.none()
}
}));
}
};
const handleRenderInteger = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderNumberNextFrame(state, options),
Submit: () => renderNumberSubmission(state, options)
});
};
};
const handleProcessInteger = options => {
return (input, state) => {
if (input.key.ctrl && input.key.name === "u") {
return processNumberClear(state);
}
switch (input.key.name) {
case "backspace":
{
return processNumberBackspace(state);
}
case "k":
case "up":
{
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: state.value === "" || state.value === "-" ? `${options.incrementBy}` : `${Number.parseInt(state.value) + options.incrementBy}`,
error: Option.none()
}
}));
}
case "j":
case "down":
{
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: state.value === "" || state.value === "-" ? `-${options.decrementBy}` : `${Number.parseInt(state.value) - options.decrementBy}`,
error: Option.none()
}
}));
}
case "enter":
case "return":
{
const parsed = Number.parseInt(state.value);
if (Number.isNaN(parsed)) {
return Effect.succeed(Action.NextFrame({
state: {
...state,
error: Option.some("Must provide an integer value")
}
}));
} else {
return Effect.match(options.validate(parsed), {
onFailure: error => Action.NextFrame({
state: {
...state,
error: Option.some(error)
}
}),
onSuccess: value => Action.Submit({
value
})
});
}
}
default:
{
return defaultIntProcessor(Option.getOrElse(input.input, () => ""), state);
}
}
};
};
const handleRenderFloat = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderNumberNextFrame(state, options),
Submit: () => renderNumberSubmission(state, options)
});
};
};
const handleProcessFloat = options => {
return (input, state) => {
if (input.key.ctrl && input.key.name === "u") {
return processNumberClear(state);
}
switch (input.key.name) {
case "backspace":
{
return processNumberBackspace(state);
}
case "k":
case "up":
{
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: state.value === "" || state.value === "-" ? `${options.incrementBy}` : `${Number.parseFloat(state.value) + options.incrementBy}`,
error: Option.none()
}
}));
}
case "j":
case "down":
{
return Effect.succeed(Action.NextFrame({
state: {
...state,
value: state.value === "" || state.value === "-" ? `-${options.decrementBy}` : `${Number.parseFloat(state.value) - options.decrementBy}`,
error: Option.none()
}
}));
}
case "enter":
case "return":
{
const parsed = Number.parseFloat(state.value);
if (Number.isNaN(parsed)) {
return Effect.succeed(Action.NextFrame({
state: {
...state,
error: Option.some("Must provide a floating point value")
}
}));
} else {
return Effect.flatMap(Effect.sync(() => EffectNumber.round(parsed, options.precision)), rounded => Effect.match(options.validate(rounded), {
onFailure: error => Action.NextFrame({
state: {
...state,
error: Option.some(error)
}
}),
onSuccess: value => Action.Submit({
value
})
}));
}
}
default:
{
return defaultFloatProcessor(Option.getOrElse(input.input, () => ""), state);
}
}
};
};
const filterAutoCompleteChoices = (choices, query) => {
const normalizedQuery = query.toLowerCase();
const indices = [];
for (let i = 0; i < choices.length; i++) {
if (choices[i].title.toLowerCase().includes(normalizedQuery)) {
indices.push(i);
}
}
return indices;
};
const updateAutoCompleteState = (state, options, query) => {
const filtered = filterAutoCompleteChoices(options.choices, query);
if (filtered.length === 0) {
return {
...state,
query,
filtered,
index: 0
};
}
if (filtered.includes(state.index)) {
return {
...state,
query,
filtered
};
}
return {
...state,
query,
filtered,
index: filtered[0]
};
};
const autoCompleteCursor = state => Option.getOrElse(Arr.findFirstIndex(state.filtered, index => index === state.index), () => 0);
const renderSelectOutput = (leadingSymbol, trailingSymbol, options, renderOptions) => renderPrompt("", options.message, leadingSymbol, trailingSymbol, renderOptions);
const renderAutoCompleteFilter = (state, options, renderOptions) => {
const filterValue = state.query.length === 0 ? renderOptions?.plain === true ? options.filterPlaceholder : Ansi.annotate(options.filterPlaceholder, Ansi.blackBright) : renderOptions?.plain === true ? state.query : Ansi.annotate(state.query, Ansi.combine(Ansi.underlined, Ansi.cyanBright));
return `[${options.filterLabel}: ${filterValue}]`;
};
const renderAutoCompleteOutput = (state, leadingSymbol, trailingSymbol, options, renderOptions) => {
const filter = renderAutoCompleteFilter(state, options, renderOptions);
return renderPrompt(filter, options.message, leadingSymbol, trailingSymbol, renderOptions);
};
const renderChoicePrefix = (state, choices, toDisplay, currentIndex, figures, renderOptions) => {
let prefix = " ";
if (currentIndex === toDisplay.startIndex && toDisplay.startIndex > 0) {
prefix = figures.arrowUp;
} else if (currentIndex === toDisplay.endIndex - 1 && toDisplay.endIndex < choices.length) {
prefix = figures.arrowDown;
}
if (renderOptions?.plain === true) {
return state === currentIndex ? figures.pointer + prefix : prefix + " ";
}
if (choices[currentIndex].disabled) {
const annotation = Ansi.combine(Ansi.bold, Ansi.blackBright);
return state === currentIndex ? Ansi.annotate(figures.pointer, annotation) + prefix : prefix + " ";
}
return state === currentIndex ? Ansi.annotate(figures.pointer, Ansi.cyanBright) + prefix : prefix + " ";
};
const renderAutoCompleteChoicePrefix = (state, options, toDisplay, currentIndex, figures, renderOptions) => {
let prefix = " ";
if (currentIndex === toDisplay.startIndex && toDisplay.startIndex > 0) {
prefix = figures.arrowUp;
} else if (currentIndex === toDisplay.endIndex - 1 && toDisplay.endIndex < state.filtered.length) {
prefix = figures.arrowDown;
}
const choiceIndex = state.filtered[currentIndex];
if (renderOptions?.plain === true) {
return state.index === choiceIndex ? figures.pointer + prefix : prefix + " ";
}
const choice = options.choices[choiceIndex];
if (choice.disabled) {
const annotation = Ansi.combine(Ansi.bold, Ansi.blackBright);
return state.index === choiceIndex ? Ansi.annotate(figures.pointer, annotation) + prefix : prefix + " ";
}
return state.index === choiceIndex ? Ansi.annotate(figures.pointer, Ansi.cyanBright) + prefix : prefix + " ";
};
const renderChoiceTitle = (choice, isSelected, renderOptions) => {
if (renderOptions?.plain === true) {
return choice.title;
}
const title = choice.title;
if (isSelected) {
return choice.disabled ? Ansi.annotate(title, Ansi.combine(Ansi.underlined, Ansi.blackBright)) : Ansi.annotate(title, Ansi.combine(Ansi.underlined, Ansi.cyanBright));
}
return choice.disabled ? Ansi.annotate(title, Ansi.combine(Ansi.strikethrough, Ansi.blackBright)) : title;
};
const renderSelectChoices = (state, options, figures, renderOptions) => {
const choices = options.choices;
const toDisplay = entriesToDisplay(state, choices.length, options.maxPerPage);
const documents = [];
for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) {
const choice = choices[index];
const isSelected = state === index;
const prefix = renderChoicePrefix(state, choices, toDisplay, index, figures, renderOptions);
const title = renderChoiceTitle(choice, isSelected, renderOptions);
const description = renderChoiceDescription(choice, isSelected, renderOptions);
documents.push(prefix + title + " " + description);
}
return documents.join("\n");
};
const renderAutoCompleteChoices = (state, options, figures, renderOptions) => {
if (state.filtered.length === 0) {
return renderOptions?.plain === true ? options.emptyMessage : Ansi.annotate(options.emptyMessage, Ansi.blackBright);
}
const cursor = autoCompleteCursor(state);
const toDisplay = entriesToDisplay(cursor, state.filtered.length, options.maxPerPage);
const documents = [];
for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) {
const choiceIndex = state.filtered[index];
const choice = options.choices[choiceIndex];
const isSelected = state.index === choiceIndex;
const prefix = renderAutoCompleteChoicePrefix(state, options, toDisplay, index, figures, renderOptions);
const title = renderChoiceTitle(choice, isSelected, renderOptions);
const description = renderChoiceDescription(choice, isSelected, renderOptions);
documents.push(prefix + title + " " + description);
}
return documents.join("\n");
};
const renderSelectNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const choices = renderSelectChoices(state, options, figures);
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const promptMsg = renderSelectOutput(leadingSymbol, trailingSymbol, options);
return Ansi.cursorHide + promptMsg + "\n" + choices;
});
const renderAutoCompleteNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const choices = renderAutoCompleteChoices(state, options, figures);
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const promptMsg = renderAutoCompleteOutput(state, leadingSymbol, trailingSymbol, options);
return Ansi.cursorHide + promptMsg + "\n" + choices;
});
const renderSelectSubmission = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const selected = options.choices[state].title;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const promptMsg = renderSelectOutput(leadingSymbol, trailingSymbol, options);
return promptMsg + " " + Ansi.annotate(selected, Ansi.white) + "\n";
});
const renderAutoCompleteSubmission = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const selected = options.choices[state.index].title;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const promptMsg = renderAutoCompleteOutput(state, leadingSymbol, trailingSymbol, options);
return promptMsg + " " + Ansi.annotate(selected, Ansi.white) + "\n";
});
const processSelectCursorUp = (state, choices) => {
if (state === 0) {
return Effect.succeed(Action.NextFrame({
state: choices.length - 1
}));
}
return Effect.succeed(Action.NextFrame({
state: state - 1
}));
};
const processSelectCursorDown = (state, choices) => {
if (state === choices.length - 1) {
return Effect.succeed(Action.NextFrame({
state: 0
}));
}
return Effect.succeed(Action.NextFrame({
state: state + 1
}));
};
const processSelectNext = (state, choices) => {
return Effect.succeed(Action.NextFrame({
state: (state + 1) % choices.length
}));
};
const processAutoCompleteCursorUp = state => {
if (state.filtered.length === 0) {
return Effect.succeed(Action.Beep());
}
const cursor = autoCompleteCursor(state);
const nextCursor = cursor === 0 ? state.filtered.length - 1 : cursor - 1;
return Effect.succeed(Action.NextFrame({
state: {
...state,
index: state.filtered[nextCursor]
}
}));
};
const processAutoCompleteCursorDown = state => {
if (state.filtered.length === 0) {
return Effect.succeed(Action.Beep());
}
const cursor = autoCompleteCursor(state);
const nextCursor = (cursor + 1) % state.filtered.length;
return Effect.succeed(Action.NextFrame({
state: {
...state,
index: state.filtered[nextCursor]
}
}));
};
const processAutoCompleteNext = state => processAutoCompleteCursorDown(state);
const processAutoCompleteBackspace = (state, options) => {
if (state.query.length === 0) {
return Effect.succeed(Action.Beep());
}
const query = state.query.slice(0, state.query.length - 1);
return Effect.succeed(Action.NextFrame({
state: updateAutoCompleteState(state, options, query)
}));
};
const processAutoCompleteClear = (state, options) => Effect.succeed(Action.NextFrame({
state: updateAutoCompleteState(state, options, "")
}));
const processAutoCompleteInput = (input, state, options) => {
if (input.length === 0) {
return Effect.succeed(Action.Beep());
}
const query = state.query + input;
return Effect.succeed(Action.NextFrame({
state: updateAutoCompleteState(state, options, query)
}));
};
const handleSelectRender = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderSelectNextFrame(state, options),
Submit: () => renderSelectSubmission(state, options)
});
};
};
const handleAutoCompleteRender = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderAutoCompleteNextFrame(state, options),
Submit: () => renderAutoCompleteSubmission(state, options)
});
};
};
const handleSelectClear = options => Effect.fnUntraced(function* (state, _) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const clearPrompt = Ansi.eraseLine + Ansi.cursorLeft;
const promptText = renderSelectOutput("?", figures.pointerSmall, options, {
plain: true
});
const choicesText = renderSelectChoices(state, options, figures, {
plain: true
});
const clearOutput = eraseText(`${promptText}\n${choicesText}`, columns);
return clearOutput + clearPrompt;
});
const handleAutoCompleteClear = options => Effect.fnUntraced(function* (state, _) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const clearPrompt = Ansi.eraseLine + Ansi.cursorLeft;
const promptText = renderAutoCompleteOutput(state, "?", figures.pointerSmall, options, {
plain: true
});
const choicesText = renderAutoCompleteChoices(state, options, figures, {
plain: true
});
const clearOutput = eraseText(`${promptText}\n${choicesText}`, columns);
return clearOutput + clearPrompt;
});
const handleSelectProcess = options => {
return (input, state) => {
switch (input.key.name) {
case "k":
case "up":
{
return processSelectCursorUp(state, options.choices);
}
case "j":
case "down":
{
return processSelectCursorDown(state, options.choices);
}
case "tab":
{
return processSelectNext(state, options.choices);
}
case "enter":
case "return":
{
const selected = options.choices[state];
if (selected.disabled) {
return Effect.succeed(Action.Beep());
}
return Effect.succeed(Action.Submit({
value: selected.value
}));
}
default:
{
return Effect.succeed(Action.Beep());
}
}
};
};
const handleAutoCompleteProcess = options => {
return (input, state) => {
if (input.key.ctrl) {
if (input.key.name === "u") {
return processAutoCompleteClear(state, options);
}
return Effect.succeed(Action.Beep());
}
switch (input.key.name) {
case "k":
case "up":
{
return processAutoCompleteCursorUp(state);
}
case "j":
case "down":
{
return processAutoCompleteCursorDown(state);
}
case "tab":
{
return processAutoCompleteNext(state);
}
case "backspace":
{
return processAutoCompleteBackspace(state, options);
}
case "enter":
case "return":
{
if (state.filtered.length === 0) {
return Effect.succeed(Action.Beep());
}
const selected = options.choices[state.index];
if (selected.disabled) {
return Effect.succeed(Action.Beep());
}
return Effect.succeed(Action.Submit({
value: selected.value
}));
}
default:
{
return processAutoCompleteInput(Option.getOrElse(input.input, () => ""), state, options);
}
}
};
};
const renderClearScreen = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const resetCurrentLine = Ansi.eraseLine + Ansi.cursorLeft;
const errorText = renderTextError(state, figures.pointerSmall, {
plain: true
});
const clearOutput = clearOutputWithError(renderTextOutput(state, "?", figures.pointerSmall, options, {
plain: true
}), columns, errorText);
return clearOutput + resetCurrentLine;
});
const renderTextInput = (nextState, options, submitted, renderOptions) => {
const text = nextState.value;
if (renderOptions?.plain === true) {
switch (options.type) {
case "hidden":
{
return "";
}
case "password":
{
return "*".repeat(text.length);
}
case "text":
{
return text;
}
}
}
const annotation = Option.isSome(nextState.error) ? Ansi.red : submitted ? Ansi.white : nextState.value.length === 0 ? Ansi.blackBright : Ansi.combine(Ansi.underlined, Ansi.cyanBright);
switch (options.type) {
case "hidden":
{
return "";
}
case "password":
{
return Ansi.annotate("*".repeat(text.length), annotation);
}
case "text":
{
return Ansi.annotate(text, annotation);
}
}
};
const renderTextError = (nextState, pointer, renderOptions) => {
if (Option.isSome(nextState.error)) {
return Arr.match(nextState.error.value.split(NEWLINE_REGEXP), {
onEmpty: () => "",
onNonEmpty: errorLines => {
if (renderOptions?.plain === true) {
return `${pointer} ${errorLines.join("\n")}`;
}
const prefix = Ansi.annotate(pointer, Ansi.red) + " ";
const lines = Arr.map(errorLines, str => annotateErrorLine(str));
return Ansi.cursorSavePosition + "\n" + prefix + lines.join("\n") + Ansi.cursorRestorePosition;
}
});
}
return "";
};
const renderTextOutput = (nextState, leadingSymbol, trailingSymbol, options, renderOptions, submitted = false) => {
const value = renderTextInput(nextState, options, submitted, renderOptions);
return renderPrompt(value, options.message, leadingSymbol, trailingSymbol, renderOptions);
};
const renderTextNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const promptMsg = renderTextOutput(state, leadingSymbol, trailingSymbol, options);
const errorMsg = renderTextError(state, figures.pointerSmall);
const offset = state.cursor - state.value.length;
return promptMsg + errorMsg + Ansi.cursorMove(offset);
});
const renderTextSubmission = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const promptMsg = renderTextOutput(state, leadingSymbol, trailingSymbol, options, undefined, true);
return promptMsg + "\n";
});
const processTextBackspace = state => {
if (state.cursor <= 0) {
return Effect.succeed(Action.Beep());
}
const beforeCursor = state.value.slice(0, state.cursor - 1);
const afterCursor = state.value.slice(state.cursor);
const cursor = state.cursor - 1;
const value = `${beforeCursor}${afterCursor}`;
return Effect.succeed(Action.NextFrame({
state: {
...state,
cursor,
value,
error: Option.none()
}
}));
};
const processTextClear = state => Effect.succeed(Action.NextFrame({
state: {
...state,
cursor: 0,
value: "",
error: Option.none()
}
}));
const processTextCursorLeft = state => {
if (state.cursor <= 0) {
return Effect.succeed(Action.Beep());
}
const cursor = state.cursor - 1;
return Effect.succeed(Action.NextFrame({
state: {
...state,
cursor,
error: Option.none()
}
}));
};
const processTextCursorRight = state => {
if (state.cursor >= state.value.length) {
return Effect.succeed(Action.Beep());
}
const cursor = Math.min(state.cursor + 1, state.value.length);
return Effect.succeed(Action.NextFrame({
state: {
...state,
cursor,
error: Option.none()
}
}));
};
const processTextCursorStart = state => Effect.succeed(Action.NextFrame({
state: {
...state,
cursor: 0,
error: Option.none()
}
}));
const processTextCursorEnd = state => Effect.succeed(Action.NextFrame({
state: {
...state,
cursor: state.value.length,
error: Option.none()
}
}));
const processTab = (state, options) => {
if (state.value === options.default) {
return Effect.succeed(Action.Beep());
}
const value = state.value.length === 0 ? options.default : state.value;
return Effect.succeed(Action.NextFrame({
state: {
...state,
value,
cursor: value.length,
error: Option.none()
}
}));
};
const defaultTextProcessor = (input, state) => {
const beforeCursor = state.value.slice(0, state.cursor);
const afterCursor = state.value.slice(state.cursor);
const value = `${beforeCursor}${input}${afterCursor}`;
const cursor = state.cursor + input.length;
return Effect.succeed(Action.NextFrame({
state: {
...state,
cursor,
value,
error: Option.none()
}
}));
};
const handleTextRender = options => {
return (state, action) => {
return Action.$match(action, {
Beep: () => Effect.succeed(renderBeep),
NextFrame: ({
state
}) => renderTextNextFrame(state, options),
Submit: () => renderTextSubmission(state, options)
});
};
};
const handleTextProcess = options => {
return (input, state) => {
if (input.key.ctrl) {
switch (input.key.name) {
case "u":
{
return processTextClear(state);
}
case "a":
{
return processTextCursorStart(state);
}
case "e":
{
return processTextCursorEnd(state);
}
default:
{
return Effect.succeed(Action.Beep());
}
}
}
switch (input.key.name) {
case "backspace":
{
return processTextBackspace(state);
}
case "left":
{
return processTextCursorLeft(state);
}
case "right":
{
return processTextCursorRight(state);
}
case "home":
{
return processTextCursorStart(state);
}
case "end":
{
return processTextCursorEnd(state);
}
case "enter":
case "return":
{
const value = state.value;
return Effect.match(options.validate(value), {
onFailure: error => Action.NextFrame({
state: {
...state,
value,
error: Option.some(error)
}
}),
onSuccess: value => Action.Submit({
value
})
});
}
case "tab":
{
return processTab(state, options);
}
default:
{
return defaultTextProcessor(Option.getOrElse(input.input, () => ""), state);
}
}
};
};
const handleTextClear = options => {
return (state, _) => {
return renderClearScreen(state, options);
};
};
const basePrompt = (options, type) => {
const opts = {
default: "",
type,
validate: Effect.succeed,
...options
};
const initialState = {
cursor: opts.default.length,
value: opts.default,
error: Option.none()
};
return custom(initialState, {
render: handleTextRender(opts),
process: handleTextProcess(opts),
clear: handleTextClear(opts)
});
};
const handleToggleClear = /*#__PURE__*/Effect.fnUntraced(function* (options) {
const terminal = yield* Terminal.Terminal;
const columns = yield* terminal.columns;
const figures = yield* platformFigures;
const clearPrompt = Ansi.eraseLine + Ansi.cursorLeft;
const toggleText = `${options.active} / ${options.inactive}`;
const promptText = renderPrompt(toggleText, options.message, "?", figures.pointerSmall, {
plain: true
});
const clearOutput = eraseText(promptText, columns);
return clearOutput + clearPrompt;
});
const renderToggle = (value, options, submitted = false) => {
const separator = Ansi.annotate("/", Ansi.blackBright);
const selectedAnnotation = Ansi.combine(Ansi.underlined, submitted ? Ansi.white : Ansi.cyanBright);
const inactive = value ? options.inactive : Ansi.annotate(options.inactive, selectedAnnotation);
const active = value ? Ansi.annotate(options.active, selectedAnnotation) : options.active;
return active + " " + separator + " " + inactive;
};
const renderToggleOutput = (toggle, leadingSymbol, trailingSymbol, options) => {
const promptLines = options.message.split(NEWLINE_REGEXP);
const prefix = leadingSymbol + " ";
if (Arr.isReadonlyArrayNonEmpty(promptLines)) {
const lines = Arr.map(promptLines, line => annotateLine(line));
return prefix + lines.join("\n") + " " + trailingSymbol + " " + toggle;
}
return prefix + " " + trailingSymbol + " " + toggle;
};
const renderToggleNextFrame = /*#__PURE__*/Effect.fnUntraced(function* (state, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate("?", Ansi.cyanBright);
const trailingSymbol = Ansi.annotate(figures.pointerSmall, Ansi.blackBright);
const toggle = renderToggle(state, options);
const promptMsg = renderToggleOutput(toggle, leadingSymbol, trailingSymbol, options);
return Ansi.cursorHide + promptMsg;
});
const renderToggleSubmission = /*#__PURE__*/Effect.fnUntraced(function* (value, options) {
const figures = yield* platformFigures;
const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green);
const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright);
const toggle = renderToggle(value, options, true);
const promptMsg = renderToggleOutput(toggle, leadingSymbol, trailingSymbol, options);
return promptMsg + "\n";
});
const activate = /*#__PURE__*/Effect.succeed(/*#__PURE__*/Action.NextFrame({
state: true
}));
const deactivate = /*#__PURE__*/Effect.succeed(/*#__PURE__*/Action.NextFrame({
state: false
}));
const handleToggleRender = options => {
return (state, action) => {
switch (action._tag) {
case "Beep":
{
return Effect.succeed(renderBeep);
}
case "NextFrame":
{
return renderToggleNextFrame(state, options);
}
case "Submit":
{
return renderToggleSubmission(state, options);
}
}
};
};
const handleToggleProcess = (input, state) => {
switch (input.key.name) {
case "0":
case "j":
case "delete":
case "right":
case "down":
{
return deactivate;
}
case "1":
case "k":
case "left":
case "up":
{
return activate;
}
case " ":
case "tab":
{
return state ? deactivate : activate;
}
case "enter":
case "return":
{
return Effect.succeed(Action.Submit({
value: state
}));
}
default:
{
return Effect.succeed(Action.Beep());
}
}
};
const entriesToDisplay = (cursor, total, maxVisible) => {
const max = maxVisible === undefined ? total : maxVisible;
let startIndex = Math.min(total - max, cursor - Math.floor(max / 2));
if (startIndex < 0) {
startIndex = 0;
}
const endIndex = Math.min(startIndex + max, total);
return {
startIndex,
endIndex
};
};
//# sourceMappingURL=Prompt.js.map

Xet Storage Details

Size:
93.7 kB
·
Xet hash:
34fc364ee1497d10759509513ebbb016ef49dfc46a4a84b8007698fa3b9c1183

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.