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