Skip to content

Latest commit

 

History

History
271 lines (183 loc) · 13.4 KB

rust-guidelines.md

File metadata and controls

271 lines (183 loc) · 13.4 KB

Rust guidelines for romm-cli

Reference for Rust CLI/TUI best practices and known improvement areas in this project. Use this when implementing features or refactoring so changes stay aligned with current ecosystem conventions (2025–2026).

For how the codebase is organized today, see architecture.md.


Industry consensus (2025–2026)

Modern Rust CLI tools (ripgrep, bat, starship, uv-style) generally follow:

Area Recommended approach
Structure Thin main.rs, library crate (lib.rs), commands/ per subcommand, separate client/ for I/O
CLI clap derive API, global flags, shell completions via clap_complete
Async tokio only at I/O boundaries; keep business logic sync where possible
Errors thiserror in library/public APIs; anyhow at the binary boundary with .context()
Logging tracing + tracing-subscriber (not raw println! for diagnostics)
HTTP reqwest + rustls (avoid OpenSSL), typed endpoints, serde models
TUI ratatui + crossterm, event loop + channels for background work
Testing assert_cmd + wiremock / httpmock for integration tests
Distribution strip = true, cross-compiled release artifacts, optional self-update — see releases.md

External references


Target architecture (API client + CLI + TUI)

Current layout (workspace — see Gap 4):

romm-cli/              # workspace root
├── romm-api/          # RommClient, endpoints, core, config, error, types
├── romm-cli/          # commands, CLI binary, completions (re-exports romm_api)
└── romm-tui/          # TUI screens, handlers, romm-tui binary

Design rules

  1. Library-first — binaries are thin wrappers; logic lives in the crate so CLI, TUI, and tests share it.
  2. One HTTP client type — everything goes through RommClient; no scattered reqwest calls.
  3. Separate frontends — romm-cli and romm-tui are independent crates; both depend on romm-api only.
  4. Typed errors at boundaries — e.g. ApiError, ConfigError, DownloadError; compose with #[from]; use anyhow only in main.
  5. Background work via channels — TUI main loop polls/receives; network/download runs on tokio tasks.

What romm-cli already does well

  • Library crate with client, commands, endpoints, core, frontend split
  • clap derive, global --json / --verbose
  • reqwest with rustls, no native-tls (good for cross-builds)
  • tracing, keyring, wiremock / assert_cmd in dev-dependencies
  • TUI uses screen modules + background task polling
  • Release profile strips binaries

Improvement gaps

Each gap below is a self-contained guideline. When working on one, read its section and check off acceptance criteria before considering it done.


Gap 1: Typed errors in the client layer

Current state: RommClient and load_config return typed errors (ApiError, ConfigError, DownloadError, composed as RommError). Some legacy command handlers still use anyhow internally and are wrapped at the CLI boundary. TUI background channels carry RommError and map to UI text via user_message() at apply time.

Recommended approach:

  • Define per-domain error enums with thiserror:
    • ApiError — HTTP status, auth, rate limits
    • ConfigError — missing file, invalid JSON, keyring failures
    • DownloadError — I/O, checksum, interrupted transfer
  • Use #[from] for automatic conversion from lower-level errors (reqwest::Error, serde_json::Error, std::io::Error).
  • Compose at higher levels with #[error(transparent)] or wrapper variants.
  • Reserve anyhow for main.rs / top-level run_app() only; map typed errors to user-facing messages and exit codes there.

Rule of thumb: Use thiserror when callers need to handle errors (match on variants). Use anyhow when callers only need to report errors (log, display, exit).

Example pattern:

