├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── date.js ├── enter-name.js ├── exit.js ├── fullscreen.js ├── nested.js └── slider.js ├── index.js ├── input.js ├── package.json ├── trim+newline.js └── trim.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "9" 7 | - "8" 8 | - "6" 9 | 10 | os: 11 | - linux 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Mathias Buus 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diffy 2 | 3 | A tiny framework for building diff based interactive command line tools. 4 | 5 | ``` 6 | npm install diffy 7 | ``` 8 | 9 | [![Build Status](https://travis-ci.org/mafintosh/diffy.svg?branch=master)](https://travis-ci.org/mafintosh/diffy) 10 | 11 | Basically React, but in the terminal powered by [ansi-diff](https://github.com/mafintosh/ansi-diff) and [neat-input](https://github.com/mafintosh/neat-input). 12 | 13 | ## Usage 14 | 15 | ``` js 16 | var diffy = require('diffy')() 17 | var trim = require('diffy/trim') 18 | 19 | diffy.render(function () { 20 | return trim(` 21 | Hello user. The time is: 22 | ${new Date()} 23 | That is all for now 24 | `) 25 | }) 26 | 27 | // re-render every 1s 28 | setInterval(() => diffy.render(), 1000) 29 | ``` 30 | 31 | You can also use `diffy` to query input from a user 32 | 33 | ``` js 34 | var diffy = require('diffy')() 35 | var trim = require('diffy/trim') 36 | var input = require('diffy/input')({style: style}) 37 | var names = [] 38 | 39 | input.on('update', () => diffy.render()) 40 | input.on('enter', (line) => names.push(line)) 41 | 42 | diffy.render(function () { 43 | return trim(` 44 | Enter your name: ${input.line()} 45 | List of names: ${names.join(', ')} 46 | `) 47 | }) 48 | 49 | function style (start, cursor, end) { 50 | return start + '[' + (cursor || ' ') + ']' + end 51 | } 52 | ``` 53 | 54 | See the examples folder for more. 55 | 56 | ## API 57 | 58 | #### `var diffy = require('diffy')([options])` 59 | 60 | Make a new diffy instance. Writes to stdout. 61 | 62 | Options include: 63 | 64 | ``` js 65 | { 66 | fullscreen: true // overtake the terminal like vim/less does 67 | } 68 | ``` 69 | 70 | Note that if you use `fullscreen: true`, the terminal will be restored 71 | on exit, even if your program crashes. 72 | 73 | #### `diffy.render([function])` 74 | 75 | Trigger a render and/or update the default render 76 | function. A render function should simply return a string 77 | containing the output you wish to display and then `diffy` will make sure to only print the diff. 78 | 79 | #### `diffy.width` 80 | 81 | Property containing the width of the terminal. 82 | 83 | #### `diffy.height` 84 | 85 | Property containing the height of the terminal. 86 | 87 | #### `diffy.on('resize')` 88 | 89 | Emitted when the terminal is resized. Triggers a render as well. 90 | 91 | #### `diffy.on('render')` 92 | 93 | Emitted just before a render happens. 94 | 95 | #### `var input = require('diffy/input')()` 96 | 97 | Get a [neat-input](https://github.com/mafintosh/neat-input) instance. Use this if you want to accept interactive input. 98 | 99 | #### `var trim = require('diffy/trim')` 100 | 101 | Helper function that trims and removes the indentation of a multiline string. Useful if you have a render function that returns an indented string like in the above example. 102 | 103 | #### `var trim = require('diffy/trim+newline')` 104 | 105 | Helper function that trims but adds a newline at the end 106 | 107 | ## Components 108 | 109 | With diffy, components are just strings you compose together to form your application. 110 | A bunch of modules already exists for this pattern, providing useful features. 111 | 112 | * [watson/menu-string](https://github.com/watson/menu-string) - Generate a menu with selectable menu items as a string. 113 | * [watson/progress-string](https://github.com/watson/progress-string) - Generate a CLI progress bar as a string that you can then output in any way you like. 114 | * [mafintosh/scrollable-string](https://github.com/mafintosh/scrollable-string) - Generate a diff friendly string that is bounded by a configurable scroll box. 115 | 116 | ## Credits 117 | 118 | Thank you to [@Fouad](https://github.com/Fouad) for donating the module name. 119 | 120 | ## License 121 | 122 | MIT 123 | -------------------------------------------------------------------------------- /examples/date.js: -------------------------------------------------------------------------------- 1 | var diffy = require('../')() 2 | var trim = require('../trim') 3 | 4 | diffy.render(function () { 5 | return trim(` 6 | Hello user. The time is: 7 | ${new Date()} 8 | That is all for now 9 | `) 10 | }) 11 | 12 | // re-render every 1s 13 | setInterval(() => diffy.render(), 1000) 14 | -------------------------------------------------------------------------------- /examples/enter-name.js: -------------------------------------------------------------------------------- 1 | var diffy = require('../')() 2 | var input = require('../input')({style: style}) 3 | var trim = require('../trim') 4 | 5 | var names = [] 6 | 7 | input.on('update', () => diffy.render()) 8 | input.on('enter', (line) => names.push(line)) 9 | 10 | diffy.render(function () { 11 | return trim(` 12 | Enter your name: ${input.line()} 13 | List of names: ${names.join(', ')} 14 | `) 15 | }) 16 | 17 | function style (start, cursor, end) { 18 | return start + '[' + (cursor || ' ') + ']' + end 19 | } 20 | -------------------------------------------------------------------------------- /examples/exit.js: -------------------------------------------------------------------------------- 1 | var diffy = require('../')() 2 | var trim = require('../trim') 3 | 4 | var text = 'lowercase' 5 | 6 | process.once('SIGINT', function () { 7 | text = 'UPPERCASE' 8 | diffy.render(render) 9 | process.nextTick(process.exit) 10 | }) 11 | 12 | diffy.render(render) 13 | 14 | // re-render every 1s 15 | setInterval(() => diffy.render(), 1000) 16 | 17 | function render () { 18 | return trim(` 19 | Hello world. 20 | Error should happen on last line: 21 | ${text} 22 | `) 23 | } 24 | -------------------------------------------------------------------------------- /examples/fullscreen.js: -------------------------------------------------------------------------------- 1 | var diffy = require('../')({fullscreen: true}) 2 | var input = require('../input')() 3 | var fs = require('fs') 4 | 5 | var src = fs.readFileSync(__filename, 'utf-8') 6 | var tmp = src 7 | var upper = false 8 | 9 | diffy.render(function () { 10 | return tmp 11 | }) 12 | 13 | input.on('enter', function () { 14 | upper = !upper 15 | tmp = upper ? src.toUpperCase() : src 16 | diffy.render() 17 | }) 18 | -------------------------------------------------------------------------------- /examples/nested.js: -------------------------------------------------------------------------------- 1 | var diffy = require('../')() 2 | var trim = require('../trim') 3 | 4 | diffy.render(function () { 5 | return trim(` 6 | Hello user. The time is: 7 | ${nestedDate()} 8 | THERE SHOULD BE NO SPACE ABOVE THIS LINE 9 | That is all for now 10 | `) 11 | }) 12 | 13 | // re-render every 1s 14 | setInterval(() => diffy.render(), 1000) 15 | 16 | function nestedDate () { 17 | return trim(` 18 | ${new Date()} 19 | `) 20 | } 21 | -------------------------------------------------------------------------------- /examples/slider.js: -------------------------------------------------------------------------------- 1 | var diffy = require('../')() 2 | var input = require('../input')() 3 | 4 | var pos = 0 5 | var ch = '>' 6 | 7 | input.on('left', function () { 8 | pos-- 9 | ch = '<' 10 | diffy.render() 11 | }) 12 | 13 | input.on('right', function () { 14 | pos++ 15 | ch = '>' 16 | diffy.render() 17 | }) 18 | 19 | diffy.render(render) 20 | 21 | function render () { 22 | if (pos < 1) pos = 1 23 | var widLen = diffy.width.toString().length 24 | var wid = Math.max(diffy.width - 2 * widLen - 1 - 6, 10) 25 | if (pos >= wid - 1) pos = wid - 2 26 | var i = 1 27 | var s = 'Move the cursor or \n[' 28 | for (; i < pos; i++) s += ' ' 29 | s += ch 30 | i++ 31 | for (; i < wid - 1; i++) s += ' ' 32 | s += '] ' + (pos - 1) + '/' + (wid - 3) + '\n' 33 | if (ch === '>') s += 'You are moving ' 34 | else s += 'You are moving ' 35 | return s 36 | } 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var differ = require('ansi-diff') 2 | var events = require('events') 3 | var util = require('util') 4 | 5 | var SMCUP = Buffer.from([0x1b, 0x5b, 0x3f, 0x31, 0x30, 0x34, 0x39, 0x68]) 6 | var RMCUP = Buffer.from([0x1b, 0x5b, 0x3f, 0x31, 0x30, 0x34, 0x39, 0x6c]) 7 | var CLEAR = Buffer.from([0x1b, 0x5b, 0x33, 0x4a, 0x1b, 0x5b, 0x48, 0x1b, 0x5b, 0x32, 0x4a]) 8 | 9 | module.exports = Diffy 10 | 11 | function Diffy (opts) { 12 | if (!(this instanceof Diffy)) return new Diffy(opts) 13 | if (!opts) opts = {} 14 | if (typeof opts === 'function') opts = {render: opts} 15 | 16 | events.EventEmitter.call(this) 17 | 18 | this.destroyed = false 19 | this.fullscreen = !!opts.fullscreen 20 | this.out = process.stdout 21 | this.out.on('resize', this._onresize.bind(this)) 22 | this.differ = differ(this._dimension()) 23 | 24 | this._destroy = this.destroy.bind(this) 25 | this._isFullscreen = false 26 | 27 | process.on('SIGWINCH', noop) 28 | process.on('exit', this._destroy) 29 | 30 | if (opts.render) this.render(opts.render) 31 | } 32 | 33 | util.inherits(Diffy, events.EventEmitter) 34 | 35 | Object.defineProperty(Diffy.prototype, 'height', { 36 | enumerable: true, 37 | get: function () { 38 | return this.differ.height 39 | } 40 | }) 41 | 42 | Object.defineProperty(Diffy.prototype, 'width', { 43 | enumerable: true, 44 | get: function () { 45 | return this.differ.width 46 | } 47 | }) 48 | 49 | Diffy.prototype.render = function (fn) { 50 | if (this.fullscreen && !this._isFullscreen) { 51 | this._isFullscreen = true 52 | this.out.write(SMCUP) 53 | this.out.write(CLEAR) 54 | } 55 | if (fn) this._render = fn 56 | this.emit('render') 57 | this.out.write(this.differ.update(this._render())) 58 | } 59 | 60 | Diffy.prototype.destroy = function () { 61 | if (this.destroyed) return 62 | this.destroyed = true 63 | process.removeListener('SIGWINCH', noop) 64 | process.removeListener('exit', this._destroy) 65 | if (this._isFullscreen) this.out.write(RMCUP) 66 | this.emit('destroy') 67 | } 68 | 69 | Diffy.prototype._onresize = function () { 70 | this.differ.resize(this._dimension()) 71 | this.emit('resize') 72 | this.render() 73 | } 74 | 75 | Diffy.prototype._dimension = function () { 76 | return { 77 | width: this.out.columns, 78 | height: this.out.rows 79 | } 80 | } 81 | 82 | function noop () { 83 | return '' 84 | } 85 | -------------------------------------------------------------------------------- /input.js: -------------------------------------------------------------------------------- 1 | module.exports = require('neat-input') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diffy", 3 | "version": "2.1.0", 4 | "description": "A tiny framework for building diff based interactive command line tools.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ansi-diff": "^1.0.10", 8 | "neat-input": "^1.9.0" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mafintosh/diffy.git" 13 | }, 14 | "scripts": { 15 | "test": "standard" 16 | }, 17 | "author": "Mathias Buus (@mafintosh)", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/mafintosh/diffy/issues" 21 | }, 22 | "homepage": "https://github.com/mafintosh/diffy", 23 | "devDependencies": { 24 | "standard": "^10.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /trim+newline.js: -------------------------------------------------------------------------------- 1 | var trim = require('./trim') 2 | var os = require('os') 3 | 4 | module.exports = trimAndNewline 5 | 6 | function trimAndNewline (s) { 7 | return trim(s) + os.EOL 8 | } 9 | -------------------------------------------------------------------------------- /trim.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = trim 3 | 4 | function trim (s) { 5 | if (!/^\r?\n/.test(s)) return s 6 | return deindent(s).trim() 7 | } 8 | 9 | function deindent (s) { 10 | if (!/^\r?\n/.test(s)) return s 11 | var indent = (s.match(/\n([ ]+)/m) || [])[1] || '' 12 | s = indent + s 13 | return s.split('\n') 14 | .map(l => replace(indent, l)) 15 | .join('\n') 16 | } 17 | 18 | function replace (prefix, line) { 19 | return line.slice(0, prefix.length) === prefix ? line.slice(prefix.length) : line 20 | } 21 | --------------------------------------------------------------------------------