bare-tui 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +261 -0
- package/ansi.js +34 -0
- package/commands.js +67 -0
- package/components/autocomplete.js +208 -0
- package/components/checkbox.js +68 -0
- package/components/filepicker.js +232 -0
- package/components/focus.js +129 -0
- package/components/help.js +117 -0
- package/components/list.js +172 -0
- package/components/paginator.js +111 -0
- package/components/progress.js +78 -0
- package/components/radio.js +113 -0
- package/components/select.js +165 -0
- package/components/spinner.js +70 -0
- package/components/stopwatch.js +88 -0
- package/components/table.js +130 -0
- package/components/textarea.js +279 -0
- package/components/textinput.js +135 -0
- package/components/timer.js +91 -0
- package/components/viewport.js +104 -0
- package/docs/autocomplete.md +80 -0
- package/docs/checkbox.md +43 -0
- package/docs/filepicker.md +80 -0
- package/docs/focus.md +68 -0
- package/docs/help.md +45 -0
- package/docs/list.md +47 -0
- package/docs/paginator.md +43 -0
- package/docs/progress.md +36 -0
- package/docs/radio.md +49 -0
- package/docs/select.md +54 -0
- package/docs/spinner.md +54 -0
- package/docs/stopwatch.md +52 -0
- package/docs/table.md +52 -0
- package/docs/textarea.md +43 -0
- package/docs/textinput.md +51 -0
- package/docs/timer.md +57 -0
- package/docs/viewport.md +39 -0
- package/index.d.ts +1 -0
- package/index.js +71 -0
- package/key.js +29 -0
- package/messages.js +62 -0
- package/mouse.js +102 -0
- package/package.json +59 -1
- package/program.js +387 -0
- package/renderer.js +67 -0
- package/style.js +473 -0
package/style.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// style — terminal layout & styling, in the spirit of Charm's lipgloss.
|
|
2
|
+
//
|
|
3
|
+
// const { style } = require('./lib/tea')
|
|
4
|
+
// style().bold(true).foreground('205').padding(1, 2).border(style.borders.rounded).render('hi')
|
|
5
|
+
//
|
|
6
|
+
// A Style is immutable and chainable: every setter returns a new Style, and
|
|
7
|
+
// render(text) applies the whole declaration in one pass — width/align, then
|
|
8
|
+
// padding, then text styling (so a background fills the padding), then border,
|
|
9
|
+
// then margin.
|
|
10
|
+
//
|
|
11
|
+
// Everything is built on visible-width measurement (`width`) that ignores ANSI
|
|
12
|
+
// escapes and counts wide glyphs as two cells — without that, borders and the
|
|
13
|
+
// join helpers would drift the instant styled text passes through them.
|
|
14
|
+
const { constants } = require('bare-ansi-escapes')
|
|
15
|
+
const ansi = require('./ansi')
|
|
16
|
+
|
|
17
|
+
const CSI = constants.CSI
|
|
18
|
+
const RESET = ansi.modifierReset
|
|
19
|
+
|
|
20
|
+
// ── width measurement ──────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g
|
|
23
|
+
const ANSI_STICKY = /\x1b\[[0-9;?]*[A-Za-z]/y
|
|
24
|
+
|
|
25
|
+
function stripAnsi(str) {
|
|
26
|
+
return String(str).replace(ANSI_RE, '')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Cells occupied by a single code point: 0 for control/combining/zero-width,
|
|
30
|
+
// 2 for wide (CJK, fullwidth, most emoji), 1 otherwise. An approximation of
|
|
31
|
+
// wcwidth covering the ranges that actually show up in TUIs.
|
|
32
|
+
function charWidth(cp) {
|
|
33
|
+
if (cp === 0) return 0
|
|
34
|
+
if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0 // C0/C1 control
|
|
35
|
+
if (isZeroWidth(cp)) return 0
|
|
36
|
+
if (isWide(cp)) return 2
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isZeroWidth(cp) {
|
|
41
|
+
return (
|
|
42
|
+
(cp >= 0x0300 && cp <= 0x036f) || // combining diacriticals
|
|
43
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
44
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
45
|
+
(cp >= 0x20d0 && cp <= 0x20ff) || // combining marks for symbols
|
|
46
|
+
(cp >= 0xfe20 && cp <= 0xfe2f) ||
|
|
47
|
+
cp === 0x200b || // zero-width space
|
|
48
|
+
(cp >= 0x200c && cp <= 0x200f) ||
|
|
49
|
+
cp === 0xfeff
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isWide(cp) {
|
|
54
|
+
return (
|
|
55
|
+
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
56
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals … punctuation
|
|
57
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana … CJK compat
|
|
58
|
+
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext A
|
|
59
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
|
|
60
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
61
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
62
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK compat ideographs
|
|
63
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK compat forms
|
|
64
|
+
(cp >= 0xff00 && cp <= 0xff60) || // fullwidth forms
|
|
65
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
66
|
+
(cp >= 0x1f300 && cp <= 0x1faff) || // emoji & symbols
|
|
67
|
+
(cp >= 0x20000 && cp <= 0x3fffd) // CJK Ext B+
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Visible width of a single line (no newlines expected here).
|
|
72
|
+
function lineWidth(line) {
|
|
73
|
+
let w = 0
|
|
74
|
+
for (const ch of stripAnsi(line)) w += charWidth(ch.codePointAt(0))
|
|
75
|
+
return w
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Visible width of a block: the widest line.
|
|
79
|
+
function width(str) {
|
|
80
|
+
let w = 0
|
|
81
|
+
for (const line of String(str).split('\n')) w = Math.max(w, lineWidth(line))
|
|
82
|
+
return w
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Line count of a block.
|
|
86
|
+
function height(str) {
|
|
87
|
+
return String(str).split('\n').length
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Truncate to `w` visible cells, preserving (and closing) escape sequences.
|
|
91
|
+
function truncate(str, w) {
|
|
92
|
+
if (w <= 0) return ''
|
|
93
|
+
let out = ''
|
|
94
|
+
let used = 0
|
|
95
|
+
let sawAnsi = false
|
|
96
|
+
let i = 0
|
|
97
|
+
while (i < str.length) {
|
|
98
|
+
if (str[i] === '\x1b') {
|
|
99
|
+
ANSI_STICKY.lastIndex = i
|
|
100
|
+
const m = ANSI_STICKY.exec(str)
|
|
101
|
+
if (m) {
|
|
102
|
+
out += m[0]
|
|
103
|
+
sawAnsi = true
|
|
104
|
+
i = ANSI_STICKY.lastIndex
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const cp = str.codePointAt(i)
|
|
109
|
+
const ch = String.fromCodePoint(cp)
|
|
110
|
+
const cw = charWidth(cp)
|
|
111
|
+
if (used + cw > w) break
|
|
112
|
+
out += ch
|
|
113
|
+
used += cw
|
|
114
|
+
i += ch.length
|
|
115
|
+
}
|
|
116
|
+
if (sawAnsi) out += RESET
|
|
117
|
+
return out
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Pad (or truncate) a line to `w` cells, positioning content by `pos`
|
|
121
|
+
// (0 left, 0.5 center, 1 right).
|
|
122
|
+
function padLine(line, w, pos = 0) {
|
|
123
|
+
const lw = lineWidth(line)
|
|
124
|
+
if (lw > w) return truncate(line, w)
|
|
125
|
+
const space = w - lw
|
|
126
|
+
if (space === 0) return line
|
|
127
|
+
if (pos <= 0) return line + ' '.repeat(space)
|
|
128
|
+
if (pos >= 1) return ' '.repeat(space) + line
|
|
129
|
+
const left = Math.floor(space * pos)
|
|
130
|
+
return ' '.repeat(left) + line + ' '.repeat(space - left)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── colors ───────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
const NAMED = {
|
|
136
|
+
black: 30,
|
|
137
|
+
red: 31,
|
|
138
|
+
green: 32,
|
|
139
|
+
yellow: 33,
|
|
140
|
+
blue: 34,
|
|
141
|
+
magenta: 35,
|
|
142
|
+
cyan: 36,
|
|
143
|
+
white: 37,
|
|
144
|
+
default: 39,
|
|
145
|
+
gray: 90,
|
|
146
|
+
grey: 90,
|
|
147
|
+
brightblack: 90,
|
|
148
|
+
brightred: 91,
|
|
149
|
+
brightgreen: 92,
|
|
150
|
+
brightyellow: 93,
|
|
151
|
+
brightblue: 94,
|
|
152
|
+
brightmagenta: 95,
|
|
153
|
+
brightcyan: 96,
|
|
154
|
+
brightwhite: 97
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Resolve a color spec to SGR params. Accepts a named color, a 0–255 ANSI-256
|
|
158
|
+
// index (number or numeric string), or a #rgb / #rrggbb truecolor hex.
|
|
159
|
+
function colorParams(spec, bg) {
|
|
160
|
+
if (spec === undefined || spec === null || spec === '') return []
|
|
161
|
+
const lead = bg ? 48 : 38
|
|
162
|
+
|
|
163
|
+
if (typeof spec === 'number') return [lead, 5, spec & 255]
|
|
164
|
+
|
|
165
|
+
const s = String(spec)
|
|
166
|
+
if (s[0] === '#') {
|
|
167
|
+
let hex = s.slice(1)
|
|
168
|
+
if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c)
|
|
169
|
+
const n = parseInt(hex, 16)
|
|
170
|
+
return [lead, 2, (n >> 16) & 255, (n >> 8) & 255, n & 255]
|
|
171
|
+
}
|
|
172
|
+
const name = s.toLowerCase()
|
|
173
|
+
if (name in NAMED) return [bg ? NAMED[name] + 10 : NAMED[name]]
|
|
174
|
+
if (/^\d+$/.test(s)) return [lead, 5, parseInt(s, 10) & 255]
|
|
175
|
+
return []
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sgr(params) {
|
|
179
|
+
return params.length ? CSI + params.join(';') + 'm' : ''
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── borders ────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
const borders = {
|
|
185
|
+
normal: {
|
|
186
|
+
topLeft: '┌',
|
|
187
|
+
top: '─',
|
|
188
|
+
topRight: '┐',
|
|
189
|
+
left: '│',
|
|
190
|
+
right: '│',
|
|
191
|
+
bottomLeft: '└',
|
|
192
|
+
bottom: '─',
|
|
193
|
+
bottomRight: '┘'
|
|
194
|
+
},
|
|
195
|
+
rounded: {
|
|
196
|
+
topLeft: '╭',
|
|
197
|
+
top: '─',
|
|
198
|
+
topRight: '╮',
|
|
199
|
+
left: '│',
|
|
200
|
+
right: '│',
|
|
201
|
+
bottomLeft: '╰',
|
|
202
|
+
bottom: '─',
|
|
203
|
+
bottomRight: '╯'
|
|
204
|
+
},
|
|
205
|
+
thick: {
|
|
206
|
+
topLeft: '┏',
|
|
207
|
+
top: '━',
|
|
208
|
+
topRight: '┓',
|
|
209
|
+
left: '┃',
|
|
210
|
+
right: '┃',
|
|
211
|
+
bottomLeft: '┗',
|
|
212
|
+
bottom: '━',
|
|
213
|
+
bottomRight: '┛'
|
|
214
|
+
},
|
|
215
|
+
double: {
|
|
216
|
+
topLeft: '╔',
|
|
217
|
+
top: '═',
|
|
218
|
+
topRight: '╗',
|
|
219
|
+
left: '║',
|
|
220
|
+
right: '║',
|
|
221
|
+
bottomLeft: '╚',
|
|
222
|
+
bottom: '═',
|
|
223
|
+
bottomRight: '╝'
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── positions ────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
const position = { top: 0, left: 0, center: 0.5, right: 1, bottom: 1 }
|
|
230
|
+
|
|
231
|
+
// CSS-like side shorthand → [top, right, bottom, left].
|
|
232
|
+
function sides(args) {
|
|
233
|
+
const a = args.map((n) => n || 0)
|
|
234
|
+
if (a.length <= 1) return [a[0] || 0, a[0] || 0, a[0] || 0, a[0] || 0]
|
|
235
|
+
if (a.length === 2) return [a[0], a[1], a[0], a[1]]
|
|
236
|
+
if (a.length === 3) return [a[0], a[1], a[2], a[1]]
|
|
237
|
+
return [a[0], a[1], a[2], a[3]]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Style ────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
class Style {
|
|
243
|
+
constructor(props = {}) {
|
|
244
|
+
this.props = props
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_with(patch) {
|
|
248
|
+
return new Style({ ...this.props, ...patch })
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
bold(v = true) {
|
|
252
|
+
return this._with({ bold: v })
|
|
253
|
+
}
|
|
254
|
+
faint(v = true) {
|
|
255
|
+
return this._with({ faint: v })
|
|
256
|
+
}
|
|
257
|
+
italic(v = true) {
|
|
258
|
+
return this._with({ italic: v })
|
|
259
|
+
}
|
|
260
|
+
underline(v = true) {
|
|
261
|
+
return this._with({ underline: v })
|
|
262
|
+
}
|
|
263
|
+
strikethrough(v = true) {
|
|
264
|
+
return this._with({ strikethrough: v })
|
|
265
|
+
}
|
|
266
|
+
reverse(v = true) {
|
|
267
|
+
return this._with({ reverse: v })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
foreground(c) {
|
|
271
|
+
return this._with({ fg: c })
|
|
272
|
+
}
|
|
273
|
+
background(c) {
|
|
274
|
+
return this._with({ bg: c })
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
width(n) {
|
|
278
|
+
return this._with({ width: n })
|
|
279
|
+
}
|
|
280
|
+
height(n) {
|
|
281
|
+
return this._with({ height: n })
|
|
282
|
+
}
|
|
283
|
+
align(pos) {
|
|
284
|
+
return this._with({ align: pos })
|
|
285
|
+
}
|
|
286
|
+
alignVertical(pos) {
|
|
287
|
+
return this._with({ alignV: pos })
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
padding(...v) {
|
|
291
|
+
return this._with({ padding: sides(v) })
|
|
292
|
+
}
|
|
293
|
+
margin(...v) {
|
|
294
|
+
return this._with({ margin: sides(v) })
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
border(chars, ...sidesOn) {
|
|
298
|
+
const on = sidesOn.length ? sides(sidesOn).map(Boolean) : [true, true, true, true]
|
|
299
|
+
return this._with({ border: chars, borderSides: on })
|
|
300
|
+
}
|
|
301
|
+
borderForeground(c) {
|
|
302
|
+
return this._with({ borderFg: c })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// SGR for text styling, opened once per inner line and closed with RESET.
|
|
306
|
+
_open() {
|
|
307
|
+
const p = this.props
|
|
308
|
+
const params = []
|
|
309
|
+
if (p.bold) params.push(1)
|
|
310
|
+
if (p.faint) params.push(2)
|
|
311
|
+
if (p.italic) params.push(3)
|
|
312
|
+
if (p.underline) params.push(4)
|
|
313
|
+
if (p.reverse) params.push(7)
|
|
314
|
+
if (p.strikethrough) params.push(9)
|
|
315
|
+
params.push(...colorParams(p.fg, false))
|
|
316
|
+
params.push(...colorParams(p.bg, true))
|
|
317
|
+
return sgr(params)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
render(text) {
|
|
321
|
+
const p = this.props
|
|
322
|
+
const pad = p.padding || [0, 0, 0, 0]
|
|
323
|
+
const mar = p.margin || [0, 0, 0, 0]
|
|
324
|
+
const align = p.align || 0
|
|
325
|
+
let lines = String(text).split('\n')
|
|
326
|
+
|
|
327
|
+
// A rectangular block is required once anything needs to fill horizontally.
|
|
328
|
+
const block =
|
|
329
|
+
!!p.border ||
|
|
330
|
+
(p.bg !== undefined && p.bg !== null) ||
|
|
331
|
+
!!p.width ||
|
|
332
|
+
align !== 0 ||
|
|
333
|
+
pad[0] ||
|
|
334
|
+
pad[1] ||
|
|
335
|
+
pad[2] ||
|
|
336
|
+
pad[3]
|
|
337
|
+
|
|
338
|
+
// 1. width + horizontal alignment
|
|
339
|
+
const contentW = p.width || width(lines.join('\n'))
|
|
340
|
+
if (block) lines = lines.map((l) => padLine(l, contentW, align))
|
|
341
|
+
else lines = lines.map((l) => (lineWidth(l) > contentW ? truncate(l, contentW) : l))
|
|
342
|
+
|
|
343
|
+
// 2. fixed height (vertical alignment)
|
|
344
|
+
if (p.height) lines = fitHeight(lines, p.height, contentW, p.alignV || 0)
|
|
345
|
+
|
|
346
|
+
// 3. horizontal + vertical padding
|
|
347
|
+
const innerW = contentW + pad[1] + pad[3]
|
|
348
|
+
if (pad[1] || pad[3]) {
|
|
349
|
+
const l = ' '.repeat(pad[3])
|
|
350
|
+
const r = ' '.repeat(pad[1])
|
|
351
|
+
lines = lines.map((line) => l + line + r)
|
|
352
|
+
}
|
|
353
|
+
const blank = ' '.repeat(innerW)
|
|
354
|
+
for (let i = 0; i < pad[0]; i++) lines.unshift(blank)
|
|
355
|
+
for (let i = 0; i < pad[2]; i++) lines.push(blank)
|
|
356
|
+
|
|
357
|
+
// 4. text styling — wraps padding too so a background fills the box.
|
|
358
|
+
// Re-apply the block's SGR after every reset *inside* the content, so a
|
|
359
|
+
// background (or any attribute) covers the whole line instead of dying at
|
|
360
|
+
// the first nested span's reset and leaving the remainder unstyled.
|
|
361
|
+
const open = this._open()
|
|
362
|
+
if (open) lines = lines.map((line) => open + line.split(RESET).join(RESET + open) + RESET)
|
|
363
|
+
|
|
364
|
+
// 5. border
|
|
365
|
+
if (p.border) lines = applyBorder(lines, innerW, p.border, p.borderSides, p.borderFg)
|
|
366
|
+
|
|
367
|
+
// 6. margin (transparent)
|
|
368
|
+
if (mar[3] || mar[1]) {
|
|
369
|
+
const l = ' '.repeat(mar[3])
|
|
370
|
+
const r = ' '.repeat(mar[1])
|
|
371
|
+
lines = lines.map((line) => l + line + r)
|
|
372
|
+
}
|
|
373
|
+
const fullW = width(lines.join('\n'))
|
|
374
|
+
const marginBlank = ' '.repeat(fullW)
|
|
375
|
+
for (let i = 0; i < mar[0]; i++) lines.unshift(marginBlank)
|
|
376
|
+
for (let i = 0; i < mar[2]; i++) lines.push(marginBlank)
|
|
377
|
+
|
|
378
|
+
return lines.join('\n')
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function fitHeight(lines, h, w, posV) {
|
|
383
|
+
if (lines.length >= h) return lines.slice(0, h)
|
|
384
|
+
const extra = h - lines.length
|
|
385
|
+
const before = posV <= 0 ? 0 : posV >= 1 ? extra : Math.floor(extra * posV)
|
|
386
|
+
const blank = ' '.repeat(w)
|
|
387
|
+
return [...Array(before).fill(blank), ...lines, ...Array(extra - before).fill(blank)]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function applyBorder(lines, innerW, chars, on, fg) {
|
|
391
|
+
const [t, r, b, l] = on
|
|
392
|
+
const paint = (s) => {
|
|
393
|
+
const params = colorParams(fg, false)
|
|
394
|
+
return params.length ? sgr(params) + s + RESET : s
|
|
395
|
+
}
|
|
396
|
+
const out = []
|
|
397
|
+
if (t) {
|
|
398
|
+
out.push(paint((l ? chars.topLeft : '') + chars.top.repeat(innerW) + (r ? chars.topRight : '')))
|
|
399
|
+
}
|
|
400
|
+
const left = l ? paint(chars.left) : ''
|
|
401
|
+
const right = r ? paint(chars.right) : ''
|
|
402
|
+
for (const line of lines) out.push(left + line + right)
|
|
403
|
+
if (b) {
|
|
404
|
+
out.push(
|
|
405
|
+
paint(
|
|
406
|
+
(l ? chars.bottomLeft : '') + chars.bottom.repeat(innerW) + (r ? chars.bottomRight : '')
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
return out
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── joins ──────────────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
// Place blocks side by side, aligning their differing heights by `pos`
|
|
416
|
+
// (0 top, 0.5 center, 1 bottom).
|
|
417
|
+
function joinHorizontal(pos, ...blocks) {
|
|
418
|
+
const cols = blocks.map((b) => String(b).split('\n'))
|
|
419
|
+
const widths = cols.map((lines) => width(lines.join('\n')))
|
|
420
|
+
const h = Math.max(...cols.map((lines) => lines.length))
|
|
421
|
+
|
|
422
|
+
const padded = cols.map((lines, i) => {
|
|
423
|
+
const w = widths[i]
|
|
424
|
+
const filled = lines.map((line) => padLine(line, w, 0))
|
|
425
|
+
const extra = h - filled.length
|
|
426
|
+
const before = pos <= 0 ? 0 : pos >= 1 ? extra : Math.floor(extra * pos)
|
|
427
|
+
const blank = ' '.repeat(w)
|
|
428
|
+
return [...Array(before).fill(blank), ...filled, ...Array(extra - before).fill(blank)]
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const out = []
|
|
432
|
+
for (let row = 0; row < h; row++) out.push(padded.map((c) => c[row]).join(''))
|
|
433
|
+
return out.join('\n')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Stack blocks vertically, aligning their differing widths by `pos`
|
|
437
|
+
// (0 left, 0.5 center, 1 right).
|
|
438
|
+
function joinVertical(pos, ...blocks) {
|
|
439
|
+
const cols = blocks.map((b) => String(b).split('\n'))
|
|
440
|
+
const w = Math.max(...cols.map((lines) => width(lines.join('\n'))))
|
|
441
|
+
const out = []
|
|
442
|
+
for (const lines of cols) {
|
|
443
|
+
for (const line of lines) out.push(padLine(line, w, pos))
|
|
444
|
+
}
|
|
445
|
+
return out.join('\n')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// The public entry point is the `style` factory, with helpers attached.
|
|
449
|
+
function style() {
|
|
450
|
+
return new Style()
|
|
451
|
+
}
|
|
452
|
+
style.Style = Style
|
|
453
|
+
style.borders = borders
|
|
454
|
+
style.position = position
|
|
455
|
+
style.joinHorizontal = joinHorizontal
|
|
456
|
+
style.joinVertical = joinVertical
|
|
457
|
+
style.width = width
|
|
458
|
+
style.height = height
|
|
459
|
+
style.truncate = truncate
|
|
460
|
+
style.stripAnsi = stripAnsi
|
|
461
|
+
|
|
462
|
+
module.exports = {
|
|
463
|
+
style,
|
|
464
|
+
Style,
|
|
465
|
+
borders,
|
|
466
|
+
position,
|
|
467
|
+
joinHorizontal,
|
|
468
|
+
joinVertical,
|
|
469
|
+
width,
|
|
470
|
+
height,
|
|
471
|
+
truncate,
|
|
472
|
+
stripAnsi
|
|
473
|
+
}
|