green-screen-react 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +114 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -22
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +35 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/GreenScreenTerminal.tsx
|
|
2
|
-
import { useState as useState5, useEffect as
|
|
2
|
+
import { useState as useState5, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback3, useMemo as useMemo2 } from "react";
|
|
3
3
|
|
|
4
4
|
// src/adapters/RestAdapter.ts
|
|
5
5
|
var RestAdapter = class {
|
|
@@ -80,6 +80,7 @@ var WebSocketAdapter = class _WebSocketAdapter {
|
|
|
80
80
|
this.pendingScreenResolver = null;
|
|
81
81
|
this.pendingConnectResolver = null;
|
|
82
82
|
this.pendingMdtResolver = null;
|
|
83
|
+
this.disconnectAckResolver = null;
|
|
83
84
|
this.connectingPromise = null;
|
|
84
85
|
this.screenListeners = /* @__PURE__ */ new Set();
|
|
85
86
|
this.statusListeners = /* @__PURE__ */ new Set();
|
|
@@ -214,7 +215,19 @@ var WebSocketAdapter = class _WebSocketAdapter {
|
|
|
214
215
|
});
|
|
215
216
|
}
|
|
216
217
|
async disconnect() {
|
|
218
|
+
const acked = new Promise((resolve) => {
|
|
219
|
+
const timer = setTimeout(resolve, 3e3);
|
|
220
|
+
this.disconnectAckResolver = () => {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
resolve();
|
|
223
|
+
};
|
|
224
|
+
});
|
|
217
225
|
this.wsSend({ type: "disconnect" });
|
|
226
|
+
try {
|
|
227
|
+
await acked;
|
|
228
|
+
} finally {
|
|
229
|
+
this.disconnectAckResolver = null;
|
|
230
|
+
}
|
|
218
231
|
this.status = { connected: false, status: "disconnected" };
|
|
219
232
|
this._sessionId = null;
|
|
220
233
|
if (this.ws) {
|
|
@@ -270,6 +283,14 @@ var WebSocketAdapter = class _WebSocketAdapter {
|
|
|
270
283
|
break;
|
|
271
284
|
}
|
|
272
285
|
case "cursor": {
|
|
286
|
+
if (this.screen) {
|
|
287
|
+
this.screen = {
|
|
288
|
+
...this.screen,
|
|
289
|
+
cursor_row: msg.data.cursor_row,
|
|
290
|
+
cursor_col: msg.data.cursor_col
|
|
291
|
+
};
|
|
292
|
+
for (const listener of this.screenListeners) listener(this.screen);
|
|
293
|
+
}
|
|
273
294
|
if (this.pendingScreenResolver) {
|
|
274
295
|
const resolver = this.pendingScreenResolver;
|
|
275
296
|
this.pendingScreenResolver = null;
|
|
@@ -310,6 +331,13 @@ var WebSocketAdapter = class _WebSocketAdapter {
|
|
|
310
331
|
resolver({ success: true });
|
|
311
332
|
}
|
|
312
333
|
break;
|
|
334
|
+
case "disconnected":
|
|
335
|
+
if (this.disconnectAckResolver) {
|
|
336
|
+
const resolver = this.disconnectAckResolver;
|
|
337
|
+
this.disconnectAckResolver = null;
|
|
338
|
+
resolver();
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
313
341
|
case "error": {
|
|
314
342
|
if (this.pendingConnectResolver) {
|
|
315
343
|
const resolver = this.pendingConnectResolver;
|
|
@@ -981,8 +1009,27 @@ var KeyboardIcon = ({ size = 14 }) => /* @__PURE__ */ jsxs2("svg", { width: size
|
|
|
981
1009
|
var MinimizeIcon = () => /* @__PURE__ */ jsx2("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx2("path", { d: "M4 14h6v6M3 21l7-7M20 10h-6V4M21 3l-7 7" }) });
|
|
982
1010
|
|
|
983
1011
|
// src/components/InlineSignIn.tsx
|
|
984
|
-
import { useState as useState4 } from "react";
|
|
1012
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
985
1013
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1014
|
+
var STORAGE_KEY = "green-screen:inline-signin";
|
|
1015
|
+
function loadStored() {
|
|
1016
|
+
if (typeof window === "undefined") return {};
|
|
1017
|
+
try {
|
|
1018
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
1019
|
+
if (!raw) return {};
|
|
1020
|
+
const parsed = JSON.parse(raw);
|
|
1021
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1022
|
+
} catch {
|
|
1023
|
+
return {};
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function saveStored(value) {
|
|
1027
|
+
if (typeof window === "undefined") return;
|
|
1028
|
+
try {
|
|
1029
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
|
|
1030
|
+
} catch {
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
986
1033
|
var PROTOCOL_OPTIONS = [
|
|
987
1034
|
{ value: "tn5250", label: "TN5250 (IBM i)" },
|
|
988
1035
|
{ value: "tn3270", label: "TN3270 (Mainframe)" },
|
|
@@ -1002,12 +1049,22 @@ var TERMINAL_TYPE_OPTIONS = {
|
|
|
1002
1049
|
]
|
|
1003
1050
|
};
|
|
1004
1051
|
function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConnect }) {
|
|
1005
|
-
const
|
|
1006
|
-
const [
|
|
1007
|
-
const [
|
|
1008
|
-
const [
|
|
1009
|
-
const [
|
|
1052
|
+
const stored = loadStored();
|
|
1053
|
+
const [host, setHost] = useState4(stored.host ?? "");
|
|
1054
|
+
const [port, setPort] = useState4(stored.port ?? "");
|
|
1055
|
+
const [selectedProtocol, setSelectedProtocol] = useState4(stored.protocol ?? defaultProtocol);
|
|
1056
|
+
const [terminalType, setTerminalType] = useState4(stored.terminalType ?? "");
|
|
1057
|
+
const [username, setUsername] = useState4(stored.username ?? "");
|
|
1010
1058
|
const [password, setPassword] = useState4("");
|
|
1059
|
+
useEffect4(() => {
|
|
1060
|
+
saveStored({
|
|
1061
|
+
host,
|
|
1062
|
+
port,
|
|
1063
|
+
protocol: selectedProtocol,
|
|
1064
|
+
terminalType,
|
|
1065
|
+
username
|
|
1066
|
+
});
|
|
1067
|
+
}, [host, port, selectedProtocol, terminalType, username]);
|
|
1011
1068
|
const termTypeOptions = TERMINAL_TYPE_OPTIONS[selectedProtocol];
|
|
1012
1069
|
const [submitted, setSubmitted] = useState4(false);
|
|
1013
1070
|
const loading = externalLoading || submitted;
|
|
@@ -1285,6 +1342,22 @@ function validateMod11(value) {
|
|
|
1285
1342
|
|
|
1286
1343
|
// src/components/GreenScreenTerminal.tsx
|
|
1287
1344
|
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1345
|
+
function isBlankContent(content) {
|
|
1346
|
+
if (!content) return true;
|
|
1347
|
+
for (let i = 0; i < content.length; i++) {
|
|
1348
|
+
const ch = content.charCodeAt(i);
|
|
1349
|
+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13 && ch !== 0 && ch !== 160) {
|
|
1350
|
+
return false;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
function formatBusyClock(ms) {
|
|
1356
|
+
const sec = Math.floor(ms / 1e3);
|
|
1357
|
+
const m = Math.floor(sec / 60);
|
|
1358
|
+
const s = sec % 60;
|
|
1359
|
+
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
1360
|
+
}
|
|
1288
1361
|
function aidByteToKeyName(aid) {
|
|
1289
1362
|
if (aid === 241) return "ENTER";
|
|
1290
1363
|
if (aid === 243) return "HELP";
|
|
@@ -1359,7 +1432,33 @@ function GreenScreenTerminal({
|
|
|
1359
1432
|
const { data: polledScreenData, error: screenError } = useTerminalScreen(adapter, pollInterval, shouldPoll);
|
|
1360
1433
|
const { sendText: _sendText, sendKey: _sendKey } = useTerminalInput(adapter);
|
|
1361
1434
|
const { connect, reconnect, loading: reconnecting, error: connectError } = useTerminalConnection(adapter);
|
|
1362
|
-
const
|
|
1435
|
+
const incomingScreenData = externalScreenData ?? polledScreenData;
|
|
1436
|
+
const lastContentfulRef = useRef4(null);
|
|
1437
|
+
useEffect5(() => {
|
|
1438
|
+
if (incomingScreenData && !isBlankContent(incomingScreenData.content)) {
|
|
1439
|
+
lastContentfulRef.current = incomingScreenData;
|
|
1440
|
+
}
|
|
1441
|
+
}, [incomingScreenData]);
|
|
1442
|
+
const isBlankLocked = !!(incomingScreenData && incomingScreenData.keyboard_locked && isBlankContent(incomingScreenData.content));
|
|
1443
|
+
const rawScreenData = useMemo2(() => {
|
|
1444
|
+
if (isBlankLocked && lastContentfulRef.current) {
|
|
1445
|
+
return { ...lastContentfulRef.current, keyboard_locked: true };
|
|
1446
|
+
}
|
|
1447
|
+
return incomingScreenData;
|
|
1448
|
+
}, [incomingScreenData, isBlankLocked]);
|
|
1449
|
+
const BUSY_OVERLAY_DELAY_MS = 600;
|
|
1450
|
+
const [lockElapsedMs, setLockElapsedMs] = useState5(0);
|
|
1451
|
+
useEffect5(() => {
|
|
1452
|
+
if (!isBlankLocked) {
|
|
1453
|
+
setLockElapsedMs(0);
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const start = Date.now();
|
|
1457
|
+
setLockElapsedMs(0);
|
|
1458
|
+
const id = setInterval(() => setLockElapsedMs(Date.now() - start), 250);
|
|
1459
|
+
return () => clearInterval(id);
|
|
1460
|
+
}, [isBlankLocked]);
|
|
1461
|
+
const showBusyOverlay = isBlankLocked && lockElapsedMs >= BUSY_OVERLAY_DELAY_MS;
|
|
1363
1462
|
const connStatus = externalStatus ?? (rawScreenData ? { connected: true, status: "authenticated" } : { connected: false, status: "disconnected" });
|
|
1364
1463
|
const { displayedContent, animatedCursorPos } = useTypingAnimation(
|
|
1365
1464
|
rawScreenData?.content,
|
|
@@ -1371,7 +1470,7 @@ function GreenScreenTerminal({
|
|
|
1371
1470
|
return { ...rawScreenData, content: displayedContent };
|
|
1372
1471
|
}, [rawScreenData, displayedContent]);
|
|
1373
1472
|
const prevScreenSigRef = useRef4(void 0);
|
|
1374
|
-
|
|
1473
|
+
useEffect5(() => {
|
|
1375
1474
|
if (screenData && onScreenChange && screenData.screen_signature !== prevScreenSigRef.current) {
|
|
1376
1475
|
prevScreenSigRef.current = screenData.screen_signature;
|
|
1377
1476
|
onScreenChange(screenData);
|
|
@@ -1381,7 +1480,7 @@ function GreenScreenTerminal({
|
|
|
1381
1480
|
const sendKey = useCallback3(async (key) => _sendKey(key), [_sendKey]);
|
|
1382
1481
|
const [optimisticEdits, setOptimisticEdits] = useState5([]);
|
|
1383
1482
|
const prevScreenContentForEdits = useRef4(void 0);
|
|
1384
|
-
|
|
1483
|
+
useEffect5(() => {
|
|
1385
1484
|
const content = rawScreenData?.content;
|
|
1386
1485
|
if (content && content !== prevScreenContentForEdits.current) {
|
|
1387
1486
|
prevScreenContentForEdits.current = content;
|
|
@@ -1395,7 +1494,7 @@ function GreenScreenTerminal({
|
|
|
1395
1494
|
const inputRef = useRef4(null);
|
|
1396
1495
|
const [syncedCursor, setSyncedCursor] = useState5(null);
|
|
1397
1496
|
const prevRawContentRef = useRef4("");
|
|
1398
|
-
|
|
1497
|
+
useEffect5(() => {
|
|
1399
1498
|
const newContent = rawScreenData?.content || "";
|
|
1400
1499
|
if (prevRawContentRef.current && newContent && newContent !== prevRawContentRef.current) {
|
|
1401
1500
|
setSyncedCursor(null);
|
|
@@ -1408,10 +1507,10 @@ function GreenScreenTerminal({
|
|
|
1408
1507
|
const reconnectTimeoutRef = useRef4(null);
|
|
1409
1508
|
const wasConnectedRef = useRef4(false);
|
|
1410
1509
|
const isConnectedRef = useRef4(false);
|
|
1411
|
-
|
|
1510
|
+
useEffect5(() => {
|
|
1412
1511
|
isConnectedRef.current = connStatus?.connected ?? false;
|
|
1413
1512
|
}, [connStatus?.connected]);
|
|
1414
|
-
|
|
1513
|
+
useEffect5(() => {
|
|
1415
1514
|
if (!autoReconnectEnabled) return;
|
|
1416
1515
|
const isConnected = connStatus?.connected;
|
|
1417
1516
|
if (isConnected) {
|
|
@@ -1470,12 +1569,12 @@ function GreenScreenTerminal({
|
|
|
1470
1569
|
setConnecting(false);
|
|
1471
1570
|
}
|
|
1472
1571
|
}, [connect, onSignIn, externalAdapter, baseUrlAdapter]);
|
|
1473
|
-
|
|
1572
|
+
useEffect5(() => {
|
|
1474
1573
|
if (connecting && screenData?.content) setConnecting(false);
|
|
1475
1574
|
}, [connecting, screenData?.content]);
|
|
1476
1575
|
const [showBootLoader, setShowBootLoader] = useState5(bootLoader !== false);
|
|
1477
1576
|
const [bootFadingOut, setBootFadingOut] = useState5(false);
|
|
1478
|
-
|
|
1577
|
+
useEffect5(() => {
|
|
1479
1578
|
if (!showBootLoader) return;
|
|
1480
1579
|
const shouldDismiss = bootLoaderReady !== void 0 ? bootLoaderReady : !!screenData?.content;
|
|
1481
1580
|
if (shouldDismiss) {
|
|
@@ -1486,7 +1585,7 @@ function GreenScreenTerminal({
|
|
|
1486
1585
|
}
|
|
1487
1586
|
}, [screenData?.content, showBootLoader, bootLoaderReady]);
|
|
1488
1587
|
const FOCUS_STORAGE_KEY = "gs-terminal-focused";
|
|
1489
|
-
|
|
1588
|
+
useEffect5(() => {
|
|
1490
1589
|
if (!autoFocusDisabled && !readOnly) {
|
|
1491
1590
|
try {
|
|
1492
1591
|
if (localStorage.getItem(FOCUS_STORAGE_KEY) === "true") {
|
|
@@ -1496,32 +1595,32 @@ function GreenScreenTerminal({
|
|
|
1496
1595
|
}
|
|
1497
1596
|
}
|
|
1498
1597
|
}, []);
|
|
1499
|
-
|
|
1598
|
+
useEffect5(() => {
|
|
1500
1599
|
if (autoFocusDisabled) return;
|
|
1501
1600
|
try {
|
|
1502
1601
|
localStorage.setItem(FOCUS_STORAGE_KEY, String(isFocused));
|
|
1503
1602
|
} catch {
|
|
1504
1603
|
}
|
|
1505
1604
|
}, [isFocused, autoFocusDisabled]);
|
|
1506
|
-
|
|
1605
|
+
useEffect5(() => {
|
|
1507
1606
|
if (isFocused) inputRef.current?.focus();
|
|
1508
1607
|
}, [isFocused]);
|
|
1509
1608
|
const hadScreenData = useRef4(false);
|
|
1510
|
-
|
|
1609
|
+
useEffect5(() => {
|
|
1511
1610
|
if (screenData?.content && !hadScreenData.current && !autoFocusDisabled && !readOnly) {
|
|
1512
1611
|
hadScreenData.current = true;
|
|
1513
1612
|
setIsFocused(true);
|
|
1514
1613
|
}
|
|
1515
1614
|
if (!screenData?.content) hadScreenData.current = false;
|
|
1516
1615
|
}, [screenData?.content, autoFocusDisabled, readOnly]);
|
|
1517
|
-
|
|
1616
|
+
useEffect5(() => {
|
|
1518
1617
|
const handleClickOutside = (event) => {
|
|
1519
1618
|
if (terminalRef.current && !terminalRef.current.contains(event.target)) setIsFocused(false);
|
|
1520
1619
|
};
|
|
1521
1620
|
if (isFocused) document.addEventListener("mousedown", handleClickOutside);
|
|
1522
1621
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
1523
1622
|
}, [isFocused]);
|
|
1524
|
-
|
|
1623
|
+
useEffect5(() => {
|
|
1525
1624
|
if (readOnly && isFocused) {
|
|
1526
1625
|
setIsFocused(false);
|
|
1527
1626
|
inputRef.current?.blur();
|
|
@@ -1907,7 +2006,7 @@ function GreenScreenTerminal({
|
|
|
1907
2006
|
const cursor = getCursorPos();
|
|
1908
2007
|
const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
|
|
1909
2008
|
const cursorInInputField = hasCursor && fields.some(
|
|
1910
|
-
(f) => f.is_input &&
|
|
2009
|
+
(f) => f.is_input && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
|
|
1911
2010
|
);
|
|
1912
2011
|
return /* @__PURE__ */ jsxs4("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
|
|
1913
2012
|
rows.map((line, index) => {
|
|
@@ -2117,6 +2216,14 @@ function GreenScreenTerminal({
|
|
|
2117
2216
|
/* @__PURE__ */ jsx4("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : connStatus?.status === "connecting" ? "Connecting..." : "Disconnected" }),
|
|
2118
2217
|
connStatus.error && !isAutoReconnecting && !reconnecting && /* @__PURE__ */ jsx4("span", { style: { fontSize: "0.75em", opacity: 0.7, maxWidth: "80%", textAlign: "center", wordBreak: "break-word" }, children: connStatus.error })
|
|
2119
2218
|
] }),
|
|
2219
|
+
showBusyOverlay && connStatus?.connected && /* @__PURE__ */ jsxs4("div", { className: "gs-busy-overlay", role: "status", "aria-live": "polite", children: [
|
|
2220
|
+
/* @__PURE__ */ jsx4(RefreshIcon, { size: 22, className: "gs-spin" }),
|
|
2221
|
+
/* @__PURE__ */ jsxs4("span", { className: "gs-busy-clock", children: [
|
|
2222
|
+
"X CLOCK\xA0\xA0",
|
|
2223
|
+
formatBusyClock(lockElapsedMs)
|
|
2224
|
+
] }),
|
|
2225
|
+
/* @__PURE__ */ jsx4("span", { className: "gs-busy-hint", children: "Waiting for host\u2026" })
|
|
2226
|
+
] }),
|
|
2120
2227
|
/* @__PURE__ */ jsx4(
|
|
2121
2228
|
"input",
|
|
2122
2229
|
{
|