use ratatui::{
Frame,
prelude::Rect,
style::Style,
text::{Line, Span},
widgets::{Block, BorderType, Borders, List, ListItem},
};
use crate::i18n;
use crate::state::AppState;
use crate::theme::theme;
pub fn render_recent(f: &mut Frame, app: &mut AppState, area: Rect) {
if !app.show_recent_pane || area.width == 0 {
app.recent_rect = None;
return;
}
let th = theme();
let recent_focused = matches!(app.focus, crate::state::Focus::Recent);
let recents = app.recent_values();
let rec_inds = crate::ui::helpers::filtered_recent_indices(app);
let rec_items: Vec<ListItem> = rec_inds
.iter()
.filter_map(|&i| recents.get(i))
.map(|s| {
ListItem::new(Span::styled(
s.clone(),
Style::default().fg(if recent_focused { th.text } else { th.subtext0 }),
))
})
.collect();
let recent_title = if recent_focused {
i18n::t(app, "app.titles.recent_focused")
} else {
i18n::t(app, "app.titles.recent")
};
let mut recent_title_spans: Vec<Span> = vec![Span::styled(
recent_title,
Style::default().fg(if recent_focused {
th.mauve
} else {
th.overlay1
}),
)];
if recent_focused && let Some(pat) = &app.pane_find {
recent_title_spans.push(Span::raw(" "));
recent_title_spans.push(Span::styled(
"/",
Style::default()
.fg(th.sapphire)
.add_modifier(ratatui::style::Modifier::BOLD),
));
recent_title_spans.push(Span::styled(pat.clone(), Style::default().fg(th.text)));
}
let rec_block = Block::default()
.title(Line::from(recent_title_spans))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(if recent_focused {
th.mauve
} else {
th.surface1
}));
let rec_list = List::new(rec_items)
.style(
Style::default()
.fg(if recent_focused { th.text } else { th.subtext0 })
.bg(th.base),
)
.block(rec_block)
.highlight_style(Style::default().fg(th.text).bg(th.surface2))
.highlight_symbol("â–¶ ");
f.render_stateful_widget(rec_list, area, &mut app.history_state);
app.recent_rect = Some((
area.x + 1,
area.y + 1,
area.width.saturating_sub(2),
area.height.saturating_sub(2),
));
}
pub fn render_news_recent(f: &mut Frame, app: &mut AppState, area: Rect) {
if !app.show_news_history_pane || area.width == 0 {
app.recent_rect = None;
if matches!(app.focus, crate::state::Focus::Recent) {
app.focus = crate::state::Focus::Search;
}
return;
}
let th = theme();
let recent_focused = matches!(app.focus, crate::state::Focus::Recent);
let recents = app.news_recent_values();
let rec_inds = crate::ui::helpers::filtered_recent_indices(app);
let rec_items: Vec<ListItem> = rec_inds
.iter()
.filter_map(|&i| recents.get(i))
.map(|s| {
ListItem::new(Span::styled(
s.clone(),
Style::default().fg(if recent_focused { th.text } else { th.subtext0 }),
))
})
.collect();
let recent_title = if recent_focused {
i18n::t(app, "app.titles.news_recent_focused")
} else {
i18n::t(app, "app.titles.news_recent")
};
let mut recent_title_spans: Vec<Span> = vec![Span::styled(
recent_title,
Style::default().fg(if recent_focused {
th.mauve
} else {
th.overlay1
}),
)];
if recent_focused && let Some(pat) = &app.pane_find {
recent_title_spans.push(Span::raw(" "));
recent_title_spans.push(Span::styled(
"/",
Style::default()
.fg(th.sapphire)
.add_modifier(ratatui::style::Modifier::BOLD),
));
recent_title_spans.push(Span::styled(pat.clone(), Style::default().fg(th.text)));
}
let rec_block = Block::default()
.title(Line::from(recent_title_spans))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(if recent_focused {
th.mauve
} else {
th.surface1
}));
let rec_list = List::new(rec_items)
.style(
Style::default()
.fg(if recent_focused { th.text } else { th.subtext0 })
.bg(th.base),
)
.block(rec_block)
.highlight_style(Style::default().fg(th.text).bg(th.surface2))
.highlight_symbol("â–¶ ");
f.render_stateful_widget(rec_list, area, &mut app.history_state);
app.recent_rect = Some((
area.x + 1,
area.y + 1,
area.width.saturating_sub(2),
area.height.saturating_sub(2),
));
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::{Terminal, backend::TestBackend};
fn init_test_translations(app: &mut crate::state::AppState) {
use std::collections::HashMap;
let mut translations = HashMap::new();
translations.insert(
"app.titles.recent".to_string(),
"Search history".to_string(),
);
translations.insert(
"app.titles.recent_focused".to_string(),
"Search history".to_string(),
);
app.translations = translations.clone();
app.translations_fallback = translations;
}
#[test]
fn recent_renders_and_records_rect_when_visible() {
let backend = TestBackend::new(100, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState::default();
init_test_translations(&mut app);
app.show_recent_pane = true;
app.load_recent_items(&["package1".to_string(), "package2".to_string()]);
term.draw(|f| {
let area = f.area();
render_recent(f, &mut app, area);
})
.expect("Failed to render recent pane");
assert!(app.recent_rect.is_some());
let (x, y, w, h) = app
.recent_rect
.expect("recent_rect should be Some after is_some() check");
assert_eq!(x, 1);
assert_eq!(y, 1);
assert_eq!(w, 98); assert_eq!(h, 28); }
#[test]
fn recent_clears_rect_when_hidden() {
let backend = TestBackend::new(100, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState::default();
init_test_translations(&mut app);
app.show_recent_pane = false;
app.recent_rect = Some((10, 10, 20, 20));
term.draw(|f| {
let area = f.area();
render_recent(f, &mut app, area);
})
.expect("Failed to render hidden recent pane");
assert!(app.recent_rect.is_none());
}
#[test]
fn recent_clears_rect_when_area_zero_width() {
let backend = TestBackend::new(100, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState::default();
init_test_translations(&mut app);
app.show_recent_pane = true;
term.draw(|f| {
let area = ratatui::prelude::Rect {
x: 0,
y: 0,
width: 0,
height: 10,
};
render_recent(f, &mut app, area);
})
.expect("Failed to render recent pane with zero width");
assert!(app.recent_rect.is_none());
}
#[test]
fn recent_displays_pane_find_when_focused() {
let backend = TestBackend::new(100, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState::default();
init_test_translations(&mut app);
app.show_recent_pane = true;
app.focus = crate::state::Focus::Recent;
app.pane_find = Some("test".to_string());
term.draw(|f| {
let area = f.area();
render_recent(f, &mut app, area);
})
.expect("Failed to render recent pane with pane find");
assert!(app.recent_rect.is_some());
}
#[test]
fn recent_hides_pane_find_when_unfocused() {
let backend = TestBackend::new(100, 30);
let mut term = Terminal::new(backend).expect("Failed to create terminal for test");
let mut app = crate::state::AppState::default();
init_test_translations(&mut app);
app.show_recent_pane = true;
app.focus = crate::state::Focus::Search;
app.pane_find = Some("test".to_string());
term.draw(|f| {
let area = f.area();
render_recent(f, &mut app, area);
})
.expect("Failed to render unfocused recent pane");
assert!(app.recent_rect.is_some());
}
}