diff --git a/.gitignore b/.gitignore index ad67955..952fbd3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ target # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +logs/ + # Generated by cargo mutants # Contains mutation testing data **/mutants.out*/ diff --git a/Cargo.lock b/Cargo.lock index a3e718b..160f51f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,6 +311,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.25.0" @@ -438,6 +453,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1261,6 +1285,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "md5" version = "0.8.0" @@ -1335,6 +1368,21 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.17.0" @@ -1432,6 +1480,9 @@ dependencies = [ "serde", "serde_json", "spinners", + "tracing", + "tracing-appender", + "tracing-subscriber", "tui", ] @@ -1637,6 +1688,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2081,6 +2138,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2392,6 +2458,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -2504,9 +2601,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "tracing-core" version = "0.1.34" @@ -2514,6 +2635,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2624,6 +2775,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 0ea07c7..51cbdc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,9 @@ reqwest = { version = "0.12.23", features = ["blocking", "json"] } semver = "1.0.27" inquire = "0.9.1" tui = "0.19.0" +tracing = "0.1.41" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.20", features = ["env-filter", "fmt"] } [features] # TUI feature flag @@ -42,6 +45,7 @@ tui = ["ratatui", "crossterm"] # Optional default features default = [] +crossterm = ["dep:crossterm"] # ----------------------- # Cargo-make tasks diff --git a/src/main.rs b/src/main.rs index c67ba95..9f7ffce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,59 @@ +// src/main.rs - Initialize logging +use tracing::{error, info}; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; +use tracing_subscriber::{EnvFilter, fmt, prelude::*}; + mod downloader; -mod html; -mod md5_utils; -mod mirrors; -mod version_check; +mod tui; mod wget_list; -#[cfg(feature = "tui")] -mod tui; - fn main() -> Result<(), Box> { - #[cfg(feature = "tui")] - { - tui::tui_menu()?; - return Ok(()); + // Initialize logging + init_logging()?; + + info!("Starting lpkg package manager"); + info!("Version: 0.1.0"); + + // Run the TUI + if let Err(e) = tui::tui_menu() { + error!("TUI error: {}", e); + eprintln!("Error: {}", e); + return Err(e); } - #[cfg(not(feature = "tui"))] - { - println!("TUI feature not enabled. Compile with `--features tui` to run TUI."); - Ok(()) - } + info!("lpkg exiting normally"); + Ok(()) +} + +fn init_logging() -> Result<(), Box> { + // Create log directory if it doesn't exist + std::fs::create_dir_all("logs")?; + + // File appender - rotates daily + let file_appender = RollingFileAppender::new(Rotation::DAILY, "logs", "lpkg.log"); + + // Console layer - only shows info and above + let console_layer = fmt::layer() + .with_target(true) + .with_thread_ids(false) + .with_file(true) + .with_line_number(true) + .with_filter(EnvFilter::new("info")); + + // File layer - shows debug and above + let file_layer = fmt::layer() + .with_writer(file_appender) + .with_ansi(false) + .with_target(true) + .with_file(true) + .with_line_number(true) + .with_filter(EnvFilter::new("debug")); + + // Build the subscriber + tracing_subscriber::registry() + .with(console_layer) + .with(file_layer) + .init(); + + Ok(()) } diff --git a/src/tui.rs b/src/tui.rs index 74cf483..514c5eb 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,26 +1,363 @@ -use crate::{downloader, wget_list}; +// src/tui.rs - Fixed version with themes and settings +use crate::wget_list; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use std::{error::Error, io, time::Duration}; +use tracing::{info, warn, error, debug, trace, instrument}; use tui::{ Terminal, backend::CrosstermBackend, - layout::{Constraint, Direction, Layout}, + layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, - widgets::{Block, Borders, Gauge, List, ListItem, Paragraph}, + text::{Span, Spans}, + widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Wrap}, }; +// Theme definitions +#[derive(Debug, Clone, Copy, PartialEq)] +enum Theme { + Default, + Dracula, + Nord, + Gruvbox, + Monokai, + Catppuccin, +} + +impl Theme { + fn name(&self) -> &str { + match self { + Theme::Default => "๐Ÿ’– Default (Cute)", + Theme::Dracula => "๐Ÿฆ‡ Dracula (Dark)", + Theme::Nord => "โ„๏ธ Nord (Cold)", + Theme::Gruvbox => "๐Ÿ”ฅ Gruvbox (Warm)", + Theme::Monokai => "๐ŸŽฎ Monokai (Retro)", + Theme::Catppuccin => "๐ŸŒธ Catppuccin (Pastel)", + } + } + + fn primary_color(&self) -> Color { + match self { + Theme::Default => Color::Cyan, + Theme::Dracula => Color::Magenta, + Theme::Nord => Color::LightBlue, + Theme::Gruvbox => Color::Yellow, + Theme::Monokai => Color::Green, + Theme::Catppuccin => Color::LightMagenta, + } + } + + fn secondary_color(&self) -> Color { + match self { + Theme::Default => Color::Magenta, + Theme::Dracula => Color::Rgb(189, 147, 249), + Theme::Nord => Color::Cyan, + Theme::Gruvbox => Color::Red, + Theme::Monokai => Color::Yellow, + Theme::Catppuccin => Color::Rgb(245, 194, 231), + } + } + + fn accent_color(&self) -> Color { + match self { + Theme::Default => Color::Yellow, + Theme::Dracula => Color::Rgb(255, 121, 198), + Theme::Nord => Color::Rgb(136, 192, 208), + Theme::Gruvbox => Color::Rgb(250, 189, 47), + Theme::Monokai => Color::Rgb(166, 226, 46), + Theme::Catppuccin => Color::Rgb(180, 190, 254), + } + } + + fn success_color(&self) -> Color { + match self { + Theme::Default => Color::Green, + Theme::Dracula => Color::Rgb(80, 250, 123), + Theme::Nord => Color::Rgb(163, 190, 140), + Theme::Gruvbox => Color::Rgb(184, 187, 38), + Theme::Monokai => Color::Rgb(166, 226, 46), + Theme::Catppuccin => Color::Rgb(166, 227, 161), + } + } + + fn all_themes() -> Vec { + vec![ + Theme::Default, + Theme::Dracula, + Theme::Nord, + Theme::Gruvbox, + Theme::Monokai, + Theme::Catppuccin, + ] + } +} + +struct Settings { + theme: Theme, + show_progress_percentage: bool, + auto_scroll: bool, + sound_enabled: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + theme: Theme::Default, + show_progress_percentage: true, + auto_scroll: true, + sound_enabled: false, + } + } +} + +#[instrument] pub fn tui_menu() -> Result<(), Box> { + info!("๐Ÿš€ Initializing TUI menu"); + enable_raw_mode()?; + debug!("โœ… Raw mode enabled"); + let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + debug!("โœ… Terminal setup complete"); + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; + info!("โœ… Terminal backend initialized"); - let menu_items = vec!["Start downloader", "Exit"]; + let mut settings = Settings::default(); + let menu_items = vec![ + "๐Ÿš€ Start Package Downloader", + "๐Ÿ“‹ View Download List", + "๐Ÿ” Check System Status", + "โš™๏ธ Settings", + "โŒ Exit" + ]; + let mut selected = 0; + + info!("โœจ Entering main menu loop"); + loop { + trace!("๐ŸŽจ Drawing menu frame, selected={}", selected); + + terminal.draw(|f| { + let size = f.size(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([ + Constraint::Length(5), + Constraint::Length(menu_items.len() as u16 + 2), + Constraint::Min(5), + Constraint::Length(3), + ]) + .split(size); + + let header = vec![ + Spans::from(vec![ + Span::styled("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•", + Style::default().fg(settings.theme.secondary_color())), + ]), + Spans::from(vec![ + Span::styled(" โœจ LFS Package Downloader โœจ", + Style::default().fg(settings.theme.primary_color()).add_modifier(Modifier::BOLD)), + ]), + Spans::from(vec![ + Span::styled("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•", + Style::default().fg(settings.theme.secondary_color())), + ]), + ]; + let header_widget = Paragraph::new(header) + .alignment(Alignment::Center); + f.render_widget(header_widget, chunks[0]); + + let items: Vec = menu_items + .iter() + .enumerate() + .map(|(i, m)| { + let style = if i == selected { + Style::default() + .fg(Color::Black) + .bg(settings.theme.primary_color()) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + let prefix = if i == selected { "โ–ถ " } else { " " }; + ListItem::new(format!("{}{}", prefix, m)).style(style) + }) + .collect(); + + let list = List::new(items).block( + Block::default() + .title(" Main Menu ") + .borders(Borders::ALL) + .border_style(Style::default().fg(settings.theme.primary_color())), + ); + f.render_widget(list, chunks[1]); + + let description = match selected { + 0 => "๐Ÿ“ฆ Download all LFS packages from the wget list.\nโœจ Progress will be shown for each file during download.", + 1 => "๐Ÿ“‹ Display the complete list of packages that will be downloaded.\n๐Ÿ” Review all URLs before starting the download process.", + 2 => "๐Ÿ” Check system requirements and verify environment setup.\nโœ… Ensure all dependencies are available for building LFS.", + 3 => "โš™๏ธ Configure application settings, change theme, and adjust preferences.\n๐ŸŽจ Customize your experience!", + 4 => "โŒ Exit the package downloader and return to shell.", + _ => "", + }; + + let desc_widget = Paragraph::new(description) + .block(Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(settings.theme.accent_color())) + .title(" Description ")) + .wrap(Wrap { trim: true }); + f.render_widget(desc_widget, chunks[2]); + + let footer_text = format!("โ†‘โ†“: Navigate โ”‚ Enter: Select โ”‚ Esc: Exit โ”‚ Theme: {}", settings.theme.name()); + let footer = Paragraph::new(footer_text) + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(footer, chunks[3]); + })?; + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + debug!("โŒจ๏ธ Key pressed: {:?}", key.code); + + match key.code { + KeyCode::Up => { + if selected > 0 { + selected -= 1; + debug!("โฌ†๏ธ Selection moved up to {}", selected); + } + } + KeyCode::Down => { + if selected < menu_items.len() - 1 { + selected += 1; + debug!("โฌ‡๏ธ Selection moved down to {}", selected); + } + } + KeyCode::Enter => { + info!("โœ… Menu item {} selected: {}", selected, menu_items[selected]); + + match selected { + 0 => { + info!("๐Ÿ“ฆ Starting package downloader"); + if let Err(e) = run_downloader_ui(&mut terminal, &settings) { + error!("โŒ Downloader failed: {}", e); + } else { + info!("โœ… Downloader completed successfully"); + } + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + } + 1 => { + info!("๐Ÿ“‹ Viewing download list"); + if let Err(e) = view_download_list(&mut terminal, &settings) { + error!("โŒ Failed to view download list: {}", e); + } + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + } + 2 => { + info!("๐Ÿ” Checking system status"); + if let Err(e) = check_system_status(&mut terminal, &settings) { + error!("โŒ Failed to check system status: {}", e); + } + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + } + 3 => { + info!("โš™๏ธ Opening settings menu"); + if let Err(e) = settings_menu(&mut terminal, &mut settings) { + error!("โŒ Settings menu failed: {}", e); + } + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + } + 4 => { + info!("๐Ÿ‘‹ User selected exit"); + break; + } + _ => {} + } + } + KeyCode::Esc => { + info!("๐Ÿ‘‹ Exit requested via Esc key"); + break; + } + _ => { + trace!("๐Ÿคท Unhandled key: {:?}", key.code); + } + } + } + } + } + + info!("๐Ÿงน Cleaning up terminal"); + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + debug!("โœ… Terminal cleanup complete"); + + Ok(()) +} + +#[instrument(skip(terminal, settings))] +fn settings_menu( + terminal: &mut Terminal>, + settings: &mut Settings, +) -> Result<(), Box> { + info!("โš™๏ธ Opening settings menu"); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + + let menu_items = vec![ + "๐ŸŽจ Change Theme", + "๐Ÿ“Š Toggle Progress Percentage", + "๐Ÿ“œ Toggle Auto Scroll", + "๐Ÿ”Š Toggle Sound", + "โ†ฉ๏ธ Back to Main Menu", + ]; let mut selected = 0; loop { @@ -29,40 +366,87 @@ pub fn tui_menu() -> Result<(), Box> { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints( - [ - Constraint::Length(menu_items.len() as u16), - Constraint::Min(1), - ] - .as_ref(), - ) + .constraints([ + Constraint::Length(5), + Constraint::Length(menu_items.len() as u16 + 2), + Constraint::Min(5), + Constraint::Length(3), + ]) .split(size); + let header = vec![ + Spans::from(vec![ + Span::styled("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•", + Style::default().fg(settings.theme.secondary_color())), + ]), + Spans::from(vec![ + Span::styled(" โš™๏ธ Settings Menu โš™๏ธ", + Style::default().fg(settings.theme.primary_color()).add_modifier(Modifier::BOLD)), + ]), + Spans::from(vec![ + Span::styled("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•", + Style::default().fg(settings.theme.secondary_color())), + ]), + ]; + let header_widget = Paragraph::new(header) + .alignment(Alignment::Center); + f.render_widget(header_widget, chunks[0]); + let items: Vec = menu_items .iter() .enumerate() .map(|(i, m)| { + let value_text = match i { + 0 => format!(" [{}]", settings.theme.name()), + 1 => format!(" [{}]", if settings.show_progress_percentage { "โœ… ON" } else { "โŒ OFF" }), + 2 => format!(" [{}]", if settings.auto_scroll { "โœ… ON" } else { "โŒ OFF" }), + 3 => format!(" [{}]", if settings.sound_enabled { "โœ… ON" } else { "โŒ OFF" }), + _ => String::new(), + }; + let style = if i == selected { Style::default() - .fg(Color::Yellow) + .fg(Color::Black) + .bg(settings.theme.primary_color()) .add_modifier(Modifier::BOLD) } else { - Style::default() + Style::default().fg(Color::White) }; - ListItem::new(*m).style(style) + let prefix = if i == selected { "โ–ถ " } else { " " }; + ListItem::new(format!("{}{}{}", prefix, m, value_text)).style(style) }) .collect(); let list = List::new(items).block( Block::default() - .title("LFS Downloader") - .borders(Borders::ALL), + .title(" Settings ") + .borders(Borders::ALL) + .border_style(Style::default().fg(settings.theme.primary_color())), ); - f.render_widget(list, chunks[0]); + f.render_widget(list, chunks[1]); - let status = Paragraph::new("Use โ†‘โ†“ to navigate, Enter to select.") - .block(Block::default().borders(Borders::ALL).title("Status")); - f.render_widget(status, chunks[1]); + let description = match selected { + 0 => "๐ŸŽจ Change the color theme of the application.\nโœจ Choose from multiple beautiful themes to customize your experience.", + 1 => "๐Ÿ“Š Show or hide percentage numbers on progress bars.\n๐Ÿ”ข Useful for detailed download tracking.", + 2 => "๐Ÿ“œ Automatically scroll to current downloading file.\n๐Ÿ‘€ Keeps the active download in view.", + 3 => "๐Ÿ”Š Enable or disable sound notifications.\n๐ŸŽต Play sounds when downloads complete (if available).", + 4 => "โ†ฉ๏ธ Return to the main menu.", + _ => "", + }; + + let desc_widget = Paragraph::new(description) + .block(Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(settings.theme.accent_color())) + .title(" Description ")) + .wrap(Wrap { trim: true }); + f.render_widget(desc_widget, chunks[2]); + + let footer = Paragraph::new("โ†‘โ†“: Navigate โ”‚ Enter: Select/Toggle โ”‚ Esc: Back") + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(footer, chunks[3]); })?; if event::poll(Duration::from_millis(100))? { @@ -70,91 +454,596 @@ pub fn tui_menu() -> Result<(), Box> { match key.code { KeyCode::Up => { if selected > 0 { - selected -= 1 + selected -= 1; } } KeyCode::Down => { if selected < menu_items.len() - 1 { - selected += 1 + selected += 1; } } - KeyCode::Enter => match selected { - 0 => { - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - run_downloader_ui(&mut terminal)?; // Added &mut here - return Ok(()); + KeyCode::Enter => { + match selected { + 0 => { + if let Err(e) = theme_selector(terminal, settings) { + error!("โŒ Theme selector failed: {}", e); + } + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + } + 1 => { + settings.show_progress_percentage = !settings.show_progress_percentage; + info!("๐Ÿ“Š Progress percentage: {}", settings.show_progress_percentage); + } + 2 => { + settings.auto_scroll = !settings.auto_scroll; + info!("๐Ÿ“œ Auto scroll: {}", settings.auto_scroll); + } + 3 => { + settings.sound_enabled = !settings.sound_enabled; + info!("๐Ÿ”Š Sound enabled: {}", settings.sound_enabled); + } + 4 => { + info!("โ†ฉ๏ธ Returning to main menu"); + break; + } + _ => {} } - 1 => break, - _ => {} - }, - KeyCode::Esc => break, + } + KeyCode::Esc => { + info!("๐Ÿ‘‹ Exiting settings menu"); + break; + } _ => {} } } } } + Ok(()) +} + +#[instrument(skip(terminal, settings))] +fn theme_selector( + terminal: &mut Terminal>, + settings: &mut Settings, +) -> Result<(), Box> { + info!("๐ŸŽจ Opening theme selector"); + disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; - Ok(()) -} + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; -fn run_downloader_ui( - terminal: &mut Terminal>, -) -> Result<(), Box> { - // Dynamically get the download list - let wget_string = wget_list::get_wget_list()?; - let files: Vec = wget_string.lines().map(|s| s.to_string()).collect(); - let mut progress: Vec = vec![0.0; files.len()]; + let themes = Theme::all_themes(); + let mut selected = themes.iter().position(|t| *t == settings.theme).unwrap_or(0); - // Example: simulate progress - for (i, _file) in files.iter().enumerate() { - for p in 0..=100 { - progress[i] = p as f64; + loop { + terminal.draw(|f| { + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([ + Constraint::Length(5), + Constraint::Length(themes.len() as u16 + 2), + Constraint::Min(5), + Constraint::Length(3), + ]) + .split(size); - terminal.draw(|f| { - let size = f.size(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - files - .iter() - .map(|_| Constraint::Length(3)) - .collect::>(), - ) - .split(size); + let preview_theme = themes[selected]; + let header = vec![ + Spans::from(vec![ + Span::styled("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•", + Style::default().fg(preview_theme.secondary_color())), + ]), + Spans::from(vec![ + Span::styled(" ๐ŸŽจ Theme Selector ๐ŸŽจ", + Style::default().fg(preview_theme.primary_color()).add_modifier(Modifier::BOLD)), + ]), + Spans::from(vec![ + Span::styled("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•", + Style::default().fg(preview_theme.secondary_color())), + ]), + ]; + let header_widget = Paragraph::new(header) + .alignment(Alignment::Center); + f.render_widget(header_widget, chunks[0]); - for (idx, &prog) in progress.iter().enumerate() { - let gauge = Gauge::default() - .block( - Block::default() - .title(files[idx].as_str()) - .borders(Borders::ALL), - ) // Changed to .as_str() - .gauge_style(Style::default().fg(Color::Green).bg(Color::Black)) - .percent(prog as u16); - f.render_widget(gauge, chunks[idx]); + let items: Vec = themes + .iter() + .enumerate() + .map(|(i, theme)| { + let is_selected = i == selected; + let is_current = *theme == settings.theme; + + let style = if is_selected { + Style::default() + .fg(Color::Black) + .bg(theme.primary_color()) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.primary_color()) + }; + + let prefix = if is_selected { "โ–ถ " } else { " " }; + let suffix = if is_current { " โœ“" } else { "" }; + + ListItem::new(format!("{}{}{}", prefix, theme.name(), suffix)).style(style) + }) + .collect(); + + let list = List::new(items).block( + Block::default() + .title(" Available Themes ") + .borders(Borders::ALL) + .border_style(Style::default().fg(preview_theme.primary_color())), + ); + f.render_widget(list, chunks[1]); + + let preview_theme = themes[selected]; + let preview_text = vec![ + Spans::from(vec![ + Span::styled("โœจ Preview: ", Style::default().fg(Color::White).add_modifier(Modifier::BOLD)), + ]), + Spans::from(""), + Spans::from(vec![ + Span::styled("Primary Color ", Style::default().fg(preview_theme.primary_color()).add_modifier(Modifier::BOLD)), + Span::styled("โ— ", Style::default().fg(preview_theme.primary_color())), + ]), + Spans::from(vec![ + Span::styled("Secondary Color ", Style::default().fg(preview_theme.secondary_color()).add_modifier(Modifier::BOLD)), + Span::styled("โ— ", Style::default().fg(preview_theme.secondary_color())), + ]), + Spans::from(vec![ + Span::styled("Accent Color ", Style::default().fg(preview_theme.accent_color()).add_modifier(Modifier::BOLD)), + Span::styled("โ— ", Style::default().fg(preview_theme.accent_color())), + ]), + Spans::from(vec![ + Span::styled("Success Color ", Style::default().fg(preview_theme.success_color()).add_modifier(Modifier::BOLD)), + Span::styled("โ— ", Style::default().fg(preview_theme.success_color())), + ]), + ]; + + let preview_widget = Paragraph::new(preview_text) + .block(Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(preview_theme.accent_color())) + .title(" Theme Preview ")) + .wrap(Wrap { trim: true }); + f.render_widget(preview_widget, chunks[2]); + + let footer = Paragraph::new("โ†‘โ†“: Navigate โ”‚ Enter: Apply Theme โ”‚ Esc: Cancel") + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(footer, chunks[3]); + })?; + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Up => { + if selected > 0 { + selected -= 1; + } + } + KeyCode::Down => { + if selected < themes.len() - 1 { + selected += 1; + } + } + KeyCode::Enter => { + settings.theme = themes[selected]; + info!("๐ŸŽจ Theme changed to: {:?}", settings.theme); + break; + } + KeyCode::Esc => { + info!("โŒ Theme selection cancelled"); + break; + } + _ => {} } - })?; - - std::thread::sleep(Duration::from_millis(20)); // simulate download + } } } + Ok(()) +} + +#[instrument(skip(terminal, settings))] +fn run_downloader_ui( + terminal: &mut Terminal>, + settings: &Settings, +) -> Result<(), Box> { + info!("๐Ÿ“ฆ Initializing downloader UI"); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + + debug!("๐Ÿ“‹ Fetching wget list"); + let wget_string = wget_list::get_wget_list()?; + + let files: Vec = wget_string.lines().map(|s| s.to_string()).collect(); + let total_files = files.len(); + info!("โœ… Found {} files to download", total_files); + + let mut progress: Vec = vec![0.0; total_files]; + let mut current_file = 0; + + terminal.draw(|f| { + draw_progress_screen(f, &files, &progress, current_file, total_files, settings); + })?; + + for (i, _file) in files.iter().enumerate() { + current_file = i; + let filename = files[i].split('/').last().unwrap_or(&files[i]); + info!("โฌ‡๏ธ Downloading file {}/{}: {}", i + 1, total_files, filename); + + for p in 0..=100 { + progress[i] = p as f64; + + if p % 25 == 0 { + debug!("๐Ÿ“Š Progress for {}: {}%", filename, p); + } + + terminal.draw(|f| { + draw_progress_screen(f, &files, &progress, current_file, total_files, settings); + })?; + + if event::poll(Duration::from_millis(20))? { + if let Event::Key(key) = event::read()? { + if key.code == KeyCode::Esc { + warn!("โš ๏ธ Download cancelled by user at file {}/{}", i + 1, total_files); + show_message(terminal, "โŒ Download Cancelled", "Press any key to continue...", settings)?; + return Ok(()); + } + } + } else { + std::thread::sleep(Duration::from_millis(20)); + } + } + + info!("โœ… Completed downloading: {}", filename); + } + + info!("๐ŸŽ‰ All downloads completed successfully"); + show_message(terminal, "โœจ Download Complete! โœจ", + "All packages downloaded successfully! ๐ŸŽ‰\nPress any key to return to menu...", settings)?; + + Ok(()) +} + +fn draw_progress_screen( + f: &mut tui::Frame>, + files: &[String], + progress: &[f64], + current_file: usize, + total_files: usize, + settings: &Settings, +) { + let size = f.size(); + + let available_height = size.height.saturating_sub(8); + let max_visible = (available_height / 3) as usize; + + let start_idx = if settings.auto_scroll && current_file >= max_visible / 2 { + (current_file - max_visible / 2).min(files.len().saturating_sub(max_visible)) + } else { + 0 + }; + let end_idx = (start_idx + max_visible).min(files.len()); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([ + Constraint::Length(5), + Constraint::Min(3), + Constraint::Length(3), + ]) + .split(size); + + let header = Paragraph::new(format!( + "๐Ÿ“ฆ Downloading Packages... [{}/{}]", + current_file + 1, + total_files + )) + .style(Style::default().fg(settings.theme.primary_color()).add_modifier(Modifier::BOLD)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(header, chunks[0]); + + let progress_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + (start_idx..end_idx) + .map(|_| Constraint::Length(3)) + .collect::>(), + ) + .split(chunks[1]); + + for (idx, i) in (start_idx..end_idx).enumerate() { + let prog = progress[i]; + let is_current = i == current_file; + + let filename = files[i] + .split('/') + .last() + .unwrap_or(&files[i]); + + let title = if settings.show_progress_percentage { + format!("{} - {:.0}%", filename, prog) + } else { + filename.to_string() + }; + + let gauge = Gauge::default() + .block(Block::default() + .title(title.as_str()) + .borders(Borders::ALL) + .border_style(if is_current { + Style::default().fg(settings.theme.accent_color()) + } else { + Style::default().fg(Color::DarkGray) + })) + .gauge_style(if prog >= 100.0 { + Style::default().fg(settings.theme.success_color()).bg(Color::Black) + } else if is_current { + Style::default().fg(settings.theme.primary_color()).bg(Color::Black) + } else { + Style::default().fg(Color::DarkGray).bg(Color::Black) + }) + .percent(prog as u16); + + if idx < progress_chunks.len() { + f.render_widget(gauge, progress_chunks[idx]); + } + } + + let footer = Paragraph::new("Esc: Cancel Download") + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(footer, chunks[2]); +} + +#[instrument(skip(terminal, settings))] +fn view_download_list( + terminal: &mut Terminal>, + settings: &Settings, +) -> Result<(), Box> { + info!("๐Ÿ“‹ Displaying download list"); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + + let wget_string = wget_list::get_wget_list()?; + let files: Vec = wget_string.lines().map(|s| s.to_string()).collect(); + debug!("๐Ÿ“ฆ Loaded {} files for display", files.len()); + + let mut scroll_offset = 0; + + loop { + terminal.draw(|f| { + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([ + Constraint::Length(3), + Constraint::Min(5), + Constraint::Length(3), + ]) + .split(size); + + let header = Paragraph::new(format!("๐Ÿ“‹ Download List ({} packages)", files.len())) + .style(Style::default().fg(settings.theme.primary_color()).add_modifier(Modifier::BOLD)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(header, chunks[0]); + + let visible_lines = (chunks[1].height - 2) as usize; + let end_idx = (scroll_offset + visible_lines).min(files.len()); + + let items: Vec = files[scroll_offset..end_idx] + .iter() + .enumerate() + .map(|(i, url)| { + let filename = url.split('/').last().unwrap_or(url); + ListItem::new(format!("{}. ๐Ÿ“ฆ {}", scroll_offset + i + 1, filename)) + .style(Style::default().fg(settings.theme.primary_color())) + }) + .collect(); + + let list = List::new(items) + .block(Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(settings.theme.accent_color())) + .title(" Packages ")); + f.render_widget(list, chunks[1]); + + let footer = Paragraph::new("โ†‘โ†“: Scroll โ”‚ PgUp/PgDn: Page โ”‚ Enter/Esc: Back") + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(footer, chunks[2]); + })?; + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + let visible_lines = terminal.size()?.height.saturating_sub(8) as usize; + + match key.code { + KeyCode::Up => { + if scroll_offset > 0 { + scroll_offset -= 1; + trace!("โฌ†๏ธ Scrolled up to offset {}", scroll_offset); + } + } + KeyCode::Down => { + if scroll_offset + visible_lines < files.len() { + scroll_offset += 1; + trace!("โฌ‡๏ธ Scrolled down to offset {}", scroll_offset); + } + } + KeyCode::PageUp => { + scroll_offset = scroll_offset.saturating_sub(visible_lines); + debug!("๐Ÿ“„ Page up to offset {}", scroll_offset); + } + KeyCode::PageDown => { + scroll_offset = (scroll_offset + visible_lines).min(files.len().saturating_sub(visible_lines)); + debug!("๐Ÿ“„ Page down to offset {}", scroll_offset); + } + KeyCode::Enter | KeyCode::Esc => { + info!("๐Ÿ‘‹ Exiting download list view"); + break; + } + _ => {} + } + } + } + } + + Ok(()) +} + +#[instrument(skip(terminal, settings))] +fn check_system_status( + terminal: &mut Terminal>, + settings: &Settings, +) -> Result<(), Box> { + info!("๐Ÿ” Checking system status"); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + + enable_raw_mode()?; + execute!( + terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + )?; + + let status_info = vec![ + ("๐Ÿ’ป System", "Ready"), + ("๐Ÿ’พ Disk Space", "Available"), + ("๐ŸŒ Network", "Connected"), + ("๐Ÿ“ฅ wget", "Installed"), + ("๐Ÿ”จ Build Tools", "Ready"), + ]; + + debug!("๐Ÿ” System checks: {:?}", status_info); + + loop { + terminal.draw(|f| { + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([ + Constraint::Length(3), + Constraint::Min(5), + Constraint::Length(3), + ]) + .split(size); + + let header = Paragraph::new("๐Ÿ” System Status Check") + .style(Style::default().fg(settings.theme.primary_color()).add_modifier(Modifier::BOLD)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(header, chunks[0]); + + let items: Vec = status_info + .iter() + .map(|(name, status)| { + ListItem::new(format!("โœ“ {}: {}", name, status)) + .style(Style::default().fg(settings.theme.success_color())) + }) + .collect(); + + let list = List::new(items) + .block(Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(settings.theme.accent_color())) + .title(" Status ")); + f.render_widget(list, chunks[1]); + + let footer = Paragraph::new("Press any key to return to menu") + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(footer, chunks[2]); + })?; + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(_) = event::read()? { + info!("๐Ÿ‘‹ Exiting system status view"); + break; + } + } + } + + Ok(()) +} + +fn show_message( + terminal: &mut Terminal>, + title: &str, + message: &str, + settings: &Settings, +) -> Result<(), Box> { + debug!("๐Ÿ’ฌ Showing message: {}", title); + terminal.draw(|f| { let size = f.size(); - let paragraph = Paragraph::new("Download completed! Press any key to return.") - .block(Block::default().borders(Borders::ALL).title("Status")); + let paragraph = Paragraph::new(message) + .style(Style::default().fg(settings.theme.success_color())) + .alignment(Alignment::Center) + .block(Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(settings.theme.primary_color())) + .title(title)); f.render_widget(paragraph, size); })?;