├── .gitignore ├── .travis.yml ├── .editorconfig ├── SECURITY.md ├── .jshintrc ├── package.json ├── LICENSE-MIT ├── README.md ├── test └── readline.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.8' 4 | - '0.10' 5 | - '0.12' 6 | - 'iojs' 7 | before_install: 8 | - npm install -g npm@latest 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | 5 | You can also send an email to admin@simonboudrias.com for direct contact with the maintainer. 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": false, 5 | "curly": false, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "immed": true, 9 | "latedef": false, 10 | "newcap": true, 11 | "noarg": true, 12 | "undef": true, 13 | "strict": true, 14 | "trailing": true, 15 | "smarttabs": true, 16 | "indent": 2, 17 | "quotmark": "single" 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readline2", 3 | "version": "1.0.1", 4 | "description": "Readline Façade fixing bugs and issues found in releases 0.8 and 0.10", 5 | "scripts": { 6 | "test": "mocha -R spec" 7 | }, 8 | "repository": "SBoudrias/readline2", 9 | "keywords": [ 10 | "cli", 11 | "terminal", 12 | "readline", 13 | "tty", 14 | "ansi" 15 | ], 16 | "author": "Simon Boudrias ", 17 | "license": "MIT", 18 | "files": [ 19 | "index.js" 20 | ], 21 | "dependencies": { 22 | "code-point-at": "^1.0.0", 23 | "is-fullwidth-code-point": "^1.0.0", 24 | "mute-stream": "0.0.5" 25 | }, 26 | "devDependencies": { 27 | "chalk": "^1.1.0", 28 | "mocha": "^2.1.0", 29 | "sinon": "^1.7.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Simon Boudrias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | readline2 [![Build Status](https://travis-ci.org/SBoudrias/readline2.png?branch=master)](https://travis-ci.org/SBoudrias/readline2) 2 | ========= 3 | 4 | Node.js (v0.8 and v0.10) had some bugs and issues with the default [Readline](http://nodejs.org/api/readline.html) module. 5 | 6 | This module include fixes seen in later version (0.11-0.12 and iojs) and ease some undesirable behavior one could see using the readline to create interatives prompts. This means `readline2` change some behaviors and as so is **not** meant to be an exact drop-in replacement. 7 | 8 | This project is extracted from the core of [Inquirer.js interactive prompt interface](https://github.com/SBoudrias/Inquirer.js) to be available as a standalone module. 9 | 10 | 11 | Documentation 12 | ------------- 13 | 14 | **Installation**: `npm install --save readline2` 15 | 16 | ### readline2.createInterface(options); -> {Interface} 17 | 18 | Present the same API as [Node.js `readline.createInterface()`](http://nodejs.org/api/readline.html) 19 | 20 | #### Improvements 21 | - Default `options.input` as `process.stdin` 22 | - Default `options.output` as `process.stdout` 23 | - `interface.stdout` is wrapped in a [MuteStream](https://github.com/isaacs/mute-stream) 24 | - Prevent `up` and `down` keys from moving through history inside the readline 25 | - Fix cursor position after a line refresh when the `Interface` prompt contains ANSI colors 26 | - Correctly return the cursor position when faced with implicit line returns 27 | 28 | 29 | License 30 | ------------- 31 | 32 | Copyright (c) 2012 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart)) 33 | Licensed under the MIT license. 34 | -------------------------------------------------------------------------------- /test/readline.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var rawReadline = require("readline"); 3 | var chalk = require("chalk"); 4 | var sinon = require("sinon"); 5 | var readline2 = require("../"); 6 | 7 | /** 8 | * Assert an Object implements an interface 9 | * @param {Object} subject - subject implementing the façade 10 | * @param {Object|Array} methods - a façace, hash or array of keys to be implemented 11 | */ 12 | 13 | assert.implement = function (subject, methods) { 14 | methods = Array.isArray(methods) ? methods : Object.keys(methods).filter(function (method) { 15 | return typeof methods[method] === 'function'; 16 | }); 17 | 18 | var pass = methods.filter(function (method) { 19 | assert(typeof subject[method] === 'function', "expected subject to implement `" + method + "`"); 20 | return typeof subject[method] !== 'function'; 21 | }); 22 | 23 | assert.ok(pass.length === 0, "expected object to implement the complete interface"); 24 | }; 25 | 26 | 27 | describe("Readline2", function() { 28 | beforeEach(function() { 29 | this.rl = readline2.createInterface(); 30 | }); 31 | 32 | it("returns an interface", function() { 33 | var opt = { input: process.stdin, output: process.stdout }; 34 | var interface2 = readline2.createInterface(opt); 35 | var interface = rawReadline.createInterface(opt); 36 | assert.implement( interface2, interface ); 37 | }); 38 | 39 | it("transform interface.output as a MuteStream", function( done ) { 40 | var expected = [ "foo", "lab" ]; 41 | this.rl.output.on("data", function( chunk ) { 42 | assert.equal( chunk, expected.shift() ); 43 | }); 44 | this.rl.output.on("end", function() { done(); }); 45 | 46 | this.rl.write("foo"); 47 | this.rl.output.mute(); 48 | this.rl.write("bar"); 49 | this.rl.output.unmute(); 50 | this.rl.write("lab"); 51 | this.rl.output.end(); 52 | }); 53 | 54 | it("position the cursor at the expected emplacement when the prompt contains ANSI control chars", function() { 55 | this.rl.setPrompt(chalk.red("readline2> ")); 56 | this.rl.output.emit("resize"); 57 | this.rl.write("answer"); 58 | assert.equal( this.rl._getCursorPos().cols, 17 ); 59 | }); 60 | 61 | it("doesn\'t write up and down arrow", function() { 62 | this.rl._historyPrev = sinon.spy(); 63 | this.rl._historyNext = sinon.spy(); 64 | process.stdin.emit("keypress", null, { name: "up" }); 65 | process.stdin.emit("keypress", null, { name: "down" }); 66 | assert( this.rl._historyPrev.notCalled ); 67 | assert( this.rl._historyNext.notCalled ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Readline API façade to fix some issues 3 | * @Note: May look a bit like Monkey patching... if you know a better way let me know. 4 | */ 5 | 6 | "use strict"; 7 | var readline = require("readline"); 8 | var MuteStream = require("mute-stream"); 9 | var codePointAt = require("code-point-at"); 10 | var isFullwidthCodePoint = require("is-fullwidth-code-point"); 11 | 12 | var Interface = module.exports = {}; 13 | 14 | 15 | /** 16 | * Create a readline interface 17 | * @param {Object} opt Readline option hash 18 | * @return {readline} the new readline interface 19 | */ 20 | 21 | Interface.createInterface = function( opt ) { 22 | opt || (opt = {}); 23 | var filteredOpt = opt; 24 | 25 | // Default `input` to stdin 26 | filteredOpt.input = opt.input || process.stdin; 27 | 28 | // Add mute capabilities to the output 29 | var ms = new MuteStream(); 30 | ms.pipe( opt.output || process.stdout ); 31 | filteredOpt.output = ms; 32 | 33 | // Create the readline 34 | var rl = readline.createInterface( filteredOpt ); 35 | 36 | // Fix bug with refreshLine 37 | var _refreshLine = rl._refreshLine; 38 | rl._refreshLine = function() { 39 | _refreshLine.call(rl); 40 | 41 | var line = this._prompt + this.line; 42 | var cursorPos = this._getCursorPos(); 43 | 44 | readline.moveCursor(this.output, -line.length, 0); 45 | readline.moveCursor(this.output, cursorPos.cols, 0); 46 | }; 47 | 48 | // Returns current cursor's position and line 49 | rl._getCursorPos = function() { 50 | var columns = this.columns; 51 | var strBeforeCursor = this._prompt + this.line.substring(0, this.cursor); 52 | var dispPos = this._getDisplayPos(strBeforeCursor); 53 | var cols = dispPos.cols; 54 | var rows = dispPos.rows; 55 | // If the cursor is on a full-width character which steps over the line, 56 | // move the cursor to the beginning of the next line. 57 | if (cols + 1 === columns && 58 | this.cursor < this.line.length && 59 | isFullwidthCodePoint(codePointAt(this.line, this.cursor))) { 60 | rows++; 61 | cols = 0; 62 | } 63 | return {cols: cols, rows: rows}; 64 | }; 65 | 66 | // Returns the last character's display position of the given string 67 | rl._getDisplayPos = function(str) { 68 | var offset = 0; 69 | var col = this.columns; 70 | var row = 0; 71 | var code; 72 | str = stripVTControlCharacters(str); 73 | for (var i = 0, len = str.length; i < len; i++) { 74 | code = codePointAt(str, i); 75 | if (code >= 0x10000) { // surrogates 76 | i++; 77 | } 78 | if (code === 0x0a) { // new line \n 79 | offset = 0; 80 | row += 1; 81 | continue; 82 | } 83 | if (isFullwidthCodePoint(code)) { 84 | if ((offset + 1) % col === 0) { 85 | offset++; 86 | } 87 | offset += 2; 88 | } else { 89 | offset++; 90 | } 91 | } 92 | var cols = offset % col; 93 | var rows = row + (offset - cols) / col; 94 | return {cols: cols, rows: rows}; 95 | }; 96 | 97 | // Prevent arrows from breaking the question line 98 | var origWrite = rl._ttyWrite; 99 | rl._ttyWrite = function( s, key ) { 100 | key || (key = {}); 101 | 102 | if ( key.name === "up" ) return; 103 | if ( key.name === "down" ) return; 104 | 105 | origWrite.apply( this, arguments ); 106 | }; 107 | 108 | return rl; 109 | }; 110 | 111 | // Regexes used for ansi escape code splitting 112 | var metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/; 113 | var functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [ 114 | '(\\d+)(?:;(\\d+))?([~^$])', 115 | '(?:M([@ #!a`])(.)(.))', // mouse 116 | '(?:1;)?(\\d+)?([a-zA-Z])' 117 | ].join('|') + ')'); 118 | 119 | /** 120 | * Tries to remove all VT control characters. Use to estimate displayed 121 | * string width. May be buggy due to not running a real state machine 122 | */ 123 | function stripVTControlCharacters (str) { 124 | str = str.replace(new RegExp(functionKeyCodeReAnywhere.source, 'g'), ''); 125 | return str.replace(new RegExp(metaKeyCodeReAnywhere.source, 'g'), ''); 126 | } 127 | --------------------------------------------------------------------------------