Skip to main content

diskr/ui/
app.rs

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}