Formatting pass

This commit is contained in:
themodernhakr 2025-05-17 01:24:03 -05:00
parent 136b3e386c
commit e925828328
9 changed files with 511 additions and 409 deletions

View File

@ -1,88 +1,92 @@
import { type ParseArgsOptionsConfig } from 'util' import { type ParseArgsOptionsConfig } from 'util';
import type { BrigadierTreeCommand, BrigadierTree } from "./types"; import type { BrigadierTreeCommand, BrigadierTree } from './types';
const commandObj: const commandObj: { _opts?: ParseArgsOptionsConfig } & Record<
{ _opts?: ParseArgsOptionsConfig } & string,
Record<string, BrigadierTreeCommand> = {} BrigadierTreeCommand
> = {};
commandObj._opts = { commandObj._opts = {
verbose: { verbose: {
type: "boolean", type: 'boolean',
short: 'v', short: 'v',
}, },
silent: { silent: {
type: "boolean", type: 'boolean',
short: "s", short: 's',
}, },
} };
commandObj.run = { commandObj.run = {
command: "run", command: 'run',
options: { options: {
speed: { speed: {
type: "string", type: 'string',
} },
} },
} };
commandObj.walk = { commandObj.walk = {
command: "walk", command: 'walk',
options: { options: {
fancy: { fancy: {
type: "boolean", type: 'boolean',
short: "f" short: 'f',
} },
} },
} };
commandObj.stand = { commandObj.stand = {
command: "stand", command: 'stand',
} };
commandObj.sit = { commandObj.sit = {
command: "sit", command: 'sit',
} };
commandObj.say = { commandObj.say = {
command: "say", command: 'say',
value: { value: {
required: true, required: true,
inputType: "string", inputType: 'string',
dataType: "string[]", dataType: 'string[]',
}, },
options: { options: {
accent: { accent: {
type: 'string', type: 'string',
default: "American", default: 'American',
} },
}, },
subcommands: [ subcommands: [
{ {
command: "talk", command: 'talk',
value: { value: {
required: true, required: true,
inputType: "string", inputType: 'string',
dataType: "string[]" dataType: 'string[]',
}, },
}, },
{ {
command: "whisper", command: 'whisper',
value: { value: {
required: true, required: true,
inputType: "string", inputType: 'string',
dataType: "string[]" dataType: 'string[]',
} },
}, },
{ {
command: "yell", command: 'yell',
value: { value: {
required: true, required: true,
inputType: "string", inputType: 'string',
dataType: "string[]" dataType: 'string[]',
} },
} },
] ],
} };
const { _opts, ...commands } = commandObj const { _opts, ...commands } = commandObj;
export const main: BrigadierTree = { _opts, commands: [...Object.values(commands)] } export const main: BrigadierTree = {
_opts,
commands: [...Object.values(commands)],
};

View File

