From c34a957a9d18fcbb5eb711b8ae11c551b10c99bd Mon Sep 17 00:00:00 2001 From: Niklas Kapelle Date: Thu, 26 Feb 2026 17:08:38 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + Cargo.lock | 92 +++++++++++ Cargo.toml | 8 + src/fomod.rs | 314 ++++++++++++++++++++++++++++++++++++ src/install_prompt.rs | 61 +++++++ src/linker.rs | 111 +++++++++++++ src/main.rs | 22 +++ src/mod_config_installer.rs | 244 ++++++++++++++++++++++++++++ 8 files changed, 854 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/fomod.rs create mode 100644 src/install_prompt.rs create mode 100644 src/linker.rs create mode 100644 src/main.rs create mode 100644 src/mod_config_installer.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a727c0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/data diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..27d9a44 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,92 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "fomod-manager" +version = "0.1.0" +dependencies = [ + "quick-xml", + "serde", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..833d2f3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fomod-manager" +version = "0.1.0" +edition = "2024" + +[dependencies] +quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] } +serde = { version = "1.0.228", features = ["derive"] } diff --git a/src/fomod.rs b/src/fomod.rs new file mode 100644 index 0000000..47c770d --- /dev/null +++ b/src/fomod.rs @@ -0,0 +1,314 @@ +// This file incorporates code from luctius/fomod: +// https://github.com/luctius/fomod/ +// Original license: MIT / Apache-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Info { + #[serde(rename = "Name")] + pub name: Option, + #[serde(rename = "Description")] + pub description: Option, + #[serde(rename = "Version")] + pub version: Option, + #[serde(rename = "Author")] + pub author: Option, + #[serde(rename = "Website")] + pub website: Option, + #[serde(rename = "CategoryId")] + pub category_id: Option, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Config { + #[serde(rename = "moduleName")] + pub module_name: String, + + #[serde(rename = "moduleImage")] + pub module_image: Option, + + #[serde(rename = "moduleDependencies")] + pub module_dependencies: Option, + + #[serde(rename = "requiredInstallFiles")] + pub required_install_files: Option, + + #[serde(rename = "installSteps")] + pub install_steps: Option, + + #[serde(rename = "conditionalFileInstalls")] + pub conditional_file_installs: Option, +} + +#[derive( + Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default, +)] +pub enum OrderEnum { + #[default] + Ascending, + Explicit, + Descending, +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluginTypeEnum { + Required, + Optional, + Recommended, + NotUsable, + CouldBeUsable, +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PluginType { + #[serde(rename = "@name")] + pub name: PluginTypeEnum, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PluginTypeDescriptor { + #[serde(rename = "$value")] + pub value: PluginTypeDescriptorEnum, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluginTypeDescriptorEnum { + #[serde(rename = "dependencyType")] + DependencyType(DependencyPluginType), + #[serde(rename = "type")] + PluginType(PluginType), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DependencyPluginType { + pub default_type: PluginType, + pub patterns: DependencyPatternList, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DependencyPatternList { + pub pattern: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DependencyPattern { + pub dependencies: CompositeDependency, + #[serde(rename = "type")] + pub typ: PluginType, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct StepList { + #[serde(rename = "@order", default)] + pub order: OrderEnum, + + #[serde(rename = "installStep")] + pub install_step: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InstallStep { + #[serde(rename = "@name")] + pub name: String, + + pub visible: Option, + + #[serde(rename = "optionalFileGroups")] + pub optional_file_groups: GroupList, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ModuleDependency { + #[serde(rename = "@operator")] + pub operator: DependencyOperator, + #[serde(rename = "$value")] + pub list: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CompositeDependency { + #[serde(rename = "fileDependency")] + File(FileDependency), + #[serde(rename = "flagDependency")] + Flag(FlagDependency), + #[serde(rename = "gameDependency")] + Game(VersionDependency), + #[serde(rename = "fommDependency")] + Fomm(VersionDependency), + #[serde(rename = "dependencies")] + Dependency(ModuleDependency), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FlagDependency { + #[serde(rename = "@flag")] + pub flag: String, + #[serde(rename = "@value")] + pub value: String, +} +impl From for FlagDependency { + fn from(scf: SetConditionFlag) -> Self { + Self { + flag: scf.name, + value: scf.flag_value, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VersionDependency { + #[serde(rename = "@version")] + pub version: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FileDependency { + #[serde(rename = "@file")] + pub file_name: String, + #[serde(rename = "@state")] + pub state: DependencyState, +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DependencyState { + Active, + Inactive, + Missing, +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DependencyOperator { + And, + Or, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FileList { + #[serde(rename = "$value")] + pub list: Option>, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FileTypeEnum { + #[serde(rename = "file")] + File(FileType), + #[serde(rename = "folder")] + Folder(FileType), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FileType { + #[serde(rename = "@source")] + pub source: String, + #[serde(rename = "@destination")] + pub destination: Option, + #[serde(rename = "@alwaysInstall")] + pub always_install: Option, + #[serde(rename = "@installIfUsable", default = "false_bool")] + pub install_if_usable: bool, + pub priority: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GroupList { + #[serde(rename = "@order", default)] + pub order: OrderEnum, + pub group: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Group { + #[serde(rename = "@name")] + pub name: String, + + #[serde(rename = "@type")] + pub typ: GroupType, + + pub plugins: PluginList, +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum GroupType { + SelectAtLeastOne, + SelectAtMostOne, + SelectExactlyOne, + SelectAll, + SelectAny, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PluginList { + #[serde(rename = "@order", default)] + pub order: OrderEnum, + + pub plugin: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Plugin { + #[serde(rename = "@name")] + pub name: String, + + pub description: String, + + pub image: Option, + + pub files: Option, + #[serde(rename = "conditionFlags")] + pub condition_flags: Option, + + #[serde(rename = "typeDescriptor")] + pub type_descriptor: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Image { + #[serde(rename = "@path")] + pub path: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HeaderImage { + #[serde(rename = "@path")] + pub path: Option, + #[serde(rename = "@showImage", default = "false_bool")] + pub show_image: bool, + #[serde(rename = "@showFade", default = "false_bool")] + pub show_fade: bool, + pub height: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConditionFlagList { + pub flag: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SetConditionFlag { + #[serde(rename = "@name")] + pub name: String, + + #[serde(rename = "$value")] + pub flag_value: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConditionalFileInstallList { + pub patterns: ConditionalInstallPatternList, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConditionalInstallPatternList { + pub pattern: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConditionalInstallPattern { + pub dependencies: CompositeDependency, + pub files: FileList, +} + +fn false_bool() -> bool { + false +} diff --git a/src/install_prompt.rs b/src/install_prompt.rs new file mode 100644 index 0000000..9fba8e8 --- /dev/null +++ b/src/install_prompt.rs @@ -0,0 +1,61 @@ +use std::io::{self, Write}; + +use crate::{fomod::GroupType, mod_config_installer::GroupPrompt}; + +pub fn prompt(p: GroupPrompt) -> Vec { + println!("=== {} ===", p.name); + println!(); + + for option in &p.options { + println!( + "[{}] {} ({:?}) => {}", + option.idx + 1, option.name, option.option_type, option.description + ); + } + + let instruction = match p.select_type { + GroupType::SelectExactlyOne => "Select one (enter number)", + GroupType::SelectAtLeastOne => "Select one or more (e.g. 1 2 3)", + GroupType::SelectAtMostOne => "Select one or none (enter number or 0 for none)", + GroupType::SelectAny => "Select any (e.g. 1 2 3, or 0 for none)", + GroupType::SelectAll => { + println!(" (all options required)"); + return (0..p.options.len()).collect(); + } + }; + + loop { + println!("=> {}", instruction); + io::stdout().flush().unwrap(); + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + input.trim().to_string(); + + let choices: Vec = input + .split_whitespace() + .filter_map(|s| s.parse::().ok()) + .filter(|&n| n > 0 && n <= p.options.len()) + .map(|n| n - 1) + .collect(); + + let valid = match p.select_type { + GroupType::SelectExactlyOne => { + choices.len() == 1 // TODO: Check not usable + } + GroupType::SelectAtLeastOne => { + !choices.is_empty() // TODO: Check not usable + } + GroupType::SelectAtMostOne => { + choices.len() <= 1 // TODO: Check not usable + } + GroupType::SelectAny => true, // TODO: Check selected not usable, + GroupType::SelectAll => unreachable!(), + }; + + if valid { + // TODO: Merge required plugins into final selection + let final_selection = choices; + return final_selection; + } + } +} diff --git a/src/linker.rs b/src/linker.rs new file mode 100644 index 0000000..e0b5397 --- /dev/null +++ b/src/linker.rs @@ -0,0 +1,111 @@ +use std::{ + fs, io, + os::unix, + path::{Path, PathBuf}, +}; + +pub struct Linker { + target: PathBuf, +} + +impl Linker { + pub fn new(target_path: &Path) -> Self { + Self { + target: target_path.to_owned(), + } + } + + #[inline] + fn install_path(&self) -> &Path { + &self.target + } + + fn link_file(&self, from: &Path, to: &Path) -> Result<(), LinkerError> { + let target = self.install_path().join(to); + + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + + unix::fs::symlink(from, target)?; + Ok(()) + } + + fn remove_link(&self, to: &Path) -> Result<(), LinkerError> { + let file = self.install_path().join(to); + let metadata = fs::symlink_metadata(&file)?; + + if !metadata.file_type().is_symlink() { + return Err(LinkerError::NotASymlink(file)); + } + + fs::remove_file(&file)?; + Ok(()) + } + + fn link_recursive(&self, from: &Path, to: &Path) -> Result<(), LinkerError> { + for entry in fs::read_dir(from)? { + let entry = entry?; + let entry_path = entry.path(); + let relative = entry_path + .strip_prefix(from) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + let target_path = to.join(relative); + + if entry_path.is_dir() { + self.link_recursive(&entry_path, &target_path)?; + } else { + self.link_file(&entry_path, &target_path)?; + } + } + + Ok(()) + } + + fn unlink_recursive(&self, from: &Path, to: &Path) -> Result<(), LinkerError> { + for entry in fs::read_dir(from)? { + let entry = entry?; + let entry_path = entry.path(); + let relative = entry_path + .strip_prefix(from) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + let target_path = to.join(relative); + + if entry_path.is_dir() { + self.unlink_recursive(&entry_path, &target_path)?; + + let install_target = self.install_path().join(&target_path); + if install_target.exists() && fs::read_dir(&install_target)?.next().is_none() { + fs::remove_dir(&install_target)?; + } + } else { + self.remove_link(&target_path)?; + } + } + + Ok(()) + } +} + +#[derive(Debug)] +pub enum LinkerError { + Io(io::Error), + NotASymlink(PathBuf), +} + +impl std::fmt::Display for LinkerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "IO error: {}", e), + Self::NotASymlink(path) => write!(f, "Tried to remove a non symlink: {:?}", path), + } + } +} + +impl std::error::Error for LinkerError {} + +impl From for LinkerError { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a7902c1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,22 @@ +use std::{error::Error, fs}; + +use crate::mod_config_installer::FomodInstaller; + +mod install_prompt; +mod mod_config_installer; +mod linker; +mod fomod; + +fn main() -> Result<(), Box> { + const XML_PATH: &str = "./data/xml/ineed.xml"; + + let xml = fs::read_to_string(XML_PATH)?; + + let config: fomod::Config = quick_xml::de::from_str(&xml)?; + + let installer = FomodInstaller::new(config, vec![], install_prompt::prompt); + + dbg!(installer.run()); + + Ok(()) +} diff --git a/src/mod_config_installer.rs b/src/mod_config_installer.rs new file mode 100644 index 0000000..19402bb --- /dev/null +++ b/src/mod_config_installer.rs @@ -0,0 +1,244 @@ +use std::{collections::HashMap, fmt::Display}; + +use crate::fomod::{ + CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum, + Group, GroupType, Plugin, PluginTypeDescriptorEnum, PluginTypeEnum, +}; + +#[derive(Debug)] +pub struct InstallerState { + /// Flags set by plugin selections throughout the install + flags: HashMap, + + /// Files to install, keyed by destination path. + /// Higher priority value wins when destinations conflict. + selected_files: Vec, +} + +impl InstallerState { + fn new() -> Self { + Self { + flags: HashMap::new(), + selected_files: Vec::new(), + } + } + + fn set_flag(&mut self, name: &str, value: &str) { + self.flags.insert(name.to_string(), value.to_string()); + } + + fn get_flag(&self, name: &str) -> Option<&str> { + self.flags.get(name).map(|s| s.as_str()) + } + + fn add_files(&mut self, files: &FileList) { + let Some(list) = &files.list else { + return; + }; + + self.selected_files.extend_from_slice(list); + } + + pub fn into_file_list(self) -> Vec { + self.selected_files + } +} + +fn evaluate_dependency( + dep: &CompositeDependency, + state: &InstallerState, + installed_plugins: &[String], +) -> bool { + match dep { + CompositeDependency::Flag(flag) => state + .get_flag(&flag.flag) + .map(|v| v == flag.value) + .unwrap_or(false), + + CompositeDependency::File(file) => { + let exists = installed_plugins + .iter() + .any(|p| p.eq_ignore_ascii_case(&file.file_name)); + + match file.state { + DependencyState::Active => exists, + DependencyState::Inactive => !exists, + DependencyState::Missing => !exists, + } + } + + CompositeDependency::Game(version) => { + // TODO: Check the game version + let _ = version; + true + } + + CompositeDependency::Fomm(version) => { + // TODO: Check the fomm version + let _ = version; + true + } + + CompositeDependency::Dependency(module) => { + let mut results = module + .list + .iter() + .map(|dep| evaluate_dependency(dep, state, installed_plugins)); + + match module.operator { + DependencyOperator::And => results.all(|r| r), + DependencyOperator::Or => results.any(|r| r), + } + } + } +} + +pub struct GroupPrompt { + pub name: String, + pub select_type: GroupType, + pub options: Vec, +} + +pub struct InstallOption { + pub name: String, + pub option_type: PluginTypeEnum, + pub idx: usize, + pub description: String, +} + +impl Display for InstallOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({:?})", self.name, self.option_type) + } +} + +fn create_group_prompt( + group: &Group, + state: &InstallerState, + installed_plugins: &[String], +) -> GroupPrompt { + let options = group + .plugins + .plugin + .iter() + .enumerate() + .map(|(idx, option)| InstallOption { + name: option.name.trim().to_owned(), + option_type: resolve_plugin_type(option, state, installed_plugins), + description: option.description.trim().to_owned(), + idx, + }) + .collect(); + + GroupPrompt { + name: group.name.clone(), + select_type: group.typ, + options, + } +} + +/// Each plugin in the install steps have a type. e.g.: Optional, Required, Recommended +/// But the type depends on dependencies +/// This function eveluates the plugin type +fn resolve_plugin_type( + plugin: &Plugin, + state: &InstallerState, + installed_plugins: &[String], +) -> PluginTypeEnum { + // Since the type_descriptor is optional return Optional if non is set + let Some(descriptor) = &plugin.type_descriptor else { + return PluginTypeEnum::Optional; + }; + + match &descriptor.value { + PluginTypeDescriptorEnum::PluginType(plugin_type) => plugin_type.name, + PluginTypeDescriptorEnum::DependencyType(dependency_plugin_type) => { + for dep in &dependency_plugin_type.patterns.pattern { + if evaluate_dependency(&dep.dependencies, state, installed_plugins) { + return dep.typ.name; + } + } + + // No dependencies was satisfied. Using default type + dependency_plugin_type.default_type.name + } + } +} + +pub struct FomodInstaller { + config: Config, + installed_plugins: Vec, + group_prompt: fn(GroupPrompt) -> Vec, +} + +impl FomodInstaller { + pub fn new( + config: Config, + installed_plugins: Vec, + group_promt: fn(GroupPrompt) -> Vec, + ) -> Self { + Self { + config, + installed_plugins, + group_prompt: group_promt, + } + } + + pub fn run(self) -> Vec { + let mut state = InstallerState::new(); + + // Always-installed files first + if let Some(required) = &self.config.required_install_files { + state.add_files(required); + } + + if let Some(install_steps) = &self.config.install_steps { + let steps = &install_steps.install_step; + + for (step_index, step) in steps.iter().enumerate() { + // Check if the step should be visible + if let Some(visible) = &step.visible { + if !evaluate_dependency(visible, &state, &self.installed_plugins) { + // Dependency to show the step not meet. Skipping. + continue; + } + } + + for group in &step.optional_file_groups.group { + // TODO: Skip groups where all plugins are NotUsable + + let prompt = create_group_prompt(group, &state, &self.installed_plugins); + + let selected_plugins = (self.group_prompt)(prompt); + + for i in selected_plugins { + let plugin = &group.plugins.plugin[i]; + + // Add files from selected plugin + if let Some(files) = &plugin.files { + state.add_files(files); + } + + // Set condition flags + if let Some(condition_flags) = &plugin.condition_flags { + for flag in &condition_flags.flag { + state.set_flag(&flag.name, &flag.flag_value); + } + } + } + } + } + } + + // Evaluate conditional file installs based on final flag state + if let Some(conditional) = &self.config.conditional_file_installs { + for pattern in &conditional.patterns.pattern { + if evaluate_dependency(&pattern.dependencies, &state, &self.installed_plugins) { + state.add_files(&pattern.files); + } + } + } + + state.into_file_list() + } +}