ibm5250 0.1.0.dev7__tar.gz → 0.1.0.dev8__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ibm5250
3
- Version: 0.1.0.dev7
3
+ Version: 0.1.0.dev8
4
4
  Summary: IBM 5250 terminal automation library
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ibm5250"
3
- version = "0.1.0.dev7"
3
+ version = "0.1.0.dev8"
4
4
  description = "IBM 5250 terminal automation library"
5
5
  requires-python = ">=3.11"
6
6
 
@@ -19,6 +19,7 @@ from .constants import (
19
19
  FFW,
20
20
  SCREEN_COLS,
21
21
  SCREEN_ROWS,
22
+ Attr,
22
23
  Command,
23
24
  Order,
24
25
  TelnetCmd,
@@ -65,6 +66,7 @@ __all__ = [
65
66
  "TelnetCmd",
66
67
  "TelnetOpt",
67
68
  "FFW",
69
+ "Attr",
68
70
  "SCREEN_ROWS",
69
71
  "SCREEN_COLS",
70
72
  # Address helpers
@@ -26,7 +26,6 @@ from dataclasses import dataclass
26
26
 
27
27
  from .constants import (
28
28
  AID,
29
- CC2,
30
29
  Command,
31
30
  TelnetCmd,
32
31
  TelnetOpt,
@@ -49,7 +48,6 @@ _TELNET_OPT_CMD_LEN = 3 # bytes: IAC + command + option (WILL/WONT/DO/DONT)
49
48
  # SNA / TN5250E wire-format framing constants
50
49
  _GDS_RECORD_ID_HI = 0x12 # SNA GDS record-type identifier, high byte
51
50
  _GDS_RECORD_ID_LO = 0xA0 # SNA GDS record-type identifier, low byte
52
- _SAVE_SCREEN_RESP_CC1 = 0x01 # CC1 value in the Save Screen response variable header
53
51
 
54
52
  # TN5250E header byte 7 (request/response flags). Real emulators set bit 0x40
55
53
  # here to signal the Attention (Attn) key -- a header-only INPUT record with no
@@ -73,6 +71,20 @@ _MAX_NEG_PASSES = 40 # Maximum Telnet option exchange iterations during negotia
73
71
 
74
72
  _TN5250E_HEADER_LEN = 10
75
73
 
74
+ # The header splits into a 6-byte fixed part (length, GDS id, flow type) and a
75
+ # variable-length part whose length is given by byte 6 -- so the 5250 payload
76
+ # begins at ``6 + header[6]`` (normally 6 + 4 = 10).
77
+ _TN5250E_FIXED_HEADER_LEN = 6
78
+
79
+
80
+ def _payload_offset(frame: bytes) -> int:
81
+ """Return the offset of the 5250 payload within a TN5250E *frame*."""
82
+ offset = _TN5250E_FIXED_HEADER_LEN + frame[_TN5250E_FIXED_HEADER_LEN]
83
+ if not _TN5250E_HEADER_LEN <= offset <= len(frame):
84
+ # Malformed variable-header length; fall back to the standard 10 bytes.
85
+ return _TN5250E_HEADER_LEN
86
+ return offset
87
+
76
88
 
77
89
  @dataclass
78
90
  class TN5250EHeader:
@@ -325,23 +337,20 @@ class TN5250Connection:
325
337
  frame = _build_tn5250e_frame(req_resp=0x00, opcode=_TN5250E_OP_CANCEL_INVITE)
326
338
  self._send_frame(frame, description="Sending Cancel-Invite response")
327
339
 
328
- def send_save_screen_response(self) -> None:
340
+ def send_save_screen_response(self, save_id: bytes) -> None:
329
341
  """Send the TN5250E-level Save Screen acknowledgment.
330
342
 
331
343
  When the server issues an ESC+SAVE_SCREEN (0x04 0x02) command the
332
344
  terminal must reply with a matching TN5250E frame whose variable header
333
- encodes ESC+RESTORE_SCREEN+CC1+CC2 (mirroring what working emulators
334
- send). This is a protocol-level exchange with no 5250 data stream
335
- payload.
345
+ encodes ESC + RESTORE_SCREEN + a two-byte identifier for the screen it
346
+ just saved. The host stores that identifier and echoes it back in the
347
+ Restore Screen command when it wants that particular screen displayed
348
+ again, so every save must use a distinct *save_id*.
336
349
  """
337
350
  if not self._tn5250e_active:
338
351
  return # save/restore handshake only occurs in TN5250E mode
339
352
 
340
- # 4-byte variable header: [ESC, RESTORE_SCREEN, CC1, CC2.RESET_MDT].
341
- # byte 9 of the fixed header carries the variable-header length.
342
- var_hdr = bytes(
343
- [Command.ESC, Command.RESTORE_SCREEN, _SAVE_SCREEN_RESP_CC1, CC2.RESET_MDT]
344
- )
353
+ var_hdr = bytes([Command.ESC, Command.RESTORE_SCREEN]) + bytes(save_id)
345
354
  frame = _build_tn5250e_frame(req_resp=0x00, opcode=len(var_hdr), var_hdr=var_hdr)
346
355
  self._send_frame(frame, description="Sending Save Screen response")
347
356
 
@@ -354,8 +363,8 @@ class TN5250Connection:
354
363
  log.debug("Short TN5250E frame (%d bytes), skipping", len(frame))
355
364
  continue
356
365
  hdr = TN5250EHeader.from_bytes(frame)
357
- self._last_recv_opcode = frame[9] # byte 9: opcode / var_len
358
- payload = bytes(frame[_TN5250E_HEADER_LEN:])
366
+ self._last_recv_opcode = frame[9] # byte 9: opcode
367
+ payload = bytes(frame[_payload_offset(frame) :])
359
368
  log.debug(
360
369
  "← TN5250E type=%02X data_type=%02X payload=%d bytes",
361
370
  hdr.record_type,
@@ -365,20 +374,16 @@ class TN5250Connection:
365
374
  else:
366
375
  frame_bytes = bytes(frame)
367
376
  # IBM i sends TN5250E-formatted records even when TN5250E was
368
- # not formally negotiated. The 10-byte fixed header begins
369
- # with a big-endian 16-bit record length that equals the total
370
- # frame size. Byte 9 of the header is the variable-header
371
- # length — that many additional bytes follow before the real
372
- # 5250 payload.
377
+ # not formally negotiated. The header begins with a big-endian
378
+ # 16-bit record length that equals the total frame size, and
379
+ # byte 6 holds the length of the variable header that follows
380
+ # it — the real 5250 payload starts after that.
373
381
  if len(frame_bytes) >= _TN5250E_HEADER_LEN and int.from_bytes(
374
382
  frame_bytes[:2], "big"
375
383
  ) == len(frame_bytes):
376
384
  hdr = TN5250EHeader.from_bytes(frame_bytes)
377
385
  self._last_recv_opcode = frame_bytes[9] # byte 9: opcode
378
- var_hdr_len = frame_bytes[
379
- 9
380
- ] # RFC 2877 variable-header length field
381
- skip = _TN5250E_HEADER_LEN + var_hdr_len
386
+ skip = _payload_offset(frame_bytes)
382
387
  payload = frame_bytes[skip:]
383
388
  # Server is speaking TN5250E — our responses must include the header too
384
389
  if not self._tn5250e_active:
@@ -387,9 +392,8 @@ class TN5250Connection:
387
392
  )
388
393
  self._tn5250e_active = True
389
394
  log.debug(
390
- "← implicit TN5250E header stripped (fixed=%d var=%d) data_type=%02X payload=%d bytes",
391
- _TN5250E_HEADER_LEN,
392
- var_hdr_len,
395
+ "← implicit TN5250E header stripped (%d bytes) data_type=%02X payload=%d bytes",
396
+ skip,
393
397
  hdr.data_type,
394
398
  len(payload),
395
399
  )
@@ -83,11 +83,12 @@ class TN5250ESubNeg:
83
83
 
84
84
 
85
85
  class Command:
86
- INLINE_WRITE_STRUCTURED_FIELD = (
87
- 0x02 # Introduces an inline WSF record within a WTD stream
86
+ ESC = 0x04 # precedes every 5250 command in a record
87
+ CLEAR_UNIT = 0x40 # erase the display, reset it to the primary size (24×80)
88
+ CLEAR_UNIT_ALTERNATE = (
89
+ 0x20 # erase the display, switch to the alternate size (27×132);
90
+ # followed by a one-byte parameter (0x00 or 0x80)
88
91
  )
89
- ESC = 0x04 # precedes Write Structured Field inline
90
- CLEAR_UNIT = 0x40 # erase the display and format table
91
92
  WRITE_TO_DISPLAY = (
92
93
  0x11 # main screen update command; followed by CC1/CC2 and orders
93
94
  )
@@ -105,16 +106,16 @@ class Command:
105
106
 
106
107
  # Write to Display control characters (CC1 / CC2)
107
108
  class CC1:
108
- """Control Character 1 — first byte after WTD command.
109
+ """Control Character 1 — first byte after the WTD command.
109
110
 
110
- Only bits 0-2 are defined by the IBM 5250 spec; bits 3-7 are reserved.
111
- Keyboard lock/unlock is controlled exclusively by CC2, not CC1.
111
+ CC1 selects which Modified Data Tags and input-field buffers the host wants
112
+ reset before the write is applied. It does **not** erase the screen and it
113
+ does **not** change the screen dimensions — those are driven exclusively by
114
+ the Clear Unit (:data:`Command.CLEAR_UNIT`) and Clear Unit Alternate
115
+ (:data:`Command.CLEAR_UNIT_ALTERNATE`) commands.
112
116
  """
113
117
 
114
118
  RESET = 0x00
115
- CLEAR_UNIT = 0x20 # Erase entire screen before write
116
- CLEAR_UNIT_ALT = 0x40 # Erase screen using alternate screen dimensions
117
- CLEAR_FORMAT_TABLE = 0x60 # Discard field definitions only; no screen erase
118
119
  SOUND_ALARM = 0x80 # Audible alarm
119
120
 
120
121
 
@@ -133,22 +134,32 @@ class CC2:
133
134
  # ---------------------------------------------------------------------------
134
135
 
135
136
  class Order:
137
+ """5250 orders embedded in a Write to Display data stream.
138
+
139
+ Note these are *5250* order codes -- they differ from the 3270 codes of the
140
+ same name (e.g. 5250 Repeat to Address is 0x02, not 0x3C).
141
+ """
142
+
136
143
  START_OF_HEADER = 0x01 # Start of Header (variable-length; skip past it)
144
+ REPEAT_TO_ADDRESS = 0x02 # Repeat to Address — 2 address bytes + 1 fill char
145
+ ERASE_TO_ADDRESS = (
146
+ 0x03 # Erase to Address — 2 address bytes + length + attribute types
147
+ )
137
148
  TRANSPARENT_DATA = 0x10 # Transparent Data — 2-byte length + raw EBCDIC
138
149
  SET_BUFFER_ADDRESS = 0x11 # Set Buffer Address — 2 address bytes
139
- ERASE_TO_ADDRESS = (
140
- 0x12 # Erase to Address — 2 address bytes (fill with nulls)
150
+ WRITE_EXTENDED_ATTRIBUTE = (
151
+ 0x12 # Write Extended Attribute — 2 bytes; not emulated
141
152
  )
142
153
  INSERT_CURSOR = 0x13 # Insert Cursor — 2 address bytes; marks cursor pos
143
154
  MOVE_CURSOR = 0x14 # Move Cursor — 2 address bytes
144
- GROUP_ERASE_TO_ADDRESS = 0x16 # Group Erase to Address — 2 address bytes (fill with nulls, within field group)
155
+ WRITE_DISPLAY_STRUCTURED_FIELD = (
156
+ 0x15 # Write to Display Structured Field — 2-byte length + payload
157
+ )
145
158
  START_FIELD = 0x1D # Start Field — 2-byte FFW [+ 2-byte FCW pairs]
146
- SET_ATTRIBUTE = 0x28 # Set Attribute — 1 attribute type + 1 value byte
147
- MODIFY_FIELD = 0x2C # Modify Field — like SF but doesn't clear data
148
- REPEAT_TO_ADDRESS = 0x3C # Repeat to Address — 2 address bytes + 1 fill char
149
159
 
150
- _KNOWN_ORDERS = frozenset(
151
- v for k, v in vars(Order).items() if not k.startswith("_")
160
+
161
+ KNOWN_ORDERS = frozenset(
162
+ v for k, v in vars(Order).items() if isinstance(v, int) and not k.startswith("_")
152
163
  )
153
164
 
154
165
  # ---------------------------------------------------------------------------
@@ -218,36 +229,56 @@ class AID:
218
229
 
219
230
 
220
231
  class FFW:
221
- """Field Format Word bits (byte 0 and byte 1 of the 2-byte FFW)."""
232
+ """Field Format Word bits (byte 0 and byte 1 of the 2-byte FFW).
222
233
 
223
- # Byte 0
234
+ Note the FFW carries *behaviour* only. A field's visual appearance comes
235
+ from the separate attribute byte that follows the FFW/FCWs in the SF order
236
+ (see :class:`Attr`).
237
+ """
238
+
239
+ # Byte 0 — bit 0x40 is the "this is an FFW" identifier and is always set.
240
+ IDENTIFIER = 0x40 # Marks the byte as a Field Format Word rather than an attribute
224
241
  BYPASS = 0x20 # Protected field — no input allowed (read-only)
225
242
  DUP_ENABLE = 0x10 # Dup key enabled — user can duplicate the previous field's value
226
- MDT = 0x01 # Modified Data Tag — set when the field has been touched
227
- AUTO_ENTER = 0x08 # Auto Enter — automatically submit when the field is filled
228
- FER = 0x04 # Field Exit Required — user must press Field Exit before leaving
229
-
230
- # Byte 1 (keyboard shift / display attribute)
243
+ MDT = 0x08 # Modified Data Tag — set when the field has been touched
244
+ # Byte 0, low 3 bits — field type (keyboard shift)
231
245
  ALPHA_SHIFT = 0x00 # Alphabetic shift — accepts any character
232
- ALPHA_ONLY = 0x04 # Alphabetic only — rejects digits and special characters
233
- NUM_SHIFT = 0x08 # Numeric shift — defaults keyboard to numeric mode
234
- NUM_ONLY = 0x18 # Numeric only — rejects non-numeric input
235
- KATA_SHIFT = 0x0C # Katakana shift — Japanese katakana character entry
236
- DIGITS_ONLY = 0x1C # Digits only — accepts 0–9 with no sign
237
- IO_ONLY = 0x10 # Inhibit Output only — field is write-protected from the keyboard
238
- SIGNED_NUM = 0x14 # Signed numeric — accepts digits plus a trailing sign character
239
- # Display attribute (high nibble of byte 1)
240
- DISP_NORM = 0x00 # Normal — green on black
241
- DISP_HIGH = 0x20 # High intensity — bright green on black
242
- DISP_RI = 0x40 # Reverse image — black on green
243
- DISP_RI_HIGH = 0x60 # Reverse image high intensity — bright black on green
244
- DISP_UNDER = 0x80 # Underline — green with underline
245
- DISP_UNDER_HI = 0xA0 # Underline high intensity — bright green with underline
246
- DISP_BLINK = 0xC0 # Blink — flashing field
247
- NON_DISPLAY = 0xE0 # Non-display — invisible; used for password entry
248
- DISPLAY_ATTR_MASK = (
249
- 0xE0 # Bitmask for the display attribute field (high 3 bits of byte 1)
250
- )
246
+ ALPHA_ONLY = 0x01 # Alphabetic only — rejects digits and special characters
247
+ NUM_SHIFT = 0x02 # Numeric shift — defaults keyboard to numeric mode
248
+ NUM_ONLY = 0x03 # Numeric only — rejects non-numeric input
249
+ KATA_SHIFT = 0x04 # Katakana shift — Japanese katakana character entry
250
+ DIGITS_ONLY = 0x05 # Digits only — accepts 0–9 with no sign
251
+ MAG_READER = 0x06 # Magnetic stripe reader input only
252
+ SIGNED_NUM = 0x07 # Signed numeric — accepts digits plus a trailing sign character
253
+ FIELD_TYPE_MASK = 0x07 # Bitmask for the field type bits of byte 0
254
+
255
+ # Byte 1
256
+ AUTO_ENTER = 0x80 # Auto Enter — automatically submit when the field is filled
257
+ FER = 0x40 # Field Exit Required — user must press Field Exit before leaving
258
+ MONOCASE = 0x20 # Monocase — lower-case input is folded to upper case
259
+ MANDATORY = 0x08 # Mandatory entry — the field may not be left empty
260
+ RIGHT_ZERO = 0x05 # Right-adjust, zero fill
261
+ RIGHT_BLANK = 0x06 # Right-adjust, blank fill
262
+ MANDATORY_FILL = 0x07 # Mandatory fill — the field must be filled completely
263
+ MANDATORY_FILL_MASK = 0x07 # Bitmask for the adjust/fill bits of byte 1
264
+
265
+
266
+ class Attr:
267
+ """5250 display attribute byte values (0x20-0x3F).
268
+
269
+ Every attribute occupies one display position and governs the appearance of
270
+ the characters that follow it until the next attribute byte.
271
+ """
272
+
273
+ NORMAL = 0x20 # Green
274
+ REVERSE = 0x21 # Green, reverse image
275
+ HIGH_INTENSITY = 0x22 # White
276
+ UNDERLINE = 0x24 # Green, underlined
277
+ NON_DISPLAY = 0x27 # Invisible — used for passwords and command strings
278
+ RED = 0x28 # Red
279
+ BASE = 0x20 # All attribute bytes have this bit pattern in their high bits
280
+ MASK = 0xE0 # ``byte & MASK == BASE`` identifies an attribute byte
281
+ NON_DISPLAY_MASK = 0x07 # ``byte & MASK == 0x07`` means non-display
251
282
 
252
283
 
253
284
  # ---------------------------------------------------------------------------
@@ -436,6 +467,10 @@ SCREEN_COLS = 80
436
467
  SCREEN_ROWS_27 = 27 # 27×132 model
437
468
  SCREEN_COLS_132 = 132
438
469
 
470
+ # Length of the screen identifier the workstation returns in its Save Screen
471
+ # acknowledgment and that the host echoes back in a Restore Screen command.
472
+ SAVE_SCREEN_ID_LEN = 2
473
+
439
474
  EBCDIC_SPACE = 0x40
440
475
  EBCDIC_NULL = 0x00
441
476
 
@@ -19,7 +19,11 @@ The **TN5250Connection** manages the TCP socket to the IBM i (AS/400) host.
19
19
  1. Reads bytes from the socket into a buffer.
20
20
  2. Scans for the IAC-EOR (Interpret As Command — End Of Record) sequence (0xFF 0xEF) which marks the end of a logical record.
21
21
  3. Un-escapes any doubled IAC bytes (0xFF 0xFF → 0xFF).
22
- 4. If TN5250E is active, strips the 10-byte header + variable header → returns the raw 5250 payload.
22
+ 4. If TN5250E is active, strips the header → returns the raw 5250 payload. The
23
+ header is six fixed bytes followed by a variable part whose **length is byte 6**
24
+ (normally 4, giving the familiar 10-byte total), so the payload starts at
25
+ `6 + frame[6]`. Byte 9 is the _opcode_, not the header length — confusing the
26
+ two silently truncates the first bytes of the payload.
23
27
  5. If the socket has no data within the timeout, raises **RecvTimeout**.
24
28
 
25
29
  ### Sending (`send_record`)
@@ -33,7 +37,12 @@ The **TN5250Connection** manages the TCP socket to the IBM i (AS/400) host.
33
37
 
34
38
  ### Save Screen Response (`send_save_screen_response`)
35
39
 
36
- When the server issues a SAVE_SCREEN command, the terminal must reply with a special TN5250E-framed acknowledgment (no 5250 payload — just a variable header encoding ESC + RESTORE_SCREEN + CC1 + CC2). This tells the server "I've saved the screen; you can now overlay it."
40
+ When the server issues a SAVE_SCREEN command, the terminal must reply with a
41
+ TN5250E-framed acknowledgment carrying `ESC + RESTORE_SCREEN + a two-byte
42
+ identifier`. This tells the server "I've saved the screen under this name; you
43
+ can now overlay it." **Every save must use a distinct identifier** — the server
44
+ stores it and echoes it back later to say _which_ saved screen it wants
45
+ restored. See [Save/Restore Screen Flow](#saverestore-screen-flow).
37
46
 
38
47
  ---
39
48
 
@@ -43,28 +52,37 @@ The **DataStreamParser** takes a raw 5250 payload (one record from the connectio
43
52
 
44
53
  ### Record Structure
45
54
 
46
- Every 5250 record starts with a **command byte**:
47
-
48
- | Command | Meaning |
49
- | ------------------------------- | ----------------------------------------------------------------------------------- |
50
- | `0x11` (Write to Display) | Main screen update — followed by CC1, CC2, then orders/data |
51
- | `0x40` (Clear Unit) | Clears the display and field definitions |
52
- | `0x02` (Save Screen) | Server wants to save the current screen (same format as WTD) |
53
- | `0x12` (Restore Screen) | Server wants to restore a previously saved screen |
54
- | `0x04` (ESC) | Escape prefix — next byte is the real command (Clear Unit, WTD, WSF, Save, Restore) |
55
- | `0xF3` (Write Structured Field) | Carries structured data like Query commands |
56
- | `0x42/0x52/0x72/0x62` | Read commands — server asks client to send back screen/field data |
57
- | `0x21` (Write Error Code) | Display error on status line |
58
- | `0x50` (Clear Format Table) | Discard field definitions |
55
+ Every command in a record is introduced by **ESC (`0x04`)**, and one record may
56
+ carry several commands back to back:
57
+
58
+ | Command | Meaning |
59
+ | ------------------------------- | ---------------------------------------------------------------------------------------- |
60
+ | `0x11` (Write to Display) | Main screen update — followed by CC1, CC2, then orders/data |
61
+ | `0x40` (Clear Unit) | Erases the display and field definitions and selects the **primary** size (24×80) |
62
+ | `0x20` (Clear Unit Alternate) | Erases the display and selects the **alternate** size (27×132); takes one parameter byte |
63
+ | `0x02` (Save Screen) | Server wants the client to save the current screen |
64
+ | `0x12` (Restore Screen) | Server wants a previously saved screen restored; followed by its two-byte identifier |
65
+ | `0xF3` (Write Structured Field) | Carries structured data like Query commands |
66
+ | `0x42/0x52/0x72/0x62` | Read commands — server asks client to send back screen/field data |
67
+ | `0x21` (Write Error Code) | Display error on status line |
68
+ | `0x50` (Clear Format Table) | Discard field definitions |
69
+
70
+ ### Screen size
71
+
72
+ The display size is **absolute and set only by the clear commands**: Clear Unit
73
+ selects 24×80, Clear Unit Alternate selects 27×132. It is never a toggle, and it
74
+ is never inferred from out-of-range buffer addresses. Getting this wrong is what
75
+ made a screen returning from a 132-column layout keep rendering as 27×132 with
76
+ stale content bleeding through.
59
77
 
60
78
  ### Control Characters (CC1 / CC2)
61
79
 
62
- After a WTD or Save Screen command, two bytes control screen-level behavior:
80
+ Write to Display is followed by two control bytes:
63
81
 
64
- - **CC1** — Screen clear mode:
65
- - `0x20`: Clear the entire screen before writing new data
66
- - `0x40`: Clear and switch to alternate screen size (24×80 ↔ 27×132)
67
- - `0x00`: Don't clear — overlay on existing content
82
+ - **CC1** — Lock/reset flags. Notably CC1 **neither erases the screen nor
83
+ changes its dimensions**; only the clear commands above do that.
84
+ - `0x01`/`0x02`: Reset pending AIDs / unlock after the write
85
+ - `0x04`: Sound the alarm
68
86
 
69
87
  - **CC2** — Keyboard and field flags:
70
88
  - `0x08`: Unlock the keyboard (let the user type)
@@ -75,24 +93,49 @@ After a WTD or Save Screen command, two bytes control screen-level behavior:
75
93
 
76
94
  After the command + CC bytes, the rest of the record is a stream of **orders** intermixed with **data bytes**:
77
95
 
78
- | Order | What it does |
79
- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
- | **SBA** — Set Buffer Address (`0x11`) + row + col | Move the write position to a specific screen cell |
81
- | **SF** — Start Field (`0x1D`) + FFW [+ FCWs] | Define a new input/output field at the current position. The FFW (Field Format Word, 2 bytes) defines field type — bypass/input, numeric, non-display, etc. Optional FCW (Field Control Word) pairs add behaviours like mandatory entry, right-adjust, or monocase. |
82
- | **MF** — Modify Field (`0x2C`) + FFW [+ FCWs] | Modify an existing field's attributes |
83
- | **IC** — Insert Cursor (`0x13`) | Place the cursor at the 2-byte address that follows (falls back to the current position only when no address bytes remain) |
84
- | **MC** — Move Cursor (`0x14`) + row + col | Move the cursor to a specific position |
85
- | **RA** — Repeat to Address (`0x3C`) + row + col + char | Repeat a character from current position up to the target |
86
- | **EA** — Erase to Address (`0x12`) + row + col | Erase (fill with nulls) from current position to target |
87
- | **TD** — Transparent Data (`0x10`) + length + data | Write raw bytes (transparent, no order interpretation) |
88
- | **SA** — Set Attribute (`0x28`) + type + value | Set a display attribute (colour, underline, etc.) |
89
- | **SOH** — Start of Header (`0x01`) + length + data | Metadata block, skipped |
96
+ These are the **5250** order codes; several share a name with a 3270 order but
97
+ use a different byte value (5250 Repeat to Address is `0x02`, not `0x3C`).
98
+
99
+ | Order | What it does |
100
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
101
+ | **SOH** — Start of Header (`0x01`) + length + data | Metadata block, skipped |
102
+ | **RA** — Repeat to Address (`0x02`) + row + col + char | Repeat a character from the current position up to **and including** the target |
103
+ | **EA** — Erase to Address (`0x03`) + row + col + len + types | Erase (fill with nulls) from the current position through the target. `len` counts itself plus the extended-attribute type bytes that follow |
104
+ | **TD** — Transparent Data (`0x10`) + length + data | Write raw bytes (transparent, no order interpretation) |
105
+ | **SBA** — Set Buffer Address (`0x11`) + row + col | Move the write position to a specific screen cell |
106
+ | **WEA** — Write Extended Attribute (`0x12`) + type + value | Extended character buffer attribute; not emulated, operands are consumed |
107
+ | **IC** — Insert Cursor (`0x13`) | Place the cursor at the 2-byte address that follows (falls back to the current position only when no address bytes remain) |
108
+ | **MC** — Move Cursor (`0x14`) + row + col | Move the cursor to a specific position |
109
+ | **WDSF** — Write to Display Structured Field (`0x15`) | GUI constructs (windows, scrollbars); not emulated, the payload is skipped |
110
+ | **SF** — Start Field (`0x1D`) | Define a field at the current position — see below |
90
111
 
91
112
  Anything that isn't a recognized order byte is treated as **EBCDIC (Extended Binary Coded Decimal Interchange Code)** character data and written sequentially into the screen buffer.
92
113
 
93
- ### Inline Structured Fields (0x02 / 0x04 in the order stream)
114
+ ### Attribute bytes
115
+
116
+ Bytes in the range **`0x20`–`0x3F`** are display attributes. They are _data_:
117
+ each one occupies a screen position (rendered as a blank) and governs the
118
+ appearance of everything after it until the next attribute. `0x27` — more
119
+ precisely any attribute whose low three bits are all set — means _non-display_,
120
+ which is how password fields and hidden command strings are rendered blank.
121
+
122
+ Because attributes are the only bytes with that bit pattern, they double as the
123
+ terminator for the variable-length part of an SF order.
124
+
125
+ ### Start Field (SF)
94
126
 
95
- IBM i sometimes embeds 3–4 byte attribute markers (`0x02 XX YY` or `0x04 XX YY ZZ`) inline within the data stream to carry extended colour/highlight info. The parser identifies these and skips over them without writing them as screen text.
127
+ SF (0x1D) [FFW1 FFW2 [FCW1 FCW2]...] ATTR LEN_HI LEN_LO
128
+
129
+ - An **output-only** field omits the FFW and FCWs entirely: the byte straight
130
+ after SF is then the attribute.
131
+ - The **FFW** (Field Format Word) defines field behaviour — bypass/input,
132
+ keyboard shift, mandatory entry, and so on. Byte 0 always has bit `0x40` set,
133
+ which is how it is told apart from an attribute byte.
134
+ - Zero or more **FCW** (Field Control Word) pairs may follow; the chain ends at
135
+ the first attribute byte, _not_ at a continuation bit.
136
+ - The attribute byte is written at the field's start position, and the field's
137
+ length is the **two bytes after it** — it is not derived from an FCW and it is
138
+ not inferred from where the next field begins.
96
139
 
97
140
  ### Write Structured Field (WSF / Query)
98
141
 
@@ -167,8 +210,9 @@ This is the central method — it:
167
210
  2. Feeds it through `parser.parse()` to get a list of `DSEvent` objects.
168
211
  3. Iterates through events looking for special commands:
169
212
  - **QUERY** → sends back a Query Reply with terminal capabilities
170
- - **SAVE_SCREEN** → deep-copies the current `Screen` onto a stack, sends the save-screen acknowledgment to the server
171
- - **RESTORE_SCREEN** → pops the saved screen off the stack, replaces the current screen with it, then applies any remaining events from the same record on top
213
+ - **SAVE_SCREEN** → deep-copies the current `Screen`, files it under a fresh identifier, and returns that identifier in the save-screen acknowledgment
214
+ - **RESTORE_SCREEN** → looks the identifier the server sent up among the saved screens, makes it current (dropping any newer saves), resizes the parser to its dimensions, then applies any remaining events from the same record on top
215
+ - **RESIZE_SCREEN** → re-creates the parser and screen at the new dimensions
172
216
  4. If no RESTORE occurred, applies all events to the screen normally.
173
217
  5. **Post-RESTORE keyboard drain**: After restoring a screen, the keyboard is locked (because it was locked when saved). The server sends a follow-up record to unlock it. The terminal tries one more `_recv_and_apply()` to consume that unlock record. If nothing comes (RecvTimeout), it moves on.
174
218
 
@@ -194,12 +238,30 @@ This is the central method — it:
194
238
  This is how the server does "popup" screens (like overlaying a detail view on a list):
195
239
 
196
240
  1. Server sends **SAVE_SCREEN** → terminal saves a copy of the current screen
241
+ under a fresh two-byte identifier and returns that identifier in the Save
242
+ Screen response
197
243
  2. Server sends a normal WTD with new content → terminal shows the popup
198
244
  3. User interacts with the popup (sends keys, gets responses)
199
- 4. Server sends **RESTORE_SCREEN** → terminal swaps back to the saved screen
245
+ 4. Server sends **RESTORE_SCREEN + identifier** → terminal swaps back to _that_
246
+ saved screen and discards any screens saved after it
200
247
  5. Server sends a follow-up WTD to unlock the keyboard on the restored screen
201
248
 
202
- The tricky part: after step 4, the restored screen has `keyboard_locked = True` (it was saved right after the client sent an AID key). The terminal must consume the unlock record (step 5) before attempting the next `send_key`, otherwise the server rejects the input.
249
+ Saves nest, so this is **not** a stack the terminal may simply pop: the server
250
+ decides how far to unwind and names the screen it wants. Unwinding two levels
251
+ in one go — restore `04 05`, Clear Unit, restore `03 04` — is normal, and
252
+ popping blindly restores the wrong screen.
253
+
254
+ A restored screen brings its own dimensions with it, so the parser must be
255
+ re-created to match whenever one is restored.
256
+
257
+ The other tricky part: after step 4, the restored screen has
258
+ `keyboard_locked = True` (it was saved right after the client sent an AID key).
259
+ The terminal must consume the unlock record (step 5) before attempting the next
260
+ `send_key`, otherwise the server rejects the input.
261
+
262
+ Screens restored this way are usually followed by _partial_ updates that repaint
263
+ only the parts of the screen that changed, so the restored snapshot has to be
264
+ byte-accurate — any error in it survives into the final display.
203
265
 
204
266
  ---
205
267