#[derive(Debug, thiserror::Error)]
pub enum ApiError {
    #[error("unauthorized — check credentials")]
    Unauthorized,
    #[error("request failed: {0}")]
    Request(#[from] reqwest::Error),
    #[error("invalid response: {0}")]
    Parse(#[from] serde_json::Error),
}

Acceptance criteria:

  • Public API of RommClient returns typed errors (or a composed RommError), not anyhow::Error
  • CLI/TUI map known variants to actionable messages (e.g. “run romm-cli init” on auth failure) via user_message() / set_error(RommError)
  • Error chains preserved via #[source] / #[from] for debugging with {:#} on typed variants

References: Microsoft error handling patterns, thiserror + anyhow guide


Gap 2: Shell completions

Current state: Completions are generated at runtime via romm-cli completions <shell> (clap_complete in the completions subcommand). No committed static scripts or build-time generation.

Recommended approach:

  • Keep clap_complete as a normal dependency; generate from Cli / Commands in the completions handler.
  • Document install steps in cli.md (romm-cli completions bash > …).

Acceptance criteria:

  • romm-cli completions <shell> covers bash, zsh, fish, PowerShell, and Elvish
  • cli.md section explains how to install completions per shell

References: Clap completions docs, Rust CLI best practices guide — completions section


Gap 3: Meaningful exit codes

Current state: exit_code() in romm-api/src/error.rs maps RommError to romm_api::exit::{SUCCESS,GENERAL,USAGE,CONFIG,API}. romm-cli/src/main.rs calls std::process::exit(exit_code(&e)). Legacy command handlers still return anyhow::Result; from_anyhow() downcasts typed domain errors at the CLI boundary. Clap parse errors exit with 2 before run_app() runs.

Recommended approach:

  • Define a small ExitCode enum or constants:
    • 0 — success
    • 1 — general/unknown failure
    • 2 — usage / invalid arguments (clap already uses 2 for parse errors if configured)
    • 3 — configuration / auth error
    • 4 — API / network error
  • Map typed errors (Gap 1) to codes in run_app() before std::process::exit.
  • Document exit codes in cli.md for scripting users.

Acceptance criteria:

  • Scripts can distinguish auth/config failures from generic errors
  • Exit codes documented in cli.md or --help long about text
  • Integration tests assert expected exit codes where relevant

References: Rust CLI error handling with clap


Gap 4: Workspace split

Current state: Workspace split complete (2026-06-06). Three members: romm-api, romm-cli, romm-tui. Rationale: workspace-split ADR. romm-cli re-exports romm_api for crates.io consumers; new embedders should depend on romm-api directly.

romm-cli/          # workspace root
├── romm-api/      # client, endpoints, types, core (no clap/ratatui)
├── romm-cli/      # binary + commands
└── romm-tui/      # binary + tui/ (depends on romm-api)
  • romm-openapi-gen bin lives in romm-api; shell completions via romm-cli completions <shell>.

Acceptance criteria:

  • Decision criteria documented; split enables third frontends without CLI/TUI deps
  • cargo test -p romm-api runs without TUI feature graph

References: Rust project structure best practices


Gap 5: TUI event / action separation

Current state: tui/app/run.rs delegates to TuiSession + poll_frame_events → map_event → App::update. Background work drains via drain_background_events → Action. Screen key mapping lives in handlers/screen_keys.rs; complex screens still use per-screen *Key actions applied in update.rs.

Recommended approach (Ratatui async template):

  • Add tui/event.rs (or tui/app/event.rs):
    • Event — raw input (key, resize, tick, background message)
    • Action — semantic intents (NavigateHome, StartDownload, Quit)
  • Main loop becomes: poll events → map to actions → app.update(action) → render.
  • Background tasks send Action on tokio::sync::mpsc instead of mutating shared state directly where possible.

Acceptance criteria:

  • Input handling centralized; render screens do not read crossterm (widgets/path picker and setup wizard input are the boundary)
  • Background completions arrive as actions via drain_background_events → App::update
  • run.rs is loop + dispatch; business logic lives in update.rs / handlers

References: Ratatui event-driven-async template, Full async actions tutorial, Component template


Gap 6: Layered configuration

Current state: Layered merge in load_config() (defaults → config.json → env → keyring) plus narrow command-specific CLI runtime overrides. Documented in module docs, api.md, and architecture.md.

Precedence model (per field):

  1. Built-in defaults
  2. config.json
  3. Environment variables (override file)
  4. OS keyring (resolve secret sentinels after merge)
  5. Command-specific CLI runtime (download --output, sync run --download-dir; init/auth login persist to file)

There is no global CLI > env for connection settings on normal commands.

Acceptance criteria:

  • Single documented precedence order (see api.md and config.rs module docs)
  • Sensitive values never logged by tracing (passwords, tokens, API keys)
  • ROMM_* env vars listed in api.md

References: Modern Rust CLI — serde flatten


Gap 7: CLI/TUI UX polish

Current state: CliPresentation centralizes color (NO_COLOR, CLICOLOR), progress (stderr / suppressed in JSON), and JSON stdout. Actionable errors via user_message(); verbose chains on stderr with --verbose. Help examples and json-output.md document scripting surfaces.

Recommended approach:

Item Guideline
Color Respect NO_COLOR and CLICOLOR=0; disable styling when stdout is not a TTY
Errors Print to stderr; use {:#} for full anyhow chains in verbose mode only
JSON mode Stable field names; version schema in docs if scripts depend on it
Progress Use indicatif for long operations; suppress bars when --json or non-TTY
Help text Every subcommand has about + examples where non-obvious

Acceptance criteria:

  • No ANSI codes when NO_COLOR is set
  • --json never interleaves human progress UI on stdout
  • Error messages suggest next step where possible (init, auth, check URL)

References: Building CLI tools with Clap (2026)


Reference projects to study

Project / template Learn from it
ratatui/templates — component Tokio + screen components + async events
ratatui/templates — event-driven-async Minimal event/action loop
ripgrep, bat, fd CLI ergonomics, performance, error messages
starship, uv Self-update, cross-platform releases, feature flags

Quick checklist before merging significant changes

  • Logic in core/ or client/, not duplicated in CLI and TUI
  • New API surface goes through endpoints/ + RommClient
  • Errors typed or given context; no new bare unwrap() in production paths
  • Tests: unit tests for pure logic; wiremock/httpmock for HTTP; assert_cmd for CLI if behavior is user-visible
  • TUI changes: background work does not block the render loop
  • romm-cli changes do not add a dependency on romm-tui

Last updated: 2026-06-25 — TUI background errors typed; plan doc statuses refreshed.