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