use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{
Block, Borders, List, ListItem, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
Wrap,
},
};
use super::app::{AppMode, FocusState, IndexStatusState, InteractiveApp};
pub fn render(f: &mut Frame, app: &mut InteractiveApp) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Length(3), Constraint::Min(1), Constraint::Length(1), ])
.split(f.area());
render_header(f, chunks[0], &*app);
render_filters(f, chunks[1], &*app);
match app.mode() {
AppMode::Help => render_help_screen(f, chunks[2], app),
AppMode::FilePreview => render_file_preview(f, chunks[2], app),
AppMode::FilterSelector => {
render_results_area(f, chunks[2], app);
let theme = app.theme().clone();
if let Some(selector) = app.filter_selector_mut() {
selector.render(f, chunks[2], &theme);
}
}
AppMode::Indexing | AppMode::Normal => render_results_area(f, chunks[2], app),
}
render_footer(f, chunks[3], app);
}
fn render_header(f: &mut Frame, area: Rect, app: &InteractiveApp) {
let palette = &app.theme().palette;
let input_focused = matches!(app.focus_state(), FocusState::Input);
let title_left = if input_focused {
" Search [TYPING - Press Tab/Enter to navigate] "
} else {
" Search [Press Tab to focus, / to type] "
};
let main_status = match app.index_status() {
IndexStatusState::Ready { file_count, .. } => {
format!("✓ {} files", file_count)
}
IndexStatusState::Missing => "⚠ No index".to_string(),
IndexStatusState::Stale { files_changed, .. } => {
format!("⚠ {} changed", files_changed)
}
IndexStatusState::Indexing { current, total, .. } => {
format!("⏳ {}/{}", current, total)
}
};
let spinner_frames = ['◐', '◓', '◑', '◒'];
let frame_idx = (app.effects().frame() / 3) as usize % spinner_frames.len();
let spinner = spinner_frames[frame_idx];
let symbol_status = match app.index_status() {
IndexStatusState::Ready { symbol_status, .. }
| IndexStatusState::Stale { symbol_status, .. }
| IndexStatusState::Indexing { symbol_status, .. } => match symbol_status {
super::app::SymbolIndexingState::Running { processed, total } => {
let percent = if *total > 0 {
(*processed as f64 / *total as f64 * 100.0) as u32
} else {
0
};
format!(" │ {} {}% [Shift+I: clear]", spinner, percent)
}
super::app::SymbolIndexingState::Completed => {
" │ ✓ symbols [Shift+I: clear]".to_string()
}
super::app::SymbolIndexingState::Failed => " │ ⚠ symbols [Shift+I: clear]".to_string(),
super::app::SymbolIndexingState::NotStarted => {
" │ ⏳ symbols... [Shift+I: clear]".to_string()
}
},
IndexStatusState::Missing => String::new(),
};
let status_indicator = format!("{}{} ", main_status, symbol_status);
let available_width = area.width.saturating_sub(2) as usize; let title_len = title_left.chars().count();
let status_len = status_indicator.chars().count();
let spaces_needed = available_width.saturating_sub(title_len + status_len);
let spacing = " ".repeat(spaces_needed);
let title_spans = vec![
Span::raw(title_left),
Span::raw(spacing),
Span::styled(status_indicator, Style::default().fg(palette.muted)),
];
let input_block = Block::default()
.borders(Borders::ALL)
.title_top(Line::from(title_spans))
.border_style(if input_focused {
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(palette.muted)
});
let input_text = app.input().value();
let input_style = if input_focused {
Style::default()
.fg(palette.foreground)
.bg(Color::Rgb(40, 40, 40)) } else {
Style::default().fg(palette.foreground).bg(Color::Black) };
let input_paragraph = Paragraph::new(input_text)
.block(input_block)
.style(input_style)
.wrap(Wrap { trim: false });
f.render_widget(input_paragraph, area);
if input_focused {
let cursor_x = area.x + 1 + app.input().visual_cursor() as u16;
let cursor_y = area.y + 1;
f.set_cursor_position((cursor_x.min(area.right() - 2), cursor_y));
}
}
fn render_filters(f: &mut Frame, area: Rect, app: &InteractiveApp) {
let filters = app.filters().clone();
let palette = app.theme().palette.clone();
let filters_focused = matches!(app.focus_state(), FocusState::Filters);
let border_style = if filters_focused {
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(palette.muted)
};
let block = Block::default()
.borders(Borders::ALL)
.title(" Filters [s: symbols, r: regex, l: lang, k: kind, e: expand, c: contains] ")
.border_style(border_style);
let mut filter_spans = vec![];
let mut pos = 0; let inactive_style = Style::default()
.fg(palette.muted)
.bg(Color::Rgb(30, 30, 30));
let symbols_text = " [s] Symbols ";
let symbols_style = if filters.symbols_mode {
Style::default()
.fg(Color::Black)
.bg(palette.badge_active)
.add_modifier(Modifier::BOLD)
} else {
inactive_style
};
let symbols_start = pos;
filter_spans.push(Span::styled(symbols_text, symbols_style));
pos += symbols_text.len();
let symbols_end = pos;
filter_spans.push(Span::raw(" "));
pos += 2;
let regex_text = " [r] Regex ";
let regex_style = if filters.regex_mode {
Style::default()
.fg(Color::Black)
.bg(palette.warning)
.add_modifier(Modifier::BOLD)
} else {
inactive_style
};
let regex_start = pos;
filter_spans.push(Span::styled(regex_text, regex_style));
pos += regex_text.len();
let regex_end = pos;
filter_spans.push(Span::raw(" "));
pos += 2;
let lang_style = if filters.language.is_some() {
Style::default()
.fg(Color::Black)
.bg(palette.info)
.add_modifier(Modifier::BOLD)
} else {
inactive_style
};
let lang_text = if let Some(ref lang) = filters.language {
format!(" [l] Lang: {} ", lang)
} else {
" [l] Lang ".to_string()
};
let lang_start = pos;
filter_spans.push(Span::styled(lang_text.clone(), lang_style));
pos += lang_text.len();
let lang_end = pos;
filter_spans.push(Span::raw(" "));
pos += 2;
let kind_style = if filters.kind.is_some() {
Style::default()
.fg(Color::Black)
.bg(palette.info)
.add_modifier(Modifier::BOLD)
} else {
inactive_style
};
let kind_text = if let Some(ref kind) = filters.kind {
format!(" [k] Kind: {} ", kind)
} else {
" [k] Kind ".to_string()
};
let kind_start = pos;
filter_spans.push(Span::styled(kind_text.clone(), kind_style));
pos += kind_text.len();
let kind_end = pos;
filter_spans.push(Span::raw(" "));
pos += 2;
let expand_text = " [e] Expand ";
let expand_style = if filters.expand {
Style::default()
.fg(Color::Black)
.bg(Color::Rgb(150, 200, 100))
.add_modifier(Modifier::BOLD)
} else {
inactive_style
};
let expand_start = pos;
filter_spans.push(Span::styled(expand_text, expand_style));
pos += expand_text.len();
let expand_end = pos;
filter_spans.push(Span::raw(" "));
pos += 2;
let contains_text = " [c] Contains ";
let contains_style = if filters.contains {
Style::default()
.fg(Color::Black)
.bg(Color::Rgb(180, 120, 200))
.add_modifier(Modifier::BOLD)
} else {
inactive_style
};
let contains_start = pos;
filter_spans.push(Span::styled(contains_text, contains_style));
pos += contains_text.len();
let contains_end = pos;
let badge_positions = app.filter_badge_positions();
badge_positions.symbols.set((symbols_start, symbols_end));
badge_positions.regex.set((regex_start, regex_end));
badge_positions.language.set((lang_start, lang_end));
badge_positions.kind.set((kind_start, kind_end));
badge_positions.expand.set((expand_start, expand_end));
badge_positions.contains.set((contains_start, contains_end));
let paragraph = Paragraph::new(Line::from(filter_spans))
.block(block)
.style(Style::default().fg(palette.foreground).bg(Color::Black)) .wrap(Wrap { trim: false });
f.render_widget(paragraph, area);
}
fn render_results_area(f: &mut Frame, area: Rect, app: &InteractiveApp) {
let palette = &app.theme().palette;
if app.indexing() {
let modal_width = 50.min(area.width.saturating_sub(4));
let modal_height = 11; let modal_x = (area.width.saturating_sub(modal_width)) / 2;
let modal_y = (area.height.saturating_sub(modal_height)) / 2;
let modal_area = Rect::new(
area.x + modal_x,
area.y + modal_y,
modal_width,
modal_height,
);
let background_paragraph = Paragraph::new("")
.block(
Block::default()
.borders(Borders::ALL)
.title(" Results ")
.border_style(Style::default().fg(palette.muted)),
)
.style(Style::default().bg(Color::Black));
f.render_widget(background_paragraph, area);
let spinner_frames = ['◐', '◓', '◑', '◒'];
let frame_idx = (app.effects().frame() / 3) as usize % spinner_frames.len();
let spinner = spinner_frames[frame_idx];
let elapsed_secs = app.indexing_elapsed_secs().unwrap_or(0);
let elapsed_text = if elapsed_secs < 60 {
format!("{}s", elapsed_secs)
} else {
format!("{}m {}s", elapsed_secs / 60, elapsed_secs % 60)
};
let (current, total, percent, status_msg) = match app.index_status() {
crate::interactive::app::IndexStatusState::Indexing {
current,
total,
status,
..
} => {
let pct = if *total > 0 {
(*current as f64 / *total as f64 * 100.0) as u32
} else {
0
};
(*current, *total, pct, status.clone())
}
_ => (0, 0, 0, "Indexing...".to_string()),
};
let bar_width = 32;
let filled = if total > 0 {
((current as f64 / total as f64) * bar_width as f64) as usize
} else {
let pos = (app.effects().frame() / 2) as usize % bar_width;
pos.min(bar_width - 4)
};
let progress_bar = if total > 0 {
let filled_chars = "█".repeat(filled);
let empty_chars = "░".repeat(bar_width.saturating_sub(filled));
format!("{}{}", filled_chars, empty_chars)
} else {
let mut chars = vec!['░'; bar_width];
for i in 0..4 {
let pos = (filled + i) % bar_width;
chars[pos] = '█';
}
chars.iter().collect()
};
let status_line = if total > 0 {
format!(
"{}/{} files ({}%) • {}",
current, total, percent, elapsed_text
)
} else {
format!("Indexing... • {}", elapsed_text)
};
let loading_lines = vec![
Line::from(vec![
Span::raw(" "),
Span::styled(
spinner.to_string(),
Style::default()
.fg(Color::Rgb(255, 150, 0))
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Building index",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![Span::styled(
progress_bar,
Style::default().fg(palette.info),
)]),
Line::from(""),
Line::from(vec![Span::styled(
status_line,
Style::default().fg(palette.muted),
)]),
Line::from(""),
Line::from(vec![Span::styled(
status_msg,
Style::default().fg(Color::Rgb(150, 150, 150)),
)]),
];
let modal = Paragraph::new(loading_lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(
Style::default()
.fg(Color::Rgb(255, 150, 0))
.add_modifier(Modifier::BOLD),
)
.title(Line::from(vec![
Span::raw(" "),
Span::styled("📦", Style::default().fg(Color::Rgb(255, 200, 0))),
Span::raw(" Indexing "),
Span::styled("📦", Style::default().fg(Color::Rgb(255, 200, 0))),
Span::raw(" "),
]))
.style(Style::default().bg(Color::Rgb(25, 20, 30))),
)
.alignment(Alignment::Center);
f.render_widget(modal, modal_area);
return;
}
if app.searching() {
let modal_width = 50.min(area.width.saturating_sub(4));
let modal_height = 9;
let modal_x = (area.width.saturating_sub(modal_width)) / 2;
let modal_y = (area.height.saturating_sub(modal_height)) / 2;
let modal_area = Rect::new(
area.x + modal_x,
area.y + modal_y,
modal_width,
modal_height,
);
let background_paragraph = Paragraph::new("")
.block(
Block::default()
.borders(Borders::ALL)
.title(" Results ")
.border_style(Style::default().fg(palette.muted)),
)
.style(Style::default().bg(Color::Black));
f.render_widget(background_paragraph, area);
let spinner_frames = ['◐', '◓', '◑', '◒'];
let frame_idx = (app.effects().frame() / 3) as usize % spinner_frames.len();
let spinner = spinner_frames[frame_idx];
let loading_lines = vec![
Line::from(vec![
Span::raw(" "),
Span::styled(
spinner.to_string(),
Style::default()
.fg(Color::Rgb(0, 200, 255))
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Searching codebase",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
)]),
Line::from(""),
Line::from(vec![Span::styled(
"━━━━━━━━━━━━━━━━",
Style::default().fg(palette.info),
)]),
Line::from(""),
Line::from(vec![Span::styled(
"Hang tight...",
Style::default().fg(palette.muted),
)]),
];
let modal = Paragraph::new(loading_lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(
Style::default()
.fg(Color::Rgb(0, 200, 255))
.add_modifier(Modifier::BOLD),
)
.title(Line::from(vec![
Span::raw(" "),
Span::styled("⚡", Style::default().fg(Color::Rgb(255, 200, 0))),
Span::raw(" Loading "),
Span::styled("⚡", Style::default().fg(Color::Rgb(255, 200, 0))),
Span::raw(" "),
]))
.style(Style::default().bg(Color::Rgb(20, 20, 30))),
)
.alignment(Alignment::Center);
f.render_widget(modal, modal_area);
return;
}
if let Some(error) = app.error_message() {
let error_text = Paragraph::new(error)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Error ")
.border_style(Style::default().fg(palette.error)),
)
.style(Style::default().fg(palette.error))
.wrap(Wrap { trim: true });
f.render_widget(error_text, area);
return;
}
if let Some(info) = app.info_message() {
let info_text = Paragraph::new(info)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Info ")
.border_style(Style::default().fg(palette.info)),
)
.style(Style::default().fg(palette.info))
.wrap(Wrap { trim: true });
f.render_widget(info_text, area);
return;
}
let results = app.results();
if results.is_empty() {
let empty_message = if app.input().value().trim().is_empty() {
if matches!(app.focus_state(), FocusState::Input) {
"Start typing to search...\n\nKeyboard shortcuts:\n j/k or ↓/↑ - Navigate results\n / - Focus search\n Esc/Enter - Unfocus search\n ? - Show help\n q - Quit"
} else {
"Press / to start typing a search query\nPress ? for full help"
}
} else {
"No results found. Try a different query.\n\nTip: Press / to edit your search"
};
let empty_text = Paragraph::new(empty_message)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Results ")
.border_style(Style::default().fg(palette.muted)),
)
.style(Style::default().fg(palette.muted))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
f.render_widget(empty_text, area);
return;
}
let theme = app.theme().load_syntect_theme();
let visible_lines = area.height.saturating_sub(2) as usize;
let mut lines_used = 0;
let mut visible_results_count = 0;
const MAX_PREVIEW_LINES: usize = 20;
for result in results.results().iter().skip(results.scroll_offset()) {
let has_symbol = !matches!(result.kind, crate::models::SymbolKind::Unknown(_))
&& result.symbol.is_some();
let symbol_lines = if has_symbol { 1 } else { 0 };
let path_lines = 1;
let preview_lines = result.preview.lines().count().min(MAX_PREVIEW_LINES);
let total_lines = symbol_lines + path_lines + preview_lines;
if lines_used + total_lines <= visible_lines {
lines_used += total_lines;
visible_results_count += 1;
} else {
break;
}
}
results.set_visible_height(visible_results_count);
let items: Vec<ListItem> = results
.visible_results(visible_results_count)
.iter()
.enumerate()
.map(|(idx, result)| {
let global_idx = idx + results.scroll_offset();
let is_selected = global_idx == results.selected_index();
let relative_path = std::path::Path::new(&result.path)
.strip_prefix(app.cwd())
.ok()
.and_then(|p| p.to_str())
.map(|p| format!("./{}", p))
.unwrap_or_else(|| result.path.clone());
let mut lines = Vec::new();
let has_symbol = !matches!(result.kind, crate::models::SymbolKind::Unknown(_))
&& result.symbol.is_some();
if has_symbol {
let symbol_text = format!("[{}] {}", result.kind, result.symbol.as_ref().unwrap());
if is_selected {
lines.push(Line::from(symbol_text));
} else {
lines.push(Line::from(vec![Span::styled(
symbol_text,
Style::default().fg(Color::Rgb(200, 150, 255)), )]));
}
}
let file_line_text = format!("{}:{}", relative_path, result.span.start_line);
if is_selected {
lines.push(Line::from(file_line_text));
} else {
lines.push(Line::from(vec![Span::styled(
file_line_text,
Style::default().fg(palette.info), )]));
}
let preview_lines_vec: Vec<String> = result
.preview
.lines()
.take(MAX_PREVIEW_LINES)
.map(|s| s.to_string())
.collect();
if is_selected {
for line in preview_lines_vec {
lines.push(Line::from(format!(" {}", line)));
}
} else {
let highlighted =
super::syntax::highlight_code_lines(&preview_lines_vec, result.lang, &theme);
for highlighted_line in highlighted {
let mut line_spans = vec![Span::raw(" ")];
line_spans.extend(highlighted_line.spans);
lines.push(Line::from(line_spans));
}
}
if is_selected {
let style = Style::default()
.fg(Color::Black)
.bg(palette.highlight)
.add_modifier(Modifier::BOLD);
ListItem::new(lines).style(style)
} else {
ListItem::new(lines)
}
})
.collect();
let result_count = results.len();
let title = format!(" Results ({}) ", result_count);
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(Style::default().fg(palette.accent)),
);
f.render_widget(list, area);
if result_count > visible_results_count {
let mut scrollbar_state =
ScrollbarState::new(result_count).position(results.selected_index());
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("▲"))
.end_symbol(Some("▼"))
.track_symbol(Some("│"))
.thumb_symbol("█")
.style(Style::default().fg(palette.accent));
f.render_stateful_widget(
scrollbar,
area.inner(ratatui::layout::Margin {
horizontal: 0,
vertical: 1,
}),
&mut scrollbar_state,
);
}
}
fn render_help_screen(f: &mut Frame, area: Rect, app: &InteractiveApp) {
let palette = &app.theme().palette;
let help_text = vec![
"",
" Reflex Interactive Mode - Keyboard Shortcuts",
" ═══════════════════════════════════════════════",
"",
" Navigation:",
" j / ↓ Move to next result",
" k / ↑ Move to previous result",
" PageDown Jump 10 results down",
" PageUp Jump 10 results up",
" Home / g Go to first result",
" End / G Go to last result",
"",
" Search:",
" / Focus search input",
" Esc Unfocus input / close help / close selector",
" Ctrl+P Previous query from history",
" Ctrl+N Next query from history",
"",
" Filters:",
" s Toggle symbols-only mode",
" r Toggle regex mode",
" l Select language filter",
" k Select symbol kind filter",
" g Add glob pattern (CLI only for now)",
" x Add exclude pattern (CLI only for now)",
" e Toggle expand mode (full definitions)",
" c Toggle contains mode (substring)",
" Ctrl+L Clear language filter",
" Ctrl+K Clear kind filter",
"",
" Actions:",
" o / Enter Open file in $EDITOR / Expand preview",
" i Trigger reindex",
" ? Toggle this help screen",
" q / Ctrl+C Quit",
"",
" Mouse:",
" Click Select result / Focus input / Toggle filters",
" Click status Trigger reindex (top-right corner)",
" Scroll Navigate results",
"",
" Press '?' to close this help screen",
"",
];
let help_paragraph = Paragraph::new(help_text.join("\n"))
.block(
Block::default()
.borders(Borders::ALL)
.title(" Help ")
.border_style(Style::default().fg(palette.accent)),
)
.style(Style::default().fg(palette.foreground))
.alignment(Alignment::Left)
.wrap(Wrap { trim: false });
f.render_widget(help_paragraph, area);
}
fn render_file_preview(f: &mut Frame, area: Rect, app: &InteractiveApp) {
let palette = &app.theme().palette;
if let Some(preview) = app.preview_content() {
let visible_height = area.height.saturating_sub(2) as usize;
let start = preview.scroll_offset();
let center = preview.center_line();
let lang = preview.language();
let theme = app.theme().load_syntect_theme();
let content_lines = preview.content();
let end = (start + visible_height).min(content_lines.len());
let lines_to_highlight: Vec<String> = content_lines[..end].to_vec();
let all_highlighted =
super::syntax::highlight_code_lines(&lines_to_highlight, lang, &theme);
let highlighted_lines: Vec<_> = all_highlighted.into_iter().skip(start).collect();
let items: Vec<ListItem> = highlighted_lines
.into_iter()
.enumerate()
.map(|(idx, highlighted_line)| {
let line_number = start + idx + 1;
let is_center = line_number == center;
let mut spans = vec![Span::styled(
format!("{:4} │ ", line_number),
Style::default().fg(palette.muted),
)];
spans.extend(highlighted_line.spans);
let line_content = Line::from(spans);
if is_center {
ListItem::new(line_content).style(
Style::default()
.bg(palette.highlight)
.add_modifier(Modifier::BOLD),
)
} else {
ListItem::new(line_content)
}
})
.collect();
let relative_path = preview
.path()
.strip_prefix(app.cwd().to_str().unwrap_or(""))
.unwrap_or(preview.path())
.trim_start_matches('/');
let relative_display = if relative_path.is_empty() {
"./".to_string()
} else {
format!("./{}", relative_path)
};
let title = format!(" {} (line {}) ", relative_display, center);
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(Style::default().fg(palette.accent)),
);
f.render_widget(list, area);
}
}
fn render_footer(f: &mut Frame, area: Rect, app: &InteractiveApp) {
let palette = &app.theme().palette;
let footer_spans = match app.mode() {
AppMode::Help => vec![
Span::styled("Press ", Style::default().fg(palette.muted)),
Span::styled(
"?",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(" to close help", Style::default().fg(palette.muted)),
],
AppMode::FilterSelector => vec![
Span::styled(
"[FILTER SELECTOR] ",
Style::default()
.fg(palette.info)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"↑↓/j/k",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(" navigate ", Style::default().fg(palette.muted)),
Span::styled(
"Enter",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(" select ", Style::default().fg(palette.muted)),
Span::styled(
"Esc",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(" cancel", Style::default().fg(palette.muted)),
],
AppMode::FilePreview => vec![
Span::styled(
"[PREVIEW MODE] ",
Style::default()
.fg(palette.info)
.add_modifier(Modifier::BOLD),
),
Span::styled("j/k scroll ", Style::default().fg(palette.muted)),
Span::styled(
"Esc",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(" close ", Style::default().fg(palette.muted)),
Span::styled(
"o",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(" open in editor", Style::default().fg(palette.muted)),
],
AppMode::Indexing | AppMode::Normal => {
let mut spans = vec![];
match app.focus_state() {
FocusState::Input => {
spans.push(Span::styled(
"[INPUT MODE] ",
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
));
}
FocusState::Filters => {
spans.push(Span::styled(
"[FILTERS MODE] ",
Style::default()
.fg(palette.info)
.add_modifier(Modifier::BOLD),
));
}
FocusState::Results => {
spans.push(Span::styled(
"[NAVIGATE MODE] ",
Style::default()
.fg(palette.success)
.add_modifier(Modifier::BOLD),
));
}
}
let hint = app.capabilities().open_hint();
spans.push(Span::styled(hint, Style::default().fg(palette.muted)));
spans.push(Span::raw(" "));
spans.push(Span::styled("? help", Style::default().fg(palette.muted)));
spans
}
};
let footer = Paragraph::new(Line::from(footer_spans))
.style(Style::default().fg(palette.foreground).bg(Color::Black));
f.render_widget(footer, area);
}