├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '9' 4 | - '8' 5 | - '7' 6 | - '6' 7 | - '5' 8 | - '4' 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Thomas Watson Steen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virtual-grid 2 | 3 | A viewport into a virtual grid of text cells. 4 | 5 | Renderes cells in a grid. Each cell contains text that is wrapped and 6 | truncated to fit inside the cell. Full support for ANSI colors. Useful 7 | as a layout manager for terminal apps. 8 | 9 | [![Build status](https://travis-ci.org/watson/virtual-grid.svg?branch=master)](https://travis-ci.org/watson/virtual-grid) 10 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install virtual-grid --save 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | const Grid = require('virtual-grid') 22 | 23 | const grid = new Grid({ 24 | height: 10, 25 | width: 20, 26 | rows: [ 27 | [{height: 5, text: 'This is the top cell spanning the entire width'}], 28 | [{width: '50%', text: 'Left column'}, {width: '50%', text: 'Right column'}] 29 | ] 30 | }) 31 | 32 | // Update the text in cell C 33 | grid.update(1, 1, 'This text have been overwritten') 34 | 35 | console.log(grid.toString()) 36 | ``` 37 | 38 | Output: 39 | 40 | ``` 41 | This is the top cell 42 | spanning the entire 43 | width 44 | 45 | 46 | Left This text 47 | column have been 48 | overwritt… 49 | 50 | 51 | ``` 52 | 53 | ## API 54 | 55 | ### `grid = new Grid(options)` 56 | 57 | Provide an `options` object as the first argument. The following options 58 | are supported: 59 | 60 | - `width` - The total width of the viewport (defaults to 61 | `process.stdout.columns`) 62 | - `height` - The total height of the viewport (defaults to 63 | `process.stdout.rows`) 64 | - `rows` - An array of rows. Each row is an array of `cell` objects 65 | 66 | A `cell` object supports the following properties: 67 | 68 | - `width` - The width of the cell. If the value is the string `auto` it 69 | will fill out the remaining space in the viewport. If the value is a 70 | string containing a percent sign (e.g. `25%`), it's treated as a 71 | percentage of the total width of the viewport. Otherwise it's treated 72 | as an integer representing the width in columns (defaults to `auto`) 73 | - `height` - The height of the cell. If the value is the string `auto` 74 | it will fill out the remaining space in the viewport. If the value is 75 | a string containing a percent sign (e.g. `25%`), it's treated as a 76 | percentage of the total height of the viewport. Otherwise it's treated 77 | as an integer representing the height in rows (defaults to `auto`) 78 | - `text` - Default text content of the cell (defaults to an empty 79 | string) 80 | - `wrap` - Set to `false` to disable automatic line wrapping (defaults 81 | to `true`) 82 | - `padding` - An optional array of cell padding for the top, right, 83 | bottom, and left edge of the cell respectively, e.g. `[0, 2, 0, 2]` 84 | for 2 chars of padding on the left and right edge only. If given an 85 | integer instead of an array, the padding is applied to all edges 86 | 87 | You can use strings instead of objects for cells. This is equivalent to 88 | `{text: cell}` 89 | 90 | ### `grid = new Grid(rows)` 91 | 92 | An alias for: 93 | 94 | ```js 95 | new Grid({rows: rows}) 96 | ``` 97 | 98 | ### Event: `update` 99 | 100 | Emitted every time a cell in the grid is updated. 101 | 102 | ### `grid.update(row, cell, text)` 103 | 104 | Update the text content of a cell. 105 | 106 | Arguments: 107 | 108 | - `row` - The row index 109 | - `cell` - The cell index inside the row 110 | - `text` - The new text content of the cell 111 | 112 | ### `grid.resize(width, height)` 113 | 114 | Resize the viewport to new `width` and `height`. 115 | 116 | ### `cell = grid.cellAt(row, index)` 117 | 118 | Return the cell at the gien `row` and index`. 119 | 120 | ### `str = grid.toString()` 121 | 122 | Render all content in all cells in the grid and return the result as one 123 | big string. 124 | 125 | ## License 126 | 127 | MIT 128 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const inherits = require('util').inherits 4 | const EventEmitter = require('events') 5 | const wrap = require('wrap-ansi') 6 | const substr = require('ansi-substring') 7 | const pad = require('fixed-width-string') 8 | 9 | inherits(Grid, EventEmitter) 10 | 11 | module.exports = Grid 12 | 13 | function Grid (opts) { 14 | if (!(this instanceof Grid)) return new Grid(opts) 15 | 16 | EventEmitter.call(this) 17 | 18 | const rows = Array.isArray(opts) ? opts : opts.rows 19 | this._rows = rows.map(function (row) { 20 | if (Array.isArray(row)) row = {cells: row} 21 | row.cells = row.cells.map(function (cell) { 22 | if (typeof cell === 'string') cell = {text: cell} 23 | return { 24 | _width: cell.width || 'auto', 25 | _height: cell.height || 'auto', 26 | text: cell.text || '', 27 | wrap: cell.wrap !== false, 28 | padding: normalizePadding(cell.padding) 29 | } 30 | }) 31 | return row 32 | }) 33 | 34 | this.resize( 35 | opts.width || process.stdout.columns, 36 | opts.height || process.stdout.rows 37 | ) 38 | } 39 | 40 | Grid.prototype.toString = function () { 41 | return this._rows.reduce(function (str, row) { 42 | for (let line = 0; line < row.height; line++) { 43 | str = row.cells.reduce(function (str, cell) { 44 | return str + (line >= cell.lines.length 45 | ? cell.whitespace 46 | : cell.lines[line]) 47 | }, str) + '\n' 48 | } 49 | return str 50 | }, '').slice(0, -1) // drop last line break 51 | } 52 | 53 | Grid.prototype.update = function (row, col, text) { 54 | const cell = this._rows[row].cells[col] 55 | cell.text = text 56 | cell.lines = renderCellLines(cell) 57 | this.emit('update') 58 | } 59 | 60 | Grid.prototype.resize = function (viewportWidth, viewportHeight) { 61 | const autoHeightRows = [] 62 | let fixedHeight = 0 63 | 64 | this._rows.forEach(function (row) { 65 | const autoWidthCells = [] 66 | let autoHeight = false 67 | 68 | // calculate cell dimentions for cells with fixed sizes 69 | row.cells.forEach(function (cell) { 70 | // in case we're resizing, reset the previously calculated dimentions 71 | cell.width = null 72 | cell.height = null 73 | 74 | if (cell._width === 'auto') autoWidthCells.push(cell) 75 | else cell.width = normalizeSize(cell._width, viewportWidth) 76 | if (cell._height === 'auto') autoHeight = true 77 | else cell.height = normalizeSize(cell._height, viewportHeight) 78 | }) 79 | 80 | // calculate width for cells with 'auto' width 81 | if (autoWidthCells.length > 0) { 82 | const remainingWidth = calcRemainingWidth(row.cells, viewportWidth) 83 | const autoWidth = Math.floor(remainingWidth / autoWidthCells.length) 84 | let cell 85 | for (let i = 0; i < autoWidthCells.length; i++) { 86 | cell = autoWidthCells[i] 87 | cell.width = autoWidth 88 | } 89 | cell.width += remainingWidth % autoWidthCells.length 90 | } 91 | 92 | if (autoHeight) { 93 | autoHeightRows.push(row) 94 | } else { 95 | // populate row height if not 'auto' 96 | row.height = rowHeight(row) 97 | fixedHeight += row.height 98 | } 99 | }) 100 | 101 | // calculate height for rows with 'auto' height 102 | if (autoHeightRows.length > 0) { 103 | const remainingHeight = viewportHeight - fixedHeight 104 | const autoHeight = Math.floor(remainingHeight / autoHeightRows.length) 105 | 106 | // set height on rows that are 'auto' 107 | let row 108 | for (let i = 0; i < autoHeightRows.length; i++) { 109 | row = autoHeightRows[i] 110 | row.height = autoHeight 111 | } 112 | row.height += remainingHeight % autoHeightRows.length 113 | 114 | // inherit row height on all calls that are 'auto' 115 | autoHeightRows.forEach(function (row) { 116 | row.cells.forEach(function (cell) { 117 | cell.height = cell.height || row.height 118 | }) 119 | }) 120 | } 121 | 122 | // populate remaining cell properties 123 | const self = this 124 | this._rows.forEach(function (row, index) { 125 | const y = calcY(self._rows, index) 126 | row.cells.forEach(function (cell, index) { 127 | cell.whitespace = Array(cell.width + 1).join(' ') 128 | cell.lines = renderCellLines(cell) 129 | cell.x = calcX(row, index) 130 | cell.y = y 131 | }) 132 | }) 133 | } 134 | 135 | Grid.prototype.cellAt = function (row, index) { 136 | const cell = this._rows[row].cells[index] 137 | return { 138 | text: cell.text, 139 | wrap: cell.wrap, 140 | width: cell.width, 141 | height: cell.height, 142 | padding: cell.padding, 143 | x: cell.x, 144 | y: cell.y 145 | } 146 | } 147 | 148 | function normalizeSize (size, max) { 149 | if (Number.isFinite(size) || size === 'auto') { 150 | // we'll deal with 'auto' when we have normalized everything else 151 | return size 152 | } else if (typeof size === 'string') { 153 | const isPct = size.indexOf('%') !== -1 154 | return isPct 155 | ? Math.round(parseInt(size, 10) / 100 * max) 156 | : parseInt(size, 10) 157 | } else { 158 | throw new Error('Unexpected size: ' + size) 159 | } 160 | } 161 | 162 | function normalizePadding (p) { 163 | // use same normalization rules as for CSS 164 | if (!p) p = [0, 0, 0, 0] 165 | else if (!Array.isArray(p)) p = [p, p, p, p] 166 | switch (p.length) { 167 | case 0: 168 | p = [0, 0, 0, 0] 169 | break 170 | case 1: 171 | p = [p[0], p[0], p[0], p[0]] 172 | break 173 | case 2: 174 | p = [p[0], p[1], p[0], p[1]] 175 | break 176 | case 3: 177 | p = [p[0], p[1], p[2], p[1]] 178 | break 179 | case 4: 180 | break 181 | default: 182 | p = p.slice(0, 4) 183 | } 184 | return p 185 | } 186 | 187 | function calcRemainingWidth (cells, viewportWidth) { 188 | const fixedWidth = cells.reduce(function (total, cell) { 189 | return total + (cell.width || 0) 190 | }, 0) 191 | return viewportWidth - fixedWidth 192 | } 193 | 194 | function renderCellLines (cell) { 195 | const topPad = cell.padding[0] 196 | const rightPad = cell.padding[1] 197 | const bottomPad = cell.padding[2] 198 | const leftPad = cell.padding[3] 199 | const textWidth = cell.width - leftPad - rightPad 200 | const textHeight = cell.height - topPad - bottomPad 201 | let text = cell.text 202 | if (cell.wrap) text = wrap(text, textWidth) 203 | let lines = text.split('\n').slice(0, textHeight) 204 | if (leftPad) { 205 | const padding = Array(leftPad + 1).join(' ') 206 | lines = lines.map(function (line) { 207 | return padding + line 208 | }) 209 | } 210 | if (topPad) lines = Array(topPad).fill('').concat(lines) 211 | if (cell.wrap) { 212 | return lines.map(function (line) { 213 | return pad(line, cell.width) 214 | }) 215 | } else { 216 | return lines.map(function (line) { 217 | return pad(substr(line, 0, textWidth), cell.width) 218 | }) 219 | } 220 | } 221 | 222 | function rowHeight (row) { 223 | return row.cells.reduce(function (height, cell) { 224 | return cell.height > height ? cell.height : height 225 | }, 0) 226 | } 227 | 228 | function calcX (row, cell) { 229 | let x = 0 230 | while (--cell >= 0) x += row.cells[cell].width 231 | return x 232 | } 233 | 234 | function calcY (rows, row) { 235 | let y = 0 236 | while (--row >= 0) y += rows[row].height 237 | return y 238 | } 239 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-grid", 3 | "version": "2.0.1", 4 | "description": "A viewport into a virtual grid of text cells", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ansi-substring": "^1.0.3", 8 | "fixed-width-string": "^1.0.0", 9 | "wrap-ansi": "^2.1.0" 10 | }, 11 | "devDependencies": { 12 | "chalk": "^2.3.0", 13 | "standard": "^10.0.3", 14 | "tape": "^4.8.0" 15 | }, 16 | "scripts": { 17 | "test": "standard && node test.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/watson/virtual-grid.git" 22 | }, 23 | "keywords": [ 24 | "term", 25 | "terminal", 26 | "columns", 27 | "cols", 28 | "column", 29 | "col", 30 | "layout", 31 | "manager", 32 | "rows", 33 | "row", 34 | "cells", 35 | "cell", 36 | "table", 37 | "grid", 38 | "windows" 39 | ], 40 | "author": "Thomas Watson (https://twitter.com/wa7son)", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/watson/virtual-grid/issues" 44 | }, 45 | "homepage": "https://github.com/watson/virtual-grid#readme", 46 | "coordinates": [ 47 | 51.3978265, 48 | 12.3975969 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const chalk = require('chalk') 5 | const Grid = require('./') 6 | 7 | test('translate percentages', function (t) { 8 | const grid = new Grid({ 9 | width: 200, 10 | height: 200, 11 | rows: [[{width: '11%', height: '33%'}]] 12 | }) 13 | 14 | t.equal(grid._rows[0].cells[0].width, 22) 15 | t.equal(grid._rows[0].cells[0].height, 66) 16 | t.end() 17 | }) 18 | 19 | test('cell as string', function (t) { 20 | const grid = new Grid({ 21 | width: 10, 22 | height: 1, 23 | rows: [['foo']] 24 | }) 25 | 26 | t.equal(grid.toString(), 27 | // 1234567890 28 | 'foo ') 29 | t.end() 30 | }) 31 | 32 | test('cell full', function (t) { 33 | const text = '0123456789\nabcdefghij\n0123456789' 34 | const grid = new Grid({ 35 | width: 10, 36 | height: 3, 37 | rows: [[{text: text}]] 38 | }) 39 | 40 | t.equal(grid.toString(), text) 41 | t.end() 42 | }) 43 | 44 | test('padding - int', function (t) { 45 | const grid = new Grid({ 46 | width: 10, 47 | height: 5, 48 | rows: [[{text: 'foo\nbar', padding: 1}]] 49 | }) 50 | 51 | t.equal(grid.toString(), 52 | // 1234567890 53 | ' \n' + 54 | ' foo \n' + 55 | ' bar \n' + 56 | ' \n' + 57 | ' ') 58 | t.end() 59 | }) 60 | 61 | test('padding - array(1)', function (t) { 62 | const grid = new Grid({ 63 | width: 10, 64 | height: 5, 65 | rows: [[{text: 'foo\nbar', padding: [1]}]] 66 | }) 67 | 68 | t.equal(grid.toString(), 69 | // 1234567890 70 | ' \n' + 71 | ' foo \n' + 72 | ' bar \n' + 73 | ' \n' + 74 | ' ') 75 | t.end() 76 | }) 77 | 78 | test('padding - array(2)', function (t) { 79 | const grid = new Grid({ 80 | width: 10, 81 | height: 5, 82 | rows: [[{text: 'foo\nbar', padding: [1, 2]}]] 83 | }) 84 | 85 | t.equal(grid.toString(), 86 | // 1234567890 87 | ' \n' + 88 | ' foo \n' + 89 | ' bar \n' + 90 | ' \n' + 91 | ' ') 92 | t.end() 93 | }) 94 | 95 | test('padding - array(3)', function (t) { 96 | const grid = new Grid({ 97 | width: 10, 98 | height: 5, 99 | rows: [[{text: 'foo\nbar', padding: [1, 2, 3]}]] 100 | }) 101 | 102 | t.equal(grid.toString(), 103 | // 1234567890 104 | ' \n' + 105 | ' foo \n' + 106 | ' \n' + 107 | ' \n' + 108 | ' ') 109 | t.end() 110 | }) 111 | 112 | test('padding right', function (t) { 113 | const grid = new Grid({ 114 | width: 10, 115 | height: 1, 116 | rows: [[{text: '12345678901234567890', padding: [0, 1, 0, 0], wrap: false}]] 117 | }) 118 | 119 | t.equal(grid.toString(), 120 | // 1234567890 121 | '123456789 ') 122 | t.end() 123 | }) 124 | 125 | test('padding - array(4)', function (t) { 126 | const grid = new Grid({ 127 | width: 10, 128 | height: 5, 129 | rows: [[{text: 'foo\nbar', padding: [1, 2, 3, 4]}]] 130 | }) 131 | 132 | t.equal(grid.toString(), 133 | // 1234567890 134 | ' \n' + 135 | ' foo \n' + 136 | ' \n' + 137 | ' \n' + 138 | ' ') 139 | t.end() 140 | }) 141 | 142 | test('cell wrap', function (t) { 143 | const grid = new Grid({ 144 | width: 10, 145 | height: 3, 146 | rows: [[{text: 'this is a test of a long line'}]] 147 | }) 148 | 149 | t.equal(grid.toString(), 150 | // 1234567890 151 | 'this is a \n' + 152 | 'test of a \n' + 153 | 'long line ') 154 | t.end() 155 | }) 156 | 157 | test('cell wrap - no whitespace', function (t) { 158 | const grid = new Grid({ 159 | width: 10, 160 | height: 3, 161 | rows: [[{text: 'thisisatestofalongline'}]] 162 | }) 163 | 164 | t.equal(grid.toString(), 165 | // 1234567890 166 | 'thisisate…\n' + 167 | ' \n' + 168 | ' ') 169 | t.end() 170 | }) 171 | 172 | test('cell wrap - ansi codes', function (t) { 173 | const grid = new Grid({ 174 | width: 10, 175 | height: 3, 176 | rows: [[{text: 'this ' + chalk.green('is') + ' a test of a long line'}]] 177 | }) 178 | 179 | t.equal(grid.toString(), 180 | // 1234567890 181 | 'this \x1b[32mis\x1b[39m a \n' + 182 | 'test of a \n' + 183 | 'long line ') 184 | t.end() 185 | }) 186 | 187 | test('no cell wrap', function (t) { 188 | const grid = new Grid({ 189 | width: 10, 190 | height: 3, 191 | rows: [[{text: 'this is a test of a long line', wrap: false}]] 192 | }) 193 | 194 | t.equal(grid.toString(), 195 | // 1234567890 196 | 'this is a \n' + 197 | ' \n' + 198 | ' ') 199 | t.end() 200 | }) 201 | 202 | test('no cell wrap - ansi codes', function (t) { 203 | const grid = new Grid({ 204 | width: 10, 205 | height: 3, 206 | rows: [[{text: 'this ' + chalk.green('is') + ' a test of a long line', wrap: false}]] 207 | }) 208 | 209 | t.equal(grid.toString(), 210 | // 1234567890 211 | 'this \x1b[32mis\x1b[39m a \n' + 212 | ' \n' + 213 | ' ') 214 | t.end() 215 | }) 216 | 217 | test('omit overflow lines', function (t) { 218 | const grid = new Grid({ 219 | width: 10, 220 | height: 3, 221 | rows: [[{text: '1\n2\n3\n4'}]] 222 | }) 223 | 224 | t.equal(grid.toString(), 225 | // 1234567890 226 | '1 \n' + 227 | '2 \n' + 228 | '3 ') 229 | t.end() 230 | }) 231 | 232 | test('two columns - even', function (t) { 233 | const grid = new Grid({ 234 | width: 20, 235 | height: 2, 236 | rows: [['left', 'right']] 237 | }) 238 | 239 | t.equal(grid.toString(), 240 | // 12345678901234567890 241 | 'left right \n' + 242 | ' ') 243 | t.end() 244 | }) 245 | 246 | test('two columns - odd', function (t) { 247 | const grid = new Grid({ 248 | width: 21, 249 | height: 2, 250 | rows: [['left', 'right']] 251 | }) 252 | 253 | t.equal(grid.toString(), 254 | // 123456789012345678901 255 | 'left right \n' + 256 | ' ') 257 | t.end() 258 | }) 259 | 260 | test('span cells', function (t) { 261 | const grid = new Grid({ 262 | width: 20, 263 | height: 10, 264 | rows: [ 265 | [{height: 3, text: 'top'}], 266 | [{width: '50%', text: 'left'}, {width: '50%', text: 'right'}] 267 | ] 268 | }) 269 | 270 | t.equal(grid.toString(), 271 | // 12345678901234567890 272 | 'top \n' + 273 | ' \n' + 274 | ' \n' + 275 | 'left right \n' + 276 | ' \n' + 277 | ' \n' + 278 | ' \n' + 279 | ' \n' + 280 | ' \n' + 281 | ' ') 282 | t.end() 283 | }) 284 | 285 | test('grid.update', function (t) { 286 | const grid = new Grid({ 287 | width: 5, 288 | height: 1, 289 | rows: [[{text: 'foo'}]] 290 | }) 291 | t.equal(grid.toString(), 'foo ') 292 | grid.update(0, 0, 'bar') 293 | t.equal(grid.toString(), 'bar ') 294 | t.end() 295 | }) 296 | 297 | test('grid.resize', function (t) { 298 | const grid = new Grid({ 299 | width: 10, 300 | height: 1, 301 | rows: [[{text: 'this is a test'}]] 302 | }) 303 | t.equal(grid.toString(), 304 | // 1234567890 305 | 'this is a ') 306 | grid.resize(7, 2) 307 | t.equal(grid.toString(), 308 | // 1234567 309 | 'this is\n' + 310 | 'a test ') 311 | t.end() 312 | }) 313 | 314 | test('grid.cellAt', function (t) { 315 | const grid = new Grid({ 316 | width: 5, 317 | height: 9, 318 | rows: [ 319 | ['a'], 320 | ['b', 'c'] 321 | ] 322 | }) 323 | t.deepEqual(grid.cellAt(0, 0), { 324 | text: 'a', 325 | wrap: true, 326 | width: 5, 327 | height: 4, 328 | padding: [0, 0, 0, 0], 329 | x: 0, 330 | y: 0 331 | }) 332 | t.deepEqual(grid.cellAt(1, 0), { 333 | text: 'b', 334 | wrap: true, 335 | width: 2, 336 | height: 5, 337 | padding: [0, 0, 0, 0], 338 | x: 0, 339 | y: 4 340 | }) 341 | t.deepEqual(grid.cellAt(1, 1), { 342 | text: 'c', 343 | wrap: true, 344 | width: 3, 345 | height: 5, 346 | padding: [0, 0, 0, 0], 347 | x: 2, 348 | y: 4 349 | }) 350 | t.end() 351 | }) 352 | 353 | test('update event', function (t) { 354 | const grid = new Grid({ 355 | width: 10, 356 | height: 1, 357 | rows: [[{}]] 358 | }) 359 | grid.on('update', function () { 360 | t.equal(grid.toString(), 'test ') 361 | t.end() 362 | }) 363 | grid.update(0, 0, 'test') 364 | }) 365 | 366 | test('https://github.com/chalk/wrap-ansi/issues/24', function (t) { 367 | const grid = new Grid({ 368 | width: 10, 369 | height: 1, 370 | rows: [['foo bar']] 371 | }) 372 | 373 | t.equal(grid.toString(), 374 | // 1234567890 375 | 'foo bar ') 376 | t.end() 377 | }) 378 | --------------------------------------------------------------------------------