zexus 1.6.8 → 1.7.2
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.
- package/README.md +12 -5
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/capability_system.py +184 -9
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +383 -34
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/bytecode.py +124 -7
- package/src/zexus/compiler/compat_runtime.py +6 -2
- package/src/zexus/compiler/lexer.py +16 -5
- package/src/zexus/compiler/parser.py +108 -7
- package/src/zexus/compiler/semantic.py +18 -19
- package/src/zexus/compiler/zexus_ast.py +26 -1
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +112 -9
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +457 -37
- package/src/zexus/evaluator/core.py +644 -50
- package/src/zexus/evaluator/expressions.py +358 -62
- package/src/zexus/evaluator/functions.py +458 -20
- package/src/zexus/evaluator/resource_limiter.py +4 -4
- package/src/zexus/evaluator/statements.py +774 -122
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/evaluator_original.py +1 -1
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -458
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +239 -9
- package/src/zexus/module_manager.py +129 -1
- package/src/zexus/object.py +76 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +1349 -408
- package/src/zexus/parser/strategy_context.py +755 -58
- package/src/zexus/parser/strategy_structural.py +121 -21
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +61 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/backend.py +261 -0
- package/src/zexus/renderer/canvas.py +78 -0
- package/src/zexus/renderer/color_system.py +201 -0
- package/src/zexus/renderer/graphics.py +31 -0
- package/src/zexus/renderer/layout.py +222 -0
- package/src/zexus/renderer/main_renderer.py +66 -0
- package/src/zexus/renderer/painter.py +30 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__init__.py +10 -2
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/runtime/load_manager.py +368 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +80 -6
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +59 -11
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +561 -17
- package/src/zexus/vm/compiler.py +818 -51
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +364 -20
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +140 -45
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3581 -531
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +137 -11
- package/src/zexus/zexus_token.py +16 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +16 -6
- package/src/zexus.egg-info/SOURCES.txt +129 -17
- package/src/zexus.egg-info/entry_points.txt +1 -0
- package/src/zexus.egg-info/requires.txt +4 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Lightweight drawing canvas used for renderer CANVAS helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Dict, List, Tuple
|
|
6
|
+
|
|
7
|
+
from .color_system import RGBColor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Canvas:
|
|
12
|
+
width: int
|
|
13
|
+
height: int
|
|
14
|
+
pixels: List[List[str]] = field(init=False)
|
|
15
|
+
colours: List[List[RGBColor | None]] = field(init=False)
|
|
16
|
+
operations: List[Tuple[str, Tuple[object, ...]]] = field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
def __post_init__(self) -> None:
|
|
19
|
+
self.pixels = [[" " for _ in range(self.width)] for _ in range(self.height)]
|
|
20
|
+
self.colours = [[None for _ in range(self.width)] for _ in range(self.height)]
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------------
|
|
23
|
+
def draw_line(self, x1: int, y1: int, x2: int, y2: int, *, char: str = "█", colour: RGBColor | None = None) -> None:
|
|
24
|
+
orig = (x1, y1, x2, y2)
|
|
25
|
+
dx = abs(x2 - x1)
|
|
26
|
+
dy = -abs(y2 - y1)
|
|
27
|
+
sx = 1 if x1 < x2 else -1
|
|
28
|
+
sy = 1 if y1 < y2 else -1
|
|
29
|
+
err = dx + dy
|
|
30
|
+
while True:
|
|
31
|
+
self._plot(x1, y1, char, colour)
|
|
32
|
+
if x1 == x2 and y1 == y2:
|
|
33
|
+
break
|
|
34
|
+
e2 = 2 * err
|
|
35
|
+
if e2 >= dy:
|
|
36
|
+
err += dy
|
|
37
|
+
x1 += sx
|
|
38
|
+
if e2 <= dx:
|
|
39
|
+
err += dx
|
|
40
|
+
y1 += sy
|
|
41
|
+
self.operations.append(("line", orig))
|
|
42
|
+
|
|
43
|
+
def draw_text(self, x: int, y: int, text: str, *, colour: RGBColor | None = None) -> None:
|
|
44
|
+
for offset, ch in enumerate(text):
|
|
45
|
+
self._plot(x + offset, y, ch, colour)
|
|
46
|
+
self.operations.append(("text", (x, y, text)))
|
|
47
|
+
|
|
48
|
+
def snapshot(self) -> Dict[str, object]:
|
|
49
|
+
return {
|
|
50
|
+
"width": self.width,
|
|
51
|
+
"height": self.height,
|
|
52
|
+
"pixels": ["".join(row) for row in self.pixels],
|
|
53
|
+
"draw_ops": list(self.operations),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
def _plot(self, x: int, y: int, char: str, colour: RGBColor | None) -> None:
|
|
58
|
+
if 0 <= x < self.width and 0 <= y < self.height:
|
|
59
|
+
self.pixels[y][x] = char
|
|
60
|
+
self.colours[y][x] = colour
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class CanvasRegistry:
|
|
65
|
+
canvases: Dict[str, Canvas] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
def create(self, *, width: int, height: int) -> str:
|
|
68
|
+
identifier = f"canvas_{len(self.canvases) + 1}"
|
|
69
|
+
self.canvases[identifier] = Canvas(width, height)
|
|
70
|
+
return identifier
|
|
71
|
+
|
|
72
|
+
def get(self, identifier: str) -> Canvas:
|
|
73
|
+
if identifier not in self.canvases:
|
|
74
|
+
raise KeyError(f"canvas '{identifier}' not found")
|
|
75
|
+
return self.canvases[identifier]
|
|
76
|
+
|
|
77
|
+
def snapshot(self) -> Dict[str, Dict[str, object]]:
|
|
78
|
+
return {name: canvas.snapshot() for name, canvas in self.canvases.items()}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Colour utilities for the Zexus renderer.
|
|
2
|
+
|
|
3
|
+
The previous implementation lived at ``renderer/color_system.py``. The new
|
|
4
|
+
version adopts dataclasses, immutability and richer typing to make the colour
|
|
5
|
+
pipeline easier to reason about and safer to share between the interpreter and
|
|
6
|
+
VM backends.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Dict, Iterable, List, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
15
|
+
return max(lo, min(hi, value))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RGBColor:
|
|
20
|
+
"""Simple immutable RGB colour representation."""
|
|
21
|
+
|
|
22
|
+
r: int
|
|
23
|
+
g: int
|
|
24
|
+
b: int
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
object.__setattr__(self, "r", int(_clamp(self.r, 0, 255)))
|
|
28
|
+
object.__setattr__(self, "g", int(_clamp(self.g, 0, 255)))
|
|
29
|
+
object.__setattr__(self, "b", int(_clamp(self.b, 0, 255)))
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Blending helpers
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
def mix(self, other: "RGBColor", ratio: float = 0.5) -> "RGBColor":
|
|
35
|
+
ratio = _clamp(ratio, 0.0, 1.0)
|
|
36
|
+
inv = 1.0 - ratio
|
|
37
|
+
return RGBColor(
|
|
38
|
+
int(self.r * inv + other.r * ratio),
|
|
39
|
+
int(self.g * inv + other.g * ratio),
|
|
40
|
+
int(self.b * inv + other.b * ratio),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def lighten(self, amount: float = 0.1) -> "RGBColor":
|
|
44
|
+
return self.mix(RGBColor(255, 255, 255), amount)
|
|
45
|
+
|
|
46
|
+
def darken(self, amount: float = 0.1) -> "RGBColor":
|
|
47
|
+
return self.mix(RGBColor(0, 0, 0), amount)
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
# Conversions
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
def to_hsv(self) -> Tuple[float, float, float]:
|
|
53
|
+
r, g, b = self.r / 255.0, self.g / 255.0, self.b / 255.0
|
|
54
|
+
c_max = max(r, g, b)
|
|
55
|
+
c_min = min(r, g, b)
|
|
56
|
+
delta = c_max - c_min
|
|
57
|
+
|
|
58
|
+
if delta == 0:
|
|
59
|
+
hue = 0.0
|
|
60
|
+
elif c_max == r:
|
|
61
|
+
hue = (60 * ((g - b) / delta)) % 360
|
|
62
|
+
elif c_max == g:
|
|
63
|
+
hue = 60 * (((b - r) / delta) + 2)
|
|
64
|
+
else:
|
|
65
|
+
hue = 60 * (((r - g) / delta) + 4)
|
|
66
|
+
|
|
67
|
+
saturation = 0.0 if c_max == 0 else delta / c_max
|
|
68
|
+
value = c_max
|
|
69
|
+
return hue, saturation, value
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def from_hsv(h: float, s: float, v: float) -> "RGBColor":
|
|
73
|
+
h = h % 360
|
|
74
|
+
s = _clamp(s, 0.0, 1.0)
|
|
75
|
+
v = _clamp(v, 0.0, 1.0)
|
|
76
|
+
|
|
77
|
+
c = v * s
|
|
78
|
+
x = c * (1 - abs(((h / 60.0) % 2) - 1))
|
|
79
|
+
m = v - c
|
|
80
|
+
|
|
81
|
+
if 0 <= h < 60:
|
|
82
|
+
r, g, b = c, x, 0
|
|
83
|
+
elif 60 <= h < 120:
|
|
84
|
+
r, g, b = x, c, 0
|
|
85
|
+
elif 120 <= h < 180:
|
|
86
|
+
r, g, b = 0, c, x
|
|
87
|
+
elif 180 <= h < 240:
|
|
88
|
+
r, g, b = 0, x, c
|
|
89
|
+
elif 240 <= h < 300:
|
|
90
|
+
r, g, b = x, 0, c
|
|
91
|
+
else:
|
|
92
|
+
r, g, b = c, 0, x
|
|
93
|
+
|
|
94
|
+
return RGBColor(int((r + m) * 255), int((g + m) * 255), int((b + m) * 255))
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
def to_ansi(self, *, background: bool = False) -> str:
|
|
98
|
+
code = 48 if background else 38
|
|
99
|
+
return f"\033[{code};2;{self.r};{self.g};{self.b}m"
|
|
100
|
+
|
|
101
|
+
def __str__(self) -> str: # pragma: no cover - human-friendly repr
|
|
102
|
+
return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ColorPalette:
|
|
106
|
+
"""Palette of named colours with simple mixing helpers."""
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
self._base = self._build_base_palette()
|
|
110
|
+
self._custom: Dict[str, RGBColor] = {}
|
|
111
|
+
self._gradients: Dict[str, List[RGBColor]] = {}
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _build_base_palette() -> Dict[str, RGBColor]:
|
|
115
|
+
values = {
|
|
116
|
+
"red": (255, 59, 48),
|
|
117
|
+
"orange": (255, 149, 0),
|
|
118
|
+
"yellow": (255, 204, 0),
|
|
119
|
+
"green": (52, 199, 89),
|
|
120
|
+
"mint": (0, 199, 190),
|
|
121
|
+
"teal": (48, 176, 199),
|
|
122
|
+
"cyan": (50, 173, 230),
|
|
123
|
+
"blue": (0, 122, 255),
|
|
124
|
+
"indigo": (88, 86, 214),
|
|
125
|
+
"purple": (175, 82, 222),
|
|
126
|
+
"pink": (255, 45, 85),
|
|
127
|
+
"brown": (162, 132, 94),
|
|
128
|
+
"white": (255, 255, 255),
|
|
129
|
+
"gray": (142, 142, 147),
|
|
130
|
+
"gray2": (174, 174, 178),
|
|
131
|
+
"gray3": (199, 199, 204),
|
|
132
|
+
"gray4": (209, 209, 214),
|
|
133
|
+
"gray5": (229, 229, 234),
|
|
134
|
+
"gray6": (242, 242, 247),
|
|
135
|
+
"black": (0, 0, 0),
|
|
136
|
+
}
|
|
137
|
+
base = {name: RGBColor(*rgb) for name, rgb in values.items()}
|
|
138
|
+
for name, colour in list(base.items()):
|
|
139
|
+
base[f"light_{name}"] = colour.lighten(0.3)
|
|
140
|
+
base[f"dark_{name}"] = colour.darken(0.3)
|
|
141
|
+
return base
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
def get(self, name: str) -> RGBColor:
|
|
145
|
+
if name in self._base:
|
|
146
|
+
return self._base[name]
|
|
147
|
+
if name in self._custom:
|
|
148
|
+
return self._custom[name]
|
|
149
|
+
raise KeyError(f"unknown colour '{name}'")
|
|
150
|
+
|
|
151
|
+
def define(self, name: str, colour: RGBColor) -> None:
|
|
152
|
+
self._custom[name] = colour
|
|
153
|
+
|
|
154
|
+
def mix(self, colour_a: str, colour_b: str, ratio: float = 0.5) -> RGBColor:
|
|
155
|
+
colour = self.get(colour_a).mix(self.get(colour_b), ratio)
|
|
156
|
+
return colour
|
|
157
|
+
|
|
158
|
+
def gradient(self, start: str, end: str, steps: int, *, name: str | None = None) -> List[RGBColor]:
|
|
159
|
+
steps = max(2, steps)
|
|
160
|
+
start_colour = self.get(start)
|
|
161
|
+
end_colour = self.get(end)
|
|
162
|
+
values = [start_colour.mix(end_colour, i / (steps - 1)) for i in range(steps)]
|
|
163
|
+
if name:
|
|
164
|
+
self._gradients[name] = values
|
|
165
|
+
return values
|
|
166
|
+
|
|
167
|
+
def list_names(self) -> Iterable[str]:
|
|
168
|
+
yield from self._base.keys()
|
|
169
|
+
yield from self._custom.keys()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Theme:
|
|
173
|
+
"""Collection of palette-derived colours."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, name: str, palette: ColorPalette) -> None:
|
|
176
|
+
self.name = name
|
|
177
|
+
self._palette = palette
|
|
178
|
+
self._colours: Dict[str, RGBColor] = {}
|
|
179
|
+
|
|
180
|
+
def set_colour(self, key: str, colour_name: str) -> None:
|
|
181
|
+
self._colours[key] = self._palette.get(colour_name)
|
|
182
|
+
|
|
183
|
+
def derive_from(self, colour_name: str) -> None:
|
|
184
|
+
base = self._palette.get(colour_name)
|
|
185
|
+
self._colours.update(
|
|
186
|
+
{
|
|
187
|
+
"primary": base,
|
|
188
|
+
"primary_light": base.lighten(0.2),
|
|
189
|
+
"primary_dark": base.darken(0.2),
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def get(self, key: str, default: str | None = None) -> RGBColor:
|
|
194
|
+
if key in self._colours:
|
|
195
|
+
return self._colours[key]
|
|
196
|
+
if default:
|
|
197
|
+
return self._palette.get(default)
|
|
198
|
+
return self._palette.get("black")
|
|
199
|
+
|
|
200
|
+
def snapshot(self) -> Dict[str, str]:
|
|
201
|
+
return {key: str(colour) for key, colour in self._colours.items()}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Utility helpers for higher-level graphics composition.
|
|
2
|
+
|
|
3
|
+
The original module exposed a number of free-form helpers. For now we provide
|
|
4
|
+
lightweight primitives that are sufficient for interpreter driven demos while
|
|
5
|
+
keeping the door open for future expansion.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Iterable, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def merge_layers(layers: Iterable[List[str]]) -> List[str]:
|
|
13
|
+
"""Merge multiple ASCII layers, preferring non-space characters."""
|
|
14
|
+
merged: List[str] = []
|
|
15
|
+
for layer in layers:
|
|
16
|
+
if not merged:
|
|
17
|
+
merged = list(layer)
|
|
18
|
+
continue
|
|
19
|
+
merged = [
|
|
20
|
+
"".join(c2 if c2 != " " else c1 for c1, c2 in zip(row1, row2))
|
|
21
|
+
for row1, row2 in zip(merged, layer)
|
|
22
|
+
]
|
|
23
|
+
return merged
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def frame(text: str, *, padding: int = 1) -> str:
|
|
27
|
+
lines = text.splitlines() or [""]
|
|
28
|
+
width = max(len(line) for line in lines)
|
|
29
|
+
padded = [" " * padding + line.ljust(width) + " " * padding for line in lines]
|
|
30
|
+
horizontal = "─" * (width + padding * 2)
|
|
31
|
+
return "\n".join([f"┌{horizontal}┐", *[f"│{line}│" for line in padded], f"└{horizontal}┘"])
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Screen/component hierarchy used by the renderer backend.
|
|
2
|
+
|
|
3
|
+
The API mirrors the original ``renderer.layout`` module but embraces dataclasses
|
|
4
|
+
and type hints. Rendering still produces ASCII output so existing snapshot
|
|
5
|
+
based tests continue to work.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, Iterable, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ScreenComponent:
|
|
15
|
+
name: str
|
|
16
|
+
properties: Dict[str, object] = field(default_factory=dict)
|
|
17
|
+
children: List["ScreenComponent"] = field(default_factory=list)
|
|
18
|
+
parent: Optional["ScreenComponent"] = None
|
|
19
|
+
type: str = "component"
|
|
20
|
+
|
|
21
|
+
def add_child(self, component: "ScreenComponent") -> None:
|
|
22
|
+
component.parent = self
|
|
23
|
+
self.children.append(component)
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
def get(self, key: str, default: object | None = None) -> object | None:
|
|
27
|
+
if key in self.properties:
|
|
28
|
+
return self.properties[key]
|
|
29
|
+
if self.parent:
|
|
30
|
+
return self.parent.get(key, default)
|
|
31
|
+
return default
|
|
32
|
+
|
|
33
|
+
def clone(self, name: Optional[str] = None) -> "ScreenComponent":
|
|
34
|
+
copied = type(self)(name or self.name, **dict(self.properties))
|
|
35
|
+
for child in self.children:
|
|
36
|
+
copied.add_child(child.clone())
|
|
37
|
+
return copied
|
|
38
|
+
|
|
39
|
+
# Rendering ---------------------------------------------------------
|
|
40
|
+
def render(self, width: int, height: int) -> List[str]: # pragma: no cover - overridden
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Screen(ScreenComponent):
|
|
46
|
+
type: str = "screen"
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
self.properties.setdefault("width", 80)
|
|
50
|
+
self.properties.setdefault("height", 24)
|
|
51
|
+
self.properties.setdefault("border", True)
|
|
52
|
+
self.properties.setdefault("title", "")
|
|
53
|
+
|
|
54
|
+
def render(self, width: int, height: int) -> List[str]:
|
|
55
|
+
screen_width = int(self.get("width", width))
|
|
56
|
+
screen_height = int(self.get("height", height))
|
|
57
|
+
show_border = bool(self.get("border", True))
|
|
58
|
+
|
|
59
|
+
canvas = [[" " for _ in range(screen_width)] for _ in range(screen_height)]
|
|
60
|
+
|
|
61
|
+
if show_border:
|
|
62
|
+
_draw_border(canvas)
|
|
63
|
+
|
|
64
|
+
title = str(self.get("title", ""))
|
|
65
|
+
if title:
|
|
66
|
+
_draw_title(canvas, title)
|
|
67
|
+
|
|
68
|
+
for child in self.children:
|
|
69
|
+
child_canvas = child.render(screen_width - 4, screen_height - 4)
|
|
70
|
+
origin_x = int(child.get("x", 2))
|
|
71
|
+
origin_y = int(child.get("y", 2))
|
|
72
|
+
_blit(canvas, child_canvas, origin_x, origin_y)
|
|
73
|
+
|
|
74
|
+
return ["".join(row) for row in canvas]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Button(ScreenComponent):
|
|
79
|
+
type: str = "button"
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
defaults = {
|
|
83
|
+
"text": "Button",
|
|
84
|
+
"width": 14,
|
|
85
|
+
"height": 3,
|
|
86
|
+
"enabled": True,
|
|
87
|
+
"x": 0,
|
|
88
|
+
"y": 0,
|
|
89
|
+
}
|
|
90
|
+
for key, value in defaults.items():
|
|
91
|
+
self.properties.setdefault(key, value)
|
|
92
|
+
|
|
93
|
+
def render(self, width: int, height: int) -> List[str]:
|
|
94
|
+
w = max(3, int(self.get("width", 14)))
|
|
95
|
+
h = max(3, int(self.get("height", 3)))
|
|
96
|
+
text = str(self.get("text", "Button"))[: w - 2]
|
|
97
|
+
enabled = bool(self.get("enabled", True))
|
|
98
|
+
border = ("┌", "┐", "└", "┘", "─", "│") if enabled else ("+", "+", "+", "+", "-", "|")
|
|
99
|
+
|
|
100
|
+
lines = [border[0] + border[4] * (w - 2) + border[1]]
|
|
101
|
+
lines.append(border[5] + text.center(w - 2) + border[5])
|
|
102
|
+
lines.append(border[2] + border[4] * (w - 2) + border[3])
|
|
103
|
+
while len(lines) < h:
|
|
104
|
+
lines.append(" " * w)
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class TextBox(ScreenComponent):
|
|
110
|
+
type: str = "textbox"
|
|
111
|
+
|
|
112
|
+
def __post_init__(self) -> None:
|
|
113
|
+
defaults = {
|
|
114
|
+
"text": "",
|
|
115
|
+
"placeholder": "",
|
|
116
|
+
"width": 20,
|
|
117
|
+
"height": 3,
|
|
118
|
+
"x": 0,
|
|
119
|
+
"y": 0,
|
|
120
|
+
}
|
|
121
|
+
for key, value in defaults.items():
|
|
122
|
+
self.properties.setdefault(key, value)
|
|
123
|
+
|
|
124
|
+
def render(self, width: int, height: int) -> List[str]:
|
|
125
|
+
w = max(4, int(self.get("width", 20)))
|
|
126
|
+
h = max(3, int(self.get("height", 3)))
|
|
127
|
+
text = str(self.get("text", "")) or str(self.get("placeholder", ""))
|
|
128
|
+
if len(text) > w - 4:
|
|
129
|
+
text = f"{text[: w - 7]}..."
|
|
130
|
+
|
|
131
|
+
lines = ["┌" + "─" * (w - 2) + "┐"]
|
|
132
|
+
lines.append("│ " + text.ljust(w - 4) + " │")
|
|
133
|
+
lines.append("└" + "─" * (w - 2) + "┘")
|
|
134
|
+
while len(lines) < h:
|
|
135
|
+
lines.append(" " * w)
|
|
136
|
+
return lines
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class Label(ScreenComponent):
|
|
141
|
+
type: str = "label"
|
|
142
|
+
|
|
143
|
+
def __post_init__(self) -> None:
|
|
144
|
+
self.properties.setdefault("text", "")
|
|
145
|
+
self.properties.setdefault("x", 0)
|
|
146
|
+
self.properties.setdefault("y", 0)
|
|
147
|
+
|
|
148
|
+
def render(self, width: int, height: int) -> List[str]:
|
|
149
|
+
return [str(self.get("text", ""))]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ScreenRegistry:
|
|
153
|
+
def __init__(self) -> None:
|
|
154
|
+
self._screens: Dict[str, Screen] = {}
|
|
155
|
+
self._components: Dict[str, ScreenComponent] = {}
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
def register_screen(self, screen: Screen) -> None:
|
|
159
|
+
self._screens[screen.name] = screen
|
|
160
|
+
|
|
161
|
+
def register_component(self, component: ScreenComponent) -> None:
|
|
162
|
+
self._components[component.name] = component
|
|
163
|
+
|
|
164
|
+
def get_screen(self, name: str) -> Screen:
|
|
165
|
+
if name not in self._screens:
|
|
166
|
+
raise KeyError(f"screen '{name}' not defined")
|
|
167
|
+
return self._screens[name]
|
|
168
|
+
|
|
169
|
+
def get_component(self, name: str) -> ScreenComponent:
|
|
170
|
+
if name not in self._components:
|
|
171
|
+
raise KeyError(f"component '{name}' not defined")
|
|
172
|
+
return self._components[name]
|
|
173
|
+
|
|
174
|
+
def clone_screen(self, name: str) -> Screen:
|
|
175
|
+
return self.get_screen(name).clone()
|
|
176
|
+
|
|
177
|
+
def list_screens(self) -> Iterable[str]:
|
|
178
|
+
return list(self._screens.keys())
|
|
179
|
+
|
|
180
|
+
def list_components(self) -> Iterable[str]:
|
|
181
|
+
return list(self._components.keys())
|
|
182
|
+
|
|
183
|
+
def snapshot(self) -> Dict[str, Screen]:
|
|
184
|
+
return dict(self._screens)
|
|
185
|
+
|
|
186
|
+
def component_snapshot(self) -> Dict[str, ScreenComponent]:
|
|
187
|
+
return dict(self._components)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Internal helpers ------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _draw_border(canvas: List[List[str]]) -> None:
|
|
193
|
+
width = len(canvas[0])
|
|
194
|
+
height = len(canvas)
|
|
195
|
+
canvas[0][0] = "╭"
|
|
196
|
+
canvas[0][width - 1] = "╮"
|
|
197
|
+
canvas[height - 1][0] = "╰"
|
|
198
|
+
canvas[height - 1][width - 1] = "╯"
|
|
199
|
+
for x in range(1, width - 1):
|
|
200
|
+
canvas[0][x] = "─"
|
|
201
|
+
canvas[height - 1][x] = "─"
|
|
202
|
+
for y in range(1, height - 1):
|
|
203
|
+
canvas[y][0] = "│"
|
|
204
|
+
canvas[y][width - 1] = "│"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _draw_title(canvas: List[List[str]], title: str) -> None:
|
|
208
|
+
width = len(canvas[0])
|
|
209
|
+
title = f" {title} "
|
|
210
|
+
title = title if len(title) < width - 2 else title[: width - 5] + "..."
|
|
211
|
+
offset = max(1, (width - len(title)) // 2)
|
|
212
|
+
for i, ch in enumerate(title):
|
|
213
|
+
canvas[0][offset + i] = ch
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _blit(canvas: List[List[str]], child: List[str], x: int, y: int) -> None:
|
|
217
|
+
for row, line in enumerate(child):
|
|
218
|
+
for col, ch in enumerate(line):
|
|
219
|
+
target_y = y + row
|
|
220
|
+
target_x = x + col
|
|
221
|
+
if 0 <= target_y < len(canvas) and 0 <= target_x < len(canvas[0]) and ch != " ":
|
|
222
|
+
canvas[target_y][target_x] = ch
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""High-level renderer facade used by the backend."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from .layout import Button, Label, Screen, ScreenComponent, ScreenRegistry, TextBox
|
|
7
|
+
from .painter import AsciiPainter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_COMPONENT_TYPES: Dict[str, type[ScreenComponent]] = {
|
|
11
|
+
"button": Button,
|
|
12
|
+
"textbox": TextBox,
|
|
13
|
+
"label": Label,
|
|
14
|
+
"screen": Screen,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ZexusScreenRenderer:
|
|
19
|
+
def __init__(self, registry: ScreenRegistry | None = None) -> None:
|
|
20
|
+
self.registry = registry or ScreenRegistry()
|
|
21
|
+
self.painter = AsciiPainter()
|
|
22
|
+
self._install_default_components()
|
|
23
|
+
|
|
24
|
+
def _install_default_components(self) -> None:
|
|
25
|
+
if "primary_button" not in self.registry.list_components():
|
|
26
|
+
self.define_component("primary_button", "button", text="Submit", color="blue")
|
|
27
|
+
if "secondary_button" not in self.registry.list_components():
|
|
28
|
+
self.define_component("secondary_button", "button", text="Cancel", color="gray")
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
def define_screen(self, name: str, **properties) -> Screen:
|
|
32
|
+
screen = Screen(name)
|
|
33
|
+
if properties:
|
|
34
|
+
screen.properties.update(properties)
|
|
35
|
+
self.registry.register_screen(screen)
|
|
36
|
+
return screen
|
|
37
|
+
|
|
38
|
+
def define_component(self, name: str, component_type: str | None = None, **properties) -> ScreenComponent:
|
|
39
|
+
if component_type is None:
|
|
40
|
+
component_type = str(properties.pop("type", "label"))
|
|
41
|
+
if component_type not in _COMPONENT_TYPES:
|
|
42
|
+
raise ValueError(f"unknown component type '{component_type}'")
|
|
43
|
+
component = _COMPONENT_TYPES[component_type](name)
|
|
44
|
+
if properties:
|
|
45
|
+
component.properties.update(properties)
|
|
46
|
+
self.registry.register_component(component)
|
|
47
|
+
return component
|
|
48
|
+
|
|
49
|
+
def add_to_screen(self, screen_name: str, component_name: str, **overrides) -> None:
|
|
50
|
+
screen = self.registry.get_screen(screen_name)
|
|
51
|
+
component = self.registry.get_component(component_name)
|
|
52
|
+
clone = component.clone()
|
|
53
|
+
clone.properties.update(overrides)
|
|
54
|
+
screen.add_child(clone)
|
|
55
|
+
|
|
56
|
+
def render_screen(self, screen_name: str, **overrides) -> str:
|
|
57
|
+
screen = self.registry.clone_screen(screen_name)
|
|
58
|
+
screen.properties.update(overrides)
|
|
59
|
+
|
|
60
|
+
width = int(screen.get("width", 80))
|
|
61
|
+
height = int(screen.get("height", 24))
|
|
62
|
+
self.painter.init_screen(width, height)
|
|
63
|
+
|
|
64
|
+
rendered_lines = screen.render(width, height)
|
|
65
|
+
self.painter.draw_lines(rendered_lines, 0, 0)
|
|
66
|
+
return self.painter.render()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""ASCII painter used by ``main_renderer``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AsciiPainter:
|
|
10
|
+
width: int = 0
|
|
11
|
+
height: int = 0
|
|
12
|
+
buffer: List[List[str]] = field(default_factory=list)
|
|
13
|
+
|
|
14
|
+
def init_screen(self, width: int, height: int) -> None:
|
|
15
|
+
self.width = width
|
|
16
|
+
self.height = height
|
|
17
|
+
self.buffer = [[" " for _ in range(width)] for _ in range(height)]
|
|
18
|
+
|
|
19
|
+
def draw_lines(self, lines: List[str], x: int, y: int) -> None:
|
|
20
|
+
for row, line in enumerate(lines):
|
|
21
|
+
target_y = y + row
|
|
22
|
+
if not (0 <= target_y < self.height):
|
|
23
|
+
continue
|
|
24
|
+
for col, char in enumerate(line):
|
|
25
|
+
target_x = x + col
|
|
26
|
+
if 0 <= target_x < self.width and char != " ":
|
|
27
|
+
self.buffer[target_y][target_x] = char
|
|
28
|
+
|
|
29
|
+
def render(self) -> str:
|
|
30
|
+
return "\n".join("".join(row) for row in self.buffer)
|