denisidoro/navi · warning

Invalid utf8 output from stty

Error message

Invalid utf8 output from stty

What it means

`terminal::width_with_shell_out` determines the terminal width by shelling out to `stty size` (or equivalent) and reading its stdout. When the child exits with code 0, the raw bytes are converted with `String::from_utf8(...).expect(...)`, which panics with "Invalid utf8 output from stty" if `stty` emitted non-UTF-8 bytes.

Source

Thrown at src/common/terminal.rs:26

fn width_with_shell_out() -> Result<u16> {
    let output = if cfg!(target_os = "macos") {
        Command::new("stty")
            .arg("-f")
            .arg("/dev/stderr")
            .arg("size")
            .stderr(Stdio::inherit())
            .output()?
    } else {
        Command::new("stty")
            .arg("size")
            .arg("-F")
            .arg("/dev/stderr")
            .stderr(Stdio::inherit())
            .output()?
    };

    if let Some(0) = output.status.code() {
        let stdout = String::from_utf8(output.stdout).expect("Invalid utf8 output from stty");
        let mut data = stdout.split_whitespace();
        data.next();
        return data
            .next()
            .expect("Not enough data")
            .parse::<u16>()
            .map_err(|_| anyhow!("Invalid width"));
    }

    Err(anyhow!("Invalid status code"))
}

pub fn width() -> u16 {
    if let Ok((w, _)) = terminal::size() {
        w
    } else {
        width_with_shell_out().unwrap_or(FALLBACK_WIDTH)
    }

View on GitHub (pinned to f7330b9ad5)

Solutions

  1. Check what `stty` resolves to (`which stty`, `type stty`) and remove any alias/shadowing wrapper
  2. Run the tool in a clean shell (CI container) to rule out environment-injected output
  3. Fall back to `tput cols` or library-based width detection (e.g. `terminal_size` crate) instead of parsing `stty` output
  4. Patch to `String::from_utf8_lossy(&output.stdout)` so bad bytes don't panic

Example fix

// before
let stdout = String::from_utf8(output.stdout).expect("Invalid utf8 output from stty");
// after
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
Defensive patterns

Strategy: fallback

Validate before calling

let out = std::process::Command::new("stty")
    .arg("size").stdin(std::process::Stdio::inherit())
    .output()?;
if out.status.success() && std::str::from_utf8(&out.stdout)
    .map(|s| s.split_whitespace().count() >= 2)
    .unwrap_or(false) { /* safe to parse */ }

Type guard

fn valid_stty_output(bytes: &[u8]) -> bool {
    std::str::from_utf8(bytes)
        .ok()
        .map(|s| {
            let mut it = s.split_whitespace();
            it.next().and_then(|a| a.parse::<u16>().ok()).is_some()
                && it.next().and_then(|b| b.parse::<u16>().ok()).is_some()
        })
        .unwrap_or(false)
}

Try / catch

// expect panics can't be caught as Err; detect the condition and fall back:
let width = if valid_stty_output(&out.stdout) {
    parse_width(&String::from_utf8_lossy(&out.stdout))
} else {
    fallback_width() // e.g. tput cols or terminal_size crate, else 80
};

Prevention

When it happens

Trigger: `stty` (or the platform-specific command) exits 0 but writes non-UTF-8 bytes to stdout — unusual locale/encoding, or a shell alias/wrapper around `stty` injecting binary output.

Common situations: Aliased or wrapped `stty` commands (e.g. a script printing extra ANSI/binary data); exotic locale environments on CI containers; a `stty` shim from a different toolchain on PATH.

Understand the failure class

Related errors


AI-assisted analysis of denisidoro/navi@f7330b9ad5 (2026-09-03). Data as JSON: /api/errors/6875e62d99ecdca4. Report an issue: GitHub.