Skip to content

Commit 8d2bbe6

Browse files
committed
fix the way terminalwrites work (use ref)
1 parent a9cd808 commit 8d2bbe6

6 files changed

Lines changed: 85 additions & 40 deletions

File tree

tsunami/app/defaultclient.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ func SetTermInputHandler(handler func(input TermInputPacket)) {
7272
engine.GetDefaultClient().SetTermInputHandler(handler)
7373
}
7474

75-
func TermWrite(id string, data64 string) error {
76-
return engine.GetDefaultClient().SendTermWrite(id, data64)
75+
func TermWrite(ref *vdom.VDomRef, data string) error {
76+
if ref == nil || !ref.HasCurrent.Load() {
77+
return nil
78+
}
79+
return engine.GetDefaultClient().SendTermWrite(ref.RefId, data)
7780
}
7881

7982
func ConfigAtom[T any](name string, defaultValue T, meta *AtomMeta) Atom[T] {

tsunami/engine/clientimpl.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package engine
55

66
import (
77
"context"
8+
"encoding/base64"
89
"encoding/json"
910
"fmt"
1011
"io/fs"
@@ -311,16 +312,16 @@ func (c *ClientImpl) SendAsyncInitiation() error {
311312
return c.SendSSEvent(ssEvent{Event: "asyncinitiation", Data: nil})
312313
}
313314

314-
func (c *ClientImpl) SendTermWrite(id string, data64 string) error {
315+
func (c *ClientImpl) SendTermWrite(refId string, data string) error {
315316
payload := rpctypes.TermWritePacket{
316-
Id: id,
317-
Data64: data64,
317+
RefId: refId,
318+
Data64: base64.StdEncoding.EncodeToString([]byte(data)),
318319
}
319-
data, err := json.Marshal(payload)
320+
jsonData, err := json.Marshal(payload)
320321
if err != nil {
321322
return err
322323
}
323-
return c.SendSSEvent(ssEvent{Event: "termwrite", Data: data})
324+
return c.SendSSEvent(ssEvent{Event: "termwrite", Data: jsonData})
324325
}
325326

326327
func (c *ClientImpl) HandleTermInput(input rpctypes.TermInputPacket) {

tsunami/frontend/src/element/tsunamiterm.tsx

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import * as React from "react";
55

66
import { base64ToArray, stringToBase64 } from "@/util/base64";
77

8-
const TermWriteEventName = "tsunami:termwrite";
9-
108
type TermSize = {
119
rows: number;
1210
cols: number;
@@ -18,9 +16,9 @@ type TermInputPayload = {
1816
data64?: string;
1917
};
2018

21-
type TermWritePayload = {
22-
id: string;
23-
data64: string;
19+
export type TsunamiTermElem = HTMLDivElement & {
20+
__termWrite: (data64: string) => void;
21+
__termFocus: () => void;
2422
};
2523

2624
async function sendTermInput(payload: TermInputPayload) {
@@ -41,13 +39,28 @@ const TsunamiTerm = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
4139
ref
4240
) {
4341
const { id, ...outerProps } = props;
44-
const outerRef = React.useRef<HTMLDivElement>(null);
42+
const outerRef = React.useRef<TsunamiTermElem>(null);
4543
const termRef = React.useRef<HTMLDivElement>(null);
4644
const terminalRef = React.useRef<Terminal | null>(null);
4745

4846
const setOuterRef = React.useCallback(
49-
(elem: HTMLDivElement) => {
47+
(elem: TsunamiTermElem) => {
5048
outerRef.current = elem;
49+
if (elem != null) {
50+
elem.__termWrite = (data64: string) => {
51+
if (data64 == null || data64 === "") {
52+
return;
53+
}
54+
try {
55+
terminalRef.current?.write(base64ToArray(data64));
56+
} catch (error) {
57+
console.error("Failed to write to terminal:", error);
58+
}
59+
};
60+
elem.__termFocus = () => {
61+
terminalRef.current?.focus();
62+
};
63+
}
5164
if (typeof ref === "function") {
5265
ref(elem);
5366
return;
@@ -114,29 +127,27 @@ const TsunamiTerm = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
114127
};
115128
}, [id]);
116129

117-
React.useEffect(() => {
118-
const handleTermWrite = (event: Event) => {
119-
const detail = (event as CustomEvent<TermWritePayload>).detail;
120-
if (detail == null || detail.id !== id || detail.data64 == null || detail.data64 === "") {
121-
return;
122-
}
123-
try {
124-
terminalRef.current?.write(base64ToArray(detail.data64));
125-
} catch (error) {
126-
console.error("Failed to process term write event:", error);
127-
}
128-
};
129-
window.addEventListener(TermWriteEventName, handleTermWrite);
130-
return () => {
131-
window.removeEventListener(TermWriteEventName, handleTermWrite);
132-
};
133-
}, [id]);
130+
const handleFocus = React.useCallback(
131+
(e: React.FocusEvent<HTMLDivElement>) => {
132+
terminalRef.current?.focus();
133+
outerProps.onFocus?.(e);
134+
},
135+
[outerProps.onFocus]
136+
);
137+
138+
const handleBlur = React.useCallback(
139+
(e: React.FocusEvent<HTMLDivElement>) => {
140+
terminalRef.current?.blur();
141+
outerProps.onBlur?.(e);
142+
},
143+
[outerProps.onBlur]
144+
);
134145

135146
return (
136-
<div {...outerProps} id={id} ref={setOuterRef}>
147+
<div {...outerProps} id={id} ref={setOuterRef as React.RefCallback<HTMLDivElement>} onFocus={handleFocus} onBlur={handleBlur}>
137148
<div ref={termRef} className="w-full h-full" />
138149
</div>
139150
);
140151
});
141152

142-
export { TermWriteEventName, TsunamiTerm };
153+
export { TsunamiTerm };

tsunami/frontend/src/model/model-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import type { TsunamiTermElem } from "@/element/tsunamiterm";
5+
46
const TextTag = "#text";
57

68
// TODO support binding
@@ -79,6 +81,22 @@ export function restoreVDomElems(backendUpdate: VDomBackendUpdate) {
7981
});
8082
}
8183

84+
export function isTsunamiTermElem(elem: HTMLElement): elem is TsunamiTermElem {
85+
return elem != null && typeof (elem as TsunamiTermElem).__termWrite === "function";
86+
}
87+
88+
export function applyTermOp(elem: TsunamiTermElem, termOp: VDomRefOperation) {
89+
const { op, params } = termOp;
90+
if (op === "termwrite") {
91+
const data64 = params?.[0];
92+
if (typeof data64 === "string" && data64 !== "") {
93+
elem.__termWrite(data64);
94+
}
95+
} else if (op === "focus") {
96+
elem.__termFocus();
97+
}
98+
}
99+
82100
export function applyCanvasOp(canvas: HTMLCanvasElement, canvasOp: VDomRefOperation, refStore: Map<string, any>) {
83101
const ctx = canvas.getContext("2d");
84102
if (!ctx) {

tsunami/frontend/src/model/tsunami-model.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
import debug from "debug";
55
import * as jotai from "jotai";
66

7-
import { TermWriteEventName } from "@/element/tsunamiterm";
87
import { arrayBufferToBase64 } from "@/util/base64";
98
import { getOrCreateClientId } from "@/util/clientid";
109
import { adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
1110
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
1211
import { getDefaultStore } from "jotai";
13-
import { applyCanvasOp, restoreVDomElems } from "./model-utils";
12+
import { applyCanvasOp, applyTermOp, isTsunamiTermElem, restoreVDomElems } from "./model-utils";
1413

1514
const dlog = debug("wave:vdom");
1615

@@ -239,8 +238,18 @@ export class TsunamiModel {
239238

240239
this.serverEventSource.addEventListener("termwrite", (event: MessageEvent) => {
241240
try {
242-
const detail = JSON.parse(event.data);
243-
window.dispatchEvent(new CustomEvent(TermWriteEventName, { detail }));
241+
const packet = JSON.parse(event.data);
242+
if (packet?.refid == null || packet?.data64 == null) {
243+
return;
244+
}
245+
const refOp: VDomRefOperation = { refid: packet.refid, op: "termwrite", params: [packet.data64] };
246+
const elem = this.getRefElem(refOp.refid);
247+
if (elem == null) {
248+
return;
249+
}
250+
if (isTsunamiTermElem(elem)) {
251+
applyTermOp(elem, refOp);
252+
}
244253
} catch (e) {
245254
console.error("Failed to parse termwrite event:", e);
246255
}
@@ -616,6 +625,10 @@ export class TsunamiModel {
616625
applyCanvasOp(elem, refOp, this.refOutputStore);
617626
continue;
618627
}
628+
if (isTsunamiTermElem(elem)) {
629+
applyTermOp(elem, refOp);
630+
continue;
631+
}
619632
if (refOp.op == "focus") {
620633
if (elem == null) {
621634
this.addErrorMessage(`Could not focus ref with id ${refOp.refid}: elem is null`);
@@ -728,8 +741,7 @@ export class TsunamiModel {
728741
vdomEvent.globaleventtype = fnDecl.globalevent;
729742
}
730743
const needsAsync =
731-
propName == "onSubmit" ||
732-
(propName == "onChange" && (e.target as HTMLInputElement)?.type === "file");
744+
propName == "onSubmit" || (propName == "onChange" && (e.target as HTMLInputElement)?.type === "file");
733745
if (needsAsync) {
734746
asyncAnnotateEvent(vdomEvent, propName, e)
735747
.then(() => {

tsunami/rpctypes/protocoltypes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,6 @@ type TermInputPacket struct {
219219
}
220220

221221
type TermWritePacket struct {
222-
Id string `json:"id"`
222+
RefId string `json:"refid"`
223223
Data64 string `json:"data64"`
224224
}

0 commit comments

Comments
 (0)