oliver 0.1.5

Lightweight CLI mod manager for Baldur's Gate 3 on Linux
Documentation
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::{path::PathBuf, process::ExitCode};
use oliver::{Settings, DEFAULT_APP_DATA_DIR};

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true), author, version, about)]
struct Cli {
    #[clap(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Prints the raw XML contents of a mod file.
    Dump(Dump),

    /// Exports the currently installed mods and the mod order to single compressed file.
    Export(Export),

    /// Imports the mods from compressed file.
    Import(Import),

    /// Install one or more mods.
    Install(Install),

    /// List all mods currently installed use.
    List(List),

    /// Print a summary of the contents in a given file.
    Parse(Parse),

    /// Uninstall one or more mods.
    Uninstall(Uninstall),
}

#[derive(Debug, Parser)]
struct Dump {
    /// Path to the file to dump.
    ///
    /// The file must be either a .pak format in the LSPK format.
    #[clap()]
    path: PathBuf,
}

#[derive(Debug, Parser)]
struct Export {
    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// The file where the exported mod data should be written.
    #[clap()]
    destination: PathBuf,
}

#[derive(Debug, Parser)]
struct Import {
    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// The file where the exported mod data should be read.
    #[clap()]
    source: PathBuf,
}

#[derive(Debug, Parser)]
struct List {
    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// Indicate the order that the mods will be loaded by the game.
    #[clap(long, short)]
    order: bool,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Parse {
    /// Paths to the files to summarize.
    ///
    /// Each file must be either a .pak format in the LSPK format or a .lsf file.
    #[clap()]
    paths: Vec<PathBuf>,

    /// Print extra information.
    #[clap(long, short)]
    verbose: bool,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Install {
    /// Print extra information.
    #[clap(long, short)]
    verbose: bool,

    /// Don't perform any actions, but validate whether command would succeed or not.
    ///
    /// If `--verbose` is also specified, invoking with `--dry-run` will indicate the version of
    /// any successfully updated addon.
    #[clap(long, short = 'n')]
    dry_run: bool,

    /// Reinstall even if the same version is already installed.
    #[clap(long, short)]
    refresh: bool,

    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// Paths to the mod files to install. By default, this will only install mods if the version
    /// is newer than the currently installed version (if any exists).
    ///
    /// The mod file can be either a `.pak` file or a zip file containing a `.pak`.
    #[clap()]
    mod_file_paths: Vec<String>,
}

#[derive(Debug, Parser)]
#[command(arg_required_else_help(true))]
struct Uninstall {
    /// Don't perform any actions, but validate whether command would succeed or not.
    #[clap(long, short = 'n')]
    dry_run: bool,

    #[clap(flatten)]
    app_data_path: AppDataPath,

    /// Indexes of the mods in the order to uninstall.
    #[clap()]
    mod_indexes: Vec<usize>,
}

#[derive(Debug, Parser)]
struct AppDataPath {
    /// Path to the `AppData` folder that contains the installation.
    ///
    /// If you're not using Steam, this will likely be in your Wine prefix under
    /// `drive_c/users/<username>/AppData`, where `<username>` is the name of your user.
    ///
    /// This directory is expected to contain the file `modsettings.lsx` in the relative path
    /// `Local/Larian Studios/Baldur's Gate 3/PlayerProfiles/Public/`.
    #[clap(
        long,
        default_value_t = DEFAULT_APP_DATA_DIR.into()
    )]
    app_data_path: String,
}

fn run() -> Result<()> {
    static_assertions_next::assert_cfg!(
        target_os = "linux",
        format!(
            "This tool currently only supports Linux. If you're interested in using this on \
             another platform, feel free to let me know by filing an issue at {}!",
            env!("CARGO_PKG_REPOSTIORY")
        )
    );

    match Cli::parse().command {
        Command::Install(Install {
            verbose,
            dry_run,
            refresh,
            mod_file_paths,
            app_data_path,
        }) => Settings::with_app_data_dir(&app_data_path.app_data_path).install_all(
            verbose,
            dry_run,
            refresh,
            mod_file_paths,
        ),
        Command::List(List {
            order,
            app_data_path,
        }) => Settings::with_app_data_dir(&app_data_path.app_data_path).list(order),
        Command::Parse(Parse { paths, verbose }) => Settings::parse(paths, verbose),
        Command::Export(Export {
            app_data_path,
            destination,
        }) => Settings::with_app_data_dir(&app_data_path.app_data_path).export(&destination),
        Command::Import(Import {
            app_data_path,
            source,
        }) => Settings::with_app_data_dir(&app_data_path.app_data_path).import(&source),
        Command::Uninstall(Uninstall {
            dry_run,
            app_data_path,
            mod_indexes,
        }) => Settings::with_app_data_dir(&app_data_path.app_data_path).uninstall_all(dry_run, mod_indexes),
        Command::Dump(Dump { path }) => Settings::dump(path),
    }
}

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("{e}");
            ExitCode::FAILURE
        }
    }
}