Implementing Rust CLI/TUI applications from specifications. Handles Cargo setup, module structure, dependency selection, error handling patterns, and system dependency documentation...
Implement Rust CLI/TUI applications following best practices for project structure, dependency management, error handling, and documentation.
Critical Rules:
Result types with proper error handlingCargo.lock for applications (not libraries)cargo check when possibleMake a todo list for all the tasks in this workflow and work on them one after another.
Read the specification, plan document, or requirements:
Identify:
Determine module structure:
CRITICAL: Document this before writing code
Common Rust system dependencies by category:
Audio:
libasound2-dev (Debian) / alsa-lib-devel (Fedora) / alsa-lib (Arch)libpulse-devlibjack-devGUI/Graphics:
libx11-dev, libxtst-dev, libxcb-devlibwayland-devlibgl1-mesa-devNetworking/Crypto:
libssl-dev, pkg-configDatabases:
libpq-devlibmysqlclient-devlibsqlite3-devCompression:
zlib1g-devlibbz2-devliblzma-devSystem utilities:
libdbus-1-devlibsystemd-devDocument platform-specific install commands in README.
Set up project manifest with appropriate metadata and dependencies.
Basic structure:
[package]
name = "project-name"
version = "0.1.0"
edition = "2021" # Use latest stable edition
authors = ["Author Name <email@example.com>"]
description = "Brief description"
license = "MIT" # or MIT-OR-Apache-2.0, GPL-3.0, etc.
repository = "https://github.com/user/repo"
keywords = ["keyword1", "keyword2"]
categories = ["command-line-utilities"]
# For binary applications
[[bin]]
name = "binary-name"
path = "src/main.rs"
# For libraries, use:
# [lib]
# name = "library_name"
# path = "src/lib.rs"
[dependencies]
# Core dependencies here
[dev-dependencies]
# Testing dependencies here
Dependency Selection Cheatsheet:
| Need | Recommended Crate | Notes |
|---|---|---|
| Error handling (app) | anyhow |
Flexible, ergonomic |
| Error handling (lib) | thiserror |
Typed errors |
| CLI parsing | clap v4 |
Feature-rich, derive API |
| TUI framework | ratatui |
Modern, actively maintained |
| Terminal handling | crossterm |
Cross-platform |
| Configuration | serde + toml |
Standard approach |
| JSON | serde_json |
Standard |
| Async runtime | tokio |
Most popular, full-featured |
| HTTP client | reqwest |
High-level, async |
| HTTP server | axum or actix-web |
Modern frameworks |
| Logging | tracing |
Structured logging |
| Date/Time | chrono |
Comprehensive |
| Regex | regex |
Standard |
| Random | rand |
Standard |
| Audio | cpal |
Cross-platform capture/playback |
| Keyboard/Mouse | enigo |
Input simulation |
| Path expansion | shellexpand |
Tilde expansion |
Version specification:
"1.0" for stable APIs (allows patch updates)"*" - be explicitCreate directory structure:
mkdir -p src
Minimal src/main.rs:
fn main() {
println!("Hello, world!");
}
OR for libraries, src/lib.rs:
//! Library documentation here
pub fn example() {
println!("Example function");
}
Initial validation:
cargo check
If successful: ā Proceed with implementation
If fails due to system deps: ā ļø Document clearly, note code can't be validated until deps installed
If fails due to code errors: Fix them before proceeding
Implement in dependency order (dependencies before dependents):
Standard module pattern:
// src/config.rs - Configuration module
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
// Configuration fields
}
impl Default for Config {
fn default() -> Self {
// Sensible defaults
}
}
impl Config {
pub fn load() -> Result<Self> {
// Load from file, create default if missing
}
pub fn save(&self) -> Result<()> {
// Save to file
}
fn validate(&self) -> Result<()> {
// Validate configuration values
}
}
State machine pattern:
// src/state.rs - Application state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppState {
Idle,
Processing,
Complete,
Error,
}
pub struct AppContext {
pub state: AppState,
// Other context fields
}
impl AppContext {
pub fn new() -> Self {
Self {
state: AppState::Idle,
}
}
pub fn transition(&mut self, new_state: AppState) {
self.state = new_state;
}
}
Error handling pattern for libraries:
// src/error.rs - Typed errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Configuration error: {0}")]
Config(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
}
pub type Result<T> = std::result::Result<T, MyError>;
Module organization:
// src/main.rs or src/lib.rs
mod config;
mod state;
mod error;
mod business_logic;
mod ui;
use anyhow::Result;
use config::Config;
use state::AppContext;
fn main() -> Result<()> {
// Application entry point
let config = Config::load()?;
let mut app = AppContext::new();
// Run application
run(config, &mut app)?;
Ok(())
}
fn run(config: Config, app: &mut AppContext) -> Result<()> {
// Main logic
Ok(())
}
Standard configuration approach:
1. Config struct with serde:
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub general: GeneralConfig,
pub service: ServiceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralConfig {
pub log_level: String,
pub data_dir: String,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
log_level: "info".to_string(),
data_dir: "~/.local/share/myapp".to_string(),
}
}
}
2. Create example config file:
Location: .config/<projectname>/config.toml.example
# Example configuration file
# Copy to ~/.config/<projectname>/config.toml and customize
[general]
# Logging level: trace, debug, info, warn, error
log_level = "info"
# Data directory for application files
data_dir = "~/.local/share/myapp"
[service]
# Service-specific settings
host = "localhost"
port = 8080
3. Config loading with validation:
impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
if !config_path.exists() {
eprintln!("Config not found, creating default at {:?}", config_path);
let config = Self::default();
config.save()?;
return Ok(config);
}
let content = fs::read_to_string(&config_path)
.context("Failed to read config file")?;
let mut config: Config = toml::from_str(&content)
.context("Failed to parse config file")?;
// Expand ~ in paths
config.general.data_dir = shellexpand::tilde(&config.general.data_dir).to_string();
config.validate()?;
Ok(config)
}
fn config_path() -> Result<PathBuf> {
let home = std::env::var("HOME")
.context("HOME environment variable not set")?;
Ok(PathBuf::from(home).join(".config/myapp/config.toml"))
}
fn validate(&self) -> Result<()> {
// Validate configuration values
if self.service.port == 0 {
anyhow::bail!("Port must be non-zero");
}
Ok(())
}
}
When to use async:
When NOT to use async:
Basic async pattern with tokio:
[dependencies]
tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::load()?;
run(config).await?;
Ok(())
}
async fn run(config: Config) -> Result<()> {
// Async operations here
Ok(())
}
For terminal UI applications:
[dependencies]
ratatui = "0.26"
crossterm = "0.27"
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
widgets::{Block, Borders, Paragraph},
Terminal,
};
use std::io;
fn main() -> Result<()> {
// Setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Run app
let result = run_app(&mut terminal);
// Restore terminal
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>) -> Result<()> {
loop {
terminal.draw(|f| {
let size = f.size();
let block = Block::default().title("App").borders(Borders::ALL);
let paragraph = Paragraph::new("Hello TUI").block(block);
f.render_widget(paragraph, size);
})?;
if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
break;
}
}
}
}
Ok(())
}
Attempt to build:
cargo check # Fast, checks compilation
cargo build # Full build
cargo build --release # Optimized build
Interpret results:
ā Success:
Compiling myproject v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 5.2s
ā Code is valid, continue
ā ļø Missing system dependencies:
error: failed to run custom build command for `alsa-sys v0.3.1`
...
The system library `alsa` required by crate `alsa-sys` was not found.
ā Expected, document in README, code structure is correct
ā Code errors:
error[E0425]: cannot find value `x` in this scope
ā Fix the code errors, re-run cargo check
Handle the build appropriately:
# If build fails due to missing deps, clean up
cargo clean
# Document the requirement clearly
echo "Build requires: libasound2-dev libx11-dev" >> README.md
Unit tests:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_validation() {
let config = Config::default();
assert!(config.validate().is_ok());
}
}
Integration tests:
Create tests/integration_test.rs:
use myproject::Config;
#[test]
fn test_full_workflow() {
// Integration test here
}
Run tests:
cargo test
Code documentation:
//! Module-level documentation
//!
//! Explain what this module does.
/// Brief description of function.
///
/// # Arguments
///
/// * `arg1` - Description
/// * `arg2` - Description
///
/// # Examples
///
/// ```
/// use myproject::example;
/// let result = example(42);
/// ```
///
/// # Errors
///
/// Returns error if...
pub fn example(arg1: i32) -> Result<String> {
// Implementation
}
Generate docs:
cargo doc --open
Create comprehensive README with:
Required sections:
# Project Name
Brief description and tagline.
## Features
- Feature 1
- Feature 2
- Feature 3
## Installation
### System Dependencies
**Ubuntu/Debian:**
```bash
sudo apt install libasound2-dev libx11-dev
```
**Fedora:**
```bash
sudo dnf install alsa-lib-devel libX11-devel
```
**Arch Linux:**
```bash
sudo pacman -S alsa-lib libx11
```
### Build from Source
```bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Clone and build
git clone https://github.com/user/project.git
cd project
cargo build --release
# Install
sudo cp target/release/myapp /usr/local/bin/
```
## Configuration
Create `~/.config/myapp/config.toml`:
```toml
# Configuration here
```
See `.config/myapp/config.toml.example` for all options.
## Usage
```bash
myapp [OPTIONS]
```
## Troubleshooting
### "System library not found"
Install the system dependencies listed above.
### Other common issues...
## License
MIT
Is this a library?
āā Yes ā Use thiserror for typed errors
ā Define error enum with all cases
ā Return Result<T, YourError>
ā
āā No (application) ā Use anyhow
Return Result<T, anyhow::Error>
Use .context() for error context
Need configuration?
āā Use serde + (toml | json | yaml)
Need CLI args?
āā Use clap with derive feature
Need TUI?
āā Use ratatui + crossterm
Need async?
āā I/O bound ā Use tokio
āā CPU bound ā Use rayon for parallelism
Need HTTP?
āā Client ā Use reqwest
āā Server ā Use axum or actix-web
What are you building?
āā Application/Binary ā Commit Cargo.lock ā
ā (Ensures reproducible builds)
ā
āā Library ā Don't commit Cargo.lock ā
(Let downstream decide versions)
Add to .gitignore
# Check compilation without building
cargo check
# Build debug version
cargo build
# Build optimized version
cargo build --release
# Run the application
cargo run
# Run with arguments
cargo run -- --arg1 value
# Run tests
cargo test
# Run specific test
cargo test test_name
# Generate and open documentation
cargo doc --open
# Check for outdated dependencies
cargo outdated # Requires cargo-outdated
# Update dependencies
cargo update
# Clean build artifacts
cargo clean
# Format code
cargo fmt
# Lint code
cargo clippy
Problem: Should I commit Cargo.lock?
Solution:
Problem: Build fails on missing system library
Solution:
Problem: Added tokio but everything got complicated
Solution:
Problem: Compile times are slow, binary is huge
Solution:
# Use minimal features
tokio = { version = "1", features = ["rt", "macros"] }
# Instead of
tokio = { version = "1", features = ["full"] }
Problem: Config paths with ~ don't work
Solution:
// Use shellexpand
let path = shellexpand::tilde("~/.config/app/config.toml");
After implementation, provide summary:
Rust Project Implementation Complete:
Project Type: [CLI / TUI / Library / Service]
Module Structure:
config.rs - Configuration managementstate.rs - Application stateDependencies:
Build Status:
Features:
Configuration:
~/.config/<project>/config.toml.config/<project>/config.toml.exampleDocumentation:
cargo doc --open for API docsNext Steps:
cargo build --releaseNotes: