scriba 0.3.0

CLI text rendering, prompts, and terminal output utilities
Documentation

scriba

Typed CLI output, prompts, and terminal rendering for Rust.

scriba helps you build clean, structured command-line interfaces with composable output primitives, interactive prompts, styled logging, and optional ASCII banners.

Features

  • 📄 Multi-format rendering: Plain, Text, Markdown, JSON, JSONL
  • 🧱 Typed output blocks and builders
  • 💬 Interactive prompts (feature: prompt)
  • 🎨 Styled logging (feature: logger)
  • 🔤 ASCII banners / figlet rendering (feature: figlet)
  • ⚙️ Feature-gated integrations
  • 🦀 Ergonomic Rust-first APIs

Installation

[dependencies]
scriba = "0.3"

# optional features
scriba = { version = "0.3", features = ["prompt", "logger", "figlet"] }

Feature Flags

Feature Enables
prompt Interactive terminal prompts via inquire
logger Styled stderr logging
figlet ASCII banner rendering

Quick Start

use scriba::{Format, Output, Ui};

fn main() -> scriba::Result<()> {
    let ui = Ui::new().with_format(Format::Markdown);

    let output = Output::new()
        .heading(1, "scriba")
        .paragraph("Clean CLI rendering.");

    ui.print(&output)?;
    Ok(())
}

Output:

# scriba

Clean CLI rendering.

Output Formats

Supported render targets:

  • Format::Plain
  • Format::Text
  • Format::Markdown
  • Format::Json
  • Format::Jsonl
let ui = Ui::new().with_format(Format::Text);

Core Output Primitives

All output is composed with Output::new().

Titles and Headings

let output = Output::new()
    .title("Deploy")
    .subtitle("production")
    .heading(2, "Summary")
    .paragraph("Release completed.");

Paragraphs, Lines, Lists

let output = Output::new()
    .paragraph("Main description.")
    .line("single line")
    .list(false, vec![
        "alpha".into(),
        "beta".into(),
    ]);

Ordered list:

.list(true, vec!["one".into(), "two".into()])

Separators

let output = Output::new()
    .paragraph("Before")
    .separator()
    .paragraph("After");

Code Blocks

