1pub mod app;
8pub mod event;
9pub mod ui;
10
11pub use app::{App, AppState, ProjectEntry};
12pub use event::{Action, Event, EventHandler};
13
14use crate::error::Result;
15use crate::trash::{delete_path, DeleteMethod};
16use crossterm::{
17 event::{DisableMouseCapture, EnableMouseCapture, KeyCode},
18 execute,
19 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
20};
21use ratatui::{backend::CrosstermBackend, Terminal};
22use std::io;
23use std::path::PathBuf;
24use std::time::Duration;
25
26pub fn run(paths: Vec<PathBuf>) -> Result<()> {
28 enable_raw_mode()?;
30 let mut stdout = io::stdout();
31 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
32 let backend = CrosstermBackend::new(stdout);
33 let mut terminal = Terminal::new(backend)?;
34
35 let mut app = App::new(paths);
37
38 let events = EventHandler::new(Duration::from_millis(50));
40
41 let result = run_app(&mut terminal, &mut app, &events);
43
44 disable_raw_mode()?;
46 execute!(
47 terminal.backend_mut(),
48 LeaveAlternateScreen,
49 DisableMouseCapture
50 )?;
51 terminal.show_cursor()?;
52
53 result
54}
55
56fn run_app(
58 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
59 app: &mut App,
60 events: &EventHandler,
61) -> Result<()> {
62 loop {
63 terminal.draw(|frame| ui::render(app, frame))?;
65
66 let event = events.next().map_err(|e| crate::error::DevSweepError::Other(e.to_string()))?;
68 match event {
69 Event::Key(key) => {
70 if app.is_searching {
72 match key.code {
73 KeyCode::Esc => app.end_search(),
74 KeyCode::Enter => app.end_search(),
75 KeyCode::Backspace => app.search_pop(),
76 KeyCode::Char(c) => app.search_push(c),
77 _ => {}
78 }
79 continue;
80 }
81
82 if app.show_help {
84 app.show_help = false;
85 continue;
86 }
87
88 let action = Action::from_key(key);
90
91 match app.state {
93 AppState::Scanning => {
94 if matches!(action, Action::Quit) {
96 app.should_quit = true;
97 }
98 }
99 AppState::Confirming => match action {
100 Action::Confirm => {
101 app.start_delete();
103 }
104 Action::TogglePermanent => {
105 app.toggle_permanent_delete();
106 }
107 Action::Cancel | Action::Quit => {
108 app.cancel_delete();
109 }
110 _ => {}
111 },
112 AppState::Ready => match action {
113 Action::Quit => {
115 app.should_quit = true;
116 }
117 Action::Up | Action::ScrollUp => app.menu_up(),
118 Action::Down | Action::ScrollDown => app.menu_down(),
119 Action::ToggleSelect | Action::Scan | Action::Confirm => {
120 app.start_scan();
122 }
123 Action::Help => app.toggle_help(),
124 _ => {}
125 },
126 AppState::Results | AppState::CacheResults | AppState::CleanerResults | AppState::Error(_) => match action {
127 Action::Quit => {
128 app.should_quit = true;
129 }
130 Action::Up => app.select_up(),
131 Action::Down => app.select_down(),
132 Action::PageUp => app.page_up(10),
133 Action::PageDown => app.page_down(10),
134 Action::Top => app.go_top(),
135 Action::Bottom => app.go_bottom(),
136 Action::ToggleSelect => app.toggle_select(),
137 Action::ToggleExpand => app.toggle_expand(),
138 Action::Expand => app.expand(),
139 Action::Collapse => app.collapse(),
140 Action::SelectAll => app.select_all(),
141 Action::DeselectAll => app.deselect_all(),
142 Action::Delete => app.request_delete(),
143 Action::Help => app.toggle_help(),
144 Action::Scan | Action::Refresh => {
145 app.start_scan();
146 }
147 Action::Search => app.start_search(),
148 Action::NextTab => app.next_tab(),
149 Action::PrevTab => app.prev_tab(),
150 Action::ScrollUp => app.scroll_up(),
151 Action::ScrollDown => app.scroll_down(),
152 Action::Back => {
153 app.go_back();
154 }
155 Action::Cancel => {
156 if !app.search_query.is_empty() {
158 app.search_query.clear();
159 app.filter_by_tab();
160 } else {
161 app.go_back();
162 }
163 }
164 _ => {}
165 },
166 AppState::Cleaning => {
167 if matches!(action, Action::Quit | Action::Cancel) {
169 app.pending_delete_items.clear();
170 app.state = AppState::Results;
171 app.status_message = Some("Cleaning cancelled".to_string());
172 }
173 }
174 }
175 }
176 Event::Tick => {
177 app.tick_animation();
179
180 if app.state == AppState::Scanning {
182 app.check_scan_progress();
183 }
184
185 if app.has_pending_delete() {
187 let items = app.take_pending_delete_items();
188 let permanent = app.permanent_delete;
189
190 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
192 delete_items(&items, permanent)
193 }));
194
195 match result {
196 Ok((success, failed, freed)) => {
197 app.deletion_complete(success, failed, freed);
198 }
199 Err(_) => {
200 app.deletion_complete(0, items.len(), 0);
202 app.status_message = Some("Error during deletion!".to_string());
203 }
204 }
205 }
206 }
207 Event::Resize(_, _) => {
208 }
210 Event::Mouse(mouse) => {
211 let action = Action::from_mouse(&mouse);
213 match action {
214 Action::ScrollUp => {
215 app.select_up();
216 app.select_up();
217 app.select_up();
218 }
219 Action::ScrollDown => {
220 app.select_down();
221 app.select_down();
222 app.select_down();
223 }
224 _ => {}
225 }
226 }
227 }
228
229 if app.should_quit {
231 break;
232 }
233 }
234
235 Ok(())
236}
237
238fn delete_items(items: &[(PathBuf, Option<String>)], permanent: bool) -> (usize, usize, u64) {
242 let mut success = 0;
243 let mut failed = 0;
244 let mut freed = 0u64;
245
246 let method = if permanent {
247 DeleteMethod::Permanent
248 } else {
249 DeleteMethod::Trash
250 };
251
252 for (path, clean_command) in items {
253 let size = if path.is_dir() {
255 dir_size(path)
256 } else {
257 std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)
258 };
259
260 let result = if let Some(cmd) = clean_command {
262 run_clean_command(cmd)
264 } else {
265 delete_path(path, method).map(|_| ())
267 };
268
269 match result {
270 Ok(_) => {
271 success += 1;
272 freed += size;
273 }
274 Err(_) => {
275 failed += 1;
276 }
277 }
278 }
279
280 (success, failed, freed)
281}
282
283fn run_clean_command(cmd: &str) -> crate::error::Result<()> {
285 use std::process::Command;
286
287 let output = if cfg!(target_os = "windows") {
288 Command::new("cmd").args(["/C", cmd]).output()
289 } else {
290 Command::new("sh").args(["-c", cmd]).output()
291 };
292
293 match output {
294 Ok(out) if out.status.success() => Ok(()),
295 Ok(out) => Err(crate::error::DevSweepError::Other(
296 String::from_utf8_lossy(&out.stderr).to_string(),
297 )),
298 Err(e) => Err(crate::error::DevSweepError::Other(e.to_string())),
299 }
300}
301
302fn dir_size(path: &PathBuf) -> u64 {
304 walkdir::WalkDir::new(path)
305 .into_iter()
306 .filter_map(|e| e.ok())
307 .filter(|e| e.file_type().is_file())
308 .filter_map(|e| e.metadata().ok())
309 .map(|m| m.len())
310 .sum()
311}