use anyhow::Result;
use crossterm::{
cursor::MoveTo,
execute,
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
terminal::{self, Clear, ClearType},
};
use std::{
collections::HashSet,
io::{self, Write},
path::Path,
};
use crate::models::FileEntry;
use crate::navigator::NavigatorMode;
use crate::search::SearchMode;
pub struct RenderContext<'a> {
pub current_dir: &'a Path,
pub entries: &'a [FileEntry],
pub selected_index: usize,
pub selected_items: &'a HashSet<usize>,
pub scroll_offset: usize,
pub terminal_height: u16,
pub mode: &'a NavigatorMode,
pub is_root: bool,
pub pattern_input: &'a str,
pub status_message: &'a Option<String>,
pub search_mode: Option<&'a SearchMode>,
pub preview_focused: bool,
}
pub struct Renderer {
}
impl Renderer {
pub fn new() -> Self {
Self {}
}
pub fn render(&self, ctx: RenderContext) -> Result<()> {
let mut stdout = io::stdout();
let (terminal_width, _) = terminal::size()?;
execute!(stdout, Clear(ClearType::All), MoveTo(0, 0))?;
self.render_header(&mut stdout, ctx.current_dir, ctx.is_root, terminal_width)?;
self.render_mode(&mut stdout, ctx.mode, ctx.pattern_input, ctx.search_mode)?;
self.render_file_list(&mut stdout, &ctx)?;
if let Some(ref msg) = ctx.status_message {
self.render_status(&mut stdout, msg, ctx.terminal_height)?;
}
self.render_footer(
&mut stdout,
ctx.mode,
ctx.is_root,
ctx.preview_focused,
ctx.terminal_height,
terminal_width,
)?;
stdout.flush()?;
Ok(())
}
fn render_header(
&self,
stdout: &mut io::Stdout,
current_dir: &Path,
is_root: bool,
terminal_width: u16,
) -> Result<()> {
let header_text = if is_root {
format!(" 📂 {} [ROOT MODE]", current_dir.display())
} else {
format!(" 📂 {}", current_dir.display())
};
execute!(
stdout,
SetBackgroundColor(Color::DarkBlue),
SetForegroundColor(Color::White),
Print(" ".repeat(terminal_width as usize)),
MoveTo(0, 0),
Print(&header_text),
ResetColor
)?;
Ok(())
}
fn render_mode(
&self,
stdout: &mut io::Stdout,
mode: &NavigatorMode,
pattern_input: &str,
search_mode: Option<&SearchMode>,
) -> Result<()> {
let mode_text = match mode {
NavigatorMode::Browse => "BROWSE".to_string(),
NavigatorMode::Select => "SELECT (Space: toggle, Enter: confirm)".to_string(),
NavigatorMode::PatternSelect => format!("PATTERN: {}_", pattern_input),
NavigatorMode::Search => {
if let Some(search) = search_mode {
format!(
"SEARCH: {}_ [Regex: {}] [Case: {}] [Content: {}]",
search.query,
if search.use_regex { "ON" } else { "OFF" },
if search.case_sensitive { "ON" } else { "OFF" },
if search.search_in_contents {
"ON"
} else {
"OFF"
}
)
} else {
"SEARCH: _".to_string()
}
}
_ => String::new(),
};
if !mode_text.is_empty() {
execute!(
stdout,
MoveTo(0, 1),
SetForegroundColor(Color::Yellow),
Print(format!(" Mode: {} ", mode_text)),
ResetColor
)?;
}
Ok(())
}
fn render_file_list(&self, stdout: &mut io::Stdout, ctx: &RenderContext) -> Result<()> {
let (terminal_width, _) = terminal::size()?;
let list_start = 3;
let visible_area = (ctx.terminal_height as usize).saturating_sub(5);
let end_index = (ctx.scroll_offset + visible_area).min(ctx.entries.len());
for (i, entry) in ctx.entries[ctx.scroll_offset..end_index].iter().enumerate() {
let row = (list_start + i) as u16;
execute!(stdout, MoveTo(0, row))?;
let display_index = ctx.scroll_offset + i;
let is_selected = ctx.selected_items.contains(&display_index);
let is_highlighted = display_index == ctx.selected_index;
let selection_marker = if is_selected { "[✓]" } else { "[ ]" };
if is_highlighted {
execute!(
stdout,
SetBackgroundColor(Color::DarkGrey),
SetForegroundColor(Color::White)
)?;
}
if *ctx.mode == NavigatorMode::Select {
execute!(stdout, Print(format!(" {} ", selection_marker)))?;
}
let display_str = if is_highlighted {
format!(" > {}", entry.display_name())
} else {
format!(" {}", entry.display_name())
};
let color = if !entry.is_accessible {
Color::DarkRed
} else if entry.is_dir {
Color::Cyan
} else if entry.is_symlink {
Color::Magenta
} else {
Color::White
};
execute!(stdout, SetForegroundColor(color), Print(&display_str))?;
if *ctx.mode == NavigatorMode::Select && ctx.is_root {
let perms = entry.permissions_string();
let ownership = entry.ownership_string();
let info = format!(" {} {}", perms, ownership);
execute!(stdout, SetForegroundColor(Color::DarkGrey), Print(&info))?;
}
if is_highlighted {
let content_len = display_str.len()
+ if *ctx.mode == NavigatorMode::Select {
4
} else {
0
}
+ if *ctx.mode == NavigatorMode::Select && ctx.is_root {
entry.permissions_string().len() + 1 + entry.ownership_string().len() + 1
} else {
0
};
let padding = (terminal_width as usize)
.saturating_sub(content_len)
.min(terminal_width as usize);
execute!(stdout, Print(" ".repeat(padding)))?;
}
execute!(stdout, ResetColor)?;
}
Ok(())
}
fn render_status(
&self,
stdout: &mut io::Stdout,
msg: &str,
terminal_height: u16,
) -> Result<()> {
let status_row = terminal_height - 2;
execute!(
stdout,
MoveTo(0, status_row),
SetForegroundColor(Color::Yellow),
Print(format!(" {} ", msg)),
ResetColor
)?;
Ok(())
}
fn render_footer(
&self,
stdout: &mut io::Stdout,
mode: &NavigatorMode,
is_root: bool,
preview_focused: bool,
terminal_height: u16,
terminal_width: u16,
) -> Result<()> {
let footer_row = terminal_height - 1;
let controls = if preview_focused {
" ↑↓: Scroll | PageUp/Down: Page | Tab: Back to Files | Esc: Close Preview"
} else if is_root {
match mode {
NavigatorMode::Browse => {
" ↑↓: Nav | Enter: Open | Ctrl+F: Search | Ctrl+B: Bookmarks | Ctrl+P: Preview | F2: Split | S: Shell | q: Quit"
}
NavigatorMode::Select => {
" ↑↓: Navigate | Space: Toggle | Enter: Confirm | c: Chmod | o: Chown | Esc: Cancel"
}
NavigatorMode::PatternSelect => {
" Type pattern | Enter: Apply | Esc: Cancel"
}
NavigatorMode::Search => {
" Type to search | Enter: Execute | Ctrl+R: Regex | Ctrl+C: Case | Ctrl+N/P: Next/Prev | Esc: Cancel"
}
_ => "",
}
} else {
match mode {
NavigatorMode::Browse => {
" ↑↓: Nav | Enter: Open | Ctrl+F: Search | Ctrl+B: Bookmarks | Ctrl+P: Preview | F2: Split | S: Shell | q: Quit"
}
NavigatorMode::Search => {
" Type to search | Enter: Execute | Ctrl+R: Regex | Ctrl+C: Case | Ctrl+N/P: Next/Prev | Esc: Cancel"
}
_ => " ↑↓: Navigate | Enter: Open | Esc: Back",
}
};
execute!(
stdout,
MoveTo(0, footer_row),
SetBackgroundColor(Color::DarkGrey),
SetForegroundColor(Color::White),
Print(controls),
Print(" ".repeat(terminal_width as usize - controls.len())),
ResetColor
)?;
Ok(())
}
}