mcp-for-i 0.1.13 → 0.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/README.md +17 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +14 -9
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +122 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +113 -100
- package/dist/controlplane/ui.js +1742 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +310 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +74 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +827 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +43 -22
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +515 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1700 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +50 -7
- package/docs/configuration.md +189 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +363 -188
- package/package.json +56 -53
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import { AID, AID_WITHOUT_DATA, CMD_CLEAR_FORMAT_TABLE, CMD_CLEAR_UNIT, CMD_CLEAR_UNIT_ALTERNATE, CMD_READ_IMMEDIATE, CMD_READ_INPUT_FIELDS, CMD_READ_MDT_ALTERNATE, CMD_READ_MDT_FIELDS, CMD_READ_MDT_IMMEDIATE_ALTERNATE, CMD_READ_SCREEN_IMMEDIATE, CMD_RESTORE_PARTIAL_SCREEN, CMD_RESTORE_SCREEN, CMD_ROLL, CMD_SAVE_PARTIAL_SCREEN, CMD_SAVE_SCREEN, CMD_WRITE_ERROR_CODE, CMD_WRITE_ERROR_CODE_TO_WINDOW, CMD_WRITE_STRUCTURED_FIELD, CMD_WRITE_TO_DISPLAY, ESC, OP_CANCEL_INVITE, OP_MESSAGE_LIGHT_OFF, OP_MESSAGE_LIGHT_ON, OP_NO_OP, OP_PUT_GET, OP_READ_IMMEDIATE, OP_READ_SCREEN, OP_SAVE_SCREEN, ORD_EA, ORD_IC, ORD_MC, ORD_RA, ORD_SBA, ORD_SF, ORD_SOH, ORD_TD, ORD_WDSF, ORD_WEA, buildGdsRecord } from "./datastream.js";
|
|
2
|
+
import { ebcdicToChar, stringToEbcdic } from "./ebcdic.js";
|
|
3
|
+
/** An input field from the 5250 format table. Positions are 0-based buffer addresses. */
|
|
4
|
+
export class Tn5250Field {
|
|
5
|
+
start;
|
|
6
|
+
length;
|
|
7
|
+
ffw;
|
|
8
|
+
fcw;
|
|
9
|
+
attr;
|
|
10
|
+
modified;
|
|
11
|
+
constructor(
|
|
12
|
+
/** Address of the first data position (the attribute byte sits just before it). */
|
|
13
|
+
start, length,
|
|
14
|
+
/** Field format word: first byte in the high 8 bits. */
|
|
15
|
+
ffw, fcw, attr) {
|
|
16
|
+
this.start = start;
|
|
17
|
+
this.length = length;
|
|
18
|
+
this.ffw = ffw;
|
|
19
|
+
this.fcw = fcw;
|
|
20
|
+
this.attr = attr;
|
|
21
|
+
this.modified = (ffw & 0x0800) !== 0;
|
|
22
|
+
}
|
|
23
|
+
/** Bypass (protected) input field: the operator cannot type into it. */
|
|
24
|
+
get bypass() { return (this.ffw & 0x2000) !== 0; }
|
|
25
|
+
/** Non-display attribute (password-style field). */
|
|
26
|
+
get hidden() { return (this.attr & 0x07) === 0x07; }
|
|
27
|
+
get shift() { return (this.ffw >> 8) & 0x07; }
|
|
28
|
+
/** Numeric only, digits only or signed numeric. */
|
|
29
|
+
get numeric() { return this.shift === 3 || this.shift === 5 || this.shift === 7; }
|
|
30
|
+
get monocase() { return (this.ffw & 0x0020) !== 0; }
|
|
31
|
+
get autoEnter() { return (this.ffw & 0x0080) !== 0; }
|
|
32
|
+
get mandatoryEntry() { return (this.ffw & 0x0008) !== 0; }
|
|
33
|
+
get rightAdjust() {
|
|
34
|
+
const value = this.ffw & 0x07;
|
|
35
|
+
return value === 5 ? "zero" : value === 6 ? "blank" : undefined;
|
|
36
|
+
}
|
|
37
|
+
get end() { return this.start + this.length - 1; }
|
|
38
|
+
contains(address) {
|
|
39
|
+
return address >= this.start && address <= this.end;
|
|
40
|
+
}
|
|
41
|
+
clone() {
|
|
42
|
+
const copy = new Tn5250Field(this.start, this.length, this.ffw, [...this.fcw], this.attr);
|
|
43
|
+
copy.modified = this.modified;
|
|
44
|
+
return copy;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class DataStreamError extends Error {
|
|
48
|
+
}
|
|
49
|
+
const MAX_SAVED_SCREENS = 32;
|
|
50
|
+
const SAVE_TAG = "MCPS";
|
|
51
|
+
/**
|
|
52
|
+
* In-memory 5250 display: the character buffer, attribute positions, the
|
|
53
|
+
* format table (input fields), cursor and keyboard state. process() applies a
|
|
54
|
+
* host record and returns any records the terminal must send back.
|
|
55
|
+
*/
|
|
56
|
+
export class Screen {
|
|
57
|
+
rows = 24;
|
|
58
|
+
cols = 80;
|
|
59
|
+
buffer = new Uint8Array(24 * 80);
|
|
60
|
+
attrMap = new Uint8Array(24 * 80);
|
|
61
|
+
fields = [];
|
|
62
|
+
cursor = 0;
|
|
63
|
+
keyboardLocked = true;
|
|
64
|
+
inputInhibited = false;
|
|
65
|
+
messageWaiting = false;
|
|
66
|
+
pendingRead = null;
|
|
67
|
+
errorRow = 23;
|
|
68
|
+
version = 0;
|
|
69
|
+
warnings = [];
|
|
70
|
+
ccsid;
|
|
71
|
+
deviceType;
|
|
72
|
+
addr = 0;
|
|
73
|
+
savedErrorLine;
|
|
74
|
+
savedScreens = new Map();
|
|
75
|
+
nextSaveId = 1;
|
|
76
|
+
constructor(options = {}) {
|
|
77
|
+
this.ccsid = options.ccsid ?? 37;
|
|
78
|
+
this.deviceType = options.deviceType ?? "3179";
|
|
79
|
+
}
|
|
80
|
+
get size() { return this.rows * this.cols; }
|
|
81
|
+
// ---------------------------------------------------------------- host input
|
|
82
|
+
/** Apply one host record. Returns outbound GDS records (already headed). */
|
|
83
|
+
process(record) {
|
|
84
|
+
const responses = [];
|
|
85
|
+
switch (record.opcode) {
|
|
86
|
+
case OP_CANCEL_INVITE:
|
|
87
|
+
responses.push(buildGdsRecord(OP_CANCEL_INVITE));
|
|
88
|
+
break;
|
|
89
|
+
case OP_MESSAGE_LIGHT_ON:
|
|
90
|
+
this.messageWaiting = true;
|
|
91
|
+
break;
|
|
92
|
+
case OP_MESSAGE_LIGHT_OFF:
|
|
93
|
+
this.messageWaiting = false;
|
|
94
|
+
break;
|
|
95
|
+
case OP_SAVE_SCREEN:
|
|
96
|
+
if (record.data.length === 0)
|
|
97
|
+
responses.push(this.saveScreen());
|
|
98
|
+
break;
|
|
99
|
+
case OP_READ_SCREEN:
|
|
100
|
+
if (record.data.length === 0)
|
|
101
|
+
responses.push(this.readScreenResponse());
|
|
102
|
+
break;
|
|
103
|
+
case OP_READ_IMMEDIATE:
|
|
104
|
+
if (record.data.length === 0)
|
|
105
|
+
responses.push(this.readImmediateResponse());
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
this.processCommands(record.data, responses);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
this.warn(`5250 data stream error: ${err?.message || err}`);
|
|
113
|
+
}
|
|
114
|
+
this.version += 1;
|
|
115
|
+
return responses;
|
|
116
|
+
}
|
|
117
|
+
processCommands(data, responses) {
|
|
118
|
+
let i = 0;
|
|
119
|
+
while (i < data.length) {
|
|
120
|
+
if (data[i] !== ESC) {
|
|
121
|
+
i += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const command = data[i + 1];
|
|
125
|
+
i += 2;
|
|
126
|
+
switch (command) {
|
|
127
|
+
case CMD_CLEAR_UNIT:
|
|
128
|
+
this.clearUnit(24, 80);
|
|
129
|
+
break;
|
|
130
|
+
case CMD_CLEAR_UNIT_ALTERNATE: {
|
|
131
|
+
const param = data[i++];
|
|
132
|
+
if (param === 0x00)
|
|
133
|
+
this.clearUnit(27, 132);
|
|
134
|
+
else
|
|
135
|
+
this.clearUnit(this.rows, this.cols);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case CMD_CLEAR_FORMAT_TABLE:
|
|
139
|
+
this.fields = [];
|
|
140
|
+
this.keyboardLocked = true;
|
|
141
|
+
break;
|
|
142
|
+
case CMD_WRITE_TO_DISPLAY:
|
|
143
|
+
i = this.writeToDisplay(data, i);
|
|
144
|
+
break;
|
|
145
|
+
case CMD_WRITE_ERROR_CODE:
|
|
146
|
+
i = this.writeErrorCode(data, i);
|
|
147
|
+
break;
|
|
148
|
+
case CMD_WRITE_ERROR_CODE_TO_WINDOW:
|
|
149
|
+
i = this.writeErrorCode(data, i + 2);
|
|
150
|
+
break;
|
|
151
|
+
case CMD_READ_INPUT_FIELDS:
|
|
152
|
+
case CMD_READ_MDT_FIELDS:
|
|
153
|
+
case CMD_READ_MDT_ALTERNATE: {
|
|
154
|
+
const cc1 = data[i] ?? 0;
|
|
155
|
+
const cc2 = data[i + 1] ?? 0;
|
|
156
|
+
i += 2;
|
|
157
|
+
this.applyCC1(cc1);
|
|
158
|
+
this.pendingRead = command;
|
|
159
|
+
// A read waits for the operator, so the keyboard opens for input.
|
|
160
|
+
this.keyboardLocked = false;
|
|
161
|
+
this.applyCC2(cc2);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case CMD_READ_IMMEDIATE:
|
|
165
|
+
case CMD_READ_MDT_IMMEDIATE_ALTERNATE:
|
|
166
|
+
responses.push(this.readImmediateResponse());
|
|
167
|
+
break;
|
|
168
|
+
case CMD_READ_SCREEN_IMMEDIATE:
|
|
169
|
+
responses.push(this.readScreenResponse());
|
|
170
|
+
break;
|
|
171
|
+
case CMD_SAVE_SCREEN:
|
|
172
|
+
responses.push(this.saveScreen());
|
|
173
|
+
break;
|
|
174
|
+
case CMD_SAVE_PARTIAL_SCREEN:
|
|
175
|
+
// Parameters are not needed: the whole screen is saved.
|
|
176
|
+
while (i < data.length && data[i] !== ESC)
|
|
177
|
+
i += 1;
|
|
178
|
+
responses.push(this.saveScreen());
|
|
179
|
+
break;
|
|
180
|
+
case CMD_RESTORE_SCREEN:
|
|
181
|
+
case CMD_RESTORE_PARTIAL_SCREEN: {
|
|
182
|
+
const startPayload = i;
|
|
183
|
+
while (i < data.length && data[i] !== ESC)
|
|
184
|
+
i += 1;
|
|
185
|
+
this.restoreScreen(data.subarray(startPayload, i));
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case CMD_ROLL:
|
|
189
|
+
this.roll(data[i] ?? 0, data[i + 1] ?? 1, data[i + 2] ?? this.rows);
|
|
190
|
+
i += 3;
|
|
191
|
+
break;
|
|
192
|
+
case CMD_WRITE_STRUCTURED_FIELD: {
|
|
193
|
+
const length = data.length >= i + 2 ? data.readUInt16BE(i) : 0;
|
|
194
|
+
if (length < 2)
|
|
195
|
+
throw new DataStreamError("invalid structured field length");
|
|
196
|
+
const body = data.subarray(i + 2, i + length);
|
|
197
|
+
i += length;
|
|
198
|
+
if (body[0] === 0xd9 && body[1] === 0x70) {
|
|
199
|
+
responses.push(this.queryReply());
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.warn(`Unsupported 5250 structured field 0x${hex(body[0])}${hex(body[1])}`);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
throw new DataStreamError(`unsupported 5250 command 0x${hex(command)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
clearUnit(rows, cols) {
|
|
212
|
+
this.rows = rows;
|
|
213
|
+
this.cols = cols;
|
|
214
|
+
this.buffer = new Uint8Array(rows * cols);
|
|
215
|
+
this.attrMap = new Uint8Array(rows * cols);
|
|
216
|
+
this.fields = [];
|
|
217
|
+
this.cursor = 0;
|
|
218
|
+
this.addr = 0;
|
|
219
|
+
this.errorRow = rows - 1;
|
|
220
|
+
this.savedErrorLine = undefined;
|
|
221
|
+
this.inputInhibited = false;
|
|
222
|
+
this.keyboardLocked = true;
|
|
223
|
+
this.pendingRead = null;
|
|
224
|
+
}
|
|
225
|
+
applyCC1(cc1) {
|
|
226
|
+
const action = cc1 & 0xe0;
|
|
227
|
+
if (action === 0)
|
|
228
|
+
return;
|
|
229
|
+
this.keyboardLocked = true;
|
|
230
|
+
const nonBypass = this.fields.filter(f => !f.bypass);
|
|
231
|
+
switch (action) {
|
|
232
|
+
case 0x40:
|
|
233
|
+
nonBypass.forEach(f => { f.modified = false; });
|
|
234
|
+
break;
|
|
235
|
+
case 0x60:
|
|
236
|
+
this.fields.forEach(f => { f.modified = false; });
|
|
237
|
+
break;
|
|
238
|
+
case 0x80:
|
|
239
|
+
nonBypass.filter(f => f.modified).forEach(f => this.nullField(f));
|
|
240
|
+
break;
|
|
241
|
+
case 0xa0:
|
|
242
|
+
nonBypass.filter(f => f.modified).forEach(f => this.nullField(f));
|
|
243
|
+
nonBypass.forEach(f => { f.modified = false; });
|
|
244
|
+
break;
|
|
245
|
+
case 0xc0:
|
|
246
|
+
nonBypass.forEach(f => { this.nullField(f); f.modified = false; });
|
|
247
|
+
break;
|
|
248
|
+
case 0xe0:
|
|
249
|
+
nonBypass.forEach(f => this.nullField(f));
|
|
250
|
+
this.fields.forEach(f => { f.modified = false; });
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
applyCC2(cc2) {
|
|
255
|
+
if (cc2 & 0x08)
|
|
256
|
+
this.keyboardLocked = false;
|
|
257
|
+
if (cc2 & 0x02)
|
|
258
|
+
this.messageWaiting = false;
|
|
259
|
+
if (cc2 & 0x01)
|
|
260
|
+
this.messageWaiting = true;
|
|
261
|
+
}
|
|
262
|
+
writeToDisplay(data, i) {
|
|
263
|
+
const cc1 = data[i] ?? 0;
|
|
264
|
+
const cc2 = data[i + 1] ?? 0;
|
|
265
|
+
i += 2;
|
|
266
|
+
this.applyCC1(cc1);
|
|
267
|
+
const outcome = this.writeOrders(data, i, this.addr);
|
|
268
|
+
this.addr = outcome.addr;
|
|
269
|
+
if (!outcome.cursorSet && outcome.fieldsDefined) {
|
|
270
|
+
const first = this.fields.find(f => !f.bypass);
|
|
271
|
+
this.cursor = first ? first.start : 0;
|
|
272
|
+
}
|
|
273
|
+
this.applyCC2(cc2);
|
|
274
|
+
return outcome.next;
|
|
275
|
+
}
|
|
276
|
+
writeErrorCode(data, i) {
|
|
277
|
+
const rowStart = this.errorRow * this.cols;
|
|
278
|
+
if (!this.savedErrorLine) {
|
|
279
|
+
this.savedErrorLine = {
|
|
280
|
+
buffer: this.buffer.slice(rowStart, rowStart + this.cols),
|
|
281
|
+
attrMap: this.attrMap.slice(rowStart, rowStart + this.cols)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const keepCursor = this.cursor;
|
|
285
|
+
const outcome = this.writeOrders(data, i, rowStart);
|
|
286
|
+
if (!outcome.cursorSet)
|
|
287
|
+
this.cursor = keepCursor;
|
|
288
|
+
this.inputInhibited = true;
|
|
289
|
+
this.keyboardLocked = true;
|
|
290
|
+
return outcome.next;
|
|
291
|
+
}
|
|
292
|
+
/** Process orders and data until the next command (ESC) or the end. */
|
|
293
|
+
writeOrders(data, i, addr) {
|
|
294
|
+
let cursorSet = false;
|
|
295
|
+
let fieldsDefined = false;
|
|
296
|
+
const size = this.size;
|
|
297
|
+
const need = (count) => {
|
|
298
|
+
if (i + count > data.length)
|
|
299
|
+
throw new DataStreamError("truncated 5250 order");
|
|
300
|
+
};
|
|
301
|
+
while (i < data.length && data[i] !== ESC) {
|
|
302
|
+
const byte = data[i++];
|
|
303
|
+
switch (byte) {
|
|
304
|
+
case ORD_SOH: {
|
|
305
|
+
need(1);
|
|
306
|
+
const length = data[i++];
|
|
307
|
+
need(length);
|
|
308
|
+
const header = data.subarray(i, i + length);
|
|
309
|
+
i += length;
|
|
310
|
+
// The error row is the 4th header byte; SOH also resets the format table.
|
|
311
|
+
if (header.length >= 4 && header[3] >= 1 && header[3] <= this.rows)
|
|
312
|
+
this.errorRow = header[3] - 1;
|
|
313
|
+
this.fields = [];
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case ORD_RA: {
|
|
317
|
+
need(3);
|
|
318
|
+
const target = this.address(data[i], data[i + 1]);
|
|
319
|
+
const ch = data[i + 2];
|
|
320
|
+
i += 3;
|
|
321
|
+
if (target >= addr) {
|
|
322
|
+
for (let p = addr; p <= target; p += 1)
|
|
323
|
+
this.put(p, ch);
|
|
324
|
+
addr = (target + 1) % size;
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
case ORD_EA: {
|
|
329
|
+
need(3);
|
|
330
|
+
const target = this.address(data[i], data[i + 1]);
|
|
331
|
+
const length = data[i + 2];
|
|
332
|
+
i += 2 + Math.max(1, length);
|
|
333
|
+
if (target >= addr) {
|
|
334
|
+
for (let p = addr; p <= target; p += 1)
|
|
335
|
+
this.put(p, 0x00);
|
|
336
|
+
addr = (target + 1) % size;
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case ORD_TD: {
|
|
341
|
+
need(2);
|
|
342
|
+
const length = data.readUInt16BE(i);
|
|
343
|
+
i += 2;
|
|
344
|
+
need(length);
|
|
345
|
+
for (let n = 0; n < length; n += 1) {
|
|
346
|
+
this.buffer[addr] = data[i + n];
|
|
347
|
+
this.attrMap[addr] = 0;
|
|
348
|
+
addr = (addr + 1) % size;
|
|
349
|
+
}
|
|
350
|
+
i += length;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case ORD_SBA:
|
|
354
|
+
need(2);
|
|
355
|
+
addr = this.address(data[i], data[i + 1]);
|
|
356
|
+
i += 2;
|
|
357
|
+
break;
|
|
358
|
+
case ORD_WEA:
|
|
359
|
+
need(2);
|
|
360
|
+
i += 2;
|
|
361
|
+
break;
|
|
362
|
+
case ORD_IC:
|
|
363
|
+
case ORD_MC:
|
|
364
|
+
need(2);
|
|
365
|
+
this.cursor = this.address(data[i], data[i + 1]);
|
|
366
|
+
cursorSet = true;
|
|
367
|
+
i += 2;
|
|
368
|
+
break;
|
|
369
|
+
case ORD_WDSF: {
|
|
370
|
+
need(2);
|
|
371
|
+
const length = data.readUInt16BE(i);
|
|
372
|
+
if (length < 2)
|
|
373
|
+
throw new DataStreamError("invalid WDSF length");
|
|
374
|
+
i += length;
|
|
375
|
+
this.warn("Write To Display Structured Field (window or GUI construct) ignored");
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case ORD_SF: {
|
|
379
|
+
const parsed = this.startField(data, i, addr);
|
|
380
|
+
i = parsed.next;
|
|
381
|
+
addr = parsed.addr;
|
|
382
|
+
if (parsed.input)
|
|
383
|
+
fieldsDefined = true;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
default:
|
|
387
|
+
this.put(addr, byte);
|
|
388
|
+
addr = (addr + 1) % size;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { next: i, addr, cursorSet, fieldsDefined };
|
|
392
|
+
}
|
|
393
|
+
startField(data, i, addr) {
|
|
394
|
+
let ffw;
|
|
395
|
+
const fcw = [];
|
|
396
|
+
if (i < data.length && (data[i] & 0xc0) === 0x40) {
|
|
397
|
+
if (i + 2 > data.length)
|
|
398
|
+
throw new DataStreamError("truncated field format word");
|
|
399
|
+
ffw = (data[i] << 8) | data[i + 1];
|
|
400
|
+
i += 2;
|
|
401
|
+
while (i < data.length && (data[i] & 0xc0) === 0x80) {
|
|
402
|
+
if (i + 2 > data.length)
|
|
403
|
+
throw new DataStreamError("truncated field control word");
|
|
404
|
+
fcw.push((data[i] << 8) | data[i + 1]);
|
|
405
|
+
i += 2;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (i + 3 > data.length)
|
|
409
|
+
throw new DataStreamError("truncated start of field");
|
|
410
|
+
const attr = data[i];
|
|
411
|
+
const length = (data[i + 1] << 8) | data[i + 2];
|
|
412
|
+
i += 3;
|
|
413
|
+
this.buffer[addr] = attr;
|
|
414
|
+
this.attrMap[addr] = 1;
|
|
415
|
+
const start = (addr + 1) % this.size;
|
|
416
|
+
if (ffw !== undefined && length > 0) {
|
|
417
|
+
this.fields = this.fields.filter(f => f.start !== start);
|
|
418
|
+
this.fields.push(new Tn5250Field(start, Math.min(length, this.size), ffw, fcw, attr));
|
|
419
|
+
this.fields.sort((a, b) => a.start - b.start);
|
|
420
|
+
}
|
|
421
|
+
return { next: i, addr: start, input: ffw !== undefined };
|
|
422
|
+
}
|
|
423
|
+
roll(control, top, bottom) {
|
|
424
|
+
const lines = control & 0x1f;
|
|
425
|
+
const down = (control & 0x80) !== 0;
|
|
426
|
+
const first = Math.max(1, top) - 1;
|
|
427
|
+
const last = Math.min(this.rows, bottom) - 1;
|
|
428
|
+
if (lines === 0 || last < first)
|
|
429
|
+
return;
|
|
430
|
+
const rowBytes = (row) => [row * this.cols, (row + 1) * this.cols];
|
|
431
|
+
const copyRow = (from, to) => {
|
|
432
|
+
const [fs, fe] = rowBytes(from);
|
|
433
|
+
this.buffer.copyWithin(to * this.cols, fs, fe);
|
|
434
|
+
this.attrMap.copyWithin(to * this.cols, fs, fe);
|
|
435
|
+
};
|
|
436
|
+
const clearRow = (row) => {
|
|
437
|
+
const [s, e] = rowBytes(row);
|
|
438
|
+
this.buffer.fill(0, s, e);
|
|
439
|
+
this.attrMap.fill(0, s, e);
|
|
440
|
+
};
|
|
441
|
+
if (down) {
|
|
442
|
+
for (let row = last; row >= first; row -= 1) {
|
|
443
|
+
if (row - lines >= first)
|
|
444
|
+
copyRow(row - lines, row);
|
|
445
|
+
else
|
|
446
|
+
clearRow(row);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
for (let row = first; row <= last; row += 1) {
|
|
451
|
+
if (row + lines <= last)
|
|
452
|
+
copyRow(row + lines, row);
|
|
453
|
+
else
|
|
454
|
+
clearRow(row);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
put(address, byte) {
|
|
459
|
+
this.buffer[address] = byte;
|
|
460
|
+
this.attrMap[address] = byte >= 0x20 && byte <= 0x3f ? 1 : 0;
|
|
461
|
+
}
|
|
462
|
+
nullField(field) {
|
|
463
|
+
for (let n = 0; n < field.length; n += 1) {
|
|
464
|
+
const p = (field.start + n) % this.size;
|
|
465
|
+
this.buffer[p] = 0;
|
|
466
|
+
this.attrMap[p] = 0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/** 1-based row/column to a buffer address. */
|
|
470
|
+
address(row, col) {
|
|
471
|
+
if (row < 1 || row > this.rows || col < 1 || col > this.cols) {
|
|
472
|
+
throw new DataStreamError(`buffer address row ${row} column ${col} is outside the ${this.rows}x${this.cols} screen`);
|
|
473
|
+
}
|
|
474
|
+
return (row - 1) * this.cols + (col - 1);
|
|
475
|
+
}
|
|
476
|
+
rowCol(address) {
|
|
477
|
+
return { row: Math.floor(address / this.cols) + 1, col: (address % this.cols) + 1 };
|
|
478
|
+
}
|
|
479
|
+
// ------------------------------------------------------------ save / restore
|
|
480
|
+
saveScreen() {
|
|
481
|
+
const id = (this.nextSaveId++ & 0xffff).toString(16).toUpperCase().padStart(4, "0");
|
|
482
|
+
this.savedScreens.set(id, {
|
|
483
|
+
rows: this.rows,
|
|
484
|
+
cols: this.cols,
|
|
485
|
+
buffer: this.buffer.slice(),
|
|
486
|
+
attrMap: this.attrMap.slice(),
|
|
487
|
+
fields: this.fields.map(f => f.clone()),
|
|
488
|
+
cursor: this.cursor,
|
|
489
|
+
errorRow: this.errorRow,
|
|
490
|
+
savedErrorLine: this.savedErrorLine,
|
|
491
|
+
inputInhibited: this.inputInhibited
|
|
492
|
+
});
|
|
493
|
+
while (this.savedScreens.size > MAX_SAVED_SCREENS) {
|
|
494
|
+
const oldest = this.savedScreens.keys().next().value;
|
|
495
|
+
this.savedScreens.delete(oldest);
|
|
496
|
+
}
|
|
497
|
+
// The host hands these bytes back in a Restore Screen command.
|
|
498
|
+
const payload = Buffer.concat([Buffer.from([ESC, CMD_RESTORE_SCREEN]), Buffer.from(`${SAVE_TAG}${id}`, "ascii")]);
|
|
499
|
+
return buildGdsRecord(OP_PUT_GET, payload);
|
|
500
|
+
}
|
|
501
|
+
restoreScreen(payload) {
|
|
502
|
+
const text = payload.toString("ascii");
|
|
503
|
+
const id = text.startsWith(SAVE_TAG) ? text.substring(SAVE_TAG.length, SAVE_TAG.length + 4) : "";
|
|
504
|
+
const saved = this.savedScreens.get(id);
|
|
505
|
+
if (!saved) {
|
|
506
|
+
this.warn("Restore Screen referenced a screen that was not saved by this session");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.rows = saved.rows;
|
|
510
|
+
this.cols = saved.cols;
|
|
511
|
+
this.buffer = saved.buffer.slice();
|
|
512
|
+
this.attrMap = saved.attrMap.slice();
|
|
513
|
+
this.fields = saved.fields.map(f => f.clone());
|
|
514
|
+
this.cursor = saved.cursor;
|
|
515
|
+
this.errorRow = saved.errorRow;
|
|
516
|
+
this.savedErrorLine = saved.savedErrorLine;
|
|
517
|
+
this.inputInhibited = saved.inputInhibited;
|
|
518
|
+
}
|
|
519
|
+
// --------------------------------------------------------- outbound records
|
|
520
|
+
readScreenResponse() {
|
|
521
|
+
return buildGdsRecord(OP_NO_OP, this.buffer);
|
|
522
|
+
}
|
|
523
|
+
readImmediateResponse() {
|
|
524
|
+
const { row, col } = this.rowCol(this.cursor);
|
|
525
|
+
return buildGdsRecord(OP_PUT_GET, Buffer.from([row, col, 0x00, ...this.fieldData(CMD_READ_INPUT_FIELDS)]));
|
|
526
|
+
}
|
|
527
|
+
/** 5250 Query reply: identifies the terminal as a 5250 display emulator. */
|
|
528
|
+
queryReply() {
|
|
529
|
+
const { row, col } = this.rowCol(this.cursor);
|
|
530
|
+
const wide = this.deviceType === "3477";
|
|
531
|
+
const deviceType = stringToEbcdic(wide ? "3477" : "3179");
|
|
532
|
+
const model = stringToEbcdic(wide ? "FC " : "002");
|
|
533
|
+
const body = [
|
|
534
|
+
0xd9, 0x70, // query class and type
|
|
535
|
+
0x80, // flag: query reply
|
|
536
|
+
0x06, 0x00, // controller hardware class: another 5250 emulator
|
|
537
|
+
0x01, 0x01, 0x00, // controller code level
|
|
538
|
+
...new Array(16).fill(0), // reserved
|
|
539
|
+
0x01, // device class: display
|
|
540
|
+
...deviceType,
|
|
541
|
+
...model,
|
|
542
|
+
0x02, // keyboard id: standard
|
|
543
|
+
0x00, // extended keyboard id
|
|
544
|
+
0x00, // reserved
|
|
545
|
+
0x00, 0x00, 0x00, 0x00, // display serial number
|
|
546
|
+
0x01, 0x00, // maximum input fields (256)
|
|
547
|
+
0x00, // control unit customization
|
|
548
|
+
0x00, 0x00, // reserved
|
|
549
|
+
...new Array(12).fill(0) // controller/display capabilities: basic 5250 only
|
|
550
|
+
];
|
|
551
|
+
const length = body.length + 2;
|
|
552
|
+
return buildGdsRecord(OP_NO_OP, Buffer.from([row, col, AID.INBOUND_WSF, length >> 8, length & 0xff, ...body]));
|
|
553
|
+
}
|
|
554
|
+
/** Field data for an inbound response, in the format the pending read asked for. */
|
|
555
|
+
fieldData(readCommand) {
|
|
556
|
+
const out = [];
|
|
557
|
+
if (readCommand === CMD_READ_INPUT_FIELDS) {
|
|
558
|
+
if (!this.fields.some(f => f.modified))
|
|
559
|
+
return out;
|
|
560
|
+
for (const field of this.fields) {
|
|
561
|
+
for (let n = 0; n < field.length; n += 1) {
|
|
562
|
+
const byte = this.buffer[(field.start + n) % this.size];
|
|
563
|
+
out.push(byte === 0 ? 0x40 : byte);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
for (const field of this.fields) {
|
|
569
|
+
if (!field.modified)
|
|
570
|
+
continue;
|
|
571
|
+
const { row, col } = this.rowCol(field.start);
|
|
572
|
+
out.push(ORD_SBA, row, col);
|
|
573
|
+
const bytes = [];
|
|
574
|
+
for (let n = 0; n < field.length; n += 1)
|
|
575
|
+
bytes.push(this.buffer[(field.start + n) % this.size]);
|
|
576
|
+
while (bytes.length > 0 && bytes[bytes.length - 1] === 0)
|
|
577
|
+
bytes.pop();
|
|
578
|
+
out.push(...bytes.map(b => (b === 0 ? 0x40 : b)));
|
|
579
|
+
}
|
|
580
|
+
return out;
|
|
581
|
+
}
|
|
582
|
+
/** Build the Put/Get record for an AID key and lock the keyboard. */
|
|
583
|
+
buildAidResponse(aid) {
|
|
584
|
+
const { row, col } = this.rowCol(this.cursor);
|
|
585
|
+
const data = [row, col, aid];
|
|
586
|
+
if (!AID_WITHOUT_DATA.has(aid)) {
|
|
587
|
+
const readCommand = this.pendingRead === CMD_READ_INPUT_FIELDS ? CMD_READ_INPUT_FIELDS : CMD_READ_MDT_FIELDS;
|
|
588
|
+
data.push(...this.fieldData(readCommand));
|
|
589
|
+
}
|
|
590
|
+
this.keyboardLocked = true;
|
|
591
|
+
this.inputInhibited = false;
|
|
592
|
+
this.pendingRead = null;
|
|
593
|
+
return buildGdsRecord(OP_PUT_GET, Buffer.from(data));
|
|
594
|
+
}
|
|
595
|
+
// ------------------------------------------------------------ operator input
|
|
596
|
+
inputFields() {
|
|
597
|
+
return this.fields.filter(f => !f.bypass);
|
|
598
|
+
}
|
|
599
|
+
fieldAt(address) {
|
|
600
|
+
return this.fields.find(f => f.contains(address));
|
|
601
|
+
}
|
|
602
|
+
setCursor(row, col) {
|
|
603
|
+
this.cursor = this.address(row, col);
|
|
604
|
+
}
|
|
605
|
+
/** Local RESET key: leave the input-inhibited (error) state. */
|
|
606
|
+
reset() {
|
|
607
|
+
if (this.savedErrorLine) {
|
|
608
|
+
const rowStart = this.errorRow * this.cols;
|
|
609
|
+
this.buffer.set(this.savedErrorLine.buffer, rowStart);
|
|
610
|
+
this.attrMap.set(this.savedErrorLine.attrMap, rowStart);
|
|
611
|
+
this.savedErrorLine = undefined;
|
|
612
|
+
}
|
|
613
|
+
if (this.inputInhibited) {
|
|
614
|
+
this.inputInhibited = false;
|
|
615
|
+
this.keyboardLocked = false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
requireInputAllowed() {
|
|
619
|
+
if (this.inputInhibited)
|
|
620
|
+
throw new Error("Input is inhibited by an error message; send the RESET key first");
|
|
621
|
+
if (this.keyboardLocked)
|
|
622
|
+
throw new Error("The keyboard is locked: the host has not finished updating the screen");
|
|
623
|
+
}
|
|
624
|
+
encodeFor(field, text) {
|
|
625
|
+
const value = field.monocase ? text.toUpperCase() : text;
|
|
626
|
+
if (field.numeric && !/^[0-9 +\-.,]*$/.test(value)) {
|
|
627
|
+
throw new Error(`Field at row ${this.rowCol(field.start).row} column ${this.rowCol(field.start).col} accepts only numeric input`);
|
|
628
|
+
}
|
|
629
|
+
return stringToEbcdic(value, this.ccsid);
|
|
630
|
+
}
|
|
631
|
+
/** Type text at the cursor, as an operator would. */
|
|
632
|
+
typeText(text) {
|
|
633
|
+
this.requireInputAllowed();
|
|
634
|
+
const field = this.fieldAt(this.cursor);
|
|
635
|
+
if (!field || field.bypass) {
|
|
636
|
+
const { row, col } = this.rowCol(this.cursor);
|
|
637
|
+
throw new Error(`The cursor (row ${row}, column ${col}) is not in an input field`);
|
|
638
|
+
}
|
|
639
|
+
const bytes = this.encodeFor(field, text);
|
|
640
|
+
const offset = this.cursor - field.start;
|
|
641
|
+
if (offset + bytes.length > field.length) {
|
|
642
|
+
throw new Error(`Text is ${bytes.length} characters but only ${field.length - offset} fit in the field from the cursor`);
|
|
643
|
+
}
|
|
644
|
+
bytes.forEach((byte, n) => {
|
|
645
|
+
const p = (field.start + offset + n) % this.size;
|
|
646
|
+
this.buffer[p] = byte;
|
|
647
|
+
this.attrMap[p] = 0;
|
|
648
|
+
});
|
|
649
|
+
field.modified = true;
|
|
650
|
+
const next = field.start + offset + bytes.length;
|
|
651
|
+
if (next > field.end)
|
|
652
|
+
this.tab();
|
|
653
|
+
else
|
|
654
|
+
this.cursor = next;
|
|
655
|
+
this.version += 1;
|
|
656
|
+
}
|
|
657
|
+
/** Replace a field's whole content. */
|
|
658
|
+
setFieldValue(field, value) {
|
|
659
|
+
this.requireInputAllowed();
|
|
660
|
+
if (field.bypass)
|
|
661
|
+
throw new Error("Field is protected");
|
|
662
|
+
let bytes = this.encodeFor(field, value);
|
|
663
|
+
if (bytes.length > field.length) {
|
|
664
|
+
throw new Error(`Value is ${bytes.length} characters but the field holds ${field.length}`);
|
|
665
|
+
}
|
|
666
|
+
const adjust = field.rightAdjust;
|
|
667
|
+
if (adjust && bytes.length > 0) {
|
|
668
|
+
const fill = adjust === "zero" ? 0xf0 : 0x40;
|
|
669
|
+
const padded = new Uint8Array(field.length).fill(fill);
|
|
670
|
+
padded.set(bytes, field.length - bytes.length);
|
|
671
|
+
bytes = padded;
|
|
672
|
+
}
|
|
673
|
+
for (let n = 0; n < field.length; n += 1) {
|
|
674
|
+
this.buffer[(field.start + n) % this.size] = n < bytes.length ? bytes[n] : 0x00;
|
|
675
|
+
this.attrMap[(field.start + n) % this.size] = 0;
|
|
676
|
+
}
|
|
677
|
+
field.modified = true;
|
|
678
|
+
this.version += 1;
|
|
679
|
+
}
|
|
680
|
+
tab() {
|
|
681
|
+
const inputs = this.inputFields();
|
|
682
|
+
if (inputs.length === 0)
|
|
683
|
+
return;
|
|
684
|
+
const next = inputs.find(f => f.start > this.cursor) || inputs[0];
|
|
685
|
+
this.cursor = next.start;
|
|
686
|
+
}
|
|
687
|
+
backTab() {
|
|
688
|
+
const inputs = this.inputFields();
|
|
689
|
+
if (inputs.length === 0)
|
|
690
|
+
return;
|
|
691
|
+
const current = inputs.find(f => f.contains(this.cursor));
|
|
692
|
+
if (current && this.cursor > current.start) {
|
|
693
|
+
this.cursor = current.start;
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const before = inputs.filter(f => f.start < this.cursor);
|
|
697
|
+
this.cursor = (before[before.length - 1] || inputs[inputs.length - 1]).start;
|
|
698
|
+
}
|
|
699
|
+
home() {
|
|
700
|
+
const first = this.inputFields()[0];
|
|
701
|
+
this.cursor = first ? first.start : 0;
|
|
702
|
+
}
|
|
703
|
+
/** Erase from the cursor to the end of the current field. */
|
|
704
|
+
eraseToEndOfField() {
|
|
705
|
+
this.requireInputAllowed();
|
|
706
|
+
const field = this.fieldAt(this.cursor);
|
|
707
|
+
if (!field || field.bypass)
|
|
708
|
+
throw new Error("The cursor is not in an input field");
|
|
709
|
+
for (let p = this.cursor; p <= field.end; p += 1)
|
|
710
|
+
this.buffer[p % this.size] = 0;
|
|
711
|
+
field.modified = true;
|
|
712
|
+
this.version += 1;
|
|
713
|
+
}
|
|
714
|
+
fieldExit() {
|
|
715
|
+
this.eraseToEndOfField();
|
|
716
|
+
this.tab();
|
|
717
|
+
}
|
|
718
|
+
// ------------------------------------------------------------------- views
|
|
719
|
+
/** Screen rows as text. Attributes, nulls and non-display areas are blanks. */
|
|
720
|
+
renderRows() {
|
|
721
|
+
const hiddenMask = new Uint8Array(this.size);
|
|
722
|
+
for (const field of this.fields) {
|
|
723
|
+
if (!field.hidden)
|
|
724
|
+
continue;
|
|
725
|
+
for (let n = 0; n < field.length; n += 1)
|
|
726
|
+
hiddenMask[(field.start + n) % this.size] = 1;
|
|
727
|
+
}
|
|
728
|
+
const rows = [];
|
|
729
|
+
let attr = 0x20;
|
|
730
|
+
let line = "";
|
|
731
|
+
for (let p = 0; p < this.size; p += 1) {
|
|
732
|
+
if (this.attrMap[p]) {
|
|
733
|
+
attr = this.buffer[p];
|
|
734
|
+
line += " ";
|
|
735
|
+
}
|
|
736
|
+
else if ((attr & 0x07) === 0x07 || hiddenMask[p]) {
|
|
737
|
+
line += " ";
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
line += ebcdicToChar(this.buffer[p], this.ccsid);
|
|
741
|
+
}
|
|
742
|
+
if ((p + 1) % this.cols === 0) {
|
|
743
|
+
rows.push(line);
|
|
744
|
+
line = "";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return rows;
|
|
748
|
+
}
|
|
749
|
+
fieldId(field) {
|
|
750
|
+
const { row, col } = this.rowCol(field.start);
|
|
751
|
+
return `r${row}c${col}`;
|
|
752
|
+
}
|
|
753
|
+
fieldText(field) {
|
|
754
|
+
let out = "";
|
|
755
|
+
for (let n = 0; n < field.length; n += 1)
|
|
756
|
+
out += ebcdicToChar(this.buffer[(field.start + n) % this.size], this.ccsid);
|
|
757
|
+
return out;
|
|
758
|
+
}
|
|
759
|
+
fieldHasValue(field) {
|
|
760
|
+
for (let n = 0; n < field.length; n += 1) {
|
|
761
|
+
const byte = this.buffer[(field.start + n) % this.size];
|
|
762
|
+
if (byte !== 0 && byte !== 0x40)
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
/** Text on the field's row to the left of it, used as a label. */
|
|
768
|
+
fieldLabel(field, rows = this.renderRows()) {
|
|
769
|
+
const { row, col } = this.rowCol(field.start);
|
|
770
|
+
const left = rows[row - 1].substring(0, Math.max(0, col - 2));
|
|
771
|
+
const trimmed = left.replace(/[\s.:]+$/, "");
|
|
772
|
+
if (trimmed.endsWith("===>"))
|
|
773
|
+
return "===>";
|
|
774
|
+
const parts = trimmed.split(/\s{3,}/);
|
|
775
|
+
return (parts[parts.length - 1] || "").trim();
|
|
776
|
+
}
|
|
777
|
+
isCommandLine(field, rows = this.renderRows()) {
|
|
778
|
+
return !field.bypass && !field.hidden && this.fieldLabel(field, rows) === "===>";
|
|
779
|
+
}
|
|
780
|
+
view() {
|
|
781
|
+
const rows = this.renderRows();
|
|
782
|
+
const fields = this.fields.map((field, index) => {
|
|
783
|
+
const { row, col } = this.rowCol(field.start);
|
|
784
|
+
const base = {
|
|
785
|
+
id: this.fieldId(field),
|
|
786
|
+
index: index + 1,
|
|
787
|
+
row,
|
|
788
|
+
col,
|
|
789
|
+
length: field.length,
|
|
790
|
+
label: this.fieldLabel(field, rows),
|
|
791
|
+
protected: field.bypass,
|
|
792
|
+
hidden: field.hidden,
|
|
793
|
+
modified: field.modified,
|
|
794
|
+
numeric: field.numeric,
|
|
795
|
+
commandLine: this.isCommandLine(field, rows)
|
|
796
|
+
};
|
|
797
|
+
if (field.hidden)
|
|
798
|
+
base.hasValue = this.fieldHasValue(field);
|
|
799
|
+
else
|
|
800
|
+
base.value = this.fieldText(field).replace(/\s+$/, "");
|
|
801
|
+
return base;
|
|
802
|
+
});
|
|
803
|
+
const view = {
|
|
804
|
+
rows: this.rows,
|
|
805
|
+
cols: this.cols,
|
|
806
|
+
cursor: this.rowCol(this.cursor),
|
|
807
|
+
keyboardLocked: this.keyboardLocked,
|
|
808
|
+
inputInhibited: this.inputInhibited,
|
|
809
|
+
messageWaiting: this.messageWaiting,
|
|
810
|
+
readPending: this.pendingRead !== null,
|
|
811
|
+
text: rows,
|
|
812
|
+
fields,
|
|
813
|
+
version: this.version
|
|
814
|
+
};
|
|
815
|
+
if (this.inputInhibited)
|
|
816
|
+
view.errorLine = rows[this.errorRow].trim();
|
|
817
|
+
return view;
|
|
818
|
+
}
|
|
819
|
+
warn(message) {
|
|
820
|
+
this.warnings.push(message);
|
|
821
|
+
if (this.warnings.length > 20)
|
|
822
|
+
this.warnings.shift();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function hex(value) {
|
|
826
|
+
return (value ?? 0).toString(16).padStart(2, "0");
|
|
827
|
+
}
|