Progress on select-options (more)

This commit is contained in:
Eric Rumsey 2025-06-03 17:08:36 -05:00
parent 4eea9bce6f
commit eb7f59e19b
3 changed files with 118 additions and 15 deletions

View File

@ -42,7 +42,7 @@ const exit: ExitConstructor = x => ({_tag: 'PainterExit', value: x})
type PainterEffect<
E,
T extends object = {},
> = (task: PainterTask<T>) => Either<PainterTask<T>, PainterExit<E>>
> = (task: PainterTask<T>) => Promise<Either<PainterTask<E, T>, PainterExit<E>>>
type EffectState = <T extends object = {}>(x: T) => T & Tag<'PainterState'>
const effectState: EffectState = x => ({_tag: 'PainterState', ...x})

View File

@ -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<Key>
@ -37,18 +40,87 @@ interface SelectOptions {
}
}
interface Event extends Tag<'KEYUP' | 'KEYDOWN' | 'EXIT' | 'ACCEPT'> {
value?: any
}
const EVENT: Record<string, Event | ((...args: any[]) => Event)> = {
type PartialSelectOptions =
& Partial<Omit<SelectOptions, 'options' | 'header'>>
& {
options: SelectOptions['options'],
header?: {
text: NonNullable<SelectOptions['header']>['text'],
format?: NonNullable<SelectOptions['header']>['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<string, Event> = {
keyup: {_tag: 'KEYUP'},
keydown: {_tag: 'KEYDOWN'},
exit: <T>(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<Either<number, Error>>
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<SelectOptions, 'keyup' | 'keydown' | 'exit' | 'accept'>) =>
(y: Key) => Option<Event>
const onKeypress: OnKeypress = x => y =>
x.keyup.some(a => keyCheck(a)(y)) ? some(EVENT.keyup) as Option<Event>
: x.keydown.some(a => keyCheck(a)(y)) ? some(EVENT.keydown) as Option<Event>
: x.exit.some(a => keyCheck(a)(y)) ? some(EVENT.exit) as Option<Event>
: x.accept.some(a => keyCheck(a)(y)) ? some(EVENT.accept) as Option<Event>
: none
type KeyListener =
(x: Pick<SelectOptions, 'keyup' | 'keydown' | 'exit' | 'accept'>) => 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<number, Error>
>
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<SelectOptions> = x => {
const selectOptions: PainterEffect<SelectOptions> = 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'}),
)

7
src/lib/util/types.ts Normal file
View File

@ -0,0 +1,7 @@
export type RecursiveRequired<T> = Required<
{
[P in keyof T]: T[P] extends object | undefined
? RecursiveRequired<Required<T[P]>>
: T[P]
}
>