You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
287 lines
9.4 KiB
287 lines
9.4 KiB
'use strict'; |
|
const align = { |
|
right: alignRight, |
|
center: alignCenter |
|
}; |
|
const top = 0; |
|
const right = 1; |
|
const bottom = 2; |
|
const left = 3; |
|
export 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; |
|
export 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 |
|
}); |
|
}
|
|
|