From 6dcb6b693aa897e7201309122b4e96990a2bfd8e Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 14 Feb 2026 09:51:58 -0700 Subject: [PATCH] Add radar scope, splash screen, Star Wars easter egg, and harden TUI New features: - P1 green phosphor radar scope widget on Track screen with LUT-optimized rendering - Splash screen with pre-baked ANSI half-block art from 16colo.rs/Mistigris - Star Wars ASCII animation via telnet (ctrl+w) with IPv6 happy eyeballs and offline fallback - Dark Side / New Hope toast notifications on theme toggle - Kitty terminal detection with cat emoji on splash Robustness (from Apollo code review): - Circuit breaker in track loop after 10 consecutive errors - Input validation for frequency, symbol rate, step size across scan/spectrum/track - Consolidated sys.path manipulation into __init__.py - Radar scope pre-computes dist/angle LUT per pixel on resize Cleanup: - Removed unused imports across lband, monitor, scan, signal_gauge - Moved Pillow/textual-image to optional dev deps (splash uses pre-baked ANSI) - Added 41-test pytest suite covering telnet IAC parsing, radar geometry, splash assets --- tui/pyproject.toml | 14 + tui/scripts/fetch_art.py | 99 ++++ tui/scripts/prebake_splash.py | 124 +++++ tui/src/skywalker_tui/__init__.py | 9 + tui/src/skywalker_tui/app.py | 68 ++- tui/src/skywalker_tui/assets/__init__.py | 0 .../skywalker_tui/assets/splash/CREDITS.md | 13 + .../skywalker_tui/assets/splash/dialtone.ans | 27 ++ .../skywalker_tui/assets/splash/dialtone.jpg | Bin 0 -> 74179 bytes .../assets/splash/prodigy-out-of-space.ans | 40 ++ .../assets/splash/prodigy-out-of-space.jpg | Bin 0 -> 3430 bytes .../assets/splash/seti-satellite.ans | 32 ++ .../assets/splash/seti-satellite.png | Bin 0 -> 2668 bytes .../assets/splash/so-far-away.ans | 32 ++ .../assets/splash/so-far-away.gif | Bin 0 -> 21388 bytes .../assets/splash/space-docker.ans | 32 ++ .../assets/splash/space-docker.gif | Bin 0 -> 37889 bytes tui/src/skywalker_tui/screens/lband.py | 7 +- tui/src/skywalker_tui/screens/monitor.py | 2 +- tui/src/skywalker_tui/screens/scan.py | 4 - tui/src/skywalker_tui/screens/spectrum.py | 30 +- tui/src/skywalker_tui/screens/splash.py | 151 ++++++ tui/src/skywalker_tui/screens/starwars.py | 448 ++++++++++++++++++ tui/src/skywalker_tui/screens/track.py | 184 +++++-- tui/src/skywalker_tui/theme.tcss | 49 ++ tui/src/skywalker_tui/widgets/radar_scope.py | 276 +++++++++++ tui/src/skywalker_tui/widgets/signal_gauge.py | 2 +- tui/src/skywalker_tui/widgets/status_bar.py | 6 - tui/tests/__init__.py | 0 tui/tests/test_radar_scope.py | 192 ++++++++ tui/tests/test_splash.py | 48 ++ tui/tests/test_telnet_stripper.py | 108 +++++ tui/uv.lock | 165 +++++++ 33 files changed, 2076 insertions(+), 86 deletions(-) create mode 100644 tui/scripts/fetch_art.py create mode 100644 tui/scripts/prebake_splash.py create mode 100644 tui/src/skywalker_tui/assets/__init__.py create mode 100644 tui/src/skywalker_tui/assets/splash/CREDITS.md create mode 100644 tui/src/skywalker_tui/assets/splash/dialtone.ans create mode 100644 tui/src/skywalker_tui/assets/splash/dialtone.jpg create mode 100644 tui/src/skywalker_tui/assets/splash/prodigy-out-of-space.ans create mode 100644 tui/src/skywalker_tui/assets/splash/prodigy-out-of-space.jpg create mode 100644 tui/src/skywalker_tui/assets/splash/seti-satellite.ans create mode 100644 tui/src/skywalker_tui/assets/splash/seti-satellite.png create mode 100644 tui/src/skywalker_tui/assets/splash/so-far-away.ans create mode 100644 tui/src/skywalker_tui/assets/splash/so-far-away.gif create mode 100644 tui/src/skywalker_tui/assets/splash/space-docker.ans create mode 100644 tui/src/skywalker_tui/assets/splash/space-docker.gif create mode 100644 tui/src/skywalker_tui/screens/splash.py create mode 100644 tui/src/skywalker_tui/screens/starwars.py create mode 100644 tui/src/skywalker_tui/widgets/radar_scope.py create mode 100644 tui/tests/__init__.py create mode 100644 tui/tests/test_radar_scope.py create mode 100644 tui/tests/test_splash.py create mode 100644 tui/tests/test_telnet_stripper.py diff --git a/tui/pyproject.toml b/tui/pyproject.toml index bd72fb8..db220c0 100644 --- a/tui/pyproject.toml +++ b/tui/pyproject.toml @@ -18,6 +18,20 @@ skywalker-tui = "skywalker_tui.app:main" [tool.hatch.build.targets.wheel] packages = ["src/skywalker_tui"] +artifacts = ["src/skywalker_tui/assets/**"] + +[project.optional-dependencies] +dev = [ + "Pillow>=10.0", +] +test = [ + "pytest>=8.0", + "pytest-asyncio>=0.24", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" [tool.ruff] target-version = "py311" diff --git a/tui/scripts/fetch_art.py b/tui/scripts/fetch_art.py new file mode 100644 index 0000000..f144244 --- /dev/null +++ b/tui/scripts/fetch_art.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Download splash art from 16colo.rs for SkyWalker-1 TUI. + +Fetches pixel/teletext art from Mistigris packs on 16colo.rs and saves +them as bundled assets for the splash screen. + +Usage: + python scripts/fetch_art.py +""" + +import urllib.request +import sys +from pathlib import Path + +ASSETS_DIR = Path(__file__).resolve().parent.parent / "src" / "skywalker_tui" / "assets" / "splash" + +ART_SOURCES = [ + { + "url": "https://16colo.rs/pack/mist0717/raw/ILLARTERATE-SETI-100_02_SATELLITE.PNG", + "filename": "seti-satellite.png", + "artist": "Illarterate", + "title": "S.E.T.I. Satellite", + "pack": "mist0717", + }, + { + "url": "https://16colo.rs/pack/mist1119/raw/192.168.10.13-DIALTONE.JPG", + "filename": "dialtone.jpg", + "artist": "192.168.10.13", + "title": "Dialtone", + "pack": "mist1119", + }, + { + "url": "https://16colo.rs/pack/mist0523/raw/BLIPPYPIXEL-SO_FAR_AWAY.GIF", + "filename": "so-far-away.gif", + "artist": "Blippypixel", + "title": "So Far Away", + "pack": "mist0523", + }, + { + "url": "https://16colo.rs/pack/mist0121/raw/JELLICA_JAKE-PRODIGY.JPG", + "filename": "prodigy-out-of-space.jpg", + "artist": "Jellica Jake", + "title": "Prodigy / Out of Space", + "pack": "mist0121", + }, + { + "url": "https://16colo.rs/pack/mist1120/raw/BLIPPYPIXEL-SPACE_DOCKER.GIF", + "filename": "space-docker.gif", + "artist": "Blippypixel", + "title": "Space Docker", + "pack": "mist1120", + }, +] + + +def fetch_all(): + ASSETS_DIR.mkdir(parents=True, exist_ok=True) + downloaded = 0 + + for art in ART_SOURCES: + dest = ASSETS_DIR / art["filename"] + if dest.exists(): + print(f" exists: {art['filename']}", file=sys.stderr) + downloaded += 1 + continue + + print(f" fetch: {art['filename']} from {art['url']}", file=sys.stderr) + try: + req = urllib.request.Request(art["url"], headers={"User-Agent": "SkyWalker-1-TUI/0.1"}) + with urllib.request.urlopen(req, timeout=30) as resp: + data = resp.read() + dest.write_bytes(data) + print(f" {len(data):,} bytes -> {dest.name}", file=sys.stderr) + downloaded += 1 + except Exception as e: + print(f" FAIL: {art['filename']}: {e}", file=sys.stderr) + + # Write CREDITS.md + credits = ASSETS_DIR / "CREDITS.md" + lines = ["# Splash Art Credits\n", ""] + lines.append("All artwork sourced from [16colo.rs](https://16colo.rs) /") + lines.append("[Mistigris](https://mistigris.com) art packs.\n") + lines.append("| File | Artist | Title | Pack |") + lines.append("|------|--------|-------|------|") + for art in ART_SOURCES: + pack_url = f"https://16colo.rs/pack/{art['pack']}" + lines.append( + f"| `{art['filename']}` | {art['artist']} " + f"| {art['title']} | [{art['pack']}]({pack_url}) |" + ) + lines.append("") + credits.write_text("\n".join(lines)) + + print(f"\n {downloaded}/{len(ART_SOURCES)} images downloaded to {ASSETS_DIR}", file=sys.stderr) + return downloaded + + +if __name__ == "__main__": + fetch_all() diff --git a/tui/scripts/prebake_splash.py b/tui/scripts/prebake_splash.py new file mode 100644 index 0000000..1217194 --- /dev/null +++ b/tui/scripts/prebake_splash.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Pre-render splash art as ANSI half-block text for instant display. + +Converts source images (PNG/JPG/GIF) into .ans files using Unicode half-block +characters with 24-bit ANSI color. This eliminates the runtime Pillow decode +and textual-image protocol detection, making splash display instant. + +Each terminal cell renders two vertical pixels using the upper-half-block +character (U+2580 '▀') with fg=top_pixel, bg=bottom_pixel for 2x vertical +resolution. + +Usage: + python scripts/prebake_splash.py [--width 80] +""" + +import argparse +from pathlib import Path + +from PIL import Image + +ASSETS_DIR = Path(__file__).resolve().parent.parent / "src" / "skywalker_tui" / "assets" / "splash" + +# Source images to pre-bake +SOURCE_FILES = [ + "seti-satellite.png", + "dialtone.jpg", + "so-far-away.gif", + "prodigy-out-of-space.jpg", + "space-docker.gif", +] + + +def image_to_ansi(img_path: Path, width: int = 80) -> str: + """Convert an image to ANSI half-block art. + + Uses ▀ (upper half block) with fg=top pixel, bg=bottom pixel to get + 2x vertical resolution. Emits 24-bit ANSI color escape sequences, + optimized to only change fg/bg when the color actually changes. + """ + img = Image.open(img_path) + + # Use first frame for animated GIFs + if hasattr(img, "n_frames") and img.n_frames > 1: + img.seek(0) + + img = img.convert("RGBA") + + # Resize maintaining aspect ratio, height rounded to even for half-blocks + ratio = img.height / img.width + pixel_height = int(width * ratio) + pixel_height += pixel_height % 2 # ensure even + img = img.resize((width, pixel_height), Image.LANCZOS) + + lines = [] + for y in range(0, pixel_height, 2): + chunks = [] + prev_fg = None + prev_bg = None + + for x in range(width): + tr, tg, tb, ta = img.getpixel((x, y)) + if y + 1 < pixel_height: + br, bg, bb, ba = img.getpixel((x, y + 1)) + else: + br, bg, bb, ba = 0, 0, 0, 0 + + # Treat near-transparent pixels as black + if ta < 128: + tr, tg, tb = 0, 0, 0 + if ba < 128: + br, bg, bb = 0, 0, 0 + + fg = (tr, tg, tb) + bk = (br, bg, bb) + + # Only emit escape codes when color changes + codes = [] + if fg != prev_fg: + codes.append(f"\033[38;2;{fg[0]};{fg[1]};{fg[2]}m") + prev_fg = fg + if bk != prev_bg: + codes.append(f"\033[48;2;{bk[0]};{bk[1]};{bk[2]}m") + prev_bg = bk + + chunks.append("".join(codes) + "\u2580") + + lines.append("".join(chunks) + "\033[0m") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Pre-bake splash art as ANSI half-block text") + parser.add_argument("--width", type=int, default=80, help="Output width in columns (default: 80)") + args = parser.parse_args() + + if not ASSETS_DIR.exists(): + print(f"Assets directory not found: {ASSETS_DIR}") + return + + for filename in SOURCE_FILES: + src = ASSETS_DIR / filename + if not src.exists(): + print(f" SKIP {filename} (not found)") + continue + + stem = src.stem + dst = ASSETS_DIR / f"{stem}.ans" + + print(f" BAKE {filename} -> {stem}.ans ({args.width} cols) ...", end=" ", flush=True) + ansi_text = image_to_ansi(src, width=args.width) + dst.write_text(ansi_text) + + # Stats + src_kb = src.stat().st_size / 1024 + dst_kb = dst.stat().st_size / 1024 + line_count = ansi_text.count("\n") + 1 + print(f"{src_kb:.1f}KB -> {dst_kb:.1f}KB ({line_count} lines)") + + print("\nDone. Pre-baked .ans files are ready for instant splash display.") + + +if __name__ == "__main__": + main() diff --git a/tui/src/skywalker_tui/__init__.py b/tui/src/skywalker_tui/__init__.py index 83dd68a..11f52c0 100644 --- a/tui/src/skywalker_tui/__init__.py +++ b/tui/src/skywalker_tui/__init__.py @@ -1,3 +1,12 @@ """Textual TUI for Genpix SkyWalker-1 DVB-S receiver.""" +import sys +from pathlib import Path + __version__ = "0.1.0" + +# Ensure skywalker_lib (in sibling tools/ directory) is importable. +# Consolidated here so app.py and status_bar.py don't duplicate this. +_tools_dir = Path(__file__).resolve().parent.parent.parent.parent / "tools" +if _tools_dir.is_dir() and str(_tools_dir) not in sys.path: + sys.path.insert(0, str(_tools_dir)) diff --git a/tui/src/skywalker_tui/app.py b/tui/src/skywalker_tui/app.py index b5da6d1..74d9c45 100644 --- a/tui/src/skywalker_tui/app.py +++ b/tui/src/skywalker_tui/app.py @@ -9,13 +9,6 @@ with Textual's built-in App.mode / _current_mode / _screen_stacks system. import argparse import sys -import os -from pathlib import Path - -# Add tools directory to path for skywalker_lib import -_tools_dir = str(Path(__file__).resolve().parent.parent.parent.parent / "tools") -if _tools_dir not in sys.path: - sys.path.insert(0, _tools_dir) from textual.app import App, ComposeResult from textual.binding import Binding @@ -57,14 +50,17 @@ class SkyWalkerApp(App): Binding("f5", "rf_mode('track')", "Track", show=True), Binding("q", "quit", "Quit", show=True), Binding("d", "toggle_dark", "Theme", show=True), + Binding("ctrl+w", "starwars", "Star Wars", show=False), ] - def __init__(self, bridge: USBBridge, initial_mode: str = "spectrum"): + def __init__(self, bridge: USBBridge, initial_mode: str = "spectrum", + show_splash: bool = True): super().__init__() self._bridge = bridge self._initial_rf_mode = initial_mode self._active_rf_mode = initial_mode self._rf_screens: dict[str, object] = {} + self._show_splash = show_splash def compose(self) -> ComposeResult: yield Header() @@ -81,18 +77,35 @@ class SkyWalkerApp(App): yield Footer() def on_mount(self) -> None: - # Initialize status bar + # Initialize status bar (lightweight) status = self.query_one(DeviceStatusBar) status.update_status(self._bridge) - # Install all mode screens into the content switcher + if self._show_splash: + # Push splash FIRST, then init mode screens behind it. + # Two-tick chain: tick 1 = splash renders, tick 2 = heavy work. + self.call_later(self._push_splash) + else: + self.call_later(self._init_mode_screens) + + def _push_splash(self) -> None: + """Push splash screen, then defer heavy mode screen init.""" + from skywalker_tui.screens.splash import SplashScreen + try: + self.push_screen(SplashScreen()) + except Exception: + pass + # Mode screens mount behind the splash overlay — pre-baked ANSI art + # renders instantly so no delay needed before heavy work starts + self.call_later(self._init_mode_screens) + + def _init_mode_screens(self) -> None: + """Mount all 5 mode screens into the content switcher.""" switcher = self.query_one("#content-area", ContentSwitcher) for mode_key, (_label, cls) in MODES.items(): screen = cls(self._bridge, id=f"screen-{mode_key}") self._rf_screens[mode_key] = screen switcher.mount(screen) - - # Activate initial mode self.action_rf_mode(self._initial_rf_mode) def action_rf_mode(self, mode: str) -> None: @@ -110,7 +123,7 @@ class SkyWalkerApp(App): btn.remove_class("-active") self.query_one(f"#btn-{mode}", Button).add_class("-active") - self.sub_title = f"DVB-S RF Tool — {MODES[mode][0]}" + self.sub_title = f"DVB-S RF Tool \u2014 {MODES[mode][0]}" def on_button_pressed(self, event: Button.Pressed) -> None: """Handle sidebar mode button clicks.""" @@ -122,6 +135,25 @@ class SkyWalkerApp(App): def action_toggle_dark(self) -> None: self.dark = not self.dark + if self.dark: + self.notify( + "Welcome to the Dark Side.", + title="The Force is strong with this one", + severity="warning", + timeout=4, + ) + else: + self.notify( + "The Force awakens.", + title="A New Hope", + severity="information", + timeout=4, + ) + + def action_starwars(self) -> None: + """Easter egg: stream ASCII Star Wars from telnet.""" + from skywalker_tui.screens.starwars import StarWarsScreen + self.push_screen(StarWarsScreen()) def main(): @@ -133,6 +165,10 @@ def main(): "--demo", action="store_true", help="Use synthetic signal data (no hardware required)", ) + parser.add_argument( + "--no-splash", action="store_true", + help="Skip the splash screen on startup", + ) parser.add_argument( "mode", nargs="?", default="spectrum", choices=list(MODES.keys()), @@ -158,7 +194,11 @@ def main(): print("Use --demo for synthetic signal data.", file=sys.stderr) sys.exit(1) - app = SkyWalkerApp(bridge=bridge, initial_mode=args.mode) + app = SkyWalkerApp( + bridge=bridge, + initial_mode=args.mode, + show_splash=not args.no_splash, + ) try: app.run() finally: diff --git a/tui/src/skywalker_tui/assets/__init__.py b/tui/src/skywalker_tui/assets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tui/src/skywalker_tui/assets/splash/CREDITS.md b/tui/src/skywalker_tui/assets/splash/CREDITS.md new file mode 100644 index 0000000..dc89688 --- /dev/null +++ b/tui/src/skywalker_tui/assets/splash/CREDITS.md @@ -0,0 +1,13 @@ +# Splash Art Credits + + +All artwork sourced from [16colo.rs](https://16colo.rs) / +[Mistigris](https://mistigris.com) art packs. + +| File | Artist | Title | Pack | +|------|--------|-------|------| +| `seti-satellite.png` | Illarterate | S.E.T.I. Satellite | [mist0717](https://16colo.rs/pack/mist0717) | +| `dialtone.jpg` | 192.168.10.13 | Dialtone | [mist1119](https://16colo.rs/pack/mist1119) | +| `so-far-away.gif` | Blippypixel | So Far Away | [mist0523](https://16colo.rs/pack/mist0523) | +| `prodigy-out-of-space.jpg` | Jellica Jake | Prodigy / Out of Space | [mist0121](https://16colo.rs/pack/mist0121) | +| `space-docker.gif` | Blippypixel | Space Docker | [mist1120](https://16colo.rs/pack/mist1120) | diff --git a/tui/src/skywalker_tui/assets/splash/dialtone.ans b/tui/src/skywalker_tui/assets/splash/dialtone.ans new file mode 100644 index 0000000..f1b9aae --- /dev/null +++ b/tui/src/skywalker_tui/assets/splash/dialtone.ans @@ -0,0 +1,27 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \ No newline at end of file diff --git a/tui/src/skywalker_tui/assets/splash/dialtone.jpg b/tui/src/skywalker_tui/assets/splash/dialtone.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5f0d851a0ccbf88810174db54060522037d38d0c GIT binary patch literal 74179 zcmbrkby!@_vM9O+8Jyrwf&~i@+}+&=4HBFHgF|o#E&+lBhu|={4h)u{!DWI6_aQ)n zB@jp+zrFW)=iK|gcmKK7>+4>trK-DHYE@PLUHZET5UDGxDFYx70Dv9>@OKr60&xEY zc=-5ukBA6~9#N2yl9Ev{QPDgECIN=03=fY0w+I&pmk76>tf;80o~4euj-{u)y=O{X zN>|r!(Elxhzx@C)HrgzPC_0Ds9Tpk=7j^>5Velg<5|XbB1NWyuiy6@SZi_S zv}y6f5K=Nx)-xYLkt>4z>CTaECk3)c1T#y*%xW1Cj2{`?c!`D+iR&)ZiQar7nZX9( z1GMx;1(pCXmO8ut(H4{uGonh5?$*?33u4G*y1+_t|G%7q)GS}*Z6y`8_6)@WT4QE*FARn2(EkPG^NOV^ZVmqymQYfA#iV)KC4 z%n*muGdI@22V7ZvTUziEWRfZ76S5?aie;-hHjTs@f3-3JFCpQ1DcvbC8ip|T19$c! zp*5z9l?Q+|ul2A#Em(r%v#}NXnwSr&{+0|8KlMYeS4^=}0jr*ImU_0Hu*gk0pX0!L zD%=f-_~3;PK0@?J{INi{?vIF2N4yUZb}RfL^|>xi?CtLC$d zAm+TXo?mw|!rnuUFy4u}mc=4>jgih&SxO|FDU*jP8`bvW12zD-%|l$4&$q4l!qM6N zL%J2HRx2y+t}nk0EDj*|&pJ^TK8Y*hA&Dag!T5an*M*82heMMLUducd;wi3H0w3-#g=_tKo}jI=W8Uq%3%O)MQVTT!LLb#P9i$nCkyBc3pW(brDY-y@@Mml zDQGqUV;xS9tI2o<41gB|!~o)2h`Ku#U_cG@!EN4Ip@J16zUbCgn7`yAJ>vfans*WK z=YX>2@-gj=Ox_4x(w>jEnDs80fDw_L-?}F}3ZSDM15`2D!>he17#R4rU|=Lj*Ft#f zsL!E8yQ%yPuj*&uO`tKQ6&pPEOF*GA-5<)7Y0|t=fzY6gt)}8?w{aSx1Pn@i5HDbh z3%!;00jL!Kut?wvPoB5gWvsv0Y)IyixQ{!$xH|IyRhUJ4BPDjn6U;x65pDy%mk}o- zpKW0ECFm)Ae`)%GD?t`pc8Rbij0m8$wT=-FbRt~VL1&UY}N*B_V`7HDlypTd# zRzkbKdJi;AyzUsC=2580Kuh<{b?aRRz~(z(0=NDC7p=Gy#O3kTO^ETP^rNEl@8hC5>!c#@GV~#!_ zJNpbvCl|?7hVm?lUo5(Lni*#G__-X+oh-t;SGE4Q9CU~_Pm6gKa4KN2Q*9W%_!v$U z5e^KZgK-FYy6w#=_v`V~vwY0|Vc}Z9AtBXD1v(3qky2ZV- zgNs_x9&IQgM|xMpJUZtGxU43LYW}WeY6S)huYxRytFj?aqb8?)cnvejCc6U)yIQld z(lK^!Gg_nj+TAkRzWn3L&9dl;anbC^{Ds8%2KNt*KI6|#x&&Qh08Y*r15H*%ehC0z zh@jVe`++$>MK-)fw#Mi@wH5Q;W#(4+=;-)xRr3OM_Puk86rTHIp-Upz;v0LaIo)vk?RT^p z>LJ?KSm1W$gZjhzyzJpcCtwsa-;xg8G42o*t8-BNMe6uY$b2l}a-G zi;0&;CrI|BeSNnzL{Ham?+vZM9s-@`VtVc4949}TQHASYOF#Ce;IZrH!U6?XeMnd^ z`dQLdC*=4#!^sHH0O;lApBLht+5JbW(0uuLpyKor39YXaS`1xZ+T8oHA3{8|2$$a< zey$@Fo8Pf3XNX$JspCEy)8t83W>d6dC+#i=$N*r&SkP>BjIiFrZB2rnlx3Hf);AOy+H`U#3$6w~gC_W+6PU0~ z^25=AZN-|o&AYwq{!H?DUklpxFFzL%clEu6P|n55qCDxfZj`u~SjJ~d=fDSB7K8~-75Zz^OJQ=cG58fP zOp5m)1UD?6I25e0FAi)YTNjJCo2R9fBs(2S^wDw@W}E#Il%FOTlB9|$fbqcqARg!& zJTC6RZm(yfX+Y3w7D7r*}emElt{v5(WyRCp*uwT1aJCmE|DuSpgk4tuUWhZDEvLR zP#`7HV0j=UbuVpM{})hE{c-<1|G@L${Ic&kn(nEqcQ5n}XO#`TqI?9l0w@v;@Ff?V zT)ae18Q4No(c=>$=2>L6NxsAfYH23JfwT( zD_rfMP@b3>p!#S%EMlhICrK~4^B}EW8&H9rqv73KxpTL-fxpE%7wGC1y3Rb|Rz`)% zM;-#x=eFeWVjm5NwOct|OXgJ}%3@Jv;lj1Lysn^QOvvS3ZGF9ic*RFs?9_H=DQT!f zu5-m&USMp&4scf6`xbE%^z%~mHsD$MBLT@R#iy!D(PnrpzO4*%niZ^ePYqXNfF)sc zXhX|xv9Gs*53lu-rqt7;EbVo1gsZ!|PK^~C{&?3$OO4lAmv+c+(e8=h&tqsLT8V1f zxjt93D`p3fdefIg@v4CJM_1>!^GEQBT|{S|pm?XO+Z%|`>(!$R+ZWIn6oQE&7h>6) z{QF>eZBHB#ifUwT~PDX2lC{lx>|4h zz+X6YdpBqaodk(zzm}A6Zojp3(b1TnPInjn({dCf?Y`OlZTl&c?_|ZB1d-ty_15+v zPeLZ`go~%3O5=PNU|3if;8eDqAZ zK!UO%C|HIkU_Yab*O-eMV%|C=3nC>d%LsR+`9Mpxw&D1B;_~9s2+h^@aeCRNds6R- zcfK8g5bC4>v$FhAWySSI>9gb2xt;T_KVAYkIr@TyIqR;)sIkW>tPrhd3p{{HmZ2=f?#&V82AagXoll z9AdNz{-|BT;LX7T;A6?Ia$Y^4@Nj*p9&4^KT}k!XcX_%nKi53w*n*))oarYlr+{8Y z0duk#a~C58uKX3=3oYtw{6167HiDDlr^y<1UbaF=ZFaf@1}E8Uz9csU zwDsnVc)%(R%vWh4=ps`SwLKv?ir6UMFwgf75kE@Tx4Swj*vYS_Q+h_FZiYd|I%2pq z19=uLsK}rej!(682yMU7JseG0BmtD^-3Ud4} z0`zhYs^X)PR%K}vz-z+5nRtlKW4mG?94*|{=LoT}haPOgSF--DPlBT`3ym|Ba?NG5cQ7F$&qc1D8Bj;X?=w9zMdu$+)6`x*x>dAaP77m z2l!ADO;^=MzU$?*l!&*vNT73#xcIs{Pi>Ecdzgf$%gCGYlswTihKxg1>+$!)ON{tn zwSZ39k5o%*((5W_e!;r%&$d*{y}(BK0X_`UzI>Xv*s!pDx;Q<2)TYxGxDLG!g~NA( zl{LnbzNn!qfP`cCVbqF|)Mh+NejD!jtZG`3w6v}_sT+)qv`9@-_t}eKRHnOfE2`}( zH1_jx{%W;(h;*31PZcuf@xLwip}xSNzG+s53yfXh!WdJgID0zwDMtNVwQ0n@G?_2S7=* zwwki1zY#e&oVZ|dUt8OkH&t$_!*Bii%|o;DF)vCd_yKu@-Wsw*l0FNY84g99yJuad5hn;ljA zLm>Xz1(e>&!An`5d@h(cnv9HTeE2r5fc4BOT^&;k6+YGQYNg@X?tp7V1WfMmX3K)^ zx20VM0bcWJozucjq#L2wG3IB5_}FuXIGHLryn2- z#~0Ke7GCv*RH_UKwc7$^_k`3wd&e(bjE-$!t-eO60>RN+chdPA=f##pI>UVuuDLP* z2{yaG0Ww*Az#JLHyLCZG@n&B-_{LTcXb0SfF_+W4SPP-fhA}K zS1Y;B90>~yOzq!`Zl@*+B44M<1M)6>X=JSE91LLbH;RKtg1olS)1!@v{#9X|%Zm)D zrTHdAZe^fUvlhZRJ%)N{4~N0$sB7U?be$l z#}}<7R#x288g5R6Q75^e@=2F^(}3mi9*;?E*+UpKk9IBwLsN|qa@Z2cAH@e4##iu# z1LzNvceXE9>e||5<@nUO>Ok*(tmbT#ZQ9eO^ z#P2KlyL#({Y21W`3*0x3ZY6_*t4tl@atcFVPqRme7?%%^Wiwdk`9cCudb>P=Z{Htl z_@P4m94kEE5+H86o^w;1Ve+3XUrV0hpMCc`{?Tm8badt88@|K`hxGaTSB@T(JIlAX z9$W?@6Bd{N+Xh>N(4 z&eFxz;ogmsv}>K0lCh%>B%0U9z<+ja`RM%nprU%Ni$YNrmsjCqOdp7I^}HK{HB%wt zU@eO!gz|ah$HB9Z`dbO#yM>)fF!d2K_GO7LVPVhLm#}*fm;JI*yTdV8QioA+th7{Pp+YX>qmOn^V9@x-?<9DK-+U`(zl+d| z;!oW+ZWL2u;=S_eu?USd{}HM~=ugc;aCuZ`;0A{n`@{Wy$8RMKdus++393&|T-X^y zAPs(b=2LBvlN-e8WyVd*mrdt?0rniTMRQ++AO7^B=u@`>C(fht^BJ9p%iR8^A@7L~ zxJWk+gd4)w-~8Jwe}#j6Gn|jJGBUE`8Vk<SN(R(6 zXsk`|Z=zDf9b_F=v?mcY&tEMxwDh@lpQ^=8Tl{L!{CRnEAST)^io^jm&1w=2ldVCF z3VNq0Be-hdkie@u%O+&_#ASWOesqgVf~)`O*}zb;^qZ}C`Il`|{c~%>xfb)jei9eI z97V^qZlIN63TR6EH}CjU(I2d9!USE^_3yI9JABKXSI+~dmGK4UZslSo^8u-ll!k`4 zyZhsZO+PBg-iUM_b}YyE*28sbDnFbD-)$W<=e*V#Ff!U4?GIUqG@FdBLrpKgR3eg+0sLi5h0%1-(sf2(919S`p_Xo!0o) z5(T|az6-H^70v!bTZakO5szx?Us@HV(D|{)mpc-a+soeTmZnkDx zKr$TkwhOknc=4+yYw?2V^7eV=6c#%*D#W;RqC>JFL-hd@7~gF{LPZ;J47wgce^QMM z6U{Vw_Xhn#S`8O`mTS~pxNg|&tbwIk79BiRS&`rA{C-J4$EK2hh8Iu;9;7C<;hIq) zbQU=^AhFRhX?qpubJHm(I86-zwyFRan4yt>AVLKI46?w3p#kUN0{{r%v?@9hI@}`P zE=*BZR+FVF^;94JRr1guw1?{W08#jZC!`VMA$|ZV7usF0e}fgM&v-UCR0)Ahll@oz zzoQ2|o#Ek)7W81N{s+ST@lk*Pw11ow|IYa*2B87}?V~^=ddOjwHF=+4;ktr2tN-oZ zUPDrobkL|)CPgD~*uoCW?dPplOy$p^nG&?o$etKlzFuz#q6kM&p{;PXmeam!+H;9z z&r14UhM}geHu1DnjAx;>cXEZBVm?Fiz54D_xdvJNU@ldk2*peLD-5Ctrk3M##dO=2 z9(Y#oR5@Jb)9|)uKsDUmrA*woPJiXZR~>0|91#5N(EXVrMyefRnghqt>m+nvWDVI_ z6tVj{G2i4wd)i`d#?Wl23|7;cPo>T^m&r6h1X~Wvc~|G}%#G};ZvJDRpX``3Nz?iP z+Q4Re_`E*afm~zXa8b7y#UD>mO$4&Yz5AFF(oW3NRkN__U$p1>#MiV_s6xEmph&0d z%(5A5`mACcg8pNum3D44TT05ntw_cQt|8I$^JKpMXuA=T-o`PS@Hz6SGb*HTESzDu zj%JYE>zz{aYSrZ6$&a2VPP6B}Av9cwQGbQ@O1Ag=43lm-gVkv((-O$vcwc&&)pXyz zVuG#mMOQy3(@;rC@xAV8x0<~q-#n)-%=H@cuCJ(=XgwS{k}527Z2fG6$z*s{VN#sK zPCjd(E!RALGV6BRcGVnEFW;+8C_EBo@RIzXTqW$QJ8?b{aa_2pW{q%Q3s9}3(-+|! zbW6*<>@zvM2eV2~&&t!XeDnKQbG~kUu?g7>M+2Ku<*+a+o82(FC@RceEE@Uv1X3Kg zdI!%@wovjEh8Ql-tUD`y{tKubf1T7flLHmM#n-LM^yVbvs%Mi>vb*Y{SQ(i*rx>VI z$$Sw^FWw-sLRzqL3tr>2CPLFpMN|pt8W$+&CbXZO|6EwO4m5s{=uWbWYbklgPoLLc zHX^OsE)i{viRAOPsvnyX5cLY&Nr-vwz;KV?5z2?hZ||m5U&EXgvmwWV-nG5!_%bvP zlBoa?a&ob#7kn;D!qvlV+;R@)pMHM+SvG&A1t626*D zZwqk=x+1=*wrQ$1PCcWt2I&UKJaWFH1)`Y9B;RlTLNV}^su=X%@#4g&&Z17oC7@6v zS8Psp(gZIz@80O%f*aow!9v3Bknm#`ZHaG`LATe7=Oph+Y9Zj1rIee2RSstK9O_r3 zy!!h#JNGC}j(n`&CYs3B0AJA(v+Y}7as9Z?vkA!qG?x;m%vh>i#Jx=C+{~gXzfzhU zW~zFwBUm%>N~+tcD4H z-Dc(TXk!L3{DLZAR9OMfHw37A9qL!@UwoCt7q7_sWpmWL-&~5F@oC9KVVGP{V36sV z*xXpa7ut%1Rn=fCfxW|98C!?gXo3C;YBExDzHzxX@{@}D=T`!~Ytw#lJbceGE_ROJ z^!mqN+{^ffIU2mlP{Sut=AA2P9FQJ~F;;#oRD10smb=%Fb5#JKm1UcUXS+?Lx5=W|pft52H&WBg(mm%ec5%x{&G}XmU}o;8(zp`rK+i zuC4JDJkr#FOi3&e%6b8JY4IT9HPj<_q#TAAcV3Dt^&4ljGLdMeJ>_iI9318@`s~9a zoL5jMA*9Bvgu_%W`TS`^Sj}q1XM5A#7*#cXa^3gF*&VF;p+V&#b@tUx!lz7>AjQPk zgQPl#BBdj@sKsnG{M3G`50K{C&o2C9o({{Qet9_0TL(DJ-wRUTZ4f42`UI`HycC*Y zGR5Z?TlJcs|>+H7MB|jp2HzNKiBZczEK* z5b763D+4-)Fh@G#RIe8XZ8aBsx%q+cTsBROL47b%!*x-9Us!5w zZR8O>KiaaGID7!MvveV@YcRWLwpN)whC8Ijn>|f5_KLWsPsMNS!ZRXuC>7h5+wXl_ zY}nLMy}>Nf0@3mMsoy($ZXAc^TkFh^LtC4JYw5heFfmE~RGYSBypH+e$);p$MoUaoTco0r_yYR*j< zi4KKR`Pp}WfuNXj{P54#sr_bu0pE)gY>w519q-N8jD9eZnWY3vkR4``Z@_J<&Br+b zZsD?2H9TtD1LTX!TG0qshR@R`__J~PS{`v~GbLaBcllMRY9FZD3_8~vEbJJy6WKj{ z{qK*0L++jwl&R@|%TJB}r)K)Uh4%mHVgdR;LL04m%pRhZ#*oDz$n+_7I9JQSM9)W8 zFq{2CBAf)oi1zM-f-Hcp@U%>LiFI%|SIu$qI8L4@HI2l1CYq~uTnpRDh0Se=F&xMk zdD0%m!laufpRfV#oy^Mjx+qIf9arx6Sg3wOg|DT#7u(Drn>L}UZe}Wdc9j~TZTVaHhX@v%u5R-n3T|Sea?Z}6|Tk`WOB?M(IP7^Tqey0Qj!=No- z-|?u~o<%)P`vh(Yl_D$@si$7l@+f9x6i$Wkv0z*0=^bKl$mhJfYL}w!4PdHN+jt~t z6Q28E`2a|K5?F(j_|*+KPF&PBGzH!YQN1b*wdj0`LpElMAx<=SG|g5j#IK&geS(d& zc((s%B$iUUtx!66l$FLK!7Z05W(inMF@(rRb2j+$6W$8%{``5f^hKmWAcrk&Mx8Yk zLYTb@(7q^4(&gIXT+b&>u{@Iw?bAugG3~xFb&AhnGRM?Ta6^)6+WokFC$C2esVUah zhG%6dU(iC%1LpX>4ZJa<*=%7Is{ByxxM~9drB~USM)C7Ti61m#6ZVSbIr-XQXNu?; z;n>qNG4U}pd=kzY9~^q?l`X>eTek?z{p2i)cv zAGlSyIm2aB0d32C{S35uMxQcHP?Yyxx83kA{w$NX#FCI8Beth>S~&-HJ9#5{nJVgV z>tNWG1H_b6T!nOTm5l(={TVCAiMOEio4DMtfFl!WiueOs8c8uSw#5hvoHPJg?Z>TO z;uP2C{Nx{Z?Np=}2VoU$s(w^3Am#24GXFq=L27~eQmlHdzSe1!o19oj2n~}EZ+1;! zdMZTL#?eT`V0t@NA+P`ROr&JtI4=S5gxru+-+H zX@`w(mF?QKdJn7f4pc4)ogRHTl9ZvQ7z6;u!Pr+xbjtX%hhw{?*Du~_;LQBkq8I}e6TS{&3zU5gkv*^ z@-+u1rso2`8eXR`M>iq0&E;)CGvP$=iA_B05@_w6o03u! zj}6H`6T%`ppox(K>*+UDEsU*Zq?6WQ?S33DR!g5CaTAn#?0 zN~mL zqN2Zi>bD*iGvn%7McuQ@<536G$6*cl-JW(TRwk))v`~j5Shsws3}r6-dI|W{#8Jnu z7Z)JC%l~8afPRgCk7gI&W(qZVwKLH%@Okj`N)|v12dC}qC*9Z9ElR(V@K;TXFB{}= zH{*FK86Y`vFn7hKT=rmf1e;Ea6w{C=i`~+T4#+BHu#}Z(9?~WSuZI?<81uyP187vA z5_x#;j@IdV3Lr$`tcg!KDi~Nuie#Yq;%BBLIo*ENK!iivRN`JpH8!BhqsQF}0A3c_ zsBrM^gPT45q2S~nz3qPh&U<5#S6&Y)-G2Za4ggpWN4+u;076GY#{{E;(J`?w9~89* zg^d`Uk%Ui=h?GI@B?c3}zGnol!8RFcZ=J=;1JkvgPLCC!ntlfJ*02QobNW_ zf`2^BbHhZrkpvN`}+fT5(KhiLWE@JpT*tKg~J*M+g2tgdZx17cgP13o>iP{$9BM zUwidm#Qzz}{~X59f4ctQ=pn8{zx4TsPzBkA4X&WBC#5; z>HPTzmgmAXZ|dYTsh)%`AJ|_*N5K*Q$G8Ch~9z~Z0jS){$ z<$0KiO=AR;(y9%iz}4hDVi={TtDe>}d7@p<3&`;S3d z3G^4=BG}s|C*w5)HBOa;>=q6MMGaHZhDawhH;pviV!La2YnbZ-uQx90 z9&)KTF8dJ+g89-f5wF$6qS){#OZ7ClUUr^vkVa;&*_%^x`=jG5F8K?*{6%oGN7u@S z}gHzhq(`k*AxOVt8zG* zbOTJJ`Ig%|Ia`B=1FV8ei#>&u#D``1Qxn|W`Tj_!t0jyQh670kbpmeY=RO>fUIDvA z1cVgt*oD64R%cm63K?a}ZO=+2SIw^X5>*)lsoDK%g|JJf<0Uqux=#BY2049}G2Wxw zL`y=!%=3oD4)1Z=HpjR;rtMsR$`JZdipHhWntXNO1EF#kMhA}xh~ry_*G<3cz$6OR z3hx5F^nE$C%W+G6!z0DFNf&hs^TuG-nbikfLrl@RG9{_x5Z`KFo-p}5gC|Mj4?sk^ z(D{5bEXd_RMpaMLPWE#MY}rx`^F6y%Ig$szOJxSSRz@YwztRl0MW%&05|rT45G0U! z##8)>YmHuubBZEe)SlkhR|O?A3h&y+glk&5tC}lW-5QYYMt!;9p7tNLY9p?E%ITz$Y4-y@e~sEf9u0$ zQyh4b{$s{xDdhEN0h!t1!_}>|s^(#GNhLT$rNq0dM4o3TRg0%jaHJPIWAl1VQ0xwA zrlt+WPBS_rfmb}?|8iMX_Kcp0QN zjq5FU!c!jG!7pB_imY3SvB~q4)3IyqsS^#j)Qv1$?%x)EEXF3#46dGtbi@ku9D#&` zm_8Ix1aVLK#l;VW2#@{+JXS=@DheyN%(s(?RQYlFcUb=dXhhCWl}WcU(Q|>9qe4sc zxQ?t9Th7cvafh?tT6PUD+40Am&NwFqbe=9Nv)cP=N-o62&qCazAjFekIBlyYnFqm6 zsi+>IZ&!!6)p{H?ANaMV8L8R6s3j6rpxodDWyR;cyWuP-{q~2|<_t71l~l9B>AzK~ zo!CNu=6EvL)=Xo9?}#dI?c(r$vLpIpJ?>J`t>bgmD&o|3UEs#yFA&-OnYeHo@#*Hr zpU*E3IJOHKMc*yDwmgdMslC&&!#=NkpS}hqK9(Ut-MRG(-iiLw{ZRW9Op>RD^eW{O zV-pUj$hkKAr5BO*W|uZ<>~-&p_exLnb7L!%jJ>sp|I{=R?hKd_EIMEXuR*))a*8rn*YIj$?<@C6%b4O_pEg!biFwT=dzNg%gny^uu@K(;_bEnw> zm(SLUkb9U8w+kG?3@U=Cz16ZL>G;wpY>&W^36FI3HZwM7(<@h_e>jCccMI2n#VtEc zZ<^=wjc=N7hl>@Z*t@+qdH`KgQ<>N&MCy*33zXWgG_0_MY)bpS>IJjZ;)L%OF1gKf zJ?(oZQ4~P&2F)qCdckXmds45;JT8(_Q}k8qZrt#knobRuE0u{dz6!)vy|M)T%q5J| z^>jRzHKpX+m#>_o6i!bmvzp%Semn>rvu#ol>F)H4`;Cd~hEAKzLU(IEOuS8e$=PQ#v;M-m>|T&RP(CtUePAH)QW8U zSqrKJuJ?(Y2hcQGZHjP1WivH3RRl3eNJmAr)ncZu6n~mUAO3DQEsXsO9LiK-(Eo&( z-^%JU)stfR(c;YPr}C2>HU0%4E5qQJwmEbK$NE>jYzaOeKY7U5ubScIC=~F7&C?7O zRK8wFLD;F3XX0+>{ni)=YIDxh)`wv=XP0d=4|BsLm<+Pbi>?Yq5_EZ`2Cs;}hfuI7 z+VFj{x3{~VAzLxm&AM=Q^?2}d!Zy;8-_vI;EUX~$Hos1sJ;U?o2Cz1_T379Yb-ABX zi)e~Qc`GvJMv?jAa#b6Oet@+OX;j$9DsJI)a^)+YYY{o9w}0En{cvh;=;5oWB^>n= zje?&}v2vHx%#;E4*q4BQX+tv~v|owFzX?8!sJi1vUO+4axXt=7#2Rb2IgJUpDfK#ovLvFs)>J z+M|Rg7{bVDnQd(pCwz21Nqw|!pirv^54|+;D0CU*D(;RjF<(SQQZJ%P__Zjssi5T2 z-*ZgG4T{*9;L+Xhg8N8@=aa0eG((A8n=D`2NWRUu!dt^nJx-sg*qZ+=8Xt`i`aZ?q1wz`t>T-nu@PxOeDpCPOa#qZi*sQqn!2d>`#i&IeTFmpQZl!Zp5kO3g5jm>9UXqy zSj#DDs&+<#wZ$56N72#xnYSV5wm6EmmVG}5(u5@>dPX64LLEgQMdZs6G{HAZlhLH% zoX?Dce~~uTs?mJ%?WDw4-7Yfxt^DwF0yyGn{_4FhSw10G2&ufAn9Lz*cB42eshPGa z|D5b?ExzE22_U8ZiI8fX_tIi6*!D2eIe2>6`h&`KjkoHPCoIGI`4NVz@}*u>e*wLy zMSVB9wSKeZ%;jo>Pe$dMJb*bY%&|m*8BXGv(-$w(V#rgjR_Bp40(&0x+W?g_p#I5* zx%tZ(bwJ9M@>17~Jo}Gj^GFuunyxWOW@hWN&Ut@IZ`i4?mH)iIctaPe_YC&Cx90Tr zU#}9CA-P~W?+}>C zCBr`E11{WK=EzZZaqcaXvEcynw*zck&C~vzJF-2xQfZ=DE6l68lc@BJd>g+F(|{1|iU8+n22lyWeSgNpL8Q`QYt;f*$;-eiUW}_#W zx#AidVyz`!Ldo)I7Bp|FO0SzfYR>*tMTy|NmL(CJ4hgpL*1G$IhxIszy(I+ATg{Up z%%5FajLB|VN#i#bJ^ej|0!s|X%~iB<%@3xWr)0eUlZ}$bi%nG4U#&O7FXX~<*dSe+ zk%Y8JyEJ~#>5Ch}{PuaPAwiJKkWrqF4#c=+&SOZq{`s7%L9TgrR49IW;pjVSDZhh( z+|R*sG=E5N`#0HeQj(BIgJe@-5kUSs;^}aF)irC1=0vS z?-G5^802Cv?~M)*W%ltrb}H{a;WBt31Ow6{@%^sj_Zeud5BYY(V`Lf?ee zv<x}2NADb^_({fuO=iA#fIu-wMv zJu_^*4{{A#!sE+lOKWaV81E{1h*zFD)s_4wp%Po&aTx@$qIuXVYIa?3wNK3OCYJfi zdKIj|79;>yrpg_N^ z8y`2@Ps)XMaiEpT1q-e~{PFQ))kyDkxx5FL#D`>jdj#_^>xdp1YVs?Tl!|$0Y+p;| zB!M7g)${A%d~z`B@4rBa3*J#r`#90{GVj}8rPfzQMw`UV!XtPEiSK9WoE)xzavIfdtdHL3wJY6QenT-eCgaLF!YQP)TQ*E>}k0z2D4XH z?9Vt705Qoo6zyJU{zj8-W*~-v9UB82_fz6)x|PP~eEI~BTEe70pYQv`1-F%=s62m% zSl^>Rlo+e6WM{-zT3AAg{v$^uIR=;x617!TG{;F#$BDx18^F9DZ;Q2QvvcCUWSZ8M zYzp+V8vA_l;Pc8xw;P0uX>tjPEN_tc;LPJ~D?8u+8ST3J^5ZaC2Ex2f*i6{M%cld{-ykwVl8)kJXXC+7JrLun3pA6V;UzwReBb_a1Pbo_iJ ztMz21%T|mTNmHl6`gublRQqi;Ae| zrD6R7gHzQa;<&>v%RKj~_0%hTYhTVIcgM(IDMzb_@DikGs}}qP9PfSV#o>Qe)m%-U z)K4hBAR#S0DEz_prG)tnrI+|&p~)E@@9(M4sK)Q6iX{vm*pz6wx)82!+b!0q?p&mF zSN;NcerhtLM7A$G^QWmL*rJoU+1Iz!amsDjW;H94tJDb*8Mq|P{P}#1a6{T$vIyK# z1t#J?U-5a6;DpUQCu)eFUaZn8D9pz0s5L1p7|+6J=<9q{%l%s0Rg0MO|rXivEU;bh}2ybeTYEzY%W&VKAqdp0#ha%3XB+1%-Kk0U_xf zM6yQ97qa6=94&nr`!&clNQf9%aEpASOK`ya$#_U=X$tWs;!Uv%9lLHROu~ElNs@Ci z)-Ut#okfvn?V_KoZl0A;*CL@&729sZufXx8CH-Z-R)$llu*SGX1i_wA0 zx=muE6WpZc>HMjk46HEG*Ou!Yg`Z*i8c&sqE1x z6Gn@LiiNtE0+?_Ivk$#^uQRT<{8U5jpi8K}ZtIzP8zD_*#5V!LD4a(E8MTzbDQH-O zA?Y#T_?ConQO+Q8i;+i;m8EQ`swAm0;!RsNp1AHWJmQtRp$Tyw->$fKo-~^FQWxn0y_m}ClfUSRg_lXh7Ow`E4Wb~m)qG;x)F z*G$H)HzY|?>Dd1Hj^Xmx#AaA@A6`*Et29A7wpNP;NT}>91Gcf2r`)gsG#s7(t#tLt zK(Mgf;-b^T@gq6y~&kiCfS+B0%mz<989Z@_>I@O<}}IzDOFl}O@|&JM&;gcpMGaH-3KsvDcm)(;^`l(~VlD(pl zYII++Ck07e9j~=3PN7dJdb)dvoujZy81TQe7~n~n)kM4b`==r`S&2qZqulb|H!p}(L zv48`TDxt=3zUD8mcmwh0PsEfQ&ylfo?r3V~QG>&3UvAa<`<5bnQ85CV-CtWxc1Gpm zBY*s(&Kcf4de~hKo8b!u_M)wMsTj-nW%DVf(rPPnLlnlR^#Y z9TkwXdNn_ZDH+?J8`6;YM**3LHy9%F&lz7W4k4*0iVbFqeDXU z(@H(LLCqd-6o0eL4{t$t6a{@CPu>pROA(#Isfe`l{@7c4FA3p9*}VDAd^rJ&m=x7W zVrx&e)p$j6YQsc{fD5@9_ils&*2ExFimmvAwlegf8lKbYlM*VtSCkT(e4oDcyc7KX znm)y}ecC%;LCc7x%qdi@E6XpJn^luP6@bQk5>51v!jB zA;!c%M9>16QC!jqM8JnnB{g5gxI!qT69dXqtNsFfR!&t4rMW_=R5`Eb-u3FlK1&69 z7{CjOsqYc_+{-*kYa$31>eEQNMQ#CE8{R^*19L{}Tm{VWyMl}rCKS>~9V`$7v>UsTS%YCz;XuxS7!DREvMvIe(g+X1! zL|2w^3un?hSCvxtMAzkpOX#IkL-OyD;%8l>{<;IRA_Ny=_VY5>e9s?Of8+8b)Gg#q za65?6L6pp>r2}Z{>j(#Eu7Vn=_0+f@D;m1j(dl>UzNEcn~dzxNthvq z`Ihh77DR;eYM;A&E6jUJDa?#{B{yi-KuR&Mo*A#68V`kKh9Cz9Xly4~cxJ6a?_N`l z+kL`2Zii&Ho4ZCcYJbw&wISeZ93E&#p?>o;eEN^Q{J#^9@)Q0=Ih}PQpYCnGk7syc z7_S9PJrZlp>>>`Pt&Q<0s>m2kF$uMDMzJW0aLIApBP@fT3ajjTJ@WYTt$y~oAJgaE zI#_#^a&?D>6dET+W&DHq@=r>C8P!EMx@zYkdW@~k`C#TC8Qgc?(Qy; zQb9l(TuDJ%{0;a0JkRI*$M2kBX6MYjhS@V`u6SSX>nuAYLo)9tuWi;|nSl2=gKGXe zXB;la#>U3hDK0Lqw-{mW8$OTna5(ezQ3ORijvTp8DJHjyvA%~)o3$Uq7MY&9*Dsk? zGAae@+z2JoM3VR8po`}&DVtt{$b?vYQHyOm*vSrMaz{e_UZV6V+UZmRg&#7pk%4#Ud_Vy+42Iu_o6u_Bp1phqJYGQ3mv6j7iL!{w?FXE>-Wr~ut^Rn?w0*Z%4xax$&0 ztu53oS6_&&eQLMWHrUwMwmF8{&2z7k7OZ+gP@^#<#K2)UIk_$DMB0kUpld?j`2770 zk{{a(?oCFFffU!ShPX?mn`f4cub+Yqaq;O8@4Gf=lFEe!kG4a1Xb9*%L57{bS3Z1h zDJ|}yyerqeYnwRM?Pp-?7A~D}+DNL&uye5M^2Cdrg_ovyymQAbBxYT{&_N_3?XbSc z=3DV_xsb}n{CCMA}fPk5y29{RMlA%rK)#lamacy1GTEJ1?a6*CC9)<>F!Wy{-m~X_(}aGG$*a# zKqoZ05;EvaDnoUS+38HcGp2qTuzXt>MlT?LW1GNpcp?v8CA_o$R2ILl51gT1=)al+pz zKB*H^Vk{bvB0ls`tkr^&bGQH+(ko`%u4j>fz#Y`myD*CZGPT~11w5CFyWuGx# zy({s6;hU<3*tZqC(?8%6r~ik#w`bM*_MDHcJ-;Z zJe3F)4Tkn)2D{UjR9sG1fOVVL6?hp?7nt|+yG`HN@tByNoWE0}PbB=C5+#xDl^maF zK4eXeHveu+{M#u}v#v5lG%D)A^cPuPl&(IV<3_Nh{(aul zn_}73y5nf3JjRJnx*+q0K84HU9N8Jb0MK&BM>ofNj@8p$Y9aejSxZ?62?vh}k_WI{ zVGeXO^PE^S@pYR2sI|~P-t4S_BwENM%bk4ojmt(pjN>$yyu$S0XM~h-Holq|R#MHC z{5iy#b3ZZB&fXLIg7@fLK`^a~d#RXL+;v$c(#jTQ$#)L>Ly+i@zuD&Xbcf4JZyZc$ zah9`nkjFNLde3vu?T7-2jOriVOqCCw5!-=*REMErvIi1gvO}31&;WrED(XTb;?1Ey z#8ms5o*gA@mjPFXTrHpA)4W2_l&Z{l$(~-&4TsRCUO2;Fq!0WP!h!+?0i0mufapvX zncu9Kov0c%0@*snGK`;iC4spE=&nRjjHHWP94ZE{Sc4`BYv7|d1!z1`&?4s$Exy&< zyVTq#vnqceqF+-A@KLT`%c!z(iwI$*632~;8c0PqHxJC3nsK2v>tpwO(L-qEuHO9U zQ7wOcp`h!Rq}c1$wZaZ1T6Ab{eZGYB>8fvzU@U80Bp4iF*Y#x{fi+Dk&TQ(*_TriC z#gm!AS1(UMo)ME?)40j@g9-#6rmec;*AXAxhBf@zzQ$8-O%?SU*qh_!K@n}j-u)^(31uPuHP_W&M7OR%K>3* z#|TNnj?74$Ct8I{jN`PKP~7yR7b!HcJkX7{!TbTC15Dt_ZH48_)rhnlI|}Wd#zsnt zU~DOOGB~2l>-nt?0`n{s59;Q|Phd}ZALNKE87f*m7E9t)oW=H5u6x2M49V=2O}KD+cyxEh zJ7G$LZ9Gw(>*yJqkJIvaissM6L=M_J(Obi)9mQ;=F7|rE!_$*iLDDY0SU`ha}*uZRbaMBXi`* zVk$F)1EIJ^URG2D{QVlS<7JqVMr8MSOWmc3>tT#d&ERTRnrKjVnQ*zVW&{2wdHubn zrKcN^pg`e=f#6wlpHpRBJHJ|lm8fCSFTsrMKzWfm)@? zdOT~)dxo8X+GT@0%yZ5wbJ@vHS9`y&KffiJ`60&s&SmX>swKbAYk}rDZ9!1l)q7pS zPr(X^F`SMqTY9E5g41h2eq|zMemDxV-X0%E!RP9TMG^<4{Ff~ zCkSW&O74pm}@$3hYUgkOB_u|E;u$4K>frFJ-)gg=PP-zQgeH z6@~6AIvu%1ELn=I=q_z_+w*^bnlq3o@%i7BOI^Wj96Wf}H*X^!7LNWRrB~lQyR37$ z$#0||zOzR0X={!9Q@mnp=0I-?vEK@aQ|~bEkUjpxMQe<*zW4`AFIcmPws4z)d1{@3+k*t6<`BJx0ql!>Z2eP#IXjXcDS+qkFlRU0C)DX~Ou`Dk8%4q!Z zT8o4Oo8&D=e(d1vH2ybsUqc|oWqg&$*n?8p^;Eo^XdUjYiS`)l!PZsSPYu``Vr5iq zQphiQ2H4}J#*0awus03WKfG?sbjql^t1{RS4O%z!znN_B8>P2Qymow+0>8k%L#_3r(4#5uUs-ZZckzxno2G%hn|8w<0P?#&rH8>ZWv!_& zmgDFrDt`0BYDK!O5<}195EgXTBlR>U=wU+05}2)UL~8P1Y7RCDH|wK-^OCXGdhg+)@Wn*PHPO8RhcvFp;j`!-vAqdj zq~_vM((lNxAn7A!)F0FIL`SeU9O8be_tEXfF&~AH(Q7YEM(HJsv2q$;JGFMP$B*%H zD=}Lt(^Tq|TUP3~K6=%4a>y(U*5!2%nz*IIdM&P&QX_y7id|Bfw=q z+_A(lxl{!hS-jX|apa21&7E=KA4cyY-!#-|b%#izIW?juTT<>0e<2eU zE5n)q!*AYj<$T8Yxm!fzSW8p7%!tFfTI`8t}b~j`P=@yA6a0@Ck0xHJ{-W?Cw zf&%KpIGM>Ua&IbY#K5v4c!RYDd7)@fNtaoY7}Z#8lE+0rHyoK?>PLNeW_V?h-Q=2} zPoh|v4!$dP({qT?)UpKDQ{uwL^2mMU{g1>S3{=Dtl1b2!Wv<(13|`#x{}^;>TnTQM z`O)uAkHx7@k)*J%9%TsJ?SMEdRtdkvt3>s`NZ>a{FU3wR;XbGOSUBEoPJ2+oaWv^) zDE-@69NLKNWvac@RfV!k>*iPan90b(?*`EYECd!&$5Gq!)ff9&_#%n2)d*hd8&Z^Qp_WhF8(VI9*I|+9 zBj3=zd7=PcoOzLGC!j1e$cwLt+T}@@T73q7XVz^jmq`4h{?_SIdjQ**!g4g0^m-F` z93rwb$rcGi8sl1#*im`sIQ4!Vj0!2Hx<59OEore+TEX{X`EeZd8km++6htvQ0?Qk} zH(?)Ia8GpA30bn(?%jUNkv40KF>-()xf(;@hw}yxX_*iAPqDU!ov`Y=eV@T`^EK?r z3>gZ@0vNp+uWBC@wR`>|d7A172tCBkhdRTvY+VgU!w7x3pYO%+RP--6N@O_xMOu|! zw$!~l%Cm4i-CjkDpo%qrRar zwk$slLqR1x_~(m0xcnHo)H@Hrl!Jmi-_q{iG+*e7o$VqTe}Bc^DMgRNS6)N;dElX} zVbWQ#jO#m($epH2ehRi=v`+A=ZY0+Nb7(i4RTBu>LN;4pC3KRy+QE8HnD*hbn{e-u@7Y4y?yP;UP=cEf#rbFOy2-*|Ri_|zyciY=)Al8gGXG#YN z@q;24^$V#JPhEoJ&V)9(4AZh$LMYZQ6Pq{K_qgFHv0v_*En)a=emYKS z<}~$}(PEZ;zs(Zxm>IgXs}VJ%-10(zlD2#>4V*(IJb5m>LPM*bZ0ICH_v9~wpv1n!o+RQhgF6k-c7WmokP_5J8Z(*&`Fawb;BxYZ@AoD^ z+W4Dn#h&1zsST75nhcd0N#nu0I@b@g53ujVt5Taj&98rLW%w!2VS;xCE+F=r-la*e zKmZ#vxLb`+T&Ne)ikbw|oSR8Qk2gq~(+6 zIWvgi%hE2%>_>$=z?Z1zLc?+QmozBaJB9h_&R{V;gx(R1S_ra>0U}}talpfjAJV;3 z7nt|)+@EtvWJgww*W{}*7f>lrIdz z>&kMPO|(&0G7oIMOqkgA~~8`vKk~HoBkhK z=?H+9Z0YZWIxUIzYt-ySedm(x10w8IGB- ze36uBd>o(%AX(!PQ16e9oxP?uW$qsX6%@+{wA5~Jq+4(TilEYWy4!-QCqT!#B_g3C zv)^gRE4*aWcuU+QyoP8xkivh{hxvVz^ed(M;?Imw+Zh_s?X=o;d0c8+{8*?&a^Ogq z(@ThS-Tmq{LxLLse^a+!f&e(*{V&A4k=bj?QI{tUbMd)NAKY$Ms}fv zdnt=v<*B}h=T#7cR5M!giOi%V?Vg)C<-CsJ{9|u_!lgQ4^y(i-14nx^V#lbFnQ+G<7G#ibtt~)@4DO+ zy=2L`l)Lx_i*jJ4M}0Q16;`oe_#%YcR>-%;8?L3tyBy z8EF@nk9jy5N|r)9^|QwI;$=`cMVOCG!0YIdUVJXAYmCkN;Uw9Iqf4%@hcJ#EDRc?Z zyJehmQD6?(I4<1JuU(b#jRZ*UpFH*flb%&&JE^_GkmCEkm_;7fHIYs6>lTg474U1t z#8~rP@80!?mDdRg&a@;UZMkGuMe9i%hI5pRTDA@7iidatO1A!Jms7KmN;u16=Mw;w z`$(waVx`)aNs zpZaK~YB!QNMq(rWlWOn;Q4gbLGRR)l&kLo z+DRk-S^oVHaR4Q^g%U5xBI*-V2OL)mm4=eH9o)FJ+m*qnUTS#n(-lQc zSe;0d2fbj<8ul;{lvR>|kr}ya`)-D69F7 zuwTm;gdX06q5cNAR0KL+vismS)_eX1N;*rkDc2&*DmL*R=uTwLvKUw+)ew&6adBjx0Vv> z-3wIrq%a0>R}1P0$f|73sEj{`dD>9TX%DL7RSB{w-ocL#x;i}nV z)%!*za<3N*ya)g!} zDmVH_DwjDpxrllvWbWNtAoCmH_~KjV5iD_0c?r9X0ATUh0R)9_h}JUCbJF^Cegifv?Ly@^{3sPp;MY;yp04X;NWvX~j+Bd{SsIyhe z-3$MVL+^W+-O%GzsZ z=#R(c^AG3rBj@xV*eW^)m6uLCbqyvD=nd+;)bLx!+Kfd&ekNp7f z&^V|#u&Mtdg__wsphxAK6j9e>b<;&pU%j@|%f}!3`LGpO3EOjbwW=p^c)FLBAR#v11?!3NG1Nw6T(MD_d+c1Tn86f!IMA#wE zn7V)`Wm4RGC#RqL3qOTfykOM6#hP$2rD5iGkK0e<1c9p#qGX>c29jRr&6y|$k_RXT zlF$Nc{5hXyts@dz5^+ULg?=~7kOK(Db#~VwWt2KUBl5E=vgnM+a<+?dI#7c zsQ8{v#^6p+4g`6An?Qaq|Ar!JmI5K2RlwAFHA_DJwI>i~IQ4Bh;cfcUy<47%Uwy!} z{~~dHI&^taSKp=6J8(OBB$?IBxrNU%ISLhMi}M8*EkFI$hAv3LO11Nng?B~ICzlCL zSEe634OsPxJHl0dVj;P?eO8!q;l{h-+?CYOzeuHZQv{9dqKRn1&N> zX2r-(4i}iuTlhKzb+Tp``m2x*;zZs^7hBG98m+*D@kn8WD_}V~b#`WO`Y7^#3Ywyh z%2yzmwfSQ8z`kSx?}IFJnVBX3P&=YnVJt@w!jFPo6Xd0&48!?E7bIzI8`N6Tx?CF}(Hv_HD=d1=^Rq;pQ4lImweTs=jp4W!Z4im0jRK+f1B zi}SxLa!53yfmWC9@C{Q#*EEDigT1#tCHlmVN&W6ufo*|p%`Q$Cu^?5X_l1F9 z(l|^+9sZhZgj+^Tqg0 zSItbK3`ppgCyE>nL!UEjh4o_klK|3X{e{V+V$xD5J5_U4#r62G|I5B=#X--BO>&+y znwnMh@R%X*hg5WDoo&_Xb4o_}HjMCIy9>E|N89(Wo9@d0G-cR1-SEzTT%_`8vGmYv zigkj<5584zMV8papMlH~dUANDY`PpbbbBbITAt#oaQ#gb?n7iTP8+AGYv&o&5e>mhqd!Gnqi_C za&;9Ue_(4SObz767|<)Dnuv_agCu<5_5>~(1nDl{HJp3{UP-&*w{V%lp_bCu|MqKe zJt>`_?pC+z>y;nltV1`?cIls!Yb!KfUxE5-#ZO(ND$g=PDz}QkAKZ?01LQL;KRXt$ zWd7{@L&&?|Xd4&+AasU_Mn`sP-xdz(rmERk za>1zb71kfEqFAz^shvI+-E zR@TxqW99;;no-3ouhh4@i5<|KhGO_VZge>oTr1Z_9D#J)%<@l_(IW4S9*()tc`;dD zLu&AOqVilDyl}|18ffK){kfHkMh@cMs}DXYY;9Mwj05`hxmi<&y~8)PR9PHfi9#35 zG@fmVIR9Cu^@ST-Xz{Ofw(coIRHxRQSenbnF&Mla%&6Wu_{$^MB zBQPyScRqahJjHzod=tytk=)fe=!?Rv8G$lb&y9o9|GyQW@3^oXprj!KjsN6TJeiY4 z6#pY;g5a7Ec1Rq2*h@dLif~Ez*j^ZbSDJ)j|8KkJ!-0=}Tm9gQ&Gk;-F3i_xJY~6Sl-}v`; zU{M>1_Ale#3z%qDi>as-9;m0&&Ae<``-`;f{aO0(g!NacrXolw7N{|GiLZhrs)B?J z_YT?sK4I3bT$S3D*2)Qv@3))x%92fVeLznH$mdRoQ|Gc6n^pLc&~g6e3fgkkM*g13 z)_4_0YPR0wi1{qk%JQoW`eUe)(J<_rg(Vvr1sQTgxd@`v^8aZKZTtLSRy_g>LI zC(K{`((J(SW8f~ZFR-r+jS~*Bk5j7h9=2OBg!|WgdpjP_rTzG;@?J3tV}sTOyDD3!&_kbM;l@L4fK6`geOjQd-AtADEs`Qh^SkYjOh)x16@C?988Jp0*-CB1wX1!0gPi zfcC@!CNKo=Q-yORbSUKO(X)CA0|uMfM++Swb`UO^dsk7n=&u0+75d!%U-hi9g!#hb zKTU}L)7bx>M{2UCb|n1T^+GzAY{InJhHHDWWKplwU!yyvCN%{OYe^C z$zmMYGy3}(y&8?GdxAMLoMVZ)+cMK$MiG70BJsfyy&mPaY-dQto6kQnZ0^|1xr2=3 zg!MC|Zt>pnAJldfAG|AP5i?BnwwQCr_UBZS+yw#>ia^GUV1ZJh2OZYE=xp2?`lB67 zSG7hDIR=J_U)ocF&^>+S%nENfJ^ZA?{T%}>C@HpJ0SFbHwVg9&46vYS@RG$VF z*7F|eS>90`-c4R;FiIYGqWwkM${&hHUCxuaFoyTpRO8dF6dzb=x#)x-wf_VNd%)Y4JQXe^@*XSon5?xSKJT=hINIO6`Kn{ zza11dE^h8@syjd$=Y-1Ru9C1=<&CPrHNv?4Ky?%`h)AWY9ztdtAEfx3Gu&3(%R$=* zxX5eLCkb|pDCDivo`N{RZ5v<55xRh>Dj7+ndzH#2@=_D0WNGQ&?IM~o|~>(4A;!o-;Ce-`dG9ErDY zh@J`l5veEc=cUsk(x`uGl)Yk=h#wQNB~OiR5mbtunCRgDP|@ARr<$?@T>tZj&EPJX z=u=KQMdr!AP2F4Y11zp+Od|c+e8U4ww}&i(lPFs*Sb((`gH$zA>f@Y2%j~Xm-zB7q z>SG32|MfW;+1ruOJWoo^Jr1P)_9bk&ClVbXtMC-H>N&3FMN+>0@rnr(Q!<~TM*$-n zP{11tjHI&NeBnSaE#T+sEj=fLGYXd+)CFJYPm`W>aD_ zb2I8m${$@jokR68uz7`I_CpV;|4HqyebH3EQ%|~GWDy(^U*%5Q; zTuhQN-^sm4vTF&uR}lNosu*^`X5LVoq6b-XHMp@nKH-Pm3>Lp?wK;Rgolu7qY$>!;;3d^q zjxe_N=MJ#3?pAtH^lZqoe|kW$|zNO3@u@6NW>mq zyfS(+D}*p{sI|}5L3$}k>in6txl*Oi# zkYQ2!4OHYHZZI$Qe>f*PkW9u31o9QQWkTmvbjVm4&2>t+Wi40VLHSx|91 z>8~`jSlB!HGp5k~mWI( zXG%yh`LfUj$nF5VFsBZneanu_(Kc-+o&wwPmt(a54Ns<_eUd(918fGVQ11dty%q3< z64C_UR7j#NG98Jd^HNN&Rzuq)vC$0XlDFwf9UftdMvU~+hD-Ss2W)57Fm~RTfaC^^rkth-AgL)C%Y1v)$_DzLEog|M8NVH({z$g-k1@>X3~t&Dmec1YTYbs z2e#5>l(57}GIp|9WMfOzSA-HsoGU`i~T{?$LX+u z14vV%L6yQ$vL{~(1hBw5_m+;m!ju`B!=8o6#YffgU)BJYu#rNCo~UbzCy3Y}=rwq)q2MD@knjzctJLmFd_EeDK6fPnE|kKKW4v@I07-#%YotiVh0IzkSEOqfq@oxvPXNUD!s ze&xTW9&_ykD46(>N6!kNkT77S<@TSOed+e?gDr_dKGETy+_l1ylP2^Dt`_eCB?PQ8 z(PYn8V3*auKrH8du2g<}9*qW44B(pSl$t+|_{6-olx592&g|9|-X1GY0$=G;0#96v z*-K3x^JMYV3eiKNxSF$dqWNHX_>NZMn)5mvQB?@w5VO|_8q-IVk<)d*A>BdW?}vcN zvp@vEWLd~)j}@3N@o{D?WF$gHepziZ0tUCxltR9SfwQ^IQyJ6$O^`*vY{*wf%GIIp zv?hp6U0OH(51-Sk%}gK3Oi(E z^nE&5V4%Ah$J5m)Z1&w&gIYKL%SW1_`4nsRGLe=sfQ<;+6 z(az6D8O8MP5dDnS!zv~V`Ab~xe0hArWJjQGYYvR>sSlE(r{o1=RC%ha>5lSDqir#< zu`~Vp+pY!#e@)SI18XBqD z-%(B1(2-Y<5jW7G^79W5j}d)jV-t;{5~NX6a|8BqY{^)K3Nts;)WAfIqXx}T+k}i}Gogs+(L9wM6Zz8;I=x8c`Zi&`{7&28}FS1W;dy7e4ha<8{~#s?ra5Mbsm* zKXJfQi*F$h7;pv+2c@qfyPVj###Jy?9Sz4cZz<_vqgztmnciJthM zy=P(FZ~ZE@X~((GhJep1~j4A9k++vErm@ewdQp~&~x>%zXjAH{&`nNIg&r3OC*_NQmf zs&*0Arhq*oRgOKY2F|4e(P3vM_|*yXZEP%)_)vEB;QbPG%&9k}al4w5Wcq>nufTjPHJ5#OZYa z&_s(?s?yuQvOeA8l@Qlf5f~>n9~mu~A4&UNHv}sAwJiER_2^-^tPNqs+Hn!D^O`$@ zQ=Hp^=e0_?z${L}Z?(qPyvFX&XQo(FcT*=UOG-*`j3x(VQD?WFN=LU0MYsH6l1lG} z^3L)Kit<-onT|!xul2mpi>;siX&=i&-bzr3goyp6IL=?SNkCwaX|5LwBbIf)Yeem^owq zoM$)v*VIwS+TFsVtO>ugyYgu8q!=BdQx@Vy% z_^df}4m17^zd1sb(POZWb)y3EUEf?lPl`_dA`$LvuqCR@Z&VhmT|dQh!XhWtd4*Jb z&>&34`qm-l)v&XSp@KFEN9$_87dhXB$6HxIozfKU0Rab3&z=U?s;q@q6Flnr2?L}l zT!EW|TP4OW?lHA3)*F!yB?grR}XqPzJlg(rMGr>=xG zPV<1*sO#M2pl}FtY@RWpSO#^gOlc(hdo>J1x&+t9T`b7%kC@@%oiCLh#Pq{RWG~kM1xTIN7Kum(3E=;bT=bI)3S)5@`~f zvF)i+7fmYbsPqlRcS1E+!$A|b+V}=E#n0<1-@2>F0D2djPGS(wGlis!r`5-q`xfHQ zYy7ms1IlVX)?cJP!E<3Je=KU3@f`6iE|9WL#Qm4H8u3l1Qstm`KR<)ZD36It_4;Kb5#8 zro?UA$tM4rb_iB!)f_kB#PqQOnX)MfpbRZD+=w&Pf8Kc#c4k}WOs!Ti1w3}+93U+{(Nc!u$SF~VQ zZSr_)w_GdIuaUZm=!TLVCIg&s&d`da-ufybxPZ;J%I+BMYDVm^ut|{~Z4|6~%uE!l z6vF!$6esC!=|L#sq`HVZr8B87V<-%vITL+C4O=eGYz8r28T03KlngL92N@-p zhQxL;`oMq5alxE|&|)x&2;YF1ufoqNV~q3C89b;TW?>9$}pvEeSJM@w^3$otusCO zmRXP!Mhe#z6cl{wi^<}x-vETY4hSy5tg5l?DjS(oPyNzZuEG@yBK9)dp*@=PiG#d|j+5 z&i8X+`41edkvTlS+C7oGne8yPpQ9$HvKAKjGGIJo3x%E;f(fyMHu%dTY-TXn>CD;pdXI4hrM-h{5>dUj~aS+IyXh`cymk`K7{583sWkPZ8 z(#A8Ke$YDHg+FoCvx~xARB#Ey4Q5V z@K92wtp|}{_NB@)!hGi<+fV~fV>X4a^v#xPkvD2@%G`!0_r#e_MELF_a6 zxb{$-%6Y8`p@p z>S3Ci3@Z^Z`tv(%aD6#_20*B|teqh>Se6STM15+m-)NR>@E(%5mi4Zd`8qs)z4kYbV1-|3|-DNRqJ=%I5P75WrUmZidpuP?y^)$j`<$gMEB!LU};@p z_C-A8I4Aj5=G4?gE1DUaftTgO`|D`+++jC$^KA4w7!O{d$nb7o%t+aUiQG8}bs_KX zA0nxJ9iUxAMeA7xaC06Zi5ZaO9(1108FvTG0A#Z2lrH}KSXg*^G_xQ_LG^@28PnWQ z7G>EL`R$ZK(>Y3Uwon>!6?Ssy=UqHx^>6J_4f_sprmT3v5nhTUJjQ=saqK{inj}aV zb18uNR>lXq;vhpX3)YfXIcO#@1o(7O@`Pf-8c8M(-kfu?wH){;33 zveu(UZYzUdn0TV~gC~wry@#U){ritEv1tl5WfBDDElNV|>;fZJQ%o7tgu@P0ieWE_ zRp!^COj80>*GgBh8fd|V(W~Ft=fQ^Yzu7!|DA-s1Dk{h_o(q`6kDrx%DtR#5Sp2jZ zIa(uYmnL91cBfQsx=?!M>eLz=qS!W)8Wr{c(t>GNh96UHaYqa5Ny!wKV)9e#;L@_DWP zGZKz3u0k)vE88l0V#pK(`iwwTi9d7w{*%Cd^d>()3Ry%WLyQMENik2>TLzu4CRj2* zgJ!Ou^;ac%-gOI*^OfNyXp587MURG?KAqe$!LEHZPc=EX+bs_dAYQreKMi@9Q0n{O zDV=pVwU7I;WJ;GGox|!ViqSvQN)+7()92-ga%>KRJa%|&A`VzF%MB_J7n_ejYe{I% za9+3RNEe^>kevVaC&7P03y^;xv~IoRh>!gm)+!Dly^^$zAD|jlf5I+1M+s5Fj(O*4 zBd@2!TQ0P(OQ&>X9eomqn=(`)%N*;bD7N zhql`?_G{1}*E23cHTf7VtL|D=o&IN5L>1=K5BCZdyCyP=Y&Ga!QThpQ@hY9NvxfSh2;IL#(_fkM-P3eUNt}v#sg1pOS8^F;HKCfdoT2kdC|wGEuJbr^<|HTh`U% zIb678qm8v{RBFeGMUD_YozaXKYZ5x^&R?pNUTSHd_E3!W)3y661Wnzif&LQg+Kds4 z^RCc;A$M>S+oqGLeKgsVm7fxr;waSnVVbCMjhZ!7Xy=eHVU1vgRTXqP73Svs!7-9B zgZ!f^_h+=vOxse1Om=(e4|t}vwgqt-7;_VA;jew1aN(^Z`va$`Jv=xZ3L^8OOf{Jn z`N$QNiM$rywgeD7MT|pF$zx%wh8eMS6q%8#AM5#>-I9d4W@H>R0q?R5Tt1?jb|_eQ z8 z1N4+ZV>7j^ib?^*pGkiLn!Dxb#>T}Ga$^e0!N9FvVkg(stuJI6*vOT)GJY8+-Y$io>!s&w4HgJaieW9qD=Uu_bYfm zeY27%wnGT>fk&Yh<%HCQEGYh_-NGye1Q9jQCMr^e35;xkT?Yi5bGEEG4Xmxs-;RB> zCu`Eyc(=#Ph-*YnwP-!_>eX#zkd*-kS<*p>*_JppY;5G{)a0o##3;&%Q&z)%TMy{R zxw=$0+PBz(eFX#tld{KPm%UWFpP|stDU-{lN}11Mx?A(Rc(<9r$aaKFawtDI+bcfV zLCw;U{aR(Cg`K-XR+%m^(ff@=o=_oXiy)fTuZTW~p~5|NrQ27~KwmF<^8m4Gxg*25CV$Bu9=$WFtg+bVzrX!jx7Ck#Gno zB}yv(=KJ&goqO25=l;WX?tPqizMjwL>+T+iobm1CfeHRVg+u;E2rQX^u~lJWS%GGn z7OvvXl8T4!9B~m%H2q1QF-q`V*s39Uq0dRKk#eI(6x*w{3y_|y5C73> zEA=H5$#oiMZt=!n$okaC3{z`Z<@Ls}D>ZVd^x!^JF4_rTndE4zu`@Ejqm@%&j1GnZ z5n-@(xLt?Xn5cBBWn=A`alP)E)#Jlxk9D|~8eUm9eET&UZT~0JRx6PbiLcyeqP9JL zPOeomU65THOjkh^rf()3bw;#H{n@y}ZS5(+#M9T`j*?3^t;d1I_tSBk$c+XerYH2+ z5uG}oPJ6+b)Za6H(+?w!t@H9*QO`a4+hZF@Hj6OIJ#p-Map}*>v8k`uL#h?yMjND- zyIXA#550l3KnT?kt>AhB(X@qRG7TnpLO8VUuB^6^#jtDoUe>l~qh{p9tNqc=MI%kC z%m8GMu9?thwQsBT$PCVGLii_(QxTHBfo-=%Sew;@8#! zepr*Z9fo013fCHSV0DWfFxvgw(Tl--1D?X$(Ye?`3*ZGE;%xN8pUS5Ho;#2@Yd&- z4Ft7u<57Uat|;OmK|f6hep0#AAMf!1ThweuZl~%!cH{GrqFl^9W0a`phkP9Lb5r6M zC`p(@KM($L)G1`5#g5paAkh0X&>s8Fp-nO1Fp-7KA4Yfym$ls!V%62361IWP)pIi7 ze{;ZLWWEwh-1yH3KQTqk9HklbP-QYkKY)8k+~F7>dKMt&IJW)rq;6LwOwFUCXvcZvhze)No6{jiTHiy}P*@;( z!h~qzeb*U6^QcIZxW%(VEQPH*G0AX2T&7zFvX$?(g=F$_+qR1p{T}~uYmh_CvKB`J}7@S?JElUTkHfkeA? z;?-u)`XdD%at{*pcrMn)LDXULd5hCm0Ak`DlQZa97QBj=imLrLnp>Pco51_UHwKRhTGy6&|A!6)0mbQ!CYBSo!OdN_08!w29cV25)UgCB>`Nx zsfZ|}DY)abLC@e24*HB&iT!-Nth-0NFrabz#F3@>3{RzF>Y5xf?qjrzN~h~g2dlM&`&d(y6ym<_RpF`R*xrtWj!fO<_D>^3o6v7 zI$(8H%t_X`W8?gLIfS~Y_yCt=&XW-W=9=w%t8aCd32aEVoN5em>koK!jKbtjBVp$A zpH%HCZEN#s(l=VKdiOHyfo1os&cNLfnC~-KCb8TL4Ff7X)WvCk?}To86Bf!gPLhS^ z)+R}R-b~;;AtsMdb0B!6=;l+^WG@WdwlV|aQ2opNZI_hHyW>*~9REOmieU|_$|>AV zUC*C9*3nIN7Ou}c33c1>b;wZ|_?=xSg&FtA56^sEHV8Tuo3vrl-Nae5v%aQJ6ckrW zZGNa@u1CW_Uy_>g@TKZ-K~0Lv{x2asf&&*8yp<(+i}`ydY#IRm0mk3Zgje!2p@RHz zKQmi2t#|dR3d1qxCjyjqGd!nw5;~Vo8s)1WBvQC8`6VGo|=<;w-}$q&RI+AJF_ z%sQpVJ8ACpJRUNBcY5Nr{b#-fov6~_L>57eq4qKl?$Mn8jwHP@$Q>m~%W9vc)gp_` z!R|)zF7Pj5OpPq8#4Kr`7-46lw|Yws;boAzvnPd^X4c#Zb2B8k*0>m?1UoQckF>}R zOBSW8s$W>?s!3{DKHkLP|3>kaC3h&PoIjT)@ZyuF?L2*}CFwu<*0t$E zm@E$=Wc6D^7jG|i_cJ1RW@ZTS$;09?bcV{4&)I5N)jD$5;@OV^v{3lApk~n~Y47~! zE4rHZ24#LPUrDp|MNC6rI~MCha?(#X<=ki~6#E5uif7vrpf$fWe_Yi_$8~J--=*mm z3Z0IRP#FAJzI|r=9l}Z^b|_+JMkAHmib~YE`wcOZ8LA!j*n8%# z*G-H%QE)K_GVRbKu)=g!3)172%_2Q{;is&pRDR+LV;e^AB~Hu4YDVOL3~J7AkV2?ycbV5yymQSC^9#Cf zcqblXA1ybN2O4p11!S5w6}4dr3XQPg-WFhdVTRn1LR3gnjL1;@BeR{54!Kl*N8#^z z{+Q{c>>Rwe?F!RXMVxP7PCEAtK0UnOOA#c+AH3kpb63z_j8YfhL-m(5UHK0?RG?8= zqxGO6@y{`(l9YNQD9di-%`<;EEZ4)-HFi_H>fR~Ul649?UMu`^5P1niq8Khfs;&*7Guc9<( zU;G75sle$@^%lH#BY2vtpO~!8o1`ElLqESZ$iL(+YgR>T;;FE%s88d&k+89N>y^>> zgz~U9JW$)nRutdO(EPJd1XF9ya0&XIucF5pJVuRA0OUES|A@l>(I!a7^z;c~MR4I8 zmez)#0W9c2DRJVOkkVwnaT1Li>S5N?-0Sy*eg0wsMRk7K^xuX^B<<>H$|dTh$5=<# zWw5{>$3aN}lRf#^JZp(eHvt}}0+nxmh4R;~yKLWtn=vY&>@hUvR5<~IkzUHx3l!Xy zglwmj9&Dp4gkN>18`?MVs8#*A=$|>V8|~+SLb{vKOQuTVe*t5DC$W4#HB2Gaj?)LG z&E!=J1qSrR0%1jAcklfu)pAIUZ6qAcR}<)OzzOsFockJKvlT2QELN&V#W|j^I zsoUHgyC)!*2dK*y;H_j56#Je!+&kjMv+i}mTPfm*fN2qKU!!v79m$Fx0Dqma zwH>GLIk^>0Z^FlZDa7b0;O`+R6u?!lMRzqF=-wj+=iYxRyu6RE;53J)mVanZ0G9AG zV(VQx`UIdR;nz&{=AtrmCRhb*{K%;Ye*E=THeI25K%+@&4cfilOr)t9m)hMA!-9)0 z#+2@8Ba!bcgjR%-Q6VD)!?X#$q!R1X8Q! z0s=}noL#w?<>WjZx=*R6rx(TxWzRBIHrvr;%iA}BQTc>*r zKhC^pCG{|{d^{5pFfV1T1f?Ffyl|oJ?z2$T(oeISPPUm%yIJpc66CD|d4yVZcf$DV zEwA3&D@dGc?*Q_wB?0cVS~YgT{%Ck97M|vXVa#f8(}5wE{##&Zn{0E917i!I8ke9V zS}~QRLVA>mHEnQEZ8Vu-N7(W&p#!Ewv3xq^imka$=X&}~XL%%xZte;0@$37VqV1YY zo3QkKxdGGit|3!x$_stYA=8IQaF5+I@*k+thr2X1+%3|kDLe%)8lDaHhFq60344Mk zG=%yT;^Re$ng*ujvQ*P+gB{nD)7w4ANLHylbw41~1)RJ}w^LkNFotO}VRaPL^QdOa z7GGzfm)V#uKQ{|=^;Cf%ic8SwY|D` zjjvF~FHxzCE=WfYyx0aWy6?Pd1(cD`zJ5=wft!F`*vSK6FX(6mXp|>^;1`v+NeAME zY}+Z60X%eB>1r^OSlythdE5vog!oRA?WX@OT_yj!*=({ZHLEcmUOQ!2<^y%bu=M1X z{Gp}GTaa9vCA*Y|=w~DLuw4HuB*O;b(=adR{}om{tu>1z0mb|yXJ;$is{@68WnkdS zArK3@uf;_p*R=t;MzQ7wGN%-j0N;^k%v&gqZt}ajSUJ`Fiix|^&fMemBbvX8yJ*Cg zCm_$BY8z2I&_`_c0p%YjW;_wj`k?0!@3~)Y&XSDNf#Js~2Cc+%&03nC$k?P)$|?R> zYWbv|S0BL|?XJH|#n<^{T(qhp*~mmfV5rS@iohpp6soaJ3Ot~4(F3VVICVmeyPnL5 z-L;T;^;b_94ZtQoisMsuDW~{B@dE=ggFDp;O&U$c|6P+A-~h3J+%utF6HIl%zlPFu_m_K;w;N~`d>_Ie87gRJR%BM%u-_2I#7T5Y*2t#RVSxMI}ey}-h z6T)@_eabLiMv>YRC&Yp`{7{FStiNeoa`-&sS2COr!tw&TbDVq#UZY}>FY3Cy;5mwg zM5H=1j+6UJxjr|vmQ=0DCO(*P7CRPm9g(*l@dpy2n%2*Zz%?8yv#X4QKfOv)+SM}1 z21{Q$rCL7(34>BTB_V)~bH{Xnu60xqbIEm+BhIqk)t1eY?qrX<3Z96!SwKhMO+u0_ z)uCn!1;;LxZ@R#O!~SCBw6wCDuD?W0QlfO_a<_wdPLNBLwkC)V^qC-fG=~8qnU)$9 zEVykxYGJZ%G+vQ%`WKJ@N=>F7swb-9uU z4!(5Vo^@?r)MGKptShOOWPg**5crs!SpC6K%w|k9VU+n6187 z!h#W0(vzp!Q0@Ge(}xfYK9Whzo`ImaT5T{UfuXY_$Akn2j{O?&~P0{_tSa77mJ$Lmob0#H~AQeXBH zcTQ1#_EL9{n(u|a%5VO+(*DT3_Z9v!MicWdPo-rC#S34AuKMy`op~}S z*V!r2>>Yb-^KQb}+DB7Q{&1rH*+dFb-@xonqs8ZabsX;w2G1S(mVfh=!`=3$+Zmp~y zPL}8m2?baWyoqN(Jpz0Bd-}?0rwpE1P;lBv5Bh6(x&fl%-zM;4hCwGPZy;YK=?DZM4Nn0RgZqoCB(=rv3NZU#Y0N(LbvD z&D>nNs7u?VDV^(=u>BGSC4;PmNLsPS+K$eWrLo|P@VpUnPTN4yagd4owm2hOdTkjq z8TkF+8|_ORuyvGsdXqb$Sq6snpxBnw9NO4AX&;MG)cV_e;ugM2P%YlSFD^BG9kF*DlL>d9Z9Ndyc;=NGRI7Hl#^Z0YTE7D+mHF7axVqb9tZmPp7dwhH~Qkg z0Dq2)w7ao;22Nt}%ISlYgLLEoEJB)vX$07NcfD%rE1i}Xfr?ze!Dk-s302+bM@E*V zW9HupoPmg{T^^Rwp%h2D)K2P5LANnOy%mU->6*3b3UJTdQ6S_?C1L3t`)Ate;<%;a z*Ryu9K8d>HqOh~j`Sl@vfw$s^|7>9{cD5qUh{isb*;z9Ef1P_8ToURZM~FJoCF${ zm6|(^>)x>)F8{AzCuZ_+#vd#BDg(N-`mA9;`z>OXYVlV}=4+bA2B|*c*~^B*`z~YY zJIwDw{AoL!SMojftEe&cBRdjlRb?^LMmKkP{q95a=c5+>^iFUxHBP#-^>S0*T@dyu zf0aZ6LqB`VhWEygp?W>e<4=m)eJKRokVCm5&5qHNm?U7I!@3V3c`u}SiCXOn#M|dK zeUc}!CiB%=&nUsEG<^g?b-2qfVpJtbqyL43>J6Kgb=lSPE*lOZGbU$IHeFIwK|ht& z&ca=?@5L*Tu6+|Jk&dtHy^LvCj>gYRK_&;b&FO=82I;W@1JaDf7Zf*nqd_CzlwaLl z+cD#f?4+PdiQmGkpvIgPOz8hTyHxK_FATzuCqM-O*gM1K%PZhEQAzFMC3cE zIDUJw(L%B6HJr`D?NRbeVi=~XP5JJ1A%j3hH^tmF?O%XBtRs`;6R5rJ zAoOttE0){Q&-K_4U&fQ&I@sgBXQ&q{ecDBBmI&I#jEI1_xr)N+2KbaT4tPfdx0ML1 z3HLy_m4wlV;AXC$iX4c8K9kH|Ffo_D4c9%b?|7JS4jv~7Lp)gLXI7WAy3&~+;Mu|Q(aKNjE$J_6Ae(_qH zII+DdktNYUU71kmg`}#Gf03$EKGkqHvZWFa|;})K`ew8|iM_`3u0Dk?A_Lr~Q1e`l9Y(hy*N_zBWV*-YD zBpr_2+=sdyVsI$a&=qd3EGXDHla!p=Wgf?1W}oFP&Fws)E)JAAk-*VLtLb0gscf~y z2Un^+ku;Yss1}+-$yHUc^A$d%eH}3Y%V>QE`X!RPm3Of;-{ae?Nx|r+8why=!L+xx zcirid_@p~eRJVoL&7taB9+-Ko2tZ47wZptH<*0UB4zK1;pG7;yno(UEmQd|3 zN$uM7`X<4}UH7O zjIrsmS@sJ@_eq`5cD@=-d^McwO|-wR?NS&0*MTX0y= z{18nAv2^jiC$1RaHp$3MP76A(;>5OJlB=9niNh%taR+!`Z}08Oomf1JugIv{QUS193d;{m4yf+est|PfG9;%D#uY$?{Iw(tzq^6dsxqC z-|4_=l*h76*lt9> zY8KF!&1FjIoDm+Gzzzu2dV@IT)Kvv+^F@OEIAA!W~VN zdv0DRc*XP*Q)3dP#J0z-iKpL#Xh2q6F+t)mH7r(_h>t^q4r`XV?P4ZUbH^(B$=j?~_TQZx zr2DH`WPVDV;Ijsi_<T8sAtlPxM(`>Mb#lX=9d0~9h`V^=SB z^;FZh&b=oGSgK9rH5H2-KH<0nATFL;^|79xZJ`sc|41s$SW)I`gdetKtFj*mEz-EJ`lgzUA-{;wP)t%T{!M<`~yte8JJQ+bO{bO&LZVltNDfw=j)h1Q7fGnya03$CkE1{ zN6{3SKWj|QKTTb5`RQd^YxqGIk!A6OQt2!4Ws=uz;$7yNu?eaMOWu zj}MDjQvm8goxMDJ(sc(-=4@WJzWA$?d?&_ykU4c#79l%@O?VUkAWrhB24Ek}ByCy# z7Rt%aAC_6Nm74NYnu=7)B6O?_!lLsMmdSGLc}Ow9*WnSQVTV?N1*Q4pu#xfV6kpJg zzq3t>y4t@64k5+GT?8ki?o23Yv%2@t7*cuUUeedIEb4R#4b-~fQJ0CEwu)3%OW;<6 z?_7&;Jo|rZLQH7Rf2+m+8X^Z)Vyj$lg1>Af#26iN{&WqCEp&-?*UBz?UOR5{r)Nr<#OJ(ZiOP6V>+aEOS3IS+n8}fF zN7lEa9>K0qv>PBL(VbB*M^2i_@n=kq_sy1X>Ygd#{7F}tZgF-dO*Pp{|2BVXe)kR; zRZzJLKdWV5ic!oK(H@+^rg&i+%3p3lHcz+hhNI7>YQ?mRESspgrxXjU-tLA+&BU~G zUt``ZBuzkVYn8|g3k$zeTdjfJ%s|=Ls-kROLm4QvKvUJ?kc<#?r+gKQ3KZ*6d+ig3 z66gW-2pn0tZ!L2iyTS?^JQg?{o(Zzr)+`~Nli8@x7jif zf3oK8ITG%?mMEnywQ86%2+zMi5tmbM=O7S-0@BQ8PeoO8oyuG1PRMq5)p4R<(qHUh zQ%1i=QOtvL;Ef-|!4J#I<)!sn@1V08C(BYjEqo*5?2Bje`Mnyt;EQKM`3)4Gma019 zip;_>yBg#*4XJIGl*4uL^fIMJ#2+e%0{S%ZslroNS+EHOF#g98>3szH8x&~1WtUFJ zg@Pu^N0KaCQKoJ#fl9Y789Qb!C4x8*hdz75O;V!VohqlGKo7%6%~cg%fJHK~7GR5a zBVJ@2ymy~3z>zv&LBXWmEQTJ6Gl)9Ui&<#*|Vdm?d=VFaN35$`aTPUbT~mYSmR@VV01CMk)&KiiXZfI67X`F!VGqQy zE7!a0>xC_9cp#>^WUfIV_aUlb5%uyI16)pp#L0{?cjKd!KYF)SF%CF)$y&s>U+RB? zZku8(?$?(r&F+8wd3GKCN9ndWMiX~qdG^5l7hT!b&d(Kg#&lI-W5>@0DjWsznIF++ zU327sj$u8eww*uawPqIJ-7?QAn!I6MrQ4NrLx=VnHP>2zhnk3E8<{4*miKctQ@R(T zVsR!T{qO59l7v6-xx<2X3t7FNN185A;^mlmH8l!-=zYgx^rB3!?tLM(4XSFF%JH~^ zZTII(s$TkqZ+cdO1R)1%Ls({#$TlC&!D$Te}y!OeX(YzcdpAXC3Bndb^&*~1d$I7R6M5$sqP)a&F-w~=!&KZQ_ zLeW;y4zOPGP4y*^B}lgImJ!ij8NSx+Szi=F~q zco;IL!Tf;4Kk@+5V+mIPf&^F|4q%bjvzV{Eq<QvG7j6IJZX^;bv%5) z%^=Nx!{Z|-8u+bqU0vPkWhB}8uBhPR#i`-e(3;$FAO0V7f-o&Fsajc;fXpA=U!TV1 z=oz!Sx9^t4(0boKSn2S*e^71o3mE8N!WbkKJNMWw&crFPP-UBg%3|<^7CJ~?rmk@_ zUwCtT)s1{8#IEL{LRiJ0^WOa(C&T;%r*)O^vF@YN`@BWO6J%wf5Ll)V zSfLP2(5SHZf0dfKl8?WUno)^Q{#k|T4ESeH^LUK2qeaBH6I7xMESz{mbH-$5;~@RG zttU0A`$T0R#wuMu93H*!+6@qfUs&&Md50&C6g>KyfaM3%rY+x>5yWRmR$o7de=sM1 z6Yez=!7~0Kgy=>$$tjDFmxc&OZATE6T%rypROs%!+(ytZ<($P8>Ss@jUn{d*otzsy zvhuy#Hu^j78o*7`_9mKZRt0wtS3&es%*xMK{?+UgTFn{cWA$WUG4xs@?_?5*(!I-g z-pOsRwxo%laX$vngQ$VL|3y)dCeb}dp>cvwK%zF1=Moqa(O2;)0oa)nH7sr$GHOG_ zZgqmUuh!7BtN&9w?b>E3ZOheaT4DqA3j#FUCkWNkR;>{W#VZSnlOAvi7Twp~bx+Z& z`NWcoVU0_zGc^e)INec#g`uQLG~Z%X8VKinR}AP&SkH`v6-jSl+(M?e;b1S;_?s2Sk0~8Fu!AEd(~z5^18U-A?zz&3tYH1- zxVM_Z?a{Y*h7^tgz-CUmG7SjI){8CWz7>_Z4Jip5>$8tlj5cjpNUF4wgaXM9Es6BsWM~B`VIVIZ!t4*bb3ED&+`Js zGL$X$sh(nwa166+bv7xaM)%+O{;CRtW@uBSXN6i^m_H_v3dk zlHaqB#BW#m9Y3Wp-29y2KrDWa$U{j#A(?>Kmp&1*yu98_omrWv3kUlURr6%4)AoxXnn_NQ)>J&mvYBs4%$8ony zc}O85nkQ6p_18Xtr(<9gV6n+Jk2Je&?I&V@tIy{z^?WTH9f2V47k`#zf(05&!8v+_#te2892a~3NI;Af!-Etvs7i6)4+ zMaG5k*kS|to)ByZC+cAQ%UM-Ukod`?pUlKnE*7-!^?G?$>q61dZAby^!?>C`qo^g z+{A|kIx_H3%0~lZz9~*uyf)+NBCl|8ezuQ>KTV$q4@gNZ;G292*W<4o6v}86))A63 zTvaLI(TZpM%wDACsA|RNu{lvCKH4Znz$M_{o)9O-`nO*s$znw==}>g#jh7ouWt_Z} zL??D4M_my8cQu%pf&L;KIYqkPCZ2jPMnB%R8Xx2?h`cXJ<+Y3b3wWdp`#mjPjtk#y z{{H~*fAQe;|HgkwCCnMcu7!|+xEv?|N$?uKlrr$eR7!0mpM>rfHR0anlUP&|^C)Ax zW@;mp(2x=A*AaGLXn5hskDvD-o=F7v&)N{KsR7YXu@v{1R+~$$H?#uZO{}=flh8otI)~Or~Pftu0G(BEEs_w}Tb`6A0 z2^KB%h<|gkEr?4NY9Q$&oiOUQ+fdyi^*XdVln@xM45FeKL2~R+!Nlzb^;J+4L3F>cB_KyUez?XWNClctF+c8VYzg#?C zee%z57~oaiM>5VVwYOK{7*RM4-VDrDVgq=BTW;XOJh;G6vrZUD#Zqu{zf$_A_B=VQ zU!cHT^6z?Mc4!&O<;~&1R^({oivGbEkz3SEVk0v!^`4bp^{_;tBNmITP={KdHq5*v zHA`lH&)2E9J4AOhyb6mZ6B1ynftan)dTnB_PYgYYKfc7F`-^I#)DSU9if0UaxbmXx z_@3F@1aQrcnDw}jkM?-H=K}US|Axqh6NotB@haF_Gd?M2E|b0pggXS3$diAQEnX%} zJ?41TFIHn2x9}D$$q;@5)7JqAO04{IVvxlC@j5j~zxE=$)D0#fxSdyhZ2VI2l9`B* zxb)>~k1ULjmAEPuI=prfw{j}6jfLw7Z!srWkJ1dNc(vcV$79wZmr|C|ruaPA~_ z+h2DajT9>1cw_Qg9Xe4`Sx2Cx2@`1S7;oL;ILXj^It%@^j5v-DT`q`tPf}w!rajAD8-+Ic-(W0 zae#i|f6MtSNO9}S37qQavM5ACIh!^<(!gT(UBb7^_ddz^)R$h~Q@(saYPQx)t26HV z56)Py`Mb*Vw&j`5J(>ZjUzuk2!Ffx_nFH9@Uw7T-l7FUHF!2T<)Gx_~qe}~Oss943 z$lgDf-gfon={{8~?{asCuP031dqiQ%Qob(k!iJ-S5>8Sn_Gk@L6Z{?t<@9GDVhY?^CsZ~Bahli)oTC)sazj6etdz6+dtq_-;ox|5I?%nP}8+a{f64e~X=_1N> zKp1)aNcl$vD$;RO&2qwTT}rExFc&6C_S9Mp1#8r;McRG&oo(t^^%pVy%r{z7I$v*8 zPg1ua%$srB4k?GAOHk=0YCGs*vzXoYQ)=c@COZl-ou%s7xm5)w7)<9cz$5wM!3%=# zDdi=P8t-?P{Lq2VpBr4a#*Faevb>OQox}WYco^p^$jjVm-jS${0^5??9EUd_3k=BYPE%x$jc!x5Ra;Q?PCxmun-$MbdA513zUJgd9ab=+S- z@}DteByKt65rcWiJ@FSH?n6_5>y%;HnZ@;E!EfSZ)05TLsXWd6=0q+99t->PQHNm=C=IfgUzo|F+siTk4N?(^d)x&NVJus*<|m5lQ^7i+{B4V_q5ApM|(YPG05y(1vs2re|k=I`8Ha zgM5Gbfj@+1rV6)w?ys(=#D71~T=95A@rju`=;rI^yK)FtG+Zp_4%yLkt_-}JUrMlq z?!1IcWZRi(9Jimq2`1FK^H@?w&AjVD`eyHms1nq_YhdQIeOYeWS`-ash50ovjAO!P zzwIQ&iV{+h zq&L!eYc(~Oc@ZuArWJO`xtK9)y&2#NlAD1{d@^TC`9Rf>g_34`8?+v^eJ12Kk8C>R zp6{jA?u_8+&fCUONiJ7CIkea)NmO{=2*CE@*?Zc zx_wO1VM2!nv(Wm>*NSRA-p*se8htTBOOxM z2uFKK?&SIy&w@KN(JxnNrndo=S?`=3|IE=nLLOefq^S~3EC~|v`3((hxU0@aJYRkw z>q2_(HsozcGHOI%ifU|*=!ae+BrNMOWHRUcOYdSzkH4F3&v<_;c9fE}qLS^|;=IgR zztqY7n2C6soj1Tzdy|iFd+>d~!${glT3gkyjA}Z$ zv7efR*8aH6;!QjeYjpK?i1n8fwq2RqR4!nNLz4mYWWPgq=X`#CkdwaV1w%YIjUhe8 z>3%COBT(P61;q|1%X{1tfNfn^SXh6z|4GrLM=|>4FwcWT8Y{&w_iITF4u;Jvw#6Tv ztCnp8Nd?LL53Y+pEZwiY|3$rIALm8?FR)-Q_pHisf~+kBb5#!DOHwh?c@SMPzC zoBPqO(mzBvD4Z#n2-7!%%}w0VyUR^5Lsah_Y5sqM z;K^sOp=jPFe3M6J%6$5#T$*aViM19#Y8vMn4n0(yZio@Zo;<3t)f=-mI2LpTlL5^9 zT!qQoW{)yP8|x@NzTP2M60(OImz(#s7Q4!*iYZMMJ}T0xJO_ob_q;%%wwcFgX$L`` zW8?!Y?=ug}v;mkfYWtqunRQVUs04y-VZn(l=6@t%D_=v~!Od?L z>%CvU<2+V^NN!5+f5yALK3PpX@@ILF3qR&vhUJcX*TnFZ2Dcvl z^M`Vs!I=yFaOI7|sp109z)y$Q)#PPjCtUzNtxV6dy@xsIkhpI`=dK>J>e#OOx?tnV zaj;EW122}=^xphjh<@h$E=1%ZaJ}nfdMj3};_ewS%R0hgHV_c2A`hS4CCED> z`Ty0pbtSju(OWGE{2B^Z+7geyfcm(S(b*&8Hu*R91hmbnT;JRM2~4V{#Mdu*h+Mbe zSXaQBMam1|X!A$r+Q^gO8IdJ`IN&jTk?uCen}h(!1~#|w`@X?~nF~>*)i>WBa*lRJ zKkF41_R}xO;{G=C=N8W)Z2l+4eb>v4^yy<2nr474f$-!)_7iZ(w{MvS5nLs1kSiy_Q>-%MvmA#a;p?$ENjP;T6^B4>lT_(P8= zSy7=G$htRw0k$=s#L3;L&8uMA_xF{jnz2h1CYHC)yU@ZzC$>Gf__8P&9_C`KT~WQC zkdlx@JU@6oqb_4U>pNfiNSFzr=TVYO^4%XJ|FRVRva(8FScXfm)Gp)LOFCH$2crAe z@e2`#lj0Dh5F@!{1>JZ#`r7Hs97hLPf;&^lbWdbs%GX-g(?Hdc5rq)Ij#9xWwLK50-C6)ybIvW>X^)7hEnzX4=g9zJa$KqAPo(1Q+C z*+u?}qcG%nWmfThT?@m$&9RWw)+t;`LqX&h{gviIhPY^4{qB16Ew;<)^BJqn`)q7hOEBjPo{C+LxcR~3tOxZ%z*|*o3Rjg7Vm_Tm zvN43NQhc(8t#3oLVp&7w5-(fX;yi%`3hY`1_dQcYYNOHDd3gso?GgDl8ja?Y4D0bw z0T&5YAgBzx;D_XKe7 zny7Fn^o#Mp8L(#!#kb*BI$Rl_!M~I)8qg-V!O@j4<wpiv=bkS(GyY1~R%>+DU?(XjH(qhHkU5dNAySo;*;!>crxYKu^-~XLl zo6LtzGD&9U%#r&(&8XyQv^b+VqJL-MaNB;cy#m+zwM2<6)_7s6UNOOFMG{Ae6%kCj z=ack8hSVS$nlvG5m^8hA081khT2BW4Ndeh1|39NJsfPFo;AwA(^E_19Nm&u2P%5_@ z#6qgiEj^wVnkzJ#S7ceVSM12@XDNZxlz56YrD-&WMg^HfXs1@Clzuq1bC1h^2x1ci_z@@=Xn2Q9*R7>d&O73FE z)lRy>GkyA5j5QNI;L-?w)zODo@`BY#@DzezhZ7G>V%;m# zO7j@1;xxc$Yh@lwBuH1;h^)X{h!h}9*36!H7o3D(&BY(v`VN;JrM@Z(edd6W4jfpU z&~t!5m`nHm)}(X40Xa;sBJ|{Rz)vYqu$e5CgoK)^`u_@MtJOCkMS2z+V0;K^fdi@z zKcP9nd0(f$2}jhbb5y@y`&*AOWix%z(qmO%n5CWje(O8Tjef1Z!fYp|WRDY~gqT&- z(UtQ`t6>2*MmgUF95K^BJsmcY)UCA%|+?ftBext{d}G&uERs=lCUKQ z2I~f6(>l|fIN~jgF57QrGxG`;)-FI4Wl&e1%zJPG}o}aY4e~E(LBxiD23v0X{ z>iZOoad!R#`;!Gt>Im=;kS(9`H?ke}z*39%OF+9oUZisImmJ22eWy7cMJiqk-qd}q zu>afC!jPafB6%@slGxC&51@AX8$`o;WNu?#i_r00+{#w{&YYOvR=#SQ{ivUQk9a7n zNq&)&v*>&LotbvSbZkWDTXLN~>8Uue4oz0iXEMhRgf5+Cpq)S$x@ONT3t9%DZl!Ha zTuJ`_F4j!~eR(GvB<$s((Huyaoh!A0QvlsZ9%VhZLzaI4TLd#2Pk8j$uKNgQPF`qA zIJ3kAHWDoZ_R=UfQ9>y;2IYCgF%22 zTtkB+;4tB_$Ps`bHn8X?b`@+g3J5C|hq8%_>i;;dVPRm90S^;Pv!wuIL=S3TDVSJ2 zI#_p`E53+Uj3SLLtgi5YNW02vN3Q+Zf~3hr*8)3@64gJ+#(El&FphqVsqIFBieFDR z>#KmqavT<>mnTL?B#>|lIZbUZZ>A@TEByd5mzoctSj%MkeJDEOpg|vH#9{&r~vKNQ{&Yr+zwXyK)+=_%W>U_V8mm!q<5ekTNw#^0{js>$CC$V zAqRut8fpRixb{_ZZf`?mV5rsq<05XTF@+N9-0I)j;86F^3|4=C=M8kfv6Q(1!mTp6 zt?Gdu=g9>~ALUsw@cCnabmM`6f7P64<8bFlnpU_NwP9>oHQ388_(OqbIiwV#X686} z+Jn>LXs*8PR+G{($G#l_wQ0YJ`&jEQ&GC;(xslz5oz5E@6-QNxl6c^aeZ&RT{}>Mq zX;#Ba8qpT!QReE>(Gir;07^^|5KY?xEo3Ew6-AZ98W1Qh3Cc!<81sO6pB}HZTVeSF zec>{Dh41{IU#vQV*?RNUU^D0YW19}_*IGbA)QG5fPkErcUcrw=bmsC z?xLz?m!j2!=R3Hn4{&VLSi=V|gvp+frbqK~`&nmVLlmdsHLpD;xEb^FlE;5=!|l6u z;uz5873Qr4AUog#XoeN}D1$AegFx>xBs3-hjLSLcck9OHF;g&N*h@rk5!&e9&K^@Rq3@2 z_DYdL66&_zbmYIze#|`%N}@v%Zl7F#m#9?l3F=p7$THMx=DRx(s2^fGZOmggyK@x7 zB3qq#ROh7ivJRsXLN?fa-(6j|aDnYPfh3>IAV;1^&%@op4b_*MTt43@+c$n+<22YRfG@ zdFQc@?X!T(4voI-zd5r;aESmcF zu0mv3F82A0>j^F4R}2uAV|I*osikYACY|L*U(x#h_Imq1FzM9Qvk@)lMF)0w+JPp> zR2AR%m7-w*(&UkLC4!KuHkPW4W`51JpgkDGWXg`QBAikEnArQ?KTGX|3 zPIhcE915&J)%LvmIw!~MP`2-N#yVMD+5`?$wl2Rs3&_bFQpptCN)e1gcxY9 ze&a#@GW)Z&YZg#tiuzjB7VK$7{ATz}ZCdjl#|Q{Kb@4+}x>a()6ctjZr9l*-CdJf3 z)18QvOAh^7M5Td1Cc6JYsAMZq*7Ng@%4SB>b^A-@Yu4tqQO^1jc!}j8pFz7XX4(kK zVb}9bFEg=%SJM}%^5kkB|JAl$>fpii>njg78DjlCMbwyyyZ#vQf?NEmy^yWQMCew)tV=_Uu3- z($dKq5Sz#D`2pc$*&3cj%$xkjzm7u(=U=8trQB{}*J9|0(S;e%27D_1zMiil z8)+P|%-_Z73zbZp^{!(Z4*(;OsXet>M*zb$p3W)0zxZQ9CGp4It3J5aK3zZ&S9)06 zxD_tTuN{r|%G+~nl47%0p>L2vw08%2@O7m?>RM|By;OEo_!*FAjOO`4E}>>Qpypa- zow<5nM-Xu{*8VuBwZ%1Z-`A}mwPHOCp0y$nZO$yYdLo%dSt{+IVi7ALAH~5I+02b* z3`j@9f#$oNlZGVSs@1qD{H{=wpzl0n2Wp%`!x6a(ZdlJU1|s8tPg8$PEBX_R^WK-U z(Kbh9+i^M(qkmz5YEeSJkLROCUEjoA31I;7#1E!L_g7Qrx}J`rdPo5}pPO`lqV?ns zR`EuC)0E0DOp4<9oX3az$1DL3)7hc`xr-p5pbEJrL{r6ROpppzz2NbLus{Q!AA
dMMxt1zvG=XJY(9wXuSIgYnV0tGf&65W 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 OF+k38{@vSuN&f@u^TeY7 literal 0 HcmV?d00001 diff --git a/tui/src/skywalker_tui/assets/splash/prodigy-out-of-space.ans b/tui/src/skywalker_tui/assets/splash/prodigy-out-of-space.ans new file mode 100644 index 0000000..f85da17 --- /dev/null +++ b/tui/src/skywalker_tui/assets/splash/prodigy-out-of-space.ans @@ -0,0 +1,40 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \ No newline at end of file diff --git a/tui/src/skywalker_tui/assets/splash/prodigy-out-of-space.jpg b/tui/src/skywalker_tui/assets/splash/prodigy-out-of-space.jpg new file mode 100644 index 0000000000000000000000000000000000000000..39366b3d6ef0ef4b98a67431cd7afee8b0aa35fc GIT binary patch literal 3430 zcmai14O~)L76+b)p+ae?C4rNf>o}!mZd53>X^wqSsX0@rnVC6iYFWOB0As3k)SR6@ z>`VMrTmCB3(lkp1tTt!H+VU%AGvzgX8|t$%pQyR_p&+>HFL?0oyZ4-P|L6SAITtck ztqioLPN5PA1nZ!0{nro(5Q2ZqATYCVoaIVzqpw;X5&-T5!XW-0Bq$UF%YoG~1cKE+ zg8UbSz(h?;(5`nClf+T?>NyXnsUePt+hb1l)xnwPLldU+RgFGf7PqyZ&$vx6TwkpBZ#0S=nlP?BvFp#fb60FiDXg2qi3pTAn4-wBHYmv@Tg^F9Hz7C{=_#DR;jvY^!tJyt0OM>}= zx|h*)IroHgHoApaJdcEKwdIlr>oRAtIz$pF`r^B5>5#|`y>;};d{3-u3P&q>WG8_3 zoGi41X#G2t#Xi{c!7U(yEF$ORs_bDoJO8VP6xlDYr-&d8wNaa{y3_jG#zN)+&1bxz>gIHMujWw4nAe7__1xKeEh zl78T#Wt7b!cmP^RazVq=>A@Esu0KJjt2pX^(g;P#yuZIjBD9erN>askTZxvB4 zwnv&Fsjv9V&62DYKa}_vAkAb ziIm^a_zt&(*%<9?f+$^KhxM*)>AO&*Ax30ZIl@GmK!7%O$V-cv>drN-k+sx0MR&( z$q`FkZi(>h)Om$|Xcr?dpV^v$3D<{Ix?hHxDowY(H>)~rIuNx*h~JA)mk6topc$aU zr|@i*Wm!jvp<+m?=s4#K|HO}3>e4y~81pzM0l700If}4C40$x2(1uq(ys;K}d18h8 z07$wd{%_ly)}77VUmx~dTCR4RmBG;b{Jwt$VuvUd^Wd^4Ql$1Njg#UWrbj$f9JK!U zeBSA#VhU69b3F^>&4aosfQV8^e$0Nsc9B@ue@C?_H z!UoP@s_L1tb-P7qiD2zy3>lY zk*h9Qd}Izh&kpwKBr&iG#61kkYqQP1kw1aKE4+JfOJB%?%ED+O#m^I)T@ZWNJo1*L zX2uYcqjC^#dX-EtO?T!rvTys+X z8TImhFOr4$pbf81MPA_*Tf%bd#=Pp>DrN3a-^Ft#coY-xZuayh8`(#^q~JuG@M$&@ z-s-n~ZV$qlg^$(Sg$MZm4u&Yqh{6BRWWouf0MKNDNiu<7X}vw}NzUvK_AK_*5|}LO z?aIEB)xt%TxfUF+(t%y`|3{I){Tiq=yGGQne*0;F0dE0gKM~BLmis_vfd`2Z;ri#^($;YUtyKf6F6Ym3cH-2dlmA z8vZ9k=_Gm=Uiz2);}0eO=*)O#5y-whVb%r=U;6UPBETGdb>?^VSGZ_Uz)Jt3zir$% zC3sQj;s8(2)U7d2tF|X?j*H#xG}DQVE&ukRX2x zGlm%e02X|nj{pEjFaQvhWD>TM^kcdd8>~YD!hErDWMl+;fYBGWg0sUH0RVM3--i>n zMfj#+NnC+DRKLOPDammzvZp6b`%Sxdki2fVL?E3zke<0&ia7Fal72queo~cUSJrai z^o_VxFHZ%VdUo1$GJ0zo*S(){9MCseKD@edrmnMAZK@w>>&|0(2U(ss8Vl5xWzDCEyDG?haj~1g3;gC$T7HY2K+bHlfou1hOuZ&2mk3{jIi>&r?I^ zUdji_R-)%3l%4m;)Bx3Cv)NRAQ-RJyB zslJOk;GyC?Jq<{!pMCd&z0!-NbfI($9WC^^sdq0>z}Gz31)fAYnsymEnrM%{9fl=4 zsv;-KpR8xyKS@*4xuy!?@Ucr16@AIy^G-0(Ie;Iu*5nu)HUrIklCnQFUZCrndPTPP z-@?xALr1T_Lg$4Z6EE_Eugi;JAN#B(0ouucn6zf9?m!K4z!S~47hb4*Tki#aRIp~i)3zwW zF73Eckx?&QVn;<^|Gl-7UAAL!yqD&LS6>%L|Bt~B@WoZMr~0dc+w#&Px^e>ip4n0n zbbCHsv%*P3?YX74YXE>N$ix_%r6Q9LM|M6C@n`W4T#OH1xUC>C*O5PdpZ*C{$^k1> zfRM>b`NX$%h#Vjg$sh!ePV|qa#QEhA#+PIJ649>&iMZQ4#^0cnYdI#CJvp=b+YAnz zm6rM3tZHPxE=D!Ye6Zd}d(q5%e^k*G=0ktg&i@q9&Ykyu(cRQ+l6lbs$0NlO5)^!}Pti*42v4X%!u_O9emfVqS@B{kz(b|nr@M9K@9YJQd3Sfxh^n07sCWk}F3Yfp~_`d}RG zY$!J4+2OfbMMXXpJo7a|Z*v$pJIL7Q^^=oo7_SQu1a5CDQ#uZZ?MUXZunQ4A;SxF36RN?Wg5pl!v+)}wY#b|D?VFe^-!-OIsCh=n3s=Ph@Kt!6ZZ9w@H+ zx)n6_5l-3({U&gBbN@PXoKsES;cePfNRb9?8Hu=%;HxV$*&S9=4^hr7&1tqq^Os8( zZZ+$9PL9tP=ySdzZz|>WtiH5a$<5*_=Fp(?R|O@orIl{|>aJVivLlx1_|G?5%dDNV z%h3L~464pd(&x;??--~;g})iWI)?B+2akz*NrqyCg>@j3*?28eo?eYAKsug;BOyGR5Jd8&w zJaXmM$97e0aBGmcaVGqTy{W|yQt`@UrDtDD!OBMSFYV(&|nqC7T{+}`hk(C|^{UPaxZ2f^etIIF(2Y+p*;wke|6vgr(~?%{)T*d5<{ zCO4ejNFcCa2bf6oK|16h#?*KVVr;>PPV~4l<<1E9gA^7-{w$;5qp8x@{ywxS<*L*@ z@-IL5Go@cF>qhIQ>7b+Gmb!TIw<-8vh=DO-^`_j&cSBiVuJNbkXpw|g`x{fE8OTo= z5bx58RHl!Axb`av*PI5ZtV?h1l~Dl%B$rN39*Ld*72h|=N6i(bF+w@v-oEbcp{~BJVXk~v_?jCV!kK^N;W1)Vr7zz aUv^-a?d~zxeGYqGClvsQhBM^+J3B@9^>R@eJ(s>-XyU1^fLA_W$q&oJX*r z!Gj1BDqP60p~Hs|Bf{G!hh4>W7u97fuyOxmI7}1(Di8^Pq)CAzQI=GBQr|#+=wdpM zNOPXQlrC|?)OmAdPM3m)uZ?q9lh@hTpb*l)SL zh=CEFO!-JES9>t8y38tb=T(LuuZ=9)F|pH%7fYrB`LOHKfGf+MeS@>@SGPTL4vl*B z>)xjKrjAD1Ol#Q1hgCifoG^|APPf<;UNE0u~tHfiDI4Uws`}<)9l+L8ww8JY2<7 zf*4+?p->rexFJdMNkiX)?;!Z0ejSRX6NxOgW1@#Jsu&}RClaV&i$^sm;Z!?T)uV-e zWTgX;G8ReWj5c-{q9fd~mfm|%ddCT6=tb$Cluu$=)_R3u1=2L;S@|0bOX||3Tv=YJ zCT&$}>E@c(NyjA%AHf1 zVb;ZGEUW#QD58eWdFZES}YBLUE=Np@Q0~Y_;ZkYptG= zAk(C~s<&sXQOufzu7b)*7_$GH+M4XGy^iqfsMKCZCb5ruj^iM*C%2JebUKuqT%g@-BL6 zD)Y{4(meCCKsW1h26Yyza>~}8aB~l4`OLF;ggSk+)UT#ds%EHSovN31ifroFuOhuN zGfPi_jM%(J9rV*lFmy6hj< zZu-V6hOYbWy!UQX>A;gN{OQBPF2Pc+CqJ+8Qqb=D?H)r7r^jE+9K?{GIgI@v%7=a-8?}D$Wo7Vzpz_m-%(z9Lgh{r_So$H8BjAHbb zC`JD$?_RVMk;ebFIJ+asr;19<9TbU@#4=KGipA^C7{k~^G`5jpaAeT(xcE9cYO#xW ztfRTU=teMliiZ*WUfBi-2^xN5hzXR>49N$`{Soq!?=vJf5Gh3MERu$e+#4h}8AM5{ z0E3STC9BW@%2B!rTN9*SCgI1)R#woHpM+ZhW%;sPl(KTfN+l-PfdO7#z?T*PW&ngq z%ysnAm@8N%5nahlv%#_$K;+;2dKjxq=HMmAR7W-ou+47%5}CmS=P<>I0%f+Tndl7C zGkFnBXNKUMx%3_g*61RRP2`MmG~`G3O2?O44;gqkAfV=O8=PGY7gfkR&asNzcHW za{BC%EJc~HG$2xH#uTSB{hv^8xiwu7HGnK!X$(WUQ=*OZr4;tpXX zo5KGla=3&nZev4>T=w{MX*pe1QwiHv=-y0{RqY^St*X^Z>Q!tmGz|xwD_rmX3%s-> zZ&po-DGtV$SwJnJ;Q~8X#EO%@e_d}|xeGJ*Vpp&T?r(wvykG!3_`eW7YlHzi;i%e8 z!5C(6hK1?a1HbCQ5)LtiMeJV^*KokWEu4YB+u#bf*u)!lag1TC;TnUOuDrQ%iE*r4 z533NZl2vVQIa=ACtk|QnjRaN|v<#`|2Tomg!OKfhlo0kA*Cta7t%+;=XPpsx;o-+T> zOY&5B?|av(0y4jbX3P(_o8n0S1<{F~)1u2;%Sbxf&VmHArB7&M8JC*IH^%aiTMIHw zmubhae({KB9qSp>TGkfsgN|_x3=a2L)Xdqnt!sViThCS1SKck^T%GA*^V-?aE;g}+ z9co>hn%Z3O^`Ap#C?Ho^q3%GonWN0?;cyuqOV%wZy-bxCL15JgO?J0}4DQutnYCCh zx4F;lj&zGTC|GrJ#oYY>c;C$6Izcmm(|l-sTXN5xzOWIbsmXYja60V0v%{Oc=b93M z;%ly^JgG!!jWe7s8*h)I;=AdKIUv^!Xt|u#IqGVM$gAm9AX^J9xs>N+<(&VP@7y5u z!X3!Gy)QTKrEvaiX*WI9%Eoup?@e`tSN+USzqZy(+dJ=)BJ`97xIb-!j?oE?6( z_o&VGvddcBtd2X}=RS3>ke%yz_j=p&es_R-Fm-x&d$iA9*0giHR^DEymB%gjy370O zj87uJ72@xJx*_1X{WsmE-qC$W9-{P~cZnSTdCo(=@ummko+?oV6yiT-{1ho9y4=MDe)2_66LA~5+i zvv+~VcWM!f|r3GD1Zj| zfD#CT3TT25*ns*{f*MGI3r2wxW`UiAf-D$@yvKMwj5&=s{M@>k3SZHKB zV0A0_7R(26Zb&Q6CwA4PHvMOQBf}+a_=b8|1i;sR_0=oOheLlRKkjLm40wfKxDd5k34YRyxUED4h;DT^NYV=^g{ z=+u%asgusglPKAfH_3oa7L+SFlg^lsy+(^nAyMntl#~D0jspdIuGd)yLyxCtm8utx zOzD(dDO*r!F_@P}u2Yrdc$Hm!#n znTIE}mcd6rcu5To34VI%g+mu~pD>s2my0=+0(JR(f;pJTP?-5wbOm*ZakH3x*_n>n zcO-?A1xJ)SNt!?@V!LyierIjHqnfVyf}WWPJ{gOlDU{tpVJhekR_0o z=^2Mfa~$awZIYcYQjw3Dm$Auy0ePVG^>>rGpoio$4!T4TN_bt_5bZg34;Z3GQGy8y zqTAG)FT`zL zrxxjki8@{yigB^&a5-9|Y|40jx2ZH?cJKcf5=sfGP^77*nw(~NrJtIr43MWdaT1bvBasaf>5m(i>A~0 zF2L#l!m6g!%B*dQhUFHrX^Mf%da@ZSVr1H}F(<8bWU{>qvcGtxB+G1kTCaqtk1o5e z=6Wpi`b^@=kUblRKT9$}d$fv$ubBU{ulCibmB@i5tF(K@wCC`ySj(5@hp-S!unQZo zTqYA;JGQHtXJqTOHv6?p%7YJkwzMj-G#j_cy0W-PpdTi(VhOV@>798yl|3S}fQyp( zhOTrQxN@s)c59O=tFcFz3+L%NhC8=-J8OeGwd2Z|J{t-iN44yfv;vB`Myt7i%DJ8Z zw?})ZKv1c{3Zz?cmV3Gu3pq2Ox_@}7RBL$5=A~7LjaFJ&N7S}%JG^^~X}qgO zPWij~61>t$yv|#;Z1$*(P`DJ^nC>Tl&uXT9OP%Ohz0`ZSlS^>1t4!Z3xrH~ovPp~C z>$r^DS&>`5@aslCm$-gOaytK;K7V<&uN!@k3X+prXZIUO`MbIMJ4xuOOuvV|EMU7l zN&&cAz+F;c<6Dyn9KZ?*0t}3V~-pAx)r$IG10o3KT@wk4dx#fxYoTp%hu zuqXVbFFeEZTV`&0!|yw}+tjl8`nc}vy*doUi(8a6tXU&VdpSJBv&O?lrMDX!#NeyM zNerX?`;kfW!9ZK5SiHsMN54DE#qlw^V7$d~<-eZ-Uz>P|?%KhuJH~@q##X#nhGeym zV4-d-pUOLVG8?wgIukSe$HSY){I@#E}2n$e>)FG$*Sco5_|O#hMJosC>!WG_=VOzhwnY{wZihtaeeW zt==HZbVbWeTYa~DbNk3i(rbM;=ExMYnIn+Fh19xj+#SKJNW;8I#Y~~cJj#=-%p$@+v~%V zyp6F6c&ALa^eoM)oXRLD$TyrsJI2pX+|TW+d3~&e5lzYO{F?=x8VJqH8_mib-E`or z!7wPrewNWHchIdE%O1VbB#qJ;7Sg5cPSu*lCjE0Rjf}pW&-dKIJB-cT3s7zRY4SYK zrbg2>JwzW(mumm3)17=SJ^jxWEpS2Y%?6o5 zWVzLfG1hC;(O{j_#YWcXVAkt`)~TV^W2Ddu{nS70)Be2I61{Fv4bg3Fn{=(e<>1x? zz0@tt(7wvoS1s5N?O7Y?*m*R?g0y z+N)iHpw;oV$IpMeci>a-FOOSQ+(FcUE9XJ-C`UI~ zo!#a=VaxyR*Y+)NVcFb2%-y%>-hXJ_^9|qFo!+-t-+kTJ`AyUUo_GG;t)~&+1Rml1 zE#U#K-pV=P6Hc2A{@&=F;T1055bog>DBdED;l;M$91h|p9^#p7;E%1}T?pHz4BaX& zVklnS7rWv*zTh1`lP(U~kUY`v_Tn2+wxGkspl|$-$d@je$L!Q4(L@X=vtoRa&G7aQ0JW9 z=?4E$CyXxN%oyjSzS_8*ww|o4m|p6<>gld-=3Eo%lunc5CF`2*<*3fo3k^{_I=zLC zf>eIzWiISwp69=Q;Eyip?IxYSJ?qE{x)w~_tbXgV7VL>W?AKoG6LO=}K36~9=m5>( z&ED-F4x7;)$6{manf&VP&gl>Uka_#{66jD zNfj`6@Xns_`9AP%uJCW2?PE^t5fAa!?vhY0v)|q>ax&DlE6n?A3!9eA7OPy>jJ*5|$Q)hmYz=*_ot;hT{S6*2{%y)V zEj>uR{lu` zRM?QAws!B_c{->OBgl*-N17Cg@}kOwD<86a=`yCnnIT)!v^I%S&PY2Q^?cOmo26q0 zSAlBv(Wom9Oa*jw*Yv2;q_1px>4J6FRjpZxZS~sKSTM1}$lg3@wPZ?|XV=QC$<{(p zZ$A~)l{8mpUY=HHWt-Y5a44!#Q#?IYnDElYZQp?93i+{QvtKEP9gDea=EiZ=rbXKs z^jpuNMN&MCI`tyIpWUj9#PRS7z@$fu=4{(F+}y2um-fy3xA5S^alclLJoz}*!;LqC zZCJJn+P8N*uYJ8bcI(`?d)E$L_jmE(-|&3^?k*$rLihS~zxw~hUslxX)6Z}ycE`er z1tawz&u5cuRvCepX@;OIg;`~qXs#J0n^(38=bLi=qh2K*+R33_ zc~*Ggo{n5%2pu9aIhYM7mNMIliY{s(!QtgdJgNkGQl0DjAzRq+7hwF72|g^1{5n$rp6i9Y_G`wnq0EK9TzS1 z(L>AJZqv!p;`GC(2)c1qNP}!N$xA1#wbEXv8*El|L{0Y7wv;_HDpc2cZ`MR_jWySA ze?52CU|0GW9jn6JD&KoEoiN}FA7YBlJmY-0&N=^|xcJ5MV(c@ov=Z8OfT1;udFGmL z&bi(Bb`H9ya^DDM1En)?dH|`Xu4$iwtL%F04a@!|42TeZ`$J*sGLPoxTDo3ilQltX`Y`EZ+0x7+HI-$vMw zjm>@h6T#Xv_$rHkX59Lt@A>-spAW$Fk;i?^DFn)17c<(iOo7N5iuV?$7{>vRafzcG z@rGA5w&8>MPECCOM?YEsFgp;HH@!hWc|C=Yi?ro6M>oxMUJ{n?JEuDXGR;4JbDoL_=W-51 z&H%zupNd?XI`6nnX$G{CG7L>TyE*?jPA*WPWUGr28&|}~%`1t@tSChb%1?rhA)6e< zB{y>k(vONXn;0!AJvv%OT_O~A3_aivo#xPa?M!#S6Q%DgiptKXU`znt{0ragn)MRU4D>O6C?j8)!N zBbil!UaGN_W#wZ*_0qs))|-Oe-Ay59Sj6sBw5B{tWlxLRV_jCVbZu=lQwv+ptyV5& zt!?*C@xejfudVxZD{rka+u;A|)wf_}>sW1zT&bBNw^QXIUa#8Q=&}~F!kz0^jf-8Z zx@@7o+-xTcJ5d{iHkpSdZ%&`9(Qh5Ks3TpfdsAxE>`6Dh^pz(|yW3J7+7`SMB*A!f zT3Vc%w_Crh@Bh{tU--6H!3Y*9akD$$Z%(TiaL4Ogohp6(n4{p0eRXUpe356q=cPH3Im2+;~s*~&jAt&an3V3;&`((ff| zf-RlmNN0MxoF??4-7(X_`nbvg=4qNUts_b^xY9~pwSw0y=tb69eu$o5oKbyhSku?f zaQ5<>A&uTy$FJ6DytS@@ookKodfAmiww_JWOYTY=IT8@kokM+7Ad}k1M)q~DbrELE z9+}zR4!5~s+hiW5J0jH%(zO#U<^OJ5+xF^qo4Yh24Z;7)tDt(yyRO`pZ#2lr>f8~$+wWqjesUa6Dui}Eqk4^{lN zX{j9zj)FN#yV;4i>>GdS+^yMcV{{0_O-!=5j#2b}9JIJ-8$u5q!Ko$U6m`-h)R z@@7OE=uK<4a-wbYY$N^3qX-q;Y4PvGD}M2eXJxvz*CoFv2k@VjXnzOa=wBJ$>}+nl z=RXhn&}T~R!e;x_dj59K%QEk1k9+G`uMN`A+VmEFy@~DqYt+Mj_qwOEr>A{J$XDEU zgcUsD8ErX2M18?Cm#^*d-h1dvf78F`JNJZNKjPEN<=a2{@sE;;rc!zyxhgp~|9C%~&6oyiWAw*V$EXatB=!lP~c!an^PIZV}QWuH{ zF_8bLiJQoYop@S#Xnl58gn^ibe^`frh>Ar6iq$uCqnL`O2#bQqgr(?qs~AYF(=dof zi@P|Bz37Ls_>1^wi)e(4X6A~gh>Wqwi^1rN3pb30HZ4?e8kz!o6i9cShmEpCS)|yE z!10Xt6^&LfjhRx7CU}iEc#imnTHMHt;8BU+ND&-2r8G_ChdJp2+w>|r*>OBcEk3DGK)H=wIkuuqluo;u>*e@%2laN80 z@_~M*X#l?oDi((trP-3jnV7Y?ow4a{G--0-Ad+WuTeVk~(rH$-x0(tlHF*EYTu!%7 zUfFzKS#@AZpIs@HD+YIxC=9*Xe`+&;=t+dZnV##}fqVHWTG>JvC7<>=pAYJw5n7)G zwx8o5DgHU2r}u2kBA^9Ipu^{yD~OzQnVjyEZ`8?H%sHG)c26t%c^z6ihZHrMsevPU zoZLyHT85&FrJ^kmD&pj$p$DVkz@B=Tqn~G^#;K%;xnxr1ow~rE&6%47iJg%+q(VoK z+jXR#m!#cUqMn&#PWq*|5T!0UrRwIP^_XP%aHVH-r0CV9acXQG_YP>%qgz;rQiEb` zN^69voVRzQPfDVL3S*~Mr&x2RbDF3B37_4_07F`&D0x8cnUyYCsoMXErzVl9WvZc8 zv|T}TsQ7uGF#)LunsOCFsgA0QstSym`c`XrT;c$#H}t7}aTpSMX`sWW@w1q{>Z>nG z5#vdw!P=a5xq-X7J-@1~%bIVAY9r1%ruU|+S{JGF$cpM0ILxZ8+bU+iH>;V!sZJTK zuZ1~kSFKrkt*ogs+{&))nxfunt1eoRI>dj%8m(@DtP7}j|mCwW_kvNkv90 zn74(i%;$Ir+b-@ZwW}njA*wkLn-voKvO&6}PYbnGIkjQik2ngZ$9i#G>zu^un7WC! z{^&eja;|8Ku^XGQ)~Z9#Uy`% zl($^Ew>8?gZ}6ldD^7@=Bg(Td-;>7qUw#<7i_whP*XzGDB`5}cb01w^qrdSqhvL(@K!mi&+-xvVlXtbOL4)%)N6PzINNfKzPBEWo{mfWLu`eUE4YCTcsa3 zkS0u)%@dc1O2B_doOyX&&)c(Mb;F2Ru@%vr7|OQu3&EmOU_(sA2@0rUgS$zbQ8e7d z;`zkkDXx79Izpqds8PX*Teph~zNsn)C%eT+j2qJ%y+P|>!icS)3lS{}r3ZAvPj|-K ztGp}xy@Bk)ZY-2??7Uh-$Lou}rjfF_fU-_Jtn~l-!%^}~R*b*Ra=%G+cH^3asnZwFWTFjsf%VkT;%KX8}491n5twpR3 z%z}rh47MIysL#f0R_MjlJIa$>&aX?P%gmU*$IgiB&Z|eE+$qoB{GIfC&-UDxoT@p? zsjsWs;&)X(9_iLU?Lc#7GHJsQpr?4q|x%BI=Vi1^c=40=M% ziA23D!JC;=9L>R}#*G`%SzXh23DqJB&UdN9Q%u#)j47^4y0Jx!T)ou}ZOtv4z}@_s zVa&n~N;kCVswi8K} z2+XWst%*|If)l&8#(39C&ALTM+1&qa%yPYn;4Lb_tI&{pWI(#Zo2}KV9o!0}-rE|v zL;cZYE#7_nxO|Pmsx{wyT;G$J(r!K6(YD{(+TT(A-{nnP0e*>@g4Vp`*D^cWVT>&Z zTiJ?C+ZZk43a+PnEZz8w)C>JTzU`uG1mYXs+zrdcy4It(&8YFKGRQp9jfsyxDVhQi zm?*8w_$}ijP9@k};v#X(4Lsq{4aG5fRtY{=WTlDo|or^ zo;i8$-ai`P@!d>^ZUvBT-;4i^TR9vg`OW3K{Q%f3>8;f17Vzm15bDizm>o>emYdUX zUBjCW%u)HF-b;<2G3o`-=`7Ic(8}YUvgJjd+Px0znZoI<-dM9v?81IprQTR(J}HA+ zgo@tiw%y|fG3#G^<6Q`tAs!)lZiU(&vU$$!0rBm&;qAt564idw_v|{>6bIqX;eo!+ zr-|*~!R}-Z?&J<>ag*muJIjz^Bezs;dx@y{{W=!Yu$J*^W;q5Q#|pLE^L~z^t8V7D}Ox2F7?eq z_5N;wPoG*^4=P;G>Q#THWIX61?@LSe^jIq@XAh4~fApV{^HbmUa^dw*5B9(w_lAD= zN;dawU-!o{>x+)`w>9vLhs|iOIh|hkOzrS(9rMAy_d4JA(6q#$^Y~BQ^GnL?D)9Gw zO!Ue!?7(dJi5~ZfZ;7Vu>!ITKoKE?9|0!^{`jM}R9v`t!zWJlv`QH)m2Y=p-4Ia1d zhJ26ptUZSY-!BGF{F=R%3=FFVWcyT3@a6-piKENyooP#&7S)Pf2JW<%~M` zp##K!i|}#&uDAcl)zy#a`z`xf>*uSk>zqIGMgNKAFX-nFNJXmt>(AZopSH59&tWg{ z^Y8ifzxzq8Jk(5yUC;XaTlw)1^O;=S+v@DQpVWH3=C`c;Nv%B8Oo?63`ukh?@elKv zT-@90?7N@TdcNkjto%tGI?j#&X2nHMT2Zvxi?fzl`tLvz6oKXm0xBS_EDFc+SHOH;qyhsdchYcxRVCR;DwXcG-k&5AvM(iX%)ExS1|cj#sFEkmC0EpDH?hOQg}&1 z;>o%JbQ%8xib*>Z5V9o%Zrm89wHqqwqVK1YQ602xk}u1lU#{4Kuww-v7npFl%8V8PIrjTXRaytWB@ z#t>luV*Bbj)aH)@J&5Qo?wbaZpT&_Jxsfzk@*zrV_9SLRN|2XgOhak$a(U&OO?C>*u|*Xz|2tnM+2ZFuhI zf7#tW`}^Pa%%AX2&T>2`20%N;F=w4~(qKVXBUa6Jf-HYINDy8#kw;X8z>&70Wd0FG z*o0N>H-v~8O66CI5BgT3iX?)l1X~ow^p#;J&gjlyC`ibn4h@d@M-V)&NTi1Sb%@~) z7A}b-j7~~dWRQJDNg;|<0$JshalQEDaWalbBVsl(mZM`c3~7K_Xp)0pno>}dP@Ml0 zO=rP(-?cf9ofZmd=2>d?`KO#AO#`P$&$RiYpC1j%=%5i1+RCGHN^0mz83tJ9ICNGJ z<~uts+UTSyA%*9g6a+fwrupglS*cO5+N2PuQc7z|sD^5)0=MQ$S*_gQ3M`rtBKx)pGyv)xJD?<&Nx} zn_gIM{eE0J#;*@$VeH70+c5BiuZxB9##<%*maAj?f$y%bTmAL3XAye#yLX$r$&vDI zj`o(qg%QGjUjR}Cf(wqy)T=f=a~f^xNeTVqe+xi~$j?2y8r0)jCXfGMG0J}eVc^$7 zazF)2P#QFg;6^ah3T+_81<4Y~vL*<^1!1Uz?~C8jPRI%edZtip;uZg%;lK`H=z{k^ z;R7kvC<{U$gwu#&D;$`^9r7@Q6%--@{$#wB?B*BZ6())Wv$W8Y8A{Q?zX5`+R7&M#=xuj*|&obT-&Km@Uo- z!Lkt~cNt5u40D=p(`7A9xt8fbCxP;TSueXeO<*=nnAC(&XpT9MWvWV@`2dzP$>_^5 z4HA~G^cN-AIKo6Qve!9`12@U8zfg(=@NK<%+NuTM; zs4{qh6Cac_7#11&P>||#r2Ql)KX2L4HAXU}Vq9rRP3q8YEfl6Aj3m}yM*DRVY#R=7;N>x8iH9%CksX!fK^_$U98ZVjHH7Sy_ zfnc@aQjM9^n*A)D2HRRPQ)tZzT12Qrb-`K9T2{UBrmFv6-D+N~`q#VaRafz}q0&I8 zRi;8Nt`x+8IO~_zu!?n1=Zq{~_48QA1~wUN-6@^eDK)GH%U!y2+NCgXMMB!NrZ;UZ zYhO##ma1}-Gezi2Z7b7C7BsfFT`h0_Wxm_Zvq0D^Ku`C{F_}d&qQ#--M}K=;miE@Q z&?TuOXNz5dN_V@+)on?q3tRA37pMQzDRH%V%F1<@wu=>Q5C7`7iMo=mR~_3_y*5<+ zwb#Cu9jpd2%L>35RpgGP7dziMntnto!81Ft!<)=JX|D` z*;^Q`@{`@HWhf`K&0XGdZ)^MJI?FlEIsI^_;5?$lqPVVYg&AFajOYg@I>U#?fa7|+ zVhJ01f{QjW{u*29dV+XULG-JqVI0juH>=T^E+=29$<Y-k`tEC3* zsHHVS&JxSZXeO#TJjA@ufh*U-2B7Db2JB)VJK4x4cC&{KZDR-f+0s__w3&_VB_ZR=rze8AJR>&I{Y>5_MR=Ob4+&vAa?lh{g$`b%}#cHt9|TYPdnSozVElM z9qk)pE;4^jcetND+FK_Xx>f7;yw5%Ef|tAD3y*lhD}L{c5B%aC-}tsmUhl`YduQ$W z_{pmR>G`!e(@n0}BNqMh7GB}fecu0b&i`CAqeuN{j_&j$njZA6hy4*%FX*S!e!;UZ zeP?RV`_AD0sk$t9)^Y}X(5t?jo_GDpb-#M&e8n|z3jD*{pvUU z_RwD{)dN8E;B$ui;1@sWJv01IpW1AQL;m*H_Iiq*mI)p`{@X+o9{&d`?58swA%F*% zf7yqC-ywGd$Q=Rr9S-<55C}I0cz_bPAq&`kOQnJ97fT&wQ3_ar=h1=Oac0WVf96nv z0I`Az=z9f7f-Sg!7$}39M}wb1f!lF|FBF3?*boSKgE`oNtyhDx1cF@(SciG2hkMu^cW8fpD1d$wE_~R2eTOnB*ad{Bh%UGtLb!;J2#Hm= zgLX2B1E@BG7>P|NhM|Xk)3%7`*K>LzHq5q&hnIQJM}FT&h@eP(pGJz{(Gg-(gw*sE z*Y|=Wh;MJBiF4SBYWIn;*NLR}HlyflrI>h;hl;_-iIKRBy~v97RvNLmU$aja@r)P7 zY&3(3uGkST){dJ8j~@RxIlMMpatHwTsDR~S_$H53n31@c zc2=m6&Pa~+#*YeOqmD~m+M%G4|tavD3PiN zYP;rl9C(0335NRDYntYNzL=P335cvhmxw8Vc`2BI`GkA9G<}JT7v`BA8JX+2k%nkq zmN}PVIeoR(l+gcpfeTiTFgb01*>j4CgPO^MpGljV7n@Njl5%;LKDm{_`I$iIXGh7A zo@kS-iFfO0oS@g6$@!a;*?sqAn<@F3(V2F;NqJ5Qn0^VIqk)-}DU+{Be`cw96R2&~ z=#61QoNq~qX^D>z`JT!tfE#I*-&ve?GL`kIaoU-l{8^qzDV#qznl(wF4RL~PS&}~~ zp2x_1h**CBIG}GBmQO>U59*u;h@al+pOBfL@EM}k>7W$4p(E;=C^~osdYP*Ep$l41 z4LY0}il9rwl+6i1{??xXs-oAqfgVbuBC4N4T6y7Vq6#XXAL^sN38F&^q(@4mG-{M% zhMGz$nU(*>k_p0iJKCMF!lkQup)Bg8DLST2TBOt|oBvs&E2^F+TBZQnrR4c-ieoTU z8lVdE*NnmMQi`lodVn}n*U`-YXXX{7qNsQXt%{jyYrnxm(1 ziEQbZeHsUcI-P+Ush-NFycw#b%BYEIsHUo$N$RMR*{E?^T3>dE$oZk+5pIUN^okXJSv^r z3Z+kao7pI!D*33til~$Mr6x%kwFs_LiK~dFtL+nQy;`AG$^g_V8rXWT*1E63+N}Os zt50qjT|D)=04!tD3F? zO|Fz$C9`;vfwE?ovi&*$kWj2HtE>UrvQqG}3L&%qYODhrtS}3(4}h@33JH4@1;zS7 zS-G;8x_IZOqZsnDJ4>@b`?O9wv@xrtdx9YhnKm3tScEyTL4&jb1dOhBguFVDxm0W- z8=>tIE3H1uw>29DLW{S-TC^ak zx7*~k$pegR3qW=YxRV>Tf!nxvdbK#2wdeXkxyc%zJ2YXNV`FP}_o#JNcN497xyAqH zw$TB)g4?rwo3kt%tvDO4wJWnOkg&Teth!6EMEjV{_Xn@Ly2abN33|J6YrAoqyezxC zv}?A;o4IgHy%w8c$1A-;Q?}$Ee zs{-yjyYTC~?<>EF2m!)NA>|vr==;4DyT0nnyanvMGP}O!JH1~^yh@|M+MB%|kT~6o zc@pfpm|MN#YXM?bJ|QCm^ozT&8^6EXyNI#AF+sm6ldxX_!Ul;wAVN$g(8AEmvK(x~ z0Fba&5=Q_r4jY_lx^jF>DFxU9^hJk7DTr)_DtxlGMiWy!akyTHuBfcq7c zJGUcDw}gAR=6tv4EV^^^DOit;grA&)uAp*m=qF8PK4L&?z0dCCze0ve7)P&=HKr-B`h5JOdZ)(j1J` zuAJ1dJj+jAzZAnp#FQ3I?a~RXQE4-1a?~b94L+CmNnkwGX$Q!H*iLNMi_O@X{MMEW zz!tpF5uDgG{KSoI!wKuZfyP7)EXk2w*$f=iCzGX=P0Z*t)P?`;v4?%ylMUL6joGn% z)lN*mn=RR<9l)-g#S1IjVy4+aJ-nSg+MoTu#SOu)o!f;;#-)wiqOGN4-CX)h(GxAv zIz8RfOu&|n)@m#TVnzaK{n*sqKhr$WJ6+NHY~D+}-8hWM>&?os%gOsgK+kML<$c}U z3`5pk-S|Dm=l0X+ecvZ-($G!T{F~GKZPM{A$k`3Qa}3Tw+bQ2|)#0t*#pCO&&q@B#Oup@$S5#Bk8plC#R1i>L7^DeS1T;ty1_hL& zNLK_D6hsjdS`fhjqznp(lmw(oNdQBapeRLp6G1|eA}t08k&=)C0TPmsT+kV>cdpkp z56+r-xaZ+KoVEAbYyHk%zx`k9obMA1^TM4yjML|Yduj%$hhJcSeI!X&PSk6D@e*s8s||r?FpHBxim#RP&8#Q zrEWGZ@uaFV1HR31R5=hM*VTSnVkE``opEVG0fyZXgN14}1V{`6*fR-|_|3?P*}&N( z-JZ@T=(f1_7KI&HViORBPLz&~7CAmLZQIy z8^~-)5I#wIPJ}qq&)+#jn0rgwu4-xJ{uo&`g4XvXB<&zoxU0YLAhlm6RU=89Bcw;u9U;#Ik#HD_6P2_` zBG*Vam5$DmkCE4Y9v(=etia}ns>w?)D0OTyHD@Y>q^s(J3nonm*v}jon^vf|%ilrF36Qok2p|u?TLy2sy)o%BvgKAXyLT^q854KM9HFI_Xmx-#k0YS>Lr7w@Wo?r z>hWC^I%Vu4z(UsCq$L6GwSge!guVNOaO21_%VOCDF!xAca%T`R=P)rF*(rs;ml;HB zf#AV9v`lRxu9N0r{#JHu={9GMtXZI9zt^X4j@h%kU}^=_q5A{qH^BsMHVtKlPXnVu z0kl|$XGuD9DR3cIPW>F*oZ+d1Po0|yhl&*m(w|{I*g!8UBBt%T7cbN^ZOP0kz)}Iv zhgw0FSvRv0ywqO5bjY6NeAKbCekxFl)fUWZNBjyMDz^gqDtL1|1It~>L zUsY={0ptOVWtiDeB1Lw2N69>uGn+0*e8QQJa9|YcEbJ_~MY5+gKsd=`4AoGEU@%#- zzCl)So>ylc_}HbRWV$eykpO03Vb-ev4n&&Hgmcyg_n|op9F8Cu6H?E)17-x4tP~}1 zK2Ov!887HwegI#HFJUah32V~>9e!H^q{IAeEq%SU<)9q%gsM;1Ti%at% zhF1$g*M+uid?y)t@l!91>vUj-gz3$VN2Lnl)NZNNqV#_do{)L9InH^JA8IrzJS1Y2 z^zh1o*^sW%fkP_z8h;(j(@S|lG1ceQQ_yUoyK6N_U`TVZg#D!pzc?C#HHDA6-iZ$M zxup0|l$SSjFAy3K60|4&ho41Z@{cpOM+xS07ZxfbZ`do^hI>CRdl6jzva%%OXO18v2d)q-zRyBbN(ktV!ns4EL&I|satSMM9*c; zuynFo_<$+G?QB<*%)wO2n@!25r=F&I@mk%Py?+3q9_qc)vT`4o`zXyrUUR|{mF{UO zsKp=Y3f!Z4%JE#!sqPc+lnrz6p^^D}d1LSXnwwGRlQLYPVEyn~gD%OS>hQo_D{*1M zKz1ZB2*CKiLd5AoEMyFgAi&q#FOQ+wQWF;BH1!}XXQ>f~9 zoiK}+!_Y*e9rXRNie=%NU}kHrL?v@+_GT2h>oP~<_GEv-Sa4S`ht1-;Jvrt(@b;&- z8q{H%u*--S=eb>1cSJy4M67`mVRFSw7hNU%k0#$p_o3Tbr4DmT*h`0qMuQY1%WZ&= zeY;O8$oJx~ASK;pF$YcQeG+HSx}U;Yis@?Xc0@W_Zwfa#I*Z$z4-&)PKm9Nf5o;Kk z@8qNsIF#+A1_SO%0Wf4It?|T^Jy18~>{O(2j-PXaWOzQ*<&*~~C+UKd4!}HRvLmfv{r6k_$uyXQM zTfXxc)Xm;WZ0z&zc-YjlALY(llqBTw0BdbB|H>xSm|Ww1SEEFpQi^|8EfJ62pYDW> zSTNb3(_0GRDBDeW(ayE>_%j=92}O}_{ELcDQ0Hd4%YStBeqJCQI2Qt!Gga(u)vjGU zh2Zsf>958F*mY@4lSqdhA zMy!yRI{!!V96_Kp|vQEJ~{n@K(1+x_*(hkP_kegg1M|sGsv!4-#kQmdI z*Xm*$%VOLE2d`WMFZYZlo0bob^l8zkxOg~iWjvvT&6qn43Cbas=xCkk*s#5xX`zlr zEun+qfgBdqo~vsZJNRu6Vckhqe%$~vAg0aV_3B^-06~-DU(;YtRxbOx_8s4 zadSo3QxBimPyq$m7K@6f!b#3oluU-55X~jH2-oo5J($VkdKu!qO8fwV>~@-(el!KC zry-Kv;U=cbo6&s#{jb>>-A;pY0BnMd1 zEkYAdeZlCpiq$sd-K9q&4O%9p?g3wWt?0+-peKR)He2O3csap@uF!v=>llbq;vms` zxx>`7U#i8rUuZ_~sQEP*IV)RPAzFS`ZfiF>QyLdz%Q>6HW>nN8THFIBl06?`BA<-PrEZNe%Ihy&itKc$KqHm7 z1bkoP94}llE4nsa#w>Peo?fh4g>(~WtU8XMn4E^;1)y(4N`7VA&{P7QE5fh9is#$pQPe(lS^&^(dkcW zQ0O_^*`YFl1d9X07097ryJ0`_55vCe8`$eZL6wxh z0=xAeuuJOYxJH#5FqXf>j^f(=f9&6Nx36Q@`uA=Z{VMixE-|jXUy;oA&ysolNiwKs z>ypv_mSjkBA9uUMx@4@@B;)>fO9r>c!N5Vs2f8b#p8UIHbYrh6ZEXtG6IE8kQnJ2~ z%$4sVnd>^)cEPuQWT+JlJzJAZm}neuB+E6doZt2fvs1VP{v7mAX8$(y{r@cJ;E&Kb z-d}@$`mcdr=JcP?bJn55{|KFno9Fup=8NML{7*PezvcD6_c-0YhFy#s@n0z>>pp+@ z#}vbhNiJVcF=buLG1LCmWcD{w42@49>NT!(CEBcLaIT!m5AFD?Q%o_RC%?t}u#Dos zSH#*)(!Zw|>A_|;jtvt5)ayGy;U)FmW1j^hemp`ct0`=ogl52&BWrcGpHqy%pB9Yw zphl|R4benD)%=esruep?bAO`cWo!3~TwKS*El*nM87L`Pf@MxQddZyp#nDgZe_Xn5 zAWu&}Pmu3*x6dO?LASg?H@rZ%K0lF>QC3n`QBqS<;j>(OPoLUker%}u_1nJy-^;e& literal 0 HcmV?d00001 diff --git a/tui/src/skywalker_tui/assets/splash/space-docker.ans b/tui/src/skywalker_tui/assets/splash/space-docker.ans new file mode 100644 index 0000000..f7fbb4f --- /dev/null +++ b/tui/src/skywalker_tui/assets/splash/space-docker.ans @@ -0,0 +1,32 @@ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ \ No newline at end of file diff --git a/tui/src/skywalker_tui/assets/splash/space-docker.gif b/tui/src/skywalker_tui/assets/splash/space-docker.gif new file mode 100644 index 0000000000000000000000000000000000000000..988ff0dff81cd3caac3bb6788989809a390a5229 GIT binary patch literal 37889 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 literal 0 HcmV?d00001 diff --git a/tui/src/skywalker_tui/screens/lband.py b/tui/src/skywalker_tui/screens/lband.py index 5a91a65..22d9ba4 100644 --- a/tui/src/skywalker_tui/screens/lband.py +++ b/tui/src/skywalker_tui/screens/lband.py @@ -4,14 +4,9 @@ Same sweep mechanics as the spectrum screen, but with LNB disabled (direct input and band allocation overlays showing what service each frequency range belongs to. """ -import sys -import os - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "tools")) - from textual.app import ComposeResult from textual.containers import Container, Horizontal, Vertical -from textual.widgets import Label, Input, Button, Static, ProgressBar, Checkbox +from textual.widgets import Label, Input, Button, Static, ProgressBar from textual import work from textual.worker import Worker diff --git a/tui/src/skywalker_tui/screens/monitor.py b/tui/src/skywalker_tui/screens/monitor.py index 61515cf..c4814c4 100644 --- a/tui/src/skywalker_tui/screens/monitor.py +++ b/tui/src/skywalker_tui/screens/monitor.py @@ -9,7 +9,7 @@ from textual.app import ComposeResult from textual.containers import Container, Horizontal, Vertical from textual.widgets import Label, Input, Button, Static from textual import work -from textual.worker import Worker, WorkerState +from textual.worker import Worker from skywalker_tui.widgets.signal_gauge import SignalGauge from skywalker_tui.widgets.sparkline_widget import SparklineWidget diff --git a/tui/src/skywalker_tui/screens/scan.py b/tui/src/skywalker_tui/screens/scan.py index aea998d..ad7eb0b 100644 --- a/tui/src/skywalker_tui/screens/scan.py +++ b/tui/src/skywalker_tui/screens/scan.py @@ -4,9 +4,6 @@ Multi-phase pipeline: coarse sweep → peak detection → fine sweep → blind s Shows progress, spectrum visualization, and a results table. """ -import struct -import time - from textual.app import ComposeResult from textual.containers import Container, Horizontal, Vertical from textual.widgets import Label, Input, Button, Static, ProgressBar @@ -157,7 +154,6 @@ class ScanScreen(Container): # Phase 1: Coarse sweep self.app.call_from_thread(self._set_phase, "Phase 1: Coarse sweep", 0) coarse_step = 10 - total_steps = max(1, int((stop - start) / coarse_step) + 1) def coarse_cb(freq, step_num, total, result): pct = (step_num + 1) / total * 100 diff --git a/tui/src/skywalker_tui/screens/spectrum.py b/tui/src/skywalker_tui/screens/spectrum.py index 4c171ad..74a5c80 100644 --- a/tui/src/skywalker_tui/screens/spectrum.py +++ b/tui/src/skywalker_tui/screens/spectrum.py @@ -109,15 +109,30 @@ class SpectrumScreen(Container): def _start_sweep(self) -> None: if self._sweeping: return - self._sweeping = True - start = float(self.query_one("#spec-start", Input).value or "950") - stop = float(self.query_one("#spec-stop", Input).value or "2150") - step = float(self.query_one("#spec-step", Input).value or "5") - dwell = int(self.query_one("#spec-dwell", Input).value or "10") - lnb_lo = float(self.query_one("#spec-lnb", Input).value or "0") + # Validate inputs + try: + start = float(self.query_one("#spec-start", Input).value or "950") + stop = float(self.query_one("#spec-stop", Input).value or "2150") + step = float(self.query_one("#spec-step", Input).value or "5") + dwell = int(float(self.query_one("#spec-dwell", Input).value or "10")) + lnb_lo = float(self.query_one("#spec-lnb", Input).value or "0") + except ValueError: + self._update_status("Invalid input — check numeric fields") + return + + if not (950 <= start <= 2150) or not (950 <= stop <= 2150): + self._update_status("Frequency out of range (950–2150 MHz)") + return + if start >= stop: + self._update_status("Start must be less than Stop") + return + if not (0.1 <= step <= 500): + self._update_status("Step out of range (0.1–500 MHz)") + return + continuous = self.query_one("#spec-continuous", Checkbox).value - + self._sweeping = True self._sweep_worker = self._do_sweep(start, stop, step, dwell, lnb_lo, continuous) def _stop_sweep(self) -> None: @@ -140,7 +155,6 @@ class SpectrumScreen(Container): sweep_num = 0 while self._sweeping: sweep_num += 1 - total_steps = max(1, int((stop - start) / step) + 1) step_count = [0] def progress_cb(freq, step_num, total, result): diff --git a/tui/src/skywalker_tui/screens/splash.py b/tui/src/skywalker_tui/screens/splash.py new file mode 100644 index 0000000..5a19ce2 --- /dev/null +++ b/tui/src/skywalker_tui/screens/splash.py @@ -0,0 +1,151 @@ +"""Splash screen — renders 16colo.rs art on startup. + +Pre-baked ANSI half-block art (.ans files) display instantly — no runtime +image decoding or terminal protocol detection needed. Generated by +scripts/prebake_splash.py from the original artwork. + +Randomly selects from bundled artwork on each launch. Auto-dismisses +after 5 seconds or on any keypress. +""" + +import os +import random +from pathlib import Path + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.screen import Screen +from textual.containers import Vertical +from textual.widgets import Static +from rich.text import Text + + +ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets" / "splash" + +# Artwork catalog — stem (sans extension), artist, title +ART_CATALOG = [ + ("seti-satellite", "Illarterate", "S.E.T.I. Satellite"), + ("dialtone", "192.168.10.13", "Dialtone"), + ("so-far-away", "Blippypixel", "So Far Away"), + ("prodigy-out-of-space", "Jellica Jake", "Prodigy / Out of Space"), + ("space-docker", "Blippypixel", "Space Docker"), +] + + +def _is_kitty() -> bool: + """Detect if running inside Kitty terminal.""" + return bool(os.environ.get("KITTY_WINDOW_ID")) + + +class SplashScreen(Screen): + """Full-screen splash with 16colo.rs art, auto-dismisses.""" + + BINDINGS = [ + Binding("escape", "dismiss_splash", "Skip", show=False), + ] + + DEFAULT_CSS = """ + SplashScreen { + align: center middle; + background: #000000; + } + SplashScreen #splash-container { + width: 100%; + height: 100%; + align: center middle; + background: #000000; + } + SplashScreen #splash-title { + height: 1; + color: #00d4aa; + text-style: bold; + text-align: center; + dock: bottom; + margin: 0 0 2 0; + } + SplashScreen #splash-credit { + height: 1; + color: #506878; + text-align: center; + dock: bottom; + margin: 0 0 1 0; + } + SplashScreen #splash-skip { + height: 1; + color: #2a3a4a; + text-align: center; + dock: bottom; + } + SplashScreen #splash-art { + width: 100%; + height: 1fr; + content-align: center middle; + background: #000000; + } + SplashScreen #splash-fallback { + width: 100%; + height: 1fr; + content-align: center middle; + color: #00d4aa; + text-style: bold; + } + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._ans_path: Path | None = None + self._artist = "" + self._title = "" + self._select_art() + + def _select_art(self) -> None: + """Pick a random artwork from available pre-baked .ans files.""" + available = [] + for stem, artist, title in ART_CATALOG: + ans_path = ASSETS_DIR / f"{stem}.ans" + if ans_path.exists(): + available.append((ans_path, artist, title)) + + if available: + self._ans_path, self._artist, self._title = random.choice(available) + + def compose(self) -> ComposeResult: + with Vertical(id="splash-container"): + if self._ans_path: + ansi_content = self._ans_path.read_text() + yield Static(Text.from_ansi(ansi_content), id="splash-art") + else: + yield Static( + "S K Y W A L K E R - 1", + id="splash-fallback", + ) + + kitty_tag = " \U0001f431" if _is_kitty() else "" + yield Static( + f"S K Y W A L K E R - 1 / DVB-S RF Tool{kitty_tag}", + id="splash-title", + ) + if self._artist: + yield Static( + f"Art by {self._artist} \u2014 16colo.rs/mistigris", + id="splash-credit", + ) + yield Static("[any key to skip]", id="splash-skip") + + def on_mount(self) -> None: + # Brief delay before accepting key dismissal — prevents eating + # keystrokes from the transition that pushed this screen + self._accept_keys = False + self.set_timer(0.3, self._enable_keys) + self.set_timer(5, self.action_dismiss_splash) + + def _enable_keys(self) -> None: + self._accept_keys = True + + def on_key(self) -> None: + if getattr(self, "_accept_keys", False): + self.action_dismiss_splash() + + def action_dismiss_splash(self) -> None: + if self.is_current: + self.app.pop_screen() diff --git a/tui/src/skywalker_tui/screens/starwars.py b/tui/src/skywalker_tui/screens/starwars.py new file mode 100644 index 0000000..9429e42 --- /dev/null +++ b/tui/src/skywalker_tui/screens/starwars.py @@ -0,0 +1,448 @@ +"""Star Wars Easter egg — ASCII Star Wars via telnet with offline fallback. + +Attempts to connect to towel.blinkenlights.nl:23 for the famous ASCII +Star Wars animation. If the network blocks port 23 (common), falls back +to a built-in opening crawl with ASCII art. + +The telnet stream uses VT100 escape sequences: + ESC[H — cursor home (frame boundary) + ESC[J — clear to end of screen + ESC[...m — SGR color/style codes +We detect ESC[H as "new frame", buffer the frame text, then render it +atomically via Rich's Text.from_ansi() which handles native ANSI styling. + +Hidden binding: ctrl+w (W for Wars/Walker). +""" + +import socket +import time + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.screen import Screen +from textual.containers import Vertical +from textual.widgets import Static, RichLog +from textual import work +from rich.text import Text + + +TELNET_HOST = "towel.blinkenlights.nl" +TELNET_PORT = 23 +RECV_SIZE = 4096 +CONNECT_TIMEOUT = 8 + + +# ── Built-in offline crawl ──────────────────────────────────────────── +# Shown when telnet is unreachable. Frames are (lines, delay_seconds). + +_CRAWL_FRAMES = [ + # Pause on black + ([""], 2.0), + # Title card + ([ + "", + "", + "", + " . . . . . . . . .", + "", + " A long time ago in a galaxy far,", + " far away....", + "", + "", + ], 3.0), + # Clear + logo + ([ + "", + "", + r" ________________. ___ .______", + r" / | / \ | _ \ ", + r" | (-----| |----`/ ^ \ | |_) |", + r" \ \ | | / /_\ \ | /", + r" .-----) | | | / _____ \ | |\ \------.", + r" |________/ |__| /__/ \__\| _| `.________|", + "", + r" ____ __ ____ ___ .______ ________.", + r" \ \ / \ / / / \ | _ \ / |", + r" \ \/ \/ / / ^ \ | |_) || (-----`", + r" \ / / /_\ \ | / \ \ ", + r" \ /\ / / _____ \ | |\ \---) |", + r" \__/ \__/ /__/ \__\|__| `._______/", + "", + "", + ], 4.0), + # Episode info + ([ + "", + "", + "", + " Episode IV.I", + "", + " A N E W F R E Q U E N C Y", + "", + "", + ], 3.0), + # Opening crawl text — the SkyWalker-1 version + ([ + " It is a period of civil engineering.", + " Rebel hackers, armed with USB cables", + " and logic analyzers, have won their", + " first victory against the evil", + " Proprietary Firmware Empire.", + "", + " During the battle, rebel spies managed", + " to steal secret plans to the Empire's", + " ultimate weapon, the GP8PSK USB protocol,", + " a device with enough power to receive", + " an entire satellite transponder.", + "", + " Pursued by the Empire's sinister agents,", + " Princess SkyWalker races home aboard her", + " DVB-S receiver, custodian of the stolen", + " vendor commands that can save her people", + " and restore signal lock to the galaxy....", + "", + ], 0.18), # per-line scroll for this frame + # Star Destroyer ASCII art + ([ + "", + "", + "", + " . ", + " /|\\ ", + " / | \\ ", + " / | \\ ", + " / | \\ ", + " ____/ | \\____ ", + " ____/ _______|_______ \\____ ", + " ____/ ____/ | \\____ \\____", + " _____/ ___/ | \\____\\_____ ", + " /______/=====_____________|_____________=====\\______\\", + " \\ \\============|============/ /", + " \\ \\ | / /", + " \\ |__________|__________| /", + " \\ | | | /", + " \\___|__________|__________|___/", + " \\ | /", + " \\_______/ \\_______/", + "", + "", + " >>> GENPIX SKYWALKER-1 DVB-S RECEIVER <<<", + " Firmware reversed. Signal acquired.", + "", + "", + ], 3.5), + # Credits + ([ + "", + " ==========================================", + " Directed by .............. Ryan Malloy", + " Firmware by ........... Genpix Electronics", + " Reversed by ........... USB sniffers & gdb", + " Radar Scope by ........ P1 green phosphor", + " Theme Music by ........ 22 kHz tone burst", + "", + " ASCII Star Wars originally by:", + " Simon Jansen (asciimation.co.nz)", + " Sten Spans (blinkenlights.nl)", + " Mike Edwards (terminal tricks)", + "", + " Telnet blocked? Blame your ISP.", + " ==========================================", + "", + "", + " [ESC to close]", + "", + ], 0), +] + + +class _TelnetStripper: + """Stateful telnet IAC sequence stripper that handles chunk boundaries. + + Telnet IAC sequences can span TCP segment boundaries. This class + buffers partial sequences across feed() calls so they're correctly + stripped even when split across recv() chunks. + """ + + def __init__(self): + self._pending = b"" + + def feed(self, data: bytes) -> bytes: + data = self._pending + data + self._pending = b"" + result = bytearray() + i = 0 + while i < len(data): + if data[i] == 0xFF: + if i + 1 >= len(data): + self._pending = data[i:] + break + if data[i + 1] == 0xFF: # escaped 0xFF + result.append(0xFF) + i += 2 + elif data[i + 1] in (0xFB, 0xFC, 0xFD, 0xFE): # WILL/WONT/DO/DONT + if i + 2 >= len(data): + self._pending = data[i:] + break + i += 3 + elif data[i + 1] == 0xFA: # Sub-negotiation + end = data.find(b"\xff\xf0", i) + if end == -1: + self._pending = data[i:] + break + i = end + 2 + else: + i += 2 + else: + result.append(data[i]) + i += 1 + return bytes(result) + + +class StarWarsScreen(Screen): + """Modal overlay streaming ASCII Star Wars from telnet, with offline fallback.""" + + BINDINGS = [ + Binding("escape", "dismiss", "Close", show=True), + Binding("q", "dismiss", "Close", show=False), + ] + + DEFAULT_CSS = """ + StarWarsScreen { + align: center middle; + background: #000000 90%; + } + StarWarsScreen #sw-container { + width: 90%; + height: 90%; + background: #000000; + border: round #1a3050; + } + StarWarsScreen #sw-header { + height: 1; + color: #e8a020; + text-style: bold; + text-align: center; + padding: 0 1; + } + StarWarsScreen #sw-log { + height: 1fr; + background: #000000; + color: #c8d0d8; + } + StarWarsScreen #sw-footer { + height: 1; + color: #506878; + text-align: center; + } + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._socket: socket.socket | None = None + self._running = False + + def compose(self) -> ComposeResult: + with Vertical(id="sw-container"): + yield Static( + "A long time ago in a galaxy far, far away....", + id="sw-header", + ) + yield RichLog(id="sw-log", wrap=False, markup=False) + yield Static("[ESC] Close", id="sw-footer") + + def on_mount(self) -> None: + self._running = True + self._stream_starwars() + + def on_unmount(self) -> None: + self._running = False + self._close_socket() + + def action_dismiss(self) -> None: + self._running = False + self._close_socket() + self.app.pop_screen() + + def _close_socket(self) -> None: + if self._socket: + try: + self._socket.close() + except Exception: + pass + self._socket = None + + def _safe_write(self, text: str) -> None: + """Write to RichLog only if screen is still mounted and running.""" + if not self._running or not self.is_mounted: + return + try: + log = self.query_one("#sw-log", RichLog) + log.write(text) + except Exception: + pass + + def _safe_clear(self) -> None: + """Clear the RichLog.""" + if not self._running or not self.is_mounted: + return + try: + self.query_one("#sw-log", RichLog).clear() + except Exception: + pass + + @work(thread=True) + def _stream_starwars(self) -> None: + """Try telnet first, fall back to built-in crawl.""" + self.app.call_from_thread( + self._safe_write, "Connecting to towel.blinkenlights.nl:23..." + ) + + if self._try_telnet(): + return # telnet worked, we're done + + # Telnet failed — play the built-in crawl + self.app.call_from_thread(self._safe_write, "Falling back to local crawl...\n") + time.sleep(1.0) + self.app.call_from_thread(self._safe_clear) + self._play_offline_crawl() + + def _try_telnet(self) -> bool: + """Attempt telnet connection. Returns True if streaming completed. + + Uses AF_UNSPEC to try both IPv6 and IPv4 (happy eyeballs style). + Many networks route IPv6 correctly but block or drop IPv4 on port 23. + """ + try: + addrs = socket.getaddrinfo(TELNET_HOST, TELNET_PORT, + socket.AF_UNSPEC, socket.SOCK_STREAM) + if not addrs: + raise socket.gaierror("No address found") + labels = [] + for fam, _, _, _, sa in addrs: + tag = "v6" if fam == socket.AF_INET6 else "v4" + labels.append(f"{tag}:{sa[0]}") + self.app.call_from_thread( + self._safe_write, f"Resolved: {', '.join(labels)}" + ) + except socket.gaierror as e: + self.app.call_from_thread( + self._safe_write, f"DNS failed: {e}" + ) + return False + + # Try each address until one connects (IPv6 first if available) + sock = None + last_err = None + for fam, socktype, proto, _canon, sa in addrs: + tag = "v6" if fam == socket.AF_INET6 else "v4" + self.app.call_from_thread( + self._safe_write, f"Trying {tag}:{sa[0]}..." + ) + try: + s = socket.socket(fam, socktype, proto) + s.settimeout(CONNECT_TIMEOUT) + s.connect(sa) + s.settimeout(2.0) + sock = s + break + except (socket.timeout, OSError) as e: + last_err = e + try: + s.close() + except Exception: + pass + + if sock is None: + self.app.call_from_thread( + self._safe_write, f"All addresses failed: {last_err}" + ) + return False + + self._socket = sock + + self.app.call_from_thread(self._safe_write, "Connected! Streaming...\n") + + stripper = _TelnetStripper() + buffer = b"" + while self._running: + try: + chunk = sock.recv(RECV_SIZE) + if not chunk: + break + clean = stripper.feed(chunk) + buffer += clean + + # ESC[H (cursor home) marks frame boundaries. + # Buffer until we have a complete frame, then render atomically. + while b"\x1b[H" in buffer: + frame_data, buffer = buffer.split(b"\x1b[H", 1) + if frame_data.strip(): + self.app.call_from_thread( + self._render_frame, frame_data + ) + + except socket.timeout: + # Timeout with no ESC[H — flush buffer as partial frame + if buffer.strip(): + self.app.call_from_thread( + self._render_frame, buffer + ) + buffer = b"" + continue + except Exception: + break + + # Flush anything remaining + if buffer.strip(): + self.app.call_from_thread(self._render_frame, buffer) + + self.app.call_from_thread(self._safe_write, "\n[Stream ended]") + self._close_socket() + return True + + def _render_frame(self, data: bytes) -> None: + """Render a complete animation frame, replacing previous content. + + Uses Rich's Text.from_ansi() to natively handle any ANSI styling + in the stream. ESC[J (clear) is stripped since we replace the + whole frame anyway. + """ + if not self._running or not self.is_mounted: + return + try: + log = self.query_one("#sw-log", RichLog) + log.clear() + # Decode frame, strip ESC[J (redundant — we clear anyway) + text = data.replace(b"\x1b[J", b"") + text = text.decode("ascii", errors="replace") + text = text.replace("\r", "") + # Rich's from_ansi() renders ANSI color/style codes natively + log.write(Text.from_ansi(text)) + except Exception: + pass + + def _play_offline_crawl(self) -> None: + """Play the built-in Star Wars opening crawl frame by frame.""" + for lines, delay in _CRAWL_FRAMES: + if not self._running: + return + + # The opening crawl frame scrolls line-by-line + if delay > 0 and delay < 0.5 and len(lines) > 3: + # Per-line scroll mode + self.app.call_from_thread(self._safe_clear) + for line in lines: + if not self._running: + return + self.app.call_from_thread(self._safe_write, line) + time.sleep(delay) + else: + # Full-frame mode + self.app.call_from_thread(self._safe_clear) + for line in lines: + if not self._running: + return + self.app.call_from_thread(self._safe_write, line) + if delay > 0: + time.sleep(delay) diff --git a/tui/src/skywalker_tui/screens/track.py b/tui/src/skywalker_tui/screens/track.py index 9e4852a..e0055c2 100644 --- a/tui/src/skywalker_tui/screens/track.py +++ b/tui/src/skywalker_tui/screens/track.py @@ -1,13 +1,14 @@ -"""Track screen — carrier/beacon tracker with logging and export. +"""Track screen — carrier/beacon tracker with radar scope, logging, and export. Locks to a single frequency and records SNR, power, lock state over time. -Displays dual sparklines, an event log for lock transitions, and stats. -Supports CSV/JSONL export. +Features a hero radar scope (P1 phosphor CRT aesthetic), dual sparklines, +event log for lock transitions, and stats. Supports CSV/JSONL export. """ import csv import json import time +from collections import deque from datetime import datetime from pathlib import Path @@ -17,12 +18,12 @@ from textual.widgets import Label, Input, Button, Static, RichLog from textual import work from textual.worker import Worker -from skywalker_tui.widgets.signal_gauge import SignalGauge +from skywalker_tui.widgets.radar_scope import RadarScope from skywalker_tui.widgets.sparkline_widget import SparklineWidget class TrackScreen(Container): - """Long-running carrier tracker with event log and export.""" + """Long-running carrier tracker with radar scope and event log.""" DEFAULT_CSS = """ TrackScreen { @@ -30,18 +31,27 @@ class TrackScreen(Container): } TrackScreen #track-main { height: 1fr; - layout: vertical; + layout: horizontal; padding: 1 2; } - TrackScreen #track-upper { - height: auto; - layout: horizontal; - } - TrackScreen #track-gauge-col { + TrackScreen #track-radar-col { width: 1fr; + min-width: 30; + layout: vertical; + } + TrackScreen #track-radar-label { + height: 1; + color: #0a5a10; + text-style: bold; + padding: 0 1; + } + TrackScreen #track-data-col { + width: 1fr; + layout: vertical; + padding: 0 0 0 1; } TrackScreen #track-sparklines { - width: 1fr; + height: auto; layout: vertical; } TrackScreen #track-log-container { @@ -101,28 +111,30 @@ class TrackScreen(Container): self._peak_snr = 0.0 self._start_time = 0.0 self._was_locked: bool | None = None - self._records: list[dict] = [] + self._records: deque[dict] = deque(maxlen=360_000) # ~10h at 10Hz def compose(self) -> ComposeResult: - with Vertical(id="track-main"): - with Horizontal(id="track-upper"): - with Vertical(id="track-gauge-col"): - yield SignalGauge(id="track-gauge") + with Horizontal(id="track-main"): + with Vertical(id="track-radar-col"): + yield Static("[#0a5a10 bold]Radar Scope[/]", id="track-radar-label") + yield RadarScope(id="track-radar") + + with Vertical(id="track-data-col"): with Vertical(id="track-sparklines"): yield SparklineWidget(title="SNR (dB)", color="#00d4aa", id="track-snr-spark") yield SparklineWidget(title="Power (dB)", color="#2196f3", id="track-power-spark") - with Vertical(id="track-log-container"): - yield Static("[#00d4aa bold]Event Log[/]", id="track-log-title") - yield RichLog(id="track-log", wrap=True, markup=True) + with Vertical(id="track-log-container"): + yield Static("[#00d4aa bold]Event Log[/]", id="track-log-title") + yield RichLog(id="track-log", wrap=True, markup=True) - with Horizontal(id="track-stats"): - yield Static("[#506878]Samples:[/] [#00d4aa]0[/]", id="trk-samples") - yield Static("[#506878]Elapsed:[/] [#00d4aa]0s[/]", id="trk-elapsed") - yield Static("[#506878]Peak SNR:[/] [#00d4aa]0.0 dB[/]", id="trk-peak") - yield Static("[#506878]Status:[/] [#e8a020]Stopped[/]", id="trk-status") + with Horizontal(id="track-stats"): + yield Static("[#506878]Samples:[/] [#00d4aa]0[/]", id="trk-samples") + yield Static("[#506878]Elapsed:[/] [#00d4aa]0s[/]", id="trk-elapsed") + yield Static("[#506878]Peak SNR:[/] [#00d4aa]0.0 dB[/]", id="trk-peak") + yield Static("[#506878]Status:[/] [#e8a020]Stopped[/]", id="trk-status") with Horizontal(id="track-controls"): yield Label("Freq (MHz):") @@ -156,6 +168,36 @@ class TrackScreen(Container): def _start_tracking(self) -> None: if self._tracking: return + + log = self.query_one("#track-log", RichLog) + + # C3: Validate inputs before starting + try: + freq = float(self.query_one("#trk-freq", Input).value or "1200") + except ValueError: + log.write("[bold #e04040]Invalid frequency — must be a number[/]") + return + try: + sr = int(float(self.query_one("#trk-sr", Input).value or "20000")) + except ValueError: + log.write("[bold #e04040]Invalid symbol rate — must be a number[/]") + return + try: + rate = float(self.query_one("#trk-rate", Input).value or "1") + except ValueError: + log.write("[bold #e04040]Invalid rate — must be a number[/]") + return + + if not (950 <= freq <= 2150): + log.write("[bold #e04040]Frequency out of range (950–2150 MHz)[/]") + return + if not (256 <= sr <= 30000): + log.write("[bold #e04040]Symbol rate out of range (256–30000 ksps)[/]") + return + if not (0.1 <= rate <= 100): + log.write("[bold #e04040]Rate out of range (0.1–100 Hz)[/]") + return + self._tracking = True self._sample_count = 0 self._peak_snr = 0.0 @@ -163,14 +205,9 @@ class TrackScreen(Container): self._records.clear() self._start_time = time.monotonic() - freq = float(self.query_one("#trk-freq", Input).value or "1200") - sr = int(self.query_one("#trk-sr", Input).value or "20000") - rate = float(self.query_one("#trk-rate", Input).value or "1") - self.query_one("#trk-status", Static).update( "[#506878]Status:[/] [bold #00d4aa]Tracking[/]" ) - log = self.query_one("#track-log", RichLog) log.clear() log.write("[#506878]Tracking started[/]") @@ -192,7 +229,8 @@ class TrackScreen(Container): @work(thread=True) def _do_track(self, freq_mhz: float, sr_ksps: int, rate: float) -> None: - """Background tracking loop.""" + """Background tracking loop with circuit breaker.""" + max_consecutive_errors = 10 interval = 1.0 / max(0.1, rate) freq_khz = int(freq_mhz * 1000) sr_sps = sr_ksps * 1000 @@ -201,14 +239,29 @@ class TrackScreen(Container): self._bridge.ensure_booted() self._bridge.tune(sr_sps, freq_khz, 0, 5) time.sleep(0.3) - except Exception: - pass + except Exception as e: + self.app.call_from_thread( + self._log_event, + f"[bold #e04040]Boot/tune failed:[/] [#e04040]{e}[/]", + ) + consecutive_errors = 0 while self._tracking: t0 = time.monotonic() try: sig = self._bridge.signal_monitor() + consecutive_errors = 0 # reset on success except Exception: + consecutive_errors += 1 + if consecutive_errors >= max_consecutive_errors: + self.app.call_from_thread( + self._log_event, + f"[bold #e04040]Circuit breaker: " + f"{max_consecutive_errors} consecutive errors, stopping[/]", + ) + self._tracking = False + self.app.call_from_thread(self._mark_stopped) + return time.sleep(interval) continue @@ -251,10 +304,19 @@ class TrackScreen(Container): if not self.is_mounted: return - self.query_one("#track-gauge", SignalGauge).update_signal(sig) - self.query_one("#track-snr-spark", SparklineWidget).push(sig.get("snr_db", 0)) + snr_db = sig.get("snr_db", 0.0) + locked = sig.get("locked", False) + + # Feed radar scope + radar = self.query_one("#track-radar", RadarScope) + radar.push(snr_db) + radar.set_locked(locked) + + # Feed sparklines + self.query_one("#track-snr-spark", SparklineWidget).push(snr_db) self.query_one("#track-power-spark", SparklineWidget).push(sig.get("power_db", -40)) + # Update stats self.query_one("#trk-samples", Static).update( f"[#506878]Samples:[/] [#00d4aa]{self._sample_count}[/]" ) @@ -278,23 +340,53 @@ class TrackScreen(Container): f"[#506878]{ts}[/] [bold #e04040]LOCK LOST[/]" ) + def _log_event(self, markup: str) -> None: + """Write a message to the event log (safe from any thread via call_from_thread).""" + if not self.is_mounted: + return + try: + self.query_one("#track-log", RichLog).write(markup) + except Exception: + pass + + def _mark_stopped(self) -> None: + """Update UI to stopped state (called from circuit breaker).""" + if not self.is_mounted: + return + try: + self.query_one("#trk-status", Static).update( + "[#506878]Status:[/] [bold #e04040]Error[/]" + ) + except Exception: + pass + def _export_csv(self) -> None: if not self._records: return - path = Path(f"skywalker-track-{datetime.now().strftime('%Y%m%d-%H%M%S')}.csv") - with open(path, "w", newline="") as f: - w = csv.DictWriter(f, fieldnames=list(self._records[0].keys())) - w.writeheader() - w.writerows(self._records) log = self.query_one("#track-log", RichLog) - log.write(f"[#00d4aa]CSV exported: {path}[/]") + path = Path(f"skywalker-track-{datetime.now().strftime('%Y%m%d-%H%M%S')}.csv") + try: + with open(path, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=list(self._records[0].keys())) + w.writeheader() + w.writerows(self._records) + log.write(f"[#00d4aa]CSV exported: {path}[/]") + except PermissionError: + log.write(f"[bold #e04040]Permission denied: {path}[/]") + except OSError as e: + log.write(f"[bold #e04040]Export failed: {e}[/]") def _export_jsonl(self) -> None: if not self._records: return - path = Path(f"skywalker-track-{datetime.now().strftime('%Y%m%d-%H%M%S')}.jsonl") - with open(path, "w") as f: - for rec in self._records: - f.write(json.dumps(rec) + "\n") log = self.query_one("#track-log", RichLog) - log.write(f"[#00d4aa]JSONL exported: {path}[/]") + path = Path(f"skywalker-track-{datetime.now().strftime('%Y%m%d-%H%M%S')}.jsonl") + try: + with open(path, "w") as f: + for rec in self._records: + f.write(json.dumps(rec) + "\n") + log.write(f"[#00d4aa]JSONL exported: {path}[/]") + except PermissionError: + log.write(f"[bold #e04040]Permission denied: {path}[/]") + except OSError as e: + log.write(f"[bold #e04040]Export failed: {e}[/]") diff --git a/tui/src/skywalker_tui/theme.tcss b/tui/src/skywalker_tui/theme.tcss index 5919970..10916d0 100644 --- a/tui/src/skywalker_tui/theme.tcss +++ b/tui/src/skywalker_tui/theme.tcss @@ -303,3 +303,52 @@ ProgressBar Bar { .right-panel { width: 1fr; } + +/* ─── Radar scope ─── */ + +RadarScope { + min-height: 12; + min-width: 24; + height: 1fr; + background: #0a0a0a; + border: round #0a2a0a; +} + +#track-radar-col { + width: 1fr; + min-width: 30; +} + +/* ─── Splash screen overlay ─── */ + +SplashScreen { + align: center middle; + background: #000000; +} + +SplashScreen #splash-container { + width: 100%; + height: 100%; + align: center middle; + background: #000000; +} + +SplashScreen #splash-image { + width: 100%; + height: 1fr; + content-align: center middle; +} + +/* ─── Star Wars overlay ─── */ + +StarWarsScreen { + align: center middle; + background: #000000 90%; +} + +StarWarsScreen #sw-container { + width: 90%; + height: 90%; + background: #000000; + border: round #1a3050; +} diff --git a/tui/src/skywalker_tui/widgets/radar_scope.py b/tui/src/skywalker_tui/widgets/radar_scope.py new file mode 100644 index 0000000..bdae576 --- /dev/null +++ b/tui/src/skywalker_tui/widgets/radar_scope.py @@ -0,0 +1,276 @@ +"""Radar scope widget — P1 green phosphor CRT aesthetic. + +Renders a circular radar display using half-block characters for 2x vertical +resolution. Each terminal cell becomes two pixel rows via the ▀ character +with independent fg (top) and bg (bottom) colors. + +The sweep beam rotates through 360 positions. Signal strength determines +radial distance from center. Older samples decay in brightness (phosphor +persistence). Concentric range rings and a crosshair provide scale reference. +""" + +import math +from collections import deque + +from textual.widget import Widget +from textual.strip import Strip +from rich.segment import Segment +from rich.style import Style + + +# P1 green phosphor palette — 8 intensity levels from dead to peak burn +PHOSPHOR = [ + "#0a0a0a", # background / dead pixel + "#0a1a0a", # very faint trace + "#0a2a0a", # dim afterglow + "#0a3a0a", # fading + "#0a4a0f", # moderate + "#0a5a10", # bright trace + "#0a6a15", # very bright + "#10ff30", # peak phosphor burn +] + +PHOSPHOR_STYLES = [Style(color=c) for c in PHOSPHOR] +BG_COLOR = "#0a0a0a" +BG_STYLE = Style(color=BG_COLOR, bgcolor=BG_COLOR) +RING_COLOR = "#0a2a0a" +CROSSHAIR_COLOR = "#0a3a0a" +LOCK_RING_COLOR = "#10ff30" + + +class RadarScope(Widget): + """Circular radar scope with phosphor decay and sweep beam.""" + + DEFAULT_CSS = """ + RadarScope { + min-height: 12; + min-width: 24; + background: #0a0a0a; + } + """ + + def __init__(self, max_samples: int = 360, **kwargs): + super().__init__(**kwargs) + self._samples: deque[float] = deque([0.0] * max_samples, maxlen=max_samples) + self._max_samples = max_samples + self._angle_idx = 0 # current sweep position (0..max_samples-1) + self._locked = False + + # Pre-computed geometry + LUTs (rebuilt on resize) + self._cx = 0.0 + self._cy = 0.0 + self._radius = 0.0 + self._cols = 0 + self._rows = 0 # pixel rows (2x terminal rows) + # Per-pixel LUTs: distance from center, angle-to-sample index + self._dist_lut: list[list[float]] = [] + self._angle_lut: list[list[int]] = [] + # Per-pixel dx/dy for crosshair checks + self._dx_lut: list[list[float]] = [] + self._dy_lut: list[list[float]] = [] + + def push(self, snr_db: float, max_snr: float = 16.0) -> None: + """Push a new signal sample. SNR is normalized to 0.0-1.0 range.""" + normalized = max(0.0, min(1.0, snr_db / max(0.1, max_snr))) + self._samples.append(normalized) + self._angle_idx = (self._angle_idx + 1) % self._max_samples + self.refresh() + + def set_locked(self, locked: bool) -> None: + if locked != self._locked: + self._locked = locked + self.refresh() + + def _recompute_geometry(self, width: int, height: int) -> None: + """Recompute center, radius, and per-pixel LUTs for current dimensions. + + Pre-computes dist/angle for every pixel so render_line() avoids + math.sqrt/math.atan2 per pixel per frame — ~O(1) lookup instead. + """ + self._cols = width + self._rows = height * 2 # 2x vertical resolution via half-blocks + # Radius fits within both dimensions, accounting for ~2:1 terminal char aspect + rx = (width - 2) / 2.0 + ry = (height * 2 - 2) / 2.0 + self._radius = min(rx, ry) + cx = width / 2.0 + cy = height # center in pixel rows (height * 2 / 2) + self._cx = cx + self._cy = cy + + # Build LUTs for all pixel rows (height * 2) × columns + pixel_rows = height * 2 + dist_lut = [] + angle_lut = [] + dx_lut = [] + dy_lut = [] + sqrt = math.sqrt + atan2 = math.atan2 + degrees = math.degrees + ms = self._max_samples + + for py in range(pixel_rows): + d_row = [] + a_row = [] + dxr = [] + dyr = [] + dy = py - cy + for px in range(width): + dx = px - cx + dist = sqrt(dx * dx + dy * dy) + d_row.append(dist) + dxr.append(dx) + dyr.append(dy) + if dist >= 1.0: + angle = atan2(dy, dx) + angle_deg = (degrees(angle) + 360) % 360 + a_row.append(int(angle_deg / 360 * ms) % ms) + else: + a_row.append(0) # center pixel — angle irrelevant + dist_lut.append(d_row) + angle_lut.append(a_row) + dx_lut.append(dxr) + dy_lut.append(dyr) + + self._dist_lut = dist_lut + self._angle_lut = angle_lut + self._dx_lut = dx_lut + self._dy_lut = dy_lut + + def render_line(self, y: int) -> Strip: + """Render one terminal row of the radar scope.""" + width = self.size.width + height = self.size.height + + if width < 4 or height < 4: + return Strip([Segment(" " * width, BG_STYLE)]) + + if self._cols != width or self._rows != height * 2: + self._recompute_geometry(width, height) + + radius = self._radius + if radius < 2: + return Strip([Segment(" " * width, BG_STYLE)]) + + # Snapshot mutable state for consistent rendering across the frame + samples = list(self._samples) + angle_idx = self._angle_idx + locked = self._locked + + # Two pixel rows per terminal row + py_top = y * 2 + py_bot = y * 2 + 1 + + # LUT rows for this terminal row + dist_top = self._dist_lut[py_top] if py_top < len(self._dist_lut) else None + dist_bot = self._dist_lut[py_bot] if py_bot < len(self._dist_lut) else None + angle_top = self._angle_lut[py_top] if py_top < len(self._angle_lut) else None + angle_bot = self._angle_lut[py_bot] if py_bot < len(self._angle_lut) else None + dx_top = self._dx_lut[py_top] if py_top < len(self._dx_lut) else None + dx_bot = self._dx_lut[py_bot] if py_bot < len(self._dx_lut) else None + dy_top = self._dy_lut[py_top] if py_top < len(self._dy_lut) else None + dy_bot = self._dy_lut[py_bot] if py_bot < len(self._dy_lut) else None + + if not dist_top or not dist_bot: + return Strip([Segment(" " * width, BG_STYLE)]) + + _pi = self._pixel_intensity + segments = [] + for x in range(width): + top_intensity = _pi( + dist_top[x], angle_top[x], dx_top[x], dy_top[x], + radius, samples, angle_idx, locked, + ) + bot_intensity = _pi( + dist_bot[x], angle_bot[x], dx_bot[x], dy_bot[x], + radius, samples, angle_idx, locked, + ) + + top_color = PHOSPHOR[top_intensity] + bot_color = PHOSPHOR[bot_intensity] + + # ▀ char: fg = top pixel, bg = bottom pixel + style = Style(color=top_color, bgcolor=bot_color) + segments.append(Segment("\u2580", style)) + + return Strip(segments) + + def _pixel_intensity(self, dist: float, sample_idx: int, + dx: float, dy: float, + radius: float, samples: list[float], + angle_idx: int, locked: bool) -> int: + """Compute phosphor intensity (0-7) for a single pixel. + + Uses pre-computed dist/sample_idx/dx/dy from LUTs (no trig per pixel). + Uses snapshot data (samples, angle_idx, locked) rather than mutable + instance state for frame-consistent rendering. + """ + # Outside the scope circle + if dist > radius + 1: + return 0 + + # Lock ring — outer edge glow + if locked and abs(dist - radius) < 1.5: + return 7 + + # Range rings at 25%, 50%, 75% radius + for ring_r in (0.25, 0.5, 0.75): + if abs(dist - radius * ring_r) < 0.7: + return 2 # dim ring + + # Outer boundary ring + if abs(dist - radius) < 0.7: + return 2 + + # Crosshair (horizontal and vertical through center) + if abs(dx) < 0.7 and dist < radius: + return 2 + if abs(dy) < 0.7 and dist < radius: + return 2 + + # Signal trace — map pixel angle to sample buffer + if dist < 1.0: + return 1 # center dot + + if dist > radius: + return 0 + + # sample_idx already computed in LUT via atan2 → degrees → index + strength = samples[sample_idx] + + # Signal renders as a blip at radial distance proportional to strength + signal_dist = strength * radius * 0.85 # 85% of radius at max + noise_floor_dist = radius * 0.05 # tiny noise floor ring + + # Distance from the signal blip center + blip_dist = abs(dist - signal_dist) + noise_dist = abs(dist - noise_floor_dist) + + # Age-based decay: how far behind the sweep beam is this angle? + age = (angle_idx - sample_idx) % self._max_samples + age_ratio = age / self._max_samples # 0 = newest, 1 = oldest + + # Intensity from signal blip proximity + if strength > 0.02 and blip_dist < 2.0: + proximity = max(0.0, 1.0 - blip_dist / 2.0) + freshness = max(0.0, 1.0 - age_ratio * 1.2) + raw_intensity = proximity * freshness * strength + return max(1, min(7, int(raw_intensity * 7 + 0.5))) + + # Sweep beam — the most recent angle is brightest + if age < 3: + beam_intensity = max(0.0, 1.0 - age / 3.0) + if dist < radius * 0.9: + return max(1, min(5, int(beam_intensity * 5))) + + # Noise floor glow + if noise_dist < 1.0 and strength > 0: + return 1 + + # Fill between center and signal (faint trail) + if strength > 0.1 and dist < signal_dist and age_ratio < 0.5: + trail = max(0.0, (1.0 - age_ratio * 2) * 0.3) + if trail > 0.05: + return 1 + + return 0 diff --git a/tui/src/skywalker_tui/widgets/signal_gauge.py b/tui/src/skywalker_tui/widgets/signal_gauge.py index 26e3630..80b6c33 100644 --- a/tui/src/skywalker_tui/widgets/signal_gauge.py +++ b/tui/src/skywalker_tui/widgets/signal_gauge.py @@ -2,7 +2,7 @@ from textual.app import ComposeResult from textual.widget import Widget -from textual.widgets import Label, Static +from textual.widgets import Static from textual.reactive import reactive diff --git a/tui/src/skywalker_tui/widgets/status_bar.py b/tui/src/skywalker_tui/widgets/status_bar.py index a771eae..edc7c9d 100644 --- a/tui/src/skywalker_tui/widgets/status_bar.py +++ b/tui/src/skywalker_tui/widgets/status_bar.py @@ -1,12 +1,6 @@ """Device status bar — connection state, firmware version, config bits.""" -import sys -import os - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "tools")) - from textual.app import ComposeResult -from textual.containers import Horizontal from textual.widget import Widget from textual.widgets import Label diff --git a/tui/tests/__init__.py b/tui/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tui/tests/test_radar_scope.py b/tui/tests/test_radar_scope.py new file mode 100644 index 0000000..5a5be1b --- /dev/null +++ b/tui/tests/test_radar_scope.py @@ -0,0 +1,192 @@ +"""Tests for the radar scope widget — LUT geometry and pixel intensity.""" + +from skywalker_tui.widgets.radar_scope import RadarScope + + +class TestRadarGeometry: + """LUT pre-computation and geometry tests.""" + + def _make_scope(self, width=40, height=20): + scope = RadarScope() + scope._recompute_geometry(width, height) + return scope + + def test_lut_dimensions(self): + scope = self._make_scope(40, 20) + # pixel rows = terminal rows * 2 + assert len(scope._dist_lut) == 40 + assert len(scope._angle_lut) == 40 + assert len(scope._dx_lut) == 40 + assert len(scope._dy_lut) == 40 + # each row has `width` columns + assert len(scope._dist_lut[0]) == 40 + assert len(scope._angle_lut[0]) == 40 + + def test_center_pixel_distance_near_zero(self): + scope = self._make_scope(40, 20) + # center = (20, 20) in pixel coords + cx_int = int(scope._cx) + cy_int = int(scope._cy) + dist = scope._dist_lut[cy_int][cx_int] + assert dist < 1.0, f"Center pixel distance should be ~0, got {dist}" + + def test_corner_pixel_outside_circle(self): + scope = self._make_scope(40, 20) + dist = scope._dist_lut[0][0] + assert dist > scope._radius, "Corner should be outside the radar circle" + + def test_radius_fits_within_bounds(self): + scope = self._make_scope(60, 30) + # radius should be ≤ half the smaller dimension + assert scope._radius <= 30 # half of width + assert scope._radius <= 29 # half of pixel height (60) - 1 + + def test_recompute_updates_on_resize(self): + scope = self._make_scope(40, 20) + r1 = scope._radius + scope._recompute_geometry(80, 40) + r2 = scope._radius + assert r2 > r1, "Larger terminal should give larger radius" + + +class TestPixelIntensity: + """Tests for _pixel_intensity with pre-computed LUT values.""" + + def _make_scope(self, width=40, height=20): + scope = RadarScope() + scope._recompute_geometry(width, height) + return scope + + def test_outside_circle_returns_zero(self): + scope = self._make_scope() + samples = [0.0] * 360 + result = scope._pixel_intensity( + dist=scope._radius + 5, + sample_idx=0, dx=20.0, dy=0.0, + radius=scope._radius, samples=samples, + angle_idx=0, locked=False, + ) + assert result == 0 + + def test_center_on_crosshair(self): + """At exact center, crosshair check (abs(dx) < 0.7) fires before center dot.""" + scope = self._make_scope() + samples = [0.0] * 360 + result = scope._pixel_intensity( + dist=0.5, sample_idx=0, dx=0.0, dy=0.0, + radius=scope._radius, samples=samples, + angle_idx=0, locked=False, + ) + assert result == 2 # crosshair overlaps center + + def test_center_off_crosshair(self): + """Near center but off crosshair axes → center dot (1).""" + scope = self._make_scope() + samples = [0.0] * 360 + result = scope._pixel_intensity( + dist=0.8, sample_idx=45, dx=0.8, dy=0.8, + radius=scope._radius, samples=samples, + angle_idx=0, locked=False, + ) + assert result == 1 # center dot, off crosshair axes + + def test_lock_ring_at_edge(self): + scope = self._make_scope() + samples = [0.0] * 360 + result = scope._pixel_intensity( + dist=scope._radius, sample_idx=0, dx=scope._radius, dy=0.0, + radius=scope._radius, samples=samples, + angle_idx=0, locked=True, + ) + assert result == 7 # peak phosphor for lock ring + + def test_no_lock_ring_when_unlocked(self): + scope = self._make_scope() + samples = [0.0] * 360 + result = scope._pixel_intensity( + dist=scope._radius, sample_idx=0, dx=scope._radius, dy=0.0, + radius=scope._radius, samples=samples, + angle_idx=0, locked=False, + ) + # Should be boundary ring (2), not lock ring (7) + assert result != 7 + + def test_crosshair_on_vertical(self): + scope = self._make_scope() + samples = [0.0] * 360 + # Point on the vertical crosshair (dx near 0, inside circle) + result = scope._pixel_intensity( + dist=scope._radius * 0.5, + sample_idx=90, dx=0.0, dy=scope._radius * 0.5, + radius=scope._radius, samples=samples, + angle_idx=0, locked=False, + ) + assert result == 2 # crosshair intensity + + def test_strong_signal_blip_near_sweep(self): + scope = self._make_scope() + samples = [0.0] * 360 + # strength 0.47 → blip at 0.47 * r * 0.85 = 0.40*r + # Clear of range rings at 0.25r, 0.50r, 0.75r (nearest gap: 0.15r ≈ 2.85px) + samples[180] = 0.47 + signal_dist = 0.47 * scope._radius * 0.85 + # Verify we're not on a range ring + r = scope._radius + for ring_r in (0.25, 0.5, 0.75): + assert abs(signal_dist - r * ring_r) > 0.7, ( + f"Signal blip at {signal_dist:.2f} overlaps range ring at {r * ring_r:.2f}" + ) + result = scope._pixel_intensity( + dist=signal_dist, + sample_idx=180, dx=5.0, dy=5.0, + radius=scope._radius, samples=samples, + angle_idx=180, locked=False, + ) + # Newest sample at sweep position should be visible + assert result >= 3 + + def test_old_signal_decays(self): + scope = self._make_scope() + samples = [0.0] * 360 + samples[0] = 0.8 + signal_dist = 0.8 * scope._radius * 0.85 + # Sample at index 0, sweep beam at index 300 → age = 300 + result = scope._pixel_intensity( + dist=signal_dist, + sample_idx=0, dx=5.0, dy=5.0, + radius=scope._radius, samples=samples, + angle_idx=300, locked=False, + ) + # Old sample should be dim or invisible + assert result <= 2 + + +class TestRadarPush: + """Tests for the push() method and normalization.""" + + def test_push_normalizes_to_range(self): + scope = RadarScope(max_samples=10) + scope.push(8.0, max_snr=16.0) + assert scope._samples[-1] == 0.5 + + def test_push_clamps_above_max(self): + scope = RadarScope(max_samples=10) + scope.push(20.0, max_snr=16.0) + assert scope._samples[-1] == 1.0 + + def test_push_clamps_below_zero(self): + scope = RadarScope(max_samples=10) + scope.push(-5.0, max_snr=16.0) + assert scope._samples[-1] == 0.0 + + def test_angle_index_wraps(self): + scope = RadarScope(max_samples=4) + for _ in range(5): + scope.push(1.0) + assert scope._angle_idx == 1 # 5 % 4 = 1 + + def test_set_locked(self): + scope = RadarScope() + assert scope._locked is False + scope.set_locked(True) + assert scope._locked is True diff --git a/tui/tests/test_splash.py b/tui/tests/test_splash.py new file mode 100644 index 0000000..9cfe4c0 --- /dev/null +++ b/tui/tests/test_splash.py @@ -0,0 +1,48 @@ +"""Tests for the splash screen art catalog and selection.""" + +from skywalker_tui.screens.splash import SplashScreen, ASSETS_DIR, ART_CATALOG + + +class TestSplashArtCatalog: + """Verify bundled pre-baked .ans art assets exist and catalog is consistent.""" + + def test_assets_dir_exists(self): + assert ASSETS_DIR.is_dir(), f"Assets dir missing: {ASSETS_DIR}" + + def test_all_catalog_entries_have_ans_files(self): + missing = [] + for stem, artist, title in ART_CATALOG: + path = ASSETS_DIR / f"{stem}.ans" + if not path.exists(): + missing.append(f"{stem}.ans") + assert not missing, f"Missing pre-baked .ans files: {missing}" + + def test_catalog_has_entries(self): + assert len(ART_CATALOG) >= 1 + + def test_ans_files_not_empty(self): + for stem, _, _ in ART_CATALOG: + path = ASSETS_DIR / f"{stem}.ans" + if path.exists(): + assert path.stat().st_size > 0, f"{stem}.ans is empty" + + def test_ans_files_contain_ansi_escapes(self): + """Pre-baked files should contain ANSI color escape sequences.""" + for stem, _, _ in ART_CATALOG: + path = ASSETS_DIR / f"{stem}.ans" + if path.exists(): + content = path.read_text() + assert "\033[" in content, f"{stem}.ans has no ANSI escapes" + assert "\u2580" in content, f"{stem}.ans has no half-block chars" + + +class TestSplashScreenInit: + """Test SplashScreen construction (no app needed).""" + + def test_selects_art_on_init(self): + screen = SplashScreen() + assert screen._ans_path is not None or len(ART_CATALOG) == 0 + if screen._ans_path: + assert screen._ans_path.exists() + assert screen._artist != "" + assert screen._title != "" diff --git a/tui/tests/test_telnet_stripper.py b/tui/tests/test_telnet_stripper.py new file mode 100644 index 0000000..94582f2 --- /dev/null +++ b/tui/tests/test_telnet_stripper.py @@ -0,0 +1,108 @@ +"""Tests for the stateful telnet IAC sequence stripper.""" + +from skywalker_tui.screens.starwars import _TelnetStripper + + +class TestTelnetStripper: + """Edge cases for IAC parsing across chunk boundaries.""" + + def test_plain_text_passthrough(self): + s = _TelnetStripper() + assert s.feed(b"hello world") == b"hello world" + + def test_strips_will_command(self): + # IAC WILL ECHO = FF FB 01 + s = _TelnetStripper() + assert s.feed(b"\xff\xfb\x01hello") == b"hello" + + def test_strips_wont_command(self): + s = _TelnetStripper() + assert s.feed(b"\xff\xfc\x03data") == b"data" + + def test_strips_do_command(self): + s = _TelnetStripper() + assert s.feed(b"\xff\xfd\x01data") == b"data" + + def test_strips_dont_command(self): + s = _TelnetStripper() + assert s.feed(b"\xff\xfe\x01data") == b"data" + + def test_escaped_0xff(self): + """Doubled 0xFF = literal 0xFF byte in content.""" + s = _TelnetStripper() + assert s.feed(b"\xff\xff") == b"\xff" + + def test_sub_negotiation(self): + # IAC SB 0x01 ... IAC SE = FF FA 01 xx xx FF F0 + s = _TelnetStripper() + data = b"before\xff\xfa\x01\x00\x00\xff\xf0after" + assert s.feed(data) == b"beforeafter" + + def test_split_iac_across_chunks(self): + """IAC command split: FF in chunk 1, FB 01 in chunk 2.""" + s = _TelnetStripper() + out1 = s.feed(b"hello\xff") + out2 = s.feed(b"\xfb\x01world") + assert out1 == b"hello" + assert out2 == b"world" + + def test_split_will_at_boundary(self): + """IAC WILL split: FF FB in chunk 1, option byte in chunk 2.""" + s = _TelnetStripper() + out1 = s.feed(b"aaa\xff\xfb") + out2 = s.feed(b"\x03bbb") + assert out1 == b"aaa" + assert out2 == b"bbb" + + def test_split_sub_negotiation(self): + """Sub-negotiation without closing IAC SE — buffers until next chunk.""" + s = _TelnetStripper() + out1 = s.feed(b"x\xff\xfa\x01\x00") + out2 = s.feed(b"\xff\xf0y") + assert out1 == b"x" + assert out2 == b"y" + + def test_multiple_commands_in_one_chunk(self): + s = _TelnetStripper() + data = b"\xff\xfb\x01\xff\xfc\x03text\xff\xfd\x01" + assert s.feed(data) == b"text" + + def test_empty_input(self): + s = _TelnetStripper() + assert s.feed(b"") == b"" + + def test_just_iac_byte(self): + """Single 0xFF byte — should buffer, waiting for next byte.""" + s = _TelnetStripper() + out1 = s.feed(b"\xff") + assert out1 == b"" + out2 = s.feed(b"\xfb\x01done") + assert out2 == b"done" + + def test_unknown_iac_command(self): + """Unknown command byte after IAC — stripped as 2-byte sequence.""" + s = _TelnetStripper() + assert s.feed(b"\xff\xf1text") == b"text" + + +class TestFrameParsing: + """Tests for the ESC[H frame boundary detection logic.""" + + def test_frame_split_on_cursor_home(self): + """ESC[H splits data into frames.""" + data = b"frame1\x1b[Hframe2\x1b[Hframe3" + frames = data.split(b"\x1b[H") + assert frames == [b"frame1", b"frame2", b"frame3"] + + def test_esc_j_in_frame(self): + """ESC[J (clear to end) can be stripped from frame data.""" + data = b"\x1b[Jsome text here" + clean = data.replace(b"\x1b[J", b"") + assert clean == b"some text here" + + def test_empty_frames_between_homes(self): + """Consecutive ESC[H produces empty frames (filtered in code).""" + data = b"\x1b[H\x1b[Htext" + frames = data.split(b"\x1b[H") + non_empty = [f for f in frames if f.strip()] + assert non_empty == [b"text"] diff --git a/tui/uv.lock b/tui/uv.lock index 15101ac..1bedef6 100644 --- a/tui/uv.lock +++ b/tui/uv.lock @@ -2,6 +2,24 @@ version = 1 revision = 3 requires-python = ">=3.11" +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "linkify-it-py" version = "2.0.3" @@ -52,6 +70,102 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + [[package]] name = "platformdirs" version = "4.7.0" @@ -61,6 +175,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/e3/1eddccb2c39ecfbe09b3add42a04abcc3fa5b468aa4224998ffb8a7e9c8f/platformdirs-4.7.0-py3-none-any.whl", hash = "sha256:1ed8db354e344c5bb6039cd727f096af975194b508e37177719d562b2b540ee6", size = 18983, upload-time = "2026-02-12T22:21:52.237Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -70,6 +193,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "pyusb" version = "1.3.1" @@ -101,11 +253,24 @@ dependencies = [ { name = "textual" }, ] +[package.optional-dependencies] +dev = [ + { name = "pillow" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + [package.metadata] requires-dist = [ + { name = "pillow", marker = "extra == 'dev'", specifier = ">=10.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.24" }, { name = "pyusb", specifier = ">=1.3" }, { name = "textual", specifier = ">=3.0" }, ] +provides-extras = ["dev", "test"] [[package]] name = "textual"