use crate::tui::app::App;
use crate::tui::help_text;
use crate::tui::theme::Theme;
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{
Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap,
};
use super::util::popup_area;
pub fn render_help_popup(frame: &mut Frame, app: &App, area: Rect) {
let popup_area = popup_area(area, 70, 80, 40, 10);
let theme = &app.theme;
let keybindings = &app.keybindings;
frame.render_widget(Clear, popup_area);
let help_lines = help_text::build_help_text(keybindings, theme);
let help_text_len = help_lines.len();
let paragraph = Paragraph::new(help_lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.modal_border()))
.title(" Help ")
.style(Style::default().bg(theme.modal_bg())),
)
.wrap(Wrap { trim: false })
.scroll((app.help_scroll, 0));
frame.render_widget(paragraph, popup_area);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.thumb_symbol("┃")
.track_symbol(Some("│"))
.style(Style::default().fg(theme.modal_border()));
let mut scrollbar_state = ScrollbarState::new(help_text_len).position(app.help_scroll as usize);
frame.render_stateful_widget(
scrollbar,
popup_area.inner(ratatui::layout::Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
pub fn render_link_picker(frame: &mut Frame, app: &App, area: Rect) {
use crate::parser::LinkTarget;
let theme = &app.theme;
let popup_area = popup_area(area, 80, 60, 30, 8);
frame.render_widget(Clear, popup_area);
let header_text = if app.link_picker.active || !app.link_picker.query.is_empty() {
format!(
"Links ({}/{}) - /: search, Enter: follow, Esc: {}",
app.link_picker.filtered_indices.len(),
app.links_in_view.len(),
if app.link_picker.active {
"stop search"
} else {
"cancel"
}
)
} else {
format!(
"Links in this section ({} found) - /: search, Tab/j/k: navigate, Enter: follow",
app.links_in_view.len()
)
};
let mut lines = vec![Line::from(vec![Span::styled(
header_text,
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)])];
if app.link_picker.active || !app.link_picker.query.is_empty() {
let search_style = if app.link_picker.active {
Style::default()
.fg(theme.modal_selected_fg())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.modal_description())
};
let cursor = if app.link_picker.active { "▌" } else { "" };
lines.push(Line::from(vec![
Span::styled("Search: ", Style::default().fg(theme.modal_key_fg())),
Span::styled(format!("{}{}", app.link_picker.query, cursor), search_style),
]));
}
lines.push(Line::from(""));
let mut selected_line_start: u16 = 0;
for (display_idx, &real_idx) in app.link_picker.filtered_indices.iter().enumerate() {
let link = &app.links_in_view[real_idx];
let is_selected = app.link_picker.selected == Some(display_idx);
if is_selected {
selected_line_start = lines.len() as u16;
}
let number = format!("[{}] ", real_idx + 1);
let link_text = &link.text;
let target_str = match &link.target {
LinkTarget::Anchor(a) => format!("#{}", a),
LinkTarget::RelativeFile { path, anchor } => {
if let Some(a) = anchor {
format!("{}#{}", path.display(), a)
} else {
path.display().to_string()
}
}
LinkTarget::WikiLink { target, .. } => format!("[[{}]]", target),
LinkTarget::External(url) => {
if url.len() > 50 {
format!("{}...", &url[..47])
} else {
url.clone()
}
}
};
if is_selected {
lines.push(Line::from(vec![
Span::styled(
"▶ ",
Style::default()
.fg(theme.modal_selected_marker())
.add_modifier(Modifier::BOLD),
),
Span::styled(
number,
Style::default()
.fg(theme.modal_key_fg())
.add_modifier(Modifier::BOLD),
),
Span::styled(
link_text.clone(),
Style::default()
.fg(theme.selection_indicator_fg)
.bg(theme.modal_selected_fg())
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
),
Span::styled(
format!(" → {}", target_str),
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::BOLD),
),
]));
} else {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(number, Style::default().fg(theme.modal_description())),
Span::styled(link_text.clone(), Style::default().fg(theme.modal_text())),
Span::styled(
format!(" → {}", target_str),
Style::default().fg(theme.modal_description()),
),
]));
}
if display_idx < app.link_picker.filtered_indices.len() - 1 {
lines.push(Line::from(""));
}
}
if app.link_picker.filtered_indices.is_empty() && !app.links_in_view.is_empty() {
lines.push(Line::from(vec![Span::styled(
"No links match your search",
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
}
lines.push(Line::from(""));
let footer_text = if app.link_picker.active {
"Type to filter • Enter: select • Esc: stop search • Backspace: delete"
} else {
"Tab/j/k: Navigate • /: Search • 1-9: Jump • p: Parent • Enter: Follow • Esc: Cancel"
};
lines.push(Line::from(vec![Span::styled(
footer_text,
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
let total_lines = lines.len();
let inner_height = popup_area.height.saturating_sub(2) as usize; let header_lines = 2; let footer_lines = 2;
let visible_area = inner_height.saturating_sub(header_lines + footer_lines);
let scroll_offset = if selected_line_start > 0 && visible_area > 0 {
let target_line = selected_line_start.saturating_sub(header_lines as u16);
let center_offset = (visible_area / 2) as u16;
if target_line > center_offset {
let max_scroll = (total_lines.saturating_sub(inner_height)) as u16;
(target_line.saturating_sub(center_offset)).min(max_scroll)
} else {
0
}
} else {
0
};
let paragraph = Paragraph::new(lines.clone())
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.modal_border()))
.title(" Link Navigator ")
.style(Style::default().bg(theme.modal_bg())),
)
.wrap(Wrap { trim: false })
.scroll((scroll_offset, 0));
frame.render_widget(paragraph, popup_area);
if total_lines > inner_height {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.thumb_symbol("┃")
.track_symbol(Some("│"))
.style(Style::default().fg(theme.modal_border()));
let mut scrollbar_state = ScrollbarState::new(total_lines).position(scroll_offset as usize);
frame.render_stateful_widget(
scrollbar,
popup_area.inner(ratatui::layout::Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
}
pub fn render_theme_picker(frame: &mut Frame, app: &App, area: Rect) {
use crate::tui::theme::ThemeName;
let theme = &app.theme;
let themes = [
(
ThemeName::OceanDark,
"Ocean Dark",
"Base16 Ocean with cool blues",
),
(ThemeName::Nord, "Nord", "Arctic, north-bluish palette"),
(
ThemeName::Dracula,
"Dracula",
"Dark theme with vibrant colors",
),
(
ThemeName::Solarized,
"Solarized",
"Precision colors for machines and people",
),
(
ThemeName::Monokai,
"Monokai",
"Sublime Text's iconic scheme",
),
(ThemeName::Gruvbox, "Gruvbox", "Retro groove color scheme"),
(
ThemeName::TokyoNight,
"Tokyo Night",
"Modern night theme for low-light",
),
(
ThemeName::CatppuccinMocha,
"Catppuccin Mocha",
"Soothing pastel theme for night coding",
),
];
let popup_area = popup_area(area, 60, 50, 35, 12);
frame.render_widget(Clear, popup_area);
let mut lines = vec![
Line::from(vec![Span::styled(
"Select Theme (j/k: preview, Enter: save, Esc: cancel)",
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]),
Line::from(""),
];
for (idx, (theme_name, name, description)) in themes.iter().enumerate() {
let is_selected = idx == app.theme_picker_selected;
let saved_theme = app.theme_picker_original.unwrap_or(app.current_theme);
let is_saved = *theme_name == saved_theme;
let (prefix, style) = if is_selected {
(
"▶ ",
Style::default()
.fg(theme.modal_selected_fg())
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(theme.modal_text()))
};
let current_marker = if is_saved { " ✓" } else { "" };
let line_text = format!("{}{}{}", prefix, name, current_marker);
lines.push(Line::from(vec![Span::styled(line_text, style)]));
if is_selected {
lines.push(Line::from(vec![Span::styled(
format!(" {}", description),
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
}
}
lines.push(Line::from(""));
let paragraph = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.modal_border()))
.title(" Theme Selector ")
.style(Style::default().bg(theme.modal_bg())),
)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, popup_area);
}
pub fn render_cell_edit_overlay(frame: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let min_width = 20u16;
let min_height = 5u16;
let width = (area.width / 2).max(min_width).min(area.width);
let height = min_height.min(area.height);
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
let edit_area = Rect {
x,
y,
width,
height,
};
frame.render_widget(Clear, edit_area);
let edit_text = format!(
"Edit Cell [{},{}]: {}_",
app.cell_edit_row, app.cell_edit_col, app.cell_edit_value
);
let paragraph = Paragraph::new(vec![
Line::from(vec![Span::styled(
"Edit Table Cell",
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![Span::styled(
edit_text,
Style::default().fg(Color::White),
)]),
Line::from(""),
Line::from(vec![Span::styled(
"Enter: Save • Esc: Cancel",
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]),
])
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.modal_border()))
.style(Style::default().bg(theme.modal_bg())),
);
frame.render_widget(paragraph, edit_area);
}
pub fn render_file_create_confirm(frame: &mut Frame, message: &str, theme: &Theme) {
let area = popup_area(frame.area(), 50, 20, 30, 7);
frame.render_widget(Clear, area);
let text = vec![
Line::from(vec![Span::styled(
"Create File?",
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![Span::styled(
message,
Style::default().fg(theme.modal_text()),
)]),
Line::from(""),
Line::from(vec![
Span::styled("[y]", Style::default().fg(theme.modal_key_fg())),
Span::styled(
" Create file ",
Style::default().fg(theme.modal_description()),
),
Span::styled("[n/Esc]", Style::default().fg(theme.modal_key_fg())),
Span::styled(" Cancel", Style::default().fg(theme.modal_description())),
]),
];
let paragraph = Paragraph::new(text)
.alignment(ratatui::layout::Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Confirm ")
.title_style(Style::default().fg(theme.modal_title()))
.border_style(Style::default().fg(theme.modal_border()))
.style(Style::default().bg(theme.modal_bg())),
);
frame.render_widget(paragraph, area);
}
pub fn render_save_width_confirm(frame: &mut Frame, width: u16, theme: &Theme) {
let area = popup_area(frame.area(), 45, 18, 28, 7);
frame.render_widget(Clear, area);
let text = vec![
Line::from(vec![Span::styled(
"Save Outline Width",
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![Span::styled(
format!("Save width {}% to config file?", width),
Style::default().fg(theme.modal_text()),
)]),
Line::from(""),
Line::from(vec![
Span::styled("[y/Enter]", Style::default().fg(theme.modal_key_fg())),
Span::styled(" Save ", Style::default().fg(theme.modal_description())),
Span::styled("[n/Esc]", Style::default().fg(theme.modal_key_fg())),
Span::styled(" Cancel", Style::default().fg(theme.modal_description())),
]),
];
let paragraph = Paragraph::new(text)
.alignment(ratatui::layout::Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Confirm ")
.title_style(Style::default().fg(theme.modal_title()))
.border_style(Style::default().fg(theme.modal_border()))
.style(Style::default().bg(theme.modal_bg())),
);
frame.render_widget(paragraph, area);
}
pub fn render_save_before_quit_confirm(frame: &mut Frame, edit_count: usize, theme: &Theme) {
let area = popup_area(frame.area(), 56, 26, 30, 10);
frame.render_widget(Clear, area);
let text = vec![
Line::from(vec![Span::styled(
"Unsaved Changes",
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![Span::styled(
format!(
"You have {} unsaved change{}.",
edit_count,
if edit_count == 1 { "" } else { "s" }
),
Style::default().fg(theme.modal_text()),
)]),
Line::from(vec![Span::styled(
"Save before quitting?",
Style::default().fg(theme.modal_text()),
)]),
Line::from(""),
Line::from(vec![
Span::styled("[y/Enter]", Style::default().fg(theme.modal_key_fg())),
Span::styled(
" Save & Quit",
Style::default().fg(theme.modal_description()),
),
]),
Line::from(vec![
Span::styled("[q]", Style::default().fg(theme.modal_key_fg())),
Span::styled(
" Quit without saving",
Style::default().fg(theme.modal_description()),
),
]),
Line::from(vec![
Span::styled("[Esc]", Style::default().fg(theme.modal_key_fg())),
Span::styled(" Cancel", Style::default().fg(theme.modal_description())),
]),
];
let paragraph = Paragraph::new(text)
.alignment(ratatui::layout::Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Confirm Quit ")
.title_style(Style::default().fg(theme.modal_title()))
.border_style(Style::default().fg(theme.modal_border()))
.style(Style::default().bg(theme.modal_bg())),
);
frame.render_widget(paragraph, area);
}
pub fn render_save_before_nav_confirm(frame: &mut Frame, edit_count: usize, theme: &Theme) {
let area = popup_area(frame.area(), 58, 28, 32, 11);
frame.render_widget(Clear, area);
let text = vec![
Line::from(vec![Span::styled(
"Unsaved Changes",
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![Span::styled(
format!(
"You have {} unsaved change{}.",
edit_count,
if edit_count == 1 { "" } else { "s" }
),
Style::default().fg(theme.modal_text()),
)]),
Line::from(vec![Span::styled(
"Save before navigating?",
Style::default().fg(theme.modal_text()),
)]),
Line::from(""),
Line::from(vec![
Span::styled("[y/Enter]", Style::default().fg(theme.modal_key_fg())),
Span::styled(
" Save & Navigate",
Style::default().fg(theme.modal_description()),
),
]),
Line::from(vec![
Span::styled("[d]", Style::default().fg(theme.modal_key_fg())),
Span::styled(
" Discard & Navigate",
Style::default().fg(theme.modal_description()),
),
]),
Line::from(vec![
Span::styled("[q]", Style::default().fg(theme.modal_key_fg())),
Span::styled(
" Discard & Quit",
Style::default().fg(theme.modal_description()),
),
]),
Line::from(vec![
Span::styled("[Esc]", Style::default().fg(theme.modal_key_fg())),
Span::styled(" Cancel", Style::default().fg(theme.modal_description())),
]),
];
let paragraph = Paragraph::new(text)
.alignment(ratatui::layout::Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Confirm Navigation ")
.title_style(Style::default().fg(theme.modal_title()))
.border_style(Style::default().fg(theme.modal_border()))
.style(Style::default().bg(theme.modal_bg())),
);
frame.render_widget(paragraph, area);
}
pub fn render_command_palette(frame: &mut Frame, app: &App, theme: &Theme) {
use crate::tui::app::PALETTE_COMMANDS;
let area = popup_area(frame.area(), 60, 50, 35, 10);
frame.render_widget(Clear, area);
let mut lines = vec![
Line::from(vec![Span::styled(
"Command Palette",
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![
Span::styled(": ", Style::default().fg(theme.modal_key_fg())),
Span::styled(
&app.command_palette.query,
Style::default().fg(theme.modal_text()),
),
Span::styled(" ", Style::default().bg(Color::White)), ]),
Line::from(""),
];
if app.command_palette.filtered.is_empty() {
lines.push(Line::from(vec![Span::styled(
" No matching commands",
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
} else {
for (display_idx, &cmd_idx) in app.command_palette.filtered.iter().enumerate() {
let cmd = &PALETTE_COMMANDS[cmd_idx];
let is_selected = display_idx == app.command_palette.selected;
let prefix = if is_selected { "▸ " } else { " " };
let style = if is_selected {
Style::default()
.fg(theme.modal_selected_marker())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.modal_text())
};
let mut spans = vec![Span::styled(prefix, style), Span::styled(cmd.name, style)];
if !cmd.aliases.is_empty() {
let aliases_str = format!(" ({})", cmd.aliases.join(", "));
spans.push(Span::styled(
aliases_str,
Style::default().fg(theme.modal_description()),
));
}
lines.push(Line::from(spans));
if is_selected {
lines.push(Line::from(vec![Span::styled(
format!(" {}", cmd.description),
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
}
}
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("↑↓/Tab", Style::default().fg(theme.modal_key_fg())),
Span::styled(
" navigate ",
Style::default().fg(theme.modal_description()),
),
Span::styled("Enter", Style::default().fg(theme.modal_key_fg())),
Span::styled(" execute ", Style::default().fg(theme.modal_description())),
Span::styled("Esc", Style::default().fg(theme.modal_key_fg())),
Span::styled(" cancel", Style::default().fg(theme.modal_description())),
]));
let paragraph = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.modal_border()))
.style(Style::default().bg(theme.modal_bg())),
);
frame.render_widget(paragraph, area);
}
pub fn render_file_picker(frame: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let popup_area = popup_area(area, 80, 60, 30, 8);
frame.render_widget(Clear, popup_area);
let file_count = app.file_picker.filtered_file_indices.len();
let dir_count = app.file_picker.filtered_dir_indices.len();
let total_items = app.file_picker_item_count();
let effective_dir = app.effective_picker_dir();
let dir_label = effective_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(".")
.to_string();
let header_text = if app.file_picker.active || !app.file_picker.query.is_empty() {
format!(
"{}: {}/{} files, {} dirs - Enter: open, Esc: {}",
dir_label,
file_count,
app.file_picker.files.len(),
dir_count,
if app.file_picker.active {
"stop search"
} else {
"cancel"
}
)
} else {
format!(
"{}: {} files, {} dirs",
dir_label,
app.file_picker.files.len(),
app.file_picker.dirs.len()
)
};
let mut lines = vec![Line::from(vec![Span::styled(
header_text,
Style::default()
.fg(theme.modal_title())
.add_modifier(Modifier::BOLD),
)])];
if app.file_picker.active || !app.file_picker.query.is_empty() {
let search_style = if app.file_picker.active {
Style::default()
.fg(theme.modal_selected_fg())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.modal_description())
};
let cursor = if app.file_picker.active { "▌" } else { "" };
lines.push(Line::from(vec![
Span::styled("Filter: ", Style::default().fg(theme.modal_key_fg())),
Span::styled(format!("{}{}", app.file_picker.query, cursor), search_style),
]));
}
lines.push(Line::from(""));
let mut selected_line_start: u16 = 0;
for (display_idx, &real_idx) in app.file_picker.filtered_file_indices.iter().enumerate() {
let file_path = &app.file_picker.files[real_idx];
let is_selected = app.file_picker.selected == Some(display_idx);
let is_current = file_path == &app.current_file_path;
if is_selected {
selected_line_start = lines.len() as u16;
}
let filename = file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
let current_marker = if is_current { " [current]" } else { "" };
let number = format!("[{}] ", real_idx + 1);
if is_selected {
lines.push(Line::from(vec![
Span::styled(
"▶ ",
Style::default()
.fg(theme.selection_indicator_fg)
.add_modifier(Modifier::BOLD),
),
Span::styled(number, Style::default().fg(theme.modal_key_fg())),
Span::styled(
filename,
Style::default()
.fg(theme.selection_indicator_fg)
.bg(theme.modal_selected_fg())
.add_modifier(Modifier::BOLD),
),
Span::styled(
current_marker,
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
),
]));
} else {
let text_style = if is_current {
Style::default()
.fg(theme.modal_text())
.add_modifier(Modifier::ITALIC)
} else {
Style::default().fg(theme.modal_text())
};
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(number, Style::default().fg(theme.modal_description())),
Span::styled(filename, text_style),
Span::styled(
current_marker,
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
),
]));
}
}
if !app.file_picker.filtered_file_indices.is_empty()
&& !app.file_picker.filtered_dir_indices.is_empty()
{
lines.push(Line::from(vec![Span::styled(
"── Directories ──",
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
}
for (dir_display_idx, &real_dir_idx) in app.file_picker.filtered_dir_indices.iter().enumerate()
{
let combined_idx = file_count + dir_display_idx;
let dir_path = &app.file_picker.dirs[real_dir_idx];
let is_selected = app.file_picker.selected == Some(combined_idx);
if is_selected {
selected_line_start = lines.len() as u16;
}
let dirname = dir_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
let display_name = format!("[DIR] {}/", dirname);
if is_selected {
lines.push(Line::from(vec![
Span::styled(
"▶ ",
Style::default()
.fg(theme.selection_indicator_fg)
.add_modifier(Modifier::BOLD),
),
Span::styled(
display_name,
Style::default()
.fg(theme.selection_indicator_fg)
.bg(theme.modal_selected_fg())
.add_modifier(Modifier::BOLD),
),
]));
} else {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(display_name, Style::default().fg(theme.modal_description())),
]));
}
}
if total_items == 0 && (!app.file_picker.files.is_empty() || !app.file_picker.dirs.is_empty()) {
lines.push(Line::from(vec![Span::styled(
"No items match your search",
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
} else if total_items == 0 {
lines.push(Line::from(vec![Span::styled(
"Empty directory",
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
}
lines.push(Line::from(""));
let footer_text = if app.file_picker.active {
"Type to filter • Enter: select • Esc: stop search • Backspace: delete"
} else {
"j/k: Navigate • /: Filter • Enter: Open • Backspace: Parent dir • h: Hidden • Esc: Cancel"
};
lines.push(Line::from(vec![Span::styled(
footer_text,
Style::default()
.fg(theme.modal_description())
.add_modifier(Modifier::ITALIC),
)]));
let total_lines = lines.len();
let inner_height = popup_area.height.saturating_sub(2) as usize;
let header_lines = if app.file_picker.active || !app.file_picker.query.is_empty() {
3
} else {
2
};
let footer_lines = 2;
let visible_area = inner_height.saturating_sub(header_lines + footer_lines);
let scroll_offset = if selected_line_start > 0 && visible_area > 0 {
let target_line = selected_line_start.saturating_sub(header_lines as u16);
let center_offset = (visible_area / 2) as u16;
if target_line > center_offset {
let max_scroll = (total_lines.saturating_sub(inner_height)) as u16;
(target_line.saturating_sub(center_offset)).min(max_scroll)
} else {
0
}
} else {
0
};
let paragraph = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.modal_border()))
.title(" File Picker ")
.style(Style::default().bg(theme.modal_bg())),
);
frame.render_widget(paragraph.scroll((scroll_offset, 0)), popup_area);
if total_lines > inner_height {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.thumb_symbol("┃")
.track_symbol(Some("│"))
.style(Style::default().fg(theme.modal_border()));
let mut scrollbar_state = ScrollbarState::new(total_lines).position(scroll_offset as usize);
frame.render_stateful_widget(
scrollbar,
popup_area.inner(ratatui::layout::Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
}