diff --git a/src/lib/painter/lifecycle.ts b/src/lib/painter/lifecycle.ts index dd65069..b3cc3ac 100644 --- a/src/lib/painter/lifecycle.ts +++ b/src/lib/painter/lifecycle.ts @@ -42,7 +42,7 @@ const exit: ExitConstructor = x => ({_tag: 'PainterExit', value: x}) type PainterEffect< E, T extends object = {}, -> = (task: PainterTask) => Either, PainterExit> +> = (task: PainterTask) => Promise, PainterExit>> type EffectState = (x: T) => T & Tag<'PainterState'> const effectState: EffectState = x => ({_tag: 'PainterState', ...x}) diff --git a/src/lib/promptsmith/select-options.ts b/src/lib/promptsmith/select-options.ts index 4b756fd..268567a 100644 --- a/src/lib/promptsmith/select-options.ts +++ b/src/lib/promptsmith/select-options.ts @@ -1,9 +1,11 @@ -import type { Key } from 'node:readline' +import * as readline from 'node:readline' +import { emitKeypressEvents, type Key } from 'node:readline' import { exit as EXIT, type PainterEffect, type PainterState, type PainterTask, + tagState, task as TASK, } from '../painter/lifecycle' import { @@ -13,8 +15,9 @@ import { writeAnsiU, } from '../util/ansi' import { DoEither, type Either, left, right } from '../util/basic/either' -import { some } from '../util/basic/option' -import type { Tag } from '../util/basic/utility' +import { DoOption, getOrElse, map, none, type Option, some } from '../util/basic/option' +import { hasTag, type Tag } from '../util/basic/utility' +import type { RecursiveRequired } from '../util/types' interface SelectOptions { keyup: Array @@ -37,18 +40,87 @@ interface SelectOptions { } } -interface Event extends Tag<'KEYUP' | 'KEYDOWN' | 'EXIT' | 'ACCEPT'> { - value?: any -} -const EVENT: Record Event)> = { +type PartialSelectOptions = + & Partial> + & { + options: SelectOptions['options'], + header?: { + text: NonNullable['text'], + format?: NonNullable['format'], + }, + } + +type SelectOptionsConfig = (x: PartialSelectOptions) => SelectOptions +const selectOptionsConfig: SelectOptionsConfig = x => ({ + keyup: x.keyup ?? [{name: 'up'}, {name: 'k'}], + keydown: x.keydown ?? [{name: 'down'}, {name: 'j'}], + exit: x.exit ?? [{name: 'esc'}], + accept: x.accept ?? [{name: 'return'}], + selector: { + selected: x.selector?.selected ?? '> ', + default: x.selector?.default ?? ' ', + }, + selected: x.selected ?? 0, + options: x.options, + header: !x.header ? undefined + : {text: x.header.text, format: x.header.format ?? ['WHITE']}, + format: { + text: { + default: x.format?.text?.default ?? ['WHITE'], + defaultSelected: x.format?.text?.defaultSelected ?? ['WHITE', 'BOLD'], + }, + selector: { + default: x.format?.selector?.default ?? x.format?.text?.default + ?? ['WHITE'], + defaultSelected: x.format?.selector?.defaultSelected ?? ['BLUE', 'BOLD'], + }, + }, +}) + +interface Event extends Tag<'KEYUP' | 'KEYDOWN' | 'EXIT' | 'ACCEPT'> {} +const EVENT: Record = { keyup: {_tag: 'KEYUP'}, keydown: {_tag: 'KEYDOWN'}, - exit: (x: T): Event => ({_tag: 'EXIT', value: x}), - accept: (x: number): Event => ({_tag: 'ACCEPT', value: x}), + exit: {_tag: 'EXIT'}, + accept: {_tag: 'ACCEPT'}, } -type RenderText = - (x: SelectOptions & Tag<'PainterState'>) => Promise> +type KeyCheck = (x: Key) => (y: Key) => boolean +const keyCheck: KeyCheck = x => y => + x.name !== y.name ? false + : x.ctrl !== y.ctrl ? false + : x.shift !== y.shift ? false + : x.meta !== y.meta ? false + : true + +type OnKeypress = + (x: Pick) => + (y: Key) => Option +const onKeypress: OnKeypress = x => y => + x.keyup.some(a => keyCheck(a)(y)) ? some(EVENT.keyup) as Option + : x.keydown.some(a => keyCheck(a)(y)) ? some(EVENT.keydown) as Option + : x.exit.some(a => keyCheck(a)(y)) ? some(EVENT.exit) as Option + : x.accept.some(a => keyCheck(a)(y)) ? some(EVENT.accept) as Option + : none + +type KeyListener = + (x: Pick) => Promise< + Event + > +const keyListener: KeyListener = x => { + readline.emitKeypressEvents(process.stdin) + if (process.stdin.isTTY) process.stdin.setRawMode(true) + return new Promise((resolve) => { + process.stdin.on( + 'keypress', + a => onKeypress(x)(a) ? resolve(onKeypress(x)(a)) : none, + ) + }) +} + +type RenderText = (x: SelectOptions & Tag<'PainterState'>) => Promise< + Either +> const renderText: RenderText = async (x) => { try { return right( @@ -62,14 +134,14 @@ const renderText: RenderText = async (x) => { ansiText(x.selector.selected), 'RESET' as AnsiBufferKey, ...x.format.text.defaultSelected, - ansiText(i), + ansiText(i + '\n'), 'RESET' as AnsiBufferKey, ] : [ ...x.format.selector.default, ansiText(x.selector.default), 'RESET' as AnsiBufferKey, ...x.format.text.default, - ansiText(i), + ansiText(i + '\n'), 'RESET' as AnsiBufferKey, ] ), @@ -80,5 +152,29 @@ const renderText: RenderText = async (x) => { } } -const selectOptions: PainterEffect = x => { +const selectOptions: PainterEffect = async (x) => { + const render = renderText(x) + const event = DoOption.start() + .bind('state', x.task.state) + . } + +renderText( + tagState( + selectOptionsConfig({ + options: ['Option one', 'Option two', 'Option three'], + }), + ), +) + +const test = selectOptionsConfig({ + options: ['on', 'two', 'three'], +}) +console.log( + onKeypress({ + keyup: test.keyup, + keydown: test.keydown, + exit: test.exit, + accept: test.accept, + })({name: 'return'}), +) diff --git a/src/lib/util/types.ts b/src/lib/util/types.ts new file mode 100644 index 0000000..7198a9c --- /dev/null +++ b/src/lib/util/types.ts @@ -0,0 +1,7 @@ +export type RecursiveRequired = Required< + { + [P in keyof T]: T[P] extends object | undefined + ? RecursiveRequired> + : T[P] + } +>