waterui-cli 0.1.3

A modern UI framework for Rust
Documentation
# CLI Crate Architecture

This document describes the architecture and conventions of the `waterui-cli` crate.

## Crate Structure

The CLI is split into two parts:

1. **Library (`cli/src/lib.rs`)** - Core logic, platform abstractions, device management
2. **Terminal (`cli/src/terminal/`)** - User interface, argument parsing, output formatting

**Key principle: Terminal handles interaction only, library handles real logic.**

```
cli/src/
├── lib.rs              # Library entry point (re-exports modules)
├── terminal/           # Binary entry point (UI layer)
│   ├── main.rs         # CLI argument parsing (clap)
│   ├── shell.rs        # Output formatting (spinners, colors, macros)
│   └── commands/       # Command implementations (thin wrappers)
├── device.rs           # Device trait and types
├── platform.rs         # Platform trait
├── project.rs          # Project management (Water.toml, Cargo.toml)
├── apple/              # Apple platform implementation
│   ├── device.rs       # AppleSimulator, MacOS, AppleDevice
│   ├── platform.rs     # ApplePlatform (iOS, macOS, simulator)
│   ├── backend.rs      # Apple backend configuration
│   └── toolchain.rs    # Xcode toolchain checking
├── android/            # Android platform implementation
│   ├── device.rs       # AndroidDevice, AndroidEmulator
│   ├── platform.rs     # AndroidPlatform
│   ├── backend.rs      # Android backend configuration
│   └── toolchain.rs    # Android SDK/NDK toolchain
├── build.rs            # RustBuild for compiling Rust code
├── toolchain/          # Toolchain utilities (doctor, brew, cmake)
├── debug/              # Hot reload server
└── templates/          # Project scaffolding templates
```

## Core Abstractions

### Device Trait (`device.rs`)

Represents something that can run an app (simulator, emulator, physical device).

```rust
pub trait Device: Send {
    type Platform: Platform;
    
    /// Launch the device (boot simulator/emulator). No-op for physical devices.
    fn launch(&self) -> impl Future<Output = eyre::Result<()>> + Send;
    
    /// Run an artifact on the device. Device must be launched first.
    fn run(&self, artifact: Artifact, options: RunOptions) 
        -> impl Future<Output = Result<Running, FailToRun>> + Send;
    
    fn platform(&self) -> Self::Platform;
}
```

**Important**: `launch()` handles booting. For emulators that need to be started from cold, `launch()` should start the emulator process and wait until it's ready. For already-connected devices, `launch()` is a no-op.

Implementations:
- `AppleSimulator` - iOS/tvOS/watchOS simulator (boots via `simctl boot`)
- `MacOS` - Current machine (no-op launch)
- `AndroidDevice` - Connected Android device (waits for device via adb)
- `AndroidEmulator` - AVD that needs to be launched (starts emulator process)

### Platform Trait (`platform.rs`)

Represents a build target platform.

```rust
pub trait Platform: Send {
    type Toolchain: Toolchain;
    type Device: Device;
    
    fn scan(&self) -> impl Future<Output = eyre::Result<Vec<Self::Device>>> + Send;
    fn build(&self, project: &Project, options: BuildOptions) -> impl Future<...>;
    fn package(&self, project: &Project, options: PackageOptions) -> impl Future<...>;
    fn clean(&self, project: &Project) -> impl Future<...>;
    fn toolchain(&self) -> Self::Toolchain;
    fn triple(&self) -> Triple;
}
```

Implementations:
- `ApplePlatform` - iOS, iOS Simulator, macOS, tvOS, etc.
- `AndroidPlatform` - Android with different ABIs (arm64-v8a, x86_64, etc.)

### Project (`project.rs`)

Manages `Water.toml` manifest and orchestrates builds.

Key methods:
- `Project::open()` - Open existing project
- `Project::create()` - Create new project
- `Project::run()` - Build, package, launch device, and run app
- `Project::build()` - Build Rust library
- `Project::package()` - Package for platform

## Terminal Layer Conventions

Terminal commands in `cli/src/terminal/commands/` should:

1. **Parse arguments** using clap
2. **Show progress** using `shell::spinner()`, `success!()`, `error!()`, etc.
3. **Delegate to library** for actual work
4. **Format output** for the user

Example pattern:
```rust
pub async fn run(args: Args) -> Result<()> {
    let project = Project::open(&args.path).await?;
    
    // Show progress
    let spinner = shell::spinner("Building...");
    
    // Delegate to library
    let result = project.build(platform, options).await;
    
    // Handle result with user-friendly output
    match result {
        Ok(_) => success!("Build complete"),
        Err(e) => error!("Build failed: {e}"),
    }
}
```

**Do NOT put heavy logic in terminal commands.** If you find yourself writing complex logic (loops, polling, process management), it belongs in the library layer.

## Device Lifecycle

The correct flow for running an app:

1. **Scan** - `Platform::scan()` returns available devices
2. **Select** - Choose a device (or create an emulator device if none available)
3. **Launch** - Terminal calls `Device::launch()` to boot simulator/emulator (can run in background while building)
4. **Run** - `Project::run()` builds, packages, and runs on the device (assumes device is already launched)

**Important**: `Project::run()` does NOT launch the device - it assumes the device is already launched and ready. The terminal layer (`water run` command) is responsible for:
- Spawning `device.launch()` as a background task
- Building and packaging the app in parallel with device launch
- Waiting for device to be ready before running the app

This allows simulator/emulator boot time to overlap with build time for better UX.

## Adding New Device Types

When adding a new device type:

1. Create a struct in the platform's `device.rs`
2. Implement `Device` trait
3. Put launching logic in `launch()` method
4. Reuse existing device's `run()` when possible

Example: `AndroidEmulator` (in `android/device.rs`):
```rust
pub struct AndroidEmulator {
    avd_name: String,
    device: OnceLock<AndroidDevice>,  // Set after launch
}

impl Device for AndroidEmulator {
    async fn launch(&self) -> Result<()> {
        // Start emulator process
        // Wait for it to boot (poll adb devices)
        // Store resulting AndroidDevice in self.device
    }
    
    async fn run(&self, artifact, options) -> Result<Running, FailToRun> {
        // Delegate to the inner AndroidDevice
        self.device.get().unwrap().run(artifact, options).await
    }
}
```

The terminal command just needs to create the right device type:
```rust
// In terminal/commands/run.rs
if let Some(dev) = devices.into_iter().next() {
    Ok(SelectedDevice::AndroidDevice(dev))
} else {
    // No connected devices - create an emulator device
    let avd = AndroidPlatform::list_avds().await?.first()...;
    Ok(SelectedDevice::AndroidEmulator(AndroidEmulator::new(avd)))
}
```

Then `Project::run()` calls `device.launch()` which handles the emulator startup.

## Error Handling

- Use `color_eyre::eyre::Result` for library functions
- Use `thiserror` for custom error enums (e.g., `FailToRun`, `FailToOpenProject`)
- Terminal layer converts errors to user-friendly messages

## Async Runtime

Uses `smol` for async:
- `smol::process::Command` for spawning processes
- `smol::spawn()` for background tasks
- `smol::Timer` for delays
- `smol::channel` for event streaming
- `smol::future::zip` for parallel operations