novel-cli 0.17.0

A set of tools for downloading novels from the web, manipulating text, and generating EPUB
Documentation
use std::env;
use std::io::{self, IsTerminal};

use bytesize::ByteSize;
use clap::Parser;
use clap_verbosity_flag::VerbosityFilter;
use color_eyre::eyre::{self, Result};
use mimalloc::MiMalloc;
use novel_cli::cmd::{
    self, bookshelf, build, check, completions, download, info, read, real_cugan, search, sign,
    template, transform, unzip, update,
};
use novel_cli::config::{Backtrace, Commands, Config};
use supports_color::Stream;
use tracing::subscriber;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_appender::rolling;
use tracing_log::LogTracer;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt::time::ChronoLocal;

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

#[tokio::main]
async fn main() -> Result<()> {
    let config = Config::parse();

    let _guard = init_log(&config)?;

    tracing::debug!("{:#?}", sys_locale::get_locale());
    tracing::debug!("{:#?}", config.verbose);

    novel_cli::init_error_hooks(is_tui(&config))?;

    if !matches!(&config.command, Commands::Completions(_)) && !io::stdout().is_terminal() {
        tracing::error!("stdout must be a terminal");
    }

    match config.command {
        Commands::Sign(config) => sign::execute(config).await?,
        Commands::Download(config) => download::execute(config).await?,
        Commands::Search(config) => search::execute(config).await?,
        Commands::Info(config) => info::execute(config).await?,
        Commands::Read(config) => read::execute(config).await?,
        Commands::Bookshelf(config) => bookshelf::execute(config).await?,
        Commands::Template(config) => template::execute(config)?,
        Commands::Transform(config) => transform::execute(config)?,
        Commands::Check(config) => check::execute(config)?,
        Commands::Build(config) => build::execute(config)?,
        Commands::Epub(config) => cmd::epub::execute(config)?,
        Commands::Zip(config) => cmd::zip::execute(config)?,
        Commands::Unzip(config) => unzip::execute(config)?,
        Commands::RealCugan(config) => real_cugan::execute(config).await?,
        Commands::Update(config) => update::execute(config).await?,
        Commands::Completions(config) => completions::execute(config)?,
    }

    if !config.verbose.is_silent() {
        if let Some(usage) = memory_stats::memory_stats() {
            tracing::info!(
                "Current physical memory usage: {}",
                ByteSize(usage.physical_mem as u64)
            );
        } else {
            tracing::error!("Couldn't get the current memory usage");
        }
    }

    Ok(())
}

fn init_log(config: &Config) -> Result<WorkerGuard> {
    if is_tui(config)
        && !config.output_log_to_file
        && !matches!(
            config.verbose.filter(),
            VerbosityFilter::Off | VerbosityFilter::Error
        )
    {
        eyre::bail!(
            "`--output-log-to-file` must be enabled when using `read` or `info` command with verbose level greater than Error: {:?}",
            config.verbose.filter()
        )
    }

    if let Some(backtrace) = &config.backtrace {
        match backtrace {
            Backtrace::ON => unsafe { env::set_var("RUST_BACKTRACE", "1") },
            Backtrace::FULL => unsafe { env::set_var("RUST_BACKTRACE", "full") },
        }
    }

    if env::var("RUST_LOG").is_err() {
        if config.verbose.is_silent() {
            unsafe {
                env::set_var("RUST_LOG", "none");
            }
        } else if config.verbose.filter() == VerbosityFilter::Trace {
            LogTracer::init()?;
            unsafe {
                env::set_var("RUST_LOG", "trace");
            }
        } else {
            unsafe {
                env::set_var(
                    "RUST_LOG",
                    format!("none,novel_api={0},novel_cli={0}", config.verbose.filter()),
                );
            }
        }
    }

    let (non_blocking, guard) = if config.output_log_to_file {
        let file_appender = rolling::never(".", "novel-cli.log");
        tracing_appender::non_blocking(file_appender)
    } else {
        tracing_appender::non_blocking(io::stdout())
    };

    let ansi = if config.output_log_to_file {
        false
    } else {
        supports_color::on(Stream::Stdout).is_some()
    };

    let subscriber = tracing_subscriber::fmt()
        .with_timer(ChronoLocal::rfc_3339())
        .with_env_filter(EnvFilter::from_default_env())
        .with_ansi(ansi)
        .with_writer(non_blocking)
        .finish();

    subscriber::set_global_default(subscriber)?;

    Ok(guard)
}

fn is_tui(config: &Config) -> bool {
    matches!(&config.command, Commands::Read(_)) || matches!(&config.command, Commands::Info(_))
}