1'use strict'; 2 3const align = { 4 right: alignRight, 5 center: alignCenter 6}; 7const top = 0; 8const right = 1; 9const bottom = 2; 10const left = 3; 11class UI { 12 constructor(opts) { 13 var _a; 14 this.width = opts.width; 15 /* c8 ignore start */ 16 this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true; 17 /* c8 ignore stop */ 18 this.rows = []; 19 } 20 span(...args) { 21 const cols = this.div(...args); 22 cols.span = true; 23 } 24 resetOutput() { 25 this.rows = []; 26 } 27 div(...args) { 28 if (args.length === 0) { 29 this.div(''); 30 } 31 if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') { 32 return this.applyLayoutDSL(args[0]); 33 } 34 const cols = args.map(arg => { 35 if (typeof arg === 'string') { 36 return this.colFromString(arg); 37 } 38 return arg; 39 }); 40 this.rows.push(cols); 41 return cols; 42 } 43 shouldApplyLayoutDSL(...args) { 44 return args.length === 1 && typeof args[0] === 'string' && 45 /[\t\n]/.test(args[0]); 46 } 47 applyLayoutDSL(str) { 48 const rows = str.split('\n').map(row => row.split('\t')); 49 let leftColumnWidth = 0; 50 // simple heuristic for layout, make sure the 51 // second column lines up along the left-hand. 52 // don't allow the first column to take up more 53 // than 50% of the screen. 54 rows.forEach(columns => { 55 if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) { 56 leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0])); 57 } 58 }); 59 // generate a table: 60 // replacing ' ' with padding calculations. 61 // using the algorithmically generated width. 62 rows.forEach(columns => { 63 this.div(...columns.map((r, i) => { 64 return { 65 text: r.trim(), 66 padding: this.measurePadding(r), 67 width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined 68 }; 69 })); 70 }); 71 return this.rows[this.rows.length - 1]; 72 } 73 colFromString(text) { 74 return { 75 text, 76 padding: this.measurePadding(text) 77 }; 78 } 79 measurePadding(str) { 80 // measure padding without ansi escape codes 81 const noAnsi = mixin.stripAnsi(str); 82 return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]; 83 } 84 toString() { 85 const lines = []; 86 this.rows.forEach(row => { 87 this.rowToString(row, lines); 88 }); 89 // don't display any lines with the 90 // hidden flag set. 91 return lines 92 .filter(line => !line.hidden) 93 .map(line => line.text) 94 .join('\n'); 95 } 96 rowToString(row, lines) { 97 this.rasterize(row).forEach((rrow, r) => { 98 let str = ''; 99 rrow.forEach((col, c) => { 100 const { width } = row[c]; // the width with padding. 101 const wrapWidth = this.negatePadding(row[c]); // the width without padding. 102 let ts = col; // temporary string used during alignment/padding. 103 if (wrapWidth > mixin.stringWidth(col)) { 104 ts += ' '.repeat(wrapWidth - mixin.stringWidth(col)); 105 } 106 // align the string within its column. 107 if (row[c].align && row[c].align !== 'left' && this.wrap) { 108 const fn = align[row[c].align]; 109 ts = fn(ts, wrapWidth); 110 if (mixin.stringWidth(ts) < wrapWidth) { 111 /* c8 ignore start */ 112 const w = width || 0; 113 /* c8 ignore stop */ 114 ts += ' '.repeat(w - mixin.stringWidth(ts) - 1); 115 } 116 } 117 // apply border and padding to string. 118 const padding = row[c].padding || [0, 0, 0, 0]; 119 if (padding[left]) { 120 str += ' '.repeat(padding[left]); 121 } 122 str += addBorder(row[c], ts, '| '); 123 str += ts; 124 str += addBorder(row[c], ts, ' |'); 125 if (padding[right]) { 126 str += ' '.repeat(padding[right]); 127 } 128 // if prior row is span, try to render the 129 // current row on the prior line. 130 if (r === 0 && lines.length > 0) { 131 str = this.renderInline(str, lines[lines.length - 1]); 132 } 133 }); 134 // remove trailing whitespace. 135 lines.push({ 136 text: str.replace(/ +$/, ''), 137 span: row.span 138 }); 139 }); 140 return lines; 141 } 142 // if the full 'source' can render in 143 // the target line, do so. 144 renderInline(source, previousLine) { 145 const match = source.match(/^ */); 146 /* c8 ignore start */ 147 const leadingWhitespace = match ? match[0].length : 0; 148 /* c8 ignore stop */ 149 const target = previousLine.text; 150 const targetTextWidth = mixin.stringWidth(target.trimEnd()); 151 if (!previousLine.span) { 152 return source; 153 } 154 // if we're not applying wrapping logic, 155 // just always append to the span. 156 if (!this.wrap) { 157 previousLine.hidden = true; 158 return target + source; 159 } 160 if (leadingWhitespace < targetTextWidth) { 161 return source; 162 } 163 previousLine.hidden = true; 164 return target.trimEnd() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimStart(); 165 } 166 rasterize(row) { 167 const rrows = []; 168 const widths = this.columnWidths(row); 169 let wrapped; 170 // word wrap all columns, and create 171 // a data-structure that is easy to rasterize. 172 row.forEach((col, c) => { 173 // leave room for left and right padding. 174 col.width = widths[c]; 175 if (this.wrap) { 176 wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n'); 177 } 178 else { 179 wrapped = col.text.split('\n'); 180 } 181 if (col.border) { 182 wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.'); 183 wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'"); 184 } 185 // add top and bottom padding. 186 if (col.padding) { 187 wrapped.unshift(...new Array(col.padding[top] || 0).fill('')); 188 wrapped.push(...new Array(col.padding[bottom] || 0).fill('')); 189 } 190 wrapped.forEach((str, r) => { 191 if (!rrows[r]) { 192 rrows.push([]); 193 } 194 const rrow = rrows[r]; 195 for (let i = 0; i < c; i++) { 196 if (rrow[i] === undefined) { 197 rrow.push(''); 198 } 199 } 200 rrow.push(str); 201 }); 202 }); 203 return rrows; 204 } 205 negatePadding(col) { 206 /* c8 ignore start */ 207 let wrapWidth = col.width || 0; 208 /* c8 ignore stop */ 209 if (col.padding) { 210 wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0); 211 } 212 if (col.border) { 213 wrapWidth -= 4; 214 } 215 return wrapWidth; 216 } 217 columnWidths(row) { 218 if (!this.wrap) { 219 return row.map(col => { 220 return col.width || mixin.stringWidth(col.text); 221 }); 222 } 223 let unset = row.length; 224 let remainingWidth = this.width; 225 // column widths can be set in config. 226 const widths = row.map(col => { 227 if (col.width) { 228 unset--; 229 remainingWidth -= col.width; 230 return col.width; 231 } 232 return undefined; 233 }); 234 // any unset widths should be calculated. 235 /* c8 ignore start */ 236 const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0; 237 /* c8 ignore stop */ 238 return widths.map((w, i) => { 239 if (w === undefined) { 240 return Math.max(unsetWidth, _minWidth(row[i])); 241 } 242 return w; 243 }); 244 } 245} 246function addBorder(col, ts, style) { 247 if (col.border) { 248 if (/[.']-+[.']/.test(ts)) { 249 return ''; 250 } 251 if (ts.trim().length !== 0) { 252 return style; 253 } 254 return ' '; 255 } 256 return ''; 257} 258// calculates the minimum width of 259// a column, based on padding preferences. 260function _minWidth(col) { 261 const padding = col.padding || []; 262 const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0); 263 if (col.border) { 264 return minWidth + 4; 265 } 266 return minWidth; 267} 268function getWindowWidth() { 269 /* c8 ignore start */ 270 if (typeof process === 'object' && process.stdout && process.stdout.columns) { 271 return process.stdout.columns; 272 } 273 return 80; 274} 275/* c8 ignore stop */ 276function alignRight(str, width) { 277 str = str.trim(); 278 const strWidth = mixin.stringWidth(str); 279 if (strWidth < width) { 280 return ' '.repeat(width - strWidth) + str; 281 } 282 return str; 283} 284function alignCenter(str, width) { 285 str = str.trim(); 286 const strWidth = mixin.stringWidth(str); 287 /* c8 ignore start */ 288 if (strWidth >= width) { 289 return str; 290 } 291 /* c8 ignore stop */ 292 return ' '.repeat((width - strWidth) >> 1) + str; 293} 294let mixin; 295function cliui(opts, _mixin) { 296 mixin = _mixin; 297 return new UI({ 298 /* c8 ignore start */ 299 width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(), 300 wrap: opts === null || opts === void 0 ? void 0 : opts.wrap 301 /* c8 ignore stop */ 302 }); 303} 304 305// Bootstrap cliui with CommonJS dependencies: 306const stringWidth = require('string-width-cjs'); 307const stripAnsi = require('strip-ansi-cjs'); 308const wrap = require('wrap-ansi-cjs'); 309function ui(opts) { 310 return cliui(opts, { 311 stringWidth, 312 stripAnsi, 313 wrap 314 }); 315} 316 317module.exports = ui; 318