Skip to content

Commit dfd4d63

Browse files
feat: optionally install northstar core mods and DLL when creating a profile
Squashed commit of the following: commit bba4ba1 Author: Emerald <emerald@mecha.garden> Date: Sun Mar 23 23:57:20 2025 -0400 polish northstar profile install ux commit 078927f Author: emerald <emerald_actual@proton.me> Date: Sat Mar 22 18:19:33 2025 -0400 install minimum profile files
1 parent 6bb0cd3 commit dfd4d63

12 files changed

+200
-32
lines changed

Cargo.lock

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ tracing-appender = "0.2.3"
6464
clap_lex = "0.7.4"
6565
which = "7.0.2"
6666
steamlocate = { git = "https://github.com/WilliamVenner/steamlocate-rs", version = "2.0.1" }
67+
semver = { version = "1.0.26", features = ["serde"] }
6768
# rustyline = {version = "10.1.0", default_features = false}
6869

6970
[package.metadata.wix]

src/core/commands/install.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use anyhow::{anyhow, Result};
1+
use anyhow::{Result, anyhow};
22
use thermite::model::ModVersion;
33
use tracing::warn;
44

55
use crate::get_answer;
66
use crate::model::ModName;
7-
use crate::traits::{Answer, Indexed};
7+
use crate::traits::{Answer, Index};
88
use crate::utils::{download_and_install, to_file_size_string};
99

