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