@ -1,82 +1,82 @@
export class BrigadierInternalError extends Error { export class BrigadierInternalError extends Error {
constructor(error: Error | undefined = undefined) { constructor(error: Error | undefined = undefined) {
super() super();
this.name = this.constructor.name; this.name = this.constructor.name;
this.message = "The argument parser experience an internal error." this.message = 'The argument parser experience an internal error.';
if (error) this.cause = error if (error) this.cause = error;
} }
} }
export class BrigadierConfigError extends Error { export class BrigadierConfigError extends Error {
constructor(message: string, error: Error | undefined = undefined) { constructor(message: string, error: Error | undefined = undefined) {
super(message) super(message);
this.name = this.constructor.name; this.name = this.constructor.name;
if (error) this.cause = error if (error) this.cause = error;
} }
} }
export class BrigadierOptionError extends Error { export class BrigadierOptionError extends Error {
#defaultCode = "ERR_PARSE_ARGS_BRIGADIER_PARSE_OPTION"; #defaultCode = 'ERR_PARSE_ARGS_BRIGADIER_PARSE_OPTION';
#defaultMessages = { #defaultMessages = {
helpPrimitive: `Run '--help' for more information on how to use this command.`, helpPrimitive: `Run '--help' for more information on how to use this command.`,
get generic() { get generic() {
return `There was an error processing on of the given options. ${this.helpPrimitive}`; return `There was an error processing on of the given options. ${this.helpPrimitive}`;
}, },
get unknownOption() { get unknownOption() {
return `Unknown option. ${this.helpPrimitive}`; return `Unknown option. ${this.helpPrimitive}`;
}, },
get invalidOptionValue() { get invalidOptionValue() {
return `An option has an invalid value. ${this.helpPrimitive}`; return `An option has an invalid value. ${this.helpPrimitive}`;
}, },
get missingOptionValue() { get missingOptionValue() {
return `An option value is missing. ${this.helpPrimitive}`; return `An option value is missing. ${this.helpPrimitive}`;
}, },
}; };
option: string | undefined = undefined; option: string | undefined = undefined;
code: string = this.#defaultCode; code: string = this.#defaultCode;
constructor(message: string, error: Error) { constructor(message: string, error: Error) {
super(message); super(message);
if ("code" in error && typeof error.code === "string") if ('code' in error && typeof error.code === 'string')
this.#switchOnCodes(error.code); this.#switchOnCodes(error.code);
this.name = this.constructor.name; this.name = this.constructor.name;
if (error) this.cause = error; if (error) this.cause = error;
} }
#switchOnCodes(code: string) { #switchOnCodes(code: string) {
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") { if (code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
this.message = this.#unknownOption(this.message); this.message = this.#unknownOption(this.message);
this.code = code; this.code = code;
return; return;
} }
if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") { if (code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE') {
this.message = this.#invalidOptionValue(this.message); this.message = this.#invalidOptionValue(this.message);
this.code = code; this.code = code;
return; return;
} }
} }
#unknownOption(message: string) { #unknownOption(message: string) {
const index = 18; const index = 18;
if (typeof message !== "string" || message[index - 3] !== "'") if (typeof message !== 'string' || message[index - 3] !== "'")
return this.#defaultMessages.unknownOption; return this.#defaultMessages.unknownOption;
this.option = this.#extractOptionString(message, index, ". "); this.option = this.#extractOptionString(message, index, '. ');
return `Unknown option '--${this.option}'. ${this.#defaultMessages.helpPrimitive}`; return `Unknown option '--${this.option}'. ${this.#defaultMessages.helpPrimitive}`;
} }
#invalidOptionValue(message: string) { #invalidOptionValue(message: string) {
const index = 10; const index = 10;
if (typeof message !== "string" || message[index - 3] !== "'") if (typeof message !== 'string' || message[index - 3] !== "'")
return this.#defaultMessages.invalidOptionValue; return this.#defaultMessages.invalidOptionValue;
this.option = this.#extractOptionString(message, index, "<value>' "); this.option = this.#extractOptionString(message, index, "<value>' ");
return `Option '--${this.option}' has an invalid value. ${this.#defaultMessages.helpPrimitive}`; return `Option '--${this.option}' has an invalid value. ${this.#defaultMessages.helpPrimitive}`;
} }
#extractOptionString(message: string, index: number, testString: string) { #extractOptionString(message: string, index: number, testString: string) {
return message.slice(index, message.indexOf(testString) - 1); return message.slice(index, message.indexOf(testString) - 1);
} }
} }

View File

