rusty-pwgen 0.1.0

Generate pronounceable or random passwords from the OS CSPRNG — a Rust port of Theodore Ts'o's `pwgen` with strict-compat mode, deterministic `-H` reproducible mode (SHA256 + ChaCha20), and a typed library API.
Documentation
//! Output formatter (FR-017 through FR-021).
//!
//! TTY detection via `std::io::IsTerminal` (stable 1.70). Terminal width
//! via `terminal_size` crate with 80-col fallback. Test-only override:
//! `RUSTY_PWGEN_TEST_COLUMNS=N` (HINT-006).

use crate::Pwgen;
use std::io::{IsTerminal, Write};

/// Whether stdout is a TTY (FR-017).
pub fn is_tty() -> bool {
    if std::env::var_os("RUSTY_PWGEN_TEST_FORCE_TTY").is_some() {
        return true;
    }
    if std::env::var_os("RUSTY_PWGEN_TEST_FORCE_NOT_TTY").is_some() {
        return false;
    }
    std::io::stdout().is_terminal()
}

/// Terminal width — `RUSTY_PWGEN_TEST_COLUMNS` overrides per HINT-006.
pub fn columns() -> usize {
    if let Some(val) = std::env::var_os("RUSTY_PWGEN_TEST_COLUMNS") {
        if let Some(s) = val.to_str() {
            if let Ok(n) = s.trim().parse::<usize>() {
                if (1..=1000).contains(&n) {
                    return n;
                }
            }
        }
        // Out-of-range or non-numeric: ignored per HINT-006.
    }
    terminal_size::terminal_size()
        .map(|(w, _h)| w.0 as usize)
        .unwrap_or(80)
}

/// Emit `count` passwords from `pwgen` according to TTY mode + flag overrides
/// (FR-018, FR-019, FR-020, FR-021).
pub fn emit_passwords(pwgen: &mut Pwgen, count: usize, force_one_col: bool, force_columnar: bool) {
    let stdout = std::io::stdout();
    let mut out = stdout.lock();

    let use_columns = if force_one_col {
        false
    } else if force_columnar {
        true
    } else {
        is_tty()
    };

    if !use_columns {
        // Single-column output: one password per line (FR-020).
        for _ in 0..count {
            let pw = pwgen.generate_one();
            let _ = writeln!(out, "{pw}");
        }
        let _ = out.flush();
        return;
    }

    // Columnar output (FR-018, FR-021).
    let term_width = columns();
    // Each column slot is `length + 1` bytes (password + space separator).
    // First, generate all passwords (we need to know their length, though they're
    // typically all the same).
    let passwords = pwgen.generate_n(count);
    if passwords.is_empty() {
        return;
    }
    let col_width = passwords[0].len() + 1;
    let cols_per_row = (term_width / col_width).max(1);

    for chunk in passwords.chunks(cols_per_row) {
        for (i, pw) in chunk.iter().enumerate() {
            if i + 1 == chunk.len() {
                let _ = writeln!(out, "{pw}");
            } else {
                let _ = write!(out, "{pw} ");
            }
        }
    }
    let _ = out.flush();
}