├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── books.js ├── example.js ├── lots.js └── simple.js ├── lib ├── Column.js ├── ColumnBuffer.js ├── ColumnLine.js ├── ColumnStream.js ├── ColumnView.js └── MainView.js ├── main.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | - "iojs-v2.5.0" 7 | branches: 8 | except: 9 | - image 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Arjun Mehta 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # columns 2 | [![Build Status](https://travis-ci.org/arjunmehta/node-columns.svg)](https://travis-ci.org/arjunmehta/node-columns) 3 | 4 | ![columns title image](https://raw.githubusercontent.com/arjunmehta/node-columns/image/image/cover.png) 5 | 6 | ![demo image](https://raw.githubusercontent.com/arjunmehta/node-columns/image/image/demo-main.gif) 7 | 8 | #### In Active Development 9 | 10 | This module is very new and is in active development. Please consider contributing: [issues](https://github.com/arjunmehta/node-columns/issues/new), [feature requests](https://github.com/arjunmehta/node-columns/issues/new) and especially [development help](https://github.com/arjunmehta/node-columns), and [tests](https://github.com/arjunmehta/node-columns). 11 | 12 | This module takes **any number of text streams and puts them neatly into full screen columns**. 13 | 14 | - **columns act as writable streams** 15 | - **a minimal, yet easily scalable interface** 16 | - **ANSI code support (colours and text styles), line wrapping and raw modes(!)** 17 | - **custom column headers, and separators for headers and columns**. 18 | - **choice of buffer flow modes for display efficiency control** 19 | 20 | 21 | ## Installation 22 | ```bash 23 | npm install --save columns 24 | ``` 25 | 26 | 27 | ## Basic Usage 28 | 29 | ### Include and Create your Columns 30 | 31 | ```javascript 32 | var columns = require('columns').create() 33 | ``` 34 | 35 | ### Add Columns 36 | Add columns to your program. Give them a name, and set options for them. 37 | 38 | ```javascript 39 | var a = columns.addColumn('Column A') 40 | // OR 41 | columns.addColumn('Column B') 42 | // OR 43 | var c = columns.addColumn('Column C', {raw : true}) // character feed 44 | ``` 45 | 46 | ### Write or Pipe Text to Your Columns! 47 | 48 | Columns act like any writable-stream does. Just add more to it, by calling the `write` method, or `pipe` from another readable stream. 49 | 50 | ```javascript 51 | setInterval(function(){ 52 | a.write((new Date().getSeconds() % 2 === 0) ? 'TICK\n' : 'TOCK\n') 53 | columns.column("Column B").write('The Time: ' + new Date() + '\n') 54 | }, 1000) 55 | 56 | process.stdin.setRawMode(true) 57 | process.stdin.pipe(c) 58 | ``` 59 | 60 | 61 | ## Advanced Usage 62 | 63 | The above was just to get you started. Columns can do a whole lot more, and can be customized in a few ways. 64 | 65 | ### Column Specific Settings 66 | 67 | You can set column specific settings in two main ways: when you create them using options, or by setting properties on already instantiated columns. **Some settings can only be set upon creation**. 68 | 69 | ```javascript 70 | var a = columns.addColumn('A', { 71 | width: '25%', 72 | header: 'Column A' 73 | }) 74 | // OR 75 | a.width = '25%' 76 | a.header = 'Column A' 77 | ``` 78 | 79 | #### Column Width 80 | 81 | By default, columns distribute their widths evenly to the size of the TTY. But column widths can also be set to a fixed value, or scaled to a percentage of the terminal width. 82 | 83 | Columns that do not fit in the terminal based on their determined width will be automatically hidden from view. They will still be able to receive input to their buffer, so written data will be displayed again if the terminal is scaled enough to fit them, or other columns are removed. 84 | 85 | ```javascript 86 | columns.column('A').width = '25%' // approximately 25% of the tty width 87 | // OR 88 | columns.column('A').width = 30 //30 tty columns wide 89 | ``` 90 | 91 | #### Line Wrapping 92 | 93 | By default, column lines are not wrapped. If you'd like to enable this experimental feature, use the `wrap` option when adding your column. 94 | 95 | ```javascript 96 | var a = columns.addColumn('A', {wrap: true}) 97 | ``` 98 | 99 | 100 | #### Custom Header 101 | 102 | By default, columns are displayed with the given column name as the header. If you'd like to set a different header, this can be done easily with the `header` property for each column. 103 | 104 | Set the `header` to `false` to hide the header and its separator. 105 | 106 | ```javascript 107 | // custom header in green 108 | columns.column('A').header = '\033[32mCustom Header\033[0m' 109 | ``` 110 | 111 | 112 | #### RAW mode 113 | 114 | By default, columns parse incoming data by new lines. If you'd like to have your columns display as the buffer comes in, create it with the `raw` option. This must be set when you create your column. 115 | 116 | ```javascript 117 | var a = columns.addColumn('A', {raw: true}) 118 | ``` 119 | 120 | 121 | ### Global Columns Settings 122 | 123 | You can customize the appearance and behaviour of your columns with global settings, which can be set in a few ways: by passing `options` through when you first create your columns; or by setting them individual properties. 124 | 125 | ```javascript 126 | var columns = require('columns').create({ 127 | column_separator: '|' 128 | }) 129 | // OR 130 | column.column_separator: '|' 131 | ``` 132 | 133 | #### Print All Column Buffers on Exit 134 | Because this module uses a special print mode during regular display, on Exit, all contents on the screen are wiped. You can optionally print out the contents of each column linearly upon exiting. Just set to the `print` option to `true`. 135 | 136 | ```javascript 137 | var columns = require('columns').create({ 138 | print: true 139 | }) 140 | ``` 141 | 142 | #### Add Custom Column and Header separators 143 | 144 | Globally, you can set the appearance of your header and column separators to any character string. These will be repeated, for the width/height of your column. Set either to `false` to not render any header. 145 | 146 | ```javascript 147 | columns.header_separator = '_-_-' 148 | ``` 149 | 150 | ```javascript 151 | columns.column_separator = '|-|' 152 | ``` 153 | 154 | #### Add Margins to the Column Set 155 | 156 | If you'd like to add space around your column set, set the margin for the `top`, `right`, `bottom` and `left` of your column set. Values represent character spaces. 157 | 158 | ```javascript 159 | columns.margin = { 160 | top: 3, 161 | right: 2, 162 | bottom: 0, 163 | left: 2 164 | } 165 | ``` 166 | 167 | #### Flow Mode 168 | You have two options with how your columns handle overflows: 169 | 170 | **Push Mode**: When your column buffer fills up past the height of your column, the text will 'push' the previous buffer up, the same behaviour as most terminals. This will essentially redraw the column output, because it is shifting every line up by 1. This is the default. 171 | 172 | **Reset Mode**: When text reaches the bottom of the column, the column view is cleared (though the buffer remains), and printing begins again at the top of the column. This is actually much more efficient (less re-writing of the screen) and recommended for remote connections. It also makes the terminal do less work. If you set this mode, you can also set how many rows of the buffer will `overflow` after reset. 173 | 174 | If you need to adjust the flow mode, it must be set when you create your columns: 175 | 176 | ```javascript 177 | var columns = require('columns').create({ 178 | flow_mode: 'reset', 179 | overflow: 4 180 | }) 181 | ``` 182 | 183 | 184 | #### Maximum Buffer Size 185 | By default, the maximum buffer size will be `500` lines per column. If you need more or less, adjust the `maximum_buffer` option to control this. Again, you'll need to set this when you are creating your columns instance: 186 | 187 | ```javascript 188 | var columns = require('columns').create({ 189 | maximum_buffer: 2000 190 | }) 191 | ``` 192 | 193 | 194 | #### Tab Size 195 | Columns has to use its own tab parsing. Set the number of spaces you'd like your tabs to print as with the `tab_size` option. Defaults to `2`. 196 | 197 | ```javascript 198 | columns.tab_size = 4 199 | ``` 200 | 201 | 202 | ## API 203 | 204 | ### Columns.create(options) 205 | Initialize and return your `columns` object. This will, by default, clear the screen and go into full terminal screen mode. 206 | 207 | - `options` **Object**: 208 | - `header_separator` **String**: Specify a string to be repeated horizontally under defined headers. 209 | - `column_separator` **String**: Specify a string to be repeated vertically between columns. 210 | - `margin` **Object**: Set the margins for the entire column set in the form: `{top: Number, right: Number, bottom: Number, left: Number }` 211 | - `flow_mode` **String**: Set the type of flow mode to use when rendering buffer overflows: `reset` or `push`. If using `reset` you can also set the `overflow` option. 212 | - `overflow` **Number**: Set along with `flow_mode:'reset'`, the number of buffer lines to overflow after reset. 213 | - `maximum_buffer` **Number**: Set the number of lines you want available stored in your buffer (Default 500). 214 | - `tab_size` **Number**: Set the number of spaces you want to render your tabs as. 215 | 216 | ```javascript 217 | var columns = require('columns').create({ 218 | column_separator: '|', 219 | flow_mode: 'push' 220 | }) 221 | ``` 222 | 223 | Will create a column set with columns separated by the `|` character with the `push` flow mode. 224 | 225 | ### columns.addColumn(name, options) 226 | Returns a new `Column` object and simultaneously adds it to your `columns`. 227 | 228 | - `name` **String**: Set the name of your column. This will by default also set the header of your column to this value. If you do not provide a name, you will not be able to refer to it with the **columns.column()** method. 229 | - `options` **Object**: 230 | - `width` **String|Number**: Set the width of this column. This can be as a percentage (**String** with `%`) or a number of terminal characters width (**Number**). 231 | - `header` **String**: Specify the header title of the column. Set to `false` to show no header. 232 | - `raw` **Boolean**: Set if you want the stream to be read in by character instead of by line. 233 | - `wrap` **Boolean**: Set to enable line wrapping. (Experimental). 234 | 235 | ```javascript 236 | var a = columns.addColumn('Column A', { 237 | width: '50%' 238 | }) 239 | // OR 240 | columns.addColumn('Column B') 241 | // OR 242 | var c = columns.addColumn({ 243 | width: 26, 244 | header: 'Column C' 245 | }) 246 | ``` 247 | 248 | ### columns.column(name) 249 | Return the column object with the given name from the column set. 250 | ```javascript 251 | var b = columns.column('Column B') 252 | b.width = '30%' 253 | ``` 254 | 255 | ### columns.removeColumn(name) 256 | Remove the column with the given name from the column set. 257 | 258 | ```javascript 259 | var b = columns.column('Column B') 260 | b.width = '30%' 261 | ``` 262 | 263 | ### columns[setting] 264 | Some settings can be set dynamically after your column set has been instantiated: 265 | 266 | - `header_separator` **String|false**: Set this to any repeatable string, and it will show up under headers for columns that have headers. Set to `false` if you'd prefer not to have header separators. 267 | - `column_separator` **String|false**: Set this to any vertically repeatable string and it will show up between columns. Set to `false` if you'd prefer not to have column separators. 268 | 269 | ```javascript 270 | columns.header_separator = '_-_-' 271 | columns.column_separator = ' | ' 272 | ``` 273 | 274 | ### column.write(chunk) 275 | Write data to your column stream!! Stream data will be encoded as `utf8`. 276 | 277 | ```javascript 278 | setInterval(function(){ 279 | column.write('The Time: ' + new Date() + '\n') 280 | }, 1000) 281 | ``` 282 | 283 | ### column.clear() 284 | Clear the column's buffer and view. 285 | 286 | ```javascript 287 | column.clear() 288 | ``` 289 | 290 | ### column.remove() 291 | Remove the column and its view from the column set. 292 | 293 | ```javascript 294 | column.remove() 295 | ``` 296 | 297 | ### column[setting] 298 | Some settings can be set dynamically after your column has been instantiated: 299 | 300 | - `width` **String|Number**: Set the width of this column. This can be as a percentage (**String** with `%`) or a number of terminal characters width (**Number**). 301 | - `header` **String|false**: Specify the header title of the column. Set to `false` to show no header. 302 | 303 | ```javascript 304 | columns.header_separator = '_-_-' 305 | columns.column_separator = ' | ' 306 | ``` 307 | 308 | ## License 309 | 310 | ``` 311 | The MIT License (MIT) 312 | Copyright (c) 2014 Arjun Mehta 313 | ``` 314 | -------------------------------------------------------------------------------- /example/books.js: -------------------------------------------------------------------------------- 1 | var keypress = require('keypress'); 2 | var request = require('request'); 3 | var brake = require('brake'); 4 | 5 | 6 | var columns = require('../main').create({ 7 | overflow: 3, 8 | maximum_buffer: 300, 9 | tab_size: 2, 10 | column_separator: ' ' 11 | // flow_mode: 'reset' 12 | }); 13 | 14 | var a = columns.addColumn('SHERLOCK HOLMES', {width: '23%', wrap: true, raw: true}), 15 | b = columns.addColumn('RELATIVITY', {width: '17%', wrap: true, raw: true}), 16 | c = columns.addColumn('HUCKLEBERRY FINN', {width: '60%', raw: true}); 17 | 18 | a.write('\033[31m'); // make this column red 19 | b.write('\033[32m'); // make this column green 20 | c.write('\033[36m'); // make this column blue 21 | 22 | request('http://mirror.its.dal.ca/gutenberg/1/6/6/1661/1661.txt').pipe(brake(50)).pipe(a); 23 | request('https://archive.org/download/theeinsteintheor11335gut/11335.txt').pipe(brake(50)).pipe(b); 24 | request('http://mirror.its.dal.ca/gutenberg/7/1/0/7105/7105.txt').pipe(brake(50)).pipe(c); 25 | 26 | 27 | // exit properly so we can restore state correctly 28 | 29 | if (process.stdin.isTTY) { 30 | 31 | keypress(process.stdin); 32 | process.stdin.setRawMode(true); 33 | 34 | process.stdin.on('keypress', function(ch, key) { 35 | 36 | if (key && ((key.ctrl && key.name == 'c') || key.name == 'q')) { 37 | process.stdin.pause(); 38 | process.exit(0); 39 | } 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | var keypress = require('keypress'); 2 | 3 | var columns = require('../main').create({ 4 | margin: { 5 | top: 5, 6 | bottom: 2, 7 | right: 5, 8 | left: 5 9 | }, 10 | column_separator: ' | ', 11 | header_separator: '-_-_', 12 | flow_mode: 'reset', 13 | overflow: 3, 14 | maximum_buffer: 300, 15 | tab_size: 2, 16 | // mode: 'debug' 17 | }); 18 | 19 | var a = columns.addColumn({ 20 | wrap: true 21 | }); 22 | 23 | columns.addColumn("A2", { 24 | width: "50%", 25 | wrap: true 26 | }); 27 | 28 | columns.addColumn("A3", { 29 | width: 40, 30 | wrap: true, 31 | header: "CUSTOM HEADER" 32 | }); 33 | 34 | columns.addColumn("A4"); 35 | 36 | columns.addColumn("A5"); 37 | 38 | 39 | 40 | // random writes 41 | 42 | count = 0; 43 | var color; 44 | 45 | setInterval(function() { 46 | a.write("A" + count + "한 글\tB한 글한 글한 글한글한글한글한 글" + "\n"); 47 | }, 220); 48 | 49 | // columns.column("A2").write('\033[43m'); 50 | 51 | setInterval(function() { 52 | if (count % 1 === 0) { 53 | color = Math.round(Math.random() * 255); 54 | } 55 | columns.column("A2").write("B" + '' + count + (count % 1 === 0 ? "\033[38;5;" + (color) + 'm' : '') + randomTruncate("BB BBBBBBBBBBBBBBBBBB BBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + "\n"); 56 | count++; 57 | }, 210); 58 | 59 | 60 | setInterval(function() { 61 | columns.column("A3").write(randomTruncate("C" + count + "0 1 \t2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 ") + "\n"); 62 | }, 1000); 63 | 64 | 65 | setInterval(function() { 66 | columns.column("A4").write(randomTruncate("D" + count + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD") + "\n"); 67 | }, 500); 68 | 69 | setInterval(function() { 70 | columns.column("A5").write(randomTruncate("E" + count + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE") + "\n"); 71 | }, 200); 72 | 73 | function randomTruncate(line) { 74 | return line.substring(0, 6 + Math.random() * (line.length - 6)); 75 | } 76 | 77 | 78 | 79 | 80 | // exit properly so we can restore state correctly 81 | 82 | if (process.stdin.isTTY) { 83 | 84 | keypress(process.stdin); 85 | process.stdin.setRawMode(true); 86 | 87 | process.stdin.on('keypress', function(ch, key) { 88 | 89 | if (key && ((key.ctrl && key.name == 'c') || key.name == 'q')) { 90 | process.stdin.pause(); 91 | process.exit(0); 92 | } 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /example/lots.js: -------------------------------------------------------------------------------- 1 | var keypress = require('keypress'); 2 | var spawn = require('child_process').spawn; 3 | 4 | var columns = require('../main').create({ 5 | // mode: 'debug' 6 | flow_mode: 'reset' 7 | }); 8 | 9 | 10 | var a = columns.addColumn("Column A"); 11 | var b = columns.addColumn("Column B"); 12 | var c = columns.addColumn("Column C", { 13 | wrap: true, 14 | raw: true 15 | }); 16 | 17 | var new_process = spawn('find', ['/', '*']); 18 | // var new_process = spawn('find', ['../', 'node_modules']); 19 | new_process.stdout.pipe(a, { end: false }); 20 | new_process.stderr.pipe(a, { end: false }); 21 | 22 | new_process.stdout.pipe(b, { end: false }); 23 | new_process.stderr.pipe(b, { end: false }); 24 | 25 | new_process.stdout.pipe(c, { end: false }); 26 | new_process.stderr.pipe(c, { end: false }); 27 | 28 | new_process.on('exit', function(code) { 29 | a.write('\nProcess exited with code: ' + code + '\n'); 30 | b.write('\nProcess exited with code: ' + code + '\n'); 31 | c.write('\nProcess exited with code: ' + code + '\n'); 32 | }); 33 | new_process.on('error', function(err) { 34 | console.log("Spawn Error:", err); 35 | }); 36 | 37 | 38 | // exit the program gracefully to clean up terminal output properly 39 | 40 | if (process.stdin.isTTY) { 41 | process.stdin.setRawMode(true); 42 | keypress(process.stdin); 43 | process.stdin.on('keypress', function(ch, key) { 44 | if (key && ((key.ctrl && key.name == 'c') || key.name == 'q')) { 45 | process.exit(0); 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /example/simple.js: -------------------------------------------------------------------------------- 1 | var keypress = require('keypress'); 2 | var columns = require('../main').create({flow_mode: 'reset'}); 3 | 4 | 5 | var a = columns.addColumn("Column A"); 6 | var b = columns.addColumn("Column B"); 7 | columns.addColumn("Column C", {raw: true, wrap: true}); 8 | 9 | setInterval(function() { 10 | a.write((new Date().getSeconds() % 2 === 0) ? "TICK\n" : "TOCK\n"); 11 | b.write("The Time: " + new Date() + "\n"); 12 | }, 1000); 13 | 14 | process.stdin.setRawMode(true); 15 | process.stdin.pipe(columns.column("Column C")); 16 | columns.column("Column C").write("Try typing something..."); 17 | 18 | 19 | // exit the program gracefully to clean up terminal output properly 20 | 21 | if (process.stdin.isTTY) { 22 | 23 | keypress(process.stdin); 24 | process.stdin.on('keypress', function(ch, key) { 25 | if (key && ((key.ctrl && key.name == 'c') || key.name == 'q')) { 26 | process.exit(0); 27 | } 28 | if (key && key.name === "backspace") { 29 | columns.column("Column C").write('\b'); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /lib/Column.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var stream = require('stream'); 3 | var PassThrough = stream.PassThrough || require('readable-stream').PassThrough; 4 | 5 | var ColumnBuffer = require('./ColumnBuffer'); 6 | var ColumnStream = require('./ColumnStream'); 7 | var ColumnView = require('./ColumnView'); 8 | 9 | util.inherits(Column, PassThrough); 10 | 11 | 12 | function Column(columns, name, opts) { 13 | 14 | opts = opts || {}; 15 | this.opts = opts; 16 | 17 | PassThrough.call(this); 18 | 19 | this.name = name; 20 | this.columns = columns; 21 | 22 | this.buffer = new ColumnBuffer(this); 23 | this.stream = new ColumnStream(this); 24 | this.view = new ColumnView(this); 25 | 26 | this.displaying = true; 27 | 28 | if (opts.width) { 29 | this.width = opts.width; 30 | } 31 | } 32 | 33 | 34 | // column getter/setter properties 35 | 36 | Object.defineProperty(Column.prototype, 'width', { 37 | get: function() { 38 | return this.opts.width; 39 | }, 40 | set: function(width) { 41 | 42 | var setWidth = width; 43 | 44 | this.opts.percentage_width = undefined; 45 | this.opts.fixed_width = undefined; 46 | 47 | if (typeof width === 'string') { 48 | if (width.indexOf('%') > -1) { 49 | 50 | setWidth = width.replace(/ /g, ''); 51 | width = setWidth.replace(/\%/g, ''); 52 | this.opts.percentage_width = ~~width / 100; 53 | } else { 54 | setWidth = ~~width; 55 | } 56 | } 57 | 58 | this.opts.width = setWidth; 59 | 60 | if (typeof setWidth === 'number') { 61 | this.opts.fixed_width = setWidth; 62 | } 63 | 64 | this.columns.view.refresh(); 65 | } 66 | }); 67 | 68 | Object.defineProperty(Column.prototype, 'header', { 69 | get: function() { 70 | return this.opts.header; 71 | }, 72 | set: function(header) { 73 | this.opts.header = header; 74 | this.view.redrawHeader(); 75 | } 76 | }); 77 | 78 | 79 | // column prototype methods 80 | 81 | Column.prototype.redraw = function() { 82 | this.view.redrawAll(); 83 | }; 84 | 85 | Column.prototype.clear = function() { 86 | this.stream.data = ['']; 87 | this.view.clear(); 88 | }; 89 | 90 | Column.prototype.remove = function() { 91 | this.columns.removeColumn(this.id); 92 | }; 93 | 94 | Column.prototype.set = function(opts) { 95 | for (var option in opts) { 96 | this[option] = opts[option]; 97 | } 98 | }; 99 | 100 | Column.prototype.addColumn = function() { 101 | this.columns.addColumn.apply(null, arguments); 102 | }; 103 | 104 | 105 | module.exports = exports = Column; 106 | -------------------------------------------------------------------------------- /lib/ColumnBuffer.js: -------------------------------------------------------------------------------- 1 | var ColumnLine = require('./ColumnLine'); 2 | 3 | 4 | function ColumnBuffer(column) { 5 | 6 | this.column = column; 7 | this.max_size = column.columns.opts.maximum_buffer; 8 | 9 | this.new_lines = 0; 10 | this.current_modified = false; 11 | 12 | this.data = column.opts.raw === true ? [new ColumnLine('', {tab_size: column.columns.opts.tab_size})] : []; 13 | 14 | this.offset = 0; 15 | } 16 | 17 | 18 | // column buffer prototype getter/setter properties 19 | 20 | Object.defineProperty(ColumnBuffer.prototype, 'size', { 21 | get: function() { 22 | return this.data.length + this.offset; 23 | } 24 | }); 25 | 26 | 27 | // column buffer prototype methods 28 | 29 | ColumnBuffer.prototype.line = function(num) { 30 | 31 | if (num - this.offset < 0) { 32 | return new ColumnLine(new Array(400).join('-')); 33 | } else { 34 | return this.data[num - this.offset]; 35 | } 36 | }; 37 | 38 | ColumnBuffer.prototype.add = function(line) { 39 | 40 | this.data.push(new ColumnLine(line, { 41 | legacy: this.data[this.data.length - 1], 42 | tab_size: this.column.columns.opts.tab_size 43 | })); 44 | 45 | this.new_lines++; 46 | 47 | if (this.data.length > this.max_size) { 48 | this.data.shift(); 49 | this.offset++; 50 | } 51 | }; 52 | 53 | ColumnBuffer.prototype.newLine = function(data) { 54 | this.add(data || ''); 55 | }; 56 | 57 | ColumnBuffer.prototype.backspace = function() { 58 | this.data[this.data.length - 1].backspace(); 59 | }; 60 | 61 | ColumnBuffer.prototype.addGeneric = function(data) { 62 | 63 | var dataSplit = data.split(/\n/); 64 | 65 | this.data[this.data.length - 1].write(dataSplit.shift()); 66 | 67 | for (var i = 0; i < dataSplit.length; i++) { 68 | this.add(dataSplit[i]); 69 | } 70 | 71 | return dataSplit.length; 72 | }; 73 | 74 | ColumnBuffer.prototype.writeToCurrentLine = function(data) { 75 | this.data[this.data.length - 1].write(data); 76 | this.current_modified = true; 77 | }; 78 | 79 | ColumnBuffer.prototype.reset = function() { 80 | this.new_lines = 0; 81 | this.current_modified = false; 82 | }; 83 | 84 | 85 | module.exports = exports = ColumnBuffer; 86 | -------------------------------------------------------------------------------- /lib/ColumnLine.js: -------------------------------------------------------------------------------- 1 | var width = require('wcwidth.js'); 2 | var ANSIState = require('ansi-state'); 3 | 4 | var MainView = require('./MainView'); 5 | var color_regex = /\x1b\[[0-9;]*m/g; 6 | 7 | 8 | // column lines are very special strings. 9 | 10 | function ColumnLine(value, opts) { 11 | 12 | opts = opts || {}; 13 | 14 | this._value = value || ''; 15 | 16 | this.tab_size = opts.tab_size || 2; 17 | this.legacy_state = opts.legacy !== undefined ? opts.legacy.ansi_state : new ANSIState(); 18 | this.ansi_state = new ANSIState(this.legacy_state); 19 | this.wrap_state = null; 20 | 21 | this.codes = {}; 22 | this.codeList = []; 23 | 24 | Object.defineProperty(this, 'length', { 25 | get: function() { 26 | return this.value.length; 27 | } 28 | }); 29 | 30 | this.buildCodes(); 31 | } 32 | 33 | ColumnLine.prototype = new String; 34 | 35 | ColumnLine.prototype.toString = ColumnLine.prototype.valueOf = function() { 36 | return this._value; 37 | }; 38 | 39 | 40 | // column line prototype getter/setter properties 41 | 42 | Object.defineProperty(ColumnLine.prototype, 'width', { 43 | get: function() { 44 | return width(this.value); 45 | } 46 | }); 47 | 48 | Object.defineProperty(ColumnLine.prototype, 'value', { 49 | get: function() { 50 | return this._value; 51 | }, 52 | set: function(value) { 53 | this._value = value; 54 | this.buildCodes(); 55 | } 56 | }); 57 | 58 | 59 | // column line prototype methods 60 | 61 | ColumnLine.prototype.backspace = function() { 62 | this.value = this.slice(0, -2); 63 | return this; 64 | }; 65 | 66 | ColumnLine.prototype.write = function(data) { 67 | this.value += data; 68 | return this; 69 | }; 70 | 71 | 72 | ColumnLine.prototype.trimmed = function(to_length, wrap) { 73 | 74 | if (wrap === true) { 75 | return this.trimmedWrap(to_length); 76 | } else { 77 | return this.trimmedNoWrap(to_length); 78 | } 79 | }; 80 | 81 | ColumnLine.prototype.trimmedNoWrap = function(to_length) { 82 | 83 | var i = 0, 84 | line = (this.replaceTabs(this.codes.strippedLine) + MainView.empty_line).substring(0, to_length), 85 | truncated = line.substring(0, to_length), 86 | truncated_width = width(truncated); 87 | 88 | while (truncated_width > to_length) { 89 | i++; 90 | truncated = line.substring(0, to_length - i); 91 | truncated_width = width(truncated); 92 | } 93 | 94 | truncated = this.insertCodes(truncated, 0); 95 | 96 | return [truncated]; 97 | }; 98 | 99 | ColumnLine.prototype.trimmedWrap = function(to_length, raw_line, offset, total_length, lineArray) { 100 | 101 | lineArray = lineArray === undefined ? [] : lineArray; 102 | offset = offset === undefined ? 0 : offset; 103 | raw_line = raw_line || this.replaceTabs(this.codes.strippedLine); 104 | total_length = total_length === undefined ? raw_line.length : total_length; 105 | 106 | var i = 0, 107 | line = raw_line + MainView.empty_line, 108 | truncated = line.substring(offset, offset + to_length), 109 | truncated_width = width(truncated); 110 | 111 | while (truncated_width > to_length) { 112 | i++; 113 | truncated = line.substring(offset, offset + to_length - i); 114 | truncated_width = width(truncated); 115 | } 116 | 117 | for (var new_offset = offset + to_length - i; new_offset >= offset; new_offset--) { 118 | if ((line[new_offset - 1] !== ' ' && line[new_offset] === ' ') || (line[new_offset - 1] === ' ' && line[new_offset] !== ' ')) break; 119 | } 120 | 121 | if (new_offset > offset) { 122 | truncated = line.substring(offset, new_offset); 123 | truncated_width = width(truncated); 124 | while (line[new_offset] === ' ') { 125 | new_offset++; 126 | } 127 | } else { 128 | new_offset = offset + to_length - i; 129 | } 130 | 131 | lineArray.push(this.insertCodes(truncated, offset) + MainView.empty_line.substring(0, to_length - truncated_width)); 132 | 133 | if (new_offset < total_length) { 134 | this.trimmedWrap(to_length, raw_line, new_offset, total_length, lineArray); 135 | } 136 | 137 | return lineArray; 138 | }; 139 | 140 | ColumnLine.prototype.replaceTabs = function(line) { 141 | 142 | var j = 0, 143 | tab_size = this.tab_size; 144 | 145 | for (var i = 0; i < line.length; i++) { 146 | if (line[i] === '\t') line = spliceSlice(line, i, 1, ' '.slice(0, tab_size - (j % tab_size))); 147 | else if (line[i] === '\r') line = spliceSlice(line, i, 1, ''); 148 | j += width(line[i]); 149 | } 150 | 151 | return line; 152 | }; 153 | 154 | ColumnLine.prototype.buildCodes = function() { 155 | 156 | var idx = 0, 157 | line = this._value, 158 | codes = this.match(color_regex), 159 | codeArray = [], 160 | code, codeObj, indexOfZero; 161 | 162 | if (codes !== null) { 163 | 164 | for (var i = 0; i < codes.length; i++) { 165 | 166 | idx = line.indexOf(codes[i]); 167 | code = codes[i]; 168 | 169 | codeObj = { 170 | code: code, 171 | idx: idx 172 | }; 173 | 174 | codeArray[i] = codeObj; 175 | line = spliceSlice(line, idx, code.length); 176 | } 177 | } 178 | 179 | this.ansi_state.updateWithArray(codes); 180 | 181 | this.codes = { 182 | codeArray: codeArray, 183 | strippedLine: line 184 | }; 185 | }; 186 | 187 | ColumnLine.prototype.insertCodes = function(line, offset) { 188 | 189 | var codes = this.codes.codeArray, 190 | length = line.length, 191 | state = [], 192 | code, idx; 193 | 194 | if (offset === 0) { 195 | this.wrap_state = new ANSIState(this.legacy_state); 196 | } 197 | 198 | for (var i = codes.length - 1; i >= 0; i--) { 199 | idx = codes[i].idx; 200 | 201 | if (idx < length + offset && idx >= offset) { 202 | code = codes[i].code; 203 | state.unshift(code); 204 | line = spliceSlice(line, idx - offset, 0, code); 205 | } 206 | } 207 | 208 | line = this.wrap_state.code + line; 209 | this.wrap_state.updateWithArray(state); 210 | 211 | return line + '\u001b[0m'; 212 | }; 213 | 214 | 215 | // helper methods 216 | 217 | function spliceSlice(str, idx, count, add) { 218 | return str.slice(0, idx) + (add || '') + str.slice(idx + count); 219 | } 220 | 221 | 222 | module.exports = exports = ColumnLine; 223 | -------------------------------------------------------------------------------- /lib/ColumnStream.js: -------------------------------------------------------------------------------- 1 | var split = require('split'); 2 | var stripBom = require('strip-bom'); 3 | 4 | // strip all codes except styles. 5 | var regex = /^(?!\x1b\[[0-9;]*m)(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/g; 6 | 7 | 8 | function ColumnStream(column) { 9 | 10 | var _this = this; 11 | var new_lines = 0; 12 | 13 | this.column = column; 14 | this.column.setEncoding('utf8'); 15 | 16 | this.buffer = column.buffer; 17 | 18 | this.newline_waiting = false; 19 | this.linesync = 1; 20 | 21 | if (column.opts.raw) { 22 | this.column.on('data', function(data) { 23 | _this.parseData(cleanData(data)); 24 | }); 25 | } else { 26 | this.column.pipe(split()).on('data', function(data) { 27 | _this.buffer.newLine(cleanData(data)); 28 | }); 29 | } 30 | } 31 | 32 | 33 | // column stream prototype methods 34 | 35 | ColumnStream.prototype.parseData = function(data) { 36 | 37 | if (data === '\r') { 38 | if (this.newline_waiting === true) { 39 | this.buffer.newLine(''); 40 | } 41 | this.newline_waiting = true; 42 | } else { 43 | 44 | if (data === '\n') { 45 | this.buffer.newLine(''); 46 | } else if (data === '\b') { 47 | this.buffer.backspace(); 48 | } else if (data.match(/\n/) !== null) { 49 | this.buffer.addGeneric(data); 50 | } else if (data === '') { 51 | 52 | } else { 53 | if (this.newline_waiting === true) { 54 | this.buffer.newLine(''); 55 | } 56 | this.buffer.writeToCurrentLine(data); 57 | } 58 | 59 | this.newline_waiting = false; 60 | } 61 | }; 62 | 63 | 64 | // helper methods 65 | 66 | function cleanData(data) { 67 | return stripBom(data).replace(regex, ''); 68 | } 69 | 70 | 71 | module.exports = exports = ColumnStream; 72 | -------------------------------------------------------------------------------- /lib/ColumnView.js: -------------------------------------------------------------------------------- 1 | var heartbeats = require('heartbeats'); 2 | var ColumnLine = require('./ColumnLine'); 3 | 4 | 5 | function ColumnView(column) { 6 | 7 | var _this = this, 8 | buffer = column.buffer, 9 | buffer_new_lines = buffer.new_lines, 10 | new_lines = 0, 11 | threshold = true; 12 | 13 | this.column = column; 14 | this.stream = column.stream; 15 | this.buffer = buffer; 16 | 17 | this.main = column.columns; 18 | 19 | this.display_cache = []; 20 | this.buffer_cache = []; 21 | 22 | this.wrap = column.opts.wrap; 23 | this.flow = this.main.opts.flow; 24 | 25 | this.cursorY = 0; 26 | this.current_line = column.opts.raw === true ? 0 : -1; 27 | this.current_line_height = column.opts.raw === true ? 1 : 0; 28 | 29 | this.x = 0; 30 | this.data_top = 0; 31 | this.data_height = 0; 32 | 33 | this.header_separator = new Array(this.main.view.width).join(this.main.header_separator); 34 | 35 | heartbeats.heart('view_refresh').createEvent(1, function(heartbeat, last) { 36 | 37 | if (column.displaying === true) { 38 | 39 | buffer_new_lines = buffer.new_lines; 40 | threshold = (buffer_new_lines < _this.data_height || buffer_new_lines - new_lines < 1000 || buffer_new_lines === new_lines) ? true : false; 41 | new_lines = buffer_new_lines; 42 | 43 | if (threshold === true) { 44 | _this.renderBuffer(buffer_new_lines); 45 | buffer.reset(); 46 | } 47 | } 48 | }); 49 | } 50 | 51 | 52 | // column view prototype methods 53 | 54 | ColumnView.prototype.recalculate = function() { 55 | 56 | var headerHeight = (this.column.header ? (this.main.header_separator ? 2 : 1) : 0); 57 | 58 | this.data_height = this.main.view.height - headerHeight; 59 | this.data_top = this.main.margin.top + headerHeight; 60 | 61 | this.header_separator = new Array(this.main.view.width).join(this.main.header_separator); 62 | }; 63 | 64 | ColumnView.prototype.redrawAll = function(separator) { 65 | 66 | this.recalculate(); 67 | this.redrawHeader(); 68 | this.rebuildFrontDisplayCache(); 69 | this.rebuildEndDisplayCache(); 70 | 71 | if (separator === true) { 72 | this.redrawSeparator(); 73 | } 74 | 75 | this.redraw(); 76 | }; 77 | 78 | ColumnView.prototype.redraw = function() { 79 | 80 | if (this.buffer.size > 0) { 81 | if (this.flow === true) { 82 | this.redrawDataFlow(0); 83 | } else { 84 | this.redrawDataEffeciently(); 85 | } 86 | } 87 | }; 88 | 89 | ColumnView.prototype.redrawHeader = function() { 90 | 91 | if (this.column.header) { 92 | cursorPos(this.x, this.main.margin.top + 0); 93 | writeToScreen(new ColumnLine(this.column.header).trimmed(this.width)[0]); 94 | 95 | 96 | if (this.main.header_separator) { 97 | cursorPos(this.x, this.main.margin.top + 1); 98 | writeToScreen(this.header_separator.substring(0, this.width)); 99 | } 100 | } 101 | }; 102 | 103 | ColumnView.prototype.redrawSeparator = function() { 104 | 105 | var column_separator = this.main.column_separator; 106 | 107 | if (column_separator) { 108 | var x = this.x - column_separator.length; 109 | 110 | for (var i = 0; i < this.main.view.height; i++) { 111 | cursorPos(x, this.main.margin.top + i); 112 | writeToScreen(column_separator); 113 | } 114 | } 115 | }; 116 | 117 | ColumnView.prototype.redrawDataFlow = function(new_lines) { 118 | 119 | this.current_line += new_lines; 120 | this.rebuildFrontDisplayCache(); 121 | 122 | if (this.display_cache.length < this.data_height - 1) { 123 | this.fillDown(0, this.data_height - 1, 0); 124 | } else { 125 | this.fillUp(this.data_height - 1, 0, this.buffer.size - 1); 126 | } 127 | }; 128 | 129 | ColumnView.prototype.redrawDataEffeciently = function() { 130 | 131 | if (this.buffer_cache.length > this.data_height) { 132 | this.clearToOverflow(); 133 | } else { 134 | this.clear(); 135 | this.fillUpUsingBuffer(this.buffer_cache.length, 0); 136 | } 137 | }; 138 | 139 | ColumnView.prototype.renderBuffer = function(new_lines) { 140 | 141 | if (this.flow === true) { 142 | if (new_lines > 0) { 143 | this.redrawDataFlow(new_lines); 144 | } else if (this.buffer.current_modified === true) { 145 | this.redrawCurrentLineFlow(); 146 | } 147 | } else { 148 | if (this.buffer.current_modified === true) { 149 | this.redrawCurrentLine(); 150 | } 151 | if (new_lines > 0) { 152 | this.printNewLines(new_lines); 153 | } 154 | } 155 | }; 156 | 157 | ColumnView.prototype.rebuildFrontDisplayCache = function() { 158 | 159 | this.display_cache = []; 160 | 161 | for (var i = 0; i < this.current_line; i++) { 162 | this.display_cache = this.display_cache.concat(this.buffer.line(i).trimmed(this.width, this.wrap)); 163 | if (this.display_cache.length > this.data_height) { 164 | break; 165 | } 166 | } 167 | }; 168 | 169 | ColumnView.prototype.rebuildEndDisplayCache = function() { 170 | 171 | this.buffer_cache = []; 172 | 173 | for (var i = this.current_line; i >= 0; i--) { 174 | this.buffer_cache = this.buffer_cache.concat(this.buffer.line(i).trimmed(this.width, this.wrap).reverse()); 175 | if (this.buffer_cache.length > this.data_height) { 176 | break; 177 | } 178 | } 179 | }; 180 | 181 | ColumnView.prototype.fillUpUsingBuffer = function(from, to) { 182 | 183 | var length = from - to > this.buffer_cache.length ? this.buffer_cache.length : from - to; 184 | from = length - to; 185 | 186 | for (var i = 0; i < length; i++) { 187 | this.cursorY = from - i - 1; 188 | this.printLineDirect(this.buffer_cache[i]); 189 | } 190 | 191 | this.cursorY = from; 192 | }; 193 | 194 | ColumnView.prototype.fillUp = function(from, to, buffer_line) { 195 | 196 | var line_array = this.buffer.line(buffer_line).trimmed(this.width, this.wrap), 197 | i = from, 198 | j = line_array.length - 1, 199 | dest = 0; 200 | 201 | 202 | while (i >= to) { 203 | 204 | this.cursorY = i; 205 | this.printLine(line_array[j], false); 206 | j--; 207 | 208 | if (j < dest) { 209 | buffer_line--; 210 | line_array = this.buffer.line(buffer_line).trimmed(this.width, this.wrap); 211 | j = line_array.length - 1; 212 | dest = 0; 213 | } 214 | 215 | i--; 216 | } 217 | 218 | this.cursorY = from + 1; 219 | }; 220 | 221 | ColumnView.prototype.fillDown = function(from, to, buffer_line) { 222 | 223 | var line_array = this.buffer.line(buffer_line).trimmed(this.width, this.wrap), 224 | i = from, 225 | j = 0, 226 | dest = line_array.length - 1; 227 | 228 | while (i <= to) { 229 | 230 | this.cursorY = i; 231 | this.printLine(line_array[j], false); 232 | j++; 233 | 234 | if (j > dest) { 235 | 236 | buffer_line++; 237 | 238 | if (buffer_line > this.buffer.size - 1) break; 239 | 240 | line_array = this.buffer.line(buffer_line).trimmed(this.width, this.wrap); 241 | j = 0; 242 | dest = line_array.length - 1; 243 | } 244 | 245 | i++; 246 | } 247 | }; 248 | 249 | ColumnView.prototype.clear = function() { 250 | 251 | var blank_line = this.main.view.empty_line.substring(0, this.width); 252 | 253 | this.cursorY = 0; 254 | for (var i = 0; i < this.data_height; i++) { 255 | this.printLineDirect(blank_line); 256 | } 257 | this.cursorY = 0; 258 | }; 259 | 260 | ColumnView.prototype.clearFrom = function(from, num_lines) { 261 | 262 | var blank_line = this.main.view.empty_line.substring(0, this.width); 263 | 264 | for (var i = 0; i < num_lines; i++) { 265 | this.cursorY = from - i; 266 | this.printLineDirect(blank_line); 267 | } 268 | this.cursorY = from - num_lines; 269 | }; 270 | 271 | ColumnView.prototype.checkOverflow = function() { 272 | 273 | if (this.cursorY + 1 > this.data_height) { 274 | this.clearToOverflow(); 275 | } 276 | }; 277 | 278 | ColumnView.prototype.clearToOverflow = function() { 279 | this.clear(); 280 | this.fillUpUsingBuffer(this.main.overflow, 0); 281 | }; 282 | 283 | ColumnView.prototype.printNewLines = function(num_lines) { 284 | 285 | if (num_lines > this.data_height) { 286 | this.current_line += num_lines; 287 | this.fillUp(this.data_height - 1, 0, this.current_line); 288 | } else { 289 | for (var i = 0; i < num_lines; i++) { 290 | this.current_line++; 291 | this.printBufferLine(this.current_line); 292 | } 293 | } 294 | }; 295 | 296 | ColumnView.prototype.printBufferLine = function(buffer_index) { 297 | 298 | var line_array = this.buffer.line(buffer_index).trimmed(this.width, this.wrap); 299 | this.current_line_height = line_array.length; 300 | 301 | for (var i = 0; i < line_array.length; i++) { 302 | this.printLine(line_array[i]); 303 | } 304 | }; 305 | 306 | ColumnView.prototype.clearCurrentLine = function() { 307 | this.clearFrom(this.cursorY, this.current_line_height); 308 | for (var i = 0; i < this.current_line_height; i++) { 309 | this.buffer_cache.shift(); 310 | } 311 | }; 312 | 313 | ColumnView.prototype.redrawCurrentLine = function() { 314 | this.clearCurrentLine(); 315 | this.printBufferLine(this.current_line); 316 | }; 317 | 318 | ColumnView.prototype.redrawCurrentLineFlow = function() { 319 | 320 | var line_array = this.buffer.line(this.current_line).trimmed(this.width, this.wrap); 321 | 322 | if (this.current_line_height < line_array.length) { 323 | this.redrawDataFlow(0); 324 | return; 325 | } 326 | 327 | this.clearCurrentLine(); 328 | this.current_line_height = line_array.length; 329 | 330 | for (var i = 0; i < line_array.length; i++) { 331 | this.printLine(line_array[i]); 332 | } 333 | }; 334 | 335 | ColumnView.prototype.printLine = function(line) { 336 | 337 | if (this.flow === false) { 338 | this.checkOverflow(); 339 | } 340 | 341 | this.addToBufferCache(line); 342 | this.printLineDirect(line); 343 | }; 344 | 345 | ColumnView.prototype.printLineDirect = function(line) { 346 | 347 | this.dataCursorPos(this.cursorY); 348 | writeToScreen(line); 349 | this.cursorY++; 350 | }; 351 | 352 | ColumnView.prototype.addToBufferCache = function(line) { 353 | 354 | this.buffer_cache.unshift(line); 355 | 356 | if (this.buffer_cache.length > this.data_height) { 357 | this.buffer_cache.pop(); 358 | } 359 | }; 360 | 361 | ColumnView.prototype.dataCursorPos = function(y) { 362 | cursorPos(this.x, this.cursorY + this.data_top); 363 | }; 364 | 365 | 366 | // helper methods 367 | 368 | function cursorPos(x, y) { 369 | process.stdout.cursorTo(x, y); 370 | } 371 | 372 | function writeToScreen(line) { 373 | process.stdout.write(line); 374 | } 375 | 376 | 377 | module.exports = exports = ColumnView; 378 | -------------------------------------------------------------------------------- /lib/MainView.js: -------------------------------------------------------------------------------- 1 | var empty_line; 2 | 3 | if (process.stdout.isTTY) { 4 | empty_line = new Array(process.stdout.columns).join(' '); 5 | process.stdout.on('resize', function() { 6 | empty_line = new Array(process.stdout.columns).join(' '); 7 | }); 8 | } else { 9 | empty_line = new Array(500).join(' '); 10 | } 11 | 12 | 13 | function MainView(columns, mode, print) { 14 | 15 | this.columns = columns; 16 | 17 | this.tty_columns = process.stdout.columns; 18 | this.tty_rows = process.stdout.rows; 19 | this.empty_line = empty_line; 20 | 21 | trackStdout(this); 22 | displayMode(this, mode, print); 23 | } 24 | 25 | Object.defineProperty(MainView, 'empty_line', { 26 | get: function() { 27 | return empty_line; 28 | } 29 | }); 30 | 31 | 32 | // main view getter/setter properties 33 | 34 | Object.defineProperty(MainView.prototype, 'width', { 35 | get: function() { 36 | return this.tty_columns - this.columns.opts.margin.left - this.columns.opts.margin.right; 37 | } 38 | }); 39 | 40 | Object.defineProperty(MainView.prototype, 'height', { 41 | get: function() { 42 | return this.tty_rows - this.columns.opts.margin.top - this.columns.opts.margin.bottom; 43 | } 44 | }); 45 | 46 | Object.defineProperty(MainView.prototype, 'empty_line', { 47 | get: function() { 48 | return empty_line; 49 | } 50 | }); 51 | 52 | 53 | // main view prototype methods 54 | 55 | MainView.prototype.refresh = function() { 56 | 57 | this.tty_columns = process.stdout.columns; 58 | this.tty_rows = process.stdout.rows; 59 | this.empty_line = new Array(this.tty_columns + 1).join(' '); 60 | process.stdout.write('\033[2J'); 61 | 62 | this.buildColumnArray(); 63 | this.calculateColumnWidths(); 64 | }; 65 | 66 | MainView.prototype.buildColumnArray = function() { 67 | 68 | this.column_view_array = []; 69 | 70 | for (var column_name in this.columns.columns) { 71 | if (this.columns.columns[column_name] !== undefined) { 72 | this.columns.columns[column_name].displaying = false; 73 | this.column_view_array.push(this.columns.columns[column_name]); 74 | } 75 | } 76 | }; 77 | 78 | MainView.prototype.calculateColumnWidths = function() { 79 | 80 | var columns = this.columns; 81 | 82 | var column, column_width, 83 | separator_size = columns.column_separator.length, 84 | total_width = this.width, 85 | number_of_column_views = this.column_view_array.length, 86 | flex_columns = [], 87 | percentage_columns = [], 88 | min_width = 5, 89 | number_of_flex_columns = 0, 90 | flex_column_width, 91 | extra, 92 | i; 93 | 94 | var available_width = total_width - (separator_size * (number_of_column_views - 1)); 95 | var main_width = available_width; 96 | var separator_percentage = separator_size / total_width; 97 | var total_separator_percentage = separator_percentage * (number_of_column_views - 1); 98 | var percentage_left = 1 - total_separator_percentage; 99 | 100 | for (i = 0; i < this.column_view_array.length; i++) { 101 | 102 | column = this.column_view_array[i]; 103 | column_width = 0; 104 | 105 | if (column.opts.percentage_width) { 106 | 107 | column_width = Math.floor(column.opts.percentage_width * percentage_left * main_width); 108 | column.view.width = column_width; 109 | percentage_columns.push(column); 110 | 111 | } else if (column.opts.fixed_width) { 112 | 113 | column_width = column.opts.fixed_width; 114 | column.view.width = column_width; 115 | 116 | } else { 117 | 118 | flex_columns.push(column); 119 | 120 | } 121 | 122 | available_width -= column_width; 123 | } 124 | 125 | number_of_flex_columns = flex_columns.length; 126 | 127 | if (available_width - (number_of_flex_columns * min_width) < 0) { 128 | this.column_view_array.pop(); 129 | this.calculateColumnWidths(); 130 | return; 131 | } 132 | 133 | if (number_of_flex_columns > 0) { 134 | 135 | flex_column_width = Math.floor(available_width / number_of_flex_columns); 136 | extra = available_width - (flex_column_width * number_of_flex_columns); 137 | 138 | for (i = 0; i < number_of_flex_columns; i++) { 139 | flex_columns[i].view.width = flex_column_width + (extra > 0 ? 1 : 0); 140 | extra--; 141 | } 142 | 143 | } else if (percentage_columns.length > 0) { 144 | 145 | var percentage_column_extra = Math.floor(available_width / percentage_columns.length); 146 | extra = available_width - (percentage_column_extra * percentage_columns.length); 147 | for (i = 0; i < percentage_columns.length; i++) { 148 | percentage_columns[i].view.width = percentage_columns[i].view.width + percentage_column_extra + (extra > 0 ? 1 : 0); 149 | extra--; 150 | } 151 | } 152 | 153 | this.calculateColumnPositions(); 154 | }; 155 | 156 | MainView.prototype.calculateColumnPositions = function() { 157 | 158 | if (this.column_view_array.length > 0) { 159 | 160 | this.column_view_array[0].view.x = this.columns.margin.left; 161 | 162 | for (var i = 1; i < this.column_view_array.length; i++) { 163 | this.column_view_array[i].view.x = this.column_view_array[i - 1].view.x + this.column_view_array[i - 1].view.width + this.columns.column_separator.length; 164 | } 165 | 166 | this.redraw(); 167 | } 168 | }; 169 | 170 | MainView.prototype.redraw = function() { 171 | 172 | for (var i = 0; i < this.column_view_array.length; i++) { 173 | this.column_view_array[i].displaying = true; 174 | this.column_view_array[i].view.redrawAll(i !== 0 ? true : false); 175 | } 176 | }; 177 | 178 | 179 | // add a listener for tty resizes 180 | 181 | function trackStdout(view) { 182 | 183 | if (process.stdout.isTTY) { 184 | process.stdout.on('resize', function() { 185 | view.refresh(); 186 | }); 187 | } 188 | } 189 | 190 | 191 | // set view modes on start/end 192 | 193 | function displayMode(main, mode, print) { 194 | 195 | var printer; 196 | var buffer; 197 | 198 | if (mode !== 'debug') { 199 | 200 | process.stdout.write('\033[?25l\033[?1049h\033[H'); 201 | process.on('exit', function() { 202 | process.stdout.write('\033[?25h\033[?1049l'); 203 | 204 | if (print) { 205 | for (var columnName in main.columns.columns) { 206 | 207 | buffer = main.columns.columns[columnName].buffer; 208 | console.log('========================================================================'); 209 | console.log(main.columns.columns[columnName].header) 210 | console.log('========================================================================'); 211 | 212 | for (var i = buffer.offset; i < buffer.size; i++) { 213 | console.log(buffer.line(i).toString()); 214 | } 215 | process.stdout.write('\033[0m'); 216 | } 217 | } 218 | }); 219 | } 220 | } 221 | 222 | 223 | module.exports = exports = MainView; 224 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // node-columns 2 | // MIT © Arjun Mehta 3 | // www.arjunmehta.net 4 | 5 | 6 | var heartbeats = require('heartbeats'); 7 | heartbeats.createHeart(Math.round(1000 / 24), 'view_refresh'); 8 | 9 | var MainView = require('./lib/MainView'); 10 | var Column = require('./lib/Column'); 11 | 12 | var idCount = 0; 13 | 14 | 15 | function create(opts) { 16 | return new Columns(opts); 17 | } 18 | 19 | function Columns(opts) { 20 | 21 | opts = opts || {}; 22 | 23 | opts.margin = opts.margin || { 24 | top: 0, 25 | right: 0, 26 | bottom: 0, 27 | left: 0 28 | }; 29 | 30 | opts.flow = opts.flow_mode === 'reset' ? false : true; 31 | opts.overflow = opts.overflow !== undefined ? opts.overflow : 3; 32 | opts.maximum_buffer = opts.maximum_buffer || 500; 33 | opts.tab_size = opts.tab_size || 2; 34 | opts.print = opts.print || false; 35 | 36 | this.opts = opts; 37 | this.view = new MainView(this, opts.mode, opts.print); 38 | this.columns = {}; 39 | } 40 | 41 | 42 | // core getter/setter properties 43 | 44 | Object.defineProperty(Columns.prototype, 'flow_mode', { 45 | get: function() { 46 | return this.opts.flow === false ? 'reset' : 'push'; 47 | }, 48 | set: function(flow) { 49 | this.opts.flow = flow === 'reset' ? false : true; 50 | } 51 | }); 52 | 53 | Object.defineProperty(Columns.prototype, 'overflow', { 54 | get: function() { 55 | return this.opts.overflow; 56 | }, 57 | set: function(overflow) { 58 | this.opts.overflow = overflow; 59 | } 60 | }); 61 | 62 | Object.defineProperty(Columns.prototype, 'margin', { 63 | get: function() { 64 | return this.opts.margin; 65 | }, 66 | set: function(margin) { 67 | this.opts.margin = margin; 68 | this.view.refresh(); 69 | } 70 | }); 71 | 72 | Object.defineProperty(Columns.prototype, 'header_separator', { 73 | get: function() { 74 | return this.opts.header_separator !== undefined ? this.opts.header_separator : '_'; 75 | }, 76 | set: function(header_separator) { 77 | this.opts.header_separator = header_separator; 78 | this.view.refresh(); 79 | } 80 | }); 81 | 82 | Object.defineProperty(Columns.prototype, 'column_separator', { 83 | get: function() { 84 | return this.opts.column_separator !== undefined ? this.opts.column_separator : ' '; 85 | }, 86 | set: function(column_separator) { 87 | this.opts.column_separator = column_separator; 88 | this.view.refresh(); 89 | } 90 | }); 91 | 92 | 93 | // core prototype methods 94 | 95 | Columns.prototype.redraw = function() { 96 | this.view.refresh(); 97 | }; 98 | 99 | Columns.prototype.column = function(name) { 100 | return this.columns[name]; 101 | }; 102 | 103 | Columns.prototype.addColumn = function(name, opts) { 104 | 105 | if (typeof name === "object" && opts === undefined) { 106 | opts = name; 107 | name = undefined; 108 | } 109 | 110 | opts = opts || {}; 111 | opts.header = opts.header !== undefined ? opts.header : name; 112 | 113 | name = name || 'column_' + (Math.random()).toString(36) + idCount++; 114 | this.columns[name] = new Column(this, name, opts); 115 | this.view.refresh(); 116 | 117 | return this.columns[name]; 118 | }; 119 | 120 | Columns.prototype.removeColumn = function(name) { 121 | this.columns[name] = undefined; 122 | this.view.refresh(); 123 | }; 124 | 125 | 126 | module.exports = exports = { 127 | create: create 128 | }; 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "columns", 3 | "version": "0.8.0", 4 | "description": "Stream your text streams into streams of column streams.", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "nodeunit test/test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/arjunmehta/node-columns.git" 12 | }, 13 | "devDependencies": { 14 | "brake": "~1.0.1", 15 | "keypress": "~0.2.1", 16 | "nodeunit": "~0.8.8", 17 | "request": "~2.51.0" 18 | }, 19 | "keywords": [ 20 | "streams", 21 | "columns", 22 | "column", 23 | "view", 24 | "terminal", 25 | "pipe", 26 | "stream" 27 | ], 28 | "author": "Arjun Mehta", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/arjunmehta/node-columns/issues" 32 | }, 33 | "homepage": "https://github.com/arjunmehta/node-columns", 34 | "dependencies": { 35 | "ansi-state": "~1.0.5", 36 | "heartbeats": "~3.1.0", 37 | "split": "~0.3.2", 38 | "strip-bom": "~1.0.0", 39 | "wcwidth.js": "~1.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var columns = require('../main').create({ 2 | mode: "debug" 3 | }); 4 | var ColumnLine = require('../lib/ColumnLine'); 5 | 6 | 7 | exports['Exported Properly'] = function(test) { 8 | test.expect(2); 9 | 10 | test.equal(typeof columns, 'object'); 11 | test.equal(typeof columns.addColumn, 'function'); 12 | 13 | test.done(); 14 | }; 15 | 16 | exports['Column Line Code and Trim Support'] = function(test) { 17 | test.expect(3); 18 | 19 | var legacy_line = new ColumnLine("\033[32;33;35;31;37;32;33;38;5;24;35;31;37;31mHi there! How are you Today?"); 20 | var line = new ColumnLine("Hi there! \033[32mHow are you Today?", { 21 | legacy: legacy_line 22 | }); 23 | 24 | test.equal(typeof line, 'object'); 25 | test.equal(line.trimmed(40).length, 1); 26 | 27 | test.equal(line.trimmed(40)[0], "\u001b[31mHi there! \u001b[32mHow are you Today? \u001b[0m"); 28 | 29 | test.done(); 30 | }; 31 | 32 | exports['Column Line Wrap'] = function(test) { 33 | test.expect(3); 34 | 35 | var line = new ColumnLine("BB BBBBBB BBBBBBB BBBBB BBBBBBB BBBBBBBBBBB BBBBBB BBBBBBB BBBBBBBB"); 36 | 37 | test.equal(line.trimmed(10, true).length, 8); 38 | test.equal(line.trimmed(10, true)[0], '\u001b[mBB BBBBBB \u001b[0m'); 39 | test.equal(line.trimmed(10, true)[4], '\u001b[mBBBBBBBBBB\u001b[0m'); 40 | 41 | test.done(); 42 | }; 43 | 44 | exports['tearDown'] = function(done) { 45 | done(); 46 | }; 47 | --------------------------------------------------------------------------------