@ -1,139 +1,193 @@
import { tryCatch } from "@maxmorozoff/try-catch-tuple"; import { tryCatch } from '@maxmorozoff/try-catch-tuple';
import { import {
parseArgs, parseArgs,
type ParseArgsConfig, type ParseArgsConfig,
type ParseArgsOptionsConfig, type ParseArgsOptionsConfig,
} from "util"; } from 'util';
import { assertArrayOfStrings } from "../util/typeCheck"; import { assertArrayOfStrings } from '../util/typeCheck';
import { BrigadierConfigError, BrigadierInternalError, BrigadierOptionError } from "./errors"; import {
import type { BrigadierTree, BrigadierParserOverrides, BrigadierTreeCommand, BrigadierOutput } from "./types"; BrigadierConfigError,
import { treeUtils } from "./tree"; BrigadierInternalError,
BrigadierOptionError,
} from './errors';
import type {
BrigadierTree,
BrigadierParserOverrides,
BrigadierTreeCommand,
BrigadierOutput,
} from './types';
import { treeUtils } from './tree';
const defaultOverrides: BrigadierParserOverrides = { const defaultOverrides: BrigadierParserOverrides = {
allowPositionals: true, allowPositionals: true,
allowNegative: true, allowNegative: true,
tokens: false, tokens: false,
strict: true, strict: true,
}; };
function findNextTree(tree: Array<BrigadierTreeCommand>, command: string) { function findNextTree(tree: Array<BrigadierTreeCommand>, command: string) {
const nextCommand = tree.filter((obj) => obj.command === command) const nextCommand = tree.filter((obj) => obj.command === command);
return nextCommand[0]?.subcommands return nextCommand[0]?.subcommands;
} }
function findCommands(tree: Array<BrigadierTreeCommand> | undefined, args: Array<string>, out: Array<string> = []) { function findCommands(
const item = args[0] tree: Array<BrigadierTreeCommand> | undefined,
if (!item || !tree) return out args: Array<string>,
out: Array<string> = []
) {
const item = args[0];
if (!item || !tree) return out;
const [commands, commandError] = tryCatch(() => treeUtils.commandArray(tree)) const [commands, commandError] = tryCatch(() => treeUtils.commandArray(tree));
if (commandError) throw commandError if (commandError) throw commandError;
if (item.slice(0, 0) === "-" || !commands.includes(item)) return out if (item.slice(0, 0) === '-' || !commands.includes(item)) return out;
const [newTree, newTreeError] = tryCatch(() => findNextTree(tree, item)) const [newTree, newTreeError] = tryCatch(() => findNextTree(tree, item));
if (newTreeError) throw newTreeError if (newTreeError) throw newTreeError;
const newArgs = args.slice(1) const newArgs = args.slice(1);
const newOut = [...out, item] const newOut = [...out, item];
return findCommands(newTree, newArgs, newOut) return findCommands(newTree, newArgs, newOut);
} }
function findOpts(tree: Array<BrigadierTreeCommand>, commands: Array<string>, opts: ParseArgsOptionsConfig = {}) { function findOpts(
const item = commands[0] tree: Array<BrigadierTreeCommand>,
if (!item) throw new TypeError("The first item in the array is missing.") commands: Array<string>,
opts: ParseArgsOptionsConfig = {}
) {
const item = commands[0];
if (!item) throw new TypeError('The first item in the array is missing.');
const commandObj = tree.filter((obj) => obj.command === item) const commandObj = tree.filter((obj) => obj.command === item);
const thisOpts = commandObj[0]?.options ? commandObj[0].options : {} const thisOpts = commandObj[0]?.options ? commandObj[0].options : {};
const nextOpts = Object.assign(opts, thisOpts) const nextOpts = Object.assign(opts, thisOpts);
if (commands.length === 1) { if (commands.length === 1) {
const config = nextOpts const config = nextOpts;
return config return config;
} }
const nextTree = commandObj[0]?.subcommands
if (!nextTree) throw new BrigadierConfigError(`The '${item}' command was expected to have subcommands.`)
return findOpts(nextTree, commands.slice(1), nextOpts)
const nextTree = commandObj[0]?.subcommands;
if (!nextTree)
throw new BrigadierConfigError(
`The '${item}' command was expected to have subcommands.`
);
return findOpts(nextTree, commands.slice(1), nextOpts);
} }
function setParsedArgsConfig( function setParsedArgsConfig(
args: Array<string>, args: Array<string>,
options: ParseArgsOptionsConfig, options: ParseArgsOptionsConfig,
overrides: BrigadierParserOverrides | undefined, overrides: BrigadierParserOverrides | undefined,
commands: Array<string> commands: Array<string>
) { ) {
if (!assertArrayOfStrings(args)) if (!assertArrayOfStrings(args))
throw new TypeError(`Parameter "args" must be an array of strings`); throw new TypeError(`Parameter "args" must be an array of strings`);
const obj = const obj =
overrides === undefined overrides === undefined
? defaultOverrides ? defaultOverrides
: Object.assign({}, defaultOverrides, overrides); : Object.assign({}, defaultOverrides, overrides);
const configObj: ParseArgsConfig = { const configObj: ParseArgsConfig = {
args, args,
options, options,
...obj, ...obj,
}; };
return { config: configObj, commands: commands }; return { config: configObj, commands: commands };
} }
function generateParsedArgsConfig(tree: BrigadierTree, args: Array<string>, overrides: BrigadierParserOverrides | undefined = undefined) { function generateParsedArgsConfig(
const [commandsArray, commandsError] = tryCatch(() => findCommands(tree.commands, args.slice(2))) tree: BrigadierTree,
if (commandsError) throw commandsError args: Array<string>,
if (!commandsArray[0]) { overrides: BrigadierParserOverrides | undefined = undefined
const opts = tree._opts ) {
return setParsedArgsConfig(args, opts, overrides, commandsArray) const [commandsArray, commandsError] = tryCatch(() =>
} findCommands(tree.commands, args.slice(2))
);
if (commandsError) throw commandsError;
if (!commandsArray[0]) {
const opts = tree._opts;
return setParsedArgsConfig(args, opts, overrides, commandsArray);
}
const [commandOpts, commandOptsError] = tryCatch(() => findOpts(tree.commands, commandsArray)) const [commandOpts, commandOptsError] = tryCatch(() =>
if (commandOptsError) throw commandOptsError findOpts(tree.commands, commandsArray)
const opts = Object.assign({}, tree._opts, commandOpts) );
return setParsedArgsConfig(args, opts, overrides, commandsArray) if (commandOptsError) throw commandOptsError;
const opts = Object.assign({}, tree._opts, commandOpts);
return setParsedArgsConfig(args, opts, overrides, commandsArray);
} }
function getParsedArgs(opts: { config: ParseArgsConfig, commands: Array<string> }) { function getParsedArgs(opts: {
const [result, error] = tryCatch(() => { config: ParseArgsConfig;
return parseArgs(opts.config); commands: Array<string>;
}); }) {
if (error) throw new BrigadierOptionError(error.message, error); const [result, error] = tryCatch(() => {
return { ...result, commands: opts.commands }; return parseArgs(opts.config);
});
if (error) throw new BrigadierOptionError(error.message, error);
return { ...result, commands: opts.commands };
} }
function buildPositionalsArray(commands: Array<string>, positionals: Array<string>) { function buildPositionalsArray(
const testSet = new Set(commands) commands: Array<string>,
return positionals.filter(str => !testSet.has(str)) positionals: Array<string>
) {
const testSet = new Set(commands);
return positionals.filter((str) => !testSet.has(str));
} }
function handleErr(error: Error) { function handleErr(error: Error) {
if (error instanceof BrigadierOptionError || error instanceof BrigadierConfigError) return error if (
return new BrigadierInternalError() error instanceof BrigadierOptionError ||
error instanceof BrigadierConfigError
)
return error;
return new BrigadierInternalError();
} }
function buildOutputObject(tree: BrigadierTree, args: Array<string>, overrides: BrigadierParserOverrides | undefined = undefined) { function buildOutputObject(
try { tree: BrigadierTree,
const [config, configError] = tryCatch(() => generateParsedArgsConfig(tree, args, overrides)) args: Array<string>,
if (configError) throw configError overrides: BrigadierParserOverrides | undefined = undefined
) {
try {
const [config, configError] = tryCatch(() =>
generateParsedArgsConfig(tree, args, overrides)
);
if (configError) throw configError;
const [parsedArgs, parsedError] = tryCatch(() => getParsedArgs(config)) const [parsedArgs, parsedError] = tryCatch(() => getParsedArgs(config));
if (parsedError) throw parsedError if (parsedError) throw parsedError;
const programPaths = { const programPaths = {
bun: parsedArgs.positionals[0] ? parsedArgs.positionals[0] : "", bun: parsedArgs.positionals[0] ? parsedArgs.positionals[0] : '',
path: parsedArgs.positionals[1] ? parsedArgs.positionals[1] : "", path: parsedArgs.positionals[1] ? parsedArgs.positionals[1] : '',
} };
const command = config.commands[0] ? { command: config.commands[0] } : {} const command = config.commands[0] ? { command: config.commands[0] } : {};
const subcommands = config.commands[1] ? { subcommands: config.commands.slice(1) } : {} const subcommands = config.commands[1]
const [positionals, positionalsError] = tryCatch(() => buildPositionalsArray(config.commands, parsedArgs.positionals.slice(2))) ? { subcommands: config.commands.slice(1) }
if (positionalsError) throw positionalsError : {};
const positionalsObj = positionals.length > 0 ? { positionals } : {} const [positionals, positionalsError] = tryCatch(() =>
const values = parsedArgs.values ? parsedArgs.values : {} buildPositionalsArray(config.commands, parsedArgs.positionals.slice(2))
);
if (positionalsError) throw positionalsError;
const positionalsObj = positionals.length > 0 ? { positionals } : {};
const values = parsedArgs.values ? parsedArgs.values : {};
const out: BrigadierOutput = Object.assign({}, programPaths, command, subcommands, positionalsObj, { values }) const out: BrigadierOutput = Object.assign(
return out {},
} catch (error) { programPaths,
throw handleErr(error as Error) command,
} subcommands,
positionalsObj,
{ values }
);
return out;
} catch (error) {
throw handleErr(error as Error);
}
} }
export const main = buildOutputObject; export const main = buildOutputObject;

