bawa 0.1.0

A TUI game save organizer
Documentation
use crate::app::StatefulList;
use anyhow::{bail, Result};
use std::{fmt::Display, path::Path};

pub struct Preset {
    name: &'static str,
    steam_app_id: &'static str,
    folder_name: &'static str,
    file_name: &'static str,
}

impl Preset {
    pub fn get_from_data_dir(&self) -> Result<Vec<String>> {
        let main_directory = match dirs::data_dir() {
            Some(mut path) => {
                #[cfg(unix)]
                {
                    let components = [
                        "Steam",
                        "steamapps",
                        "compatdata",
                        self.steam_app_id,
                        "pfx",
                        "drive_c",
                        "users",
                        "steamuser",
                        "AppData",
                        "Roaming",
                    ];
                    path.extend(components);
                }
                path.join(self.folder_name)
            }
            None => bail!("Couldn't find data directory"),
        };

        self.get_savefiles(&main_directory)
    }

    pub fn get_from_documents_dir(&self) -> Result<Vec<String>> {
        #[cfg(unix)]
        let documents_dir = match dirs::data_dir() {
            Some(path) => path.join(format!(
                "Steam/steamapps/compatdata/{}/pfx/drive_c/users/steamuser/Documents",
                self.steam_app_id
            )),
            None => bail!("Couldn't find data directory"),
        };
        #[cfg(windows)]
        let documents_dir = match dirs::document_dir() {
            Some(path) => path,
            None => bail!("Couldn't find documents directory"),
        };

        let main_directory = documents_dir.join(self.folder_name);

        self.get_savefiles(&main_directory)
    }

    fn get_savefiles(&self, directory: &Path) -> Result<Vec<String>> {
        let mut paths = Vec::new();
        for entry in directory.read_dir()? {
            let mut path = entry?.path();
            if path.is_dir() && is_steam_id(&path.file_name().unwrap_or_default().to_string_lossy())
            {
                path.push(self.file_name);
                paths.push(path.to_string_lossy().into_owned());
            }
        }
        Ok(paths)
    }
}

impl Display for Preset {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.name)
    }
}

const DARK_SOULS_REMASTERED: Preset = Preset {
    name: "Dark Souls Remastered",
    steam_app_id: "570940",
    #[cfg(unix)]
    folder_name: "NBGI/DARK SOULS REMASTERED",
    #[cfg(windows)]
    folder_name: "NBGI\\DARK SOULS REMASTERED",
    file_name: "DRAKS0005.sl2",
};

const DARK_SOULS2: Preset = Preset {
    name: "Dark Souls II",
    steam_app_id: "236430",
    folder_name: "DarkSoulsII",
    file_name: "DARKSII0000.sl2",
};

const DARK_SOULS2_SOTFS: Preset = Preset {
    name: "Dark Souls II: SotFS",
    steam_app_id: "335300",
    folder_name: "DarkSoulsII",
    file_name: "DS2SOFS0000.sl2",
};

const DARK_SOULS3: Preset = Preset {
    name: "Dark Souls III",
    steam_app_id: "374320",
    folder_name: "DarkSoulsIII",
    file_name: "DS30000.sl2",
};

const SEKIRO: Preset = Preset {
    name: "Sekiro",
    steam_app_id: "814380",
    folder_name: "Sekiro",
    file_name: "S0000.sl2",
};

const ELDEN_RING: Preset = Preset {
    name: "Elden Ring",
    steam_app_id: "1245620",
    folder_name: "EldenRing",
    file_name: "ER0000.sl2",
};

pub fn is_64_bit_steam_id(dir_name: &str) -> bool {
    dir_name.starts_with("76561")
        && dir_name.len() == 17
        && dir_name.chars().all(|c| c.is_ascii_digit())
}

pub fn is_steam_id(dir_name: &str) -> bool {
    const INDIVIDUAL_IDENTIFIER: u64 = 0x0110000100000000;

    if is_64_bit_steam_id(dir_name) {
        return true;
    }

    let Ok(hex) = u64::from_str_radix(dir_name, 16) else {
        return true;
    };

    is_64_bit_steam_id(&hex.to_string())
        || is_64_bit_steam_id(&(hex + INDIVIDUAL_IDENTIFIER).to_string())
}

#[derive(Default)]
pub enum Step {
    #[default]
    EnterName,
    PresetOrManual(bool),
    Presets(StatefulList<Preset>),
    SaveFileLocations(StatefulList<String>),
    EnterPath,
}

#[derive(Default)]
pub struct CreatingGame {
    pub name: Option<String>,
    pub edit: bool,
    pub step: Step,
}

impl CreatingGame {
    pub fn edit_path() -> Self {
        Self {
            name: None,
            edit: true,
            step: Step::PresetOrManual(false),
        }
    }

    pub fn load_presets(&mut self) {
        let presets = vec![
            DARK_SOULS_REMASTERED,
            DARK_SOULS2,
            DARK_SOULS2_SOTFS,
            DARK_SOULS3,
            SEKIRO,
            ELDEN_RING,
        ];

        self.step = Step::Presets(StatefulList::with_items(presets));
    }
}