├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .npmrc ├── .travis.yml ├── code-of-conduct.md ├── contributing.md ├── index.js ├── license.md ├── media ├── confirm.gif ├── header.gif ├── hidden.gif ├── input.gif ├── interactive.gif ├── keypress.gif ├── quiz.gif ├── secure.gif └── usage.gif ├── package.json ├── readme.md └── src ├── confirm.js ├── hidden.js ├── input.js ├── interactive.js ├── keypress.js ├── menu.js ├── nav.js ├── prompt.js ├── qoa.js ├── quiz.js ├── secure.js └── text.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [{*.json, *.yml}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Technical Info (please complete the following information)** 20 | - OS: 21 | - Qoa Version: 22 | - Node.js Version: 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the feature request here. 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | yarn.lock 4 | 5 | # logs 6 | *.log 7 | 8 | # OS 9 | .DS_Store 10 | 11 | # IDE 12 | .vscode 13 | .idea 14 | *.swp 15 | *.swo -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 8 5 | - 6 6 | before_install: 7 | - npm install --global npm@6.5.0 8 | - npm --version 9 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at klaussinani@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Qoa 2 | 3 | Thank you for taking the time to contribute to Qoa! 4 | 5 | Please note that this project is released with a [Contributor Code of Conduct](code-of-conduct.md). By participating in this project you agree to abide by its terms. 6 | 7 | ## How to contribute 8 | 9 | ### Improve documentation 10 | 11 | Typo corrections, error fixes, better explanations, more examples etc. Open an issue regarding anything that you think it could be improved! You can use the [`docs` label](https://github.com/klaussinani/qoa/labels/docs) to find out what others have suggested! 12 | 13 | ### Improve issues 14 | 15 | Sometimes reported issues lack information, are not reproducible, or are even plain invalid. Help us out to make them easier to resolve. Handling issues takes a lot of time that we could rather spend on fixing bugs and adding features. 16 | 17 | ### Give feedback on issues 18 | 19 | We're always looking for more opinions on discussions in the issue tracker. It's a good opportunity to influence the future direction of the project. 20 | 21 | The [`question` label](https://github.com/klaussinani/qoa/labels/question) is a good place to find ongoing discussions. 22 | 23 | ### Write code 24 | 25 | You can use issue labels to discover issues you could help us out with! 26 | 27 | - [`feature request` issues](https://github.com/klaussinani/qoa/labels/feature%20request) are features we are open to including 28 | - [`bug` issues](https://github.com/klaussinani/qoa/labels/bug) are known bugs we would like to fix 29 | - [`future` issues](https://github.com/klaussinani/qoa/labels/future) are those that we'd like to get to, but not anytime soon. Please check before working on these since we may not yet want to take on the burden of supporting those features 30 | - on the [`help wanted`](https://github.com/klaussinani/qoa/labels/help%20wanted) label you can always find something exciting going on 31 | 32 | You may find an issue is assigned, or has the [`assigned` label](https://github.com/klaussinani/qoa/labels/assigned). Please double-check before starting on this issue because somebody else is likely already working on it 33 | 34 | ### Say hi 35 | 36 | Come over and say hi anytime you feel like on [Gitter](https://gitter.im/klaussinani/qoa). 37 | 38 | ### Translating Documentation 39 | 40 | #### Create a Translation 41 | 42 | - Ensure that the document is not already translated in your target language. 43 | - Add the name of the language to the document as an extension, e.g: `readme.JP.md` 44 | - Create a Pull Request including the language in the title, e.g: `Readme: Japanese Translation` 45 | 46 | ### Submitting an issue 47 | 48 | - Search the issue tracker before opening an issue 49 | - Ensure you're using the latest version of Qoa 50 | - Use a descriptive title 51 | - Include as much information as possible; 52 | - Steps to reproduce the issue 53 | - Error message 54 | - Qoa version 55 | - Operating system **etc** 56 | 57 | ### Submitting a pull request 58 | 59 | - Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work 60 | - Try making the pull request from a [topic branch](https://github.com/dchelimsky/rspec/wiki/Topic-Branches) if it is of crucial importance 61 | - Use a descriptive title for the pull request and commits 62 | - You might be asked to do changes to your pull request, you can do that by just [updating the existing one](https://github.com/RichardLitt/docs/blob/master/amending-a-commit-guide.md) 63 | 64 | > Based on project [AVA](https://github.com/avajs/ava/blob/master/contributing.md)'s contributing.md 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Qoa = require('./src/qoa'); 3 | 4 | module.exports = Object.assign(new Qoa(), {Qoa}); 5 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - present Klaus Sinani (klaussinani.github.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /media/confirm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/confirm.gif -------------------------------------------------------------------------------- /media/header.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/header.gif -------------------------------------------------------------------------------- /media/hidden.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/hidden.gif -------------------------------------------------------------------------------- /media/input.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/input.gif -------------------------------------------------------------------------------- /media/interactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/interactive.gif -------------------------------------------------------------------------------- /media/keypress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/keypress.gif -------------------------------------------------------------------------------- /media/quiz.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/quiz.gif -------------------------------------------------------------------------------- /media/secure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/secure.gif -------------------------------------------------------------------------------- /media/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaudiosinani/qoa/2cfce0426c72645e4a4f8c18c92e14e42eeb827f/media/usage.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qoa", 3 | "version": "0.2.0", 4 | "description": "Minimal interactive command-line prompts", 5 | "license": "MIT", 6 | "repository": "klaussinani/qoa", 7 | "author": { 8 | "name": "Klaus Sinani", 9 | "email": "klaussinani@gmail.com", 10 | "url": "https://klaussinani.github.io" 11 | }, 12 | "engines": { 13 | "node": ">=6" 14 | }, 15 | "files": [ 16 | "src", 17 | "index.js" 18 | ], 19 | "keywords": [ 20 | "cli", 21 | "command-line", 22 | "interactive", 23 | "interfaces", 24 | "prompts" 25 | ], 26 | "scripts": { 27 | "test": "xo" 28 | }, 29 | "devDependencies": { 30 | "xo": "*" 31 | }, 32 | "xo": { 33 | "space": 2, 34 | "rules": { 35 | "no-await-in-loop": 0 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | Qoa 3 |

4 | 5 |

6 | Minimal interactive command-line prompts 7 |

8 | 9 |
10 | Header 11 |
12 | 13 |

14 | 15 | Build Status 16 | 17 |

18 | 19 |
20 |
21 | Sponsored by: 22 |
23 | 24 |
25 | Better Stack 26 |
27 | 28 | Spot, Resolve, and Prevent Downtime. 29 | 30 |
31 |
32 | 33 | ## Description 34 | 35 | Lightweight and without any external dependencies qoa enables you to receive various types of user input through a set of intuitive, interactive & verbose command-line prompts. The library utilizes a simple & minimal usage syntax and contains 7 configurable console interfaces, such as plain text, confirmation & password/secret prompts as well as single keypress, quiz & multiple-choice navigable menus. 36 | 37 | You can now support the development process through [GitHub Sponsors](https://github.com/sponsors/klaussinani). 38 | 39 | Visit the [contributing guidelines](https://github.com/klaussinani/qoa/blob/master/contributing.md#translating-documentation) to learn more on how to translate this document into more languages. 40 | 41 | Come over to [Gitter](https://gitter.im/klaussinani/qoa) or [Twitter](https://twitter.com/klaussinani) to share your thoughts on the project. 42 | 43 | ## Highlights 44 | 45 | - 7 out-of-the-box interactive prompts 46 | - Zero dependencies 47 | - Lightweight & fast [8.8kB / 71ms](https://bundlephobia.com/result?p=qoa) 48 | - Clean & concise output 49 | - Simple & minimal usage syntax 50 | - Navigation, quiz & keypress menus 51 | - Secure & hidden input interfaces 52 | - Utilizes async/await expressions 53 | - Configurable & customizable 54 | 55 | ## Contents 56 | 57 | - [Description](#description) 58 | - [Highlights](#highlights) 59 | - [Install](#install) 60 | - [Usage](#usage) 61 | - [Prompts](#prompts) 62 | - [Configuration](#configuration) 63 | - [API](#api) 64 | - [Development](#development) 65 | - [Related](#related) 66 | - [Team](#team) 67 | - [Sponsors](#sponsors) 68 | - [License](#license) 69 | 70 | ## Install 71 | 72 | ### Yarn 73 | 74 | ```bash 75 | yarn add qoa 76 | ``` 77 | 78 | ### NPM 79 | 80 | ```bash 81 | npm install qoa 82 | ``` 83 | 84 | ## Usage 85 | 86 | Import qoa and start using any of the available prompts: 87 | 88 | - `confirm` 89 | - `hidden` 90 | - `input` 91 | - `interactive` 92 | - `keypress` 93 | - `quiz` 94 | - `secure` 95 | 96 | In order to sequentially create & display a series of prompts, the asynchronous unary `qoa.prompt` function can be used. The function accepts as input an array of objects, where each object contains the configuration of its corresponding prompt. The display order of the prompts is based on the order in which the configuration objects are defined inside the array. A new object containing the user's response to each prompt is finally returned by the function. 97 | 98 | ```js 99 | const qoa = require('qoa'); 100 | 101 | const {log} = console; 102 | 103 | const ps = [ 104 | { 105 | type: 'input', 106 | query: 'Type your username:', 107 | handle: 'username' 108 | }, 109 | { 110 | type: 'secure', 111 | query: 'Type your password:', 112 | handle: 'password' 113 | } 114 | ]; 115 | 116 | qoa.prompt(ps).then(log); 117 | //=> { username: 'klaussinani', password: 'token' } 118 | ``` 119 | 120 |
121 | Usage 122 |
123 | 124 | Alternatively, for non sequential use-cases, each prompt can be individually initialized through its respective asynchronous unary function, where each function accepts as input an object containing the prompt's properties/configuration. 125 | 126 | ```js 127 | const qoa = require('qoa'); 128 | 129 | const {log} = console; 130 | 131 | const login = async () => { 132 | const username = await qoa.input({ 133 | query: 'Type your username:', 134 | handle: 'username' 135 | }); 136 | 137 | const password = await qoa.secure({ 138 | query: 'Type your password:', 139 | handle: 'password' 140 | }); 141 | 142 | return Object.assign({}, username, password); 143 | } 144 | 145 | login().then(log); 146 | //=> { username: 'klaussinani', password: 'token' } 147 | ``` 148 | 149 | ## Prompts 150 | 151 | ### Confirm Prompt 152 | 153 | Initializes a text based input prompt with two `accept` & `deny` options. Based on the input provided by the user, the query displayed by the prompt is confirmed or rejected. In order for the query to be confirmed, thus return `true`, the user must provide exactly the indicated `accept` string (strict equality). On any other input the query is rejected & `false` is returned. The return value is a new object with the prompt result stored under the specified `handle` property. 154 | 155 | ```js 156 | const qoa = require('qoa'); 157 | 158 | const {log} = console; 159 | 160 | const confirm = { 161 | type: 'confirm', 162 | query: 'Update Qoa to latest version?', 163 | handle: 'update', 164 | accept: 'Y', 165 | deny: 'n' 166 | }; 167 | 168 | // using the `prompt` async method 169 | qoa.prompt([confirm]).then(log); 170 | //=> { update: true } 171 | 172 | // using the `confirm` async method 173 | qoa.confirm(confirm).then(log); 174 | //=> { update: true } 175 | ``` 176 | 177 |
178 | Confirm Prompt 179 |
180 | 181 | ### Hidden Prompt 182 | 183 | Initializes a text based prompt, where each character typed by the user is automatically hidden. The return value is a new object with the prompt result stored under the specified `handle` property. 184 | 185 | ```js 186 | const qoa = require('qoa'); 187 | 188 | const {log} = console; 189 | 190 | const hidden = { 191 | type: 'hidden', 192 | query: '[sudo] password for admin:', 193 | handle: 'sudo' 194 | }; 195 | 196 | // using the `prompt` async method 197 | qoa.prompt([hidden]).then(log); 198 | //=> { sudo: 'admin' } 199 | 200 | // using the `hidden` async method 201 | qoa.hidden(hidden).then(log); 202 | //=> { sudo: 'admin' } 203 | ``` 204 | 205 |
206 | Hidden Prompt 207 |
208 | 209 | ### Input Prompt 210 | 211 | Initializes a text based prompt, where input can be freely provided by the user. The return value is a new object with the prompt result stored under the specified `handle` property. 212 | 213 | ```js 214 | const qoa = require('qoa'); 215 | 216 | const {log} = console; 217 | 218 | const input = { 219 | type: 'input', 220 | query: 'Select your username:', 221 | handle: 'username' 222 | }; 223 | 224 | // using the `prompt` async method 225 | qoa.prompt([input]).then(log); 226 | //=> { username: 'klaussinani' } 227 | 228 | // using the `input` async method 229 | qoa.input(input).then(log); 230 | //=> { username: 'klaussinani' } 231 | ``` 232 | 233 |
234 | Input Prompt 235 |
236 | 237 | ### Interactive Prompt 238 | 239 | Initializes an interactive navigable menu based prompt, where the user can navigate within a set of options and select only one of them. The options can be defined in the `menu` array while the navigation indicator can be customized through the `symbol` option and if omitted the default string `'>'` will be used. The return value is new object with the selected option stored under the specified `handle` property. The interactive menu can be navigated through the `up arrow`/`k` & `down arrow`/`j` keys. 240 | 241 | ```js 242 | const qoa = require('qoa'); 243 | 244 | const {log} = console; 245 | 246 | const interactive = { 247 | type: 'interactive', 248 | query: 'What is your favorite treat?', 249 | handle: 'treat', 250 | symbol: '>', 251 | menu: [ 252 | 'Chocolate', 253 | 'Cupcakes', 254 | 'Ice-Cream' 255 | ] 256 | }; 257 | 258 | // using the `prompt` async method 259 | qoa.prompt([interactive]).then(log); 260 | //=> { treat: 'Cupcakes' } 261 | 262 | // using the `interactive` async method 263 | qoa.interactive(interactive).then(log); 264 | //=> { treat: 'Cupcakes' } 265 | ``` 266 | 267 |
268 | Interactive Prompt 269 |
270 | 271 | ### Keypress Prompt 272 | 273 | Initializes an non-navigable menu based prompt, where the user can select one of the options defined in the `menu` array, by pressing the unique key corresponding to it. The options can be up to `9`, and the keys are integers `x` where `1 <= x <= 9`. The return value is new object with the selected option stored under the specified `handle` property. 274 | 275 | ```js 276 | const qoa = require('qoa'); 277 | 278 | const {log} = console; 279 | 280 | const keypress = { 281 | type: 'keypress', 282 | query: 'How useful are the new features?', 283 | handle: 'features', 284 | menu: [ 285 | 'Meh', 286 | 'Averagely', 287 | 'Very', 288 | 'Super' 289 | ] 290 | }; 291 | 292 | // using the `prompt` async method 293 | qoa.prompt([keypress]).then(log); 294 | //=> { features: 'Very' } 295 | 296 | // using the `keypress` async method 297 | qoa.keypress(keypress).then(log); 298 | //=> { features: 'Very' } 299 | ``` 300 | 301 |
302 | Keypress Prompt 303 |
304 | 305 | ### Quiz Prompt 306 | 307 | Initializes an interactive navigable menu based prompt, where the user can navigate within a set options and select only one of them. The displayed menu is consisted of a number `amount` of options, of which the value of `answer` is by default one of them, corresponding to the correct answer to the query, and the rest `amount - 1` are randomly selected from the `choices` array. The navigation indicator can be customized through the `symbol` option and if omitted the default string `'>'` is used. The return value is new object containing the selected option, stored under the specified `handle` property, and a boolean `isCorrect` attribute, indicating whether the choice made by the user was the `answer` one. The quiz menu can be navigated through the `up arrow`/`k` & `down arrow`/`j` keys. 308 | 309 | ```js 310 | const qoa = require('qoa'); 311 | 312 | const {log} = console; 313 | 314 | const quiz = { 315 | type: 'quiz', 316 | query: 'How far is the moon from Earth?', 317 | handle: 'distance', 318 | answer: '333400 km', 319 | symbol: '>', 320 | amount: 4, 321 | choices: [ 322 | '190000 km', 323 | '280500 km', 324 | '333400 km', 325 | '560000 km', 326 | '890500 km' 327 | ] 328 | }; 329 | 330 | // using the `prompt` async method 331 | qoa.prompt([quiz]).then(log); 332 | //=> { distance: { answer: '333400 km', isCorrect: true } } 333 | 334 | // using the `quiz` async method 335 | qoa.quiz(quiz).then(log); 336 | //=> { distance: { answer: '333400 km', isCorrect: true } } 337 | ``` 338 | 339 |
340 | Quiz Prompt 341 |
342 | 343 | ### Secure Prompt 344 | 345 | Initializes a text based prompt, where each character typed by the user is automatically replaced with the `*` symbol. The return value is an object with the prompt result stored under the specified `handle` property. 346 | 347 | ```js 348 | const qoa = require('qoa'); 349 | 350 | const {log} = console; 351 | 352 | const secure = { 353 | type: 'secure', 354 | query: 'What\'s your password:', 355 | handle: 'password' 356 | }; 357 | 358 | // using the `prompt` async method 359 | qoa.prompt([secure]).then(log); 360 | //=> { password: 'password' } 361 | 362 | // using the `secure` async method 363 | qoa.secure(secure).then(log); 364 | //=> { password: 'password' } 365 | ``` 366 | 367 |
368 | Secure Prompt 369 |
370 | 371 | ## Configuration 372 | 373 | Qoa can be collectively configured through the unary `qoa.config()` function, which accepts an object containing the following two attributes: `prefix` & `underlineQuery`. The configuration is applied to all prompts belonging to the targeted qoa instance. 374 | 375 | ##### `prefix` 376 | 377 | - Type: `String` 378 | - Default: `''` 379 | 380 | A string to be included as prefix to the query of each prompt. 381 | 382 | ##### `underlineQuery` 383 | 384 | - Type: `Boolean` 385 | - Default: `false` 386 | 387 | Underline the query of each prompt. 388 | 389 | ```js 390 | const qoa = require('qoa'); 391 | 392 | qoa.config({ 393 | prefix: '>', // Use the `>` string as prefix to all queries 394 | underlineQuery: false // Do not underline queries 395 | }) 396 | 397 | qoa.secure({ 398 | type: 'secure', 399 | query: 'Type your password:', 400 | handle: 'password' 401 | }); 402 | //=> > Type your password: ****** 403 | ``` 404 | 405 | Additionally, for individual customization the unary `prefix` & `underlineQuery` functions are available. 406 | 407 | ```js 408 | const qoa = require('qoa'); 409 | 410 | // Use the `>` string as prefix to all queries 411 | qoa.prefix('>'); 412 | 413 | // Do not underline queries 414 | qoa.underlineQuery(false); 415 | 416 | qoa.secure({ 417 | type: 'secure', 418 | query: 'Type your password:', 419 | handle: 'password' 420 | }); 421 | //=> > Type your password: ****** 422 | ``` 423 | 424 | ## API 425 | 426 | #### qoa.`prompt([, configObj])` 427 | 428 | - Type: `Function` 429 | - Async: `True` 430 | - Returns: `Object` 431 | 432 | Sequentially create & display a series of prompts. 433 | 434 | ##### `configObj` 435 | 436 | - Type: `Object` 437 | 438 | Object containing the configuration of a prompt. Can hold any of the documented [options](#Usage). 439 | 440 | #### qoa.`confirm({ type, query, handle, accept, deny })` 441 | 442 | - Type: `Function` 443 | - Async: `True` 444 | - Returns: `Object` 445 | 446 | Create and display a `confirm` prompt. 447 | 448 | ##### `type` 449 | 450 | - Type: `String` 451 | - Default: `'confirm'` 452 | 453 | Indicates the type of the prompt. The option is **mandatory** when it is part of the configuration object inside the array passed to `qoa.prompt()` function. Can be considered **optional** when it is part of the object passed to the `qoa.confirm()` function. 454 | 455 | ##### `query` 456 | 457 | - Type: `String` 458 | 459 | The query to be displayed by the prompt. 460 | 461 | ##### `handle` 462 | 463 | - Type: `String` 464 | 465 | The name of the attribute under which the prompt result will be saved, inside the returned object. 466 | 467 | ##### `accept` 468 | 469 | - Type: `String` 470 | - Default: `'Y'` 471 | 472 | The string to be typed in order for the prompt to be confirmed. 473 | 474 | ##### `deny` 475 | 476 | - Type: `String` 477 | - Default: `'n'` 478 | 479 | The string to be typed in order for the prompt to be rejected. 480 | 481 | #### qoa.`hidden({ type, query, handle })` 482 | 483 | - Type: `Function` 484 | - Async: `True` 485 | - Returns: `Object` 486 | 487 | Create and display a `hidden` prompt. 488 | 489 | ##### `type` 490 | 491 | - Type: `String` 492 | - Default: `'hidden'` 493 | 494 | Indicates the type of the prompt. The option is **mandatory** when it is part of the configuration object inside the array passed to `qoa.prompt()` function. Can be considered **optional** when it is part of the object passed to the `qoa.hidden()` function. 495 | 496 | ##### `query` 497 | 498 | - Type: `String` 499 | 500 | The query to be displayed by the prompt. 501 | 502 | ##### `handle` 503 | 504 | - Type: `String` 505 | 506 | The name of the attribute under which the prompt result will be saved, inside the returned object. 507 | 508 | #### qoa.`input({ type, query, handle })` 509 | 510 | - Type: `Function` 511 | - Async: `True` 512 | - Returns: `Object` 513 | 514 | Create and display an `input` prompt. 515 | 516 | ##### `type` 517 | 518 | - Type: `String` 519 | - Default: `'input'` 520 | 521 | Indicates the type of the prompt. The option is **mandatory** when it is part of the configuration object inside the array passed to `qoa.prompt()` function. Can be considered **optional** when it is part of the object passed to the `qoa.input()` function. 522 | 523 | ##### `query` 524 | 525 | - Type: `String` 526 | 527 | The query to be displayed by the prompt. 528 | 529 | ##### `handle` 530 | 531 | - Type: `String` 532 | 533 | The name of the attribute under which the prompt result will be saved, inside the returned object. 534 | 535 | #### qoa.`interactive({ type, query, handle, symbol, menu })` 536 | 537 | - Type: `Function` 538 | - Async: `True` 539 | - Returns: `Object` 540 | 541 | Create and display an `interactive` prompt. 542 | 543 | ##### `type` 544 | 545 | - Type: `String` 546 | - Default: `'interactive'` 547 | 548 | Indicates the type of the prompt. The option is **mandatory** when it is part of the configuration object inside the array passed to `qoa.prompt()` function. Can be considered **optional** when it is part of the object passed to the `qoa.interactive()` function. 549 | 550 | ##### `query` 551 | 552 | - Type: `String` 553 | 554 | The query to be displayed by the prompt. 555 | 556 | ##### `handle` 557 | 558 | - Type: `String` 559 | 560 | The name of the attribute under which the prompt result will be saved, inside the returned object. 561 | 562 | ##### `symbol` 563 | 564 | - Type: `String` 565 | - Default: `'>'` 566 | 567 | The string to be used as the navigation indicator for the menu. 568 | can be customized through the symbol option and if omitted the default string '>' will be used. 569 | 570 | ##### `menu` 571 | 572 | - Type: `String[]` 573 | 574 | The array containing the menu options. 575 | 576 | #### qoa.`keypress({ type, query, handle, menu })` 577 | 578 | - Type: `Function` 579 | - Async: `True` 580 | - Returns: `Object` 581 | 582 | Create and display a `keypress` prompt. 583 | 584 | ##### `type` 585 | 586 | - Type: `String` 587 | - Default: `'keypress'` 588 | 589 | Indicates the type of the prompt. The option is **mandatory** when it is part of the configuration object inside the array passed to `qoa.prompt()` function. Can be considered **optional** when it is part of the object passed to the `qoa.keypress()` function. 590 | 591 | ##### `query` 592 | 593 | - Type: `String` 594 | 595 | The query to be displayed by the prompt. 596 | 597 | ##### `handle` 598 | 599 | - Type: `String` 600 | 601 | The name of the attribute under which the prompt result will be saved, inside the returned object. 602 | 603 | ##### `menu` 604 | 605 | - Type: `String[]` 606 | 607 | The array containing the menu options. 608 | 609 | #### qoa.`quiz({ type, query, handle, answer, symbol, amount, choices })` 610 | 611 | - Type: `Function` 612 | - Async: `True` 613 | - Returns: `Object` 614 | 615 | Create and display a `quiz` prompt. 616 | 617 | ##### `type` 618 | 619 | - Type: `String` 620 | - Default: `'quiz'` 621 | 622 | Indicates the type of the prompt. The option is **mandatory** when it is part of the configuration object inside the array passed to `qoa.prompt()` function. Can be considered **optional** when it is part of the object passed to the `qoa.quiz()` function. 623 | 624 | ##### `query` 625 | 626 | - Type: `String` 627 | 628 | The query to be displayed by the prompt. 629 | 630 | ##### `handle` 631 | 632 | - Type: `String` 633 | 634 | The name of the attribute under which the prompt result will be saved, inside the returned object. 635 | 636 | ##### `answer` 637 | 638 | - Type: `String` 639 | 640 | The correct answer to the quiz. 641 | 642 | ##### `symbol` 643 | 644 | - Type: `String` 645 | - Default: `'>'` 646 | 647 | The string to be used as the navigation indicator for the menu. 648 | 649 | ##### `amount` 650 | 651 | - Type: `Number` 652 | - Default: `3` 653 | 654 | The number of options to be included to the menu. 655 | 656 | ##### `choices` 657 | 658 | - Type: `String[]` 659 | 660 | The array containing the candidate menu options. 661 | 662 | #### qoa.`secure({ type, query, handle })` 663 | 664 | - Type: `Function` 665 | - Async: `True` 666 | - Returns: `Object` 667 | 668 | Create and display a `secure` prompt. 669 | 670 | ##### `type` 671 | 672 | - Type: `String` 673 | - Default: `'secure'` 674 | 675 | Indicates the type of the prompt. The option is **mandatory** when it is part of the configuration object inside the array passed to `qoa.prompt()` function. Can be considered **optional** when it is part of the object passed to the `qoa.secure()` function. 676 | 677 | ##### `query` 678 | 679 | - Type: `String` 680 | 681 | The query to be displayed by the prompt. 682 | 683 | ##### `handle` 684 | 685 | - Type: `String` 686 | 687 | The name of the attribute under which the prompt result will be saved, inside the returned object. 688 | 689 | #### qoa.`config({ prefix, underlineQuery })` 690 | 691 | - Type: `Function` 692 | - Async: `False` 693 | 694 | Collectively configure a qoa instance. 695 | 696 | ##### `prefix` 697 | 698 | - Type: `String` 699 | - Default: `''` 700 | 701 | A string to be included as prefix to the query of each prompt. 702 | 703 | ##### `underlineQuery` 704 | 705 | - Type: `Boolean` 706 | - Default: `false` 707 | 708 | Underline the query of each prompt. 709 | 710 | #### qoa.`prefix(str)` 711 | 712 | - Type: `Function` 713 | - Async: `False` 714 | 715 | Add a string as prefix to the query of each prompt belonging to the targeted qoa instance. 716 | 717 | ##### `str` 718 | 719 | - Type: `String` 720 | 721 | A string to be included as prefix to the query of each prompt. 722 | 723 | #### qoa.`underlineQuery(status)` 724 | 725 | - Type: `Function` 726 | - Async: `False` 727 | 728 | Underline the query of each prompt belonging to the targeted qoa instance. 729 | 730 | ##### `status` 731 | 732 | - Type: `Boolean` 733 | 734 | Underline the query of each prompt. 735 | 736 | #### qoa.`clearScreen()` 737 | 738 | - Type: `Function` 739 | - Async: `False` 740 | 741 | Move the cursor to the top-left corner of the console and clear everything below it. 742 | 743 | ## Development 744 | 745 | For more info on how to contribute to the project, please read the [contributing guidelines](https://github.com/klaussinani/qoa/blob/master/contributing.md). 746 | 747 | - Fork the repository and clone it to your machine 748 | - Navigate to your local fork: `cd qoa` 749 | - Install the project dependencies: `npm install` or `yarn install` 750 | - Lint code for errors: `npm test` or `yarn test` 751 | 752 | ## Related 753 | 754 | - [signale](https://github.com/klaussinani/signale) - Highly configurable logging utility 755 | - [taskbook](https://github.com/klaussinani/taskbook) - Tasks, boards & notes for the command-line habitat 756 | - [hyperocean](https://github.com/klaussinani/hyperocean) - Deep oceanic blue Hyper terminal theme 757 | 758 | ## Team 759 | 760 | - Klaudio Sinani [(@klaudiosinani)](https://github.com/klaudiosinani) 761 | 762 | ## Sponsors 763 | 764 | A big thank you to all the people and companies supporting our Open Source work: 765 | 766 | - [Better Stack: Spot, Resolve, and Prevent Downtime.](https://betterstack.com/) 767 | 768 | ## License 769 | 770 | [MIT](https://github.com/klaussinani/qoa/blob/master/license.md) 771 | -------------------------------------------------------------------------------- /src/confirm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Text = require('./text'); 3 | 4 | class Confirm extends Text { 5 | constructor(opts = {}) { 6 | super(opts); 7 | this._deny = opts.deny || 'n'; 8 | this._accept = opts.accept || 'Y'; 9 | } 10 | 11 | _responses() { 12 | return `[${this._accept}/${this._deny}] `; 13 | } 14 | 15 | get _question() { 16 | return this._formatQuery() + this._responses(); 17 | } 18 | 19 | request() { 20 | return new Promise(resolve => { 21 | const result = {}; 22 | 23 | const prompt = this._createPrompt(); 24 | 25 | prompt.question(this._question, answer => { 26 | result[this._handle] = answer === this._accept; 27 | prompt.close(); 28 | resolve(result); 29 | }); 30 | }); 31 | } 32 | } 33 | 34 | module.exports = Confirm; 35 | -------------------------------------------------------------------------------- /src/hidden.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const readline = require('readline'); 3 | const Text = require('./text'); 4 | 5 | class Hidden extends Text { 6 | constructor(opts = {}) { 7 | super(opts); 8 | } 9 | 10 | _clearLastChar() { 11 | this._clearChars(1); 12 | } 13 | 14 | request() { 15 | let secret = ''; 16 | const prompt = this._createPrompt(); 17 | 18 | const onkeypress = (char, key) => { 19 | const {ctrl, name} = key; 20 | 21 | if (key && ctrl && name === 'c') { 22 | this._input.pause(); 23 | } 24 | 25 | switch (name) { 26 | case 'return': 27 | this._input.pause(); 28 | break; 29 | 30 | case 'backspace': 31 | secret = this._removeLastChar(secret); 32 | break; 33 | 34 | default: 35 | if (!char || char.length > 2) { 36 | return; 37 | } 38 | 39 | secret += prompt.line; 40 | prompt.line = ''; 41 | this._clearLastChar(); 42 | break; 43 | } 44 | }; 45 | 46 | return new Promise(resolve => { 47 | const result = {}; 48 | 49 | readline.emitKeypressEvents(this._input); 50 | this._input.on('keypress', onkeypress); 51 | 52 | prompt.question(this._formatQuery(), () => { 53 | result[this._handle] = secret; 54 | this._input.removeListener('keypress', onkeypress); 55 | prompt.close(); 56 | resolve(result); 57 | }); 58 | }); 59 | } 60 | } 61 | 62 | module.exports = Hidden; 63 | -------------------------------------------------------------------------------- /src/input.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Text = require('./text'); 3 | 4 | class Input extends Text { 5 | constructor(opts = {}) { 6 | super(opts); 7 | } 8 | 9 | request() { 10 | return new Promise(resolve => { 11 | const result = {}; 12 | 13 | const prompt = this._createPrompt(); 14 | 15 | prompt.question(this._formatQuery(), answer => { 16 | result[this._handle] = answer; 17 | prompt.close(); 18 | resolve(result); 19 | }); 20 | }); 21 | } 22 | } 23 | 24 | module.exports = Input; 25 | -------------------------------------------------------------------------------- /src/interactive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const readline = require('readline'); 3 | const Nav = require('./nav'); 4 | 5 | class Interactive extends Nav { 6 | constructor(opts = {}) { 7 | super(opts); 8 | } 9 | 10 | request() { 11 | const answer = {}; 12 | 13 | const onkeypress = (_, key) => { 14 | const {name, ctrl} = key; 15 | 16 | if (key && ctrl && name === 'c') { 17 | this._cursor.show(); 18 | this._input.pause(); 19 | } 20 | 21 | switch (name) { 22 | case 'up': 23 | case 'k': 24 | this._moveUpwards(); 25 | break; 26 | 27 | case 'down': 28 | case 'j': 29 | this._moveDownwards(); 30 | break; 31 | 32 | case 'return': 33 | answer[this._handle] = this._menu[this._idx]; 34 | this._emitter.emit('selection'); 35 | this._displaySelection(answer[this._handle]); 36 | break; 37 | 38 | default: 39 | break; 40 | } 41 | }; 42 | 43 | return new Promise(resolve => { 44 | this._cursor.hide(); 45 | this._displayQuestion(); 46 | 47 | this._input.resume(); 48 | this._input.setRawMode(true); 49 | readline.emitKeypressEvents(this._input); 50 | 51 | this._input.on('keypress', onkeypress); 52 | 53 | this._emitter.on('selection', () => { 54 | this._cursor.show(); 55 | this._input.pause(); 56 | this._input.setRawMode(false); 57 | this._input.removeListener('keypress', onkeypress); 58 | resolve(answer); 59 | }); 60 | }); 61 | } 62 | } 63 | 64 | module.exports = Interactive; 65 | -------------------------------------------------------------------------------- /src/keypress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const readline = require('readline'); 3 | const Menu = require('./menu'); 4 | 5 | const {log} = console; 6 | 7 | class Keypress extends Menu { 8 | constructor(opts = {}) { 9 | super(opts); 10 | } 11 | 12 | get _formatItem() { 13 | return { 14 | menu: (x, i) => ` (${i + 1}) ${x}`, 15 | selected: (x, i) => ` (${i}) ${x}` 16 | }; 17 | } 18 | 19 | get _menuItems() { 20 | return this._menu.slice(0, 9).map(this._formatItem.menu); 21 | } 22 | 23 | _displayMenu() { 24 | log(this._menuItems.join('\n')); 25 | } 26 | 27 | _displaySelection(x, n) { 28 | this._clearMenu(); 29 | log(this._formatItem.selected(x, n)); 30 | } 31 | 32 | _displayQuestion() { 33 | log(this._formatQuery()); 34 | this._displayMenu(); 35 | } 36 | 37 | request() { 38 | const answer = {}; 39 | 40 | const onkeypress = (_, key) => { 41 | const {ctrl, name} = key; 42 | 43 | if (key && ctrl && name === 'c') { 44 | this._cursor.show(); 45 | return this._input.pause(); 46 | } 47 | 48 | const n = Number(name); 49 | 50 | if (n >= 1 && n <= this._menu.length) { 51 | const selection = this._menu[n - 1]; 52 | answer[this._handle] = selection; 53 | this._emitter.emit('selection'); 54 | this._displaySelection(selection, n); 55 | } 56 | }; 57 | 58 | return new Promise(resolve => { 59 | this._cursor.hide(); 60 | this._displayQuestion(); 61 | 62 | this._input.resume(); 63 | this._input.setRawMode(true); 64 | readline.emitKeypressEvents(this._input); 65 | 66 | this._input.on('keypress', onkeypress); 67 | 68 | this._emitter.on('selection', () => { 69 | this._cursor.show(); 70 | this._input.pause(); 71 | this._input.setRawMode(false); 72 | this._input.removeListener('keypress', onkeypress); 73 | resolve(answer); 74 | }); 75 | }); 76 | } 77 | } 78 | 79 | module.exports = Keypress; 80 | -------------------------------------------------------------------------------- /src/menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const events = require('events'); 3 | const Prompt = require('./prompt'); 4 | 5 | class Menu extends Prompt { 6 | constructor(opts = {}) { 7 | super(opts); 8 | this._menu = opts.menu || []; 9 | this._emitter = new events.EventEmitter(); 10 | } 11 | 12 | get _cursor() { 13 | return { 14 | hide: () => { 15 | if (!this._output.isTTY) { 16 | return; 17 | } 18 | 19 | this._output.write('\u001B[?25l'); 20 | }, 21 | show: () => { 22 | if (!this._output.isTTY) { 23 | return; 24 | } 25 | 26 | this._output.write('\u001B[?25h'); 27 | } 28 | }; 29 | } 30 | 31 | _clearLines(n) { 32 | for (let i = 0; i < n; i++) { 33 | this._output.moveCursor(0, -1); 34 | this._output.clearLine(); 35 | this._output.cursorTo(0); 36 | } 37 | } 38 | 39 | _clearMenu() { 40 | this._clearLines(this._menu.length); 41 | } 42 | } 43 | 44 | module.exports = Menu; 45 | -------------------------------------------------------------------------------- /src/nav.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Menu = require('./menu'); 3 | 4 | const {log} = console; 5 | 6 | class Nav extends Menu { 7 | constructor(opts = {}) { 8 | super(opts); 9 | this._idx = 0; 10 | this._symbol = opts.symbol || '>'; 11 | } 12 | 13 | get _formatItem() { 14 | return { 15 | selected: x => ` ${this._symbol} ${x}`, 16 | menu: x => ` ${this._whitespace(this._symbol.length)} ${x}` 17 | }; 18 | } 19 | 20 | get _menuItems() { 21 | return this._menu.map((x, i) => { 22 | if (i === this._idx) { 23 | return this._formatItem.selected(x); 24 | } 25 | 26 | return this._formatItem.menu(x); 27 | }); 28 | } 29 | 30 | _whitespace(n) { 31 | return new Array(n + 1).join(' '); 32 | } 33 | 34 | _decrementIdx() { 35 | this._idx = (this._idx === 0 ? this._menu.length : this._idx) - 1; 36 | } 37 | 38 | _incrementIdx() { 39 | this._idx = this._idx === this._menu.length - 1 ? 0 : this._idx + 1; 40 | } 41 | 42 | _displayMenu() { 43 | log(this._menuItems.join('\n')); 44 | } 45 | 46 | _refreshMenu() { 47 | this._clearMenu(); 48 | this._displayMenu(); 49 | } 50 | 51 | _displayQuestion() { 52 | log(this._formatQuery()); 53 | this._displayMenu(); 54 | } 55 | 56 | _displaySelection(x) { 57 | this._clearMenu(); 58 | log(this._formatItem.selected(x)); 59 | } 60 | 61 | _moveUpwards() { 62 | this._decrementIdx(); 63 | this._refreshMenu(); 64 | } 65 | 66 | _moveDownwards() { 67 | this._incrementIdx(); 68 | this._refreshMenu(); 69 | } 70 | } 71 | 72 | module.exports = Nav; 73 | -------------------------------------------------------------------------------- /src/prompt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Prompt { 4 | constructor(opts = {}) { 5 | this._type = opts.type; 6 | this._query = opts.query; 7 | this._handle = opts.handle; 8 | this._prefix = opts.prefix || ''; 9 | this._underline = opts.underline || false; 10 | this._input = opts.input || process.stdin; 11 | this._output = opts.output || process.stdout; 12 | } 13 | 14 | _underlineText(x) { 15 | return `\u001B[4m${x}\u001B[24m`; 16 | } 17 | 18 | _formatQuery() { 19 | const query = []; 20 | 21 | if (this._prefix) { 22 | query.push(this._prefix); 23 | } 24 | 25 | query.push(this._underline ? this._underlineText(this._query) : this._query); 26 | 27 | return query.join(' ') + ' '; 28 | } 29 | } 30 | 31 | module.exports = Prompt; 32 | -------------------------------------------------------------------------------- /src/qoa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Confirm = require('./confirm'); 3 | const Hidden = require('./hidden'); 4 | const Input = require('./input'); 5 | const Interactive = require('./interactive'); 6 | const Keypress = require('./keypress'); 7 | const Quiz = require('./quiz'); 8 | const Secure = require('./secure'); 9 | 10 | class Qoa { 11 | constructor(opts = {}) { 12 | this._prefix = opts.prefix; 13 | this._underlineQuery = opts.underline; 14 | } 15 | 16 | _buildConfig(x) { 17 | return Object.assign(x, { 18 | prefix: this._prefix, 19 | underlineQuery: this._underlineQuery 20 | }); 21 | } 22 | 23 | config(x) { 24 | const {prefix, underlineQuery} = x; 25 | this._prefix = prefix; 26 | this._underlineQuery = underlineQuery; 27 | } 28 | 29 | prefix(str) { 30 | this._prefix = str; 31 | } 32 | 33 | underlineQuery(status) { 34 | this._underline = status; 35 | } 36 | 37 | confirm(x) { 38 | return new Confirm(this._buildConfig(x)).request(); 39 | } 40 | 41 | hidden(x) { 42 | return new Hidden(this._buildConfig(x)).request(); 43 | } 44 | 45 | interactive(x) { 46 | return new Interactive(this._buildConfig(x)).request(); 47 | } 48 | 49 | input(x) { 50 | return new Input(this._buildConfig(x)).request(); 51 | } 52 | 53 | keypress(x) { 54 | return new Keypress(this._buildConfig(x)).request(); 55 | } 56 | 57 | quiz(x) { 58 | return new Quiz(this._buildConfig(x)).request(); 59 | } 60 | 61 | secure(x) { 62 | return new Secure(this._buildConfig(x)).request(); 63 | } 64 | 65 | clearScreen() { 66 | process.stdout.cursorTo(0, 0); 67 | process.stdout.clearScreenDown(); 68 | } 69 | 70 | async prompt(questions) { 71 | const answers = {}; 72 | 73 | for (const x of questions) { 74 | switch (x.type) { 75 | case 'confirm': 76 | Object.assign(answers, await this.confirm(x)); 77 | break; 78 | 79 | case 'hidden': 80 | Object.assign(answers, await this.hidden(x)); 81 | break; 82 | 83 | case 'input': 84 | Object.assign(answers, await this.input(x)); 85 | break; 86 | 87 | case 'interactive': 88 | Object.assign(answers, await this.interactive(x)); 89 | break; 90 | 91 | case 'keypress': 92 | Object.assign(answers, await this.keypress(x)); 93 | break; 94 | 95 | case 'quiz': 96 | Object.assign(answers, await this.quiz(x)); 97 | break; 98 | 99 | case 'secure': 100 | Object.assign(answers, await this.secure(x)); 101 | break; 102 | 103 | default: 104 | break; 105 | } 106 | } 107 | 108 | return answers; 109 | } 110 | } 111 | 112 | module.exports = Qoa; 113 | -------------------------------------------------------------------------------- /src/quiz.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const readline = require('readline'); 3 | const Nav = require('./nav'); 4 | 5 | class Quiz extends Nav { 6 | constructor(opts = {}) { 7 | super(opts); 8 | this._answer = opts.answer; 9 | this._choices = opts.choices; 10 | this._amount = opts.amount || 3; 11 | 12 | this._generateMenuItems(); 13 | } 14 | 15 | _randomIdx(bound) { 16 | return Math.floor(Math.random() * bound); 17 | } 18 | 19 | _generateMenuItems() { 20 | while (this._menu.length < this._amount - 1) { 21 | const x = this._choices[this._randomIdx(this._choices.length)]; 22 | if (x !== this._answer && !this._menu.includes(x)) { 23 | this._menu.push(x); 24 | } 25 | } 26 | 27 | this._menu.splice(this._randomIdx(this._amount), 0, this._answer); 28 | } 29 | 30 | request() { 31 | const result = {}; 32 | 33 | const onkeypress = (_, key) => { 34 | const {name, ctrl} = key; 35 | 36 | if (key && ctrl && name === 'c') { 37 | this._cursor.show(); 38 | this._input.pause(); 39 | } 40 | 41 | switch (name) { 42 | case 'up': 43 | case 'k': 44 | this._moveUpwards(); 45 | break; 46 | 47 | case 'down': 48 | case 'j': 49 | this._moveDownwards(); 50 | break; 51 | 52 | case 'return': 53 | result[this._handle] = Object.assign({}, { 54 | answer: this._menu[this._idx], 55 | isCorrect: this._answer === this._menu[this._idx] 56 | }); 57 | this._emitter.emit('selection'); 58 | this._displaySelection(result[this._handle].answer); 59 | break; 60 | 61 | default: 62 | break; 63 | } 64 | }; 65 | 66 | return new Promise(resolve => { 67 | this._cursor.hide(); 68 | this._displayQuestion(); 69 | 70 | this._input.resume(); 71 | this._input.setRawMode(true); 72 | readline.emitKeypressEvents(this._input); 73 | 74 | this._input.on('keypress', onkeypress); 75 | 76 | this._emitter.on('selection', () => { 77 | this._cursor.show(); 78 | this._input.pause(); 79 | this._input.setRawMode(false); 80 | this._input.removeListener('keypress', onkeypress); 81 | resolve(result); 82 | }); 83 | }); 84 | } 85 | } 86 | 87 | module.exports = Quiz; 88 | -------------------------------------------------------------------------------- /src/secure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const readline = require('readline'); 3 | const Text = require('./text'); 4 | 5 | class Secure extends Text { 6 | constructor(opts = {}) { 7 | super(opts); 8 | this._symbol = opts.symbol || '*'; 9 | } 10 | 11 | _secureStr(n) { 12 | return new Array(n + 1).join(this._symbol); 13 | } 14 | 15 | _displaySecureStr(n) { 16 | const str = this._secureStr(n); 17 | this._output.write(str); 18 | } 19 | 20 | request() { 21 | let secret = ''; 22 | const prompt = this._createPrompt(); 23 | 24 | const onkeypress = (char, key) => { 25 | const {ctrl, name} = key; 26 | 27 | if (key && ctrl && name === 'c') { 28 | this._input.pause(); 29 | } 30 | 31 | switch (name) { 32 | case 'return': 33 | this._input.pause(); 34 | break; 35 | 36 | case 'backspace': 37 | this._clearChars(secret.length); 38 | secret = this._removeLastChar(secret); 39 | this._displaySecureStr(secret.length); 40 | break; 41 | 42 | default: 43 | if (!char || char.length > 2) { 44 | return; 45 | } 46 | 47 | secret += prompt.line; 48 | prompt.line = ''; 49 | this._clearChars(secret.length); 50 | this._displaySecureStr(secret.length); 51 | break; 52 | } 53 | }; 54 | 55 | return new Promise(resolve => { 56 | const result = {}; 57 | 58 | readline.emitKeypressEvents(this._input); 59 | this._input.on('keypress', onkeypress); 60 | 61 | prompt.question(this._formatQuery(), () => { 62 | result[this._handle] = secret; 63 | this._input.removeListener('keypress', onkeypress); 64 | prompt.close(); 65 | resolve(result); 66 | }); 67 | }); 68 | } 69 | } 70 | 71 | module.exports = Secure; 72 | -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {createInterface} = require('readline'); 3 | const Prompt = require('./prompt'); 4 | 5 | class Text extends Prompt { 6 | constructor(opts = {}) { 7 | super(opts); 8 | this._historySize = 0; 9 | this._promptSymbol = ''; 10 | } 11 | 12 | get _promptOpts() { 13 | return { 14 | input: this._input, 15 | output: this._output, 16 | prompt: this._promptSymbol, 17 | historySize: this._historySize 18 | }; 19 | } 20 | 21 | _createPrompt() { 22 | return createInterface(this._promptOpts); 23 | } 24 | 25 | _clearChars(n) { 26 | this._output.moveCursor(-n, 0); 27 | this._output.clearLine(1); 28 | } 29 | 30 | _removeLastChar(x) { 31 | return x.slice(0, x.length - 1); 32 | } 33 | } 34 | 35 | module.exports = Text; 36 | --------------------------------------------------------------------------------