├── .editorconfig ├── .gitignore ├── example.js ├── index.js ├── license.md ├── package.json └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | # Use tabs in JavaScript and JSON. 14 | [**.{js,json}] 15 | indent_style = tab 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | .nvm-version 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const wrap = require('.') 4 | 5 | const prompt = wrap({ 6 | value: 0, 7 | up: function () { 8 | this.value++ 9 | this.render() 10 | }, 11 | down: function () { 12 | this.value-- 13 | this.render() 14 | }, 15 | calls: 0, 16 | render: function () { 17 | this.out.write(`The value is ${this.value}. – ${this.calls++} calls`) 18 | } 19 | }) 20 | 21 | prompt 22 | .then(console.log) 23 | .catch(console.error) 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const differ = require('ansi-diff-stream') 4 | const esc = require('ansi-escapes') 5 | const onKeypress = require('@derhuerst/cli-on-key') 6 | const termSize = require('window-size').get 7 | 8 | const action = (key) => { 9 | let code = key.raw.charCodeAt(0) 10 | 11 | if (key.ctrl) { 12 | if (key.name === 'a') return 'first' 13 | if (key.name === 'c') return 'abort' 14 | if (key.name === 'd') return 'abort' 15 | if (key.name === 'e') return 'last' 16 | if (key.name === 'g') return 'reset' 17 | } 18 | if (key.name === 'return') return 'submit' 19 | if (key.name === 'enter') return 'submit' // ctrl + J 20 | if (key.name === 'backspace') return 'delete' 21 | if (key.name === 'abort') return 'abort' 22 | if (key.name === 'escape') return 'abort' 23 | if (key.name === 'tab') return 'next' 24 | 25 | if (key.name === 'up') return 'up' 26 | if (key.name === 'down') return 'down' 27 | if (key.name === 'right') return 'right' 28 | if (key.name === 'left') return 'left' 29 | if (code === 8747) return 'left' // alt + B 30 | if (code === 402) return 'right' // alt + F 31 | 32 | return false 33 | } 34 | 35 | const onResize = (stream, cb) => { 36 | stream.on('resize', cb) 37 | const stopListening = () => { 38 | stream.removeListener('resize', cb) 39 | } 40 | return stopListening 41 | } 42 | 43 | const wrap = (p) => { 44 | p.out = differ() 45 | p.out.pipe(process.stdout) 46 | 47 | p.bell = () => { 48 | process.stdout.write(esc.beep) 49 | } 50 | if ('function' !== typeof p._) p._ = p.bell 51 | 52 | const onKey = (key) => { 53 | let a = action(key) 54 | if (a === 'abort') return p.close() 55 | if (a === false) p._(key.raw) 56 | else if ('function' === typeof p[a]) p[a](key) 57 | else process.stdout.write(esc.beep) 58 | } 59 | 60 | const onNewSize = () => { 61 | const {width, height} = termSize() 62 | p.out.reset() 63 | p.render(true) 64 | } 65 | 66 | let offKeypress, offResize 67 | const pause = () => { 68 | if (!offKeypress) return 69 | offKeypress() 70 | offKeypress = null 71 | offResize() 72 | offResize = null 73 | process.stdout.write(esc.cursorShow) 74 | } 75 | p.pause = pause 76 | const resume = () => { 77 | if (offKeypress) return 78 | offKeypress = onKeypress(process.stdin, onKey) 79 | offResize = onResize(process.stdout, onNewSize) 80 | process.stdout.write(esc.cursorHide) 81 | } 82 | p.resume = resume 83 | 84 | return new Promise((resolve, reject) => { 85 | let isClosed = false 86 | p.close = () => { 87 | if (isClosed) return null 88 | isClosed = true 89 | 90 | p.out.unpipe(process.stdout) 91 | pause() 92 | 93 | if (p.aborted) reject(p.value) 94 | else resolve(p.value) 95 | } 96 | process.on('beforeExit', () => p.close()) 97 | 98 | if ('function' !== typeof p.submit) p.submit = p.close 99 | resume() 100 | p.render(true) 101 | }) 102 | } 103 | 104 | module.exports = Object.assign(wrap, {action}) 105 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Jannis R 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prompt-skeleton", 3 | "description": "A consistent behavior for CLI prompts.", 4 | "version": "1.0.4", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "keywords": [ 10 | "command", 11 | "line", 12 | "cli", 13 | "prompt", 14 | "skeleton", 15 | "behavior", 16 | "key bindings" 17 | ], 18 | "author": "Jannis R ", 19 | "homepage": "https://github.com/derhuerst/prompt-skeleton", 20 | "repository": "git://github.com/derhuerst/prompt-skeleton.git", 21 | "license": "ISC", 22 | "engines": { 23 | "node": ">=4" 24 | }, 25 | "dependencies": { 26 | "@derhuerst/cli-on-key": "^0.1.0", 27 | "ansi-diff-stream": "^1.2.1", 28 | "ansi-escapes": "^3.1.0", 29 | "pass-stream": "^1.0.0", 30 | "window-size": "^1.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # prompt-skeleton 2 | 3 | This project aims to bring a **consistent behavior to CLI apps**. 4 | 5 | Use [`cli-prompter`](https://github.com/ahdinosaur/cli-prompter) if you want to have batteries included or if you want to chain prompts. 6 | 7 | [![npm version](https://img.shields.io/npm/v/prompt-skeleton.svg)](https://www.npmjs.com/package/prompt-skeleton) 8 | [![dependency status](https://img.shields.io/david/derhuerst/prompt-skeleton.svg)](https://david-dm.org/derhuerst/prompt-skeleton#info=dependencies) 9 | ![ISC-licensed](https://img.shields.io/github/license/derhuerst/prompt-skeleton.svg) 10 | [![support me via GitHub Sponsors](https://img.shields.io/badge/support%20me-donate-fa7664.svg)](https://github.com/sponsors/derhuerst) 11 | [![chat with me on Twitter](https://img.shields.io/badge/chat%20with%20me-on%20Twitter-1da1f2.svg)](https://twitter.com/derhuerst) 12 | 13 | Instead of letting prompts parse user input by themselves, *prompt-skeleton* provides a [standard set of actions like `submit`](#actions), which prompts can act on by exposing methods. The key bindings are [readline](https://de.wikipedia.org/wiki/GNU_readline)-inspired. 14 | 15 | 16 | ## Projects using `prompt-skeleton` 17 | 18 | Prompts: 19 | 20 | - [`sms-cli`](https://github.com/derhuerst/sms-cli) 21 | - [`date-prompt`](https://github.com/derhuerst/date-prompt) 22 | - [`mail-prompt`](https://github.com/derhuerst/mail-prompt) 23 | - [`multiselect-prompt`](https://github.com/derhuerst/multiselect-prompt) 24 | - [`number-prompt`](https://github.com/derhuerst/number-prompt) 25 | - [`range-prompt`](https://github.com/derhuerst/range-prompt) 26 | - [`select-prompt`](https://github.com/derhuerst/select-prompt) 27 | - [`text-prompt`](https://github.com/derhuerst/text-prompt) 28 | - [`tree-select-prompt`](https://github.com/derhuerst/tree-select-prompt) 29 | - [`cli-autocomplete`](https://github.com/derhuerst/cli-autocomplete) 30 | - [`switch-prompt`](https://github.com/derhuerst/switch-prompt) 31 | 32 | Other command line interfaces: 33 | 34 | - [`really-basic-chat-ui`](https://github.com/derhuerst/really-basic-chat-ui) 35 | - [`command-irail`](https://github.com/iRail/command-irail) 36 | - [`cli-2048`](https://github.com/derhuerst/cli-2048) 37 | - [`cli-minesweeper`](https://github.com/derhuerst/cli-minesweeper) 38 | - [`tiny-cli-editor`](https://github.com/derhuerst/tiny-cli-editor) 39 | 40 | 41 | ## Installing 42 | 43 | ``` 44 | npm install prompt-skeleton 45 | ``` 46 | 47 | 48 | ## Usage 49 | 50 | ```js 51 | wrap(prompt) // Promise 52 | ``` 53 | 54 | To render to screen, [`write`](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback) to `prompt.out`. Because `prompt.out` is a [`ansi-diff-stream`](https://www.npmjs.com/package/ansi-diff-stream#usage), you can also clear the screen manually be calling `prompt.out.clear()`. 55 | 56 | ### Actions 57 | 58 | You can process any of these actions by exposing a method `prompt[action]`. 59 | 60 | - `first`/`last` – move to the first/last letter/digit 61 | - `left`/`right` 62 | - `up`/`down` 63 | - `next` - for tabbing 64 | - `delete` – remove letter/digit left to the cursor 65 | - `space` 66 | - `submit` – success, close the prompt 67 | - `abort` – failure, close the prompt 68 | - `reset` 69 | 70 | ### Example 71 | 72 | This renders a prompt that lets you pick a number. 73 | 74 | ```js 75 | const wrap = require('prompt-skeleton') 76 | 77 | const prompt = wrap({ 78 | value: 0, 79 | up: function () { 80 | this.value++ 81 | this.render() 82 | }, 83 | down: function () { 84 | this.value-- 85 | this.render() 86 | }, 87 | render: function () { 88 | this.out.write(`The value is ${this.value}.`) 89 | } 90 | }) 91 | 92 | prompt 93 | .then((val) => { 94 | // prompt succeeded, do something with the value 95 | }) 96 | .catch((val) => { 97 | // prompt aborted, do something with the value 98 | }) 99 | ``` 100 | 101 | 102 | ## Contributing 103 | 104 | If you **have a question**, **found a bug** or want to **propose a feature**, have a look at [the issues page](https://github.com/derhuerst/prompt-skeleton/issues). 105 | --------------------------------------------------------------------------------