├── .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 | 
4 | [](https://www.npmjs.com/package/cliui)
5 | [](https://conventionalcommits.org)
6 | 
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 | }
--------------------------------------------------------------------------------