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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// select — a compact dropdown over a fixed list of options.
|
|
2
|
+
//
|
|
3
|
+
// Where autocomplete is for *open* typing with a filtered menu, select is for a
|
|
4
|
+
// *closed* set you pick from. It shows one line (the current choice) until you
|
|
5
|
+
// open it, then a menu of options to choose from. Like autocomplete it splits
|
|
6
|
+
// rendering in two so a dropdown never reflows the layout:
|
|
7
|
+
//
|
|
8
|
+
// view() → the one-line control (always one line, stable width)
|
|
9
|
+
// menuView() → the dropdown rows, or '' when closed — overlay this where it
|
|
10
|
+
// fits (below a top-anchored control, above a bottom one)
|
|
11
|
+
//
|
|
12
|
+
// const fruit = select.create({
|
|
13
|
+
// options: ['apple', 'banana', 'cherry'],
|
|
14
|
+
// placeholder: 'pick one'
|
|
15
|
+
// }).focus()
|
|
16
|
+
// fruit.value() // 'apple', or null while nothing is chosen
|
|
17
|
+
//
|
|
18
|
+
// Key contract (mirrors the other field controls): while *closed* it consumes
|
|
19
|
+
// only space (to open) and never enter, so a parent form keeps enter for
|
|
20
|
+
// submit. While *open* it owns the menu — up/down move, enter or space commit,
|
|
21
|
+
// esc cancels — which is fine because a form won't submit with a menu open.
|
|
22
|
+
const key = require('../key')
|
|
23
|
+
const ansi = require('../ansi')
|
|
24
|
+
const { style } = require('../style')
|
|
25
|
+
|
|
26
|
+
const dim = (s) => ansi.modifierDim + s + ansi.modifierReset
|
|
27
|
+
|
|
28
|
+
const keys = {
|
|
29
|
+
open: key.binding({ keys: ['space'], help: { key: 'space', desc: 'open' } }),
|
|
30
|
+
up: key.binding({ keys: ['up', 'k'], help: { key: '↑/k', desc: 'up' } }),
|
|
31
|
+
down: key.binding({ keys: ['down', 'j'], help: { key: '↓/j', desc: 'down' } }),
|
|
32
|
+
commit: key.binding({ keys: ['enter', 'space'], help: { key: 'enter', desc: 'select' } }),
|
|
33
|
+
cancel: key.binding({ keys: ['esc'], help: { key: 'esc', desc: 'cancel' } })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Normalise an option to { label, value }. Bare values become their own label.
|
|
37
|
+
function normalize(opt) {
|
|
38
|
+
if (opt !== null && typeof opt === 'object') {
|
|
39
|
+
const value = 'value' in opt ? opt.value : opt.label
|
|
40
|
+
return { label: String(opt.label ?? opt.value ?? ''), value }
|
|
41
|
+
}
|
|
42
|
+
return { label: String(opt), value: opt }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class Select {
|
|
46
|
+
constructor(opts = {}) {
|
|
47
|
+
this.options = (opts.options || []).map(normalize)
|
|
48
|
+
this.selected = opts.selected ?? -1 // committed choice; -1 = none yet
|
|
49
|
+
this.placeholder = opts.placeholder || 'select…'
|
|
50
|
+
this.focused = !!opts.focused
|
|
51
|
+
this.maxVisible = opts.maxVisible || 6
|
|
52
|
+
this.openGlyph = opts.openGlyph || '▾'
|
|
53
|
+
|
|
54
|
+
this.open = false
|
|
55
|
+
this.highlight = 0 // cursor within the menu while open
|
|
56
|
+
this._clampSelected()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
focus() {
|
|
60
|
+
this.focused = true
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
blur() {
|
|
65
|
+
this.focused = false
|
|
66
|
+
this.open = false
|
|
67
|
+
return this
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The committed value, or null when nothing is chosen.
|
|
71
|
+
value() {
|
|
72
|
+
const o = this.options[this.selected]
|
|
73
|
+
return o ? o.value : null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
selectedOption() {
|
|
77
|
+
return this.options[this.selected] || null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setOptions(options) {
|
|
81
|
+
this.options = (options || []).map(normalize)
|
|
82
|
+
this._clampSelected()
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Select by value; no-op if absent.
|
|
87
|
+
setValue(v) {
|
|
88
|
+
const i = this.options.findIndex((o) => o.value === v)
|
|
89
|
+
if (i >= 0) this.selected = i
|
|
90
|
+
return this
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
update(msg) {
|
|
94
|
+
if (!this.focused || !msg || msg.type !== 'key') return [this, null]
|
|
95
|
+
|
|
96
|
+
if (!this.open) {
|
|
97
|
+
if (key.matches(msg, keys.open) && this.options.length) {
|
|
98
|
+
this.open = true
|
|
99
|
+
this.highlight = this.selected >= 0 ? this.selected : 0
|
|
100
|
+
}
|
|
101
|
+
return [this, null]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Menu is open: it owns navigation, commit and cancel.
|
|
105
|
+
if (key.matches(msg, keys.cancel)) this.open = false
|
|
106
|
+
else if (key.matches(msg, keys.up)) this._move(-1)
|
|
107
|
+
else if (key.matches(msg, keys.down)) this._move(1)
|
|
108
|
+
else if (key.matches(msg, keys.commit)) {
|
|
109
|
+
this.selected = this.highlight
|
|
110
|
+
this.open = false
|
|
111
|
+
}
|
|
112
|
+
return [this, null]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_move(delta) {
|
|
116
|
+
if (!this.options.length) return
|
|
117
|
+
this.highlight = Math.max(0, Math.min(this.highlight + delta, this.options.length - 1))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_clampSelected() {
|
|
121
|
+
if (this.selected < -1) this.selected = -1
|
|
122
|
+
if (this.selected > this.options.length - 1) this.selected = this.options.length - 1
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
view() {
|
|
126
|
+
const pointer = this.focused ? '› ' : ' '
|
|
127
|
+
const chosen = this.options[this.selected]
|
|
128
|
+
const label = chosen ? chosen.label : dim(this.placeholder)
|
|
129
|
+
return pointer + label + ' ' + this.openGlyph
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The dropdown, or '' when closed. Highlight is drawn black-on-magenta to
|
|
133
|
+
// match autocomplete's menu; a footer notes options scrolled out of view.
|
|
134
|
+
menuView() {
|
|
135
|
+
if (!this.open || !this.options.length) return ''
|
|
136
|
+
|
|
137
|
+
// Keep the highlight inside a maxVisible-row window.
|
|
138
|
+
const n = this.options.length
|
|
139
|
+
const start =
|
|
140
|
+
n > this.maxVisible
|
|
141
|
+
? Math.max(0, Math.min(this.highlight - this.maxVisible + 1, n - this.maxVisible))
|
|
142
|
+
: 0
|
|
143
|
+
const shown = this.options.slice(start, start + this.maxVisible)
|
|
144
|
+
|
|
145
|
+
const rows = shown.map((o, i) => {
|
|
146
|
+
const isSel = start + i === this.highlight
|
|
147
|
+
if (isSel) {
|
|
148
|
+
return style()
|
|
149
|
+
.foreground('black')
|
|
150
|
+
.background('magenta')
|
|
151
|
+
.render(' ' + o.label + ' ')
|
|
152
|
+
}
|
|
153
|
+
return ' ' + o.label
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
if (n > shown.length) rows.push(dim(` …${n - shown.length} more`))
|
|
157
|
+
return rows.join('\n')
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function create(opts) {
|
|
162
|
+
return new Select(opts)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { create, Select, keys }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// spinner — a Cmd-driven animated spinner.
|
|
2
|
+
//
|
|
3
|
+
// This is the reference for a *command-driven* component. It animates by
|
|
4
|
+
// re-issuing a tick Cmd each frame; the parent model just routes 'spinner.tick'
|
|
5
|
+
// Msgs into update() and threads the returned Cmd up to the Program:
|
|
6
|
+
//
|
|
7
|
+
// class App {
|
|
8
|
+
// constructor () { this.spinner = spinner.create() }
|
|
9
|
+
// init () { return this.spinner.init() }
|
|
10
|
+
// update (msg) {
|
|
11
|
+
// const [s, cmd] = this.spinner.update(msg)
|
|
12
|
+
// this.spinner = s
|
|
13
|
+
// return [this, cmd]
|
|
14
|
+
// }
|
|
15
|
+
// view () { return this.spinner.view() + ' loading…' }
|
|
16
|
+
// }
|
|
17
|
+
//
|
|
18
|
+
// Each spinner has a unique id and a monotonic tag so that strays — a second
|
|
19
|
+
// spinner's ticks, or a duplicated/late tick — can't double-drive the loop.
|
|
20
|
+
const { tick } = require('../commands')
|
|
21
|
+
|
|
22
|
+
// A few frame sets. `dots` is the default.
|
|
23
|
+
const dots = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
24
|
+
const line = ['|', '/', '-', '\\']
|
|
25
|
+
const points = ['∙∙∙', '●∙∙', '∙●∙', '∙∙●']
|
|
26
|
+
|
|
27
|
+
let nextId = 1
|
|
28
|
+
|
|
29
|
+
class Spinner {
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
this.frames = opts.frames || dots
|
|
32
|
+
this.fps = opts.fps || 10
|
|
33
|
+
this.frame = 0
|
|
34
|
+
this.id = nextId++ // distinguishes this spinner's ticks from any other's
|
|
35
|
+
this.tag = 0 // bumped each accepted tick; stale ticks are ignored
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Returns the Cmd that starts the animation. Call from the parent's init().
|
|
39
|
+
init() {
|
|
40
|
+
return this._tick()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_tick() {
|
|
44
|
+
const id = this.id
|
|
45
|
+
const tag = this.tag
|
|
46
|
+
const ms = Math.max(1, Math.round(1000 / this.fps))
|
|
47
|
+
return tick(ms, () => ({ type: 'spinner.tick', id, tag }))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
update(msg) {
|
|
51
|
+
if (!msg || msg.type !== 'spinner.tick') return [this, null]
|
|
52
|
+
// Ignore another spinner's ticks and any tick that isn't the one we're
|
|
53
|
+
// currently waiting on (duplicates, late fires).
|
|
54
|
+
if (msg.id !== this.id || msg.tag !== this.tag) return [this, null]
|
|
55
|
+
|
|
56
|
+
this.frame = (this.frame + 1) % this.frames.length
|
|
57
|
+
this.tag++
|
|
58
|
+
return [this, this._tick()]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
view() {
|
|
62
|
+
return this.frames[this.frame]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function create(opts) {
|
|
67
|
+
return new Spinner(opts)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { create, Spinner, dots, line, points }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// stopwatch — counts elapsed time upward.
|
|
2
|
+
//
|
|
3
|
+
// Cmd-driven like the spinner: start() returns a tick Cmd, and each accepted
|
|
4
|
+
// 'stopwatch.tick' advances `elapsed` and re-issues the next tick. id + tag
|
|
5
|
+
// guard against strays so pausing/resuming can't double-drive it.
|
|
6
|
+
//
|
|
7
|
+
// this.sw = stopwatch.create()
|
|
8
|
+
// init() { return this.sw.start() }
|
|
9
|
+
// update(msg) { const [sw, cmd] = this.sw.update(msg); this.sw = sw; return [this, cmd] }
|
|
10
|
+
// view() { return this.sw.view() } // "01:23"
|
|
11
|
+
const { tick } = require('../commands')
|
|
12
|
+
|
|
13
|
+
let nextId = 1
|
|
14
|
+
|
|
15
|
+
function pad(n) {
|
|
16
|
+
return String(n).padStart(2, '0')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ms → "M:SS" / "MM:SS" / "H:MM:SS"
|
|
20
|
+
function format(ms) {
|
|
21
|
+
const total = Math.floor(ms / 1000)
|
|
22
|
+
const h = Math.floor(total / 3600)
|
|
23
|
+
const m = Math.floor((total % 3600) / 60)
|
|
24
|
+
const s = total % 60
|
|
25
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class Stopwatch {
|
|
29
|
+
constructor(opts = {}) {
|
|
30
|
+
this.interval = opts.interval || 1000
|
|
31
|
+
this.elapsed = opts.elapsed || 0
|
|
32
|
+
this.running = false
|
|
33
|
+
this.id = nextId++
|
|
34
|
+
this.tag = 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_tick() {
|
|
38
|
+
const id = this.id
|
|
39
|
+
const tag = this.tag
|
|
40
|
+
return tick(this.interval, () => ({ type: 'stopwatch.tick', id, tag }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
start() {
|
|
44
|
+
if (this.running) return null
|
|
45
|
+
this.running = true
|
|
46
|
+
return this._tick()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
stop() {
|
|
50
|
+
this.running = false
|
|
51
|
+
this.tag++ // invalidate any in-flight tick
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toggle() {
|
|
56
|
+
return this.running ? this.stop() : this.start()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
reset() {
|
|
60
|
+
this.elapsed = 0
|
|
61
|
+
return null // a running stopwatch keeps ticking
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
update(msg) {
|
|
65
|
+
if (
|
|
66
|
+
!msg ||
|
|
67
|
+
msg.type !== 'stopwatch.tick' ||
|
|
68
|
+
msg.id !== this.id ||
|
|
69
|
+
msg.tag !== this.tag ||
|
|
70
|
+
!this.running
|
|
71
|
+
) {
|
|
72
|
+
return [this, null]
|
|
73
|
+
}
|
|
74
|
+
this.elapsed += this.interval
|
|
75
|
+
this.tag++
|
|
76
|
+
return [this, this._tick()]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
view() {
|
|
80
|
+
return format(this.elapsed)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function create(opts) {
|
|
85
|
+
return new Stopwatch(opts)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { create, Stopwatch, format }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// table — fixed-width columns with selectable, scrolling rows.
|
|
2
|
+
//
|
|
3
|
+
// const t = table.create({
|
|
4
|
+
// columns: [{ title: 'Name', width: 12 }, { title: 'Lang', width: 8 }],
|
|
5
|
+
// rows: [['corestore', 'js'], ['hypercore', 'js']],
|
|
6
|
+
// height: 8
|
|
7
|
+
// })
|
|
8
|
+
//
|
|
9
|
+
// Cells are truncated/padded to their column width (ANSI-aware), the selection
|
|
10
|
+
// is a reverse-video bar, and the body scrolls in a `height`-row window. Like
|
|
11
|
+
// list/viewport it always responds to its keys — the parent decides routing.
|
|
12
|
+
const key = require('../key')
|
|
13
|
+
const { style, width, truncate } = require('../style')
|
|
14
|
+
|
|
15
|
+
const header = (s) => style().bold(true).render(s)
|
|
16
|
+
const selected = (s) => style().reverse(true).render(s)
|
|
17
|
+
const dim = (s) => style().faint(true).render(s)
|
|
18
|
+
|
|
19
|
+
const keys = {
|
|
20
|
+
up: key.binding({ keys: ['up', 'k'], help: { key: '↑/k', desc: 'up' } }),
|
|
21
|
+
down: key.binding({ keys: ['down', 'j'], help: { key: '↓/j', desc: 'down' } }),
|
|
22
|
+
pageUp: key.binding({ keys: ['pageup'] }),
|
|
23
|
+
pageDown: key.binding({ keys: ['pagedown'] }),
|
|
24
|
+
top: key.binding({ keys: ['home'], help: { key: 'home', desc: 'top' } }),
|
|
25
|
+
bottom: key.binding({ keys: ['end'], help: { key: 'end', desc: 'bottom' } })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Truncate/pad a value to exactly `w` visible cells.
|
|
29
|
+
function cell(value, w) {
|
|
30
|
+
const text = String(value ?? '')
|
|
31
|
+
if (width(text) > w) return truncate(text, w)
|
|
32
|
+
return text + ' '.repeat(w - width(text))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class Table {
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
this.columns = opts.columns || []
|
|
38
|
+
this.rows = opts.rows || []
|
|
39
|
+
this.height = opts.height || 10 // visible body rows
|
|
40
|
+
this.rule = opts.rule || '─'
|
|
41
|
+
this.cursor = 0
|
|
42
|
+
this.offset = 0
|
|
43
|
+
this._clamp()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get totalWidth() {
|
|
47
|
+
if (!this.columns.length) return 0
|
|
48
|
+
const cols = this.columns.reduce((sum, c) => sum + c.width, 0)
|
|
49
|
+
return cols + (this.columns.length - 1) // single-space gutters
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
selectedRow() {
|
|
53
|
+
return this.rows[this.cursor] || null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setRows(rows) {
|
|
57
|
+
this.rows = rows
|
|
58
|
+
this._clamp()
|
|
59
|
+
return this
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setColumns(columns) {
|
|
63
|
+
this.columns = columns
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
gotoTop() {
|
|
68
|
+
this.cursor = 0
|
|
69
|
+
this.offset = 0
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
gotoBottom() {
|
|
74
|
+
this.cursor = Math.max(0, this.rows.length - 1)
|
|
75
|
+
this.offset = Math.max(0, this.rows.length - this.height)
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_move(delta) {
|
|
80
|
+
if (!this.rows.length) return
|
|
81
|
+
this.cursor = Math.max(0, Math.min(this.cursor + delta, this.rows.length - 1))
|
|
82
|
+
if (this.cursor < this.offset) this.offset = this.cursor
|
|
83
|
+
else if (this.cursor >= this.offset + this.height) {
|
|
84
|
+
this.offset = this.cursor - this.height + 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_clamp() {
|
|
89
|
+
this.cursor = Math.max(0, Math.min(this.cursor, Math.max(0, this.rows.length - 1)))
|
|
90
|
+
const maxOffset = Math.max(0, this.rows.length - this.height)
|
|
91
|
+
this.offset = Math.max(0, Math.min(this.offset, maxOffset))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
update(msg) {
|
|
95
|
+
if (!msg || msg.type !== 'key') return [this, null]
|
|
96
|
+
if (key.matches(msg, keys.up)) this._move(-1)
|
|
97
|
+
else if (key.matches(msg, keys.down)) this._move(1)
|
|
98
|
+
else if (key.matches(msg, keys.pageUp)) this._move(-this.height)
|
|
99
|
+
else if (key.matches(msg, keys.pageDown)) this._move(this.height)
|
|
100
|
+
else if (key.matches(msg, keys.top)) this.gotoTop()
|
|
101
|
+
else if (key.matches(msg, keys.bottom)) this.gotoBottom()
|
|
102
|
+
return [this, null]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_row(cells) {
|
|
106
|
+
return this.columns.map((c, i) => cell(cells[i], c.width)).join(' ')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
view() {
|
|
110
|
+
const lines = []
|
|
111
|
+
lines.push(header(this._row(this.columns.map((c) => c.title))))
|
|
112
|
+
lines.push(dim(this.rule.repeat(this.totalWidth)))
|
|
113
|
+
|
|
114
|
+
const end = Math.min(this.offset + this.height, this.rows.length)
|
|
115
|
+
const body = []
|
|
116
|
+
for (let i = this.offset; i < end; i++) {
|
|
117
|
+
const line = this._row(this.rows[i])
|
|
118
|
+
body.push(i === this.cursor ? selected(line) : line)
|
|
119
|
+
}
|
|
120
|
+
while (body.length < this.height) body.push(' '.repeat(this.totalWidth))
|
|
121
|
+
|
|
122
|
+
return lines.concat(body).join('\n')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function create(opts) {
|
|
127
|
+
return new Table(opts)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { create, Table, keys }
|