protonup-rs 0.3.0

TUI Program for Custom Proton Download and installation written in rust
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use inquire::{Confirm, MultiSelect, Select, Text};
use std::fmt;
use std::fs;
use std::fs::create_dir_all;
use std::sync::atomic::Ordering;
use std::thread;
use std::{sync::Arc, time::Duration};
use structopt::StructOpt;
mod file_path;

use libprotonup::{constants, file, github, utils};

#[derive(Debug, StructOpt)]
struct Opt {
    // /// install a specific version
    // #[structopt(short, long)]
    // tag: Option<String>,
    // #[structopt(short, long)]
    // /// list installed versions
    // list: Option<String>,
    // /// remove existing installations
    // #[structopt(short, long)]
    // remove: Option<String>,
    // /// set specific output
    // #[structopt(short, long)]
    // output: Option<String>,
    // /// set installation directory
    // #[structopt(short, long)]
    // dir: Option<String>,
    // /// disable prompts and logs
    /// Skip Menu and download latest directly
    #[structopt(short, long)]
    quick_download: bool,
    #[structopt(short = "f", long)]
    quick_download_flatpak: bool,
    // /// download only
    // #[structopt(long)]
    // download: bool,
    // /// list available versions
    // #[structopt(long)]
    // releases: bool,
}

#[derive(Debug, Copy, Clone)]
#[allow(clippy::upper_case_acronyms)]
enum Menu {
    QuickUpdate,
    QuickUpdateFlatpak,
    ChoseReleases,
    ChoseReleasesFlatpak,
    ChoseReleasesCustomDir,
}

impl Menu {
    // could be generated by macro
    const VARIANTS: &'static [Menu] = &[
        Self::QuickUpdate,
        Self::QuickUpdateFlatpak,
        Self::ChoseReleases,
        Self::ChoseReleasesFlatpak,
        Self::ChoseReleasesCustomDir,
    ];
}

impl fmt::Display for Menu {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Self::QuickUpdate => write!(f, "Quick Update (Download latest GE Proton)"),
            Self::QuickUpdateFlatpak => write!(
                f,
                "Quick Update (Download latest GE Proton) for Flatpak Steam"
            ),
            Self::ChoseReleases => write!(f, "Chose GE Proton Releases from list"),
            Self::ChoseReleasesFlatpak => {
                write!(f, "Chose GE Proton Releases from list for Flatpak Steam")
            }
            Self::ChoseReleasesCustomDir => write!(
                f,
                "Chose GE Proton Releases and install to custom directory"
            ),
        }
    }
}

fn tag_menu(options: Vec<String>) -> Vec<String> {
    let answer = MultiSelect::new("Select the Versions you want to download :", options)
        .with_default(&vec![0 as usize])
        .prompt();

    match answer {
        Ok(list) => return list,

        Err(_) => {
            println!("The tag list could not be processed");
            return vec![];
        }
    }
}

fn confirm_menu(text: String) -> bool {
    let answer = Confirm::new(&text)
        .with_default(false)
        .with_help_message("If you chose yes, we will re install it.")
        .prompt();

    match answer {
        Ok(choice) => choice,
        Err(_) => false,
    }
}

