Compare commits

...

6 Commits

Author SHA1 Message Date
bdeebfee4f changed the way files get resolved on mod install
to better handle fomod and it's required interactivity
2026-03-30 22:07:46 +02:00
7e20cd370c removed callback function from fomod installer 2026-03-29 22:48:21 +02:00
ea50f4d59b improved mod list 2026-03-29 15:36:53 +02:00
49f38cb21a get mod game type 2026-03-29 15:36:35 +02:00
e70c6e6901 handle instance select in own state 2026-03-28 21:23:13 +01:00
93676901c0 moved block for mod_list out of app 2026-03-28 20:48:50 +01:00
10 changed files with 371 additions and 216 deletions

View File

@@ -7,5 +7,5 @@ mod load_order;
pub use activate::{ActivationError, activate_instance}; pub use activate::{ActivationError, activate_instance};
pub use download::handle_nxm; pub use download::handle_nxm;
pub use include::insert_mod_to_instance; pub use include::insert_mod_to_instance;
pub use install::resolve_files_for_install; pub use install::{resolve_files_for_install, ResolveFileResult};
pub use load_order::{LoadOrderError, create_loadorder}; pub use load_order::{LoadOrderError, create_loadorder};

View File

@@ -4,30 +4,34 @@ use std::{
}; };
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use log::{debug, trace};
use crate::{ use crate::{
fomod, install_prompt, fomod,
mod_config_installer::run_fomod_installer, types::{ModConfig, ModFile, RootConfig},
types::{ModConfig, ModFile, ModdedInstance, RootConfig},
utils::{resolve_case_insensitive, walk_all_files}, utils::{resolve_case_insensitive, walk_all_files},
}; };
pub fn resolve_files_for_install( pub fn resolve_files_for_install(
root_config: &RootConfig, root_config: &RootConfig,
instance: &ModdedInstance,
mod_to_install: &ModConfig, mod_to_install: &ModConfig,
) -> anyhow::Result<Vec<ModFile>> { ) -> anyhow::Result<ResolveFileResult> {
let mod_location = root_config.mod_location().join(mod_to_install.path()); let mod_location = root_config.mod_location().join(mod_to_install.path());
let files = match determain_mod_kind(mod_to_install, &mod_location)? { let result = match determain_mod_kind(mod_to_install, &mod_location)? {
ModKind::Fomod(xml_path) => install_fomod(instance, xml_path, &mod_location)?, ModKind::Fomod(xml_path) => {
ModKind::EmbeddedData(_data_path) => install_from_dir(mod_to_install, mod_location)?, let module_config = fomod::Config::load_from_file(xml_path)?;
ModKind::Root => install_root(mod_to_install, mod_location)?, ResolveFileResult::Fomod(module_config)
ModKind::Unkown => install_from_dir_to_data(mod_to_install, mod_location)?, }
ModKind::EmbeddedData(_data_path) => {
ResolveFileResult::Files(install_from_dir(mod_to_install, mod_location)?)
}
ModKind::Root => ResolveFileResult::Files(install_root(mod_to_install, mod_location)?),
ModKind::Unkown => {
ResolveFileResult::Files(install_from_dir_to_data(mod_to_install, mod_location)?)
}
}; };
Ok(files) Ok(result)
} }
fn determain_mod_kind( fn determain_mod_kind(
@@ -51,32 +55,38 @@ fn determain_mod_kind(
} }
} }
fn install_fomod( // fn install_fomod(
instance: &ModdedInstance, // instance: &ModdedInstance,
module_config_path: impl AsRef<Path>, // module_config_path: impl AsRef<Path>,
mod_root: impl AsRef<Path>, // mod_root: impl AsRef<Path>,
) -> anyhow::Result<Vec<ModFile>> { // ) -> anyhow::Result<Vec<ModFile>> {
debug!("Running FOmod installer"); // debug!("Running FOmod installer");
let module_config = fomod::Config::load_from_file(module_config_path)?; // let module_config = fomod::Config::load_from_file(module_config_path)?;
//
let active_plugins: Vec<_> = instance // let active_plugins: Vec<_> = instance
.active_plugins() // .active_plugins()
.map(|e| e.to_string_lossy()) // .map(|e| e.to_string_lossy())
.map(|e| e.to_string()) // .map(|e| e.to_string())
.collect(); // .collect();
//
trace!("Current loded plugins: {:?}", active_plugins); // trace!("Current loded plugins: {:?}", active_plugins);
let files = run_fomod_installer(module_config, &active_plugins, install_prompt::prompt)?; //
// let mut installer = FomodInstaller::new(&module_config, &active_plugins);
let mod_files: Vec<_> = files // let mut selection: Option<Vec<usize>> = None;
.iter() // while let Some(prompt) = installer.run_step(selection.as_deref()) {
.map(|f| ModFile::from_installer(f.clone(), &mod_root)) // selection = Some(install_prompt::prompt(prompt));
.collect::<Result<Vec<_>, _>>()? // }
.into_iter() // let files = installer.finalize();
.flatten() //
.collect(); // let mod_files: Vec<_> = files
Ok(mod_files) // .iter()
} // .map(|f| ModFile::from_installer(f.clone(), &mod_root))
// .collect::<Result<Vec<_>, _>>()?
// .into_iter()
// .flatten()
// .collect();
// Ok(mod_files)
// }
fn install_from_dir( fn install_from_dir(
mod_config: &ModConfig, mod_config: &ModConfig,
@@ -143,6 +153,11 @@ enum ModKind {
Unkown, Unkown,
} }
pub enum ResolveFileResult {
Files(Vec<ModFile>),
Fomod(fomod::Config),
}
fn should_be_included(path: impl AsRef<Path>) -> bool { fn should_be_included(path: impl AsRef<Path>) -> bool {
matches!( matches!(
path.as_ref().extension().and_then(|e| e.to_str()), path.as_ref().extension().and_then(|e| e.to_str()),

View File

@@ -5,9 +5,14 @@ use std::{error::Error, path::Path};
use fomod_manager::{ use fomod_manager::{
actions::{ actions::{
activate_instance, create_loadorder, handle_nxm, insert_mod_to_instance, ResolveFileResult, activate_instance, create_loadorder, handle_nxm, insert_mod_to_instance,
resolve_files_for_install, resolve_files_for_install,
}, cli::{self, Args}, nexus::NexusAPI, tui, types::RootConfig },
cli::{self, Args},
mod_config_installer::FomodInstaller,
nexus::NexusAPI,
tui,
types::RootConfig,
}; };
fn command_activate( fn command_activate(
@@ -26,7 +31,19 @@ fn command_add(root_config: &RootConfig, instance_id: &str, mod_id: &str) -> any
.mod_by_id(mod_id) .mod_by_id(mod_id)
.ok_or(anyhow!("Can't find mod in config"))?; .ok_or(anyhow!("Can't find mod in config"))?;
let files = resolve_files_for_install(root_config, &instance, &mod_to_install)?; let files = match resolve_files_for_install(root_config, &mod_to_install)? {
ResolveFileResult::Files(mod_files) => mod_files,
ResolveFileResult::Fomod(module_config) => {
let mod_location = root_config.mod_location().join(mod_to_install.path());
let active_plugins: Vec<String> = instance.active_plugins().collect();
let mut installer = FomodInstaller::new(&module_config, &active_plugins);
let mut selection: Option<Vec<usize>> = None;
while let Some(prompt) = installer.run_step(selection.as_deref()) {
selection = Some(fomod_manager::install_prompt::prompt(prompt));
}
installer.finalize(mod_location)?
}
};
match insert_mod_to_instance(&mut instance, &mod_to_install, &files, 0) { match insert_mod_to_instance(&mut instance, &mod_to_install, &files, 0) {
None => { None => {
@@ -101,7 +118,7 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
cli::Commands::Tui => { cli::Commands::Tui => {
tui::run(&mut root_config)?; tui::run(&mut root_config)?;
}, }
} }
Ok(()) Ok(())

View File

@@ -1,10 +1,13 @@
use std::{collections::HashMap, fmt::Display}; use std::{collections::HashMap, fmt::Display, io, path::Path};
use log::{debug, warn}; use log::{debug, warn};
use crate::fomod::{ use crate::{
CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum, fomod::{
Group, GroupType, ModuleDependency, Plugin, PluginTypeDescriptorEnum, PluginTypeEnum, CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum,
Group, GroupType, ModuleDependency, Plugin, PluginTypeDescriptorEnum, PluginTypeEnum,
},
types::ModFile,
}; };
#[derive(Debug)] #[derive(Debug)]
@@ -117,6 +120,7 @@ fn evaluate_module_depbendecy(
} }
} }
#[derive(Debug)]
pub struct GroupPrompt { pub struct GroupPrompt {
pub name: String, pub name: String,
pub select_type: GroupType, pub select_type: GroupType,
@@ -146,6 +150,7 @@ impl GroupPrompt {
} }
} }
#[derive(Debug)]
pub struct InstallOption { pub struct InstallOption {
pub name: String, pub name: String,
pub option_type: PluginTypeEnum, pub option_type: PluginTypeEnum,
@@ -191,66 +196,90 @@ fn resolve_plugin_type(
} }
} }
pub fn run_fomod_installer( pub struct FomodInstaller<'a> {
fomod_config: Config, state: InstallerState,
installed_plugins: &[String], current_step: (usize, usize),
group_prompt: fn(GroupPrompt) -> Vec<usize>, config: &'a Config,
) -> anyhow::Result<Vec<FileTypeEnum>> { installed_plugins: &'a [String],
let mut state = InstallerState::new(); }
// Always-installed files first impl<'a> FomodInstaller<'a> {
if let Some(required) = &fomod_config.required_install_files { pub fn new(fomod_config: &'a Config, installed_plugins: &'a [String]) -> Self {
state.add_files(required); let mut state = InstallerState::new();
if let Some(required) = &fomod_config.required_install_files {
state.add_files(required);
}
Self {
state,
current_step: (0, 0),
config: fomod_config,
installed_plugins,
}
} }
if let Some(install_steps) = fomod_config.install_steps { pub fn run_step(&mut self, selection: Option<&[usize]>) -> Option<GroupPrompt> {
let steps = &install_steps.install_step; let Some(install_steps) = &self.config.install_steps else {
return None;
};
for step in steps { let step = install_steps.install_step.get(self.current_step.0)?;
// Check if the step should be visible
if step
.visible
.as_ref()
.is_some_and(|v| !evaluate_module_depbendecy(v, &state, installed_plugins))
{
// Dependency to show the step not meet. Skipping.
continue;
}
for group in &step.optional_file_groups.group { // Check if the step should be visible
// TODO: Skip groups where all plugins are NotUsable if step
.visible
.as_ref()
.is_some_and(|v| !evaluate_module_depbendecy(v, &self.state, self.installed_plugins))
{
// Dependency to show the step not meet. Skipping.
self.current_step = (self.current_step.0 + 1, 0);
return self.run_step(selection);
}
let prompt = GroupPrompt::new(group, &state, installed_plugins); let Some(group) = step.optional_file_groups.group.get(self.current_step.1) else {
self.current_step = (self.current_step.0 + 1, 0);
return self.run_step(selection);
};
let selected_plugins = (group_prompt)(prompt); // TODO: Skip groups where all plugins are NotUsable
match selection {
Some(selected_plugins) => {
for i in selected_plugins { for i in selected_plugins {
let plugin = &group.plugins.plugin[i]; let plugin = &group.plugins.plugin[*i];
// Add files from selected plugin // Add files from selected plugin
if let Some(files) = &plugin.files { if let Some(files) = &plugin.files {
state.add_files(files); self.state.add_files(files);
} }
// Set condition flags // Set condition flags
if let Some(condition_flags) = &plugin.condition_flags { if let Some(condition_flags) = &plugin.condition_flags {
for flag in &condition_flags.flag { for flag in &condition_flags.flag {
state.set_flag(&flag.name, &flag.flag_value); self.state.set_flag(&flag.name, &flag.flag_value);
} }
} }
} }
// Next step
self.current_step = (self.current_step.0, self.current_step.1 + 1);
self.run_step(None)
} }
None => Some(GroupPrompt::new(group, &self.state, self.installed_plugins)),
} }
} }
// Evaluate conditional file installs based on final flag state pub fn finalize(self, mod_root: impl AsRef<Path>) -> Result<Vec<ModFile>, io::Error> {
if let Some(conditional) = &fomod_config.conditional_file_installs { let files: Vec<_> = self
for pattern in &conditional.patterns.pattern { .state
if evaluate_module_depbendecy(&pattern.dependencies, &state, installed_plugins) { .into_file_list()
state.add_files(&pattern.files); .iter()
} .map(|f| ModFile::from_installer(f.clone(), &mod_root))
} .collect::<Result<Vec<_>, _>>()?
} .into_iter()
.flatten()
.collect();
Ok(state.into_file_list()) Ok(files)
}
} }

View File

@@ -1,16 +1,20 @@
use std::io; use std::io;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use log::error;
use ratatui::{ use ratatui::{
DefaultTerminal, Frame, DefaultTerminal, Frame,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::Style, widgets::{StatefulWidget, Widget},
widgets::{Block, Borders, StatefulWidget, TableState, Widget},
}; };
use crate::{ use crate::{
tui::{instance::InstanceSelect, mod_list::ModList, status::StatusBar}, tui::{
types::RootConfig, instance::{InstanceSelect, InstanceSelectState},
mod_list::{ModList, ModListState},
status::StatusBar,
},
types::{ModdedInstance, RootConfig},
}; };
pub fn run(root_config: &mut RootConfig) -> anyhow::Result<()> { pub fn run(root_config: &mut RootConfig) -> anyhow::Result<()> {
@@ -21,22 +25,27 @@ pub fn run(root_config: &mut RootConfig) -> anyhow::Result<()> {
#[derive(Debug)] #[derive(Debug)]
struct App<'a> { struct App<'a> {
root_config: &'a mut RootConfig, root_config: &'a mut RootConfig,
loaded_instance: Option<ModdedInstance>,
exit: bool, exit: bool,
mod_list_state: TableState, mod_list_state: ModListState,
selected_instance: Option<String>, selected_instance_state: InstanceSelectState,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
fn new(root_config: &'a mut RootConfig) -> Self { fn new(root_config: &'a mut RootConfig) -> Self {
let mut state = TableState::default(); let mut mod_list_state = ModListState::new();
state.select(Some(0)); // select first row by default mod_list_state.update_list(root_config, None);
Self { Self {
root_config, root_config,
mod_list_state: state,
selected_instance: None, loaded_instance: None,
exit: false, exit: false,
mod_list_state,
selected_instance_state: InstanceSelectState::new(),
} }
} }
@@ -67,16 +76,18 @@ impl<'a> App<'a> {
match key_event.code { match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => self.exit(), KeyCode::Esc | KeyCode::Char('q') => self.exit(),
KeyCode::Up | KeyCode::Char('k') => { KeyCode::Up | KeyCode::Char('k') => {
self.mod_list_state.select_previous(); self.mod_list_state.select_prev();
} }
KeyCode::Down | KeyCode::Char('j') => { KeyCode::Down | KeyCode::Char('j') => {
self.mod_list_state.select_next(); self.mod_list_state.select_next();
} }
KeyCode::Right | KeyCode::Char('l') => { KeyCode::Right | KeyCode::Char('l') => {
self.next_instance(); self.selected_instance_state.next_instance(self.root_config);
self.load_instance();
} }
KeyCode::Left | KeyCode::Char('h') => { KeyCode::Left | KeyCode::Char('h') => {
self.prev_instance(); self.selected_instance_state.prev_instance(self.root_config);
self.load_instance();
} }
_ => {} _ => {}
} }
@@ -86,56 +97,22 @@ impl<'a> App<'a> {
self.exit = true; self.exit = true;
} }
fn next_instance(&mut self) { fn load_instance(&mut self) {
let mut instances = self.root_config.instances(); let Some(selected) = self.selected_instance_state.instance() else {
instances.sort();
if instances.is_empty() {
self.selected_instance = None;
return; return;
}
let next = match &self.selected_instance {
None => instances.first().cloned(),
Some(curr) => {
let idx = instances.iter().position(|x| x == curr);
match idx {
Some(i) => {
let next_index = (i + 1) % instances.len();
instances.get(next_index).cloned()
}
None => instances.first().cloned(),
}
}
}; };
self.selected_instance = next; match self.root_config.load_instance_by_id(selected) {
} Ok(instance) => {
self.loaded_instance = Some(instance);
fn prev_instance(&mut self) { self.mod_list_state
let mut instances = self.root_config.instances(); .update_list(self.root_config, self.loaded_instance.as_ref());
instances.sort();
if instances.is_empty() {
self.selected_instance = None;
return;
}
let prev = match &self.selected_instance {
None => instances.last().cloned(),
Some(curr) => {
let idx = instances.iter().position(|x| x == curr);
match idx {
Some(i) => {
let prev_index = if i == 0 { instances.len() - 1 } else { i - 1 };
instances.get(prev_index).cloned()
}
None => Some(instances[instances.len() - 1].clone()),
}
} }
}; Err(err) => {
error!("Failed to load instance: {err}");
self.selected_instance = prev; self.exit();
}
}
} }
} }
@@ -149,23 +126,12 @@ impl<'a> Widget for &mut App<'a> {
.constraints([ .constraints([
Constraint::Length(3), Constraint::Length(3),
Constraint::Min(1), Constraint::Min(1),
Constraint::Length(1), // single line for keybindings Constraint::Length(1),
]) ])
.split(area); .split(area);
InstanceSelect::new(self.selected_instance.clone()).render(chunks[0], buf); InstanceSelect.render(chunks[0], buf, &mut self.selected_instance_state);
ModList.render(chunks[1], buf, &mut self.mod_list_state);
let list_block = Block::default()
.title("Mod list")
.borders(Borders::ALL)
.style(Style::default());
ModList::new(self.root_config).block(list_block).render(
chunks[1],
buf,
&mut self.mod_list_state,
);
StatusBar.render(chunks[2], buf); StatusBar.render(chunks[2], buf);
} }
} }

