diff --git a/src/tui.rs b/src/tui.rs index 6a4ddeb..6e152a5 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,4 +1,6 @@ #[cfg(feature = "tui")] +use crate::{downloader, md5_utils, mirrors, wget_list}; +#[cfg(feature = "tui")] use crossterm::{ event::{self, Event, KeyCode}, execute, @@ -13,16 +15,14 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem, ListState}, }; #[cfg(feature = "tui")] -use std::collections::HashMap; -#[cfg(feature = "tui")] -use std::io::{self, stdout}; -#[cfg(feature = "tui")] -use std::path::PathBuf; -#[cfg(feature = "tui")] -use std::time::{Duration, Instant}; - -#[cfg(feature = "tui")] -use crate::{downloader, mirrors, wget_list}; +use std::{ + collections::HashMap, + io::{self, stdout}, + path::PathBuf, + sync::{Arc, Mutex}, + thread, + time::Duration, +}; #[cfg(feature = "tui")] fn init_environment() -> PathBuf { @@ -51,28 +51,22 @@ fn select_mirrors_tui(mirrors: Vec) -> Vec { terminal .draw(|f| { let size = f.size(); - let block = Block::default() - .title("Select mirrors (space to toggle, Enter to confirm)") - .borders(Borders::ALL); - f.render_widget(block, size); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints(vec![Constraint::Length(3); mirrors.len()]) - .split(size); - - for (i, mirror) in mirrors.iter().enumerate() { - let mut style = Style::default(); - if Some(i) == state.selected() { - style = style.bg(Color::Red).fg(Color::White); - } - let prefix = if selected[i] { "[x] " } else { "[ ] " }; - let list_item = ListItem::new(format!("{}{}", prefix, mirror)).style(style); - let list = - List::new(vec![list_item]).block(Block::default().borders(Borders::ALL)); - f.render_widget(list, chunks[i]); - } + let items: Vec = mirrors + .iter() + .enumerate() + .map(|(i, mirror)| { + let prefix = if selected[i] { "[x] " } else { "[ ] " }; + ListItem::new(format!("{}{}", prefix, mirror)) + }) + .collect(); + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title("Select mirrors"), + ) + .highlight_symbol(">> "); + f.render_stateful_widget(list, size, &mut state); }) .unwrap(); @@ -115,8 +109,8 @@ fn select_mirrors_tui(mirrors: Vec) -> Vec { } #[cfg(feature = "tui")] -fn download_packages(lfs_sources: &PathBuf) { - let mirrors_list = mirrors::fetch_mirrors().unwrap_or_else(|_| vec![]); +fn download_packages_tui(lfs_sources: &PathBuf) { + let mirrors_list = mirrors::fetch_mirrors().unwrap_or_default(); let selected_mirrors = select_mirrors_tui(mirrors_list); if selected_mirrors.is_empty() { println!("⚠️ No mirrors selected!"); @@ -130,7 +124,7 @@ fn download_packages(lfs_sources: &PathBuf) { } let mut md5_map = HashMap::new(); - if let Ok(md5_content) = crate::md5_utils::get_md5sums() { + if let Ok(md5_content) = md5_utils::get_md5sums() { for line in md5_content.lines() { if let Some((hash, filename)) = line.split_once(' ') { md5_map.insert(filename.to_string(), hash.to_string()); @@ -138,28 +132,75 @@ fn download_packages(lfs_sources: &PathBuf) { } } - for file in wget_list { - let mut downloaded = false; - for mirror in &selected_mirrors { - print!("⬇️ Downloading {} from {} ... ", file, mirror); - io::stdout().flush().unwrap(); + let download_state: Arc>> = Arc::new(Mutex::new( + wget_list + .iter() + .map(|f| (f.clone(), "Pending".into())) + .collect(), + )); - let start = Instant::now(); - if downloader::download_files(&file, lfs_sources, Some(mirror.clone()), Some(&md5_map)) + let download_state_clone = Arc::clone(&download_state); + let mirrors_clone = selected_mirrors.clone(); + let lfs_sources = lfs_sources.clone(); + + thread::spawn(move || { + for file in &wget_list { + let mut status = "Failed".to_string(); + for mirror in &mirrors_clone { + if downloader::download_files( + file, + &lfs_sources, + Some(mirror.clone()), + Some(&md5_map), + ) .is_ok() - { - println!("✅ done in {:?}", start.elapsed()); - downloaded = true; - break; - } else { - println!("⚠️ failed, trying next mirror..."); + { + status = format!("Downloaded from {}", mirror); + break; + } + } + let mut state = download_state_clone.lock().unwrap(); + if let Some(entry) = state.iter_mut().find(|(f, _)| f == file) { + entry.1 = status.clone(); } } - if !downloaded { - println!("❌ Failed to download {}", file); + }); + + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen).unwrap(); + enable_raw_mode().unwrap(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).unwrap(); + + loop { + terminal + .draw(|f| { + let size = f.size(); + let items: Vec = { + let state = download_state.lock().unwrap(); + state + .iter() + .map(|(f, s)| ListItem::new(format!("{}: {}", f, s))) + .collect() + }; + let list = List::new(items).block( + Block::default() + .title("Downloading Packages") + .borders(Borders::ALL), + ); + f.render_widget(list, size); + }) + .unwrap(); + + let state = download_state.lock().unwrap(); + if state.iter().all(|(_, s)| s != "Pending") { + break; } + thread::sleep(Duration::from_millis(100)); } + disable_raw_mode().unwrap(); + execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap(); println!("🎉 All downloads finished!"); } @@ -222,15 +263,19 @@ pub fn tui_menu() -> Result<(), Box> { } } KeyCode::Enter => match state.selected() { - Some(0) => lfs_sources = Some(init_environment()), + Some(0) => { + lfs_sources = Some(init_environment()); + } Some(1) => { if let Some(ref path) = lfs_sources { - download_packages(path); + download_packages_tui(path); } else { println!("⚠️ Please initialize environment first!"); } } - Some(2) => println!("🔍 Status selected! (TODO)"), + Some(2) => { + println!("🔍 Status selected! (TODO)"); + } Some(3) | _ => break, }, KeyCode::Esc => break,