green-screen-react 1.0.3 → 1.1.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/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +185 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +185 -57
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +63 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -65,6 +65,8 @@ interface TerminalAdapter {
|
|
|
65
65
|
sendText(text: string): Promise<SendResult>;
|
|
66
66
|
/** Send a special key (ENTER, F1-F24, TAB, etc.) */
|
|
67
67
|
sendKey(key: string): Promise<SendResult>;
|
|
68
|
+
/** Set cursor position (click-to-position) */
|
|
69
|
+
setCursor?(row: number, col: number): Promise<SendResult>;
|
|
68
70
|
/** Establish a connection, optionally with sign-in config */
|
|
69
71
|
connect(config?: ConnectConfig): Promise<SendResult>;
|
|
70
72
|
/** Close the connection */
|
|
@@ -100,7 +102,7 @@ interface GreenScreenTerminalProps {
|
|
|
100
102
|
embedded?: boolean;
|
|
101
103
|
/** Show the header bar (default true) */
|
|
102
104
|
showHeader?: boolean;
|
|
103
|
-
/** Enable typing animation (default
|
|
105
|
+
/** Enable typing animation (default false) */
|
|
104
106
|
typingAnimation?: boolean;
|
|
105
107
|
/** Typing animation budget in ms (default 60) */
|
|
106
108
|
typingBudgetMs?: number;
|
|
@@ -227,6 +229,7 @@ declare class RestAdapter implements TerminalAdapter {
|
|
|
227
229
|
getStatus(): Promise<ConnectionStatus>;
|
|
228
230
|
sendText(text: string): Promise<SendResult>;
|
|
229
231
|
sendKey(key: string): Promise<SendResult>;
|
|
232
|
+
setCursor(row: number, col: number): Promise<SendResult>;
|
|
230
233
|
connect(config?: ConnectConfig): Promise<SendResult>;
|
|
231
234
|
disconnect(): Promise<SendResult>;
|
|
232
235
|
reconnect(): Promise<SendResult>;
|
|
@@ -273,6 +276,7 @@ declare class WebSocketAdapter implements TerminalAdapter {
|
|
|
273
276
|
getStatus(): Promise<ConnectionStatus>;
|
|
274
277
|
sendText(text: string): Promise<SendResult>;
|
|
275
278
|
sendKey(key: string): Promise<SendResult>;
|
|
279
|
+
setCursor(row: number, col: number): Promise<SendResult>;
|
|
276
280
|
connect(config?: ConnectConfig): Promise<SendResult>;
|
|
277
281
|
/**
|
|
278
282
|
* Reattach to an existing proxy session (e.g. after page reload).
|
package/dist/index.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ interface TerminalAdapter {
|
|
|
65
65
|
sendText(text: string): Promise<SendResult>;
|
|
66
66
|
/** Send a special key (ENTER, F1-F24, TAB, etc.) */
|
|
67
67
|
sendKey(key: string): Promise<SendResult>;
|
|
68
|
+
/** Set cursor position (click-to-position) */
|
|
69
|
+
setCursor?(row: number, col: number): Promise<SendResult>;
|
|
68
70
|
/** Establish a connection, optionally with sign-in config */
|
|
69
71
|
connect(config?: ConnectConfig): Promise<SendResult>;
|
|
70
72
|
/** Close the connection */
|
|
@@ -100,7 +102,7 @@ interface GreenScreenTerminalProps {
|
|
|
100
102
|
embedded?: boolean;
|
|
101
103
|
/** Show the header bar (default true) */
|
|
102
104
|
showHeader?: boolean;
|
|
103
|
-
/** Enable typing animation (default
|
|
105
|
+
/** Enable typing animation (default false) */
|
|
104
106
|
typingAnimation?: boolean;
|
|
105
107
|
/** Typing animation budget in ms (default 60) */
|
|
106
108
|
typingBudgetMs?: number;
|
|
@@ -227,6 +229,7 @@ declare class RestAdapter implements TerminalAdapter {
|
|
|
227
229
|
getStatus(): Promise<ConnectionStatus>;
|
|
228
230
|
sendText(text: string): Promise<SendResult>;
|
|
229
231
|
sendKey(key: string): Promise<SendResult>;
|
|
232
|
+
setCursor(row: number, col: number): Promise<SendResult>;
|
|
230
233
|
connect(config?: ConnectConfig): Promise<SendResult>;
|
|
231
234
|
disconnect(): Promise<SendResult>;
|
|
232
235
|
reconnect(): Promise<SendResult>;
|
|
@@ -273,6 +276,7 @@ declare class WebSocketAdapter implements TerminalAdapter {
|
|
|
273
276
|
getStatus(): Promise<ConnectionStatus>;
|
|
274
277
|
sendText(text: string): Promise<SendResult>;
|
|
275
278
|
sendKey(key: string): Promise<SendResult>;
|
|
279
|
+
setCursor(row: number, col: number): Promise<SendResult>;
|
|
276
280
|
connect(config?: ConnectConfig): Promise<SendResult>;
|
|
277
281
|
/**
|
|
278
282
|
* Reattach to an existing proxy session (e.g. after page reload).
|
package/dist/index.js
CHANGED
|
@@ -99,6 +99,9 @@ var RestAdapter = class {
|
|
|
99
99
|
async sendKey(key) {
|
|
100
100
|
return this.request("POST", "/send-key", { key });
|
|
101
101
|
}
|
|
102
|
+
async setCursor(row, col) {
|
|
103
|
+
return this.request("POST", "/set-cursor", { row, col });
|
|
104
|
+
}
|
|
102
105
|
async connect(config) {
|
|
103
106
|
return this.request("POST", "/connect", config);
|
|
104
107
|
}
|
|
@@ -168,6 +171,9 @@ var WebSocketAdapter = class _WebSocketAdapter {
|
|
|
168
171
|
async sendKey(key) {
|
|
169
172
|
return this.sendAndWaitForScreen({ type: "key", key });
|
|
170
173
|
}
|
|
174
|
+
async setCursor(row, col) {
|
|
175
|
+
return this.sendAndWaitForScreen({ type: "setCursor", row, col });
|
|
176
|
+
}
|
|
171
177
|
async connect(config) {
|
|
172
178
|
await this.ensureWebSocket();
|
|
173
179
|
if (!config) {
|
|
@@ -188,7 +194,8 @@ var WebSocketAdapter = class _WebSocketAdapter {
|
|
|
188
194
|
port: config.port,
|
|
189
195
|
protocol: config.protocol,
|
|
190
196
|
username: config.username,
|
|
191
|
-
password: config.password
|
|
197
|
+
password: config.password,
|
|
198
|
+
terminalType: config.terminalType
|
|
192
199
|
});
|
|
193
200
|
});
|
|
194
201
|
}
|
|
@@ -267,6 +274,19 @@ var WebSocketAdapter = class _WebSocketAdapter {
|
|
|
267
274
|
}
|
|
268
275
|
break;
|
|
269
276
|
}
|
|
277
|
+
case "cursor": {
|
|
278
|
+
if (this.pendingScreenResolver) {
|
|
279
|
+
const resolver = this.pendingScreenResolver;
|
|
280
|
+
this.pendingScreenResolver = null;
|
|
281
|
+
resolver({
|
|
282
|
+
cursor_row: msg.data.cursor_row,
|
|
283
|
+
cursor_col: msg.data.cursor_col,
|
|
284
|
+
content: this.screen?.content ?? "",
|
|
285
|
+
screen_signature: this.screen?.screen_signature ?? ""
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
270
290
|
case "status":
|
|
271
291
|
this.status = msg.data;
|
|
272
292
|
for (const listener of this.statusListeners) listener(msg.data);
|
|
@@ -937,6 +957,11 @@ var RefreshIcon = ({ size = 12, className }) => /* @__PURE__ */ (0, import_jsx_r
|
|
|
937
957
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" })
|
|
938
958
|
] });
|
|
939
959
|
var KeyIcon = ({ size = 12, style: s }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: s, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" }) });
|
|
960
|
+
var HelpIcon = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
961
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
962
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
|
|
963
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
|
|
964
|
+
] });
|
|
940
965
|
var MinimizeIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 14h6v6M3 21l7-7M20 10h-6V4M21 3l-7 7" }) });
|
|
941
966
|
|
|
942
967
|
// src/components/InlineSignIn.tsx
|
|
@@ -948,12 +973,26 @@ var PROTOCOL_OPTIONS = [
|
|
|
948
973
|
{ value: "vt", label: "VT220" },
|
|
949
974
|
{ value: "hp6530", label: "HP 6530 (NonStop)" }
|
|
950
975
|
];
|
|
976
|
+
var TERMINAL_TYPE_OPTIONS = {
|
|
977
|
+
tn5250: [
|
|
978
|
+
{ value: "IBM-3179-2", label: "24 \xD7 80 (Standard)" },
|
|
979
|
+
{ value: "IBM-3477-FC", label: "27 \xD7 132 (Wide)" }
|
|
980
|
+
],
|
|
981
|
+
tn3270: [
|
|
982
|
+
{ value: "IBM-3278-2", label: "24 \xD7 80 (Model 2)" },
|
|
983
|
+
{ value: "IBM-3278-3", label: "32 \xD7 80 (Model 3)" },
|
|
984
|
+
{ value: "IBM-3278-4", label: "43 \xD7 80 (Model 4)" },
|
|
985
|
+
{ value: "IBM-3278-5", label: "27 \xD7 132 (Model 5)" }
|
|
986
|
+
]
|
|
987
|
+
};
|
|
951
988
|
function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConnect }) {
|
|
952
989
|
const [host, setHost] = (0, import_react4.useState)("");
|
|
953
990
|
const [port, setPort] = (0, import_react4.useState)("");
|
|
954
991
|
const [selectedProtocol, setSelectedProtocol] = (0, import_react4.useState)(defaultProtocol);
|
|
992
|
+
const [terminalType, setTerminalType] = (0, import_react4.useState)("");
|
|
955
993
|
const [username, setUsername] = (0, import_react4.useState)("");
|
|
956
994
|
const [password, setPassword] = (0, import_react4.useState)("");
|
|
995
|
+
const termTypeOptions = TERMINAL_TYPE_OPTIONS[selectedProtocol];
|
|
957
996
|
const [submitted, setSubmitted] = (0, import_react4.useState)(false);
|
|
958
997
|
const loading = externalLoading || submitted;
|
|
959
998
|
const handleSubmit = (e) => {
|
|
@@ -964,7 +1003,8 @@ function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConn
|
|
|
964
1003
|
port: port ? parseInt(port, 10) : 23,
|
|
965
1004
|
protocol: selectedProtocol,
|
|
966
1005
|
...username.trim() ? { username: username.trim() } : {},
|
|
967
|
-
...password ? { password } : {}
|
|
1006
|
+
...password ? { password } : {},
|
|
1007
|
+
...terminalType ? { terminalType } : {}
|
|
968
1008
|
});
|
|
969
1009
|
};
|
|
970
1010
|
const inputStyle = {
|
|
@@ -1020,7 +1060,14 @@ function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConn
|
|
|
1020
1060
|
"Protocol ",
|
|
1021
1061
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#ef4444" }, children: "*" })
|
|
1022
1062
|
] }),
|
|
1023
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("select", { style: { ...inputStyle, appearance: "none" }, value: selectedProtocol, onChange: (e) =>
|
|
1063
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("select", { style: { ...inputStyle, appearance: "none" }, value: selectedProtocol, onChange: (e) => {
|
|
1064
|
+
setSelectedProtocol(e.target.value);
|
|
1065
|
+
setTerminalType("");
|
|
1066
|
+
}, children: PROTOCOL_OPTIONS.map((o) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: o.value, children: o.label }, o.value)) })
|
|
1067
|
+
] }),
|
|
1068
|
+
termTypeOptions && termTypeOptions.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
1069
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: labelStyle, children: "Screen Size" }),
|
|
1070
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("select", { style: { ...inputStyle, appearance: "none" }, value: terminalType || termTypeOptions[0].value, onChange: (e) => setTerminalType(e.target.value), children: termTypeOptions.map((o) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: o.value, children: o.label }, o.value)) })
|
|
1024
1071
|
] }),
|
|
1025
1072
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
1026
1073
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: labelStyle, children: "Username" }),
|
|
@@ -1046,6 +1093,7 @@ var noopAdapter = {
|
|
|
1046
1093
|
getStatus: async () => ({ connected: false, status: "disconnected" }),
|
|
1047
1094
|
sendText: async () => noopResult,
|
|
1048
1095
|
sendKey: async () => noopResult,
|
|
1096
|
+
setCursor: async () => noopResult,
|
|
1049
1097
|
connect: async () => noopResult,
|
|
1050
1098
|
disconnect: async () => noopResult,
|
|
1051
1099
|
reconnect: async () => noopResult
|
|
@@ -1064,7 +1112,7 @@ function GreenScreenTerminal({
|
|
|
1064
1112
|
maxReconnectAttempts: maxAttempts = 5,
|
|
1065
1113
|
embedded = false,
|
|
1066
1114
|
showHeader = true,
|
|
1067
|
-
typingAnimation =
|
|
1115
|
+
typingAnimation = false,
|
|
1068
1116
|
typingBudgetMs = 60,
|
|
1069
1117
|
inlineSignIn = true,
|
|
1070
1118
|
defaultProtocol: signInDefaultProtocol,
|
|
@@ -1120,9 +1168,18 @@ function GreenScreenTerminal({
|
|
|
1120
1168
|
}, [screenData, onScreenChange]);
|
|
1121
1169
|
const sendText = (0, import_react5.useCallback)(async (text) => _sendText(text), [_sendText]);
|
|
1122
1170
|
const sendKey = (0, import_react5.useCallback)(async (key) => _sendKey(key), [_sendKey]);
|
|
1171
|
+
const [optimisticEdits, setOptimisticEdits] = (0, import_react5.useState)([]);
|
|
1172
|
+
const prevScreenSigForEdits = (0, import_react5.useRef)(void 0);
|
|
1173
|
+
(0, import_react5.useEffect)(() => {
|
|
1174
|
+
if (rawScreenData?.screen_signature && rawScreenData.screen_signature !== prevScreenSigForEdits.current) {
|
|
1175
|
+
prevScreenSigForEdits.current = rawScreenData.screen_signature;
|
|
1176
|
+
setOptimisticEdits([]);
|
|
1177
|
+
}
|
|
1178
|
+
}, [rawScreenData?.screen_signature]);
|
|
1123
1179
|
const [inputText, setInputText] = (0, import_react5.useState)("");
|
|
1124
1180
|
const [isFocused, setIsFocused] = (0, import_react5.useState)(false);
|
|
1125
1181
|
const [showSignInHint, setShowSignInHint] = (0, import_react5.useState)(false);
|
|
1182
|
+
const [showShortcuts, setShowShortcuts] = (0, import_react5.useState)(false);
|
|
1126
1183
|
const prevAutoSignedIn = (0, import_react5.useRef)(false);
|
|
1127
1184
|
(0, import_react5.useEffect)(() => {
|
|
1128
1185
|
if (autoSignedIn && !prevAutoSignedIn.current) setShowSignInHint(true);
|
|
@@ -1265,11 +1322,38 @@ function GreenScreenTerminal({
|
|
|
1265
1322
|
inputRef.current?.blur();
|
|
1266
1323
|
}
|
|
1267
1324
|
}, [readOnly, isFocused]);
|
|
1268
|
-
const
|
|
1325
|
+
const screenContentRef = (0, import_react5.useRef)(null);
|
|
1326
|
+
const charWidthRef = (0, import_react5.useRef)(0);
|
|
1327
|
+
const handleTerminalClick = (0, import_react5.useCallback)((e) => {
|
|
1269
1328
|
if (readOnly) return;
|
|
1270
1329
|
setIsFocused(true);
|
|
1271
1330
|
inputRef.current?.focus();
|
|
1272
|
-
|
|
1331
|
+
const contentEl = screenContentRef.current;
|
|
1332
|
+
if (!contentEl || !screenData?.fields) return;
|
|
1333
|
+
if (!charWidthRef.current) {
|
|
1334
|
+
const span = document.createElement("span");
|
|
1335
|
+
span.style.cssText = "position:absolute;visibility:hidden;font:inherit;white-space:pre";
|
|
1336
|
+
span.textContent = "X";
|
|
1337
|
+
contentEl.appendChild(span);
|
|
1338
|
+
charWidthRef.current = span.getBoundingClientRect().width;
|
|
1339
|
+
contentEl.removeChild(span);
|
|
1340
|
+
}
|
|
1341
|
+
const rect = contentEl.getBoundingClientRect();
|
|
1342
|
+
const x = e.clientX - rect.left;
|
|
1343
|
+
const y = e.clientY - rect.top;
|
|
1344
|
+
const ROW_HEIGHT = 21;
|
|
1345
|
+
const charWidth = charWidthRef.current;
|
|
1346
|
+
if (!charWidth) return;
|
|
1347
|
+
const clickedRow = Math.floor(y / ROW_HEIGHT);
|
|
1348
|
+
const clickedCol = Math.floor(x / charWidth);
|
|
1349
|
+
if (clickedRow < 0 || clickedRow >= (screenData.rows || 24) || clickedCol < 0 || clickedCol >= (screenData.cols || 80)) return;
|
|
1350
|
+
setSyncedCursor({ row: clickedRow, col: clickedCol });
|
|
1351
|
+
adapter.setCursor?.(clickedRow, clickedCol).then((r) => {
|
|
1352
|
+
if (r?.cursor_row !== void 0) {
|
|
1353
|
+
setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
}, [readOnly, screenData, adapter]);
|
|
1273
1357
|
const getCurrentField = (0, import_react5.useCallback)(() => {
|
|
1274
1358
|
const fields = screenData?.fields || [];
|
|
1275
1359
|
const cursorRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
|
|
@@ -1279,12 +1363,6 @@ function GreenScreenTerminal({
|
|
|
1279
1363
|
}
|
|
1280
1364
|
return null;
|
|
1281
1365
|
}, [screenData, syncedCursor]);
|
|
1282
|
-
const canTypeMore = (0, import_react5.useCallback)((additionalChars = 1) => {
|
|
1283
|
-
const currentField = getCurrentField();
|
|
1284
|
-
if (!currentField) return true;
|
|
1285
|
-
const cursorCol = (syncedCursor?.col ?? screenData?.cursor_col ?? 0) + inputText.length;
|
|
1286
|
-
return cursorCol + additionalChars <= currentField.col + currentField.length;
|
|
1287
|
-
}, [getCurrentField, syncedCursor, screenData, inputText]);
|
|
1288
1366
|
const handleKeyDown = async (e) => {
|
|
1289
1367
|
if (readOnly) {
|
|
1290
1368
|
e.preventDefault();
|
|
@@ -1296,19 +1374,22 @@ function GreenScreenTerminal({
|
|
|
1296
1374
|
inputRef.current?.blur();
|
|
1297
1375
|
return;
|
|
1298
1376
|
}
|
|
1299
|
-
if (e.key === "
|
|
1377
|
+
if (e.ctrlKey && e.key === "r") {
|
|
1300
1378
|
e.preventDefault();
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1379
|
+
const kr = await sendKey("RESET");
|
|
1380
|
+
if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (e.ctrlKey && e.key === "Enter") {
|
|
1384
|
+
e.preventDefault();
|
|
1385
|
+
const kr = await sendKey("FIELD_EXIT");
|
|
1386
|
+
if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
|
|
1307
1387
|
return;
|
|
1308
1388
|
}
|
|
1309
1389
|
const keyMap = {
|
|
1310
1390
|
Enter: "ENTER",
|
|
1311
1391
|
Tab: "TAB",
|
|
1392
|
+
Backspace: "BACKSPACE",
|
|
1312
1393
|
Delete: "DELETE",
|
|
1313
1394
|
ArrowUp: "UP",
|
|
1314
1395
|
ArrowDown: "DOWN",
|
|
@@ -1324,11 +1405,6 @@ function GreenScreenTerminal({
|
|
|
1324
1405
|
e.preventDefault();
|
|
1325
1406
|
const fKey = e.key.toUpperCase();
|
|
1326
1407
|
if (/^F([1-9]|1[0-9]|2[0-4])$/.test(fKey)) {
|
|
1327
|
-
if (inputText) {
|
|
1328
|
-
const r = await sendText(inputText);
|
|
1329
|
-
setInputText("");
|
|
1330
|
-
if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
|
|
1331
|
-
}
|
|
1332
1408
|
const kr = await sendKey(fKey);
|
|
1333
1409
|
if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
|
|
1334
1410
|
return;
|
|
@@ -1336,11 +1412,6 @@ function GreenScreenTerminal({
|
|
|
1336
1412
|
}
|
|
1337
1413
|
if (keyMap[e.key]) {
|
|
1338
1414
|
e.preventDefault();
|
|
1339
|
-
if (inputText) {
|
|
1340
|
-
const r = await sendText(inputText);
|
|
1341
|
-
setInputText("");
|
|
1342
|
-
if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
|
|
1343
|
-
}
|
|
1344
1415
|
const kr = await sendKey(keyMap[e.key]);
|
|
1345
1416
|
if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
|
|
1346
1417
|
}
|
|
@@ -1351,30 +1422,28 @@ function GreenScreenTerminal({
|
|
|
1351
1422
|
return;
|
|
1352
1423
|
}
|
|
1353
1424
|
const newText = e.target.value;
|
|
1354
|
-
if (newText.
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1425
|
+
if (newText.length > inputText.length) {
|
|
1426
|
+
const newChars = newText.substring(inputText.length);
|
|
1427
|
+
const curRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
|
|
1428
|
+
const curCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
|
|
1429
|
+
const edits = [];
|
|
1430
|
+
for (let i = 0; i < newChars.length; i++) {
|
|
1431
|
+
edits.push({ row: curRow, col: curCol + i, ch: newChars[i] });
|
|
1359
1432
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const charsToAdd = newText.length - inputText.length;
|
|
1366
|
-
if (charsToAdd > 0 && !canTypeMore(charsToAdd)) {
|
|
1367
|
-
e.target.value = inputText;
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
setInputText(newText);
|
|
1433
|
+
setOptimisticEdits((prev) => [...prev, ...edits]);
|
|
1434
|
+
setSyncedCursor({ row: curRow, col: curCol + newChars.length });
|
|
1435
|
+
sendText(newChars).then((r) => {
|
|
1436
|
+
if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
|
|
1437
|
+
});
|
|
1371
1438
|
}
|
|
1439
|
+
setInputText("");
|
|
1440
|
+
e.target.value = "";
|
|
1372
1441
|
};
|
|
1373
1442
|
const termCols = screenData?.cols || profile.defaultCols;
|
|
1374
1443
|
const getCursorPos = () => {
|
|
1375
1444
|
if (animatedCursorPos) return animatedCursorPos;
|
|
1376
1445
|
let cursorRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
|
|
1377
|
-
let cursorCol =
|
|
1446
|
+
let cursorCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
|
|
1378
1447
|
while (cursorCol >= termCols) {
|
|
1379
1448
|
cursorCol -= termCols;
|
|
1380
1449
|
cursorRow += 1;
|
|
@@ -1401,7 +1470,8 @@ function GreenScreenTerminal({
|
|
|
1401
1470
|
const inputFields = fields.filter((f) => f.row === rowIndex && f.is_input);
|
|
1402
1471
|
const highlightedFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_highlighted);
|
|
1403
1472
|
const reverseFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_reverse);
|
|
1404
|
-
const
|
|
1473
|
+
const colorFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.color && !f.is_highlighted && !f.is_reverse);
|
|
1474
|
+
const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields, ...colorFields];
|
|
1405
1475
|
if (allRowFields.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line });
|
|
1406
1476
|
const sorted = [...allRowFields].sort((a, b) => a.col - b.col);
|
|
1407
1477
|
const segs = [];
|
|
@@ -1412,12 +1482,15 @@ function GreenScreenTerminal({
|
|
|
1412
1482
|
const fe = Math.min(field.col + field.length, cols);
|
|
1413
1483
|
if (fs > lastEnd) segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line.substring(lastEnd, fs) }, `t${idx}`));
|
|
1414
1484
|
const fc = line.substring(fs, fe);
|
|
1485
|
+
const colorVar = field.color ? `var(--gs-${field.color}, var(--gs-green))` : void 0;
|
|
1415
1486
|
if (field.is_input) {
|
|
1416
1487
|
const fieldWidth = Math.min(field.length, cols - fs);
|
|
1417
1488
|
const fieldClass = showSignInHint ? "gs-confirmed-field" : field.is_underscored ? "gs-input-field" : void 0;
|
|
1418
|
-
segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: fieldClass || void 0, style: { display: "inline-block", width: `${fieldWidth}ch`, overflow: "hidden" }, children: fc }, `f${idx}`));
|
|
1489
|
+
segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: fieldClass || void 0, style: { display: "inline-block", width: `${fieldWidth}ch`, overflow: "hidden", color: colorVar }, children: fc }, `f${idx}`));
|
|
1419
1490
|
} else if (field.is_reverse) {
|
|
1420
|
-
segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: "#
|
|
1491
|
+
segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: colorVar || "var(--gs-red, #FF5555)", fontWeight: "bold" }, children: fc }, `v${idx}`));
|
|
1492
|
+
} else if (colorVar) {
|
|
1493
|
+
segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: colorVar }, children: fc }, `h${idx}`));
|
|
1421
1494
|
} else if (field.is_highlighted) {
|
|
1422
1495
|
segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: fc }, `h${idx}`));
|
|
1423
1496
|
} else {
|
|
@@ -1461,17 +1534,24 @@ function GreenScreenTerminal({
|
|
|
1461
1534
|
const ROW_HEIGHT = 21;
|
|
1462
1535
|
const cursor = getCursorPos();
|
|
1463
1536
|
const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
|
|
1537
|
+
const cursorInInputField = hasCursor && fields.some(
|
|
1538
|
+
(f) => f.is_input && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
|
|
1539
|
+
);
|
|
1464
1540
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
|
|
1465
1541
|
rows.map((line, index) => {
|
|
1466
1542
|
let displayLine = line;
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1543
|
+
const rowEdits = optimisticEdits.filter((e) => e.row === index);
|
|
1544
|
+
if (rowEdits.length > 0) {
|
|
1545
|
+
const chars = displayLine.split("");
|
|
1546
|
+
for (const edit of rowEdits) {
|
|
1547
|
+
if (edit.col >= 0 && edit.col < chars.length) chars[edit.col] = edit.ch;
|
|
1548
|
+
}
|
|
1549
|
+
displayLine = chars.join("");
|
|
1470
1550
|
}
|
|
1471
1551
|
const headerSegments = index === 0 ? profile.colors.parseHeaderRow(displayLine) : null;
|
|
1472
1552
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: headerSegments ? "" : profile.colors.getRowColorClass(index, displayLine, termRows), style: { height: `${ROW_HEIGHT}px`, lineHeight: `${ROW_HEIGHT}px`, whiteSpace: "pre", position: "relative" }, children: [
|
|
1473
1553
|
headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields),
|
|
1474
|
-
|
|
1554
|
+
cursorInInputField && !showSignInHint && index === cursor.row && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-cursor", style: { position: "absolute", left: `${cursor.col}ch`, width: "1ch", height: `${ROW_HEIGHT}px`, top: 0, pointerEvents: "none" } })
|
|
1475
1555
|
] }, index);
|
|
1476
1556
|
}),
|
|
1477
1557
|
showSignInHint && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-signin-hint", children: "Signed in \u2014 press Enter to continue" }),
|
|
@@ -1512,7 +1592,9 @@ function GreenScreenTerminal({
|
|
|
1512
1592
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "TERMINAL" }),
|
|
1513
1593
|
isFocused && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-focused", children: "FOCUSED" }),
|
|
1514
1594
|
screenData?.timestamp && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-timestamp", children: new Date(screenData.timestamp).toLocaleTimeString() }),
|
|
1515
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-hint", children: readOnly ? "Read-only" : isFocused ? "ESC to exit focus" : "Click to control" })
|
|
1595
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-hint", children: readOnly ? "Read-only" : isFocused ? "ESC to exit focus" : "Click to control" }),
|
|
1596
|
+
screenData?.keyboard_locked && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-lock", children: "X II" }),
|
|
1597
|
+
screenData?.insert_mode && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-ins", children: "INS" })
|
|
1516
1598
|
] }),
|
|
1517
1599
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-header-right", children: [
|
|
1518
1600
|
connStatus?.status && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
|
|
@@ -1521,6 +1603,10 @@ function GreenScreenTerminal({
|
|
|
1521
1603
|
e.stopPropagation();
|
|
1522
1604
|
onMinimize();
|
|
1523
1605
|
}, className: "gs-btn-icon", title: "Minimize terminal", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MinimizeIcon, {}) }),
|
|
1606
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: (e) => {
|
|
1607
|
+
e.stopPropagation();
|
|
1608
|
+
setShowShortcuts((s) => !s);
|
|
1609
|
+
}, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(HelpIcon, { size: 12 }) }),
|
|
1524
1610
|
headerRight
|
|
1525
1611
|
] })
|
|
1526
1612
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
@@ -1529,7 +1615,9 @@ function GreenScreenTerminal({
|
|
|
1529
1615
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: profile.headerLabel }),
|
|
1530
1616
|
isFocused && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-focused", children: "FOCUSED" }),
|
|
1531
1617
|
screenData?.timestamp && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-timestamp", children: new Date(screenData.timestamp).toLocaleTimeString() }),
|
|
1532
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-hint", children: readOnly ? "Read-only mode" : isFocused ? "ESC to exit focus" : "Click terminal to control" })
|
|
1618
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-hint", children: readOnly ? "Read-only mode" : isFocused ? "ESC to exit focus" : "Click terminal to control" }),
|
|
1619
|
+
screenData?.keyboard_locked && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-lock", children: "X II" }),
|
|
1620
|
+
screenData?.insert_mode && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-ins", children: "INS" })
|
|
1533
1621
|
] }),
|
|
1534
1622
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-header-right", children: [
|
|
1535
1623
|
connStatus && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-status-group", children: connStatus.connected ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
@@ -1544,6 +1632,10 @@ function GreenScreenTerminal({
|
|
|
1544
1632
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
|
|
1545
1633
|
connStatus.username && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-host", children: connStatus.username })
|
|
1546
1634
|
] }),
|
|
1635
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: (e) => {
|
|
1636
|
+
e.stopPropagation();
|
|
1637
|
+
setShowShortcuts((s) => !s);
|
|
1638
|
+
}, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(HelpIcon, { size: 12 }) }),
|
|
1547
1639
|
headerRight
|
|
1548
1640
|
] })
|
|
1549
1641
|
] }) }),
|
|
@@ -1559,8 +1651,44 @@ function GreenScreenTerminal({
|
|
|
1559
1651
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AlertTriangleIcon, { size: 14 }),
|
|
1560
1652
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: String(screenError) })
|
|
1561
1653
|
] }),
|
|
1562
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-screen-content", children: renderScreen() }),
|
|
1654
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: screenContentRef, className: "gs-screen-content", children: renderScreen() }),
|
|
1563
1655
|
overlay,
|
|
1656
|
+
showShortcuts && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-shortcuts-panel", children: [
|
|
1657
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-shortcuts-header", children: [
|
|
1658
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "Keyboard Shortcuts" }),
|
|
1659
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "gs-btn-icon", onClick: () => setShowShortcuts(false), style: { pointerEvents: "auto" }, children: "\xD7" })
|
|
1660
|
+
] }),
|
|
1661
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("table", { className: "gs-shortcuts-table", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tbody", { children: [
|
|
1662
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
1663
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Ctrl+Enter" }),
|
|
1664
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Field Exit" })
|
|
1665
|
+
] }),
|
|
1666
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
1667
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Ctrl+R" }),
|
|
1668
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Reset" })
|
|
1669
|
+
] }),
|
|
1670
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
1671
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Insert" }),
|
|
1672
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Insert / Overwrite" })
|
|
1673
|
+
] }),
|
|
1674
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
1675
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Page Up" }),
|
|
1676
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Roll Down" })
|
|
1677
|
+
] }),
|
|
1678
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
1679
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Page Down" }),
|
|
1680
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Roll Up" })
|
|
1681
|
+
] }),
|
|
1682
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
1683
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Click" }),
|
|
1684
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Focus / Position cursor" })
|
|
1685
|
+
] }),
|
|
1686
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
|
|
1687
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Escape" }),
|
|
1688
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Exit focus mode" })
|
|
1689
|
+
] })
|
|
1690
|
+
] }) })
|
|
1691
|
+
] }),
|
|
1564
1692
|
connStatus && !connStatus.connected && screenData && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-overlay", children: [
|
|
1565
1693
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WifiOffIcon, { size: 28 }),
|
|
1566
1694
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : "Disconnected" })
|
|
@@ -1573,7 +1701,7 @@ function GreenScreenTerminal({
|
|
|
1573
1701
|
value: inputText,
|
|
1574
1702
|
onChange: handleInput,
|
|
1575
1703
|
onKeyDown: handleKeyDown,
|
|
1576
|
-
style: { position: "absolute", opacity: 0, pointerEvents: "none" },
|
|
1704
|
+
style: { position: "absolute", opacity: 0, pointerEvents: "none", fontSize: "13px", lineHeight: "21px", fontFamily: "var(--gs-font)", padding: 0, border: "none", height: "21px" },
|
|
1577
1705
|
autoComplete: "off",
|
|
1578
1706
|
autoCorrect: "off",
|
|
1579
1707
|
autoCapitalize: "off",
|