@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/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
- let style = resolveTerminalStyle(node.props.style, context?.theme);
151
- const spanKinds: ResolvedStyleSpan[] = typeof node.props.style === "string"
152
- ? [styleSpan(node.props.style)]
153
- : style ? [styleSpan("#style", style)] : [];
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 overlayGeometry(node: TerminalElementNode) {
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: positiveInteger(node.props.x, "Overlay x"),
751
- y: positiveInteger(node.props.y, "Overlay y"),
752
- width: positiveInteger(node.props.width, "Overlay width"),
753
- height: positiveInteger(node.props.height, "Overlay 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
- const rendered = renderOverlayChildFrame(node);
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
- const payload: TerminalListPressEventPayload = { type: "press", id, index: currentIndex, value: items[currentIndex] };
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
- const id = String(node.props.id || "");
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
- dispatchNodeEvent(node, "press", { type: "press", id: String(node.props.id || "") });
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" | "click";
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
- onclick?(event: TerminalButtonPressEventPayload): void;
366
- action?(event: TerminalButtonPressEventPayload): void;
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
- x: number;
443
- y: number;
444
- width: number;
445
- height: number;
463
+ margin: TerminalOverlayMarginValue | TerminalOverlayMarginAxes;
446
464
  trapFocus?: boolean;
447
465
  }
448
466