green-screen-react 1.1.2 → 1.2.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.js CHANGED
@@ -102,6 +102,14 @@ var RestAdapter = class {
102
102
  async setCursor(row, col) {
103
103
  return this.request("POST", "/set-cursor", { row, col });
104
104
  }
105
+ async readMdt(modifiedOnly = true) {
106
+ const qs = modifiedOnly ? "" : "?includeUnmodified=1";
107
+ const resp = await this.request(
108
+ "GET",
109
+ `/read-mdt${qs}`
110
+ );
111
+ return resp?.fields ?? [];
112
+ }
105
113
  async connect(config) {
106
114
  return this.request("POST", "/connect", config);
107
115
  }
@@ -122,9 +130,12 @@ var WebSocketAdapter = class _WebSocketAdapter {
122
130
  this.status = { connected: false, status: "disconnected" };
123
131
  this.pendingScreenResolver = null;
124
132
  this.pendingConnectResolver = null;
133
+ this.pendingMdtResolver = null;
125
134
  this.connectingPromise = null;
126
135
  this.screenListeners = /* @__PURE__ */ new Set();
127
136
  this.statusListeners = /* @__PURE__ */ new Set();
137
+ this.sessionLostListeners = /* @__PURE__ */ new Set();
138
+ this.sessionResumedListeners = /* @__PURE__ */ new Set();
128
139
  this._sessionId = null;
129
140
  this.workerUrl = (options.workerUrl || _WebSocketAdapter.detectEnvUrl() || "http://localhost:3001").replace(/\/+$/, "");
130
141
  }
@@ -159,6 +170,22 @@ var WebSocketAdapter = class _WebSocketAdapter {
159
170
  this.statusListeners.add(listener);
160
171
  return () => this.statusListeners.delete(listener);
161
172
  }
173
+ /**
174
+ * Subscribe to session-lost notifications. Fires when the proxy detects
175
+ * that the server-side session has terminated (host TCP drop, idle
176
+ * timeout, explicit destroy). Integrators use this to prompt a reconnect
177
+ * or swap to a "session expired" UI without relying on error-string
178
+ * matching.
179
+ */
180
+ onSessionLost(listener) {
181
+ this.sessionLostListeners.add(listener);
182
+ return () => this.sessionLostListeners.delete(listener);
183
+ }
184
+ /** Subscribe to session-resumed notifications (fires after a successful `reattach`). */
185
+ onSessionResumed(listener) {
186
+ this.sessionResumedListeners.add(listener);
187
+ return () => this.sessionResumedListeners.delete(listener);
188
+ }
162
189
  async getScreen() {
163
190
  return this.screen;
164
191
  }
@@ -174,6 +201,25 @@ var WebSocketAdapter = class _WebSocketAdapter {
174
201
  async setCursor(row, col) {
175
202
  return this.sendAndWaitForScreen({ type: "setCursor", row, col });
176
203
  }
204
+ async readMdt(modifiedOnly = true) {
205
+ await this.ensureWebSocket();
206
+ return new Promise((resolve) => {
207
+ if (this.pendingMdtResolver) {
208
+ const old = this.pendingMdtResolver;
209
+ this.pendingMdtResolver = null;
210
+ old([]);
211
+ }
212
+ const timeout = setTimeout(() => {
213
+ this.pendingMdtResolver = null;
214
+ resolve([]);
215
+ }, 5e3);
216
+ this.pendingMdtResolver = (fields) => {
217
+ clearTimeout(timeout);
218
+ resolve(fields);
219
+ };
220
+ this.wsSend({ type: "readMdt", modifiedOnly });
221
+ });
222
+ }
177
223
  async connect(config) {
178
224
  await this.ensureWebSocket();
179
225
  if (!config) {
@@ -287,6 +333,22 @@ var WebSocketAdapter = class _WebSocketAdapter {
287
333
  }
288
334
  break;
289
335
  }
336
+ case "mdt": {
337
+ if (this.pendingMdtResolver) {
338
+ const resolver = this.pendingMdtResolver;
339
+ this.pendingMdtResolver = null;
340
+ resolver(msg.data?.fields ?? []);
341
+ }
342
+ break;
343
+ }
344
+ case "session.lost": {
345
+ for (const listener of this.sessionLostListeners) listener(msg.sessionId, msg.status);
346
+ break;
347
+ }
348
+ case "session.resumed": {
349
+ for (const listener of this.sessionResumedListeners) listener(msg.sessionId);
350
+ break;
351
+ }
290
352
  case "status":
291
353
  this.status = msg.data;
292
354
  for (const listener of this.statusListeners) listener(msg.data);
@@ -957,10 +1019,15 @@ var RefreshIcon = ({ size = 12, className }) => /* @__PURE__ */ (0, import_jsx_r
957
1019
  /* @__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" })
958
1020
  ] });
959
1021
  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" })
1022
+ var KeyboardIcon = ({ 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: [
1023
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "2", y: "6", width: "20", height: "12", rx: "2", ry: "2" }),
1024
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "6", y1: "10", x2: "6", y2: "10" }),
1025
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "10", y1: "10", x2: "10", y2: "10" }),
1026
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "14", y1: "10", x2: "14", y2: "10" }),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "18", y1: "10", x2: "18", y2: "10" }),
1028
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "6", y1: "14", x2: "6", y2: "14" }),
1029
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "18", y1: "14", x2: "18", y2: "14" }),
1030
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "10", y1: "14", x2: "14", y2: "14" })
964
1031
  ] });
965
1032
  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" }) });
966
1033
 
@@ -1085,8 +1152,201 @@ function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConn
1085
1152
  ] });
1086
1153
  }
1087
1154
 
1155
+ // src/utils/attribute.ts
1156
+ function decodeAttrByte(byte) {
1157
+ const type = byte & 7;
1158
+ const colorGroup = byte & 24;
1159
+ const highIntensity = (byte & 2) !== 0;
1160
+ let color = "green";
1161
+ switch (colorGroup) {
1162
+ case 0:
1163
+ color = highIntensity ? "white" : "green";
1164
+ break;
1165
+ case 8:
1166
+ color = "red";
1167
+ break;
1168
+ // red stays red at HI
1169
+ case 16:
1170
+ color = highIntensity ? "yellow" : "turquoise";
1171
+ break;
1172
+ case 24:
1173
+ color = highIntensity ? "blue" : "pink";
1174
+ break;
1175
+ }
1176
+ const nonDisplay = type === 7;
1177
+ const underscore = type === 4 || type === 5 || type === 6;
1178
+ const reverse = type === 5;
1179
+ const columnSeparator = type === 1 || type === 3;
1180
+ const hiTypeBit = type === 2 || type === 3 || type === 6;
1181
+ return {
1182
+ color,
1183
+ highIntensity: highIntensity || hiTypeBit,
1184
+ underscore,
1185
+ reverse,
1186
+ blink: false,
1187
+ // 5250 has no base-attr blink; blink comes from WEA highlight
1188
+ nonDisplay,
1189
+ columnSeparator
1190
+ };
1191
+ }
1192
+ function decodeExtColor(byte) {
1193
+ switch (byte & 15) {
1194
+ case 0:
1195
+ return "green";
1196
+ case 1:
1197
+ return "blue";
1198
+ case 2:
1199
+ return "red";
1200
+ case 3:
1201
+ return "pink";
1202
+ case 4:
1203
+ return "turquoise";
1204
+ case 5:
1205
+ return "yellow";
1206
+ case 6:
1207
+ return "white";
1208
+ case 7:
1209
+ return void 0;
1210
+ // default — inherit
1211
+ // Reverse-image pairs: distinct tokens so themes can override
1212
+ case 8:
1213
+ return "green";
1214
+ // reverse green → typically same color, background flip
1215
+ case 9:
1216
+ return "blue";
1217
+ case 10:
1218
+ return "red";
1219
+ case 11:
1220
+ return "pink";
1221
+ case 12:
1222
+ return "turquoise";
1223
+ case 13:
1224
+ return "yellow";
1225
+ case 14:
1226
+ return "white";
1227
+ case 15:
1228
+ return void 0;
1229
+ default:
1230
+ return void 0;
1231
+ }
1232
+ }
1233
+ function extColorIsReverse(byte) {
1234
+ const v = byte & 15;
1235
+ return v >= 8 && v <= 14;
1236
+ }
1237
+ function decodeExtHighlight(byte) {
1238
+ return {
1239
+ underscore: (byte & 1) !== 0,
1240
+ reverse: (byte & 2) !== 0,
1241
+ blink: (byte & 4) !== 0,
1242
+ columnSeparator: (byte & 8) !== 0
1243
+ };
1244
+ }
1245
+ function cssVarForColor(color) {
1246
+ if (!color) return "var(--gs-green, #10b981)";
1247
+ return `var(--gs-${color}, var(--gs-green, #10b981))`;
1248
+ }
1249
+
1250
+ // src/utils/validation.ts
1251
+ function filterFieldChar(field, ch, isLastPosition = false) {
1252
+ let out = ch;
1253
+ if (field.monocase) {
1254
+ out = out.toUpperCase();
1255
+ }
1256
+ const cp = out.charCodeAt(0);
1257
+ const isAsciiLetter = cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122;
1258
+ const isAsciiDigit = cp >= 48 && cp <= 57;
1259
+ switch (field.shift_type) {
1260
+ case "alpha_only": {
1261
+ if (isAsciiLetter || out === "," || out === "-" || out === "." || out === " ") return out;
1262
+ return null;
1263
+ }
1264
+ case "numeric_only": {
1265
+ if (isAsciiDigit || out === "-" || out === "+" || out === "," || out === "." || out === " ") return out;
1266
+ return null;
1267
+ }
1268
+ case "digits_only": {
1269
+ if (isAsciiDigit) return out;
1270
+ return null;
1271
+ }
1272
+ case "signed_num": {
1273
+ if (isAsciiDigit) return out;
1274
+ if (isLastPosition && (out === "-" || out === "+")) return out;
1275
+ return null;
1276
+ }
1277
+ case "katakana": {
1278
+ if (cp >= 65377 && cp <= 65439 || isAsciiDigit || out === " ") return out;
1279
+ return null;
1280
+ }
1281
+ case "alpha":
1282
+ case "numeric_shift":
1283
+ case "io":
1284
+ case void 0:
1285
+ default:
1286
+ return out;
1287
+ }
1288
+ }
1289
+ function filterFieldInput(field, input, startOffset) {
1290
+ let out = "";
1291
+ let rejected = false;
1292
+ for (let i = 0; i < input.length; i++) {
1293
+ const pos = startOffset + i;
1294
+ const isLast = pos === field.length - 1;
1295
+ const transformed = filterFieldChar(field, input[i], isLast);
1296
+ if (transformed === null) {
1297
+ rejected = true;
1298
+ } else {
1299
+ out += transformed;
1300
+ }
1301
+ }
1302
+ return { out, rejected };
1303
+ }
1304
+ function validateMod10(value) {
1305
+ const digits = value.replace(/\D/g, "");
1306
+ if (digits.length < 2) return true;
1307
+ let sum = 0;
1308
+ let alt = false;
1309
+ for (let i = digits.length - 1; i >= 0; i--) {
1310
+ let d = digits.charCodeAt(i) - 48;
1311
+ if (alt) {
1312
+ d *= 2;
1313
+ if (d > 9) d -= 9;
1314
+ }
1315
+ sum += d;
1316
+ alt = !alt;
1317
+ }
1318
+ return sum % 10 === 0;
1319
+ }
1320
+ function validateMod11(value) {
1321
+ const digits = value.replace(/\D/g, "");
1322
+ if (digits.length < 2) return true;
1323
+ const body = digits.slice(0, -1);
1324
+ const check = digits.charCodeAt(digits.length - 1) - 48;
1325
+ let sum = 0;
1326
+ let weight = 2;
1327
+ for (let i = body.length - 1; i >= 0; i--) {
1328
+ sum += (body.charCodeAt(i) - 48) * weight;
1329
+ weight++;
1330
+ if (weight > 7) weight = 2;
1331
+ }
1332
+ const remainder = sum % 11;
1333
+ const expected = remainder === 0 ? 0 : 11 - remainder;
1334
+ return expected === check;
1335
+ }
1336
+
1088
1337
  // src/components/GreenScreenTerminal.tsx
1089
1338
  var import_jsx_runtime4 = require("react/jsx-runtime");
1339
+ function aidByteToKeyName(aid) {
1340
+ if (aid === 241) return "ENTER";
1341
+ if (aid === 243) return "HELP";
1342
+ if (aid === 244) return "PAGEUP";
1343
+ if (aid === 245) return "PAGEDOWN";
1344
+ if (aid === 246) return "PRINT";
1345
+ if (aid === 189) return "CLEAR";
1346
+ if (aid >= 49 && aid <= 60) return `F${aid - 48}`;
1347
+ if (aid >= 177 && aid <= 188) return `F${aid - 176 + 12}`;
1348
+ return null;
1349
+ }
1090
1350
  var noopResult = { success: false, error: "No adapter configured" };
1091
1351
  var noopAdapter = {
1092
1352
  getScreen: async () => null,
@@ -1117,14 +1377,16 @@ function GreenScreenTerminal({
1117
1377
  inlineSignIn = true,
1118
1378
  defaultProtocol: signInDefaultProtocol,
1119
1379
  onSignIn,
1120
- autoSignedIn,
1121
1380
  autoFocusDisabled = false,
1122
1381
  bootLoader,
1382
+ bootLoaderReady,
1123
1383
  headerRight,
1384
+ statusActions,
1124
1385
  overlay,
1125
1386
  onNotification,
1126
1387
  onScreenChange,
1127
1388
  onMinimize,
1389
+ showShortcutsButton = true,
1128
1390
  className,
1129
1391
  style
1130
1392
  }) {
@@ -1169,22 +1431,17 @@ function GreenScreenTerminal({
1169
1431
  const sendText = (0, import_react5.useCallback)(async (text) => _sendText(text), [_sendText]);
1170
1432
  const sendKey = (0, import_react5.useCallback)(async (key) => _sendKey(key), [_sendKey]);
1171
1433
  const [optimisticEdits, setOptimisticEdits] = (0, import_react5.useState)([]);
1172
- const prevScreenSigForEdits = (0, import_react5.useRef)(void 0);
1434
+ const prevScreenContentForEdits = (0, import_react5.useRef)(void 0);
1173
1435
  (0, import_react5.useEffect)(() => {
1174
- if (rawScreenData?.screen_signature && rawScreenData.screen_signature !== prevScreenSigForEdits.current) {
1175
- prevScreenSigForEdits.current = rawScreenData.screen_signature;
1436
+ const content = rawScreenData?.content;
1437
+ if (content && content !== prevScreenContentForEdits.current) {
1438
+ prevScreenContentForEdits.current = content;
1176
1439
  setOptimisticEdits([]);
1177
1440
  }
1178
- }, [rawScreenData?.screen_signature]);
1441
+ }, [rawScreenData?.content]);
1179
1442
  const [inputText, setInputText] = (0, import_react5.useState)("");
1180
1443
  const [isFocused, setIsFocused] = (0, import_react5.useState)(false);
1181
- const [showSignInHint, setShowSignInHint] = (0, import_react5.useState)(false);
1182
1444
  const [showShortcuts, setShowShortcuts] = (0, import_react5.useState)(false);
1183
- const prevAutoSignedIn = (0, import_react5.useRef)(false);
1184
- (0, import_react5.useEffect)(() => {
1185
- if (autoSignedIn && !prevAutoSignedIn.current) setShowSignInHint(true);
1186
- prevAutoSignedIn.current = !!autoSignedIn;
1187
- }, [autoSignedIn]);
1188
1445
  const terminalRef = (0, import_react5.useRef)(null);
1189
1446
  const inputRef = (0, import_react5.useRef)(null);
1190
1447
  const [syncedCursor, setSyncedCursor] = (0, import_react5.useState)(null);
@@ -1194,7 +1451,6 @@ function GreenScreenTerminal({
1194
1451
  if (prevRawContentRef.current && newContent && newContent !== prevRawContentRef.current) {
1195
1452
  setSyncedCursor(null);
1196
1453
  setInputText("");
1197
- if (showSignInHint) setShowSignInHint(false);
1198
1454
  }
1199
1455
  prevRawContentRef.current = newContent;
1200
1456
  }, [rawScreenData?.content]);
@@ -1257,11 +1513,9 @@ function GreenScreenTerminal({
1257
1513
  const newAdapter = new RestAdapter({ baseUrl: `http://${config.host}${port}` });
1258
1514
  setInternalAdapter(newAdapter);
1259
1515
  await newAdapter.connect(config);
1260
- if (config.username && config.password) setShowSignInHint(true);
1261
1516
  return;
1262
1517
  }
1263
1518
  await connect(config);
1264
- if (config.username && config.password) setShowSignInHint(true);
1265
1519
  } catch (err) {
1266
1520
  setSignInError(err instanceof Error ? err.message : String(err));
1267
1521
  setConnecting(false);
@@ -1273,13 +1527,15 @@ function GreenScreenTerminal({
1273
1527
  const [showBootLoader, setShowBootLoader] = (0, import_react5.useState)(bootLoader !== false);
1274
1528
  const [bootFadingOut, setBootFadingOut] = (0, import_react5.useState)(false);
1275
1529
  (0, import_react5.useEffect)(() => {
1276
- if (screenData?.content && showBootLoader) {
1530
+ if (!showBootLoader) return;
1531
+ const shouldDismiss = bootLoaderReady !== void 0 ? bootLoaderReady : !!screenData?.content;
1532
+ if (shouldDismiss) {
1277
1533
  setBootFadingOut(true);
1278
1534
  setShowBootLoader(false);
1279
1535
  const timer = setTimeout(() => setBootFadingOut(false), 400);
1280
1536
  return () => clearTimeout(timer);
1281
1537
  }
1282
- }, [screenData?.content, showBootLoader]);
1538
+ }, [screenData?.content, showBootLoader, bootLoaderReady]);
1283
1539
  const FOCUS_STORAGE_KEY = "gs-terminal-focused";
1284
1540
  (0, import_react5.useEffect)(() => {
1285
1541
  if (!autoFocusDisabled && !readOnly) {
@@ -1347,13 +1603,25 @@ function GreenScreenTerminal({
1347
1603
  const clickedRow = Math.floor(y / ROW_HEIGHT);
1348
1604
  const clickedCol = Math.floor(x / charWidth);
1349
1605
  if (clickedRow < 0 || clickedRow >= (screenData.rows || 24) || clickedCol < 0 || clickedCol >= (screenData.cols || 80)) return;
1606
+ const clickedField = screenData.fields.find(
1607
+ (f) => f.row === clickedRow && clickedCol >= f.col && clickedCol < f.col + f.length
1608
+ );
1609
+ const ptrAid = clickedField && clickedField.pointer_aid;
1610
+ if (ptrAid) {
1611
+ const keyName = aidByteToKeyName(ptrAid);
1612
+ if (keyName) {
1613
+ setSyncedCursor({ row: clickedRow, col: clickedCol });
1614
+ adapter.setCursor?.(clickedRow, clickedCol).then(() => sendKey(keyName));
1615
+ return;
1616
+ }
1617
+ }
1350
1618
  setSyncedCursor({ row: clickedRow, col: clickedCol });
1351
1619
  adapter.setCursor?.(clickedRow, clickedCol).then((r) => {
1352
1620
  if (r?.cursor_row !== void 0) {
1353
1621
  setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1354
1622
  }
1355
1623
  });
1356
- }, [readOnly, screenData, adapter]);
1624
+ }, [readOnly, screenData, adapter, sendKey]);
1357
1625
  const getCurrentField = (0, import_react5.useCallback)(() => {
1358
1626
  const fields = screenData?.fields || [];
1359
1627
  const cursorRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
@@ -1363,6 +1631,31 @@ function GreenScreenTerminal({
1363
1631
  }
1364
1632
  return null;
1365
1633
  }, [screenData, syncedCursor]);
1634
+ const readFieldValue = (0, import_react5.useCallback)((field) => {
1635
+ if (!screenData?.content) return "";
1636
+ const lines = screenData.content.split("\n");
1637
+ const line = lines[field.row] || "";
1638
+ return line.substring(field.col, field.col + field.length).replace(/\s+$/, "");
1639
+ }, [screenData?.content]);
1640
+ const [validationError, setValidationError] = (0, import_react5.useState)(null);
1641
+ const runSelfCheck = (0, import_react5.useCallback)(() => {
1642
+ const fields = screenData?.fields || [];
1643
+ for (const f of fields) {
1644
+ if (!f.is_input) continue;
1645
+ const val = readFieldValue(f);
1646
+ if (!val) continue;
1647
+ if (f.self_check_mod10 && !validateMod10(val)) {
1648
+ setValidationError(`Invalid check digit (MOD10) in field at row ${f.row + 1}, col ${f.col + 1}`);
1649
+ return false;
1650
+ }
1651
+ if (f.self_check_mod11 && !validateMod11(val)) {
1652
+ setValidationError(`Invalid check digit (MOD11) in field at row ${f.row + 1}, col ${f.col + 1}`);
1653
+ return false;
1654
+ }
1655
+ }
1656
+ setValidationError(null);
1657
+ return true;
1658
+ }, [screenData?.fields, readFieldValue]);
1366
1659
  const handleKeyDown = async (e) => {
1367
1660
  if (readOnly) {
1368
1661
  e.preventDefault();
@@ -1401,18 +1694,19 @@ function GreenScreenTerminal({
1401
1694
  End: "END",
1402
1695
  Insert: "INSERT"
1403
1696
  };
1404
- if (e.key.startsWith("F") && e.key.length <= 3) {
1697
+ if (/^F([1-9]|1[0-9]|2[0-4])$/.test(e.key)) {
1405
1698
  e.preventDefault();
1406
- const fKey = e.key.toUpperCase();
1407
- if (/^F([1-9]|1[0-9]|2[0-4])$/.test(fKey)) {
1408
- const kr = await sendKey(fKey);
1409
- if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1410
- return;
1411
- }
1699
+ if (!runSelfCheck()) return;
1700
+ const kr = await sendKey(e.key);
1701
+ if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1702
+ return;
1412
1703
  }
1413
1704
  if (keyMap[e.key]) {
1414
1705
  e.preventDefault();
1415
- const kr = await sendKey(keyMap[e.key]);
1706
+ const k = keyMap[e.key];
1707
+ const isSubmit = k === "ENTER" || k === "PAGEUP" || k === "PAGEDOWN";
1708
+ if (isSubmit && !runSelfCheck()) return;
1709
+ const kr = await sendKey(k);
1416
1710
  if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
1417
1711
  }
1418
1712
  };
@@ -1423,18 +1717,32 @@ function GreenScreenTerminal({
1423
1717
  }
1424
1718
  const newText = e.target.value;
1425
1719
  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] });
1720
+ let newChars = newText.substring(inputText.length);
1721
+ const curField = getCurrentField();
1722
+ if (curField && (curField.shift_type || curField.monocase)) {
1723
+ const curColAbs = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1724
+ const startOffset = Math.max(0, curColAbs - curField.col);
1725
+ const { out, rejected } = filterFieldInput(curField, newChars, startOffset);
1726
+ if (rejected) {
1727
+ const what = curField.shift_type === "digits_only" || curField.shift_type === "numeric_only" || curField.shift_type === "signed_num" ? "digits only" : curField.shift_type === "alpha_only" ? "letters only" : curField.shift_type === "katakana" ? "katakana only" : "character not allowed";
1728
+ setValidationError(`Field accepts ${what}`);
1729
+ setTimeout(() => setValidationError(null), 1500);
1730
+ }
1731
+ newChars = out;
1732
+ }
1733
+ if (newChars.length > 0) {
1734
+ const curRow = syncedCursor?.row ?? screenData?.cursor_row ?? 0;
1735
+ const curCol = syncedCursor?.col ?? screenData?.cursor_col ?? 0;
1736
+ const edits = [];
1737
+ for (let i = 0; i < newChars.length; i++) {
1738
+ edits.push({ row: curRow, col: curCol + i, ch: newChars[i] });
1739
+ }
1740
+ setOptimisticEdits((prev) => [...prev, ...edits]);
1741
+ setSyncedCursor({ row: curRow, col: curCol + newChars.length });
1742
+ sendText(newChars).then((r) => {
1743
+ if (r.cursor_row !== void 0) setSyncedCursor({ row: r.cursor_row, col: r.cursor_col });
1744
+ });
1432
1745
  }
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
- });
1438
1746
  }
1439
1747
  setInputText("");
1440
1748
  e.target.value = "";
@@ -1466,41 +1774,156 @@ function GreenScreenTerminal({
1466
1774
  if (lastIndex < text.length) segments.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: text.substring(lastIndex) }, `${keyPrefix}-e`));
1467
1775
  return segments.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: segments }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: text });
1468
1776
  }, []);
1469
- const renderRowWithFields = (0, import_react5.useCallback)((line, rowIndex, fields) => {
1777
+ const renderRowWithFields = (0, import_react5.useCallback)((line, rowIndex, fields, cursorRow, cursorCol) => {
1470
1778
  const inputFields = fields.filter((f) => f.row === rowIndex && f.is_input);
1471
1779
  const highlightedFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_highlighted);
1472
1780
  const reverseFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_reverse);
1473
1781
  const colorFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.color && !f.is_highlighted && !f.is_reverse);
1474
1782
  const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields, ...colorFields];
1475
- if (allRowFields.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line });
1783
+ const cols = screenData?.cols || profile.defaultCols;
1784
+ const extAttrs = screenData?.ext_attrs;
1785
+ const dbcsCont = screenData?.dbcs_cont;
1786
+ const dbcsContSet = dbcsCont && dbcsCont.length > 0 ? new Set(dbcsCont) : null;
1787
+ const hasExtOnRow = !!extAttrs && (() => {
1788
+ for (let c = 0; c < cols; c++) {
1789
+ if (extAttrs[rowIndex * cols + c]) return true;
1790
+ }
1791
+ return false;
1792
+ })();
1793
+ const hasDbcsOnRow = !!dbcsContSet && (() => {
1794
+ for (let c = 0; c < cols; c++) {
1795
+ if (dbcsContSet.has(rowIndex * cols + c)) return true;
1796
+ }
1797
+ return false;
1798
+ })();
1799
+ if (allRowFields.length === 0 && !hasExtOnRow && !hasDbcsOnRow) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line });
1476
1800
  const sorted = [...allRowFields].sort((a, b) => a.col - b.col);
1477
1801
  const segs = [];
1478
1802
  let lastEnd = 0;
1479
- const cols = screenData?.cols || profile.defaultCols;
1803
+ const renderPlainRun = (runStart, runEnd, keyPrefix) => {
1804
+ if (runStart >= runEnd) return;
1805
+ if (!hasExtOnRow && !hasDbcsOnRow) {
1806
+ segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line.substring(runStart, runEnd) }, keyPrefix));
1807
+ return;
1808
+ }
1809
+ let pos = runStart;
1810
+ while (pos < runEnd) {
1811
+ const addr = rowIndex * cols + pos;
1812
+ const ext = extAttrs?.[addr];
1813
+ const isContCell = dbcsContSet?.has(addr);
1814
+ if (isContCell) {
1815
+ segs.push(
1816
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1817
+ "span",
1818
+ {
1819
+ style: { display: "inline-block", width: "1ch" },
1820
+ "aria-hidden": "true"
1821
+ },
1822
+ `${keyPrefix}-dc${pos}`
1823
+ )
1824
+ );
1825
+ pos++;
1826
+ continue;
1827
+ }
1828
+ if (ext) {
1829
+ const color = ext.color !== void 0 ? decodeExtColor(ext.color) : void 0;
1830
+ const colorReverse = ext.color !== void 0 && extColorIsReverse(ext.color);
1831
+ const hl = ext.highlight !== void 0 ? decodeExtHighlight(ext.highlight) : void 0;
1832
+ const style2 = {};
1833
+ if (color) style2.color = cssVarForColor(color);
1834
+ if (colorReverse) {
1835
+ style2.background = cssVarForColor(color);
1836
+ style2.color = "#000";
1837
+ }
1838
+ if (hl?.reverse) {
1839
+ style2.background = "currentColor";
1840
+ style2.color = "#000";
1841
+ }
1842
+ if (hl?.underscore) style2.textDecoration = "underline";
1843
+ if (hl?.blink) style2.animation = "gs-blink 1s steps(2, start) infinite";
1844
+ segs.push(
1845
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: style2, children: line[pos] }, `${keyPrefix}-x${pos}`)
1846
+ );
1847
+ pos++;
1848
+ continue;
1849
+ }
1850
+ let runEndPlain = pos + 1;
1851
+ while (runEndPlain < runEnd) {
1852
+ const a = rowIndex * cols + runEndPlain;
1853
+ if (extAttrs?.[a] || dbcsContSet?.has(a)) break;
1854
+ runEndPlain++;
1855
+ }
1856
+ segs.push(
1857
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line.substring(pos, runEndPlain) }, `${keyPrefix}-p${pos}`)
1858
+ );
1859
+ pos = runEndPlain;
1860
+ }
1861
+ };
1480
1862
  sorted.forEach((field, idx) => {
1481
1863
  const fs = field.col;
1482
1864
  const fe = Math.min(field.col + field.length, cols);
1483
- if (fs > lastEnd) segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line.substring(lastEnd, fs) }, `t${idx}`));
1865
+ if (fs > lastEnd) renderPlainRun(lastEnd, fs, `t${idx}`);
1484
1866
  const fc = line.substring(fs, fe);
1485
- const colorVar = field.color ? `var(--gs-${field.color}, var(--gs-green))` : void 0;
1867
+ const cursorInField = cursorRow === field.row && cursorCol >= fs && cursorCol < field.col + field.length;
1868
+ const entryAttr = cursorInField && field.highlight_entry_attr !== void 0 ? decodeAttrByte(field.highlight_entry_attr) : null;
1869
+ const baseColor = entryAttr?.color ?? field.color;
1870
+ const colorVar = baseColor ? cssVarForColor(baseColor) : void 0;
1871
+ const fieldStyle = {};
1872
+ if (colorVar) fieldStyle.color = colorVar;
1873
+ if (entryAttr?.reverse) {
1874
+ fieldStyle.background = "currentColor";
1875
+ fieldStyle.color = "#000";
1876
+ }
1877
+ if (entryAttr?.underscore) fieldStyle.textDecoration = "underline";
1878
+ if (entryAttr?.highIntensity) fieldStyle.fontWeight = "bold";
1879
+ const isPassword = field.is_non_display;
1880
+ const displayText = isPassword ? " ".repeat(fc.length) : fc;
1486
1881
  if (field.is_input) {
1487
1882
  const fieldWidth = Math.min(field.length, cols - fs);
1488
- const fieldClass = showSignInHint ? "gs-confirmed-field" : field.is_underscored ? "gs-input-field" : void 0;
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}`));
1883
+ const fieldClass = field.is_underscored ? "gs-input-field" : void 0;
1884
+ const extra = [];
1885
+ if (field.is_dbcs) extra.push("gs-dbcs-field");
1886
+ if (cursorInField) extra.push("gs-field-active");
1887
+ const composedClass = [fieldClass, ...extra].filter(Boolean).join(" ") || void 0;
1888
+ segs.push(
1889
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1890
+ "span",
1891
+ {
1892
+ className: composedClass,
1893
+ style: {
1894
+ display: "inline-block",
1895
+ width: `${fieldWidth}ch`,
1896
+ overflow: "hidden",
1897
+ ...fieldStyle
1898
+ },
1899
+ children: displayText
1900
+ },
1901
+ `f${idx}`
1902
+ )
1903
+ );
1490
1904
  } else if (field.is_reverse) {
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}`));
1905
+ segs.push(
1906
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1907
+ "span",
1908
+ {
1909
+ style: { color: colorVar || "var(--gs-red, #FF5555)", fontWeight: "bold", ...fieldStyle },
1910
+ children: displayText
1911
+ },
1912
+ `v${idx}`
1913
+ )
1914
+ );
1915
+ } else if (colorVar || entryAttr) {
1916
+ segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: fieldStyle, children: displayText }, `h${idx}`));
1494
1917
  } else if (field.is_highlighted) {
1495
- segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: fc }, `h${idx}`));
1918
+ segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: displayText }, `h${idx}`));
1496
1919
  } else {
1497
- segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: fc }, `h${idx}`));
1920
+ segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: displayText }, `h${idx}`));
1498
1921
  }
1499
1922
  lastEnd = fe;
1500
1923
  });
1501
- if (lastEnd < line.length) segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line.substring(lastEnd) }, "te"));
1924
+ if (lastEnd < line.length) renderPlainRun(lastEnd, line.length, "te");
1502
1925
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: segs });
1503
- }, [renderTextWithUnderlines, screenData?.cols, profile.defaultCols, showSignInHint]);
1926
+ }, [renderTextWithUnderlines, screenData, profile.defaultCols]);
1504
1927
  const renderScreen = () => {
1505
1928
  if (showBootLoader && !screenData?.content) {
1506
1929
  if (bootLoader === false) return null;
@@ -1514,10 +1937,10 @@ function GreenScreenTerminal({
1514
1937
  }
1515
1938
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: `${screenData?.cols || profile.defaultCols}ch`, height: `${(screenData?.rows || profile.defaultRows) * 21}px`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { textAlign: "center" }, children: [
1516
1939
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { color: "#808080", marginBottom: "12px" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TerminalIcon, { size: 40 }) }),
1517
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : "Not connected" }),
1940
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: connStatus?.status === "connecting" ? "#f59e0b" : connStatus?.status === "loading" ? "#94a3b8" : "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : connStatus?.status === "connecting" ? "Connecting..." : connStatus?.status === "loading" ? "Loading..." : "Not connected" }),
1518
1941
  !connStatus?.connected && isUsingDefaultAdapter && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: { fontFamily: "var(--gs-font)", fontSize: "11px", color: "#606060", marginTop: "8px" }, children: [
1519
1942
  "Start the proxy: ",
1520
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy --mock" })
1943
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy" })
1521
1944
  ] })
1522
1945
  ] }) });
1523
1946
  }
@@ -1535,7 +1958,7 @@ function GreenScreenTerminal({
1535
1958
  const cursor = getCursorPos();
1536
1959
  const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
1537
1960
  const cursorInInputField = hasCursor && fields.some(
1538
- (f) => f.is_input && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
1961
+ (f) => f.is_input && !f.is_non_display && f.row === cursor.row && cursor.col >= f.col && cursor.col < f.col + f.length
1539
1962
  );
1540
1963
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
1541
1964
  rows.map((line, index) => {
@@ -1550,11 +1973,33 @@ function GreenScreenTerminal({
1550
1973
  }
1551
1974
  const headerSegments = index === 0 ? profile.colors.parseHeaderRow(displayLine) : null;
1552
1975
  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: [
1553
- headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields),
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" } })
1976
+ headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields, cursor.row, cursor.col),
1977
+ cursorInInputField && 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" } })
1555
1978
  ] }, index);
1556
1979
  }),
1557
- showSignInHint && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-signin-hint", children: "Signed in \u2014 press Enter to continue" }),
1980
+ validationError && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1981
+ "div",
1982
+ {
1983
+ role: "alert",
1984
+ style: {
1985
+ position: "absolute",
1986
+ left: 0,
1987
+ right: 0,
1988
+ bottom: 0,
1989
+ padding: "2px 6px",
1990
+ background: "var(--gs-red, #FF5555)",
1991
+ color: "#000",
1992
+ fontFamily: "var(--gs-font)",
1993
+ fontSize: "12px",
1994
+ zIndex: 10
1995
+ },
1996
+ onClick: () => setValidationError(null),
1997
+ children: [
1998
+ validationError,
1999
+ " \u2014 click to dismiss"
2000
+ ]
2001
+ }
2002
+ ),
1558
2003
  screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { position: "absolute", bottom: 0, right: 0, fontFamily: "var(--gs-font)", fontSize: "10px", color: "var(--gs-green, #10b981)", pointerEvents: "none", opacity: 0.6 }, children: [
1559
2004
  String(screenData.cursor_row + 1).padStart(2, "0"),
1560
2005
  "/",
@@ -1585,6 +2030,15 @@ function GreenScreenTerminal({
1585
2030
  return "#64748b";
1586
2031
  }
1587
2032
  };
2033
+ const handleShortcutSend = (0, import_react5.useCallback)(async (key) => {
2034
+ if (readOnly) return;
2035
+ const isSubmit = key === "ENTER" || key === "PAGEUP" || key === "PAGEDOWN" || /^F([1-9]|1[0-9]|2[0-4])$/.test(key);
2036
+ if (isSubmit && !runSelfCheck()) return;
2037
+ setIsFocused(true);
2038
+ inputRef.current?.focus();
2039
+ const kr = await sendKey(key);
2040
+ if (kr.cursor_row !== void 0) setSyncedCursor({ row: kr.cursor_row, col: kr.cursor_col });
2041
+ }, [readOnly, runSelfCheck, sendKey]);
1588
2042
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `gs-terminal ${isFocused ? "gs-terminal-focused" : ""} ${className || ""}`, style, children: [
1589
2043
  showHeader && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-header", children: embedded ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1590
2044
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "gs-header-left", children: [
@@ -1597,16 +2051,17 @@ function GreenScreenTerminal({
1597
2051
  screenData?.insert_mode && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-ins", children: "INS" })
1598
2052
  ] }),
1599
2053
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-header-right", children: [
1600
- connStatus?.status && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
1601
- connStatus && (connStatus.connected ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WifiIcon, { size: 12, style: { color: "var(--gs-green, #10b981)" } }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WifiOffIcon, { size: 12, style: { color: "#FF6B00" } })),
2054
+ connStatus?.status && connStatus.status !== "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
2055
+ connStatus && connStatus.status !== "loading" && (connStatus.connected ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WifiIcon, { size: 12, style: { color: "var(--gs-green, #10b981)" } }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WifiOffIcon, { size: 12, style: { color: "#FF6B00" } })),
2056
+ statusActions,
1602
2057
  onMinimize && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: (e) => {
1603
2058
  e.stopPropagation();
1604
2059
  onMinimize();
1605
2060
  }, 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) => {
2061
+ showShortcutsButton && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: (e) => {
1607
2062
  e.stopPropagation();
1608
2063
  setShowShortcuts((s) => !s);
1609
- }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(HelpIcon, { size: 12 }) }),
2064
+ }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyboardIcon, { size: 12 }) }),
1610
2065
  headerRight
1611
2066
  ] })
1612
2067
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
@@ -1620,7 +2075,7 @@ function GreenScreenTerminal({
1620
2075
  screenData?.insert_mode && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-badge-ins", children: "INS" })
1621
2076
  ] }),
1622
2077
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-header-right", children: [
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: [
2078
+ connStatus && connStatus.status !== "loading" && /* @__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: [
1624
2079
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WifiIcon, { size: 12, style: { color: "var(--gs-green, #10b981)" } }),
1625
2080
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-host", children: connStatus.host })
1626
2081
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
@@ -1632,10 +2087,11 @@ function GreenScreenTerminal({
1632
2087
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyIcon, { size: 12, style: { color: getStatusColor(connStatus.status) } }),
1633
2088
  connStatus.username && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-host", children: connStatus.username })
1634
2089
  ] }),
1635
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: (e) => {
2090
+ statusActions,
2091
+ showShortcutsButton && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: (e) => {
1636
2092
  e.stopPropagation();
1637
2093
  setShowShortcuts((s) => !s);
1638
- }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(HelpIcon, { size: 12 }) }),
2094
+ }, className: "gs-btn-icon", title: "Keyboard shortcuts", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyboardIcon, { size: 12 }) }),
1639
2095
  headerRight
1640
2096
  ] })
1641
2097
  ] }) }),
@@ -1656,29 +2112,47 @@ function GreenScreenTerminal({
1656
2112
  showShortcuts && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-shortcuts-panel", children: [
1657
2113
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-shortcuts-header", children: [
1658
2114
  /* @__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" })
2115
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "gs-btn-icon", onClick: () => setShowShortcuts(false), children: "\xD7" })
1660
2116
  ] }),
2117
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-shortcuts-section-title", children: "Actions" }),
2118
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("table", { className: "gs-shortcuts-table", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: [
2119
+ ["Enter", "Submit", "ENTER"],
2120
+ ["Tab", "Next field", "TAB"],
2121
+ ["Backspace", "Backspace", "BACKSPACE"],
2122
+ ["Delete", "Delete", "DELETE"],
2123
+ ["Insert", "Insert / Overwrite", "INSERT"],
2124
+ ["Home", "Home", "HOME"],
2125
+ ["End", "End", "END"],
2126
+ ["Page Up", "Roll Down", "PAGEUP"],
2127
+ ["Page Down", "Roll Up", "PAGEDOWN"],
2128
+ ["Ctrl+Enter", "Field Exit", "FIELD_EXIT"],
2129
+ ["Ctrl+R", "Reset", "RESET"],
2130
+ ["\u2014", "Help", "HELP"],
2131
+ ["\u2014", "Clear", "CLEAR"],
2132
+ ["\u2014", "Print", "PRINT"]
2133
+ ].map(([label, desc, key]) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { className: "gs-shortcut-row", onClick: (e) => {
2134
+ e.stopPropagation();
2135
+ handleShortcutSend(key);
2136
+ }, children: [
2137
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: label }),
2138
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: desc })
2139
+ ] }, key)) }) }),
2140
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-shortcuts-section-title", children: "Function keys" }),
2141
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-shortcuts-fkeys", children: Array.from({ length: 24 }, (_, i) => `F${i + 1}`).map((fk) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2142
+ "button",
2143
+ {
2144
+ className: "gs-shortcut-fkey",
2145
+ onClick: (e) => {
2146
+ e.stopPropagation();
2147
+ handleShortcutSend(fk);
2148
+ },
2149
+ title: `Send ${fk}`,
2150
+ children: fk
2151
+ },
2152
+ fk
2153
+ )) }),
2154
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-shortcuts-section-title", children: "Info" }),
1661
2155
  /* @__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
2156
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1683
2157
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { className: "gs-shortcut-key", children: "Click" }),
1684
2158
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: "Focus / Position cursor" })
@@ -1691,7 +2165,8 @@ function GreenScreenTerminal({
1691
2165
  ] }),
1692
2166
  connStatus && !connStatus.connected && screenData && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gs-overlay", children: [
1693
2167
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WifiOffIcon, { size: 28 }),
1694
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : "Disconnected" })
2168
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: isAutoReconnecting || reconnecting ? "Reconnecting..." : connStatus?.status === "connecting" ? "Connecting..." : "Disconnected" }),
2169
+ connStatus.error && !isAutoReconnecting && !reconnecting && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: "0.75em", opacity: 0.7, maxWidth: "80%", textAlign: "center", wordBreak: "break-word" }, children: connStatus.error })
1695
2170
  ] }),
1696
2171
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1697
2172
  "input",
@@ -1705,7 +2180,19 @@ function GreenScreenTerminal({
1705
2180
  autoComplete: "off",
1706
2181
  autoCorrect: "off",
1707
2182
  autoCapitalize: "off",
1708
- spellCheck: false
2183
+ spellCheck: false,
2184
+ lang: (() => {
2185
+ const f = getCurrentField();
2186
+ if (!f) return void 0;
2187
+ if (f.is_dbcs || f.is_dbcs_either) return "ja";
2188
+ return void 0;
2189
+ })(),
2190
+ inputMode: (() => {
2191
+ const f = getCurrentField();
2192
+ if (!f) return void 0;
2193
+ if (f.is_dbcs) return "text";
2194
+ return "text";
2195
+ })()
1709
2196
  }
1710
2197
  )
1711
2198
  ]