1use std::borrow::ToOwned;
2use std::cmp;
3use std::path::Path;
4
5use itertools::Itertools;
6use ratatui::buffer::Buffer;
7use ratatui::layout::{Alignment, Rect};
8use ratatui::style::Stylize;
9use ratatui::text::Text;
10use ratatui::widgets::{HighlightSpacing, Widget};
11use ratatui::{
12 style::{Modifier, Style},
13 widgets::{Block, List, ListItem, ListState, StatefulWidget},
14};
15
16use crate::search::find_files;
17use crate::util::colors::color_config;
18
19#[derive(Debug, Clone)]
20enum MdFileComponent {
21 File(MdFile),
22 Spacer,
23}
24
25#[derive(Debug, Clone, Default)]
26pub struct MdFile {
27 pub path: String,
28 pub name: String,
29}
30
31impl MdFile {
32 #[must_use]
33 pub fn new(path: String, name: String) -> Self {
34 Self { path, name }
35 }
36
37 #[must_use]
38 pub fn path_str(&self) -> &str {
39 &self.path
40 }
41
42 #[must_use]
43 pub fn path(&self) -> &Path {
44 Path::new(&self.path)
45 }
46
47 #[must_use]
48 pub fn name(&self) -> &str {
49 &self.name
50 }
51}
52
53impl From<MdFile> for ListItem<'_> {
54 fn from(val: MdFile) -> Self {
55 let mut text = Text::default();
56 text.extend([
57 val.name.clone().fg(color_config().file_tree_name_color),
58 val.path
59 .clone()
60 .italic()
61 .fg(color_config().file_tree_path_color),
62 ]);
63 ListItem::new(text)
64 }
65}
66
67impl From<MdFileComponent> for ListItem<'_> {
68 fn from(value: MdFileComponent) -> Self {
69 match value {
70 MdFileComponent::File(f) => f.into(),
71 MdFileComponent::Spacer => ListItem::new(Text::raw("")),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Default)]
77pub struct FileTree {
78 all_files: Vec<MdFile>,
79 files: Vec<MdFileComponent>,
80 page: u32,
81 list_state: ListState,
82 search: Option<String>,
83 loaded: bool,
84}
85
86impl FileTree {
87 #[must_use]
88 pub fn new() -> Self {
89 Self {
90 all_files: Vec::new(),
91 files: Vec::new(),
92 list_state: ListState::default(),
93 page: 0,
94 search: None,
95 loaded: false,
96 }
97 }
98
99 #[must_use]
100 pub fn loaded(&self) -> bool {
101 self.loaded
102 }
103
104 #[must_use]
105 pub fn finish(self) -> Self {
106 let mut this = self;
107 this.loaded = true;
108 this
109 }
110
111 pub fn sort(&mut self) {
112 let filtered: Vec<&MdFile> = self
113 .files
114 .iter()
115 .filter_map(|c| match c {
116 MdFileComponent::File(f) => Some(f),
117 MdFileComponent::Spacer => None,
118 })
119 .sorted_unstable_by(|a, b| a.name.cmp(&b.name))
120 .collect();
121
122 let spacers = vec![MdFileComponent::Spacer; filtered.len()];
123
124 self.files = filtered
125 .into_iter()
126 .zip(spacers)
127 .flat_map(|(f, s)| vec![MdFileComponent::File(f.to_owned()), s])
128 .collect::<Vec<_>>();
129 }
130
131 pub fn sort_name(&mut self) {
132 let (mut files, mut spacers): (Vec<_>, Vec<_>) = self
134 .files
135 .drain(..)
136 .partition(|c| matches!(c, MdFileComponent::File(_)));
137
138 files.sort_unstable_by(|a, b| match (a, b) {
140 (MdFileComponent::File(fa), MdFileComponent::File(fb)) => {
141 let a = fa
142 .path()
143 .to_str()
144 .unwrap()
145 .trim_start_matches("./")
146 .trim_start_matches(char::is_alphabetic);
147 let b = fb
148 .path()
149 .to_str()
150 .unwrap()
151 .trim_start_matches("./")
152 .trim_start_matches(char::is_alphabetic);
153
154 b.to_lowercase().cmp(&a.to_lowercase())
155 }
156 _ => unreachable!(), });
158
159 let mut result = Vec::with_capacity(files.len() + spacers.len());
161 while let (Some(file), Some(spacer)) = (files.pop(), spacers.pop()) {
162 result.push(file);
163 result.push(spacer);
164 }
165
166 self.files = result;
168 }
169
170 pub fn search(&mut self, query: Option<&str>) {
171 self.state_mut().select(None);
172 self.page = 0;
173 self.search = query.map(ToOwned::to_owned);
174 match query {
175 Some(query) => {
176 self.files = find_files(&self.all_files, query)
177 .into_iter()
178 .map(MdFileComponent::File)
179 .collect();
180 }
181 None => {
182 self.files = self
183 .all_files
184 .iter()
185 .cloned()
186 .map(MdFileComponent::File)
187 .collect();
188 }
189 }
190 self.fill_spacers();
191 }
192
193 fn fill_spacers(&mut self) {
194 let spacers = vec![MdFileComponent::Spacer; self.files.len()];
195 self.files = self
196 .files
197 .iter()
198 .cloned()
199 .zip(spacers)
200 .flat_map(|(f, s)| vec![f, s])
201 .collect::<Vec<_>>();
202 }
203
204 pub fn next(&mut self, height: u16) {
205 let i = match self.list_state.selected() {
206 Some(i) => {
207 if i >= self.files.len() - 2 {
208 0
209 } else {
210 i + 2
211 }
212 }
213 None => 0,
214 };
215 self.page = (i / self.partition(height)) as u32;
216 self.list_state.select(Some(i));
217 }
218
219 pub fn previous(&mut self, height: u16) {
220 let i = match self.list_state.selected() {
221 Some(i) => {
222 if i == 0 {
223 self.files.len() - 2
224 } else {
225 i.saturating_sub(2)
226 }
227 }
228 None => 0,
229 };
230 self.page = (i / self.partition(height)) as u32;
231 self.list_state.select(Some(i));
232 }
233
234 pub fn next_page(&mut self, height: u16) {
235 let partition = self.partition(height);
236 let i = match self.list_state.selected() {
237 Some(i) => {
238 if i + partition >= self.files.len() {
239 0
240 } else {
241 i + partition
242 }
243 }
244 None => 0,
245 };
246 self.page = (i / partition) as u32;
247 self.list_state.select(Some(i));
248 }
249
250 pub fn previous_page(&mut self, height: u16) {
251 let partition = self.partition(height);
252 let i = match self.list_state.selected() {
253 Some(i) => {
254 if i < partition {
255 self.files.len().saturating_sub(partition)
256 } else {
257 i.saturating_sub(partition)
258 }
259 }
260 None => 0,
261 };
262 self.page = (i / partition) as u32;
263 self.list_state.select(Some(i));
264 }
265
266 pub fn first(&mut self) {
267 self.list_state.select(Some(0));
268 self.page = 0;
269 }
270
271 pub fn last(&mut self, height: u16) {
272 let partition = self.partition(height);
273 let i = self.files.len() - 2;
274 self.list_state.select(Some(i));
275 self.page = (i / partition) as u32;
276 }
277
278 pub fn unselect(&mut self) {
279 self.list_state.select(None);
280 }
281
282 #[must_use]
283 pub fn selected(&self) -> Option<&MdFile> {
284 match self.list_state.selected() {
285 Some(i) => self.files.get(i).and_then(|f| match f {
286 MdFileComponent::File(f) => Some(f),
287 MdFileComponent::Spacer => None,
288 }),
289 None => None,
290 }
291 }
292
293 pub fn add_file(&mut self, file: MdFile) {
294 self.all_files.push(file.clone());
295 self.files.push(MdFileComponent::File(file));
296 self.files.push(MdFileComponent::Spacer);
297 }
298
299 #[must_use]
300 pub fn files(&self) -> Vec<&MdFile> {
301 self.files
302 .iter()
303 .filter_map(|f| match f {
304 MdFileComponent::File(f) => Some(f),
305 MdFileComponent::Spacer => None,
306 })
307 .collect::<Vec<&MdFile>>()
308 }
309
310 #[must_use]
311 pub fn all_files(&self) -> &Vec<MdFile> {
312 &self.all_files
313 }
314
315 fn partition(&self, height: u16) -> usize {
316 let partition_size = usize::midpoint(height as usize, 2);
317
318 if partition_size.is_multiple_of(2) {
319 partition_size
320 } else {
321 partition_size + 1
322 }
323 }
324
325 #[must_use]
326 pub fn state(&self) -> &ListState {
327 &self.list_state
328 }
329
330 #[must_use]
331 pub fn height(&self, height: u16) -> usize {
332 cmp::min(
333 self.partition(height) / 2 * 3,
334 self.files
335 .iter()
336 .filter(|f| matches!(f, MdFileComponent::File(_)))
337 .count()
338 * 3,
339 )
340 }
341
342 pub fn state_mut(&mut self) -> &mut ListState {
343 &mut self.list_state
344 }
345}
346
347impl Widget for FileTree {
348 fn render(self, area: Rect, buf: &mut Buffer) {
349 let mut state = self.state().to_owned();
350 let file_len = self.files.len();
351 let partition = self.partition(area.height);
352
353 let items = if let Some(iter) = self
354 .files
355 .chunks(self.partition(area.height))
356 .nth(self.page as usize)
357 {
358 iter.to_owned()
359 } else {
360 self.files
361 };
362
363 state.select(state.selected().map(|i| i % partition));
364
365 let y_height = items.len() / 2 * 3;
366
367 let items = List::new(items)
368 .block(
369 Block::default()
370 .title("MD-TUI")
371 .add_modifier(Modifier::BOLD)
372 .title_alignment(Alignment::Center),
373 )
374 .highlight_style(
375 Style::default()
376 .fg(color_config().file_tree_selected_fg_color)
377 .add_modifier(Modifier::BOLD),
378 )
379 .highlight_symbol("\u{02503} ")
380 .repeat_highlight_symbol(true)
381 .highlight_spacing(HighlightSpacing::Always);
382
383 StatefulWidget::render(items, area, buf, &mut state);
384
385 let area = Rect {
386 y: area.y + y_height as u16 + 2,
387 ..area
388 };
389
390 let total_pages = usize::div_ceil(file_len, partition);
391
392 let page_count_str = format!(" {}/{}", self.page + 1, total_pages);
393
394 let page_count = Text::styled(
395 page_count_str,
396 Style::default().fg(color_config().file_tree_page_count_color),
397 );
398
399 page_count.render(area, buf);
400 }
401}