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.resetOutput = function () {
28 UI.prototype.div = function () {
29 if (arguments.length === 0) this.div('')
30 if (this.wrap && this._shouldApplyLayoutDSL.apply(this, arguments)) {
31 return this._applyLayoutDSL(arguments[0])
36 for (var i = 0, arg; (arg = arguments[i]) !== undefined; i++) {
37 if (typeof arg === 'string') cols.push(this._colFromString(arg))
45 UI.prototype._shouldApplyLayoutDSL = function () {
46 return arguments.length === 1 && typeof arguments[0] === 'string' &&
47 /[\t\n]/.test(arguments[0])
50 UI.prototype._applyLayoutDSL = function (str) {
52 var rows = str.split('\n')
53 var leftColumnWidth = 0
55 // simple heuristic for layout, make sure the
56 // second column lines up along the left-hand.
57 // don't allow the first column to take up more
58 // than 50% of the screen.
59 rows.forEach(function (row) {
60 var columns = row.split('\t')
61 if (columns.length > 1 && stringWidth(columns[0]) > leftColumnWidth) {
62 leftColumnWidth = Math.min(
63 Math.floor(_this.width * 0.5),
64 stringWidth(columns[0])
70 // replacing ' ' with padding calculations.
71 // using the algorithmically generated width.
72 rows.forEach(function (row) {
73 var columns = row.split('\t')
74 _this.div.apply(_this, columns.map(function (r, i) {
77 padding: _this._measurePadding(r),
78 width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
83 return this.rows[this.rows.length - 1]
86 UI.prototype._colFromString = function (str) {
89 padding: this._measurePadding(str)
93 UI.prototype._measurePadding = function (str) {
94 // measure padding without ansi escape codes
95 var noAnsi = stripAnsi(str)
96 return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]
99 UI.prototype.toString = function () {
103 _this.rows.forEach(function (row, i) {
104 _this.rowToString(row, lines)
107 // don't display any lines with the
109 lines = lines.filter(function (line) {
113 return lines.map(function (line) {
118 UI.prototype.rowToString = function (row, lines) {
121 var rrows = this._rasterize(row)
127 rrows.forEach(function (rrow, r) {
129 rrow.forEach(function (col, c) {
130 ts = '' // temporary string used during alignment/padding.
131 width = row[c].width // the width with padding.
132 wrapWidth = _this._negatePadding(row[c]) // the width without padding.
136 for (var i = 0; i < wrapWidth - stringWidth(col); i++) {
140 // align the string within its column.
141 if (row[c].align && row[c].align !== 'left' && _this.wrap) {
142 ts = align[row[c].align](ts, wrapWidth)
143 if (stringWidth(ts) < wrapWidth) ts += new Array(width - stringWidth(ts)).join(' ')
146 // apply border and padding to string.
147 padding = row[c].padding || [0, 0, 0, 0]
148 if (padding[left]) str += new Array(padding[left] + 1).join(' ')
149 str += addBorder(row[c], ts, '| ')
151 str += addBorder(row[c], ts, ' |')
152 if (padding[right]) str += new Array(padding[right] + 1).join(' ')
154 // if prior row is span, try to render the
155 // current row on the prior line.
156 if (r === 0 && lines.length > 0) {
157 str = _this._renderInline(str, lines[lines.length - 1])
161 // remove trailing whitespace.
163 text: str.replace(/ +$/, ''),
171 function addBorder (col, ts, style) {
173 if (/[.']-+[.']/.test(ts)) return ''
174 else if (ts.trim().length) return style
180 // if the full 'source' can render in
181 // the target line, do so.
182 UI.prototype._renderInline = function (source, previousLine) {
183 var leadingWhitespace = source.match(/^ */)[0].length
184 var target = previousLine.text
185 var targetTextWidth = stringWidth(target.trimRight())
187 if (!previousLine.span) return source
189 // if we're not applying wrapping logic,
190 // just always append to the span.
192 previousLine.hidden = true
193 return target + source
196 if (leadingWhitespace < targetTextWidth) return source
198 previousLine.hidden = true
200 return target.trimRight() + new Array(leadingWhitespace - targetTextWidth + 1).join(' ') + source.trimLeft()
203 UI.prototype._rasterize = function (row) {
208 var widths = this._columnWidths(row)
211 // word wrap all columns, and create
212 // a data-structure that is easy to rasterize.
213 row.forEach(function (col, c) {
214 // leave room for left and right padding.
215 col.width = widths[c]
216 if (_this.wrap) wrapped = wrap(col.text, _this._negatePadding(col), { hard: true }).split('\n')
217 else wrapped = col.text.split('\n')
220 wrapped.unshift('.' + new Array(_this._negatePadding(col) + 3).join('-') + '.')
221 wrapped.push("'" + new Array(_this._negatePadding(col) + 3).join('-') + "'")
224 // add top and bottom padding.
226 for (i = 0; i < (col.padding[top] || 0); i++) wrapped.unshift('')
227 for (i = 0; i < (col.padding[bottom] || 0); i++) wrapped.push('')
230 wrapped.forEach(function (str, r) {
231 if (!rrows[r]) rrows.push([])
235 for (var i = 0; i < c; i++) {
236 if (rrow[i] === undefined) rrow.push('')
245 UI.prototype._negatePadding = function (col) {
246 var wrapWidth = col.width
247 if (col.padding) wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0)
248 if (col.border) wrapWidth -= 4
252 UI.prototype._columnWidths = function (row) {
255 var unset = row.length
257 var remainingWidth = this.width
259 // column widths can be set in config.
260 row.forEach(function (col, i) {
263 widths[i] = col.width
264 remainingWidth -= col.width
266 widths[i] = undefined
270 // any unset widths should be calculated.
271 if (unset) unsetWidth = Math.floor(remainingWidth / unset)
272 widths.forEach(function (w, i) {
273 if (!_this.wrap) widths[i] = row[i].width || stringWidth(row[i].text)
274 else if (w === undefined) widths[i] = Math.max(unsetWidth, _minWidth(row[i]))
280 // calculates the minimum width of
281 // a column, based on padding preferences.
282 function _minWidth (col) {
283 var padding = col.padding || []
284 var minWidth = 1 + (padding[left] || 0) + (padding[right] || 0)
285 if (col.border) minWidth += 4
289 function getWindowWidth () {
290 if (typeof process === 'object' && process.stdout && process.stdout.columns) return process.stdout.columns
293 function alignRight (str, width) {
296 var strWidth = stringWidth(str)
298 if (strWidth < width) {
299 padding = new Array(width - strWidth + 1).join(' ')
305 function alignCenter (str, width) {
308 var strWidth = stringWidth(str.trim())
310 if (strWidth < width) {
311 padding = new Array(parseInt((width - strWidth) / 2, 10) + 1).join(' ')
317 module.exports = function (opts) {
321 width: (opts || {}).width || getWindowWidth() || 80,
322 wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true