Files
lix/tests/functional2/testlib/terminal_code_eater.py
T
Commentator2.0 427696a58d fix codestyle of functional2
Fixing up codestyle issues found within functional2 for later adding
ruff formatter

Change-Id: I0b280587c8243137184091a6d36df3dfe7568eb7
2025-05-10 21:13:45 +02:00

114 lines
4.3 KiB
Python

# copy pasta from libutil-support's terminal-code-eater.cc
import enum
import dataclasses
class State(enum.Enum):
ExpectESC = 1
ExpectESCSeq = 2
InCSIParams = 3
InCSIIntermediates = 4
InOSCParams = 5
InOSCST = 6
@dataclasses.dataclass
class TerminalCodeEater:
state: State = State.ExpectESC
def feed(self, data: bytes) -> bytes:
def is_param_char(char: int) -> bool:
return 48 <= char <= 63
def is_intermediate_char(char: int) -> bool:
return 32 <= char <= 47
def is_final_char(char: int) -> bool:
return 64 <= char <= 126
ret = bytearray()
for c in data:
match self.state:
case State.ExpectESC:
match c:
case 0x1B: # \e
self._transition(State.ExpectESCSeq)
continue
case 0xD: # \r
continue
ret.append(c)
case State.ExpectESCSeq:
match c:
case 0x5B:
# corresponds to CSI ('[')
self._transition(State.InCSIParams)
continue
case 0x5D:
# corresponds to OSC (']')
self._transition(State.InOSCParams)
continue
# FIXME(jade): whatever this was, we do not know how to
# delimit it, so we just eat the next character and
# keep going. Should we actually eat it?
case _:
self._transition(State.ExpectESC)
continue
# https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
# A CSI sequence is: CSI [\x30-\x3f]* [\x20-\x2f]* [\x40-\x7e]
# ^ params ^ intermediates ^ final byte
case State.InCSIParams:
if is_final_char(c):
self._transition(State.ExpectESC)
continue
if is_intermediate_char(c):
self._transition(State.InCSIIntermediates)
continue
if is_param_char(c):
continue
msg = f"Corrupt escape sequence, at {c:x}"
raise ValueError(msg)
case State.InCSIIntermediates:
if is_final_char(c):
self._transition(State.ExpectESC)
continue
if is_intermediate_char(c):
continue
msg = f"Corrupt escape sequence in intermediates, at {c:x}"
raise ValueError(msg)
# An OSC is OSC [\x20-\x7e]* ST per ECMA-48
# where OSC is \x1b ] and ST is \x1b \.
case State.InOSCParams:
# first part of ST
if c == 0x1B:
self._transition(State.InOSCST)
continue
# OSC sequences can be ended by BEL on old xterms
if c == 0x07:
self._transition(State.ExpectESC)
continue
if c < 0x20 or c == 0x7F:
msg = f"Corrupt OSC sequence, at {c:x}"
raise ValueError(msg)
# either way, eat it
continue
case State.InOSCST:
# ST ends by \
if c == 0x5C: # \
self._transition(State.ExpectESC)
elif c < 0x20 or c > 0x7E:
msg = f"Corrupt OSC sequence in ST, at {c:x}"
raise ValueError(msg)
else:
self._transition(State.InOSCParams)
continue
return bytes(ret)
def _transition(self, new_state: State):
self.state = new_state
def eat_terminal_codes(s: bytes) -> bytes:
return TerminalCodeEater().feed(s)