├── .coveralls.yml ├── .eslintrc ├── .github └── workflows │ ├── ci.yaml │ └── release-please.yml ├── .gitignore ├── .nycrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── deno.ts ├── index.mjs ├── lib └── index.ts ├── package.json ├── renovate.json ├── rollup.config.js ├── screenshot.png ├── test ├── cliui.mjs └── deno │ └── cliui-test.ts ├── tsconfig.json └── tsconfig.test.json /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: NiRhyj91Z2vtgob6XdEAqs83rzNnbMZUu 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.ts", 5 | "parser": "@typescript-eslint/parser", 6 | "rules": { 7 | "no-unused-vars": "off", 8 | "no-useless-constructor": "off", 9 | "@typescript-eslint/no-unused-vars": "error", 10 | "@typescript-eslint/no-useless-constructor": "error" 11 | } 12 | } 13 | ], 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint/eslint-plugin" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | types: [ assigned, opened, synchronize, reopened, labeled ] 7 | name: ci 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node: [20, 22] 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: node --version 20 | - run: npm install 21 | env: 22 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 23 | - run: npm test 24 | - run: npm run check 25 | windows: 26 | runs-on: windows-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: 22 32 | - run: npm install 33 | env: 34 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 35 | - run: npm test 36 | coverage: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: 22 43 | - run: npm install 44 | env: 45 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 46 | - run: npm test 47 | - run: npm run coverage 48 | deno: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v2 52 | - uses: actions/setup-node@v1 53 | with: 54 | node-version: 22 55 | - run: npm install 56 | - run: npm run compile 57 | - uses: denolib/setup-deno@v2 58 | with: 59 | deno-version: v1.x 60 | - run: | 61 | deno --version 62 | deno test test/deno/cliui-test.ts 63 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: bcoe/release-please-action@v3 11 | id: release 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | release-type: node 15 | package-name: cliui 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 14 20 | - run: npm install 21 | env: 22 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 23 | - run: npm run compile 24 | - name: push Deno release 25 | run: | 26 | git config user.name github-actions[bot] 27 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 28 | git remote add gh-token "https://${{ secrets.GITHUB_TOKEN}}@github.com/yargs/cliui.git" 29 | git checkout -b deno 30 | git add -f build 31 | git commit -a -m 'chore: ${{ steps.release.outputs.tag_name }} release' 32 | git push origin +deno 33 | git tag -a ${{ steps.release.outputs.tag_name }}-deno -m 'chore: ${{ steps.release.outputs.tag_name }} release' 34 | git push origin ${{ steps.release.outputs.tag_name }}-deno 35 | if: ${{ steps.release.outputs.release_created }} 36 | - uses: actions/setup-node@v1 37 | with: 38 | node-version: 14 39 | registry-url: 'https://external-dot-oss-automation.appspot.com/' 40 | if: ${{ steps.release.outputs.release_created }} 41 | - run: npm install 42 | if: ${{ steps.release.outputs.release_created }} 43 | - run: npm publish 44 | env: 45 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 46 | if: ${{ steps.release.outputs.release_created }} 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .nyc_output 4 | package-lock.json 5 | coverage 6 | build 7 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "build/test/**", 4 | "test/**" 5 | ], 6 | "reporter": [ 7 | "html", 8 | "text" 9 | ], 10 | "lines": 99.0, 11 | "branches": "95", 12 | "statements": "99.0" 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [9.0.1](https://github.com/yargs/cliui/compare/v9.0.0...v9.0.1) (2025-03-17) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * make require("cliui") work as expected for CJS ([04ccc25](https://github.com/yargs/cliui/commit/04ccc250e30a059292c03fa1ef0a8661f8d93dfe)) 11 | 12 | ## [9.0.0](https://github.com/yargs/cliui/compare/v8.0.1...v9.0.0) (2025-03-16) 13 | 14 | 15 | ### ⚠ BREAKING CHANGES 16 | 17 | * cliui is now ESM only ([#165](https://github.com/yargs/cliui/issues/165)) 18 | 19 | ### Features 20 | 21 | * cliui is now ESM only ([#165](https://github.com/yargs/cliui/issues/165)) ([5a521de](https://github.com/yargs/cliui/commit/5a521de7ea88f262236394c8d96775bcf50ff0a4)) 22 | 23 | ## [8.0.1](https://github.com/yargs/cliui/compare/v8.0.0...v8.0.1) (2022-10-01) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **deps:** move rollup-plugin-ts to dev deps ([#124](https://github.com/yargs/cliui/issues/124)) ([7c8bd6b](https://github.com/yargs/cliui/commit/7c8bd6ba024d61e4eeae310c7959ab8ab6829081)) 29 | 30 | ## [8.0.0](https://github.com/yargs/cliui/compare/v7.0.4...v8.0.0) (2022-09-30) 31 | 32 | 33 | ### ⚠ BREAKING CHANGES 34 | 35 | * **deps:** drop Node 10 to release CVE-2021-3807 patch (#122) 36 | 37 | ### Bug Fixes 38 | 39 | * **deps:** drop Node 10 to release CVE-2021-3807 patch ([#122](https://github.com/yargs/cliui/issues/122)) ([f156571](https://github.com/yargs/cliui/commit/f156571ce4f2ebf313335e3a53ad905589da5a30)) 40 | 41 | ### [7.0.4](https://www.github.com/yargs/cliui/compare/v7.0.3...v7.0.4) (2020-11-08) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * **deno:** import UIOptions from definitions ([#97](https://www.github.com/yargs/cliui/issues/97)) ([f04f343](https://www.github.com/yargs/cliui/commit/f04f3439bc78114c7e90f82ff56f5acf16268ea8)) 47 | 48 | ### [7.0.3](https://www.github.com/yargs/cliui/compare/v7.0.2...v7.0.3) (2020-10-16) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **exports:** node 13.0 and 13.1 require the dotted object form _with_ a string fallback ([#93](https://www.github.com/yargs/cliui/issues/93)) ([eca16fc](https://www.github.com/yargs/cliui/commit/eca16fc05d26255df3280906c36d7f0e5b05c6e9)) 54 | 55 | ### [7.0.2](https://www.github.com/yargs/cliui/compare/v7.0.1...v7.0.2) (2020-10-14) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * **exports:** node 13.0-13.6 require a string fallback ([#91](https://www.github.com/yargs/cliui/issues/91)) ([b529d7e](https://www.github.com/yargs/cliui/commit/b529d7e432901af1af7848b23ed6cf634497d961)) 61 | 62 | ### [7.0.1](https://www.github.com/yargs/cliui/compare/v7.0.0...v7.0.1) (2020-08-16) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * **build:** main should be build/index.cjs ([dc29a3c](https://www.github.com/yargs/cliui/commit/dc29a3cc617a410aa850e06337b5954b04f2cb4d)) 68 | 69 | ## [7.0.0](https://www.github.com/yargs/cliui/compare/v6.0.0...v7.0.0) (2020-08-16) 70 | 71 | 72 | ### ⚠ BREAKING CHANGES 73 | 74 | * tsc/ESM/Deno support (#82) 75 | * modernize deps and build (#80) 76 | 77 | ### Build System 78 | 79 | * modernize deps and build ([#80](https://www.github.com/yargs/cliui/issues/80)) ([339d08d](https://www.github.com/yargs/cliui/commit/339d08dc71b15a3928aeab09042af94db2f43743)) 80 | 81 | 82 | ### Code Refactoring 83 | 84 | * tsc/ESM/Deno support ([#82](https://www.github.com/yargs/cliui/issues/82)) ([4b777a5](https://www.github.com/yargs/cliui/commit/4b777a5fe01c5d8958c6708695d6aab7dbe5706c)) 85 | 86 | ## [6.0.0](https://www.github.com/yargs/cliui/compare/v5.0.0...v6.0.0) (2019-11-10) 87 | 88 | 89 | ### ⚠ BREAKING CHANGES 90 | 91 | * update deps, drop Node 6 92 | 93 | ### Code Refactoring 94 | 95 | * update deps, drop Node 6 ([62056df](https://www.github.com/yargs/cliui/commit/62056df)) 96 | 97 | ## [5.0.0](https://github.com/yargs/cliui/compare/v4.1.0...v5.0.0) (2019-04-10) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * Update wrap-ansi to fix compatibility with latest versions of chalk. ([#60](https://github.com/yargs/cliui/issues/60)) ([7bf79ae](https://github.com/yargs/cliui/commit/7bf79ae)) 103 | 104 | 105 | ### BREAKING CHANGES 106 | 107 | * Drop support for node < 6. 108 | 109 | 110 | 111 | 112 | ## [4.1.0](https://github.com/yargs/cliui/compare/v4.0.0...v4.1.0) (2018-04-23) 113 | 114 | 115 | ### Features 116 | 117 | * add resetOutput method ([#57](https://github.com/yargs/cliui/issues/57)) ([7246902](https://github.com/yargs/cliui/commit/7246902)) 118 | 119 | 120 | 121 | 122 | ## [4.0.0](https://github.com/yargs/cliui/compare/v3.2.0...v4.0.0) (2017-12-18) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * downgrades strip-ansi to version 3.0.1 ([#54](https://github.com/yargs/cliui/issues/54)) ([5764c46](https://github.com/yargs/cliui/commit/5764c46)) 128 | * set env variable FORCE_COLOR. ([#56](https://github.com/yargs/cliui/issues/56)) ([7350e36](https://github.com/yargs/cliui/commit/7350e36)) 129 | 130 | 131 | ### Chores 132 | 133 | * drop support for node < 4 ([#53](https://github.com/yargs/cliui/issues/53)) ([b105376](https://github.com/yargs/cliui/commit/b105376)) 134 | 135 | 136 | ### Features 137 | 138 | * add fallback for window width ([#45](https://github.com/yargs/cliui/issues/45)) ([d064922](https://github.com/yargs/cliui/commit/d064922)) 139 | 140 | 141 | ### BREAKING CHANGES 142 | 143 | * officially drop support for Node < 4 144 | 145 | 146 | 147 | 148 | ## [3.2.0](https://github.com/yargs/cliui/compare/v3.1.2...v3.2.0) (2016-04-11) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * reduces tarball size ([acc6c33](https://github.com/yargs/cliui/commit/acc6c33)) 154 | 155 | ### Features 156 | 157 | * adds standard-version for release management ([ff84e32](https://github.com/yargs/cliui/commit/ff84e32)) 158 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software 4 | for any purpose with or without fee is hereby granted, provided 5 | that the above copyright notice and this permission notice 6 | appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE 11 | LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES 12 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 13 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 14 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cliui 2 | 3 | ![ci](https://github.com/yargs/cliui/workflows/ci/badge.svg) 4 | [![NPM version](https://img.shields.io/npm/v/cliui.svg)](https://www.npmjs.com/package/cliui) 5 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 6 | ![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/cliui) 7 | 8 | easily create complex multi-column command-line-interfaces. 9 | 10 | ## Example 11 | 12 | ```bash 13 | npm i cliui@latest chalk@latest 14 | ``` 15 | 16 | ```js 17 | const ui = require('cliui')() 18 | const {Chalk} = require('chalk'); 19 | const chalk = new Chalk(); 20 | 21 | ui.div('Usage: $0 [command] [options]') 22 | 23 | ui.div({ 24 | text: 'Options:', 25 | padding: [2, 0, 1, 0] 26 | }) 27 | 28 | ui.div( 29 | { 30 | text: "-f, --file", 31 | width: 20, 32 | padding: [0, 4, 0, 4] 33 | }, 34 | { 35 | text: "the file to load." + 36 | chalk.green("(if this description is long it wraps).") 37 | , 38 | width: 20 39 | }, 40 | { 41 | text: chalk.red("[required]"), 42 | align: 'right' 43 | } 44 | ) 45 | 46 | console.log(ui.toString()) 47 | ``` 48 | 49 | ## Deno/ESM Support 50 | 51 | As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and 52 | [ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules): 53 | 54 | ```typescript 55 | import cliui from "cliui"; 56 | import chalk from "chalk"; 57 | // Deno: import cliui from "https://deno.land/x/cliui/deno.ts"; 58 | 59 | const ui = cliui({}) 60 | 61 | ui.div('Usage: $0 [command] [options]') 62 | 63 | ui.div({ 64 | text: 'Options:', 65 | padding: [2, 0, 1, 0] 66 | }) 67 | 68 | ui.div( 69 | { 70 | text: "-f, --file", 71 | width: 20, 72 | padding: [0, 4, 0, 4] 73 | }, 74 | { 75 | text: "the file to load." + 76 | chalk.green("(if this description is long it wraps).") 77 | , 78 | width: 20 79 | }, 80 | { 81 | text: chalk.red("[required]"), 82 | align: 'right' 83 | } 84 | ) 85 | 86 | console.log(ui.toString()) 87 | ``` 88 | 89 | 90 | 91 | ## Layout DSL 92 | 93 | cliui exposes a simple layout DSL: 94 | 95 | If you create a single `ui.div`, passing a string rather than an 96 | object: 97 | 98 | * `\n`: characters will be interpreted as new rows. 99 | * `\t`: characters will be interpreted as new columns. 100 | * `\s`: characters will be interpreted as padding. 101 | 102 | **as an example...** 103 | 104 | ```js 105 | var ui = require('./')({ 106 | width: 60 107 | }) 108 | 109 | ui.div( 110 | 'Usage: node ./bin/foo.js\n' + 111 | ' \t provide a regex\n' + 112 | ' \t provide a glob\t [required]' 113 | ) 114 | 115 | console.log(ui.toString()) 116 | ``` 117 | 118 | **will output:** 119 | 120 | ```shell 121 | Usage: node ./bin/foo.js 122 | provide a regex 123 | provide a glob [required] 124 | ``` 125 | 126 | ## Methods 127 | 128 | ```js 129 | cliui = require('cliui') 130 | ``` 131 | 132 | ### cliui({width: integer}) 133 | 134 | Specify the maximum width of the UI being generated. 135 | If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`. 136 | 137 | ### cliui({wrap: boolean}) 138 | 139 | Enable or disable the wrapping of text in a column. 140 | 141 | ### cliui.div(column, column, column) 142 | 143 | Create a row with any number of columns, a column 144 | can either be a string, or an object with the following 145 | options: 146 | 147 | * **text:** some text to place in the column. 148 | * **width:** the width of a column. 149 | * **align:** alignment, `right` or `center`. 150 | * **padding:** `[top, right, bottom, left]`. 151 | * **border:** should a border be placed around the div? 152 | 153 | ### cliui.span(column, column, column) 154 | 155 | Similar to `div`, except the next row will be appended without 156 | a new line being created. 157 | 158 | ### cliui.resetOutput() 159 | 160 | Resets the UI elements of the current cliui instance, maintaining the values 161 | set for `width` and `wrap`. 162 | -------------------------------------------------------------------------------- /deno.ts: -------------------------------------------------------------------------------- 1 | // Bootstrap cliui with CommonJS dependencies: 2 | import { cliui, UI } from './build/lib/index.js' 3 | import type { UIOptions } from './build/lib/index.d.ts' 4 | import stringWidth from 'string-width' 5 | import stripAnsi from 'strip-ansi' 6 | import wrapAnsi from 'wrap-ansi' 7 | 8 | export default function ui (opts: UIOptions): UI { 9 | return cliui(opts, { 10 | stringWidth, 11 | stripAnsi, 12 | wrap: wrapAnsi 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | // Bootstrap cliui with CommonJS dependencies: 2 | import { cliui } from './build/lib/index.js' 3 | import stringWidth from 'string-width' 4 | import stripAnsi from 'strip-ansi' 5 | import wrapAnsi from 'wrap-ansi' 6 | 7 | export default function ui (opts) { 8 | return cliui(opts, { 9 | stringWidth, 10 | stripAnsi, 11 | wrap: wrapAnsi 12 | }) 13 | } 14 | 15 | export {ui as 'module.exports'}; 16 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const align = { 4 | right: alignRight, 5 | center: alignCenter 6 | } 7 | 8 | const top = 0 9 | const right = 1 10 | const bottom = 2 11 | const left = 3 12 | 13 | export interface UIOptions { 14 | width: number; 15 | wrap?: boolean; 16 | rows?: string[]; 17 | } 18 | 19 | interface Column { 20 | text: string; 21 | width?: number; 22 | align?: 'right'|'left'|'center', 23 | padding: number[], 24 | border?: boolean; 25 | } 26 | 27 | interface ColumnArray extends Array { 28 | span: boolean; 29 | } 30 | 31 | interface Line { 32 | hidden?: boolean; 33 | text: string; 34 | span?: boolean; 35 | } 36 | 37 | interface Mixin { 38 | stringWidth: Function; 39 | stripAnsi: Function; 40 | wrap: Function; 41 | } 42 | 43 | export class UI { 44 | width: number; 45 | wrap: boolean; 46 | rows: ColumnArray[]; 47 | 48 | constructor (opts: UIOptions) { 49 | this.width = opts.width 50 | this.wrap = opts.wrap ?? true 51 | this.rows = [] 52 | } 53 | 54 | span (...args: ColumnArray) { 55 | const cols = this.div(...args) 56 | cols.span = true 57 | } 58 | 59 | resetOutput () { 60 | this.rows = [] 61 | } 62 | 63 | div (...args: (Column|string)[]): ColumnArray { 64 | if (args.length === 0) { 65 | this.div('') 66 | } 67 | 68 | if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') { 69 | return this.applyLayoutDSL(args[0]) 70 | } 71 | 72 | const cols = args.map(arg => { 73 | if (typeof arg === 'string') { 74 | return this.colFromString(arg) 75 | } 76 | return arg 77 | }) as ColumnArray 78 | 79 | this.rows.push(cols) 80 | return cols 81 | } 82 | 83 | private shouldApplyLayoutDSL (...args: (Column|string)[]): boolean { 84 | return args.length === 1 && typeof args[0] === 'string' && 85 | /[\t\n]/.test(args[0]) 86 | } 87 | 88 | private applyLayoutDSL (str: string): ColumnArray { 89 | const rows = str.split('\n').map(row => row.split('\t')) 90 | let leftColumnWidth = 0 91 | 92 | // simple heuristic for layout, make sure the 93 | // second column lines up along the left-hand. 94 | // don't allow the first column to take up more 95 | // than 50% of the screen. 96 | rows.forEach(columns => { 97 | if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) { 98 | leftColumnWidth = Math.min( 99 | Math.floor(this.width * 0.5), 100 | mixin.stringWidth(columns[0]) 101 | ) 102 | } 103 | }) 104 | 105 | // generate a table: 106 | // replacing ' ' with padding calculations. 107 | // using the algorithmically generated width. 108 | rows.forEach(columns => { 109 | this.div(...columns.map((r, i) => { 110 | return { 111 | text: r.trim(), 112 | padding: this.measurePadding(r), 113 | width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined 114 | } as Column 115 | })) 116 | }) 117 | 118 | return this.rows[this.rows.length - 1] 119 | } 120 | 121 | private colFromString (text: string): Column { 122 | return { 123 | text, 124 | padding: this.measurePadding(text) 125 | } 126 | } 127 | 128 | private measurePadding (str: string): number[] { 129 | // measure padding without ansi escape codes 130 | const noAnsi = mixin.stripAnsi(str) 131 | return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length] 132 | } 133 | 134 | toString (): string { 135 | const lines: Line[] = [] 136 | 137 | this.rows.forEach(row => { 138 | this.rowToString(row, lines) 139 | }) 140 | 141 | // don't display any lines with the 142 | // hidden flag set. 143 | return lines 144 | .filter(line => !line.hidden) 145 | .map(line => line.text) 146 | .join('\n') 147 | } 148 | 149 | rowToString (row: ColumnArray, lines: Line[]) { 150 | this.rasterize(row).forEach((rrow, r) => { 151 | let str = '' 152 | rrow.forEach((col: string, c: number) => { 153 | const { width } = row[c] // the width with padding. 154 | const wrapWidth = this.negatePadding(row[c]) // the width without padding. 155 | 156 | let ts = col // temporary string used during alignment/padding. 157 | 158 | if (wrapWidth > mixin.stringWidth(col)) { 159 | ts += ' '.repeat(wrapWidth - mixin.stringWidth(col)) 160 | } 161 | 162 | // align the string within its column. 163 | if (row[c].align && row[c].align !== 'left' && this.wrap) { 164 | const fn = align[(row[c].align as 'right'|'center')] 165 | ts = fn(ts, wrapWidth) 166 | if (mixin.stringWidth(ts) < wrapWidth) { 167 | ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1) 168 | } 169 | } 170 | 171 | // apply border and padding to string. 172 | const padding = row[c].padding || [0, 0, 0, 0] 173 | if (padding[left]) { 174 | str += ' '.repeat(padding[left]) 175 | } 176 | 177 | str += addBorder(row[c], ts, '| ') 178 | str += ts 179 | str += addBorder(row[c], ts, ' |') 180 | if (padding[right]) { 181 | str += ' '.repeat(padding[right]) 182 | } 183 | 184 | // if prior row is span, try to render the 185 | // current row on the prior line. 186 | if (r === 0 && lines.length > 0) { 187 | str = this.renderInline(str, lines[lines.length - 1]) 188 | } 189 | }) 190 | 191 | // remove trailing whitespace. 192 | lines.push({ 193 | text: str.replace(/ +$/, ''), 194 | span: row.span 195 | }) 196 | }) 197 | 198 | return lines 199 | } 200 | 201 | // if the full 'source' can render in 202 | // the target line, do so. 203 | private renderInline (source: string, previousLine: Line) { 204 | const match = source.match(/^ */) 205 | const leadingWhitespace = match ? match[0].length : 0 206 | const target = previousLine.text 207 | const targetTextWidth = mixin.stringWidth(target.trimRight()) 208 | 209 | if (!previousLine.span) { 210 | return source 211 | } 212 | 213 | // if we're not applying wrapping logic, 214 | // just always append to the span. 215 | if (!this.wrap) { 216 | previousLine.hidden = true 217 | return target + source 218 | } 219 | 220 | if (leadingWhitespace < targetTextWidth) { 221 | return source 222 | } 223 | 224 | previousLine.hidden = true 225 | 226 | return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft() 227 | } 228 | 229 | private rasterize (row: ColumnArray) { 230 | const rrows: string[][] = [] 231 | const widths = this.columnWidths(row) 232 | let wrapped 233 | 234 | // word wrap all columns, and create 235 | // a data-structure that is easy to rasterize. 236 | row.forEach((col, c) => { 237 | // leave room for left and right padding. 238 | col.width = widths[c] 239 | if (this.wrap) { 240 | wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n') 241 | } else { 242 | wrapped = col.text.split('\n') 243 | } 244 | 245 | if (col.border) { 246 | wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.') 247 | wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'") 248 | } 249 | 250 | // add top and bottom padding. 251 | if (col.padding) { 252 | wrapped.unshift(...new Array(col.padding[top] || 0).fill('')) 253 | wrapped.push(...new Array(col.padding[bottom] || 0).fill('')) 254 | } 255 | 256 | wrapped.forEach((str: string, r: number) => { 257 | if (!rrows[r]) { 258 | rrows.push([]) 259 | } 260 | 261 | const rrow = rrows[r] 262 | 263 | for (let i = 0; i < c; i++) { 264 | if (rrow[i] === undefined) { 265 | rrow.push('') 266 | } 267 | } 268 | 269 | rrow.push(str) 270 | }) 271 | }) 272 | 273 | return rrows 274 | } 275 | 276 | private negatePadding (col: Column) { 277 | let wrapWidth = col.width || 0 278 | if (col.padding) { 279 | wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0) 280 | } 281 | 282 | if (col.border) { 283 | wrapWidth -= 4 284 | } 285 | 286 | return wrapWidth 287 | } 288 | 289 | private columnWidths (row: ColumnArray) { 290 | if (!this.wrap) { 291 | return row.map(col => { 292 | return col.width || mixin.stringWidth(col.text) 293 | }) 294 | } 295 | 296 | let unset = row.length 297 | let remainingWidth = this.width 298 | 299 | // column widths can be set in config. 300 | const widths = row.map(col => { 301 | if (col.width) { 302 | unset-- 303 | remainingWidth -= col.width 304 | return col.width 305 | } 306 | 307 | return undefined 308 | }) 309 | 310 | // any unset widths should be calculated. 311 | const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0 312 | 313 | return widths.map((w, i) => { 314 | if (w === undefined) { 315 | return Math.max(unsetWidth, _minWidth(row[i])) 316 | } 317 | 318 | return w 319 | }) 320 | } 321 | } 322 | 323 | function addBorder (col: Column, ts: string, style: string) { 324 | if (col.border) { 325 | if (/[.']-+[.']/.test(ts)) { 326 | return '' 327 | } 328 | 329 | if (ts.trim().length !== 0) { 330 | return style 331 | } 332 | 333 | return ' ' 334 | } 335 | 336 | return '' 337 | } 338 | 339 | // calculates the minimum width of 340 | // a column, based on padding preferences. 341 | function _minWidth (col: Column) { 342 | const padding = col.padding || [] 343 | const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0) 344 | if (col.border) { 345 | return minWidth + 4 346 | } 347 | 348 | return minWidth 349 | } 350 | 351 | function getWindowWidth (): number { 352 | /* c8 ignore next 5: depends on terminal */ 353 | if (typeof process === 'object' && process.stdout && process.stdout.columns) { 354 | return process.stdout.columns 355 | } 356 | return 80 357 | } 358 | 359 | function alignRight (str: string, width: number): string { 360 | str = str.trim() 361 | const strWidth = mixin.stringWidth(str) 362 | 363 | if (strWidth < width) { 364 | return ' '.repeat(width - strWidth) + str 365 | } 366 | 367 | return str 368 | } 369 | 370 | function alignCenter (str: string, width: number): string { 371 | str = str.trim() 372 | const strWidth = mixin.stringWidth(str) 373 | 374 | /* c8 ignore next 3 */ 375 | if (strWidth >= width) { 376 | return str 377 | } 378 | 379 | return ' '.repeat((width - strWidth) >> 1) + str 380 | } 381 | 382 | let mixin: Mixin 383 | export function cliui (opts: Partial, _mixin: Mixin) { 384 | mixin = _mixin 385 | return new UI({ 386 | width: opts?.width || getWindowWidth(), 387 | wrap: opts?.wrap 388 | }) 389 | } 390 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cliui", 3 | "version": "9.0.1", 4 | "description": "easily create complex multi-column command-line-interfaces", 5 | "main": "build/index.mjs", 6 | "exports": { 7 | ".": "./index.mjs" 8 | }, 9 | "type": "module", 10 | "module": "./index.mjs", 11 | "scripts": { 12 | "check": "standardx '**/*.ts' && standardx '**/*.js'", 13 | "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js'", 14 | "pretest": "rimraf build && tsc -p tsconfig.test.json", 15 | "test": "c8 mocha ./test/*.mjs", 16 | "postest": "check", 17 | "coverage": "c8 report --check-coverage", 18 | "precompile": "rimraf build", 19 | "compile": "tsc", 20 | "prepare": "npm run compile" 21 | }, 22 | "repository": "yargs/cliui", 23 | "standard": { 24 | "ignore": [ 25 | "**/example/**" 26 | ], 27 | "globals": [ 28 | "it" 29 | ] 30 | }, 31 | "keywords": [ 32 | "cli", 33 | "command-line", 34 | "layout", 35 | "design", 36 | "console", 37 | "wrap", 38 | "table" 39 | ], 40 | "author": "Ben Coe ", 41 | "license": "ISC", 42 | "dependencies": { 43 | "string-width": "^7.2.0", 44 | "strip-ansi": "^7.1.0", 45 | "wrap-ansi": "^9.0.0" 46 | }, 47 | "devDependencies": { 48 | "@types/node": "^22.13.10", 49 | "@typescript-eslint/eslint-plugin": "^4.0.0", 50 | "@typescript-eslint/parser": "^4.0.0", 51 | "c8": "^10.1.3", 52 | "chai": "^5.2.0", 53 | "chalk": "^5.4.1", 54 | "cross-env": "^7.0.2", 55 | "eslint": "^7.6.0", 56 | "eslint-plugin-import": "^2.22.0", 57 | "eslint-plugin-n": "^14.0.0", 58 | "gts": "^6.0.2", 59 | "mocha": "^11.1.0", 60 | "rimraf": "^6.0.1", 61 | "standardx": "^7.0.0", 62 | "typescript": "^5.8.2" 63 | }, 64 | "files": [ 65 | "build", 66 | "index.mjs", 67 | "!*.d.ts" 68 | ], 69 | "engines": { 70 | "node": ">=20" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "pinVersions": false, 6 | "rebaseStalePrs": true, 7 | "gitAuthor": null 8 | } 9 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import ts from 'rollup-plugin-ts' 2 | 3 | const output = { 4 | format: 'cjs', 5 | file: './build/index.cjs', 6 | exports: 'default' 7 | } 8 | 9 | if (process.env.NODE_ENV === 'test') output.sourcemap = true 10 | 11 | export default { 12 | input: './lib/cjs.ts', 13 | output, 14 | plugins: [ 15 | ts({ /* options */ }) 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yargs/cliui/2737977df41e728bd8c7d3ca0658498273cecce5/screenshot.png -------------------------------------------------------------------------------- /test/cliui.mjs: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import cliui from '../index.mjs' 3 | import stripAnsi from 'strip-ansi' 4 | import { should } from 'chai' 5 | 6 | /* global describe, it */ 7 | should() 8 | 9 | // Force chalk to enable color, if it's disabled the test fails. 10 | process.env.FORCE_COLOR = 1 11 | 12 | describe('cliui', () => { 13 | describe('resetOutput', () => { 14 | it('should set lines to empty', () => { 15 | const ui = cliui() 16 | ui.div('i am a value that would be in a line') 17 | ui.resetOutput() 18 | ui.toString().length.should.be.equal(0) 19 | }) 20 | }) 21 | 22 | describe('div', () => { 23 | it("wraps text at 'width' if a single column is given", () => { 24 | const ui = cliui({ 25 | width: 10 26 | }) 27 | 28 | ui.div('i am a string that should be wrapped') 29 | 30 | ui.toString().split('\n').forEach(row => { 31 | row.length.should.be.lte(10) 32 | }) 33 | }) 34 | 35 | it('evenly divides text across columns if multiple columns are given', () => { 36 | const ui = cliui({ 37 | width: 40 38 | }) 39 | 40 | ui.div( 41 | { text: 'i am a string that should be wrapped', width: 15 }, 42 | 'i am a second string that should be wrapped', 43 | 'i am a third string that should be wrapped' 44 | ) 45 | 46 | // total width of all columns is <= 47 | // the width cliui is initialized with. 48 | ui.toString().split('\n').forEach(row => { 49 | row.length.should.be.lte(40) 50 | }) 51 | 52 | // it should wrap each column appropriately. 53 | const expected = [ 54 | 'i am a string i am a i am a third', 55 | 'that should be second string that', 56 | 'wrapped string that should be', 57 | ' should be wrapped', 58 | ' wrapped' 59 | ] 60 | 61 | ui.toString().split('\n').should.eql(expected) 62 | }) 63 | 64 | it('allows for a blank row to be appended', () => { 65 | const ui = cliui({ 66 | width: 40 67 | }) 68 | 69 | ui.div() 70 | 71 | // it should wrap each column appropriately. 72 | const expected = [''] 73 | 74 | ui.toString().split('\n').should.eql(expected) 75 | }) 76 | }) 77 | 78 | describe('_columnWidths', () => { 79 | it('uses same width for each column by default', () => { 80 | const ui = cliui({ 81 | width: 40 82 | }) 83 | const widths = ui.columnWidths([{}, {}, {}]) 84 | 85 | widths[0].should.equal(13) 86 | widths[1].should.equal(13) 87 | widths[2].should.equal(13) 88 | }) 89 | 90 | it('divides width over remaining columns if first column has width specified', () => { 91 | const ui = cliui({ 92 | width: 40 93 | }) 94 | const widths = ui.columnWidths([{ width: 20 }, {}, {}]) 95 | 96 | widths[0].should.equal(20) 97 | widths[1].should.equal(10) 98 | widths[2].should.equal(10) 99 | }) 100 | 101 | it('divides width over remaining columns if middle column has width specified', () => { 102 | const ui = cliui({ 103 | width: 40 104 | }) 105 | const widths = ui.columnWidths([{}, { width: 10 }, {}]) 106 | 107 | widths[0].should.equal(15) 108 | widths[1].should.equal(10) 109 | widths[2].should.equal(15) 110 | }) 111 | 112 | it('keeps track of remaining width if multiple columns have width specified', () => { 113 | const ui = cliui({ 114 | width: 40 115 | }) 116 | const widths = ui.columnWidths([{ width: 20 }, { width: 12 }, {}]) 117 | 118 | widths[0].should.equal(20) 119 | widths[1].should.equal(12) 120 | widths[2].should.equal(8) 121 | }) 122 | 123 | it('uses a sane default if impossible widths are specified', () => { 124 | const ui = cliui({ 125 | width: 40 126 | }) 127 | const widths = ui.columnWidths([{ width: 30 }, { width: 30 }, { padding: [0, 2, 0, 1] }]) 128 | 129 | widths[0].should.equal(30) 130 | widths[1].should.equal(30) 131 | widths[2].should.equal(4) 132 | }) 133 | }) 134 | 135 | describe('alignment', () => { 136 | it('allows a column to be right aligned', () => { 137 | const ui = cliui({ 138 | width: 40 139 | }) 140 | 141 | ui.div( 142 | 'i am a string', 143 | { text: 'i am a second string', align: 'right' }, 144 | 'i am a third string that should be wrapped' 145 | ) 146 | 147 | // it should right-align the second column. 148 | const expected = [ 149 | 'i am a stringi am a secondi am a third', 150 | ' stringstring that', 151 | ' should be', 152 | ' wrapped' 153 | ] 154 | 155 | ui.toString().split('\n').should.eql(expected) 156 | }) 157 | 158 | it('allows a column to be center aligned', () => { 159 | const ui = cliui({ 160 | width: 60 161 | }) 162 | 163 | ui.div( 164 | 'i am a string', 165 | { text: 'i am a second string', align: 'center', padding: [0, 2, 0, 2] }, 166 | 'i am a third string that should be wrapped' 167 | ) 168 | 169 | // it should right-align the second column. 170 | const expected = [ 171 | 'i am a string i am a second i am a third string', 172 | ' string that should be', 173 | ' wrapped' 174 | ] 175 | 176 | ui.toString().split('\n').should.eql(expected) 177 | }) 178 | }) 179 | 180 | describe('padding', () => { 181 | it('handles left/right padding', () => { 182 | const ui = cliui({ 183 | width: 40 184 | }) 185 | 186 | ui.div( 187 | { text: 'i have padding on my left', padding: [0, 0, 0, 4] }, 188 | { text: 'i have padding on my right', padding: [0, 2, 0, 0], align: 'center' }, 189 | { text: 'i have no padding', padding: [0, 0, 0, 0] } 190 | ) 191 | 192 | // it should add left/right padding to columns. 193 | const expected = [ 194 | ' i have i have i have no', 195 | ' padding padding on padding', 196 | ' on my my right', 197 | ' left' 198 | ] 199 | 200 | ui.toString().split('\n').should.eql(expected) 201 | }) 202 | 203 | it('handles top/bottom padding', () => { 204 | const ui = cliui({ 205 | width: 40 206 | }) 207 | 208 | ui.div( 209 | 'i am a string', 210 | { text: 'i am a second string', padding: [2, 0, 0, 0] }, 211 | { text: 'i am a third string that should be wrapped', padding: [0, 0, 1, 0] } 212 | ) 213 | 214 | // it should add top/bottom padding to second 215 | // and third columns. 216 | const expected = [ 217 | 'i am a string i am a third', 218 | ' string that', 219 | ' i am a secondshould be', 220 | ' string wrapped', 221 | '' 222 | ] 223 | 224 | ui.toString().split('\n').should.eql(expected) 225 | }) 226 | 227 | it('preserves leading whitespace as padding', () => { 228 | const ui = cliui() 229 | 230 | ui.div(' LEADING WHITESPACE') 231 | ui.div('\u001b[34m with ansi\u001b[39m') 232 | 233 | const expected = [ 234 | ' LEADING WHITESPACE', 235 | ' with ansi' 236 | ] 237 | 238 | ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) 239 | }) 240 | }) 241 | 242 | describe('border', () => { 243 | it('allows a border to be placed around a div', () => { 244 | const ui = cliui({ 245 | width: 40 246 | }) 247 | 248 | ui.div( 249 | { text: 'i am a first string', padding: [0, 0, 0, 0], border: true }, 250 | { text: 'i am a second string', padding: [1, 0, 0, 0], border: true } 251 | ) 252 | 253 | const expected = [ 254 | '.------------------.', 255 | '| i am a first |.------------------.', 256 | '| string || i am a second |', 257 | "'------------------'| string |", 258 | " '------------------'" 259 | ] 260 | 261 | ui.toString().split('\n').should.eql(expected) 262 | }) 263 | }) 264 | 265 | describe('wrap', () => { 266 | it('allows wordwrap to be disabled', () => { 267 | const ui = cliui({ 268 | wrap: false 269 | }) 270 | 271 | ui.div( 272 | { text: 'i am a string', padding: [0, 1, 0, 0] }, 273 | { text: 'i am a second string', padding: [0, 2, 0, 0] }, 274 | { text: 'i am a third string that should not be wrapped', padding: [0, 0, 0, 2] } 275 | ) 276 | 277 | ui.toString().should.equal('i am a string i am a second string i am a third string that should not be wrapped') 278 | }) 279 | }) 280 | 281 | describe('span', () => { 282 | it('appends the next row to the end of the prior row if it fits', () => { 283 | const ui = cliui({ 284 | width: 40 285 | }) 286 | 287 | ui.span( 288 | { text: 'i am a string that will be wrapped', width: 30 } 289 | ) 290 | 291 | ui.div( 292 | { text: ' [required] [default: 99]', align: 'right' } 293 | ) 294 | 295 | const expected = [ 296 | 'i am a string that will be', 297 | 'wrapped [required] [default: 99]' 298 | ] 299 | 300 | ui.toString().split('\n').should.eql(expected) 301 | }) 302 | 303 | it('does not append the string if it does not fit on the prior row', () => { 304 | const ui = cliui({ 305 | width: 40 306 | }) 307 | 308 | ui.span( 309 | { text: 'i am a string that will be wrapped', width: 30 } 310 | ) 311 | 312 | ui.div( 313 | { text: 'i am a second row', align: 'left' } 314 | ) 315 | 316 | const expected = [ 317 | 'i am a string that will be', 318 | 'wrapped', 319 | 'i am a second row' 320 | ] 321 | 322 | ui.toString().split('\n').should.eql(expected) 323 | }) 324 | 325 | it('always appends text to prior span if wrap is disabled', () => { 326 | const ui = cliui({ 327 | wrap: false, 328 | width: 40 329 | }) 330 | 331 | ui.span( 332 | { text: 'i am a string that will be wrapped', width: 30 } 333 | ) 334 | 335 | ui.div( 336 | { text: 'i am a second row', align: 'left', padding: [0, 0, 0, 3] } 337 | ) 338 | 339 | ui.div('a third line') 340 | 341 | const expected = [ 342 | 'i am a string that will be wrapped i am a second row', 343 | 'a third line' 344 | ] 345 | 346 | ui.toString().split('\n').should.eql(expected) 347 | }) 348 | 349 | it('appends to prior line appropriately when strings contain ansi escape codes', () => { 350 | const ui = cliui({ 351 | width: 40 352 | }) 353 | 354 | ui.span( 355 | { text: chalk.green('i am a string that will be wrapped'), width: 30 } 356 | ) 357 | 358 | ui.div( 359 | { text: chalk.blue(' [required] [default: 99]'), align: 'right' } 360 | ) 361 | 362 | const expected = [ 363 | 'i am a string that will be', 364 | 'wrapped [required] [default: 99]' 365 | ] 366 | 367 | ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) 368 | }) 369 | }) 370 | 371 | describe('layoutDSL', () => { 372 | it('turns tab into multiple columns', () => { 373 | const ui = cliui({ 374 | width: 60 375 | }) 376 | 377 | ui.div( 378 | ' \tmy awesome regex\n \tanother row\t a third column' 379 | ) 380 | 381 | const expected = [ 382 | ' my awesome regex', 383 | ' another row a third column' 384 | ] 385 | 386 | ui.toString().split('\n').should.eql(expected) 387 | }) 388 | 389 | it('turns newline into multiple rows', () => { 390 | const ui = cliui({ 391 | width: 40 392 | }) 393 | 394 | ui.div( 395 | 'Usage: $0\n \t my awesome regex\n \t my awesome glob\t [required]' 396 | ) 397 | const expected = [ 398 | 'Usage: $0', 399 | ' my awesome regex', 400 | ' my awesome [required]', 401 | ' glob' 402 | ] 403 | 404 | ui.toString().split('\n').should.eql(expected) 405 | }) 406 | 407 | it('aligns rows appropriately when they contain ansi escape codes', () => { 408 | const ui = cliui({ 409 | width: 40 410 | }) 411 | 412 | ui.div( 413 | ' \t ' + chalk.red('my awesome regex') + '\t [regex]\n ' + chalk.blue('') + '\t my awesome glob\t [required]' 414 | ) 415 | 416 | const expected = [ 417 | ' my awesome [regex]', 418 | ' regex', 419 | ' my awesome [required]', 420 | ' glob' 421 | ] 422 | 423 | ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) 424 | }) 425 | 426 | it('ignores ansi escape codes when measuring padding', () => { 427 | // Forcefully enable color-codes for this test 428 | const { enabled, level } = chalk 429 | chalk.enabled = true 430 | chalk.level = 1 431 | 432 | const ui = cliui({ 433 | width: 25 434 | }) 435 | 436 | // using figlet font 'Shadow' rendering of text 'true' here 437 | ui.div( 438 | chalk.blue(' | \n __| __| | | _ \\ \n | | | | __/ \n \\__| _| \\__,_| \\___| \n ') 439 | ) 440 | 441 | // relevant part is first line - leading whitespace should be preserved as left padding 442 | const expected = [ 443 | ' |', 444 | ' __| __| | | _ \\', 445 | ' | | | | __/', 446 | ' \\__| _| \\__,_| \\___|', 447 | ' ' 448 | ] 449 | 450 | ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) 451 | chalk.enabled = enabled 452 | chalk.level = level 453 | }) 454 | 455 | it('correctly handles lack of ansi escape codes when measuring padding', () => { 456 | const ui = cliui({ 457 | width: 25 458 | }) 459 | 460 | // using figlet font 'Shadow' rendering of text 'true' here 461 | ui.div( 462 | ' | \n __| __| | | _ \\ \n | | | | __/ \n \\__| _| \\__,_| \\___| \n ' 463 | ) 464 | 465 | // The difference 466 | const expected = [ 467 | ' |', 468 | ' __| __| | | _ \\', 469 | ' | | | | __/', 470 | ' \\__| _| \\__,_| \\___|', 471 | '' 472 | ] 473 | 474 | ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected) 475 | }) 476 | 477 | it('does not apply DSL if wrap is false', () => { 478 | const ui = cliui({ 479 | width: 40, 480 | wrap: false 481 | }) 482 | 483 | ui.div( 484 | 'Usage: $0\ttwo\tthree' 485 | ) 486 | 487 | ui.toString().should.eql('Usage: $0\ttwo\tthree') 488 | }) 489 | }) 490 | }) 491 | -------------------------------------------------------------------------------- /test/deno/cliui-test.ts: -------------------------------------------------------------------------------- 1 | /* global Deno */ 2 | 3 | import { 4 | assert, 5 | assertEquals 6 | } from 'https://deno.land/std/testing/asserts.ts' 7 | import cliui from '../../deno.ts' 8 | 9 | // Just run a couple of the tests as a light check working from the Deno runtime. 10 | 11 | Deno.test("wraps text at 'width' if a single column is given", () => { 12 | const ui = cliui({ 13 | width: 10 14 | }) 15 | 16 | ui.div('i am a string that should be wrapped') 17 | 18 | ui.toString().split('\n').forEach((row: string) => { 19 | assert(row.length <= 10) 20 | }) 21 | }) 22 | 23 | Deno.test('evenly divides text across columns if multiple columns are given', () => { 24 | const ui = cliui({ 25 | width: 40 26 | }) 27 | 28 | ui.div( 29 | { text: 'i am a string that should be wrapped', width: 15 }, 30 | 'i am a second string that should be wrapped', 31 | 'i am a third string that should be wrapped' 32 | ) 33 | 34 | // total width of all columns is <= 35 | // the width cliui is initialized with. 36 | ui.toString().split('\n').forEach((row: string) => { 37 | assert(row.length <= 40) 38 | }) 39 | 40 | // it should wrap each column appropriately. 41 | // TODO: we should flesh out the Deno and ESM implementation 42 | // such that it spreads words out over multiple columns appropriately: 43 | const expected = [ 44 | 'i am a string i am a i am a third', 45 | 'that should be second string that', 46 | 'wrapped string that should be', 47 | ' should be wrapped', 48 | ' wrapped' 49 | ] 50 | 51 | ui.toString().split('\n').forEach((line: string, i: number) => { 52 | assertEquals(line, expected[i]) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": ".", 6 | "sourceMap": false, 7 | "target": "es2017", 8 | "moduleResolution": "node", 9 | "module": "es2015" 10 | }, 11 | "include": [ 12 | "lib/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "lib/cjs.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": true 5 | } 6 | } --------------------------------------------------------------------------------