CLI & config
Coming from Python / Node? No
argparse, noyargs, nodotenv. Reading arguments and environment is a few direct calls understd::env; layering argv over env over defaults is a smallstd::cli::Resolver. Rich flag parsing (--name=value, subcommands) is library territory, not built into the language.
Arguments and environment
Section titled “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 atarg(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
Section titled “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
Section titled “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 = stderrstd::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 terminalFor 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
Section titled “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.