# 0001 — GUI is a second frontend over a shared core Status: accepted, 2026-11-01; the "no browser" constraint is superseded by [0003](0003-web-gateway.md), which adds a browser frontend through a gateway. ## Context The client is a terminal UI built around `ClientCore`, which is already headless and tested without a terminal (`tests/client_core.rs` drives it with a fake screen). The GUI must add a second presentation of the same product with these constraints: - Installed desktop application. No browser, no webview, no macOS. - Linux x86_64 and Windows x86_64, cross-compiled where possible; i686 stays TUI-only. Windows 10 is the floor. - "Lightweight": ~40 MB binary, ~150 MB idle memory, ~1 s cold start. - Effects are tweened 2D animation: reels, wheel, cards, chips, particles, a live crash curve. No 3D, no audio for now. - egui is chosen for rendering; the repository is GPL-3.0-or-later. The existing boundary is terminal-shaped: `Effect` carries lines of text, `handle_key` carries arrow keys, and `Game::render` returns a formatted board. None of it can be reused by a GUI as-is. ## Decision 1. **One brain, two frontends.** `ClientCore` owns state, navigation, and flows. Frontends send `Command` and render `View`. The TUI is the first adapter and stays green throughout the refactor; roulette is ported first because it has the largest UI surface. 2. **The boundary is semantic, not textual.** `Command` covers app flows (connect, trust, login, play, leave) and game actions (`PlaceBet(BetTarget)`, `Spin`, `Hit`, `CashOut`). `View` covers app screens (`Connect`, `Trust`, `TrustMismatch`, `Login`, `Lobby`, `History`, `Game(GameView)`) and per-game state (reels, felt with chips and cursor, hand with legal actions, crash phase and multiplier). 3. **The core owns navigation.** Which screen is showing and which game is open is state, not presentation; both frontends must behave identically. 4. **egui (eframe).** Immediate mode suits per-frame custom drawing; forms, text input, and scrolling come free; MIT/Apache; ~10–20 MB. Slint was rejected for its retained/declarative model, which fights per-frame drawing of the crash curve and particles. Macroquad was rejected because every input field and list would be hand-built. Webviews were rejected on memory and system dependencies. 5. **A `gui/` workspace member.** The existing package keeps the core, TUI, and server bins and all current test paths. egui's ~150 transitive crates must never enter the TUI or server build graph — the same mistake as the package-wide `native-tls` dependency. 6. **A blocking network thread with channels.** The core is I/O-free; the adapter owns the TLS stream. One thread reads/writes, forwards every line, and wakes the UI via `egui::Context::request_repaint`. No async runtime: the protocol has one request in flight plus pushes. Request deadlines live in the shared adapter so both frontends time out identically. 7. **First contact is explicit.** The handshake completes with a record-only decision, then the adapter asks before sending anything. New key: modal with the fingerprint. Changed key: hard stop with an explicit "forget and reconnect". The TUI keeps its print-and-accept prompt via the same state machine. 8. **Single-file binaries with embedded assets**, named `zagan-bet-gui---[.exe]`, plus bash/PowerShell installers. The site lists both. No auto-update; the lobby points at the download page when the server advertises a newer release. ## Consequences - The client core is rewritten at its boundary before any GUI code exists: `Effect`, the `Game` trait, and `handle_key` all change. This is the cost of keeping one implementation of reserve/settle/forfeit and payout rules. - The TUI's terminal concerns (prompt editing, wheel animation, interactive input) move into the TUI adapter, where they belong. - The GUI must be themed deliberately; egui's defaults look like a debug tool. A style spike on one reference screen gates the game ports. - Windows binaries are unsigned; SmartScreen will warn until a certificate is bought. No auto-update means players download again. - The refactor is testable headlessly: the TUI adapter and the core tests remain the regression net, and `View` can be asserted without pixels. ## Alternatives considered - **Separate GUI client** reusing only rules and protocol: faster to start, duplicates connection/session/timeout flows and lets the two clients drift on the invariants in `AGENTS.md`. Rejected. - **Hybrid: GUI reads `GameState` directly**: avoids the refactor but creates a second parser of stringly-typed data, guaranteeing renderer drift. Rejected. - **Slint**: best declarative animation story and smallest runtime, but the retained model is wrong for per-frame custom drawing. Rejected after the license objection (GPL) was removed by open-sourcing. - **Tauri / webview**: easiest effects, but a system webview breaks the memory budget and adds a distro-dependent dependency. Rejected. - **tokio**: no concurrency to exploit. Rejected.