Create propmptsmith lib with prototype select-option
This commit is contained in:
parent
825fe82168
commit
4fd0118ac1
44
src/lib/promptsmith/README.md
Normal file
44
src/lib/promptsmith/README.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
## Basic Inputs
|
||||||
|
|
||||||
|
- [ ] **Text Input** – User types a free-form response.
|
||||||
|
- [ ] **Password Input** – Masked input for sensitive information.
|
||||||
|
- [ ] **Multi-line Input** – Allows extended text responses.
|
||||||
|
- [ ] **Autocomplete** – Suggests completions based on partial input.
|
||||||
|
|
||||||
|
## Selection & Choice
|
||||||
|
|
||||||
|
- [ ] **Single Choice (Radio List)** – User selects one option from a list.
|
||||||
|
- [ ] **Multiple Choice (Checkbox List)** – User selects multiple options.
|
||||||
|
- [ ] **Hierarchical Menus** – Options lead to submenus for deeper selection.
|
||||||
|
- [ ] **Searchable Select** – Allows filtering of choices dynamically.
|
||||||
|
|
||||||
|
## Confirmation & Boolean Responses
|
||||||
|
|
||||||
|
- [ ] **Yes/No Confirmation** – Quick validation prompt.
|
||||||
|
- [ ] **Custom Boolean Toggle** – Enables/disables a setting interactively.
|
||||||
|
|
||||||
|
## Numeric & Range Inputs
|
||||||
|
|
||||||
|
- [ ] **Direct Number Input** – Accepts numeric values.
|
||||||
|
- [ ] **Slider/Range Selector** – Interactive range selection (e.g., choose a value between 1–10).
|
||||||
|
- [ ] **Incremental Stepper** – Adjusts numeric values up/down.
|
||||||
|
|
||||||
|
## Progress & Status Indicators
|
||||||
|
|
||||||
|
- [ ] **Loading Indicators** – Displays progress for background tasks.
|
||||||
|
- [ ] **Spinners & Animations** – Adds visual feedback while waiting.
|
||||||
|
- [ ] **Progress Bars** – Tracks completion percentage.
|
||||||
|
|
||||||
|
## Navigation & Actions
|
||||||
|
|
||||||
|
- [ ] **Interactive Tables** – Allows selection from tabular data.
|
||||||
|
- [ ] **File Picker** – User selects files interactively.
|
||||||
|
- [ ] **Date/Time Picker** – User selects a timestamp interactively.
|
||||||
|
- [ ] **Shortcut Actions** – Fast execution based on predefined keys.
|
||||||
|
- [ ] **Undo/Redo Prompts** – Allows correction of previous inputs.
|
||||||
|
|
||||||
|
## Advanced & Custom Interactions
|
||||||
|
- [ ] **Drag-and-Drop Items (in CLI format)** – Reorder items interactively.
|
||||||
|
- [ ] **Text-Based Dialogs** – Simulates step-by-step conversational input.
|
||||||
|
- [ ] **Inline Editing** – Lets users modify text inline.
|
||||||
|
- [ ] **Command Suggestions** – Auto-suggests commands based on context.
|
||||||
128
src/lib/promptsmith/select-options.ts
Normal file
128
src/lib/promptsmith/select-options.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env bun run
|
||||||
|
import * as readline from 'node:readline';
|
||||||
|
import { TerminalUtils, type CursorPosition } from '../util/terminal';
|
||||||
|
import { assertArrayOfStrings } from '../util/typeCheck';
|
||||||
|
|
||||||
|
export type PromptsmithConfig = {
|
||||||
|
format?: {
|
||||||
|
selected?: string,
|
||||||
|
selectedText?: string,
|
||||||
|
defualt?: string,
|
||||||
|
defualtText?: string,
|
||||||
|
},
|
||||||
|
selector?: {
|
||||||
|
selected?: string,
|
||||||
|
default?: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pure function to render options without side effects
|
||||||
|
const formatText = (text: string, format: string): string => `${format}${text}\x1B[0m`;
|
||||||
|
|
||||||
|
const renderOptions = (options: string[], selectedIndex: number, pos: CursorPosition, config: PromptsmithConfig): string[] => {
|
||||||
|
TerminalUtils.cursor.restorePosition(pos);
|
||||||
|
TerminalUtils.clear.below();
|
||||||
|
|
||||||
|
return [
|
||||||
|
formatText('Use the ↑ and ↓ keys to select an option. Press ENTER to confirm.\n', '\x1B[1m'), // Bold
|
||||||
|
...options.map((option, index) => {
|
||||||
|
const isSelected = index === selectedIndex;
|
||||||
|
const selector = formatText(
|
||||||
|
isSelected ? config.selector.selected as string : config.selector.default as string,
|
||||||
|
isSelected ? 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
|
||||||
|
const updateSelection = (selectedIndex: number, keyName: string, 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
|
||||||
|
const handleKeyPress = async (options: 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();
|
||||||
|
readline.emitKeypressEvents(process.stdin);
|
||||||
|
|
||||||
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
||||||
|
|
||||||
|
const onKeyPress = async (_: string, key: readline.Key) => {
|
||||||
|
const newIndex = updateSelection(selectedIndex, key.name as string, options);
|
||||||
|
|
||||||
|
if (key.name === 'return') {
|
||||||
|
cleanup();
|
||||||
|
resolve(options[newIndex] as string);
|
||||||
|
return;
|
||||||
|
} else if (key.name === 'c' && key.ctrl) {
|
||||||
|
cleanup();
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderOptions(options, newIndex, pos, config).forEach(line => console.log(line));
|
||||||
|
selectedIndex = newIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
process.stdin.on('keypress', onKeyPress);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
process.stdin.removeListener('keypress', onKeyPress);
|
||||||
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
||||||
|
TerminalUtils.cursor.restorePosition(pos)
|
||||||
|
TerminalUtils.clear.below()
|
||||||
|
process.stdin.pause();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setConfig = (config: PromptsmithConfig) => {
|
||||||
|
if (!config.format) config.format = {}
|
||||||
|
|
||||||
|
if (!config.format.selected) config.format.selected = '\x1B[32m\x1B[1m'
|
||||||
|
if (!config.format.selectedText) config.format.selectedText = config.format.selected
|
||||||
|
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.default) config.selector.default = ' '
|
||||||
|
if (!config.selector.selected) config.selector.selected = '> '
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function using pure composition
|
||||||
|
const defaultConfig: PromptsmithConfig = {
|
||||||
|
format: {
|
||||||
|
selected: '\x1B[32m\x1B[1m',
|
||||||
|
},
|
||||||
|
selector: {
|
||||||
|
selected: '> ',
|
||||||
|
default: ' '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const interactiveSelect = async (options: string[], config: PromptsmithConfig = {}): Promise<string> => {
|
||||||
|
const newConfig = setConfig(config)
|
||||||
|
|
||||||
|
const pos = await TerminalUtils.cursor.getCursorPosition();
|
||||||
|
renderOptions(options, 0, pos, newConfig).forEach(line => console.log(line));
|
||||||
|
return handleKeyPress(options, 0, pos, newConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
(async () => {
|
||||||
|
const options = ['Option A', 'Option B', 'Option C'];
|
||||||
|
const selectedOption = await interactiveSelect(options, { selector: { selected: '* ' } });
|
||||||
|
console.log(`You selected: ${selectedOption}`);
|
||||||
|
|
||||||
|
process.stdin.pause(); // Ensures stdin closes AFTER selection
|
||||||
|
})();
|
||||||
|
|
||||||
49
src/lib/util/terminal.ts
Normal file
49
src/lib/util/terminal.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
export type CursorPosition = { row: number; col: number };
|
||||||
|
|
||||||
|
export const TerminalUtils = {
|
||||||
|
clear: {
|
||||||
|
screen: () => process.stdout.write('\x1Bc'),
|
||||||
|
content: () => process.stdout.write('\x1B[2J'),
|
||||||
|
cursorHome: () => process.stdout.write('\x1B[H'),
|
||||||
|
line: () => process.stdout.write('\x1B[K'),
|
||||||
|
below: () => process.stdout.write('\x1B[J'),
|
||||||
|
above: () => process.stdout.write('\x1B[1J'),
|
||||||
|
clear: () => console.clear(),
|
||||||
|
},
|
||||||
|
|
||||||
|
cursor: { /** 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 */
|
||||||
|
async getCursorPosition(): Promise<CursorPosition> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const stdin = process.stdin;
|
||||||
|
stdin.setRawMode(true);
|
||||||
|
stdin.resume();
|
||||||
|
stdin.setEncoding('utf8');
|
||||||
|
|
||||||
|
stdin.once('data', (data) => {
|
||||||
|
const match = /\[(\d+);(\d+)R/.exec(data.toString());
|
||||||
|
resolve({
|
||||||
|
row: parseInt(match?.[1] ?? '0', 10),
|
||||||
|
col: parseInt(match?.[2] ?? '0', 10),
|
||||||
|
});
|
||||||
|
stdin.setRawMode(false);
|
||||||
|
stdin.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdout.write('\x1B[6n'); // Ask terminal for cursor position
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Stores detected cursor position in a constant */
|
||||||
|
async storePosition(): Promise<CursorPosition> {
|
||||||
|
return TerminalUtils.cursor.getCursorPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Restores the passed cursor position */
|
||||||
|
restorePosition: (pos: CursorPosition) => TerminalUtils.cursor.moveCursor(pos.row, pos.col),
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user