71
71
"OTGV" , "OTGC" ,"meml" , "memu" , "box1"
72
72
)
73
73
# fmt: on
74
- _STRING_CAPABILITY_NAMES = {name : i for i , name in enumerate (_STRING_NAMES )}
75
74
76
75
77
76
def _get_terminfo_dirs () -> list [Path ]:
@@ -322,10 +321,6 @@ class TermInfo:
322
321
terminal_name : str | bytes | None
323
322
fallback : bool = True
324
323
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 )
329
324
_capabilities : dict [str , bytes ] = field (default_factory = dict )
330
325
331
326
def __post_init__ (self ) -> None :
@@ -362,9 +357,12 @@ def __post_init__(self) -> None:
362
357
def _parse_terminfo_file (self , terminal_name : str ) -> None :
363
358
"""Parse a terminfo file.
364
359
360
+ Populate the _capabilities dict for easy retrieval
361
+
365
362
Based on ncurses implementation in:
366
363
- ncurses/tinfo/read_entry.c:_nc_read_termtype()
367
364
- ncurses/tinfo/read_entry.c:_nc_read_file_entry()
365
+ - ncurses/tinfo/lib_ti.c:tigetstr()
368
366
"""
369
367
data = _read_terminfo_file (terminal_name )
370
368
too_short = f"TermInfo file for { terminal_name !r} too short"
@@ -377,107 +375,67 @@ def _parse_terminfo_file(self, terminal_name: str) -> None:
377
375
)
378
376
379
377
if magic == MAGIC16 :
380
- number_format = "<h" # 16-bit signed
381
378
number_size = 2
382
379
elif magic == MAGIC32 :
383
- number_format = "<i" # 32-bit signed
384
380
number_size = 4
385
381
else :
386
382
raise ValueError (
387
383
f"TermInfo file for { terminal_name !r} uses unknown magic"
388
384
)
389
385
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)
396
390
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 ])
402
391
offset += bool_count
403
-
404
- # Align to even byte boundary for numbers
405
392
if offset % 2 :
393
+ # Align to even byte boundary for numbers
406
394
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 )
418
398
419
399
# 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
427
408
428
409
# Read string table
429
410
if offset + str_size > len (data ):
430
411
raise ValueError (too_short )
431
412
string_table = data [offset : offset + str_size ]
432
413
433
414
# 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 ) :
436
417
if off < 0 :
437
- strings .append (CANCELLED_STRING )
418
+ # CANCELLED_STRING; we do not store those
419
+ continue
438
420
elif off < len (string_table ):
439
421
# 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.
454
426
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 .
457
429
458
- Based on ncurses implementation in:
459
- - ncurses/tinfo/lib_ti.c:tigetstr()
430
+ self ._capabilities = capabilities
460
431
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.
464
434
"""
465
435
if not isinstance (cap , str ):
466
436
raise TypeError (f"`cap` must be a string, not { type (cap )} " )
467
437
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 )
481
439
482
440
483
441
def tparm (cap_bytes : bytes , * params : int ) -> bytes :
0 commit comments