urma-cli 0.2.2

Command-line program for URMA identities, wallets, Wire, archives and Git
use crate::git_terminal;
use std::{
    fs::File,
    io::Write,
    sync::Mutex,
    time::{Duration, Instant},
};
use tracing::{
    Event, Subscriber,
    field::{Field, Visit},
};
use tracing_subscriber::{
    Layer,
    fmt::{
        format::Writer,
        time::{FormatTime, SystemTime},
    },
    layer::Context,
};

#[derive(Default)]
struct Update {
    phase: String,
    done: u64,
    total: u64,
    counted: bool,
}

impl Visit for Update {
    fn record_str(&mut self, field: &Field, value: &str) {
        if field.name() == "phase" {
            self.phase = value.to_owned();
        }
    }

    fn record_u64(&mut self, field: &Field, value: u64) {
        match field.name() {
            "done" => self.done = value,
            "total" => {
                self.total = value;
                self.counted = true;
            }
            _ => {}
        }
    }

    fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {}
}

struct LoggedProgress {
    file: File,
    phase: String,
    at: Instant,
}

pub(crate) struct ProgressLayer {
    logged: Mutex<LoggedProgress>,
    enabled: bool,
}

impl ProgressLayer {
    pub(crate) fn new(file: File, enabled: bool) -> Self {
        Self {
            logged: Mutex::new(LoggedProgress {
                file,
                phase: String::new(),
                at: Instant::now(),
            }),
            enabled,
        }
    }
}

impl LoggedProgress {
    fn record(&mut self, update: &Update) {
        let mut timestamp = String::new();
        match SystemTime.format_time(&mut Writer::new(&mut timestamp)) {
            Ok(()) => (),
            Err(cause) => panic!("cannot format progress timestamp: {cause}"),
        }
        let line = format!(
            "{timestamp} INFO urma_activity: Progress phase={:?} done={} total={} counted={}\n",
            update.phase, update.done, update.total, update.counted
        );
        match self.file.write_all(line.as_bytes()) {
            Ok(()) => (),
            Err(cause) => panic!("cannot write progress log: {cause}"),
        }
    }
}

impl<S: Subscriber> Layer<S> for ProgressLayer {
    fn on_event(&self, event: &Event<'_>, _context: Context<'_, S>) {
        let mut update = Update::default();
        event.record(&mut update);
        if !update.phase.is_empty() {
            git_terminal::stage_progress(&update.phase, update.done, update.total, update.counted);
            if !self.enabled {
                return;
            }
            let mut logged = match self.logged.lock() {
                Ok(logged) => logged,
                Err(cause) => panic!("progress log lock poisoned: {cause}"),
            };
            if logged.phase != update.phase
                || (update.counted && update.done == update.total)
                || logged.at.elapsed() >= Duration::from_secs(5)
            {
                logged.record(&update);
                logged.phase = update.phase;
                logged.at = Instant::now();
            }
        }
    }
}