# build-aux/ansiterm.py - Generate terminal "control seqences" for ANSI X3.64 terminals. # # https://github.com/emissary-ingress/emissary/blob/v1.13.7/releng/lib/ansiterm.py # Copyright (C) 2021 Datawire # SPDX-License-Identifier: Apache-2.0 # # Copyright (C) 2025 Luke T. Shumaker # SPDX-License-Identifier: AGPL-3.0-or-later """Generate terminal "control seqences" for ANSI X3.64 terminals. Or rather, ECMA-48 terminals, because ANSI withdrew X3.64 in favor of ECMA-48. - https://ecma-international.org/publications-and-standards/standards/ecma-48/ - https://ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf (high-quality text PDF) - https://ecma-international.org/wp-content/uploads/ECMA-48_4th_edition_december_1986.pdf (scan of a print copy, no OCR) - https://ecma-international.org/wp-content/uploads/ECMA-48_3rd_edition_march_1984.pdf (scan of a print copy, no OCR) - https://ecma-international.org/wp-content/uploads/ECMA-48_2nd_edition_august_1979.pdf (scan of a print copy, no OCR) - I haven't found 1st edition (September 1976) "control sequences" are a subset of "escape codes"; which are so named because they start with the ASCII ESC character ("\033"). If you're going to try to read ECMA-48, be aware the notation they use for a byte is "AB/CD" where AB and CD are zero-padded decimal numbers that represent 4-bit sequences. It's easiest to think of them as hexadecimal byte; for example, when ECMA-48 says "11/15", think "hexadecimal 0xBF". And then to make sense of that hexadecimal number, you'll want to have an ASCII reference table handy. This implementation is not complete/exhaustive; it only supports the things that we've found it handy to support. """ import typing # pylint: disable=unused-variable __all__ = [ "cs", "clear_rest_of_line", "clear_rest_of_screen", "cursor_up", "sgr", ] @typing.overload def cs(params: list[int | float], op: str) -> str: ... @typing.overload def cs(op: str) -> str: ... def cs(arg1, arg2=None): # type: ignore """cs returns a formatted 'control sequence' (ECMA-48 §5.4). This only supports text/ASCII ("7-bit") control seqences, and does support binary ("8-bit") control seqeneces. This only supports standard parameters (ECMA-48 §5.4.1.a / §5.4.2), and does NOT support "experimental"/"private" parameters (ECMA-48 §5.4.1.b). """ csi = "\033[" if arg2: params: list[int | float] = arg1 op: str = arg2 else: params = [] op = arg1 return csi + (";".join(str(n).replace(".", ":") for n in params)) + op # The "EL" ("Erase in Line") control seqence (ECMA-48 §8.3.41) with no # parameters. clear_rest_of_line = cs("K") # The "ED" ("Erase in Display^H^H^H^H^H^H^HPage") control seqence # (ECMA-48 §8.3.39) with no parameters. clear_rest_of_screen = cs("J") def cursor_up(lines: int = 1) -> str: """Generate the "CUU" ("CUrsor Up") control sequence (ECMA-48 §8.3.22).""" if lines == 1: return cs("A") return cs([lines], "A") def _sgr_code(code: int) -> "_SGR": def getter(self: "_SGR") -> "_SGR": return _SGR(self._params + [code]) # pylint: disable=protected-access return typing.cast("_SGR", property(getter)) class _SGR: def __init__(self, params: list[int | float] | None = None) -> None: self._params = params or [] def __str__(self) -> str: return cs(self._params, "m") def __eq__(self, other: typing.Any) -> bool: if not isinstance(other, _SGR): return NotImplemented return self._params == other._params reset = _sgr_code(0) bold = _sgr_code(1) fg_blk = _sgr_code(30) fg_red = _sgr_code(31) fg_grn = _sgr_code(32) fg_yel = _sgr_code(33) fg_blu = _sgr_code(34) fg_prp = _sgr_code(35) fg_cyn = _sgr_code(36) fg_wht = _sgr_code(37) # 38 is 8bit/24bit color fg_def = _sgr_code(39) bg_blk = _sgr_code(40) bg_red = _sgr_code(41) bg_grn = _sgr_code(42) bg_yel = _sgr_code(43) bg_blu = _sgr_code(44) bg_prp = _sgr_code(45) bg_cyn = _sgr_code(46) bg_wht = _sgr_code(47) # 48 is 8bit/24bit color bg_def = _sgr_code(49) # sgr builds "Set Graphics Rendition" control sequences (ECMA-48 # §8.3.117). sgr = _SGR()