Compare commits
9 Commits
master
...
feature/tu
| Author | SHA1 | Date | |
|---|---|---|---|
|
bdeebfee4f
|
|||
|
7e20cd370c
|
|||
|
ea50f4d59b
|
|||
|
49f38cb21a
|
|||
|
e70c6e6901
|
|||
|
93676901c0
|
|||
|
2322cd00d2
|
|||
|
d746e830fd
|
|||
|
ac7b07ee3d
|
1002
Cargo.lock
generated
1002
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,13 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
clap = { version = "4.5.60", features = ["derive"] }
|
clap = { version = "4.5.60", features = ["derive"] }
|
||||||
|
crossterm = "0.29.0"
|
||||||
env_logger = "0.11.9"
|
env_logger = "0.11.9"
|
||||||
globset = "0.4.18"
|
globset = "0.4.18"
|
||||||
libloot = "0.29.0"
|
libloot = "0.29.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] }
|
quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] }
|
||||||
|
ratatui = "0.30.0"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
sevenz-rust2 = { version = "0.20.2" }
|
sevenz-rust2 = { version = "0.20.2" }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ pub enum Commands {
|
|||||||
LoadOrder { instance: String },
|
LoadOrder { instance: String },
|
||||||
ApiCheck,
|
ApiCheck,
|
||||||
Download { url: String },
|
Download { url: String },
|
||||||
|
Tui,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ pub mod types;
|
|||||||
pub mod unpacker;
|
pub mod unpacker;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
|
pub mod tui;
|
||||||
|
|||||||
23
src/main.rs
23
src/main.rs
@@ -5,11 +5,13 @@ 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},
|
cli::{self, Args},
|
||||||
|
mod_config_installer::FomodInstaller,
|
||||||
nexus::NexusAPI,
|
nexus::NexusAPI,
|
||||||
|
tui,
|
||||||
types::RootConfig,
|
types::RootConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,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 => {
|
||||||
@@ -71,7 +85,7 @@ fn command_download(root_config: &mut RootConfig, raw_url: &str) -> anyhow::Resu
|
|||||||
|
|
||||||
fn setup_logger() {
|
fn setup_logger() {
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
.filter_level(log::LevelFilter::max())
|
.filter_level(log::LevelFilter::Off)
|
||||||
.format_timestamp(None)
|
.format_timestamp(None)
|
||||||
.filter_module("ureq_proto::util", log::LevelFilter::Debug)
|
.filter_module("ureq_proto::util", log::LevelFilter::Debug)
|
||||||
.filter_module("rustls::client::hs", log::LevelFilter::Debug)
|
.filter_module("rustls::client::hs", log::LevelFilter::Debug)
|
||||||
@@ -102,6 +116,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
cli::Commands::Download { url } => {
|
cli::Commands::Download { url } => {
|
||||||
command_download(&mut root_config, &url)?;
|
command_download(&mut root_config, &url)?;
|
||||||
}
|
}
|
||||||
|
cli::Commands::Tui => {
|
||||||
|
tui::run(&mut root_config)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -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::{
|
||||||
|
fomod::{
|
||||||
CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum,
|
CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum,
|
||||||
Group, GroupType, ModuleDependency, Plugin, PluginTypeDescriptorEnum, PluginTypeEnum,
|
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> {
|
||||||
|
pub fn new(fomod_config: &'a Config, installed_plugins: &'a [String]) -> Self {
|
||||||
|
let mut state = InstallerState::new();
|
||||||
if let Some(required) = &fomod_config.required_install_files {
|
if let Some(required) = &fomod_config.required_install_files {
|
||||||
state.add_files(required);
|
state.add_files(required);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(install_steps) = fomod_config.install_steps {
|
Self {
|
||||||
let steps = &install_steps.install_step;
|
state,
|
||||||
|
current_step: (0, 0),
|
||||||
|
config: fomod_config,
|
||||||
|
installed_plugins,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_step(&mut self, selection: Option<&[usize]>) -> Option<GroupPrompt> {
|
||||||
|
let Some(install_steps) = &self.config.install_steps else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let step = install_steps.install_step.get(self.current_step.0)?;
|
||||||
|
|
||||||
for step in steps {
|
|
||||||
// Check if the step should be visible
|
// Check if the step should be visible
|
||||||
if step
|
if step
|
||||||
.visible
|
.visible
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|v| !evaluate_module_depbendecy(v, &state, installed_plugins))
|
.is_some_and(|v| !evaluate_module_depbendecy(v, &self.state, self.installed_plugins))
|
||||||
{
|
{
|
||||||
// Dependency to show the step not meet. Skipping.
|
// Dependency to show the step not meet. Skipping.
|
||||||
continue;
|
self.current_step = (self.current_step.0 + 1, 0);
|
||||||
|
return self.run_step(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
for group in &step.optional_file_groups.group {
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: Skip groups where all plugins are NotUsable
|
// TODO: Skip groups where all plugins are NotUsable
|
||||||
|
|
||||||
let prompt = GroupPrompt::new(group, &state, installed_plugins);
|
match selection {
|
||||||
|
Some(selected_plugins) => {
|
||||||
let selected_plugins = (group_prompt)(prompt);
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate conditional file installs based on final flag state
|
// Next step
|
||||||
if let Some(conditional) = &fomod_config.conditional_file_installs {
|
self.current_step = (self.current_step.0, self.current_step.1 + 1);
|
||||||
for pattern in &conditional.patterns.pattern {
|
self.run_step(None)
|
||||||
if evaluate_module_depbendecy(&pattern.dependencies, &state, installed_plugins) {
|
|
||||||
state.add_files(&pattern.files);
|
|
||||||
}
|
}
|
||||||
|
None => Some(GroupPrompt::new(group, &self.state, self.installed_plugins)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(state.into_file_list())
|
pub fn finalize(self, mod_root: impl AsRef<Path>) -> Result<Vec<ModFile>, io::Error> {
|
||||||
|
let files: Vec<_> = self
|
||||||
|
.state
|
||||||
|
.into_file_list()
|
||||||
|
.iter()
|
||||||
|
.map(|f| ModFile::from_installer(f.clone(), &mod_root))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/tui.rs
Normal file
6
src/tui.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
mod app;
|
||||||
|
mod mod_list;
|
||||||
|
mod status;
|
||||||
|
mod instance;
|
||||||
|
|
||||||
|
pub use app::run;
|
||||||
137
src/tui/app.rs
Normal file
137
src/tui/app.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use log::error;
|
||||||
|
use ratatui::{
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
widgets::{StatefulWidget, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
tui::{
|
||||||
|
instance::{InstanceSelect, InstanceSelectState},
|
||||||
|
mod_list::{ModList, ModListState},
|
||||||
|
status::StatusBar,
|
||||||
|
},
|
||||||
|
types::{ModdedInstance, RootConfig},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(root_config: &mut RootConfig) -> anyhow::Result<()> {
|
||||||
|
ratatui::run(|terminal| App::new(root_config).run(terminal))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct App<'a> {
|
||||||
|
root_config: &'a mut RootConfig,
|
||||||
|
|
||||||
|
loaded_instance: Option<ModdedInstance>,
|
||||||
|
|
||||||
|
exit: bool,
|
||||||
|
|
||||||
|
mod_list_state: ModListState,
|
||||||
|
selected_instance_state: InstanceSelectState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> App<'a> {
|
||||||
|
fn new(root_config: &'a mut RootConfig) -> Self {
|
||||||
|
let mut mod_list_state = ModListState::new();
|
||||||
|
mod_list_state.update_list(root_config, None);
|
||||||
|
Self {
|
||||||
|
root_config,
|
||||||
|
|
||||||
|
loaded_instance: None,
|
||||||
|
|
||||||
|
exit: false,
|
||||||
|
mod_list_state,
|
||||||
|
selected_instance_state: InstanceSelectState::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||||
|
terminal.clear()?;
|
||||||
|
while !self.exit {
|
||||||
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, frame: &mut Frame) {
|
||||||
|
frame.render_widget(self, frame.area());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_events(&mut self) -> io::Result<()> {
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
|
||||||
|
self.handle_key_event(key_event)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Esc | KeyCode::Char('q') => self.exit(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
|
self.mod_list_state.select_prev();
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
|
self.mod_list_state.select_next();
|
||||||
|
}
|
||||||
|
KeyCode::Right | KeyCode::Char('l') => {
|
||||||
|
self.selected_instance_state.next_instance(self.root_config);
|
||||||
|
self.load_instance();
|
||||||
|
}
|
||||||
|
KeyCode::Left | KeyCode::Char('h') => {
|
||||||
|
self.selected_instance_state.prev_instance(self.root_config);
|
||||||
|
self.load_instance();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&mut self) {
|
||||||
|
self.exit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_instance(&mut self) {
|
||||||
|
let Some(selected) = self.selected_instance_state.instance() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.root_config.load_instance_by_id(selected) {
|
||||||
|
Ok(instance) => {
|
||||||
|
self.loaded_instance = Some(instance);
|
||||||
|
self.mod_list_state
|
||||||
|
.update_list(self.root_config, self.loaded_instance.as_ref());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to load instance: {err}");
|
||||||
|
self.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for &mut App<'a> {
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
InstanceSelect.render(chunks[0], buf, &mut self.selected_instance_state);
|
||||||
|
ModList.render(chunks[1], buf, &mut self.mod_list_state);
|
||||||
|
StatusBar.render(chunks[2], buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/tui/instance.rs
Normal file
92
src/tui/instance.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::Rect,
|
||||||
|
style::Style,
|
||||||
|
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::types::RootConfig;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InstanceSelectState {
|
||||||
|
selected: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstanceSelectState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InstanceSelect;
|
||||||
|
|
||||||
|
impl StatefulWidget for InstanceSelect {
|
||||||
|
type State = InstanceSelectState;
|
||||||
|
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
let list_block = Block::default()
|
||||||
|
.title("Instance")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
Paragraph::new(state.selected.clone().unwrap_or("None".to_owned()))
|
||||||
|
.block(list_block)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/tui/mod_list.rs
Normal file
114
src/tui/mod_list.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
widgets::{Block, Borders, Cell, Row, StatefulWidget, Table, TableState},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::types::{ModConfig, ModdedInstance, RootConfig};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ModListState {
|
||||||
|
table_state: TableState,
|
||||||
|
items: Vec<ListItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModListState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
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());
|
||||||
|
|
||||||
|
let mut items: Vec<_> = root_config
|
||||||
|
.mods()
|
||||||
|
.iter()
|
||||||
|
.filter(|e| instance_game_type.clone().is_none_or(|gt| e.1.game() == gt))
|
||||||
|
.map(|(id, config)| ListItem {
|
||||||
|
id: id.to_owned(),
|
||||||
|
mod_config: config.clone(),
|
||||||
|
included: included_ids
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|set| set.contains(id.as_str())),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
items.sort_by_key(|item| item.id.clone());
|
||||||
|
|
||||||
|
self.items = items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
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) {
|
||||||
|
let block = Block::default()
|
||||||
|
.title("Mod list")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
let rows: Vec<Row> = state
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|item| {
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(item.mod_config.name().unwrap_or(&item.id)),
|
||||||
|
Cell::from(item.id.as_str()),
|
||||||
|
Cell::from(if item.included { "" } else { "" }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let table = Table::new(
|
||||||
|
rows,
|
||||||
|
[
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.row_highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Yellow)
|
||||||
|
.bg(Color::DarkGray)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.block(block);
|
||||||
|
|
||||||
|
StatefulWidget::render(table, area, buf, &mut state.table_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/tui/status.rs
Normal file
12
src/tui/status.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use ratatui::{text::Line, widgets::Widget};
|
||||||
|
|
||||||
|
pub struct StatusBar;
|
||||||
|
|
||||||
|
impl Widget for StatusBar {
|
||||||
|
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized {
|
||||||
|
|
||||||
|
Line::from("Up Down Left right").render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,10 +87,18 @@ impl RootConfig {
|
|||||||
self.mods.get(id).map(|e| e.clone().add_id(id))
|
self.mods.get(id).map(|e| e.clone().add_id(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mods(&self) -> &HashMap<String, ModConfig> {
|
||||||
|
&self.mods
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_mod(&mut self, new_mod: &ModConfig) {
|
pub fn add_mod(&mut self, new_mod: &ModConfig) {
|
||||||
self.mods.insert(new_mod.id().to_owned(), new_mod.clone());
|
self.mods.insert(new_mod.id().to_owned(), new_mod.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn instances(&self) -> Vec<String> {
|
||||||
|
self.instances.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_instance_by_id(&self, id: &str) -> Result<ModdedInstance, ConfigReadWriteError> {
|
pub fn load_instance_by_id(&self, id: &str) -> Result<ModdedInstance, ConfigReadWriteError> {
|
||||||
debug!("Loading instance {}", id);
|
debug!("Loading instance {}", id);
|
||||||
let conf = self
|
let conf = self
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user