chore: scaffold tvctl foundation
Set up the Rust crate, baseline module layout, and project docs so the repository matches the design bundle and builds cleanly as a starting point.
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
# 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 <tab>` 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.
|
||||
Reference in New Issue
Block a user