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 type { Key } from 'node:readline'
|
||||||
import * as readline from 'node:readline';
|
import {
|
||||||
import { TerminalUtils, type CursorPosition } from '../util/terminal';
|
exit as EXIT,
|
||||||
import { assertArrayOfStrings } from '../util/typeCheck';
|
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 = {
|
interface SelectOptions {
|
||||||
format?: {
|
keyup: Array<Key>
|
||||||
selected?: string;
|
keydown: Array<Key>
|
||||||
selectedText?: string;
|
exit: Array<Key>
|
||||||
defualt?: string;
|
accept: Array<Key>
|
||||||
defualtText?: string;
|
selector: string
|
||||||
};
|
options: Array<string>
|
||||||
selector?: {
|
header: {text: string, format: Array<AnsiBufferKey | Uint8Array>}
|
||||||
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: {
|
format: {
|
||||||
selected: '\x1B[32m\x1B[1m',
|
text: {
|
||||||
},
|
default: Array<AnsiBufferKey | Uint8Array>,
|
||||||
selector: {
|
defaultSelected: Array<AnsiBufferKey | Uint8Array>,
|
||||||
selected: '> ',
|
},
|
||||||
default: ' ',
|
selector: {
|
||||||
},
|
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();
|
interface Event extends Tag<'KEYUP' | 'KEYDOWN' | 'EXIT' | 'ACCEPT'> {
|
||||||
renderOptions(options, 0, pos, newConfig).forEach((line) =>
|
value?: any
|
||||||
console.log(line)
|
}
|
||||||
);
|
const EVENT: Record<string, Event | ((...args: any[]) => Event)> = {
|
||||||
return handleKeyPress(options, 0, pos, newConfig);
|
keyup: {_tag: 'KEYUP'},
|
||||||
};
|
keydown: {_tag: 'KEYDOWN'},
|
||||||
|
exit: <T>(x: T): Event => ({_tag: 'EXIT', value: x}),
|
||||||
|
accept: (x: number): Event => ({_tag: 'ACCEPT', value: x}),
|
||||||
|
}
|
||||||
|
|
||||||
// Execution
|
type RenderText =
|
||||||
(async () => {
|
(x: SelectOptions & Tag<'PainterState'>) => Promise<Either<number, Error>>
|
||||||
const options = ['Option A', 'Option B', 'Option C'];
|
// const renderText: RenderText = x =>
|
||||||
const selectedOption = await interactiveSelect(options, {
|
//
|
||||||
selector: { selected: '* ' },
|
// const selectOptions: PainterEffect<SelectOptions> = x =>
|
||||||
});
|
// DoEither(right(x)).start()
|
||||||
console.log(`You selected: ${selectedOption}`);
|
// .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>
|
type Writer = (x: Uint8Array | Array<Uint8Array>) => Promise<number>
|
||||||
const writer: Writer = x => Bun.write(Bun.stdout, x)
|
const writer: Writer = x => Bun.write(Bun.stdout, x)
|
||||||
|
|
||||||
export const ANSI = {
|
const ANSI = {
|
||||||
// Text Styles
|
// Text Styles
|
||||||
RESET: '\x1B[0m',
|
RESET: '\x1B[0m',
|
||||||
BOLD: '\x1B[1m',
|
BOLD: '\x1B[1m',
|
||||||
@ -56,7 +56,7 @@ export const ANSI = {
|
|||||||
CLEAR_BELOW: '\x1B[J',
|
CLEAR_BELOW: '\x1B[J',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ANSI_BUFFERS = {
|
const ANSI_BUFFERS = {
|
||||||
// Static code buffers
|
// Static code buffers
|
||||||
RESET: encoder.encode(ANSI.RESET),
|
RESET: encoder.encode(ANSI.RESET),
|
||||||
BOLD: encoder.encode(ANSI.BOLD),
|
BOLD: encoder.encode(ANSI.BOLD),
|
||||||
@ -136,10 +136,11 @@ const writeAnsiU: WriteAnsiU = x =>
|
|||||||
)))
|
)))
|
||||||
|
|
||||||
// For CommonJS compatibility
|
// For CommonJS compatibility
|
||||||
export default {
|
export {
|
||||||
ANSI,
|
ANSI,
|
||||||
ANSI_BUFFERS,
|
ANSI_BUFFERS,
|
||||||
ANSI_DYNAMIC,
|
ANSI_DYNAMIC,
|
||||||
|
type AnsiBufferKey,
|
||||||
ansiText,
|
ansiText,
|
||||||
writeAnsi,
|
writeAnsi,
|
||||||
writeAnsiU,
|
writeAnsiU,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user