@cel-tui/core 0.3.0 → 0.4.1
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/package.json +2 -2
- package/src/cel.ts +103 -107
- package/src/emitter.ts +65 -6
- package/src/keys.ts +141 -64
- package/src/layout.ts +7 -24
- package/src/paint.ts +13 -151
- package/src/primitives/text-input.ts +7 -5
- package/src/scroll.ts +77 -0
- package/src/terminal.ts +11 -3
- package/src/text-layout.ts +250 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cel-tui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Core framework engine for cel-tui — primitives, layout, rendering, input",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"layout"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@cel-tui/types": "0.
|
|
37
|
+
"@cel-tui/types": "0.4.1",
|
|
38
38
|
"get-east-asian-width": "^1.5.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
package/src/cel.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Node,
|
|
3
|
+
TextInputNode,
|
|
4
|
+
TextInputProps,
|
|
5
|
+
Theme,
|
|
6
|
+
} from "@cel-tui/types";
|
|
2
7
|
import { CellBuffer } from "./cell-buffer.js";
|
|
3
8
|
import { emitBuffer, emitDiff, defaultTheme } from "./emitter.js";
|
|
4
9
|
import {
|
|
@@ -8,7 +13,7 @@ import {
|
|
|
8
13
|
collectKeyPressHandlers,
|
|
9
14
|
collectFocusable,
|
|
10
15
|
} from "./hit-test.js";
|
|
11
|
-
import {
|
|
16
|
+
import { decodeKeyEvents, isEditingKey, type KeyInput } from "./keys.js";
|
|
12
17
|
import { layout, type LayoutNode } from "./layout.js";
|
|
13
18
|
import {
|
|
14
19
|
paint,
|
|
@@ -26,10 +31,18 @@ import {
|
|
|
26
31
|
type EditState,
|
|
27
32
|
} from "./text-edit.js";
|
|
28
33
|
import type { Terminal } from "./terminal.js";
|
|
29
|
-
import {
|
|
34
|
+
import { getMaxScrollOffset, getScrollStep } from "./scroll.js";
|
|
30
35
|
|
|
31
36
|
type RenderFn = () => Node | Node[];
|
|
32
37
|
|
|
38
|
+
type TerminalCursorState =
|
|
39
|
+
| { visible: false }
|
|
40
|
+
| {
|
|
41
|
+
visible: true;
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
33
46
|
let terminal: Terminal | null = null;
|
|
34
47
|
let activeTheme: Theme = defaultTheme;
|
|
35
48
|
let renderFn: RenderFn | null = null;
|
|
@@ -60,6 +73,9 @@ const uncontrolledScrollOffsets = new Map<string, number>();
|
|
|
60
73
|
/** Nodes whose props were stamped with scrollOffset during the last paint. */
|
|
61
74
|
let stampedScrollNodes: { node: Node; key: string }[] = [];
|
|
62
75
|
|
|
76
|
+
/** The cursor state currently expected on the terminal. */
|
|
77
|
+
let lastTerminalCursor: TerminalCursorState | null = { visible: false };
|
|
78
|
+
|
|
63
79
|
function doRender(): void {
|
|
64
80
|
renderScheduled = false;
|
|
65
81
|
if (!renderFn || !terminal) return;
|
|
@@ -105,43 +121,38 @@ function doRender(): void {
|
|
|
105
121
|
unstampUncontrolledFocus();
|
|
106
122
|
unstampUncontrolledScroll();
|
|
107
123
|
|
|
124
|
+
const nextCursor = getDesiredTerminalCursor();
|
|
125
|
+
|
|
108
126
|
// Emit to terminal — differential when possible
|
|
109
127
|
if (prevBuffer) {
|
|
110
|
-
const output = emitDiff(prevBuffer, currentBuffer, activeTheme
|
|
128
|
+
const output = emitDiff(prevBuffer, currentBuffer, activeTheme, {
|
|
129
|
+
cursor: nextCursor,
|
|
130
|
+
previousCursor: lastTerminalCursor,
|
|
131
|
+
});
|
|
111
132
|
if (output.length > 0) terminal.write(output);
|
|
112
133
|
} else {
|
|
113
|
-
const output = emitBuffer(currentBuffer, activeTheme
|
|
134
|
+
const output = emitBuffer(currentBuffer, activeTheme, {
|
|
135
|
+
cursor: nextCursor,
|
|
136
|
+
previousCursor: lastTerminalCursor,
|
|
137
|
+
});
|
|
114
138
|
terminal.write(output);
|
|
115
139
|
}
|
|
116
140
|
|
|
117
|
-
|
|
118
|
-
// This gives us a blinking cursor for free (terminal-managed blink).
|
|
119
|
-
positionTerminalCursor();
|
|
141
|
+
lastTerminalCursor = nextCursor;
|
|
120
142
|
}
|
|
121
143
|
|
|
122
144
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
145
|
+
* Resolve the final terminal cursor state for the current frame.
|
|
146
|
+
* The cursor is shown only for a focused TextInput whose cursor is visible
|
|
147
|
+
* within the clipped viewport.
|
|
126
148
|
*/
|
|
127
|
-
function
|
|
128
|
-
if (!terminal) return;
|
|
129
|
-
|
|
130
|
-
// Find focused TextInput in the layout tree (check stamped state during
|
|
131
|
-
// render — we need to check controlled focus in the current layouts)
|
|
149
|
+
function getDesiredTerminalCursor(): TerminalCursorState {
|
|
132
150
|
const focusedTI = findFocusedTextInputLayout();
|
|
133
|
-
if (focusedTI) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// CUP: move cursor to (row, col) — 1-indexed
|
|
139
|
-
terminal.write(`\x1b[${pos.y + 1};${pos.x + 1}H`);
|
|
140
|
-
terminal.showCursor();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
terminal.hideCursor();
|
|
151
|
+
if (!focusedTI) return { visible: false };
|
|
152
|
+
|
|
153
|
+
const props = focusedTI.node.props as import("@cel-tui/types").TextInputProps;
|
|
154
|
+
const pos = getTextInputCursorScreenPos(props, focusedTI.rect);
|
|
155
|
+
return pos ? { visible: true, x: pos.x, y: pos.y } : { visible: false };
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
/**
|
|
@@ -189,25 +200,49 @@ const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
|
189
200
|
// need to remember the offset we already dispatched via onScroll.
|
|
190
201
|
let batchScrollOffsets: Map<object, number> | null = null;
|
|
191
202
|
|
|
203
|
+
// Tracks the focused TextInput's latest edit state during a batched keyboard
|
|
204
|
+
// chunk. The layout tree does not re-render until the next tick, so subsequent
|
|
205
|
+
// keys in the same chunk must see the updated value/cursor immediately.
|
|
206
|
+
let batchTextInputEdits: Map<TextInputProps, EditState> | null = null;
|
|
207
|
+
|
|
192
208
|
function handleInput(data: string): void {
|
|
193
|
-
// Terminals may batch multiple mouse events into one data chunk.
|
|
194
|
-
// Scan for all SGR mouse sequences and handle each one.
|
|
195
209
|
SGR_MOUSE_RE.lastIndex = 0;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
210
|
+
batchTextInputEdits = new Map();
|
|
211
|
+
|
|
212
|
+
let lastIndex = 0;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
let match = SGR_MOUSE_RE.exec(data);
|
|
199
216
|
while (match) {
|
|
217
|
+
if (match.index > lastIndex) {
|
|
218
|
+
handleKeyChunk(data.slice(lastIndex, match.index));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (batchScrollOffsets === null) {
|
|
222
|
+
batchScrollOffsets = new Map();
|
|
223
|
+
}
|
|
224
|
+
|
|
200
225
|
const mouse = parseSgrMatch(match);
|
|
201
226
|
if (mouse) handleMouseEvent(mouse);
|
|
227
|
+
|
|
228
|
+
lastIndex = match.index + match[0].length;
|
|
202
229
|
match = SGR_MOUSE_RE.exec(data);
|
|
203
230
|
}
|
|
231
|
+
|
|
232
|
+
if (lastIndex < data.length) {
|
|
233
|
+
handleKeyChunk(data.slice(lastIndex));
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
204
236
|
batchScrollOffsets = null;
|
|
205
|
-
|
|
237
|
+
batchTextInputEdits = null;
|
|
206
238
|
}
|
|
239
|
+
}
|
|
207
240
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
241
|
+
function handleKeyChunk(data: string): void {
|
|
242
|
+
if (data.length === 0) return;
|
|
243
|
+
for (const key of decodeKeyEvents(data)) {
|
|
244
|
+
handleKeyEvent(key);
|
|
245
|
+
}
|
|
211
246
|
}
|
|
212
247
|
|
|
213
248
|
interface MouseEvent {
|
|
@@ -453,49 +488,6 @@ function changeFocus(target: LayoutNode | null): void {
|
|
|
453
488
|
}
|
|
454
489
|
}
|
|
455
490
|
|
|
456
|
-
/**
|
|
457
|
-
* Compute the maximum scroll offset for a scrollable container or TextInput.
|
|
458
|
-
* Returns 0 if content fits within the viewport.
|
|
459
|
-
*/
|
|
460
|
-
function getMaxScrollOffset(target: LayoutNode): number {
|
|
461
|
-
const { rect, children } = target;
|
|
462
|
-
|
|
463
|
-
// TextInput: compute content height from wrapped text value
|
|
464
|
-
if (target.node.type === "textinput") {
|
|
465
|
-
const w = rect.width;
|
|
466
|
-
const value = target.node.props.value;
|
|
467
|
-
let lineCount = 0;
|
|
468
|
-
for (const rawLine of value.split("\n")) {
|
|
469
|
-
const lw = visibleWidth(rawLine);
|
|
470
|
-
lineCount += w > 0 && lw > w ? Math.ceil(lw / w) : 1;
|
|
471
|
-
}
|
|
472
|
-
return Math.max(0, lineCount - rect.height);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const isVertical = target.node.type === "vstack";
|
|
476
|
-
const props = target.node.type !== "text" ? target.node.props : null;
|
|
477
|
-
const padX = (props as any)?.padding?.x ?? 0;
|
|
478
|
-
const padY = (props as any)?.padding?.y ?? 0;
|
|
479
|
-
|
|
480
|
-
if (isVertical) {
|
|
481
|
-
let contentHeight = 0;
|
|
482
|
-
for (const child of children) {
|
|
483
|
-
const childBottom = child.rect.y + child.rect.height - rect.y;
|
|
484
|
-
if (childBottom > contentHeight) contentHeight = childBottom;
|
|
485
|
-
}
|
|
486
|
-
// Viewport is the inner height (minus bottom padding)
|
|
487
|
-
// Content starts at padY, so contentHeight includes top padding offset
|
|
488
|
-
return Math.max(0, contentHeight + padY - rect.height);
|
|
489
|
-
} else {
|
|
490
|
-
let contentWidth = 0;
|
|
491
|
-
for (const child of children) {
|
|
492
|
-
const childRight = child.rect.x + child.rect.width - rect.x;
|
|
493
|
-
if (childRight > contentWidth) contentWidth = childRight;
|
|
494
|
-
}
|
|
495
|
-
return Math.max(0, contentWidth + padX - rect.width);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
491
|
/**
|
|
500
492
|
* Resolve the current scroll offset for a layout node.
|
|
501
493
|
* Checks controlled (props.scrollOffset), then uncontrolled (path-based map).
|
|
@@ -539,7 +531,8 @@ function handleMouseEvent(event: MouseEvent): void {
|
|
|
539
531
|
if (event.type === "scroll-up" || event.type === "scroll-down") {
|
|
540
532
|
const target = findScrollTarget(path);
|
|
541
533
|
if (target) {
|
|
542
|
-
const
|
|
534
|
+
const step = getScrollStep(target);
|
|
535
|
+
const delta = event.type === "scroll-up" ? -step : step;
|
|
543
536
|
const maxOffset = getMaxScrollOffset(target);
|
|
544
537
|
|
|
545
538
|
if (target.node.type === "textinput") {
|
|
@@ -608,7 +601,18 @@ function findClickFocusTarget(path: LayoutNode[]): LayoutNode | null {
|
|
|
608
601
|
return null;
|
|
609
602
|
}
|
|
610
603
|
|
|
611
|
-
function
|
|
604
|
+
function getTextInputEditState(props: TextInputProps): EditState {
|
|
605
|
+
const batched = batchTextInputEdits?.get(props);
|
|
606
|
+
if (batched) return batched;
|
|
607
|
+
return {
|
|
608
|
+
value: props.value,
|
|
609
|
+
cursor: getTextInputCursor(props),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function handleKeyEvent(event: KeyInput): void {
|
|
614
|
+
const { key, text } = event;
|
|
615
|
+
|
|
612
616
|
// --- Focus traversal keys ---
|
|
613
617
|
|
|
614
618
|
// Tab / Shift+Tab: cycle through focusable elements
|
|
@@ -682,12 +686,10 @@ function handleKeyEvent(key: string, rawData?: string): void {
|
|
|
682
686
|
}
|
|
683
687
|
|
|
684
688
|
// --- TextInput key routing ---
|
|
685
|
-
// Find the focused TextInput (if any) to route editing keys
|
|
686
689
|
const focusedInput = findFocusedTextInput();
|
|
687
690
|
|
|
688
691
|
if (focusedInput) {
|
|
689
|
-
const props = focusedInput.node
|
|
690
|
-
.props as import("@cel-tui/types").TextInputProps;
|
|
692
|
+
const props = focusedInput.node.props as TextInputProps;
|
|
691
693
|
|
|
692
694
|
// onKeyPress fires before editing — return false prevents the default action
|
|
693
695
|
if (props.onKeyPress) {
|
|
@@ -698,12 +700,12 @@ function handleKeyEvent(key: string, rawData?: string): void {
|
|
|
698
700
|
}
|
|
699
701
|
}
|
|
700
702
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const cursor = getTextInputCursor(props);
|
|
704
|
-
const editState: EditState = { value: props.value, cursor };
|
|
705
|
-
let newState: EditState | null = null;
|
|
703
|
+
let newState: EditState | null = null;
|
|
704
|
+
const editState = getTextInputEditState(props);
|
|
706
705
|
|
|
706
|
+
if (text !== undefined) {
|
|
707
|
+
newState = insertChar(editState, text);
|
|
708
|
+
} else if (isEditingKey(key)) {
|
|
707
709
|
switch (key) {
|
|
708
710
|
case "backspace":
|
|
709
711
|
newState = deleteBackward(editState);
|
|
@@ -719,8 +721,7 @@ function handleKeyEvent(key: string, rawData?: string): void {
|
|
|
719
721
|
case "end":
|
|
720
722
|
{
|
|
721
723
|
const tiPadX =
|
|
722
|
-
(focusedInput.node as
|
|
723
|
-
.props.padding?.x ?? 0;
|
|
724
|
+
(focusedInput.node as TextInputNode).props.padding?.x ?? 0;
|
|
724
725
|
const contentWidth = Math.max(
|
|
725
726
|
0,
|
|
726
727
|
focusedInput.rect.width - tiPadX * 2,
|
|
@@ -745,24 +746,17 @@ function handleKeyEvent(key: string, rawData?: string): void {
|
|
|
745
746
|
case "plus":
|
|
746
747
|
newState = insertChar(editState, "+");
|
|
747
748
|
break;
|
|
748
|
-
default:
|
|
749
|
-
// Single printable character — use raw data to preserve case
|
|
750
|
-
if (key.length === 1 && rawData && rawData.length === 1) {
|
|
751
|
-
newState = insertChar(editState, rawData);
|
|
752
|
-
} else if (key.length === 1) {
|
|
753
|
-
newState = insertChar(editState, key);
|
|
754
|
-
}
|
|
755
|
-
break;
|
|
756
749
|
}
|
|
750
|
+
}
|
|
757
751
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
cel.render();
|
|
764
|
-
return;
|
|
752
|
+
if (newState && newState !== editState) {
|
|
753
|
+
batchTextInputEdits?.set(props, newState);
|
|
754
|
+
setTextInputCursor(props, newState.cursor);
|
|
755
|
+
if (newState.value !== editState.value) {
|
|
756
|
+
props.onChange(newState.value);
|
|
765
757
|
}
|
|
758
|
+
cel.render();
|
|
759
|
+
return;
|
|
766
760
|
}
|
|
767
761
|
}
|
|
768
762
|
|
|
@@ -890,6 +884,7 @@ export const cel = {
|
|
|
890
884
|
init(term: Terminal, options?: { theme?: Theme }): void {
|
|
891
885
|
terminal = term;
|
|
892
886
|
activeTheme = options?.theme ?? defaultTheme;
|
|
887
|
+
lastTerminalCursor = { visible: false };
|
|
893
888
|
terminal.start(handleInput, () => cel.render());
|
|
894
889
|
},
|
|
895
890
|
|
|
@@ -935,6 +930,7 @@ export const cel = {
|
|
|
935
930
|
stampedNode = null;
|
|
936
931
|
uncontrolledScrollOffsets.clear();
|
|
937
932
|
stampedScrollNodes = [];
|
|
933
|
+
lastTerminalCursor = { visible: false };
|
|
938
934
|
activeTheme = defaultTheme;
|
|
939
935
|
},
|
|
940
936
|
|
package/src/emitter.ts
CHANGED
|
@@ -100,8 +100,61 @@ function hasStyle(cell: Cell): boolean {
|
|
|
100
100
|
const SYNC_START = "\x1b[?2026h";
|
|
101
101
|
const SYNC_END = "\x1b[?2026l";
|
|
102
102
|
const CURSOR_HOME = "\x1b[H";
|
|
103
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
104
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
105
|
+
const SAVE_CURSOR = "\x1b7";
|
|
106
|
+
const RESTORE_CURSOR = "\x1b8";
|
|
103
107
|
const RESET = "\x1b[0m";
|
|
104
108
|
|
|
109
|
+
type CursorState =
|
|
110
|
+
| { visible: false }
|
|
111
|
+
| {
|
|
112
|
+
visible: true;
|
|
113
|
+
x: number;
|
|
114
|
+
y: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
interface EmitOptions {
|
|
118
|
+
cursor?: CursorState;
|
|
119
|
+
previousCursor?: CursorState | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sameVisibleCursor(
|
|
123
|
+
a: CursorState | null | undefined,
|
|
124
|
+
b: CursorState | null | undefined,
|
|
125
|
+
): boolean {
|
|
126
|
+
return (
|
|
127
|
+
a?.visible === true && b?.visible === true && a.x === b.x && a.y === b.y
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getCursorControls(
|
|
132
|
+
options: EmitOptions | undefined,
|
|
133
|
+
hasPaintOutput: boolean,
|
|
134
|
+
): { prefix: string; suffix: string } {
|
|
135
|
+
const cursor = options?.cursor ?? { visible: false };
|
|
136
|
+
const previous = options?.previousCursor ?? null;
|
|
137
|
+
|
|
138
|
+
if (sameVisibleCursor(cursor, previous)) {
|
|
139
|
+
return hasPaintOutput
|
|
140
|
+
? { prefix: SAVE_CURSOR, suffix: RESTORE_CURSOR }
|
|
141
|
+
: { prefix: "", suffix: "" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (cursor.visible) {
|
|
145
|
+
return {
|
|
146
|
+
prefix: "",
|
|
147
|
+
suffix:
|
|
148
|
+
`\x1b[${cursor.y + 1};${cursor.x + 1}H` +
|
|
149
|
+
(previous?.visible === true ? "" : SHOW_CURSOR),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return previous?.visible === true
|
|
154
|
+
? { prefix: "", suffix: HIDE_CURSOR }
|
|
155
|
+
: { prefix: "", suffix: "" };
|
|
156
|
+
}
|
|
157
|
+
|
|
105
158
|
/**
|
|
106
159
|
* Emit a full cell buffer as an ANSI string for terminal output.
|
|
107
160
|
*
|
|
@@ -114,13 +167,16 @@ const RESET = "\x1b[0m";
|
|
|
114
167
|
*
|
|
115
168
|
* @param buf - The cell buffer to render.
|
|
116
169
|
* @param theme - Color theme mapping. Defaults to the ANSI 16 theme.
|
|
170
|
+
* @param options - Optional terminal cursor state to apply before the synchronized output ends.
|
|
117
171
|
* @returns A complete ANSI string ready to write to the terminal.
|
|
118
172
|
*/
|
|
119
173
|
export function emitBuffer(
|
|
120
174
|
buf: CellBuffer,
|
|
121
175
|
theme: Theme = defaultTheme,
|
|
176
|
+
options?: EmitOptions,
|
|
122
177
|
): string {
|
|
123
|
-
|
|
178
|
+
const cursorControls = getCursorControls(options, true);
|
|
179
|
+
let out = SYNC_START + cursorControls.prefix + CURSOR_HOME;
|
|
124
180
|
|
|
125
181
|
let lastStyle: Cell | null = null;
|
|
126
182
|
|
|
@@ -153,7 +209,7 @@ export function emitBuffer(
|
|
|
153
209
|
out += RESET;
|
|
154
210
|
}
|
|
155
211
|
|
|
156
|
-
out += SYNC_END;
|
|
212
|
+
out += cursorControls.suffix + SYNC_END;
|
|
157
213
|
return out;
|
|
158
214
|
}
|
|
159
215
|
|
|
@@ -167,18 +223,21 @@ export function emitBuffer(
|
|
|
167
223
|
* @param prev - The previous buffer.
|
|
168
224
|
* @param next - The new buffer.
|
|
169
225
|
* @param theme - Color theme mapping. Defaults to the ANSI 16 theme.
|
|
226
|
+
* @param options - Optional terminal cursor state to apply before the synchronized output ends.
|
|
170
227
|
* @returns An ANSI string with only the changed cells.
|
|
171
228
|
*/
|
|
172
229
|
export function emitDiff(
|
|
173
230
|
prev: CellBuffer,
|
|
174
231
|
next: CellBuffer,
|
|
175
232
|
theme: Theme = defaultTheme,
|
|
233
|
+
options?: EmitOptions,
|
|
176
234
|
): string {
|
|
177
|
-
let out = SYNC_START;
|
|
178
|
-
|
|
179
235
|
const changes = prev.diff(next);
|
|
236
|
+
const cursorControls = getCursorControls(options, changes.length > 0);
|
|
237
|
+
let out = SYNC_START + cursorControls.prefix;
|
|
238
|
+
|
|
180
239
|
if (changes.length === 0) {
|
|
181
|
-
return out + SYNC_END;
|
|
240
|
+
return out + cursorControls.suffix + SYNC_END;
|
|
182
241
|
}
|
|
183
242
|
|
|
184
243
|
let lastStyle: Cell | null = null;
|
|
@@ -222,6 +281,6 @@ export function emitDiff(
|
|
|
222
281
|
out += RESET;
|
|
223
282
|
}
|
|
224
283
|
|
|
225
|
-
out += SYNC_END;
|
|
284
|
+
out += cursorControls.suffix + SYNC_END;
|
|
226
285
|
return out;
|
|
227
286
|
}
|