Skip to content

Commit 28153fe

Browse files
authored
gh-135621: Simplify TermInfo (GH-136916)
1 parent dee6501 commit 28153fe

File tree

1 file changed

+33
-75
lines changed

1 file changed

+33
-75
lines changed

Lib/_pyrepl/terminfo.py

Lines changed: 33 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
"OTGV", "OTGC","meml", "memu", "box1"
7272
)
7373
# fmt: on
74-
_STRING_CAPABILITY_NAMES = {name: i for i, name in enumerate(_STRING_NAMES)}
7574

7675

7776
def _get_terminfo_dirs() -> list[Path]:
@@ -322,10 +321,6 @@ class TermInfo:
322321
terminal_name: str | bytes | None
323322
fallback: bool = True
324323

325-
_names: list[str] = field(default_factory=list)
326-
_booleans: list[int] = field(default_factory=list)
327-
_numbers: list[int] = field(default_factory=list)
328-
_strings: list[bytes | None] = field(default_factory=list)
329324
_capabilities: dict[str, bytes] = field(default_factory=dict)
330325

331326
def __post_init__(self) -> None:
@@ -362,9 +357,12 @@ def __post_init__(self) -> None:
362357
def _parse_terminfo_file(self, terminal_name: str) -> None:
363358
"""Parse a terminfo file.
364359
360+
Populate the _capabilities dict for easy retrieval
361+
365362
Based on ncurses implementation in:
366363
- ncurses/tinfo/read_entry.c:_nc_read_termtype()
367364
- ncurses/tinfo/read_entry.c:_nc_read_file_entry()
365+
- ncurses/tinfo/lib_ti.c:tigetstr()
368366
"""
369367
data = _read_terminfo_file(terminal_name)
370368
too_short = f"TermInfo file for {terminal_name!r} too short"
@@ -377,107 +375,67 @@ def _parse_terminfo_file(self, terminal_name: str) -> None:
377375
)
378376

379377
if magic == MAGIC16:
380-
number_format = "<h" # 16-bit signed
381378
number_size = 2
382379
elif magic == MAGIC32:
383-
number_format = "<i" # 32-bit signed
384380
number_size = 4
385381
else:
386382
raise ValueError(
387383
f"TermInfo file for {terminal_name!r} uses unknown magic"
388384
)
389385

390-
# Read terminal names
391-
if offset + name_size > len(data):
392-
raise ValueError(too_short)
393-
names = data[offset : offset + name_size - 1].decode(
394-
"ascii", errors="ignore"
395-
)
386+
# Skip data than PyREPL doesn't need:
387+
# - names (`|`-separated ASCII strings)
388+
# - boolean capabilities (bytes with value 0 or 1)
389+
# - numbers (little-endian integers, `number_size` bytes each)
396390
offset += name_size
397-
398-
# Read boolean capabilities
399-
if offset + bool_count > len(data):
400-
raise ValueError(too_short)
401-
booleans = list(data[offset : offset + bool_count])
402391
offset += bool_count
403-
404-
# Align to even byte boundary for numbers
405392
if offset % 2:
393+
# Align to even byte boundary for numbers
406394
offset += 1
407-
408-
# Read numeric capabilities
409-
numbers = []
410-
for i in range(num_count):
411-
if offset + number_size > len(data):
412-
raise ValueError(too_short)
413-
num = struct.unpack(
414-
number_format, data[offset : offset + number_size]
415-
)[0]
416-
numbers.append(num)
417-
offset += number_size
395+
offset += num_count * number_size
396+
if offset > len(data):
397+
raise ValueError(too_short)
418398

419399
# Read string offsets
420-
string_offsets = []
421-
for i in range(str_count):
422-
if offset + 2 > len(data):
423-
raise ValueError(too_short)
424-
off = struct.unpack("<h", data[offset : offset + 2])[0]
425-
string_offsets.append(off)
426-
offset += 2
400+
end_offset = offset + 2 * str_count
401+
if offset > len(data):
402+
raise ValueError(too_short)
403+
string_offset_data = data[offset:end_offset]
404+
string_offsets = [
405+
off for [off] in struct.iter_unpack("<h", string_offset_data)
406+
]
407+
offset = end_offset
427408

428409
# Read string table
429410
if offset + str_size > len(data):
430411
raise ValueError(too_short)
431412
string_table = data[offset : offset + str_size]
432413

433414
# Extract strings from string table
434-
strings: list[bytes | None] = []
435-
for off in string_offsets:
415+
capabilities = {}
416+
for cap, off in zip(_STRING_NAMES, string_offsets):
436417
if off < 0:
437-
strings.append(CANCELLED_STRING)
418+
# CANCELLED_STRING; we do not store those
419+
continue
438420
elif off < len(string_table):
439421
# Find null terminator
440-
end = off
441-
while end < len(string_table) and string_table[end] != 0:
442-
end += 1
443-
if end <= len(string_table):
444-
strings.append(string_table[off:end])
445-
else:
446-
strings.append(ABSENT_STRING)
447-
else:
448-
strings.append(ABSENT_STRING)
449-
450-
self._names = names.split("|")
451-
self._booleans = booleans
452-
self._numbers = numbers
453-
self._strings = strings
422+
end = string_table.find(0, off)
423+
if end >= 0:
424+
capabilities[cap] = string_table[off:end]
425+
# in other cases this is ABSENT_STRING; we don't store those.
454426

455-
def get(self, cap: str) -> bytes | None:
456-
"""Get terminal capability string by name.
427+
# Note: we don't support extended capabilities since PyREPL doesn't
428+
# need them.
457429

458-
Based on ncurses implementation in:
459-
- ncurses/tinfo/lib_ti.c:tigetstr()
430+
self._capabilities = capabilities
460431

461-
The ncurses version searches through compiled terminfo data structures.
462-
This version first checks parsed terminfo data, then falls back to
463-
hardcoded capabilities.
432+
def get(self, cap: str) -> bytes | None:
433+
"""Get terminal capability string by name.
464434
"""
465435
if not isinstance(cap, str):
466436
raise TypeError(f"`cap` must be a string, not {type(cap)}")
467437

468-
if self._capabilities:
469-
# Fallbacks populated, use them
470-
return self._capabilities.get(cap)
471-
472-
# Look up in standard capabilities first
473-
if cap in _STRING_CAPABILITY_NAMES:
474-
index = _STRING_CAPABILITY_NAMES[cap]
475-
if index < len(self._strings):
476-
return self._strings[index]
477-
478-
# Note: we don't support extended capabilities since PyREPL doesn't
479-
# need them.
480-
return None
438+
return self._capabilities.get(cap)
481439

482440

483441
def tparm(cap_bytes: bytes, *params: int) -> bytes:

0 commit comments

Comments
 (0)