2022-03-28 18:41:35 -07:00
//! Profile management subcommand
use crate ::util ::{
2022-07-15 15:39:38 +00:00
confirm_async , prompt_async , select_async , table , table_path_display ,
2022-03-28 18:41:35 -07:00
} ;
use daedalus ::modded ::LoaderVersion ;
2023-03-31 11:00:43 -07:00
use dunce ::canonicalize ;
2022-03-28 18:41:35 -07:00
use eyre ::{ ensure , Result } ;
2022-06-27 15:53:25 -07:00
use futures ::prelude ::* ;
2022-03-28 18:41:35 -07:00
use paris ::* ;
use std ::path ::{ Path , PathBuf } ;
2022-07-15 15:39:38 +00:00
use tabled ::Tabled ;
2022-06-27 15:53:25 -07:00
use theseus ::prelude ::* ;
2023-04-05 19:04:09 -07:00
use theseus ::profile_create ::profile_create ;
2022-03-28 18:41:35 -07:00
use tokio ::fs ;
2022-06-27 15:53:25 -07:00
use tokio_stream ::wrappers ::ReadDirStream ;
2022-03-28 18:41:35 -07:00
2022-07-15 15:39:38 +00:00
#[ derive(argh::FromArgs, Debug) ]
2022-03-28 18:41:35 -07:00
#[ argh(subcommand, name = " profile " ) ]
2022-07-15 15:39:38 +00:00
/// manage Minecraft instances
2022-03-28 18:41:35 -07:00
pub struct ProfileCommand {
#[ argh(subcommand) ]
action : ProfileSubcommand ,
}
2022-07-15 15:39:38 +00:00
#[ derive(argh::FromArgs, Debug) ]
2022-03-28 18:41:35 -07:00
#[ argh(subcommand) ]
pub enum ProfileSubcommand {
Init ( ProfileInit ) ,
List ( ProfileList ) ,
Remove ( ProfileRemove ) ,
Run ( ProfileRun ) ,
}
2022-07-15 15:39:38 +00:00
#[ derive(argh::FromArgs, Debug) ]
2022-03-28 18:41:35 -07:00
#[ argh(subcommand, name = " init " ) ]
/// create a new profile and manage it with Theseus
pub struct ProfileInit {
#[ argh(positional, default = " std::env::current_dir().unwrap() " ) ]
/// the path of the newly created profile
path : PathBuf ,
#[ argh(option) ]
/// the name of the profile
name : Option < String > ,
#[ argh(option) ]
/// the game version of the profile
game_version : Option < String > ,
#[ argh(option, from_str_fn(modloader_from_str)) ]
/// the modloader to use
modloader : Option < ModLoader > ,
#[ argh(option) ]
/// the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
loader_version : Option < String > ,
}
impl ProfileInit {
pub async fn run (
& self ,
_args : & crate ::Args ,
_largs : & ProfileCommand ,
) -> Result < ( ) > {
// TODO: validate inputs from args early
2022-06-27 15:53:25 -07:00
let state = State ::get ( ) . await ? ;
2022-03-28 18:41:35 -07:00
if self . path . exists ( ) {
ensure! (
self . path . is_dir ( ) ,
" Attempted to create profile in something other than a folder! "
) ;
ensure! (
2022-06-27 15:53:25 -07:00
! self . path . join ( " profile.json " ) . exists ( ) ,
2022-03-28 18:41:35 -07:00
" Profile already exists! Perhaps you want `profile add` instead? "
) ;
if ReadDirStream ::new ( fs ::read_dir ( & self . path ) . await ? )
. next ( )
. await
. is_some ( )
{
warn! ( " You are trying to create a profile in a non-empty directory. If this is an instance from another launcher, please be sure to properly fill the profile.json fields! " ) ;
if ! confirm_async (
String ::from ( " Do you wish to continue " ) ,
false ,
)
. await ?
{
eyre ::bail! ( " Aborted! " ) ;
}
}
} else {
fs ::create_dir_all ( & self . path ) . await ? ;
}
info! (
" Creating profile at path {} " ,
2023-03-31 11:00:43 -07:00
& canonicalize ( & self . path ) ? . display ( )
2022-03-28 18:41:35 -07:00
) ;
// TODO: abstract default prompting
let name = match & self . name {
Some ( name ) = > name . clone ( ) ,
None = > {
let default = self . path . file_name ( ) . unwrap ( ) . to_string_lossy ( ) ;
prompt_async (
String ::from ( " Instance name " ) ,
Some ( default . into_owned ( ) ) ,
)
. await ?
}
} ;
let game_version = match & self . game_version {
Some ( version ) = > version . clone ( ) ,
None = > {
2022-06-27 15:53:25 -07:00
let default = & state . metadata . minecraft . latest . release ;
2022-03-28 18:41:35 -07:00
prompt_async (
String ::from ( " Game version " ) ,
Some ( default . clone ( ) ) ,
)
. await ?
}
} ;
let loader = match & self . modloader {
Some ( loader ) = > * loader ,
None = > {
let choice = select_async (
" Modloader " . to_owned ( ) ,
& [ " vanilla " , " fabric " , " forge " ] ,
)
. await ? ;
match choice {
0 = > ModLoader ::Vanilla ,
1 = > ModLoader ::Fabric ,
2 = > ModLoader ::Forge ,
_ = > eyre ::bail! (
" Invalid modloader ID: {choice}. This is a bug in the launcher! "
) ,
}
}
} ;
let loader = if loader ! = ModLoader ::Vanilla {
let version = match & self . loader_version {
Some ( version ) = > String ::from ( version ) ,
None = > prompt_async (
String ::from (
" Modloader version (latest, stable, or a version ID) " ,
) ,
Some ( String ::from ( " latest " ) ) ,
)
. await ? ,
} ;
let filter = | it : & LoaderVersion | match version . as_str ( ) {
" latest " = > true ,
" stable " = > it . stable ,
2023-03-30 16:18:33 -07:00
id = > it . id = = * id ,
2022-03-28 18:41:35 -07:00
} ;
let loader_data = match loader {
2022-06-27 15:53:25 -07:00
ModLoader ::Forge = > & state . metadata . forge ,
ModLoader ::Fabric = > & state . metadata . fabric ,
2022-03-28 18:41:35 -07:00
_ = > eyre ::bail! ( " Could not get manifest for loader {loader}. This is a bug in the CLI! " ) ,
} ;
2023-03-30 16:18:33 -07:00
let loaders = & loader_data . game_versions
2022-03-28 18:41:35 -07:00
. iter ( )
. find ( | it | it . id = = game_version )
. ok_or_else ( | | eyre ::eyre! ( " Modloader {loader} unsupported for Minecraft version {game_version} " ) ) ?
. loaders ;
let loader_version =
loaders . iter ( ) . cloned ( ) . find ( filter ) . ok_or_else ( | | {
eyre ::eyre! (
" Invalid version {version} for modloader {loader} "
)
} ) ? ;
Some ( ( loader_version , loader ) )
} else {
None
} ;
2023-04-05 19:04:09 -07:00
profile_create (
name ,
game_version ,
loader . clone ( ) . map ( | x | x . 1 ) . unwrap_or ( ModLoader ::Vanilla ) ,
loader . map ( | x | x . 0. id ) ,
None ,
None ,
)
. await ? ;
2022-03-28 18:41:35 -07:00
success! (
" Successfully created instance, it is now available to use with Theseus! "
) ;
Ok ( ( ) )
}
}
2022-07-15 15:39:38 +00:00
#[ derive(argh::FromArgs, Debug) ]
2022-03-28 18:41:35 -07:00
/// list all managed profiles
#[ argh(subcommand, name = " list " ) ]
pub struct ProfileList { }
#[ derive(Tabled) ]
struct ProfileRow < ' a > {
name : & ' a str ,
#[ field(display_with = " table_path_display " ) ]
path : & ' a Path ,
#[ header( " game version " ) ]
game_version : & ' a str ,
loader : & ' a ModLoader ,
#[ header( " loader version " ) ]
loader_version : & ' a str ,
}
impl < ' a > From < & ' a Profile > for ProfileRow < ' a > {
fn from ( it : & ' a Profile ) -> Self {
Self {
name : & it . metadata . name ,
path : & it . path ,
game_version : & it . metadata . game_version ,
loader : & it . metadata . loader ,
loader_version : it
. metadata
. loader_version
. as_ref ( )
. map_or ( " " , | it | & it . id ) ,
}
}
}
2022-06-27 15:53:25 -07:00
impl < ' a > From < & ' a Path > for ProfileRow < ' a > {
fn from ( it : & ' a Path ) -> Self {
Self {
name : " ? " ,
path : it ,
game_version : " ? " ,
loader : & ModLoader ::Vanilla ,
loader_version : " ? " ,
}
}
}
2022-03-28 18:41:35 -07:00
impl ProfileList {
pub async fn run (
& self ,
_args : & crate ::Args ,
_largs : & ProfileCommand ,
) -> Result < ( ) > {
2022-07-15 15:39:38 +00:00
let profiles = profile ::list ( ) . await ? ;
2023-04-05 19:04:09 -07:00
let rows = profiles . values ( ) . map ( ProfileRow ::from ) ;
2022-03-28 18:41:35 -07:00
2022-07-15 15:39:38 +00:00
let table = table ( rows ) . with (
2022-03-28 18:41:35 -07:00
tabled ::Modify ::new ( tabled ::Column ( 1 ..= 1 ) )
. with ( tabled ::MaxWidth ::wrapping ( 40 ) ) ,
) ;
println! ( " {table} " ) ;
2022-06-27 15:53:25 -07:00
2022-03-28 18:41:35 -07:00
Ok ( ( ) )
}
}
2022-07-15 15:39:38 +00:00
#[ derive(argh::FromArgs, Debug) ]
2022-03-28 18:41:35 -07:00
/// unmanage a profile
#[ argh(subcommand, name = " remove " ) ]
pub struct ProfileRemove {
#[ argh(positional, default = " std::env::current_dir().unwrap() " ) ]
/// the profile to get rid of
profile : PathBuf ,
}
impl ProfileRemove {
pub async fn run (
& self ,
_args : & crate ::Args ,
_largs : & ProfileCommand ,
) -> Result < ( ) > {
2023-03-31 11:00:43 -07:00
let profile = canonicalize ( & self . profile ) ? ;
2022-03-28 18:41:35 -07:00
info! ( " Removing profile {} from Theseus " , self . profile . display ( ) ) ;
2022-06-27 15:53:25 -07:00
2022-03-28 18:41:35 -07:00
if confirm_async ( String ::from ( " Do you wish to continue " ) , true ) . await ? {
2023-04-05 19:04:09 -07:00
profile ::remove ( & profile ) . await ? ;
State ::sync ( ) . await ? ;
success! ( " Profile removed! " ) ;
2022-03-28 18:41:35 -07:00
} else {
error! ( " Aborted! " ) ;
}
Ok ( ( ) )
}
}
2022-07-15 15:39:38 +00:00
#[ derive(argh::FromArgs, Debug) ]
2022-03-28 18:41:35 -07:00
/// run a profile
#[ argh(subcommand, name = " run " ) ]
pub struct ProfileRun {
#[ argh(positional, default = " std::env::current_dir().unwrap() " ) ]
/// the profile to run
profile : PathBuf ,
2022-07-15 15:39:38 +00:00
#[ argh(option) ]
/// the user to authenticate with
user : Option < uuid ::Uuid > ,
2022-03-28 18:41:35 -07:00
}
impl ProfileRun {
pub async fn run (
& self ,
_args : & crate ::Args ,
_largs : & ProfileCommand ,
) -> Result < ( ) > {
info! ( " Starting profile at path {}... " , self . profile . display ( ) ) ;
2023-03-31 11:00:43 -07:00
let path = canonicalize ( & self . profile ) ? ;
2022-03-28 18:41:35 -07:00
2022-07-15 15:39:38 +00:00
let id = future ::ready ( self . user . ok_or ( ( ) ) )
. or_else ( | _ | async move {
let state = State ::get ( ) . await ? ;
let settings = state . settings . read ( ) . await ;
settings . default_user
. ok_or ( eyre ::eyre! (
" Could not find any users, please add one using the `user add` command. "
) )
} )
. await ? ;
2023-04-13 12:03:15 -07:00
let credentials = auth ::refresh ( id ) . await ? ;
2022-03-28 18:41:35 -07:00
2023-04-07 14:43:21 -07:00
let proc_lock = profile ::run_credentials ( & path , & credentials ) . await ? ;
2023-03-31 11:00:43 -07:00
let mut proc = proc_lock . write ( ) . await ;
2023-04-05 10:07:59 -07:00
process ::wait_for ( & mut proc ) . await ? ;
2022-03-28 18:41:35 -07:00
success! ( " Process exited successfully! " ) ;
Ok ( ( ) )
}
}
impl ProfileCommand {
2022-07-15 15:39:38 +00:00
pub async fn run ( & self , args : & crate ::Args ) -> Result < ( ) > {
dispatch! ( & self . action , ( args , self ) = > {
ProfileSubcommand ::Init ,
ProfileSubcommand ::List ,
ProfileSubcommand ::Remove ,
ProfileSubcommand ::Run
} )
2022-03-28 18:41:35 -07:00
}
}
fn modloader_from_str ( it : & str ) -> core ::result ::Result < ModLoader , String > {
match it {
" vanilla " = > Ok ( ModLoader ::Vanilla ) ,
" forge " = > Ok ( ModLoader ::Forge ) ,
" fabric " = > Ok ( ModLoader ::Fabric ) ,
_ = > Err ( String ::from ( " Invalid modloader: {it} " ) ) ,
}
}