@trendr/core 0.1.0

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/src/select.js ADDED
@@ -0,0 +1,151 @@
1
+ import { jsx } from '../jsx-runtime.js'
2
+ import { createSignal } from './signal.js'
3
+ import { useInput, useTheme } from './hooks.js'
4
+ import { registerOverlay, getInstanceLayout, getContext } from './renderer.js'
5
+
6
+ export function Select({ items, selected, onSelect, focused = false, overlay = false, maxVisible = 10, placeholder = 'select...', renderItem, style: userStyle }) {
7
+ const { accent = 'cyan' } = useTheme()
8
+ const defaults = {
9
+ border: 'single',
10
+ borderColor: accent,
11
+ bg: null,
12
+ cursorBg: accent,
13
+ cursorTextColor: 'black',
14
+ color: null,
15
+ focusedBg: accent,
16
+ focusedColor: 'black',
17
+ }
18
+ const s = { ...defaults, ...userStyle }
19
+
20
+ const [open, setOpen] = createSignal(false)
21
+ const [cursor, setCursor] = createSignal(0)
22
+ const [scroll, setScroll] = createSignal(0)
23
+
24
+ useInput((event) => {
25
+ if (!focused) return
26
+ const { key } = event
27
+
28
+ if (!open()) {
29
+ if (key === 'return' || key === 'space') {
30
+ const idx = items.indexOf(selected)
31
+ setCursor(idx >= 0 ? idx : 0)
32
+ setOpen(true)
33
+ event.stopPropagation()
34
+ }
35
+ return
36
+ }
37
+
38
+ const len = items.length
39
+ if (key === 'up' || key === 'k') { setCursor(c => Math.max(0, c - 1)); event.stopPropagation() }
40
+ else if (key === 'down' || key === 'j') { setCursor(c => Math.min(len - 1, c + 1)); event.stopPropagation() }
41
+ else if (key === 'return' || key === 'space') { onSelect?.(items[cursor()]); setOpen(false); event.stopPropagation() }
42
+ else if (key === 'escape') { setOpen(false); event.stopPropagation() }
43
+ })
44
+
45
+ const display = selected ?? placeholder
46
+ const collapsed = jsx('text', {
47
+ style: {
48
+ bg: focused ? s.focusedBg : null,
49
+ color: focused ? s.focusedColor : (selected ? s.color : 'gray'),
50
+ bold: focused,
51
+ },
52
+ children: `\u25be ${display}`,
53
+ })
54
+
55
+ if (!open()) return collapsed
56
+
57
+ const maxLen = items.reduce((m, v) => Math.max(m, v.length), 0)
58
+
59
+ let maxRows = Math.min(items.length, maxVisible)
60
+
61
+ if (overlay) {
62
+ const layout = getInstanceLayout()
63
+ const ctx = getContext()
64
+ const termH = ctx?.stream?.rows ?? 24
65
+ const anchorY = layout.y + 1
66
+ const available = termH - anchorY - 2
67
+ if (available > 0) maxRows = Math.min(maxRows, available)
68
+ }
69
+
70
+ const scrollable = items.length > maxRows
71
+ const visibleCount = maxRows
72
+
73
+ const cur = cursor()
74
+ const sc = scroll()
75
+ let newScroll = sc
76
+ if (cur < sc) newScroll = cur
77
+ else if (cur >= sc + visibleCount) newScroll = cur - visibleCount + 1
78
+ newScroll = Math.max(0, Math.min(newScroll, items.length - visibleCount))
79
+ if (newScroll !== sc) setScroll(newScroll)
80
+
81
+ const visible = items.slice(newScroll, newScroll + visibleCount)
82
+
83
+ const thumbH = scrollable ? Math.max(1, Math.round((visibleCount / items.length) * visibleCount)) : 0
84
+ const maxSc = items.length - visibleCount
85
+ const thumbStart = scrollable && maxSc > 0 ? Math.round((newScroll / maxSc) * (visibleCount - thumbH)) : 0
86
+
87
+ const dropdownChildren = visible.map((item, vi) => {
88
+ const i = vi + newScroll
89
+ const isCursor = i === cur
90
+
91
+ const barChar = scrollable
92
+ ? (vi >= thumbStart && vi < thumbStart + thumbH ? '\u2588' : '\u2502')
93
+ : null
94
+
95
+ const row = (content) => {
96
+ if (!scrollable) return content
97
+ const barIsThumb = vi >= thumbStart && vi < thumbStart + thumbH
98
+ return jsx('box', {
99
+ key: i,
100
+ style: { flexDirection: 'row' },
101
+ children: [
102
+ content,
103
+ jsx('text', {
104
+ style: { color: barIsThumb ? accent : 'gray', dim: !barIsThumb },
105
+ children: ' ' + barChar,
106
+ }),
107
+ ],
108
+ })
109
+ }
110
+
111
+ if (renderItem) {
112
+ const content = jsx('box', {
113
+ key: scrollable ? undefined : i,
114
+ style: { bg: isCursor ? s.cursorBg : s.bg, flexGrow: 1 },
115
+ children: renderItem(item, { selected: isCursor, index: i }),
116
+ })
117
+ return row(content)
118
+ }
119
+
120
+ const content = jsx('box', {
121
+ key: scrollable ? undefined : i,
122
+ style: { bg: isCursor ? s.cursorBg : s.bg, paddingX: 1, flexGrow: 1 },
123
+ children: jsx('text', {
124
+ style: { color: isCursor ? s.cursorTextColor : s.color },
125
+ children: item,
126
+ }),
127
+ })
128
+ return row(content)
129
+ })
130
+
131
+ const dropdownH = visibleCount + 2
132
+
133
+ const dropdown = jsx('box', {
134
+ style: {
135
+ flexDirection: 'column',
136
+ border: s.border,
137
+ borderColor: s.borderColor,
138
+ height: dropdownH,
139
+ width: maxLen + 4 + (scrollable ? 2 : 0),
140
+ bg: s.bg,
141
+ },
142
+ children: dropdownChildren,
143
+ })
144
+
145
+ if (overlay) {
146
+ registerOverlay(dropdown)
147
+ return collapsed
148
+ }
149
+
150
+ return jsx('box', { style: { flexDirection: 'column' }, children: [collapsed, dropdown] })
151
+ }
package/src/signal.js ADDED
@@ -0,0 +1,159 @@
1
+ let currentEffect = null
2
+ let currentScope = null
3
+ let pendingEffects = null
4
+ let batchDepth = 0
5
+ let schedulerHook = null
6
+ let hookRegistrar = null
7
+
8
+ export function setSchedulerHook(fn) {
9
+ schedulerHook = fn
10
+ }
11
+
12
+ export function setHookRegistrar(fn) {
13
+ hookRegistrar = fn
14
+ }
15
+
16
+ export function createSignalRaw(value) {
17
+ const subs = new Set()
18
+
19
+ function get() {
20
+ if (currentEffect) subs.add(currentEffect)
21
+ return value
22
+ }
23
+
24
+ function set(next) {
25
+ const v = typeof next === 'function' ? next(value) : next
26
+ if (v === value) return
27
+ value = v
28
+ if (batchDepth > 0) {
29
+ for (const s of subs) pendingEffects.add(s)
30
+ } else {
31
+ const snapshot = [...subs]
32
+ for (const s of snapshot) s.run()
33
+ }
34
+ if (schedulerHook && batchDepth === 0) schedulerHook()
35
+ }
36
+
37
+ return [get, set]
38
+ }
39
+
40
+ export function createSignal(value) {
41
+ if (hookRegistrar) {
42
+ return hookRegistrar(() => createSignalRaw(value))
43
+ }
44
+ return createSignalRaw(value)
45
+ }
46
+
47
+ export function createEffect(fn) {
48
+ const effect = {
49
+ fn,
50
+ cleanup: null,
51
+ run() {
52
+ if (effect.cleanup) effect.cleanup()
53
+ const prev = currentEffect
54
+ currentEffect = effect
55
+ try {
56
+ const result = fn()
57
+ effect.cleanup = typeof result === 'function' ? result : null
58
+ } finally {
59
+ currentEffect = prev
60
+ }
61
+ },
62
+ }
63
+
64
+ effect.run()
65
+
66
+ if (currentScope) currentScope.effects.push(effect)
67
+
68
+ return effect
69
+ }
70
+
71
+ export function createMemo(fn) {
72
+ const [get, set] = createSignal(undefined)
73
+ createEffect(() => set(fn()))
74
+ return get
75
+ }
76
+
77
+ export function batch(fn) {
78
+ if (batchDepth === 0) pendingEffects = new Set()
79
+ batchDepth++
80
+ try {
81
+ fn()
82
+ } finally {
83
+ batchDepth--
84
+ if (batchDepth === 0) {
85
+ const effects = [...pendingEffects]
86
+ pendingEffects = null
87
+ for (const e of effects) e.run()
88
+ if (schedulerHook) schedulerHook()
89
+ }
90
+ }
91
+ }
92
+
93
+ export function untrack(fn) {
94
+ const prev = currentEffect
95
+ currentEffect = null
96
+ try {
97
+ return fn()
98
+ } finally {
99
+ currentEffect = prev
100
+ }
101
+ }
102
+
103
+ export function onCleanup(fn) {
104
+ if (currentScope) currentScope.cleanups.push(fn)
105
+ else if (currentEffect) {
106
+ const prev = currentEffect.cleanup
107
+ currentEffect.cleanup = prev
108
+ ? () => { prev(); fn() }
109
+ : fn
110
+ }
111
+ }
112
+
113
+ export function createScope(fn) {
114
+ const scope = {
115
+ effects: [],
116
+ children: [],
117
+ cleanups: [],
118
+ parent: currentScope,
119
+ }
120
+ if (currentScope) currentScope.children.push(scope)
121
+
122
+ const prev = currentScope
123
+ currentScope = scope
124
+ try {
125
+ fn()
126
+ } finally {
127
+ currentScope = prev
128
+ }
129
+ return scope
130
+ }
131
+
132
+ export function disposeScope(scope) {
133
+ for (const child of scope.children) disposeScope(child)
134
+ for (const effect of scope.effects) {
135
+ if (effect.cleanup) effect.cleanup()
136
+ }
137
+ for (const fn of scope.cleanups) fn()
138
+ scope.effects.length = 0
139
+ scope.children.length = 0
140
+ scope.cleanups.length = 0
141
+ }
142
+
143
+ export function getCurrentScope() {
144
+ return currentScope
145
+ }
146
+
147
+ export function runInScope(scope, fn) {
148
+ const prev = currentScope
149
+ currentScope = scope
150
+ try {
151
+ return fn()
152
+ } finally {
153
+ currentScope = prev
154
+ }
155
+ }
156
+
157
+ export function notifyScheduler() {
158
+ if (schedulerHook) schedulerHook()
159
+ }
package/src/spinner.js ADDED
@@ -0,0 +1,21 @@
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
+ import { createSignal } from './signal.js'
3
+ import { useInterval, useTheme } from './hooks.js'
4
+ import { registerHook } from './renderer.js'
5
+
6
+ const FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F']
7
+
8
+ export function Spinner({ label, color, interval = 80 }) {
9
+ const { accent = 'cyan' } = useTheme()
10
+ const c = color ?? accent
11
+ const [frame, setFrame] = createSignal(0)
12
+ useInterval(() => setFrame(f => (f + 1) % FRAMES.length), interval)
13
+
14
+ const children = [jsx('text', { style: { color: c }, children: FRAMES[frame()] })]
15
+
16
+ if (label != null) {
17
+ children.push(jsx('text', { children: ` ${label}` }))
18
+ }
19
+
20
+ return jsxs('box', { style: { flexDirection: 'row' }, children })
21
+ }
package/src/table.js ADDED
@@ -0,0 +1,51 @@
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
+ import { useTheme } from './hooks.js'
3
+ import { List } from './list.js'
4
+
5
+ export function Table({ columns, data, selected, onSelect, focused = true }) {
6
+ const { accent = 'cyan' } = useTheme()
7
+
8
+ const header = jsxs('box', {
9
+ style: { flexDirection: 'row' },
10
+ children: columns.map((col, i) =>
11
+ jsx('text', {
12
+ key: i,
13
+ style: {
14
+ width: col.width,
15
+ flexGrow: col.flexGrow,
16
+ color: 'gray',
17
+ dim: true,
18
+ bold: true,
19
+ overflow: 'truncate',
20
+ paddingX: col.paddingX ?? 1,
21
+ },
22
+ children: col.header,
23
+ })
24
+ ),
25
+ })
26
+
27
+ return jsx(List, {
28
+ items: data,
29
+ selected,
30
+ onSelect,
31
+ focused,
32
+ header,
33
+ renderItem: (row, { selected: isSel }) =>
34
+ jsxs('box', {
35
+ style: { flexDirection: 'row', bg: isSel ? (focused ? accent : 'gray') : null },
36
+ children: columns.map((col, i) =>
37
+ jsx('text', {
38
+ key: i,
39
+ style: {
40
+ width: col.width,
41
+ flexGrow: col.flexGrow,
42
+ overflow: 'truncate',
43
+ paddingX: col.paddingX ?? 1,
44
+ color: isSel && focused ? 'black' : (col.color ?? null),
45
+ },
46
+ children: col.render ? col.render(row, isSel) : String(row[col.key] ?? ''),
47
+ })
48
+ ),
49
+ }),
50
+ })
51
+ }
package/src/tabs.js ADDED
@@ -0,0 +1,30 @@
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
+ import { useInput } from './hooks.js'
3
+
4
+ export function Tabs({ items, selected, onSelect, focused = true }) {
5
+ useInput(({ key }) => {
6
+ if (!focused) return
7
+
8
+ const idx = items.indexOf(selected)
9
+ if (idx === -1) return
10
+
11
+ if (key === 'left' || key === 'shift-tab') {
12
+ onSelect(items[(idx - 1 + items.length) % items.length])
13
+ } else if (key === 'right' || key === 'tab') {
14
+ onSelect(items[(idx + 1) % items.length])
15
+ }
16
+ })
17
+
18
+ const children = items.map(item => {
19
+ const isSelected = item === selected
20
+ return jsx('text', {
21
+ style: isSelected ? { inverse: true, bold: true } : { color: 'gray' },
22
+ children: ` ${item} `,
23
+ })
24
+ })
25
+
26
+ return jsxs('box', {
27
+ style: { flexDirection: 'row', gap: 1 },
28
+ children,
29
+ })
30
+ }
@@ -0,0 +1,286 @@
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
+ import { createSignal } from './signal.js'
3
+ import { useInput, useLayout } from './hooks.js'
4
+ import { registerHook } from './renderer.js'
5
+
6
+ export function wrapForEditor(text, width) {
7
+ if (width <= 0) return [{ start: 0, end: 0, hard: true }]
8
+ if (text.length === 0) return [{ start: 0, end: 0, hard: true }]
9
+
10
+ const lines = []
11
+ let pos = 0
12
+
13
+ while (pos < text.length) {
14
+ const nlIdx = text.indexOf('\n', pos)
15
+ const logicalEnd = nlIdx === -1 ? text.length : nlIdx
16
+ const segment = text.slice(pos, logicalEnd)
17
+
18
+ if (segment.length === 0) {
19
+ lines.push({ start: pos, end: pos, hard: true })
20
+ pos = logicalEnd + 1
21
+ continue
22
+ }
23
+
24
+ let segStart = 0
25
+ while (segStart < segment.length) {
26
+ const remaining = segment.slice(segStart)
27
+
28
+ if (remaining.length <= width) {
29
+ lines.push({ start: pos + segStart, end: pos + segStart + remaining.length, hard: true })
30
+ segStart += remaining.length
31
+ break
32
+ }
33
+
34
+ const chunk = remaining.slice(0, width)
35
+ const lastSpace = chunk.lastIndexOf(' ')
36
+
37
+ if (lastSpace > 0) {
38
+ lines.push({ start: pos + segStart, end: pos + segStart + lastSpace, hard: false })
39
+ segStart += lastSpace + 1
40
+ } else {
41
+ lines.push({ start: pos + segStart, end: pos + segStart + width, hard: false })
42
+ segStart += width
43
+ }
44
+ }
45
+
46
+ pos = logicalEnd + (nlIdx !== -1 ? 1 : 0)
47
+ }
48
+
49
+ if (text.length > 0 && text[text.length - 1] === '\n') {
50
+ lines.push({ start: text.length, end: text.length, hard: true })
51
+ }
52
+
53
+ if (lines.length === 0) {
54
+ lines.push({ start: 0, end: 0, hard: true })
55
+ }
56
+
57
+ return lines
58
+ }
59
+
60
+ export function cursorToDisplay(cursor, lineMap) {
61
+ for (let row = 0; row < lineMap.length; row++) {
62
+ const line = lineMap[row]
63
+ if (cursor >= line.start && cursor <= line.end) {
64
+ if (cursor === line.end && !line.hard && row + 1 < lineMap.length) {
65
+ return { row: row + 1, col: 0 }
66
+ }
67
+ return { row, col: cursor - line.start }
68
+ }
69
+ }
70
+ const last = lineMap[lineMap.length - 1]
71
+ return { row: lineMap.length - 1, col: last.end - last.start }
72
+ }
73
+
74
+ export function displayToCursor(row, col, lineMap) {
75
+ const r = Math.max(0, Math.min(row, lineMap.length - 1))
76
+ const line = lineMap[r]
77
+ const maxCol = line.end - line.start
78
+ const c = Math.max(0, Math.min(col, maxCol))
79
+ return line.start + c
80
+ }
81
+
82
+ function ensureVisible(cursorRow, scroll, height, totalLines) {
83
+ const maxScroll = Math.max(0, totalLines - height)
84
+ if (scroll > maxScroll) scroll = maxScroll
85
+ if (cursorRow < scroll) return cursorRow
86
+ if (cursorRow >= scroll + height) return cursorRow - height + 1
87
+ return scroll
88
+ }
89
+
90
+ export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused = true, maxHeight = 10 }) {
91
+ const [value, setValue] = createSignal('')
92
+ const [cursor, setCursor] = createSignal(0)
93
+ const ref = registerHook(() => ({ scroll: 0, goalCol: null }))
94
+ const layout = useLayout()
95
+
96
+ function update(v, c) {
97
+ setValue(v)
98
+ setCursor(c)
99
+ ref.goalCol = null
100
+ if (onChange) onChange(v)
101
+ }
102
+
103
+ useInput((event) => {
104
+ if (!focused) return
105
+
106
+ const { key, raw, ctrl, meta } = event
107
+ const v = value()
108
+ const c = cursor()
109
+
110
+ if (meta && key === '\r') {
111
+ if (onSubmit) onSubmit(v)
112
+ update('', 0)
113
+ ref.scroll = 0
114
+ event.stopPropagation()
115
+ return
116
+ }
117
+
118
+ if (key === 'return') {
119
+ update(v.slice(0, c) + '\n' + v.slice(c), c + 1)
120
+ event.stopPropagation()
121
+ return
122
+ }
123
+
124
+ if (key === 'escape') {
125
+ if (onCancel) {
126
+ onCancel()
127
+ event.stopPropagation()
128
+ }
129
+ return
130
+ }
131
+
132
+ if (key === 'backspace') {
133
+ if (c > 0) update(v.slice(0, c - 1) + v.slice(c), c - 1)
134
+ event.stopPropagation()
135
+ return
136
+ }
137
+
138
+ if (key === 'delete') {
139
+ if (c < v.length) update(v.slice(0, c) + v.slice(c + 1), c)
140
+ event.stopPropagation()
141
+ return
142
+ }
143
+
144
+ if (key === 'left') {
145
+ setCursor(Math.max(0, c - 1))
146
+ ref.goalCol = null
147
+ event.stopPropagation()
148
+ return
149
+ }
150
+
151
+ if (key === 'right') {
152
+ setCursor(Math.min(v.length, c + 1))
153
+ ref.goalCol = null
154
+ event.stopPropagation()
155
+ return
156
+ }
157
+
158
+ if (key === 'up' || key === 'down') {
159
+ const w = layout.width || 80
160
+ const lineMap = wrapForEditor(v, w)
161
+ const pos = cursorToDisplay(c, lineMap)
162
+ const goal = ref.goalCol !== null ? ref.goalCol : pos.col
163
+ ref.goalCol = goal
164
+
165
+ const newRow = key === 'up' ? pos.row - 1 : pos.row + 1
166
+ if (newRow >= 0 && newRow < lineMap.length) {
167
+ setCursor(displayToCursor(newRow, goal, lineMap))
168
+ }
169
+ event.stopPropagation()
170
+ return
171
+ }
172
+
173
+ if (key === 'home' || (ctrl && raw === '\x01')) {
174
+ const w = layout.width || 80
175
+ const lineMap = wrapForEditor(v, w)
176
+ const pos = cursorToDisplay(c, lineMap)
177
+ setCursor(lineMap[pos.row].start)
178
+ ref.goalCol = null
179
+ event.stopPropagation()
180
+ return
181
+ }
182
+
183
+ if (key === 'end' || (ctrl && raw === '\x05')) {
184
+ const w = layout.width || 80
185
+ const lineMap = wrapForEditor(v, w)
186
+ const pos = cursorToDisplay(c, lineMap)
187
+ setCursor(lineMap[pos.row].end)
188
+ ref.goalCol = null
189
+ event.stopPropagation()
190
+ return
191
+ }
192
+
193
+ if (ctrl && raw === '\x15') {
194
+ const before = v.slice(0, c)
195
+ const nlIdx = before.lastIndexOf('\n')
196
+ const lineStart = nlIdx + 1
197
+ update(v.slice(0, lineStart) + v.slice(c), lineStart)
198
+ event.stopPropagation()
199
+ return
200
+ }
201
+
202
+ if (ctrl && raw === '\x0b') {
203
+ const after = v.slice(c)
204
+ const nlIdx = after.indexOf('\n')
205
+ const deleteEnd = nlIdx === -1 ? v.length : c + nlIdx
206
+ update(v.slice(0, c) + v.slice(deleteEnd), c)
207
+ event.stopPropagation()
208
+ return
209
+ }
210
+
211
+ if (ctrl && raw === '\x17') {
212
+ const before = v.slice(0, c)
213
+ const after = v.slice(c)
214
+ const trimmed = before.replace(/\S+\s*$/, '')
215
+ update(trimmed + after, trimmed.length)
216
+ event.stopPropagation()
217
+ return
218
+ }
219
+
220
+ if (!ctrl && !meta && raw.length === 1 && raw >= ' ') {
221
+ update(v.slice(0, c) + raw + v.slice(c), c + raw.length)
222
+ event.stopPropagation()
223
+ }
224
+ })
225
+
226
+ const v = value()
227
+ const c = cursor()
228
+ const w = layout.width || 0
229
+
230
+ if (!v && placeholder && !focused) {
231
+ return jsx('text', { style: { color: 'gray', flexGrow: 1 }, children: placeholder })
232
+ }
233
+
234
+ const effectiveWidth = w || 80
235
+ const lineMap = wrapForEditor(v, effectiveWidth)
236
+ const displayPos = cursorToDisplay(c, lineMap)
237
+
238
+ const displayHeight = Math.max(1, Math.min(lineMap.length, maxHeight))
239
+ ref.scroll = ensureVisible(displayPos.row, ref.scroll, displayHeight, lineMap.length)
240
+ const scroll = ref.scroll
241
+
242
+ const visibleLines = lineMap.slice(scroll, scroll + displayHeight)
243
+
244
+ if (!v && placeholder && focused) {
245
+ return jsx('box', {
246
+ style: { flexDirection: 'column', height: 1, minHeight: 1, flexGrow: 1 },
247
+ children: jsxs('box', {
248
+ style: { flexDirection: 'row', height: 1 },
249
+ children: [
250
+ jsx('text', { style: { inverse: true, color: 'gray' }, children: placeholder[0] }),
251
+ placeholder.length > 1 && jsx('text', { style: { color: 'gray' }, children: placeholder.slice(1) }),
252
+ ],
253
+ }),
254
+ })
255
+ }
256
+
257
+ const rows = visibleLines.map((line, i) => {
258
+ const row = scroll + i
259
+ const content = v.slice(line.start, line.end)
260
+ const hasCursor = focused && row === displayPos.row
261
+
262
+ if (!hasCursor) {
263
+ return jsx('text', { key: row, children: content || ' ' })
264
+ }
265
+
266
+ const cursorCol = displayPos.col
267
+ const before = content.slice(0, cursorCol)
268
+ const cursorChar = content[cursorCol] || ' '
269
+ const after = content.slice(cursorCol + 1)
270
+
271
+ return jsxs('box', {
272
+ key: row,
273
+ style: { flexDirection: 'row', height: 1 },
274
+ children: [
275
+ before && jsx('text', { children: before }),
276
+ jsx('text', { style: { inverse: true }, children: cursorChar }),
277
+ after && jsx('text', { children: after }),
278
+ ],
279
+ })
280
+ })
281
+
282
+ return jsx('box', {
283
+ style: { flexDirection: 'column', height: displayHeight, minHeight: 1, flexGrow: 1 },
284
+ children: rows,
285
+ })
286
+ }