Progress on re-write of select-options

This commit is contained in:
Eric Rumsey 2025-06-02 15:38:58 -05:00
parent e9216cc3e4
commit 459080d1e6
3 changed files with 232 additions and 167 deletions

View File

@ -0,0 +1,169 @@
#!/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
})();

View File

@ -1,169 +1,64 @@
#!/usr/bin/env bun run
import * as readline from 'node:readline';
import { TerminalUtils, type CursorPosition } from '../util/terminal';
import { assertArrayOfStrings } from '../util/typeCheck';
import type { Key } from 'node:readline'
import {
exit as EXIT,
type PainterEffect,
type PainterState,
type PainterTask,
task as TASK,
} from '../painter/lifecycle'
import { type AnsiBufferKey } from '../util/ansi'
import { DoEither, type Either, right } from '../util/basic/either'
import { some } from '../util/basic/option'
import type { Tag } from '../util/basic/utility'
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 = {
interface SelectOptions {
keyup: Array<Key>
keydown: Array<Key>
exit: Array<Key>
accept: Array<Key>
selector: string
options: Array<string>
header: {text: string, format: Array<AnsiBufferKey | Uint8Array>}
format: {
selected: '\x1B[32m\x1B[1m',
text: {
default: Array<AnsiBufferKey | Uint8Array>,
defaultSelected: Array<AnsiBufferKey | Uint8Array>,
},
selector: {
selected: '> ',
default: ' ',
default: Array<AnsiBufferKey | Uint8Array>,
defaultSelected: Array<AnsiBufferKey | Uint8Array>,
},
};
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);
};
interface Event extends Tag<'KEYUP' | 'KEYDOWN' | 'EXIT' | 'ACCEPT'> {
value?: any
}
const EVENT: Record<string, Event | ((...args: any[]) => Event)> = {
keyup: {_tag: 'KEYUP'},
keydown: {_tag: 'KEYDOWN'},
exit: <T>(x: T): Event => ({_tag: 'EXIT', value: x}),
accept: (x: number): Event => ({_tag: 'ACCEPT', value: x}),
}
// Execution
(async () => {
const options = ['Option A', 'Option B', 'Option C'];
const selectedOption = await interactiveSelect(options, {
selector: { selected: '* ' },
});
console.log(`You selected: ${selectedOption}`);
type RenderText =
(x: SelectOptions & Tag<'PainterState'>) => Promise<Either<number, Error>>
// const renderText: RenderText = x =>
//
// const selectOptions: PainterEffect<SelectOptions> = x =>
// DoEither(right(x)).start()
// .bind('')
process.stdin.pause(); // Ensures stdin closes AFTER selection
})();
const test = async () => {
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('First')
}, 300)
})
console.log('Second')
console.log(await myPromise)
return some(await myPromise + 'Third')
}
console.log(await test())

View File

@ -5,7 +5,7 @@ const emptyBytes = encoder.encode('')
type Writer = (x: Uint8Array | Array<Uint8Array>) => Promise<number>
const writer: Writer = x => Bun.write(Bun.stdout, x)
export const ANSI = {
const ANSI = {
// Text Styles
RESET: '\x1B[0m',
BOLD: '\x1B[1m',
@ -56,7 +56,7 @@ export const ANSI = {
CLEAR_BELOW: '\x1B[J',
}
export const ANSI_BUFFERS = {
const ANSI_BUFFERS = {
// Static code buffers
RESET: encoder.encode(ANSI.RESET),
BOLD: encoder.encode(ANSI.BOLD),
@ -136,10 +136,11 @@ const writeAnsiU: WriteAnsiU = x =>
)))
// For CommonJS compatibility
export default {
export {
ANSI,
ANSI_BUFFERS,
ANSI_DYNAMIC,
type AnsiBufferKey,
ansiText,
writeAnsi,
writeAnsiU,