@trendr/core 0.2.8 → 0.2.9
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/index.js +1 -0
- package/package.json +2 -2
- package/src/ansi.js +16 -4
- package/src/diff.js +15 -4
- package/src/focus.js +7 -22
- package/src/input.js +36 -14
- package/src/layout.js +4 -0
- package/src/renderer.js +133 -7
- package/src/scrollback.js +9 -0
- package/src/serialize.js +46 -0
- package/src/text-area.js +3 -3
package/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export { useFocus, useFocusTrap } from './src/focus.js'
|
|
|
27
27
|
export { useHotkey } from './src/hotkey.js'
|
|
28
28
|
export { useToast } from './src/toast.js'
|
|
29
29
|
export { Fragment } from './src/element.js'
|
|
30
|
+
export { Scrollback } from './src/scrollback.js'
|
|
30
31
|
export { altScreen, exitAltScreen, showCursor, hideCursor, clearScreen, clearLine } from './src/ansi.js'
|
|
31
32
|
export { animated, useAnimated, spring, ease, decay } from './src/animation.js'
|
|
32
33
|
export { linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeOutElastic, easeOutBounce } from './src/animation.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trendr/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"pick-list": "node esbuild.config.js && node dist/pick-list.js",
|
|
48
48
|
"table-demo": "node esbuild.config.js && node dist/table-demo.js",
|
|
49
49
|
"menubar": "node esbuild.config.js && node dist/menubar.js",
|
|
50
|
-
"
|
|
50
|
+
"inline-chat": "node esbuild.config.js && node dist/inline-chat.js",
|
|
51
51
|
"demo": "node esbuild.config.js && node demo/server.js",
|
|
52
52
|
"demo:build": "node esbuild.config.js"
|
|
53
53
|
},
|
package/src/ansi.js
CHANGED
|
@@ -10,6 +10,7 @@ export const hideCursor = `${ESC}?25l`
|
|
|
10
10
|
export const showCursor = `${ESC}?25h`
|
|
11
11
|
export const clearScreen = `${ESC}2J`
|
|
12
12
|
export const clearLine = `${ESC}2K`
|
|
13
|
+
export const clearDown = `${ESC}0J`
|
|
13
14
|
export const altScreen = `${ESC}?1049h`
|
|
14
15
|
export const exitAltScreen = `${ESC}?1049l`
|
|
15
16
|
export const sgrReset = `${ESC}0m`
|
|
@@ -43,14 +44,25 @@ const NAMED_COLORS = {
|
|
|
43
44
|
brightBlue: 12, brightMagenta: 13, brightCyan: 14, brightWhite: 15,
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function expandHex(color) {
|
|
48
|
+
if (color.length === 7) return color
|
|
49
|
+
if (color.length === 4) {
|
|
50
|
+
const r = color[1], g = color[2], b = color[3]
|
|
51
|
+
return `#${r}${r}${g}${g}${b}${b}`
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
function parseColor(color, offset) {
|
|
47
57
|
if (color == null) return null
|
|
48
58
|
if (typeof color === 'number') return `${offset};5;${color}`
|
|
49
59
|
if (color in NAMED_COLORS) return `${offset};5;${NAMED_COLORS[color]}`
|
|
50
|
-
if (color.
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
60
|
+
if (color.charCodeAt(0) === 35) {
|
|
61
|
+
const hex = expandHex(color)
|
|
62
|
+
if (!hex) return null
|
|
63
|
+
const r = parseInt(hex.slice(1, 3), 16)
|
|
64
|
+
const g = parseInt(hex.slice(3, 5), 16)
|
|
65
|
+
const b = parseInt(hex.slice(5, 7), 16)
|
|
54
66
|
return `${offset};2;${r};${g};${b}`
|
|
55
67
|
}
|
|
56
68
|
return null
|
package/src/diff.js
CHANGED
|
@@ -56,6 +56,15 @@ function writeSgr(fg, bg, attrs) {
|
|
|
56
56
|
buf[pos++] = 0x6d
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function expandHex(color) {
|
|
60
|
+
if (color.length === 7) return color
|
|
61
|
+
if (color.length === 4) {
|
|
62
|
+
const r = color[1], g = color[2], b = color[3]
|
|
63
|
+
return `#${r}${r}${g}${g}${b}${b}`
|
|
64
|
+
}
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
function writeColor(color, offset) {
|
|
60
69
|
if (typeof color === 'number' || color in NAMED_COLORS) {
|
|
61
70
|
const idx = typeof color === 'number' ? color : NAMED_COLORS[color]
|
|
@@ -65,10 +74,12 @@ function writeColor(color, offset) {
|
|
|
65
74
|
buf[pos++] = 53 // '5'
|
|
66
75
|
buf[pos++] = 0x3b
|
|
67
76
|
writeNum(idx)
|
|
68
|
-
} else if (color.charCodeAt(0) === 35
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const
|
|
77
|
+
} else if (color.charCodeAt(0) === 35) {
|
|
78
|
+
const hex = expandHex(color)
|
|
79
|
+
if (!hex) return
|
|
80
|
+
const r = parseInt(hex.slice(1, 3), 16)
|
|
81
|
+
const g = parseInt(hex.slice(3, 5), 16)
|
|
82
|
+
const b = parseInt(hex.slice(5, 7), 16)
|
|
72
83
|
buf[pos++] = 0x3b
|
|
73
84
|
writeNum(offset)
|
|
74
85
|
buf[pos++] = 0x3b
|
package/src/focus.js
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
|
-
import { createSignalRaw
|
|
1
|
+
import { createSignalRaw } from './signal.js'
|
|
2
2
|
import { registerHook } from './renderer.js'
|
|
3
3
|
import { useInput } from './hooks.js'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// while active, keeps tab/shift-tab from propagating past the trapping
|
|
6
|
+
// component to focus managers registered before it. handlers fire
|
|
7
|
+
// innermost-first, so any useFocus registered deeper in the tree handles the
|
|
8
|
+
// tab first; this handler then stops propagation so shallower managers don't
|
|
9
|
+
// also react. when traps nest, the innermost active one fires first and halts
|
|
10
|
+
// the rest, so it becomes the effective boundary
|
|
7
11
|
export function useFocusTrap(active) {
|
|
8
|
-
const state = registerHook(() => {
|
|
9
|
-
const id = Symbol()
|
|
10
|
-
onCleanup(() => {
|
|
11
|
-
const idx = focusTrapStack.indexOf(id)
|
|
12
|
-
if (idx >= 0) focusTrapStack.splice(idx, 1)
|
|
13
|
-
})
|
|
14
|
-
return { id, wasActive: false }
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
if (active && !state.wasActive) {
|
|
18
|
-
focusTrapStack.push(state.id)
|
|
19
|
-
state.wasActive = true
|
|
20
|
-
} else if (!active && state.wasActive) {
|
|
21
|
-
const idx = focusTrapStack.indexOf(state.id)
|
|
22
|
-
if (idx >= 0) focusTrapStack.splice(idx, 1)
|
|
23
|
-
state.wasActive = false
|
|
24
|
-
}
|
|
25
|
-
|
|
26
12
|
useInput((event) => {
|
|
27
13
|
if (!active) return
|
|
28
14
|
if (event.key === 'tab' || event.key === 'shift-tab') {
|
|
@@ -122,7 +108,6 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
|
122
108
|
const cur = state.current()
|
|
123
109
|
|
|
124
110
|
if (state.stack.length > 0) return
|
|
125
|
-
if (focusTrapStack.length > 0) return
|
|
126
111
|
|
|
127
112
|
if (cycle === 'tab' && (key === 'tab' || key === 'shift-tab')) {
|
|
128
113
|
const idx = findTopLevel(cur)
|
package/src/input.js
CHANGED
|
@@ -52,6 +52,31 @@ export function parseMouse(raw) {
|
|
|
52
52
|
return { type: 'mouse', action, button: buttonName, x, y }
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const MODIFY_OTHER_RE = /^\x1b\[27;(\d+);(\d+)~$/
|
|
56
|
+
const CSI_U_RE = /^\x1b\[(\d+)(?:;(\d+))?u$/
|
|
57
|
+
|
|
58
|
+
function codeToKey(code) {
|
|
59
|
+
if (code === 13 || code === 10) return 'return'
|
|
60
|
+
if (code === 9) return 'tab'
|
|
61
|
+
if (code === 27) return 'escape'
|
|
62
|
+
if (code === 32) return 'space'
|
|
63
|
+
if (code === 127 || code === 8) return 'backspace'
|
|
64
|
+
return String.fromCodePoint(code)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// modifier param is 1-based with a bitmask in the low bits: shift=1, alt=2,
|
|
68
|
+
// ctrl=4 (so plain=1, shift=2, ctrl=5, etc)
|
|
69
|
+
function modifiedKey(code, mod, raw) {
|
|
70
|
+
const bits = Math.max(0, mod - 1)
|
|
71
|
+
return {
|
|
72
|
+
key: codeToKey(code),
|
|
73
|
+
ctrl: (bits & 4) !== 0,
|
|
74
|
+
meta: (bits & 2) !== 0,
|
|
75
|
+
shift: (bits & 1) !== 0,
|
|
76
|
+
raw,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
export function parseKey(data) {
|
|
56
81
|
const raw = typeof data === 'string' ? data : data.toString()
|
|
57
82
|
|
|
@@ -59,6 +84,12 @@ export function parseKey(data) {
|
|
|
59
84
|
return { key: SPECIAL_KEYS[raw], ctrl: false, meta: false, shift: false, raw }
|
|
60
85
|
}
|
|
61
86
|
|
|
87
|
+
let m = MODIFY_OTHER_RE.exec(raw)
|
|
88
|
+
if (m) return modifiedKey(parseInt(m[2], 10), parseInt(m[1], 10), raw)
|
|
89
|
+
|
|
90
|
+
m = CSI_U_RE.exec(raw)
|
|
91
|
+
if (m) return modifiedKey(parseInt(m[1], 10), m[2] ? parseInt(m[2], 10) : 1, raw)
|
|
92
|
+
|
|
62
93
|
if (raw.length === 1) {
|
|
63
94
|
const code = raw.charCodeAt(0)
|
|
64
95
|
|
|
@@ -90,21 +121,12 @@ export function splitKeys(data) {
|
|
|
90
121
|
while (i < raw.length) {
|
|
91
122
|
if (raw[i] === '\x1b') {
|
|
92
123
|
if (i + 1 < raw.length && raw[i + 1] === '[') {
|
|
124
|
+
// generic csi: optional private marker, any number of ;-separated
|
|
125
|
+
// numeric params, then a final byte. covers arrows, mouse (\x1b[<..M),
|
|
126
|
+
// csi-u (\x1b[13;2u) and modifyOtherKeys (\x1b[27;2;13~)
|
|
93
127
|
let j = i + 2
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
j++
|
|
97
|
-
while (j < raw.length && ((raw[j] >= '0' && raw[j] <= '9') || raw[j] === ';')) j++
|
|
98
|
-
if (j < raw.length) j++
|
|
99
|
-
keys.push(raw.slice(i, j))
|
|
100
|
-
i = j
|
|
101
|
-
continue
|
|
102
|
-
}
|
|
103
|
-
while (j < raw.length && raw[j] >= '0' && raw[j] <= '9') j++
|
|
104
|
-
if (j < raw.length && raw[j] === ';') {
|
|
105
|
-
j++
|
|
106
|
-
while (j < raw.length && raw[j] >= '0' && raw[j] <= '9') j++
|
|
107
|
-
}
|
|
128
|
+
if (j < raw.length && (raw[j] === '<' || raw[j] === '?')) j++
|
|
129
|
+
while (j < raw.length && ((raw[j] >= '0' && raw[j] <= '9') || raw[j] === ';' || raw[j] === ':')) j++
|
|
108
130
|
if (j < raw.length) j++
|
|
109
131
|
keys.push(raw.slice(i, j))
|
|
110
132
|
i = j
|
package/src/layout.js
CHANGED
|
@@ -246,6 +246,10 @@ function layoutFlex(children, ctx) {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
export function intrinsicHeight(node, availW, availH) {
|
|
250
|
+
return measureChild(node, childStyle(node), false, availW, availH).height
|
|
251
|
+
}
|
|
252
|
+
|
|
249
253
|
function measureChild(child, cs, isRow, availW, availH) {
|
|
250
254
|
const leaf = getLeaf(child)
|
|
251
255
|
if (!leaf) return { width: 0, height: 0 }
|
package/src/renderer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createBuffer, clearBuffer, fillRect, writeText, dimBuffer, blitRect } from './buffer.js'
|
|
2
2
|
import { diff } from './diff.js'
|
|
3
|
-
import {
|
|
3
|
+
import { bufferToLines } from './serialize.js'
|
|
4
|
+
import { computeLayout, resolveBorderEdges, intrinsicHeight } from './layout.js'
|
|
4
5
|
import { Fragment } from './element.js'
|
|
5
6
|
import { createScheduler } from './scheduler.js'
|
|
6
7
|
import { createInputHandler } from './input.js'
|
|
@@ -606,7 +607,40 @@ function resolveForFrame(element, parent, instances, counters, visited, scope) {
|
|
|
606
607
|
return node
|
|
607
608
|
}
|
|
608
609
|
|
|
609
|
-
|
|
610
|
+
function findScrollback(node) {
|
|
611
|
+
if (!node) return null
|
|
612
|
+
if (typeof node.type === 'function' && node.type.__scrollback) return node
|
|
613
|
+
if (node._resolved) {
|
|
614
|
+
const found = findScrollback(node._resolved)
|
|
615
|
+
if (found) return found
|
|
616
|
+
}
|
|
617
|
+
if (node._resolvedChildren) {
|
|
618
|
+
for (const child of node._resolvedChildren) {
|
|
619
|
+
const found = findScrollback(child)
|
|
620
|
+
if (found) return found
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return null
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// render a one-shot element (a committed scrollback item) to ANSI lines. it
|
|
627
|
+
// gets its own throwaway instance map so it never pollutes the live tree's
|
|
628
|
+
// cache, and scopes are disposed immediately since committed content is frozen
|
|
629
|
+
function renderElementToLines(element, width) {
|
|
630
|
+
if (element == null) return []
|
|
631
|
+
const instances = new Map()
|
|
632
|
+
const counters = new Map()
|
|
633
|
+
const tree = resolveForFrame(element, null, instances, counters, null, '')
|
|
634
|
+
const h = Math.max(1, intrinsicHeight(tree, width, 100000))
|
|
635
|
+
computeLayout(tree, { x: 0, y: 0, width, height: h })
|
|
636
|
+
const buf = createBuffer(width, h)
|
|
637
|
+
paintTree(tree, buf, null, null, null)
|
|
638
|
+
const lines = bufferToLines(buf)
|
|
639
|
+
for (const inst of instances.values()) disposeScope(inst.scope)
|
|
640
|
+
return lines
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function mount(rootComponent, { stream, stdin, title, theme, onExit: onExitCb, altScreen = true, inline = false } = {}) {
|
|
610
644
|
const out = stream ?? process.stdout
|
|
611
645
|
const inp = stdin ?? process.stdin
|
|
612
646
|
|
|
@@ -626,6 +660,72 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
626
660
|
let forceFullPaint = false
|
|
627
661
|
let prevHadOverlays = false
|
|
628
662
|
|
|
663
|
+
// inline mode state: how many scrollback items have been committed, how many
|
|
664
|
+
// lines the live region occupied last emit, and that emit's text for skipping
|
|
665
|
+
let flushedCount = 0
|
|
666
|
+
let prevLiveLines = 0
|
|
667
|
+
let prevLiveText = null
|
|
668
|
+
|
|
669
|
+
function inlineFrame() {
|
|
670
|
+
const prevCtx = activeContext
|
|
671
|
+
activeContext = ctx
|
|
672
|
+
overlays = []
|
|
673
|
+
|
|
674
|
+
const counters = new Map()
|
|
675
|
+
const visited = new Set()
|
|
676
|
+
const element = { type: rootComponent, props: {}, key: null }
|
|
677
|
+
const tree = resolveForFrame(element, null, instances, counters, visited, '')
|
|
678
|
+
|
|
679
|
+
const sb = findScrollback(tree)
|
|
680
|
+
const items = sb?.props?.items ?? []
|
|
681
|
+
const renderItem = sb?.props?.render
|
|
682
|
+
|
|
683
|
+
let committed = ''
|
|
684
|
+
if (renderItem && items.length > flushedCount) {
|
|
685
|
+
for (let i = flushedCount; i < items.length; i++) {
|
|
686
|
+
const lines = renderElementToLines(renderItem(items[i], i), width)
|
|
687
|
+
for (const ln of lines) committed += ln + '\r\n'
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// items can only grow while mounted; a shrink means the app reset its log,
|
|
691
|
+
// which we can't un-print, so just resync the counter
|
|
692
|
+
flushedCount = items.length
|
|
693
|
+
|
|
694
|
+
const liveHeight = Math.min(height, Math.max(1, intrinsicHeight(tree, width, height)))
|
|
695
|
+
computeLayout(tree, { x: 0, y: 0, width, height: liveHeight })
|
|
696
|
+
|
|
697
|
+
for (const [key, inst] of instances) {
|
|
698
|
+
if (!visited.has(key)) {
|
|
699
|
+
disposeScope(inst.scope)
|
|
700
|
+
instances.delete(key)
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const liveBuf = createBuffer(width, liveHeight)
|
|
705
|
+
paintTree(tree, liveBuf, null, null, null)
|
|
706
|
+
const liveLines = bufferToLines(liveBuf)
|
|
707
|
+
lastInlineBuf = liveBuf
|
|
708
|
+
|
|
709
|
+
activeContext = prevCtx
|
|
710
|
+
|
|
711
|
+
const liveText = liveLines.join('\r\n')
|
|
712
|
+
if (!committed && liveText === prevLiveText) return
|
|
713
|
+
|
|
714
|
+
let out_ = ''
|
|
715
|
+
if (prevLiveLines > 0) {
|
|
716
|
+
out_ += '\r'
|
|
717
|
+
if (prevLiveLines > 1) out_ += ansi.moveUp(prevLiveLines - 1)
|
|
718
|
+
out_ += ansi.clearDown
|
|
719
|
+
}
|
|
720
|
+
out_ += committed + liveText
|
|
721
|
+
|
|
722
|
+
out.write(ansi.hideCursor + out_)
|
|
723
|
+
prevLiveLines = liveLines.length
|
|
724
|
+
prevLiveText = liveText
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
let lastInlineBuf = null
|
|
728
|
+
|
|
629
729
|
function frame() {
|
|
630
730
|
const prevCtx = activeContext
|
|
631
731
|
activeContext = ctx
|
|
@@ -741,20 +841,37 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
741
841
|
|
|
742
842
|
const scheduler = createScheduler({
|
|
743
843
|
fps: 60,
|
|
744
|
-
onFrame: frame,
|
|
844
|
+
onFrame: inline ? inlineFrame : frame,
|
|
745
845
|
})
|
|
746
846
|
|
|
747
847
|
setSchedulerHook(scheduler.requestFrame)
|
|
748
848
|
|
|
749
|
-
|
|
849
|
+
// inline mode stays on the main screen buffer so native scrollback survives:
|
|
850
|
+
// no alt screen, no clear, no mouse capture (so the terminal handles scroll
|
|
851
|
+
// and text selection itself)
|
|
852
|
+
if (inline) {
|
|
853
|
+
out.write(ansi.hideCursor + (title ? ansi.setTitle(title) : ''))
|
|
854
|
+
} else {
|
|
855
|
+
out.write((altScreen ? ansi.altScreen : '') + ansi.hideCursor + ansi.clearScreen + ansi.enableMouse + (title ? ansi.setTitle(title) : ''))
|
|
856
|
+
}
|
|
750
857
|
if (inp.isTTY && inp.setRawMode) inp.setRawMode(true)
|
|
751
858
|
|
|
752
|
-
|
|
859
|
+
if (inline) inlineFrame()
|
|
860
|
+
else frame()
|
|
753
861
|
scheduler.requestFrame()
|
|
754
862
|
|
|
755
863
|
const onResize = () => {
|
|
756
864
|
width = out.columns ?? 80
|
|
757
865
|
height = out.rows ?? 24
|
|
866
|
+
if (inline) {
|
|
867
|
+
// the terminal reflows committed history on its own; drop our live-region
|
|
868
|
+
// geometry and repaint it fresh below wherever the cursor landed
|
|
869
|
+
prevLiveLines = 0
|
|
870
|
+
prevLiveText = null
|
|
871
|
+
out.write('\r\n')
|
|
872
|
+
scheduler.forceFrame()
|
|
873
|
+
return
|
|
874
|
+
}
|
|
758
875
|
prev = createBuffer(width, height)
|
|
759
876
|
curr = createBuffer(width, height)
|
|
760
877
|
out.write(ansi.clearScreen)
|
|
@@ -786,7 +903,11 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
786
903
|
}
|
|
787
904
|
instances.clear()
|
|
788
905
|
|
|
789
|
-
|
|
906
|
+
if (inline) {
|
|
907
|
+
out.write(ansi.sgrReset + ansi.showCursor + '\r\n')
|
|
908
|
+
} else {
|
|
909
|
+
out.write(ansi.sgrReset + ansi.disableMouse + ansi.showCursor + (altScreen ? ansi.exitAltScreen : ansi.moveTo(height, 1) + '\n'))
|
|
910
|
+
}
|
|
790
911
|
if (inp.isTTY && inp.setRawMode) inp.setRawMode(false)
|
|
791
912
|
activeContext = null
|
|
792
913
|
setSchedulerHook(null)
|
|
@@ -799,6 +920,11 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
799
920
|
process.on('exit', onExit)
|
|
800
921
|
|
|
801
922
|
function repaint() {
|
|
923
|
+
if (inline) {
|
|
924
|
+
prevLiveText = null
|
|
925
|
+
scheduler.forceFrame()
|
|
926
|
+
return
|
|
927
|
+
}
|
|
802
928
|
prev = createBuffer(width, height)
|
|
803
929
|
curr = createBuffer(width, height)
|
|
804
930
|
forceFullPaint = true
|
|
@@ -808,5 +934,5 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
808
934
|
|
|
809
935
|
ctx.repaint = repaint
|
|
810
936
|
|
|
811
|
-
return { unmount, repaint, getBuffer: () => prev }
|
|
937
|
+
return { unmount, repaint, getBuffer: () => (inline ? lastInlineBuf : prev) }
|
|
812
938
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// marker component for inline mode. everything inside Scrollback is the
|
|
2
|
+
// committed transcript: append-only items that get printed once into native
|
|
3
|
+
// terminal scrollback and never touched again. the renderer reads its props
|
|
4
|
+
// directly during the inline frame, so the component itself renders nothing
|
|
5
|
+
export function Scrollback() {
|
|
6
|
+
return null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
Scrollback.__scrollback = true
|
package/src/serialize.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { sgr } from './ansi.js'
|
|
2
|
+
|
|
3
|
+
const RESET = '\x1b[0m'
|
|
4
|
+
|
|
5
|
+
function isBlank(cell) {
|
|
6
|
+
return cell.ch === ' ' && cell.fg == null && cell.bg == null && cell.attrs === 0
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// turn a cell buffer into one ANSI string per row, trimming trailing blank
|
|
10
|
+
// cells so short lines don't carry filler. used by inline mode to emit both
|
|
11
|
+
// committed scrollback items and the live region
|
|
12
|
+
export function bufferToLines(buf) {
|
|
13
|
+
const { width, height, cells } = buf
|
|
14
|
+
const lines = new Array(height)
|
|
15
|
+
|
|
16
|
+
for (let y = 0; y < height; y++) {
|
|
17
|
+
const row = y * width
|
|
18
|
+
|
|
19
|
+
let last = -1
|
|
20
|
+
for (let x = 0; x < width; x++) {
|
|
21
|
+
const c = cells[row + x]
|
|
22
|
+
if (c.ch !== '' && !isBlank(c)) last = x
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let line = ''
|
|
26
|
+
let curFg, curBg, curAttrs
|
|
27
|
+
let open = false
|
|
28
|
+
|
|
29
|
+
for (let x = 0; x <= last; x++) {
|
|
30
|
+
const c = cells[row + x]
|
|
31
|
+
if (c.ch === '') continue
|
|
32
|
+
if (!open || c.fg !== curFg || c.bg !== curBg || c.attrs !== curAttrs) {
|
|
33
|
+
line += sgr(c.fg, c.bg, c.attrs)
|
|
34
|
+
curFg = c.fg
|
|
35
|
+
curBg = c.bg
|
|
36
|
+
curAttrs = c.attrs
|
|
37
|
+
open = true
|
|
38
|
+
}
|
|
39
|
+
line += c.ch
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines[y] = open ? line + RESET : line
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return lines
|
|
46
|
+
}
|
package/src/text-area.js
CHANGED
|
@@ -119,7 +119,7 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
119
119
|
|
|
120
120
|
resetBlink()
|
|
121
121
|
|
|
122
|
-
const { key, raw, ctrl, meta } = event
|
|
122
|
+
const { key, raw, ctrl, meta, shift } = event
|
|
123
123
|
const v = value()
|
|
124
124
|
const c = cursor()
|
|
125
125
|
|
|
@@ -131,10 +131,10 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const isSubmitKey = submitOnEnter
|
|
134
|
-
? (key === 'return' && !meta)
|
|
134
|
+
? (key === 'return' && !meta && !shift)
|
|
135
135
|
: (meta && key === '\r')
|
|
136
136
|
const isNewlineKey = submitOnEnter
|
|
137
|
-
? (meta && key === '\r')
|
|
137
|
+
? ((shift && key === 'return') || (meta && key === '\r'))
|
|
138
138
|
: (key === 'return')
|
|
139
139
|
|
|
140
140
|
if (isSubmitKey) {
|