├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6.0' 4 | - '8.0' 5 | -------------------------------------------------------------------------------- /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 | # scrollable-string 2 | 3 | Generate a diff friendly string that is bounded by a configurable scroll box. 4 | 5 | ``` 6 | npm install scrollable-string 7 | ``` 8 | 9 | [![Build Status](https://travis-ci.org/mafintosh/scrollable-string.svg?branch=master)](https://travis-ci.org/mafintosh/scrollable-string) 10 | 11 | Made for usage with [ansi-diff-stream](https://github.com/mafintosh/ansi-diff-stream) and friends 12 | 13 | ## Usage 14 | 15 | ``` js 16 | var scrollable = require('scrollable-string') 17 | 18 | var multiLineString = ` 19 | foo 20 | bar 21 | baz 22 | ` 23 | 24 | var str = scrollable(multiLineString, { 25 | maxHeight: 2 // max 32 rows high 26 | }) 27 | 28 | console.log(str.toString()) // prints foo\nbar\n 29 | str.down() 30 | console.log(str.toString()) // prints bar\nbaz\n 31 | str.down() 32 | console.log(str.toString()) // still prints bar\nbaz\n 33 | ``` 34 | 35 | ## API 36 | 37 | #### `var str = scrollable(string, [options])` 38 | 39 | Create a new scrollable string. Options include: 40 | 41 | ``` js 42 | { 43 | maxHeight: 32, // max rows height 44 | minHeight: 0 // min height (will pad the input string) 45 | } 46 | ``` 47 | 48 | #### Event: `update` 49 | 50 | Emitted after either `str.up()` or `str.down()` have been sucessfully 51 | called. 52 | 53 | #### `var moved = str.setPosition(pos)` 54 | 55 | Set absolute scroll position. 56 | 57 | Returns `true` if the position changed, `false` if not. 58 | 59 | #### `var moved = str.up()` 60 | 61 | Move the view-port up. 62 | 63 | Returns `true` if the position changed, `false` if not. 64 | 65 | #### `var moved = str.down()` 66 | 67 | Move the view-port down. 68 | 69 | Returns `true` if the position changed, `false` if not. 70 | 71 | #### `var moved = str.move(inc)` 72 | 73 | Move the view-port up or down, i.e `str.move(-5)` to move 5 lines up. 74 | 75 | Returns `true` if the position changed, `false` if not. 76 | 77 | #### `var moved = str.bottom()` 78 | 79 | Move to bottom. 80 | 81 | Returns `true` if the position changed, `false` if not. 82 | 83 | #### `var moved = str.top()` 84 | 85 | Move to top. 86 | 87 | Returns `true` if the position changed, `false` if not. 88 | 89 | #### `var rows = str.height()` 90 | 91 | Returns the height of the string in rows. 92 | 93 | #### `var changed = str.resize(options)` 94 | 95 | Resize the view-port. Takes same options as the constructor. 96 | 97 | Returns `true` if either the position or the height changed, `false` if not. 98 | 99 | #### `var string = str.toString()` 100 | 101 | Returns the string rendered by the view-port. 102 | 103 | #### `var percentage = str.pct()` 104 | 105 | Returns the current scroll position in percent (a number between `0` and 106 | `1`). 107 | 108 | #### `var bool = str.atBottom()` 109 | 110 | Check if the view-port is at the bottom. 111 | 112 | #### `var bool = str.atTop()` 113 | 114 | Check if the view-port is at the top. 115 | 116 | #### `var bool = str.scrollable()` 117 | 118 | Check if the view-port is not at the top or bottom. 119 | 120 | ## License 121 | 122 | MIT 123 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var box = require('./') 2 | var input = require('neat-input')() 3 | var diff = require('ansi-diff-stream')() 4 | 5 | diff.pipe(process.stdout) 6 | 7 | var lines = generateLines() 8 | var s = box(lines, {maxHeight: height()}) 9 | 10 | process.stdout.on('resize', function () { 11 | s.resize({maxHeight: height()}) 12 | diff.clear() 13 | render() 14 | }) 15 | 16 | input.on('up', function () { 17 | s.up() 18 | render() 19 | }) 20 | 21 | input.on('down', function () { 22 | s.down() 23 | render() 24 | }) 25 | 26 | render() 27 | 28 | function height () { 29 | return Math.max(0, process.stdout.rows - 5) 30 | } 31 | 32 | function render () { 33 | var output = s.toString() 34 | 35 | output = 36 | (s.atTop() ? '(at top)' : '(up arrow to scroll up)') + '\n' + 37 | output + 38 | (s.atBottom() ? '(at bottom)' : '(down arrow to scroll down)') 39 | 40 | diff.write(output) 41 | } 42 | 43 | function generateLines () { 44 | return Array(50).join('\n').split('\n').map((_, i) => 'line #' + i).join('\n') 45 | } 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits 2 | var os = require('os') 3 | var EventEmitter = require('events') 4 | 5 | inherits(Box, EventEmitter) 6 | 7 | module.exports = Box 8 | 9 | function Box (string, opts) { 10 | if (!(this instanceof Box)) return new Box(string, opts) 11 | 12 | EventEmitter.call(this) 13 | 14 | if (typeof string !== 'string' && !opts) { 15 | opts = string 16 | string = '' 17 | } 18 | if (!opts) opts = {} 19 | if (opts.height) opts.minHeight = opts.maxHeight = opts.height 20 | 21 | this.minHeight = opts.minHeight || 0 22 | this.maxHeight = opts.maxHeight || 32 23 | this.position = 0 24 | this.set(string || opts.string || '') 25 | } 26 | 27 | Box.prototype.setPosition = function (pos) { 28 | var oldPosition = this.position 29 | var maxPosition = Math.max(0, this.lines.length - 1 - this.maxHeight) 30 | 31 | if (pos < 0) this.position = 0 32 | else if (pos > maxPosition) this.position = maxPosition 33 | else this.position = pos 34 | 35 | var moved = this.position !== oldPosition 36 | if (moved) this.emit('update') 37 | return moved 38 | } 39 | 40 | Box.prototype.scrollable = function () { 41 | return !this.atTop() || !this.atBottom() 42 | } 43 | 44 | Box.prototype.atTop = function () { 45 | return this.position === 0 46 | } 47 | 48 | Box.prototype.atBottom = function () { 49 | var lines = this.lines.length - 1 50 | return lines - this.position <= this.maxHeight 51 | } 52 | 53 | Box.prototype.pct = function () { 54 | var bottomEdge = this.position + this.maxHeight 55 | var lines = this.lines.length - 1 56 | return Math.min(1, bottomEdge / lines) 57 | } 58 | 59 | Box.prototype.moveToTop = 60 | Box.prototype.top = function () { 61 | return this.setPosition(0) 62 | } 63 | 64 | Box.prototype.moveToBottom = 65 | Box.prototype.bottom = function () { 66 | return this.setPosition(Infinity) 67 | } 68 | 69 | Box.prototype.move = function (inc) { 70 | return this.setPosition(this.position + inc) 71 | } 72 | 73 | Box.prototype.up = function () { 74 | return this.setPosition(this.position - 1) 75 | } 76 | 77 | Box.prototype.down = function () { 78 | return this.setPosition(this.position + 1) 79 | } 80 | 81 | Box.prototype.set = function (string) { 82 | if (!/\n$/.test(string)) string += os.EOL 83 | this.string = string 84 | this.lines = string.split(os.EOL) 85 | var moved = this.setPosition(this.position) 86 | if (!moved) this.emit('update') 87 | return true 88 | } 89 | 90 | Box.prototype.height = function () { 91 | var lines = this.lines.length - 1 92 | var remainingLines = lines - this.position 93 | 94 | if (remainingLines < this.minHeight) return this.minHeight 95 | if (remainingLines > this.maxHeight) return this.maxHeight 96 | return remainingLines 97 | } 98 | 99 | Box.prototype.resize = function (opts) { 100 | var oldHeight = this.height() 101 | if (opts.height) opts.minHeight = opts.maxHeight = opts.height 102 | this.maxHeight = opts.maxHeight || this.maxHeight 103 | this.minHeight = opts.minHeight || this.minHeight 104 | var moved = this.setPosition(this.position) 105 | if (moved) return true 106 | var changed = oldHeight !== this.height() 107 | if (changed) this.emit('update') 108 | return changed 109 | } 110 | 111 | Box.prototype.toString = 112 | Box.prototype.render = function () { 113 | var needsNewline = this.position + this.maxHeight < this.lines.length 114 | var lines = this.lines.slice(this.position, this.position + this.maxHeight) 115 | 116 | while (lines.length <= this.minHeight) { 117 | needsNewline = false 118 | lines.push('') 119 | } 120 | 121 | return lines.join(os.EOL) + (needsNewline ? os.EOL : '') 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollable-string", 3 | "version": "1.3.1", 4 | "description": "Generate a diff friendly string that is bounded by a configurable scroll box", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "ansi-diff-stream": "^1.2.0", 9 | "neat-input": "^1.3.0", 10 | "standard": "^10.0.3", 11 | "tape": "^4.8.0" 12 | }, 13 | "scripts": { 14 | "test": "standard && tape test.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/mafintosh/scrollable-string.git" 19 | }, 20 | "author": "Mathias Buus (@mafintosh)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/mafintosh/scrollable-string/issues" 24 | }, 25 | "homepage": "https://github.com/mafintosh/scrollable-string" 26 | } 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var scrollable = require('./') 3 | 4 | tape('box.toString()', function (t) { 5 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 6 | t.equal(str.toString(), 'foo\n') 7 | t.end() 8 | }) 9 | 10 | tape('box.setPosition()', function (t) { 11 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 12 | t.equal(str.toString(), 'foo\n') 13 | t.notOk(str.setPosition(0)) 14 | t.equal(str.toString(), 'foo\n') 15 | t.ok(str.setPosition(1)) 16 | t.equal(str.toString(), 'bar\n') 17 | t.ok(str.setPosition(42)) 18 | t.equal(str.toString(), 'baz\n') 19 | t.notOk(str.setPosition(42)) 20 | t.end() 21 | }) 22 | 23 | tape('box.move()', function (t) { 24 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 25 | t.equal(str.toString(), 'foo\n') 26 | t.notOk(str.move(0)) 27 | t.equal(str.toString(), 'foo\n') 28 | t.ok(str.move(1)) 29 | t.equal(str.toString(), 'bar\n') 30 | t.ok(str.move(42)) 31 | t.equal(str.toString(), 'baz\n') 32 | t.notOk(str.move(42)) 33 | t.ok(str.move(-2)) 34 | t.equal(str.toString(), 'foo\n') 35 | t.end() 36 | }) 37 | 38 | tape('box.atTop()', function (t) { 39 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 40 | t.ok(str.atTop()) 41 | str.move(1) 42 | t.notOk(str.atTop()) 43 | t.end() 44 | }) 45 | 46 | tape('box.atBottom()', function (t) { 47 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 48 | t.notOk(str.atBottom()) 49 | str.move(2) 50 | t.ok(str.atBottom()) 51 | t.end() 52 | }) 53 | 54 | tape('box.scrollable()', function (t) { 55 | var s1 = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 56 | t.ok(s1.scrollable()) 57 | var s2 = scrollable('foo\nbar\nbaz\n', {maxHeight: 3}) 58 | t.notOk(s2.scrollable()) 59 | var s3 = scrollable('foo\nbar\nbaz\n', {maxHeight: 4}) 60 | t.notOk(s3.scrollable()) 61 | t.end() 62 | }) 63 | 64 | tape('box.top()', function (t) { 65 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 66 | str.move(1) 67 | t.equal(str.toString(), 'bar\n') 68 | t.ok(str.top()) 69 | t.equal(str.toString(), 'foo\n') 70 | t.notOk(str.top()) 71 | t.end() 72 | }) 73 | 74 | tape('box.bottom()', function (t) { 75 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 76 | t.equal(str.toString(), 'foo\n') 77 | t.ok(str.bottom()) 78 | t.equal(str.toString(), 'baz\n') 79 | t.notOk(str.bottom()) 80 | t.end() 81 | }) 82 | 83 | tape('box.moveToTop()', function (t) { 84 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 85 | t.equal(str.toString(), 'foo\n') 86 | t.notOk(str.moveToTop()) 87 | str.move(2) 88 | t.equal(str.toString(), 'baz\n') 89 | t.ok(str.moveToTop()) 90 | t.equal(str.toString(), 'foo\n') 91 | t.end() 92 | }) 93 | 94 | tape('box.moveToBottom()', function (t) { 95 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 96 | t.equal(str.toString(), 'foo\n') 97 | t.ok(str.moveToBottom()) 98 | t.equal(str.toString(), 'baz\n') 99 | t.notOk(str.moveToBottom()) 100 | t.end() 101 | }) 102 | 103 | tape('box.up()', function (t) { 104 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 105 | t.equal(str.toString(), 'foo\n') 106 | t.notOk(str.up()) 107 | str.move(2) 108 | t.equal(str.toString(), 'baz\n') 109 | t.ok(str.up()) 110 | t.equal(str.toString(), 'bar\n') 111 | t.ok(str.up()) 112 | t.equal(str.toString(), 'foo\n') 113 | t.notOk(str.up()) 114 | t.end() 115 | }) 116 | 117 | tape('box.down()', function (t) { 118 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 119 | t.equal(str.toString(), 'foo\n') 120 | t.ok(str.down()) 121 | t.equal(str.toString(), 'bar\n') 122 | t.ok(str.down()) 123 | t.equal(str.toString(), 'baz\n') 124 | t.notOk(str.down()) 125 | t.end() 126 | }) 127 | 128 | tape('box.set()', function (t) { 129 | var str = scrollable({maxHeight: 1}) 130 | 131 | str.set('foo\nbar\nbaz\n') 132 | 133 | t.same(str.toString(), 'foo\n') 134 | t.ok(str.down()) 135 | t.same(str.toString(), 'bar\n') 136 | t.ok(str.down()) 137 | t.same(str.toString(), 'baz\n') 138 | t.notOk(str.down()) 139 | t.same(str.toString(), 'baz\n') 140 | t.ok(str.up()) 141 | t.same(str.toString(), 'bar\n') 142 | 143 | str.moveToTop() 144 | str.set('1\n2\n') 145 | 146 | t.same(str.toString(), '1\n') 147 | t.ok(str.down()) 148 | t.same(str.toString(), '2\n') 149 | t.notOk(str.down()) 150 | t.same(str.toString(), '2\n') 151 | t.notOk(str.down()) 152 | t.same(str.toString(), '2\n') 153 | t.ok(str.up()) 154 | t.same(str.toString(), '1\n') 155 | 156 | t.end() 157 | }) 158 | 159 | tape('minHeight', function (t) { 160 | var str = scrollable({minHeight: 5}) 161 | 162 | str.set('foo\nbar\nbaz\n') 163 | t.same(str.toString(), 'foo\nbar\nbaz\n\n\n') 164 | 165 | str.set('foo\nbar\nbaz\n1\n2\n3\n4\n') 166 | t.same(str.toString(), 'foo\nbar\nbaz\n1\n2\n3\n4\n') 167 | t.end() 168 | }) 169 | 170 | tape('box.height()', function (t) { 171 | var str 172 | str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 173 | t.equal(str.height(), 1) 174 | str = scrollable('foo\nbar\nbaz\n', {maxHeight: 100}) 175 | t.equal(str.height(), 3) 176 | str = scrollable('foo\nbar\nbaz\n', {minHeight: 10}) 177 | t.equal(str.height(), 10) 178 | str = scrollable('foo\nbar\nbaz\n', {minHeight: 10, maxHeight: 100}) 179 | t.equal(str.height(), 10) 180 | str = scrollable('foo\nbar\nbaz\n', {height: 42}) 181 | t.equal(str.height(), 42) 182 | t.end() 183 | }) 184 | 185 | tape('box.resize()', function (t) { 186 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 187 | t.equal(str.toString(), 'foo\n') 188 | t.notOk(str.resize({height: 1})) 189 | t.equal(str.toString(), 'foo\n') 190 | t.ok(str.resize({height: 2})) 191 | t.equal(str.toString(), 'foo\nbar\n') 192 | t.notOk(str.resize({height: 2})) 193 | t.equal(str.toString(), 'foo\nbar\n') 194 | t.ok(str.resize({height: 3})) 195 | t.equal(str.toString(), 'foo\nbar\nbaz\n') 196 | t.notOk(str.resize({height: 3})) 197 | t.equal(str.toString(), 'foo\nbar\nbaz\n') 198 | t.notOk(str.resize({maxHeight: 42})) 199 | t.equal(str.toString(), 'foo\nbar\nbaz\n') 200 | t.ok(str.resize({minHeight: 4})) 201 | t.equal(str.toString(), 'foo\nbar\nbaz\n\n') 202 | t.notOk(str.resize({minHeight: 4})) 203 | t.equal(str.toString(), 'foo\nbar\nbaz\n\n') 204 | t.end() 205 | }) 206 | 207 | tape('update event', function (t) { 208 | var str = scrollable('foo\nbar\nbaz\n', {maxHeight: 1}) 209 | var updates = 0 210 | str.on('update', function () { 211 | updates++ 212 | }) 213 | t.equal(updates, 0, 'initial value: 0') 214 | str.up() 215 | t.equal(updates, 0, 'up: 0 -> 0') 216 | str.down() 217 | t.equal(updates, 1, 'down: 0 -> 1') 218 | str.down() 219 | t.equal(updates, 2, 'down: 1 -> 2') 220 | str.down() 221 | t.equal(updates, 2, 'down: 2 -> 2') 222 | str.top() 223 | t.equal(updates, 3, 'top: 2 -> 0') 224 | str.top() 225 | t.equal(updates, 3, 'top: 0 -> 0') 226 | str.bottom() 227 | t.equal(updates, 4, 'bottom: 0 -> 2') 228 | str.bottom() 229 | t.equal(updates, 4, 'bottom: 0 -> 2') 230 | str.setPosition(1) 231 | t.equal(updates, 5, 'setPosition: 2 -> 1') 232 | str.setPosition(1) 233 | t.equal(updates, 5, 'setPosition: 2 -> 1') 234 | str.set('new content\nfoo\nbar\nbaz\n') 235 | t.equal(updates, 6, 'set: 1 -> 1') 236 | str.resize({height: 42}) 237 | t.equal(updates, 7, 'resize: 1 -> 0') 238 | str.resize({height: 1}) 239 | t.equal(updates, 8, 'resize: 0 -> 0') 240 | t.end() 241 | }) 242 | 243 | tape('str.pct() - fully visible', function (t) { 244 | var str = scrollable('foo\nbar\nbaz', {maxHeight: 4}) 245 | t.equal(str.pct(), 1) 246 | t.end() 247 | }) 248 | 249 | tape('str.pct() - not fully visible', function (t) { 250 | var str = scrollable('1\n2\n3\n4\n5\n6', {maxHeight: 3}) 251 | t.equal(str.pct(), 0.5) 252 | str.down() 253 | t.equal(str.pct(), 4 / 6) 254 | str.down() 255 | t.equal(str.pct(), 5 / 6) 256 | str.down() 257 | t.equal(str.pct(), 1) 258 | t.end() 259 | }) 260 | --------------------------------------------------------------------------------