@valyrianjs/terminal 0.1.2 → 0.2.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/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +8 -3
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +107 -25
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +0 -6
- package/dist/runtime.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +135 -6
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +23 -9
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +30 -25
- package/docs/cookbook.md +1 -1
- package/docs/core-concepts.md +1 -1
- package/docs/interaction-model.md +12 -2
- package/docs/primitive-gallery.md +14 -9
- package/llms-full.txt +58 -38
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/primitives.ts +8 -5
- package/src/render.ts +122 -25
- package/src/runtime.ts +0 -6
- package/src/session.ts +154 -6
- package/src/types.ts +27 -9
package/src/render.ts
CHANGED
|
@@ -14,6 +14,25 @@ export interface TerminalRenderContext {
|
|
|
14
14
|
theme?: TerminalTheme;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function validateRenderContextDimension(name: "cols" | "rows", value: number) {
|
|
18
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
19
|
+
throw new RangeError(`Invalid render context ${name}: expected an integer >= 1`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateRenderContext(context: TerminalRenderContext | undefined) {
|
|
25
|
+
if (typeof context === "undefined") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
cols: validateRenderContextDimension("cols", context.cols),
|
|
31
|
+
rows: validateRenderContextDimension("rows", context.rows),
|
|
32
|
+
theme: context.theme
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
const VISUAL_STATE_ORDER: TerminalVisualState[] = [
|
|
18
37
|
"disabled", "readonly", "loading", "empty", "muted", "error", "warning", "success", "invalid", "valid", "placeholder", "selection", "selected", "current", "expanded", "collapsed", "checked", "unchecked", "indeterminate", "editing", "submitted", "dragging", "dropTarget", "capturing", "focus", "hover", "pressed"
|
|
19
38
|
];
|
|
@@ -108,6 +127,20 @@ function styleSpan(kind: string, style?: TerminalStyleDefinition): ResolvedStyle
|
|
|
108
127
|
return typeof style === "undefined" ? { kind } : { kind, style };
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
const BASE_STYLE_KIND_BY_TAG: Partial<Record<TerminalElementNode["tag"], string>> = {
|
|
131
|
+
"terminal-button": "button.base",
|
|
132
|
+
"terminal-input": "input.base",
|
|
133
|
+
"terminal-editor": "editor.base",
|
|
134
|
+
"terminal-list": "list.base",
|
|
135
|
+
"terminal-scroll": "scroll.base",
|
|
136
|
+
"terminal-log-view": "log.base",
|
|
137
|
+
"terminal-overlay": "overlay.base"
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function baseStyleKindForNode(node: TerminalElementNode) {
|
|
141
|
+
return BASE_STYLE_KIND_BY_TAG[node.tag];
|
|
142
|
+
}
|
|
143
|
+
|
|
111
144
|
function nodeStates(node: TerminalElementNode) {
|
|
112
145
|
const declared = Array.isArray(node.props.state) ? node.props.state : typeof node.props.state === "string" ? [node.props.state] : [];
|
|
113
146
|
const states = new Set<TerminalVisualState>();
|
|
@@ -127,7 +160,7 @@ function resolveLayoutStyle(baseKind: string, node: TerminalElementNode, context
|
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
function resolveNodeLayoutStyle(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
130
|
-
return resolveTerminalStyle(node.props.style, context?.theme);
|
|
163
|
+
return mergeStyleDefinitions(resolveTerminalStyle(baseStyleKindForNode(node), context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
|
|
131
164
|
}
|
|
132
165
|
|
|
133
166
|
function decoratedControlFrame(content: string[], style: TerminalStyleDefinition | undefined) {
|
|
@@ -146,11 +179,17 @@ function fullFrameSpans(kinds: string[], width: number, height: number): Termina
|
|
|
146
179
|
return spans;
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
function resolveNodeStyle(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
182
|
+
function resolveNodeStyle(node: TerminalElementNode, context?: TerminalRenderContext, options: { includeBase?: boolean } = {}) {
|
|
183
|
+
const baseKind = options.includeBase === false ? undefined : baseStyleKindForNode(node);
|
|
184
|
+
let style = resolveTerminalStyle(baseKind, context?.theme);
|
|
185
|
+
const spanKinds: ResolvedStyleSpan[] = typeof baseKind === "undefined" ? [] : [styleSpan(baseKind)];
|
|
186
|
+
const explicitStyle = resolveTerminalStyle(node.props.style, context?.theme);
|
|
187
|
+
if (typeof node.props.style === "string") {
|
|
188
|
+
spanKinds.push(styleSpan(node.props.style));
|
|
189
|
+
} else if (explicitStyle) {
|
|
190
|
+
spanKinds.push(styleSpan("#style", explicitStyle));
|
|
191
|
+
}
|
|
192
|
+
style = mergeStyleDefinitions(style, explicitStyle);
|
|
154
193
|
for (const state of nodeStates(node)) {
|
|
155
194
|
const stateStyle = node.props.styles?.[state];
|
|
156
195
|
if (stateStyle) {
|
|
@@ -745,18 +784,69 @@ function renderStandaloneFixedFrame(node: TerminalElementNode, context?: Termina
|
|
|
745
784
|
: constrainFrame(frame, { width: size });
|
|
746
785
|
}
|
|
747
786
|
|
|
748
|
-
function
|
|
787
|
+
function overlayMarginValue(value: unknown, axisSize: number, label: string) {
|
|
788
|
+
if (typeof value === "number") {
|
|
789
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
790
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
791
|
+
}
|
|
792
|
+
return value;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (typeof value === "string") {
|
|
796
|
+
const match = value.match(/^(\d+(?:\.\d+)?)%$/);
|
|
797
|
+
if (!match) {
|
|
798
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const percent = Number(match[1]);
|
|
802
|
+
if (!Number.isFinite(percent) || percent < 0) {
|
|
803
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return Math.round(axisSize * percent / 100);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function overlayMargins(margin: unknown, width: number, height: number) {
|
|
813
|
+
if (typeof margin === "number" || typeof margin === "string") {
|
|
814
|
+
const x = overlayMarginValue(margin, width, "Overlay margin");
|
|
815
|
+
const y = overlayMarginValue(margin, height, "Overlay margin");
|
|
816
|
+
return { x, y };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (margin && typeof margin === "object" && !Array.isArray(margin)) {
|
|
820
|
+
const axes = margin as { x?: unknown; y?: unknown };
|
|
821
|
+
return {
|
|
822
|
+
x: overlayMarginValue(axes.x, width, "Overlay margin x"),
|
|
823
|
+
y: overlayMarginValue(axes.y, height, "Overlay margin y")
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
throw new RangeError("Overlay margin is required");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function overlayGeometry(node: TerminalElementNode, width: number, height: number) {
|
|
831
|
+
const margin = overlayMargins(node.props.margin, width, height);
|
|
832
|
+
const overlayWidth = width - margin.x * 2;
|
|
833
|
+
const overlayHeight = height - margin.y * 2;
|
|
834
|
+
|
|
835
|
+
if (overlayWidth < 1 || overlayHeight < 1) {
|
|
836
|
+
throw new RangeError("Overlay margin leaves no renderable area");
|
|
837
|
+
}
|
|
838
|
+
|
|
749
839
|
return {
|
|
750
|
-
x:
|
|
751
|
-
y:
|
|
752
|
-
width:
|
|
753
|
-
height:
|
|
840
|
+
x: margin.x + 1,
|
|
841
|
+
y: margin.y + 1,
|
|
842
|
+
width: overlayWidth,
|
|
843
|
+
height: overlayHeight
|
|
754
844
|
};
|
|
755
845
|
}
|
|
756
846
|
|
|
757
|
-
function renderOverlayChildFrame(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
758
|
-
const geometry = overlayGeometry(node);
|
|
759
|
-
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height }), { width: geometry.width, height: geometry.height });
|
|
847
|
+
function renderOverlayChildFrame(node: TerminalElementNode, width: number, height: number, context?: TerminalRenderContext) {
|
|
848
|
+
const geometry = overlayGeometry(node, width, height);
|
|
849
|
+
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height });
|
|
760
850
|
frame = addContainerStyleSpans(frame, node, resolveNodeStyle(node, context));
|
|
761
851
|
if (node.props.id && isFocusable(node)) {
|
|
762
852
|
frame = addFocusableHitbox(frame, node as TerminalFocusNode);
|
|
@@ -766,8 +856,10 @@ function renderOverlayChildFrame(node: TerminalElementNode, context?: TerminalRe
|
|
|
766
856
|
|
|
767
857
|
function applyDirectOverlays(base: TerminalFrame, overlays: TerminalElementNode[], context?: TerminalRenderContext) {
|
|
768
858
|
let frame = base;
|
|
859
|
+
const width = Math.max(1, getFrameWidth(base));
|
|
860
|
+
const height = Math.max(1, getFrameHeight(base));
|
|
769
861
|
for (const overlay of overlays) {
|
|
770
|
-
const rendered = renderOverlayChildFrame(overlay, context);
|
|
862
|
+
const rendered = renderOverlayChildFrame(overlay, width, height, context);
|
|
771
863
|
frame = overlayFrame(frame, rendered.frame, rendered.geometry);
|
|
772
864
|
}
|
|
773
865
|
return frame;
|
|
@@ -829,8 +921,12 @@ function renderPaneFrame(node: TerminalElementNode, context?: TerminalRenderCont
|
|
|
829
921
|
return overlays.length ? applyDirectOverlays(frame, overlays, context) : frame;
|
|
830
922
|
}
|
|
831
923
|
|
|
832
|
-
function renderStandaloneOverlayFrame(node: TerminalElementNode) {
|
|
833
|
-
|
|
924
|
+
function renderStandaloneOverlayFrame(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
925
|
+
if (!context) {
|
|
926
|
+
throw new RangeError("Standalone Overlay requires exact render context dimensions");
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const rendered = renderOverlayChildFrame(node, context.cols, context.rows, context);
|
|
834
930
|
return rendered.frame;
|
|
835
931
|
}
|
|
836
932
|
|
|
@@ -983,7 +1079,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
983
1079
|
case "terminal-fixed":
|
|
984
1080
|
return renderStandaloneFixedFrame(node, context);
|
|
985
1081
|
case "terminal-overlay":
|
|
986
|
-
return renderStandaloneOverlayFrame(node);
|
|
1082
|
+
return renderStandaloneOverlayFrame(node, context);
|
|
987
1083
|
case "terminal-log-view":
|
|
988
1084
|
return renderLogViewFrame(node, context);
|
|
989
1085
|
case "terminal-list": {
|
|
@@ -1018,7 +1114,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1018
1114
|
}
|
|
1019
1115
|
}
|
|
1020
1116
|
const frame = createFrame(decorated.lines, [], decorated.cursor, spans);
|
|
1021
|
-
const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context).spanKinds);
|
|
1117
|
+
const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1022
1118
|
if (!node.props.id) {
|
|
1023
1119
|
return styled;
|
|
1024
1120
|
}
|
|
@@ -1039,7 +1135,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1039
1135
|
case "terminal-text": {
|
|
1040
1136
|
const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : plainText(node.children.map(textContent).join(""));
|
|
1041
1137
|
const frame = createFrame(value.split("\n"));
|
|
1042
|
-
return addFullFrameSpans(frame, resolveNodeStyle(node, context).spanKinds);
|
|
1138
|
+
return addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1043
1139
|
}
|
|
1044
1140
|
case "terminal-input": {
|
|
1045
1141
|
const value = typeof node.props.value !== "undefined" ? node.props.value : node.props.placeholder || "";
|
|
@@ -1058,7 +1154,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1058
1154
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1059
1155
|
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
|
|
1060
1156
|
const spans = fullFrameSpans(["input.base"], width, height);
|
|
1061
|
-
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
|
|
1157
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1062
1158
|
}
|
|
1063
1159
|
|
|
1064
1160
|
const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length }, inputPadding);
|
|
@@ -1067,7 +1163,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1067
1163
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1068
1164
|
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
|
|
1069
1165
|
const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...decorated.spans];
|
|
1070
|
-
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
|
|
1166
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1071
1167
|
}
|
|
1072
1168
|
case "terminal-editor":
|
|
1073
1169
|
return addFullFrameSpans(renderEditorFrame(node), resolveNodeStyle(node, context).spanKinds);
|
|
@@ -1080,7 +1176,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1080
1176
|
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height }] : [];
|
|
1081
1177
|
const kinds = ["button.base", ...nodeStates(node).map((state) => `button.${state}`)];
|
|
1082
1178
|
const spans = fullFrameSpans(kinds, width, height);
|
|
1083
|
-
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
|
|
1179
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1084
1180
|
}
|
|
1085
1181
|
default:
|
|
1086
1182
|
return mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
|
|
@@ -1098,9 +1194,10 @@ export function renderTerminalNode(node: TerminalNode, context?: TerminalRenderC
|
|
|
1098
1194
|
return renderTerminalFrame(node, context).lines.join("\n");
|
|
1099
1195
|
}
|
|
1100
1196
|
|
|
1101
|
-
export function renderTerminal(input: any): string {
|
|
1197
|
+
export function renderTerminal(input: any, context?: TerminalRenderContext): string {
|
|
1198
|
+
const renderContext = validateRenderContext(context);
|
|
1102
1199
|
return renderValyrianTerminal(input)
|
|
1103
|
-
.map((node) => renderTerminalNode(node))
|
|
1200
|
+
.map((node) => renderTerminalNode(node, renderContext))
|
|
1104
1201
|
.filter(Boolean)
|
|
1105
1202
|
.join("\n")
|
|
1106
1203
|
.trimEnd();
|
package/src/runtime.ts
CHANGED
|
@@ -61,9 +61,6 @@ function hasEventListener(node: DomNode, eventName: string) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function resolveDispatchEventName(node: DomNode, eventName: string) {
|
|
64
|
-
if (eventName === "press" && !hasEventListener(node, "press") && hasEventListener(node, "click")) {
|
|
65
|
-
return "click";
|
|
66
|
-
}
|
|
67
64
|
if (eventName === "change" && !hasEventListener(node, "change") && hasEventListener(node, "input")) {
|
|
68
65
|
return "input";
|
|
69
66
|
}
|
|
@@ -158,9 +155,6 @@ function normalizeValyrianInput(input: any): any {
|
|
|
158
155
|
const tag = typeof input.tag === "string" ? input.tag : wrapComponent(input.tag);
|
|
159
156
|
if (typeof tag === "string" && isTerminalTag(tag)) {
|
|
160
157
|
const terminalProps = { ...props };
|
|
161
|
-
if (tag === "terminal-button" && typeof props.action === "function" && typeof props.onpress !== "function" && typeof props.onclick !== "function") {
|
|
162
|
-
props.onclick = props.action;
|
|
163
|
-
}
|
|
164
158
|
if (typeof props.style === "object") {
|
|
165
159
|
Reflect.deleteProperty(props, "style");
|
|
166
160
|
}
|
package/src/session.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
TerminalCaptureEventPayload,
|
|
25
25
|
TerminalClipboardAdapter,
|
|
26
26
|
TerminalFocusNode,
|
|
27
|
+
TerminalHitbox,
|
|
27
28
|
TerminalListChangeEventPayload,
|
|
28
29
|
TerminalListPressEventPayload,
|
|
29
30
|
TerminalMountOptions,
|
|
@@ -71,6 +72,7 @@ const KNOWN_TERMINAL_KEY_SEQUENCES = [
|
|
|
71
72
|
];
|
|
72
73
|
const ESCAPE = "\u001b";
|
|
73
74
|
const CSI_PREFIX = "\u001b[";
|
|
75
|
+
const DOUBLE_PRESS_INTERVAL_MS = 500;
|
|
74
76
|
|
|
75
77
|
function isBracketedPasteStartPrefix(value: string) {
|
|
76
78
|
return value.length > 0 && value.length < BRACKETED_PASTE_START.length && BRACKETED_PASTE_START.startsWith(value);
|
|
@@ -198,6 +200,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
198
200
|
let pendingPasteChunk = "";
|
|
199
201
|
let pendingKeyChunk = "";
|
|
200
202
|
let pendingEscapeFlush: ReturnType<typeof setTimeout> | null = null;
|
|
203
|
+
let lastPrimaryPress: { id: string; tag: string; row: number | null; at: number } | null = null;
|
|
201
204
|
let destroyed = false;
|
|
202
205
|
let autoProjectionEnabled = false;
|
|
203
206
|
let suppressAutoProjection = false;
|
|
@@ -492,6 +495,57 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
492
495
|
}
|
|
493
496
|
}
|
|
494
497
|
|
|
498
|
+
|
|
499
|
+
function dispatchListPressEvent(node: TerminalFocusNode, type: TerminalListPressEventPayload["type"], index: number) {
|
|
500
|
+
const id = node.props.id;
|
|
501
|
+
if (!id) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
505
|
+
if (typeof items[index] === "undefined") {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
const payload: TerminalListPressEventPayload = { type, id, index, value: items[index] };
|
|
509
|
+
return dispatchNodeEvent(node, type, payload);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function dispatchButtonPressEvent(node: TerminalFocusNode, type: "press" | "doublepress" | "contextpress") {
|
|
513
|
+
const id = String(node.props.id || "");
|
|
514
|
+
if (!id) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return dispatchNodeEvent(node, type, { type, id });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function dispatchListPointerPressEvent(node: TerminalFocusNode, type: "doublepress" | "contextpress", row: number) {
|
|
521
|
+
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
522
|
+
const index = Math.max(0, Math.min(items.length - 1, row - 1));
|
|
523
|
+
return dispatchListPressEvent(node, type, index);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function dispatchInputContextPressEvent(node: TerminalFocusNode, hitbox: TerminalHitbox, x: number | null, y: number | null) {
|
|
527
|
+
const id = String(node.props.id || "");
|
|
528
|
+
if (!id) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const value = stripTerminalControls(node.props.value ?? "");
|
|
532
|
+
const current = normalizeInputState(inputStateById.get(id), value.length);
|
|
533
|
+
const cursor = typeof x === "number" ? cursorFromHitbox(hitbox, x) : current.cursor;
|
|
534
|
+
return dispatchNodeEvent(node, "contextpress", { type: "contextpress", id, value, cursor, x, y });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function dispatchScrollContextPressEvent(node: TerminalFocusNode, row: number, x: number | null, y: number | null) {
|
|
538
|
+
if (!node.props.id) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
const lines = visibleScrollLines(node);
|
|
542
|
+
const index = Math.max(0, Math.min(lines.length - 1, row - 1));
|
|
543
|
+
if (typeof lines[index] === "undefined") {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
return dispatchNodeEvent(node, "contextpress", { type: "contextpress", id: node.props.id, row: index + 1, value: lines[index], x, y });
|
|
547
|
+
}
|
|
548
|
+
|
|
495
549
|
function emitMouseRowEvent(node: TerminalFocusNode, type: TerminalRowPointerEventPayload["type"], row: number, x: number | null = null, y: number | null = null) {
|
|
496
550
|
if (!node.props.id) {
|
|
497
551
|
return;
|
|
@@ -621,10 +675,8 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
621
675
|
if (!id) {
|
|
622
676
|
return currentOutput;
|
|
623
677
|
}
|
|
624
|
-
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
625
678
|
const currentIndex = listIndexById.get(id) || 0;
|
|
626
|
-
|
|
627
|
-
dispatchNodeEvent(node, "press", payload);
|
|
679
|
+
dispatchListPressEvent(node, "press", currentIndex);
|
|
628
680
|
return rerender();
|
|
629
681
|
}
|
|
630
682
|
|
|
@@ -821,8 +873,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
821
873
|
return currentOutput;
|
|
822
874
|
case "button.press":
|
|
823
875
|
if (node?.tag === "terminal-button") {
|
|
824
|
-
|
|
825
|
-
dispatchNodeEvent(node, "press", { type: "press", id });
|
|
876
|
+
dispatchButtonPressEvent(node, "press");
|
|
826
877
|
return rerender();
|
|
827
878
|
}
|
|
828
879
|
return currentOutput;
|
|
@@ -946,7 +997,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
946
997
|
if (id) {
|
|
947
998
|
focusedId = node.props.id || focusedId;
|
|
948
999
|
}
|
|
949
|
-
|
|
1000
|
+
dispatchButtonPressEvent(node, "press");
|
|
950
1001
|
return rerender();
|
|
951
1002
|
},
|
|
952
1003
|
clickAt(x: number, y: number) {
|
|
@@ -1100,6 +1151,77 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1100
1151
|
}
|
|
1101
1152
|
}
|
|
1102
1153
|
|
|
1154
|
+
|
|
1155
|
+
function isPrimaryMouseButton(button: number) {
|
|
1156
|
+
return button < 64 && (button & 3) === 0;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function isContextMouseButton(button: number) {
|
|
1160
|
+
return button < 64 && (button & 3) === 2;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function isDoublePrimaryPress(hitbox: { id: string; tag: string }, row: number | null) {
|
|
1164
|
+
const now = Date.now();
|
|
1165
|
+
const isDouble = Boolean(
|
|
1166
|
+
lastPrimaryPress
|
|
1167
|
+
&& lastPrimaryPress.id === hitbox.id
|
|
1168
|
+
&& lastPrimaryPress.tag === hitbox.tag
|
|
1169
|
+
&& lastPrimaryPress.row === row
|
|
1170
|
+
&& now - lastPrimaryPress.at <= DOUBLE_PRESS_INTERVAL_MS
|
|
1171
|
+
);
|
|
1172
|
+
lastPrimaryPress = { id: hitbox.id, tag: hitbox.tag, row, at: now };
|
|
1173
|
+
return isDouble;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function doublePressAt(hitbox: { id: string; tag: string; y1: number }, x: number, y: number) {
|
|
1177
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
1178
|
+
if (!node) {
|
|
1179
|
+
return currentOutput;
|
|
1180
|
+
}
|
|
1181
|
+
focusedId = hitbox.id;
|
|
1182
|
+
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
1183
|
+
if (node.tag === "terminal-button") {
|
|
1184
|
+
dispatchButtonPressEvent(node, "doublepress");
|
|
1185
|
+
return rerender();
|
|
1186
|
+
}
|
|
1187
|
+
if (node.tag === "terminal-list") {
|
|
1188
|
+
dispatchListPointerPressEvent(node, "doublepress", sourceRowFromHitbox(node, hitbox, y));
|
|
1189
|
+
return rerender();
|
|
1190
|
+
}
|
|
1191
|
+
return currentOutput;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function contextPressAt(x: number, y: number) {
|
|
1195
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
1196
|
+
if (!hitbox) {
|
|
1197
|
+
clearSemanticHover(undefined, x, y);
|
|
1198
|
+
return currentOutput;
|
|
1199
|
+
}
|
|
1200
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
1201
|
+
if (!node) {
|
|
1202
|
+
return currentOutput;
|
|
1203
|
+
}
|
|
1204
|
+
focusedId = hitbox.id;
|
|
1205
|
+
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
1206
|
+
if (node.tag === "terminal-button") {
|
|
1207
|
+
dispatchButtonPressEvent(node, "contextpress");
|
|
1208
|
+
return rerender();
|
|
1209
|
+
}
|
|
1210
|
+
if (node.tag === "terminal-list") {
|
|
1211
|
+
dispatchListPointerPressEvent(node, "contextpress", sourceRowFromHitbox(node, hitbox, y));
|
|
1212
|
+
return rerender();
|
|
1213
|
+
}
|
|
1214
|
+
if (node.tag === "terminal-input") {
|
|
1215
|
+
dispatchInputContextPressEvent(node, hitbox, x, y);
|
|
1216
|
+
return rerender();
|
|
1217
|
+
}
|
|
1218
|
+
if (node.tag === "terminal-scroll") {
|
|
1219
|
+
dispatchScrollContextPressEvent(node, sourceRowFromHitbox(node, hitbox, y), x, y);
|
|
1220
|
+
return rerender();
|
|
1221
|
+
}
|
|
1222
|
+
return currentOutput;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1103
1225
|
function processInputStream(value: string) {
|
|
1104
1226
|
if (!value) {
|
|
1105
1227
|
return;
|
|
@@ -1161,6 +1283,11 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1161
1283
|
if (parsed.type === "mouse") {
|
|
1162
1284
|
if (parsed.action === "press") {
|
|
1163
1285
|
const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
|
|
1286
|
+
if (isContextMouseButton(parsed.button)) {
|
|
1287
|
+
lastPrimaryPress = null;
|
|
1288
|
+
contextPressAt(parsed.x, parsed.y);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1164
1291
|
if (hitbox?.tag === "terminal-input") {
|
|
1165
1292
|
mouseSelectionId = hitbox.id;
|
|
1166
1293
|
}
|
|
@@ -1168,7 +1295,28 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1168
1295
|
if (hitbox && node && shouldPointerCapture(node)) {
|
|
1169
1296
|
setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
|
|
1170
1297
|
}
|
|
1298
|
+
const isPrimaryPress = isPrimaryMouseButton(parsed.button);
|
|
1299
|
+
const isDoublePressEligible = Boolean(
|
|
1300
|
+
hitbox
|
|
1301
|
+
&& node
|
|
1302
|
+
&& (hitbox.tag === "terminal-button" || hitbox.tag === "terminal-list")
|
|
1303
|
+
);
|
|
1304
|
+
const primaryPressRow = hitbox && node && hitbox.tag === "terminal-list"
|
|
1305
|
+
? sourceRowFromHitbox(node, hitbox, parsed.y)
|
|
1306
|
+
: null;
|
|
1307
|
+
const shouldDispatchDoublePress = Boolean(
|
|
1308
|
+
isPrimaryPress
|
|
1309
|
+
&& isDoublePressEligible
|
|
1310
|
+
&& hitbox
|
|
1311
|
+
&& isDoublePrimaryPress(hitbox, primaryPressRow)
|
|
1312
|
+
);
|
|
1313
|
+
if (isPrimaryPress && !isDoublePressEligible) {
|
|
1314
|
+
lastPrimaryPress = null;
|
|
1315
|
+
}
|
|
1171
1316
|
session.clickAt(parsed.x, parsed.y);
|
|
1317
|
+
if (hitbox && shouldDispatchDoublePress) {
|
|
1318
|
+
doublePressAt(hitbox, parsed.x, parsed.y);
|
|
1319
|
+
}
|
|
1172
1320
|
} else if (parsed.action === "drag") {
|
|
1173
1321
|
if (mouseSelectionId) {
|
|
1174
1322
|
setCursorFromHitbox(mouseSelectionId, parsed.x, true);
|
package/src/types.ts
CHANGED
|
@@ -287,7 +287,7 @@ export interface TerminalEditorCancelEventPayload extends TerminalValueEventPayl
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
export interface TerminalButtonPressEventPayload extends TerminalEventPayloadBase {
|
|
290
|
-
type: "press" | "
|
|
290
|
+
type: "press" | "doublepress" | "contextpress";
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
export interface TerminalListChangeEventPayload<T = any> extends TerminalValueEventPayloadBase<T> {
|
|
@@ -296,7 +296,7 @@ export interface TerminalListChangeEventPayload<T = any> extends TerminalValueEv
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
export interface TerminalListPressEventPayload<T = any> extends TerminalValueEventPayloadBase<T> {
|
|
299
|
-
type: "press";
|
|
299
|
+
type: "press" | "doublepress" | "contextpress";
|
|
300
300
|
index: number;
|
|
301
301
|
}
|
|
302
302
|
|
|
@@ -307,6 +307,16 @@ export type TerminalChangeEventPayload =
|
|
|
307
307
|
|
|
308
308
|
export type TerminalPressEventPayload = TerminalButtonPressEventPayload | TerminalListPressEventPayload<any>;
|
|
309
309
|
|
|
310
|
+
export interface TerminalInputContextPressEventPayload extends TerminalValueEventPayloadBase<string>, TerminalPointerCoordinates {
|
|
311
|
+
type: "contextpress";
|
|
312
|
+
cursor: number;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface TerminalScrollContextPressEventPayload extends TerminalRowPointerEventPayloadBase {
|
|
316
|
+
type: "contextpress";
|
|
317
|
+
value: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
310
320
|
export type TerminalMouseEventType = "hover" | "rowenter" | "rowleave";
|
|
311
321
|
|
|
312
322
|
export type TerminalPointerSource = "press" | "drag" | "release";
|
|
@@ -347,6 +357,7 @@ export interface TerminalInputProps extends TerminalFocusableProps, TerminalStyl
|
|
|
347
357
|
onchange?(event: TerminalInputChangeEventPayload): void;
|
|
348
358
|
oninput?(event: TerminalInputChangeEventPayload): void;
|
|
349
359
|
onsubmit?(event: TerminalInputSubmitEventPayload): void;
|
|
360
|
+
oncontextpress?(event: TerminalInputContextPressEventPayload): void;
|
|
350
361
|
}
|
|
351
362
|
|
|
352
363
|
export interface TerminalEditorProps extends TerminalFocusableProps, TerminalStyleProps {
|
|
@@ -362,18 +373,20 @@ export interface TerminalEditorProps extends TerminalFocusableProps, TerminalSty
|
|
|
362
373
|
export interface TerminalButtonProps extends TerminalFocusableProps, TerminalStyleProps {
|
|
363
374
|
label?: string;
|
|
364
375
|
onpress?(event: TerminalButtonPressEventPayload): void;
|
|
365
|
-
|
|
366
|
-
|
|
376
|
+
ondoublepress?(event: TerminalButtonPressEventPayload): void;
|
|
377
|
+
oncontextpress?(event: TerminalButtonPressEventPayload): void;
|
|
367
378
|
}
|
|
368
379
|
|
|
369
380
|
export interface TerminalListProps<T = any> extends TerminalFocusableProps, TerminalPointerCaptureProps, TerminalStyleProps {
|
|
370
|
-
items?: T[];
|
|
381
|
+
items?: readonly T[];
|
|
371
382
|
virtualized?: boolean;
|
|
372
383
|
itemHeight?: 1;
|
|
373
384
|
overscan?: number;
|
|
374
385
|
renderItem?(item: T, index: number): string;
|
|
375
386
|
onchange?(event: TerminalListChangeEventPayload<T>): void;
|
|
376
387
|
onpress?(event: TerminalListPressEventPayload<T>): void;
|
|
388
|
+
ondoublepress?(event: TerminalListPressEventPayload<T>): void;
|
|
389
|
+
oncontextpress?(event: TerminalListPressEventPayload<T>): void;
|
|
377
390
|
onhover?(event: TerminalListPointerEventPayload<T>): void;
|
|
378
391
|
onrowenter?(event: TerminalListPointerEventPayload<T>): void;
|
|
379
392
|
onrowleave?(event: TerminalListPointerEventPayload<T>): void;
|
|
@@ -386,6 +399,7 @@ export interface TerminalScrollViewProps extends TerminalLayoutProps, TerminalPo
|
|
|
386
399
|
onhover?(event: TerminalScrollPointerEventPayload): void;
|
|
387
400
|
onrowenter?(event: TerminalScrollPointerEventPayload): void;
|
|
388
401
|
onrowleave?(event: TerminalScrollPointerEventPayload): void;
|
|
402
|
+
oncontextpress?(event: TerminalScrollContextPressEventPayload): void;
|
|
389
403
|
oncapturestart?(event: TerminalCaptureEventPayload): void;
|
|
390
404
|
oncaptureend?(event: TerminalCaptureEventPayload): void;
|
|
391
405
|
}
|
|
@@ -438,11 +452,15 @@ export interface TerminalFixedProps {
|
|
|
438
452
|
size: number;
|
|
439
453
|
}
|
|
440
454
|
|
|
455
|
+
export type TerminalOverlayMarginValue = number | `${number}%`;
|
|
456
|
+
|
|
457
|
+
export interface TerminalOverlayMarginAxes {
|
|
458
|
+
x: TerminalOverlayMarginValue;
|
|
459
|
+
y: TerminalOverlayMarginValue;
|
|
460
|
+
}
|
|
461
|
+
|
|
441
462
|
export interface TerminalOverlayProps extends TerminalFocusableProps, TerminalStyleProps {
|
|
442
|
-
|
|
443
|
-
y: number;
|
|
444
|
-
width: number;
|
|
445
|
-
height: number;
|
|
463
|
+
margin: TerminalOverlayMarginValue | TerminalOverlayMarginAxes;
|
|
446
464
|
trapFocus?: boolean;
|
|
447
465
|
}
|
|
448
466
|
|