# DESIGN.md # tvctl — Design Decisions and Rationale # This document explains the why behind decisions in PROJECT_MAP.md. # Read this if you want to understand the reasoning, not just the choices. --- ## Vision tvctl exists because the Roku ECP API (and similar TV control APIs) are fully capable but completely inaccessible to technical users without writing custom code. There is no good CLI tool, no stable local API, and no abstraction layer that works across TV brands. The goal is to be that abstraction layer — the foundation that other tools are built on, and a useful tool in its own right. --- ## Why a Daemon? A stateless CLI that talks directly to TVs on every invocation would be simpler to build but has fundamental limitations: - Device discovery (SSDP) takes time — you don't want it on every command - The app cache needs to live somewhere persistent and shared - An HTTP API for tool builders needs a persistent process - State caching requires something running continuously The daemon owns all of this. The CLI is a thin client. This is the correct architecture for a tool with these requirements. --- ## Why Unix Socket for CLI, HTTP for API? Two different audiences, two different needs: **CLI users** are local, trusted, and interactive. A Unix socket is: - Faster than HTTP for local IPC - Inherently secure (filesystem permissions, user-only access) - No port to conflict with other software - The right tool for local process communication **Tool builders** need HTTP because: - Every language has an HTTP client - REST is a universal interface - It works from scripts, Home Assistant, stream decks, anything - Versioning (`/v1/`) is natural in HTTP Keeping these separate means the HTTP API port is optional — you can disable it entirely and still use the full CLI. Security-conscious users appreciate this. --- ## Why Resource-Verb CLI Pattern? ``` tvctl device list ← resource-verb tvctl list-devices ← verb-resource (rejected) ``` Resource-verb was chosen because: 1. It mirrors REST naturally — `GET /devices` → `tvctl device list` 2. It scales — as commands grow, the namespace stays organized 3. Tab completion is more useful — type `tvctl device ` to see all device commands 4. It matches tools users already know (`git remote add`, `kubectl pod get`) The 1:1 mapping between CLI and API is intentional and valuable. Learn one, know both. --- ## Why Organic App Cache? The temptation is to ship a pre-populated database of app IDs. This was explicitly rejected: - It requires ongoing maintenance as apps are added/removed - It would contain apps the user doesn't have installed (bloat, confusion) - App IDs are stable — once discovered, they never need to be re-fetched - The first-fetch delay is tiny and happens once per app, ever The organic approach means the cache is always perfectly accurate for this user's TV. It grows automatically through normal use. It requires zero maintenance. --- ## Why Loopback-Only HTTP by Default? The HTTP API exposes control over your TV. Binding to `0.0.0.0` by default would mean anyone on your network could control your TV without authentication. Loopback-only (`127.0.0.1`) is the secure default. Users who want network exposure can change `http_host` in config — but they make that choice consciously. There is no authentication system in v1. The security model is network isolation. This is appropriate for a local tool used by one person. --- ## Why User-Level Systemd Service? `tvctl daemon install` installs a systemd **user** service, not a system service. This means: - No `sudo` required at any point - The daemon runs as the installing user - The Unix socket is owned by that user - Uninstalling is clean and doesn't require root System-level services are for system daemons. tvctld is a user tool. --- ## Why TOML for Config? - It's the de facto standard in the Rust ecosystem - Human-readable and editable without special knowledge - Better than YAML (no indentation pitfalls, no Norway problem) - Better than JSON for config (supports comments, more readable) - `serde` + `toml` crate support is first-class in Rust --- ## Why kebab-case for Key Names? ``` volume-up ← chosen VolumeUp ← Roku's internal name (rejected at surface) volume_up ← snake_case (rejected) ``` - kebab-case is the Unix CLI convention for multi-word values - It's consistent with how flags are named (`--json`, `--device`) - The adapter layer handles translation to platform-specific names - Users never need to know Roku uses `VolumeUp` internally --- ## The Adapter Abstraction This is the most important architectural decision in the project. The adapter trait is a wall between the platform world and the application world. Everything above the adapter layer speaks tvctld's language. Everything below speaks the platform's language. Consequences: - Adding Google TV support means writing one new file (`src/adapters/googletv/`) - The CLI, API, registry, and cache don't change at all - Platform bugs are isolated to their adapter - Testing adapters is straightforward — mock the trait The dev methods (`dev_install`, `dev_reload`, `dev_logs`) have default implementations that return `NotSupported`. This means the trait can be implemented by platforms that don't support developer mode without those platforms needing to handle it explicitly. --- ## State Cache Design State is cached in memory only and cleared on daemon restart. This is intentional: - TVs don't push state — you have to poll - Cached state can be stale by definition - Pretending to have live state you don't have is dishonest and confusing - Every state response includes a `timestamp` so callers know how fresh it is In a future version, WebSocket streaming or polling could make state more live. For v1, honest polling with clear timestamps is the right approach. --- ## App Resolution Priority When resolving an app name to a platform ID: 1. Per-device installed app list (in memory, freshest) 2. Platform cache (on disk, still accurate) 3. Live fetch from TV (fallback, always works) This order minimizes network calls while always being able to resolve. The user can also bypass resolution entirely with a raw `--id` flag.