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.
- Crate:
runi-clion crates.io - API reference: docs.rs/runi-cli
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 |
|---|---|---|
bool | Flag: presence sets true | (rejected — use option) |
T: FromArg | Required value | Required 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) andsupports_color_stdout()— the launcher uses these internally to keep--help | catplain when redirected.
See runi-cli/examples/tint_demo.rs in the repo for a full Tint demo.