Progress on re-write of select-options
This commit is contained in:
parent
e9216cc3e4
commit
459080d1e6
169
src/lib/promptsmith/select-options.old.ts
Normal file
169
src/lib/promptsmith/select-options.old.ts
Normal 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
|
||||
})();
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user