twitcher 0.1.8

Find template switch mutations in genomic data
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{LazyLock, OnceLock};
use std::time::Instant;

use clap::{Args, Parser};
use clap_verbosity_flag::{Verbosity, WarnLevel};

mod common;
mod reads;
mod vcf;
mod worker;

use indicatif::ProgressStyle;
use reads::Command as ReadsCommand;
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufWriter};
use tracing::info;
use tracing_indicatif::filter::{IndicatifFilter, hide_indicatif_span_fields};
use tracing_indicatif::{IndicatifLayer, IndicatifWriter, writer};
use tracing_subscriber::fmt::format::DefaultFields;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::{EnvFilter, Layer};
use vcf::cli::Command as VcfCommand;

use crate::common::stats;

static THIS_EXE: LazyLock<PathBuf> =
    LazyLock::new(|| std::env::current_exe().expect("Cannot find the executable on the FS"));

type VerbositySelector = Verbosity<WarnLevel>;

static THIS_LOG_LEVEL: OnceLock<VerbositySelector> = OnceLock::new();

static STDERR_LOG_WRITER: OnceLock<IndicatifWriter<writer::Stderr>> = OnceLock::new();

#[derive(Parser, Debug)]
#[allow(clippy::large_enum_variant)]
#[command(version)]
#[command(name = "twitcher")]
#[command(about = "Template sWITCH alignER -- Reannotate variant calls and try to explain mutation clusters with short-range template switches.", long_about = None)]
enum Cli {
    /// Annotate VCF files with template switch mutations
    Vcf(CliCommand<VcfCommand>),
    /// Find and extract template switch mutations in reads
    Reads(CliCommand<ReadsCommand>),
    #[clap(hide = true)]
    Worker,
}

trait RunnableCommand {
    async fn run(self) -> anyhow::Result<()>;
}

#[derive(Parser, Clone, Debug)]
struct CliCommand<T: Args + RunnableCommand> {
    #[command(flatten)]
    pub command: T,

    // Write statistics to a file
    #[arg(long = "stat-output")]
    pub stats: Option<String>,

    #[command(flatten)]
    pub log_level: VerbositySelector,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    match cli {
        Cli::Worker => worker::run(),
        Cli::Vcf(cc) => run_command(cc)?,
        Cli::Reads(cc) => run_command(cc)?,
    }
    Ok(())
}

fn run_command<T: Args + RunnableCommand>(cc: CliCommand<T>) -> anyhow::Result<()> {
    setup_tracing(cc.log_level);

    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;

    rt.block_on(async {
        let start = Instant::now();
        cc.command.run().await?;
        let elapsed = start.elapsed();

        info!("Runtime: {elapsed:?}");
        if let Some(file) = cc.stats {
            let mut file = BufWriter::new(File::create(file).await?);
            for (k, v) in stats::get_stats() {
                file.write_all(format!("{k}{v}").as_bytes()).await?;
            }
        } else {
            info!("Counters:");
            for (k, v) in stats::get_stats() {
                info!("\t{k}: {v}");
            }
        }

        Ok(())
    })
}

fn setup_tracing(log_level: VerbositySelector) {
    THIS_LOG_LEVEL.get_or_init(|| log_level);
    let pb_layer = IndicatifLayer::new()
        .with_progress_style(
            ProgressStyle::with_template("{elapsed} [{wide_bar}] {msg} {pos:>7}/{len:7}")
                .unwrap()
                .progress_chars("=> "),
        )
        .with_filter(IndicatifFilter::new(false));
    let writer = pb_layer.inner().get_stderr_writer();
    STDERR_LOG_WRITER.get_or_init(|| writer.clone());
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::fmt::layer()
                .compact()
                .without_time()
                .with_target(false)
                .fmt_fields(hide_indicatif_span_fields(DefaultFields::new()))
                .with_writer(writer)
                .with_filter(
                    EnvFilter::from_str(&format!(
                        "{},lib_tsalign=warn,template_switch_error_free_inners=warn",
                        log_level.tracing_level_filter()
                    ))
                    .unwrap(),
                ),
        )
        .with(pb_layer)
        .init();
}

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

    /// Verify that the CLI definition does not contain any logic errors.
    #[test]
    fn verify_cli() {
        use clap::CommandFactory;
        Cli::command().debug_assert();
    }
}