1
|
var stringWidth = require('string-width')
|
2
|
var stripAnsi = require('strip-ansi')
|
3
|
var wrap = require('wrap-ansi')
|
4
|
var align = {
|
5
|
right: alignRight,
|
6
|
center: alignCenter
|
7
|
}
|
8
|
var top = 0
|
9
|
var right = 1
|
10
|
var bottom = 2
|
11
|
var left = 3
|
12
|
|
13
|
function UI (opts) {
|
14
|
this.width = opts.width
|
15
|
this.wrap = opts.wrap
|
16
|
this.rows = []
|
17
|
}
|
18
|
|
19
|
UI.prototype.span = function () {
|
20
|
var cols = this.div.apply(this, arguments)
|
21
|
cols.span = true
|
22
|
}
|
23
|
|
24
|
UI.prototype.resetOutput = function () {
|
25
|
this.rows = []
|
26
|
}
|
27
|
|
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])
|
32
|
}
|
33
|
|
34
|
var cols = []
|
35
|
|
36
|
for (var i = 0, arg; (arg = arguments[i]) !== undefined; i++) {
|
37
|
if (typeof arg === 'string') cols.push(this._colFromString(arg))
|
38
|
else cols.push(arg)
|
39
|
}
|
40
|
|
41
|
this.rows.push(cols)
|
42
|
return cols
|
43
|
}
|
44
|
|
45
|
UI.prototype._shouldApplyLayoutDSL = function () {
|
46
|
return arguments.length === 1 && typeof arguments[0] === 'string' &&
|
47
|
/[\t\n]/.test(arguments[0])
|
48
|
}
|
49
|
|
50
|
UI.prototype._applyLayoutDSL = function (str) {
|
51
|
var _this = this
|
52
|
var rows = str.split('\n')
|
53
|
var leftColumnWidth = 0
|
54
|
|
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])
|
65
|
)
|
66
|
}
|
67
|
})
|
68
|
|
69
|
// generate a table:
|
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) {
|
75
|
return {
|
76
|
text: r.trim(),
|
77
|
padding: _this._measurePadding(r),
|
78
|
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
|
79
|
}
|
80
|
}))
|
81
|
})
|
82
|
|
83
|
return this.rows[this.rows.length - 1]
|
84
|
}
|
85
|
|
86
|
UI.prototype._colFromString = function (str) {
|
87
|
return {
|
88
|
text: str,
|
89
|
padding: this._measurePadding(str)
|
90
|
}
|
91
|
}
|
92
|
|
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]
|
97
|
}
|
98
|
|
99
|
UI.prototype.toString = function () {
|
100
|
var _this = this
|
101
|
var lines = []
|
102
|
|
103
|
_this.rows.forEach(function (row, i) {
|
104
|
_this.rowToString(row, lines)
|
105
|
})
|
106
|
|
107
|
// don't display any lines with the
|
108
|
// hidden flag set.
|
109
|
lines = lines.filter(function (line) {
|
110
|
return !line.hidden
|
111
|
})
|
112
|
|
113
|
return lines.map(function (line) {
|
114
|
return line.text
|
115
|
}).join('\n')
|
116
|
}
|
117
|
|
118
|
UI.prototype.rowToString = function (row, lines) {
|
119
|
var _this = this
|
120
|
var padding
|
121
|
var rrows = this._rasterize(row)
|
122
|
var str = ''
|
123
|
var ts
|
124
|
var width
|
125
|
var wrapWidth
|
126
|
|
127
|
rrows.forEach(function (rrow, r) {
|
128
|
str = ''
|
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.
|
133
|
|
134
|
ts += col
|
135
|
|
136
|
for (var i = 0; i < wrapWidth - stringWidth(col); i++) {
|
137
|
ts += ' '
|
138
|
}
|
139
|
|
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(' ')
|
144
|
}
|
145
|
|
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, '| ')
|
150
|
str += ts
|
151
|
str += addBorder(row[c], ts, ' |')
|
152
|
if (padding[right]) str += new Array(padding[right] + 1).join(' ')
|
153
|
|
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])
|
158
|
}
|
159
|
})
|
160
|
|
161
|
// remove trailing whitespace.
|
162
|
lines.push({
|
163
|
text: str.replace(/ +$/, ''),
|
164
|
span: row.span
|
165
|
})
|
166
|
})
|
167
|
|
168
|
return lines
|
169
|
}
|
170
|
|
171
|
function addBorder (col, ts, style) {
|
172
|
if (col.border) {
|
173
|
if (/[.']-+[.']/.test(ts)) return ''
|
174
|
else if (ts.trim().length) return style
|
175
|
else return ' '
|
176
|
}
|
177
|
return ''
|
178
|
}
|
179
|
|
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())
|
186
|
|
187
|
if (!previousLine.span) return source
|
188
|
|
189
|
// if we're not applying wrapping logic,
|
190
|
// just always append to the span.
|
191
|
if (!this.wrap) {
|
192
|
previousLine.hidden = true
|
193
|
return target + source
|
194
|
}
|
195
|
|
196
|
if (leadingWhitespace < targetTextWidth) return source
|
197
|
|
198
|
previousLine.hidden = true
|
199
|
|
200
|
return target.trimRight() + new Array(leadingWhitespace - targetTextWidth + 1).join(' ') + source.trimLeft()
|
201
|
}
|
202
|
|
203
|
UI.prototype._rasterize = function (row) {
|
204
|
var _this = this
|
205
|
var i
|
206
|
var rrow
|
207
|
var rrows = []
|
208
|
var widths = this._columnWidths(row)
|
209
|
var wrapped
|
210
|
|
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')
|
218
|
|
219
|
if (col.border) {
|
220
|
wrapped.unshift('.' + new Array(_this._negatePadding(col) + 3).join('-') + '.')
|
221
|
wrapped.push("'" + new Array(_this._negatePadding(col) + 3).join('-') + "'")
|
222
|
}
|
223
|
|
224
|
// add top and bottom padding.
|
225
|
if (col.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('')
|
228
|
}
|
229
|
|
230
|
wrapped.forEach(function (str, r) {
|
231
|
if (!rrows[r]) rrows.push([])
|
232
|
|
233
|
rrow = rrows[r]
|
234
|
|
235
|
for (var i = 0; i < c; i++) {
|
236
|
if (rrow[i] === undefined) rrow.push('')
|
237
|
}
|
238
|
rrow.push(str)
|
239
|
})
|
240
|
})
|
241
|
|
242
|
return rrows
|
243
|
}
|
244
|
|
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
|
249
|
return wrapWidth
|
250
|
}
|
251
|
|
252
|
UI.prototype._columnWidths = function (row) {
|
253
|
var _this = this
|
254
|
var widths = []
|
255
|
var unset = row.length
|
256
|
var unsetWidth
|
257
|
var remainingWidth = this.width
|
258
|
|
259
|
// column widths can be set in config.
|
260
|
row.forEach(function (col, i) {
|
261
|
if (col.width) {
|
262
|
unset--
|
263
|
widths[i] = col.width
|
264
|
remainingWidth -= col.width
|
265
|
} else {
|
266
|
widths[i] = undefined
|
267
|
}
|
268
|
})
|
269
|
|
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]))
|
275
|
})
|
276
|
|
277
|
return widths
|
278
|
}
|
279
|
|
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
|
286
|
return minWidth
|
287
|
}
|
288
|
|
289
|
function getWindowWidth () {
|
290
|
if (typeof process === 'object' && process.stdout && process.stdout.columns) return process.stdout.columns
|
291
|
}
|
292
|
|
293
|
function alignRight (str, width) {
|
294
|
str = str.trim()
|
295
|
var padding = ''
|
296
|
var strWidth = stringWidth(str)
|
297
|
|
298
|
if (strWidth < width) {
|
299
|
padding = new Array(width - strWidth + 1).join(' ')
|
300
|
}
|
301
|
|
302
|
return padding + str
|
303
|
}
|
304
|
|
305
|
function alignCenter (str, width) {
|
306
|
str = str.trim()
|
307
|
var padding = ''
|
308
|
var strWidth = stringWidth(str.trim())
|
309
|
|
310
|
if (strWidth < width) {
|
311
|
padding = new Array(parseInt((width - strWidth) / 2, 10) + 1).join(' ')
|
312
|
}
|
313
|
|
314
|
return padding + str
|
315
|
}
|
316
|
|
317
|
module.exports = function (opts) {
|
318
|
opts = opts || {}
|
319
|
|
320
|
return new UI({
|
321
|
width: (opts || {}).width || getWindowWidth() || 80,
|
322
|
wrap: typeof opts.wrap === 'boolean' ? opts.wrap : true
|
323
|
})
|
324
|
}
|