#[tokio::main]
async fn main() {
    let Opt {
        // tag,
        // list,
        // remove,
        // output,
        // dir,
        quick_download,
        quick_download_flatpak,
        // download,
        // releases,
    } = Opt::from_args();

    if quick_download {
        download_file("latest", constants::DEFAULT_INSTALL_DIR.to_string())
            .await
            .unwrap();
        return;
    }
    if quick_download_flatpak {
        download_file("latest", constants::DEFAULT_INSTALL_DIR_FLATPAK.to_string())
            .await
            .unwrap();
        return;
    }
    let answer: Menu = Select::new("ProtonUp Menu: Chose your action:", Menu::VARIANTS.to_vec())
        .prompt()
        .unwrap();

    match answer {
        Menu::QuickUpdate => {
            let tag = github::fetch_data_from_tag("latest").await.unwrap();

            if file::check_if_exists(
                constants::DEFAULT_INSTALL_DIR.to_owned(),
                tag.version.clone(),
            ) {
                if !confirm_menu(format!(
                    "Version {} exists in installation path. Overwrite ?",
                    tag.version
                )) {
                    return;
                }
            }

            download_file("latest", constants::DEFAULT_INSTALL_DIR.to_string())
                .await
                .unwrap();
            return;
        }
        Menu::QuickUpdateFlatpak => {
            let tag = github::fetch_data_from_tag("latest").await.unwrap();

            if file::check_if_exists(
                constants::DEFAULT_INSTALL_DIR_FLATPAK.to_owned(),
                tag.version.clone(),
            ) {
                if !confirm_menu(format!(
                    "Version {} exists in installation path. Overwrite ?",
                    tag.version
                )) {
                    return;
                }
            }

            download_file("latest", constants::DEFAULT_INSTALL_DIR.to_string())
                .await
                .unwrap();
            return;
        }
        Menu::ChoseReleases => {
            let release_list = libprotonup::github::list_releases().await.unwrap();
            let tag_list: Vec<String> = release_list
                .into_iter()
                .map(|r| (r.tag_name.clone()))
                .collect();
            let list = tag_menu(tag_list);
            for tag in list.iter() {
                if file::check_if_exists(constants::DEFAULT_INSTALL_DIR.to_owned(), tag.to_owned())
                {
                    if !confirm_menu(format!(
                        "Version {} exists in installation path. Overwrite ?",
                        tag
                    )) {
                        return;
                    }
                }
                download_file(tag, constants::DEFAULT_INSTALL_DIR.to_string())
                    .await
                    .unwrap();
            }
            return;
        }
        Menu::ChoseReleasesFlatpak => {
            let release_list = libprotonup::github::list_releases().await.unwrap();
            let tag_list: Vec<String> = release_list
                .into_iter()
                .map(|r| (r.tag_name.clone()))
                .collect();
            let list = tag_menu(tag_list);
            for tag in list.iter() {
                if file::check_if_exists(
                    constants::DEFAULT_INSTALL_DIR_FLATPAK.to_owned(),
                    tag.to_owned(),
                ) {
                    if !confirm_menu(format!(
                        "Version {} exists in installation path. Overwrite ?",
                        tag
                    )) {
                        return;
                    }
                }
                download_file(tag, constants::DEFAULT_INSTALL_DIR_FLATPAK.to_string())
                    .await
                    .unwrap();
            }
            return;
        }
        Menu::ChoseReleasesCustomDir => {
            let current_dir = std::env::current_dir().unwrap();
            let help_message = format!("Current directory: {}", current_dir.to_string_lossy());
            let answer = Text::new("Installation Path:")
                .with_autocomplete(file_path::FilePathCompleter::default())
                .with_help_message(&help_message)
                .prompt();

            let chosen_path = match answer {
                Ok(path) => path,
                Err(error) => {
                    println!(
                        "Error choosing custom path. Using the default. Error: {:?}",
                        error
                    );
                    constants::DEFAULT_INSTALL_DIR.to_string()
                }
            };
            let release_list = libprotonup::github::list_releases().await.unwrap();
            let tag_list: Vec<String> = release_list
                .into_iter()
                .map(|r| (r.tag_name.clone()))
                .collect();
            let list = tag_menu(tag_list);
            for tag in list.iter() {
                if file::check_if_exists(constants::DEFAULT_INSTALL_DIR.to_owned(), tag.to_owned())
                {
                    if !confirm_menu(format!(
                        "Version {} exists in installation path. Overwrite ?",
                        tag
                    )) {
                        return;
                    }
                }

                download_file(tag, chosen_path.clone()).await.unwrap();
            }
            return;
        }
    }
}

pub async fn download_file(tag: &str, install_path: String) -> Result<(), String> {
    let install_dir = utils::expand_tilde(install_path).unwrap();
    let mut temp_dir = utils::expand_tilde(constants::TEMP_DIR).unwrap();

    let download = github::fetch_data_from_tag(tag).await.unwrap();

    temp_dir.push(format!("{}.tar.gz", &download.version));

    // install_dir
    create_dir_all(&install_dir).unwrap();

    let git_hash = file::download_file_into_memory(&download.sha512sum)
        .await
        .unwrap();

    if temp_dir.exists() {
        fs::remove_file(&temp_dir).unwrap();
    }

    let (progress, done) = file::create_progress_trackers();
    let progress_read = Arc::clone(&progress);
    let done_read = Arc::clone(&done);
    let url = String::from(&download.download);
    let i_dir = String::from(install_dir.to_str().unwrap());

    // start ProgressBar in another thread
    thread::spawn(move || {
        let pb = ProgressBar::with_draw_target(
            Some(download.size),
            ProgressDrawTarget::stderr_with_hz(20),
        );
        pb.set_style(ProgressStyle::default_bar()
        .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec})").unwrap()
        .progress_chars("#>-"));
        pb.set_message(format!("Downloading {}", url.split('/').last().unwrap()));
        let wait_time = Duration::from_millis(50); // 50ms wait is about 20Hz
        loop {
            let newpos = progress_read.load(Ordering::Relaxed);
            pb.set_position(newpos as u64);
            if done_read.load(Ordering::Relaxed) {
                break;
            }
            thread::sleep(wait_time);
        }
        pb.set_message(format!("Downloaded {} to {}", url, i_dir));
        pb.abandon(); // closes progress bas without blanking terminal

        println!("Checking file integrity"); // This is being printed here because the progress bar needs to be closed before printing.
    });

    file::download_file_progress(
        download.download,
        download.size,
        temp_dir.clone(),
        progress,
        done,
    )
    .await
    .unwrap();
    if !file::hash_check_file(temp_dir.to_str().unwrap().to_string(), git_hash).unwrap() {
        return Err("Failed checking file hash".to_string());
    }
    println!("Unpacking files into install location. Please wait");
    file::decompress(temp_dir, install_dir.clone()).unwrap();
    println!(
        "Done! Restart Steam. Proton GE installed in {}",
        install_dir.to_string_lossy()
    );
    return Ok(());
}