View File

@ -1,13 +1,12 @@
import { demoTree } from "."; import { demoTree } from '.';
import type { BrigadierTreeCommand } from "./types"; import type { BrigadierTreeCommand } from './types';
function commandArray(array: Array<BrigadierTreeCommand>) { function commandArray(array: Array<BrigadierTreeCommand>) {
const out: Array<string> = [] const out: Array<string> = [];
array.forEach((obj) => out.push(obj.command)) array.forEach((obj) => out.push(obj.command));
return out return out;
} }
export const treeUtils = { export const treeUtils = {
commandArray commandArray,
} };

View File

@ -1,49 +1,48 @@
import { type ParseArgsOptionsConfig } from 'util' import { type ParseArgsOptionsConfig } from 'util';
export type BrigadierCommandValue = export type BrigadierCommandValue =
"string" | | 'string'
"number" | | 'number'
"boolean" | | 'boolean'
"string[]" | | 'string[]'
"number[]" | | 'number[]'
"boolean[]" | | 'boolean[]'
"mixed[]" | 'mixed[]';
export type BrigadierTreeCommand = { export type BrigadierTreeCommand = {
command: string, command: string;
value?: { value?: {
required: boolean, required: boolean;
inputType: "string" | "boolean", inputType: 'string' | 'boolean';
dataType: BrigadierCommandValue, dataType: BrigadierCommandValue;
default?: string | boolean default?: string | boolean;
} };
options?: ParseArgsOptionsConfig, options?: ParseArgsOptionsConfig;
subcommands?: Array<BrigadierTreeCommand> subcommands?: Array<BrigadierTreeCommand>;
} };
export type BrigadierTree = { export type BrigadierTree = {
_opts: ParseArgsOptionsConfig, _opts: ParseArgsOptionsConfig;
commands: Array<BrigadierTreeCommand> commands: Array<BrigadierTreeCommand>;
} };
export type BrigadierOutput = { export type BrigadierOutput = {
bun: string, bun: string;
path: string, path: string;
command?: string, command?: string;
subcommands?: Array<string>, subcommands?: Array<string>;
positionals?: Array<string>, positionals?: Array<string>;
values?: Record<string, string | boolean> values?: Record<string, string | boolean>;
} };
export type BrigadierParserOverrides = { export type BrigadierParserOverrides = {
strict?: boolean; strict?: boolean;
allowPositionals?: boolean; allowPositionals?: boolean;
allowNegative?: boolean; allowNegative?: boolean;
tokens?: boolean; tokens?: boolean;
}; };
export type BrigadierInput = BrigadierTree & { export type BrigadierInput = BrigadierTree & {
args: Array<string> args: Array<string>;
overrides?: BrigadierParserOverrides; overrides?: BrigadierParserOverrides;
}; };

