use crate::app::{App, AppMode};
use ratatui::{
Terminal,
layout::{Constraint, Direction, Layout, Alignment},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph, Clear},
};
use std::io::Result;
pub fn render_ui<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &App) -> Result<()> {
terminal.draw(|f| {
let size = f.size();
f.render_widget(Clear, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ].as_ref())
.split(f.size());
match app.mode {
AppMode::Normal => {
let instructions = Paragraph::new(Line::from(vec![
Span::raw("Press "),
Span::styled("s", Style::default().add_modifier(Modifier::BOLD).fg(Color::Green)),
Span::raw(" to search, "),
Span::styled("↑↓/jk", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)),
Span::raw(" to navigate, "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD).fg(Color::Red)),
Span::raw(" to quit"),
]))
.block(Block::default().title("🏴☠️ TUI Torrent").borders(Borders::ALL));
f.render_widget(instructions, chunks[0]);
},
AppMode::Search => {
let search_text = format!("Search: {}", app.search_query);
let search_bar = Paragraph::new(search_text)
.style(Style::default().fg(Color::Yellow))
.block(Block::default().title("🔍 Enter Search Query (Press Enter to search, Esc to cancel)").borders(Borders::ALL));
f.render_widget(search_bar, chunks[0]);
},
AppMode::Searching => {
let searching_text = format!("Searching for: {}", app.search_query);
let loading_indicator = app.get_loading_indicator();
let title = format!("{} Searching Multiple Sources...", loading_indicator);
let search_bar = Paragraph::new(searching_text)
.style(Style::default().fg(Color::LightYellow))
.block(Block::default().title(title).borders(Borders::ALL));
f.render_widget(search_bar, chunks[0]);
},
AppMode::Results => {
let result_info = Paragraph::new(Line::from(vec![
Span::raw("Press "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD).fg(Color::Green)),
Span::raw(" to download, "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD).fg(Color::Red)),
Span::raw(" to go back, "),
Span::styled("↑↓/jk", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)),
Span::raw(" to navigate"),
]))
.block(Block::default().title(format!("📋 Search Results ({})", app.search_results.len())).borders(Borders::ALL));
f.render_widget(result_info, chunks[0]);
}
}
let status_color = if app.search_in_progress {
Color::Blue
} else if app.status_message.contains("failed") || app.status_message.contains("error") {
Color::Red
} else {
Color::Green
};
let status_text = if app.search_in_progress {
format!("{} {}", app.get_loading_indicator(), app.search_progress)
} else {
app.status_message.clone()
};
let status_bar = Paragraph::new(status_text)
.style(Style::default().fg(status_color))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL));
f.render_widget(status_bar, chunks[2]);
match app.mode {
AppMode::Normal | AppMode::Search => {
if app.active_downloads.is_empty() {
let empty_msg = Paragraph::new("No active downloads. Press 's' to search for torrents.")
.style(Style::default().fg(Color::Gray))
.alignment(Alignment::Center)
.block(Block::default().title("📥 Active Downloads").borders(Borders::ALL));
f.render_widget(empty_msg, chunks[1]);
} else {
let items: Vec<ListItem> = app.active_downloads
.iter()
.enumerate()
.map(|(i, t)| {
let progress = if let (Ok(completed), Ok(total)) = (t.completed_length.parse::<u64>(), t.total_length.parse::<u64>()) {
if total > 0 {
format!(" ({:.1}%)", (completed as f64 / total as f64) * 100.0)
} else {
String::new()
}
} else {
String::new()
};
let title = format!(
"📁 {} - {}/{} bytes{} @ {} B/s",
t.status, t.completed_length, t.total_length, progress, t.download_speed
);
let style = if i == app.selected_index && app.mode == AppMode::Normal {
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
} else {
Style::default()
};
ListItem::new(title).style(style)
})
.collect();
let downloads = List::new(items).block(
Block::default()
.title("📥 Active Downloads")
.borders(Borders::ALL),
);
f.render_widget(downloads, chunks[1]);
}
},
AppMode::Searching => {
let loading_indicator = app.get_loading_indicator();
let loading_msg = format!("{} Searching Multiple Sources\n\n{} {}\n\n{} Please wait while we fetch results...",
loading_indicator, loading_indicator, app.search_progress, loading_indicator);
let loading_widget = Paragraph::new(loading_msg)
.style(Style::default().fg(Color::Blue))
.alignment(Alignment::Center)
.block(Block::default().title(format!("{} Searching", loading_indicator)).borders(Borders::ALL));
f.render_widget(loading_widget, chunks[1]);
},
AppMode::Results => {
if app.search_results.is_empty() {
let empty_msg = Paragraph::new("No results found. Try a different search term.")
.style(Style::default().fg(Color::Red))
.alignment(Alignment::Center)
.block(Block::default().title("📋 Search Results").borders(Borders::ALL));
f.render_widget(empty_msg, chunks[1]);
} else {
let items: Vec<ListItem> = app.search_results
.iter()
.enumerate()
.map(|(i, result)| {
let source_color = match result.source.as_str() {
"YTS" => Color::Green,
"PirateBay" => Color::Blue,
"1337x" => Color::Magenta,
_ => Color::Gray,
};
let display_name = if result.name.len() > 60 {
format!("{}...", &result.name[..57])
} else {
result.name.clone()
};
let title = Line::from(vec![
Span::styled(format!("[{}] ", result.source), Style::default().fg(source_color).add_modifier(Modifier::BOLD)),
Span::raw(display_name),
Span::styled(format!(" | {} | ", result.size), Style::default().fg(Color::Cyan)),
Span::styled(format!("S:{}", result.seeders), Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled(format!("L:{}", result.leechers), Style::default().fg(Color::Red)),
]);
let style = if i == app.selected_index {
Style::default().bg(Color::DarkGray)
} else {
Style::default()
};
ListItem::new(title).style(style)
})
.collect();
let results_list = List::new(items).block(
Block::default()
.title("📋 Torrent Results")
.borders(Borders::ALL),
);
f.render_widget(results_list, chunks[1]);
}
}
}
})?;
Ok(())
}