use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph};
use crate::tui::app::App;
use crate::tui::state::Pane;
pub fn render(f: &mut Frame, app: &App, area: Rect) {
if app.active_pane == Pane::TreeSearch {
let line = Line::from(vec![
Span::styled("/", Style::default().fg(Color::Yellow)),
Span::raw(app.detail_tree.search_query.as_str()),
]);
f.render_widget(Paragraph::new(line), area);
f.set_cursor_position((
area.x + 1 + app.detail_tree.search_query.len() as u16,
area.y,
));
if !app.detail_tree.search_completions.is_empty() {
let max_items = 8;
let completions = &app.detail_tree.search_completions;
let n = completions.len().min(max_items) as u16;
let dropdown_height = n + 2;
let max_label_width = completions
.iter()
.take(max_items)
.map(|c| c.label.len())
.max()
.unwrap_or(0) as u16;
let dropdown_width = (max_label_width + 3)
.max(20)
.min(area.width.saturating_sub(1));
let dropdown_area = Rect {
x: area.x + 1,
y: area.y.saturating_sub(dropdown_height),
width: dropdown_width,
height: dropdown_height,
};
f.render_widget(Clear, dropdown_area);
let items: Vec<ListItem> = completions
.iter()
.take(max_items)
.map(|c| ListItem::new(c.label.as_str()))
.collect();
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow)),
)
.highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(Color::Yellow),
);
let mut state = ListState::default();
state.select(Some(app.detail_tree.search_completion_selected));
f.render_stateful_widget(list, dropdown_area, &mut state);
}
return;
}
if app.active_pane == Pane::CommandMode {
if let Some(cmd) = &app.command {
let line = Line::from(vec![
Span::styled(":", Style::default().fg(Color::Cyan)),
Span::raw(cmd.buf.input.as_str()),
]);
f.render_widget(Paragraph::new(line), area);
f.set_cursor_position((area.x + 1 + cmd.buf.cursor as u16, area.y));
}
return;
}
if app.active_pane == Pane::YankPrompt {
let line = Line::from(vec![Span::styled(
"Copy as: [t]ext / [h]ex",
Style::default().fg(Color::Yellow),
)]);
f.render_widget(Paragraph::new(line), area);
return;
}
if app.active_pane != Pane::FilterInput {
if let Some(msg) = &app.detail_tree.yank_message {
let line = Line::from(Span::styled(
msg.as_str(),
Style::default().fg(Color::Green),
));
f.render_widget(Paragraph::new(line), area);
return;
}
if !app.filter.applied.is_empty() {
let line = Line::from(vec![
Span::styled("/", Style::default().fg(Color::DarkGray)),
Span::styled(
app.filter.applied.as_str(),
Style::default().fg(Color::DarkGray),
),
]);
f.render_widget(Paragraph::new(line), area);
} else if !app.detail_tree.search_query.is_empty() {
let line = Line::from(vec![
Span::styled("/", Style::default().fg(Color::DarkGray)),
Span::styled(
app.detail_tree.search_query.as_str(),
Style::default().fg(Color::DarkGray),
),
]);
f.render_widget(Paragraph::new(line), area);
}
return;
}
if let Some(ref err) = app.filter.error_message {
let line = Line::from(Span::styled(err.as_str(), Style::default().fg(Color::Red)));
f.render_widget(Paragraph::new(line), area);
f.set_cursor_position((area.x + 1 + app.filter.buf.cursor as u16, area.y));
return;
}
let line = Line::from(vec![
Span::styled("/", Style::default().fg(Color::Cyan)),
Span::raw(app.filter.buf.input.as_str()),
]);
f.render_widget(Paragraph::new(line), area);
f.set_cursor_position((area.x + 1 + app.filter.buf.cursor as u16, area.y));
if app.filter.completion_visible && !app.filter.completions.is_empty() {
let max_items = 8;
let n = app.filter.completions.len().min(max_items) as u16;
let dropdown_height = n + 2;
let max_label_width = app
.filter
.completions
.iter()
.take(max_items)
.map(|c| c.label.len())
.max()
.unwrap_or(0) as u16;
let dropdown_width = (max_label_width + 3)
.max(20)
.min(area.width.saturating_sub(1));
let dropdown_area = Rect {
x: area.x + 1,
y: area.y.saturating_sub(dropdown_height),
width: dropdown_width,
height: dropdown_height,
};
f.render_widget(Clear, dropdown_area);
let items: Vec<ListItem> = app
.filter
.completions
.iter()
.take(max_items)
.map(|c| ListItem::new(c.label.as_str()))
.collect();
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
)
.highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(Color::Cyan),
);
let mut state = ListState::default();
state.select(Some(app.filter.completion_selected));
f.render_stateful_widget(list, dropdown_area, &mut state);
}
}
#[cfg(all(test, feature = "tui"))]
mod tests {
use super::*;
use crate::tui::state::Pane;
use crate::tui::test_util::{make_test_app, render_to_string};
fn last_line(dump: &str) -> String {
dump.lines().last().unwrap_or("").to_string()
}
#[test]
fn command_line_default_empty() {
let app = make_test_app(1);
let dump = render_to_string(60, 5, |f| {
let area = Rect {
x: 0,
y: 4,
width: 60,
height: 1,
};
render(f, &app, area);
});
let line = last_line(&dump);
assert!(
line.trim().is_empty(),
"expected blank command line, got: {line:?}"
);
}
#[test]
fn command_line_shows_applied_filter_dim() {
let mut app = make_test_app(1);
app.filter.applied = "udp".into();
let dump = render_to_string(60, 5, |f| {
let area = Rect {
x: 0,
y: 4,
width: 60,
height: 1,
};
render(f, &app, area);
});
assert!(last_line(&dump).contains("/udp"), "dump: {dump}");
}
#[test]
fn command_line_filter_input_shows_slash() {
let mut app = make_test_app(1);
app.active_pane = Pane::FilterInput;
app.filter.buf.input = "tcp".into();
app.filter.buf.cursor = 3;
let dump = render_to_string(60, 5, |f| {
let area = Rect {
x: 0,
y: 4,
width: 60,
height: 1,
};
render(f, &app, area);
});
assert!(last_line(&dump).contains("/tcp"), "dump: {dump}");
}
#[test]
fn command_line_command_mode_shows_colon() {
let mut app = make_test_app(1);
app.active_pane = Pane::CommandMode;
let mut buf = crate::tui::cursor::CursorBuffer::new();
buf.input = "w".into();
buf.cursor = 1;
app.command = Some(crate::tui::state::CommandState { buf });
let dump = render_to_string(60, 5, |f| {
let area = Rect {
x: 0,
y: 4,
width: 60,
height: 1,
};
render(f, &app, area);
});
assert!(last_line(&dump).contains(":w"), "dump: {dump}");
}
#[test]
fn command_line_yank_prompt() {
let mut app = make_test_app(1);
app.active_pane = Pane::YankPrompt;
let dump = render_to_string(60, 5, |f| {
let area = Rect {
x: 0,
y: 4,
width: 60,
height: 1,
};
render(f, &app, area);
});
assert!(last_line(&dump).contains("Copy as:"), "dump: {dump}");
}
}