View File

@ -1,3 +1,5 @@
# Features to Impliment
## Basic Inputs ## Basic Inputs
- [ ] **Text Input** User types a free-form response. - [ ] **Text Input** User types a free-form response.

View File

@ -4,125 +4,166 @@ import { TerminalUtils, type CursorPosition } from '../util/terminal';
import { assertArrayOfStrings } from '../util/typeCheck'; import { assertArrayOfStrings } from '../util/typeCheck';
export type PromptsmithConfig = { export type PromptsmithConfig = {
format?: { format?: {
selected?: string, selected?: string;
selectedText?: string, selectedText?: string;
defualt?: string, defualt?: string;
defualtText?: string, defualtText?: string;
}, };
selector?: { selector?: {
selected?: string, selected?: string;
default?: string, default?: string;
} };
} };
// A pure function to render options without side effects // A pure function to render options without side effects
const formatText = (text: string, format: string): string => `${format}${text}\x1B[0m`; const formatText = (text: string, format: string): string =>
`${format}${text}\x1B[0m`;
const renderOptions = (options: string[], selectedIndex: number, pos: CursorPosition, config: PromptsmithConfig): string[] => { const renderOptions = (
TerminalUtils.cursor.restorePosition(pos); options: string[],
TerminalUtils.clear.below(); selectedIndex: number,
pos: CursorPosition,
config: PromptsmithConfig
): string[] => {
TerminalUtils.cursor.restorePosition(pos);
TerminalUtils.clear.below();
return [ return [
formatText('Use the ↑ and ↓ keys to select an option. Press ENTER to confirm.\n', '\x1B[1m'), // Bold formatText(
...options.map((option, index) => { 'Use the ↑ and ↓ keys to select an option. Press ENTER to confirm.\n',
const isSelected = index === selectedIndex; '\x1B[1m'
const selector = formatText( ), // Bold
isSelected ? config.selector.selected as string : config.selector.default as string, ...options.map((option, index) => {
isSelected ? config.format.selected as string : config.format.defualt as string const isSelected = index === selectedIndex;
) const selector = formatText(
const optionText = formatText( isSelected
option, ? (config.selector.selected as string)
isSelected ? config.format.selectedText as string : config.format.defualtText as string : (config.selector.default as string),
) isSelected
return selector + optionText ? (config.format.selected as string)
}), : (config.format.defualt as string)
]; );
const optionText = formatText(
option,
isSelected
? (config.format.selectedText as string)
: (config.format.defualtText as string)
);
return selector + optionText;
}),
];
}; };
// A pure function to update selected index based on input key // A pure function to update selected index based on input key
const updateSelection = (selectedIndex: number, keyName: string, options: string[]): number => const updateSelection = (
keyName === 'up' ? Math.max(selectedIndex - 1, 0) : selectedIndex: number,
keyName === 'down' ? Math.min(selectedIndex + 1, options.length - 1) : keyName: string,
selectedIndex; options: string[]
): number =>
keyName === 'up'
? Math.max(selectedIndex - 1, 0)
: keyName === 'down'
? Math.min(selectedIndex + 1, options.length - 1)
: selectedIndex;
// Handles keypress events as pure transformations // Handles keypress events as pure transformations
const handleKeyPress = async (options: string[], selectedIndex: number, pos: CursorPosition, config: PromptsmithConfig): Promise<string> => { const handleKeyPress = async (
return new Promise((resolve) => { options: string[],
if (!assertArrayOfStrings(options)) throw new TypeError("'options' should be Array<string>.") selectedIndex: number,
pos: CursorPosition,
config: PromptsmithConfig
): Promise<string> => {
return new Promise((resolve) => {
if (!assertArrayOfStrings(options))
throw new TypeError("'options' should be Array<string>.");
process.stdin.resume(); process.stdin.resume();
readline.emitKeypressEvents(process.stdin); readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY) process.stdin.setRawMode(true); if (process.stdin.isTTY) process.stdin.setRawMode(true);
const onKeyPress = async (_: string, key: readline.Key) => { const onKeyPress = async (_: string, key: readline.Key) => {
const newIndex = updateSelection(selectedIndex, key.name as string, options); const newIndex = updateSelection(
selectedIndex,
key.name as string,
options
);
if (key.name === 'return') { if (key.name === 'return') {
cleanup(); cleanup();
resolve(options[newIndex] as string); resolve(options[newIndex] as string);
return; return;
} else if (key.name === 'c' && key.ctrl) { } else if (key.name === 'c' && key.ctrl) {
cleanup(); cleanup();
process.exit(); process.exit();
} }
renderOptions(options, newIndex, pos, config).forEach(line => console.log(line)); renderOptions(options, newIndex, pos, config).forEach((line) =>
selectedIndex = newIndex; console.log(line)
}; );
selectedIndex = newIndex;
};
process.stdin.on('keypress', onKeyPress); process.stdin.on('keypress', onKeyPress);
const cleanup = () => { const cleanup = () => {
process.stdin.removeListener('keypress', onKeyPress); process.stdin.removeListener('keypress', onKeyPress);
if (process.stdin.isTTY) process.stdin.setRawMode(false); if (process.stdin.isTTY) process.stdin.setRawMode(false);
TerminalUtils.cursor.restorePosition(pos) TerminalUtils.cursor.restorePosition(pos);
TerminalUtils.clear.below() TerminalUtils.clear.below();
process.stdin.pause(); process.stdin.pause();
}; };
}); });
}; };
const setConfig = (config: PromptsmithConfig) => { const setConfig = (config: PromptsmithConfig) => {
if (!config.format) config.format = {} if (!config.format) config.format = {};
if (!config.format.selected) config.format.selected = '\x1B[32m\x1B[1m' if (!config.format.selected) config.format.selected = '\x1B[32m\x1B[1m';
if (!config.format.selectedText) config.format.selectedText = config.format.selected if (!config.format.selectedText)
if (!config.format.defualt) config.format.defualt = '' config.format.selectedText = config.format.selected;
if (!config.format.defualtText) config.format.defualtText = config.format.defualt if (!config.format.defualt) config.format.defualt = '';
if (!config.format.defualtText)
config.format.defualtText = config.format.defualt;
if (!config.selector) config.selector = {} if (!config.selector) config.selector = {};
if (!config.selector.default) config.selector.default = ' ' if (!config.selector.default) config.selector.default = ' ';
if (!config.selector.selected) config.selector.selected = '> ' if (!config.selector.selected) config.selector.selected = '> ';
return config return config;
} };
// Main function using pure composition // Main function using pure composition
const defaultConfig: PromptsmithConfig = { const defaultConfig: PromptsmithConfig = {
format: { format: {
selected: '\x1B[32m\x1B[1m', selected: '\x1B[32m\x1B[1m',
}, },
selector: { selector: {
selected: '> ', selected: '> ',
default: ' ' default: ' ',
} },
} };
const interactiveSelect = async (options: string[], config: PromptsmithConfig = {}): Promise<string> => { const interactiveSelect = async (
const newConfig = setConfig(config) options: string[],
config: PromptsmithConfig = {}
): Promise<string> => {
const newConfig = setConfig(config);
const pos = await TerminalUtils.cursor.getCursorPosition(); const pos = await TerminalUtils.cursor.getCursorPosition();
renderOptions(options, 0, pos, newConfig).forEach(line => console.log(line)); renderOptions(options, 0, pos, newConfig).forEach((line) =>
return handleKeyPress(options, 0, pos, newConfig); console.log(line)
);
return handleKeyPress(options, 0, pos, newConfig);
}; };
// Execution // Execution
(async () => { (async () => {
const options = ['Option A', 'Option B', 'Option C']; const options = ['Option A', 'Option B', 'Option C'];
const selectedOption = await interactiveSelect(options, { selector: { selected: '* ' } }); const selectedOption = await interactiveSelect(options, {
console.log(`You selected: ${selectedOption}`); selector: { selected: '* ' },
});
console.log(`You selected: ${selectedOption}`);
process.stdin.pause(); // Ensures stdin closes AFTER selection process.stdin.pause(); // Ensures stdin closes AFTER selection
})(); })();

