ibm5250 0.1.0.dev6__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.
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/PKG-INFO +1 -1
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/pyproject.toml +1 -1
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/__init__.py +2 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/connection.py +43 -31
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/constants.py +93 -51
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/data-stream-logic.md +110 -41
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/datastream.py +179 -243
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/screen.py +104 -56
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/terminal.py +78 -57
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/uv.lock +1 -1
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/.github/copilot-instructions.md +0 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/.github/workflows/README.md +0 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/.github/workflows/publish.yml +0 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/.gitignore +0 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/LICENSE +0 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/README.md +0 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/scripts/compute_version.py +0 -0
- {ibm5250-0.1.0.dev6 → ibm5250-0.1.0.dev8}/src/ibm5250/exceptions.py +0 -0
|
@@ -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,13 +26,13 @@ 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,
|
|
33
32
|
TN5250EDataType,
|
|
34
33
|
TN5250ERecordType,
|
|
35
34
|
TN5250ESubNeg,
|
|
35
|
+
encode_addr,
|
|
36
36
|
)
|
|
37
37
|
from .exceptions import ConnectionError, ProtocolError, RecvTimeout
|
|
38
38
|
|
|
@@ -48,9 +48,6 @@ _TELNET_OPT_CMD_LEN = 3 # bytes: IAC + command + option (WILL/WONT/DO/DONT)
|
|
|
48
48
|
# SNA / TN5250E wire-format framing constants
|
|
49
49
|
_GDS_RECORD_ID_HI = 0x12 # SNA GDS record-type identifier, high byte
|
|
50
50
|
_GDS_RECORD_ID_LO = 0xA0 # SNA GDS record-type identifier, low byte
|
|
51
|
-
_VAR_HDR_OP_HI = 0x08 # TN5250E variable-header opcode byte 0
|
|
52
|
-
_VAR_HDR_OP_LO = 0x2C # TN5250E variable-header opcode byte 1
|
|
53
|
-
_SAVE_SCREEN_RESP_CC1 = 0x01 # CC1 value in the Save Screen response variable header
|
|
54
51
|
|
|
55
52
|
# TN5250E header byte 7 (request/response flags). Real emulators set bit 0x40
|
|
56
53
|
# here to signal the Attention (Attn) key -- a header-only INPUT record with no
|
|
@@ -74,6 +71,20 @@ _MAX_NEG_PASSES = 40 # Maximum Telnet option exchange iterations during negotia
|
|
|
74
71
|
|
|
75
72
|
_TN5250E_HEADER_LEN = 10
|
|
76
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
|
+
|
|
77
88
|
|
|
78
89
|
@dataclass
|
|
79
90
|
class TN5250EHeader:
|
|
@@ -82,10 +93,12 @@ class TN5250EHeader:
|
|
|
82
93
|
err_flag: int = 0x00
|
|
83
94
|
|
|
84
95
|
aid: int = AID.NO_AID # AID byte placed in the variable header (default: no-AID)
|
|
96
|
+
cursor_row: int = 0x00
|
|
97
|
+
cursor_col: int = 0x00
|
|
85
98
|
|
|
86
99
|
def to_bytes(self, payload_len: int) -> bytes:
|
|
87
|
-
# 3-byte variable header:
|
|
88
|
-
var_hdr = bytes([
|
|
100
|
+
# 3-byte variable header: cursor row, cursor column, AID.
|
|
101
|
+
var_hdr = bytes([self.cursor_row, self.cursor_col, self.aid])
|
|
89
102
|
var_len = len(var_hdr)
|
|
90
103
|
total = payload_len + _TN5250E_HEADER_LEN + var_len
|
|
91
104
|
return (
|
|
@@ -238,6 +251,8 @@ class TN5250Connection:
|
|
|
238
251
|
*,
|
|
239
252
|
data_type: int = TN5250EDataType.INPUT,
|
|
240
253
|
aid: int = AID.NO_AID,
|
|
254
|
+
cursor_pos: int | None = None,
|
|
255
|
+
cols: int = 80,
|
|
241
256
|
) -> None:
|
|
242
257
|
"""Send a 5250 data record to the host.
|
|
243
258
|
|
|
@@ -248,16 +263,21 @@ class TN5250Connection:
|
|
|
248
263
|
raise ConnectionError("Not connected")
|
|
249
264
|
|
|
250
265
|
escaped = _iac_escape(data)
|
|
266
|
+
cursor_row, cursor_col = (
|
|
267
|
+
encode_addr(cursor_pos, cols) if cursor_pos is not None else (0, 0)
|
|
268
|
+
)
|
|
251
269
|
|
|
252
270
|
if self._tn5250e_active:
|
|
253
271
|
hdr = TN5250EHeader(
|
|
254
272
|
data_type=data_type,
|
|
255
273
|
aid=aid,
|
|
274
|
+
cursor_row=cursor_row,
|
|
275
|
+
cursor_col=cursor_col,
|
|
256
276
|
).to_bytes(len(escaped))
|
|
257
277
|
frame = hdr + escaped
|
|
258
278
|
else:
|
|
259
|
-
# Basic TN5250
|
|
260
|
-
frame = bytes([aid]) + escaped
|
|
279
|
+
# Basic TN5250 carries AID and cursor address in the data stream.
|
|
280
|
+
frame = bytes([aid, cursor_row, cursor_col]) + escaped
|
|
261
281
|
|
|
262
282
|
frame += bytes([TelnetCmd.INTERPRET_AS_COMMAND, TelnetCmd.END_OF_RECORD])
|
|
263
283
|
|
|
@@ -317,23 +337,20 @@ class TN5250Connection:
|
|
|
317
337
|
frame = _build_tn5250e_frame(req_resp=0x00, opcode=_TN5250E_OP_CANCEL_INVITE)
|
|
318
338
|
self._send_frame(frame, description="Sending Cancel-Invite response")
|
|
319
339
|
|
|
320
|
-
def send_save_screen_response(self) -> None:
|
|
340
|
+
def send_save_screen_response(self, save_id: bytes) -> None:
|
|
321
341
|
"""Send the TN5250E-level Save Screen acknowledgment.
|
|
322
342
|
|
|
323
343
|
When the server issues an ESC+SAVE_SCREEN (0x04 0x02) command the
|
|
324
344
|
terminal must reply with a matching TN5250E frame whose variable header
|
|
325
|
-
encodes ESC+RESTORE_SCREEN+
|
|
326
|
-
|
|
327
|
-
|
|
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*.
|
|
328
349
|
"""
|
|
329
350
|
if not self._tn5250e_active:
|
|
330
351
|
return # save/restore handshake only occurs in TN5250E mode
|
|
331
352
|
|
|
332
|
-
|
|
333
|
-
# byte 9 of the fixed header carries the variable-header length.
|
|
334
|
-
var_hdr = bytes(
|
|
335
|
-
[Command.ESC, Command.RESTORE_SCREEN, _SAVE_SCREEN_RESP_CC1, CC2.RESET_MDT]
|
|
336
|
-
)
|
|
353
|
+
var_hdr = bytes([Command.ESC, Command.RESTORE_SCREEN]) + bytes(save_id)
|
|
337
354
|
frame = _build_tn5250e_frame(req_resp=0x00, opcode=len(var_hdr), var_hdr=var_hdr)
|
|
338
355
|
self._send_frame(frame, description="Sending Save Screen response")
|
|
339
356
|
|
|
@@ -346,8 +363,8 @@ class TN5250Connection:
|
|
|
346
363
|
log.debug("Short TN5250E frame (%d bytes), skipping", len(frame))
|
|
347
364
|
continue
|
|
348
365
|
hdr = TN5250EHeader.from_bytes(frame)
|
|
349
|
-
self._last_recv_opcode = frame[9] # byte 9: opcode
|
|
350
|
-
payload = bytes(frame[
|
|
366
|
+
self._last_recv_opcode = frame[9] # byte 9: opcode
|
|
367
|
+
payload = bytes(frame[_payload_offset(frame) :])
|
|
351
368
|
log.debug(
|
|
352
369
|
"← TN5250E type=%02X data_type=%02X payload=%d bytes",
|
|
353
370
|
hdr.record_type,
|
|
@@ -357,20 +374,16 @@ class TN5250Connection:
|
|
|
357
374
|
else:
|
|
358
375
|
frame_bytes = bytes(frame)
|
|
359
376
|
# IBM i sends TN5250E-formatted records even when TN5250E was
|
|
360
|
-
# not formally negotiated. The
|
|
361
|
-
#
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
# 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.
|
|
365
381
|
if len(frame_bytes) >= _TN5250E_HEADER_LEN and int.from_bytes(
|
|
366
382
|
frame_bytes[:2], "big"
|
|
367
383
|
) == len(frame_bytes):
|
|
368
384
|
hdr = TN5250EHeader.from_bytes(frame_bytes)
|
|
369
385
|
self._last_recv_opcode = frame_bytes[9] # byte 9: opcode
|
|
370
|
-
|
|
371
|
-
9
|
|
372
|
-
] # RFC 2877 variable-header length field
|
|
373
|
-
skip = _TN5250E_HEADER_LEN + var_hdr_len
|
|
386
|
+
skip = _payload_offset(frame_bytes)
|
|
374
387
|
payload = frame_bytes[skip:]
|
|
375
388
|
# Server is speaking TN5250E — our responses must include the header too
|
|
376
389
|
if not self._tn5250e_active:
|
|
@@ -379,9 +392,8 @@ class TN5250Connection:
|
|
|
379
392
|
)
|
|
380
393
|
self._tn5250e_active = True
|
|
381
394
|
log.debug(
|
|
382
|
-
"← implicit TN5250E header stripped (
|
|
383
|
-
|
|
384
|
-
var_hdr_len,
|
|
395
|
+
"← implicit TN5250E header stripped (%d bytes) data_type=%02X payload=%d bytes",
|
|
396
|
+
skip,
|
|
385
397
|
hdr.data_type,
|
|
386
398
|
len(payload),
|
|
387
399
|
)
|
|
@@ -83,10 +83,12 @@ class TN5250ESubNeg:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class Command:
|
|
86
|
-
|
|
87
|
-
|
|
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
92
|
WRITE_TO_DISPLAY = (
|
|
91
93
|
0x11 # main screen update command; followed by CC1/CC2 and orders
|
|
92
94
|
)
|
|
@@ -104,16 +106,16 @@ class Command:
|
|
|
104
106
|
|
|
105
107
|
# Write to Display control characters (CC1 / CC2)
|
|
106
108
|
class CC1:
|
|
107
|
-
"""Control Character 1 — first byte after WTD command.
|
|
109
|
+
"""Control Character 1 — first byte after the WTD command.
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
111
116
|
"""
|
|
112
117
|
|
|
113
118
|
RESET = 0x00
|
|
114
|
-
CLEAR_UNIT = 0x20 # Erase entire screen before write
|
|
115
|
-
CLEAR_UNIT_ALT = 0x40 # Erase screen using alternate screen dimensions
|
|
116
|
-
CLEAR_FORMAT_TABLE = 0x60 # Discard field definitions only; no screen erase
|
|
117
119
|
SOUND_ALARM = 0x80 # Audible alarm
|
|
118
120
|
|
|
119
121
|
|
|
@@ -121,11 +123,10 @@ class CC2:
|
|
|
121
123
|
"""Control Character 2 — second byte after WTD command."""
|
|
122
124
|
|
|
123
125
|
RESET = 0x00
|
|
124
|
-
UNLOCK_KEYBOARD =
|
|
125
|
-
RESET_MDT =
|
|
126
|
-
RESET_MDT_UNLOCK =
|
|
126
|
+
UNLOCK_KEYBOARD = 0x08
|
|
127
|
+
RESET_MDT = 0x10 # Clear all MDT bits
|
|
128
|
+
RESET_MDT_UNLOCK = 0x18
|
|
127
129
|
SET_MDT = 0x04 # Set MDT on all input fields
|
|
128
|
-
MOVE_CURSOR = 0x08 # IC order sets cursor, not default position
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
# ---------------------------------------------------------------------------
|
|
@@ -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
|
-
|
|
140
|
-
0x12 #
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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).
|
|
233
|
+
|
|
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
|
+
"""
|
|
222
238
|
|
|
223
|
-
# Byte 0
|
|
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 =
|
|
227
|
-
|
|
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 =
|
|
233
|
-
NUM_SHIFT =
|
|
234
|
-
NUM_ONLY =
|
|
235
|
-
KATA_SHIFT =
|
|
236
|
-
DIGITS_ONLY =
|
|
237
|
-
|
|
238
|
-
SIGNED_NUM =
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
|
@@ -541,10 +576,17 @@ def decode_addr(hi: int, lo: int, cols: int = SCREEN_COLS) -> int:
|
|
|
541
576
|
|
|
542
577
|
|
|
543
578
|
def pos_to_rowcol(pos: int, cols: int = SCREEN_COLS) -> tuple[int, int]:
|
|
544
|
-
"""Convert a linear position to (row, col)
|
|
545
|
-
|
|
579
|
+
"""Convert a zero-based linear position to a 1-based ``(row, col)``."""
|
|
580
|
+
if pos < 0:
|
|
581
|
+
raise ValueError("Buffer position must be non-negative")
|
|
582
|
+
row, col = divmod(pos, cols)
|
|
583
|
+
return row + 1, col + 1
|
|
546
584
|
|
|
547
585
|
|
|
548
586
|
def rowcol_to_pos(row: int, col: int, cols: int = SCREEN_COLS) -> int:
|
|
549
|
-
"""Convert (row, col) to a linear position
|
|
550
|
-
|
|
587
|
+
"""Convert a 1-based ``(row, col)`` to a zero-based linear position."""
|
|
588
|
+
if row < 1 or col < 1:
|
|
589
|
+
raise ValueError("Screen coordinates start at (1, 1)")
|
|
590
|
+
if col > cols:
|
|
591
|
+
raise ValueError(f"Column must be between 1 and {cols}")
|
|
592
|
+
return (row - 1) * cols + col - 1
|