@vibecook/ghosttea-react 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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/TerminalSurface.d.ts +28 -0
  4. package/dist/TerminalSurface.d.ts.map +1 -0
  5. package/dist/TerminalSurface.js +614 -0
  6. package/dist/TerminalSurface.js.map +1 -0
  7. package/dist/bindings/action-route.d.ts +155 -0
  8. package/dist/bindings/action-route.d.ts.map +1 -0
  9. package/dist/bindings/action-route.js +196 -0
  10. package/dist/bindings/action-route.js.map +1 -0
  11. package/dist/bindings/fixtures/extensions.json +10 -0
  12. package/dist/bindings/fixtures/keybinds-linux-default.json +505 -0
  13. package/dist/bindings/fixtures/keybinds-macos-default.json +660 -0
  14. package/dist/bindings/ghostty-actions.d.ts +247 -0
  15. package/dist/bindings/ghostty-actions.d.ts.map +1 -0
  16. package/dist/bindings/ghostty-actions.js +431 -0
  17. package/dist/bindings/ghostty-actions.js.map +1 -0
  18. package/dist/bindings/ghostty-bindings.d.ts +88 -0
  19. package/dist/bindings/ghostty-bindings.d.ts.map +1 -0
  20. package/dist/bindings/ghostty-bindings.js +122 -0
  21. package/dist/bindings/ghostty-bindings.js.map +1 -0
  22. package/dist/bindings/ghostty-triggers.d.ts +47 -0
  23. package/dist/bindings/ghostty-triggers.d.ts.map +1 -0
  24. package/dist/bindings/ghostty-triggers.js +378 -0
  25. package/dist/bindings/ghostty-triggers.js.map +1 -0
  26. package/dist/bindings/index.d.ts +5 -0
  27. package/dist/bindings/index.d.ts.map +1 -0
  28. package/dist/bindings/index.js +5 -0
  29. package/dist/bindings/index.js.map +1 -0
  30. package/dist/catalog-budget.d.ts +22 -0
  31. package/dist/catalog-budget.d.ts.map +1 -0
  32. package/dist/catalog-budget.js +53 -0
  33. package/dist/catalog-budget.js.map +1 -0
  34. package/dist/cell-width.d.ts +3 -0
  35. package/dist/cell-width.d.ts.map +1 -0
  36. package/dist/cell-width.js +19 -0
  37. package/dist/cell-width.js.map +1 -0
  38. package/dist/context.d.ts +9 -0
  39. package/dist/context.d.ts.map +1 -0
  40. package/dist/context.js +13 -0
  41. package/dist/context.js.map +1 -0
  42. package/dist/cursor-invalidation.d.ts +3 -0
  43. package/dist/cursor-invalidation.d.ts.map +1 -0
  44. package/dist/cursor-invalidation.js +4 -0
  45. package/dist/cursor-invalidation.js.map +1 -0
  46. package/dist/frame-resync.d.ts +16 -0
  47. package/dist/frame-resync.d.ts.map +1 -0
  48. package/dist/frame-resync.js +164 -0
  49. package/dist/frame-resync.js.map +1 -0
  50. package/dist/frame-sequence.d.ts +14 -0
  51. package/dist/frame-sequence.d.ts.map +1 -0
  52. package/dist/frame-sequence.js +16 -0
  53. package/dist/frame-sequence.js.map +1 -0
  54. package/dist/index.d.ts +6 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +5 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/keyboard-input.d.ts +18 -0
  59. package/dist/keyboard-input.d.ts.map +1 -0
  60. package/dist/keyboard-input.js +101 -0
  61. package/dist/keyboard-input.js.map +1 -0
  62. package/dist/performance.d.ts +66 -0
  63. package/dist/performance.d.ts.map +1 -0
  64. package/dist/performance.js +21 -0
  65. package/dist/performance.js.map +1 -0
  66. package/dist/renderers/canvas-renderer.d.ts +12 -0
  67. package/dist/renderers/canvas-renderer.d.ts.map +1 -0
  68. package/dist/renderers/canvas-renderer.js +117 -0
  69. package/dist/renderers/canvas-renderer.js.map +1 -0
  70. package/dist/renderers/render-damage.d.ts +7 -0
  71. package/dist/renderers/render-damage.d.ts.map +1 -0
  72. package/dist/renderers/render-damage.js +16 -0
  73. package/dist/renderers/render-damage.js.map +1 -0
  74. package/dist/renderers/types.d.ts +65 -0
  75. package/dist/renderers/types.d.ts.map +1 -0
  76. package/dist/renderers/types.js +31 -0
  77. package/dist/renderers/types.js.map +1 -0
  78. package/dist/renderers/webgpu-renderer.d.ts +27 -0
  79. package/dist/renderers/webgpu-renderer.d.ts.map +1 -0
  80. package/dist/renderers/webgpu-renderer.js +1746 -0
  81. package/dist/renderers/webgpu-renderer.js.map +1 -0
  82. package/dist/runtime.d.ts +70 -0
  83. package/dist/runtime.d.ts.map +1 -0
  84. package/dist/runtime.js +1021 -0
  85. package/dist/runtime.js.map +1 -0
  86. package/dist/scroll-input.d.ts +6 -0
  87. package/dist/scroll-input.d.ts.map +1 -0
  88. package/dist/scroll-input.js +14 -0
  89. package/dist/scroll-input.js.map +1 -0
  90. package/dist/selection-input.d.ts +16 -0
  91. package/dist/selection-input.d.ts.map +1 -0
  92. package/dist/selection-input.js +67 -0
  93. package/dist/selection-input.js.map +1 -0
  94. package/dist/selection-text.d.ts +4 -0
  95. package/dist/selection-text.d.ts.map +1 -0
  96. package/dist/selection-text.js +28 -0
  97. package/dist/selection-text.js.map +1 -0
  98. package/dist/styles.css +75 -0
  99. package/dist/terminal-bindings.d.ts +22 -0
  100. package/dist/terminal-bindings.d.ts.map +1 -0
  101. package/dist/terminal-bindings.js +22 -0
  102. package/dist/terminal-bindings.js.map +1 -0
  103. package/dist/terminal-render.worker.d.ts +2 -0
  104. package/dist/terminal-render.worker.d.ts.map +1 -0
  105. package/dist/terminal-render.worker.js +3165 -0
  106. package/dist/terminal-render.worker.js.map +7 -0
  107. package/dist/worker-messages.d.ts +111 -0
  108. package/dist/worker-messages.d.ts.map +1 -0
  109. package/dist/worker-messages.js +2 -0
  110. package/dist/worker-messages.js.map +1 -0
  111. package/dist/workspace/RemoteSessionPalette.d.ts +12 -0
  112. package/dist/workspace/RemoteSessionPalette.d.ts.map +1 -0
  113. package/dist/workspace/RemoteSessionPalette.js +135 -0
  114. package/dist/workspace/RemoteSessionPalette.js.map +1 -0
  115. package/dist/workspace/Workspace.d.ts +69 -0
  116. package/dist/workspace/Workspace.d.ts.map +1 -0
  117. package/dist/workspace/Workspace.js +536 -0
  118. package/dist/workspace/Workspace.js.map +1 -0
  119. package/dist/workspace/hotkeys.d.ts +27 -0
  120. package/dist/workspace/hotkeys.d.ts.map +1 -0
  121. package/dist/workspace/hotkeys.js +29 -0
  122. package/dist/workspace/hotkeys.js.map +1 -0
  123. package/dist/workspace/index.d.ts +8 -0
  124. package/dist/workspace/index.d.ts.map +1 -0
  125. package/dist/workspace/index.js +8 -0
  126. package/dist/workspace/index.js.map +1 -0
  127. package/dist/workspace/pane-layout.d.ts +35 -0
  128. package/dist/workspace/pane-layout.d.ts.map +1 -0
  129. package/dist/workspace/pane-layout.js +139 -0
  130. package/dist/workspace/pane-layout.js.map +1 -0
  131. package/dist/workspace/pending-cache.d.ts +5 -0
  132. package/dist/workspace/pending-cache.d.ts.map +1 -0
  133. package/dist/workspace/pending-cache.js +22 -0
  134. package/dist/workspace/pending-cache.js.map +1 -0
  135. package/dist/workspace/session-scope.d.ts +3 -0
  136. package/dist/workspace/session-scope.d.ts.map +1 -0
  137. package/dist/workspace/session-scope.js +6 -0
  138. package/dist/workspace/session-scope.js.map +1 -0
  139. package/dist/workspace/themes.d.ts +18 -0
  140. package/dist/workspace/themes.d.ts.map +1 -0
  141. package/dist/workspace/themes.js +19 -0
  142. package/dist/workspace/themes.js.map +1 -0
  143. package/dist/workspace/workspace-model.d.ts +59 -0
  144. package/dist/workspace/workspace-model.d.ts.map +1 -0
  145. package/dist/workspace/workspace-model.js +257 -0
  146. package/dist/workspace/workspace-model.js.map +1 -0
  147. package/dist/workspace/workspace-tabs.d.ts +41 -0
  148. package/dist/workspace/workspace-tabs.d.ts.map +1 -0
  149. package/dist/workspace/workspace-tabs.js +133 -0
  150. package/dist/workspace/workspace-tabs.js.map +1 -0
  151. package/dist/workspace.css +453 -0
  152. package/package.json +75 -0
