oliver 0.1.5

Lightweight CLI mod manager for Baldur's Gate 3 on Linux
Documentation
use anyhow::Result;
use larian_formats::{bg3::raw::Save, lspk};
use quick_xml::se::Serializer;
use serde::Serialize;
use std::{
    fs::{File, ReadDir},
    io::{BufReader, Read, Seek, Write},
    path::{Path, PathBuf},
};
use crate::read_only_guard::ReadOnlyGuard;

pub const DEFAULT_APP_DATA_DIR: &str =
    "~/.local/share/Steam/steamapps/compatdata/1086940/pfx/drive_c/users/steamuser/AppData";

#[derive(Debug)]
pub struct Settings {
    mods_path: PathBuf,
    pub(crate) settings_path: PathBuf,
}

impl Default for Settings {
    fn default() -> Self {
        Self::with_app_data_dir(DEFAULT_APP_DATA_DIR)
    }
}

impl Settings {
    pub fn with_app_data_dir(path: &str) -> Self {
        let app_data_dir = match std::env::var("HOME") {
            Ok(home_dir) if path.starts_with('~') => path.replacen('~', &home_dir, 1),
            _ => path.into(),
        };

        let settings_path = [
            &app_data_dir,
            "Local",
            "Larian Studios",
            "Baldur's Gate 3",
            "PlayerProfiles",
            "Public",
            "modsettings.lsx",
        ]
        .into_iter()
        .collect();

        let mods_path = [
            &app_data_dir,
            "Local",
            "Larian Studios",
            "Baldur's Gate 3",
            "Mods",
        ]
        .into_iter()
        .collect();

        Self {
            mods_path,
            settings_path,
        }
    }

    pub fn get_mod_file_path(&self, mod_file_name: impl AsRef<Path>) -> PathBuf {
        self.mods_path.join(mod_file_name)
    }

    pub fn open_mod_file(&self, mod_file_name: impl AsRef<Path>) -> Result<File> {
        let file = File::open(self.get_mod_file_path(mod_file_name))?;
        Ok(file)
    }

    pub fn read_mod_file(&self, mod_file_name: impl AsRef<Path>) -> Result<lspk::Reader<File>> {
        let file = self.open_mod_file(mod_file_name)?;
        let reader = lspk::Reader::new(file)?;
        Ok(reader)
    }

    pub fn read_mod_files_dir(&self) -> Result<ReadDir> {
        let entries = std::fs::read_dir(&self.mods_path)?;
        Ok(entries)
    }

    pub fn open_mod_settings_file(&self) -> Result<impl Read + Write + Seek> {
        ReadOnlyGuard::new(&self.settings_path)
    }

    pub fn read_current_installed_mods(&self) -> Result<Save> {
        let settings_file = self.open_mod_settings_file()?;

        let parsed_settings = quick_xml::de::from_reader(BufReader::new(settings_file))?;
        Ok(parsed_settings)
    }

    pub fn write_settings_file(&self, save: &Save) -> Result<()> {
        let mut xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".to_string();
        let mut ser = Serializer::new(&mut xml);
        ser.indent(' ', 4);
        save.serialize(ser)?;
        xml.push('\n');

        let mut temp_writable = ReadOnlyGuard::new(&self.settings_path)?;
        temp_writable.write_all(xml.replace("/>\n", " />\n").as_ref())?;
        Ok(())
    }
}