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(_))
}