diff --git a/src/lib/promptsmith/select-options.old.ts b/src/lib/promptsmith/select-options.old.ts new file mode 100644 index 0000000..82031b4 --- /dev/null +++ b/src/lib/promptsmith/select-options.old.ts @@ -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 => { + return new Promise((resolve) => { + if (!assertArrayOfStrings(options)) + throw new TypeError("'options' should be Array."); + + 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 => { + 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 +})(); diff --git a/src/lib/promptsmith/select-options.ts b/src/lib/promptsmith/select-options.ts index 82031b4..965a1bd 100644 --- a/src/lib/promptsmith/select-options.ts +++ b/src/lib/promptsmith/select-options.ts @@ -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 => { - return new Promise((resolve) => { - if (!assertArrayOfStrings(options)) - throw new TypeError("'options' should be Array."); - - 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 + keydown: Array + exit: Array + accept: Array + selector: string + options: Array + header: {text: string, format: Array} format: { - selected: '\x1B[32m\x1B[1m', - }, - selector: { - selected: '> ', - default: ' ', - }, -}; -const interactiveSelect = async ( - options: string[], - config: PromptsmithConfig = {} -): Promise => { - const newConfig = setConfig(config); + text: { + default: Array, + defaultSelected: Array, + }, + selector: { + default: Array, + defaultSelected: Array, + }, + } +} - 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 Event)> = { + keyup: {_tag: 'KEYUP'}, + keydown: {_tag: 'KEYDOWN'}, + exit: (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> +// const renderText: RenderText = x => +// +// const selectOptions: PainterEffect = 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()) diff --git a/src/lib/util/ansi.ts b/src/lib/util/ansi.ts index 386ff87..095c844 100644 --- a/src/lib/util/ansi.ts +++ b/src/lib/util/ansi.ts @@ -5,7 +5,7 @@ const emptyBytes = encoder.encode('') type Writer = (x: Uint8Array | Array) => Promise 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,