green-screen-react 0.4.0 → 1.0.1
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/README.md +33 -259
- package/dist/index.d.mts +40 -91
- package/dist/index.d.ts +40 -91
- package/dist/index.js +208 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +207 -64
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +28 -1
- package/package.json +5 -3
- package/LICENSE +0 -21
package/dist/index.mjs
CHANGED
|
@@ -61,15 +61,39 @@ var RestAdapter = class {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
// src/adapters/WebSocketAdapter.ts
|
|
64
|
-
var WebSocketAdapter = class {
|
|
64
|
+
var WebSocketAdapter = class _WebSocketAdapter {
|
|
65
65
|
constructor(options = {}) {
|
|
66
66
|
this.ws = null;
|
|
67
67
|
this.screen = null;
|
|
68
68
|
this.status = { connected: false, status: "disconnected" };
|
|
69
|
-
this.
|
|
69
|
+
this.pendingScreenResolver = null;
|
|
70
|
+
this.pendingConnectResolver = null;
|
|
71
|
+
this.connectingPromise = null;
|
|
70
72
|
this.screenListeners = /* @__PURE__ */ new Set();
|
|
71
73
|
this.statusListeners = /* @__PURE__ */ new Set();
|
|
72
|
-
this.
|
|
74
|
+
this._sessionId = null;
|
|
75
|
+
this.workerUrl = (options.workerUrl || _WebSocketAdapter.detectEnvUrl() || "http://localhost:3001").replace(/\/+$/, "");
|
|
76
|
+
}
|
|
77
|
+
static detectEnvUrl() {
|
|
78
|
+
try {
|
|
79
|
+
if (typeof import.meta !== "undefined" && import.meta.env) {
|
|
80
|
+
const env = import.meta.env;
|
|
81
|
+
return env.VITE_GREEN_SCREEN_URL || env.VITE_WORKER_URL || void 0;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const p = typeof globalThis !== "undefined" && globalThis.process;
|
|
87
|
+
if (p && p.env) {
|
|
88
|
+
return p.env.NEXT_PUBLIC_GREEN_SCREEN_URL || p.env.REACT_APP_GREEN_SCREEN_URL || void 0;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
/** The proxy-side session ID (available after connect or reattach) */
|
|
95
|
+
get sessionId() {
|
|
96
|
+
return this._sessionId;
|
|
73
97
|
}
|
|
74
98
|
/** Subscribe to real-time screen updates */
|
|
75
99
|
onScreen(listener) {
|
|
@@ -100,24 +124,13 @@ var WebSocketAdapter = class {
|
|
|
100
124
|
}
|
|
101
125
|
return new Promise((resolve) => {
|
|
102
126
|
const timeout = setTimeout(() => {
|
|
127
|
+
this.pendingConnectResolver = null;
|
|
103
128
|
resolve({ success: false, error: "Connection timeout" });
|
|
104
129
|
}, 3e4);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (msg.type === "connected") {
|
|
109
|
-
clearTimeout(timeout);
|
|
110
|
-
this.ws?.removeEventListener("message", onMessage);
|
|
111
|
-
resolve({ success: true });
|
|
112
|
-
} else if (msg.type === "error") {
|
|
113
|
-
clearTimeout(timeout);
|
|
114
|
-
this.ws?.removeEventListener("message", onMessage);
|
|
115
|
-
resolve({ success: false, error: msg.message });
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
}
|
|
130
|
+
this.pendingConnectResolver = (result) => {
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
resolve(result);
|
|
119
133
|
};
|
|
120
|
-
this.ws?.addEventListener("message", onMessage);
|
|
121
134
|
this.wsSend({
|
|
122
135
|
type: "connect",
|
|
123
136
|
host: config.host,
|
|
@@ -128,9 +141,29 @@ var WebSocketAdapter = class {
|
|
|
128
141
|
});
|
|
129
142
|
});
|
|
130
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Reattach to an existing proxy session (e.g. after page reload).
|
|
146
|
+
* The proxy keeps the TCP connection alive; this just reconnects the
|
|
147
|
+
* WebSocket and receives the current screen.
|
|
148
|
+
*/
|
|
149
|
+
async reattach(sessionId) {
|
|
150
|
+
await this.ensureWebSocket();
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const timeout = setTimeout(() => {
|
|
153
|
+
this.pendingConnectResolver = null;
|
|
154
|
+
resolve({ success: false, error: "Reattach timeout" });
|
|
155
|
+
}, 1e4);
|
|
156
|
+
this.pendingConnectResolver = (result) => {
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
resolve(result);
|
|
159
|
+
};
|
|
160
|
+
this.wsSend({ type: "reattach", sessionId });
|
|
161
|
+
});
|
|
162
|
+
}
|
|
131
163
|
async disconnect() {
|
|
132
164
|
this.wsSend({ type: "disconnect" });
|
|
133
165
|
this.status = { connected: false, status: "disconnected" };
|
|
166
|
+
this._sessionId = null;
|
|
134
167
|
if (this.ws) {
|
|
135
168
|
this.ws.close();
|
|
136
169
|
this.ws = null;
|
|
@@ -140,7 +173,7 @@ var WebSocketAdapter = class {
|
|
|
140
173
|
async reconnect() {
|
|
141
174
|
return { success: false, error: "Use disconnect() then connect() instead" };
|
|
142
175
|
}
|
|
143
|
-
/** Close the WebSocket without sending disconnect */
|
|
176
|
+
/** Close the WebSocket without sending disconnect (session stays alive on proxy) */
|
|
144
177
|
dispose() {
|
|
145
178
|
if (this.ws) {
|
|
146
179
|
this.ws.close();
|
|
@@ -149,7 +182,8 @@ var WebSocketAdapter = class {
|
|
|
149
182
|
}
|
|
150
183
|
async ensureWebSocket() {
|
|
151
184
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
|
|
152
|
-
|
|
185
|
+
if (this.connectingPromise) return this.connectingPromise;
|
|
186
|
+
this.connectingPromise = new Promise((resolve, reject) => {
|
|
153
187
|
const wsUrl = this.workerUrl.replace(/^http/, "ws") + "/ws";
|
|
154
188
|
this.ws = new WebSocket(wsUrl);
|
|
155
189
|
this.ws.onopen = () => resolve();
|
|
@@ -165,39 +199,61 @@ var WebSocketAdapter = class {
|
|
|
165
199
|
this.status = { connected: false, status: "disconnected" };
|
|
166
200
|
for (const listener of this.statusListeners) listener(this.status);
|
|
167
201
|
};
|
|
202
|
+
}).finally(() => {
|
|
203
|
+
this.connectingPromise = null;
|
|
168
204
|
});
|
|
205
|
+
return this.connectingPromise;
|
|
169
206
|
}
|
|
170
207
|
handleMessage(msg) {
|
|
171
208
|
switch (msg.type) {
|
|
172
|
-
case "screen":
|
|
209
|
+
case "screen": {
|
|
173
210
|
this.screen = msg.data;
|
|
174
211
|
for (const listener of this.screenListeners) listener(msg.data);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.
|
|
178
|
-
|
|
212
|
+
if (this.pendingScreenResolver) {
|
|
213
|
+
const resolver = this.pendingScreenResolver;
|
|
214
|
+
this.pendingScreenResolver = null;
|
|
215
|
+
resolver(msg.data);
|
|
179
216
|
}
|
|
180
217
|
break;
|
|
218
|
+
}
|
|
181
219
|
case "status":
|
|
182
220
|
this.status = msg.data;
|
|
183
221
|
for (const listener of this.statusListeners) listener(msg.data);
|
|
184
222
|
break;
|
|
185
|
-
case "
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
this.
|
|
189
|
-
|
|
223
|
+
case "connected":
|
|
224
|
+
this._sessionId = msg.sessionId ?? null;
|
|
225
|
+
if (this.pendingConnectResolver) {
|
|
226
|
+
const resolver = this.pendingConnectResolver;
|
|
227
|
+
this.pendingConnectResolver = null;
|
|
228
|
+
resolver({ success: true });
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
case "error": {
|
|
232
|
+
if (this.pendingConnectResolver) {
|
|
233
|
+
const resolver = this.pendingConnectResolver;
|
|
234
|
+
this.pendingConnectResolver = null;
|
|
235
|
+
resolver({ success: false, error: msg.message });
|
|
236
|
+
} else if (this.pendingScreenResolver) {
|
|
237
|
+
const resolver = this.pendingScreenResolver;
|
|
238
|
+
this.pendingScreenResolver = null;
|
|
239
|
+
resolver(null);
|
|
190
240
|
}
|
|
191
241
|
break;
|
|
242
|
+
}
|
|
192
243
|
}
|
|
193
244
|
}
|
|
194
245
|
sendAndWaitForScreen(msg) {
|
|
195
246
|
return new Promise((resolve) => {
|
|
247
|
+
if (this.pendingScreenResolver) {
|
|
248
|
+
const old = this.pendingScreenResolver;
|
|
249
|
+
this.pendingScreenResolver = null;
|
|
250
|
+
old(this.screen);
|
|
251
|
+
}
|
|
196
252
|
const timeout = setTimeout(() => {
|
|
197
|
-
this.
|
|
253
|
+
this.pendingScreenResolver = null;
|
|
198
254
|
resolve({ success: true, ...this.screenToResult() });
|
|
199
255
|
}, 5e3);
|
|
200
|
-
this.
|
|
256
|
+
this.pendingScreenResolver = (screen) => {
|
|
201
257
|
clearTimeout(timeout);
|
|
202
258
|
if (screen) {
|
|
203
259
|
resolve({
|
|
@@ -210,7 +266,7 @@ var WebSocketAdapter = class {
|
|
|
210
266
|
} else {
|
|
211
267
|
resolve({ success: false, error: "No screen data received" });
|
|
212
268
|
}
|
|
213
|
-
}
|
|
269
|
+
};
|
|
214
270
|
this.wsSend(msg);
|
|
215
271
|
});
|
|
216
272
|
}
|
|
@@ -841,20 +897,23 @@ var PROTOCOL_OPTIONS = [
|
|
|
841
897
|
{ value: "vt", label: "VT220" },
|
|
842
898
|
{ value: "hp6530", label: "HP 6530 (NonStop)" }
|
|
843
899
|
];
|
|
844
|
-
function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
|
|
900
|
+
function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConnect }) {
|
|
845
901
|
const [host, setHost] = useState4("");
|
|
846
902
|
const [port, setPort] = useState4("");
|
|
847
903
|
const [selectedProtocol, setSelectedProtocol] = useState4(defaultProtocol);
|
|
848
904
|
const [username, setUsername] = useState4("");
|
|
849
905
|
const [password, setPassword] = useState4("");
|
|
906
|
+
const [submitted, setSubmitted] = useState4(false);
|
|
907
|
+
const loading = externalLoading || submitted;
|
|
850
908
|
const handleSubmit = (e) => {
|
|
851
909
|
e.preventDefault();
|
|
910
|
+
setSubmitted(true);
|
|
852
911
|
onConnect({
|
|
853
912
|
host,
|
|
854
|
-
port: port ? parseInt(port, 10) :
|
|
913
|
+
port: port ? parseInt(port, 10) : 23,
|
|
855
914
|
protocol: selectedProtocol,
|
|
856
|
-
username,
|
|
857
|
-
password
|
|
915
|
+
...username.trim() ? { username: username.trim() } : {},
|
|
916
|
+
...password ? { password } : {}
|
|
858
917
|
});
|
|
859
918
|
};
|
|
860
919
|
const inputStyle = {
|
|
@@ -877,6 +936,16 @@ function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
|
|
|
877
936
|
color: "var(--gs-muted, #94a3b8)",
|
|
878
937
|
fontFamily: "var(--gs-font)"
|
|
879
938
|
};
|
|
939
|
+
if (loading) {
|
|
940
|
+
return /* @__PURE__ */ jsxs3("div", { className: "gs-signin", style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "12px" }, children: [
|
|
941
|
+
/* @__PURE__ */ jsx3(RefreshIcon, { size: 28, className: "gs-spin" }),
|
|
942
|
+
/* @__PURE__ */ jsxs3("div", { style: { fontSize: "11px", letterSpacing: "0.15em", textTransform: "uppercase", color: "var(--gs-muted)", fontFamily: "var(--gs-font)" }, children: [
|
|
943
|
+
"Connecting to ",
|
|
944
|
+
host || "host",
|
|
945
|
+
"..."
|
|
946
|
+
] })
|
|
947
|
+
] });
|
|
948
|
+
}
|
|
880
949
|
return /* @__PURE__ */ jsxs3("form", { onSubmit: handleSubmit, className: "gs-signin", children: [
|
|
881
950
|
/* @__PURE__ */ jsxs3("div", { style: { textAlign: "center", marginBottom: "16px" }, children: [
|
|
882
951
|
/* @__PURE__ */ jsx3(TerminalIcon, { size: 28 }),
|
|
@@ -884,7 +953,10 @@ function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
|
|
|
884
953
|
] }),
|
|
885
954
|
/* @__PURE__ */ jsxs3("div", { className: "gs-signin-row", children: [
|
|
886
955
|
/* @__PURE__ */ jsxs3("div", { style: { flex: 1 }, children: [
|
|
887
|
-
/* @__PURE__ */
|
|
956
|
+
/* @__PURE__ */ jsxs3("label", { style: labelStyle, children: [
|
|
957
|
+
"Host ",
|
|
958
|
+
/* @__PURE__ */ jsx3("span", { style: { color: "#ef4444" }, children: "*" })
|
|
959
|
+
] }),
|
|
888
960
|
/* @__PURE__ */ jsx3("input", { style: inputStyle, value: host, onChange: (e) => setHost(e.target.value), placeholder: "192.168.1.100", required: true, autoFocus: true })
|
|
889
961
|
] }),
|
|
890
962
|
/* @__PURE__ */ jsxs3("div", { style: { width: "72px" }, children: [
|
|
@@ -893,22 +965,25 @@ function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
|
|
|
893
965
|
] })
|
|
894
966
|
] }),
|
|
895
967
|
/* @__PURE__ */ jsxs3("div", { children: [
|
|
896
|
-
/* @__PURE__ */
|
|
968
|
+
/* @__PURE__ */ jsxs3("label", { style: labelStyle, children: [
|
|
969
|
+
"Protocol ",
|
|
970
|
+
/* @__PURE__ */ jsx3("span", { style: { color: "#ef4444" }, children: "*" })
|
|
971
|
+
] }),
|
|
897
972
|
/* @__PURE__ */ jsx3("select", { style: { ...inputStyle, appearance: "none" }, value: selectedProtocol, onChange: (e) => setSelectedProtocol(e.target.value), children: PROTOCOL_OPTIONS.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: o.label }, o.value)) })
|
|
898
973
|
] }),
|
|
899
974
|
/* @__PURE__ */ jsxs3("div", { children: [
|
|
900
975
|
/* @__PURE__ */ jsx3("label", { style: labelStyle, children: "Username" }),
|
|
901
|
-
/* @__PURE__ */ jsx3("input", { style: inputStyle, value: username, onChange: (e) => setUsername(e.target.value),
|
|
976
|
+
/* @__PURE__ */ jsx3("input", { style: inputStyle, value: username, onChange: (e) => setUsername(e.target.value), autoComplete: "username" })
|
|
902
977
|
] }),
|
|
903
978
|
/* @__PURE__ */ jsxs3("div", { children: [
|
|
904
979
|
/* @__PURE__ */ jsx3("label", { style: labelStyle, children: "Password" }),
|
|
905
|
-
/* @__PURE__ */ jsx3("input", { style: inputStyle, type: "password", value: password, onChange: (e) => setPassword(e.target.value),
|
|
980
|
+
/* @__PURE__ */ jsx3("input", { style: inputStyle, type: "password", value: password, onChange: (e) => setPassword(e.target.value), autoComplete: "current-password" })
|
|
906
981
|
] }),
|
|
907
982
|
error && /* @__PURE__ */ jsxs3("div", { style: { color: "#FF6B00", fontSize: "11px", fontFamily: "var(--gs-font)", display: "flex", alignItems: "center", gap: "6px" }, children: [
|
|
908
983
|
/* @__PURE__ */ jsx3(AlertTriangleIcon, { size: 12 }),
|
|
909
984
|
/* @__PURE__ */ jsx3("span", { children: error })
|
|
910
985
|
] }),
|
|
911
|
-
/* @__PURE__ */ jsx3("button", { type: "submit", disabled:
|
|
986
|
+
/* @__PURE__ */ jsx3("button", { type: "submit", disabled: !host, className: "gs-signin-btn", children: "Connect" })
|
|
912
987
|
] });
|
|
913
988
|
}
|
|
914
989
|
|
|
@@ -927,6 +1002,7 @@ var noopAdapter = {
|
|
|
927
1002
|
function GreenScreenTerminal({
|
|
928
1003
|
adapter: externalAdapter,
|
|
929
1004
|
baseUrl,
|
|
1005
|
+
workerUrl,
|
|
930
1006
|
protocol,
|
|
931
1007
|
protocolProfile: customProfile,
|
|
932
1008
|
screenData: externalScreenData,
|
|
@@ -942,6 +1018,8 @@ function GreenScreenTerminal({
|
|
|
942
1018
|
inlineSignIn = true,
|
|
943
1019
|
defaultProtocol: signInDefaultProtocol,
|
|
944
1020
|
onSignIn,
|
|
1021
|
+
autoSignedIn,
|
|
1022
|
+
autoFocusDisabled = false,
|
|
945
1023
|
bootLoader,
|
|
946
1024
|
headerRight,
|
|
947
1025
|
overlay,
|
|
@@ -957,11 +1035,16 @@ function GreenScreenTerminal({
|
|
|
957
1035
|
() => baseUrl ? new RestAdapter({ baseUrl }) : null,
|
|
958
1036
|
[baseUrl]
|
|
959
1037
|
);
|
|
1038
|
+
const workerUrlAdapter = useMemo2(
|
|
1039
|
+
() => workerUrl ? new WebSocketAdapter({ workerUrl }) : null,
|
|
1040
|
+
[workerUrl]
|
|
1041
|
+
);
|
|
960
1042
|
const defaultWsAdapter = useMemo2(
|
|
961
|
-
() => !externalAdapter && !baseUrl ? new WebSocketAdapter() : null,
|
|
962
|
-
[externalAdapter, baseUrl]
|
|
1043
|
+
() => !externalAdapter && !baseUrl && !workerUrl ? new WebSocketAdapter() : null,
|
|
1044
|
+
[externalAdapter, baseUrl, workerUrl]
|
|
963
1045
|
);
|
|
964
|
-
const adapter = externalAdapter ?? baseUrlAdapter ?? internalAdapter ?? defaultWsAdapter ?? noopAdapter;
|
|
1046
|
+
const adapter = externalAdapter ?? baseUrlAdapter ?? workerUrlAdapter ?? internalAdapter ?? defaultWsAdapter ?? noopAdapter;
|
|
1047
|
+
const isUsingDefaultAdapter = adapter === defaultWsAdapter;
|
|
965
1048
|
const shouldPoll = pollInterval > 0 && !externalScreenData;
|
|
966
1049
|
const { data: polledScreenData, error: screenError } = useTerminalScreen(adapter, pollInterval, shouldPoll);
|
|
967
1050
|
const { sendText: _sendText, sendKey: _sendKey } = useTerminalInput(adapter);
|
|
@@ -988,6 +1071,12 @@ function GreenScreenTerminal({
|
|
|
988
1071
|
const sendKey = useCallback3(async (key) => _sendKey(key), [_sendKey]);
|
|
989
1072
|
const [inputText, setInputText] = useState5("");
|
|
990
1073
|
const [isFocused, setIsFocused] = useState5(false);
|
|
1074
|
+
const [showSignInHint, setShowSignInHint] = useState5(false);
|
|
1075
|
+
const prevAutoSignedIn = useRef4(false);
|
|
1076
|
+
useEffect4(() => {
|
|
1077
|
+
if (autoSignedIn && !prevAutoSignedIn.current) setShowSignInHint(true);
|
|
1078
|
+
prevAutoSignedIn.current = !!autoSignedIn;
|
|
1079
|
+
}, [autoSignedIn]);
|
|
991
1080
|
const terminalRef = useRef4(null);
|
|
992
1081
|
const inputRef = useRef4(null);
|
|
993
1082
|
const [syncedCursor, setSyncedCursor] = useState5(null);
|
|
@@ -997,6 +1086,7 @@ function GreenScreenTerminal({
|
|
|
997
1086
|
if (prevRawContentRef.current && newContent && newContent !== prevRawContentRef.current) {
|
|
998
1087
|
setSyncedCursor(null);
|
|
999
1088
|
setInputText("");
|
|
1089
|
+
if (showSignInHint) setShowSignInHint(false);
|
|
1000
1090
|
}
|
|
1001
1091
|
prevRawContentRef.current = newContent;
|
|
1002
1092
|
}, [rawScreenData?.content]);
|
|
@@ -1044,17 +1134,34 @@ function GreenScreenTerminal({
|
|
|
1044
1134
|
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
|
|
1045
1135
|
};
|
|
1046
1136
|
}, [connStatus?.connected, autoReconnectAttempt, isAutoReconnecting, reconnecting, reconnect, autoReconnectEnabled, maxAttempts, onNotification]);
|
|
1137
|
+
const [connecting, setConnecting] = useState5(false);
|
|
1138
|
+
const [signInError, setSignInError] = useState5(null);
|
|
1047
1139
|
const handleSignIn = useCallback3(async (config) => {
|
|
1048
|
-
onSignIn
|
|
1049
|
-
|
|
1050
|
-
const port = config.port ? `:${config.port}` : "";
|
|
1051
|
-
const newAdapter = new RestAdapter({ baseUrl: `http://${config.host}${port}` });
|
|
1052
|
-
setInternalAdapter(newAdapter);
|
|
1053
|
-
await newAdapter.connect(config);
|
|
1140
|
+
if (onSignIn) {
|
|
1141
|
+
onSignIn(config);
|
|
1054
1142
|
return;
|
|
1055
1143
|
}
|
|
1056
|
-
|
|
1144
|
+
setConnecting(true);
|
|
1145
|
+
setSignInError(null);
|
|
1146
|
+
try {
|
|
1147
|
+
if (!externalAdapter && !baseUrlAdapter) {
|
|
1148
|
+
const port = config.port ? `:${config.port}` : "";
|
|
1149
|
+
const newAdapter = new RestAdapter({ baseUrl: `http://${config.host}${port}` });
|
|
1150
|
+
setInternalAdapter(newAdapter);
|
|
1151
|
+
await newAdapter.connect(config);
|
|
1152
|
+
if (config.username && config.password) setShowSignInHint(true);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
await connect(config);
|
|
1156
|
+
if (config.username && config.password) setShowSignInHint(true);
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
setSignInError(err instanceof Error ? err.message : String(err));
|
|
1159
|
+
setConnecting(false);
|
|
1160
|
+
}
|
|
1057
1161
|
}, [connect, onSignIn, externalAdapter, baseUrlAdapter]);
|
|
1162
|
+
useEffect4(() => {
|
|
1163
|
+
if (connecting && screenData?.content) setConnecting(false);
|
|
1164
|
+
}, [connecting, screenData?.content]);
|
|
1058
1165
|
const [showBootLoader, setShowBootLoader] = useState5(bootLoader !== false);
|
|
1059
1166
|
const [bootFadingOut, setBootFadingOut] = useState5(false);
|
|
1060
1167
|
useEffect4(() => {
|
|
@@ -1065,6 +1172,35 @@ function GreenScreenTerminal({
|
|
|
1065
1172
|
return () => clearTimeout(timer);
|
|
1066
1173
|
}
|
|
1067
1174
|
}, [screenData?.content, showBootLoader]);
|
|
1175
|
+
const FOCUS_STORAGE_KEY = "gs-terminal-focused";
|
|
1176
|
+
useEffect4(() => {
|
|
1177
|
+
if (!autoFocusDisabled && !readOnly) {
|
|
1178
|
+
try {
|
|
1179
|
+
if (localStorage.getItem(FOCUS_STORAGE_KEY) === "true") {
|
|
1180
|
+
setIsFocused(true);
|
|
1181
|
+
}
|
|
1182
|
+
} catch {
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}, []);
|
|
1186
|
+
useEffect4(() => {
|
|
1187
|
+
if (autoFocusDisabled) return;
|
|
1188
|
+
try {
|
|
1189
|
+
localStorage.setItem(FOCUS_STORAGE_KEY, String(isFocused));
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
}, [isFocused, autoFocusDisabled]);
|
|
1193
|
+
useEffect4(() => {
|
|
1194
|
+
if (isFocused) inputRef.current?.focus();
|
|
1195
|
+
}, [isFocused]);
|
|
1196
|
+
const hadScreenData = useRef4(false);
|
|
1197
|
+
useEffect4(() => {
|
|
1198
|
+
if (screenData?.content && !hadScreenData.current && !autoFocusDisabled && !readOnly) {
|
|
1199
|
+
hadScreenData.current = true;
|
|
1200
|
+
setIsFocused(true);
|
|
1201
|
+
}
|
|
1202
|
+
if (!screenData?.content) hadScreenData.current = false;
|
|
1203
|
+
}, [screenData?.content, autoFocusDisabled, readOnly]);
|
|
1068
1204
|
useEffect4(() => {
|
|
1069
1205
|
const handleClickOutside = (event) => {
|
|
1070
1206
|
if (terminalRef.current && !terminalRef.current.contains(event.target)) setIsFocused(false);
|
|
@@ -1215,7 +1351,7 @@ function GreenScreenTerminal({
|
|
|
1215
1351
|
const highlightedFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_highlighted);
|
|
1216
1352
|
const reverseFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_reverse);
|
|
1217
1353
|
const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields];
|
|
1218
|
-
if (allRowFields.length === 0) return /* @__PURE__ */ jsx4("span", { children:
|
|
1354
|
+
if (allRowFields.length === 0) return /* @__PURE__ */ jsx4("span", { children: line });
|
|
1219
1355
|
const sorted = [...allRowFields].sort((a, b) => a.col - b.col);
|
|
1220
1356
|
const segs = [];
|
|
1221
1357
|
let lastEnd = 0;
|
|
@@ -1223,21 +1359,24 @@ function GreenScreenTerminal({
|
|
|
1223
1359
|
sorted.forEach((field, idx) => {
|
|
1224
1360
|
const fs = field.col;
|
|
1225
1361
|
const fe = Math.min(field.col + field.length, cols);
|
|
1226
|
-
if (fs > lastEnd) segs.push(/* @__PURE__ */ jsx4("span", { children:
|
|
1362
|
+
if (fs > lastEnd) segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(lastEnd, fs) }, `t${idx}`));
|
|
1227
1363
|
const fc = line.substring(fs, fe);
|
|
1228
1364
|
if (field.is_input) {
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1365
|
+
const fieldWidth = Math.min(field.length, cols - fs);
|
|
1366
|
+
const fieldClass = showSignInHint ? "gs-confirmed-field" : field.is_underscored ? "gs-input-field" : void 0;
|
|
1367
|
+
segs.push(/* @__PURE__ */ jsx4("span", { className: fieldClass || void 0, style: { display: "inline-block", width: `${fieldWidth}ch`, overflow: "hidden" }, children: fc }, `f${idx}`));
|
|
1231
1368
|
} else if (field.is_reverse) {
|
|
1232
1369
|
segs.push(/* @__PURE__ */ jsx4("span", { style: { color: "#ef4444", fontWeight: "bold" }, children: fc }, `v${idx}`));
|
|
1233
|
-
} else {
|
|
1370
|
+
} else if (field.is_highlighted) {
|
|
1234
1371
|
segs.push(/* @__PURE__ */ jsx4("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: fc }, `h${idx}`));
|
|
1372
|
+
} else {
|
|
1373
|
+
segs.push(/* @__PURE__ */ jsx4("span", { children: fc }, `h${idx}`));
|
|
1235
1374
|
}
|
|
1236
1375
|
lastEnd = fe;
|
|
1237
1376
|
});
|
|
1238
|
-
if (lastEnd < line.length) segs.push(/* @__PURE__ */ jsx4("span", { children:
|
|
1377
|
+
if (lastEnd < line.length) segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(lastEnd) }, "te"));
|
|
1239
1378
|
return /* @__PURE__ */ jsx4(Fragment, { children: segs });
|
|
1240
|
-
}, [renderTextWithUnderlines, screenData?.cols, profile.defaultCols]);
|
|
1379
|
+
}, [renderTextWithUnderlines, screenData?.cols, profile.defaultCols, showSignInHint]);
|
|
1241
1380
|
const renderScreen = () => {
|
|
1242
1381
|
if (showBootLoader && !screenData?.content) {
|
|
1243
1382
|
if (bootLoader === false) return null;
|
|
@@ -1247,11 +1386,15 @@ function GreenScreenTerminal({
|
|
|
1247
1386
|
if (bootFadingOut && screenData?.content) return /* @__PURE__ */ jsx4("div", { className: "gs-fade-in", children: renderScreenContent() });
|
|
1248
1387
|
if (!screenData?.content) {
|
|
1249
1388
|
if (inlineSignIn && !connStatus?.connected) {
|
|
1250
|
-
return /* @__PURE__ */ jsx4("div", { style: { width:
|
|
1389
|
+
return /* @__PURE__ */ jsx4("div", { style: { width: "100%", height: `${(screenData?.rows || profile.defaultRows) * 21}px`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx4(InlineSignIn, { defaultProtocol: signInDefaultProtocol || protocol || "tn5250", loading: connecting || reconnecting, error: signInError || connectError, onConnect: handleSignIn }) });
|
|
1251
1390
|
}
|
|
1252
1391
|
return /* @__PURE__ */ jsx4("div", { style: { width: `${screenData?.cols || profile.defaultCols}ch`, height: `${(screenData?.rows || profile.defaultRows) * 21}px`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxs4("div", { style: { textAlign: "center" }, children: [
|
|
1253
1392
|
/* @__PURE__ */ jsx4("div", { style: { color: "#808080", marginBottom: "12px" }, children: /* @__PURE__ */ jsx4(TerminalIcon, { size: 40 }) }),
|
|
1254
|
-
/* @__PURE__ */ jsx4("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : "Not connected" })
|
|
1393
|
+
/* @__PURE__ */ jsx4("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : "Not connected" }),
|
|
1394
|
+
!connStatus?.connected && isUsingDefaultAdapter && /* @__PURE__ */ jsxs4("p", { style: { fontFamily: "var(--gs-font)", fontSize: "11px", color: "#606060", marginTop: "8px" }, children: [
|
|
1395
|
+
"Start the proxy: ",
|
|
1396
|
+
/* @__PURE__ */ jsx4("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy --mock" })
|
|
1397
|
+
] })
|
|
1255
1398
|
] }) });
|
|
1256
1399
|
}
|
|
1257
1400
|
return renderScreenContent();
|
|
@@ -1266,8 +1409,7 @@ function GreenScreenTerminal({
|
|
|
1266
1409
|
const fields = screenData.fields || [];
|
|
1267
1410
|
const ROW_HEIGHT = 21;
|
|
1268
1411
|
const cursor = getCursorPos();
|
|
1269
|
-
const
|
|
1270
|
-
const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0 && (hasInputFields || screenData.cursor_row !== 0 || screenData.cursor_col !== 0);
|
|
1412
|
+
const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
|
|
1271
1413
|
return /* @__PURE__ */ jsxs4("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
|
|
1272
1414
|
rows.map((line, index) => {
|
|
1273
1415
|
let displayLine = line;
|
|
@@ -1278,9 +1420,10 @@ function GreenScreenTerminal({
|
|
|
1278
1420
|
const headerSegments = index === 0 ? profile.colors.parseHeaderRow(displayLine) : null;
|
|
1279
1421
|
return /* @__PURE__ */ jsxs4("div", { className: headerSegments ? "" : profile.colors.getRowColorClass(index, displayLine, termRows), style: { height: `${ROW_HEIGHT}px`, lineHeight: `${ROW_HEIGHT}px`, whiteSpace: "pre", position: "relative" }, children: [
|
|
1280
1422
|
headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ jsx4("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields),
|
|
1281
|
-
hasCursor && index === cursor.row && /* @__PURE__ */ jsx4("span", { className: "gs-cursor", style: { position: "absolute", left: `${cursor.col}ch`, width: "1ch", height: `${ROW_HEIGHT}px`, top: 0, pointerEvents: "none" } })
|
|
1423
|
+
hasCursor && !showSignInHint && index === cursor.row && /* @__PURE__ */ jsx4("span", { className: "gs-cursor", style: { position: "absolute", left: `${cursor.col}ch`, width: "1ch", height: `${ROW_HEIGHT}px`, top: 0, pointerEvents: "none" } })
|
|
1282
1424
|
] }, index);
|
|
1283
1425
|
}),
|
|
1426
|
+
showSignInHint && /* @__PURE__ */ jsx4("div", { className: "gs-signin-hint", children: "Signed in \u2014 press Enter to continue" }),
|
|
1284
1427
|
screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0 && /* @__PURE__ */ jsxs4("span", { style: { position: "absolute", bottom: 0, right: 0, fontFamily: "var(--gs-font)", fontSize: "10px", color: "var(--gs-green, #10b981)", pointerEvents: "none", opacity: 0.6 }, children: [
|
|
1285
1428
|
String(screenData.cursor_row + 1).padStart(2, "0"),
|
|
1286
1429
|
"/",
|