rusty-pv 0.1.0

Pipe viewer — a Rust port of Andrew Wood's `pv(1)` with progress bar, ETA, rate display, token-bucket rate limiting, IEC/SI unit math, SIGWINCH-aware terminal redraw, SIGUSR1 size refresh, multi-instance cursor coordination, and a typed library API.
Documentation
//! Compatibility-mode precedence ladder (FR-038, AD-007).
//!
//! Active mode is computed once at startup from (high → low):
//! 1. `--strict` flag on argv
//! 2. `RUSTY_PV_STRICT=1` env var
//! 3. argv[0] basename matches `pv` / `pv-alias`
//!
//! `--no-strict` overrides all three lower-precedence sources.

/// Whether to apply Default-mode extensions or Strict upstream parity.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompatibilityMode {
    /// Ergonomic Default mode (clap-styled diagnostics, conflicts_with).
    #[default]
    Default,
    /// Strict-compat mode (byte-equal upstream stderr, last-wins flag resolution).
    Strict,
}

/// Resolve the active mode from `argv` and environment per FR-038.
pub fn resolve(argv: &[String]) -> CompatibilityMode {
    if argv.iter().any(|a| a == "--no-strict") {
        return CompatibilityMode::Default;
    }
    if argv.iter().any(|a| a == "--strict") {
        return CompatibilityMode::Strict;
    }
    if std::env::var("RUSTY_PV_STRICT").as_deref() == Ok("1") {
        return CompatibilityMode::Strict;
    }
    if let Some(first) = argv.first() {
        let base = basename(first);
        if base.eq_ignore_ascii_case("pv") || base.eq_ignore_ascii_case("pv-alias") {
            return CompatibilityMode::Strict;
        }
    }
    CompatibilityMode::Default
}

/// Extract the basename from a path-like string. Strips `.exe` suffix on
/// Windows and handles both `/` and `\` separators.
pub fn basename(path: &str) -> &str {
    let last = path
        .rsplit_once(['/', '\\'])
        .map(|(_, b)| b)
        .unwrap_or(path);
    last.strip_suffix(".exe").unwrap_or(last)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn explicit_strict_flag() {
        let argv = vec!["rusty-pv".into(), "--strict".into()];
        assert_eq!(resolve(&argv), CompatibilityMode::Strict);
    }

    #[test]
    fn no_strict_overrides_strict_flag() {
        let argv = vec!["rusty-pv".into(), "--strict".into(), "--no-strict".into()];
        assert_eq!(resolve(&argv), CompatibilityMode::Default);
    }

    #[test]
    fn argv0_pv_triggers_strict() {
        let argv = vec!["/usr/local/bin/pv".into()];
        assert_eq!(resolve(&argv), CompatibilityMode::Strict);
    }

    #[test]
    fn argv0_pv_alias_triggers_strict() {
        let argv = vec!["pv-alias".into()];
        assert_eq!(resolve(&argv), CompatibilityMode::Strict);
    }

    #[test]
    fn argv0_rusty_pv_is_default() {
        let argv = vec!["rusty-pv".into()];
        assert_eq!(resolve(&argv), CompatibilityMode::Default);
    }

    #[test]
    fn basename_strips_exe_on_windows_path() {
        assert_eq!(basename("C:\\bin\\pv.exe"), "pv");
        assert_eq!(basename("/usr/bin/pv"), "pv");
        assert_eq!(basename("pv"), "pv");
    }
}