Compare commits
1 Commits
master
...
ac7b07ee3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@@ -7,10 +7,7 @@ use fomod_manager::{
|
|||||||
actions::{
|
actions::{
|
||||||
activate_instance, create_loadorder, handle_nxm, insert_mod_to_instance,
|
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},
|
|
||||||
nexus::NexusAPI,
|
|
||||||
types::RootConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn command_activate(
|
fn command_activate(
|
||||||
@@ -71,7 +68,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 +99,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(())
|
||||||
|
|||||||
4
src/tui.rs
Normal file
4
src/tui.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
mod app;
|
||||||
|
mod mod_list;
|
||||||
|
|
||||||
|
pub use app::run;
|
||||||
95
src/tui/app.rs
Normal file
95
src/tui/app.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use ratatui::{
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
layout::Rect,
|
||||||
|
style::Style,
|
||||||
|
widgets::{Block, Borders, StatefulWidget, TableState, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{tui::mod_list::ModList, types::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,
|
||||||
|
exit: bool,
|
||||||
|
|
||||||
|
mod_list_state: TableState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> App<'a> {
|
||||||
|
fn new(root_config: &'a mut RootConfig) -> Self {
|
||||||
|
let mut state = TableState::default();
|
||||||
|
state.select(Some(0)); // select first row by default
|
||||||
|
|
||||||
|
Self {
|
||||||
|
root_config,
|
||||||
|
mod_list_state: state,
|
||||||
|
exit: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_previous();
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
|
self.mod_list_state.select_next();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&mut self) {
|
||||||
|
self.exit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for &mut App<'a> {
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let list_block = Block::default()
|
||||||
|
.title("Mod list")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
ModList::new(self.root_config).block(list_block).render(
|
||||||
|
area,
|
||||||
|
buf,
|
||||||
|
&mut self.mod_list_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/tui/mod_list.rs
Normal file
74
src/tui/mod_list.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
widgets::{Block, Cell, Row, StatefulWidget, Table, TableState},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::types::{ModConfig, RootConfig};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ListItem<'a> {
|
||||||
|
mod_config: &'a ModConfig,
|
||||||
|
id: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ModList<'a> {
|
||||||
|
items: Vec<ListItem<'a>>,
|
||||||
|
block: Option<Block<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ModList<'a> {
|
||||||
|
pub fn new(root_config: &'a RootConfig) -> Self {
|
||||||
|
let mut items: Vec<_> = root_config
|
||||||
|
.mods()
|
||||||
|
.iter()
|
||||||
|
.map(|(id, config)| ListItem {
|
||||||
|
id,
|
||||||
|
mod_config: config,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
items.sort_by_key(|item| item.id);
|
||||||
|
Self { items, block: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||||
|
self.block = Some(block);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> StatefulWidget for ModList<'a> {
|
||||||
|
type State = TableState;
|
||||||
|
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
let rows: Vec<Row> = self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|item| {
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(item.mod_config.name().unwrap_or(item.id)),
|
||||||
|
Cell::from(item.id),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let table = Table::new(rows, [Constraint::Fill(1), Constraint::Fill(1)])
|
||||||
|
.row_highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Yellow)
|
||||||
|
.bg(Color::DarkGray)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.highlight_symbol(">> ");
|
||||||
|
|
||||||
|
let table = match self.block {
|
||||||
|
Some(b) => table.block(b),
|
||||||
|
None => table,
|
||||||
|
};
|
||||||
|
|
||||||
|
StatefulWidget::render(table, area, buf, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,6 +87,10 @@ 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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user