tnz3270-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/index.cjs +5489 -0
- package/dist/index.d.cts +1305 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +1305 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5384 -0
- package/package.json +61 -0
- package/src/automation/ati.ts +1057 -0
- package/src/automation/file-transfer.ts +305 -0
- package/src/core/base.ts +59 -0
- package/src/core/buffer.ts +228 -0
- package/src/core/keyboard.ts +396 -0
- package/src/core/parser.ts +422 -0
- package/src/core/screen.ts +110 -0
- package/src/core/telnet.ts +213 -0
- package/src/core/tnz-state.ts +4 -0
- package/src/core/tnz.ts +2403 -0
- package/src/index.ts +113 -0
- package/src/types.ts +348 -0
- package/src/utils/codepage.ts +281 -0
- package/src/utils/session-utils.ts +94 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","sources":["../src/types.ts","../src/utils/session-utils.ts","../src/utils/codepage.ts","../src/core/base.ts","../src/core/tnz.ts","../src/automation/ati.ts","../src/core/telnet.ts"],"sourcesContent":["/**\n * Core types, constants, and interfaces for TN3270 terminal emulation.\n *\n *\n * @module types\n */\n\n// ---------------------------------------------------------------------------\n// 3270 AID (Attention Identifier) codes\n// Sent from terminal to host to identify the key pressed.\n// Reference: tnz.py line 192, and 3270 Data Stream Architecture Reference\n// ---------------------------------------------------------------------------\n\nexport const AID = {\n NONE: 0x60,\n ENTER: 0x7d,\n PF1: 0xf1,\n PF2: 0xf2,\n PF3: 0xf3,\n PF4: 0xf4,\n PF5: 0xf5,\n PF6: 0xf6,\n PF7: 0xf7,\n PF8: 0xf8,\n PF9: 0xf9,\n PF10: 0x7a,\n PF11: 0x7b,\n PF12: 0x7c,\n PF13: 0xc1,\n PF14: 0xc2,\n PF15: 0xc3,\n PF16: 0xc4,\n PF17: 0xc5,\n PF18: 0xc6,\n PF19: 0xc7,\n PF20: 0xc8,\n PF21: 0xc9,\n PF22: 0x4a,\n PF23: 0x4b,\n PF24: 0x4c,\n PA1: 0x6c,\n PA2: 0x6e,\n PA3: 0x6b,\n CLEAR: 0x6d,\n CLEAR_PARTITION: 0x6a,\n SYSREQ: 0xf0,\n STRUCTURED_FIELD: 0x88,\n READ_PARTITION: 0x61,\n TRIGGER: 0x7e,\n} as const;\n\nexport type AidCode = (typeof AID)[keyof typeof AID];\n\n// ---------------------------------------------------------------------------\n// 3270 Command codes\n// Sent from host to terminal.\n// ---------------------------------------------------------------------------\n\nexport const CMD = {\n WRITE: 0xf1,\n ERASE_WRITE: 0xf5,\n ERASE_WRITE_ALTERNATE: 0x7e,\n READ_BUFFER: 0xf2,\n READ_MODIFIED: 0xf6,\n READ_MODIFIED_ALL: 0x6e,\n ERASE_ALL_UNPROTECTED: 0x6f,\n WRITE_STRUCTURED_FIELD: 0xf3,\n} as const;\n\nexport type CommandCode = (typeof CMD)[keyof typeof CMD];\n\n// ---------------------------------------------------------------------------\n// 3270 Order codes\n// Embedded in data stream to control buffer positioning and field creation.\n// ---------------------------------------------------------------------------\n\nexport const ORDER = {\n /** Start Field */\n SF: 0x1d,\n /** Set Buffer Address */\n SBA: 0x11,\n /** Insert Cursor */\n IC: 0x13,\n /** Program Tab */\n PT: 0x05,\n /** Repeat to Address */\n RA: 0x3c,\n /** Erase Unprotected to Address */\n EUA: 0x12,\n /** Set Attribute */\n SA: 0x28,\n /** Start Field Extended */\n SFE: 0x29,\n /** Modify Field */\n MF: 0x2c,\n /** Graphic Escape */\n GE: 0x08,\n} as const;\n\nexport type OrderCode = (typeof ORDER)[keyof typeof ORDER];\n\n// ---------------------------------------------------------------------------\n// Telnet protocol constants\n// Reference: RFC 854, RFC 855, RFC 1576 (TN3270), RFC 2355 (TN3270E)\n// ---------------------------------------------------------------------------\n\nexport const TELNET = {\n // Telnet commands\n IAC: 0xff,\n DONT: 0xfe,\n DO: 0xfd,\n WONT: 0xfc,\n WILL: 0xfb,\n SB: 0xfa,\n GA: 0xf9,\n EL: 0xf8,\n EC: 0xf7,\n AYT: 0xf6,\n AO: 0xf5,\n IP: 0xf4,\n BREAK: 0xf3,\n NOP: 0xf1,\n SE: 0xf0,\n EOR: 0xef,\n\n // Telnet options\n OPT_BINARY: 0x00,\n OPT_TERMINAL_TYPE: 0x18,\n OPT_EOR: 0x19,\n OPT_TN3270E: 0x28,\n OPT_START_TLS: 0x2e,\n\n // Terminal-type subnegotiation\n TERMINAL_TYPE_IS: 0x00,\n TERMINAL_TYPE_SEND: 0x01,\n\n // TN3270E subnegotiation\n TN3270E_SEND: 0x09,\n TN3270E_DEVICE_TYPE: 0x02,\n TN3270E_IS: 0x04,\n TN3270E_REQUEST: 0x07,\n TN3270E_FUNCTIONS: 0x03,\n TN3270E_RESPONSES: 0x02,\n\n // START_TLS subnegotiation\n START_TLS_FOLLOWS: 0x01,\n} as const;\n\n// ---------------------------------------------------------------------------\n// 3270 Extended Attribute types\n// Used in SA (Set Attribute), SFE (Start Field Extended), MF (Modify Field)\n// ---------------------------------------------------------------------------\n\nexport const ATTR_TYPE = {\n ALL: 0x00,\n FIELD_ATTRIBUTE: 0xc0,\n EXTENDED_HIGHLIGHTING: 0x41,\n FOREGROUND_COLOR: 0x42,\n BACKGROUND_COLOR: 0x43,\n CHARACTER_SET: 0x43,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Extended Highlighting values\n// ---------------------------------------------------------------------------\n\nexport const ExtendedHighlight = {\n DEFAULT: 0x00,\n NORMAL: 0xf0,\n BLINK: 0xf1,\n REVERSE_VIDEO: 0xf2,\n UNDERSCORE: 0xf4,\n INTENSIFY: 0xf8,\n} as const;\n\nexport type ExtendedHighlightValue =\n (typeof ExtendedHighlight)[keyof typeof ExtendedHighlight];\n\n// ---------------------------------------------------------------------------\n// 3270 Color values\n// ---------------------------------------------------------------------------\n\nexport const Color = {\n DEFAULT: 0x00,\n NEUTRAL_BLACK: 0xf0,\n BLUE: 0xf1,\n RED: 0xf2,\n PINK: 0xf3,\n GREEN: 0xf4,\n TURQUOISE: 0xf5,\n YELLOW: 0xf6,\n NEUTRAL_WHITE: 0xf7,\n} as const;\n\nexport type ColorValue = (typeof Color)[keyof typeof Color];\n\n// ---------------------------------------------------------------------------\n// Field attribute bit flags\n// Reference: 3270 Data Stream Architecture, Chapter 4\n// ---------------------------------------------------------------------------\n\nexport const FA = {\n /** Protected field */\n PROTECTED: 0x20,\n /** Numeric-only field */\n NUMERIC: 0x10,\n /** Modified Data Tag */\n MDT: 0x01,\n\n // Display/intensity combinations (bits 3-4)\n DISPLAY_NOT_PEN: 0x00,\n DISPLAY_PEN: 0x04,\n INTENSIFIED_PEN: 0x08,\n NON_DISPLAY: 0x0c,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Structured Field identifiers\n// Reference: tnz.py query reply and WSF processing\n// ---------------------------------------------------------------------------\n\nexport const SF_ID = {\n READ_PARTITION: 0x01,\n QUERY_REPLY: 0x81,\n DDM: 0xd0,\n} as const;\n\n// Query Reply types\nexport const QR_TYPE = {\n USABLE_AREA: 0x81,\n HIGHLIGHT: 0x87,\n COLOR: 0x86,\n REPLY_MODES: 0x88,\n IMPLICIT_PARTITION: 0xa6,\n CHARACTER_SETS: 0x85,\n DDM: 0x95,\n RPQ_NAMES: 0xa1,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Interfaces\n// ---------------------------------------------------------------------------\n\n/** Options for creating a Tnz connection. */\nexport interface TnzOptions {\n /** Use TLS/SSL for the connection (default: false) */\n secure?: boolean;\n /** Verify the server certificate (default: true) */\n verifyCert?: boolean;\n /** EBCDIC encoding to use (default: 'cp037') */\n encoding?: string;\n /** Terminal type string sent during negotiation */\n terminalType?: string;\n /** Enable TN3270E protocol negotiation */\n useTn3270e?: boolean;\n /** Logical unit name for TN3270E */\n luName?: string;\n /** Alternate screen rows */\n amaxRow?: number;\n /** Alternate screen columns */\n amaxCol?: number;\n /** Maximum TLS security level */\n secLevel?: number;\n /** Minimum TLS version */\n sslMinimumTls?: string;\n /** SSL verification mode */\n sslVerify?: string;\n /** Optional callback fired when the screen is updated by the host. */\n onScreenUpdate?: () => void;\n}\n\n/** Snapshot of connection security state. */\nexport interface ConnectionState {\n /** Whether the connection uses TLS */\n secure: boolean;\n /** Whether the server certificate was verified as trusted */\n certVerified: boolean;\n /** Whether the server hostname was verified */\n hostVerified: boolean;\n /** Whether 3270 data stream mode is active (vs NVT) */\n tn3270: boolean;\n /** Whether TN3270E protocol is in use */\n tn3270e: boolean;\n /** Whether STARTTLS upgrade completed */\n startTlsCompleted: boolean;\n}\n\n/** Describes a field on the 3270 screen. */\nexport interface FieldAttribute {\n /** Buffer address of the field attribute byte */\n address: number;\n /** Raw attribute byte value */\n attribute: number;\n /** Whether the field is protected from input */\n isProtected: boolean;\n /** Whether the field accepts only numeric input */\n isNumeric: boolean;\n /** Whether the field has been modified */\n isModified: boolean;\n /** Whether the field is visible */\n isDisplay: boolean;\n /** Whether the field is intensified (bright) */\n isIntensified: boolean;\n}\n\n/** Session presentation space size (rows x columns). */\nexport interface ScreenSize {\n rows: number;\n cols: number;\n}\n\n/**\n * Codec entry for EBCDIC encoding/decoding.\n * Maps an EBCDIC code page to encode/decode functions.\n */\nexport interface CodecEntry {\n /** Code page name (e.g. 'cp037') */\n name: string;\n /** Numeric code page ID extracted from name */\n codePageNumber: number;\n /** Encode Unicode string to EBCDIC bytes */\n encode: (str: string) => Buffer;\n /** Decode EBCDIC bytes to Unicode string */\n decode: (buf: Buffer) => string;\n}\n\n/**\n * DDM (Distributed Data Management) file transfer options.\n * Used by IND$FILE GET/PUT operations.\n */\nexport interface FileTransferOptions {\n /** Transfer mode */\n mode?: 'ascii' | 'binary';\n /** Record format */\n recfm?: 'F' | 'V' | 'U';\n /** Logical record length */\n lrecl?: number;\n /** Block size */\n blksize?: number;\n /** Append to existing file on download */\n append?: boolean;\n /** Space allocation for upload */\n space?: {\n primary: number;\n secondary: number;\n unit: 'tracks' | 'cylinders' | 'avblock';\n };\n}\n","/**\n * Session utility functions.\n *\n * Provides session presentation space size calculations.\n *\n * @module utils/session-utils\n */\n\nimport type { ScreenSize } from '../types';\n\n/**\n * HOD-defined session presentation space sizes.\n * Maps numeric PS size IDs to [rows, cols] dimensions.\n *\n */\nconst SESSION_PS_SIZES: Record<string, ScreenSize> = {\n '2': { rows: 24, cols: 80 },\n '3': { rows: 32, cols: 80 },\n '4': { rows: 43, cols: 80 },\n '5': { rows: 27, cols: 132 },\n '6': { rows: 24, cols: 132 },\n '7': { rows: 36, cols: 80 },\n '8': { rows: 36, cols: 132 },\n '9': { rows: 48, cols: 80 },\n '10': { rows: 48, cols: 132 },\n '11': { rows: 72, cols: 80 },\n '12': { rows: 72, cols: 132 },\n '13': { rows: 144, cols: 80 },\n '14': { rows: 144, cols: 132 },\n '15': { rows: 25, cols: 80 },\n '16': { rows: 25, cols: 132 },\n '17': { rows: 62, cols: 160 },\n '18': { rows: 26, cols: 80 },\n '19': { rows: 26, cols: 132 },\n};\n\n/**\n * Convert a SESSION_PS_SIZE value to rows and columns.\n *\n * Accepts either:\n * - A numeric string matching an HOD-defined size ID (e.g. \"2\" for 24x80)\n * - A \"rowsXcols\" notation (e.g. \"43X80\", case-insensitive)\n *\n * @throws {ValueError} if the value cannot be parsed\n *\n */\nexport function sessionPsSize(psSize: string | number): ScreenSize {\n const key = String(psSize);\n const predefined = SESSION_PS_SIZES[key];\n if (predefined) {\n return predefined;\n }\n\n const parts = key.toUpperCase().split('X', 2);\n if (parts.length === 2) {\n const rows = parseInt(parts[0], 10);\n const cols = parseInt(parts[1], 10);\n if (!Number.isNaN(rows) && !Number.isNaN(cols) && rows > 0 && cols > 0) {\n return { rows, cols };\n }\n }\n\n throw new Error('Not a SESSION_PS_SIZE value');\n}\n\n/**\n * Constrain a screen size to the 14-bit buffer address limitation.\n *\n * 3270 buffer addresses can be encoded in 14 bits, limiting the total\n * buffer size to 16383 characters. This function trims the given\n * dimensions to fit within that limit while maintaining minimum sizes\n * of 24 rows and 80 columns.\n *\n */\nexport function sessionPs14bit(maxH: number, maxW: number): ScreenSize {\n let rows = Math.max(maxH, 24);\n let cols = Math.max(maxW, 80);\n rows = Math.min(rows, 204); // 16383 / 80\n cols = Math.min(cols, 682); // 16383 / 24\n\n if (rows >= 127 && cols >= 129) {\n return { rows: 127, cols: 129 }; // 127 * 129 = 16383\n }\n\n if (rows >= 129 && cols >= 127) {\n return { rows: 129, cols: 127 }; // 129 * 127 = 16383\n }\n\n if (rows * cols <= 16383) {\n return { rows, cols };\n }\n\n return { rows: Math.floor(16383 / cols), cols };\n}\n","/**\n * EBCDIC code page support.\n *\n * Zero-dependency EBCDIC encode/decode using built-in lookup tables.\n * Supports cp037 (US EBCDIC), cp1047 (z/OS Latin-1), and cp310 (APL).\n * Additional code pages can be registered at runtime.\n *\n *\n * @module utils/codepage\n */\n\nimport type { CodecEntry } from '../types';\n\n// ---------------------------------------------------------------------------\n// CP037 - US/Canada EBCDIC (most common)\n// Reference: https://en.wikipedia.org/wiki/Code_page_37\n// ---------------------------------------------------------------------------\n\n// prettier-ignore\nconst CP037_TO_UNICODE: number[] = [\n 0x0000,0x0001,0x0002,0x0003,0x009C,0x0009,0x0086,0x007F,0x0097,0x008D,0x008E,0x000B,0x000C,0x000D,0x000E,0x000F,\n 0x0010,0x0011,0x0012,0x0013,0x009D,0x0085,0x0008,0x0087,0x0018,0x0019,0x0092,0x008F,0x001C,0x001D,0x001E,0x001F,\n 0x0080,0x0081,0x0082,0x0083,0x0084,0x000A,0x0017,0x001B,0x0088,0x0089,0x008A,0x008B,0x008C,0x0005,0x0006,0x0007,\n 0x0090,0x0091,0x0016,0x0093,0x0094,0x0095,0x0096,0x0004,0x0098,0x0099,0x009A,0x009B,0x0014,0x0015,0x009E,0x001A,\n 0x0020,0x00A0,0x00E2,0x00E4,0x00E0,0x00E1,0x00E3,0x00E5,0x00E7,0x00F1,0x00A2,0x002E,0x003C,0x0028,0x002B,0x007C,\n 0x0026,0x00E9,0x00EA,0x00EB,0x00E8,0x00ED,0x00EE,0x00EF,0x00EC,0x00DF,0x0021,0x0024,0x002A,0x0029,0x003B,0x00AC,\n 0x002D,0x002F,0x00C2,0x00C4,0x00C0,0x00C1,0x00C3,0x00C5,0x00C7,0x00D1,0x00A6,0x002C,0x0025,0x005F,0x003E,0x003F,\n 0x00F8,0x00C9,0x00CA,0x00CB,0x00C8,0x00CD,0x00CE,0x00CF,0x00CC,0x0060,0x003A,0x0023,0x0040,0x0027,0x003D,0x0022,\n 0x00D8,0x0061,0x0062,0x0063,0x0064,0x0065,0x0066,0x0067,0x0068,0x0069,0x00AB,0x00BB,0x00F0,0x00FD,0x00FE,0x00B1,\n 0x00B0,0x006A,0x006B,0x006C,0x006D,0x006E,0x006F,0x0070,0x0071,0x0072,0x00AA,0x00BA,0x00E6,0x00B8,0x00C6,0x00A4,\n 0x00B5,0x007E,0x0073,0x0074,0x0075,0x0076,0x0077,0x0078,0x0079,0x007A,0x00A1,0x00BF,0x00D0,0x00DD,0x00DE,0x00AE,\n 0x005E,0x00A3,0x00A5,0x00B7,0x00A9,0x00A7,0x00B6,0x00BC,0x00BD,0x00BE,0x005B,0x005D,0x00AF,0x00A8,0x00B4,0x00D7,\n 0x007B,0x0041,0x0042,0x0043,0x0044,0x0045,0x0046,0x0047,0x0048,0x0049,0x00AD,0x00F4,0x00F6,0x00F2,0x00F3,0x00F5,\n 0x007D,0x004A,0x004B,0x004C,0x004D,0x004E,0x004F,0x0050,0x0051,0x0052,0x00B9,0x00FB,0x00FC,0x00F9,0x00FA,0x00FF,\n 0x005C,0x00F7,0x0053,0x0054,0x0055,0x0056,0x0057,0x0058,0x0059,0x005A,0x00B2,0x00D4,0x00D6,0x00D2,0x00D3,0x00D5,\n 0x0030,0x0031,0x0032,0x0033,0x0034,0x0035,0x0036,0x0037,0x0038,0x0039,0x00B3,0x00DB,0x00DC,0x00D9,0x00DA,0x009F,\n];\n\n// ---------------------------------------------------------------------------\n// CP1047 - z/OS Latin-1 (Open Systems)\n// Reference: https://en.wikipedia.org/wiki/EBCDIC_1047\n// ---------------------------------------------------------------------------\n\n// prettier-ignore\nconst CP1047_TO_UNICODE: number[] = [\n 0x0000,0x0001,0x0002,0x0003,0x009C,0x0009,0x0086,0x007F,0x0097,0x008D,0x008E,0x000B,0x000C,0x000D,0x000E,0x000F,\n 0x0010,0x0011,0x0012,0x0013,0x009D,0x0085,0x0008,0x0087,0x0018,0x0019,0x0092,0x008F,0x001C,0x001D,0x001E,0x001F,\n 0x0080,0x0081,0x0082,0x0083,0x0084,0x000A,0x0017,0x001B,0x0088,0x0089,0x008A,0x008B,0x008C,0x0005,0x0006,0x0007,\n 0x0090,0x0091,0x0016,0x0093,0x0094,0x0095,0x0096,0x0004,0x0098,0x0099,0x009A,0x009B,0x0014,0x0015,0x009E,0x001A,\n 0x0020,0x00A0,0x00E2,0x00E4,0x00E0,0x00E1,0x00E3,0x00E5,0x00E7,0x00F1,0x00A2,0x002E,0x003C,0x0028,0x002B,0x007C,\n 0x0026,0x00E9,0x00EA,0x00EB,0x00E8,0x00ED,0x00EE,0x00EF,0x00EC,0x00DF,0x0021,0x0024,0x002A,0x0029,0x003B,0x005E,\n 0x002D,0x002F,0x00C2,0x00C4,0x00C0,0x00C1,0x00C3,0x00C5,0x00C7,0x00D1,0x00A6,0x002C,0x0025,0x005F,0x003E,0x003F,\n 0x00F8,0x00C9,0x00CA,0x00CB,0x00C8,0x00CD,0x00CE,0x00CF,0x00CC,0x0060,0x003A,0x0023,0x0040,0x0027,0x003D,0x0022,\n 0x00D8,0x0061,0x0062,0x0063,0x0064,0x0065,0x0066,0x0067,0x0068,0x0069,0x00AB,0x00BB,0x00F0,0x00FD,0x00FE,0x00B1,\n 0x00B0,0x006A,0x006B,0x006C,0x006D,0x006E,0x006F,0x0070,0x0071,0x0072,0x00AA,0x00BA,0x00E6,0x00B8,0x00C6,0x00A4,\n 0x00B5,0x007E,0x0073,0x0074,0x0075,0x0076,0x0077,0x0078,0x0079,0x007A,0x00A1,0x00BF,0x00D0,0x005B,0x00DE,0x00AE,\n 0x00AC,0x00A3,0x00A5,0x00B7,0x00A9,0x00A7,0x00B6,0x00BC,0x00BD,0x00BE,0x00DD,0x00A8,0x00AF,0x005D,0x00B4,0x00D7,\n 0x007B,0x0041,0x0042,0x0043,0x0044,0x0045,0x0046,0x0047,0x0048,0x0049,0x00AD,0x00F4,0x00F6,0x00F2,0x00F3,0x00F5,\n 0x007D,0x004A,0x004B,0x004C,0x004D,0x004E,0x004F,0x0050,0x0051,0x0052,0x00B9,0x00FB,0x00FC,0x00F9,0x00FA,0x00FF,\n 0x005C,0x00F7,0x0053,0x0054,0x0055,0x0056,0x0057,0x0058,0x0059,0x005A,0x00B2,0x00D4,0x00D6,0x00D2,0x00D3,0x00D5,\n 0x0030,0x0031,0x0032,0x0033,0x0034,0x0035,0x0036,0x0037,0x0038,0x0039,0x00B3,0x00DB,0x00DC,0x00D9,0x00DA,0x009F,\n];\n\n// ---------------------------------------------------------------------------\n// CP310 - APL Graphic Symbols (3278T terminal)\n// ---------------------------------------------------------------------------\n\n// prettier-ignore\nconst CP310_TO_UNICODE: number[] = [\n // 0x00-0x3F: all invalid\n 0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n 0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n 0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n 0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n // 0x40: SPACE, 0x41-0x49: italic A-I\n 0x0020,0x1D434,0x1D435,0x1D436,0x1D437,0x1D438,0x1D439,0x1D43A,0x1D43B,0x1D43C,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n // 0x50: invalid, 0x51-0x59: italic J-R\n 0xFFFD,0x1D43D,0x1D43E,0x1D43F,0x1D440,0x1D441,0x1D442,0x1D443,0x1D444,0x1D445,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n // 0x60-0x61: invalid, 0x62-0x69: italic S-Z\n 0xFFFD,0xFFFD,0x1D446,0x1D447,0x1D448,0x1D449,0x1D44A,0x1D44B,0x1D44C,0x1D44D,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n // 0x70-0x7F: APL symbols\n 0x22C4,0x2227,0x00A8,0x233B,0x2378,0x2377,0x22A2,0x22A3,0x2228,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,\n // 0x80-0x8F: tilde, box drawing, arrows\n 0x223C,0x2551,0x2550,0x23B8,0x23B9,0x2502,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0x2191,0x2193,0x2264,0x2308,0x230A,0x2192,\n // 0x90-0x9F: quad, blocks, set theory\n 0x2395,0x258C,0x2590,0x2580,0x2584,0x2588,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0x2283,0x2282,0x2311,0x25CB,0x00B1,0x2190,\n // 0xA0-0xAF\n 0x00AF,0x00B0,0x2500,0x2219,0x2099,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0xFFFD,0x2229,0x222A,0x22A5,0x005B,0x2265,0x2218,\n // 0xB0-0xBF\n 0x237A,0x2208,0x2373,0x2374,0x2375,0xFFFD,0x00D7,0x2216,0x00F7,0xFFFD,0x2207,0x2206,0x22A4,0x005D,0x2260,0x2502,\n // 0xC0-0xCF\n 0x007B,0x207D,0x207A,0x25A0,0x2514,0x250C,0x251C,0x2534,0x00A7,0xFFFD,0x2372,0x2371,0x2337,0x233D,0x2342,0x2349,\n // 0xD0-0xDF\n 0x007D,0x207E,0x207B,0x253C,0x2518,0x2510,0x2524,0x252C,0x00B6,0xFFFD,0x2336,0x01C3,0x2352,0x234B,0x235E,0x235D,\n // 0xE0-0xEF\n 0x2261,0x2081,0x2082,0x2083,0x2364,0x2365,0x236A,0x20AC,0xFFFD,0xFFFD,0x233F,0x2340,0x2235,0x2296,0x2339,0x2355,\n // 0xF0-0xFF: superscript digits, APL operators\n 0x2070,0x00B9,0x00B2,0x00B3,0x2074,0x2075,0x2076,0x2077,0x2078,0x2079,0xFFFD,0x236B,0x2359,0x235F,0x234E,0xFFFD,\n];\n\n// ---------------------------------------------------------------------------\n// Code page registry\n// ---------------------------------------------------------------------------\n\ninterface CodePageTable {\n decodeTable: number[];\n encodeTable: Map<number, number>;\n}\n\n/** Build a reverse encode map from a decode table. */\nfunction buildEncodeTable(decodeTable: number[]): Map<number, number> {\n const map = new Map<number, number>();\n for (let i = 0; i < decodeTable.length; i++) {\n const codePoint = decodeTable[i];\n if (codePoint !== 0xfffd && !map.has(codePoint)) {\n map.set(codePoint, i);\n }\n }\n return map;\n}\n\n/** Built-in code page tables. */\nconst BUILTIN_TABLES: Record<string, number[]> = {\n cp037: CP037_TO_UNICODE,\n cp1047: CP1047_TO_UNICODE,\n cp310: CP310_TO_UNICODE,\n};\n\n/** Cache of compiled code page tables (built on first use). */\nconst compiledTables = new Map<string, CodePageTable>();\n\n/** Get or compile a code page table. */\nfunction getTable(name: string): CodePageTable | undefined {\n const cached = compiledTables.get(name);\n if (cached) return cached;\n\n const decodeTable = BUILTIN_TABLES[name];\n if (!decodeTable) return undefined;\n\n const table: CodePageTable = {\n decodeTable,\n encodeTable: buildEncodeTable(decodeTable),\n };\n compiledTables.set(name, table);\n return table;\n}\n\n// ---------------------------------------------------------------------------\n// Special byte translations for screen display\n// ---------------------------------------------------------------------------\n\n/**\n * EBCDIC control bytes that should display as spaces.\n */\nconst DC_TO_DISPLAY: ReadonlyMap<number, number> = new Map([\n [0x00, 0x40], // NULL -> space\n [0x0c, 0x40], // FF -> space\n [0x0d, 0x40], // CR -> space\n [0x15, 0x40], // NL -> space\n [0x19, 0x40], // EM -> space\n [0xff, 0x40], // EO -> space\n]);\n\n/**\n * Special EBCDIC control characters with Unicode display representations.\n */\nconst SPECIAL_DISPLAY_CHARS: ReadonlyMap<number, number> = new Map([\n [0x1a, 0x2218], // SUB -> solid circle\n [0x1c, 0x2611], // DUP -> check-mark\n [0x1e, 0x2612], // FM -> x-mark\n]);\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the numeric code page ID from an encoding name.\n * E.g. \"cp037\" -> 37, \"cp1047\" -> 1047\n */\nfunction extractCodePageNumber(encoding: string): number {\n const match = encoding.match(/(\\d+)$/);\n if (!match) {\n throw new Error(\n `Encoding '${encoding}' does not end in a code page number`,\n );\n }\n return parseInt(match[1], 10);\n}\n\n/**\n * Create a CodecEntry for the given encoding name.\n *\n * Built-in encodings: cp037 (US EBCDIC), cp1047 (z/OS), cp310 (APL).\n * Additional code pages can be registered with {@link registerCodePage}.\n *\n * @throws {Error} if the encoding is not supported\n */\nexport function getCodec(encoding: string): CodecEntry {\n const normalized = encoding.toLowerCase().replace('-', '');\n const table = getTable(normalized);\n\n if (!table) {\n throw new Error(`Unsupported encoding: ${encoding}`);\n }\n\n const codePageNumber = extractCodePageNumber(normalized);\n\n return {\n name: normalized,\n codePageNumber,\n encode(str: string): Buffer {\n const bytes: number[] = [];\n for (const char of str) {\n const codePoint = char.codePointAt(0)!;\n const byte = table.encodeTable.get(codePoint);\n bytes.push(byte !== undefined ? byte : 0x3f); // 0x3f = SUB\n }\n return Buffer.from(bytes);\n },\n decode(buf: Buffer): string {\n const chars: string[] = [];\n for (let i = 0; i < buf.length; i++) {\n chars.push(String.fromCodePoint(table.decodeTable[buf[i]]));\n }\n return chars.join('');\n },\n };\n}\n\n/**\n * Check if an encoding is supported.\n */\nexport function isEncodingSupported(encoding: string): boolean {\n const normalized = encoding.toLowerCase().replace('-', '');\n return normalized in BUILTIN_TABLES;\n}\n\n/**\n * Register a custom code page.\n *\n * @param name - Code page name (e.g. \"cp500\")\n * @param decodeTable - Array of 256 Unicode code points\n */\nexport function registerCodePage(\n name: string,\n decodeTable: number[],\n): void {\n if (decodeTable.length !== 256) {\n throw new Error('Decode table must have exactly 256 entries');\n }\n const normalized = name.toLowerCase().replace('-', '');\n BUILTIN_TABLES[normalized] = decodeTable;\n compiledTables.delete(normalized); // invalidate cache\n}\n\n/**\n * Translate raw EBCDIC data-character bytes into displayable bytes.\n * Replaces control characters (NULL, FF, CR, NL, EM, EO) with\n * EBCDIC space (0x40) so they render as blanks.\n */\nexport function translateDataToDisplay(data: Uint8Array): Uint8Array {\n const result = new Uint8Array(data.length);\n for (let i = 0; i < data.length; i++) {\n const replacement = DC_TO_DISPLAY.get(data[i]);\n result[i] = replacement !== undefined ? replacement : data[i];\n }\n return result;\n}\n\n/**\n * Get the Unicode display character for a special EBCDIC control byte.\n * Returns undefined if the byte is not a special display character.\n */\nexport function getSpecialDisplayChar(byte: number): string | undefined {\n const codePoint = SPECIAL_DISPLAY_CHARS.get(byte);\n if (codePoint !== undefined) {\n return String.fromCodePoint(codePoint);\n }\n return undefined;\n}\n","/**\n * Base types and utilities for TNZ core modules.\n * This file helps avoid circular dependencies between tnz.ts and its sub-modules.\n */\n\n/** General Tnz error. */\nexport class TnzError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TnzError';\n }\n}\n\n/** Error that may be related to terminal characteristics. */\nexport class TnzTerminalError extends TnzError {\n constructor(message: string) {\n super(message);\n this.name = 'TnzTerminalError';\n }\n}\n\n/** Error processing file transfer. */\nexport class TnzTransferError extends TnzError {\n constructor(message: string) {\n super(message);\n this.name = 'TnzTransferError';\n }\n}\n\n/** 3270 data stream read state. */\nexport enum ReadState {\n NORMAL = 'NORMAL',\n RENTER = 'RENTER',\n RREAD = 'RREAD',\n}\n\n/**\n * Translate 6-bit control integer to printable EBCDIC byte value.\n *\n * Used for buffer address encoding where bits 0-1 are reserved.\n * See figure D-1 in 3270 Data Stream Programmers Reference.\n *\n */\nexport function bit6(controlInt: number): number {\n controlInt &= 0x3f; // zero bits 0,1\n const cc11 = controlInt | 0xc0; // bits 0,1 = 11\n\n if (controlInt === 48) return cc11; // 11 0000 -> 0xF0\n\n const cc01 = controlInt | 0x40; // bits 0,1 = 01\n\n if (controlInt === 33) return cc01; // 10 0001 -> 0x61\n\n if ((controlInt & 0x0f) > 0 && (controlInt & 0x0f) < 10) {\n return cc11; // low nibble 1-9\n }\n\n return cc01;\n}\n","/**\n * TN3270 terminal class.\n *\n * Core class for telnet-3270 connection, protocol negotiation,\n * screen buffers, and keyboard input. One instance per terminal session.\n *\n *\n * @module core/tnz\n */\n\nimport * as net from 'node:net';\nimport * as tls from 'node:tls';\n\nimport { AID, CMD, ORDER, QR_TYPE, SF_ID, TELNET } from '../types';\nimport { TnzError, TnzTerminalError, bit6, ReadState } from './base';\nimport * as kb from './keyboard';\nimport * as screen from './screen';\nimport * as bufUtil from './buffer';\n\nimport type { CodecEntry, TnzOptions } from '../types';\nexport { TnzError, TnzTerminalError, TnzTransferError, bit6, ReadState } from './base';\nimport { getCodec } from '../utils/codepage';\nimport {\n escapeIac,\n findIacSequences,\n unescapeIac,\n} from './telnet';\n\n// ---------------------------------------------------------------------------\n// Tnz class\n// ---------------------------------------------------------------------------\n\n/** Options for Tnz.connect(). */\ninterface ConnectOptions {\n /** Use TLS/SSL for the connection (default: false) */\n secure?: boolean;\n /** Verify the server certificate (default: true) */\n verifyCert?: boolean;\n}\n\n/**\n * TN3270 terminal — one instance per connection.\n *\n * Handles telnet negotiation, 3270 data stream parsing, screen buffers,\n * and keyboard input. Used directly or wrapped by Ati for automation.\n *\n * Week 2 scope: constructor, connect, telnet negotiation, send methods,\n * wait, address helpers, buffer planes. 3270 command processing is stubbed\n * and will be implemented in Week 3.\n */\nimport { EventEmitter } from 'node:events';\n\nexport class Tnz extends EventEmitter {\n // -- Public state --\n\n /** Whether to negotiate TN3270E protocol */\n useTn3270e = false;\n /** Logical unit name for TN3270E */\n luName: string | null = null;\n /** Number of colors supported (default 768) */\n colors = 768;\n\n /** Terminal type string sent during negotiation */\n terminalType = 'IBM-DYNAMIC';\n /** Default screen rows */\n dmaxRow = 24;\n /** Default screen columns */\n dmaxCol = 80;\n /** Alternate screen rows */\n amaxRow = 24;\n /** Alternate screen columns */\n amaxCol = 80;\n /** Current screen rows */\n maxRow = 24;\n /** Current screen columns */\n maxCol = 80;\n /** Total buffer size (maxRow * maxCol) */\n bufferSize = 1920; // 24 * 80\n\n /** Cursor address (0-based into buffer) */\n curadd = 0;\n /** Current buffer address for data stream processing */\n bufadd = 0;\n /** Whether buffer addresses are 16-bit (vs 12-bit/14-bit) */\n addr16bit = false;\n\n /** Current AID (Attention Identifier) */\n aid: number = AID.NONE;\n /** PWAIT/TWAIT input inhibit */\n pwait = false;\n /** System lock input inhibit */\n systemLockWait = true;\n /** Current 3270 read state */\n readState = ReadState.NORMAL;\n /** Input operation mode */\n inop = 0x06; // right initialization (RM)\n /** Input partition ID */\n inpid = 0;\n\n /** Whether the terminal claims color capability */\n capableColor = false;\n\n /** Character buffer updated flag */\n updated = false;\n /** Session/connection lost (false=connected, true=normal close, Error=error) */\n seslost: boolean | Error = false;\n\n /** Last command string (for file transfer) */\n lastcmd: string | null = null;\n\n // -- Buffer planes (6 parallel arrays) --\n\n /** Data characters (EBCDIC) */\n planeDc: Uint8Array;\n /** Field attributes */\n planeFa: Uint8Array;\n /** Extended highlighting */\n planeEh: Uint8Array;\n /** Character set */\n planeCs: Uint8Array;\n /** Foreground color */\n planeFg: Uint8Array;\n /** Background color */\n planeBg: Uint8Array;\n\n // -- Network I/O counters --\n\n /** Total bytes sent */\n bytesSent = 0;\n /** Total bytes received */\n bytesReceived = 0;\n /** Local binary mode active */\n binaryLocal = false;\n /** Remote binary mode active */\n binaryRemote = false;\n\n // -- Telnet option negotiation tracking --\n\n /** Options we have sent DO for */\n localDo = new Set<number>();\n /** Options we have sent WILL for */\n localWill = new Set<number>();\n /** Options we have sent WONT for */\n localWont = new Set<number>();\n /** Options we have sent DONT for */\n localDont = new Set<number>();\n /** Options the remote has sent DO for */\n remoteDo = new Set<number>();\n /** Options the remote has sent WILL for */\n remoteWill = new Set<number>();\n /** Options the remote has sent WONT for */\n remoteWont = new Set<number>();\n /** Options the remote has sent DONT for */\n remoteDont = new Set<number>();\n\n // -- DDM limits --\n\n /** DDM inbound limit */\n _limin = 32639;\n /** DDM outbound limit */\n _limout = 32767;\n\n // -- Instance name --\n\n /** Name of this Tnz instance */\n name: string;\n\n // -- Encoding --\n\n tn3270eNegotiated = false;\n /** GE (Graphic Escape) support: 0=none, 1=supported for char set F1 */\n alt = 0;\n /** Character set ID for index 0x00 */\n cs00 = 697;\n /** Code page for index 0x00 */\n cp00 = 37;\n /** Character set ID for index 0xF1 */\n csF1 = 0;\n /** Code page for index 0xF1 */\n cpF1 = 0;\n\n // -- Private state --\n\n private _socket: net.Socket | tls.TLSSocket | null = null;\n private _secure = false;\n private _certVerified = false;\n private _hostVerified = false;\n private _startTlsHostname: string | null = null;\n private _startTlsCompleted = false;\n private _verifyCert = true;\n private _eor = false;\n /** @internal */ _tn3270e = false;\n private _workBuffer = Buffer.alloc(0);\n private _pendingRecord = Buffer.alloc(0);\n private _sendBuf: Buffer[] = [];\n private _waiting = false;\n private _waitRv: boolean | null = null;\n private _eventResolvers = new Set<() => void>();\n /** @internal reply mode — used in Week 3 command processing */\n _replyMode = 0; // Field mode\n /** @internal reply character attrs — used in Week 3 command processing */\n _replyCattrs = Buffer.alloc(0);\n private _extendedColorMode = false;\n /** @internal extended highlighting proc state — used in Week 3 */\n _procEh = 0;\n /** @internal character set proc state — used in Week 3 */\n _procCs = 0;\n /** @internal foreground color proc state — used in Week 3 */\n _procFg = 0;\n /** @internal background color proc state — used in Week 3 */\n _procBg = 0;\n private _encoding = 'cp037';\n private _codec: CodecEntry;\n /** @internal */ _codecF1: CodecEntry | null = null;\n \n /** Optional callback fired when the screen is updated by the host. */\n onScreenUpdate?: () => void;\n\n // -- Read lines (stub for later) --\n readlines: unknown = null;\n readlinesPa2 = true;\n\n constructor(name?: string, options?: TnzOptions) {\n super();\n this.name = name ?? `tnz-${Date.now()}`;\n\n // Apply options\n if (options) {\n if (options.useTn3270e !== undefined) {\n this.useTn3270e = options.useTn3270e;\n }\n if (options.luName !== undefined) this.luName = options.luName;\n if (options.terminalType !== undefined) {\n this.terminalType = options.terminalType;\n }\n if (options.amaxRow !== undefined) this.amaxRow = options.amaxRow;\n if (options.amaxCol !== undefined) this.amaxCol = options.amaxCol;\n if (options.encoding !== undefined) this._encoding = options.encoding;\n if (options.onScreenUpdate !== undefined) {\n this.onScreenUpdate = options.onScreenUpdate;\n }\n }\n\n // Initialize codec\n this._codec = getCodec(this._encoding);\n this.cp00 = this._codec.codePageNumber;\n\n // Initialize buffer planes\n this.planeDc = new Uint8Array(this.bufferSize);\n this.planeFa = new Uint8Array(this.bufferSize);\n this.planeEh = new Uint8Array(this.bufferSize);\n this.planeCs = new Uint8Array(this.bufferSize);\n this.planeFg = new Uint8Array(this.bufferSize);\n this.planeBg = new Uint8Array(this.bufferSize);\n }\n\n // -- Encoding property --\n\n /** Get the current EBCDIC encoding name. */\n get encoding(): string {\n return this._encoding;\n }\n\n /** Set the EBCDIC encoding (e.g. 'cp037', 'cp1047'). */\n set encoding(value: string) {\n this._codec = getCodec(value);\n this._encoding = value;\n this.cp00 = this._codec.codePageNumber;\n }\n\n /** Get the primary codec. */\n get codec(): CodecEntry {\n return this._codec;\n }\n\n /**\n * Register a secondary encoding for Graphic Escape (GE) character set.\n * Used for APL characters (cp310) on char set index 0xF1.\n */\n setGeEncoding(encoding: string): void {\n this._codecF1 = getCodec(encoding);\n this.cpF1 = this._codecF1.codePageNumber;\n if (this.cpF1 === 310) {\n this.alt = 1; // Support GE for char set ID F1\n this.csF1 = 963;\n } else {\n this.csF1 = 697;\n }\n }\n\n // -- Connection state properties --\n\n /** Whether the connection is secure (TLS). */\n get secure(): boolean {\n return this._secure;\n }\n\n /** Whether the server certificate was verified. */\n get certVerified(): boolean {\n return this._certVerified;\n }\n\n /** Whether the server hostname was verified. */\n get hostVerified(): boolean {\n return this._hostVerified;\n }\n\n /** Whether STARTTLS upgrade completed. */\n get startTlsCompleted(): boolean {\n return this._startTlsCompleted;\n }\n\n /** Whether TN3270E protocol is in use. */\n get tn3270e(): boolean {\n return this._tn3270e;\n }\n\n /** Whether EOR mode is active. */\n get eorMode(): boolean {\n return this._eor;\n }\n\n /** Whether screen uses extended color mode. */\n get extendedColorMode(): boolean {\n return this._extendedColorMode;\n }\n\n /** Whether DDM transfer is in progress. */\n ddmInProgress(): boolean {\n return false; // stub — Phase 3\n }\n\n // =========================================================================\n // Connection\n // =========================================================================\n\n /**\n * Connect to a TN3270 host.\n *\n * @param host - Hostname or IP (default: '127.0.0.1')\n * @param port - Port number (default: 23 for plain, 992 for TLS)\n * @param options - Connection options (secure, verifyCert)\n * @throws {TnzError} if already connected\n *\n */\n async connect(\n host?: string,\n port?: number,\n options?: ConnectOptions,\n ): Promise<void> {\n if (this._socket) {\n throw new TnzError('Already connected');\n }\n\n const secure = options?.secure ?? false;\n const verifyCert = options?.verifyCert ?? true;\n this._verifyCert = verifyCert;\n const actualHost = host ?? '127.0.0.1';\n const actualPort = port ?? (secure ? 992 : 23);\n\n // Save hostname for potential STARTTLS later\n if (!secure && host) {\n this._startTlsHostname = actualHost;\n }\n\n return new Promise<void>((resolve, reject) => {\n let socket: net.Socket;\n\n const onConnect = (): void => {\n this.seslost = false;\n if (secure && socket instanceof tls.TLSSocket) {\n this._secure = true;\n if (verifyCert) {\n this._certVerified = !socket.authorizationError;\n this._hostVerified = !socket.authorizationError;\n }\n }\n resolve();\n };\n\n const onError = (err: Error): void => {\n this.seslost = err;\n this._setEvent();\n // Only reject if we haven't resolved yet\n reject(err);\n };\n\n if (secure) {\n socket = tls.connect(\n {\n host: actualHost,\n port: actualPort,\n rejectUnauthorized: verifyCert,\n servername: actualHost,\n },\n onConnect,\n );\n } else {\n socket = net.createConnection(\n { host: actualHost, port: actualPort },\n onConnect,\n );\n }\n\n this._socket = socket;\n socket.on('data', (data: Buffer) => this._dataReceived(data));\n socket.on('error', onError);\n socket.on('close', () => {\n if (!this.seslost) this.seslost = true;\n this._socket = null;\n this._setEvent();\n });\n socket.on('end', () => {\n // EOF — server closed its end\n });\n });\n }\n\n /**\n * Upgrade the current plaintext connection to TLS (STARTTLS).\n *\n * Called when the server sends IAC SB START_TLS FOLLOWS.\n *\n */\n private async _startTls(): Promise<void> {\n const oldSocket = this._socket;\n if (!oldSocket) {\n throw new TnzError('No socket to upgrade');\n }\n\n // Remove listeners from plain socket (TLS will wrap it)\n oldSocket.removeAllListeners('data');\n oldSocket.removeAllListeners('error');\n oldSocket.removeAllListeners('close');\n oldSocket.removeAllListeners('end');\n this._socket = null;\n\n return new Promise<void>((resolve, reject) => {\n const tlsSocket = tls.connect({\n socket: oldSocket,\n rejectUnauthorized: this._verifyCert,\n servername: this._startTlsHostname ?? undefined,\n });\n\n tlsSocket.once('secureConnect', () => {\n this._socket = tlsSocket;\n this._secure = true;\n this._startTlsCompleted = true;\n if (this._verifyCert) {\n this._certVerified = !tlsSocket.authorizationError;\n this._hostVerified = !tlsSocket.authorizationError;\n }\n\n // Re-attach event handlers\n tlsSocket.on('data', (data: Buffer) => this._dataReceived(data));\n tlsSocket.on('error', (err: Error) => {\n this.seslost = err;\n this._setEvent();\n });\n tlsSocket.on('close', () => {\n if (!this.seslost) this.seslost = true;\n this._socket = null;\n this._setEvent();\n });\n\n // Flush any buffered data\n this.send();\n resolve();\n });\n\n tlsSocket.once('error', (err: Error) => {\n this.seslost = err;\n this._setEvent();\n reject(err);\n });\n });\n }\n\n /**\n * Close the connection immediately (abort).\n *\n */\n close(): void {\n const socket = this._socket;\n if (socket) {\n this._socket = null;\n socket.destroy();\n }\n }\n\n /**\n * Shut down the connection gracefully.\n *\n */\n shutdown(): void {\n this.close();\n }\n\n // =========================================================================\n // Wait\n // =========================================================================\n\n /**\n * Signal that an event occurred (data received, session lost, etc.).\n * Wakes up any pending wait() call.\n */\n private _setEvent(): void {\n for (const resolve of this._eventResolvers) {\n resolve();\n }\n this._eventResolvers.clear();\n }\n\n /**\n * Wait for an event (data received, timeout, or session lost).\n *\n * @param timeout - Timeout in seconds (undefined = wait forever)\n * @returns true if session lost, false if timeout, null otherwise\n * @throws {TnzError} if already waiting\n *\n */\n async wait(timeout?: number): Promise<boolean | null> {\n if (this.seslost) {\n return true;\n }\n\n if (this._waiting) {\n throw new TnzError('Already waiting');\n }\n\n this._waiting = true;\n this._waitRv = null;\n\n try {\n await new Promise<void>((resolve) => {\n this._eventResolvers.add(resolve);\n\n if (timeout !== undefined) {\n const timer = setTimeout(() => {\n this._eventResolvers.delete(resolve);\n if (this._waitRv === null) {\n this._waitRv = false;\n }\n resolve();\n }, timeout * 1000);\n\n // Store original resolve to clear timer on event\n const originalResolve = resolve;\n this._eventResolvers.delete(resolve);\n const wrappedResolve = (): void => {\n clearTimeout(timer);\n originalResolve();\n };\n this._eventResolvers.add(wrappedResolve);\n }\n });\n\n if (this.seslost) return true;\n return this._waitRv;\n } finally {\n this._waitRv = null;\n this._waiting = false;\n }\n }\n\n // =========================================================================\n // Send methods\n // =========================================================================\n\n /**\n * Send data to the host (IAC-escaped) and flush the send buffer.\n *\n * If data is provided, it is IAC-escaped and appended to the buffer.\n * Then all buffered data is written to the socket.\n *\n */\n send(data?: Buffer): void {\n if (data && data.length > 0) {\n this._sendBuf.push(escapeIac(data));\n }\n\n const socket = this._socket;\n if (!socket) return;\n if (socket.closed) return;\n\n if (this._sendBuf.length === 0) return;\n\n const combined = Buffer.concat(this._sendBuf);\n socket.write(combined);\n this.bytesSent += combined.length;\n this._sendBuf.length = 0;\n }\n\n /**\n * Send a 3270-DATA record to the host.\n * Adds TN3270E header if in TN3270E mode, IAC-escapes, appends IAC EOR.\n *\n */\n send3270Data(value: Buffer): void {\n const escaped = escapeIac(value);\n if (this._tn3270e) {\n // 3270-DATA TN3270E header: 5 zero bytes\n this._sendBuf.push(Buffer.alloc(5));\n }\n this._sendBuf.push(escaped);\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.EOR]));\n this.send();\n }\n\n /**\n * Send a record to the host with IAC escaping and EOR.\n * (No TN3270E header — used for TN3270E RESPONSE records.)\n *\n */\n sendRec(value: Buffer): void {\n const escaped = escapeIac(value);\n this._sendBuf.push(escaped);\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.EOR]));\n this.send();\n }\n\n /**\n * Send IAC WILL to the host.\n *\n */\n sendWill(opt: number, buffer = false): void {\n this.localWill.add(opt);\n this.localWont.delete(opt);\n\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.WILL, opt]));\n if (!buffer) this.send();\n }\n\n /**\n * Send IAC WONT to the host.\n *\n */\n sendWont(opt: number, buffer = false): void {\n this.localWont.add(opt);\n this.localWill.delete(opt);\n\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.WONT, opt]));\n if (!buffer) this.send();\n }\n\n /**\n * Send IAC DO to the host.\n *\n */\n sendDo(opt: number, buffer = false): void {\n if (opt === TELNET.OPT_BINARY) {\n this.binaryRemote = true;\n } else if (opt === TELNET.OPT_EOR) {\n this._eor = true;\n }\n\n this.localDo.add(opt);\n this.localDont.delete(opt);\n\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.DO, opt]));\n if (!buffer) this.send();\n }\n\n /**\n * Send IAC DONT to the host.\n *\n */\n sendDont(opt: number, buffer = false): void {\n if (opt === TELNET.OPT_BINARY) {\n this.binaryRemote = false;\n } else if (opt === TELNET.OPT_EOR) {\n this._eor = false;\n }\n\n this.localDont.add(opt);\n this.localDo.delete(opt);\n\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.DONT, opt]));\n if (!buffer) this.send();\n }\n\n /**\n * Send subnegotiation data (bookended with IAC SB ... IAC SE).\n *\n */\n sendSub(value: Buffer, buffer = false): void {\n const escaped = escapeIac(value);\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.SB]));\n this._sendBuf.push(escaped);\n this._sendBuf.push(Buffer.from([TELNET.IAC, TELNET.SE]));\n if (!buffer) this.send();\n }\n\n /**\n * Send the terminal type subnegotiation.\n *\n */\n sendTerminalType(buffer = false): void {\n const data = Buffer.concat([\n Buffer.from([TELNET.OPT_TERMINAL_TYPE, TELNET.TERMINAL_TYPE_IS]),\n Buffer.from(this.terminalType, 'ascii'),\n ]);\n this.sendSub(data, buffer);\n }\n\n /**\n * Send a single-byte telnet command (e.g., NOP, BRK, IP).\n *\n * @param code - Command code (241-249)\n * @throws {TnzError} if code is not in valid range\n *\n */\n sendCommand(code: number): void {\n if (code < 241 || code > 249) {\n throw new TnzError(`Telnet command ${code} not valid`);\n }\n this._sendBuf.push(Buffer.from([TELNET.IAC, code]));\n this.send();\n }\n\n // =========================================================================\n // Address helpers\n // =========================================================================\n\n /**\n * Decode a 2-byte encoded buffer address to an integer.\n *\n * Handles 12-bit (6+6), 14-bit, and 16-bit addressing modes.\n *\n */\n address(addressBytes: Buffer): number {\n if (addressBytes.length !== 2) {\n throw new TnzError('address_bytes must be exactly 2 bytes');\n }\n\n const byte0 = addressBytes[0];\n const byte1 = addressBytes[1];\n\n // 12-bit mode: bit 0 of byte0 is set (0x40 mask)\n if (!this.addr16bit && byte0 & 0x40) {\n const high6 = byte0 & 0x3f;\n const low6 = byte1 & 0x3f;\n return high6 * 64 + low6;\n }\n\n // Reserved mode check\n if (!this.addr16bit && byte0 & 0x80) {\n throw new TnzError('reserved address mode');\n }\n\n // 14-bit or 16-bit mode\n let addr = (byte0 << 8) | byte1;\n\n // Weird case: 16-bit addr > buffer_size, try as 12-bit\n if (this.addr16bit && addr > this.bufferSize) {\n this.addr16bit = false;\n addr = this.address(addressBytes);\n this.addr16bit = true;\n }\n\n return addr;\n }\n\n\n // =========================================================================\n // Telnet negotiation and sending\n // =========================================================================\n\n /**\n * Handle pending record data between IAC commands.\n *\n */\n private _dataTelnet(buff: Buffer, start: number, stop: number): void {\n if (start >= stop) return;\n\n if (!this._eor) {\n // Unexpected data in non-EOR mode\n return;\n }\n\n this._pendingRecord = Buffer.concat([\n this._pendingRecord,\n buff.subarray(start, stop),\n ]);\n }\n\n /**\n * Process received data from the socket.\n *\n * Parses telnet IAC sequences, handles EOR records, subnegotiation,\n * and dispatches to _process() and _proc3270ds().\n *\n * @param buff - Raw bytes received from the socket\n * @returns Number of bytes consumed\n *\n */\n _dataReceived(buff: Buffer): number {\n // Prepend any leftover bytes from previous call\n if (this._workBuffer.length > 0) {\n buff = Buffer.from(Buffer.concat([this._workBuffer, buff]));\n }\n\n let byteStart = 0;\n let subcStart: number | null = null;\n\n try {\n const matches = findIacSequences(buff);\n\n for (const mat of matches) {\n const cmdByte = mat.command;\n\n // IAC IAC — escaped data byte 0xFF, skip\n if (cmdByte === 0xff) continue;\n\n if (subcStart !== null) {\n // Inside subnegotiation — waiting for IAC SE\n if (cmdByte === TELNET.SE) {\n // Process subneg: includes IAC SB prefix, excludes IAC SE\n this._process(buff.subarray(subcStart, mat.start));\n byteStart = mat.end;\n subcStart = null;\n }\n // Ignore other commands inside subnegotiation\n } else if (cmdByte === TELNET.EOR && this._eor) {\n // End of record\n this._waitRv = true;\n this._setEvent();\n\n // Collect record data: pending + current chunk\n const recRaw = Buffer.from(\n Buffer.concat([\n this._pendingRecord,\n Buffer.from(buff.subarray(byteStart, mat.start)),\n ]),\n );\n const rec = unescapeIac(recRaw);\n this._pendingRecord = Buffer.alloc(0);\n this.bytesReceived += rec.length;\n byteStart = mat.end;\n\n try {\n this._proc3270ds(rec);\n } catch (err) {\n if (err instanceof TnzError) {\n this.seslost = err;\n return byteStart;\n }\n throw err;\n }\n } else if (cmdByte === TELNET.SB) {\n // Start of subnegotiation\n subcStart = mat.start; // includes IAC SB\n this._dataTelnet(buff, byteStart, subcStart);\n byteStart = subcStart;\n } else {\n // Other IAC command (DO, DONT, WILL, WONT, etc.)\n this._dataTelnet(buff, byteStart, mat.start);\n byteStart = mat.end;\n\n // Build the full command buffer for _process\n const cmdLen = mat.end - mat.start;\n this._process(buff.subarray(mat.start, mat.start + cmdLen));\n }\n }\n\n // Non-EOR mode: process any remaining data\n if (!this._eor && byteStart < buff.length) {\n this._dataTelnet(buff, byteStart, buff.length);\n byteStart = buff.length;\n }\n } finally {\n // Save unprocessed bytes for next call\n this._workBuffer = Buffer.from(buff.subarray(byteStart));\n }\n\n return byteStart;\n }\n\n // =========================================================================\n // Telnet option negotiation\n // =========================================================================\n\n /**\n * Process a telnet command or subnegotiation.\n *\n * The data buffer starts with IAC and contains the full command.\n * For subnegotiations, it includes IAC SB ... (without trailing IAC SE).\n *\n */\n _process(data: Buffer): void {\n if (data.length < 2) return;\n\n const iacByte = data[0];\n const cmdByte = data[1];\n\n if (iacByte !== TELNET.IAC) return;\n\n // ----- IAC DO -----\n if (cmdByte === TELNET.DO && data.length >= 3) {\n const opt = data[2];\n\n if (opt === TELNET.OPT_TN3270E) {\n if (this.useTn3270e) {\n this.sendWill(opt, true);\n } else {\n this.sendWont(opt, true);\n }\n } else if (opt === TELNET.OPT_BINARY) {\n if (!this.localWill.has(opt)) {\n this.sendWill(opt, true);\n }\n } else if (opt === TELNET.OPT_TERMINAL_TYPE) {\n if (!this.localWill.has(opt)) {\n this.sendWill(opt, true);\n }\n } else if (opt === TELNET.OPT_EOR) {\n if (!this.localWill.has(opt)) {\n this.sendWill(opt, true);\n }\n if (!this.localDo.has(opt)) {\n this.sendDo(opt, true);\n }\n } else if (opt === TELNET.OPT_START_TLS) {\n if (!this.localWill.has(opt)) {\n this.sendWill(opt, true);\n }\n // Send START_TLS FOLLOWS subnegotiation\n this.sendSub(\n Buffer.from([TELNET.OPT_START_TLS, TELNET.START_TLS_FOLLOWS]),\n true,\n );\n } else {\n // Unknown option (e.g. Timing mark) — refuse\n this.sendWont(opt, true);\n }\n\n this.remoteDo.add(opt);\n this.remoteDont.delete(opt);\n\n // ----- IAC DONT -----\n } else if (cmdByte === TELNET.DONT && data.length >= 3) {\n const opt = data[2];\n\n this.remoteDont.add(opt);\n this.remoteDo.delete(opt);\n\n // Don't send WONT for BINARY or EOR\n if (opt !== TELNET.OPT_BINARY && opt !== TELNET.OPT_EOR) {\n if (!this.localWont.has(opt)) {\n this.sendWont(opt, true);\n }\n }\n\n // ----- IAC WILL -----\n } else if (cmdByte === TELNET.WILL && data.length >= 3) {\n const opt = data[2];\n\n // TRANSMIT-BINARY: grant permission\n if (opt === TELNET.OPT_BINARY && !this.binaryRemote) {\n this.sendDo(opt, true);\n }\n\n this.remoteWill.add(opt);\n this.remoteWont.delete(opt);\n\n // ----- IAC WONT -----\n } else if (cmdByte === TELNET.WONT && data.length >= 3) {\n const opt = data[2];\n\n this.remoteWont.add(opt);\n this.remoteWill.delete(opt);\n\n // ----- IAC EOR -----\n } else if (cmdByte === TELNET.EOR) {\n // Handled in _dataReceived\n // (this case is for when EOR appears without eor mode)\n\n // ----- IAC SB TN3270E SEND DEVICE-TYPE -----\n } else if (\n data.length === 5 &&\n data[1] === TELNET.SB &&\n data[2] === TELNET.OPT_TN3270E &&\n data[3] === 0x08 && // SEND\n data[4] === TELNET.TN3270E_DEVICE_TYPE // 0x02\n ) {\n // Reply: TN3270E DEVICE-TYPE REQUEST <terminal-type>\n const rsp: number[] = [\n TELNET.OPT_TN3270E,\n TELNET.TN3270E_DEVICE_TYPE,\n TELNET.TN3270E_REQUEST,\n ];\n const ttBytes = Buffer.from(this.terminalType, 'ascii');\n const rspBuf = Buffer.concat([Buffer.from(rsp), ttBytes]);\n\n if (this.luName) {\n // Add CONNECT <lu_name>\n const luBytes = Buffer.from(this.luName, 'ascii');\n const connectBuf = Buffer.concat([\n rspBuf,\n Buffer.from([0x01]), // CONNECT\n luBytes,\n ]);\n this.sendSub(connectBuf);\n } else {\n this.sendSub(rspBuf);\n }\n\n // ----- IAC SB TN3270E DEVICE-TYPE IS -----\n } else if (\n data.length >= 5 &&\n data[1] === TELNET.SB &&\n data[2] === TELNET.OPT_TN3270E &&\n data[3] === TELNET.TN3270E_DEVICE_TYPE &&\n data[4] === TELNET.TN3270E_IS\n ) {\n // Request FUNCTIONS: RESPONSES (0x02)\n const funb = Buffer.from([TELNET.TN3270E_RESPONSES]);\n this.sendSub(\n Buffer.from([\n TELNET.OPT_TN3270E,\n TELNET.TN3270E_FUNCTIONS,\n TELNET.TN3270E_REQUEST,\n ...funb,\n ]),\n );\n\n // Enter TN3270E mode\n this.binaryLocal = true;\n this.binaryRemote = true;\n this._eor = true;\n this._tn3270e = true;\n\n // ----- IAC SB TN3270E FUNCTIONS IS -----\n } else if (\n data.length >= 5 &&\n data[1] === TELNET.SB &&\n data[2] === TELNET.OPT_TN3270E &&\n data[3] === TELNET.TN3270E_FUNCTIONS &&\n data[4] === TELNET.TN3270E_IS\n ) {\n // Functions confirmed — log only\n\n // ----- IAC SB TERMINAL-TYPE SEND -----\n } else if (\n data.length === 4 &&\n data[1] === TELNET.SB &&\n data[2] === TELNET.OPT_TERMINAL_TYPE &&\n data[3] === TELNET.TERMINAL_TYPE_SEND\n ) {\n this.sendTerminalType(true);\n\n // ----- IAC SB START_TLS FOLLOWS -----\n } else if (\n data.length === 4 &&\n data[1] === TELNET.SB &&\n data[2] === TELNET.OPT_START_TLS &&\n data[3] === TELNET.START_TLS_FOLLOWS\n ) {\n // Initiate TLS upgrade (async — fire and forget)\n this._startTls().catch((err: unknown) => {\n const error =\n err instanceof Error ? err : new Error(String(err));\n this.seslost = error;\n this._setEvent();\n });\n\n // ----- IAC command (NOP through GA, 241-249) -----\n } else if (\n iacByte === TELNET.IAC &&\n cmdByte >= 241 &&\n cmdByte <= 249\n ) {\n // Log only — no action needed\n\n // ----- Unknown -----\n } else {\n // Unknown telnet sequence — log as warning\n }\n\n // Flush any buffered responses\n this.send();\n }\n\n // =========================================================================\n // 3270 data stream processing\n // =========================================================================\n\n /**\n * Process a complete 3270 data stream record.\n *\n * Handles TN3270E headers (if in TN3270E mode) and dispatches\n * to command-specific processors.\n *\n * Week 2: TN3270E header parsing + dispatch framework.\n * Week 3: actual command processing (Write, Erase/Write, etc.)\n *\n */\n _proc3270ds(data: Buffer): void {\n if (data.length === 0) return;\n\n let responseFlag = 0;\n let seqNumber = 0;\n\n if (this._tn3270e) {\n if (data.length < 5) {\n throw new TnzError('TN3270E record too short for header');\n }\n\n const header = data.subarray(0, 5);\n data = data.subarray(5);\n\n const dataType = header[0];\n // const requestFlag = header[1];\n responseFlag = header[2];\n seqNumber = (header[3] << 8) | header[4];\n\n if (dataType === 0) {\n // 3270-DATA — continue processing\n } else if (dataType === 1) {\n throw new TnzError('DATA-TYPE SCS-DATA not implemented');\n } else if (dataType === 2) {\n throw new TnzError('DATA-TYPE RESPONSE not implemented');\n } else if (dataType === 3) {\n throw new TnzError('DATA-TYPE BIND-IMAGE not implemented');\n } else if (dataType === 4) {\n throw new TnzError('DATA-TYPE UNBIND not implemented');\n } else if (dataType === 5) {\n throw new TnzError('DATA-TYPE NVT-DATA not implemented');\n } else if (dataType === 6) {\n throw new TnzError('DATA-TYPE REQUEST not implemented');\n } else if (dataType === 7) {\n throw new TnzError('DATA-TYPE SSCP-LU-DATA not implemented');\n } else {\n throw new TnzError(`DATA-TYPE ${dataType} not implemented`);\n }\n }\n\n if (data.length === 0) return;\n\n // Dispatch to command processor\n const command = data[0];\n this._processCommand(data, command);\n\n // Send TN3270E response if requested\n if (responseFlag === 2) {\n const rsp = Buffer.from([\n 0x02, // DATA-TYPE=RESPONSE\n 0x00, // REQUEST-FLAG=0\n 0x00, // success\n (seqNumber >> 8) & 0xff,\n seqNumber & 0xff,\n 0x00, // Device End (successful)\n ]);\n this.sendRec(rsp);\n }\n }\n\n /**\n * Dispatch a 3270 command to the appropriate processor.\n *\n */\n private _processCommand(data: Buffer, command: number): void {\n const start = 0;\n const stop = data.length;\n\n switch (command) {\n case CMD.WRITE: // 0xF1\n case 0x01: // SCS alias\n this._processW(data, start, stop);\n break;\n case CMD.ERASE_WRITE: // 0xF5\n case 0x05: // SCS alias\n this._processEw(data, start, stop);\n break;\n case CMD.ERASE_WRITE_ALTERNATE: // 0x7E\n case 0x0d: // SCS alias\n this._processEwa(data, start, stop);\n break;\n case CMD.READ_BUFFER: // 0xF2\n case 0x02: // SCS alias\n if (stop - start !== 1) {\n throw new TnzError(`RB must be 1 byte, got ${stop - start}`);\n }\n this.readState = ReadState.NORMAL;\n this._readBuffer();\n break;\n case CMD.READ_MODIFIED: // 0xF6\n case 0x06: // SCS alias\n if (stop - start !== 1) {\n throw new TnzError(`RM must be 1 byte, got ${stop - start}`);\n }\n this.sendAid(this.aid);\n break;\n case CMD.READ_MODIFIED_ALL: // 0x6E\n if (stop - start !== 1) {\n throw new TnzError(`RMA must be 1 byte, got ${stop - start}`);\n }\n this.readState = ReadState.NORMAL;\n this.sendAid(this.aid, false);\n break;\n case CMD.ERASE_ALL_UNPROTECTED: // 0x6F\n case 0x0f: // SCS alias\n if (stop - start !== 1) {\n throw new TnzError(`EAU must be 1 byte, got ${stop - start}`);\n }\n this._processEau();\n break;\n case CMD.WRITE_STRUCTURED_FIELD: // 0xF3\n case 0x11: // SCS alias\n this._processWsf(data, start, stop);\n break;\n default:\n throw new TnzError(\n `Unknown 3270 command: 0x${command.toString(16)}`,\n );\n }\n\n if (this.onScreenUpdate) {\n this.onScreenUpdate();\n }\n }\n\n // =========================================================================\n // Buffer helpers (Delegated)\n // =========================================================================\n\n static ucba(buf: Uint8Array, addr: number, bytes: Uint8Array | number[], start = 0, end?: number): void { bufUtil.ucba(buf, addr, bytes, start, end); }\n static rcba(buf: Uint8Array, saddr: number, eaddr: number): Uint8Array { return bufUtil.rcba(buf, saddr, eaddr); }\n \n addressBytes(addr: number): Buffer { return bufUtil.addressBytes(this, addr); }\n\n /** @internal */ _checkAddress(address: number): void { bufUtil._checkAddress(this, address); }\n /** @internal */ _erase(saddr: number, eaddr: number): void { bufUtil._erase(this, saddr, eaddr); }\n /** @internal */ _eraseInput(saddr: number, eaddr: number): void { bufUtil._eraseInput(this, saddr, eaddr); }\n /** @internal */ _field(address: number): [number, number] { return bufUtil._field(this, address); }\n nextField(address: number): [number, number] { return bufUtil.nextField(this, address); }\n _charAddrs(saddr: number, eaddr: number): Generator<number> { return bufUtil._charAddrs(this, saddr, eaddr); }\n fields(saddr?: number, eaddr?: number): Generator<[number, number]> { return bufUtil.fields(this, saddr, eaddr); }\n /** @internal */ _tab(saddr: number, _eaddr = 0): number { return bufUtil._tab(this, saddr, _eaddr); }\n\n /** @internal */ addressDecode(data: Buffer, start: number): number {\n return bufUtil.addressDecode(data, start);\n }\n\n /** @internal */ _processSa(cat: number, cav: number, addr?: number): void {\n if (cat === 0x00) {\n // Reset all character attributes\n if (addr !== undefined) {\n this.planeEh[addr] = 0;\n this.planeCs[addr] = 0;\n this.planeFg[addr] = 0;\n this.planeBg[addr] = 0;\n } else {\n this._procEh = 0;\n this._procCs = 0;\n this._procFg = 0;\n this._procBg = 0;\n }\n } else if (cat === 0x41) {\n if (addr !== undefined) this.planeEh[addr] = cav;\n else this._procEh = cav;\n } else if (cat === 0x42) {\n if (!this._extendedColorMode) this._extendedColorMode = true;\n if (addr !== undefined) this.planeFg[addr] = cav;\n else this._procFg = cav;\n } else if (cat === 0x43) {\n if (addr !== undefined) this.planeCs[addr] = cav;\n else this._procCs = cav;\n } else if (cat === 0x45) {\n if (!this._extendedColorMode) this._extendedColorMode = true;\n if (addr !== undefined) this.planeBg[addr] = cav;\n else this._procBg = cav;\n } else {\n throw new TnzError(`Bad character attribute type: 0x${cat.toString(16)}`);\n }\n }\n\n // =========================================================================\n // State reset helpers\n // =========================================================================\n\n /**\n * Reset buffer planes and optionally resize to alternate screen.\n *\n */\n eraseReset(useAlternate = false): void {\n this._extendedColorMode = false;\n\n if (useAlternate) {\n this.maxRow = this.amaxRow;\n this.maxCol = this.amaxCol;\n } else {\n this.maxRow = this.dmaxRow;\n this.maxCol = this.dmaxCol;\n }\n\n const bufSize = this.maxRow * this.maxCol;\n this.bufferSize = bufSize;\n this.planeDc = new Uint8Array(bufSize);\n this.planeFa = new Uint8Array(bufSize);\n this.planeEh = new Uint8Array(bufSize);\n this.planeCs = new Uint8Array(bufSize);\n this.planeFg = new Uint8Array(bufSize);\n this.planeBg = new Uint8Array(bufSize);\n this.addr16bit = bufSize >= 16384;\n this.curadd = 0;\n }\n\n /**\n * Reset the MDT (Modified Data Tag) for all fields.\n *\n */\n /** @internal */ _resetMdt(): void {\n const planeFa = this.planeFa;\n for (let i = 0; i < this.bufferSize; i++) {\n const fattr = planeFa[i];\n if (fattr) {\n const nattr = bit6(fattr & 0xfe); // turn off MDT (bit 0)\n if (fattr !== nattr) {\n planeFa[i] = nattr;\n }\n }\n }\n }\n\n /**\n * Reset partition state.\n *\n */\n /** @internal */ _resetPartition(): void {\n this._replyMode = 0; // Field mode\n this._replyCattrs = Buffer.alloc(0);\n }\n\n /**\n * Restore keyboard after host processing.\n *\n */\n /** @internal */ _restoreKeyboard(): void {\n this.aid = AID.NONE;\n this.readState = ReadState.NORMAL;\n this.systemLockWait = false;\n this.inop = 0x06; // RM\n this.pwait = false;\n }\n\n /**\n * Set field attributes from SFE/MF attribute pairs.\n *\n * @returns new index past the consumed attribute bytes\n *\n */\n /** @internal */ _setAttributes(\n addr: number,\n data: Buffer,\n idx: number,\n ): number {\n const count = data[idx];\n let pos = idx + 1;\n\n for (let i = 0; i < count; i++) {\n const attrType = data[pos];\n const attrValue = data[pos + 1];\n pos += 2;\n\n if (attrType === 0xc0) {\n // 3270 field attribute\n this.planeFa[addr] = bit6(attrValue);\n } else if (attrType === 0x41) {\n // Extended highlighting\n this.planeEh[addr] = attrValue;\n } else if (attrType === 0x42) {\n // Foreground color\n if (!this._extendedColorMode) {\n this._extendedColorMode = true;\n }\n this.planeFg[addr] = attrValue;\n } else if (attrType === 0x43) {\n // Character set\n this.planeCs[addr] = attrValue;\n } else if (attrType === 0x45) {\n // Background color\n if (!this._extendedColorMode) {\n this._extendedColorMode = true;\n }\n this.planeBg[addr] = attrValue;\n } else {\n throw new TnzError(`Bad field attribute type: ${attrType}`);\n }\n }\n\n return pos;\n }\n\n // =========================================================================\n // WCC processing\n // =========================================================================\n\n /**\n * Process a WCC (Write Control Character).\n *\n * When forMdt=true, only the MDT reset bit is checked (called before\n * orders processing). When forMdt=false, the full WCC is processed\n * (called after orders processing).\n *\n */\n /** @internal */ _processWcc(wcc: number, forMdt = false): void {\n if (forMdt) {\n if (wcc & 0x01) {\n // Bit 7: reset modified data tags\n this._resetMdt();\n }\n } else {\n if (wcc & 0x40) {\n // Bit 1: reset partition\n this._resetPartition();\n }\n // Bit 4 (0x08): start printer — not implemented\n // Bit 5 (0x04): sound alarm — not implemented in headless\n if (wcc & 0x02) {\n // Bit 6: keyboard restore\n this._restoreKeyboard();\n }\n }\n }\n\n // =========================================================================\n // Order processing\n // =========================================================================\n\n /** Order byte values that trigger order processing */\n private static readonly ORDER_BYTES = new Set<number>([\n ORDER.PT, // 0x05\n ORDER.GE, // 0x08\n ORDER.SBA, // 0x11\n ORDER.EUA, // 0x12\n ORDER.IC, // 0x13\n ORDER.SF, // 0x1D\n ORDER.SA, // 0x28\n ORDER.SFE, // 0x29\n ORDER.MF, // 0x2C\n ORDER.RA, // 0x3C\n ]);\n\n /**\n * Find the next order byte in the data stream.\n * Returns -1 if no order found.\n */\n private static _findOrder(\n data: Buffer,\n start: number,\n end: number,\n ): number {\n for (let i = start; i < end; i++) {\n if (Tnz.ORDER_BYTES.has(data[i])) return i;\n }\n return -1;\n }\n\n /**\n * Process orders and data in a 3270 write data stream.\n *\n */\n private _processOrdersData(\n data: Buffer,\n start: number,\n end: number,\n ): void {\n this.bufadd = this.curadd;\n this._procEh = 0;\n this._procCs = 0;\n this._procFg = 0;\n this._procBg = 0;\n let ptErase = false;\n\n while (start < end) {\n const ordIdx = Tnz._findOrder(data, start, end);\n\n if (ordIdx < 0) {\n // No more orders — rest is character data\n this._processCharData(data, start, end);\n return;\n }\n\n if (start < ordIdx) {\n // Character data before the order\n this._processCharData(data, start, ordIdx);\n ptErase = true;\n }\n\n const result = this._processOrder(data, ordIdx, end, ptErase);\n start = result.nextIdx;\n ptErase = result.ptErase;\n }\n }\n\n /**\n * Dispatch a single order.\n *\n */\n private _processOrder(\n data: Buffer,\n start: number,\n stop: number,\n ptErase: boolean,\n ): { nextIdx: number; ptErase: boolean } {\n const orderByte = data[start];\n const bufSize = this.bufferSize;\n\n switch (orderByte) {\n // ----- PT (Program Tab) 0x05 -----\n case ORDER.PT: {\n const oldadd = this.bufadd;\n if (!this.planeFa[oldadd] && ptErase) {\n const [addr0] = this.nextField(oldadd);\n if (addr0 > 0) {\n ptErase = false;\n }\n const eraseEnd = addr0 >= 0 ? addr0 : 0;\n this._erase(oldadd, eraseEnd);\n }\n this.bufadd = this._tab(oldadd, 0);\n return { nextIdx: start + 1, ptErase };\n }\n\n // ----- GE (Graphic Escape) 0x08 -----\n case ORDER.GE: {\n if (stop - start < 2) {\n throw new TnzError(`GE requires 2 bytes, got ${stop - start}`);\n }\n ptErase = false;\n const geByte = data[start + 1];\n const addr1 = this.bufadd;\n this.planeDc[addr1] = geByte;\n this.planeFa[addr1] = 0;\n this.planeEh[addr1] = this._procEh;\n this.planeCs[addr1] = 0xf1;\n this.planeFg[addr1] = this._procFg;\n this.planeBg[addr1] = this._procBg;\n this.bufadd = (addr1 + 1) % bufSize;\n return { nextIdx: start + 2, ptErase };\n }\n\n // ----- SBA (Set Buffer Address) 0x11 -----\n case ORDER.SBA: {\n if (stop - start < 3) {\n throw new TnzError(`SBA requires 3 bytes, got ${stop - start}`);\n }\n ptErase = false;\n const newAddr = this.address(data.subarray(start + 1, start + 3));\n this._checkAddress(newAddr);\n this.bufadd = newAddr;\n return { nextIdx: start + 3, ptErase };\n }\n\n // ----- EUA (Erase Unprotected to Address) 0x12 -----\n case ORDER.EUA: {\n if (stop - start < 3) {\n throw new TnzError(\n `EUA requires 3 bytes, got ${stop - start}`,\n );\n }\n ptErase = false;\n const euaAddr = this.address(data.subarray(start + 1, start + 3));\n this._checkAddress(euaAddr);\n this._eraseInput(this.bufadd, euaAddr);\n this.bufadd = euaAddr;\n return { nextIdx: start + 3, ptErase };\n }\n\n // ----- IC (Insert Cursor) 0x13 -----\n case ORDER.IC: {\n ptErase = false;\n this.curadd = this.bufadd;\n return { nextIdx: start + 1, ptErase };\n }\n\n // ----- SF (Start Field) 0x1D -----\n case ORDER.SF: {\n if (stop - start < 2) {\n throw new TnzError(`SF requires 2 bytes, got ${stop - start}`);\n }\n ptErase = false;\n const fattr = data[start + 1];\n const sfAddr = this.bufadd;\n this.planeDc[sfAddr] = 0;\n this.planeFa[sfAddr] = bit6(fattr);\n this.planeEh[sfAddr] = 0;\n this.planeCs[sfAddr] = 0;\n this.planeFg[sfAddr] = 0;\n this.planeBg[sfAddr] = 0;\n this.bufadd = (sfAddr + 1) % bufSize;\n return { nextIdx: start + 2, ptErase };\n }\n\n // ----- SA (Set Attribute) 0x28 -----\n case ORDER.SA: {\n if (stop - start < 3) {\n throw new TnzError(`SA requires 3 bytes, got ${stop - start}`);\n }\n ptErase = false;\n this._processSa(data[start + 1], data[start + 2]);\n return { nextIdx: start + 3, ptErase };\n }\n\n // ----- SFE (Start Field Extended) 0x29 -----\n case ORDER.SFE: {\n ptErase = false;\n const sfeAddr = this.bufadd;\n this.planeDc[sfeAddr] = 0;\n this.planeFa[sfeAddr] = bit6(0); // default = 0x40\n this.planeEh[sfeAddr] = 0;\n this.planeCs[sfeAddr] = 0;\n this.planeFg[sfeAddr] = 0;\n this.planeBg[sfeAddr] = 0;\n\n const sfeEnd = this._setAttributes(sfeAddr, data, start + 1);\n this.bufadd = (sfeAddr + 1) % bufSize;\n return { nextIdx: sfeEnd, ptErase };\n }\n\n // ----- MF (Modify Field) 0x2C -----\n case ORDER.MF: {\n ptErase = false;\n const mfAddr = this.bufadd;\n if (!this.planeFa[mfAddr]) {\n throw new TnzTerminalError(`Not a field: ${mfAddr}`);\n }\n const mfEnd = this._setAttributes(mfAddr, data, start + 1);\n this.bufadd = (mfAddr + 1) % bufSize;\n return { nextIdx: mfEnd, ptErase };\n }\n\n // ----- RA (Repeat to Address) 0x3C -----\n case ORDER.RA: {\n if (stop - start < 4) {\n throw new TnzError(`RA requires 4 bytes, got ${stop - start}`);\n }\n ptErase = false;\n const stopAddr = this.address(data.subarray(start + 1, start + 3));\n let csAttr = this._procCs;\n let returnIdx = start + 4;\n let dataByte = data[start + 3];\n\n if (dataByte === ORDER.GE) {\n // GE inside RA\n csAttr = 0xf1;\n dataByte = data[returnIdx];\n returnIdx++;\n }\n\n this._checkAddress(stopAddr);\n\n const raStart = this.bufadd;\n let rlen: number;\n if (raStart < stopAddr) {\n rlen = stopAddr - raStart;\n } else if (stopAddr < raStart) {\n rlen = stopAddr + bufSize - raStart;\n } else {\n rlen = bufSize;\n }\n\n const fillDc = new Uint8Array(rlen).fill(dataByte);\n const fillZero = new Uint8Array(rlen);\n const fillEh = new Uint8Array(rlen).fill(this._procEh);\n const fillCs = new Uint8Array(rlen).fill(csAttr);\n const fillFg = new Uint8Array(rlen).fill(this._procFg);\n const fillBg = new Uint8Array(rlen).fill(this._procBg);\n\n const ucba = Tnz.ucba;\n ucba(this.planeDc, raStart, fillDc);\n ucba(this.planeFa, raStart, fillZero);\n ucba(this.planeEh, raStart, fillEh);\n ucba(this.planeCs, raStart, fillCs);\n ucba(this.planeFg, raStart, fillFg);\n ucba(this.planeBg, raStart, fillBg);\n\n this.bufadd = stopAddr;\n return { nextIdx: returnIdx, ptErase };\n }\n\n default:\n throw new TnzError(`Unknown order: 0x${orderByte.toString(16)}`);\n }\n }\n\n /**\n * Process host character data — write EBCDIC bytes into buffer planes.\n *\n */\n private _processCharData(\n data: Buffer,\n begIdx: number,\n endIdx: number,\n ): void {\n const dataLen = endIdx - begIdx;\n if (dataLen <= 0) return;\n\n const saddr = this.bufadd;\n const ucba = Tnz.ucba;\n\n ucba(this.planeDc, saddr, data, begIdx, endIdx);\n ucba(this.planeFa, saddr, new Uint8Array(dataLen));\n ucba(this.planeEh, saddr, new Uint8Array(dataLen).fill(this._procEh));\n ucba(this.planeCs, saddr, new Uint8Array(dataLen).fill(this._procCs));\n ucba(this.planeFg, saddr, new Uint8Array(dataLen).fill(this._procFg));\n ucba(this.planeBg, saddr, new Uint8Array(dataLen).fill(this._procBg));\n\n this.bufadd = (this.bufadd + dataLen) % this.bufferSize;\n }\n\n // =========================================================================\n // Write command implementations\n // =========================================================================\n\n /**\n * Process W (Write) command.\n *\n */\n private _processW(\n data: Buffer,\n start: number,\n stop: number,\n ): void {\n if (stop - start <= 1) return; // no WCC\n\n this._processWcc(data[start + 1], true); // MDT reset pass\n this._processOrdersData(data, start + 2, stop);\n this._processWcc(data[start + 1]); // full WCC pass\n this.updated = true;\n }\n\n /**\n * Process EW (Erase/Write) command.\n *\n */\n private _processEw(\n data: Buffer,\n start: number,\n stop: number,\n ): void {\n if (stop - start <= 1) return; // no WCC\n\n this.lastcmd = '';\n this.eraseReset(false);\n this._processOrdersData(data, start + 2, stop);\n this._processWcc(data[start + 1]);\n this.updated = true;\n }\n\n /**\n * Process EWA (Erase/Write Alternate) command.\n *\n */\n private _processEwa(\n data: Buffer,\n start: number,\n stop: number,\n ): void {\n if (stop - start <= 1) return; // no WCC\n\n this.lastcmd = '';\n this.eraseReset(true);\n this._processOrdersData(data, start + 2, stop);\n this._processWcc(data[start + 1]);\n this.updated = true;\n }\n\n /**\n * Process EAU (Erase All Unprotected) command.\n *\n */\n private _processEau(): void {\n this._eraseInput(0, 0);\n this._resetMdt();\n this.keyHome();\n this._restoreKeyboard();\n }\n\n // =========================================================================\n // Read command implementations\n // =========================================================================\n\n /**\n * Send AID response to the host.\n *\n * If `short` is true, only the AID byte is sent (short read).\n * Otherwise, AID + cursor address + modified field data.\n *\n */\n sendAid(aid: number, short?: boolean): void {\n if (short === undefined) {\n // PAx or CLEAR are short reads by default\n short = aid >= 0x6b && aid <= 0x6f;\n }\n\n if (short) {\n // Short read: just AID (no cursor address for PAx/CLEAR according to protocol,\n // rec = bytes([aid]) ... if short: self.send_3270_data(rec) return\n this.send3270Data(Buffer.from([aid]));\n return;\n }\n\n // Full read: AID + cursor + modified fields\n const baddr = this.addressBytes(this.curadd);\n const parts: Buffer[] = [Buffer.from([aid, baddr[0], baddr[1]])];\n\n // Scan for modified fields\n for (const [faddr, fattr] of this.fields()) {\n if (!(fattr & 0x01)) continue; // not modified\n\n // Find the start of field data (position after FA)\n const dataStart = (faddr + 1) % this.bufferSize;\n\n // Find end of field (next FA or wrap)\n const [nextFaddr] = this.nextField(dataStart);\n const dataEnd = nextFaddr >= 0 ? nextFaddr : dataStart;\n\n // SBA + address\n const fieldAddrBytes = this.addressBytes(dataStart);\n parts.push(\n Buffer.from([\n ORDER.SBA,\n fieldAddrBytes[0],\n fieldAddrBytes[1],\n ]),\n );\n\n // Field data characters (with GE for charset F1)\n this._appendCharBytes(parts, dataStart, dataEnd);\n }\n\n this.send3270Data(Buffer.concat(parts));\n }\n\n /**\n * Process RB (Read Buffer) — send entire buffer contents.\n *\n * Linear scan through the buffer. Each position is either a field\n * attribute (emitted as SF/SFE) or character data.\n *\n */\n /** @internal */ _readBuffer(): void {\n const baddr = this.addressBytes(this.curadd);\n const parts: Buffer[] = [Buffer.from([this.aid, baddr[0], baddr[1]])];\n\n const replyMode = this._replyMode;\n const replyCattrs = this._replyCattrs;\n let ehAttr = 0;\n let fgAttr = 0;\n let bgAttr = 0;\n\n for (let addr = 0; addr < this.bufferSize; addr++) {\n const fattr = this.planeFa[addr];\n\n if (fattr) {\n // Field attribute position — emit SF or SFE\n if (replyMode) {\n const sfe: number[] = [ORDER.SFE, 0];\n\n const eh = this.planeEh[addr];\n if (eh) { sfe[1]++; sfe.push(0x41, eh); }\n\n const fg = this.planeFg[addr];\n if (fg) { sfe[1]++; sfe.push(0x42, fg); }\n\n const cs = this.planeCs[addr];\n if (cs) { sfe[1]++; sfe.push(0x43, cs); }\n\n const bg = this.planeBg[addr];\n if (bg) { sfe[1]++; sfe.push(0x45, bg); }\n\n if (sfe[1] > 0) {\n sfe[1]++;\n sfe.push(0xc0, fattr);\n parts.push(Buffer.from(sfe));\n } else {\n parts.push(Buffer.from([ORDER.SF, fattr]));\n }\n } else {\n parts.push(Buffer.from([ORDER.SF, fattr]));\n }\n\n // Reset character-mode tracking after a field boundary\n ehAttr = 0;\n fgAttr = 0;\n bgAttr = 0;\n } else {\n // Character data position\n if (replyMode === 2) {\n // Character mode — emit SA orders for attribute changes\n if (replyCattrs.includes(0x41)) {\n const eh1 = this.planeEh[addr];\n if (eh1 !== ehAttr) {\n parts.push(Buffer.from([ORDER.SA, 0x41, eh1]));\n ehAttr = eh1;\n }\n }\n if (replyCattrs.includes(0x42)) {\n const fg1 = this.planeFg[addr];\n if (fg1 !== fgAttr) {\n parts.push(Buffer.from([ORDER.SA, 0x42, fg1]));\n fgAttr = fg1;\n }\n }\n if (replyCattrs.includes(0x45)) {\n const bg1 = this.planeBg[addr];\n if (bg1 !== bgAttr) {\n parts.push(Buffer.from([ORDER.SA, 0x45, bg1]));\n bgAttr = bg1;\n }\n }\n }\n\n // Emit character (with GE prefix if charset 0xF1)\n if (this.planeCs[addr] === 0xf1) {\n parts.push(Buffer.from([ORDER.GE, this.planeDc[addr]]));\n } else {\n parts.push(Buffer.from([this.planeDc[addr]]));\n }\n }\n }\n\n this.send3270Data(Buffer.concat(parts));\n }\n\n /**\n * Append character data bytes to a parts list, inserting GE for\n * characters from character set 0xF1.\n *\n * Uses linear indexing; both saddr and eaddr must be in [0, bufferSize].\n * When saddr < eaddr, emits characters in that range.\n * When saddr === eaddr, emits nothing.\n *\n */\n /** @internal */ _appendCharBytes(\n parts: Buffer[],\n saddr: number,\n eaddr: number,\n ): void {\n if (saddr >= eaddr) return;\n\n for (let pos = saddr; pos < eaddr; pos++) {\n if (this.planeCs[pos] === 0xf1) {\n parts.push(Buffer.from([ORDER.GE, this.planeDc[pos]]));\n } else {\n parts.push(Buffer.from([this.planeDc[pos]]));\n }\n }\n }\n\n // =========================================================================\n // Write Structured Field (WSF)\n // =========================================================================\n\n /**\n * Process WSF (Write Structured Field) command.\n *\n * Parses the structured field chain and dispatches each SF.\n *\n */\n private _processWsf(data: Buffer, start: number, stop: number): void {\n if (stop - start < 4) {\n throw new TnzError(`WSF needs 4 bytes, got ${stop - start}`);\n }\n\n let i = start + 1;\n while (i < stop) {\n let sfLen = (data[i] << 8) | data[i + 1];\n if (sfLen === 0) sfLen = stop - i;\n\n if (sfLen < 3) {\n throw new TnzError(`Bad structured field length: ${sfLen}`);\n }\n\n if (i + sfLen > stop) {\n throw new TnzError('WSF len and data inconsistent');\n }\n\n const sfId = data[i + 2];\n this._processWsfById(sfId, data, i, i + sfLen);\n i += sfLen;\n }\n }\n\n /**\n * Dispatch a single structured field by its ID.\n *\n */\n private _processWsfById(\n sfId: number,\n data: Buffer,\n start: number,\n stop: number,\n ): void {\n switch (sfId) {\n case SF_ID.READ_PARTITION: // 0x01\n this._wsfReadPartition(data, start, stop);\n break;\n case 0x03: // Erase/Reset\n this._wsfEraseReset(data, start, stop);\n break;\n case 0x09: // Set Reply Mode\n this._wsfSetReplyMode(data, start, stop);\n break;\n case 0x40: // Outbound 3270DS\n this._wsfOutbound3270ds(data, start, stop);\n break;\n case SF_ID.DDM: // 0xD0\n this.emit('ddm', data.subarray(start, stop));\n break;\n default:\n throw new TnzError(`Bad Structured Field ID: ${sfId}`);\n }\n }\n\n /**\n * Process Read Partition structured field.\n *\n */\n private _wsfReadPartition(\n data: Buffer,\n start: number,\n _stop: number,\n ): void {\n const pid = data[start + 3];\n const rpType = data[start + 4];\n\n if ((rpType === 0x02 || rpType === 0x03) && pid !== 0xff) {\n throw new TnzTerminalError(`pid=${pid}, type=${rpType}`);\n }\n\n this.readState = ReadState.RREAD;\n\n if (rpType === 0x02) {\n // Query\n this.inop = rpType;\n this._queryReply();\n } else if (rpType === 0x03) {\n // Query List\n this.inop = rpType;\n this._queryReply();\n } else if (rpType === 0x6e) {\n // Read Modified All (RMA)\n this.inpid = pid;\n this.inop = rpType;\n this.sendAid(AID.READ_PARTITION, false);\n } else if (rpType === 0xf2) {\n // Read Buffer (RB)\n this.inpid = pid;\n this.inop = rpType;\n this.aid = AID.READ_PARTITION;\n this._readBuffer();\n } else if (rpType === 0xf6) {\n // Read Modified (RM)\n this.inpid = pid;\n this.inop = rpType;\n this.sendAid(AID.READ_PARTITION);\n } else {\n throw new TnzTerminalError(\n `Unknown Read Partition type: 0x${rpType.toString(16)}`,\n );\n }\n }\n\n /**\n * Process Erase/Reset structured field.\n *\n */\n private _wsfEraseReset(\n data: Buffer,\n start: number,\n stop: number,\n ): void {\n this._extendedColorMode = false;\n if (stop - start < 4) {\n throw new TnzError(\n `Erase/Reset needs 4 bytes, got ${stop - start}`,\n );\n }\n this.eraseReset(Boolean(data[start + 3] & 0x80));\n this.updated = true;\n }\n\n /**\n * Process Set Reply Mode structured field.\n *\n */\n private _wsfSetReplyMode(\n data: Buffer,\n start: number,\n stop: number,\n ): void {\n const pid = data[start + 3];\n if (pid) {\n throw new TnzError('Non-zero PID not implemented');\n }\n\n const mode = data[start + 4];\n if (mode <= 1) {\n // Field or Extended Field mode\n this._replyCattrs = Buffer.alloc(0);\n } else if (mode === 2) {\n // Character mode\n this._replyCattrs = Buffer.from(data.subarray(start + 5, stop));\n } else {\n throw new TnzError(`Bad reply mode: ${mode}`);\n }\n this._replyMode = mode;\n }\n\n /**\n * Process Outbound 3270DS structured field.\n *\n * Dispatches embedded 3270 commands (Write, EW, EWA, EAU).\n *\n */\n private _wsfOutbound3270ds(\n data: Buffer,\n start: number,\n stop: number,\n ): void {\n const pid = data[start + 3];\n const cmdByte = data[start + 4];\n\n switch (cmdByte) {\n case CMD.WRITE: // 0xF1\n this._processW(data, start + 4, stop);\n break;\n case CMD.ERASE_WRITE: // 0xF5\n if (pid) throw new TnzError('Non-zero PID not implemented');\n this._processEw(data, start + 4, stop);\n break;\n case CMD.ERASE_WRITE_ALTERNATE: // 0x7E\n if (pid) throw new TnzError('Non-zero PID not implemented');\n this._processEwa(data, start + 4, stop);\n break;\n case CMD.ERASE_ALL_UNPROTECTED: // 0x6F\n if (stop - start !== 5) {\n throw new TnzError(`EAU must be 5 bytes, got ${stop - start}`);\n }\n this._processEau();\n break;\n default:\n throw new TnzError(\n `Unknown Outbound 3270DS command: 0x${cmdByte.toString(16)}`,\n );\n }\n }\n\n// =========================================================================\n // Query Reply\n // =========================================================================\n\n /**\n * Build and send the Query Reply data stream.\n *\n * Responds to Read Partition Query / Query List with terminal\n * capabilities: Usable Area, Character Sets, Color, Highlight,\n * Reply Modes, DDM, Implicit Partitions.\n *\n */\n private _queryReply(): void {\n const parts: Buffer[] = [];\n\n // AID = Structured Field\n parts.push(Buffer.from([AID.STRUCTURED_FIELD]));\n\n // ---- Summary Query Reply (0x80) ----\n const summaryQcodes: number[] = [\n 0x80, // Summary\n QR_TYPE.USABLE_AREA, // 0x81\n QR_TYPE.CHARACTER_SETS, // 0x85\n ];\n if (this.capableColor) {\n summaryQcodes.push(QR_TYPE.COLOR); // 0x86\n }\n summaryQcodes.push(\n QR_TYPE.HIGHLIGHT, // 0x87\n QR_TYPE.REPLY_MODES, // 0x88\n QR_TYPE.DDM, // 0x95\n QR_TYPE.IMPLICIT_PARTITION, // 0xA6\n );\n\n const summaryBody = Buffer.from([\n QR_TYPE.USABLE_AREA, // 0x81 = Query Reply ID\n ...summaryQcodes,\n ]);\n this._addQueryReplyField(parts, summaryBody);\n\n // ---- Usable Area Query Reply (0x81) ----\n const usableArea = Buffer.alloc(14);\n usableArea[0] = QR_TYPE.USABLE_AREA; // 0x81\n usableArea[1] = 0x01; // Flags: 12/14-bit addressing\n usableArea[2] = 0x00; // Flags\n usableArea.writeUInt16BE(this.amaxCol, 3); // Width\n usableArea.writeUInt16BE(this.amaxRow, 5); // Height\n usableArea[7] = 0x00; // Units: inches\n usableArea.writeUInt16BE(1, 8); // Xr numerator\n usableArea.writeUInt16BE(96, 10); // Xr denominator\n usableArea.writeUInt16BE(1, 12); // Yr numerator\n // Need 2 more bytes for Yr denominator + AW + AH\n const usableAreaFull = Buffer.alloc(19);\n usableArea.copy(usableAreaFull);\n usableAreaFull.writeUInt16BE(96, 14); // Yr denominator\n usableAreaFull[16] = 0x06; // AW (X units in default cell)\n usableAreaFull[17] = 0x0c; // AH (Y units in default cell)\n usableAreaFull[18] = 0x00; // padding\n this._addQueryReplyField(\n parts,\n usableAreaFull.subarray(0, 18),\n );\n\n // ---- Implicit Partition Query Reply (0xA6) ----\n const implPart = Buffer.alloc(15);\n implPart[0] = QR_TYPE.IMPLICIT_PARTITION; // 0xA6\n implPart[1] = 0x00; // Flags\n implPart[2] = 0x00; // Flags\n implPart[3] = 0x0b; // Length of self-defining parameter\n implPart[4] = 0x01; // Implicit Partition Sizes\n implPart[5] = 0x00; // Flags\n implPart.writeUInt16BE(this.dmaxCol, 6); // WD\n implPart.writeUInt16BE(this.dmaxRow, 8); // HD\n implPart.writeUInt16BE(this.amaxCol, 10); // WA\n implPart.writeUInt16BE(this.amaxRow, 12); // HA\n this._addQueryReplyField(parts, implPart.subarray(0, 14));\n\n // ---- Character Sets Query Reply (0x85) ----\n const csFlags1 = this.alt ? 0x82 : 0x02;\n const csHeader = Buffer.from([\n QR_TYPE.CHARACTER_SETS, // 0x85\n csFlags1, // Flags (1)\n 0x00, // Flags (2)\n 0x06, // SDW\n 0x0c, // SDH\n 0x00, 0x00, 0x00, 0x00, // FORM\n 0x07, // DL (descriptor length)\n ]);\n\n // Descriptor 1 (primary)\n const csDesc1 = Buffer.alloc(7);\n csDesc1[0] = 0x00; // SET\n csDesc1[1] = 0x00; // Flags\n csDesc1[2] = 0x00; // LCID\n csDesc1.writeUInt16BE(this.cs00, 3);\n csDesc1.writeUInt16BE(this.cp00, 5);\n\n let csBuf: Buffer;\n if (this.alt) {\n // Descriptor 2 (GE/APL)\n const csDesc2 = Buffer.alloc(7);\n csDesc2[0] = 0x01; // SET\n csDesc2[1] = 0x00; // Flags\n csDesc2[2] = 0xf1; // LCID\n csDesc2.writeUInt16BE(this.csF1, 3);\n csDesc2.writeUInt16BE(this.cpF1, 5);\n csBuf = Buffer.concat([csHeader, csDesc1, csDesc2]);\n } else {\n csBuf = Buffer.concat([csHeader, csDesc1]);\n }\n this._addQueryReplyField(parts, csBuf);\n\n // ---- Highlight Query Reply (0x87) ----\n const hlBuf = Buffer.from([\n QR_TYPE.HIGHLIGHT, // 0x87\n 0x05, // 5 pairs\n 0x00, 0xf0, // default -> normal\n 0xf1, 0xf1, // blink -> blink\n 0xf2, 0xf2, // reverse -> reverse\n 0xf4, 0xf4, // underscore -> underscore\n 0xf8, 0xf8, // intensify -> intensify\n ]);\n this._addQueryReplyField(parts, hlBuf);\n\n // ---- Reply Modes Query Reply (0x88) ----\n const rmBuf = Buffer.from([\n QR_TYPE.REPLY_MODES, // 0x88\n 0x00, // Field mode\n 0x01, // Extended Field mode\n 0x02, // Character mode\n ]);\n this._addQueryReplyField(parts, rmBuf);\n\n // ---- DDM Query Reply (0x95) ----\n const ddmBuf = Buffer.alloc(10);\n ddmBuf[0] = QR_TYPE.DDM; // 0x95\n ddmBuf[1] = 0x00; // Flags\n ddmBuf[2] = 0x00; // Flags\n ddmBuf.writeUInt16BE(this._limin, 3);\n ddmBuf.writeUInt16BE(this._limout, 5);\n ddmBuf[7] = 0x01; // NSS\n ddmBuf[8] = 0x01; // DDMSS\n this._addQueryReplyField(parts, ddmBuf.subarray(0, 9));\n\n // ---- Color Query Reply (0x86) ----\n if (this.capableColor) {\n const colorBuf = Buffer.from([\n QR_TYPE.COLOR, // 0x86\n 0x00, // Flags\n 0x08, // NP (8 pairs)\n 0x00, 0xf4, // Default -> Green\n 0xf1, 0xf1, // Blue -> Blue\n 0xf2, 0xf2, // Red -> Red\n 0xf3, 0xf3, // Pink -> Pink\n 0xf4, 0xf4, // Green -> Green\n 0xf5, 0xf5, // Turquoise -> Turquoise\n 0xf6, 0xf6, // Yellow -> Yellow\n 0xf7, 0xf7, // White -> White\n ]);\n this._addQueryReplyField(parts, colorBuf);\n }\n\n this.send3270Data(Buffer.concat(parts));\n }\n\n /**\n * Helper: wrap a Query Reply body with length prefix + 0x81 marker.\n */\n private _addQueryReplyField(\n parts: Buffer[],\n body: Buffer,\n ): void {\n const len = body.length + 3; // 2 for length + 1 for 0x81 marker\n const header = Buffer.alloc(3);\n header.writeUInt16BE(len, 0);\n header[2] = 0x81; // Query Reply ID\n parts.push(header);\n parts.push(body);\n }\n\n // =========================================================================\n // Keyboard methods (Delegated)\n // =========================================================================\n\n keyHome(): void { kb.keyHome(this); }\n isProtected(address: number): boolean { return kb.isProtected(this, address); }\n isProtectedAttr(fattr: number): boolean { return kb.isProtectedAttr(fattr); }\n isUnprotected(): boolean { return kb.isUnprotected(this); }\n keyAid(aid: number): void { kb.keyAid(this, aid); }\n enter(text?: string): void { kb.enter(this, text); }\n \n pf1(): void { this.keyAid(AID.PF1); }\n pf2(): void { this.keyAid(AID.PF2); }\n pf3(): void { this.keyAid(AID.PF3); }\n pf4(): void { this.keyAid(AID.PF4); }\n pf5(): void { this.keyAid(AID.PF5); }\n pf6(): void { this.keyAid(AID.PF6); }\n pf7(): void { this.keyAid(AID.PF7); }\n pf8(): void { this.keyAid(AID.PF8); }\n pf9(): void { this.keyAid(AID.PF9); }\n pf10(): void { this.keyAid(AID.PF10); }\n pf11(): void { this.keyAid(AID.PF11); }\n pf12(): void { this.keyAid(AID.PF12); }\n pf13(): void { this.keyAid(AID.PF13); }\n pf14(): void { this.keyAid(AID.PF14); }\n pf15(): void { this.keyAid(AID.PF15); }\n pf16(): void { this.keyAid(AID.PF16); }\n pf17(): void { this.keyAid(AID.PF17); }\n pf18(): void { this.keyAid(AID.PF18); }\n pf19(): void { this.keyAid(AID.PF19); }\n pf20(): void { this.keyAid(AID.PF20); }\n pf21(): void { this.keyAid(AID.PF21); }\n pf22(): void { this.keyAid(AID.PF22); }\n pf23(): void { this.keyAid(AID.PF23); }\n pf24(): void { this.keyAid(AID.PF24); }\n\n pa1(): void { this.keyAid(AID.PA1); }\n pa2(): void { this.keyAid(AID.PA2); }\n pa3(): void { this.keyAid(AID.PA3); }\n clear(): void { this.keyAid(AID.CLEAR); }\n\n keyCurDown(): void { kb.keyCurDown(this); }\n keyCurUp(): void { kb.keyCurUp(this); }\n keyCurLeft(): void { kb.keyCurLeft(this); }\n keyCurRight(): void { kb.keyCurRight(this); }\n setCursorPosition(row: number, col: number): void { kb.setCursorPosition(this, row, col); }\n setDataAt(text: string, row?: number, col?: number): number { return kb.setDataAt(this, text, row, col); }\n\n keyTab(): void { kb.keyTab(this); }\n keyBacktab(): void { kb.keyBacktab(this); }\n keyNewline(): void { kb.keyNewline(this); }\n keyEnd(): void { kb.keyEnd(this); }\n keyData(text: string): number { return kb.keyData(this, text); }\n keyInsData(text: string): number { return kb.keyInsData(this, text); }\n keyDelete(): boolean { return kb.keyDelete(this); }\n keyBackspace(): boolean { return kb.keyBackspace(this); }\n keyEraseEof(): boolean { return kb.keyEraseEof(this); }\n keyEraseInput(): void { kb.keyEraseInput(this); }\n attn(): void { kb.attn(this); }\n\n // =========================================================================\n // Screen reading helpers (Delegated)\n // =========================================================================\n\n scrstr(saddr = 0, eaddr = 0, rstrip?: boolean): string {\n return screen.scrstr(this, saddr, eaddr, rstrip);\n }\n\n scrhas(text: string, saddr = 0): boolean {\n return screen.scrhas(this, text, saddr);\n }\n}\n","/**\n * ATI (Automated Task Interpreter) automation layer.\n *\n * Manages multiple TN3270 sessions and provides a high-level API\n * for sending keystrokes, waiting for screen conditions, and\n * extracting screen data.\n *\n *\n * @module automation/ati\n */\n\nimport { Tnz } from '../core/tnz';\nimport type { TnzOptions } from '../types';\nimport { FileTransfer } from './file-transfer';\n\n// ---------------------------------------------------------------------------\n// Error class\n// ---------------------------------------------------------------------------\n\n/** ATI automation error. */\nexport class AtiError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AtiError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Sentinel constants\n// ---------------------------------------------------------------------------\n\n/** Sentinel for case-insensitive search. */\nexport const CASI = Symbol('CASI');\n\n/** Sentinel for end-of-line extraction. */\nexport const EOL = Symbol('EOL');\n\n/** Sentinel for first-occurrence search. */\nexport const FIRST = Symbol('FIRST');\n\n/** Sentinel for last-occurrence search. */\nexport const LAST = Symbol('LAST');\n\n// ---------------------------------------------------------------------------\n// Mnemonic key strings\n// ---------------------------------------------------------------------------\n\nexport const enter = '[enter]';\nexport const clear = '[clear]';\nexport const pa1 = '[pa1]';\nexport const pa2 = '[pa2]';\nexport const pa3 = '[pa3]';\nexport const pf1 = '[pf1]';\nexport const pf2 = '[pf2]';\nexport const pf3 = '[pf3]';\nexport const pf4 = '[pf4]';\nexport const pf5 = '[pf5]';\nexport const pf6 = '[pf6]';\nexport const pf7 = '[pf7]';\nexport const pf8 = '[pf8]';\nexport const pf9 = '[pf9]';\nexport const pf10 = '[pf10]';\nexport const pf11 = '[pf11]';\nexport const pf12 = '[pf12]';\nexport const pf13 = '[pf13]';\nexport const pf14 = '[pf14]';\nexport const pf15 = '[pf15]';\nexport const pf16 = '[pf16]';\nexport const pf17 = '[pf17]';\nexport const pf18 = '[pf18]';\nexport const pf19 = '[pf19]';\nexport const pf20 = '[pf20]';\nexport const pf21 = '[pf21]';\nexport const pf22 = '[pf22]';\nexport const pf23 = '[pf23]';\nexport const pf24 = '[pf24]';\nexport const tab = '[tab]';\nexport const backtab = '[backtab]';\nexport const home = '[home]';\nexport const newline = '[newline]';\nexport const curdown = '[curdown]';\nexport const curleft = '[curleft]';\nexport const curright = '[curright]';\nexport const curup = '[curup]';\nexport const del = '[delete]';\nexport const eraseeof = '[eraseeof]';\nexport const insert = '[insert]';\nexport const reset = '[reset]';\nexport const attn = '[attn]';\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\n/** Options for connecting a new session. */\nexport interface SessionOptions {\n /** Hostname to connect to. */\n host: string;\n /** Port number (default 23). */\n port?: number;\n /** Use TLS/SSL (default false). */\n secure?: boolean;\n /** Verify server certificate (default true). */\n verifyCert?: boolean;\n /** Alternate screen rows. */\n amaxRow?: number;\n /** Alternate screen columns. */\n amaxCol?: number;\n /** EBCDIC encoding (e.g. 'cp037'). */\n encoding?: string;\n /** Use TN3270E mode. */\n useTn3270e?: boolean;\n /** LU name for TN3270E. */\n luName?: string;\n /** Terminal type string. */\n terminalType?: string;\n}\n\n/** A WHEN block registration. */\ninterface WhenEntry {\n /** Condition function. */\n condition: () => boolean;\n /** Action function. */\n action: () => void;\n /** Priority (lower = higher priority, default 1). */\n priority: number;\n /** Whether actively monitoring. */\n active: boolean;\n /** Label name. */\n name: string;\n}\n\n// ---------------------------------------------------------------------------\n// AID key dispatch table\n// ---------------------------------------------------------------------------\n\n/** Map of mnemonic strings to Tnz method names for AID keys. */\nconst AID_KEYS: Record<string, string> = {\n '[enter]': 'enter',\n '[clear]': 'clear',\n '[pa1]': 'pa1',\n '[pa2]': 'pa2',\n '[pa3]': 'pa3',\n '[pf1]': 'pf1',\n '[pf2]': 'pf2',\n '[pf3]': 'pf3',\n '[pf4]': 'pf4',\n '[pf5]': 'pf5',\n '[pf6]': 'pf6',\n '[pf7]': 'pf7',\n '[pf8]': 'pf8',\n '[pf9]': 'pf9',\n '[pf10]': 'pf10',\n '[pf11]': 'pf11',\n '[pf12]': 'pf12',\n '[pf13]': 'pf13',\n '[pf14]': 'pf14',\n '[pf15]': 'pf15',\n '[pf16]': 'pf16',\n '[pf17]': 'pf17',\n '[pf18]': 'pf18',\n '[pf19]': 'pf19',\n '[pf20]': 'pf20',\n '[pf21]': 'pf21',\n '[pf22]': 'pf22',\n '[pf23]': 'pf23',\n '[pf24]': 'pf24',\n '[attn]': 'attn',\n};\n\n/** Map of mnemonic strings to Tnz method names for movement/editing. */\nconst MOVE_KEYS: Record<string, string> = {\n '[tab]': 'keyTab',\n '[backtab]': 'keyBacktab',\n '[home]': 'keyHome',\n '[newline]': 'keyNewline',\n '[curdown]': 'keyCurDown',\n '[curleft]': 'keyCurLeft',\n '[curright]': 'keyCurRight',\n '[curup]': 'keyCurUp',\n '[delete]': 'keyDelete',\n '[eraseeof]': 'keyEraseEof',\n};\n\n// ---------------------------------------------------------------------------\n// Ati class\n// ---------------------------------------------------------------------------\n\n/**\n * Automation layer for managing TN3270 sessions.\n *\n * Provides session management, send/wait/when primitives, and\n * screen reading helpers (scrhas, extract).\n *\n */\nexport class Ati {\n // -- Session registry --\n\n /** Map of uppercase session names to Tnz instances. */\n private _sessions = new Map<string, Tnz>();\n \n /** Map of uppercase session names to FileTransfer instances. */\n private _fileTransfers = new Map<string, FileTransfer>();\n\n /** Current session name (uppercase). */\n private _currentSession = 'NONE';\n\n // -- Internal variables --\n\n /** Last return code. */\n rc = 0;\n\n /** Hit row (1-based) from last scrhas/extract. */\n hitRow = 1;\n\n /** Hit column (1-based) from last scrhas/extract. */\n hitCol = 1;\n\n /** Hit string from last scrhas. */\n hitStr = '';\n\n /** Last string sent by send(). */\n sendStr = '';\n\n /** Name of lost session (empty = none). */\n seslost = '';\n\n /** Default wait timeout in seconds. */\n maxWait = 120;\n\n /** Centiseconds between wait re-checks (1-99, i.e. 10ms-990ms). */\n waitSleep = 5;\n\n /** Seconds to wait for keyboard unlock in send(). */\n keyUnlock = 60;\n\n /** Whether ONERROR causes wait timeout to throw. */\n onError = false;\n\n // -- WHEN blocks --\n\n /** Registered WHEN entries. */\n private _whens: WhenEntry[] = [];\n\n /** Whether currently executing a WHEN block. */\n private _inWhen = false;\n\n /** Whether a WHEN fired during this cycle. */\n private _ranWhen = false;\n\n // -- User variables --\n\n /** User-defined variables (uppercase keys). */\n private _vars = new Map<string, string>();\n\n // =========================================================================\n // Session management\n // =========================================================================\n\n /** Get current session name. */\n get session(): string {\n return this._currentSession;\n }\n\n /** Set current session (switch or connect). */\n set session(name: string) {\n const unam = name.toUpperCase().trim();\n if (!unam) throw new AtiError('no session name');\n\n if (this._sessions.has(unam)) {\n // Switch to existing session\n this._currentSession = unam;\n this.rc = 1;\n } else {\n // New session — must be connected via connectSession()\n throw new AtiError(\n `Session ${unam} not found. Use connectSession() to create.`,\n );\n }\n }\n\n /** Get space-separated list of session names. */\n get sessions(): string {\n return [...this._sessions.keys()].join(' ');\n }\n\n /**\n * Connect a new session.\n *\n * @param name - Session name (will be uppercased)\n * @param options - Connection options\n * @returns The connected Tnz instance\n */\n async connectSession(\n name: string,\n options: SessionOptions,\n ): Promise<Tnz> {\n const unam = name.toUpperCase().trim();\n if (!unam) throw new AtiError('no session name');\n if (this._sessions.has(unam)) {\n throw new AtiError(`${unam} already established`);\n }\n\n const tnzOpts: TnzOptions = {};\n if (options.amaxRow !== undefined) tnzOpts.amaxRow = options.amaxRow;\n if (options.amaxCol !== undefined) tnzOpts.amaxCol = options.amaxCol;\n if (options.encoding !== undefined) tnzOpts.encoding = options.encoding;\n if (options.useTn3270e !== undefined) {\n tnzOpts.useTn3270e = options.useTn3270e;\n }\n if (options.luName !== undefined) tnzOpts.luName = options.luName;\n if (options.terminalType !== undefined) {\n tnzOpts.terminalType = options.terminalType;\n }\n\n const tns = new Tnz(unam, tnzOpts);\n await tns.connect(options.host, options.port ?? 23, {\n secure: options.secure,\n verifyCert: options.verifyCert,\n });\n\n this._sessions.set(unam, tns);\n this._fileTransfers.set(unam, new FileTransfer(tns));\n this._currentSession = unam;\n this.rc = 0;\n return tns;\n }\n\n /**\n * Register an already-created Tnz instance as a session.\n *\n * Useful for testing or when the Tnz instance is created externally.\n */\n registerSession(name: string, tnz: Tnz): void {\n const unam = name.toUpperCase().trim();\n if (!unam) throw new AtiError('no session name');\n if (this._sessions.has(unam)) {\n throw new AtiError(`${unam} already established`);\n }\n this._sessions.set(unam, tnz);\n this._fileTransfers.set(unam, new FileTransfer(tnz));\n this._currentSession = unam;\n }\n\n /**\n * Get the Tnz instance for a session.\n *\n * @param name - Session name (defaults to current session)\n * @returns The Tnz instance, or undefined if not found\n */\n getTnz(name?: string): Tnz | undefined {\n const key = name?.toUpperCase().trim() ?? this._currentSession;\n return this._sessions.get(key);\n }\n\n /**\n * Drop (disconnect) the current session.\n *\n */\n dropSession(): void {\n const session = this._currentSession;\n if (session === 'NONE' || !this._sessions.has(session)) {\n return;\n }\n\n const tns = this._sessions.get(session)!;\n this._sessions.delete(session);\n this._fileTransfers.delete(session);\n\n // Pick next session\n if (this._sessions.size > 0) {\n this._currentSession = this._sessions.keys().next().value!;\n } else {\n this._currentSession = 'NONE';\n }\n\n tns.shutdown();\n }\n\n /**\n * Rename the current session.\n *\n */\n renameSession(newName: string): void {\n const unam = newName.toUpperCase().trim();\n if (!unam) throw new AtiError('no session name');\n\n const session = this._currentSession;\n if (session === 'NONE' || !this._sessions.has(session)) {\n throw new AtiError('no active session');\n }\n if (session === unam) return;\n if (this._sessions.has(unam)) {\n throw new AtiError(`${unam} already established`);\n }\n\n const tns = this._sessions.get(session)!;\n const ft = this._fileTransfers.get(session)!;\n this._sessions.delete(session);\n this._fileTransfers.delete(session);\n \n this._sessions.set(unam, tns);\n this._fileTransfers.set(unam, ft);\n this._currentSession = unam;\n }\n\n // =========================================================================\n // Screen reading\n // =========================================================================\n\n /**\n * Get current screen row count.\n */\n get maxRow(): number {\n const tns = this.getTnz();\n return tns ? tns.maxRow : 0;\n }\n\n /**\n * Get current screen column count.\n */\n get maxCol(): number {\n const tns = this.getTnz();\n return tns ? tns.maxCol : 0;\n }\n\n /**\n * Get cursor row (1-based).\n */\n get curRow(): number {\n const tns = this.getTnz();\n if (!tns) return 0;\n return Math.floor(tns.curadd / tns.maxCol) + 1;\n }\n\n /**\n * Get cursor column (1-based).\n */\n get curCol(): number {\n const tns = this.getTnz();\n if (!tns) return 0;\n return (tns.curadd % tns.maxCol) + 1;\n }\n\n /**\n * Whether keyboard is locked.\n */\n get keyLock(): boolean {\n const tns = this.getTnz();\n if (!tns) return false;\n return tns.pwait || tns.systemLockWait;\n }\n\n /**\n * Check if the current session screen contains a string.\n *\n * Simple overload: `scrhas(text)` — searches entire screen.\n *\n * @param text - Text to search for\n * @param caseInsensitive - Whether to search case-insensitively\n * @returns true if found\n *\n */\n scrhas(text: string, caseInsensitive = false): boolean {\n const tns = this.getTnz();\n if (!tns) {\n this.rc = 12;\n return false;\n }\n\n const scrStr = tns.scrstr(0, 0, false);\n const found = caseInsensitive\n ? scrStr.toLowerCase().includes(text.toLowerCase())\n : scrStr.includes(text);\n\n if (found) {\n const idx = caseInsensitive\n ? scrStr.toLowerCase().indexOf(text.toLowerCase())\n : scrStr.indexOf(text);\n const hitRow = Math.floor(idx / tns.maxCol) + 1;\n const hitCol = (idx % tns.maxCol) + 1;\n this.hitRow = hitRow;\n this.hitCol = hitCol;\n this.hitStr = text;\n this.rc = 0;\n return true;\n }\n\n this.rc = 1;\n return false;\n }\n\n /**\n * Extract text from the current session screen.\n *\n * @param length - Number of characters to extract, or EOL symbol\n * @param row - Start row (1-based, default 1)\n * @param col - Start column (1-based, default 1)\n * @returns Extracted text\n *\n */\n extract(\n length: number | typeof EOL,\n row = 1,\n col = 1,\n ): string {\n const tns = this.getTnz();\n if (!tns) {\n this.rc = 12;\n return '';\n }\n\n const maxCol = tns.maxCol;\n const maxRow = tns.maxRow;\n const scrSize = maxCol * maxRow;\n\n // Handle negative row/col (relative to screen edge)\n let r = row;\n let c = col;\n if (r <= 0) r = maxRow + r + 1;\n if (c <= 0) c = maxCol + c + 1;\n\n const start = (r - 1) * maxCol + (c - 1);\n if (start < 0 || start >= scrSize) {\n this.rc = 8;\n return '';\n }\n\n const scrStr = tns.scrstr(0, 0, false);\n\n let end: number;\n if (length === EOL) {\n // Extract to end of current row\n const rowEnd = Math.floor(start / maxCol) * maxCol + maxCol;\n end = rowEnd;\n } else {\n if (length < 1) {\n this.rc = 3;\n return '';\n }\n end = start + length;\n if (end > scrSize) {\n this.rc = 9;\n end = scrSize;\n } else {\n this.rc = 0;\n }\n }\n\n const hitY = Math.floor(start / maxCol);\n const hitX = start % maxCol;\n this.hitRow = hitY + 1;\n this.hitCol = hitX + 1;\n\n return scrStr.slice(start, end);\n }\n\n // =========================================================================\n // Send\n // =========================================================================\n\n /**\n * Send keystrokes and/or AID keys to the current session.\n *\n * The value string can contain plain text and mnemonic keys\n * like `[enter]`, `[pf3]`, `[tab]`, `[home]`, etc.\n *\n * AID keys (`[enter]`, `[clear]`, `[pa1-3]`, `[pf1-24]`, `[attn]`)\n * terminate the send — anything after is ignored.\n *\n * Movement/editing keys (`[tab]`, `[backtab]`, `[home]`, `[newline]`,\n * `[curdown]`, `[curleft]`, `[curright]`, `[curup]`, `[delete]`,\n * `[eraseeof]`) do NOT terminate the send.\n *\n * `[insert]` switches to insert mode; `[reset]` switches back.\n *\n * @param value - String with text and/or mnemonic keys\n * @param pos - Optional cursor position [row, col] (1-based)\n * @returns Return code (0=success, 4=partial, 12=session lost)\n *\n */\n async send(value: string, pos?: [number, number]): Promise<number> {\n const tns = this.getTnz();\n if (!tns || this._currentSession === 'NONE') {\n this.rc = 12;\n return 12;\n }\n\n if (tns.seslost) {\n this.rc = 12;\n return 12;\n }\n\n // Wait for keyboard to unlock (unless bypassing with reset)\n if (!value.startsWith(reset)) {\n const startTime = Date.now();\n const endTime = startTime + this.keyUnlock * 1000;\n \n while (this.keyLock) {\n if (Date.now() >= endTime) {\n // Keyboard lock timeout\n this.rc = 14;\n return 14;\n }\n if (tns.seslost) {\n this.rc = 12;\n return 12;\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n }\n\n // Position cursor if requested\n if (pos) {\n tns.setCursorPosition(pos[0], pos[1]);\n }\n\n let useInsert = false;\n let sendStr = '';\n let rest = value;\n let sent = false;\n\n while (rest.length > 0) {\n const bracketIdx = rest.indexOf('[');\n if (bracketIdx < 0) {\n // All plain text\n const cnt = useInsert\n ? tns.keyInsData(rest)\n : tns.keyData(rest);\n sendStr += rest.slice(0, cnt);\n break;\n }\n\n // Type plain text before the bracket\n if (bracketIdx > 0) {\n const plain = rest.slice(0, bracketIdx);\n if (useInsert) {\n tns.keyInsData(plain);\n } else {\n tns.keyData(plain);\n }\n sendStr += plain;\n rest = rest.slice(bracketIdx);\n }\n\n // Handle `[[` escape (literal `[`)\n if (rest.startsWith('[[')) {\n if (useInsert) {\n tns.keyInsData('[');\n } else {\n tns.keyData('[');\n }\n sendStr += '[[';\n rest = rest.slice(2);\n continue;\n }\n\n // Try to match a mnemonic key\n let matched = false;\n\n // Check AID keys (these terminate send)\n for (const [mnemonic, method] of Object.entries(AID_KEYS)) {\n if (rest.startsWith(mnemonic)) {\n (tns as unknown as Record<string, () => void>)[method]();\n sendStr += mnemonic;\n rest = rest.slice(mnemonic.length);\n sent = true;\n matched = true;\n break;\n }\n }\n if (sent) break;\n\n // Check movement/editing keys (these do NOT terminate)\n for (const [mnemonic, method] of Object.entries(MOVE_KEYS)) {\n if (rest.startsWith(mnemonic)) {\n (tns as unknown as Record<string, () => void>)[method]();\n sendStr += mnemonic;\n rest = rest.slice(mnemonic.length);\n matched = true;\n break;\n }\n }\n if (matched) continue;\n\n // Special: [insert] and [reset]\n if (rest.startsWith(insert)) {\n useInsert = true;\n sendStr += insert;\n rest = rest.slice(insert.length);\n continue;\n }\n\n if (rest.startsWith(reset)) {\n useInsert = false;\n sendStr += reset;\n rest = rest.slice(reset.length);\n continue;\n }\n\n // Unknown mnemonic\n throw new AtiError('unknown mnemonic: ' + rest.slice(0, 20));\n }\n\n this.sendStr = sendStr;\n\n const atiRc = sendStr === value ? 0 : 4;\n this.rc = atiRc;\n return atiRc;\n }\n\n // =========================================================================\n // File Transfer (IND$FILE)\n // =========================================================================\n\n /**\n * Put host file from local file.\n *\n * @param filename - Local file path to upload\n * @param parms - IND$FILE parameters (e.g. 'DATASET.NAME ASCII CRLF')\n */\n async putFile(filename: string, parms: string): Promise<void> {\n const ft = this._fileTransfers.get(this._currentSession);\n if (!ft) throw new AtiError('No active session for file transfer');\n await ft.putFile(filename, parms);\n }\n\n /**\n * Get host file into local file.\n *\n * @param parms - IND$FILE parameters (e.g. 'DATASET.NAME ASCII CRLF')\n * @param filename - Local file path to save to\n */\n async getFile(parms: string, filename: string): Promise<void> {\n const ft = this._fileTransfers.get(this._currentSession);\n if (!ft) throw new AtiError('No active session for file transfer');\n await ft.getFile(parms, filename);\n }\n\n // =========================================================================\n // Wait\n // =========================================================================\n\n /**\n * Wait for a condition or timeout.\n *\n * @param timeout - Timeout in seconds (default: maxWait)\n * @param condition - Condition function (optional)\n * @returns 0=timeout, 1=condition met, 12=session lost\n *\n */\n async wait(\n timeout?: number,\n condition?: () => boolean,\n ): Promise<number> {\n const tout = timeout ?? this.maxWait;\n const startTime = Date.now();\n const endTime = startTime + tout * 1000;\n\n while (true) {\n // Check condition\n if (condition) {\n if (condition()) {\n this.rc = 1;\n return 1;\n }\n }\n\n // Run WHENs\n const whenRan = this._runWhens();\n\n // Re-check condition after WHENs\n if (whenRan && condition) {\n if (condition()) {\n this.rc = 1;\n return 1;\n }\n }\n\n // Check session health\n const tns = this.getTnz();\n if (tns && tns.seslost) {\n this.seslost = this._currentSession;\n this.rc = 12;\n return 12;\n }\n\n // Check timeout\n const remaining = endTime - Date.now();\n if (remaining <= 0) {\n if (this.onError && condition) {\n throw new AtiError('WAIT TIMEOUT occurred');\n }\n this.rc = 0;\n return 0;\n }\n\n // Sleep for waitSleep (centiseconds) or remaining time, whichever is less\n const sleepMs = Math.min(\n this.waitSleep * 10,\n remaining,\n );\n await new Promise<void>((resolve) =>\n setTimeout(resolve, sleepMs),\n );\n }\n }\n\n // =========================================================================\n // WHEN blocks\n // =========================================================================\n\n /**\n * Register a WHEN block.\n *\n * @param name - Label for this WHEN block\n * @param condition - Condition function\n * @param action - Action to execute when condition is true\n * @param priority - Priority (lower = higher, default 1)\n *\n */\n whenOn(\n name: string,\n condition: () => boolean,\n action: () => void,\n priority = 1,\n ): void {\n const unam = name.toUpperCase().trim();\n\n // Check if already registered\n const existing = this._whens.find((w) => w.name === unam);\n if (existing) {\n existing.condition = condition;\n existing.action = action;\n existing.priority = priority;\n existing.active = true;\n return;\n }\n\n this._whens.push({\n name: unam,\n condition,\n action,\n priority,\n active: true,\n });\n\n // Run immediately on registration (unless inside a WHEN)\n if (!this._inWhen) {\n try {\n this._inWhen = true;\n if (condition()) {\n action();\n }\n } finally {\n this._inWhen = false;\n }\n }\n }\n\n /**\n * Turn off a WHEN block.\n *\n * @param name - Label of the WHEN block to deactivate\n */\n whenOff(name: string): void {\n const unam = name.toUpperCase().trim();\n const entry = this._whens.find((w) => w.name === unam);\n if (entry) {\n entry.active = false;\n }\n }\n\n /**\n * Run all active WHEN blocks in priority order.\n *\n * @returns true if any WHEN body actually ran\n */\n private _runWhens(): boolean {\n if (this._inWhen) return false;\n\n // Sort by priority (lower = higher priority)\n const sorted = this._whens\n .filter((w) => w.active)\n .sort((a, b) => a.priority - b.priority);\n\n this._ranWhen = false;\n try {\n this._inWhen = true;\n for (const when of sorted) {\n if (when.condition()) {\n when.action();\n this._ranWhen = true;\n }\n }\n } finally {\n this._inWhen = false;\n }\n\n return this._ranWhen;\n }\n\n // =========================================================================\n // Variables\n // =========================================================================\n\n /**\n * Get the value of a variable.\n * Returns the variable value, or the uppercased name if not set.\n *\n */\n value(name: string): string {\n const unam = name.toUpperCase().trim();\n\n // Check special internal variables first\n switch (unam) {\n case 'SESSION':\n return this._currentSession;\n case 'SESSIONS':\n return this.sessions;\n case 'MAXROW':\n return String(this.maxRow);\n case 'MAXCOL':\n return String(this.maxCol);\n case 'CURROW':\n return String(this.curRow);\n case 'CURCOL':\n return String(this.curCol);\n case 'HITROW':\n return String(this.hitRow);\n case 'HITCOL':\n return String(this.hitCol);\n case 'HITSTR':\n return this.hitStr;\n case 'KEYLOCK':\n return this.keyLock ? '1' : '0';\n case 'SENDSTR':\n return this.sendStr;\n case 'SESLOST':\n return this.seslost;\n case 'RC':\n return String(this.rc);\n case 'MAXWAIT':\n return String(this.maxWait);\n case 'WAITSLEEP':\n return String(this.waitSleep);\n case 'KEYUNLOCK':\n return String(this.keyUnlock);\n case 'ONERROR':\n return this.onError ? '1' : '0';\n default:\n break;\n }\n\n // Check user variables\n const val = this._vars.get(unam);\n if (val !== undefined) return val;\n\n // ATI convention: unset variable = its own uppercased name\n return unam;\n }\n\n /**\n * Set a variable value.\n *\n * @param name - Variable name\n * @param val - Value to set\n *\n */\n set(name: string, val: string | number | boolean): void {\n const unam = name.toUpperCase().trim();\n const valStr =\n val === true ? '1' : val === false ? '0' : String(val);\n\n // Handle special internal variables\n switch (unam) {\n case 'SESSION':\n this.session = valStr;\n return;\n case 'MAXWAIT':\n this.maxWait = Ati._parseSeconds(valStr);\n return;\n case 'WAITSLEEP':\n // Centiseconds (1-99, i.e. 10ms-990ms)\n this.waitSleep = Math.max(1, Math.min(99, Math.round(Number(valStr))));\n return;\n case 'KEYUNLOCK':\n this.keyUnlock = Math.max(1, Number(valStr));\n return;\n case 'ONERROR':\n this.onError = valStr !== '0' && valStr !== '';\n return;\n case 'RC':\n this.rc = Number(valStr);\n return;\n default:\n break;\n }\n\n // Read-only checks\n const readOnly = new Set([\n 'SESSIONS', 'MAXCOL', 'MAXROW', 'CURCOL', 'CURROW',\n 'HITCOL', 'HITROW', 'HITSTR', 'KEYLOCK', 'SENDSTR',\n 'SESLOST',\n ]);\n if (readOnly.has(unam)) {\n throw new AtiError(`${unam} is read-only`);\n }\n\n // Store as user variable\n this._vars.set(unam, valStr);\n }\n\n /**\n * Drop (delete) a user variable.\n *\n */\n drop(name: string): void {\n const unam = name.toUpperCase().trim();\n\n if (unam === 'SESSION') {\n this.dropSession();\n return;\n }\n\n this._vars.delete(unam);\n }\n\n // =========================================================================\n // Utility\n // =========================================================================\n\n /**\n * Parse a time string like \"[[hh:]mm:]ss\" to seconds.\n */\n static _parseSeconds(value: string | number): number {\n if (typeof value === 'number') return value;\n const parts = value.split(':').map(Number);\n if (parts.length === 1) return parts[0];\n if (parts.length === 2) return parts[0] * 60 + parts[1];\n if (parts.length === 3) {\n return parts[0] * 3600 + parts[1] * 60 + parts[2];\n }\n return Number(value);\n }\n\n /**\n * Parse a numeric value from a string (ATI-style).\n * Extracts leading digits, returns 0 for empty/non-numeric.\n *\n */\n static num(value: string | number): number {\n if (typeof value === 'number') return value;\n const match = value.match(/^-?\\d+/);\n return match ? Number(match[0]) : 0;\n }\n}\n","/**\n * Stateless telnet negotiation helpers.\n *\n * Provides IAC sequence parsing, packet building, and IAC escaping/unescaping.\n * All functions are pure — no side effects, no state.\n *\n * Reference: RFC 854 (Telnet), RFC 855 (Options), RFC 2355 (TN3270E)\n *\n * @module core/telnet\n */\n\nimport { TELNET } from '../types';\n\n// ---------------------------------------------------------------------------\n// IAC sequence parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Describes a single IAC command sequence found in a buffer.\n *\n */\nexport interface IacMatch {\n /** Byte offset where the IAC sequence starts */\n start: number;\n /** Byte offset past the end of the IAC sequence */\n end: number;\n /** The command byte (second byte after IAC) */\n command: number;\n /** The option byte (third byte), present for WILL/WONT/DO/DONT */\n option?: number;\n}\n\n/**\n * Find all IAC command sequences in a buffer.\n *\n * - IAC followed by a byte in 0x00-0xFA or 0xFF → 2-byte sequence\n * - IAC followed by WILL/WONT/DO/DONT (0xFB-0xFE) + option → 3-byte sequence\n *\n * @param buf - Buffer to scan\n * @returns Array of IAC match descriptors\n */\nexport function findIacSequences(buf: Buffer): IacMatch[] {\n const matches: IacMatch[] = [];\n let i = 0;\n while (i < buf.length - 1) {\n if (buf[i] !== TELNET.IAC) {\n i++;\n continue;\n }\n\n const cmd = buf[i + 1];\n\n if (cmd >= 0xfb && cmd <= 0xfe) {\n // 3-byte: IAC + WILL/WONT/DO/DONT + option\n if (i + 2 >= buf.length) break; // incomplete\n matches.push({\n start: i,\n end: i + 3,\n command: cmd,\n option: buf[i + 2],\n });\n i += 3;\n } else {\n // 2-byte: IAC + command/data (0x00-0xFA or 0xFF)\n matches.push({\n start: i,\n end: i + 2,\n command: cmd,\n });\n i += 2;\n }\n }\n return matches;\n}\n\n// ---------------------------------------------------------------------------\n// IAC escaping / unescaping\n// ---------------------------------------------------------------------------\n\n/**\n * Escape IAC bytes in data for transmission.\n * Replaces every 0xFF with 0xFF 0xFF.\n *\n */\nexport function escapeIac(data: Buffer): Buffer {\n // Fast path: no IAC bytes\n if (!data.includes(0xff)) return data;\n\n const result: number[] = [];\n for (let i = 0; i < data.length; i++) {\n result.push(data[i]);\n if (data[i] === 0xff) {\n result.push(0xff);\n }\n }\n return Buffer.from(result);\n}\n\n/**\n * Unescape IAC sequences in received record data.\n * - IAC IAC (0xFF 0xFF) → single 0xFF\n * - Other IAC sequences are removed (shouldn't appear in record data)\n *\n * where `__repl` returns b\"\\xff\" for IAC IAC, b\"\" for everything else.\n */\nexport function unescapeIac(data: Buffer): Buffer {\n // Fast path: no IAC bytes\n if (!data.includes(0xff)) return data;\n\n const result: number[] = [];\n let i = 0;\n while (i < data.length) {\n if (data[i] === 0xff && i + 1 < data.length) {\n if (data[i + 1] === 0xff) {\n // IAC IAC → single 0xFF\n result.push(0xff);\n i += 2;\n } else if (data[i + 1] >= 0xfb && data[i + 1] <= 0xfe) {\n // 3-byte IAC command — skip entirely\n i += 3;\n } else {\n // 2-byte IAC command — skip entirely\n i += 2;\n }\n } else {\n result.push(data[i]);\n i++;\n }\n }\n return Buffer.from(result);\n}\n\n// ---------------------------------------------------------------------------\n// Packet builders\n// ---------------------------------------------------------------------------\n\n/** Build an IAC WILL packet. */\nexport function buildWill(opt: number): Buffer {\n return Buffer.from([TELNET.IAC, TELNET.WILL, opt]);\n}\n\n/** Build an IAC WONT packet. */\nexport function buildWont(opt: number): Buffer {\n return Buffer.from([TELNET.IAC, TELNET.WONT, opt]);\n}\n\n/** Build an IAC DO packet. */\nexport function buildDo(opt: number): Buffer {\n return Buffer.from([TELNET.IAC, TELNET.DO, opt]);\n}\n\n/** Build an IAC DONT packet. */\nexport function buildDont(opt: number): Buffer {\n return Buffer.from([TELNET.IAC, TELNET.DONT, opt]);\n}\n\n/**\n * Build an IAC SB ... IAC SE subnegotiation packet.\n * The value is IAC-escaped automatically.\n */\nexport function buildSub(value: Buffer): Buffer {\n const escaped = escapeIac(value);\n const result = Buffer.alloc(escaped.length + 4);\n result[0] = TELNET.IAC;\n result[1] = TELNET.SB;\n escaped.copy(result, 2);\n result[result.length - 2] = TELNET.IAC;\n result[result.length - 1] = TELNET.SE;\n return result;\n}\n\n/** Build an IAC EOR packet. */\nexport function buildEor(): Buffer {\n return Buffer.from([TELNET.IAC, TELNET.EOR]);\n}\n\n/**\n * Build a single-byte telnet command packet (IAC + command).\n * Valid command codes: 241 (NOP) through 249 (GA).\n *\n * @throws {RangeError} if code is not in 241-249\n */\nexport function buildCommand(code: number): Buffer {\n if (code < 241 || code > 249) {\n throw new RangeError(`Telnet command ${code} not valid (must be 241-249)`);\n }\n return Buffer.from([TELNET.IAC, code]);\n}\n\n// ---------------------------------------------------------------------------\n// Option name lookup\n// ---------------------------------------------------------------------------\n\n/** Human-readable names for telnet option codes. */\nconst OPTION_NAMES: Record<number, string> = {\n 0x00: 'TRANSMIT-BINARY',\n 0x01: 'ECHO',\n 0x03: 'SUPPRESS-GO-AHEAD',\n 0x06: 'TIMING-MARK',\n 0x18: 'TERMINAL-TYPE',\n 0x19: 'END-OF-RECORD',\n 0x1d: '3270-REGIME',\n 0x28: 'TN3270E',\n 0x2e: 'START_TLS',\n};\n\n/**\n * Get a human-readable name for a telnet option code.\n *\n */\nexport function optionName(opt: number): string {\n return OPTION_NAMES[opt] ?? `OPT(${opt})`;\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,GAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,OAAA,WAAA,GAAA,eAAA,GAAA;AACA,cAAA,GAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,WAAA,WAAA,GAAA,eAAA,GAAA;AACA,cAAA,KAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,SAAA,WAAA,KAAA,eAAA,KAAA;AACA,cAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,iBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,sBAAA,WAAA,iBAAA,eAAA,iBAAA;AACA,cAAA,KAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,UAAA,WAAA,KAAA,eAAA,KAAA;AACA,cAAA,EAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,KAAA;AACP;AACA;AACA;AACA;AACO,cAAA,OAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,cAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA,6BAAA,MAAA;AACA;AACA,kBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,mBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACzQA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,aAAA,2BAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,cAAA,8BAAA,UAAA;;AC5BP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,QAAA,oBAAA,UAAA;AACP;AACA;AACA;AACO,iBAAA,mBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACO,iBAAA,sBAAA,OAAA,UAAA,GAAA,UAAA;AACP;AACA;AACA;AACA;AACO,iBAAA,qBAAA;;ACzCP;AACA;AACA;AACA;AACA;AACO,cAAA,QAAA,SAAA,KAAA;AACP;AACA;AACA;AACO,cAAA,gBAAA,SAAA,QAAA;AACP;AACA;AACA;AACO,cAAA,gBAAA,SAAA,QAAA;AACP;AACA;AACA;AACO,aAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,IAAA;;AC7BP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA;AACA,UAAA,cAAA;AACA;AACA;AACA;AACA;AACA;;AAYO,cAAA,GAAA,SAAA,YAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAA,SAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAA,KAAA;AACA;AACA;AACA;AACA,aAAA,UAAA;AACA;AACA,aAAA,UAAA;AACA;AACA,aAAA,UAAA;AACA;AACA,aAAA,UAAA;AACA;AACA,aAAA,UAAA;AACA;AACA,aAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAA,GAAA;AACA;AACA,eAAA,GAAA;AACA;AACA,eAAA,GAAA;AACA;AACA,eAAA,GAAA;AACA;AACA,cAAA,GAAA;AACA;AACA,gBAAA,GAAA;AACA;AACA,gBAAA,GAAA;AACA;AACA,gBAAA,GAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,MAAA,CAAA,WAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,+BAAA,UAAA;AACA;AACA;AACA;AACA;AACA,yCAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oDAAA,cAAA,GAAA,OAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAAA,OAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA,mBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,mBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,sBAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAA,UAAA,uBAAA,UAAA;AACA,qBAAA,UAAA,iCAAA,UAAA;AACA,gCAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA,8CAAA,SAAA;AACA,4CAAA,SAAA;AACA;AACA,yCAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wDAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6CAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9iBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACO,cAAA,QAAA,SAAA,KAAA;AACP;AACA;AACA;AACO,cAAA,IAAA;AACP;AACO,cAAA,GAAA;AACP;AACO,cAAA,KAAA;AACP;AACO,cAAA,IAAA;AACA,cAAA,KAAA;AACA,cAAA,KAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,GAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,IAAA;AACA,cAAA,GAAA;AACA,cAAA,OAAA;AACA,cAAA,IAAA;AACA,cAAA,OAAA;AACA,cAAA,OAAA;AACA,cAAA,OAAA;AACA,cAAA,QAAA;AACA,cAAA,KAAA;AACA,cAAA,GAAA;AACA,cAAA,QAAA;AACA,cAAA,MAAA;AACA,cAAA,KAAA;AACA,cAAA,IAAA;AACP;AACO,UAAA,cAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,GAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0CAAA,cAAA,GAAA,OAAA,CAAA,GAAA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAA,GAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2BAAA,GAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAA,GAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAA,OAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8CAAA,OAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8CAAA,OAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uDAAA,OAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/SA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,gBAAA,MAAA,MAAA,GAAA,QAAA;AACP;AACA;AACA;AACA;AACA;AACO,iBAAA,SAAA,OAAA,MAAA,GAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,WAAA,OAAA,MAAA,GAAA,MAAA;AACP;AACO,iBAAA,SAAA,eAAA,MAAA;AACP;AACO,iBAAA,SAAA,eAAA,MAAA;AACP;AACO,iBAAA,OAAA,eAAA,MAAA;AACP;AACO,iBAAA,SAAA,eAAA,MAAA;AACP;AACA;AACA;AACA;AACO,iBAAA,QAAA,QAAA,MAAA,GAAA,MAAA;AACP;AACO,iBAAA,QAAA,IAAA,MAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,YAAA,gBAAA,MAAA;AACP;AACA;AACA;AACA;AACO,iBAAA,UAAA;;;;"}
|