cli-forge 0.2.5

Unified CLI framework: runtime command registration with styled output through one API.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
<h1 align="center">
    <img width="99" alt="Rust logo" src="https://raw.githubusercontent.com/jamesgober/rust-collection/72baabd71f00e14aa9184efcb16fa3deddda3a0a/assets/rust-logo.svg">
    <br><b>cli-forge</b><br>
    <sub><sup>API REFERENCE</sup></sub>
</h1>
<div align="center">
    <sup>
        <a href="../README.md" title="Project Home"><b>HOME</b></a>
        <span>&nbsp;&nbsp;</span>
        <span>API</span>
        <span>&nbsp;&nbsp;</span>
        <a href="../CHANGELOG.md" title="Changelog"><b>CHANGELOG</b></a>
        <span>&nbsp;&nbsp;</span>
        <a href="../dev/ROADMAP.md" title="Roadmap"><b>ROADMAP</b></a>
    </sup>
</div>
<br>

> Complete reference for every public item in `cli-forge`, with examples.
>
> **Status:** the output layer below — `out`/`err`, the three styling paths, and
> the color model — is implemented and stable as of **v0.2.5**. The
> command/registration surface (`Command`/`App`) is the FROZEN planned design and
> is marked _(planned, v0.3.0)_; its signatures are the contract sibling crates
> build against and will not drift. See [`dev/ROADMAP.md`]../dev/ROADMAP.md.

## Table of Contents

- [Overview]#overview
- [Installation]#installation
- [Quick Start]#quick-start
- [Output: `out` / `err`]#output
- [Styling path 1 — builder: `style`]#builder
- [Styling path 2 — tags: `parse`]#tags
- [Styling path 3 — named registry: `define_tag` / `tag`]#registry
- [Colors and terminal behavior]#colors
- [Commands: `Command` / `App`]#commands
- [Feature flags]#feature-flags
- [Performance notes]#performance

---

## Overview

cli-forge unifies argument parsing and styled output under one API, with commands
that register at runtime. The design goal is the lightness of argh with the reach
of clap, and — unlike either — output styling lives in the *same* system as
parsing, so extensions (tables, progress, gradients) all speak one layer.

It owns parsing, output, command registration, and help. It does NOT own tables,
progress bars, gradients, layouts, or shells — those are sibling crates in the
cli collection that build on this crate's output API.

This release delivers the output layer: a near-direct plain path (`out`/`err`)
and three ways to add color and attributes that all render to identical bytes for
the same intent, over one cross-platform terminal backend.

---

## Installation

```toml
[dependencies]
cli-forge = "0.2"
```

Color is on by default. For a build that never emits escape sequences (the API
stays complete; every styled value renders as its plain text):

```toml
[dependencies]
cli-forge = { version = "0.2", default-features = false, features = ["std"] }
```

---

## Quick Start

```rust
use cli_forge::{define_tag, err, out, parse, style, tag};

// Plain output — the common case, one call, no allocation for a literal.
out("building...");
err("something went wrong");

// Styling, three ways, all rendering to the same bytes for the same intent:
out(style("done").green().bold());                  // builder
parse("<c=red><b>ERROR:</b></c> <c=#ff8800>low disk</c>"); // inline tags
define_tag("error", style("").red().bold());        // named registry
out(tag("error").render_with("build failed"));
```

---

<h2 id="output">Output: <code>out</code> / <code>err</code></h2>

The plain path. No tag parsing, no styling work — the value is formatted straight
to the stream and followed by a newline. This is the hot path and stays cheap: a
string literal is a near-direct write with no heap allocation.

```rust
pub fn out<T: std::fmt::Display>(value: T);
pub fn err<T: std::fmt::Display>(value: T);
```

**Parameters**

- `value` — anything that implements [`Display`]. A `&str` is written directly; a
  [`Style`]#builder renders on the way out via its own `Display`; a `String`
  from [`parse`]#tags or [`tag`]#registry is written verbatim. `out` writes to
  standard output, `err` to standard error.