View File

@@ -1,29 +1,91 @@
use ratatui::{ use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style, style::Style,
widgets::{Block, Borders, Paragraph, Widget}, widgets::{Block, Borders, Paragraph, StatefulWidget, Widget},
}; };
pub struct InstanceSelect { use crate::types::RootConfig;
#[derive(Debug)]
pub struct InstanceSelectState {
selected: Option<String>, selected: Option<String>,
} }
impl InstanceSelect { impl InstanceSelectState {
pub fn new(selected: Option<String>) -> Self { pub fn new() -> Self {
Self { selected } Self { selected: None }
}
pub fn instance(&self) -> Option<&str> {
self.selected.as_deref()
}
pub fn next_instance(&mut self, root_config: &RootConfig) {
let mut instances = root_config.instances();
instances.sort();
if instances.is_empty() {
self.selected = None;
return;
}
let next = match &self.selected {
None => instances.first().cloned(),
Some(curr) => {
let idx = instances.iter().position(|x| x == curr);
match idx {
Some(i) => {
let next_index = (i + 1) % instances.len();
instances.get(next_index).cloned()
}
None => instances.first().cloned(),
}
}
};
self.selected = next;
}
pub fn prev_instance(&mut self, root_config: &RootConfig) {
let mut instances = root_config.instances();
instances.sort();
if instances.is_empty() {
self.selected = None;
return;
}
let prev = match &self.selected {
None => instances.last().cloned(),
Some(curr) => {
let idx = instances.iter().position(|x| x == curr);
match idx {
Some(i) => {
let prev_index = if i == 0 { instances.len() - 1 } else { i - 1 };
instances.get(prev_index).cloned()
}
None => Some(instances[instances.len() - 1].clone()),
}
}
};
self.selected = prev;
} }
} }
impl Widget for InstanceSelect { pub struct InstanceSelect;
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where impl StatefulWidget for InstanceSelect {
Self: Sized, type State = InstanceSelectState;
{
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let list_block = Block::default() let list_block = Block::default()
.title("Instance") .title("Instance")
.borders(Borders::ALL) .borders(Borders::ALL)
.style(Style::default()); .style(Style::default());
Paragraph::new(self.selected.unwrap_or("None".to_owned())) Paragraph::new(state.selected.clone().unwrap_or("None".to_owned()))
.block(list_block) .block(list_block)
.render(area, buf); .render(area, buf);
} }

View File

@@ -1,74 +1,114 @@
use std::collections::HashSet;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Rect}, layout::{Constraint, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Cell, Row, StatefulWidget, Table, TableState}, widgets::{Block, Borders, Cell, Row, StatefulWidget, Table, TableState},
}; };
use crate::types::{ModConfig, RootConfig}; use crate::types::{ModConfig, ModdedInstance, RootConfig};
#[derive(Debug)] #[derive(Debug)]
pub struct ListItem<'a> { pub struct ModListState {
mod_config: &'a ModConfig, table_state: TableState,
id: &'a str, items: Vec<ListItem>,
} }
#[derive(Debug)] impl ModListState {
pub struct ModList<'a> { pub fn new() -> Self {
items: Vec<ListItem<'a>>, Self {
block: Option<Block<'a>>, table_state: TableState::new(),
} items: Vec::new(),
}
}
pub fn select_next(&mut self) {
self.table_state.select_next();
}
pub fn select_prev(&mut self) {
self.table_state.select_previous();
}
pub fn update_list(
&mut self,
root_config: &RootConfig,
loaded_instance: Option<&ModdedInstance>,
) {
let instance_game_type = loaded_instance
.and_then(|e| root_config.game_by_id(e.game_id()))
.map(|e| e.game_type());
let included_ids: Option<HashSet<_>> =
loaded_instance.map(|instance| instance.mods().iter().map(|m| m.mod_id()).collect());
impl<'a> ModList<'a> {
pub fn new(root_config: &'a RootConfig) -> Self {
let mut items: Vec<_> = root_config let mut items: Vec<_> = root_config
.mods() .mods()
.iter() .iter()
.filter(|e| instance_game_type.clone().is_none_or(|gt| e.1.game() == gt))
.map(|(id, config)| ListItem { .map(|(id, config)| ListItem {
id, id: id.to_owned(),
mod_config: config, mod_config: config.clone(),
included: included_ids
.as_ref()
.is_some_and(|set| set.contains(id.as_str())),
}) })
.collect(); .collect();
items.sort_by_key(|item| item.id); items.sort_by_key(|item| item.id.clone());
Self { items, block: None }
}
pub fn block(mut self, block: Block<'a>) -> Self { self.items = items;
self.block = Some(block);
self
} }
} }
impl<'a> StatefulWidget for ModList<'a> { #[derive(Debug)]
type State = TableState; struct ListItem {
mod_config: ModConfig,
id: String,
included: bool,
}
#[derive(Debug)]
pub struct ModList;
impl StatefulWidget for ModList {
type State = ModListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let rows: Vec<Row> = self let block = Block::default()
.title("Mod list")
.borders(Borders::ALL)
.style(Style::default());
let rows: Vec<Row> = state
.items .items
.iter() .iter()
.map(|item| { .map(|item| {
Row::new(vec![ Row::new(vec![
Cell::from(item.mod_config.name().unwrap_or(item.id)), Cell::from(item.mod_config.name().unwrap_or(&item.id)),
Cell::from(item.id), Cell::from(item.id.as_str()),
Cell::from(if item.included { "" } else { "" }),
]) ])
}) })
.collect(); .collect();
let table = Table::new(rows, [Constraint::Fill(1), Constraint::Fill(1)]) let table = Table::new(
.row_highlight_style( rows,
Style::default() [
.fg(Color::Yellow) Constraint::Fill(1),
.bg(Color::DarkGray) Constraint::Fill(1),
.add_modifier(Modifier::BOLD), Constraint::Fill(1),
) ],
.highlight_symbol(">> "); )
.row_highlight_style(
Style::default()
.fg(Color::Yellow)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
)
.block(block);
let table = match self.block { StatefulWidget::render(table, area, buf, &mut state.table_state);
Some(b) => table.block(b),
None => table,
};
StatefulWidget::render(table, area, buf, state);
} }
} }

