1 var wrap = require('wordwrap'),
3 right: require('right-align'),
4 center: require('center-align')
12 this.width = opts.width
17 UI.prototype.span = function () {
18 var cols = this.div.apply(this, arguments)
22 UI.prototype.div = function () {
23 if (arguments.length === 0) this.div('')
24 if (this.wrap && this._shouldApplyLayoutDSL.apply(this, arguments)) {
25 return this._applyLayoutDSL(arguments[0])
30 for (var i = 0, arg; (arg = arguments[i]) !== undefined; i++) {
31 if (typeof arg === 'string') cols.push(this._colFromString(arg))
39 UI.prototype._shouldApplyLayoutDSL = function () {
40 return arguments.length === 1 && typeof arguments[0] === 'string' &&
41 /[\t\n]/.test(arguments[0])
44 UI.prototype._applyLayoutDSL = function (str) {
46 rows = str.split('\n'),
49 // simple heuristic for layout, make sure the
50 // second column lines up along the left-hand.
51 // don't allow the first column to take up more
52 // than 50% of the screen.
53 rows.forEach(function (row) {
54 var columns = row.split('\t')
55 if (columns.length > 1 && columns[0].length > leftColumnWidth) {
56 leftColumnWidth = Math.min(
57 Math.floor(_this.width * 0.5),
64 // replacing ' ' with padding calculations.
65 // using the algorithmically generated width.
66 rows.forEach(function (row) {
67 var columns = row.split('\t')
68 _this.div.apply(_this, columns.map(function (r, i) {
71 padding: [0, r.match(/\s*$/)[0].length, 0, r.match(/^\s*/)[0].length],
72 width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
77 return this.rows[this.rows.length - 1]
80 UI.prototype._colFromString = function (str) {
86 UI.prototype.toString = function () {
90 _this.rows.forEach(function (row, i) {
91 _this.rowToString(row, lines)
94 // don't display any lines with the
96 lines = lines.filter(function (line) {
100 return lines.map(function (line) {
105 UI.prototype.rowToString = function (row, lines) {
108 rrows = this._rasterize(row),
114 rrows.forEach(function (rrow, r) {
116 rrow.forEach(function (col, c) {
117 ts = '' // temporary string used during alignment/padding.
118 width = row[c].width // the width with padding.
119 wrapWidth = _this._negatePadding(row[c]) // the width without padding.
121 for (var i = 0; i < Math.max(wrapWidth, col.length); i++) {
122 ts += col.charAt(i) || ' '
125 // align the string within its column.
126 if (row[c].align && row[c].align !== 'left' && _this.wrap) {
127 ts = align[row[c].align](ts.trim() + '\n' + new Array(wrapWidth + 1).join(' '))
129 if (ts.length < wrapWidth) ts += new Array(width - ts.length).join(' ')
132 // add left/right padding and print string.
133 paddingLeft = (row[c].padding || [0, 0, 0, 0])[left]
134 if (paddingLeft) str += new Array(row[c].padding[left] + 1).join(' ')
136 if (row[c].padding && row[c].padding[right]) str += new Array(row[c].padding[right] + 1).join(' ')
138 // if prior row is span, try to render the
139 // current row on the prior line.
140 if (r === 0 && lines.length > 0) {
141 str = _this._renderInline(str, lines[lines.length - 1], paddingLeft)
145 // remove trailing whitespace.
147 text: str.replace(/ +$/, ''),
155 // if the full 'source' can render in
156 // the target line, do so.
157 UI.prototype._renderInline = function (source, previousLine, paddingLeft) {
158 var target = previousLine.text,
161 if (!previousLine.span) return source
163 // if we're not applying wrapping logic,
164 // just always append to the span.
166 previousLine.hidden = true
167 return target + source
170 for (var i = 0, tc, sc; i < Math.max(source.length, target.length); i++) {
171 tc = target.charAt(i) || ' '
172 sc = source.charAt(i) || ' '
173 // we tried to overwrite a character in the other string.
174 if (tc !== ' ' && sc !== ' ') return source
175 // there is not enough whitespace to maintain padding.
176 if (sc !== ' ' && i < paddingLeft + target.length) return source
178 if (tc === ' ') str += sc
182 previousLine.hidden = true
187 UI.prototype._rasterize = function (row) {
192 widths = this._columnWidths(row),
195 // word wrap all columns, and create
196 // a data-structure that is easy to rasterize.
197 row.forEach(function (col, c) {
198 // leave room for left and right padding.
199 col.width = widths[c]
200 if (_this.wrap) wrapped = wrap.hard(_this._negatePadding(col))(col.text).split('\n')
201 else wrapped = col.text.split('\n')
203 // add top and bottom padding.
205 for (i = 0; i < (col.padding[top] || 0); i++) wrapped.unshift('')
206 for (i = 0; i < (col.padding[bottom] || 0); i++) wrapped.push('')
209 wrapped.forEach(function (str, r) {
210 if (!rrows[r]) rrows.push([])
214 for (var i = 0; i < c; i++) {
215 if (rrow[i] === undefined) rrow.push('')
224 UI.prototype._negatePadding = function (col) {
225 var wrapWidth = col.width
226 if (col.padding) wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0)
230 UI.prototype._columnWidths = function (row) {
235 remainingWidth = this.width
237 // column widths can be set in config.
238 row.forEach(function (col, i) {
241 widths[i] = col.width
242 remainingWidth -= col.width
244 widths[i] = undefined
248 // any unset widths should be calculated.
249 if (unset) unsetWidth = Math.floor(remainingWidth / unset)
250 widths.forEach(function (w, i) {
251 if (!_this.wrap) widths[i] = row[i].width || row[i].text.length
252 else if (w === undefined) widths[i] = Math.max(unsetWidth, _minWidth(row[i]))
258 // calculates the minimum width of
259 // a column, based on padding preferences.
260 function _minWidth (col) {
261 var padding = col.padding || []
263 return 1 + (padding[left] || 0) + (padding[right] || 0)
266 module.exports = function (opts) {
270 width: (opts || {}).width || 80,
271 wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true