@valyrianjs/terminal 0.2.1 → 0.2.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/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +12 -14
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- package/dist/events.js.map +1 -1
- package/dist/frame-style.d.ts +7 -0
- package/dist/frame-style.d.ts.map +1 -0
- package/dist/frame-style.js +27 -0
- package/dist/frame-style.js.map +1 -0
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +53 -23
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +8 -1
- package/dist/mouse.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +87 -48
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -0
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts +7 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +114 -0
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +6 -3
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +5 -5
- package/docs/primitive-gallery.md +4 -4
- package/examples/basic.tsx +22 -0
- package/examples/cli.tsx +55 -0
- package/examples/demo.tsx +98 -0
- package/examples/docs/background-fill.tsx +107 -0
- package/examples/docs/component-composition.tsx +140 -0
- package/examples/docs/cursor.tsx +121 -0
- package/examples/docs/employees-list.tsx +138 -0
- package/examples/docs/hello.tsx +98 -0
- package/examples/docs/interactive-note.tsx +111 -0
- package/examples/docs/module-api-dashboard.tsx +307 -0
- package/examples/docs/module-flux-store.tsx +181 -0
- package/examples/docs/module-form-workflow.tsx +339 -0
- package/examples/docs/module-forms.tsx +218 -0
- package/examples/docs/module-money.tsx +175 -0
- package/examples/docs/module-native-store.tsx +188 -0
- package/examples/docs/module-pulses.tsx +142 -0
- package/examples/docs/module-query.tsx +209 -0
- package/examples/docs/module-request.tsx +194 -0
- package/examples/docs/module-state-workbench.tsx +283 -0
- package/examples/docs/module-tasks.tsx +223 -0
- package/examples/docs/module-translate.tsx +194 -0
- package/examples/docs/module-utils.tsx +168 -0
- package/examples/docs/module-valyrian-core.tsx +159 -0
- package/examples/docs/pizza-builder.tsx +463 -0
- package/examples/docs/primitive-activity-console.tsx +113 -0
- package/examples/docs/primitive-command-panel.tsx +186 -0
- package/examples/docs/primitive-data-explorer.tsx +155 -0
- package/examples/docs/primitive-input-workbench.tsx +128 -0
- package/examples/docs/primitive-layout-shell.tsx +115 -0
- package/examples/docs/responsive-split.tsx +186 -0
- package/examples/docs/style-system.tsx +209 -0
- package/examples/docs/theme-colors.tsx +225 -0
- package/examples/docs/virtualized-list-workbench.tsx +232 -0
- package/examples/opencode-dogfood-app.tsx +215 -0
- package/examples/opencode-dogfood-lifecycle.tsx +194 -0
- package/examples/opencode-dogfood.tsx +11 -0
- package/llms-full.txt +16 -13
- package/package.json +3 -2
- package/src/ansi.ts +12 -14
- package/src/events.ts +2 -0
- package/src/frame-style.ts +36 -0
- package/src/layout.ts +57 -24
- package/src/mouse.ts +10 -1
- package/src/render.ts +92 -48
- package/src/session.ts +2 -0
- package/src/text.ts +148 -0
- package/src/types.ts +3 -0
package/src/render.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { constrainFrame, createFrame, cropFrame, fitFrame, getFrameHeight, getFrameWidth, mergeHorizontal, mergeVertical, overlayFrame, shiftFrame } from "./layout.js";
|
|
2
2
|
import { getSelectionRange, normalizeInputState } from "./events.js";
|
|
3
|
+
import { markFullFrameSpan, markFullRowSpan } from "./frame-style.js";
|
|
3
4
|
import { createEditorState } from "./editor-state.js";
|
|
4
5
|
import { isFocusable, textContent } from "./tree.js";
|
|
5
6
|
import { renderValyrianTerminal } from "./runtime.js";
|
|
6
|
-
import { plainText } from "./text.js";
|
|
7
|
+
import { cursorCellOffset, padEndTerminalCells, plainText, sliceTerminalCells, terminalCellToStringIndex, terminalCellWidth, terminalGraphemes } from "./text.js";
|
|
7
8
|
import { resolveTerminalStyle } from "./theme.js";
|
|
8
9
|
|
|
9
10
|
import type { InputInteractionState, TerminalElementNode, TerminalFocusNode, TerminalFrame, TerminalHitbox, TerminalNode, TerminalSpacing, TerminalSplitBreakpoint, TerminalSplitSize, TerminalStyleDefinition, TerminalStyleSpan, TerminalTheme, TerminalVisualState } from "./types.js";
|
|
@@ -173,7 +174,7 @@ function fullFrameSpans(kinds: string[], width: number, height: number): Termina
|
|
|
173
174
|
const spans: TerminalStyleSpan[] = [];
|
|
174
175
|
for (const kind of kinds) {
|
|
175
176
|
for (let y = 1; y <= height; y += 1) {
|
|
176
|
-
spans.push({ kind, x1: 1, x2: width + 1, y });
|
|
177
|
+
spans.push(markFullFrameSpan({ kind, x1: 1, x2: width + 1, y }));
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
return spans;
|
|
@@ -209,7 +210,7 @@ function padFrameSides(frame: TerminalFrame, padding: SpacingSides) {
|
|
|
209
210
|
const bottomLines = new Array<string>(padding.bottom).fill(" ".repeat(contentWidth));
|
|
210
211
|
const lines = [
|
|
211
212
|
...topLines,
|
|
212
|
-
...frame.lines.map((line) => `${" ".repeat(padding.left)}${line
|
|
213
|
+
...frame.lines.map((line) => `${" ".repeat(padding.left)}${padEndTerminalCells(line, width)}${" ".repeat(padding.right)}`),
|
|
213
214
|
...bottomLines
|
|
214
215
|
];
|
|
215
216
|
return shiftFrame(createFrame(lines, frame.hitboxes, frame.cursor, frame.spans), padding.left, padding.top);
|
|
@@ -227,7 +228,7 @@ function addBorder(frame: TerminalFrame, border: BorderSides) {
|
|
|
227
228
|
lines.push(`${border.left ? chars.topLeft : chars.horizontal}${chars.horizontal.repeat(Math.max(0, width - left - right))}${border.right ? chars.topRight : chars.horizontal}`.slice(0, width));
|
|
228
229
|
}
|
|
229
230
|
for (const line of frame.lines) {
|
|
230
|
-
lines.push(`${border.left ? chars.vertical : ""}${line
|
|
231
|
+
lines.push(`${border.left ? chars.vertical : ""}${padEndTerminalCells(line, innerWidth)}${border.right ? chars.vertical : ""}`);
|
|
231
232
|
}
|
|
232
233
|
if (border.bottom) {
|
|
233
234
|
lines.push(`${border.left ? chars.bottomLeft : chars.horizontal}${chars.horizontal.repeat(Math.max(0, width - left - right))}${border.right ? chars.bottomRight : chars.horizontal}`.slice(0, width));
|
|
@@ -252,7 +253,7 @@ function addFullFrameSpans(frame: TerminalFrame, kinds: ResolvedStyleSpan[]) {
|
|
|
252
253
|
const spans = frame.spans.slice();
|
|
253
254
|
for (const span of kinds) {
|
|
254
255
|
for (let y = 1; y <= height; y += 1) {
|
|
255
|
-
spans.push({ ...span, x1: 1, x2: width + 1, y });
|
|
256
|
+
spans.push(markFullFrameSpan({ ...span, x1: 1, x2: width + 1, y }));
|
|
256
257
|
}
|
|
257
258
|
}
|
|
258
259
|
return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
|
|
@@ -325,15 +326,22 @@ function wrapPlainText(value: string, width: number) {
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
let remaining = sourceRow;
|
|
328
|
-
while (remaining
|
|
329
|
-
const slice = remaining
|
|
329
|
+
while (terminalCellWidth(remaining) > width) {
|
|
330
|
+
const slice = sliceTerminalCells(remaining, width);
|
|
331
|
+
if (slice.length === 0) {
|
|
332
|
+
const [firstGrapheme = ""] = terminalGraphemes(remaining);
|
|
333
|
+
rows.push(firstGrapheme);
|
|
334
|
+
remaining = remaining.slice(firstGrapheme.length);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
330
338
|
const breakAt = slice.lastIndexOf(" ");
|
|
331
339
|
if (breakAt > 0 && breakAt >= Math.floor(width * 0.6)) {
|
|
332
340
|
rows.push(remaining.slice(0, breakAt));
|
|
333
341
|
remaining = remaining.slice(breakAt + 1);
|
|
334
342
|
} else {
|
|
335
343
|
rows.push(slice);
|
|
336
|
-
remaining = remaining.slice(
|
|
344
|
+
remaining = remaining.slice(slice.length);
|
|
337
345
|
}
|
|
338
346
|
}
|
|
339
347
|
rows.push(remaining);
|
|
@@ -465,7 +473,8 @@ function decorateContainerFrame(frame: TerminalFrame, node: TerminalElementNode,
|
|
|
465
473
|
if ((typeof width === "number" && width > 0) || (typeof height === "number" && height > 0)) {
|
|
466
474
|
next = constrainFrame(next, {
|
|
467
475
|
width: typeof width === "number" && width > 0 ? width : undefined,
|
|
468
|
-
height: typeof height === "number" && height > 0 ? height : undefined
|
|
476
|
+
height: typeof height === "number" && height > 0 ? height : undefined,
|
|
477
|
+
expandFullFrameSpans: true
|
|
469
478
|
});
|
|
470
479
|
}
|
|
471
480
|
}
|
|
@@ -474,7 +483,8 @@ function decorateContainerFrame(frame: TerminalFrame, node: TerminalElementNode,
|
|
|
474
483
|
if (options.constrain) {
|
|
475
484
|
next = constrainFrame(next, {
|
|
476
485
|
width: typeof options.width === "undefined" ? positiveDimension(node.props.width, "width") : options.width,
|
|
477
|
-
height: typeof options.height === "undefined" ? positiveDimension(node.props.height, "height") : options.height
|
|
486
|
+
height: typeof options.height === "undefined" ? positiveDimension(node.props.height, "height") : options.height,
|
|
487
|
+
expandFullFrameSpans: true
|
|
478
488
|
});
|
|
479
489
|
}
|
|
480
490
|
next = addContainerStyleSpans(next, node, resolved);
|
|
@@ -538,6 +548,19 @@ function resolveContainerChildContext(node: TerminalElementNode, dimensions: { w
|
|
|
538
548
|
};
|
|
539
549
|
}
|
|
540
550
|
|
|
551
|
+
function interactiveTextMetadata(value: string) {
|
|
552
|
+
const textLength = value.length;
|
|
553
|
+
const textCellToStringIndex = terminalCellToStringIndex(value);
|
|
554
|
+
const usesLinearIndexes = textCellToStringIndex.length === textLength + 1
|
|
555
|
+
&& textCellToStringIndex.every((index, cellOffset) => index === cellOffset);
|
|
556
|
+
|
|
557
|
+
if (usesLinearIndexes) {
|
|
558
|
+
return { textLength };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return { textLength, textCellToStringIndex };
|
|
562
|
+
}
|
|
563
|
+
|
|
541
564
|
function renderInputLine(value: string, inputState: InputInteractionState, padding: SpacingSides = { top: 0, right: 0, bottom: 0, left: 0 }) {
|
|
542
565
|
const state = normalizeInputState(inputState, value.length);
|
|
543
566
|
const { start, end } = getSelectionRange(state);
|
|
@@ -546,12 +569,29 @@ function renderInputLine(value: string, inputState: InputInteractionState, paddi
|
|
|
546
569
|
const textStart = padding.left + 1;
|
|
547
570
|
return {
|
|
548
571
|
line: paddedLine,
|
|
549
|
-
cursor: { x: textStart + state.cursor, y: 1 },
|
|
550
|
-
spans: start === end ? [] : [{ kind: "input.selection", x1: textStart + start, x2: textStart + end, y: 1 }]
|
|
572
|
+
cursor: { x: textStart + cursorCellOffset(value, state.cursor), y: 1 },
|
|
573
|
+
spans: start === end ? [] : [{ kind: "input.selection", x1: textStart + cursorCellOffset(value, start), x2: textStart + cursorCellOffset(value, end), y: 1 }]
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function resolveEditorDimensions(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
578
|
+
const explicitWidth = positiveDimension(node.props.width, "width");
|
|
579
|
+
const explicitHeight = positiveDimension(node.props.height, "height");
|
|
580
|
+
return {
|
|
581
|
+
width: typeof explicitWidth !== "undefined"
|
|
582
|
+
? explicitWidth
|
|
583
|
+
: node.props.fill === true
|
|
584
|
+
? fillContextDimension(context, "width", "Editor")
|
|
585
|
+
: undefined,
|
|
586
|
+
height: typeof explicitHeight !== "undefined"
|
|
587
|
+
? explicitHeight
|
|
588
|
+
: node.props.fill === true
|
|
589
|
+
? fillContextDimension(context, "height", "Editor")
|
|
590
|
+
: undefined
|
|
551
591
|
};
|
|
552
592
|
}
|
|
553
593
|
|
|
554
|
-
function renderEditorFrame(node: TerminalElementNode) {
|
|
594
|
+
function renderEditorFrame(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
555
595
|
const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : "";
|
|
556
596
|
const placeholder = typeof node.props.placeholder !== "undefined" ? plainText(node.props.placeholder) : "";
|
|
557
597
|
const displayValue = value.length === 0 && !node.props.__focused && placeholder ? placeholder : value;
|
|
@@ -566,23 +606,28 @@ function renderEditorFrame(node: TerminalElementNode) {
|
|
|
566
606
|
|
|
567
607
|
return `> ${line.slice(0, focusedColumn)}|${line.slice(focusedColumn)}`;
|
|
568
608
|
});
|
|
569
|
-
const width = lines.reduce((max, line) => Math.max(max, line
|
|
570
|
-
const cursor = node.props.__focused ? { x: 3 + focusedColumn, y: focusedLine + 1 } : null;
|
|
571
|
-
const spans: TerminalStyleSpan[] = node.props.__focused ? [{ kind: "focus", x1: 1, x2: Math.max(2, lines[focusedLine]
|
|
572
|
-
const
|
|
609
|
+
const width = lines.reduce((max, line) => Math.max(max, terminalCellWidth(line)), 0);
|
|
610
|
+
const cursor = node.props.__focused ? { x: 3 + cursorCellOffset(focusedState.lines[focusedLine], focusedColumn), y: focusedLine + 1 } : null;
|
|
611
|
+
const spans: TerminalStyleSpan[] = node.props.__focused ? [{ kind: "focus", x1: 1, x2: Math.max(2, terminalCellWidth(lines[focusedLine]) + 1), y: focusedLine + 1 }] : [];
|
|
612
|
+
const dimensions = resolveEditorDimensions(node, context);
|
|
573
613
|
const frame = createFrame(lines, [], cursor, spans);
|
|
574
|
-
const scrollOffset = node.props.__focused && typeof height !== "undefined"
|
|
575
|
-
? Math.min(Math.max(0, focusedLine - height + 1), Math.max(0, lines.length - height))
|
|
614
|
+
const scrollOffset = node.props.__focused && typeof dimensions.height !== "undefined"
|
|
615
|
+
? Math.min(Math.max(0, focusedLine - dimensions.height + 1), Math.max(0, lines.length - dimensions.height))
|
|
576
616
|
: 0;
|
|
577
|
-
const
|
|
617
|
+
const croppedFrame = typeof dimensions.height === "undefined"
|
|
578
618
|
? frame
|
|
579
|
-
:
|
|
619
|
+
: scrollOffset > 0
|
|
620
|
+
? cropFrame(frame, scrollOffset, dimensions.height)
|
|
621
|
+
: frame;
|
|
622
|
+
const constrainedFrame = typeof dimensions.width === "undefined" && typeof dimensions.height === "undefined"
|
|
623
|
+
? croppedFrame
|
|
624
|
+
: constrainFrame(croppedFrame, { ...dimensions, expandFullFrameSpans: true });
|
|
580
625
|
|
|
581
626
|
if (!node.props.id) {
|
|
582
627
|
return constrainedFrame;
|
|
583
628
|
}
|
|
584
629
|
|
|
585
|
-
return createFrame(constrainedFrame.lines, [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(1, getFrameWidth(constrainedFrame)
|
|
630
|
+
return createFrame(constrainedFrame.lines, [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(1, getFrameWidth(constrainedFrame)), y1: 1, y2: getFrameHeight(constrainedFrame), textStartX: 3, ...interactiveTextMetadata(value) }], constrainedFrame.cursor, constrainedFrame.spans);
|
|
586
631
|
}
|
|
587
632
|
|
|
588
633
|
function notifyLayoutContextProbe(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
@@ -618,13 +663,12 @@ function renderTableFrame(node: TerminalElementNode, context?: TerminalRenderCon
|
|
|
618
663
|
const cells = new Array<TerminalFrame>(columnCount).fill(createFrame([""])).map((cell, index) => row[index] || cell);
|
|
619
664
|
const rowHeight = cells.reduce((max, cell) => Math.max(max, getFrameHeight(cell)), 0);
|
|
620
665
|
const normalized = cells.map((cell, index) => ({
|
|
621
|
-
frame: cell,
|
|
622
|
-
width: columnWidths[index]
|
|
623
|
-
lines: [...cell.lines.map((line) => line.padEnd(columnWidths[index], " ")), ...new Array<string>(Math.max(0, rowHeight - getFrameHeight(cell))).fill(" ".repeat(columnWidths[index]))]
|
|
666
|
+
frame: fitFrame(cell, columnWidths[index], rowHeight, { expandFullFrameSpans: true }),
|
|
667
|
+
width: columnWidths[index]
|
|
624
668
|
}));
|
|
625
669
|
|
|
626
670
|
for (let rowIndex = 0; rowIndex < rowHeight; rowIndex += 1) {
|
|
627
|
-
lines.push(normalized.map((cell) => cell.lines[rowIndex]).join(" | "));
|
|
671
|
+
lines.push(normalized.map((cell) => cell.frame.lines[rowIndex]).join(" | "));
|
|
628
672
|
}
|
|
629
673
|
|
|
630
674
|
let xOffset = 0;
|
|
@@ -775,7 +819,7 @@ function renderSplitFrame(node: TerminalElementNode, context?: TerminalRenderCon
|
|
|
775
819
|
if (cellWidth <= 0 || cellHeight <= 0) {
|
|
776
820
|
continue;
|
|
777
821
|
}
|
|
778
|
-
frames.push(constrainFrame(renderSplitChildFrame(node.children[index], cellWidth, cellHeight, context), { width: cellWidth, height: cellHeight }));
|
|
822
|
+
frames.push(constrainFrame(renderSplitChildFrame(node.children[index], cellWidth, cellHeight, context), { width: cellWidth, height: cellHeight, expandFullFrameSpans: true }));
|
|
779
823
|
}
|
|
780
824
|
|
|
781
825
|
const frame = frames.length
|
|
@@ -816,7 +860,7 @@ function renderBodyFrame(children: TerminalNode[], props: TerminalElementNode["p
|
|
|
816
860
|
}
|
|
817
861
|
|
|
818
862
|
function renderFixedChildFrame(node: TerminalElementNode, width: number, height: number) {
|
|
819
|
-
return constrainFrame(renderBodyFrame(node.children, {}, { cols: width, rows: height }), { width, height });
|
|
863
|
+
return constrainFrame(renderBodyFrame(node.children, {}, { cols: width, rows: height }), { width, height, expandFullFrameSpans: true });
|
|
820
864
|
}
|
|
821
865
|
|
|
822
866
|
function renderFixedCompositionFrame(node: TerminalElementNode, width: number, height: number): TerminalFrame {
|
|
@@ -859,14 +903,14 @@ function renderFixedCompositionFrame(node: TerminalElementNode, width: number, h
|
|
|
859
903
|
const leftFrames = fixedNodes.left.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
|
|
860
904
|
const rightFrames = fixedNodes.right.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
|
|
861
905
|
const bodyFrames = bodyWidth > 0
|
|
862
|
-
? [constrainFrame(renderBodyFrame(bodyChildren, node.props, { cols: bodyWidth, rows: middleHeight }), { width: bodyWidth, height: middleHeight })]
|
|
906
|
+
? [constrainFrame(renderBodyFrame(bodyChildren, node.props, { cols: bodyWidth, rows: middleHeight }), { width: bodyWidth, height: middleHeight, expandFullRowSpans: true })]
|
|
863
907
|
: [];
|
|
864
908
|
|
|
865
|
-
middleFrames.push(constrainFrame(mergeHorizontal([...leftFrames, ...bodyFrames, ...rightFrames]), { width, height: middleHeight }));
|
|
909
|
+
middleFrames.push(constrainFrame(mergeHorizontal([...leftFrames, ...bodyFrames, ...rightFrames]), { width, height: middleHeight, expandFullRowSpans: true }));
|
|
866
910
|
}
|
|
867
911
|
|
|
868
912
|
const frame = mergeVertical([...topFrames, ...middleFrames, ...bottomFrames]);
|
|
869
|
-
return constrainFrame(frame, { width, height });
|
|
913
|
+
return constrainFrame(frame, { width, height, expandFullFrameSpans: true });
|
|
870
914
|
}
|
|
871
915
|
|
|
872
916
|
function renderStandaloneFixedFrame(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
@@ -940,7 +984,7 @@ function overlayGeometry(node: TerminalElementNode, width: number, height: numbe
|
|
|
940
984
|
|
|
941
985
|
function renderOverlayChildFrame(node: TerminalElementNode, width: number, height: number, context?: TerminalRenderContext) {
|
|
942
986
|
const geometry = overlayGeometry(node, width, height);
|
|
943
|
-
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height });
|
|
987
|
+
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height, expandFullRowSpans: true });
|
|
944
988
|
frame = addContainerStyleSpans(frame, node, resolveNodeStyle(node, context));
|
|
945
989
|
if (node.props.id && isFocusable(node)) {
|
|
946
990
|
frame = addFocusableHitbox(frame, node as TerminalFocusNode);
|
|
@@ -993,7 +1037,7 @@ function renderScreenFrame(node: TerminalElementNode, context?: TerminalRenderCo
|
|
|
993
1037
|
}
|
|
994
1038
|
base = mergeVertical(parts);
|
|
995
1039
|
if (context && overlays.length) {
|
|
996
|
-
base = constrainFrame(base, { width: context.cols, height: context.rows });
|
|
1040
|
+
base = constrainFrame(base, { width: context.cols, height: context.rows, expandFullRowSpans: true });
|
|
997
1041
|
}
|
|
998
1042
|
}
|
|
999
1043
|
|
|
@@ -1066,13 +1110,13 @@ function renderLogViewFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1066
1110
|
}
|
|
1067
1111
|
}
|
|
1068
1112
|
if (typeof innerWidth !== "undefined") {
|
|
1069
|
-
frame = constrainFrame(frame, { width: innerWidth, height: innerHeight });
|
|
1113
|
+
frame = constrainFrame(frame, { width: innerWidth, height: innerHeight, expandFullFrameSpans: true });
|
|
1070
1114
|
}
|
|
1071
1115
|
}
|
|
1072
1116
|
frame = padFrameSides(frame, padding);
|
|
1073
1117
|
frame = addBorder(frame, border);
|
|
1074
1118
|
if (typeof dimensions.width !== "undefined") {
|
|
1075
|
-
frame = constrainFrame(frame, { width: dimensions.width, height: dimensions.height });
|
|
1119
|
+
frame = constrainFrame(frame, { width: dimensions.width, height: dimensions.height, expandFullFrameSpans: true });
|
|
1076
1120
|
} else if (typeof dimensions.height !== "undefined") {
|
|
1077
1121
|
frame = cropFrame(frame, 0, dimensions.height);
|
|
1078
1122
|
while (typeof dimensions.height !== "undefined" && getFrameHeight(frame) < dimensions.height) {
|
|
@@ -1103,18 +1147,18 @@ function renderSeparatedRowFrame(frames: TerminalFrame[], separator = " | ") {
|
|
|
1103
1147
|
const frame = frames[index];
|
|
1104
1148
|
const width = widths[index];
|
|
1105
1149
|
for (let row = 0; row < height; row += 1) {
|
|
1106
|
-
lines[row] += (frame.lines[row] || ""
|
|
1150
|
+
lines[row] += padEndTerminalCells(frame.lines[row] || "", width);
|
|
1107
1151
|
if (index < frames.length - 1) {
|
|
1108
1152
|
lines[row] += separator;
|
|
1109
1153
|
}
|
|
1110
1154
|
}
|
|
1111
|
-
const shifted = shiftFrame(frame, xOffset, 0);
|
|
1155
|
+
const shifted = shiftFrame(fitFrame(frame, width, height, { expandFullFrameSpans: true }), xOffset, 0);
|
|
1112
1156
|
hitboxes.push(...shifted.hitboxes);
|
|
1113
1157
|
spans.push(...shifted.spans);
|
|
1114
1158
|
if (!cursor && frame.cursor) {
|
|
1115
1159
|
cursor = { x: frame.cursor.x + xOffset, y: frame.cursor.y };
|
|
1116
1160
|
}
|
|
1117
|
-
xOffset += width + (index < frames.length - 1 ? separator
|
|
1161
|
+
xOffset += width + (index < frames.length - 1 ? terminalCellWidth(separator) : 0);
|
|
1118
1162
|
}
|
|
1119
1163
|
|
|
1120
1164
|
return createFrame(lines, hitboxes, cursor, spans);
|
|
@@ -1150,7 +1194,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1150
1194
|
if (node.tag === "terminal-scroll") {
|
|
1151
1195
|
const offset = numeric(node.props.__scrollOffset, 0);
|
|
1152
1196
|
if (typeof dimensions.width !== "undefined") {
|
|
1153
|
-
frame = constrainFrame(frame, { width: dimensions.width });
|
|
1197
|
+
frame = constrainFrame(frame, { width: dimensions.width, expandFullFrameSpans: true });
|
|
1154
1198
|
}
|
|
1155
1199
|
const height = numeric(dimensions.height ?? node.props.height, getFrameHeight(frame));
|
|
1156
1200
|
frame = cropFrame(frame, offset, height || getFrameHeight(frame));
|
|
@@ -1159,7 +1203,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1159
1203
|
const spans = frame.spans.slice();
|
|
1160
1204
|
for (let index = 0; index < frame.lines.length; index += 1) {
|
|
1161
1205
|
const row = index + 1;
|
|
1162
|
-
const width = frame.lines[index]
|
|
1206
|
+
const width = terminalCellWidth(frame.lines[index]) + 1;
|
|
1163
1207
|
if (highlightRows.includes(row)) {
|
|
1164
1208
|
spans.push({ kind: "highlight", x1: 1, x2: width, y: row });
|
|
1165
1209
|
}
|
|
@@ -1241,15 +1285,15 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1241
1285
|
for (let index = 0; index < frameLines.length; index += 1) {
|
|
1242
1286
|
const sourceIndex = frameItemIndexes[index];
|
|
1243
1287
|
const y = itemY + index;
|
|
1244
|
-
spans.push({ kind: "list.base", x1: 1, x2: width + 1, y });
|
|
1288
|
+
spans.push(markFullRowSpan({ kind: "list.base", x1: 1, x2: width + 1, y }));
|
|
1245
1289
|
if (selectedIndex !== null && selectedIndex !== activeIndex && sourceIndex === selectedIndex) {
|
|
1246
|
-
spans.push({ kind: "list.selected", x1: 1, x2: width + 1, y });
|
|
1290
|
+
spans.push(markFullRowSpan({ kind: "list.selected", x1: 1, x2: width + 1, y }));
|
|
1247
1291
|
}
|
|
1248
1292
|
if (sourceIndex === activeIndex) {
|
|
1249
|
-
spans.push({ kind: "list.current", x1: 1, x2: width + 1, y });
|
|
1293
|
+
spans.push(markFullRowSpan({ kind: "list.current", x1: 1, x2: width + 1, y }));
|
|
1250
1294
|
}
|
|
1251
1295
|
if (sourceIndex === hoveredIndex) {
|
|
1252
|
-
spans.push({ kind: "list.hover", x1: 1, x2: width + 1, y });
|
|
1296
|
+
spans.push(markFullRowSpan({ kind: "list.hover", x1: 1, x2: width + 1, y }));
|
|
1253
1297
|
}
|
|
1254
1298
|
}
|
|
1255
1299
|
const listHitboxes: TerminalHitbox[] = [];
|
|
@@ -1289,7 +1333,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1289
1333
|
}
|
|
1290
1334
|
const frame = createFrame(decorated.lines, [...listHitboxes, ...itemHitboxes, ...decorated.hitboxes], decorated.cursor, spans);
|
|
1291
1335
|
const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1292
|
-
return typeof frameHeight === "number" ? constrainFrame(styled, { height: frameHeight }) : styled;
|
|
1336
|
+
return typeof frameHeight === "number" ? constrainFrame(styled, { height: frameHeight, expandFullFrameSpans: true }) : styled;
|
|
1293
1337
|
}
|
|
1294
1338
|
case "terminal-table":
|
|
1295
1339
|
return renderTableFrame(node, context);
|
|
@@ -1323,7 +1367,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1323
1367
|
const decorated = addBorder(createFrame([line]), inputBorder);
|
|
1324
1368
|
const width = Math.max(1, getFrameWidth(decorated));
|
|
1325
1369
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1326
|
-
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX,
|
|
1370
|
+
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, ...interactiveTextMetadata(stringValue) }] : [];
|
|
1327
1371
|
const spans = fullFrameSpans(["input.base"], width, height);
|
|
1328
1372
|
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1329
1373
|
}
|
|
@@ -1332,12 +1376,12 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1332
1376
|
const decorated = addBorder(createFrame([rendered.line], [], rendered.cursor, rendered.spans), inputBorder);
|
|
1333
1377
|
const width = Math.max(1, getFrameWidth(decorated));
|
|
1334
1378
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1335
|
-
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX,
|
|
1379
|
+
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, ...interactiveTextMetadata(stringValue) }] : [];
|
|
1336
1380
|
const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...decorated.spans];
|
|
1337
1381
|
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1338
1382
|
}
|
|
1339
1383
|
case "terminal-editor":
|
|
1340
|
-
return addFullFrameSpans(renderEditorFrame(node), resolveNodeStyle(node, context).spanKinds);
|
|
1384
|
+
return addFullFrameSpans(renderEditorFrame(node, context), resolveNodeStyle(node, context).spanKinds);
|
|
1341
1385
|
case "terminal-button": {
|
|
1342
1386
|
const label = typeof node.props.label !== "undefined" ? plainText(node.props.label) : plainText(node.children.map(textContent).join(""));
|
|
1343
1387
|
const layoutStyle = resolveLayoutStyle("button.base", node, context);
|
package/src/session.ts
CHANGED
package/src/text.ts
CHANGED
|
@@ -5,6 +5,20 @@ const C1_CSI_TERMINAL_CONTROL = /\u009b[0-?]*[ -/]*[@-~]/g;
|
|
|
5
5
|
const ESC_TERMINAL_CONTROL = /\u001b[ -/]*[0-~]/g;
|
|
6
6
|
const C1_TERMINAL_CONTROL = /[\u0080-\u009f]/g;
|
|
7
7
|
const C0_TERMINAL_CONTROL = /[\u0000-\u0009\u000b-\u001f\u007f]/g;
|
|
8
|
+
const COMBINING_MARK = /\p{Mark}/u;
|
|
9
|
+
const EMOJI_PRESENTATION = /\p{Extended_Pictographic}/u;
|
|
10
|
+
|
|
11
|
+
type GraphemeSegmenter = {
|
|
12
|
+
segment(value: string): Iterable<{ segment: string }>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const Segmenter = (Intl as unknown as {
|
|
16
|
+
Segmenter?: new (locale?: string, options?: { granularity: "grapheme" }) => GraphemeSegmenter;
|
|
17
|
+
}).Segmenter;
|
|
18
|
+
|
|
19
|
+
const GRAPHEME_SEGMENTER = typeof Segmenter === "function"
|
|
20
|
+
? new Segmenter(undefined, { granularity: "grapheme" })
|
|
21
|
+
: null;
|
|
8
22
|
|
|
9
23
|
export function stripTerminalControls(value: unknown) {
|
|
10
24
|
return String(value)
|
|
@@ -18,3 +32,137 @@ export function stripTerminalControls(value: unknown) {
|
|
|
18
32
|
}
|
|
19
33
|
|
|
20
34
|
export const plainText = stripTerminalControls;
|
|
35
|
+
|
|
36
|
+
export function terminalGraphemes(value: string): string[] {
|
|
37
|
+
if (GRAPHEME_SEGMENTER !== null) {
|
|
38
|
+
return Array.from(GRAPHEME_SEGMENTER.segment(value), (part) => part.segment);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Array.from(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isWideCodePoint(codePoint: number) {
|
|
45
|
+
return (
|
|
46
|
+
codePoint >= 0x1100 && (
|
|
47
|
+
codePoint <= 0x115f
|
|
48
|
+
|| codePoint === 0x2329
|
|
49
|
+
|| codePoint === 0x232a
|
|
50
|
+
|| (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f)
|
|
51
|
+
|| (codePoint >= 0xac00 && codePoint <= 0xd7a3)
|
|
52
|
+
|| (codePoint >= 0xf900 && codePoint <= 0xfaff)
|
|
53
|
+
|| (codePoint >= 0xfe10 && codePoint <= 0xfe19)
|
|
54
|
+
|| (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
|
|
55
|
+
|| (codePoint >= 0xff00 && codePoint <= 0xff60)
|
|
56
|
+
|| (codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
57
|
+
|| (codePoint >= 0x20000 && codePoint <= 0x3fffd)
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isZeroWidthCodePoint(codePoint: number, char: string) {
|
|
63
|
+
return codePoint === 0x200d
|
|
64
|
+
|| (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
|
|
65
|
+
|| (codePoint >= 0xe0100 && codePoint <= 0xe01ef)
|
|
66
|
+
|| COMBINING_MARK.test(char);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function graphemeCellWidth(grapheme: string) {
|
|
70
|
+
if (grapheme.length === 0) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (grapheme.includes("\u200d") || EMOJI_PRESENTATION.test(grapheme)) {
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let width = 0;
|
|
79
|
+
for (const char of Array.from(grapheme)) {
|
|
80
|
+
const codePoint = char.codePointAt(0);
|
|
81
|
+
if (typeof codePoint !== "number") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (isZeroWidthCodePoint(codePoint, char)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
width = Math.max(width, isWideCodePoint(codePoint) ? 2 : 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return width;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function terminalCellWidth(value: unknown) {
|
|
94
|
+
const text = stripTerminalControls(value);
|
|
95
|
+
return terminalGraphemes(text).reduce((width, grapheme) => width + graphemeCellWidth(grapheme), 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function sliceTerminalCells(value: string, maxCells: number) {
|
|
99
|
+
const limit = Math.max(0, Math.trunc(Number(maxCells) || 0));
|
|
100
|
+
let width = 0;
|
|
101
|
+
let output = "";
|
|
102
|
+
|
|
103
|
+
for (const grapheme of terminalGraphemes(value)) {
|
|
104
|
+
const graphemeWidth = graphemeCellWidth(grapheme);
|
|
105
|
+
if (graphemeWidth > 0 && width + graphemeWidth > limit) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
output += grapheme;
|
|
109
|
+
width += graphemeWidth;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function dropTerminalCells(value: string, cells: number) {
|
|
116
|
+
const limit = Math.max(0, Math.trunc(Number(cells) || 0));
|
|
117
|
+
let width = 0;
|
|
118
|
+
let output = "";
|
|
119
|
+
let dropping = true;
|
|
120
|
+
|
|
121
|
+
for (const grapheme of terminalGraphemes(value)) {
|
|
122
|
+
if (dropping) {
|
|
123
|
+
const graphemeWidth = graphemeCellWidth(grapheme);
|
|
124
|
+
if (width < limit || (graphemeWidth === 0 && width <= limit)) {
|
|
125
|
+
width += graphemeWidth;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
dropping = false;
|
|
129
|
+
}
|
|
130
|
+
output += grapheme;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return output;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function padEndTerminalCells(value: string, width: number) {
|
|
137
|
+
const size = Math.max(0, Math.trunc(Number(width) || 0));
|
|
138
|
+
const visibleWidth = terminalCellWidth(value);
|
|
139
|
+
return visibleWidth >= size ? value : `${value}${" ".repeat(size - visibleWidth)}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function terminalCellToStringIndex(value: string) {
|
|
143
|
+
const indexes: number[] = [0];
|
|
144
|
+
let cellOffset = 0;
|
|
145
|
+
let stringIndex = 0;
|
|
146
|
+
|
|
147
|
+
for (const grapheme of terminalGraphemes(value)) {
|
|
148
|
+
const graphemeWidth = graphemeCellWidth(grapheme);
|
|
149
|
+
const nextStringIndex = stringIndex + grapheme.length;
|
|
150
|
+
|
|
151
|
+
if (graphemeWidth > 0) {
|
|
152
|
+
for (let cell = 1; cell < graphemeWidth; cell += 1) {
|
|
153
|
+
indexes[cellOffset + cell] = nextStringIndex;
|
|
154
|
+
}
|
|
155
|
+
cellOffset += graphemeWidth;
|
|
156
|
+
indexes[cellOffset] = nextStringIndex;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
stringIndex = nextStringIndex;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
indexes[cellOffset] = stringIndex;
|
|
163
|
+
return indexes;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function cursorCellOffset(value: string, cursor: number) {
|
|
167
|
+
return terminalCellWidth(value.slice(0, Math.max(0, Math.trunc(Number(cursor) || 0))));
|
|
168
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -129,6 +129,7 @@ export interface TerminalHitbox {
|
|
|
129
129
|
y2: number;
|
|
130
130
|
textStartX?: number;
|
|
131
131
|
textLength?: number;
|
|
132
|
+
textCellToStringIndex?: number[];
|
|
132
133
|
itemOffset?: number;
|
|
133
134
|
itemIndexes?: number[];
|
|
134
135
|
contentY?: number;
|
|
@@ -387,7 +388,9 @@ export interface TerminalInputProps extends TerminalFocusableProps, TerminalStyl
|
|
|
387
388
|
export interface TerminalEditorProps extends TerminalFocusableProps, TerminalStyleProps {
|
|
388
389
|
value?: string;
|
|
389
390
|
placeholder?: string;
|
|
391
|
+
width?: number;
|
|
390
392
|
height?: number;
|
|
393
|
+
fill?: boolean;
|
|
391
394
|
onchange?(event: TerminalEditorChangeEventPayload): void;
|
|
392
395
|
oninput?(event: TerminalEditorChangeEventPayload): void;
|
|
393
396
|
onsubmit?(event: TerminalEditorSubmitEventPayload): void;
|