use crate::cli::ProfileCommand;
use crate::error::AppResult;
use crate::target::GAME;
use std::path::Path;
use thunderstore_engine::profile::{layout, modlist};
fn require_profile(base: &Path, name: &str) -> AppResult<()> {
layout::require_profile(base, GAME, name)
.map_err(|bad| crate::target::profile_error(bad.into()))?;
Ok(())
}
pub fn run(base: &Path, command: &ProfileCommand) -> AppResult<()> {
match command {
ProfileCommand::List => {
let selected = layout::read_selection(base, GAME);
let names = layout::list(base, GAME)?;
if names.is_empty() {
println!(
"vmm: {}",
crate::error::advice_message(
"there are no profiles yet.",
"Nothing was changed.",
&["vmm profile create <name>"],
)
);
return Ok(());
}
for name in names {
let marker = if selected.as_deref() == Some(name.as_str()) {
"*"
} else {
" "
};
match modlist::read(&layout::profile_dir(base, GAME, &name)) {
Ok(mods) => println!("{marker} {name} ({} mods)", mods.len()),
Err(error) => println!("{marker} {name} (mods.yml unreadable: {error})"),
}
}
}
ProfileCommand::Create { name } => {
let dir = layout::create(base, GAME, name)?;
println!("created {}", dir.display());
}
ProfileCommand::Delete { name } => {
let was_selected = layout::read_selection(base, GAME).as_deref() == Some(name.as_str());
require_profile(base, name)?;
layout::delete(base, GAME, name)?;
println!("deleted {name}");
if was_selected {
println!("cleared the selection; commands now use the game directory");
}
}
ProfileCommand::Rename { from, to } => {
require_profile(base, from)?;
layout::rename(base, GAME, from, to)?;
println!("renamed {from} to {to}");
}
ProfileCommand::Duplicate { from, to } => {
require_profile(base, from)?;
let dir = layout::duplicate(base, GAME, from, to)?;
println!("duplicated {from} to {}", dir.display());
}
ProfileCommand::Use { name } => {
layout::use_profile(base, GAME, name).map_err(crate::target::profile_error)?;
println!("using profile {name}");
}
ProfileCommand::Clear => {
layout::clear_selection(base, GAME)?;
println!("cleared the selection; commands now use the game directory");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use thunderstore_engine::profile::layout;
#[test]
fn naming_a_missing_profile_advises_the_same_way_however_it_was_named() {
let dir = tempdir().unwrap();
let game_dir = tempdir().unwrap();
let base = dir.path();
let selecting = run(
base,
&ProfileCommand::Use {
name: "nope".to_string(),
},
)
.unwrap_err();
let resolving = crate::target::resolve(
base.to_path_buf(),
game_dir.path().to_path_buf(),
Some("nope"),
)
.unwrap_err();
assert_eq!(selecting.to_string(), resolving.to_string());
assert_eq!(
resolving.to_string(),
crate::target::missing_profile_error("nope").to_string()
);
}
#[test]
fn every_command_taking_an_existing_profile_advises_the_same_way_about_a_missing_one() {
let dir = tempdir().unwrap();
let base = dir.path();
let expected = crate::target::missing_profile_error("nope").to_string();
let commands: [(&str, ProfileCommand); 4] = [
(
"use",
ProfileCommand::Use {
name: "nope".to_string(),
},
),
(
"delete",
ProfileCommand::Delete {
name: "nope".to_string(),
},
),
(
"rename",
ProfileCommand::Rename {
from: "nope".to_string(),
to: "other".to_string(),
},
),
(
"duplicate",
ProfileCommand::Duplicate {
from: "nope".to_string(),
to: "other".to_string(),
},
),
];
for (label, command) in &commands {
let message = run(base, command).unwrap_err().to_string();
assert_eq!(message, expected, "`profile {label}` worded it differently");
}
assert!(!layout::profile_dir(base, GAME, "other").exists());
}
#[test]
fn create_duplicate_rename_delete_lifecycle() {
let dir = tempdir().unwrap();
let base = dir.path();
run(
base,
&ProfileCommand::Create {
name: "main".to_string(),
},
)
.unwrap();
assert!(layout::profile_dir(base, GAME, "main").is_dir());
assert!(
run(
base,
&ProfileCommand::Create {
name: "main".to_string(),
}
)
.is_err()
);
run(
base,
&ProfileCommand::Duplicate {
from: "main".to_string(),
to: "copy".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Rename {
from: "copy".to_string(),
to: "renamed".to_string(),
},
)
.unwrap();
assert_eq!(
layout::list(base, GAME).unwrap(),
vec!["main".to_string(), "renamed".to_string()]
);
run(
base,
&ProfileCommand::Delete {
name: "renamed".to_string(),
},
)
.unwrap();
assert_eq!(layout::list(base, GAME).unwrap(), vec!["main".to_string()]);
}
#[test]
fn use_requires_an_existing_profile() {
let dir = tempdir().unwrap();
let base = dir.path();
assert!(
run(
base,
&ProfileCommand::Use {
name: "ghost".to_string(),
}
)
.is_err()
);
assert_eq!(layout::read_selection(base, GAME), None);
run(
base,
&ProfileCommand::Create {
name: "main".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Use {
name: "main".to_string(),
},
)
.unwrap();
assert_eq!(layout::read_selection(base, GAME), Some("main".to_string()));
}
#[test]
fn renaming_the_selected_profile_follows_the_selection() {
let dir = tempdir().unwrap();
let base = dir.path();
run(
base,
&ProfileCommand::Create {
name: "main".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Use {
name: "main".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Rename {
from: "main".to_string(),
to: "primary".to_string(),
},
)
.unwrap();
assert_eq!(
layout::read_selection(base, GAME),
Some("primary".to_string())
);
}
#[test]
fn deleting_the_selected_profile_clears_the_selection() {
let dir = tempdir().unwrap();
let base = dir.path();
run(
base,
&ProfileCommand::Create {
name: "main".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Create {
name: "other".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Use {
name: "main".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Delete {
name: "main".to_string(),
},
)
.unwrap();
assert_eq!(layout::read_selection(base, GAME), None);
run(
base,
&ProfileCommand::Use {
name: "other".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Create {
name: "third".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Delete {
name: "third".to_string(),
},
)
.unwrap();
assert_eq!(
layout::read_selection(base, GAME),
Some("other".to_string())
);
}
#[test]
fn clear_returns_to_game_dir_mode() {
let dir = tempdir().unwrap();
let base = dir.path();
run(
base,
&ProfileCommand::Create {
name: "main".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Use {
name: "main".to_string(),
},
)
.unwrap();
run(base, &ProfileCommand::Clear).unwrap();
assert_eq!(layout::read_selection(base, GAME), None);
}
#[test]
fn list_succeeds_with_no_profiles() {
let dir = tempdir().unwrap();
assert!(run(dir.path(), &ProfileCommand::List).is_ok());
}
#[test]
fn list_survives_a_profile_with_an_unparseable_mods_yml() {
let dir = tempdir().unwrap();
let base = dir.path();
run(
base,
&ProfileCommand::Create {
name: "good".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Create {
name: "bad".to_string(),
},
)
.unwrap();
std::fs::write(
layout::profile_dir(base, GAME, "bad").join("mods.yml"),
"not: [valid",
)
.unwrap();
assert!(run(base, &ProfileCommand::List).is_ok());
}
#[test]
fn list_marks_the_selected_profile() {
let dir = tempdir().unwrap();
let base = dir.path();
run(
base,
&ProfileCommand::Create {
name: "main".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Create {
name: "spare".to_string(),
},
)
.unwrap();
run(
base,
&ProfileCommand::Use {
name: "main".to_string(),
},
)
.unwrap();
assert_eq!(layout::read_selection(base, GAME).as_deref(), Some("main"));
assert!(run(base, &ProfileCommand::List).is_ok());
}
}