Skip to content

Commit

Permalink
feat(renderer): added hook and stdout support
Browse files Browse the repository at this point in the history
Now a observable hook gets passed in to the renderer to instantly render data instead of waiting for
the matching timeout.

re #31
  • Loading branch information
cenk1cenk2 committed May 25, 2020
1 parent 85b69ff commit bd73c68
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 124 deletions.
68 changes: 68 additions & 0 deletions examples/get-user-input.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,74 @@ async function main (): Promise<void> {
logger.fail(e)
}

logger.start('More complicated prompt.')
task = new Listr<Ctx>([
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt<boolean>('Select', { message: 'Do you love me?', choices: [ 'test', 'test', 'test', 'test' ] })
}
}
], { concurrent: false })

try {
const context = await task.run()
logger.success(`Context: ${JSON.stringify(context)}`)
} catch(e) {
logger.fail(e)
}

logger.start('Very complicated prompt.')
task = new Listr<Ctx>([
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt<boolean>('Survey', {
name: 'experience',
message: 'Please rate your experience',
scale: [
{ name: '1', message: 'Strongly Disagree' },
{ name: '2', message: 'Disagree' },
{ name: '3', message: 'Neutral' },
{ name: '4', message: 'Agree' },
{ name: '5', message: 'Strongly Agree' }
],
margin: [ 0, 0, 2, 1 ],
choices: [
{
name: 'interface',
message: 'The website has a friendly interface.'
},
{
name: 'navigation',
message: 'The website is easy to navigate.'
},
{
name: 'images',
message: 'The website usually has good images.'
},
{
name: 'upload',
message: 'The website makes it easy to upload images.'
},
{
name: 'colors',
message: 'The website has a pleasing color palette.'
}
]

})
}
}
], { concurrent: false })

try {
const context = await task.run()
logger.success(`Context: ${JSON.stringify(context)}`)
} catch(e) {
logger.fail(e)
}

}

main()
58 changes: 53 additions & 5 deletions examples/ink-react.example.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import delay from 'delay'
import { render, Color } from 'ink'
import React, { useState, useEffect } from 'react'
import { Box, Color, render } from 'ink'
import React, { Fragment, useEffect, useState } from 'react'

