ibm5250 0.1.0.dev0__tar.gz → 0.1.0.dev2__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.
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/.github/copilot-instructions.md +1 -1
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/.github/workflows/README.md +14 -5
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/.github/workflows/publish.yml +16 -1
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/.gitignore +4 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/PKG-INFO +2 -2
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/README.md +5 -5
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/pyproject.toml +1 -1
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/connection.py +121 -31
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/data-stream-logic.md +23 -22
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/terminal.py +22 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/uv.lock +1 -1
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/LICENSE +0 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/scripts/compute_version.py +0 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/__init__.py +0 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/constants.py +0 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/datastream.py +0 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/exceptions.py +0 -0
- {ibm5250-0.1.0.dev0 → ibm5250-0.1.0.dev2}/src/ibm5250/screen.py +0 -0
|
@@ -53,7 +53,7 @@ interact.
|
|
|
53
53
|
row*cols+col — always use `encode_addr`/`decode_addr`/`rowcol_to_pos`/
|
|
54
54
|
`pos_to_rowcol` rather than hand-rolling the math).
|
|
55
55
|
6. **`exceptions.py`** — all custom exceptions derive from `IBM5250Error`.
|
|
56
|
-
Note `RecvTimeout` is
|
|
56
|
+
Note `RecvTimeout` is _not_ fatal (socket alive, server just idle) — it's
|
|
57
57
|
distinct from `ConnectionError`.
|
|
58
58
|
|
|
59
59
|
Key stateful quirk: after a server-initiated **SAVE_SCREEN**/**RESTORE_SCREEN**
|
|
@@ -7,11 +7,11 @@ tokens or secrets are stored in this repository.
|
|
|
7
7
|
|
|
8
8
|
## Branch behavior
|
|
9
9
|
|
|
10
|
-
| Branch
|
|
11
|
-
|
|
12
|
-
| `main`
|
|
13
|
-
| `develop`
|
|
14
|
-
| any other branch
|
|
10
|
+
| Branch | Test | Build | Publish | Version format |
|
|
11
|
+
| ---------------- | :--: | :---: | :-------------------------: | --------------------------------------------------------------------------------- |
|
|
12
|
+
| `main` | ✅ | ✅ | ✅ (real PyPI, production) | `X.Y.Z` (patch auto-incremented from the latest published release for that `X.Y`) |
|
|
13
|
+
| `develop` | ✅ | ✅ | ✅ (real PyPI, development) | `X.Y.Z.devN` (`N` auto-incremented per base version) |
|
|
14
|
+
| any other branch | ✅ | ✅ | ❌ | n/a (build artifact only, not published) |
|
|
15
15
|
|
|
16
16
|
The base version (`project.version` in `pyproject.toml`) is **never modified in
|
|
17
17
|
the repository**. `scripts/compute_version.py` queries PyPI's public JSON API
|
|
@@ -19,6 +19,15 @@ the repository**. `scripts/compute_version.py` queries PyPI's public JSON API
|
|
|
19
19
|
version number, and `uv version` applies it only to the ephemeral CI checkout
|
|
20
20
|
before `uv build` runs.
|
|
21
21
|
|
|
22
|
+
## Formatting
|
|
23
|
+
|
|
24
|
+
A separate `format` job runs [Prettier](https://prettier.io/) (via
|
|
25
|
+
`npx prettier --check .`) against the repo — predominantly YAML and Markdown
|
|
26
|
+
files — to keep formatting consistent, using Prettier's default rules (no
|
|
27
|
+
project-level config). `build` depends on both `test` and `format` passing.
|
|
28
|
+
Run `npx prettier --write .` locally to auto-fix formatting issues before
|
|
29
|
+
pushing.
|
|
30
|
+
|
|
22
31
|
## One-time setup required on PyPI
|
|
23
32
|
|
|
24
33
|
Trusted Publishing must be configured on the `ibm5250` project at
|
|
@@ -28,9 +28,24 @@ jobs:
|
|
|
28
28
|
- name: Run tests
|
|
29
29
|
run: uv run pytest
|
|
30
30
|
|
|
31
|
+
format:
|
|
32
|
+
name: Format (Prettier)
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- name: Checkout
|
|
36
|
+
uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- name: Setup Node.js
|
|
39
|
+
uses: actions/setup-node@v4
|
|
40
|
+
with:
|
|
41
|
+
node-version: 20
|
|
42
|
+
|
|
43
|
+
- name: Check formatting with Prettier
|
|
44
|
+
run: npx --yes prettier --check .
|
|
45
|
+
|
|
31
46
|
build:
|
|
32
47
|
name: Build
|
|
33
|
-
needs: test
|
|
48
|
+
needs: [test, format]
|
|
34
49
|
runs-on: ubuntu-latest
|
|
35
50
|
outputs:
|
|
36
51
|
version: ${{ steps.version.outputs.version }}
|
|
@@ -59,11 +59,11 @@ sure `ruff check` and `pytest` both pass.
|
|
|
59
59
|
Every push runs the test suite in CI. What gets built and published to PyPI
|
|
60
60
|
depends on the branch:
|
|
61
61
|
|
|
62
|
-
| Branch
|
|
63
|
-
|
|
64
|
-
| `main`
|
|
65
|
-
| `develop`
|
|
66
|
-
| any other branch
|
|
62
|
+
| Branch | Tested | Built | Published to PyPI | Version format |
|
|
63
|
+
| ---------------- | :----: | :---: | :----------------------: | -------------------------------------------------------- |
|
|
64
|
+
| `main` | ✅ | ✅ | ✅ production | `X.Y.Z` (patch auto-incremented from the latest release) |
|
|
65
|
+
| `develop` | ✅ | ✅ | ✅ development | `X.Y.Z.devN` |
|
|
66
|
+
| any other branch | ✅ | ✅ | ❌ (build artifact only) | n/a |
|
|
67
67
|
|
|
68
68
|
- Contribute by branching off `develop` and opening PRs into `develop`.
|
|
69
69
|
`develop` publishes `.devN` releases so changes can be tried out before
|
|
@@ -52,6 +52,19 @@ _VAR_HDR_OP_HI = 0x08 # TN5250E variable-header opcode byte 0
|
|
|
52
52
|
_VAR_HDR_OP_LO = 0x2C # TN5250E variable-header opcode byte 1
|
|
53
53
|
_SAVE_SCREEN_RESP_CC1 = 0x01 # CC1 value in the Save Screen response variable header
|
|
54
54
|
|
|
55
|
+
# TN5250E header byte 7 (request/response flags). Real emulators set bit 0x40
|
|
56
|
+
# here to signal the Attention (Attn) key -- a header-only INPUT record with no
|
|
57
|
+
# AID and no variable header. Observed on the wire when the PC "Esc" key (mapped
|
|
58
|
+
# to Attn) is pressed: 00 0a 12 a0 00 00 04 40 00 00.
|
|
59
|
+
_TN5250E_ATTN_FLAG = 0x40
|
|
60
|
+
|
|
61
|
+
# TN5250E header byte 9 operation code (RFC 2877 §4.3). After the workstation
|
|
62
|
+
# sends Attn, the host cancels its outstanding read invite by sending a
|
|
63
|
+
# header-only Cancel-Invite record (opcode 0x0A, no payload). The workstation
|
|
64
|
+
# must acknowledge it by echoing the same Cancel-Invite record back before the
|
|
65
|
+
# host will run its Attention program (e.g. the System Request / Assist menu).
|
|
66
|
+
_TN5250E_OP_CANCEL_INVITE = 0x0A
|
|
67
|
+
|
|
55
68
|
_MAX_NEG_PASSES = 40 # Maximum Telnet option exchange iterations during negotiation
|
|
56
69
|
|
|
57
70
|
|
|
@@ -244,13 +257,61 @@ class TN5250Connection:
|
|
|
244
257
|
|
|
245
258
|
frame += bytes([TelnetCmd.INTERPRET_AS_COMMAND, TelnetCmd.END_OF_RECORD])
|
|
246
259
|
|
|
260
|
+
self._send_frame(frame, description=f"→ {len(data)} payload bytes: {data.hex()}")
|
|
261
|
+
|
|
262
|
+
def _send_frame(self, frame: bytes, *, description: str) -> None:
|
|
263
|
+
"""Send a fully-framed (IAC-EOR terminated) record under the send lock."""
|
|
264
|
+
if self._sock is None:
|
|
265
|
+
raise ConnectionError("Not connected")
|
|
266
|
+
log.debug(description)
|
|
247
267
|
with self._lock:
|
|
248
268
|
try:
|
|
249
269
|
self._sock.sendall(frame)
|
|
250
270
|
except OSError as exc:
|
|
251
271
|
raise ConnectionError(f"Send failed: {exc}") from exc
|
|
252
272
|
|
|
253
|
-
|
|
273
|
+
def send_attn(self) -> None:
|
|
274
|
+
"""Send the Attention (Attn) signal to the host.
|
|
275
|
+
|
|
276
|
+
Attn is *not* an AID key. In TN5250E it is a header-only INPUT record
|
|
277
|
+
whose byte 7 carries the Attn flag (``0x40``) with no variable header
|
|
278
|
+
and no payload -- exactly what emulators emit when the PC "Esc" key
|
|
279
|
+
(mapped to Attn) is pressed. On the AS/400 this typically raises the
|
|
280
|
+
System Request / Assist menu. In basic (non-TN5250E) mode the Attn key
|
|
281
|
+
is signalled with a Telnet BREAK (RFC 1205).
|
|
282
|
+
"""
|
|
283
|
+
if not self._tn5250e_active:
|
|
284
|
+
# Basic TN5250: the Attn key is signalled with a Telnet BREAK.
|
|
285
|
+
frame = bytes(
|
|
286
|
+
[
|
|
287
|
+
TelnetCmd.INTERPRET_AS_COMMAND,
|
|
288
|
+
TelnetCmd.BREAK,
|
|
289
|
+
TelnetCmd.INTERPRET_AS_COMMAND,
|
|
290
|
+
TelnetCmd.END_OF_RECORD,
|
|
291
|
+
]
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
# Header-only INPUT record: byte 7 carries the Attn flag.
|
|
295
|
+
frame = _build_tn5250e_frame(req_resp=_TN5250E_ATTN_FLAG, opcode=0x00)
|
|
296
|
+
|
|
297
|
+
self._send_frame(frame, description="Sending Attn signal")
|
|
298
|
+
|
|
299
|
+
def send_cancel_invite_response(self) -> None:
|
|
300
|
+
"""Acknowledge a host Cancel-Invite by echoing it back.
|
|
301
|
+
|
|
302
|
+
After the workstation sends Attn, the host replies with a header-only
|
|
303
|
+
Cancel-Invite record (opcode ``0x0A``, no payload) to cancel its
|
|
304
|
+
outstanding read invite. The workstation must echo the identical record
|
|
305
|
+
back; only then does the host run its Attention program (which raises,
|
|
306
|
+
for example, the System Request / Assist menu). Without this ack the
|
|
307
|
+
host stalls and never sends the follow-up screen.
|
|
308
|
+
"""
|
|
309
|
+
if not self._tn5250e_active:
|
|
310
|
+
return # cancel-invite handshake only occurs in TN5250E mode
|
|
311
|
+
|
|
312
|
+
# Header-only INPUT record: byte 9 opcode = Cancel-Invite, no payload.
|
|
313
|
+
frame = _build_tn5250e_frame(req_resp=0x00, opcode=_TN5250E_OP_CANCEL_INVITE)
|
|
314
|
+
self._send_frame(frame, description="Sending Cancel-Invite response")
|
|
254
315
|
|
|
255
316
|
def send_save_screen_response(self) -> None:
|
|
256
317
|
"""Send the TN5250E-level Save Screen acknowledgment.
|
|
@@ -261,42 +322,16 @@ class TN5250Connection:
|
|
|
261
322
|
send). This is a protocol-level exchange with no 5250 data stream
|
|
262
323
|
payload.
|
|
263
324
|
"""
|
|
264
|
-
if self._sock is None:
|
|
265
|
-
raise ConnectionError("Not connected")
|
|
266
325
|
if not self._tn5250e_active:
|
|
267
326
|
return # save/restore handshake only occurs in TN5250E mode
|
|
268
327
|
|
|
269
|
-
# 4-byte variable header: [ESC, RESTORE_SCREEN, CC1, CC2.RESET_MDT]
|
|
328
|
+
# 4-byte variable header: [ESC, RESTORE_SCREEN, CC1, CC2.RESET_MDT].
|
|
329
|
+
# byte 9 of the fixed header carries the variable-header length.
|
|
270
330
|
var_hdr = bytes(
|
|
271
331
|
[Command.ESC, Command.RESTORE_SCREEN, _SAVE_SCREEN_RESP_CC1, CC2.RESET_MDT]
|
|
272
332
|
)
|
|
273
|
-
|
|
274
|
-
frame =
|
|
275
|
-
bytes(
|
|
276
|
-
[
|
|
277
|
-
(total >> 8) & 0xFF,
|
|
278
|
-
total & 0xFF, # record length
|
|
279
|
-
_GDS_RECORD_ID_HI,
|
|
280
|
-
_GDS_RECORD_ID_LO, # GDS identifier
|
|
281
|
-
0x00,
|
|
282
|
-
0x00, # flags
|
|
283
|
-
TN5250EDataType.INPUT, # data type (matches request)
|
|
284
|
-
0x00,
|
|
285
|
-
0x00, # seq / asap-exit
|
|
286
|
-
len(var_hdr), # variable header length = 4
|
|
287
|
-
]
|
|
288
|
-
)
|
|
289
|
-
+ var_hdr
|
|
290
|
-
)
|
|
291
|
-
frame = _iac_escape(frame) + bytes(
|
|
292
|
-
[TelnetCmd.INTERPRET_AS_COMMAND, TelnetCmd.END_OF_RECORD]
|
|
293
|
-
)
|
|
294
|
-
log.debug("Sending Save Screen response")
|
|
295
|
-
with self._lock:
|
|
296
|
-
try:
|
|
297
|
-
self._sock.sendall(frame)
|
|
298
|
-
except OSError as exc:
|
|
299
|
-
raise ConnectionError(f"Send failed: {exc}") from exc
|
|
333
|
+
frame = _build_tn5250e_frame(req_resp=0x00, opcode=len(var_hdr), var_hdr=var_hdr)
|
|
334
|
+
self._send_frame(frame, description="Sending Save Screen response")
|
|
300
335
|
|
|
301
336
|
def recv_record(self) -> bytes:
|
|
302
337
|
"""Block until a complete record arrives; return the raw 5250 payload."""
|
|
@@ -369,6 +404,24 @@ class TN5250Connection:
|
|
|
369
404
|
"""True if TN5250E enhanced mode was successfully negotiated."""
|
|
370
405
|
return self._tn5250e_active
|
|
371
406
|
|
|
407
|
+
@property
|
|
408
|
+
def last_recv_opcode(self) -> int:
|
|
409
|
+
"""Return byte 9 (opcode/var_len) from the last received TN5250E frame.
|
|
410
|
+
|
|
411
|
+
Read-poll responses must echo this value back to the server.
|
|
412
|
+
"""
|
|
413
|
+
return self._last_recv_opcode
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def last_recv_was_cancel_invite(self) -> bool:
|
|
417
|
+
"""True if the last received TN5250E record was a Cancel-Invite.
|
|
418
|
+
|
|
419
|
+
The host sends a header-only Cancel-Invite (opcode ``0x0A``, no 5250
|
|
420
|
+
payload) after the workstation presses Attn. It must be acknowledged
|
|
421
|
+
via :meth:`send_cancel_invite_response`.
|
|
422
|
+
"""
|
|
423
|
+
return self._last_recv_opcode == _TN5250E_OP_CANCEL_INVITE
|
|
424
|
+
|
|
372
425
|
# ------------------------------------------------------------------
|
|
373
426
|
# Internal — Telnet negotiation
|
|
374
427
|
# ------------------------------------------------------------------
|
|
@@ -698,3 +751,40 @@ class TN5250Connection:
|
|
|
698
751
|
def _iac_escape(data: bytes) -> bytes:
|
|
699
752
|
"""Double any IAC (0xFF) bytes in *data* for safe Telnet transmission."""
|
|
700
753
|
return data.replace(b"\xff", b"\xff\xff")
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _build_tn5250e_frame(*, req_resp: int, opcode: int, var_hdr: bytes = b"") -> bytes:
|
|
757
|
+
"""Build an IAC-EOR-terminated TN5250E frame with no 5250 payload.
|
|
758
|
+
|
|
759
|
+
Produces the fixed 10-byte header (RFC 2877 §3.3) followed by an optional
|
|
760
|
+
variable header, IAC-escapes it, and appends the IAC-EOR record terminator.
|
|
761
|
+
Used for the header-only control records: Attn, Cancel-Invite response and
|
|
762
|
+
Save Screen response.
|
|
763
|
+
|
|
764
|
+
Parameters
|
|
765
|
+
----------
|
|
766
|
+
req_resp:
|
|
767
|
+
Byte 7 (request/response flags) — e.g. the Attn flag.
|
|
768
|
+
opcode:
|
|
769
|
+
Byte 9 (operation code / variable-header length).
|
|
770
|
+
var_hdr:
|
|
771
|
+
Optional variable-header bytes appended after the fixed header.
|
|
772
|
+
"""
|
|
773
|
+
total = _TN5250E_HEADER_LEN + len(var_hdr)
|
|
774
|
+
header = bytes(
|
|
775
|
+
[
|
|
776
|
+
(total >> 8) & 0xFF,
|
|
777
|
+
total & 0xFF, # 0-1: logical record length
|
|
778
|
+
_GDS_RECORD_ID_HI,
|
|
779
|
+
_GDS_RECORD_ID_LO, # 2-3: GDS identifier
|
|
780
|
+
0x00,
|
|
781
|
+
0x00, # 4-5: reserved
|
|
782
|
+
TN5250EDataType.INPUT, # 6: data type (workstation → host)
|
|
783
|
+
req_resp, # 7: request/response flags
|
|
784
|
+
0x00, # 8: error recovery
|
|
785
|
+
opcode, # 9: opcode / variable-header length
|
|
786
|
+
]
|
|
787
|
+
)
|
|
788
|
+
return _iac_escape(header + var_hdr) + bytes(
|
|
789
|
+
[TelnetCmd.INTERPRET_AS_COMMAND, TelnetCmd.END_OF_RECORD]
|
|
790
|
+
)
|
|
@@ -44,16 +44,16 @@ The **DataStreamParser** takes a raw 5250 payload (one record from the connectio
|
|
|
44
44
|
|
|
45
45
|
Every 5250 record starts with a **command byte**:
|
|
46
46
|
|
|
47
|
-
| Command
|
|
48
|
-
|
|
49
|
-
| `0x11` (Write to Display)
|
|
50
|
-
| `0x02` (Save Screen)
|
|
51
|
-
| `0x12` (Restore Screen)
|
|
52
|
-
| `0x04` (ESC)
|
|
53
|
-
| `0xF3` (Write Structured Field) | Carries structured data like Query commands
|
|
54
|
-
| `0x42/0x52/0x72/0x62`
|
|
55
|
-
| `0x21` (Write Error Code)
|
|
56
|
-
| `0x50` (Clear Format Table)
|
|
47
|
+
| Command | Meaning |
|
|
48
|
+
| ------------------------------- | ----------------------------------------------------------------------- |
|
|
49
|
+
| `0x11` (Write to Display) | Main screen update — followed by CC1, CC2, then orders/data |
|
|
50
|
+
| `0x02` (Save Screen) | Server wants to save the current screen (same format as WTD) |
|
|
51
|
+
| `0x12` (Restore Screen) | Server wants to restore a previously saved screen |
|
|
52
|
+
| `0x04` (ESC) | Escape prefix — next byte is the real command (WTD, WSF, Save, Restore) |
|
|
53
|
+
| `0xF3` (Write Structured Field) | Carries structured data like Query commands |
|
|
54
|
+
| `0x42/0x52/0x72/0x62` | Read commands — server asks client to send back screen/field data |
|
|
55
|
+
| `0x21` (Write Error Code) | Display error on status line |
|
|
56
|
+
| `0x50` (Clear Format Table) | Discard field definitions |
|
|
57
57
|
|
|
58
58
|
### Control Characters (CC1 / CC2)
|
|
59
59
|
|
|
@@ -73,18 +73,18 @@ After a WTD or Save Screen command, two bytes control screen-level behavior:
|
|
|
73
73
|
|
|
74
74
|
After the command + CC bytes, the rest of the record is a stream of **orders** intermixed with **data bytes**:
|
|
75
75
|
|
|
76
|
-
| Order
|
|
77
|
-
|
|
78
|
-
| **SBA** — Set Buffer Address (`0x11`) + row + col
|
|
79
|
-
| **SF** — Start Field (`0x1D`) + FFW [+ FCWs]
|
|
80
|
-
| **MF** — Modify Field (`0x2C`) + FFW [+ FCWs]
|
|
81
|
-
| **IC** — Insert Cursor (`0x13`)
|
|
82
|
-
| **MC** — Move Cursor (`0x14`) + row + col
|
|
83
|
-
| **RA** — Repeat to Address (`0x3C`) + row + col + char | Repeat a character from current position up to the target
|
|
84
|
-
| **EA** — Erase to Address (`0x12`) + row + col
|
|
85
|
-
| **TD** — Transparent Data (`0x10`) + length + data
|
|
86
|
-
| **SA** — Set Attribute (`0x28`) + type + value
|
|
87
|
-
| **SOH** — Start of Header (`0x01`) + length + data
|
|
76
|
+
| Order | What it does |
|
|
77
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| **SBA** — Set Buffer Address (`0x11`) + row + col | Move the write position to a specific screen cell |
|
|
79
|
+
| **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. |
|
|
80
|
+
| **MF** — Modify Field (`0x2C`) + FFW [+ FCWs] | Modify an existing field's attributes |
|
|
81
|
+
| **IC** — Insert Cursor (`0x13`) | Place the cursor at the current position |
|
|
82
|
+
| **MC** — Move Cursor (`0x14`) + row + col | Move the cursor to a specific position |
|
|
83
|
+
| **RA** — Repeat to Address (`0x3C`) + row + col + char | Repeat a character from current position up to the target |
|
|
84
|
+
| **EA** — Erase to Address (`0x12`) + row + col | Erase (fill with nulls) from current position to target |
|
|
85
|
+
| **TD** — Transparent Data (`0x10`) + length + data | Write raw bytes (transparent, no order interpretation) |
|
|
86
|
+
| **SA** — Set Attribute (`0x28`) + type + value | Set a display attribute (colour, underline, etc.) |
|
|
87
|
+
| **SOH** — Start of Header (`0x01`) + length + data | Metadata block, skipped |
|
|
88
88
|
|
|
89
89
|
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.
|
|
90
90
|
|
|
@@ -99,6 +99,7 @@ When the server sends a WSF Query command, the parser emits a **QUERY** event. T
|
|
|
99
99
|
### Events Produced
|
|
100
100
|
|
|
101
101
|
Each order/data run becomes a `DSEvent` with a type like:
|
|
102
|
+
|
|
102
103
|
- `CLEAR_SCREEN` — wipe the buffer
|
|
103
104
|
- `RESIZE_SCREEN` — change dimensions
|
|
104
105
|
- `SET_POS` — move write cursor
|
|
@@ -217,6 +217,20 @@ class Terminal:
|
|
|
217
217
|
"""Send an AID key by name, e.g. ``"Enter"``, ``"F3"``, ``"Clear"``."""
|
|
218
218
|
self.send_key(AID.from_name(name), wait=wait)
|
|
219
219
|
|
|
220
|
+
def send_attn(self, *, wait: bool = True) -> None:
|
|
221
|
+
"""Send the Attention (Attn) signal to the host.
|
|
222
|
+
|
|
223
|
+
Attn is a protocol-level attention indication -- *not* an AID key -- and
|
|
224
|
+
is what 5250 emulators send for the PC "Esc" key. On the AS/400 it
|
|
225
|
+
typically raises the System Request / Assist menu. By default this
|
|
226
|
+
blocks until the host responds; pass ``wait=False`` to return
|
|
227
|
+
immediately after sending.
|
|
228
|
+
"""
|
|
229
|
+
self._conn.send_attn()
|
|
230
|
+
self._screen._keyboard_locked = True
|
|
231
|
+
if wait:
|
|
232
|
+
self._recv_and_apply()
|
|
233
|
+
|
|
220
234
|
def type_into(
|
|
221
235
|
self,
|
|
222
236
|
field: Field | FieldProxy,
|
|
@@ -517,6 +531,14 @@ class Terminal:
|
|
|
517
531
|
|
|
518
532
|
def _recv_and_apply(self) -> None:
|
|
519
533
|
received_payload = self._conn.recv_record()
|
|
534
|
+
# A header-only Cancel-Invite (no 5250 payload) is the host's reply to
|
|
535
|
+
# our Attn: it cancels the outstanding read invite. We must echo it back
|
|
536
|
+
# to let the host run its Attention program (e.g. the Assist menu);
|
|
537
|
+
# otherwise the host stalls and never sends the follow-up screen.
|
|
538
|
+
if not received_payload and self._conn.last_recv_was_cancel_invite:
|
|
539
|
+
log.debug("Received Cancel-Invite; echoing acknowledgment")
|
|
540
|
+
self._conn.send_cancel_invite_response()
|
|
541
|
+
return
|
|
520
542
|
parsed_events = list(self._parser.parse(received_payload))
|
|
521
543
|
# If this record has screen content, the pending save is no longer back-to-back
|
|
522
544
|
for ev in parsed_events:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|