@@ -0,0 +1,614 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useGhostteaRuntime } from "./context.js";
4
+ import { terminalKeyboardLayout, terminalKeyDown, terminalKeyUp } from "./keyboard-input.js";
5
+ import { terminalEffectShouldConsume } from "./bindings/action-route.js";
6
+ import { resolveTerminalBinding } from "./terminal-bindings.js";
7
+ import { CELL_WIDTH, LINE_HEIGHT, ORIGIN_X, ORIGIN_Y } from "./renderers/types.js";
8
+ import { accumulateWheelRows, wheelDeltaPixels } from "./scroll-input.js";
9
+ import { adjustSelectionFocus, usesLocalSelection } from "./selection-input.js";
10
+ export function viewportSelection(selection, scrollbar, cols, rows) {
11
+ if (!selection)
12
+ return null;
13
+ const forward = selection.anchor.row < selection.focus.row ||
14
+ (selection.anchor.row === selection.focus.row && selection.anchor.column <= selection.focus.column);
15
+ const start = forward ? selection.anchor : selection.focus;
16
+ const end = forward ? selection.focus : selection.anchor;
17
+ const viewportStart = scrollbar.offset;
18
+ const viewportEnd = viewportStart + rows - 1;
19
+ if (end.row < viewportStart || start.row > viewportEnd)
20
+ return null;
21
+ return {
22
+ anchor: {
23
+ column: start.row < viewportStart ? 0 : start.column,
24
+ row: Math.max(0, start.row - viewportStart),
25
+ },
26
+ focus: {
27
+ column: end.row > viewportEnd ? Math.max(0, cols - 1) : end.column,
28
+ row: Math.min(rows - 1, end.row - viewportStart),
29
+ },
30
+ };
31
+ }
32
+ export function TerminalSurface(props) {
33
+ const { session } = props;
34
+ return _jsx(TerminalSurfaceSession, { ...props }, `${session.id}:${session.handle}`);
35
+ }
36
+ function TerminalSurfaceSession({ session, theme, active = true, platform, visible = true, controlsResize = active, onActivate, readClipboard, onCopyAvailabilityChange, onContextMenu, onToggleFullscreen: _onToggleFullscreen, onMenuAction, }) {
37
+ // Fullscreen is owned by Workspace platform routing (toggle_fullscreen).
38
+ void _onToggleFullscreen;
39
+ const terminalRuntime = useGhostteaRuntime();
40
+ const interactive = session.readWrite;
41
+ const canvasRef = useRef(null);
42
+ const inputRef = useRef(null);
43
+ const gridRef = useRef({ cols: session.cols, rows: session.rows });
44
+ const [viewId] = useState(() => crypto.randomUUID());
45
+ const [inputFocused, setInputFocused] = useState(false);
46
+ const selectionAnchorRef = useRef(null);
47
+ const selectionRef = useRef(null);
48
+ const selectionAllRef = useRef(false);
49
+ const pointerModeRef = useRef(null);
50
+ const wheelDeltaRef = useRef(0);
51
+ const pendingScrollRowsRef = useRef(0);
52
+ const scrollFrameRef = useRef(null);
53
+ const pendingScrollToRef = useRef(null);
54
+ const scrollToFrameRef = useRef(null);
55
+ const scrollbarHideTimerRef = useRef(null);
56
+ const selectionAutoScrollTimerRef = useRef(null);
57
+ const selectionAutoScrollEdgeRef = useRef(null);
58
+ const scrollbarDragRef = useRef(null);
59
+ const forwardedKeysRef = useRef(new Map());
60
+ const restoreInputFocusRef = useRef(false);
61
+ const [scrollbar, setScrollbar] = useState(() => terminalRuntime.scrollbar(session.handle) ?? {
62
+ total: session.rows,
63
+ offset: 0,
64
+ length: session.rows,
65
+ });
66
+ const scrollbarRef = useRef(scrollbar);
67
+ const [scrollbarVisible, setScrollbarVisible] = useState(false);
68
+ const setLocalSelection = useCallback((selection, selectAll = false) => {
69
+ selectionRef.current = selection;
70
+ selectionAllRef.current = selection !== null && selectAll;
71
+ onCopyAvailabilityChange?.(selection !== null);
72
+ }, [onCopyAvailabilityChange]);
73
+ useEffect(() => {
74
+ if (!onCopyAvailabilityChange)
75
+ return;
76
+ onCopyAvailabilityChange(selectionRef.current !== null);
77
+ return () => onCopyAvailabilityChange(false);
78
+ }, [onCopyAvailabilityChange]);
79
+ const releaseForwardedKeys = useCallback(() => {
80
+ for (const event of forwardedKeysRef.current.values()) {
81
+ terminalRuntime.sendKey(session.id, viewId, {
82
+ ...event,
83
+ type: "up",
84
+ repeat: false,
85
+ timestamp: performance.now(),
86
+ });
87
+ }
88
+ forwardedKeysRef.current.clear();
89
+ }, [session.id, terminalRuntime, viewId]);
90
+ useEffect(() => {
91
+ terminalRuntime.setTheme(session.handle, theme, viewId);
92
+ }, [session.handle, terminalRuntime, theme, viewId]);
93
+ useEffect(() => {
94
+ void terminalKeyboardLayout.refresh();
95
+ }, []);
96
+ const executeTerminalEffect = useCallback((effect) => {
97
+ const { cols, rows } = gridRef.current;
98
+ const total = Math.max(rows, scrollbarRef.current.total);
99
+ const viewportLen = Math.max(1, scrollbarRef.current.length || rows);
100
+ if (effect.type === "copy") {
101
+ if (!selectionRef.current)
102
+ return false;
103
+ void terminalRuntime
104
+ .copySelection(session.id, viewId, selectionRef.current, selectionAllRef.current)
105
+ .catch((error) => console.error("[terminal-runtime] clipboard copy failed", error));
106
+ return true;
107
+ }
108
+ if (effect.type === "select_all") {
109
+ setLocalSelection({ anchor: { column: 0, row: 0 }, focus: { column: cols - 1, row: total - 1 } }, true);
110
+ terminalRuntime.setSelection(session.handle, viewportSelection(selectionRef.current, scrollbarRef.current, cols, rows), viewId);
111
+ return true;
112
+ }
113
+ if (effect.type === "paste") {
114
+ if (!interactive || !readClipboard)
115
+ return false;
116
+ void Promise.resolve(readClipboard())
117
+ .then((text) => {
118
+ if (!text)
119
+ return;
120
+ selectionAnchorRef.current = null;
121
+ setLocalSelection(null);
122
+ terminalRuntime.setSelection(session.handle, null, viewId);
123
+ terminalRuntime.paste(session.id, viewId, text);
124
+ })
125
+ .catch((error) => console.error("[terminal-runtime] clipboard paste failed", error));
126
+ return true;
127
+ }
128
+ if (effect.type === "text" || effect.type === "clear_screen") {
129
+ if (!interactive && effect.type === "text")
130
+ return false;
131
+ selectionAnchorRef.current = null;
132
+ setLocalSelection(null);
133
+ terminalRuntime.setSelection(session.handle, null, viewId);
134
+ terminalRuntime.sendText(session.id, viewId, effect.type === "clear_screen" ? "\u000c" : effect.text);
135
+ return true;
136
+ }
137
+ if (effect.type === "scroll_to_top") {
138
+ terminalRuntime.scrollTo(session.id, viewId, 0);
139
+ return true;
140
+ }
141
+ if (effect.type === "scroll_to_bottom") {
142
+ terminalRuntime.scrollTo(session.id, viewId, Math.max(0, total - viewportLen));
143
+ return true;
144
+ }
145
+ if (effect.type === "scroll_to_row") {
146
+ terminalRuntime.scrollTo(session.id, viewId, Math.max(0, effect.row));
147
+ return true;
148
+ }
149
+ if (effect.type === "scroll_to_selection") {
150
+ const selection = selectionRef.current;
151
+ if (!selection)
152
+ return false;
153
+ const row = Math.min(selection.anchor.row, selection.focus.row);
154
+ terminalRuntime.scrollTo(session.id, viewId, Math.max(0, row));
155
+ return true;
156
+ }
157
+ if (effect.type === "scroll_page_up") {
158
+ terminalRuntime.scroll(session.id, viewId, -rows);
159
+ return true;
160
+ }
161
+ if (effect.type === "scroll_page_down") {
162
+ terminalRuntime.scroll(session.id, viewId, rows);
163
+ return true;
164
+ }
165
+ if (effect.type === "scroll_page_fractional") {
166
+ const delta = Math.trunc(rows * effect.amount);
167
+ if (delta !== 0)
168
+ terminalRuntime.scroll(session.id, viewId, delta);
169
+ return true;
170
+ }
171
+ if (effect.type === "scroll_page_lines") {
172
+ if (effect.lines !== 0)
173
+ terminalRuntime.scroll(session.id, viewId, effect.lines);
174
+ return true;
175
+ }
176
+ if (effect.type === "adjust_selection") {
177
+ const selection = selectionRef.current;
178
+ if (!selection)
179
+ return false;
180
+ const nextFocus = adjustSelectionFocus(selection.focus, effect.direction, {
181
+ cols,
182
+ rows,
183
+ totalRows: total,
184
+ });
185
+ if (!nextFocus)
186
+ return false;
187
+ setLocalSelection({ anchor: selection.anchor, focus: nextFocus });
188
+ // Keep keyboard-adjusted focus visible (Ghostty scrolls selection into view).
189
+ const offset = scrollbarRef.current.offset;
190
+ if (nextFocus.row < offset) {
191
+ terminalRuntime.scrollTo(session.id, viewId, nextFocus.row);
192
+ }
193
+ else if (nextFocus.row >= offset + rows) {
194
+ terminalRuntime.scrollTo(session.id, viewId, Math.max(0, nextFocus.row - rows + 1));
195
+ }
196
+ terminalRuntime.setSelection(session.handle, viewportSelection(selectionRef.current, scrollbarRef.current, cols, rows), viewId);
197
+ return true;
198
+ }
199
+ return false;
200
+ }, [interactive, readClipboard, session.handle, session.id, setLocalSelection, terminalRuntime, viewId]);
201
+ useEffect(() => {
202
+ if (!onMenuAction)
203
+ return;
204
+ return onMenuAction((action) => {
205
+ const focused = document.activeElement;
206
+ const editingAnotherControl = focused !== inputRef.current &&
207
+ (focused instanceof HTMLInputElement ||
208
+ focused instanceof HTMLTextAreaElement ||
209
+ (focused instanceof HTMLElement && focused.isContentEditable));
210
+ if (!active || editingAnotherControl)
211
+ return;
212
+ const effect = action === "copy"
213
+ ? { type: "copy" }
214
+ : action === "paste"
215
+ ? { type: "paste" }
216
+ : action === "select-all"
217
+ ? { type: "select_all" }
218
+ : action === "clear-screen"
219
+ ? { type: "clear_screen" }
220
+ : null;
221
+ if (effect)
222
+ executeTerminalEffect(effect);
223
+ });
224
+ }, [active, executeTerminalEffect, onMenuAction]);
225
+ useEffect(() => {
226
+ if (active && document.hasFocus())
227
+ inputRef.current?.focus({ preventScroll: true });
228
+ }, [active]);
229
+ useEffect(() => {
230
+ const canvas = canvasRef.current;
231
+ if (!canvas)
232
+ return;
233
+ const handle = terminalRuntime.mount(session.id, session.handle, viewId, canvas);
234
+ let { cols, rows } = gridRef.current;
235
+ const observer = new ResizeObserver(([entry]) => {
236
+ if (!entry)
237
+ return;
238
+ handle.resize(entry.contentRect.width, entry.contentRect.height, window.devicePixelRatio);
239
+ const nextCols = Math.max(2, Math.floor((entry.contentRect.width - ORIGIN_X * 2) / CELL_WIDTH));
240
+ const nextRows = Math.max(1, Math.floor((entry.contentRect.height - ORIGIN_Y * 2) / LINE_HEIGHT));
241
+ if (nextCols !== cols || nextRows !== rows) {
242
+ cols = nextCols;
243
+ rows = nextRows;
244
+ gridRef.current = { cols, rows };
245
+ terminalRuntime.resize(session.id, viewId, cols, rows);
246
+ }
247
+ });
248
+ observer.observe(canvas);
249
+ return () => {
250
+ observer.disconnect();
251
+ handle.dispose();
252
+ };
253
+ }, [session.handle, session.id, terminalRuntime, viewId]);
254
+ useEffect(() => {
255
+ terminalRuntime.setVisible(session.handle, visible, viewId);
256
+ }, [session.handle, terminalRuntime, viewId, visible]);
257
+ useEffect(() => {
258
+ const onScrollbar = (event) => {
259
+ const detail = event.detail;
260
+ if (detail.sessionHandle !== session.handle)
261
+ return;
262
+ scrollbarRef.current = detail.scrollbar;
263
+ setScrollbar(detail.scrollbar);
264
+ const { cols, rows } = gridRef.current;
265
+ const edge = selectionAutoScrollEdgeRef.current;
266
+ const anchor = selectionAnchorRef.current;
267
+ if (edge && anchor && pointerModeRef.current === "selection") {
268
+ setLocalSelection({
269
+ anchor,
270
+ focus: {
271
+ column: edge.column,
272
+ row: detail.scrollbar.offset + (edge.direction < 0 ? 0 : rows - 1),
273
+ },
274
+ });
275
+ }
276
+ terminalRuntime.setSelection(session.handle, viewportSelection(selectionRef.current, detail.scrollbar, cols, rows), viewId);
277
+ };
278
+ terminalRuntime.addEventListener("scrollbar-state", onScrollbar);
279
+ return () => terminalRuntime.removeEventListener("scrollbar-state", onScrollbar);
280
+ }, [session.handle, setLocalSelection, terminalRuntime, viewId]);
281
+ useEffect(() => () => {
282
+ if (scrollFrameRef.current !== null)
283
+ window.cancelAnimationFrame(scrollFrameRef.current);
284
+ if (scrollToFrameRef.current !== null)
285
+ window.cancelAnimationFrame(scrollToFrameRef.current);
286
+ if (scrollbarHideTimerRef.current !== null)
287
+ window.clearTimeout(scrollbarHideTimerRef.current);
288
+ if (selectionAutoScrollTimerRef.current !== null)
289
+ window.clearInterval(selectionAutoScrollTimerRef.current);
290
+ }, []);
291
+ useEffect(() => {
292
+ if (!controlsResize || !interactive)
293
+ return;
294
+ const { cols, rows } = gridRef.current;
295
+ terminalRuntime.claimResizeControl(session.handle, viewId, cols, rows);
296
+ }, [controlsResize, interactive, session.handle, terminalRuntime, viewId]);
297
+ useEffect(() => {
298
+ const syncFocus = () => {
299
+ const { cols, rows } = gridRef.current;
300
+ terminalRuntime.setFocused(session.handle, viewId, active && inputFocused && document.hasFocus() && document.activeElement === inputRef.current, cols, rows);
301
+ };
302
+ const onWindowBlur = () => {
303
+ restoreInputFocusRef.current = restoreInputFocusRef.current || document.activeElement === inputRef.current;
304
+ releaseForwardedKeys();
305
+ const { cols, rows } = gridRef.current;
306
+ terminalRuntime.setFocused(session.handle, viewId, false, cols, rows);
307
+ };
308
+ const onWindowFocus = () => {
309
+ if (active && restoreInputFocusRef.current)
310
+ inputRef.current?.focus({ preventScroll: true });
311
+ syncFocus();
312
+ };
313
+ window.addEventListener("blur", onWindowBlur);
314
+ window.addEventListener("focus", onWindowFocus);
315
+ syncFocus();
316
+ return () => {
317
+ window.removeEventListener("blur", onWindowBlur);
318
+ window.removeEventListener("focus", onWindowFocus);
319
+ releaseForwardedKeys();
320
+ const { cols, rows } = gridRef.current;
321
+ terminalRuntime.setFocused(session.handle, viewId, false, cols, rows);
322
+ };
323
+ }, [active, inputFocused, releaseForwardedKeys, session.handle, terminalRuntime, viewId]);
324
+ const onKeyDown = (event) => {
325
+ if (event.nativeEvent.isComposing)
326
+ return;
327
+ // Terminal-scoped Ghostty binds only (workspace/platform/unhandled owned by Workspace).
328
+ const terminalMatch = resolveTerminalBinding(event.nativeEvent, platform);
329
+ if (terminalMatch) {
330
+ const applied = executeTerminalEffect(terminalMatch.effect);
331
+ // Performable binds only consume when applied (Ghostty Binding.Flags.performable).
332
+ if (terminalEffectShouldConsume(terminalMatch.effect, applied, terminalMatch.flags)) {
333
+ event.preventDefault();
334
+ event.stopPropagation();
335
+ }
336
+ return;
337
+ }
338
+ if (!interactive) {
339
+ event.preventDefault();
340
+ return;
341
+ }
342
+ selectionAnchorRef.current = null;
343
+ setLocalSelection(null);
344
+ terminalRuntime.setSelection(session.handle, null, viewId);
345
+ if (event.ctrlKey && !event.altKey && event.key.toLowerCase() === "c") {
346
+ terminalRuntime.interrupt(session.id, viewId);
347
+ event.preventDefault();
348
+ event.stopPropagation();
349
+ return;
350
+ }
351
+ const terminalEvent = terminalKeyDown(event.nativeEvent);
352
+ terminalRuntime.sendKey(session.id, viewId, terminalEvent);
353
+ forwardedKeysRef.current.set(event.code, terminalEvent);
354
+ event.preventDefault();
355
+ event.stopPropagation();
356
+ };
357
+ const onKeyUp = (event) => {
358
+ const pressed = forwardedKeysRef.current.get(event.code);
359
+ if (!pressed)
360
+ return;
361
+ forwardedKeysRef.current.delete(event.code);
362
+ terminalRuntime.sendKey(session.id, viewId, terminalKeyUp(pressed, event.nativeEvent));
363
+ event.preventDefault();
364
+ event.stopPropagation();
365
+ };
366
+ const onPaste = (event) => {
367
+ if (!interactive) {
368
+ event.preventDefault();
369
+ return;
370
+ }
371
+ const text = event.clipboardData.getData("text/plain");
372
+ if (!text)
373
+ return;
374
+ terminalRuntime.paste(session.id, viewId, text);
375
+ event.preventDefault();
376
+ };
377
+ const pointFromPointer = (event) => {
378
+ const bounds = event.currentTarget.getBoundingClientRect();
379
+ const { cols, rows } = gridRef.current;
380
+ return {
381
+ column: Math.max(0, Math.min(cols - 1, Math.floor((event.clientX - bounds.left - ORIGIN_X) / CELL_WIDTH))),
382
+ row: scrollbarRef.current.offset +
383
+ Math.max(0, Math.min(rows - 1, Math.floor((event.clientY - bounds.top - ORIGIN_Y) / LINE_HEIGHT))),
384
+ };
385
+ };
386
+ const sendMouse = (event, action, button) => {
387
+ const bounds = event.currentTarget.getBoundingClientRect();
388
+ terminalRuntime.sendMouse(session.id, viewId, {
389
+ action,
390
+ button,
391
+ x: Math.max(0, event.clientX - bounds.left),
392
+ y: Math.max(0, event.clientY - bounds.top),
393
+ screenWidth: Math.max(1, Math.round(bounds.width)),
394
+ screenHeight: Math.max(1, Math.round(bounds.height)),
395
+ cellWidth: Math.max(1, Math.round(CELL_WIDTH)),
396
+ cellHeight: LINE_HEIGHT,
397
+ paddingLeft: ORIGIN_X,
398
+ paddingTop: ORIGIN_Y,
399
+ shift: event.shiftKey,
400
+ control: event.ctrlKey,
401
+ alt: event.altKey,
402
+ meta: event.metaKey,
403
+ });
404
+ };
405
+ const mouseButton = (button) => (button === 0 ? 1 : button === 1 ? 3 : button === 2 ? 2 : 0);
406
+ const revealScrollbar = () => {
407
+ setScrollbarVisible(true);
408
+ if (scrollbarHideTimerRef.current !== null)
409
+ window.clearTimeout(scrollbarHideTimerRef.current);
410
+ scrollbarHideTimerRef.current = window.setTimeout(() => {
411
+ scrollbarHideTimerRef.current = null;
412
+ if (!scrollbarDragRef.current)
413
+ setScrollbarVisible(false);
414
+ }, 850);
415
+ };
416
+ const queueScrollRows = (rows) => {
417
+ pendingScrollRowsRef.current += rows;
418
+ if (scrollFrameRef.current !== null)
419
+ return;
420
+ scrollFrameRef.current = window.requestAnimationFrame(() => {
421
+ scrollFrameRef.current = null;
422
+ const pending = pendingScrollRowsRef.current;
423
+ pendingScrollRowsRef.current = 0;
424
+ if (pending !== 0)
425
+ terminalRuntime.scroll(session.id, viewId, Math.max(-1_000, Math.min(1_000, pending)));
426
+ });
427
+ };
428
+ const queueScrollTo = (row) => {
429
+ pendingScrollToRef.current = row;
430
+ if (scrollToFrameRef.current !== null)
431
+ return;
432
+ scrollToFrameRef.current = window.requestAnimationFrame(() => {
433
+ scrollToFrameRef.current = null;
434
+ const pending = pendingScrollToRef.current;
435
+ pendingScrollToRef.current = null;
436
+ if (pending !== null)
437
+ terminalRuntime.scrollTo(session.id, viewId, pending);
438
+ });
439
+ };
440
+ const stopSelectionAutoScroll = () => {
441
+ selectionAutoScrollEdgeRef.current = null;
442
+ if (selectionAutoScrollTimerRef.current !== null) {
443
+ window.clearInterval(selectionAutoScrollTimerRef.current);
444
+ selectionAutoScrollTimerRef.current = null;
445
+ }
446
+ };
447
+ const updateSelectionAutoScroll = (direction, column) => {
448
+ if (direction === 0) {
449
+ stopSelectionAutoScroll();
450
+ return;
451
+ }
452
+ selectionAutoScrollEdgeRef.current = { direction, column };
453
+ if (selectionAutoScrollTimerRef.current !== null)
454
+ return;
455
+ selectionAutoScrollTimerRef.current = window.setInterval(() => {
456
+ const edge = selectionAutoScrollEdgeRef.current;
457
+ if (edge)
458
+ queueScrollRows(edge.direction);
459
+ }, 40);
460
+ };
461
+ const onPointerDown = (event) => {
462
+ onActivate?.();
463
+ event.currentTarget.focus({ preventScroll: true });
464
+ const mouseTracking = terminalRuntime.isMouseTracking(session.handle);
465
+ const localSelection = !interactive || usesLocalSelection(mouseTracking, event.shiftKey);
466
+ if (event.button === 2 && localSelection)
467
+ return;
468
+ event.currentTarget.setPointerCapture(event.pointerId);
469
+ event.preventDefault();
470
+ if (!localSelection) {
471
+ pointerModeRef.current = "mouse";
472
+ selectionAnchorRef.current = null;
473
+ setLocalSelection(null);
474
+ terminalRuntime.setSelection(session.handle, null, viewId);
475
+ sendMouse(event, "press", mouseButton(event.button));
476
+ return;
477
+ }
478
+ pointerModeRef.current = "selection";
479
+ const point = pointFromPointer(event);
480
+ selectionAnchorRef.current = point;
481
+ setLocalSelection({ anchor: point, focus: point });
482
+ const { cols, rows } = gridRef.current;
483
+ terminalRuntime.setSelection(session.handle, viewportSelection(selectionRef.current, scrollbarRef.current, cols, rows), viewId);
484
+ };
485
+ const onPointerMove = (event) => {
486
+ if (!event.currentTarget.hasPointerCapture(event.pointerId))
487
+ return;
488
+ if (pointerModeRef.current === "mouse") {
489
+ const button = event.buttons & 1 ? 1 : event.buttons & 4 ? 3 : event.buttons & 2 ? 2 : 0;
490
+ sendMouse(event, "motion", button);
491
+ return;
492
+ }
493
+ const anchor = selectionAnchorRef.current;
494
+ if (!anchor || pointerModeRef.current !== "selection")
495
+ return;
496
+ const point = pointFromPointer(event);
497
+ const bounds = event.currentTarget.getBoundingClientRect();
498
+ updateSelectionAutoScroll(event.clientY < bounds.top ? -1 : event.clientY > bounds.bottom ? 1 : 0, point.column);
499
+ setLocalSelection({ anchor, focus: point });
500
+ const { cols, rows } = gridRef.current;
501
+ terminalRuntime.setSelection(session.handle, viewportSelection(selectionRef.current, scrollbarRef.current, cols, rows), viewId);
502
+ };
503
+ const onPointerUp = (event) => {
504
+ stopSelectionAutoScroll();
505
+ if (pointerModeRef.current === "mouse") {
506
+ sendMouse(event, "release", mouseButton(event.button));
507
+ }
508
+ else if (pointerModeRef.current === "selection" && selectionRef.current) {
509
+ const { anchor, focus } = selectionRef.current;
510
+ if (anchor.row === focus.row && anchor.column === focus.column) {
511
+ setLocalSelection(null);
512
+ terminalRuntime.setSelection(session.handle, null, viewId);
513
+ }
514
+ else {
515
+ void terminalRuntime
516
+ .copySelection(session.id, viewId, selectionRef.current, selectionAllRef.current)
517
+ .catch((error) => console.error("[terminal-runtime] clipboard copy failed", error));
518
+ }
519
+ }
520
+ pointerModeRef.current = null;
521
+ selectionAnchorRef.current = null;
522
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
523
+ event.currentTarget.releasePointerCapture(event.pointerId);
524
+ }
525
+ };
526
+ const onWheel = (event) => {
527
+ event.preventDefault();
528
+ const accumulated = accumulateWheelRows(wheelDeltaRef.current, wheelDeltaPixels(event.deltaY, event.deltaMode, gridRef.current.rows, LINE_HEIGHT), LINE_HEIGHT);
529
+ wheelDeltaRef.current = accumulated.remainder;
530
+ const { rows } = accumulated;
531
+ if (rows === 0)
532
+ return;
533
+ if (terminalRuntime.isMouseTracking(session.handle) && !event.shiftKey) {
534
+ const button = rows < 0 ? 4 : 5;
535
+ for (let count = 0; count < Math.min(12, Math.abs(rows)); count += 1) {
536
+ sendMouse(event, "press", button);
537
+ }
538
+ }
539
+ else {
540
+ revealScrollbar();
541
+ queueScrollRows(rows);
542
+ }
543
+ };
544
+ const scrollbarScrollable = scrollbar.total > scrollbar.length;
545
+ const scrollbarMaxOffset = Math.max(0, scrollbar.total - scrollbar.length);
546
+ const scrollbarPosition = scrollbarMaxOffset === 0 ? 0 : Math.min(1, scrollbar.offset / scrollbarMaxOffset);
547
+ const scrollbarSize = scrollbar.total === 0 ? 1 : Math.min(1, scrollbar.length / scrollbar.total);
548
+ const onScrollbarPointerDown = (event) => {
549
+ if (!scrollbarScrollable)
550
+ return;
551
+ event.preventDefault();
552
+ event.stopPropagation();
553
+ onActivate?.();
554
+ const track = event.currentTarget;
555
+ const bounds = track.getBoundingClientRect();
556
+ const thumbHeight = Math.max(24, bounds.height * scrollbarSize);
557
+ const travel = Math.max(1, bounds.height - thumbHeight);
558
+ const thumb = event.target.closest(".terminal-scrollbar-thumb");
559
+ let startOffset = scrollbar.offset;
560
+ if (!thumb) {
561
+ startOffset = Math.round(Math.max(0, Math.min(1, (event.clientY - bounds.top - thumbHeight / 2) / travel)) * scrollbarMaxOffset);
562
+ queueScrollTo(startOffset);
563
+ }
564
+ scrollbarDragRef.current = {
565
+ pointerId: event.pointerId,
566
+ startY: event.clientY,
567
+ startOffset,
568
+ maxOffset: scrollbarMaxOffset,
569
+ travel,
570
+ };
571
+ track.setPointerCapture(event.pointerId);
572
+ revealScrollbar();
573
+ };
574
+ const onScrollbarPointerMove = (event) => {
575
+ const drag = scrollbarDragRef.current;
576
+ if (!drag || drag.pointerId !== event.pointerId)
577
+ return;
578
+ const row = Math.round(Math.max(0, Math.min(drag.maxOffset, drag.startOffset + ((event.clientY - drag.startY) / drag.travel) * drag.maxOffset)));
579
+ queueScrollTo(row);
580
+ revealScrollbar();
581
+ };
582
+ const onScrollbarPointerUp = (event) => {
583
+ const drag = scrollbarDragRef.current;
584
+ if (!drag || drag.pointerId !== event.pointerId)
585
+ return;
586
+ scrollbarDragRef.current = null;
587
+ if (event.currentTarget.hasPointerCapture(event.pointerId))
588
+ event.currentTarget.releasePointerCapture(event.pointerId);
589
+ revealScrollbar();
590
+ };
591
+ return (_jsxs("div", { className: "terminal-surface", children: [_jsx("canvas", { ref: canvasRef, "aria-label": `Terminal session ${session.id}` }), _jsx("textarea", { ref: inputRef, className: "terminal-input", autoCapitalize: "off", autoComplete: "off", spellCheck: false, readOnly: !interactive, "aria-label": "Terminal input", onFocus: () => {
592
+ onActivate?.();
593
+ restoreInputFocusRef.current = true;
594
+ setInputFocused(true);
595
+ }, onBlur: () => {
596
+ restoreInputFocusRef.current = !document.hasFocus();
597
+ setInputFocused(false);
598
+ releaseForwardedKeys();
599
+ }, onKeyDown: onKeyDown, onKeyUp: onKeyUp, onPaste: onPaste, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, onWheel: onWheel, onContextMenu: (event) => {
600
+ event.preventDefault();
601
+ onContextMenu?.(selectionRef.current !== null);
602
+ }, onCompositionEnd: (event) => {
603
+ terminalRuntime.sendText(session.id, viewId, event.currentTarget.value);
604
+ event.currentTarget.value = "";
605
+ }, onInput: (event) => {
606
+ if (!event.nativeEvent.isComposing)
607
+ event.currentTarget.value = "";
608
+ } }), scrollbarScrollable ? (_jsx("div", { className: `terminal-scrollbar${scrollbarVisible ? " is-visible" : ""}`, role: "scrollbar", "aria-label": "Terminal scrollback", "aria-orientation": "vertical", "aria-valuemin": 0, "aria-valuemax": scrollbarMaxOffset, "aria-valuenow": scrollbar.offset, onPointerDown: onScrollbarPointerDown, onPointerMove: onScrollbarPointerMove, onPointerUp: onScrollbarPointerUp, onPointerCancel: onScrollbarPointerUp, children: _jsx("div", { className: "terminal-scrollbar-thumb", style: {
609
+ height: `${scrollbarSize * 100}%`,
610
+ top: `${scrollbarPosition * 100}%`,
611
+ transform: `translateY(-${scrollbarPosition * 100}%)`,
612
+ } }) })) : null] }));
613
+ }
614
+ //# sourceMappingURL=TerminalSurface.js.map