use crate::scanner::{self, FsNode};
use crate::ui::widgets;
use anyhow::Result;
use crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton, MouseEvent,
MouseEventKind,
};
use crossterm::execute;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::Rect;
use ratatui::Terminal;
use std::io;
use std::path::PathBuf;
use std::time::Duration;
pub struct App {
pub root: FsNode,
pub cursor_path: Vec<usize>,
pub selected: usize,
pub scroll: usize,
pub visible_rows: usize,
pub should_quit: bool,
pub marked: Vec<PathBuf>,
pub list_area: Rect,
}
impl App {
pub fn new(root: FsNode) -> Self {
Self {
root,
cursor_path: Vec::new(),
selected: 0,
scroll: 0,
visible_rows: 10,
should_quit: false,
marked: Vec::new(),
list_area: Rect::default(),
}
}
pub fn current(&self) -> &FsNode {
let mut node = &self.root;
for &i in &self.cursor_path {
node = &node.children[i];
}
node
}
pub fn breadcrumb(&self) -> String {
self.current().path.display().to_string()
}
pub fn marked_total(&self) -> u64 {
fn find_size(node: &FsNode, target: &PathBuf) -> Option<u64> {
if &node.path == target {
return Some(node.size);
}
for c in &node.children {
if let Some(s) = find_size(c, target) {
return Some(s);
}
}
None
}
self.marked.iter().filter_map(|p| find_size(&self.root, p)).sum()
}
pub fn set_visible_rows(&mut self, rows: usize) {
self.visible_rows = rows.max(1);
self.clamp_scroll();
}
fn clamp_scroll(&mut self) {
let len = self.current().children.len();
if self.selected < self.scroll {
self.scroll = self.selected;
} else if self.selected >= self.scroll + self.visible_rows {
self.scroll = self.selected + 1 - self.visible_rows;
}
let max_scroll = len.saturating_sub(self.visible_rows);
if self.scroll > max_scroll {
self.scroll = max_scroll;
}
}
pub fn descend(&mut self) {
let node = self.current();
if node.is_dir && !node.children.is_empty() && self.selected < node.children.len() {
let child_is_dir = node.children[self.selected].is_dir;
if child_is_dir {
self.cursor_path.push(self.selected);
self.selected = 0;
self.scroll = 0;
}
}
}
pub fn ascend(&mut self) {
if let Some(prev) = self.cursor_path.pop() {
self.selected = prev;
self.scroll = 0;
self.clamp_scroll();
}
}
pub fn move_down(&mut self) {
let len = self.current().children.len();
if len > 0 && self.selected + 1 < len {
self.selected += 1;
self.clamp_scroll();
}
}
pub fn move_up(&mut self) {
if self.selected > 0 {
self.selected -= 1;
self.clamp_scroll();
}
}
pub fn page_down(&mut self) {
let len = self.current().children.len();
if len == 0 {
return;
}
self.selected = (self.selected + self.visible_rows).min(len - 1);
self.clamp_scroll();
}
pub fn page_up(&mut self) {
self.selected = self.selected.saturating_sub(self.visible_rows);
self.clamp_scroll();
}
pub fn go_top(&mut self) {
self.selected = 0;
self.clamp_scroll();
}
pub fn go_bottom(&mut self) {
let len = self.current().children.len();
self.selected = len.saturating_sub(1);
self.clamp_scroll();
}
fn row_to_index(&self, row: u16) -> Option<usize> {
let first_row = self.list_area.y.saturating_add(1);
let last_row = self.list_area.y.saturating_add(self.list_area.height.saturating_sub(2));
if row < first_row || row > last_row {
return None;
}
let offset = (row - first_row) as usize;
let idx = self.scroll + offset;
if idx < self.current().children.len() {
Some(idx)
} else {
None
}
}
pub fn handle_mouse(&mut self, ev: MouseEvent) {
match ev.kind {
MouseEventKind::Down(MouseButton::Left) => {
if let Some(idx) = self.row_to_index(ev.row) {
if idx == self.selected {
self.descend();
} else {
self.selected = idx;
self.clamp_scroll();
}
}
}
MouseEventKind::Down(MouseButton::Right) => self.ascend(),
MouseEventKind::ScrollDown => {
for _ in 0..3 {
self.move_down();
}
}
MouseEventKind::ScrollUp => {
for _ in 0..3 {
self.move_up();
}
}
_ => {}
}
}
pub fn toggle_mark(&mut self) {
let node = self.current();
if let Some(child) = node.children.get(self.selected) {
let path = child.path.clone();
if let Some(pos) = self.marked.iter().position(|p| p == &path) {
self.marked.remove(pos);
} else {
self.marked.push(path);
}
}
}
}
pub fn launch() -> Result<()> {
let target = PathBuf::from(".");
let opts = scanner::walker::WalkOptions::default();
let result = scanner::walker::scan_path(&target, &opts)?;
let mut root = result.root;
root.sort_by_size_desc();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(root);
let res = run_loop(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
res
}
fn run_loop(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) -> Result<()> {
while !app.should_quit {
terminal.draw(|f| widgets::draw(f, app))?;
if event::poll(Duration::from_millis(200))? {
match event::read()? {
Event::Key(key) => {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
KeyCode::Down | KeyCode::Char('j') => app.move_down(),
KeyCode::Up | KeyCode::Char('k') => app.move_up(),
KeyCode::Right | KeyCode::Enter | KeyCode::Char('l') => app.descend(),
KeyCode::Left | KeyCode::Backspace | KeyCode::Char('h') => app.ascend(),
KeyCode::PageDown => app.page_down(),
KeyCode::PageUp => app.page_up(),
KeyCode::Home | KeyCode::Char('g') => app.go_top(),
KeyCode::End | KeyCode::Char('G') => app.go_bottom(),
KeyCode::Char(' ') => app.toggle_mark(),
_ => {}
}
}
Event::Mouse(mouse_ev) => app.handle_mouse(mouse_ev),
_ => {}
}
}
}
Ok(())
}