green-screen-react 1.0.2 → 1.1.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.
package/dist/index.mjs CHANGED
@@ -49,6 +49,9 @@ var RestAdapter = class {
49
49
  async sendKey(key) {
50
50
  return this.request("POST", "/send-key", { key });
51
51
  }
52
+ async setCursor(row, col) {
53
+ return this.request("POST", "/set-cursor", { row, col });
54
+ }
52
55
  async connect(config) {
53
56
  return this.request("POST", "/connect", config);
54
57
  }
@@ -117,6 +120,9 @@ var WebSocketAdapter = class _WebSocketAdapter {
117
120
  async sendKey(key) {
118
121
  return this.sendAndWaitForScreen({ type: "key", key });
119
122
  }
123
+ async setCursor(row, col) {
124
+ return this.sendAndWaitForScreen({ type: "setCursor", row, col });
125
+ }
120
126
  async connect(config) {
121
127
  await this.ensureWebSocket();
122
128
  if (!config) {
@@ -137,7 +143,8 @@ var WebSocketAdapter = class _WebSocketAdapter {
137
143
  port: config.port,
138
144
  protocol: config.protocol,
139
145
  username: config.username,
140
- password: config.password
146
+ password: config.password,
147
+ terminalType: config.terminalType
141
148
  });
142
149
  });
143
150
  }
@@ -216,6 +223,19 @@ var WebSocketAdapter = class _WebSocketAdapter {
216
223
  }
217
224
  break;
218
225
  }
226
+ case "cursor": {
227
+ if (this.pendingScreenResolver) {
228
+ const resolver = this.pendingScreenResolver;
229
+ this.pendingScreenResolver = null;
230
+ resolver({
231
+ cursor_row: msg.data.cursor_row,
232
+ cursor_col: msg.data.cursor_col,
233
+ content: this.screen?.content ?? "",
234
+ screen_signature: this.screen?.screen_signature ?? ""
235
+ });
236
+ }
237
+ break;
238
+ }
219
239
  case "status":
220
240
  this.status = msg.data;
221
241
  for (const listener of this.statusListeners) listener(msg.data);
@@ -886,6 +906,11 @@ var RefreshIcon = ({ size = 12, className }) => /* @__PURE__ */ jsxs2("svg", { w
886
906
  /* @__PURE__ */ jsx2("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" })
887
907
  ] });
888
908
  var KeyIcon = ({ size = 12, style: s }) => /* @__PURE__ */ jsx2("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: s, children: /* @__PURE__ */ jsx2("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" }) });
909
+ var HelpIcon = ({ size = 14 }) => /* @__PURE__ */ jsxs2("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
910
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
911
+ /* @__PURE__ */ jsx2("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
912
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
913
+ ] });
889
914
  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" }) });
890
915
 
891
916
  // src/components/InlineSignIn.tsx
@@ -897,12 +922,26 @@ var PROTOCOL_OPTIONS = [
897
922
  { value: "vt", label: "VT220" },
898
923
  { value: "hp6530", label: "HP 6530 (NonStop)" }
899
924
  ];
925
+ var TERMINAL_TYPE_OPTIONS = {
926
+ tn5250: [
927
+ { value: "IBM-3179-2", label: "24 \xD7 80 (Standard)" },
928
+ { value: "IBM-3477-FC", label: "27 \xD7 132 (Wide)" }
929
+ ],
930
+ tn3270: [
931
+ { value: "IBM-3278-2", label: "24 \xD7 80 (Model 2)" },
932
+ { value: "IBM-3278-3", label: "32 \xD7 80 (Model 3)" },
933
+ { value: "IBM-3278-4", label: "43 \xD7 80 (Model 4)" },
934
+ { value: "IBM-3278-5", label: "27 \xD7 132 (Model 5)" }
935
+ ]
936
+ };
900
937
  function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConnect }) {
901
938
  const [host, setHost] = useState4("");
902
939
  const [port, setPort] = useState4("");
903
940
  const [selectedProtocol, setSelectedProtocol] = useState4(defaultProtocol);
941
+ const [terminalType, setTerminalType] = useState4("");
904
942
  const [username, setUsername] = useState4("");
905
943
  const [password, setPassword] = useState4("");
944
+ const termTypeOptions = TERMINAL_TYPE_OPTIONS[selectedProtocol];
906
945
  const [submitted, setSubmitted] = useState4(false);
907
946
  const loading = externalLoading || submitted;
908
947
  const handleSubmit = (e) => {
@@ -913,7 +952,8 @@ function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConn
913
952
  port: port ? parseInt(port, 10) : 23,
914
953
  protocol: selectedProtocol,
915
954
  ...username.trim() ? { username: username.trim() } : {},
916
- ...password ? { password } : {}
955
+ ...password ? { password } : {},
956
+ ...terminalType ? { terminalType } : {}
917
957
  });
918
958
  };
919
959
  const inputStyle = {
@@ -969,7 +1009,14 @@ function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConn
969
1009
  "Protocol ",
970
1010
  /* @__PURE__ */ jsx3("span", { style: { color: "#ef4444" }, children: "*" })
971
1011
  ] }),
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)) })
1012
+ /* @__PURE__ */ jsx3("select", { style: { ...inputStyle, appearance: "none" }, value: selectedProtocol, onChange: (e) => {
1013
+ setSelectedProtocol(e.target.value);
1014
+ setTerminalType("");
1015
+ }, children: PROTOCOL_OPTIONS.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: o.label }, o.value)) })
1016
+ ] }),
1017
+ termTypeOptions && termTypeOptions.length > 1 && /* @__PURE__ */ jsxs3("div", { children: [
1018
+ /* @__PURE__ */ jsx3("label", { style: labelStyle, children: "Screen Size" }),
1019
+ /* @__PURE__ */ jsx3("select", { style: { ...inputStyle, appearance: "none" }, value: terminalType || termTypeOptions[0].value, onChange: (e) => setTerminalType(e.target.value), children: termTypeOptions.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: o.label }, o.value)) })
973
1020
  ] }),
974
1021
  /* @__PURE__ */ jsxs3("div", { children: [
975
1022
  /* @__PURE__ */ jsx3("label", { style: labelStyle, children: "Username" }),
@@ -995,6 +1042,7 @@ var noopAdapter = {
995
1042
  getStatus: async () => ({ connected: false, status: "disconnected" }),
996
1043
  sendText: async () => noopResult,
997
1044
  sendKey: async () => noopResult,
1045
+ setCursor: async () => noopResult,
998
1046
  connect: async () => noopResult,
999
1047
  disconnect: async () => noopResult,
1000
1048
  reconnect: async () => noopResult
@@ -1013,7 +1061,7 @@ function GreenScreenTerminal({
1013
1061
  maxReconnectAttempts: maxAttempts = 5,
1014
1062
  embedded = false,
1015
1063
  showHeader = true,
1016
- typingAnimation = true,
1064
+ typingAnimation = false,
1017
1065
  typingBudgetMs = 60,
1018
1066
  inlineSignIn = true,
1019
1067
  defaultProtocol: signInDefaultProtocol,
@@ -1069,9 +1117,18 @@ function GreenScreenTerminal({
1069
1117
  }, [screenData, onScreenChange]);
1070
1118
  const sendText = useCallback3(async (text) => _sendText(text), [_sendText]);
1071
1119
  const sendKey = useCallback3(async (key) => _sendKey(key), [_sendKey]);
1120
+ const [optimisticEdits, setOptimisticEdits] = useState5([]);
1121
+ const prevScreenSigForEdits = useRef4(void 0);
1122
+ useEffect4(() => {
1123
+ if (rawScreenData?.screen_signature && rawScreenData.screen_signature !== prevScreenSigForEdits.current) {
1124
+ prevScreenSigForEdits.current = rawScreenData.screen_signature;
1125
+ setOptimisticEdits([]);
1126
+ }
1127
+ }, [rawScreenData?.screen_signature]);
1072
1128
  const [inputText, setInputText] = useState5("");
1073
1129
  const [isFocused, setIsFocused] = useState5(false);
1074
1130
  const [showSignInHint, setShowSignInHint] = useState5(false);
1131
+ const [showShortcuts, setShowShortcuts] = useState5(false);
1075
1132
  const prevAutoSignedIn = useRef4(false);
1076
1133
  useEffect4(() => {
1077
1134
  if (autoSignedIn && !prevAutoSignedIn.current) setShowSignInHint(true);
@@ -1214,11 +1271,38 @@ function GreenScreenTerminal({
1214
1271
  inputRef.current?.blur();
1215
1272
  }
1216
1273
  }, [readOnly, isFocused]);
1217
- const handleTerminalClick = useCallback3(() => {
1274
+ const screenContentRef = useRef4(null);
1275
+ const charWidthRef = useRef4(0);
1276
+ const handleTerminalClick = useCallback3((e) => {
1218
1277
  if (readOnly) return;
1219
1278
  setIsFocused(true);
1220
1279
  inputRef.current?.focus();
1221
- }, [readOnly]);
1280
+ const contentEl = screenContentRef.current;
1281
+ if (!contentEl || !screenData?.fields) return;
1282
+ if (!charWidthRef.current) {
1283
+ const span = document.createElement("span");
1284
+ span.style.cssText = "position:absolute;visibility:hidden;font:inherit;white-space:pre";
1285
+ span.textContent = "X";
1286
+ contentEl.appendChild(span);
1287
+ charWidthRef.current = span.getBoundingClientRect().width;
1288
+ contentEl.removeChild(span);
1289
+ }
1290
+ const rect = contentEl.getBoundingClientRect();
1291
+ const x = e.clientX - rect.left;
1292
+ const y = e.clientY - rect.top;
1293
+ const ROW_HEIGHT = 21;
1294
+ const charWidth = charWidthRef.current;
1295
+ if (!charWidth) return;
1296
+ const clickedRow = Math.floor(y / ROW_HEIGHT);
1297
+ const clickedCol = Math.floor(x / charWidth);
1298
+ if (clickedRow < 0 || clickedRow >= (screenData.rows || 24) || clickedCol < 0 || clickedCol >= (screenData.cols || 80)) return;
1299
+ setSyncedCursor({ row: clickedRow, col: clickedCol });
1300
+ adapter.setCursor?.(clickedRow, clickedCol).then((r) => {
1301
+ if (r?.cursor_row !== void 0) {
1302
+ setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1303
+ }
1304
+ });
1305
+ }, [readOnly, screenData, adapter]);
1222
1306
  const getCurrentField = useCallback3(() => {
1223
1307
  const fields = screenData?.fields || [];
1224
1308
  const cursorRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
@@ -1228,12 +1312,6 @@ function GreenScreenTerminal({
1228
1312
  }
1229
1313
  return null;
1230
1314
  }, [screenData, syncedCursor]);
1231
- const canTypeMore = useCallback3((additionalChars = 1) => {
1232
- const currentField = getCurrentField();
1233
- if (!currentField) return true;
1234
- const cursorCol = (syncedCursor?.col ?? screenData?.cursor_col ?? 0) + inputText.length;
1235
- return cursorCol + additionalChars <= currentField.col + currentField.length;
1236
- }, [getCurrentField, syncedCursor, screenData, inputText]);
1237
1315
  const handleKeyDown = async (e) => {
1238
1316
  if (readOnly) {
1239
1317
  e.preventDefault();
@@ -1245,19 +1323,22 @@ function GreenScreenTerminal({
1245
1323
  inputRef.current?.blur();
1246
1324
  return;
1247
1325
  }
1248
- if (e.key === "Backspace") {
1326
+ if (e.ctrlKey && e.key === "r") {
1249
1327
  e.preventDefault();
1250
- if (inputText.length > 0) {
1251
- setInputText((prev) => prev.slice(0, -1));
1252
- } else {
1253
- const keyResult = await sendKey("BACKSPACE");
1254
- if (keyResult.cursor_row !== void 0) setSyncedCursor({ row: keyResult.cursor_row, col: keyResult.cursor_col });
1255
- }
1328
+ const kr = await sendKey("RESET");
1329
+ if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1330
+ return;
1331
+ }
1332
+ if (e.ctrlKey && e.key === "Enter") {
1333
+ e.preventDefault();
1334
+ const kr = await sendKey("FIELD_EXIT");
1335
+ if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1256
1336
  return;
1257
1337
  }
1258
1338
  const keyMap = {
1259
1339
  Enter: "ENTER",
1260
1340
  Tab: "TAB",
1341
+ Backspace: "BACKSPACE",
1261
1342
  Delete: "DELETE",
1262
1343
  ArrowUp: "UP",
1263
1344
  ArrowDown: "DOWN",
@@ -1273,11 +1354,6 @@ function GreenScreenTerminal({
1273
1354
  e.preventDefault();
1274
1355
  const fKey = e.key.toUpperCase();
1275
1356
  if (/^F([1-9]|1[0-9]|2[0-4])$/.test(fKey)) {
1276
- if (inputText) {
1277
- const r = await sendText(inputText);
1278
- setInputText("");
1279
- if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1280
- }
1281
1357
  const kr = await sendKey(fKey);
1282
1358
  if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1283
1359
  return;
@@ -1285,11 +1361,6 @@ function GreenScreenTerminal({
1285
1361
  }
1286
1362
  if (keyMap[e.key]) {
1287
1363
  e.preventDefault();
1288
- if (inputText) {
1289
- const r = await sendText(inputText);
1290
- setInputText("");
1291
- if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1292
- }
1293
1364
  const kr = await sendKey(keyMap[e.key]);
1294
1365
  if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1295
1366
  }
@@ -1300,30 +1371,28 @@ function GreenScreenTerminal({
1300
1371
  return;
1301
1372
  }
1302
1373
  const newText = e.target.value;
1303
- if (newText.includes("\n")) {
1304
- const textToSend = newText.replace("\n", "");
1305
- if (textToSend) {
1306
- const r = await sendText(textToSend);
1307
- if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1374
+ if (newText.length > inputText.length) {
1375
+ const newChars = newText.substring(inputText.length);
1376
+ const curRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
1377
+ const curCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1378
+ const edits = [];
1379
+ for (let i = 0; i < newChars.length; i++) {
1380
+ edits.push({ row: curRow, col: curCol + i, ch: newChars[i] });
1308
1381
  }
1309
- const kr = await sendKey("ENTER");
1310
- if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1311
- setInputText("");
1312
- e.target.value = "";
1313
- } else {
1314
- const charsToAdd = newText.length - inputText.length;
1315
- if (charsToAdd > 0 && !canTypeMore(charsToAdd)) {
1316
- e.target.value = inputText;
1317
- return;
1318
- }
1319
- setInputText(newText);
1382
+ setOptimisticEdits((prev) => [...prev, ...edits]);
1383
+ setSyncedCursor({ row: curRow, col: curCol + newChars.length });
1384
+ sendText(newChars).then((r) => {
1385
+ if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1386
+ });
1320
1387
  }
1388
+ setInputText("");
1389
+ e.target.value = "";
1321
1390
  };
1322
1391
  const termCols = screenData?.cols || profile.defaultCols;
1323
1392
  const getCursorPos = () => {
1324
1393
  if (animatedCursorPos) return animatedCursorPos;
1325
1394
  let cursorRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
1326
- let cursorCol = (syncedCursor?.col ?? screenData?.cursor_col ?? 0) + inputText.length;
1395
+ let cursorCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1327
1396
  while (cursorCol >= termCols) {
1328
1397
  cursorCol -= termCols;
1329
1398
  cursorRow += 1;
@@ -1350,7 +1419,8 @@ function GreenScreenTerminal({
1350
1419
  const inputFields = fields.filter((f) => f.row === rowIndex && f.is_input);
1351
1420
  const highlightedFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_highlighted);
1352
1421
  const reverseFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_reverse);
1353
- const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields];
1422
+ const colorFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.color && !f.is_highlighted && !f.is_reverse);
1423
+ const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields, ...colorFields];
1354
1424
  if (allRowFields.length === 0) return /* @__PURE__ */ jsx4("span", { children: line });
1355
1425
  const sorted = [...allRowFields].sort((a, b) => a.col - b.col);
1356
1426
  const segs = [];
@@ -1361,12 +1431,15 @@ function GreenScreenTerminal({
1361
1431
  const fe = Math.min(field.col + field.length, cols);
1362
1432
  if (fs > lastEnd) segs.push(/* @__PURE__ */ jsx4("span", { children: line.substring(lastEnd, fs) }, `t${idx}`));
1363
1433
  const fc = line.substring(fs, fe);
1434
+ const colorVar = field.color ? `var(--gs-${field.color}, var(--gs-green))` : void 0;
1364
1435
  if (field.is_input) {
1365
1436
  const fieldWidth = Math.min(field.length, cols - fs);
1366
1437
  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}`));
1438
+ segs.push(/* @__PURE__ */ jsx4("span", { className: fieldClass || void 0, style: { display: "inline-block", width: `${fieldWidth}ch`, overflow: "hidden", color: colorVar }, children: fc }, `f${idx}`));
1368
1439
  } else if (field.is_reverse) {
1369
- segs.push(/* @__PURE__ */ jsx4("span", { style: { color: "#ef4444", fontWeight: "bold" }, children: fc }, `v${idx}`));
1440
+ segs.push(/* @__PURE__ */ jsx4("span", { style: { color: colorVar || "var(--gs-red, #FF5555)", fontWeight: "bold" }, children: fc }, `v${idx}`));
1441
+ } else if (colorVar) {
1442
+ segs.push(/* @__PURE__ */ jsx4("span", { style: { color: colorVar }, children: fc }, `h${idx}`));
1370
1443
  } else if (field.is_highlighted) {
1371
1444
  segs.push(/* @__PURE__ */ jsx4("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: fc }, `h${idx}`));
1372
1445
  } else {
@@ -1410,17 +1483,24 @@ function GreenScreenTerminal({
1410
1483
  const ROW_HEIGHT = 21;
1411
1484
  const cursor = getCursorPos();
1412
1485
  const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
1486
+ const cursorInInputField = hasCursor && fields.some(
1487
+ (f) => f.is_input && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
1488
+ );
1413
1489
  return /* @__PURE__ */ jsxs4("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
1414
1490
  rows.map((line, index) => {
1415
1491
  let displayLine = line;
1416
- if (hasCursor && index === cursor.row && inputText && !animatedCursorPos) {
1417
- const baseCol = syncedCursor?.col ?? screenData.cursor_col ?? 0;
1418
- displayLine = (line.substring(0, baseCol) + inputText + line.substring(baseCol + inputText.length)).substring(0, cols).padEnd(cols, " ");
1492
+ const rowEdits = optimisticEdits.filter((e) => e.row === index);
1493
+ if (rowEdits.length > 0) {
1494
+ const chars = displayLine.split("");
1495
+ for (const edit of rowEdits) {
1496
+ if (edit.col >= 0 && edit.col < chars.length) chars[edit.col] = edit.ch;
1497
+ }
1498
+ displayLine = chars.join("");
1419
1499
  }
1420
1500
  const headerSegments = index === 0 ? profile.colors.parseHeaderRow(displayLine) : null;
1421
1501
  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: [
1422
1502
  headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ jsx4("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields),
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" } })
1503
+ cursorInInputField && !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" } })
1424
1504
  ] }, index);
1425
1505
  }),
1426
1506
  showSignInHint && /* @__PURE__ */ jsx4("div", { className: "gs-signin-hint", children: "Signed in \u2014 press Enter to continue" }),
@@ -1461,7 +1541,9 @@ function GreenScreenTerminal({
1461
1541
  /* @__PURE__ */ jsx4("span", { children: "TERMINAL" }),
1462
1542
  isFocused && /* @__PURE__ */ jsx4("span", { className: "gs-badge-focused", children: "FOCUSED" }),
1463
1543
  screenData?.timestamp && /* @__PURE__ */ jsx4("span", { className: "gs-timestamp", children: new Date(screenData.timestamp).toLocaleTimeString() }),
1464
- /* @__PURE__ */ jsx4("span", { className: "gs-hint", children: readOnly ? "Read-only" : isFocused ? "ESC to exit focus" : "Click to control" })
1544
+ /* @__PURE__ */ jsx4("span", { className: "gs-hint", children: readOnly ? "Read-only" : isFocused ? "ESC to exit focus" : "Click to control" }),
1545
+ screenData?.keyboard_locked && /* @__PURE__ */ jsx4("span", { className: "gs-badge-lock", children: "X II" }),
1546
+ screenData?.insert_mode && /* @__PURE__ */ jsx4("span", { className: "gs-badge-ins", children: "INS" })
1465
1547
  ] }),
1466
1548
  /* @__PURE__ */ jsxs4("div", { className: "gs-header-right", children: [
1467
1549
  connStatus?.status && /* @__PURE__ */ jsx4(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
@@ -1470,6 +1552,10 @@ function GreenScreenTerminal({
1470
1552
  e.stopPropagation();
1471
1553
  onMinimize();
1472
1554
  }, className: "gs-btn-icon", title: "Minimize terminal", children: /* @__PURE__ */ jsx4(MinimizeIcon, {}) }),
1555
+ /* @__PURE__ */ jsx4("button", { onClick: (e) => {
1556
+ e.stopPropagation();
1557
+ setShowShortcuts((s) => !s);
1558
+ }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ jsx4(HelpIcon, { size: 12 }) }),
1473
1559
  headerRight
1474
1560
  ] })
1475
1561
  ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
@@ -1478,7 +1564,9 @@ function GreenScreenTerminal({
1478
1564
  /* @__PURE__ */ jsx4("span", { children: profile.headerLabel }),
1479
1565
  isFocused && /* @__PURE__ */ jsx4("span", { className: "gs-badge-focused", children: "FOCUSED" }),
1480
1566
  screenData?.timestamp && /* @__PURE__ */ jsx4("span", { className: "gs-timestamp", children: new Date(screenData.timestamp).toLocaleTimeString() }),
1481
- /* @__PURE__ */ jsx4("span", { className: "gs-hint", children: readOnly ? "Read-only mode" : isFocused ? "ESC to exit focus" : "Click terminal to control" })
1567
+ /* @__PURE__ */ jsx4("span", { className: "gs-hint", children: readOnly ? "Read-only mode" : isFocused ? "ESC to exit focus" : "Click terminal to control" }),
1568
+ screenData?.keyboard_locked && /* @__PURE__ */ jsx4("span", { className: "gs-badge-lock", children: "X II" }),
1569
+ screenData?.insert_mode && /* @__PURE__ */ jsx4("span", { className: "gs-badge-ins", children: "INS" })
1482
1570
  ] }),
1483
1571
  /* @__PURE__ */ jsxs4("div", { className: "gs-header-right", children: [
1484
1572
  connStatus && /* @__PURE__ */ jsx4("div", { className: "gs-status-group", children: connStatus.connected ? /* @__PURE__ */ jsxs4(Fragment, { children: [
@@ -1493,6 +1581,10 @@ function GreenScreenTerminal({
1493
1581
  /* @__PURE__ */ jsx4(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
1494
1582
  connStatus.username && /* @__PURE__ */ jsx4("span", { className: "gs-host", children: connStatus.username })
1495
1583
  ] }),
1584
+ /* @__PURE__ */ jsx4("button", { onClick: (e) => {
1585
+ e.stopPropagation();
1586
+ setShowShortcuts((s) => !s);
1587
+ }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ jsx4(HelpIcon, { size: 12 }) }),
1496
1588
  headerRight
1497
1589
  ] })
1498
1590
  ] }) }),
@@ -1508,8 +1600,44 @@ function GreenScreenTerminal({
1508
1600
  /* @__PURE__ */ jsx4(AlertTriangleIcon, { size: 14 }),
1509
1601
  /* @__PURE__ */ jsx4("span", { children: String(screenError) })
1510
1602
  ] }),
1511
- /* @__PURE__ */ jsx4("div", { className: "gs-screen-content", children: renderScreen() }),
1603
+ /* @__PURE__ */ jsx4("div", { ref: screenContentRef, className: "gs-screen-content", children: renderScreen() }),
1512
1604
  overlay,
1605
+ showShortcuts && /* @__PURE__ */ jsxs4("div", { className: "gs-shortcuts-panel", children: [
1606
+ /* @__PURE__ */ jsxs4("div", { className: "gs-shortcuts-header", children: [
1607
+ /* @__PURE__ */ jsx4("span", { children: "Keyboard Shortcuts" }),
1608
+ /* @__PURE__ */ jsx4("button", { className: "gs-btn-icon", onClick: () => setShowShortcuts(false), style: { pointerEvents: "auto" }, children: "\xD7" })
1609
+ ] }),
1610
+ /* @__PURE__ */ jsx4("table", { className: "gs-shortcuts-table", children: /* @__PURE__ */ jsxs4("tbody", { children: [
1611
+ /* @__PURE__ */ jsxs4("tr", { children: [
1612
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Ctrl+Enter" }),
1613
+ /* @__PURE__ */ jsx4("td", { children: "Field Exit" })
1614
+ ] }),
1615
+ /* @__PURE__ */ jsxs4("tr", { children: [
1616
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Ctrl+R" }),
1617
+ /* @__PURE__ */ jsx4("td", { children: "Reset" })
1618
+ ] }),
1619
+ /* @__PURE__ */ jsxs4("tr", { children: [
1620
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Insert" }),
1621
+ /* @__PURE__ */ jsx4("td", { children: "Insert / Overwrite" })
1622
+ ] }),
1623
+ /* @__PURE__ */ jsxs4("tr", { children: [
1624
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Page Up" }),
1625
+ /* @__PURE__ */ jsx4("td", { children: "Roll Down" })
1626
+ ] }),
1627
+ /* @__PURE__ */ jsxs4("tr", { children: [
1628
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Page Down" }),
1629
+ /* @__PURE__ */ jsx4("td", { children: "Roll Up" })
1630
+ ] }),
1631
+ /* @__PURE__ */ jsxs4("tr", { children: [
1632
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Click" }),
1633
+ /* @__PURE__ */ jsx4("td", { children: "Focus / Position cursor" })
1634
+ ] }),
1635
+ /* @__PURE__ */ jsxs4("tr", { children: [
1636
+ /* @__PURE__ */ jsx4("td", { className: "gs-shortcut-key", children: "Escape" }),
1637
+ /* @__PURE__ */ jsx4("td", { children: "Exit focus mode" })
1638
+ ] })
1639
+ ] }) })
1640
+ ] }),
1513
1641
  connStatus && !connStatus.connected && screenData && /* @__PURE__ */ jsxs4("div", { className: "gs-overlay", children: [
1514
1642
  /* @__PURE__ */ jsx4(WifiOffIcon, { size: 28 }),
1515
1643
  /* @__PURE__ */ jsx4("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : "Disconnected" })
@@ -1522,7 +1650,7 @@ function GreenScreenTerminal({
1522
1650
  value: inputText,
1523
1651
  onChange: handleInput,
1524
1652
  onKeyDown: handleKeyDown,
1525
- style: { position: "absolute", opacity: 0, pointerEvents: "none" },
1653
+ style: { position: "absolute", opacity: 0, pointerEvents: "none", fontSize: "13px", lineHeight: "21px", fontFamily: "var(--gs-font)", padding: 0, border: "none", height: "21px" },
1526
1654
  autoComplete: "off",
1527
1655
  autoCorrect: "off",
1528
1656
  autoCapitalize: "off",