use ratatui::{ buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::Line, widgets::{StatefulWidget, Widget}, }; use std::{ cell::RefCell, env, fs, io, mem, path::{Path, PathBuf}, rc::Rc, }; use crate::{config::Config, directory_operations}; pub enum ScrollingDirection { Up, Down, } pub struct FilesWidget; pub struct FilesState { selected: Option, top: usize, files: Vec, current_dir: PathBuf, config: Rc>, } impl FilesState { pub fn get_current(&self) -> Option<&Path> { Some(self.files[self.selected?].as_path()) } pub fn scroll(&mut self, direction: ScrollingDirection) { if self.files.is_empty() { return; } match direction { ScrollingDirection::Down => self.scroll_down(), ScrollingDirection::Up => self.scroll_up(), } } fn scroll_down(&mut self) { match self.selected { Some(v) => self.selected = Some((v + 1).min(self.files.len() - 1)), None => self.selected = Some(0), } } fn scroll_up(&mut self) { self.selected = Some(self.selected.unwrap_or(0).saturating_sub(1)); } pub fn go_up(&mut self) -> io::Result<()> { let new_path = self.current_dir.parent().unwrap_or(Path::new("/")); if !directory_operations::can_read(new_path) { return Ok(()); } self.current_dir.pop(); self.update_contents() } fn update_contents(&mut self) -> io::Result<()> { self.files = fs::read_dir(&self.current_dir)? .filter_map(|entry| entry.ok()) .map(|entry| entry.path()) .collect(); directory_operations::sort_dir(&self.config.borrow(), &mut self.files); self.selected = if self.files.is_empty() { None } else { Some(0) }; self.top = 0; Ok(()) } pub fn go_inner(&mut self) -> io::Result<()> { let Some(selected) = self.selected else { return Ok(()); }; if !self.files[selected].is_dir() { return Ok(()); } if !directory_operations::can_read(self.files[selected].as_path()) { return Ok(()); } self.current_dir = self.files.swap_remove(selected); self.update_contents()?; Ok(()) } pub fn toggle_hidden(&mut self) -> io::Result<()> { let current_state = self.config.borrow().show_hidden; self.set_hidden(!current_state)?; Ok(()) } fn set_hidden(&mut self, state: bool) -> io::Result<()> { { let mut cfg = self.config.borrow_mut(); cfg.show_hidden = state; } self.update_contents()?; Ok(()) } pub fn new(config: Rc>) -> io::Result { let current_dir = env::current_dir()?; let mut output = FilesState { selected: None, files: vec![], top: 0, current_dir, config, }; output.update_contents()?; Ok(output) } } impl StatefulWidget for FilesWidget { type State = FilesState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let layout = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(area); let visible_rows = layout[0].height as usize; if let Some(selected) = state.selected { if selected < state.top { state.top = selected; } else if selected >= state.top + visible_rows { state.top = selected - visible_rows + 1; } } let mut items: Vec = state .files .iter() .skip(state.top) .take(visible_rows) .map(|file| { Line::default().spans(vec![ " ", file.file_name() .expect("Files should have a valid name") .to_str() .expect("Files should have a valid name"), ]) }) .collect(); let relative_selected = state.selected.map(|s| s - state.top); if let Some(index) = relative_selected && let Some(item) = items.get_mut(index) { let mut old = mem::take(item); old = old.patch_style(Style::default().fg(Color::Blue)); old.spans[0] = "> ".into(); *item = old; } let rows_layout = Layout::vertical(vec![Constraint::Length(1); visible_rows]).split(layout[0]); for (item, area) in items.into_iter().zip(rows_layout.iter()) { item.render(*area, buf); } } }