1 var stringWidth = require('string-width')
2 var stripAnsi = require('strip-ansi')
3 var wrap = require('wrap-ansi')
14 this.width = opts.width
19 UI.prototype.span = function () {
20 var cols = this.div.apply(this, arguments)
24 UI.prototype.div = function () {
25 if (arguments.length === 0) this.div('')
26 if (this.wrap && this._shouldApplyLayoutDSL.apply(this, arguments)) {
27 return this._applyLayoutDSL(arguments[0])
32 for (var i = 0, arg; (arg = arguments[i]) !== undefined; i++) {
33 if (typeof arg === 'string') cols.push(this._colFromString(arg))
41 UI.prototype._shouldApplyLayoutDSL = function () {
42 return arguments.length === 1 && typeof arguments[0] === 'string' &&
43 /[\t\n]/.test(arguments[0])
46 UI.prototype._applyLayoutDSL = function (str) {
48 var rows = str.split('\n')
49 var leftColumnWidth = 0
51 // simple heuristic for layout, make sure the
52 // second column lines up along the left-hand.
53 // don't allow the first column to take up more
54 // than 50% of the screen.
55 rows.forEach(function (row) {
56 var columns = row.split('\t')
57 if (columns.length > 1 && stringWidth(columns[0]) > leftColumnWidth) {
58 leftColumnWidth = Math.min(
59 Math.floor(_this.width * 0.5),
60 stringWidth(columns[0])
66 // replacing ' ' with padding calculations.
67 // using the algorithmically generated width.
68 rows.forEach(function (row) {
69 var columns = row.split('\t')
70 _this.div.apply(_this, columns.map(function (r, i) {
73 padding: _this._measurePadding(r),
74 width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
79 return this.rows[this.rows.length - 1]
82 UI.prototype._colFromString = function (str) {
85 padding: this._measurePadding(str)
89 UI.prototype._measurePadding = function (str) {
90 // measure padding without ansi escape codes
91 var noAnsi = stripAnsi(str)
92 return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]
95 UI.prototype.toString = function () {
99 _this.rows.forEach(function (row, i) {
100 _this.rowToString(row, lines)
103 // don't display any lines with the
105 lines = lines.filter(function (line) {
109 return lines.map(function (line) {
114 UI.prototype.rowToString = function (row, lines) {
117 var rrows = this._rasterize(row)
123 rrows.forEach(function (rrow, r) {
125 rrow.forEach(function (col, c) {
126 ts = '' // temporary string used during alignment/padding.
127 width = row[c].width // the width with padding.
128 wrapWidth = _this._negatePadding(row[c]) // the width without padding.
132 for (var i = 0; i < wrapWidth - stringWidth(col); i++) {
136 // align the string within its column.
137 if (row[c].align && row[c].align !== 'left' && _this.wrap) {
138 ts = align[row[c].align](ts, wrapWidth)
139 if (stringWidth(ts) < wrapWidth) ts += new Array(width - stringWidth(ts)).join(' ')
142 // apply border and padding to string.
143 padding = row[c].padding || [0, 0, 0, 0]
144 if (padding[left]) str += new Array(padding[left] + 1).join(' ')
145 str += addBorder(row[c], ts, '| ')
147 str += addBorder(row[c], ts, ' |')
148 if (padding[right]) str += new Array(padding[right] + 1).join(' ')
150 // if prior row is span, try to render the
151 // current row on the prior line.
152 if (r === 0 && lines.length > 0) {
153 str = _this._renderInline(str, lines[lines.length - 1])
157 // remove trailing whitespace.
159 text: str.replace(/ +$/, ''),
167 function addBorder (col, ts, style) {
169 if (/[.']-+[.']/.test(ts)) return ''
170 else if (ts.trim().length) return style
176 // if the full 'source' can render in
177 // the target line, do so.
178 UI.prototype._renderInline = function (source, previousLine) {
179 var leadingWhitespace = source.match(/^ */)[0].length
180 var target = previousLine.text
181 var targetTextWidth = stringWidth(target.trimRight())
183 if (!previousLine.span) return source
185 // if we're not applying wrapping logic,
186 // just always append to the span.
188 previousLine.hidden = true
189 return target + source
192 if (leadingWhitespace < targetTextWidth) return source
194 previousLine.hidden = true
196 return target.trimRight() + new Array(leadingWhitespace - targetTextWidth + 1).join(' ') + source.trimLeft()
199 UI.prototype._rasterize = function (row) {
204 var widths = this._columnWidths(row)
207 // word wrap all columns, and create
208 // a data-structure that is easy to rasterize.
209 row.forEach(function (col, c) {
210 // leave room for left and right padding.
211 col.width = widths[c]
212 if (_this.wrap) wrapped = wrap(col.text, _this._negatePadding(col), {hard: true}).split('\n')
213 else wrapped = col.text.split('\n')
216 wrapped.unshift('.' + new Array(_this._negatePadding(col) + 3).join('-') + '.')
217 wrapped.push("'" + new Array(_this._negatePadding(col) + 3).join('-') + "'")
220 // add top and bottom padding.
222 for (i = 0; i < (col.padding[top] || 0); i++) wrapped.unshift('')
223 for (i = 0; i < (col.padding[bottom] || 0); i++) wrapped.push('')
226 wrapped.forEach(function (str, r) {
227 if (!rrows[r]) rrows.push([])
231 for (var i = 0; i < c; i++) {
232 if (rrow[i] === undefined) rrow.push('')
241 UI.prototype._negatePadding = function (col) {
242 var wrapWidth = col.width
243 if (col.padding) wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0)
244 if (col.border) wrapWidth -= 4
248 UI.prototype._columnWidths = function (row) {
251 var unset = row.length
253 var remainingWidth = this.width
255 // column widths can be set in config.
256 row.forEach(function (col, i) {
259 widths[i] = col.width
260 remainingWidth -= col.width
262 widths[i] = undefined
266 // any unset widths should be calculated.
267 if (unset) unsetWidth = Math.floor(remainingWidth / unset)
268 widths.forEach(function (w, i) {
269 if (!_this.wrap) widths[i] = row[i].width || stringWidth(row[i].text)
270 else if (w === undefined) widths[i] = Math.max(unsetWidth, _minWidth(row[i]))
276 // calculates the minimum width of
277 // a column, based on padding preferences.
278 function _minWidth (col) {
279 var padding = col.padding || []
280 var minWidth = 1 + (padding[left] || 0) + (padding[right] || 0)
281 if (col.border) minWidth += 4
285 function alignRight (str, width) {
288 var strWidth = stringWidth(str)
290 if (strWidth < width) {
291 padding = new Array(width - strWidth + 1).join(' ')
297 function alignCenter (str, width) {
300 var strWidth = stringWidth(str.trim())
302 if (strWidth < width) {
303 padding = new Array(parseInt((width - strWidth) / 2, 10) + 1).join(' ')
309 module.exports = function (opts) {
313 width: (opts || {}).width || 80,
314 wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true