Spaces:
Paused
Paused
; | |
const align = { | |
right: alignRight, | |
center: alignCenter | |
}; | |
const top = 0; | |
const right = 1; | |
const bottom = 2; | |
const left = 3; | |
class UI { | |
constructor(opts) { | |
var _a; | |
this.width = opts.width; | |
this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true; | |
this.rows = []; | |
} | |
span(...args) { | |
const cols = this.div(...args); | |
cols.span = true; | |
} | |
resetOutput() { | |
this.rows = []; | |
} | |
div(...args) { | |
if (args.length === 0) { | |
this.div(''); | |
} | |
if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') { | |
return this.applyLayoutDSL(args[0]); | |
} | |
const cols = args.map(arg => { | |
if (typeof arg === 'string') { | |
return this.colFromString(arg); | |
} | |
return arg; | |
}); | |
this.rows.push(cols); | |
return cols; | |
} | |
shouldApplyLayoutDSL(...args) { | |
return args.length === 1 && typeof args[0] === 'string' && | |
/[\t\n]/.test(args[0]); | |
} | |
applyLayoutDSL(str) { | |
const rows = str.split('\n').map(row => row.split('\t')); | |
let leftColumnWidth = 0; | |
// simple heuristic for layout, make sure the | |
// second column lines up along the left-hand. | |
// don't allow the first column to take up more | |
// than 50% of the screen. | |
rows.forEach(columns => { | |
if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) { | |
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0])); | |
} | |
}); | |
// generate a table: | |
// replacing ' ' with padding calculations. | |
// using the algorithmically generated width. | |
rows.forEach(columns => { | |
this.div(...columns.map((r, i) => { | |
return { | |
text: r.trim(), | |
padding: this.measurePadding(r), | |
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined | |
}; | |
})); | |
}); | |
return this.rows[this.rows.length - 1]; | |
} | |
colFromString(text) { | |
return { | |
text, | |
padding: this.measurePadding(text) | |
}; | |
} | |
measurePadding(str) { | |
// measure padding without ansi escape codes | |
const noAnsi = mixin.stripAnsi(str); | |
return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]; | |
} | |
toString() { | |
const lines = []; | |
this.rows.forEach(row => { | |
this.rowToString(row, lines); | |
}); | |
// don't display any lines with the | |
// hidden flag set. | |
return lines | |
.filter(line => !line.hidden) | |
.map(line => line.text) | |
.join('\n'); | |
} | |
rowToString(row, lines) { | |
this.rasterize(row).forEach((rrow, r) => { | |
let str = ''; | |
rrow.forEach((col, c) => { | |
const { width } = row[c]; // the width with padding. | |
const wrapWidth = this.negatePadding(row[c]); // the width without padding. | |
let ts = col; // temporary string used during alignment/padding. | |
if (wrapWidth > mixin.stringWidth(col)) { | |
ts += ' '.repeat(wrapWidth - mixin.stringWidth(col)); | |
} | |
// align the string within its column. | |
if (row[c].align && row[c].align !== 'left' && this.wrap) { | |
const fn = align[row[c].align]; | |
ts = fn(ts, wrapWidth); | |
if (mixin.stringWidth(ts) < wrapWidth) { | |
ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1); | |
} | |
} | |
// apply border and padding to string. | |
const padding = row[c].padding || [0, 0, 0, 0]; | |
if (padding[left]) { | |
str += ' '.repeat(padding[left]); | |
} | |
str += addBorder(row[c], ts, '| '); | |
str += ts; | |
str += addBorder(row[c], ts, ' |'); | |
if (padding[right]) { | |
str += ' '.repeat(padding[right]); | |
} | |
// if prior row is span, try to render the | |
// current row on the prior line. | |
if (r === 0 && lines.length > 0) { | |
str = this.renderInline(str, lines[lines.length - 1]); | |
} | |
}); | |
// remove trailing whitespace. | |
lines.push({ | |
text: str.replace(/ +$/, ''), | |
span: row.span | |
}); | |
}); | |
return lines; | |
} | |
// if the full 'source' can render in | |
// the target line, do so. | |
renderInline(source, previousLine) { | |
const match = source.match(/^ */); | |
const leadingWhitespace = match ? match[0].length : 0; | |
const target = previousLine.text; | |
const targetTextWidth = mixin.stringWidth(target.trimRight()); | |
if (!previousLine.span) { | |
return source; | |
} | |
// if we're not applying wrapping logic, | |
// just always append to the span. | |
if (!this.wrap) { | |
previousLine.hidden = true; | |
return target + source; | |
} | |
if (leadingWhitespace < targetTextWidth) { | |
return source; | |
} | |
previousLine.hidden = true; | |
return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft(); | |
} | |
rasterize(row) { | |
const rrows = []; | |
const widths = this.columnWidths(row); | |
let wrapped; | |
// word wrap all columns, and create | |
// a data-structure that is easy to rasterize. | |
row.forEach((col, c) => { | |
// leave room for left and right padding. | |
col.width = widths[c]; | |
if (this.wrap) { | |
wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n'); | |
} | |
else { | |
wrapped = col.text.split('\n'); | |
} | |
if (col.border) { | |
wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.'); | |
wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'"); | |
} | |
// add top and bottom padding. | |
if (col.padding) { | |
wrapped.unshift(...new Array(col.padding[top] || 0).fill('')); | |
wrapped.push(...new Array(col.padding[bottom] || 0).fill('')); | |
} | |
wrapped.forEach((str, r) => { | |
if (!rrows[r]) { | |
rrows.push([]); | |
} | |
const rrow = rrows[r]; | |
for (let i = 0; i < c; i++) { | |
if (rrow[i] === undefined) { | |
rrow.push(''); | |
} | |
} | |
rrow.push(str); | |
}); | |
}); | |
return rrows; | |
} | |
negatePadding(col) { | |
let wrapWidth = col.width || 0; | |
if (col.padding) { | |
wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0); | |
} | |
if (col.border) { | |
wrapWidth -= 4; | |
} | |
return wrapWidth; | |
} | |
columnWidths(row) { | |
if (!this.wrap) { | |
return row.map(col => { | |
return col.width || mixin.stringWidth(col.text); | |
}); | |
} | |
let unset = row.length; | |
let remainingWidth = this.width; | |
// column widths can be set in config. | |
const widths = row.map(col => { | |
if (col.width) { | |
unset--; | |
remainingWidth -= col.width; | |
return col.width; | |
} | |
return undefined; | |
}); | |
// any unset widths should be calculated. | |
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0; | |
return widths.map((w, i) => { | |
if (w === undefined) { | |
return Math.max(unsetWidth, _minWidth(row[i])); | |
} | |
return w; | |
}); | |
} | |
} | |
function addBorder(col, ts, style) { | |
if (col.border) { | |
if (/[.']-+[.']/.test(ts)) { | |
return ''; | |
} | |
if (ts.trim().length !== 0) { | |
return style; | |
} | |
return ' '; | |
} | |
return ''; | |
} | |
// calculates the minimum width of | |
// a column, based on padding preferences. | |
function _minWidth(col) { | |
const padding = col.padding || []; | |
const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0); | |
if (col.border) { | |
return minWidth + 4; | |
} | |
return minWidth; | |
} | |
function getWindowWidth() { | |
/* istanbul ignore next: depends on terminal */ | |
if (typeof process === 'object' && process.stdout && process.stdout.columns) { | |
return process.stdout.columns; | |
} | |
return 80; | |
} | |
function alignRight(str, width) { | |
str = str.trim(); | |
const strWidth = mixin.stringWidth(str); | |
if (strWidth < width) { | |
return ' '.repeat(width - strWidth) + str; | |
} | |
return str; | |
} | |
function alignCenter(str, width) { | |
str = str.trim(); | |
const strWidth = mixin.stringWidth(str); | |
/* istanbul ignore next */ | |
if (strWidth >= width) { | |
return str; | |
} | |
return ' '.repeat((width - strWidth) >> 1) + str; | |
} | |
let mixin; | |
function cliui(opts, _mixin) { | |
mixin = _mixin; | |
return new UI({ | |
width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(), | |
wrap: opts === null || opts === void 0 ? void 0 : opts.wrap | |
}); | |
} | |
// Bootstrap cliui with CommonJS dependencies: | |
const stringWidth = require('string-width'); | |
const stripAnsi = require('strip-ansi'); | |
const wrap = require('wrap-ansi'); | |
function ui(opts) { | |
return cliui(opts, { | |
stringWidth, | |
stripAnsi, | |
wrap | |
}); | |
} | |
module.exports = ui; | |