1010
use owo_colors::OwoColorize;
@@ -26,7 +26,7 @@ pub fn install(mods: Vec<ModName>, assume_yes: bool, force: bool, no_cache: bool
2626
}
2727
if let Some(m) = remote_index.get_item(&mn) {
2828
if let Some(version) = &mn.version {
29-
let Some(mv) = m.get_version(version) else {
29+
let Some(mv) = m.get_version(version.to_string()) else {
3030
println!(
3131
"Package {} has no version {}",
3232
format!("{}.{}", mn.author, mn.name).bright_cyan(),

src/core/commands/northstar.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::time::Duration;
44

55
use crate::config::{DIRS, InstallType, SteamType};
66
use crate::model::Cache;
7-
use crate::traits::{Answer, Indexed};
7+
use crate::traits::{Answer, Index};
88
use crate::utils::{ensure_dir, init_msg};
99
use crate::{NstarCommands, config::CONFIG, model::ModName};
1010
use crate::{get_answer, modfile};

src/core/commands/profile.rs

+76-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
use std::{
22
ffi::OsString,
3-
fs,
3+
fs::{self, File},
44
io::{ErrorKind, IsTerminal, Write},
55
path::{Path, PathBuf},
6+
time::Duration,
67
};
78

89
use anyhow::{Result, anyhow};
9-
use clap::{Subcommand, ValueHint};
10+
use clap::{Args, Subcommand, ValueHint};
1011
use clap_complete::ArgValueCompleter;
1112
use copy_dir::copy_dir;
13+
use indicatif::{ProgressBar, ProgressStyle};
1214
use owo_colors::OwoColorize;
15+
use semver::Version;
16+
use thermite::core::manage::install_northstar_profile;
1317

14-
use crate::{config::CONFIG, update_cfg, utils::init_msg};
18+
use crate::{
19+
config::{CONFIG, DIRS},
20+
get_answer,
21+
model::{Cache, ModName},
22+
traits::Answer,
23+
update_cfg,
24+
utils::{download_northstar, init_msg},
25+
};
1526

1627
#[derive(Subcommand)]
1728
pub enum ProfileCommands {
@@ -41,9 +52,9 @@ pub enum ProfileCommands {
4152
///Name of the profile to create
4253
#[clap(value_hint = ValueHint::DirPath)]
4354
name: OsString,
44-
///Remove any existing folder of the same name
45-
#[arg(long, short)]
46-
force: bool,
55+
56+
#[command(flatten)]
57+
options: NewOptions,
4758
},
4859

4960
#[clap(alias = "dupe", alias = "cp", alias = "copy")]
@@ -58,10 +69,28 @@ pub enum ProfileCommands {
5869
},
5970
}
6071

61-
pub fn handle(command: &ProfileCommands) -> Result<()> {
72+
#[derive(Args, Clone)]
73+
pub struct NewOptions {
74+
///Don't inlcude Norhtstar core files and mods
75+
#[arg(long, short)]
76+
empty: bool,
77+
///Remove any existing folder of the same name
78+
#[arg(long, short)]
79+
force: bool,
80+
///Answer "yes" to any prompts
81+
#[arg(long, short)]
82+
yes: bool,
83+
///The version of Northstar to use when for this profile
84+
///
85+
/// Leave unset for latest
86+
#[arg(long, short, conflicts_with = "empty")]
87+
version: Option<Version>,
88+
}
89+
90+
pub fn handle(command: &ProfileCommands, no_cache: bool) -> Result<()> {
6291
match command {
6392
ProfileCommands::List => list_profiles(),
64-
ProfileCommands::New { name, force } => new_profile(name, *force),
93+
ProfileCommands::New { name, options } => new_profile(name, options.clone(), no_cache),
6594
ProfileCommands::Clone { source, new, force } => clone_profile(source, new, *force),
6695
ProfileCommands::Select { name } => activate_profile(name),
6796
ProfileCommands::Ignore { name } => {
@@ -173,14 +202,14 @@ fn list_profiles() -> Result<()> {
173202
Ok(())
174203
}
175204

176-
fn new_profile(name: &OsString, force: bool) -> Result<()> {
205+
fn new_profile(name: &OsString, options: NewOptions, no_cache: bool) -> Result<()> {
177206
let Some(dir) = CONFIG.game_dir() else {
178207
return Err(init_msg());
179208
};
180209

181210
let prof = dir.join(name);
182211
if prof.try_exists()? {
183-
if force {
212+
if options.force {
184213
fs::remove_dir_all(&prof)?;
185214
} else {
186215
println!("A folder of that name already exists, remove it first");
@@ -189,7 +218,43 @@ fn new_profile(name: &OsString, force: bool) -> Result<()> {
189218
}
190219
fs::create_dir(&prof)?;
191220

192-
println!("Created profile {:?}", name.bright_cyan());
221+
if !options.empty {
222+
let nsname = ModName::new("northstar", "Northstar", options.version.clone());
223+
let cache = Cache::from_dir(DIRS.cache_dir())?;
224+
let file = if !no_cache
225+
&& let Some(nstar) = if options.version.is_some() {
226+
dbg!(cache.get(nsname))
227+
} else {
228+
cache.get_any(nsname)
229+
} {
230+
File::open(nstar)?
231+
} else {
232+
let ans = if let Some(version) = options.version.as_ref() {
233+
get_answer!(options.yes, "Download Northstar {}? [Y/n] ", version)?
234+
} else {
235+
get_answer!(options.yes, "Download latest Northstar? [Y/n] ")?
236+
};
237+
238+
if ans.is_no() {
239+
println!("Not downloading Northstar, aborting");
240+
return Ok(());
241+
} else {
242+
download_northstar(options.version)?
243+
}
244+
};
245+
246+
let bar = ProgressBar::new_spinner()
247+
.with_style(
248+
ProgressStyle::with_template("{prefix}{spinner:.cyan}")?
249+
.tick_strings(&[" ", ". ", ".. ", "...", " "]),
250+
)
251+
.with_prefix("Installing Northstar core files");
252+
bar.enable_steady_tick(Duration::from_millis(500));
253+
install_northstar_profile(file, prof)?;
254+
bar.finish();
255+
}
256+
257+
println!("Created profile {}", name.display().bright_cyan());
193258

194259
Ok(())
195260
}

src/core/commands/search.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::traits::Indexed;
1+
use crate::traits::Index;
22
use anyhow::Result;
33
use owo_colors::OwoColorize;
44
use textwrap::Options;

src/core/commands/update.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
core::commands::northstar,
66
get_answer,
77
model::ModName,
8-
traits::{Answer, Indexed},
8+
traits::{Answer, Index},
99
utils::{download_and_install, to_file_size_string},
1010
};
1111
use anyhow::Result;

src/macros.rs

+6
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,19 @@ macro_rules! get_answer {
6060
($yes:expr) => {
6161
get_answer!($yes, "OK? [Y/n]: ")
6262
};
63+
(yes:expr, $msg:literal) => {
64+
get_answer!($yes, format!($msg))
65+
};
6366
($yes:expr, $msg:expr) => {
6467
if $yes {
6568
Ok(String::new())
6669
} else {
6770
$crate::readln!($msg)
6871
}
6972
};
73+
($yes:expr, $msg:literal, $($arg:expr),*) => {
74+
get_answer!($yes, format!($msg, $($arg,)*))
75+
}
7076
}
7177

7278
#[macro_export]

src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ fn main() -> ExitCode {
387387
no_wait,
388388
args: extra_args,
389389
} => core::run(no_profile, no_wait, extra_args),
390-
Commands::Profile { command } => profile::handle(&command),
390+
Commands::Profile { command } => profile::handle(&command, cli.no_cache),
391391
};
392392

393393
if let Err(e) = res {

src/model.rs

+31-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use std::{
55
path::{Path, PathBuf},
66
};
77

8-
use anyhow::{anyhow, Result};
9-
use thermite::model::{InstalledMod, Manifest, Mod};
8+
use anyhow::{Result, anyhow};
9+
use semver::Version;
10+
use thermite::model::{InstalledMod, Manifest, Mod, ModVersion};
1011
use tracing::{debug, warn};
1112

1213
use crate::utils::validate_modname;
@@ -21,14 +22,14 @@ pub struct Package {
2122
pub struct ModName {
2223
pub author: String,
2324
pub name: String,
24-
pub version: Option<String>,
25+
pub version: Option<Version>,
2526
}
2627

2728
impl ModName {
2829
pub fn new(
2930
author: impl Into<String>,
3031
name: impl Into<String>,
31-
version: Option<String>,
32+
version: Option<Version>,
3233
) -> Self {
3334
Self {
3435
author: author.into(),
@@ -62,7 +63,7 @@ impl From<InstalledMod> for ModName {
6263
Self {
6364
author: value.author,
6465
name: value.manifest.name,
65-
version: Some(value.manifest.version_number),
66+
version: value.manifest.version_number.parse().ok(),
6667
}
6768
}
6869
}
@@ -72,7 +73,7 @@ impl From<&InstalledMod> for ModName {
7273
Self {
7374
author: value.author.clone(),
7475
name: value.manifest.name.clone(),
75-
version: Some(value.manifest.version_number.clone()),
76+
version: value.manifest.version_number.parse().ok(),
7677
}
7778
}
7879
}
@@ -82,7 +83,7 @@ impl From<Mod> for ModName {
8283
Self {
8384
author: value.author,
8485
name: value.name,
85-
version: Some(value.latest),
86+
version: value.latest.parse().ok(),
8687
}
8788
}
8889
}
@@ -92,7 +93,7 @@ impl From<&Mod> for ModName {
9293
Self {
9394
author: value.author.clone(),
9495
name: value.name.clone(),
95-
version: Some(value.latest.clone()),
96+
version: value.latest.parse().ok(),
9697
}
9798
}
9899
}
@@ -204,8 +205,30 @@ impl Cache {
204205
self.root.join(format!("{name}"))
205206
}
206207

208+
pub fn get_any(&self, name: impl AsRef<ModName>) -> Option<&PathBuf> {
209+
let name = name.as_ref();
210+
let mut keys = self
211+
.packages
212+
.keys()
213+
.filter(|k| {
214+
k.author.to_lowercase() == name.author.to_lowercase()
215+
&& k.name.to_lowercase() == name.name.to_lowercase()
216+
})
217+
.collect::<Vec<_>>();
218+
219+
keys.sort_by(|a, b| {
220+
a.version
221+
.as_ref()
222+
.and_then(|av| b.version.as_ref().map(|bv| av.cmp(bv)))
223+
.unwrap_or(std::cmp::Ordering::Equal)
224+
});
225+
226+
self.packages.get(keys.first()?)
227+
}
228+
207229
#[inline]
208230
pub fn get(&self, name: impl AsRef<ModName>) -> Option<&PathBuf> {
231+
dbg!(&self.packages);
209232
self.packages.get(name.as_ref())
210233
}
211234

src/traits.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ pub trait Answer {
1212
fn is_yes(&self) -> bool;
1313
}
1414

15-
pub trait Indexed<T> {
15+
pub trait Index<T> {
1616
fn get_item(&self, name: &ModName) -> Option<&T>;
1717
fn search(&self, term: &str) -> Vec<&T>;
1818
}
1919

20-
impl Indexed<Mod> for Vec<Mod> {
20+
impl Index<Mod> for Vec<Mod> {
2121
fn get_item(&self, name: &ModName) -> Option<&Mod> {
2222
self.iter().find(|v| {
2323
v.name.to_lowercase() == name.name.to_lowercase()
@@ -59,7 +59,7 @@ impl Indexed<Mod> for Vec<Mod> {
5959
}
6060
}
6161

62-
impl Indexed<InstalledMod> for Vec<InstalledMod> {
62+
impl Index<InstalledMod> for Vec<InstalledMod> {
6363
fn get_item(&self, name: &ModName) -> Option<&InstalledMod> {
6464
self.iter()
6565
.find(|v| v.mod_json.name.to_lowercase() == name.name.to_lowercase())

0 commit comments

Comments
 (0)