View File

@@ -93,6 +93,10 @@ impl ModConfig {
pub fn nexus_id(&self) -> Option<&NexusID> { pub fn nexus_id(&self) -> Option<&NexusID> {
self.nexus_id.as_ref() self.nexus_id.as_ref()
} }
pub fn game(&self) -> GameType {
self.game.clone()
}
} }
fn is_false(b: &bool) -> bool { fn is_false(b: &bool) -> bool {

View File

@@ -1,5 +1,4 @@
use std::{ use std::{
ffi::OsStr,
fs::{self, read_to_string}, fs::{self, read_to_string},
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -106,8 +105,12 @@ impl ModdedInstance {
&self.mods &self.mods
} }
pub fn active_plugins(&self) -> impl Iterator<Item = &OsStr> { pub fn active_plugins(&self) -> impl Iterator<Item = String> {
self.mods.iter().flat_map(|e| e.active_plugins()) self.mods
.iter()
.flat_map(|e| e.active_plugins())
.map(|e| e.to_string_lossy())
.map(|e| e.to_string())
} }
} }
@@ -156,6 +159,5 @@ mod tests {
let new_mod = InstalledMod::new("mod1", 1); let new_mod = InstalledMod::new("mod1", 1);
cfg.update_or_create_mod(&new_mod); cfg.update_or_create_mod(&new_mod);
} }
} }

