paper 0.4.0

A terminal-based editor with goals to maximize simplicity and efficiency.
Documentation
//! Implements the logging functionality of `paper`.
use {
    chrono::Local,
    fehler::throws,
    log::{info, LevelFilter, Log, Metadata, Record, SetLoggerError},
    parse_display::FromStr as ParseFromStr,
    std::{
        fs::File,
        io::{self, Write},
        sync::{Arc, RwLock},
    },
    structopt::StructOpt,
    thiserror::Error,
};

/// Parses the `LevelFilter` of the logger from the number of occurrences of the verbose flag.
const fn parse_log_level(occurrences: u64) -> LevelFilter {
    match occurrences {
        0 => LevelFilter::Warn,
        1 => LevelFilter::Info,
        2 => LevelFilter::Debug,
        _ => LevelFilter::Trace,
    }
}

/// Creates a [`Logger`] and initializes it as the logger.
#[throws(InitLoggerError)]
pub(crate) fn init(config: LogConfig) {
    let logger = Logger::new(config.components.unwrap_or_default())?;

    log::set_boxed_logger(Box::new(logger))?;
    log::set_max_level(config.level);
    info!("Logger initialized");
}

/// Components that log records.
#[derive(Clone, Debug, ParseFromStr, PartialEq)]
pub(crate) enum LogComponent {
    /// The `starship` component.
    Starship,
}

/// Records all logs generated by the application.
struct Logger {
    /// The file where logs shall be recorded.
    file: Arc<RwLock<File>>,
    /// The components that will be logged.
    components: Vec<LogComponent>,
}

impl Logger {
    /// Creates a new [`Logger`].
    #[throws(CreateLoggerError)]
    fn new(components: Vec<LogComponent>) -> Self {
        let log_filename = "paper.log".to_string();

        Self {
            file: Arc::new(RwLock::new(File::create(&log_filename).map_err(
                |error| CreateLoggerError {
                    file: log_filename,
                    error,
                },
            )?)),
            components,
        }
    }
}

impl Log for Logger {
    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
        if metadata.target().starts_with("starship") {
            self.components.contains(&LogComponent::Starship)
        } else {
            true
        }
    }

    fn log(&self, record: &Record<'_>) {
        if self.enabled(record.metadata()) {
            if let Ok(mut file) = self.file.write() {
                #[allow(unused_must_use)] // Log::log() does not propagate errors.
                {
                    writeln!(
                        file,
                        "{} [{}]: {}",
                        Local::now().format("%F %T"),
                        record.level(),
                        record.args()
                    );
                }
            }
        }
    }

    fn flush(&self) {
        if let Ok(mut file) = self.file.write() {
            #[allow(unused_must_use)] // Log::flush() does not propagate errors.
            {
                file.flush();
            }
        }
    }
}

/// The configuration of the application logger.
#[derive(Clone, Debug, StructOpt)]
pub struct LogConfig {
    /// Enables logs for components.
    #[structopt(long("log"), value_name("COMPONENT"), require_equals(true), possible_values(&["starship"]))]
    components: Option<Vec<LogComponent>>,
    /// Increases the logging verbosity - can be repeated upto 3 times.
    #[structopt(short("v"), parse(from_occurrences = parse_log_level))]
    level: LevelFilter,
}

impl Default for LogConfig {
    #[inline]
    fn default() -> Self {
        LogConfig {
            components: None,
            level: LevelFilter::Warn,
        }
    }
}

/// An error initializing the logger.
#[derive(Debug, Error)]
pub enum InitLoggerError {
    /// An error creating the logger.
    #[error(transparent)]
    Create(#[from] CreateLoggerError),
    /// An error setting the logger.
    #[error("unable to set logger: {0}")]
    Set(#[from] SetLoggerError),
}

/// An error creating a [`Logger`].
#[derive(Debug, Error)]
#[error("unable to create log file `{file}`: {error}")]
pub struct CreateLoggerError {
    /// The path of the log file.
    file: String,
    /// The error.
    #[source]
    error: io::Error,
}