ttyengine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ttyengine/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ _NAME = "ttyengine"
2
+
3
+
4
+ def _pyproject_version():
5
+ import re
6
+ from pathlib import Path
7
+
8
+ table = re.compile(r"^\s*\[+\s*([^\[\]]+?)\s*\]+\s*(?:#.*)?$")
9
+ field = re.compile(r"""^\s*(name|version)\s*=\s*(?:"([^"]*)"|'([^']*)')\s*(?:#.*)?$""")
10
+ try:
11
+ with open(Path(__file__).resolve().parent.parent / "pyproject.toml", encoding="utf-8") as f:
12
+ lines = f.read().splitlines()
13
+ except OSError:
14
+ return None
15
+ fields = {}
16
+ current = None
17
+ for line in lines:
18
+ header = table.match(line)
19
+ if header:
20
+ current = header.group(1)
21
+ continue
22
+ found = field.match(line) if current == "project" else None
23
+ if found:
24
+ fields[found.group(1)] = found.group(2) if found.group(2) is not None else found.group(3)
25
+ return fields.get("version") if fields.get("name") == _NAME else None
26
+
27
+
28
+ def _installed_version():
29
+ try:
30
+ from importlib.metadata import version
31
+ return version(_NAME)
32
+ except Exception:
33
+ return None
34
+
35
+
36
+ __version__ = _installed_version() or _pyproject_version() or "0+unknown"
37
+
38
+ __all__ = ["Session", "World", "Simulation", "start", "__version__"]
39
+
40
+
41
+ def __getattr__(name):
42
+ if name in ("Session", "World", "Simulation", "start"):
43
+ from . import launcher
44
+ return getattr(launcher, name)
45
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
ttyengine/ansi.py ADDED
@@ -0,0 +1,201 @@
1
+ import os
2
+ import re
3
+
4
+ ESC = chr(27)
5
+ BEL = chr(7)
6
+ CSI_INTRO = '['
7
+ OSC_INTRO = ']'
8
+ DCS_INTRO = 'P'
9
+ SS3_INTRO = 'O'
10
+ CSI = ESC + CSI_INTRO
11
+ OSC = ESC + OSC_INTRO
12
+ DCS = ESC + DCS_INTRO
13
+ SS3 = ESC + SS3_INTRO
14
+ ST = ESC + '\\'
15
+
16
+ PREFIX_ENV = 'TTYENGINE_ANSI_PREFIX'
17
+ SUFFIX_ENV = 'TTYENGINE_ANSI_SUFFIX'
18
+ NO_COLOR_ENV = 'NO_COLOR'
19
+
20
+ ESC_RE = re.escape(ESC)
21
+ BEL_RE = re.escape(BEL)
22
+ CSI_RE = re.escape(CSI)
23
+ OSC_RE = re.escape(OSC)
24
+ DCS_RE = re.escape(DCS)
25
+ ST_RE = re.escape(ST)
26
+
27
+ _prefix = ''
28
+ _suffix = ''
29
+ _version = 0
30
+
31
+
32
+ def csi(params, final):
33
+ return CSI + params + final
34
+
35
+
36
+ def sgr(*params):
37
+ return CSI + ';'.join([str(int(p)) for p in params]) + 'm'
38
+
39
+
40
+ def fg(red, green, blue):
41
+ return '%s38;2;%d;%d;%dm' % (CSI, red, green, blue)
42
+
43
+
44
+ def bg(red, green, blue):
45
+ return '%s48;2;%d;%d;%dm' % (CSI, red, green, blue)
46
+
47
+
48
+ def move(row, column):
49
+ return '%s%d;%dH' % (CSI, row, column)
50
+
51
+
52
+ def osc(body):
53
+ return OSC + body + BEL
54
+
55
+
56
+ def mode(number, on):
57
+ return '%s?%d%s' % (CSI, number, 'h' if on else 'l')
58
+
59
+
60
+ def modes(numbers, on):
61
+ return CSI + '?' + ';'.join([str(number) for number in numbers]) + ('h' if on else 'l')
62
+
63
+
64
+ RESET = sgr(0)
65
+ RESETS = (RESET, sgr())
66
+ BOLD = sgr(1)
67
+ ITALIC = sgr(3)
68
+ UNDERLINE = sgr(4)
69
+ HOME = csi('', 'H')
70
+ CLEAR = csi('2', 'J')
71
+ CLEAR_SCROLLBACK = csi('3', 'J')
72
+ CLEAR_ALL = HOME + CLEAR + CLEAR_SCROLLBACK
73
+ ERASE_LINE = csi('', 'K')
74
+ ERASE_BELOW = csi('', 'J')
75
+ FULL_RESET = ESC + 'c'
76
+ SAVE_CURSOR = ESC + '7'
77
+ RESTORE_CURSOR = ESC + '8'
78
+ SYNC_BEGIN = mode(2026, True)
79
+ SYNC_END = mode(2026, False)
80
+
81
+ _ANY = re.compile(
82
+ ESC_RE + r'(?:' + re.escape(CSI_INTRO) + r'[0-?]*[ -/]*[@-~]|'
83
+ + re.escape(OSC_INTRO) + r'[^' + BEL_RE + ESC_RE + r']*(?:' + BEL_RE + '|' + ST_RE + r')|[ -/]*[0-~])'
84
+ )
85
+
86
+
87
+ def _decode(text):
88
+ try:
89
+ return text.encode('ascii', 'backslashreplace').decode('unicode_escape')
90
+ except UnicodeDecodeError:
91
+ return text
92
+
93
+
94
+ def set_wrapper(prefix=None, suffix=None):
95
+ global _prefix, _suffix, _version
96
+ if prefix is None:
97
+ prefix = _decode(os.environ.get(PREFIX_ENV, ''))
98
+ if suffix is None:
99
+ suffix = _decode(os.environ.get(SUFFIX_ENV, ''))
100
+ if not isinstance(prefix, str) or not isinstance(suffix, str):
101
+ raise TypeError('wrapper prefix and suffix must be strings')
102
+ _prefix, _suffix = prefix, suffix
103
+ _version += 1
104
+
105
+
106
+ def get_wrapper():
107
+ return _prefix, _suffix
108
+
109
+
110
+ def wrapped():
111
+ return bool(_prefix or _suffix)
112
+
113
+
114
+ def wrap(code):
115
+ return _prefix + code + _suffix
116
+
117
+
118
+ def wrap_all(text):
119
+ if not (_prefix or _suffix):
120
+ return text
121
+ return _ANY.sub(lambda match: _prefix + match.group(0) + _suffix, text)
122
+
123
+
124
+ def _surround(body):
125
+ before = '(?:%s)?' % re.escape(_prefix) if _prefix else ''
126
+ after = '(?:%s)?' % re.escape(_suffix) if _suffix else ''
127
+ return before + '(?:' + body + ')' + after
128
+
129
+
130
+ class Pattern:
131
+ def __init__(self, body, flags=0):
132
+ self.body = body
133
+ self.flags = flags
134
+ self._seen = -1
135
+ self._regex = None
136
+
137
+ def regex(self):
138
+ if self._seen != _version:
139
+ self._regex = re.compile(_surround(self.body), self.flags)
140
+ self._seen = _version
141
+ return self._regex
142
+
143
+ def search(self, *args):
144
+ return self.regex().search(*args)
145
+
146
+ def match(self, *args):
147
+ return self.regex().match(*args)
148
+
149
+ def split(self, *args):
150
+ return self.regex().split(*args)
151
+
152
+ def sub(self, *args):
153
+ return self.regex().sub(*args)
154
+
155
+ def finditer(self, *args):
156
+ return self.regex().finditer(*args)
157
+
158
+ def findall(self, *args):
159
+ return self.regex().findall(*args)
160
+
161
+
162
+ SGR = Pattern(CSI_RE + r'([0-9;]*)m')
163
+ SEQUENCE = Pattern(
164
+ '(' + CSI_RE + r'[0-9;]*[A-Za-z]|' + OSC_RE + r'[^' + BEL_RE + r']*' + BEL_RE + '?|' + ESC_RE + r'[()]?[A-Za-z0-9])'
165
+ )
166
+
167
+
168
+ def color_enabled():
169
+ return not os.environ.get(NO_COLOR_ENV)
170
+
171
+
172
+ def monochrome(no_color=None):
173
+ return (not color_enabled()) if no_color is None else bool(no_color)
174
+
175
+
176
+ def _drop_color(match):
177
+ params = match.group(1).split(';')
178
+ kept = []
179
+ i = 0
180
+ while i < len(params):
181
+ token = params[i]
182
+ code = int(token) if token else 0
183
+ if code in (38, 48, 58):
184
+ kind = params[i + 1] if i + 1 < len(params) else ''
185
+ i += 3 if kind == '5' else 5 if kind == '2' else 1
186
+ continue
187
+ if 30 <= code <= 39 or 40 <= code <= 49 or code == 59 or 90 <= code <= 97 or 100 <= code <= 107:
188
+ i += 1
189
+ continue
190
+ kept.append(token)
191
+ i += 1
192
+ if not kept:
193
+ return ''
194
+ return CSI + ';'.join(kept) + 'm'
195
+
196
+
197
+ def strip_color(text):
198
+ return SGR.sub(_drop_color, text)
199
+
200
+
201
+ set_wrapper()