import { Listr } from '@root/index'
import { Logger } from '@utils/logger'
Expand All @@ -13,7 +12,7 @@ const logger = new Logger({ useIcons: false })
async function main (): Promise<void> {
let task: Listr<Ctx, any>

logger.start('Example output from a task.')
logger.start('Example output from a task with stdout.')

task = new Listr<Ctx, any>([
{
Expand All @@ -37,7 +36,56 @@ async function main (): Promise<void> {

const { unmount, waitUntilExit } = render(<Counter />, task.stdout())

setTimeout(unmount, 10000)
setTimeout(unmount, 2000)

return waitUntilExit()
},
options: {
persistentOutput: true
}
}
], { concurrent: false, renderer: 'default' })

try {
const context = await task.run()
logger.success(`Context: ${JSON.stringify(context)}`)
} catch(e) {
logger.fail(e)
}

logger.start('Example output from a task with stdout.')

task = new Listr<Ctx, any>([
{
title: 'This task will show INK as output.',
task: async (ctx, task): Promise<any> => {
const { unmount, waitUntilExit } = render(
<Fragment>
<Box justifyContent="flex-start">
<Box>X</Box>
</Box>

<Box justifyContent="center">
<Box>X</Box>
</Box>

<Box justifyContent="flex-end">
<Box>X</Box>
</Box>

<Box justifyContent="space-between">
<Box>X</Box>
<Box>Y</Box>
</Box>

<Box justifyContent="space-around">
<Box>X</Box>
<Box>Y</Box>
</Box>
</Fragment>
, task.stdout())

setTimeout(unmount, 2000)

return waitUntilExit()
},
Expand Down
11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,31 +57,28 @@
"chalk": "^4.0.0",
"cli-cursor": "^3.1.0",
"cli-truncate": "^2.1.0",
"elegant-spinner": "^2.0.0",
"enquirer": "^2.3.5",
"figures": "^3.2.0",
"indent-string": "^4.0.0",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"pad": "^3.2.0",
"rxjs": "^6.5.5",
"through": "^2.3.8",
"through2": "^3.0.1",
"uuid": "^7.0.2"
},
"devDependencies": {
"@cenk1cenk2/eslint-config": "*",
"@types/jest": "^25.2.3",
"@types/node": "^14.0.1",
"@types/node": "^14.0.5",
"@types/rewire": "^2.5.28",
"@types/uuid": "^7.0.2",
"cz-conventional-changelog": "3.2.0",
"delay": "^4.3.0",
"eslint": "^7.0.0",
"eslint": "^7.1.0",
"husky": "^4.2.5",
"ink": "^2.7.1",
"jest": "^26.0.1",
"lint-staged": "^10.2.4",
"lint-staged": "^10.2.6",
"prettier": "^2.0.5",
"react": "^16.13.1",
"rewire": "^5.0.0",
Expand All @@ -90,7 +87,7 @@
"ts-node": "^8.10.1",
"tsconfig-paths": "^3.9.0",
"tscpaths": "^0.0.9",
"typescript": "^3.9.2"
"typescript": "^3.9.3"
},
"config": {
"commitizen": {
Expand Down
8 changes: 4 additions & 4 deletions src/interfaces/listr.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable } from 'rxjs'
import { Observable, Subject } from 'rxjs'
import { Readable } from 'stream'

import { stateConstants } from '@constants/state.constants'
Expand Down Expand Up @@ -37,7 +37,7 @@ export interface ListrTaskObject<Ctx, Renderer extends ListrRendererFactory> ext
options: ListrOptions
rendererOptions: ListrGetRendererOptions<Renderer>
rendererTaskOptions: ListrGetRendererTaskOptions<Renderer>
spinner?: () => string
renderHook$: Subject<void>
hasSubtasks(): boolean
isPending(): boolean
isSkipped(): boolean
Expand All @@ -64,7 +64,7 @@ export interface ListrTaskWrapper<Ctx, Renderer extends ListrRendererFactory> {
skip(message: string): void
run(ctx?: Ctx, task?: ListrTaskWrapper<Ctx, Renderer>): Promise<void>
prompt<T = any, P extends PromptTypes = PromptTypes>(type: P, options: PromptOptionsType<P>): Promise<T>
stdout(): NodeJS.WriteStream & NodeJS.WritableStream
stdout(): NodeJS.WritableStream
}

export type ListrTaskResult<Ctx> = string | Promise<any> | ListrClass<Ctx, ListrRendererFactory, any> | Readable | Observable<any>
Expand Down Expand Up @@ -160,7 +160,7 @@ export interface ListrRendererFactory {
rendererOptions: Record<string, any>
rendererTaskOptions: Record<string, any>
nonTTY: boolean
new(tasks: readonly ListrTaskObject<any, ListrRendererFactory>[], options: typeof ListrRenderer.rendererOptions): ListrRenderer
new(tasks: readonly ListrTaskObject<any, ListrRendererFactory>[], options: typeof ListrRenderer.rendererOptions, renderHook$?: Subject<void>): ListrRenderer
}

export type ListrRendererValue = 'silent' | 'default' | 'verbose' | ListrRendererFactory
Expand Down
34 changes: 12 additions & 22 deletions src/lib/task-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cursorTo } from 'readline'
/* eslint-disable no-control-regex */
import through from 'through'

import { stateConstants } from '@constants/state.constants'
Expand All @@ -19,6 +19,7 @@ export class TaskWrapper<Ctx, Renderer extends ListrRendererFactory> implements
type: 'TITLE',
data: title
})

}

get title (): string {
Expand All @@ -32,6 +33,7 @@ export class TaskWrapper<Ctx, Renderer extends ListrRendererFactory> implements
type: 'DATA',
data
})

}

get output (): string {
Expand All @@ -45,6 +47,7 @@ export class TaskWrapper<Ctx, Renderer extends ListrRendererFactory> implements
type: 'STATE',
data
})

}

public newListr (task: ListrTask<Ctx, Renderer> | ListrTask<Ctx, Renderer>[], options?: ListrSubClassOptions<Ctx, Renderer>): Listr<Ctx, any, any> {
Expand Down Expand Up @@ -81,31 +84,18 @@ export class TaskWrapper<Ctx, Renderer extends ListrRendererFactory> implements
}

