1use crate::scanner::{self, FsNode};
2use crate::ui::widgets;
3use anyhow::Result;
4use crossterm::event::{
5 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton, MouseEvent,
6 MouseEventKind,
7};
8use crossterm::execute;
9use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
10use ratatui::backend::CrosstermBackend;
11use ratatui::layout::Rect;
12use ratatui::Terminal;
13use std::io;
14use std::path::PathBuf;
15use std::time::Duration;
16
17pub struct App {
18 pub root: FsNode,
19 pub cursor_path: Vec<usize>,
20 pub selected: usize,
21 pub scroll: usize,
22 pub visible_rows: usize,
23 pub should_quit: bool,
24 pub marked: Vec<PathBuf>,
25 pub list_area: Rect,
26}
27
28impl App {
29 pub fn new(root: FsNode) -> Self {
30 Self {
31 root,
32 cursor_path: Vec::new(),
33 selected: 0,
34 scroll: 0,
35 visible_rows: 10,
36 should_quit: false,
37 marked: Vec::new(),
38 list_area: Rect::default(),
39 }
40 }
41
42 pub fn current(&self) -> &FsNode {
43 let mut node = &self.root;
44 for &i in &self.cursor_path {
45 node = &node.children[i];
46 }
47 node
48 }
49
50 pub fn breadcrumb(&self) -> String {
51 self.current().path.display().to_string()
52 }
53
54 pub fn marked_total(&self) -> u64 {
55 fn find_size(node: &FsNode, target: &PathBuf) -> Option<u64> {
56 if &node.path == target {
57 return Some(node.size);
58 }
59 for c in &node.children {
60 if let Some(s) = find_size(c, target) {
61 return Some(s);
62 }
63 }
64 None
65 }
66 self.marked.iter().filter_map(|p| find_size(&self.root, p)).sum()
67 }
68
69 pub fn set_visible_rows(&mut self, rows: usize) {
70 self.visible_rows = rows.max(1);
71 self.clamp_scroll();
72 }
73
74 fn clamp_scroll(&mut self) {
75 let len = self.current().children.len();
76 if self.selected < self.scroll {
77 self.scroll = self.selected;
78 } else if self.selected >= self.scroll + self.visible_rows {
79 self.scroll = self.selected + 1 - self.visible_rows;
80 }
81 let max_scroll = len.saturating_sub(self.visible_rows);
82 if self.scroll > max_scroll {
83 self.scroll = max_scroll;
84 }
85 }
86
87 pub fn descend(&mut self) {
88 let node = self.current();
89 if node.is_dir && !node.children.is_empty() && self.selected < node.children.len() {
90 let child_is_dir = node.children[self.selected].is_dir;
91 if child_is_dir {
92 self.cursor_path.push(self.selected);
93 self.selected = 0;
94 self.scroll = 0;
95 }
96 }
97 }
98
99 pub fn ascend(&mut self) {
100 if let Some(prev) = self.cursor_path.pop() {
101 self.selected = prev;
102 self.scroll = 0;
103 self.clamp_scroll();
104 }
105 }
106
107 pub fn move_down(&mut self) {
108 let len = self.current().children.len();
109 if len > 0 && self.selected + 1 < len {
110 self.selected += 1;
111 self.clamp_scroll();
112 }
113 }
114
115 pub fn move_up(&mut self) {
116 if self.selected > 0 {
117 self.selected -= 1;
118 self.clamp_scroll();
119 }
120 }
121
122 pub fn page_down(&mut self) {
123 let len = self.current().children.len();
124 if len == 0 {
125 return;
126 }
127 self.selected = (self.selected + self.visible_rows).min(len - 1);
128 self.clamp_scroll();
129 }
130
131 pub fn page_up(&mut self) {
132 self.selected = self.selected.saturating_sub(self.visible_rows);
133 self.clamp_scroll();
134 }
135
136 pub fn go_top(&mut self) {
137 self.selected = 0;
138 self.clamp_scroll();
139 }
140
141 pub fn go_bottom(&mut self) {
142 let len = self.current().children.len();
143 self.selected = len.saturating_sub(1);
144 self.clamp_scroll();
145 }
146
147 fn row_to_index(&self, row: u16) -> Option<usize> {
148 let first_row = self.list_area.y.saturating_add(1);
149 let last_row = self.list_area.y.saturating_add(self.list_area.height.saturating_sub(2));
150 if row < first_row || row > last_row {
151 return None;
152 }
153 let offset = (row - first_row) as usize;
154 let idx = self.scroll + offset;
155 if idx < self.current().children.len() {
156 Some(idx)
157 } else {
158 None
159 }
160 }
161
162 pub fn handle_mouse(&mut self, ev: MouseEvent) {
163 match ev.kind {
164 MouseEventKind::Down(MouseButton::Left) => {
165 if let Some(idx) = self.row_to_index(ev.row) {
166 if idx == self.selected {
167 self.descend();
168 } else {
169 self.selected = idx;
170 self.clamp_scroll();
171 }
172 }
173 }
174 MouseEventKind::Down(MouseButton::Right) => self.ascend(),
175 MouseEventKind::ScrollDown => {
176 for _ in 0..3 {
177 self.move_down();
178 }
179 }
180 MouseEventKind::ScrollUp => {
181 for _ in 0..3 {
182 self.move_up();
183 }
184 }
185 _ => {}
186 }
187 }
188
189 pub fn toggle_mark(&mut self) {
190 let node = self.current();
191 if let Some(child) = node.children.get(self.selected) {
192 let path = child.path.clone();
193 if let Some(pos) = self.marked.iter().position(|p| p == &path) {
194 self.marked.remove(pos);
195 } else {
196 self.marked.push(path);
197 }
198 }
199 }
200}
201
202pub fn launch() -> Result<()> {
203 let target = PathBuf::from(".");
204 let opts = scanner::walker::WalkOptions::default();
205 let result = scanner::walker::scan_path(&target, &opts)?;
206 let mut root = result.root;
207 root.sort_by_size_desc();
208
209 enable_raw_mode()?;
210 let mut stdout = io::stdout();
211 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
212 let backend = CrosstermBackend::new(stdout);
213 let mut terminal = Terminal::new(backend)?;
214
215 let mut app = App::new(root);
216 let res = run_loop(&mut terminal, &mut app);
217
218 disable_raw_mode()?;
219 execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
220 terminal.show_cursor()?;
221
222 res
223}
224
225fn run_loop(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) -> Result<()> {
226 while !app.should_quit {
227 terminal.draw(|f| widgets::draw(f, app))?;
228 if event::poll(Duration::from_millis(200))? {
229 match event::read()? {
230 Event::Key(key) => {
231 if key.kind != KeyEventKind::Press {
232 continue;
233 }
234 match key.code {
235 KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
236 KeyCode::Down | KeyCode::Char('j') => app.move_down(),
237 KeyCode::Up | KeyCode::Char('k') => app.move_up(),
238 KeyCode::Right | KeyCode::Enter | KeyCode::Char('l') => app.descend(),
239 KeyCode::Left | KeyCode::Backspace | KeyCode::Char('h') => app.ascend(),
240 KeyCode::PageDown => app.page_down(),
241 KeyCode::PageUp => app.page_up(),
242 KeyCode::Home | KeyCode::Char('g') => app.go_top(),
243 KeyCode::End | KeyCode::Char('G') => app.go_bottom(),
244 KeyCode::Char(' ') => app.toggle_mark(),
245 _ => {}
246 }
247 }
248 Event::Mouse(mouse_ev) => app.handle_mouse(mouse_ev),
249 _ => {}
250 }
251 }
252 }
253 Ok(())
254}