file-explorer/src/file_select.rs
2026-02-01 12:01:52 +01:00

189 lines
4.9 KiB
Rust

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<usize>,
top: usize,
files: Vec<PathBuf>,
current_dir: PathBuf,
config: Rc<RefCell<Config>>,
}
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<RefCell<Config>>) -> io::Result<Self> {
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<Line> = 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);
}
}
}