UNPKG

cli-chain

Version:

chainable cli creation, minimal, interactive, powerful.

419 lines (357 loc) • 9.49 kB
/** * @file a Stepper persisting chain * @exports StepperPersistance * @requires configstore * @requires fliplog * @requires inquirer * @requires prettydate * @requires flipchain/ChainedMap */ const ChainedMap = require('flipchain/ChainedMap') const ConfigStore = require('configstore') const BottomBar = require('inquirer/lib/ui/bottom-bar') const {inquirer, log, prettydate} = require('./deps') const Core = require('./Core') const keys = { up: '\u001b[A', right: '\u001b[C', down: '\u001b[B', delete: '', } let debug = false /** * @description * check if there are any saved states * prompt with a list of saved states * allow deleting of those saved states * * @prop {boolean} showing showing the list * @prop {boolean} deleting prompting deleting of an item * @prop {boolean} hydrated already rehydrated, when prompting to rehydrate * @prop {boolean} confirming asking to resume, don't listen to keypresses * @prop {number} indx current _session_ index, varies from Stepper.indx * @prop {string} hash key to match sessions that are able to be used for a hash * @prop {Object} stdin for subscribing to stdin events (for going back) * @prop {Function} onStdIn subscriber for stdin * @prop {Object} sessions configstore sessions... * @prop {ConfigStore} store configstore for persisting * @prop {Object} prompter inquirer instance * @prop {Object} ui inquirer.ui * @type {ChainedMap} */ class StepperPersistance extends ChainedMap { /** * @param {Stepper} parent parent chain */ // constructor(parent) { // super(parent) // } // --- ops --- /** * @since 0.0.11 * @desc setup and reset * @param {Array<Step>} [steps = null] steps to use as key * @return {StepperPersistance} @chainable */ reset(steps = null) { let hash = steps if (hash === null && this.hash) hash = this.hash else hash = JSON.stringify(hash) this.key = hash + '.sessions' this.store = new ConfigStore('cli-chain') this.indx = 0 this.showing = false this.deleting = false if (!this.store.has(this.key)) this.store.set(this.key, []) this.sessions = this.store.get(this.key) return this } /** * @since 0.0.11 * @see configstore, Stepper, StepperPersistance.reset * @desc checks if there are existing stored sessions * @param {Object} steps stringified steps to serve as a hash * @return {boolean} hydratable or not */ isHydratable(steps) { if (this.hydrated === true) return false this.reset(steps) const hasSessions = this.sessions.length !== 0 // log.quick({sessions: this.sessions, hasSessions, hydrated: this.hydrated}) return hasSessions } /** * @since 0.0.11 * @see configstore * @desc save the current index and state * @return {StepperPersistance} @chainable */ persist() { const {store, parent} = this const data = { indx: parent.indx, steps: parent.steps, data: parent.data, now: Date.now(), } this.sessions.push(data) store.set(this.key, this.sessions) log .blue('\nšŸ‘‹ saved session: \n') .json({ data: data.data, file: log.colored(store.path, 'underline'), }) .echo() return this } // --- setup / helpers --- /** * @protected * @since 0.0.11 * @desc unsubscribes * @return {StepperPersistance} @chainable */ removeStdIn() { log.red('removing stdin').echo(debug) this.stdin.removeListener('data', this.onStdIn) try { this.ui.close() } catch (e) { // ignore } return this } /** * @since 0.0.11 * @desc subscribes to std in, sets .stdin and .onStdIn * @event data process.stdin.data * @return {StepperPersistance} @chainable */ setupStdIn() { if (this.stdin) this.removeStdIn() const bb = new BottomBar({bottomBar: ''}) this.stdin = process.stdin /** * @param {Buffer} buffer stdin buffer * @return {Buffer} stdin buffer */ this.onStdIn = buffer => { const key = buffer.toString() // safety if (this.deleting) { return buffer } if (this.showing === true) { this.showing = false log.clear() } if (this.confirming === true) { return buffer } if (!this.sessions[this.indx]) { log.red('no sessions at current').echo(debug) return buffer } switch (key) { case keys.delete: { log.bold('pressed [backspace]').echo(debug) this.deleting = true this.handleDelete() break } case keys.up: { log.bold('pressed [up]').echo(debug) this.indx-- break } case keys.down: { log.bold('pressed [down]').echo(debug) this.indx++ break } case keys.right: { log.bold('pressed [right]').echo(debug) this.showing = true this.echoCurrent() break } default: { log.bold('pressed [other?]').echo(debug) const bbm = '[right arrow] to show data, [backspace] to delete' bb.updateBottomBar(bbm) console.log('\n') } } // went back to top if (!this.sessions[this.indx]) this.indx = 0 return buffer } this.stdin.on('data', this.onStdIn) return this } /** * @protected * @since 0.0.11 * @desc echos current session when using [->] or deleting * @see this.promptDelete * @return {StepperPersistance} @chainable */ echoCurrent() { const session = this.sessions[this.indx] log .text('\n\n') .json({ answers: session.data, index: session.indx, indx: this.indx, }) .echo() return this } /** * @since 0.0.11 * @desc transforms data with prettydate, * colors a message, * maps sessions to prompts * @return {Object} latest session, message */ getData() { let sessions = this.store.get(this.key) const last = sessions[sessions.length - 1] const when = prettydate(last.now) const {indx, data} = last let message = 'resume a session? ' message += log.colored('last one was ' + when, 'dim') const choices = sessions.reverse().map((session, i) => { return { message: when, name: when, } }) return {message, sessions, last, when, indx, data, choices} } // --- delete --- /** * @protected * @since 0.0.11 * @see this.promptDelete, this.handleCancelDelete * @desc deletes current index, updates configstore * @return {StepperPersistance} @chainable */ handleDelete() { delete this.sessions[this.indx] this.sessions = this.sessions.filter(session => session) this.store.set(this.key, this.sessions) log.cyan('deleted :-)').echo() return this } /** * @since 0.0.11 * @see this.rehydrate * @desc removes listener / resets, starts again * @return {StepperPersistance} @chainable */ handleCancelDelete() { log.blue('NOT deleted :-)').echo() this.deleting = false this.removeStdIn() return this.reset() } /** * @TODO: use Core here * @since 0.0.11 * @see this.echoCurrent, this.handleDelete, this.handleCancelDelete * @desc closes ui, prompts to delete * @return {StepperPersistance} @chainable */ promptDelete() { this.ui.close() this.echoCurrent() inquirer .prompt([ { type: 'confirm', name: 'delete', message: 'delete?', }, ]) .then(answers => { if (answers.delete) this.handleDelete() else this.handleCancelDelete() }) return this } // --- rehydrate --- /** * @TODO use a .toConfig of steps * @protected * @since 0.0.11 * @see this.thenner, Stepper._start * @desc rehydrate from a saved state * @param {number} indx * @param {Array<*>} data * @return {StepperPersistance} @chainable */ rehydrate(indx, data) { this.parent.indx = indx this.parent.data = data this.hydrated = true this.parent._start() return this } /** * @since 0.0.11 * @see this.getData * @desc prompts to rehydrate * @return {Class} @chainable */ thenner() { this.setupStdIn() let {message, sessions, last, when, indx, data, choices} = this.getData() this.confirming = true this.prompter = inquirer.prompt([ { type: 'confirm', name: 'resume', message, }, { type: 'list', message: 'sessions:', name: 'sessions', choices, when: answers => { this.confirming = false return answers.resume }, }, ]) this.ui = this.prompter.ui /* prettier-ignore */ this.prompter .then(answers => { if (answers.resume === false) { this.removeStdIn() this.parent._start() return } // log.quick(answers, this.sessions[this.indx]) log .green('\nresuming session\n') .json({ when, data, indx, }) .echo() this.removeStdIn() this.rehydrate(indx, data) }, {input: this.stdin}) .catch(log.catch) return this } } module.exports = StepperPersistance