todotxt-tui 0.3.0

Todo.txt TUI is a highly customizable terminal-based application for managing your todo tasks. It follows the todo.txt format and offers a wide range of configuration options to suit your needs.
Documentation
use log::LevelFilter;
use log4rs::{
    append::file::FileAppender,
    config::{Appender, Root},
    encode::pattern::PatternEncoder,
    Config as LogConfig,
};
use std::{env, error::Error, io::stdin, path::PathBuf, process::exit};
use todotxt_tui::{ConfMerge, Config, UI};

/// Initializes the logging system.
///
/// Reads the log configuration file path from the environment variable.
/// If it is not set, the default path `config_folder/log4rs.yaml` is used.
fn log_init() -> Result<(), Box<dyn Error>> {
    let config_folder = Config::config_folder();
    let log_file = match env::var(format!(
        "{}LOGCONFIG",
        format_args!(
            "{}_",
            env!("CARGO_PKG_NAME").to_uppercase().replace('-', "_")
        )
    )) {
        Ok(log_file) => PathBuf::from(log_file),
        Err(_) => config_folder.join("log4rs.yaml"),
    };
    if log_file.exists() {
        log4rs::init_file(log_file, Default::default())?;
    } else {
        let logfile = FileAppender::builder()
            .encoder(Box::new(PatternEncoder::new("{d} [{h({l})}] {M}: {m}{n}")))
            .build(config_folder.join("log.log"))?;
        let log_cofnig = LogConfig::builder()
            .appender(Appender::builder().build("logfile", Box::new(logfile)))
            .build(Root::builder().appender("logfile").build(LevelFilter::Info))?;
        log4rs::init_config(log_cofnig)?;
    }
    Ok(())
}

/// Handles missing configuration file errors by prompting the user.
///
/// If the underlying error is an IO `NotFound` error, prompts the user to initialize
/// the configuration with default settings. If the user agrees, exports the default
/// configuration and exits. If the user declines, exits with an error code.
/// If the error is not a missing configuration file error, returns it unchanged.
fn ask_to_create_config(err: anyhow::Error) -> anyhow::Error {
    fn ask() -> bool {
        println!("Do you want to initialize it with default configuration? [y/N]");
        loop {
            let mut s = String::new();
            stdin()
                .read_line(&mut s)
                .expect("Did not enter a correct string");
            match s.trim().to_lowercase().as_str() {
                "y" | "yes" => return true,
                "n" | "no" | "" => return false,
                _ => {
                    println!("You must say y or n")
                }
            }
        }
    }

    // Check if the root cause is an IO NotFound error
    if let Some(io_err) = err.root_cause().downcast_ref::<std::io::Error>() {
        if io_err.kind() == std::io::ErrorKind::NotFound {
            // Extract path from the context message (format: "\"path\"")
            let path = Config::config_folder().join(concat!(env!("CARGO_PKG_NAME"), ".toml"));
            println!("Configuration file: {} does not exist.", path.display());
            if ask() {
                if let Err(e) = Config::export_default(&path) {
                    return e;
                }
                println!("Configuration exported, please update todo_path.");
                exit(0);
            } else {
                exit(1);
            }
        }
    }
    err
}

fn main() {
    let run = || -> Result<(), Box<dyn Error>> {
        log_init()?;
        log::trace!("===== START LOGGING =====");
        let config = Config::from_args(env::args()).map_err(ask_to_create_config)?;
        let mut ui = UI::build(&config)?;
        log::trace!("===== STARTING UI =====");
        ui.run()?;
        Ok(())
    };
    if let Err(e) = run() {
        eprintln!("{}", e);
        exit(1);
    }
}