public stdout (): NodeJS.WriteStream & NodeJS.WritableStream {
let buffer: string[] = []

return through((chunk: string) => {
// delete bell icon since it causes some problems
// eslint-disable-next-line no-control-regex
const bellIconRegexp = new RegExp(/\u0007/mg)

chunk.replace(bellIconRegexp, '')

// eslint-disable-next-line no-control-regex
const deleteMultiLineRegexp = new RegExp(/\u001b\[([0-9]*)G/mg)
const pattern = new RegExp(
'(?:\\u001b|\\u009b)\\[[\\=><~/#&.:=?%@~_-]*[0-9]*[\\a-ln-tqyz=><~/#&.:=?%@~_-]+',
'gmi')

const regexpIterator = chunk.matchAll(deleteMultiLineRegexp)
const matchedDeleteMultiLine = [ ...regexpIterator ]
matchedDeleteMultiLine.forEach((match) => {
if (match?.[1]) {
buffer = buffer.slice(-match?.[1])
}
})

chunk.replace(deleteMultiLineRegexp, '')

buffer = [ ...buffer, chunk ]

this.output = buffer.join('')
chunk = chunk.replace(pattern, '')
chunk = chunk.replace(new RegExp(/\u0007/, 'gmi'), '')
if (chunk !== '') {
this.output = chunk
}

})
}
Expand Down
8 changes: 8 additions & 0 deletions src/lib/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class Task<Ctx, Renderer extends ListrRendererFactory> extends Subject<Li
public prompt: boolean | PromptError
public exitOnError: boolean
public rendererTaskOptions: ListrGetRendererTaskOptions<Renderer>
public renderHook$: Subject<void>
private enabled: boolean
private enabledFn: ListrTask<Ctx, Renderer>['enabled']

Expand All @@ -43,6 +44,10 @@ export class Task<Ctx, Renderer extends ListrRendererFactory> extends Subject<Li
this.enabledFn = this.tasks?.enabled || ((): boolean => true)
// task options
this.rendererTaskOptions = this.tasks.options

this.renderHook$ = this.listr.renderHook$
this.subscribe(() => { this.renderHook$.next() })

}

set state$ (state: StateConstants) {
Expand All @@ -52,6 +57,7 @@ export class Task<Ctx, Renderer extends ListrRendererFactory> extends Subject<Li
type: 'STATE',
data: state
})

}

async check (ctx: Ctx): Promise<void> {
Expand All @@ -69,6 +75,7 @@ export class Task<Ctx, Renderer extends ListrRendererFactory> extends Subject<Li
data: this.enabled
})
}

}

hasSubtasks (): boolean {
Expand Down Expand Up @@ -117,6 +124,7 @@ export class Task<Ctx, Renderer extends ListrRendererFactory> extends Subject<Li
// switch to silent renderer since already rendering
const rendererClass = getRenderer('silent')
result.rendererClass = rendererClass.renderer
result.renderHook$.subscribe((): void => { this.renderHook$.next() })

// assign subtasks
this.subtasks = result.tasks
Expand Down
4 changes: 3 additions & 1 deletion src/listr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pMap from 'p-map'
import { Subject } from 'rxjs'

import { stateConstants } from '@constants/state.constants'
import {
Expand Down Expand Up @@ -26,6 +27,7 @@ implements ListrClass<Ctx, Renderer, FallbackRenderer> {
public err: ListrError[] = []
public rendererClass: ListrRendererFactory
public rendererClassOptions: ListrGetRendererOptions<ListrRendererFactory>
public renderHook$: Subject<void> = new Subject<void>()
private concurrency: number
private renderer: ListrRenderer

Expand Down Expand Up @@ -97,7 +99,7 @@ implements ListrClass<Ctx, Renderer, FallbackRenderer> {

// start the renderer
if (!this.renderer) {
this.renderer = new this.rendererClass(this.tasks, this.rendererClassOptions)
this.renderer = new this.rendererClass(this.tasks, this.rendererClassOptions, this.renderHook$)
}

this.renderer.render()
Expand Down

0 comments on commit bd73c68

Please sign in to comment.