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

CLI & config

Coming from Python / Node? No argparse, no yargs, no dotenv. Reading arguments and environment is a few direct calls under std::env; layering argv over env over defaults is a small std::cli::Resolver. Rich flag parsing (--name=value, subcommands) is library territory, not built into the language.

Arguments and environment

fn main() {
    let n = std::env::args_count();        // includes the program name
    let first = std::env::arg(1);          // positional arg 1
    let port  = std::env::arg_or(2, "8080");  // with a default

    let home  = std::env::var("HOME");     // environment variable
    let debug = std::env::var_exists("DEBUG");
}
  • arg(0) is the program name; user arguments start at arg(1).
  • arg_or(i, default) is the everyday form — no bounds-checking dance.
  • var(name) reads an environment variable; var_exists(name) tests for one.

Layered configuration

A common need: a setting should come from a command-line argument if given, else an environment variable, else a built-in default. std::cli::Resolver expresses that precedence directly:

fn main() {
    let cfg = std::cli::Resolver { prefix: "MYAPP" };

    // argv positional "port", else $MYAPP_PORT, else "8080"
    let port = cfg.get("port", "8080");
    let host = cfg.get("host", "127.0.0.1");

    println("listening on ", host, ":", port);
}

The resolver checks the argument, then the prefixed environment variable (MYAPP_PORT), then the supplied default. Empty values fall through to the next layer rather than counting as “set.”

Interactive terminal I/O

For a tool that draws to the terminal or reads keystrokes, a few std:: primitives cover the OS surface without an FFI dependency.

std::term::is_tty(fd) answers “is this a terminal?” — the usual guard for whether to emit color:

let color = std::term::is_tty(2);   // fd 2 = stderr

std::term::size() returns a TermSize { cols, rows } record (and {0, 0} when stdout isn’t a tty). std::term::RawMode is a guard locus that puts the terminal in raw mode for its lifetime — no line buffering, no echo — and restores it on scope exit, and on a panic or unhandled error too via an atexit backstop:

fn main() {
    let raw = std::term::RawMode { };       // birth: enter raw mode
    // ... read keys, draw frames ...
}                                           // dissolve: restore the terminal

For the bytes themselves, std::io::stdin::read_byte(timeout_ms) polls one byte (0..255, -1 on timeout, -2 on EOF), and std::io::stdout::write_bytes(s) does a raw, unbuffered write — it fflushes first so it stays ordered with any println output:

loop {
    let b = std::io::stdin::read_byte(100);   // 100ms poll
    if b == -1 { continue; }                    // timeout: redraw, tick, …
    if b == -2 { break; }                       // EOF
    std::io::stdout::write_bytes("got a key\r\n");
}

These are primitives, not a TUI — key decoding and styling live in a library on top of them.

Where this fits

This is the boundary between the outside world and your program. The idiomatic shape, building on the app locus: main resolves configuration, then constructs the app locus with it.

locus App {
    params { host: String = "127.0.0.1"; port: String = "8080"; }
    fn run() { println("listening on ", self.host, ":", self.port); }
}

fn main() {
    let cfg = std::cli::Resolver { prefix: "MYAPP" };
    let app = App {
        host: cfg.get("host", "127.0.0.1"),
        port: cfg.get("port", "8080"),
    };
    app.run();
}

Configuration enters once, at the edge, and flows inward as typed locus state — never read again from a global deep inside the program. That keeps every setting owned by exactly one locus, the rule from The locus, gently.

Next: seeing what your program is doing — Logging.