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 +45 -0
- ttyengine/ansi.py +201 -0
- ttyengine/ascii_styler.py +473 -0
- ttyengine/components.py +4718 -0
- ttyengine/compositor.py +780 -0
- ttyengine/errors.py +181 -0
- ttyengine/figlet-LICENSE.txt +27 -0
- ttyengine/figlet.py +430 -0
- ttyengine/fonts.zip +0 -0
- ttyengine/image_reader.py +813 -0
- ttyengine/input_handler.py +814 -0
- ttyengine/launcher.py +822 -0
- ttyengine/region.py +180 -0
- ttyengine/scene.py +614 -0
- ttyengine/sound.py +1439 -0
- ttyengine/terminal.py +283 -0
- ttyengine-0.1.0.dist-info/METADATA +903 -0
- ttyengine-0.1.0.dist-info/RECORD +21 -0
- ttyengine-0.1.0.dist-info/WHEEL +5 -0
- ttyengine-0.1.0.dist-info/licenses/LICENSE +373 -0
- ttyengine-0.1.0.dist-info/top_level.txt +1 -0
ttyengine/terminal.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import os, sys, re, time, codecs, contextlib
|
|
2
|
+
from . import ansi
|
|
3
|
+
|
|
4
|
+
_WINDOWS = sys.platform == 'win32'
|
|
5
|
+
if _WINDOWS:
|
|
6
|
+
import msvcrt
|
|
7
|
+
else:
|
|
8
|
+
import select, termios, tty
|
|
9
|
+
|
|
10
|
+
QUERY_TIMEOUT = 5.0
|
|
11
|
+
|
|
12
|
+
MODE_SYNC = 2026
|
|
13
|
+
MODE_MOUSE_CLICK = 1000
|
|
14
|
+
MODE_MOUSE_DRAG = 1002
|
|
15
|
+
MODE_MOUSE_MOTION = 1003
|
|
16
|
+
MODE_MOUSE_SGR = 1006
|
|
17
|
+
|
|
18
|
+
_RGB = r'rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)(?:' + ansi.BEL_RE + '|' + ansi.ST_RE + ')'
|
|
19
|
+
_CPR = re.compile(ansi.CSI_RE + r'[0-9]+;[0-9]+R')
|
|
20
|
+
_CPR_GROUPS = re.compile(ansi.CSI_RE + r'([0-9]+);([0-9]+)R')
|
|
21
|
+
_XTVERSION = re.compile(ansi.DCS_RE + r'>\|([^' + ansi.ESC_RE + r']*)' + ansi.ST_RE)
|
|
22
|
+
_BGCOLOR = re.compile(ansi.OSC_RE + '11;' + _RGB)
|
|
23
|
+
_FGCOLOR = re.compile(ansi.OSC_RE + '10;' + _RGB)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def xparse_color(component, bits=16):
|
|
27
|
+
if bits not in (8, 16):
|
|
28
|
+
raise ValueError(f"bits must be 8 or 16, got {bits}")
|
|
29
|
+
size = len(component)
|
|
30
|
+
value = int(component, 16)
|
|
31
|
+
if size == 4:
|
|
32
|
+
wide = value
|
|
33
|
+
elif size == 3:
|
|
34
|
+
wide = (value << 4) | (value >> 8)
|
|
35
|
+
elif size == 2:
|
|
36
|
+
wide = value * 0x0101
|
|
37
|
+
elif size == 1:
|
|
38
|
+
wide = value * 0x1111
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError(f"hex component must be 1-4 characters, got {size}")
|
|
41
|
+
return wide >> 8 if bits == 8 else wide
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Terminal:
|
|
45
|
+
def __init__(self, stream=None):
|
|
46
|
+
self.stream = stream if stream is not None else sys.__stdout__
|
|
47
|
+
self._pending = ''
|
|
48
|
+
self._decoder = codecs.getincrementaldecoder('utf-8')('replace')
|
|
49
|
+
self._cbreak = False
|
|
50
|
+
self._modes = {}
|
|
51
|
+
self._modes_ok = False
|
|
52
|
+
self._modes_dead = False
|
|
53
|
+
self._apple_found = None
|
|
54
|
+
try:
|
|
55
|
+
self._fd = sys.__stdin__.fileno() if sys.__stdin__.isatty() else None
|
|
56
|
+
except Exception:
|
|
57
|
+
self._fd = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_a_tty(self):
|
|
61
|
+
try:
|
|
62
|
+
return bool(self.stream.isatty())
|
|
63
|
+
except Exception:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def _size(self):
|
|
67
|
+
for stream in (self.stream, sys.__stdout__):
|
|
68
|
+
try:
|
|
69
|
+
size = os.get_terminal_size(stream.fileno())
|
|
70
|
+
return size.lines, size.columns
|
|
71
|
+
except (OSError, ValueError, AttributeError, TypeError):
|
|
72
|
+
pass
|
|
73
|
+
return int(os.getenv('LINES', '25')), int(os.getenv('COLUMNS', '80'))
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def height(self):
|
|
77
|
+
return self._size()[0]
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def width(self):
|
|
81
|
+
return self._size()[1]
|
|
82
|
+
|
|
83
|
+
def _write(self, text):
|
|
84
|
+
try:
|
|
85
|
+
self.stream.write(ansi.wrap_all(text))
|
|
86
|
+
self.stream.flush()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def take(self):
|
|
91
|
+
text, self._pending = self._pending, ''
|
|
92
|
+
return text
|
|
93
|
+
|
|
94
|
+
def ungetch(self, text):
|
|
95
|
+
self._pending = text + self._pending
|
|
96
|
+
|
|
97
|
+
def kbhit(self, timeout=None):
|
|
98
|
+
if self._fd is None:
|
|
99
|
+
return False
|
|
100
|
+
if _WINDOWS:
|
|
101
|
+
end = None if timeout is None else time.monotonic() + timeout
|
|
102
|
+
while not msvcrt.kbhit():
|
|
103
|
+
if end is not None and time.monotonic() >= end:
|
|
104
|
+
return False
|
|
105
|
+
time.sleep(0.005)
|
|
106
|
+
return True
|
|
107
|
+
try:
|
|
108
|
+
return bool(select.select([self._fd], [], [], timeout)[0])
|
|
109
|
+
except (OSError, ValueError):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def _read_available(self):
|
|
113
|
+
if self._fd is None:
|
|
114
|
+
return ''
|
|
115
|
+
if _WINDOWS:
|
|
116
|
+
chars = []
|
|
117
|
+
while msvcrt.kbhit():
|
|
118
|
+
chars.append(msvcrt.getwch())
|
|
119
|
+
return ''.join(chars)
|
|
120
|
+
chunks = []
|
|
121
|
+
while True:
|
|
122
|
+
try:
|
|
123
|
+
ready, _, _ = select.select([self._fd], [], [], 0)
|
|
124
|
+
if not ready:
|
|
125
|
+
break
|
|
126
|
+
data = os.read(self._fd, 4096)
|
|
127
|
+
except (OSError, ValueError):
|
|
128
|
+
break
|
|
129
|
+
if not data:
|
|
130
|
+
break
|
|
131
|
+
chunks.append(data)
|
|
132
|
+
if len(data) < 4096:
|
|
133
|
+
break
|
|
134
|
+
return self._decoder.decode(b''.join(chunks)) if chunks else ''
|
|
135
|
+
|
|
136
|
+
def _read_until(self, pattern, timeout):
|
|
137
|
+
start = time.monotonic()
|
|
138
|
+
buf = self.take()
|
|
139
|
+
while True:
|
|
140
|
+
buf += self._read_available()
|
|
141
|
+
match = pattern.search(buf)
|
|
142
|
+
if match is not None or len(buf) > 65536:
|
|
143
|
+
break
|
|
144
|
+
left = timeout - (time.monotonic() - start)
|
|
145
|
+
if left <= 0 or not self.kbhit(left):
|
|
146
|
+
break
|
|
147
|
+
return match, buf
|
|
148
|
+
|
|
149
|
+
def _query(self, query, pattern, timeout):
|
|
150
|
+
if not self.is_a_tty or self._fd is None:
|
|
151
|
+
return None
|
|
152
|
+
with self._rare():
|
|
153
|
+
self._write(query + ansi.csi('6', 'n'))
|
|
154
|
+
boundary, data = self._read_until(_CPR, timeout)
|
|
155
|
+
if boundary is not None:
|
|
156
|
+
data = data[:boundary.start()] + data[boundary.end():]
|
|
157
|
+
found = pattern.search(data)
|
|
158
|
+
if found is not None:
|
|
159
|
+
data = data[:found.start()] + data[found.end():]
|
|
160
|
+
self.ungetch(data)
|
|
161
|
+
return found
|
|
162
|
+
|
|
163
|
+
@contextlib.contextmanager
|
|
164
|
+
def _rare(self):
|
|
165
|
+
if self._cbreak:
|
|
166
|
+
yield
|
|
167
|
+
else:
|
|
168
|
+
with self.cbreak():
|
|
169
|
+
yield
|
|
170
|
+
|
|
171
|
+
@contextlib.contextmanager
|
|
172
|
+
def cbreak(self):
|
|
173
|
+
if _WINDOWS or self._fd is None:
|
|
174
|
+
yield
|
|
175
|
+
return
|
|
176
|
+
saved = termios.tcgetattr(self._fd)
|
|
177
|
+
was = self._cbreak
|
|
178
|
+
tty.setcbreak(self._fd, termios.TCSANOW)
|
|
179
|
+
self._cbreak = True
|
|
180
|
+
try:
|
|
181
|
+
yield
|
|
182
|
+
finally:
|
|
183
|
+
termios.tcsetattr(self._fd, termios.TCSADRAIN, saved)
|
|
184
|
+
self._cbreak = was
|
|
185
|
+
|
|
186
|
+
@contextlib.contextmanager
|
|
187
|
+
def _sequence(self, enter, leave):
|
|
188
|
+
styled = self.is_a_tty
|
|
189
|
+
if styled:
|
|
190
|
+
self._write(enter)
|
|
191
|
+
try:
|
|
192
|
+
yield
|
|
193
|
+
finally:
|
|
194
|
+
if styled:
|
|
195
|
+
self._write(leave)
|
|
196
|
+
|
|
197
|
+
def fullscreen(self):
|
|
198
|
+
return self._sequence(ansi.mode(1049, True) + ansi.csi('22;0;0', 't'),
|
|
199
|
+
ansi.mode(1049, False) + ansi.csi('23;0;0', 't'))
|
|
200
|
+
|
|
201
|
+
def hidden_cursor(self):
|
|
202
|
+
return self._sequence(ansi.mode(25, False), ansi.mode(12, False) + ansi.mode(25, True))
|
|
203
|
+
|
|
204
|
+
def no_line_wrap(self):
|
|
205
|
+
return self._sequence(ansi.mode(7, False), ansi.mode(7, True))
|
|
206
|
+
|
|
207
|
+
def _apple(self, timeout):
|
|
208
|
+
if os.environ.get('TERM_PROGRAM') == 'Apple_Terminal':
|
|
209
|
+
return True
|
|
210
|
+
if self._apple_found is None:
|
|
211
|
+
match = self._query(ansi.csi('>', 'q'), _XTVERSION, timeout)
|
|
212
|
+
self._apple_found = bool(match) and match.group(1).startswith('Apple_Terminal')
|
|
213
|
+
return self._apple_found
|
|
214
|
+
|
|
215
|
+
def get_cursor(self, timeout=QUERY_TIMEOUT):
|
|
216
|
+
if not self.is_a_tty or self._fd is None:
|
|
217
|
+
return None
|
|
218
|
+
with self._rare():
|
|
219
|
+
self._write(ansi.csi('6', 'n'))
|
|
220
|
+
found, data = self._read_until(_CPR_GROUPS, timeout)
|
|
221
|
+
if found is not None:
|
|
222
|
+
data = data[:found.start()] + data[found.end():]
|
|
223
|
+
self.ungetch(data)
|
|
224
|
+
if found is None:
|
|
225
|
+
return None
|
|
226
|
+
return int(found.group(1)) - 1, int(found.group(2)) - 1
|
|
227
|
+
|
|
228
|
+
def get_dec_mode(self, mode, timeout=QUERY_TIMEOUT):
|
|
229
|
+
if mode in self._modes:
|
|
230
|
+
return self._modes[mode]
|
|
231
|
+
if self._modes_dead or self._apple(timeout):
|
|
232
|
+
return -1
|
|
233
|
+
pattern = re.compile(ansi.CSI_RE + r'\?%d;([0-4])\$y' % mode)
|
|
234
|
+
match = self._query(ansi.csi('?%d$' % mode, 'p'), pattern, timeout)
|
|
235
|
+
if match is None:
|
|
236
|
+
if self.is_a_tty and self._fd is not None and not self._modes_ok:
|
|
237
|
+
self._modes_dead = True
|
|
238
|
+
return -1
|
|
239
|
+
self._modes_ok = True
|
|
240
|
+
self._modes[mode] = int(match.group(1))
|
|
241
|
+
return self._modes[mode]
|
|
242
|
+
|
|
243
|
+
@contextlib.contextmanager
|
|
244
|
+
def dec_modes_enabled(self, *modes, timeout=QUERY_TIMEOUT):
|
|
245
|
+
turned = [mode for mode in modes if self.get_dec_mode(mode, timeout) == 2]
|
|
246
|
+
if turned and self.is_a_tty:
|
|
247
|
+
self._write(ansi.modes(turned, True))
|
|
248
|
+
for mode in turned:
|
|
249
|
+
self._modes[mode] = 1
|
|
250
|
+
try:
|
|
251
|
+
yield
|
|
252
|
+
finally:
|
|
253
|
+
if turned and self.is_a_tty:
|
|
254
|
+
self._write(ansi.modes(turned, False))
|
|
255
|
+
for mode in turned:
|
|
256
|
+
self._modes[mode] = 2
|
|
257
|
+
|
|
258
|
+
def mouse_enabled(self, *, clicks=True, report_drag=False, report_motion=False, timeout=QUERY_TIMEOUT):
|
|
259
|
+
modes = [MODE_MOUSE_SGR]
|
|
260
|
+
if report_motion:
|
|
261
|
+
modes.append(MODE_MOUSE_MOTION)
|
|
262
|
+
elif report_drag:
|
|
263
|
+
modes.append(MODE_MOUSE_DRAG)
|
|
264
|
+
elif clicks:
|
|
265
|
+
modes.append(MODE_MOUSE_CLICK)
|
|
266
|
+
return self.dec_modes_enabled(*modes, timeout=timeout)
|
|
267
|
+
|
|
268
|
+
def synchronized_output(self, timeout=QUERY_TIMEOUT):
|
|
269
|
+
return self.dec_modes_enabled(MODE_SYNC, timeout=timeout)
|
|
270
|
+
|
|
271
|
+
def _color(self, query, pattern, timeout, bits):
|
|
272
|
+
if bits not in (8, 16):
|
|
273
|
+
raise ValueError(f"bits must be 8 or 16, got {bits}")
|
|
274
|
+
match = self._query(query, pattern, timeout)
|
|
275
|
+
if match is None:
|
|
276
|
+
return (-1, -1, -1)
|
|
277
|
+
return tuple(xparse_color(value, bits) for value in match.groups())
|
|
278
|
+
|
|
279
|
+
def get_bgcolor(self, timeout=QUERY_TIMEOUT, bits=16):
|
|
280
|
+
return self._color(ansi.osc('11;?'), _BGCOLOR, timeout, bits)
|
|
281
|
+
|
|
282
|
+
def get_fgcolor(self, timeout=QUERY_TIMEOUT, bits=16):
|
|
283
|
+
return self._color(ansi.osc('10;?'), _FGCOLOR, timeout, bits)
|