Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

runi-cli

runi-cli packs two small things together: a zero-dependency command-line argument parser (the "launcher") and Tint, a chainable API for ANSI-colored terminal text. The launcher mirrors Uni's @command / @option / @argument ergonomics and integrates with Tint for styled help output.

The launcher

Single-command CLI

Annotate a struct with #[derive(Command)] and pick up a full trait implementation — no hand-written schema or parser code.

use runi_cli::{Command, Launcher, Runnable, Result};

/// Greet someone from the command line.
#[derive(Command)]
#[command(name = "greet")]
struct Greeter {
    /// Shout instead of whisper.
    #[option("-l,--loud")]
    loud: bool,

    /// How many times to greet.
    #[option("-n,--count")]
    count: Option<u32>,

    /// Who to greet.
    #[argument]
    target: String,
}

impl Runnable for Greeter {
    fn run(&self) -> Result<()> {
        let times = self.count.unwrap_or(1);
        for _ in 0..times {
            if self.loud {
                println!("HELLO, {}!", self.target.to_uppercase());
            } else {
                println!("hello, {}", self.target);
            }
        }
        Ok(())
    }
}

fn main() {
    Launcher::<Greeter>::of().execute();
}

Invoke as greet --loud -n 3 world. The -h/--help flag is always available and renders a Tint-styled banner that matches the field layout.

Subcommands

Nest structs under an enum and generate the dispatch table with the same #[derive(Command)].

use runi_cli::{Command, Launcher, ParseResult, Result, SubCommandOf, CommandSchema};

struct GitApp { verbose: bool }

impl Command for GitApp {
    fn schema() -> CommandSchema {
        CommandSchema::new("git", "A toy VCS").flag("-v,--verbose", "Verbose")
    }
    fn from_parsed(p: &ParseResult) -> Result<Self> {
        Ok(Self { verbose: p.flag("--verbose") })
    }
}

#[derive(Command)]
#[command(description = "Initialize a repository")]
struct InitCmd {
    /// Directory to initialize (defaults to the current one).
    #[argument]
    dir: Option<String>,
}

impl SubCommandOf<GitApp> for InitCmd {
    fn run(&self, git: &GitApp) -> Result<()> {
        let dir = self.dir.as_deref().unwrap_or(".");
        if git.verbose { println!("initializing {dir}"); }
        Ok(())
    }
}

#[derive(Command)]
#[command(description = "Clone a repository")]
struct CloneCmd {
    /// Repository URL.
    #[argument]
    url: String,
    /// Clone depth.
    #[option("--depth")]
    depth: Option<u32>,
}

impl SubCommandOf<GitApp> for CloneCmd {
    fn run(&self, _: &GitApp) -> Result<()> {
        println!("cloning {}", self.url);
        Ok(())
    }
}

#[derive(Command)]
enum GitSub {
    Init(InitCmd),
    Clone(CloneCmd),
}

fn main() {
    // GitSub::register_on is generated by the enum derive — it attaches
    // every variant to the launcher in one shot.
    GitSub::register_on(Launcher::<GitApp>::of()).execute();
}

Field type → CLI semantics

The field type is load-bearing. The derive picks the right ParseResult call based on what you wrote, so a correctly-typed field is also a correctly-validated CLI.

Field type#[option] meaning#[argument] meaning
boolFlag: presence sets true(rejected — use option)
T: FromArgRequired valueRequired positional
Option<T>Optional value (None when absent)Optional positional
Vec<T>Repeatable value (-f a -f b[a, b])(rejected — use option)

T: FromArg is automatic for every T: FromStr — so i32, u64, f64, String, PathBuf, and your own types with a FromStr impl all work without extra glue.

Descriptions

Precedence: explicit description = "..." → doc comment (///) → empty. Doc comments on the struct become the top banner; doc comments on fields become option/argument descriptions.

Manual implementation (escape hatch)

The derive generates code against a public trait + builder API, so you can always hand-write the impl for dynamic schemas:

use runi_cli::{Command, CommandSchema, ParseResult, Result};

struct MyApp { verbose: bool, file: std::path::PathBuf }

impl Command for MyApp {
    fn schema() -> CommandSchema {
        CommandSchema::new("my-app", "My application")
            .flag("-v,--verbose", "Enable verbose output")
            .argument("file", "Input file to process")
    }
    fn from_parsed(p: &ParseResult) -> Result<Self> {
        Ok(Self {
            verbose: p.flag("--verbose"),
            file: p.require::<std::path::PathBuf>("file")?,
        })
    }
}

Disabling the derive

The derive ships behind a default derive feature. Turn it off if you only want the runtime trait + builder:

runi-cli = { version = "*", default-features = false }

Tint — terminal styling

use runi_cli::{Tint, supports_color};

fn main() {
    if supports_color() {
        println!("{}", Tint::red().bold().paint("error: something went wrong"));
        println!("{}", Tint::cyan().italic().paint("hint: try --help"));
    }
}
  • Foreground colors (basic + bright) and basic backgrounds
  • ANSI 256-color and 24-bit RGB support (foreground + background)
  • Chainable style modifiers: bold, italic, underline, dimmed, strikethrough
  • supports_color() (stderr) and supports_color_stdout() — the launcher uses these internally to keep --help | cat plain when redirected.

See runi-cli/examples/tint_demo.rs in the repo for a full Tint demo.