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 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 true) */
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 true) */
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) => setSelectedProtocol(e.target.value), children: PROTOCOL_OPTIONS.map((o) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: o.value, children: o.label }, o.value)) })
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 = true,
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 handleTerminalClick = (0, import_react5.useCallback)(() => {
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
- }, [readOnly]);
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 === "Backspace") {
1377
+ if (e.ctrlKey && e.key === "r") {
1300
1378
  e.preventDefault();
1301
- if (inputText.length > 0) {
1302
- setInputText((prev) => prev.slice(0, -1));
1303
- } else {
1304
- const keyResult = await sendKey("BACKSPACE");
1305
- if (keyResult.cursor_row !== void 0) setSyncedCursor({ row: keyResult.cursor_row, col: keyResult.cursor_col });
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.includes("\n")) {
1355
- const textToSend = newText.replace("\n", "");
1356
- if (textToSend) {
1357
- const r = await sendText(textToSend);
1358
- if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
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
- const kr = await sendKey("ENTER");
1361
- if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1362
- setInputText("");
1363
- e.target.value = "";
1364
- } else {
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 = (syncedCursor?.col ?? screenData?.cursor_col ?? 0) + inputText.length;
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 allRowFields = [...inputFields, ...highlightedFields, ...reverseFields];
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: "#ef4444", fontWeight: "bold" }, children: fc }, `v${idx}`));
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
- if (hasCursor && index === cursor.row && inputText && !animatedCursorPos) {
1468
- const baseCol = syncedCursor?.col ?? screenData.cursor_col ?? 0;
1469
- displayLine = (line.substring(0, baseCol) + inputText + line.substring(baseCol + inputText.length)).substring(0, cols).padEnd(cols, " ");
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
- hasCursor && !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" } })
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",