diff --git a/package.json b/package.json index 259c2697e..7263ebe05 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "object-treeify": "^1.1.33", "password-prompt": "^1.1.2", "semver": "^7.5.3", + "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "supports-color": "^8.1.1", @@ -55,6 +56,7 @@ "@types/proxyquire": "^1.3.28", "@types/semver": "^7.5.0", "@types/shelljs": "^0.8.11", + "@types/slice-ansi": "^4.0.0", "@types/strip-ansi": "^5.2.1", "@types/supports-color": "^8.1.1", "@types/wordwrap": "^1.0.1", @@ -114,4 +116,4 @@ "pretest": "yarn build --noEmit && tsc -p test --noEmit --skipLibCheck" }, "types": "lib/index.d.ts" -} \ No newline at end of file +} diff --git a/src/cli-ux/styled/table.ts b/src/cli-ux/styled/table.ts index 962c89caf..04f8b0cf2 100644 --- a/src/cli-ux/styled/table.ts +++ b/src/cli-ux/styled/table.ts @@ -9,6 +9,7 @@ import {stdout} from '../stream' const sw = require('string-width') const {orderBy} = require('natural-orderby') +const sliceAnsi = require('slice-ansi') class Table> { options: table.Options & { printLine(s: any): any } @@ -284,7 +285,12 @@ class Table> { const colorWidth = (d.length - visualWidth) let cell = d.padEnd(width + colorWidth) if ((cell.length - colorWidth) > width || visualWidth === width) { - cell = cell.slice(0, width - 2) + '… ' + // truncate the cell, preserving ANSI escape sequences, and keeping + // into account the width of fullwidth unicode characters + cell = sliceAnsi(cell, 0, width - 2) + '… ' + // pad with spaces; this is necessary in case the original string + // contained fullwidth characters which cannot be split + cell += ' '.repeat(width - sw(cell)) } l += cell diff --git a/test/cli-ux/styled/table.test.ts b/test/cli-ux/styled/table.test.ts index 3383fb5bd..977954dd4 100644 --- a/test/cli-ux/styled/table.test.ts +++ b/test/cli-ux/styled/table.test.ts @@ -1,6 +1,7 @@ import {expect, fancy} from 'fancy-test' import {ux} from '../../../src' +import * as screen from '../../../src/screen' /* eslint-disable camelcase */ const apps = [ @@ -317,6 +318,38 @@ describe('styled/table', () => { 321 supertable-test-2${ws} 123 supertable-test-1${ws}\n`) }) + + const orig = { + stdtermwidth: screen.stdtermwidth, + CLI_UX_SKIP_TTY_CHECK: process.env.CLI_UX_SKIP_TTY_CHECK, + } + + fancy + .do(() => { + Object.assign(screen, {stdtermwidth: 9}) + process.env.CLI_UX_SKIP_TTY_CHECK = 'true' + }) + .finally(() => { + Object.assign(screen, {stdtermwidth: orig.stdtermwidth}) + process.env.CLI_UX_SKIP_TTY_CHECK = orig.CLI_UX_SKIP_TTY_CHECK + }) + .stdout({stripColor: false}) + .end('correctly truncates columns with fullwidth characters or ansi escape sequences', output => { + /* eslint-disable camelcase */ + const app4 = { + build_stack: { + name: 'heroku-16', + }, + id: '456', + name: '\u001B[31m超级表格—测试\u001B[0m', + web_url: 'https://supertable-test-1.herokuapp.com/', + } + /* eslint-enable camelcase */ + + ux.table([...apps, app4 as any], {name: {}}, {'no-header': true}) + expect(output.stdout).to.equal(` super…${ws} + super…${ws} + \u001B[31m超级\u001B[39m…${ws}${ws}\n`) + }) }) }) - diff --git a/yarn.lock b/yarn.lock index 3cdead2f3..a4e45c202 100644 --- a/yarn.lock +++ b/yarn.lock @@ -722,6 +722,11 @@ dependencies: "@sinonjs/fake-timers" "^7.1.0" +"@types/slice-ansi@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/slice-ansi/-/slice-ansi-4.0.0.tgz#eb40dfbe3ac5c1de61f6bcb9ed471f54baa989d6" + integrity sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ== + "@types/strip-ansi@^5.2.1": version "5.2.1" resolved "https://registry.yarnpkg.com/@types/strip-ansi/-/strip-ansi-5.2.1.tgz#acd97f1f091e332bb7ce697c4609eb2370fa2a92"