let output = Output::new()
    .code(Some("rust".into()), r#"fn main() {}"#);

Plain Output

Use when your command should emit one scalar value for scripts or shell pipelines.

use scriba::{Format, Output, Ui};

let ui = Ui::new().with_format(Format::Plain);
let output = Output::new().plain("hello");

ui.print(&output)?;

Output:

hello

Supported scalar values:

  • string
  • number
  • boolean
  • null

Structured Data

Key / Value Data Map

Use top-level structured fields with .data(...).

let output = Output::new()
    .data("version", "1.0")
    .data("ready", true);

Useful for JSON, Markdown summaries, and metadata.

JSON Block

let output = Output::new()
    .json(serde_json::json!({
        "name": "scriba",
        "ok": true
    }));

JSON Lines (JSONL)

Use jsonl_record(...) for streaming records or line-delimited events.

use scriba::{Format, Output, Ui};

let ui = Ui::new().with_format(Format::Jsonl);

let output = Output::new()
    .jsonl_record(serde_json::json!({
        "event": "start",
        "id": 1
    }))
    .jsonl_record(serde_json::json!({
        "event": "done",
        "id": 1
    }));

ui.print(&output)?;

Output:

{"event":"start","id":1}
{"event":"done","id":1}

If no explicit records are provided, blocks can be emitted as JSONL entries.

Key / Value Blocks

Use for compact metadata sections.

let output = Output::new()
    .key_value("project", "scriba")
    .key_value("env", "prod");

Markdown:

- **project**: scriba
- **env**: prod

Text:

project: scriba
env: prod

Sequential calls are grouped automatically.

Definition Lists

Use for glossary-style output or descriptive labels.

let output = Output::new()
    .definition("Project", "scriba")
    .definition("Environment", "production");

Text:

Project:
  scriba

Environment:
  production

Status Messages

Use semantic states for results and summaries.

use scriba::{Output, StatusKind};

let output = Output::new()
    .status(StatusKind::Ok, "Deployment complete")
    .status(StatusKind::Warning, "Using cached config");

Available kinds:

  • StatusKind::Info
  • StatusKind::Ok
  • StatusKind::Warning
  • StatusKind::Error

Tables

use scriba::{Output, Table};

let table = Table::new(
    vec!["name".into(), "value".into()],
    vec![
        vec!["alpha".into(), "1".into()],
        vec!["beta".into(), "2".into()],
    ],
);

let output = Output::new()
    .table(Some("Items".into()), table);

Indexed Tables

Add row numbers automatically.

let table = Table::new(headers, rows).with_index();

Custom header:

let table = Table::new(headers, rows)
    .with_index_header("row");

Prompts (prompt feature)

use scriba::Ui;

fn main() -> scriba::Result<()> {
    let ui = Ui::new().interactive(true);

    let name = ui.text("Project name?", None, None)?;
    println!("Hello {name}");

    Ok(())
}

Confirm

let proceed = ui.confirm("Continue?", true)?;

Select

use scriba::{SelectOption, SelectRequest};

let env = ui.select(&SelectRequest::new(
    "Select environment",
    vec![
        SelectOption::new("dev", "Development"),
        SelectOption::new("prod", "Production"),
    ],
))?;

Multi Select

use scriba::{MultiSelectOption, MultiSelectRequest};

let values = ui.multiselect(&MultiSelectRequest::new(
    "Choose targets",
    vec![
        MultiSelectOption::new("api", "API").selected(true),
        MultiSelectOption::new("web", "Web"),
    ],
))?;

Pagination

Both SelectRequest and MultiSelectRequest support an optional page size for long option lists:

let env = ui.select(&SelectRequest::new("Select environment", options)
    .with_page_size(10))?;

let targets = ui.multiselect(&MultiSelectRequest::new("Choose targets", options)
    .with_page_size(10))?;

Envelope

Wrap any output in a JSON envelope with a configurable layout and optional execution metadata. Enabled by setting EnvelopeMode::Json on the Ui.

Flat layout (default)

use scriba::{
    Format, Output, Ui,
    envelope::{EnvelopeConfig, EnvelopeLayout, EnvelopeMode, Meta},
};

fn main() -> scriba::Result<()> {
    let output = Output::new()
        .data("environment", "production")
        .data("version", "1.4.2");

    let meta = Meta::default()
        .with_command("deploy".into())
        .with_duration_ms(312)
        .with_dry_run(false);

    let ui = Ui::new()
        .with_format(Format::Json)
        .with_envelope(
            EnvelopeConfig::default()
                .with_mode(EnvelopeMode::Json)
                .with_layout(EnvelopeLayout::Flat),
        );

    ui.print_with_meta(&output, Some(&meta), true)?;
    Ok(())
}

Output:

{
  "ok": true,
  "format": "json",
  "content": { "environment": "production", "version": "1.4.2" },
  "meta": { "command": "deploy", "duration_ms": 312, "dry_run": false }
}

Nested layout

ok, format, and all metadata fields are merged under the meta key.

let ui = Ui::new()
    .with_format(Format::Json)
    .with_envelope(
        EnvelopeConfig::default()
            .with_mode(EnvelopeMode::Json)
            .with_layout(EnvelopeLayout::Nested),
    );

ui.print_with_meta(&output, Some(&meta), true)?;

Output:

{
  "meta": { "ok": true, "format": "json", "command": "deploy", "dry_run": false },
  "content": { ... }
}

Custom field names

use scriba::envelope::{EnvelopeConfig, EnvelopeFields, EnvelopeMode};

let ui = Ui::new()
    .with_format(Format::Json)
    .with_envelope(
        EnvelopeConfig::default()
            .with_mode(EnvelopeMode::Json)
            .with_fields(EnvelopeFields {
                ok_field: "success".into(),
                format_field: "type".into(),
                content_field: "result".into(),
                meta_field: "context".into(),
            }),
    );

Meta

Meta carries optional structured context about the command invocation.

use scriba::envelope::Meta;

let meta = Meta::default()
    .with_command("deploy".into())
    .with_version("1.0.0".into())
    .with_scope("production".into())
    .with_duration_ms(342)
    .with_timestamp("2025-01-01T00:00:00Z".into())
    .with_dry_run(false)
    .with_extra("region", "us-east-1")
    .with_extra("actor", "ci")
    .with_extra_map([
        ("trace_id".into(), serde_json::json!("abc-123")),
        ("run_id".into(), serde_json::json!(42)),
    ]);

All fields are optional and omitted from the JSON output when not set.

Runnable examples

cargo run --example envelope_flat
cargo run --example envelope_nested
cargo run --example envelope_custom_fields
cargo run --example envelope_meta

Logger (logger feature)

use scriba::Ui;

let ui = Ui::new();

ui.logger().info("starting");
ui.logger().ok("done");
ui.logger().warn("careful");
ui.logger().error("failed");
ui.logger().detail("verbose detail");
ui.logger().debug("debug line");
ui.logger().trace("trace line");
ui.logger().kv("region", "us-east-1");

Figlet (figlet feature)

use scriba::figlet;

let banner = figlet::render("scriba")?;
println!("{banner}");

Custom font:

let banner = figlet::render_with_font("scriba", "slant")?;

Built-in fonts include:

  • standard
  • small
  • big
  • slant
  • smblock
  • mono12
  • future
  • wideterm
  • mono9

Design Goals

scriba is built around:

  • simple APIs first
  • standard types where possible
  • composable builders
  • feature-gated integrations
  • no macros required

Recommended Primitive by Use Case

Need Use
Single shell value plain()
Human-readable report headings + paragraphs
Metadata key_value()
Glossary / labels definition()
State / result status()
Structured object data() / json()
Event stream jsonl_record()
Tabular data table()
Numbered rows with_index()
JSON envelope EnvelopeConfig
Execution metadata Meta

Roadmap

v0.4.0

  • table layout variants (Full, Compact, Stacked)
  • richer styling options

Backlog

  • output streaming
  • optional derive macros
  • richer prompt theming capabilities

License

MIT OR Apache-2.0