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/launcher.py
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import os, sys, time, threading, traceback, contextlib, inspect, ctypes, socket, subprocess, tempfile, shlex
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
from . import ansi
|
|
4
|
+
from .errors import attempt, highlight_traceback
|
|
5
|
+
|
|
6
|
+
DEBUG_HOLD = ansi.osc('777;hold')
|
|
7
|
+
DEBUG_RECEIVER = r"""
|
|
8
|
+
import sys, socket, codecs
|
|
9
|
+
if sys.platform == "win32":
|
|
10
|
+
import ctypes
|
|
11
|
+
kernel32 = ctypes.windll.kernel32
|
|
12
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
13
|
+
if __BACK__:
|
|
14
|
+
hwnd = kernel32.GetConsoleWindow()
|
|
15
|
+
if hwnd: ctypes.windll.user32.SetWindowPos(hwnd, 1, 0, 0, 0, 0, 19)
|
|
16
|
+
out = sys.stdout
|
|
17
|
+
out.write(__OPEN__)
|
|
18
|
+
out.flush()
|
|
19
|
+
sock = socket.socket()
|
|
20
|
+
sock.connect(("127.0.0.1", __PORT__))
|
|
21
|
+
decode = codecs.getincrementaldecoder("utf-8")("replace").decode
|
|
22
|
+
hold = False
|
|
23
|
+
marker = __HOLD__
|
|
24
|
+
while True:
|
|
25
|
+
data = sock.recv(65536)
|
|
26
|
+
if not data:
|
|
27
|
+
break
|
|
28
|
+
text = decode(data)
|
|
29
|
+
if marker in text:
|
|
30
|
+
text = text.replace(marker, "")
|
|
31
|
+
hold = True
|
|
32
|
+
out.write(text)
|
|
33
|
+
out.flush()
|
|
34
|
+
out.write(__CLOSE__)
|
|
35
|
+
out.flush()
|
|
36
|
+
if hold:
|
|
37
|
+
try: input()
|
|
38
|
+
except Exception: pass
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def spawn_console(script):
|
|
42
|
+
if sys.platform == "win32":
|
|
43
|
+
for cmd in (['conhost.exe', sys.executable, '-c', script], [sys.executable, '-c', script]):
|
|
44
|
+
try:
|
|
45
|
+
return subprocess.Popen(cmd, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
continue
|
|
48
|
+
return None
|
|
49
|
+
if sys.platform == "darwin":
|
|
50
|
+
fd, path = tempfile.mkstemp(prefix='ttyengine_debug_', suffix='.py')
|
|
51
|
+
with os.fdopen(fd, 'w') as f:
|
|
52
|
+
f.write(script)
|
|
53
|
+
command = f'{shlex.quote(sys.executable)} {shlex.quote(path)}; rm -f {shlex.quote(path)}; exit'
|
|
54
|
+
command = command.replace('\\', '\\\\').replace('"', '\\"')
|
|
55
|
+
return subprocess.Popen(['osascript', '-e', f'tell application "Terminal" to do script "{command}"'],
|
|
56
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
57
|
+
for term in ['x-terminal-emulator', 'gnome-terminal', 'xterm', 'konsole']:
|
|
58
|
+
try:
|
|
59
|
+
return subprocess.Popen([term, '--' if term == 'gnome-terminal' else '-e', sys.executable, '-c', script],
|
|
60
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
61
|
+
stderr=subprocess.DEVNULL, close_fds=True)
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
continue
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def format_dinfo(value):
|
|
67
|
+
if value is None:
|
|
68
|
+
lines = []
|
|
69
|
+
elif isinstance(value, str):
|
|
70
|
+
lines = value.split('\n')
|
|
71
|
+
elif isinstance(value, dict):
|
|
72
|
+
lines = [line for k, v in value.items() for line in f'{k}: {v}'.split('\n')]
|
|
73
|
+
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
74
|
+
lines = [line for v in value for line in str(v).split('\n')]
|
|
75
|
+
else:
|
|
76
|
+
lines = str(value).split('\n')
|
|
77
|
+
return ansi.HOME + (ansi.ERASE_LINE + '\n').join(lines) + ansi.ERASE_LINE + ansi.ERASE_BELOW
|
|
78
|
+
|
|
79
|
+
def getargs(func, args):
|
|
80
|
+
if not callable(func): return {}
|
|
81
|
+
sig = inspect.signature(func)
|
|
82
|
+
par = sig.parameters
|
|
83
|
+
if any(p.kind == p.VAR_KEYWORD for p in par.values()):
|
|
84
|
+
return args
|
|
85
|
+
resargs = {k: v for k, v in args.items() if k in par}
|
|
86
|
+
return resargs
|
|
87
|
+
|
|
88
|
+
def latency(samples=1000):
|
|
89
|
+
latencies = []
|
|
90
|
+
for _ in range(samples):
|
|
91
|
+
start = time.perf_counter()
|
|
92
|
+
time.sleep(0)
|
|
93
|
+
end = time.perf_counter()
|
|
94
|
+
latencies.append(end - start)
|
|
95
|
+
return latencies
|
|
96
|
+
|
|
97
|
+
def measure_latency(samples=200, percentile=0.95):
|
|
98
|
+
values = sorted(latency(samples))
|
|
99
|
+
return values[min(len(values) - 1, int(len(values) * percentile))]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
#MODULES
|
|
103
|
+
from . import scene
|
|
104
|
+
from . import region as regions
|
|
105
|
+
from .compositor import Compositor
|
|
106
|
+
from .terminal import Terminal, MODE_SYNC
|
|
107
|
+
from .input_handler import inp
|
|
108
|
+
from .sound import Mixer, set_ffmpeg
|
|
109
|
+
|
|
110
|
+
#WINDOWS INTIALIZATION
|
|
111
|
+
def universal_init():
|
|
112
|
+
if os.name == 'nt':
|
|
113
|
+
hOut = ctypes.windll.kernel32.GetStdHandle(-11)
|
|
114
|
+
dwMode = ctypes.c_uint32()
|
|
115
|
+
ctypes.windll.kernel32.GetConsoleMode(hOut, ctypes.byref(dwMode))
|
|
116
|
+
ctypes.windll.kernel32.SetConsoleMode(hOut, dwMode.value | 0x0004)
|
|
117
|
+
try:
|
|
118
|
+
universal_init()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
HANDLE_STATES = ('start', 'update', 'final')
|
|
123
|
+
|
|
124
|
+
_SPIN_MARGIN = 0.016 if sys.platform == 'win32' and sys.version_info < (3, 11) else 0.002
|
|
125
|
+
|
|
126
|
+
def handle_state(state):
|
|
127
|
+
if isinstance(state, str):
|
|
128
|
+
name = state.strip().lower()
|
|
129
|
+
if name in HANDLE_STATES:
|
|
130
|
+
return name
|
|
131
|
+
elif isinstance(state, int) and not isinstance(state, bool) and 0 <= state < len(HANDLE_STATES):
|
|
132
|
+
return HANDLE_STATES[state]
|
|
133
|
+
raise ValueError(f"invalid handle state {state!r}: use 'start'/0, 'update'/1 or 'final'/2")
|
|
134
|
+
|
|
135
|
+
def is_handle(obj):
|
|
136
|
+
return obj is not None and any(callable(getattr(obj, n, None)) for n in HANDLE_STATES)
|
|
137
|
+
|
|
138
|
+
def handle_class(handle, state='start'):
|
|
139
|
+
if inspect.isclass(handle):
|
|
140
|
+
return handle
|
|
141
|
+
if not callable(handle):
|
|
142
|
+
raise TypeError(f"launch() expects a class or a function, got {type(handle).__name__}")
|
|
143
|
+
name = handle_state(state)
|
|
144
|
+
base = getattr(handle, '__name__', None) or type(handle).__name__
|
|
145
|
+
return type(base, (), {
|
|
146
|
+
name: staticmethod(handle),
|
|
147
|
+
'_factory': name == 'start',
|
|
148
|
+
'__doc__': getattr(handle, '__doc__', None),
|
|
149
|
+
'__module__': getattr(handle, '__module__', __name__),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
#WORLD CLASS
|
|
153
|
+
class World():
|
|
154
|
+
def __init__(self, session=None, autoreg=True, front=True, **kwargs):
|
|
155
|
+
self.session = session
|
|
156
|
+
self.handles = []
|
|
157
|
+
self.scene = scene.Scene()
|
|
158
|
+
self._layer = 0
|
|
159
|
+
self.last_tick = time.perf_counter()
|
|
160
|
+
self.pause = False
|
|
161
|
+
self.pausephysics = False
|
|
162
|
+
self.pauserendering = False
|
|
163
|
+
self.autoterm = True
|
|
164
|
+
self.autodim = True
|
|
165
|
+
self.autobg = True
|
|
166
|
+
self.autofps = True
|
|
167
|
+
self._region = None
|
|
168
|
+
self._placed = False
|
|
169
|
+
for setting, val in kwargs.items():
|
|
170
|
+
if val is not None: setattr(self, setting, val)
|
|
171
|
+
if self.session is not None:
|
|
172
|
+
self.session._fit(self.scene, self)
|
|
173
|
+
if autoreg: attempt(lambda: self.register(front))
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def region(self):
|
|
177
|
+
return self._region
|
|
178
|
+
@region.setter
|
|
179
|
+
def region(self, value):
|
|
180
|
+
self._region = None if value is None else regions.parse(value)
|
|
181
|
+
if value is not None:
|
|
182
|
+
self._placed = True
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def layer(self):
|
|
186
|
+
return self._layer
|
|
187
|
+
@layer.setter
|
|
188
|
+
def layer(self, value):
|
|
189
|
+
self._layer = value
|
|
190
|
+
self.sort()
|
|
191
|
+
|
|
192
|
+
def sort(self, layer=None):
|
|
193
|
+
if self.session is not None:
|
|
194
|
+
self.session.worlds = sorted(list(self.session.worlds), key=lambda w: getattr(w, '_layer', 0) if w is not self or layer is None else layer)
|
|
195
|
+
|
|
196
|
+
class Handle():
|
|
197
|
+
def __init__(self, handle, world):
|
|
198
|
+
self.world = world
|
|
199
|
+
self.pause = False
|
|
200
|
+
self.handleargs = {
|
|
201
|
+
'scene': self.world.scene,
|
|
202
|
+
'handle': self,
|
|
203
|
+
'world': self.world,
|
|
204
|
+
'session': self.world.session,
|
|
205
|
+
'sound': getattr(self.world.session, 'sound', None),
|
|
206
|
+
'simulation': getattr(self.world.session, 'simulation', None),
|
|
207
|
+
}
|
|
208
|
+
self.handle = handle(**getargs(handle, self.handleargs))
|
|
209
|
+
if not hasattr(self.handle, 'priority'):
|
|
210
|
+
self.priority = 0
|
|
211
|
+
start = getattr(self.handle, 'start', None)
|
|
212
|
+
self.startargs = getargs(start, self.handleargs)
|
|
213
|
+
self.world.handles.append(self)
|
|
214
|
+
result = start(**self.startargs) if start else None
|
|
215
|
+
if getattr(self.handle, '_factory', False) and is_handle(result):
|
|
216
|
+
self.handle = result
|
|
217
|
+
start = getattr(result, 'start', None)
|
|
218
|
+
self.startargs = getargs(start, self.handleargs)
|
|
219
|
+
if start: start(**self.startargs)
|
|
220
|
+
@property
|
|
221
|
+
def priority(self):
|
|
222
|
+
return getattr(self.handle, 'priority', 0)
|
|
223
|
+
@priority.setter
|
|
224
|
+
def priority(self, value):
|
|
225
|
+
self.handle.priority = value
|
|
226
|
+
def func(self, key: str):
|
|
227
|
+
fu = getattr(self.handle, key, None)
|
|
228
|
+
method_cache_attr = f"_{key}_method"
|
|
229
|
+
args_cache_attr = f"{key}args"
|
|
230
|
+
if fu != getattr(self, method_cache_attr, None):
|
|
231
|
+
setattr(self, method_cache_attr, fu)
|
|
232
|
+
if fu is not None:
|
|
233
|
+
ar = getargs(fu, self.handleargs)
|
|
234
|
+
setattr(self, args_cache_attr, ar)
|
|
235
|
+
else:
|
|
236
|
+
if hasattr(self, args_cache_attr):
|
|
237
|
+
delattr(self, args_cache_attr)
|
|
238
|
+
if fu:
|
|
239
|
+
return lambda: fu(**getattr(self, args_cache_attr))
|
|
240
|
+
return None
|
|
241
|
+
def cancel(self):
|
|
242
|
+
self.world.handles = [h for h in self.world.handles if h is not self]
|
|
243
|
+
|
|
244
|
+
def ordered_handles(self):
|
|
245
|
+
return sorted(self.handles, key=lambda h: -h.priority)
|
|
246
|
+
|
|
247
|
+
def launch(self, handle=None, safe=True, state='start'):
|
|
248
|
+
if handle is None:
|
|
249
|
+
return lambda h: self.launch(h, safe=safe, state=state)
|
|
250
|
+
cls = handle_class(handle, state)
|
|
251
|
+
return (attempt(lambda: self.Handle(cls, self))
|
|
252
|
+
if safe else self.Handle(cls, self))
|
|
253
|
+
|
|
254
|
+
def cancel(self, handle):
|
|
255
|
+
self.handles = [h for h in self.handles if h is not handle]
|
|
256
|
+
|
|
257
|
+
def register(self, front=True):
|
|
258
|
+
if self.session is not None:
|
|
259
|
+
if self not in self.session.worlds:
|
|
260
|
+
self.session.worlds.append(self)
|
|
261
|
+
self.sync_colors()
|
|
262
|
+
if front: self.front()
|
|
263
|
+
else: self.back()
|
|
264
|
+
self.sort()
|
|
265
|
+
self.session.latency = self.session.latency
|
|
266
|
+
with self.session._resume:
|
|
267
|
+
self.session._resume.notify_all()
|
|
268
|
+
|
|
269
|
+
def sync_colors(self):
|
|
270
|
+
if self.session is not None:
|
|
271
|
+
self.scene.bgcolor = self.session.bgcolor
|
|
272
|
+
self.scene.fgcolor = self.session.fgcolor
|
|
273
|
+
|
|
274
|
+
def unregister(self):
|
|
275
|
+
if self.session is not None:
|
|
276
|
+
self.session.worlds = [w for w in self.session.worlds if w is not self]
|
|
277
|
+
|
|
278
|
+
def front(self):
|
|
279
|
+
if self.session is not None:
|
|
280
|
+
ll = None
|
|
281
|
+
l = 0
|
|
282
|
+
for w in self.session.worlds:
|
|
283
|
+
if ll is None:
|
|
284
|
+
ll = w._layer
|
|
285
|
+
l = w._layer
|
|
286
|
+
continue
|
|
287
|
+
if w._layer > ll: l = w._layer
|
|
288
|
+
self.layer = l + 1
|
|
289
|
+
|
|
290
|
+
def back(self):
|
|
291
|
+
if self.session is not None:
|
|
292
|
+
ll = None
|
|
293
|
+
l = 0
|
|
294
|
+
for w in self.session.worlds:
|
|
295
|
+
if ll is None:
|
|
296
|
+
ll = w._layer
|
|
297
|
+
l = w._layer
|
|
298
|
+
continue
|
|
299
|
+
if w._layer < ll: l = w._layer
|
|
300
|
+
self.layer = l - 1
|
|
301
|
+
|
|
302
|
+
def tick_ready(self):
|
|
303
|
+
now = time.perf_counter()
|
|
304
|
+
interval = 1.0 / self.scene.framerate
|
|
305
|
+
if now - self.last_tick >= interval:
|
|
306
|
+
self.last_tick = max(self.last_tick + interval, now - interval)
|
|
307
|
+
return True
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
#SESSION CLASS
|
|
311
|
+
class Session():
|
|
312
|
+
APIS = {'inp': True, 'style': True, 'sound': True}
|
|
313
|
+
LEGACY = ('termxoffset', 'termyoffset', 'termwidth', 'termheight')
|
|
314
|
+
SETTINGS = {'pausephysics': False, 'pauserendering': False, 'optimize': True, 'mouse': True, 'vsync': True, 'refresh': None,
|
|
315
|
+
'autoterm': True, 'autodim': True, 'autobg': True, 'outerbg': True, 'clearonstop': True, 'autofps': True,
|
|
316
|
+
'region': None,
|
|
317
|
+
'fullscreen': None, 'hidecursor': None, 'no_color': None}
|
|
318
|
+
|
|
319
|
+
def __init__(self, watchfocus=True, trackmotion=True, ffmpeg=None, **kwargs):
|
|
320
|
+
unknown = [k for k in kwargs if k not in self.APIS and k not in self.SETTINGS and k not in self.LEGACY]
|
|
321
|
+
if unknown:
|
|
322
|
+
raise TypeError(f"Session() got unexpected keyword argument(s): {', '.join(unknown)}")
|
|
323
|
+
self.worlds = []
|
|
324
|
+
self.simulation = None
|
|
325
|
+
self.World = lambda front=True, **kwargs: World(session=self, autoreg=True, front=front, **kwargs)
|
|
326
|
+
self.term = Terminal()
|
|
327
|
+
self.apis = {name: kwargs.get(name, default) for name, default in self.APIS.items()}
|
|
328
|
+
self.inp = inp(term=self.term, watchfocus=watchfocus, trackmotion=trackmotion) if self.apis['inp'] else None
|
|
329
|
+
if ffmpeg:
|
|
330
|
+
set_ffmpeg(ffmpeg)
|
|
331
|
+
self.sound = Mixer(listener=self._listener) if self.apis['sound'] else None
|
|
332
|
+
self._compositor = Compositor()
|
|
333
|
+
self.running = False
|
|
334
|
+
self._pause = False
|
|
335
|
+
self._dinfo = None
|
|
336
|
+
self._debug = None
|
|
337
|
+
self._sync_available = None
|
|
338
|
+
self._latency = measure_latency()
|
|
339
|
+
self._idle_scene = scene.Scene() if self.apis['style'] else None
|
|
340
|
+
bg = self.term.get_bgcolor(bits=8)
|
|
341
|
+
self._bgcolor = bg if bg != (-1, -1, -1) else (0, 0, 0)
|
|
342
|
+
fg = self.term.get_fgcolor(bits=8)
|
|
343
|
+
self._fgcolor = fg if fg != (-1, -1, -1) else (204, 204, 204)
|
|
344
|
+
self._sync_colors()
|
|
345
|
+
self._resume = threading.Condition()
|
|
346
|
+
self._region = regions.Region(0, 0, None, None)
|
|
347
|
+
self._anchor_row = None
|
|
348
|
+
for name, default in self.SETTINGS.items():
|
|
349
|
+
setattr(self, name, kwargs.get(name, default))
|
|
350
|
+
for name in self.LEGACY:
|
|
351
|
+
if name in kwargs:
|
|
352
|
+
setattr(self, name, kwargs[name])
|
|
353
|
+
if self._idle_scene is not None:
|
|
354
|
+
self._fit(self._idle_scene)
|
|
355
|
+
|
|
356
|
+
def _layout(self, sc, world, w, h):
|
|
357
|
+
termx, termy = (w, h) if self.autoterm and (world is None or world.autoterm) else (sc.termx, sc.termy)
|
|
358
|
+
dimx, dimy, dimxoffset, dimyoffset = sc.dimx, sc.dimy, sc.dimxoffset, sc.dimyoffset
|
|
359
|
+
if self.autodim and (world is None or world.autodim):
|
|
360
|
+
spec = None if world is None else world.region
|
|
361
|
+
if spec is None:
|
|
362
|
+
dimx, dimy = termx, termy
|
|
363
|
+
if world is not None and world._placed:
|
|
364
|
+
dimxoffset = dimyoffset = 0
|
|
365
|
+
else:
|
|
366
|
+
x, y, dimx, dimy = regions.resolve(spec, termx, termy)
|
|
367
|
+
dimxoffset, dimyoffset = x, -y
|
|
368
|
+
return termx, termy, dimx, dimy, dimxoffset, dimyoffset
|
|
369
|
+
|
|
370
|
+
def _fit(self, sc, world=None, region=None):
|
|
371
|
+
if region is None:
|
|
372
|
+
region = self._resolve_region()
|
|
373
|
+
sc.termx, sc.termy, sc.dimx, sc.dimy, sc.dimxoffset, sc.dimyoffset = self._layout(sc, world, region[2], region[3])
|
|
374
|
+
if world is not None and world.region is None:
|
|
375
|
+
world._placed = False
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def bgcolor(self):
|
|
379
|
+
return self._bgcolor
|
|
380
|
+
@bgcolor.setter
|
|
381
|
+
def bgcolor(self, value):
|
|
382
|
+
self._bgcolor = value
|
|
383
|
+
self._sync_colors()
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def fgcolor(self):
|
|
387
|
+
return self._fgcolor
|
|
388
|
+
@fgcolor.setter
|
|
389
|
+
def fgcolor(self, value):
|
|
390
|
+
self._fgcolor = value
|
|
391
|
+
self._sync_colors()
|
|
392
|
+
|
|
393
|
+
def _sync_colors(self):
|
|
394
|
+
for w in self.worlds:
|
|
395
|
+
w.sync_colors()
|
|
396
|
+
if self._idle_scene is not None:
|
|
397
|
+
self._idle_scene.bgcolor = self._bgcolor
|
|
398
|
+
self._idle_scene.fgcolor = self._fgcolor
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def dinfo(self):
|
|
402
|
+
return self._dinfo
|
|
403
|
+
@dinfo.setter
|
|
404
|
+
def dinfo(self, value):
|
|
405
|
+
self._dinfo = value
|
|
406
|
+
self._debug_push()
|
|
407
|
+
|
|
408
|
+
def debug(self, title=None, back=False):
|
|
409
|
+
if self._debug is not None:
|
|
410
|
+
return True
|
|
411
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
412
|
+
try:
|
|
413
|
+
server.bind(('127.0.0.1', 0))
|
|
414
|
+
server.listen(1)
|
|
415
|
+
opening = ((ansi.osc('0;' + title) if title is not None else '')
|
|
416
|
+
+ ansi.mode(1049, False) + ansi.mode(1049, True) + ansi.mode(25, False) + ansi.CLEAR + ansi.HOME)
|
|
417
|
+
script = (DEBUG_RECEIVER
|
|
418
|
+
.replace('__OPEN__', repr(opening))
|
|
419
|
+
.replace('__HOLD__', repr(DEBUG_HOLD))
|
|
420
|
+
.replace('__CLOSE__', repr(ansi.mode(25, True)))
|
|
421
|
+
.replace('__PORT__', str(server.getsockname()[1]))
|
|
422
|
+
.replace('__BACK__', repr(bool(back))))
|
|
423
|
+
proc = spawn_console(script)
|
|
424
|
+
if proc is None:
|
|
425
|
+
return False
|
|
426
|
+
server.settimeout(5)
|
|
427
|
+
try:
|
|
428
|
+
conn, _ = server.accept()
|
|
429
|
+
except socket.timeout:
|
|
430
|
+
try: proc.kill()
|
|
431
|
+
except Exception: pass
|
|
432
|
+
return False
|
|
433
|
+
finally:
|
|
434
|
+
server.close()
|
|
435
|
+
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
436
|
+
conn.settimeout(1.0)
|
|
437
|
+
try: sys.stdout.reconfigure(encoding='utf-8')
|
|
438
|
+
except Exception: pass
|
|
439
|
+
self._debug = {'proc': proc, 'conn': conn, 'last': None, 'hold': False}
|
|
440
|
+
if self.inp and self.inp.wf:
|
|
441
|
+
self.inp.focused = False
|
|
442
|
+
self._debug_push()
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
def undebug(self):
|
|
446
|
+
d, self._debug = self._debug, None
|
|
447
|
+
if d is None:
|
|
448
|
+
return
|
|
449
|
+
try: d['conn'].close()
|
|
450
|
+
except OSError: pass
|
|
451
|
+
if sys.platform != 'darwin' and not d['hold']:
|
|
452
|
+
try: d['proc'].kill()
|
|
453
|
+
except Exception: pass
|
|
454
|
+
if self.inp and self.inp.wf:
|
|
455
|
+
self.inp.focused = True
|
|
456
|
+
|
|
457
|
+
def _debug_send(self, frame):
|
|
458
|
+
try:
|
|
459
|
+
self._debug['conn'].sendall(frame.encode('utf-8'))
|
|
460
|
+
return True
|
|
461
|
+
except (OSError, ValueError):
|
|
462
|
+
self.undebug()
|
|
463
|
+
return False
|
|
464
|
+
|
|
465
|
+
def _debug_push(self):
|
|
466
|
+
d = self._debug
|
|
467
|
+
if d is None:
|
|
468
|
+
return
|
|
469
|
+
frame = format_dinfo(self._dinfo)
|
|
470
|
+
if frame != d['last'] and self._debug_send(frame):
|
|
471
|
+
d['last'] = frame
|
|
472
|
+
|
|
473
|
+
def _debug_trace(self):
|
|
474
|
+
d = self._debug
|
|
475
|
+
if d is None:
|
|
476
|
+
return
|
|
477
|
+
d['hold'] = True
|
|
478
|
+
self._debug_send(ansi.CLEAR_ALL + highlight_traceback(traceback.format_exc()) + DEBUG_HOLD)
|
|
479
|
+
|
|
480
|
+
def _listener(self):
|
|
481
|
+
if not self.worlds:
|
|
482
|
+
return None
|
|
483
|
+
sc = self.worlds[-1].scene
|
|
484
|
+
return (sc.camx + sc.dimx / 2.0, sc.camy - sc.dimy / 2.0)
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def styler(self):
|
|
488
|
+
if not self.apis['style']:
|
|
489
|
+
return None
|
|
490
|
+
return (self.worlds[-1].scene if self.worlds else self._idle_scene).styler
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def pause(self):
|
|
494
|
+
return self._pause
|
|
495
|
+
@pause.setter
|
|
496
|
+
def pause(self, value):
|
|
497
|
+
self._pause = value
|
|
498
|
+
if not value:
|
|
499
|
+
with self._resume:
|
|
500
|
+
self._resume.notify_all()
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def latency(self):
|
|
504
|
+
return self._latency
|
|
505
|
+
@latency.setter
|
|
506
|
+
def latency(self, value):
|
|
507
|
+
self._latency = value
|
|
508
|
+
if self.autofps:
|
|
509
|
+
for w in self.worlds:
|
|
510
|
+
if w.autofps: w.scene.maxframerate = 1 / max(value, 1e-6)
|
|
511
|
+
|
|
512
|
+
@property
|
|
513
|
+
def region(self):
|
|
514
|
+
return self._region
|
|
515
|
+
@region.setter
|
|
516
|
+
def region(self, value):
|
|
517
|
+
self._region = regions.parse(value)
|
|
518
|
+
if not self.running:
|
|
519
|
+
self._anchor_row = None
|
|
520
|
+
|
|
521
|
+
def _legacy_region(index):
|
|
522
|
+
def get(self):
|
|
523
|
+
return self._region[index]
|
|
524
|
+
def set(self, value):
|
|
525
|
+
parts = list(self._region)
|
|
526
|
+
parts[index] = value
|
|
527
|
+
self.region = parts
|
|
528
|
+
return property(get, set)
|
|
529
|
+
|
|
530
|
+
termxoffset = _legacy_region(0)
|
|
531
|
+
termyoffset = _legacy_region(1)
|
|
532
|
+
termwidth = _legacy_region(2)
|
|
533
|
+
termheight = _legacy_region(3)
|
|
534
|
+
del _legacy_region
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def resolved_region(self):
|
|
538
|
+
return self._resolve_region()
|
|
539
|
+
|
|
540
|
+
def _resolve_region(self):
|
|
541
|
+
return regions.resolve(self._region, self.term.width, self.term.height, self._anchor_row or 0)
|
|
542
|
+
|
|
543
|
+
def _anchor(self):
|
|
544
|
+
self._anchor_row = None
|
|
545
|
+
if not regions.uses_cursor(self._region):
|
|
546
|
+
return
|
|
547
|
+
tw, th = self.term.width, self.term.height
|
|
548
|
+
want = 1 if self._region.height is None else max(1, regions.resolve(self._region._replace(y=0), tw, th).height)
|
|
549
|
+
want = min(want, th)
|
|
550
|
+
here = self.term.get_cursor()
|
|
551
|
+
if here is not None and here[1] > 0:
|
|
552
|
+
sys.stdout.write('\r\n')
|
|
553
|
+
sys.stdout.write('\n' * want)
|
|
554
|
+
sys.stdout.flush()
|
|
555
|
+
here = self.term.get_cursor()
|
|
556
|
+
self._anchor_row = max(0, th - want if here is None else here[0] - want)
|
|
557
|
+
|
|
558
|
+
def _release(self):
|
|
559
|
+
row = self._anchor_row
|
|
560
|
+
if row is None:
|
|
561
|
+
return
|
|
562
|
+
x, y, w, h = self._resolve_region()
|
|
563
|
+
last = max(row, min(self.term.height - 1, y + max(h, 1) - 1))
|
|
564
|
+
if self.clearonstop:
|
|
565
|
+
sys.stdout.write(ansi.move(row + 1, 1))
|
|
566
|
+
else:
|
|
567
|
+
sys.stdout.write(ansi.move(last + 1, 1) + '\r\n')
|
|
568
|
+
sys.stdout.flush()
|
|
569
|
+
self._anchor_row = None
|
|
570
|
+
|
|
571
|
+
def _footprint(self, region):
|
|
572
|
+
x, y, w, h = region
|
|
573
|
+
if not self.worlds:
|
|
574
|
+
return region
|
|
575
|
+
right = bottom = 0
|
|
576
|
+
for world in self.worlds:
|
|
577
|
+
sc = world.scene
|
|
578
|
+
termx, termy, dimx, dimy, dimxoffset, dimyoffset = self._layout(sc, world, w, h)
|
|
579
|
+
view = Compositor.view(SimpleNamespace(
|
|
580
|
+
termx=termx, termy=termy, dimx=dimx, dimy=dimy,
|
|
581
|
+
dimxoffset=dimxoffset, dimyoffset=dimyoffset,
|
|
582
|
+
), w, h)
|
|
583
|
+
right = max(right, view[2])
|
|
584
|
+
bottom = max(bottom, view[3])
|
|
585
|
+
return (x, y, right, bottom)
|
|
586
|
+
|
|
587
|
+
def _vsync_available(self):
|
|
588
|
+
if self._sync_available is None:
|
|
589
|
+
self._sync_available = self.term.get_dec_mode(MODE_SYNC) in (1, 2)
|
|
590
|
+
return self._sync_available
|
|
591
|
+
|
|
592
|
+
def _partial(self):
|
|
593
|
+
if not regions.is_default(self._region):
|
|
594
|
+
return True
|
|
595
|
+
full = self._resolve_region()
|
|
596
|
+
return self._footprint(full) != full
|
|
597
|
+
|
|
598
|
+
def style(self, *args, **kwargs):
|
|
599
|
+
if not self.apis['style']:
|
|
600
|
+
raise RuntimeError("the style API is disabled for this Session (Session(style=False))")
|
|
601
|
+
return self.styler.style(*args, **kwargs)
|
|
602
|
+
|
|
603
|
+
def run_attempt(self, **kwargs):
|
|
604
|
+
if(not self.running):
|
|
605
|
+
self.running = True
|
|
606
|
+
for setting, val in kwargs.items():
|
|
607
|
+
if val is not None: setattr(self, setting, val)
|
|
608
|
+
self._anchor()
|
|
609
|
+
if self._idle_scene is not None:
|
|
610
|
+
self._fit(self._idle_scene)
|
|
611
|
+
for world in self.worlds:
|
|
612
|
+
self._fit(world.scene, world)
|
|
613
|
+
with contextlib.ExitStack() as main_stack:
|
|
614
|
+
opt_stack = main_stack.enter_context(contextlib.ExitStack())
|
|
615
|
+
is_optimized = False
|
|
616
|
+
changed_optimized = False
|
|
617
|
+
optt_stack = main_stack.enter_context(contextlib.ExitStack())
|
|
618
|
+
is_mouse = False
|
|
619
|
+
|
|
620
|
+
fs = hc = not self._partial()
|
|
621
|
+
while self.running:
|
|
622
|
+
if changed_optimized: changed_optimized = False
|
|
623
|
+
if self.optimize and not is_optimized:
|
|
624
|
+
fs = self.fullscreen if self.fullscreen is not None else not self._partial()
|
|
625
|
+
hc = self.hidecursor if self.hidecursor is not None else True
|
|
626
|
+
if fs: opt_stack.enter_context(self.term.fullscreen())
|
|
627
|
+
opt_stack.enter_context(self.term.cbreak())
|
|
628
|
+
if hc: opt_stack.enter_context(self.term.hidden_cursor())
|
|
629
|
+
opt_stack.enter_context(self.term.no_line_wrap())
|
|
630
|
+
opt_stack.enter_context(self.term.synchronized_output())
|
|
631
|
+
is_optimized = True
|
|
632
|
+
changed_optimized = True
|
|
633
|
+
if is_mouse:
|
|
634
|
+
optt_stack.close()
|
|
635
|
+
optt_stack = main_stack.enter_context(contextlib.ExitStack())
|
|
636
|
+
optt_stack.enter_context(
|
|
637
|
+
self.term.mouse_enabled(report_drag=True, report_motion=True)
|
|
638
|
+
)
|
|
639
|
+
elif not self.optimize and is_optimized:
|
|
640
|
+
opt_stack.close()
|
|
641
|
+
opt_stack = main_stack.enter_context(contextlib.ExitStack())
|
|
642
|
+
is_optimized = False
|
|
643
|
+
if self.mouse and not is_mouse:
|
|
644
|
+
optt_stack.enter_context(self.term.mouse_enabled(report_drag=True, report_motion=True))
|
|
645
|
+
is_mouse = True
|
|
646
|
+
elif not self.mouse and is_mouse:
|
|
647
|
+
optt_stack.close()
|
|
648
|
+
optt_stack = main_stack.enter_context(contextlib.ExitStack())
|
|
649
|
+
is_mouse = False
|
|
650
|
+
|
|
651
|
+
if(not self._pause):
|
|
652
|
+
if self.inp: self.inp.update()
|
|
653
|
+
framerate_groups = {}
|
|
654
|
+
for w in self.worlds:
|
|
655
|
+
if w.pause: continue
|
|
656
|
+
fps = w.scene.framerate
|
|
657
|
+
if fps not in framerate_groups:
|
|
658
|
+
framerate_groups[fps] = []
|
|
659
|
+
framerate_groups[fps].append(w)
|
|
660
|
+
ready_fps = set()
|
|
661
|
+
for fps, group in framerate_groups.items():
|
|
662
|
+
now = time.perf_counter()
|
|
663
|
+
if any(now - w.last_tick >= 1.0 / fps for w in group):
|
|
664
|
+
ready_fps.add(fps)
|
|
665
|
+
|
|
666
|
+
itworlds = [w for w in self.worlds if not w.pause and w.scene.framerate in ready_fps and w.tick_ready()]
|
|
667
|
+
|
|
668
|
+
region = self._resolve_region()
|
|
669
|
+
for idx, world in enumerate(itworlds):
|
|
670
|
+
self._fit(world.scene, world, region)
|
|
671
|
+
|
|
672
|
+
for handle in world.ordered_handles():
|
|
673
|
+
if handle.pause: continue
|
|
674
|
+
update = handle.func('update')
|
|
675
|
+
if update:
|
|
676
|
+
update()
|
|
677
|
+
|
|
678
|
+
dim_changed = any([w.scene.checkres() for w in self.worlds if not w.pause]) or changed_optimized
|
|
679
|
+
if dim_changed:
|
|
680
|
+
self._compositor.invalidate()
|
|
681
|
+
|
|
682
|
+
for idx, world in enumerate(itworlds):
|
|
683
|
+
world.scene.tick(not (world.pausephysics or self.pausephysics))
|
|
684
|
+
|
|
685
|
+
for handle in world.ordered_handles():
|
|
686
|
+
if handle.pause: continue
|
|
687
|
+
final = handle.func('final')
|
|
688
|
+
if final:
|
|
689
|
+
final()
|
|
690
|
+
|
|
691
|
+
world.scene.flush()
|
|
692
|
+
|
|
693
|
+
region = self._footprint(self._resolve_region())
|
|
694
|
+
bounds = (self.term.width, self.term.height)
|
|
695
|
+
active = [] if self.pauserendering else [w for w in itworlds if not w.pauserendering]
|
|
696
|
+
self._compositor.no_color = self.no_color
|
|
697
|
+
self._compositor.vsync = self.vsync
|
|
698
|
+
self._compositor.refresh = self.refresh
|
|
699
|
+
frame = self._compositor.compose(
|
|
700
|
+
self.worlds, active, region, bounds,
|
|
701
|
+
self.bgcolor if self.outerbg else None,
|
|
702
|
+
self.autobg,
|
|
703
|
+
)
|
|
704
|
+
if frame:
|
|
705
|
+
if not (hc and fs): frame = ansi.wrap(ansi.SAVE_CURSOR) + frame + ansi.wrap(ansi.RESTORE_CURSOR)
|
|
706
|
+
if self.vsync and self._vsync_available(): frame = ansi.wrap(ansi.SYNC_BEGIN) + frame + ansi.wrap(ansi.SYNC_END)
|
|
707
|
+
self._compositor.vblank()
|
|
708
|
+
sys.stdout.write(frame)
|
|
709
|
+
sys.stdout.flush()
|
|
710
|
+
|
|
711
|
+
now = time.perf_counter()
|
|
712
|
+
next_tick = min(
|
|
713
|
+
(min(w.last_tick for w in group) + 1.0 / fps
|
|
714
|
+
for fps, group in framerate_groups.items()),
|
|
715
|
+
default=now + (1.0 / 60 if self.worlds else 0)
|
|
716
|
+
)
|
|
717
|
+
sleep = next_tick - now
|
|
718
|
+
if sleep > self._latency and not self._pause:
|
|
719
|
+
tar = time.perf_counter() + sleep
|
|
720
|
+
coarse = sleep - _SPIN_MARGIN
|
|
721
|
+
if coarse > 0: time.sleep(coarse)
|
|
722
|
+
while time.perf_counter() < tar: pass
|
|
723
|
+
elif self._pause or len(self.worlds) == 0:
|
|
724
|
+
with self._resume:
|
|
725
|
+
self._resume.wait_for(lambda: not self._pause and len(self.worlds) > 0)
|
|
726
|
+
|
|
727
|
+
if self.inp:
|
|
728
|
+
self.inp.stop_all()
|
|
729
|
+
self.inp.cleanup()
|
|
730
|
+
if self.sound: self.sound.close()
|
|
731
|
+
if self.clearonstop:
|
|
732
|
+
if self._partial():
|
|
733
|
+
sys.stdout.write(ansi.wrap(ansi.SAVE_CURSOR) + Compositor.vacate(self._compositor.region, (-1, -1, 0, 0), (self.term.width, self.term.height)) + ansi.wrap(ansi.RESET) + ansi.wrap(ansi.RESTORE_CURSOR)
|
|
734
|
+
if self._compositor.region else '')
|
|
735
|
+
else:
|
|
736
|
+
sys.stdout.write(ansi.wrap_all(ansi.CLEAR_ALL))
|
|
737
|
+
sys.stdout.flush()
|
|
738
|
+
self._release()
|
|
739
|
+
self.undebug()
|
|
740
|
+
|
|
741
|
+
def run(self, **kwargs):
|
|
742
|
+
attempt(lambda: self.run_attempt(**kwargs), self._debug_trace)
|
|
743
|
+
|
|
744
|
+
def stop(self):
|
|
745
|
+
self.running = False
|
|
746
|
+
with self._resume:
|
|
747
|
+
self._resume.notify_all()
|
|
748
|
+
|
|
749
|
+
class Simulation():
|
|
750
|
+
def __init__(self, *sessions):
|
|
751
|
+
self.sessions = []
|
|
752
|
+
for session in sessions:
|
|
753
|
+
self.add(session)
|
|
754
|
+
|
|
755
|
+
@property
|
|
756
|
+
def worlds(self):
|
|
757
|
+
return [w for s in self.sessions for w in s.worlds]
|
|
758
|
+
|
|
759
|
+
@property
|
|
760
|
+
def handles(self):
|
|
761
|
+
return [h for w in self.worlds for h in w.handles]
|
|
762
|
+
|
|
763
|
+
@property
|
|
764
|
+
def scenes(self):
|
|
765
|
+
return [w.scene for w in self.worlds]
|
|
766
|
+
|
|
767
|
+
def __getitem__(self, index):
|
|
768
|
+
return self.sessions[index]
|
|
769
|
+
|
|
770
|
+
def __len__(self):
|
|
771
|
+
return len(self.sessions)
|
|
772
|
+
|
|
773
|
+
def __iter__(self):
|
|
774
|
+
return iter(self.sessions)
|
|
775
|
+
|
|
776
|
+
def add(self, session):
|
|
777
|
+
session.simulation = self
|
|
778
|
+
if session not in self.sessions:
|
|
779
|
+
self.sessions.append(session)
|
|
780
|
+
return session
|
|
781
|
+
|
|
782
|
+
def session(self, **kwargs):
|
|
783
|
+
return self.add(Session(**kwargs))
|
|
784
|
+
|
|
785
|
+
def _session(self, ref=None):
|
|
786
|
+
if ref is None:
|
|
787
|
+
return self.sessions[-1] if self.sessions else self.session()
|
|
788
|
+
if isinstance(ref, Session):
|
|
789
|
+
return self.add(ref)
|
|
790
|
+
return self.sessions[ref]
|
|
791
|
+
|
|
792
|
+
def world(self, session=None, front=True, **kwargs):
|
|
793
|
+
return self._session(session).World(front=front, **kwargs)
|
|
794
|
+
|
|
795
|
+
def _world(self, ref=None, session=None):
|
|
796
|
+
if isinstance(ref, World):
|
|
797
|
+
return ref
|
|
798
|
+
target = self._session(session)
|
|
799
|
+
if ref is None:
|
|
800
|
+
return target.worlds[-1] if target.worlds else target.World()
|
|
801
|
+
return target.worlds[ref]
|
|
802
|
+
|
|
803
|
+
def launch(self, *handles, world=None, session=None, safe=True, state='start'):
|
|
804
|
+
target = self._world(world, session)
|
|
805
|
+
return [target.launch(handle, safe=safe, state=state) for handle in handles]
|
|
806
|
+
|
|
807
|
+
def run(self, session=None, **settings):
|
|
808
|
+
self._session(session).run(**settings)
|
|
809
|
+
return self
|
|
810
|
+
|
|
811
|
+
def stop(self):
|
|
812
|
+
for session in self.sessions:
|
|
813
|
+
session.running = False
|
|
814
|
+
|
|
815
|
+
def start(*launch, **settings):
|
|
816
|
+
simulation = Simulation()
|
|
817
|
+
simulation.session()
|
|
818
|
+
if launch:
|
|
819
|
+
simulation.world()
|
|
820
|
+
simulation.launch(*launch)
|
|
821
|
+
simulation.run(**settings)
|
|
822
|
+
return simulation
|