argv-todo 0.1.0

A Vim-style terminal todo manager with nested tasks, priorities, search, and local SQLite storage
use anyhow::{Context, Result};
use std::{
    fs::{self, OpenOptions},
    io::Write,
    path::Path,
};

use super::{Config, SortOrder, TaskView, parsing::parse};
use crate::db::Priority;

impl Config {
    pub(crate) fn save_database_path(&mut self, path: &str) -> Result<()> {
        self.save_setting("database_path", toml::Value::String(path.into()))?;
        self.database_path = path.into();
        Ok(())
    }

    pub(crate) fn save_task_view(&mut self, view: TaskView) -> Result<()> {
        self.save_setting("task_view", toml::Value::String(view.label().into()))?;
        self.task_view = view;
        Ok(())
    }

    pub(crate) fn save_default_priority(&mut self, priority: Priority) -> Result<()> {
        self.save_setting(
            "default_priority",
            toml::Value::String(priority.label().into()),
        )?;
        self.default_priority = priority;
        Ok(())
    }

    pub(crate) fn save_show_completed(&mut self, show: bool) -> Result<()> {
        self.save_setting("show_completed", toml::Value::Boolean(show))?;
        self.show_completed = show;
        Ok(())
    }

    pub(crate) fn save_sort_order(&mut self, order: SortOrder) -> Result<()> {
        self.save_setting("sort_order", toml::Value::String(order.label().into()))?;
        self.sort_order = order;
        Ok(())
    }

    pub(crate) fn save_show_hints(&mut self, show: bool) -> Result<()> {
        self.save_setting("show_hints", toml::Value::Boolean(show))?;
        self.show_hints = show;
        Ok(())
    }

    fn save_setting(&self, setting: &str, value: toml::Value) -> Result<()> {
        let mut content = fs::read_to_string(&self.path)
            .with_context(|| format!("Could not read config {}", self.path.display()))?;
        parse(&content).with_context(|| format!("Invalid config {}", self.path.display()))?;
        let table = toml::de::DeTable::parse(&content)?;
        let span = table
            .get_ref()
            .iter()
            .find_map(|(key, value)| (key.get_ref().as_ref() == setting).then(|| value.span()));
        let value = value.to_string();
        if let Some(span) = span {
            content.replace_range(span, &value);
        } else {
            if !content.is_empty() && !content.ends_with('\n') {
                content.push('\n');
            }
            content.push_str(&format!("{setting} = {value}\n"));
        }
        parse(&content)?;
        write_config(&self.path, &content)?;
        Ok(())
    }
}

fn write_config(path: &Path, content: &str) -> Result<()> {
    let temporary = path.with_file_name(format!(".config.toml.{}.tmp", std::process::id()));
    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&temporary)
        .with_context(|| format!("Could not save config {}", path.display()))?;
    let result = (|| -> std::io::Result<()> {
        file.set_permissions(fs::metadata(path)?.permissions())?;
        file.write_all(content.as_bytes())?;
        file.sync_all()?;
        drop(file);
        fs::rename(&temporary, path)
    })();
    if result.is_err() {
        let _ = fs::remove_file(&temporary);
    }
    result.with_context(|| format!("Could not save config {}", path.display()))
}