mirror of
https://github.com/python/cpython.git
synced 2025-11-09 18:11:38 +00:00
[3.14] gh-135621: Simplify TermInfo (GH-136916) (#136925)
This commit is contained in:
parent
af8d1f56f5
commit
11378e1c85
1 changed files with 32 additions and 74 deletions
|
|
@ -71,7 +71,6 @@
|
||||||
"OTGV", "OTGC","meml", "memu", "box1"
|
"OTGV", "OTGC","meml", "memu", "box1"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
_STRING_CAPABILITY_NAMES = {name: i for i, name in enumerate(_STRING_NAMES)}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_terminfo_dirs() -> list[Path]:
|
def _get_terminfo_dirs() -> list[Path]:
|
||||||
|
|
@ -322,10 +321,6 @@ class TermInfo:
|
||||||
terminal_name: str | bytes | None
|
terminal_name: str | bytes | None
|
||||||
fallback: bool = True
|
fallback: bool = True
|
||||||
|
|
||||||
_names: list[str] = field(default_factory=list)
|
|
||||||
_booleans: list[int] = field(default_factory=list)
|
|
||||||
_numbers: list[int] = field(default_factory=list)
|
|
||||||
_strings: list[bytes | None] = field(default_factory=list)
|
|
||||||
_capabilities: dict[str, bytes] = field(default_factory=dict)
|
_capabilities: dict[str, bytes] = field(default_factory=dict)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
|
@ -362,9 +357,12 @@ def __post_init__(self) -> None:
|
||||||
def _parse_terminfo_file(self, terminal_name: str) -> None:
|
def _parse_terminfo_file(self, terminal_name: str) -> None:
|
||||||
"""Parse a terminfo file.
|
"""Parse a terminfo file.
|
||||||
|
|
||||||
|
Populate the _capabilities dict for easy retrieval
|
||||||
|
|
||||||
Based on ncurses implementation in:
|
Based on ncurses implementation in:
|
||||||
- ncurses/tinfo/read_entry.c:_nc_read_termtype()
|
- ncurses/tinfo/read_entry.c:_nc_read_termtype()
|
||||||
- ncurses/tinfo/read_entry.c:_nc_read_file_entry()
|
- ncurses/tinfo/read_entry.c:_nc_read_file_entry()
|
||||||
|
- ncurses/tinfo/lib_ti.c:tigetstr()
|
||||||
"""
|
"""
|
||||||
data = _read_terminfo_file(terminal_name)
|
data = _read_terminfo_file(terminal_name)
|
||||||
too_short = f"TermInfo file for {terminal_name!r} too short"
|
too_short = f"TermInfo file for {terminal_name!r} too short"
|
||||||
|
|
@ -377,53 +375,36 @@ def _parse_terminfo_file(self, terminal_name: str) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
if magic == MAGIC16:
|
if magic == MAGIC16:
|
||||||
number_format = "<h" # 16-bit signed
|
|
||||||
number_size = 2
|
number_size = 2
|
||||||
elif magic == MAGIC32:
|
elif magic == MAGIC32:
|
||||||
number_format = "<i" # 32-bit signed
|
|
||||||
number_size = 4
|
number_size = 4
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"TermInfo file for {terminal_name!r} uses unknown magic"
|
f"TermInfo file for {terminal_name!r} uses unknown magic"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Read terminal names
|
# Skip data than PyREPL doesn't need:
|
||||||
if offset + name_size > len(data):
|
# - names (`|`-separated ASCII strings)
|
||||||
raise ValueError(too_short)
|
# - boolean capabilities (bytes with value 0 or 1)
|
||||||
names = data[offset : offset + name_size - 1].decode(
|
# - numbers (little-endian integers, `number_size` bytes each)
|
||||||
"ascii", errors="ignore"
|
|
||||||
)
|
|
||||||
offset += name_size
|
offset += name_size
|
||||||
|
|
||||||
# Read boolean capabilities
|
|
||||||
if offset + bool_count > len(data):
|
|
||||||
raise ValueError(too_short)
|
|
||||||
booleans = list(data[offset : offset + bool_count])
|
|
||||||
offset += bool_count
|
offset += bool_count
|
||||||
|
|
||||||
# Align to even byte boundary for numbers
|
|
||||||
if offset % 2:
|
if offset % 2:
|
||||||
|
# Align to even byte boundary for numbers
|
||||||
offset += 1
|
offset += 1
|
||||||
|
offset += num_count * number_size
|
||||||
# Read numeric capabilities
|
if offset > len(data):
|
||||||
numbers = []
|
|
||||||
for i in range(num_count):
|
|
||||||
if offset + number_size > len(data):
|
|
||||||
raise ValueError(too_short)
|
raise ValueError(too_short)
|
||||||
num = struct.unpack(
|
|
||||||
number_format, data[offset : offset + number_size]
|
|
||||||
)[0]
|
|
||||||
numbers.append(num)
|
|
||||||
offset += number_size
|
|
||||||
|
|
||||||
# Read string offsets
|
# Read string offsets
|
||||||
string_offsets = []
|
end_offset = offset + 2 * str_count
|
||||||
for i in range(str_count):
|
if offset > len(data):
|
||||||
if offset + 2 > len(data):
|
|
||||||
raise ValueError(too_short)
|
raise ValueError(too_short)
|
||||||
off = struct.unpack("<h", data[offset : offset + 2])[0]
|
string_offset_data = data[offset:end_offset]
|
||||||
string_offsets.append(off)
|
string_offsets = [
|
||||||
offset += 2
|
off for [off] in struct.iter_unpack("<h", string_offset_data)
|
||||||
|
]
|
||||||
|
offset = end_offset
|
||||||
|
|
||||||
# Read string table
|
# Read string table
|
||||||
if offset + str_size > len(data):
|
if offset + str_size > len(data):
|
||||||
|
|
@ -431,54 +412,31 @@ def _parse_terminfo_file(self, terminal_name: str) -> None:
|
||||||
string_table = data[offset : offset + str_size]
|
string_table = data[offset : offset + str_size]
|
||||||
|
|
||||||
# Extract strings from string table
|
# Extract strings from string table
|
||||||
strings: list[bytes | None] = []
|
capabilities = {}
|
||||||
for off in string_offsets:
|
for cap, off in zip(_STRING_NAMES, string_offsets):
|
||||||
if off < 0:
|
if off < 0:
|
||||||
strings.append(CANCELLED_STRING)
|
# CANCELLED_STRING; we do not store those
|
||||||
|
continue
|
||||||
elif off < len(string_table):
|
elif off < len(string_table):
|
||||||
# Find null terminator
|
# Find null terminator
|
||||||
end = off
|
end = string_table.find(0, off)
|
||||||
while end < len(string_table) and string_table[end] != 0:
|
if end >= 0:
|
||||||
end += 1
|
capabilities[cap] = string_table[off:end]
|
||||||
if end <= len(string_table):
|
# in other cases this is ABSENT_STRING; we don't store those.
|
||||||
strings.append(string_table[off:end])
|
|
||||||
else:
|
|
||||||
strings.append(ABSENT_STRING)
|
|
||||||
else:
|
|
||||||
strings.append(ABSENT_STRING)
|
|
||||||
|
|
||||||
self._names = names.split("|")
|
# Note: we don't support extended capabilities since PyREPL doesn't
|
||||||
self._booleans = booleans
|
# need them.
|
||||||
self._numbers = numbers
|
|
||||||
self._strings = strings
|
self._capabilities = capabilities
|
||||||
|
|
||||||
def get(self, cap: str) -> bytes | None:
|
def get(self, cap: str) -> bytes | None:
|
||||||
"""Get terminal capability string by name.
|
"""Get terminal capability string by name.
|
||||||
|
|
||||||
Based on ncurses implementation in:
|
|
||||||
- ncurses/tinfo/lib_ti.c:tigetstr()
|
|
||||||
|
|
||||||
The ncurses version searches through compiled terminfo data structures.
|
|
||||||
This version first checks parsed terminfo data, then falls back to
|
|
||||||
hardcoded capabilities.
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(cap, str):
|
if not isinstance(cap, str):
|
||||||
raise TypeError(f"`cap` must be a string, not {type(cap)}")
|
raise TypeError(f"`cap` must be a string, not {type(cap)}")
|
||||||
|
|
||||||
if self._capabilities:
|
|
||||||
# Fallbacks populated, use them
|
|
||||||
return self._capabilities.get(cap)
|
return self._capabilities.get(cap)
|
||||||
|
|
||||||
# Look up in standard capabilities first
|
|
||||||
if cap in _STRING_CAPABILITY_NAMES:
|
|
||||||
index = _STRING_CAPABILITY_NAMES[cap]
|
|
||||||
if index < len(self._strings):
|
|
||||||
return self._strings[index]
|
|
||||||
|
|
||||||
# Note: we don't support extended capabilities since PyREPL doesn't
|
|
||||||
# need them.
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def tparm(cap_bytes: bytes, *params: int) -> bytes:
|
def tparm(cap_bytes: bytes, *params: int) -> bytes:
|
||||||
"""Parameterize a terminal capability string.
|
"""Parameterize a terminal capability string.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue