use crate::app_structs::AppState;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Gauge, List, ListItem, Paragraph, Tabs, Wrap},
Frame,
};
pub fn ui(f: &mut Frame, state: &mut AppState) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(3), Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
.split(f.area());
let header = Paragraph::new(vec![
Line::from(vec![
Span::styled("✨ ", Style::default().fg(Color::Rgb(220, 180, 255))), Span::styled(
"Flerp Text Analysis TUI",
Style::default()
.fg(Color::Rgb(170, 200, 255)) .add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![
Span::styled("📂 File: ", Style::default().fg(Color::Rgb(150, 150, 150))), Span::styled(
&state.file_name,
Style::default().fg(Color::Rgb(200, 220, 255)),
), ]),
])
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(170, 200, 255))) .title(Span::styled(
" ⚜️ Flerp ⚜️ ", Style::default()
.fg(Color::Rgb(158, 210, 243)) .add_modifier(Modifier::BOLD),
)),
)
.alignment(Alignment::Center);
f.render_widget(header, chunks[0]);
let tab_titles = vec!["📊 Overview", "🔑 Keywords", "🔎 Search", "📜 Content"];
let tabs = Tabs::new(tab_titles)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(180, 220, 200))) .title(Span::styled(
"🚀 Navigation 🚀",
Style::default()
.fg(Color::Rgb(180, 220, 200)) .add_modifier(Modifier::BOLD),
)),
)
.style(
Style::default()
.fg(Color::Rgb(210, 210, 210))
.bg(Color::Rgb(70, 70, 90)),
) .highlight_style(
Style::default()
.fg(Color::Rgb(255, 220, 180)) .bg(Color::Rgb(100, 120, 170)) .add_modifier(Modifier::BOLD),
)
.select(state.current_tab)
.divider(Span::styled(
" | ",
Style::default().fg(Color::Rgb(130, 130, 150)),
)); f.render_widget(tabs, chunks[1]);
match state.current_tab {
0 => render_overview(f, chunks[2], state),
1 => render_keywords(f, chunks[2], state),
2 => render_search(f, chunks[2], state),
3 => render_content(f, chunks[2], state),
_ => {}
}
let footer_text = if state.search_mode {
"▶️ Press Enter to confirm search, Esc to cancel ◀️"
} else {
"🚪 'q' to quit | ⇆ Tab to switch | 🔍 '/' to search | Aa 'c' for case sensitivity"
};
let footer = Paragraph::new(footer_text)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(255, 200, 220))) .title(Span::styled(
"💡 Help 💡",
Style::default()
.fg(Color::Rgb(255, 200, 220)) .add_modifier(Modifier::BOLD),
)),
)
.style(Style::default().fg(Color::Rgb(240, 240, 180))) .alignment(Alignment::Center);
f.render_widget(footer, chunks[3]);
if state.search_mode {
render_search_input(f, state);
}
}
fn render_overview(f: &mut Frame, area: Rect, state: &AppState) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(8), Constraint::Min(5), ])
.split(area);
let stats_text = vec![
Line::from(vec![
Span::styled("📏 Lines: ", Style::default().fg(Color::Rgb(180, 220, 255))), Span::styled(
state.structural_analysis.lines.to_string(),
Style::default().fg(Color::Rgb(255, 230, 200)), ),
]),
Line::from(vec![
Span::styled("📝 Words: ", Style::default().fg(Color::Rgb(180, 220, 255))), Span::styled(
state.structural_analysis.words.to_string(),
Style::default().fg(Color::Rgb(255, 230, 200)), ),
]),
Line::from(vec![
Span::styled(
"🔤 Characters: ",
Style::default().fg(Color::Rgb(180, 220, 255)),
), Span::styled(
state.structural_analysis.characters.to_string(),
Style::default().fg(Color::Rgb(255, 230, 200)), ),
]),
Line::from(vec![
Span::styled(
"📑 Stanzas: ",
Style::default().fg(Color::Rgb(180, 220, 255)),
), Span::styled(
state.structural_analysis.stanzas.to_string(),
Style::default().fg(Color::Rgb(255, 230, 200)), ),
]),
Line::from(""),
Line::from(vec![
Span::styled(
"Aa Case Sensitive: ",
Style::default().fg(Color::Rgb(170, 170, 170)),
), Span::styled(
if state.case_sensitive {
"ON ✔️"
} else {
"OFF ❌"
},
Style::default().fg(if state.case_sensitive {
Color::Rgb(180, 255, 180) } else {
Color::Rgb(255, 180, 180) }),
),
]),
];
let stats = Paragraph::new(stats_text)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(180, 255, 180))) .title(Span::styled(
"📊 File Statistics 📊",
Style::default()
.fg(Color::Rgb(180, 255, 180)) .add_modifier(Modifier::BOLD),
)),
)
.wrap(Wrap { trim: true });
f.render_widget(stats, chunks[0]);
let max_val = state
.structural_analysis
.words
.max(state.structural_analysis.lines)
.max(100) as f64;
let words_ratio = (state.structural_analysis.words as f64 / max_val).min(1.0);
let lines_ratio = (state.structural_analysis.lines as f64 / max_val).min(1.0);
let progress_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Length(3)])
.split(chunks[1]);
let words_gauge = Gauge::default()
.block(
Block::default()
.borders(Borders::ALL)
.title("Words Progress")
.border_style(Style::default().fg(Color::Rgb(176, 220, 160))), )
.gauge_style(
Style::default()
.fg(Color::Rgb(176, 220, 160)) .bg(Color::Rgb(80, 80, 100)) .add_modifier(Modifier::ITALIC),
)
.ratio(words_ratio)
.label(format!("{:.0}%", words_ratio * 100.0));
f.render_widget(words_gauge, progress_chunks[0]);
let lines_gauge = Gauge::default()
.block(
Block::default()
.borders(Borders::ALL)
.title("Lines Progress")
.border_style(Style::default().fg(Color::Rgb(180, 220, 200))), )
.gauge_style(
Style::default()
.fg(Color::Rgb(180, 220, 200)) .bg(Color::Rgb(80, 80, 100)) .add_modifier(Modifier::ITALIC),
)
.ratio(lines_ratio)
.label(format!("{:.0}%", lines_ratio * 100.0));
f.render_widget(lines_gauge, progress_chunks[1]);
}
fn render_keywords(f: &mut Frame, area: Rect, state: &AppState) {
let items: Vec<ListItem> = state
.keywords
.iter()
.enumerate()
.map(|(i, (keyword, count))| {
ListItem::new(Line::from(vec![
Span::styled(
format!("{:02}. ", i + 1),
Style::default().fg(Color::Rgb(150, 150, 150)), ),
Span::styled(
keyword,
Style::default()
.fg(Color::Rgb(180, 255, 180)) .add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" ({})", count),
Style::default().fg(Color::Rgb(180, 220, 255)), ),
]))
})
.collect();
let keywords_list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(255, 230, 200))) .title(Span::styled(
"🔑 Top Keywords 🔑",
Style::default()
.fg(Color::Rgb(255, 230, 200)) .add_modifier(Modifier::BOLD),
)),
)
.highlight_style(
Style::default()
.bg(Color::Rgb(255, 220, 180)) .fg(Color::Rgb(60, 60, 80)) .add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_widget(keywords_list, area);
}
fn render_search(f: &mut Frame, area: Rect, state: &mut AppState) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(5), ])
.split(area);
let search_info = Paragraph::new(vec![
Line::from(vec![
Span::styled("🔍 Query: ", Style::default().fg(Color::Rgb(170, 200, 255))), Span::styled(
&state.search_query,
Style::default().fg(Color::Rgb(255, 220, 180)),
), ]),
Line::from(vec![
Span::styled(
"🎯 Results: ",
Style::default().fg(Color::Rgb(170, 200, 255)),
), Span::styled(
state.search_results.len().to_string(),
Style::default().fg(Color::Rgb(180, 255, 180)), ),
]),
])
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(220, 180, 255))) .title(Span::styled(
"🔎 Search Info 🔎",
Style::default()
.fg(Color::Rgb(220, 180, 255)) .add_modifier(Modifier::BOLD),
)),
);
f.render_widget(search_info, chunks[0]);
if state.search_results.is_empty() {
let no_results = Paragraph::new("🤷 No results found. Press '/' to start searching. 🤷")
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(120, 120, 120))) .title("Search Results"),
)
.style(Style::default().fg(Color::Rgb(150, 150, 150))) .alignment(Alignment::Center);
f.render_widget(no_results, chunks[1]);
} else {
let items: Vec<ListItem> = state
.search_results
.iter()
.enumerate()
.map(|(i, line)| {
ListItem::new(Line::from(vec![
Span::styled(
format!("{:02}. ", i + 1),
Style::default().fg(Color::Rgb(150, 150, 150)), ),
Span::styled(line, Style::default().fg(Color::Rgb(220, 220, 220))), ]))
})
.collect();
let results_list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(180, 255, 180))) .title(Span::styled(
"📜 Search Results 📜",
Style::default()
.fg(Color::Rgb(180, 255, 180)) .add_modifier(Modifier::BOLD),
)),
)
.highlight_style(
Style::default()
.bg(Color::Rgb(180, 255, 180)) .fg(Color::Rgb(60, 60, 80)) .add_modifier(Modifier::BOLD),
)
.highlight_symbol("-> ");
f.render_stateful_widget(results_list, chunks[1], &mut state.result_list_state);
}
}
fn render_content(f: &mut Frame, area: Rect, state: &AppState) {
let content = if state.file_content.is_empty() {
"🚫 No file loaded. Load a file to see its content here. 🚫".to_string()
} else {
state
.file_content
.lines()
.take(50)
.collect::<Vec<_>>()
.join("\n")
};
let paragraph = Paragraph::new(content)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(200, 220, 255))) .title(Span::styled(
"📄 File Content (First 50 lines) 📄",
Style::default()
.fg(Color::Rgb(200, 220, 255)) .add_modifier(Modifier::BOLD),
)),
)
.wrap(Wrap { trim: true })
.style(Style::default().fg(Color::Rgb(210, 210, 210)));
f.render_widget(paragraph, area);
}
fn render_search_input(f: &mut Frame, state: &AppState) {
let popup_area = centered_rect(60, 25, f.area());
f.render_widget(Clear, popup_area);
let input_text = format!("Search 🔎: {}_", state.search_query);
let input = Paragraph::new(input_text)
.style(
Style::default()
.fg(Color::Rgb(255, 220, 180)) .bg(Color::Rgb(60, 60, 80)), )
.block(
Block::default()
.borders(Borders::ALL)
.title(Span::styled(
"✏️ Enter Search Query ✏️",
Style::default()
.fg(Color::Rgb(255, 180, 220)) .add_modifier(Modifier::BOLD),
))
.border_style(Style::default().fg(Color::Rgb(255, 180, 220))), )
.alignment(Alignment::Center);
f.render_widget(input, popup_area);
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}