View File

@ -3,47 +3,50 @@
export type CursorPosition = { row: number; col: number }; export type CursorPosition = { row: number; col: number };
export const TerminalUtils = { export const TerminalUtils = {
clear: { clear: {
screen: () => process.stdout.write('\x1Bc'), screen: () => process.stdout.write('\x1Bc'),
content: () => process.stdout.write('\x1B[2J'), content: () => process.stdout.write('\x1B[2J'),
cursorHome: () => process.stdout.write('\x1B[H'), cursorHome: () => process.stdout.write('\x1B[H'),
line: () => process.stdout.write('\x1B[K'), line: () => process.stdout.write('\x1B[K'),
below: () => process.stdout.write('\x1B[J'), below: () => process.stdout.write('\x1B[J'),
above: () => process.stdout.write('\x1B[1J'), above: () => process.stdout.write('\x1B[1J'),
clear: () => console.clear(), clear: () => console.clear(),
}, },
cursor: { /** Move cursor to a specific row & column */ cursor: {
moveCursor: (row: number, col: number) => process.stdout.write(`\x1B[${row};${col}H`), /** Move cursor to a specific row & column */
moveCursor: (row: number, col: number) =>
process.stdout.write(`\x1B[${row};${col}H`),
/** Queries the terminal for the current cursor position */ /** Queries the terminal for the current cursor position */
async getCursorPosition(): Promise<CursorPosition> { async getCursorPosition(): Promise<CursorPosition> {
return new Promise((resolve) => { return new Promise((resolve) => {
const stdin = process.stdin; const stdin = process.stdin;
stdin.setRawMode(true); stdin.setRawMode(true);
stdin.resume(); stdin.resume();
stdin.setEncoding('utf8'); stdin.setEncoding('utf8');
stdin.once('data', (data) => { stdin.once('data', (data) => {
const match = /\[(\d+);(\d+)R/.exec(data.toString()); const match = /\[(\d+);(\d+)R/.exec(data.toString());
resolve({ resolve({
row: parseInt(match?.[1] ?? '0', 10), row: parseInt(match?.[1] ?? '0', 10),
col: parseInt(match?.[2] ?? '0', 10), col: parseInt(match?.[2] ?? '0', 10),
}); });
stdin.setRawMode(false); stdin.setRawMode(false);
stdin.pause(); stdin.pause();
}); });
process.stdout.write('\x1B[6n'); // Ask terminal for cursor position process.stdout.write('\x1B[6n'); // Ask terminal for cursor position
}); });
}, },
/** Stores detected cursor position in a constant */ /** Stores detected cursor position in a constant */
async storePosition(): Promise<CursorPosition> { async storePosition(): Promise<CursorPosition> {
return TerminalUtils.cursor.getCursorPosition(); return TerminalUtils.cursor.getCursorPosition();
}, },
/** Restores the passed cursor position */ /** Restores the passed cursor position */
restorePosition: (pos: CursorPosition) => TerminalUtils.cursor.moveCursor(pos.row, pos.col), restorePosition: (pos: CursorPosition) =>
} TerminalUtils.cursor.moveCursor(pos.row, pos.col),
},
}; };

View File

@ -1,5 +1,5 @@
export function assertArrayOfStrings(arr: unknown) { export function assertArrayOfStrings(arr: unknown) {
if (!Array.isArray(arr)) return false; if (!Array.isArray(arr)) return false;
if (!arr.every((item: unknown) => typeof item === "string")) return false; if (!arr.every((item: unknown) => typeof item === 'string')) return false;
return true; return true;
} }