├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── color.js ├── index.js ├── license ├── package-lock.json ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.json] 12 | indent_size = 2 13 | indent_style = space 14 | 15 | [*.md] 16 | indent_style = space 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | - 14 15 | - 12 16 | - 10 17 | os: 18 | - ubuntu-latest 19 | - macos-latest 20 | - windows-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | 5 | *.log 6 | .*.swp 7 | .npmignore 8 | -------------------------------------------------------------------------------- /color.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const columns = require('.'); 3 | 4 | // prettier-ignore 5 | const values = [ 6 | 'blue' + chalk.bgBlue('berry'), 7 | '笔菠萝' + chalk.yellow('苹果笔'), 8 | chalk.red('apple'), 'pomegranate', 9 | 'durian', chalk.green('star fruit'), 10 | 'パイナップル', 'apricot', 'banana', 11 | 'pineapple', chalk.bgRed.yellow('orange') 12 | ]; 13 | 14 | console.log(''); 15 | console.log(columns(values)); 16 | console.log(''); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stringWidth = require('string-width'); 4 | const stripAnsi = require('strip-ansi'); 5 | 6 | const concat = Array.prototype.concat; 7 | const defaults = { 8 | character: ' ', 9 | newline: '\n', 10 | padding: 2, 11 | sort: true, 12 | width: 0, 13 | }; 14 | 15 | function byPlainText(a, b) { 16 | const plainA = stripAnsi(a); 17 | const plainB = stripAnsi(b); 18 | 19 | if (plainA === plainB) { 20 | return 0; 21 | } 22 | 23 | if (plainA > plainB) { 24 | return 1; 25 | } 26 | 27 | return -1; 28 | } 29 | 30 | function makeArray() { 31 | return []; 32 | } 33 | 34 | function makeList(count) { 35 | return Array.apply(null, Array(count)); 36 | } 37 | 38 | function padCell(fullWidth, character, value) { 39 | const valueWidth = stringWidth(value); 40 | const filler = makeList(fullWidth - valueWidth + 1); 41 | 42 | return value + filler.join(character); 43 | } 44 | 45 | function toRows(rows, cell, i) { 46 | rows[i % rows.length].push(cell); 47 | 48 | return rows; 49 | } 50 | 51 | function toString(arr) { 52 | return arr.join(''); 53 | } 54 | 55 | function columns(values, options) { 56 | values = concat.apply([], values); 57 | options = Object.assign({}, defaults, options); 58 | 59 | let cells = values.filter(Boolean).map(String); 60 | 61 | if (options.sort !== false) { 62 | cells = cells.sort(byPlainText); 63 | } 64 | 65 | const termWidth = options.width || process.stdout.columns; 66 | const cellWidth = 67 | Math.max.apply(null, cells.map(stringWidth)) + options.padding; 68 | const columnCount = Math.floor(termWidth / cellWidth) || 1; 69 | const rowCount = Math.ceil(cells.length / columnCount) || 1; 70 | 71 | if (columnCount === 1) { 72 | return cells.join(options.newline); 73 | } 74 | 75 | return cells 76 | .map(padCell.bind(null, cellWidth, options.character)) 77 | .reduce(toRows, makeList(rowCount).map(makeArray)) 78 | .map(toString) 79 | .join(options.newline); 80 | } 81 | 82 | module.exports = columns; 83 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Shannon Moeller (shannonmoeller.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli-columns", 3 | "version": "4.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-regex": { 8 | "version": "5.0.1", 9 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 10 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 11 | }, 12 | "ansi-styles": { 13 | "version": "4.3.0", 14 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 15 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 16 | "dev": true, 17 | "requires": { 18 | "color-convert": "^2.0.1" 19 | } 20 | }, 21 | "chalk": { 22 | "version": "4.1.2", 23 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 24 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 25 | "dev": true, 26 | "requires": { 27 | "ansi-styles": "^4.1.0", 28 | "supports-color": "^7.1.0" 29 | } 30 | }, 31 | "color-convert": { 32 | "version": "2.0.1", 33 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 34 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 35 | "dev": true, 36 | "requires": { 37 | "color-name": "~1.1.4" 38 | } 39 | }, 40 | "color-name": { 41 | "version": "1.1.4", 42 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 43 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 44 | "dev": true 45 | }, 46 | "emoji-regex": { 47 | "version": "8.0.0", 48 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 49 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 50 | }, 51 | "has-flag": { 52 | "version": "4.0.0", 53 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 54 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 55 | "dev": true 56 | }, 57 | "is-fullwidth-code-point": { 58 | "version": "3.0.0", 59 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 60 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 61 | }, 62 | "string-width": { 63 | "version": "4.2.3", 64 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 65 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 66 | "requires": { 67 | "emoji-regex": "^8.0.0", 68 | "is-fullwidth-code-point": "^3.0.0", 69 | "strip-ansi": "^6.0.1" 70 | } 71 | }, 72 | "strip-ansi": { 73 | "version": "6.0.1", 74 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 75 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 76 | "requires": { 77 | "ansi-regex": "^5.0.1" 78 | } 79 | }, 80 | "supports-color": { 81 | "version": "7.2.0", 82 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 83 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 84 | "dev": true, 85 | "requires": { 86 | "has-flag": "^4.0.0" 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli-columns", 3 | "version": "4.0.0", 4 | "description": "Columnated lists for the CLI.", 5 | "scripts": { 6 | "lint": "npx eslint --fix \"*.js\" && npx prettier --write \"*.js\"", 7 | "test": "node test.js && node color.js", 8 | "cover": "npx c8 npm test" 9 | }, 10 | "keywords": [ 11 | "ansi", 12 | "cli", 13 | "column", 14 | "columnate", 15 | "columns", 16 | "grid", 17 | "list", 18 | "log", 19 | "ls", 20 | "row", 21 | "rows", 22 | "unicode", 23 | "unix" 24 | ], 25 | "author": "Shannon Moeller (http://shannonmoeller.com)", 26 | "homepage": "https://github.com/shannonmoeller/cli-columns#readme", 27 | "repository": "shannonmoeller/cli-columns", 28 | "license": "MIT", 29 | "main": "index.js", 30 | "files": [ 31 | "*.js" 32 | ], 33 | "dependencies": { 34 | "string-width": "^4.2.3", 35 | "strip-ansi": "^6.0.1" 36 | }, 37 | "devDependencies": { 38 | "chalk": "^4.1.2" 39 | }, 40 | "engines": { 41 | "node": ">= 10" 42 | }, 43 | "eslintConfig": { 44 | "extends": "eslint:recommended", 45 | "env": { 46 | "node": true 47 | }, 48 | "parserOptions": { 49 | "ecmaVersion": 8 50 | } 51 | }, 52 | "prettier": { 53 | "singleQuote": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `cli-columns` 2 | 3 | [![NPM version][npm-img]][npm-url] [![Downloads][downloads-img]][npm-url] 4 | 5 | Columnated lists for the CLI. Unicode and ANSI safe. 6 | 7 | ## Install 8 | 9 | $ npm install --save cli-columns 10 | 11 | ## Usage 12 | 13 | ```js 14 | const columns = require('cli-columns'); 15 | const chalk = require('chalk'); 16 | 17 | const values = [ 18 | 'blue' + chalk.bgBlue('berry'), 19 | '笔菠萝' + chalk.yellow('苹果笔'), 20 | chalk.red('apple'), 'pomegranate', 21 | 'durian', chalk.green('star fruit'), 22 | 'パイナップル', 'apricot', 'banana', 23 | 'pineapple', chalk.bgRed.yellow('orange') 24 | ]; 25 | 26 | console.log(columns(values)); 27 | ``` 28 | 29 | screenshot 30 | 31 | ## API 32 | 33 | ### columns(values [, options]): String 34 | 35 | - `values` `{Array}` Array of strings to display. 36 | - `options` `{Object}` 37 | - `character` `{String}` (default: `' '`) Padding character. 38 | - `newline` `{String}` (default: `'\n'`) Newline character. 39 | - `padding` `{Number}` (default: `2`) Space between columns. 40 | - `sort` `{Boolean}` (default: `true`) Whether to sort results. 41 | - `width` `{Number}` (default: `process.stdout.columns`) Max width of list. 42 | 43 | Sorts and formats a list of values into columns suitable to display in a given width. 44 | 45 | ## Contribute 46 | 47 | Standards for this project, including tests, code coverage, and semantics are enforced with a build tool. Pull requests must include passing tests with 100% code coverage and no linting errors. 48 | 49 | ### Test 50 | 51 | $ npm test 52 | 53 | ---- 54 | 55 | MIT © [Shannon Moeller](http://shannonmoeller.com) 56 | 57 | [downloads-img]: http://img.shields.io/npm/dm/cli-columns.svg?style=flat-square 58 | [npm-img]: http://img.shields.io/npm/v/cli-columns.svg?style=flat-square 59 | [npm-url]: https://npmjs.org/package/cli-columns 60 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const chalk = require('chalk'); 5 | const stripAnsi = require('strip-ansi'); 6 | const columns = require('./index.js'); 7 | const tests = []; 8 | 9 | function test(msg, fn) { 10 | tests.push([msg, fn]); 11 | } 12 | 13 | process.nextTick(async function run() { 14 | for (const [msg, fn] of tests) { 15 | try { 16 | await fn(assert); 17 | console.log(`pass - ${msg}`); 18 | } catch (error) { 19 | console.error(`fail - ${msg}`, error); 20 | process.exit(1); 21 | } 22 | } 23 | }); 24 | 25 | // prettier-ignore 26 | test('should print one column list', t => { 27 | const cols = columns(['foo', ['bar', 'baz'], ['bar', 'qux']], { 28 | width: 1 29 | }); 30 | 31 | const expected = 32 | 'bar\n' + 33 | 'bar\n' + 34 | 'baz\n' + 35 | 'foo\n' + 36 | 'qux'; 37 | 38 | t.equal(cols, expected); 39 | }); 40 | 41 | // prettier-ignore 42 | test('should print three column list', t => { 43 | const cols = columns(['foo', ['bar', 'baz'], ['bat', 'qux']], { 44 | width: 16 45 | }); 46 | 47 | const expected = 48 | 'bar baz qux \n' + 49 | 'bat foo '; 50 | 51 | t.equal(cols, expected); 52 | }); 53 | 54 | // prettier-ignore 55 | test('should print complex list', t => { 56 | const cols = columns( 57 | [ 58 | 'foo', 'bar', 'baz', 59 | chalk.cyan('嶜憃撊') + ' 噾噿嚁', 60 | 'blue' + chalk.bgBlue('berry'), 61 | chalk.red('apple'), 'pomegranate', 62 | 'durian', chalk.green('star fruit'), 63 | 'apricot', 'banana pineapple' 64 | ], 65 | { 66 | width: 80 67 | } 68 | ); 69 | 70 | const expected = 71 | 'apple bar durian star fruit \n' + 72 | 'apricot baz foo 嶜憃撊 噾噿嚁 \n' + 73 | 'banana pineapple blueberry pomegranate '; 74 | 75 | t.equal(stripAnsi(cols), expected); 76 | }); 77 | 78 | // prettier-ignore 79 | test('should optionally not sort', t => { 80 | const cols = columns( 81 | [ 82 | 'foo', 'bar', 'baz', 83 | chalk.cyan('嶜憃撊') + ' 噾噿嚁', 84 | 'blue' + chalk.bgBlue('berry'), 85 | chalk.red('apple'), 'pomegranate', 86 | 'durian', chalk.green('star fruit'), 87 | 'apricot', 'banana pineapple' 88 | ], 89 | { 90 | sort: false, 91 | width: 80 92 | } 93 | ); 94 | 95 | const expected = 96 | 'foo 嶜憃撊 噾噿嚁 pomegranate apricot \n' + 97 | 'bar blueberry durian banana pineapple \n' + 98 | 'baz apple star fruit '; 99 | 100 | t.equal(stripAnsi(cols), expected); 101 | }); 102 | --------------------------------------------------------------------------------