mkwim 0.1.0

Create a bootable Windows installation image from an ISO
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2026 Jarkko Sakkinen

//! Progress reporting for interactive use.
//!
//! Reporters draw to stdout only when stdout is a terminal, and are silent
//! no-ops otherwise (e.g. when piped into a file or another program), so the
//! output stays clean for scripting. Logs continue to flow to stderr via
//! tracing, so the two never clobber each other.

use std::io::{self, IsTerminal, Write};
use std::time::Duration;

use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};

/// Whether progress bars should be drawn.
fn stdout_is_tty() -> bool {
    std::io::stdout().is_terminal()
}

/// A progress reporter that is a silent no-op when stdout is not a terminal.
pub struct Reporter {
    bar: ProgressBar,
}

impl Reporter {
    /// Byte-level bar with a known total, e.g. for streaming `install.wim`.
    pub fn bytes(message: &str, total: u64) -> Self {
        let bar = if stdout_is_tty() {
            let bar = ProgressBar::new(total);
            bar.set_style(
                ProgressStyle::with_template(
                    "{msg} {bar:40.cyan/blue} {bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
                )
                .unwrap()
                .progress_chars("=>-"),
            );
            bar.set_message(message.to_string());
            bar.set_draw_target(ProgressDrawTarget::stdout());
            bar
        } else {
            ProgressBar::hidden()
        };
        Self { bar }
    }

    /// Indeterminate spinner, e.g. for the WIM split step or bulk file copy.
    pub fn spinner(message: &str) -> Self {
        let bar = if stdout_is_tty() {
            let bar = ProgressBar::new_spinner();
            bar.set_style(ProgressStyle::with_template("{spinner} {msg}").unwrap());
            bar.set_message(message.to_string());
            bar.set_draw_target(ProgressDrawTarget::stdout());
            bar.enable_steady_tick(Duration::from_millis(100));
            bar
        } else {
            ProgressBar::hidden()
        };
        Self { bar }
    }

    /// Advance the byte counter (no-op when hidden).
    pub fn inc(&self, n: u64) {
        self.bar.inc(n);
    }

    /// Update the current status message.
    pub fn set_message(&self, message: &str) {
        self.bar.set_message(message.to_string());
    }

    /// Mark the operation complete and leave the final bar in place.
    pub fn finish(&self) {
        self.bar.finish();
    }
}

/// Wrap a writer so that every byte written advances a [`Reporter`].
pub struct ProgressWrite<W> {
    inner: W,
    reporter: Reporter,
}

impl<W> ProgressWrite<W> {
    pub fn new(inner: W, reporter: Reporter) -> Self {
        Self { inner, reporter }
    }

    /// Finish the underlying reporter.
    pub fn finish(&self) {
        self.reporter.finish();
    }
}

impl<W: Write> Write for ProgressWrite<W> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let n = self.inner.write(buf)?;
        self.reporter.inc(n as u64);
        Ok(n)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.inner.flush()
    }
}