1use std::any::Any;
2use std::collections::HashSet;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6use crossterm::event::{KeyCode, MouseButton, MouseEvent, MouseEventKind};
7use ratatui::buffer::Buffer;
8use ratatui::layout::Rect;
9use ratatui::style::Style;
10use ratatui::text::Line;
11use ratatui::widgets::{Block, Paragraph, Widget};
12use typ_core::{KeyChord, Panel, PanelEvent, RenderContext};
13
14#[derive(Debug, Clone)]
16pub struct Entry {
17 pub path: PathBuf,
18 pub depth: usize,
20 pub is_dir: bool,
21}
22
23pub struct TreePanel {
24 root: PathBuf,
25 entries: Vec<Entry>,
28 expanded: HashSet<PathBuf>,
29 selected: usize,
30 top_line: usize,
31 height: usize,
32}
33
34impl TreePanel {
35 pub fn new(root: &Path) -> Result<Self> {
36 let mut panel = Self {
37 root: root.to_path_buf(),
38 entries: Vec::new(),
39 expanded: HashSet::new(),
40 selected: 0,
41 top_line: 0,
42 height: 0,
43 };
44 panel.rebuild()?;
45 Ok(panel)
46 }
47
48 pub fn entry_count(&self) -> usize {
49 self.entries.len()
50 }
51
52 pub fn selected(&self) -> Option<&Path> {
53 self.entries.get(self.selected).map(|e| e.path.as_path())
54 }
55
56 pub fn depth_of_selection(&self) -> usize {
57 self.entries.get(self.selected).map_or(0, |e| e.depth)
58 }
59
60 pub fn root(&self) -> &Path {
61 &self.root
62 }
63
64 fn rebuild(&mut self) -> Result<()> {
67 let previous = self.selected().map(Path::to_path_buf);
68 let mut entries = Vec::new();
69 collect(&self.root, 0, &self.expanded, &mut entries)?;
70 self.entries = entries;
71 self.selected = previous
72 .and_then(|p| self.entries.iter().position(|e| e.path == p))
73 .unwrap_or(self.selected)
74 .min(self.entries.len().saturating_sub(1));
75 Ok(())
76 }
77
78 fn move_selection(&mut self, delta: i32) {
79 let last = self.entries.len().saturating_sub(1) as i64;
80 self.selected = (self.selected as i64 + delta as i64).clamp(0, last) as usize;
81 self.scroll_to_selection();
82 }
83
84 fn scroll_to_selection(&mut self) {
85 if self.height == 0 {
86 return;
87 }
88 if self.selected < self.top_line {
89 self.top_line = self.selected;
90 } else if self.selected >= self.top_line + self.height {
91 self.top_line = self.selected - self.height + 1;
92 }
93 }
94
95 fn set_expanded(&mut self, path: PathBuf, expand: bool) -> Vec<PanelEvent> {
96 if expand {
97 self.expanded.insert(path);
98 } else {
99 self.expanded.retain(|p| !p.starts_with(&path));
103 }
104 match self.rebuild() {
105 Ok(()) => vec![PanelEvent::NeedsRedraw],
106 Err(e) => vec![PanelEvent::Notify {
107 level: typ_core::NotifyLevel::Error,
108 message: format!("{e:#}"),
109 }],
110 }
111 }
112
113 fn activate(&mut self) -> Vec<PanelEvent> {
115 let Some(entry) = self.entries.get(self.selected).cloned() else {
116 return Vec::new();
117 };
118 if entry.is_dir {
119 let expand = !self.expanded.contains(&entry.path);
120 self.set_expanded(entry.path, expand)
121 } else {
122 vec![PanelEvent::OpenFile {
123 path: entry.path,
124 line: 0,
125 col: 0,
126 }]
127 }
128 }
129
130 fn list_area(area: Rect) -> Rect {
132 Block::bordered().inner(area)
133 }
134}
135
136fn collect(
139 dir: &Path,
140 depth: usize,
141 expanded: &HashSet<PathBuf>,
142 out: &mut Vec<Entry>,
143) -> Result<()> {
144 let mut children: Vec<Entry> = std::fs::read_dir(dir)
145 .with_context(|| format!("reading {}", dir.display()))?
146 .filter_map(|e| e.ok())
147 .map(|e| Entry {
148 is_dir: e.path().is_dir(),
149 path: e.path(),
150 depth,
151 })
152 .collect();
153 children.sort_by_key(|e| {
154 (
155 !e.is_dir,
156 e.path
157 .file_name()
158 .and_then(|n| n.to_str())
159 .unwrap_or("")
160 .to_lowercase(),
161 )
162 });
163
164 for child in children {
165 let descend = child.is_dir && expanded.contains(&child.path);
166 let path = child.path.clone();
167 out.push(child);
168 if descend {
169 collect(&path, depth + 1, expanded, out)?;
170 }
171 }
172 Ok(())
173}
174
175impl Panel for TreePanel {
176 fn name(&self) -> &'static str {
177 "tree"
178 }
179
180 fn title(&self) -> String {
181 self.root
182 .file_name()
183 .and_then(|n| n.to_str())
184 .unwrap_or("/")
185 .to_string()
186 }
187
188 fn render(&mut self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
189 let border = if ctx.is_focused {
190 ctx.theme.border_focused
191 } else {
192 ctx.theme.border
193 };
194 let block = Block::bordered()
195 .border_style(Style::default().fg(border))
196 .title(self.title());
197 let inner = block.inner(area);
198 block.render(area, buf);
199
200 self.height = inner.height as usize;
201 let end = (self.top_line + self.height).min(self.entries.len());
202 let lines: Vec<Line> = (self.top_line..end)
203 .map(|i| {
204 let entry = &self.entries[i];
205 let name = entry
206 .path
207 .file_name()
208 .and_then(|n| n.to_str())
209 .unwrap_or("?");
210 let marker = if !entry.is_dir {
214 " "
215 } else if self.expanded.contains(&entry.path) {
216 "v "
217 } else {
218 "> "
219 };
220 let label = format!("{}{marker}{name}", " ".repeat(entry.depth));
221 let style = if i == self.selected {
222 Style::default()
223 .fg(ctx.theme.selection_fg)
224 .bg(ctx.theme.selection_bg)
225 } else {
226 Style::default().fg(ctx.theme.fg)
227 };
228 Line::styled(label, style)
229 })
230 .collect();
231 Paragraph::new(lines)
232 .style(Style::default().bg(ctx.theme.bg))
233 .render(inner, buf);
234 }
235
236 fn handle_key(&mut self, chord: KeyChord) -> Vec<PanelEvent> {
237 match chord.raw.code {
238 KeyCode::Down => self.move_selection(1),
239 KeyCode::Up => self.move_selection(-1),
240 KeyCode::Enter => return self.activate(),
241 KeyCode::Right => {
242 if let Some(e) = self.entries.get(self.selected).cloned()
243 && e.is_dir
244 && !self.expanded.contains(&e.path)
245 {
246 return self.set_expanded(e.path, true);
247 }
248 }
249 KeyCode::Left => {
250 if let Some(e) = self.entries.get(self.selected).cloned()
251 && e.is_dir
252 && self.expanded.contains(&e.path)
253 {
254 return self.set_expanded(e.path, false);
255 }
256 }
257 _ => return Vec::new(),
258 }
259 vec![PanelEvent::NeedsRedraw]
260 }
261
262 fn handle_mouse(&mut self, event: MouseEvent, panel_area: Rect) -> Vec<PanelEvent> {
263 if event.kind != MouseEventKind::Down(MouseButton::Left) {
264 return Vec::new();
265 }
266 let inner = Self::list_area(panel_area);
267 let row = event.row.saturating_sub(inner.y) as usize;
268 let idx = self.top_line + row;
269 if idx >= self.entries.len() {
270 return Vec::new();
271 }
272 if idx == self.selected {
275 return self.activate();
276 }
277 self.selected = idx;
278 vec![PanelEvent::NeedsRedraw]
279 }
280
281 fn handle_scroll(&mut self, delta: i32, _panel_area: Rect) -> Vec<PanelEvent> {
282 let max_top = self.entries.len().saturating_sub(self.height.max(1));
283 self.top_line = (self.top_line as i64 + delta as i64).clamp(0, max_top as i64) as usize;
284 vec![PanelEvent::NeedsRedraw]
285 }
286
287 fn as_any(&self) -> &dyn Any {
288 self
289 }
290 fn as_any_mut(&mut self) -> &mut dyn Any {
291 self
292 }
293}