**Behavior**

- A trailing newline is always appended (these are line-oriented, like
  `println!`/`eprintln!`).
- A failed write — a closed pipe, for instance — is silently ignored. A
  fire-and-forget print must not panic or abort the program.

**Examples**

Plain lines and formatted values:

```rust
use cli_forge::out;

out("compiling 12 crates");
out(format!("compiled {} of {} targets", 12, 12));

let path = "config.toml";
out(format!("wrote {path}"));
```

Errors and diagnostics on standard error:

```rust
use cli_forge::{err, style};

err("error: missing required argument `--input`");
err(style("error:").red().bold()); // styled marker, plain text follows
```

Mixing plain and styled in the same stream:

```rust
use cli_forge::{out, style};

out("Summary");
out(style("  3 passed").green());
out(style("  1 failed").red().bold());
```

---

<h2 id="builder">Styling path 1 — builder: <code>style</code></h2>

Function-call styling. Chain color and attribute methods onto a value, then drop
the result into [`out`](#output) — [`Style`] implements [`Display`]. Best when the
style is computed or one-off.

```rust
pub fn style<S: Into<String>>(text: S) -> Style;

impl Style {
    // The eight standard named colors:
    pub fn black(self) -> Style;
    pub fn red(self) -> Style;
    pub fn green(self) -> Style;
    pub fn yellow(self) -> Style;
    pub fn blue(self) -> Style;
    pub fn magenta(self) -> Style;
    pub fn cyan(self) -> Style;
    pub fn white(self) -> Style;
    // 24-bit color:
    pub fn hex(self, hex: &str) -> Style;       // "#rrggbb" or "rrggbb"
    pub fn rgb(self, r: u8, g: u8, b: u8) -> Style;
    // Attributes:
    pub fn bold(self) -> Style;
    pub fn underline(self) -> Style;
    // Render to an owned String:
    pub fn render(&self) -> String;
}
// Style: Display, Clone, Debug
```

**`style(text)`**

- `text` — anything convertible into a `String`, so both string literals and
  owned `String`s work. The returned `Style` starts plain.

**The named-color methods** (`black` … `white`) each set the foreground to one of
the eight standard ANSI colors and return `self`, so they chain. Setting a color
twice keeps the last one.

**`hex(hex)`**

- `hex` — a hex color string. A leading `#` is optional; the rest must be exactly
  six hex digits. An invalid string leaves the current color unchanged, so the
  builder never fails or panics.

**`rgb(r, g, b)`**

- `r`, `g`, `b` — the red, green, and blue channels, each `0..=255`. Equivalent to
  the matching `hex` string. On terminals without 24-bit support the color is
  downgraded at render time (see [Colors]#colors).

**`bold()` / `underline()`** set the respective attribute and return `self`.

**`render(&self) -> String`** renders to an owned `String`, the same bytes
producing the value via `Display`. The color depth matches the terminal detected
for standard output, so on a pipe or under `NO_COLOR` the result is the plain
text.

> Method call order does not affect the output. Parameters are always emitted in
> a fixed canonical order — bold, underline, then color — so the same intent
> yields the same bytes no matter how you chain.

**Examples**

Named colors and attributes:

```rust
use cli_forge::{out, style};

out(style("PASS").green().bold());
out(style("note").cyan());
out(style("deprecated").yellow().underline());
```

24-bit color via hex and rgb:

```rust
use cli_forge::{out, style};

out(style("amber warning").hex("#ff8800"));
out(style("teal ok").rgb(0, 200, 120));
out(style("https://example.com").hex("#3b82f6").underline());
```

Rendering to a string instead of printing — for logging, tables, or further
composition:

```rust
use cli_forge::style;

let label = style("ERROR").red().bold().render();
let line = format!("{label}: {}", "build failed");
assert!(line.contains("ERROR"));
```

Invalid hex is ignored rather than panicking:

```rust
use cli_forge::style;

let s = style("x").hex("not-a-color"); // color left unset
assert_eq!(s.render(), "x");
```

---

<h2 id="tags">Styling path 2 — tags: <code>parse</code></h2>

Inline markup. The whole line is one string with tags; parsing cost is paid only
here, never in [`out`](#output). Best when the text and its styling are written
together, like a template.

```rust
pub fn parse<S: AsRef<str>>(tags: S);
```

**Parameters**

- `tags` — anything that is `AsRef<str>` (a `&str` or `String`). The styled result
  is printed to standard output, followed by a newline.

**Tag grammar**

| Tag | Effect |
|-----|--------|
| `<b>…</b>` | bold |
| `<u>…</u>` | underline |
| `<c=VALUE>…</c>` | foreground color; `VALUE` is a named color, `#rrggbb`, or `r,g,b` |
| `</>` | close the most recently opened tag, whatever it was |

Tags nest. Anything that is not a recognized tag is emitted verbatim, so `parse`
never rejects input — a stray `<` or an unknown `<tag>` simply prints as written.
A `<c=…>` with an unparseable value opens a balanced span that inherits the
surrounding color rather than failing.

**Examples**

A diagnostic line with a colored, bold marker:

```rust
use cli_forge::parse;

parse("<c=red><b>ERROR:</b></c> <c=#ff8800>disk almost full</c>");
```

Nested and mixed styling in one template:

```rust
use cli_forge::parse;

parse("<b>tests</b>: <c=green>12 passed</c>, <c=red>1 failed</c>, <c=128,128,128>3 skipped</c>");
```

Plain text and stray delimiters pass through unharmed:

```rust
use cli_forge::parse;

parse("use a < b to compare; <unknown> tags print literally");
```

The rendered bytes are identical to the equivalent [builder](#builder) output for
the same intent — `parse("<c=red><b>X</b></c>")` matches
`style("X").red().bold()`.

---

<h2 id="registry">Styling path 3 — named registry: <code>define_tag</code> / <code>tag</code></h2>

Define a style once by name, recall it anywhere. The DRY path: describe the look
in one place — even one module — and reuse it by name across the program. Best
when the same style recurs.

```rust
pub fn define_tag<S: Into<String>>(name: S, style: Style);
pub fn tag(name: &str) -> Tag;

impl Tag {
    pub fn render_with(&self, text: &str) -> String;
}
// Tag: Clone, Copy, Debug
```

**`define_tag(name, style)`**

- `name` — the lookup key, anything convertible into a `String`.
- `style` — a [`Style`] whose color and attributes are captured; its *text* is
  ignored, so the idiom is to define from an empty `style("")`. Defining the same
  name again replaces the previous definition.

**`tag(name) -> Tag`**

- `name` — the key passed to `define_tag`. An unknown name yields a `Tag` that
  renders its text plain, so missing definitions degrade gracefully rather than
  erroring.

**`Tag::render_with(text) -> String`**

- `text` — the text to render with the captured style. Returns an owned `String`;
  color depth matches the terminal detected for standard output.

**Examples**

Define a small palette up front, reuse it everywhere:

```rust
use cli_forge::{define_tag, out, style, tag};

define_tag("ok", style("").green().bold());
define_tag("warn", style("").yellow().bold());
define_tag("fail", style("").red().bold());

out(tag("ok").render_with("[ok]   resolve dependencies"));
out(tag("warn").render_with("[warn] no lockfile found"));
out(tag("fail").render_with("[fail] smoke test"));
```

Reuse across modules — a name defined anywhere resolves everywhere:

```rust
use cli_forge::{define_tag, style};

mod theme {
    use cli_forge::{define_tag, style};
    pub fn install() {
        define_tag("heading", style("").bold().underline());
    }
}

theme::install();
// ...elsewhere:
use cli_forge::{out, tag};
out(tag("heading").render_with("Results"));
```

Unknown names render plain instead of failing:

```rust
use cli_forge::tag;

assert_eq!(tag("never-defined").render_with("text"), "text");
```

---

<h2 id="colors">Colors and terminal behavior</h2>

**Color depth and graceful degradation.** The terminal's capability is detected
once, from standard output, and applied to all styled rendering. A 24-bit color is
emitted exactly on a true-color terminal; on a 256-color terminal it is downgraded
to the nearest cube entry; on a 16-color terminal it is downgraded to the nearest
of the eight standard colors. Named colors always map to their standard code and
never need downgrading.

**When color is dropped entirely** (styled values render as plain text):

- standard output is not a terminal (a pipe or a file);
- the `NO_COLOR` environment variable is set (and non-empty);
- `TERM=dumb`;
- the crate is built without the `color` feature.

`CLICOLOR_FORCE` (set and not `0`) forces color on, overriding a non-terminal
stream and `NO_COLOR`. Depth then comes from `COLORTERM`
(`truecolor`/`24bit` ⇒ 24-bit) and `TERM` (`*256color*` ⇒ 256-color), defaulting
to the 16 standard colors.

**Windows.** The Windows console is driven through the same ANSI backend as Unix
terminals; virtual-terminal processing is enabled automatically the first time
color is used. If it cannot be enabled, output falls back to plain text rather
than printing visible escape sequences.

---

<h2 id="commands">Commands: <code>Command</code> / <code>App</code></h2>

_(planned, v0.3.0)_ A recursive command tree; an `App` registry that accepts
commands registered **from anywhere**, not just `main`. Commands can be hidden
from help or marked auth-gated. The signatures below are frozen.

```rust,ignore
use cli_forge::{App, Command};

let mut app = App::new("forge")
    .help_header("forge — project constructor")
    .help_footer("docs: https://github.com/jamesgober/cli-forge");

app.register(
    Command::new("init")
        .about("bootstrap a planned lib")
        .run(|m| { /* ... */ })
);
app.register(Command::new("secret").hidden(true).run(|_| {}));
app.register(Command::new("publish").requires_auth(true).run(|_| {}));

let matches = app.parse();
```

```rust,ignore
impl Command {
    pub fn new(name: &str) -> Command;
    pub fn about(self, text: &str) -> Command;
    pub fn arg(self, arg: Arg) -> Command;
    pub fn subcommand(self, cmd: Command) -> Command;
    pub fn hidden(self, yes: bool) -> Command;
    pub fn requires_auth(self, yes: bool) -> Command;
    pub fn run(self, handler: impl Fn(&Matches)) -> Command;
}
impl App {
    pub fn new(name: &str) -> App;
    pub fn register(&mut self, cmd: Command);
    pub fn help_header(self, text: &str) -> App;
    pub fn help_footer(self, text: &str) -> App;
    pub fn parse(&self) -> Matches;
}
```

---

<h2 id="feature-flags">Feature flags</h2>

| Feature | Default | Description |
|---------|---------|-------------|
| `std` | yes | Standard library: terminal detection and the stdout/stderr writers. |
| `color` | yes | ANSI / styled output. Implies `std`. Disable for plain output (still complete). |
| `auth` | no | Reserved for enforcement of the `requires_auth` command flag (v0.5.0); no effect yet. |

cli-forge's core has no heavy mandatory dependencies. The only platform-specific
piece is enabling the Windows console's ANSI mode, pulled in by `color` on Windows
targets alone.

---

<h2 id="performance">Performance notes</h2>

The plain path is the hot path and is allocation-free for a string literal: `out`
formats the value straight to the stream with no intermediate buffer. This is
proven by a counting-allocator test, not asserted — see `tests/allocation.rs`.

Local Criterion means (Windows x86_64, Rust stable, release build):

| Operation | ns/op |
|-----------|------:|
| `out` plain write (`&str`) | ~10 |
| builder render, named color + bold | ~50 |
| builder render, 24-bit color | ~75 |
| named-registry render | ~43 |

The styling paths cost more than the plain path because they build an owned
`String` and encode escape sequences; that cost is paid only when you opt into
color. Reproduce with `cargo bench --bench bench`.

---

<sub>Copyright &copy; 2026 <strong>James Gober</strong>.</sub>