summaryrefslogtreecommitdiff
path: root/build-aux/ansiterm.py
blob: aa9c26bb5fab1e978df21b32efe9f6e1d2c61328 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# 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 <lukeshu@lukeshu.com>
# 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()