mod help;
mod input_bar;
mod port_selector;
pub mod settings;
mod status_bar;
mod terminal_view;
mod quicksend_bar;
mod macro_selector;
mod filter_popup;
use ratatui::prelude::*;
use ratatui::text::{Line, Span};
use crate::app::{App, Mode};
use crate::theme::Theme;
pub fn render(app: &mut App, frame: &mut Frame) {
let area = frame.area();
let has_search = app.mode == Mode::Search || !app.search.query.is_empty();
let has_quicksend = !app.quicksend.is_empty();
let mut constraints = vec![
Constraint::Length(1), ];
constraints.push(Constraint::Min(3)); if has_quicksend {
constraints.push(Constraint::Length(1)); }
if has_search {
constraints.push(Constraint::Length(1)); }
constraints.push(Constraint::Length(1)); constraints.push(Constraint::Length(1));
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
let mut idx = 0;
let r = chunks[idx];
app.layout.status_bar = (r.x, r.y, r.width, r.height);
status_bar::render(app, frame, chunks[idx]);
idx += 1;
let r = chunks[idx];
app.layout.terminal_view = (r.x, r.y, r.width, r.height);
terminal_view::render(app, frame, chunks[idx]);
idx += 1;
if has_quicksend {
quicksend_bar::render(app, frame, chunks[idx]);
idx += 1;
}
if has_search {
render_search_bar(app, frame, chunks[idx]);
idx += 1;
}
let r = chunks[idx];
app.layout.input_bar = (r.x, r.y, r.width, r.height);
input_bar::render(app, frame, chunks[idx]);
idx += 1;
render_help_hints(app, frame, chunks[idx]);
match app.mode {
Mode::PortSelect => {
port_selector::render(app, frame, area);
}
Mode::Settings => {
settings::render(app, frame, area);
}
Mode::Help => {
help::render(app, frame, area);
}
Mode::MacroSelect => {
macro_selector::render(app, frame, area);
}
Mode::Filter => {
filter_popup::render(app, frame, area);
}
_ => {}
}
}
fn render_search_bar(app: &App, frame: &mut Frame, area: Rect) {
let is_active = app.mode == Mode::Search;
let style = if is_active {
Theme::input_bar_active()
} else {
Theme::input_bar()
};
let status = app.search.match_status();
let spans = vec![
Span::styled(" /", Theme::help_key()),
Span::styled(&app.search.query, style),
Span::styled(" ", style),
Span::styled(status, Theme::timestamp()),
];
let line = Line::from(spans);
let paragraph = ratatui::widgets::Paragraph::new(line).style(style);
frame.render_widget(paragraph, area);
if is_active {
frame.set_cursor_position((
area.x + 2 + app.search.query.len() as u16,
area.y,
));
}
}
fn render_help_hints(app: &App, frame: &mut Frame, area: Rect) {
let hints = match app.mode {
Mode::Normal => vec![
Span::styled("i", Theme::help_key()),
Span::styled(": input ", Theme::help_bar()),
Span::styled("j/k", Theme::help_key()),
Span::styled(": scroll ", Theme::help_bar()),
Span::styled("/", Theme::help_key()),
Span::styled(": search ", Theme::help_bar()),
Span::styled("h", Theme::help_key()),
Span::styled(": hex ", Theme::help_bar()),
Span::styled("p", Theme::help_key()),
Span::styled(": ports ", Theme::help_bar()),
Span::styled("s", Theme::help_key()),
Span::styled(": settings ", Theme::help_bar()),
Span::styled("l", Theme::help_key()),
Span::styled(": log ", Theme::help_bar()),
Span::styled("?", Theme::help_key()),
Span::styled(": help", Theme::help_bar()),
],
Mode::Input => vec![
Span::styled("Enter", Theme::help_key()),
Span::styled(": send ", Theme::help_bar()),
Span::styled("↑/↓", Theme::help_key()),
Span::styled(": history ", Theme::help_bar()),
Span::styled("PgUp/Dn", Theme::help_key()),
Span::styled(": scroll ", Theme::help_bar()),
Span::styled("^P", Theme::help_key()),
Span::styled(": ports ", Theme::help_bar()),
Span::styled("^S", Theme::help_key()),
Span::styled(": settings ", Theme::help_bar()),
Span::styled("Esc", Theme::help_key()),
Span::styled(": browse", Theme::help_bar()),
],
Mode::Search => vec![
Span::styled("Enter", Theme::help_key()),
Span::styled(": confirm ", Theme::help_bar()),
Span::styled("↑/↓", Theme::help_key()),
Span::styled(": prev/next ", Theme::help_bar()),
Span::styled("Esc", Theme::help_key()),
Span::styled(": cancel", Theme::help_bar()),
],
Mode::PortSelect => vec![
Span::styled("Enter", Theme::help_key()),
Span::styled(": connect ", Theme::help_bar()),
Span::styled("j/k", Theme::help_key()),
Span::styled(": navigate ", Theme::help_bar()),
Span::styled("r", Theme::help_key()),
Span::styled(": refresh ", Theme::help_bar()),
Span::styled("Esc", Theme::help_key()),
Span::styled(": close", Theme::help_bar()),
],
Mode::Settings => vec![
Span::styled("↑/↓", Theme::help_key()),
Span::styled(": select ", Theme::help_bar()),
Span::styled("←/→", Theme::help_key()),
Span::styled(": change ", Theme::help_bar()),
Span::styled("Enter", Theme::help_key()),
Span::styled(": apply ", Theme::help_bar()),
Span::styled("Esc", Theme::help_key()),
Span::styled(": cancel", Theme::help_bar()),
],
Mode::Help => vec![
Span::styled("Esc", Theme::help_key()),
Span::styled(": close help", Theme::help_bar()),
],
Mode::MacroSelect => vec![
Span::styled("Enter", Theme::help_key()),
Span::styled(": run ", Theme::help_bar()),
Span::styled("j/k", Theme::help_key()),
Span::styled(": navigate ", Theme::help_bar()),
Span::styled("Esc", Theme::help_key()),
Span::styled(": close", Theme::help_bar()),
],
Mode::Filter => vec![
Span::styled("Enter", Theme::help_key()),
Span::styled(": add filter ", Theme::help_bar()),
Span::styled("Tab", Theme::help_key()),
Span::styled(": ±mode ", Theme::help_bar()),
Span::styled("d", Theme::help_key()),
Span::styled(": delete ", Theme::help_bar()),
Span::styled("Esc", Theme::help_key()),
Span::styled(": close", Theme::help_bar()),
],
};
let rx = format_bytes(app.total_rx_bytes());
let tx = format_bytes(app.total_tx_bytes());
let mut prefix = format!(" RX: {} TX: {}", rx, tx);
if app.logger.is_active {
prefix.push_str(" ●REC");
}
prefix.push_str(" │ ");
let mut line_spans = vec![
Span::styled(prefix, Theme::help_bar()),
];
line_spans.extend(hints);
let line = Line::from(line_spans);
let paragraph = ratatui::widgets::Paragraph::new(line)
.style(Theme::help_bar());
frame.render_widget(paragraph, area);
}
fn format_bytes(bytes: u64) -> String {
if bytes < 1024 {
format!("{}B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1}KB", bytes as f64 / 1024.0)
} else {
format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
}
}