mod models; mod utils; use crate::{models::vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions}, utils::{with_progressbar, with_spinner}}; use clap::{ Parser, Subcommand, builder::styling::{self}, }; use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme}; use futures_util::StreamExt; use sha1::{Digest, Sha1}; use std::error::Error; use tokio::io::AsyncWriteExt; fn styles() -> styling::Styles { styling::Styles::styled() .header(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) .literal(styling::AnsiColor::Cyan.on_default() | styling::Effects::BOLD) } #[derive(Parser)] #[command(name = "mcsm", version, about, styles = styles())] struct Mcsm { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { Init { name: String }, } #[tokio::main] async fn main() -> Result<(), Box> { let mcsm = Mcsm::parse(); match mcsm.command { Commands::Init { name: project_name } => { tokio::fs::create_dir(&project_name).await?; std::env::set_current_dir(&project_name)?; let loaders = vec!["Vanilla", "Fabric"]; let loader_index = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Select Loader") .items(&loaders) .default(0) .interact()?; match loader_index { 0 => { const URL: &str = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; let res = with_spinner("", "", || async { let res: VanillaVersions = reqwest::get(URL).await?.json().await?; Ok(res) }).await?; let release_versions = res .versions .iter() .filter(|version| version.version_type == VanillaVersionType::Release) .collect::>(); let release_version_names = release_versions .iter() .map(|version| version.id.clone()) .collect::>(); let version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) .with_prompt("Select Version") .items(&release_version_names) .default(0) .interact()?; let version_detail: VanillaVersionDetail = reqwest::get(release_versions[version_index].url.clone()) .await? .json() .await?; let start_msg = format!( "downloading server.jar for {} {}", loaders[loader_index], release_version_names[version_index] ); with_progressbar(&start_msg, "download finished", version_detail.downloads.server.size, |pb| async move { let server_bytes = reqwest::get(version_detail.downloads.server.url).await?; let mut file = tokio::fs::File::create("server.jar").await?; let mut stream = server_bytes.bytes_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk?; file.write_all(&chunk).await?; pb.inc(chunk.len() as u64); } Ok(()) }).await?; with_spinner("verifying sha1 hash", "sha1 hash verified", || async { let data = tokio::fs::read("server.jar").await?; let mut hasher = Sha1::new(); hasher.update(&data); let hash = hasher.finalize(); let hex: String = hash.iter().map(|b| format!("{:02x}", b)).collect(); if hex != version_detail.downloads.server.sha1 { tokio::fs::remove_file("server.jar").await?; return Err("sha1 verifaction failed for downloaded file".into()); } Ok(()) }).await?; } 1 => {} _ => return Err("Invalid selection".into()), } } } Ok(()) }