View File

@@ -20,7 +20,12 @@ fn add_plain() -> Result<(), Box<dyn Error>> {
let mod_to_install = root_config let mod_to_install = root_config
.mod_by_id("add_test_plain") .mod_by_id("add_test_plain")
.expect("Mod not found"); .expect("Mod not found");
let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; let files_to_add = match resolve_files_for_install(&root_config, &mod_to_install)? {
fomod_manager::actions::ResolveFileResult::Files(mod_files) => mod_files,
_ => {
panic!("Resolved files have wrong type");
}
};
insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0);
@@ -53,7 +58,12 @@ fn add_nested() -> Result<(), Box<dyn Error>> {
let mod_to_install = root_config let mod_to_install = root_config
.mod_by_id("add_test_nested") .mod_by_id("add_test_nested")
.expect("Mod not found"); .expect("Mod not found");
let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; let files_to_add = match resolve_files_for_install(&root_config, &mod_to_install)? {
fomod_manager::actions::ResolveFileResult::Files(mod_files) => mod_files,
_ => {
panic!("Resolved files have wrong type");
}
};
insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0);
@@ -86,7 +96,12 @@ fn add_root() -> Result<(), Box<dyn Error>> {
let mod_to_install = root_config let mod_to_install = root_config
.mod_by_id("add_test_root") .mod_by_id("add_test_root")
.expect("Mod not found"); .expect("Mod not found");
let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; let files_to_add = match resolve_files_for_install(&root_config, &mod_to_install)? {
fomod_manager::actions::ResolveFileResult::Files(mod_files) => mod_files,
_ => {
panic!("Resolved files have wrong type");
}
};
insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0);
@@ -117,7 +132,12 @@ fn add_filter() -> Result<(), Box<dyn Error>> {
let mod_to_install = root_config let mod_to_install = root_config
.mod_by_id("add_test_filter") .mod_by_id("add_test_filter")
.expect("Mod not found"); .expect("Mod not found");
let files_to_add = resolve_files_for_install(&root_config, &instance, &mod_to_install)?; let files_to_add = match resolve_files_for_install(&root_config, &mod_to_install)? {
fomod_manager::actions::ResolveFileResult::Files(mod_files) => mod_files,
_ => {
panic!("Resolved files have wrong type");
}
};
insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0); insert_mod_to_instance(&mut instance, &mod_to_install, &files_to_add, 0);