1use std::{
2 cmp::Ordering,
3 path::{Path, PathBuf},
4};
5
6use basalt_core::obsidian::{Note, VaultEntry};
7use ratatui::widgets::ListState;
8
9use crate::config::Symbols;
10
11use super::Item;
12
13#[derive(Debug, Default, Copy, Clone, PartialEq)]
14pub enum Sort {
15 #[default]
16 Asc,
17 Desc,
18}
19
20#[derive(Debug, Default, Copy, Clone, PartialEq)]
21pub enum Visibility {
22 Hidden,
23 #[default]
24 Visible,
25 FullWidth,
26}
27
28#[derive(Debug, Default, Clone, PartialEq)]
29pub struct ExplorerState {
30 pub(crate) title: String,
31 pub(crate) selected_note: Option<Note>,
32 pub(crate) selected_item_index: Option<usize>,
33 pub(crate) selected_item_path: Option<PathBuf>,
34 pub(crate) items: Vec<Item>,
35 pub(crate) flat_items: Vec<(Item, usize)>,
36 pub(crate) visibility: Visibility,
37 pub(crate) active: bool,
38 pub(crate) sort: Sort,
39 pub(crate) list_state: ListState,
40
41 pub(crate) symbols: Symbols,
42
43 pub(crate) editing: bool,
44}
45
46fn calculate_offset(row: usize, items_count: usize, window_height: usize) -> usize {
73 let half = window_height / 2;
74
75 if row + half > items_count.saturating_sub(1) {
76 items_count.saturating_sub(window_height)
77 } else {
78 row.saturating_sub(half)
79 }
80}
81
82pub fn flatten(sort: Sort, depth: usize) -> impl Fn(&Item) -> Vec<(Item, usize)> {
83 move |item| match item {
84 Item::File(..) => vec![(item.clone(), depth)],
85 Item::Directory {
86 expanded: true,
87 items,
88 ..
89 } => [(item.clone(), depth)]
90 .into_iter()
91 .chain({
92 let mut items = items.clone();
93 items.sort_by(sort_items_by(sort));
94 items
95 .iter()
96 .flat_map(flatten(sort, depth + 1))
97 .collect::<Vec<_>>()
98 })
99 .collect(),
100 Item::Directory {
101 expanded: false, ..
102 } => [(item.clone(), depth)].to_vec(),
103 }
104}
105
106fn sort_items_by(sort: Sort) -> impl Fn(&Item, &Item) -> Ordering {
107 move |a, b| match (a.is_dir(), b.is_dir()) {
108 (true, false) => Ordering::Less,
109 (false, true) => Ordering::Greater,
110 (true, true) => natord::compare(a.name(), b.name()),
111 _ => {
112 let a = a.name().to_lowercase();
113 let b = b.name().to_lowercase();
114 match sort {
115 Sort::Asc => natord::compare(&a, &b),
116 Sort::Desc => natord::compare(&b, &a),
117 }
118 }
119 }
120}
121
122impl ExplorerState {
123 pub fn new(title: &str, items: Vec<VaultEntry>, symbols: &Symbols) -> Self {
124 let items: Vec<Item> = items.into_iter().map(|entry| entry.into()).collect();
125 let sort = Sort::default();
126
127 let mut state = ExplorerState {
128 title: title.to_string(),
129 sort,
130 active: true,
131 visibility: Visibility::Visible,
132 selected_item_index: None,
133 selected_item_path: None,
134 selected_note: None,
135 symbols: symbols.clone(),
136 list_state: ListState::default().with_selected(Some(0)),
137 ..Default::default()
138 };
139
140 state.flatten_with_items(&items);
141 state
142 }
143
144 pub fn set_active(&mut self, active: bool) {
145 self.active = active;
146 }
147
148 fn map_to_item(&self, entry: VaultEntry) -> Item {
149 match entry {
150 VaultEntry::Directory {
151 name,
152 path,
153 entries,
154 } => {
155 let expanded = self
156 .flat_items
157 .iter()
158 .find_map(|(item, _)| match item {
159 Item::Directory {
160 path: item_path,
161 expanded,
162 ..
163 } if &path == item_path => Some(*expanded),
164 _ => None,
165 })
166 .unwrap_or(false);
167
168 Item::Directory {
169 name,
170 path,
171 expanded,
172 items: entries
173 .into_iter()
174 .map(|entry| self.map_to_item(entry))
175 .collect(),
176 }
177 }
178 _ => entry.into(),
179 }
180 }
181
182 pub fn with_entries(&mut self, entries: Vec<VaultEntry>, select: Option<PathBuf>) {
183 let items: Vec<Item> = entries
184 .into_iter()
185 .map(|entry| self.map_to_item(entry))
186 .collect();
187
188 self.flatten_with_items(&items);
189
190 if let Some(path) = select {
191 if let Some(index) = self.flat_items.iter().position(|(item, _)| match item {
192 Item::File(note) => note.path() == path,
193 Item::Directory { path: dir_path, .. } => dir_path == &path,
194 }) {
195 self.list_state.select(Some(index));
196 self.selected_item_index = Some(index);
197 self.selected_item_path = Some(path);
198 }
199 }
200 }
201
202 pub fn hide_pane(&mut self) {
203 match self.visibility {
204 Visibility::FullWidth => self.visibility = Visibility::Visible,
205 Visibility::Visible => self.visibility = Visibility::Hidden,
206 _ => {}
207 }
208 }
209
210 pub fn expand_pane(&mut self) {
211 match self.visibility {
212 Visibility::Hidden => self.visibility = Visibility::Visible,
213 Visibility::Visible => self.visibility = Visibility::FullWidth,
214 _ => {}
215 }
216 }
217
218 pub fn toggle(&mut self) {
219 if self.is_open() {
220 self.visibility = Visibility::Hidden;
221 } else {
222 self.visibility = Visibility::Visible;
223 }
224 }
225
226 pub fn flatten_with_sort(&mut self, sort: Sort) {
227 let mut items = self.items.clone();
228 items.sort_by(sort_items_by(sort));
229
230 self.flat_items = items.iter().flat_map(flatten(sort, 0)).collect();
231 self.items = items;
232 self.sort = sort;
233 }
234
235 pub fn flatten_with_items(&mut self, items: &[Item]) {
236 let mut items = items.to_vec();
237 items.sort_by(sort_items_by(self.sort));
238
239 self.flat_items = items.iter().flat_map(flatten(self.sort, 0)).collect();
240 self.items = items.to_vec();
241 }
242
243 pub fn sort(&mut self) {
244 let sort = match self.sort {
245 Sort::Asc => Sort::Desc,
246 Sort::Desc => Sort::Asc,
247 };
248
249 self.flatten_with_sort(sort)
250 }
251
252 pub fn update_offset_mut(&mut self, window_height: usize) -> &Self {
253 if !self.items.is_empty() {
254 let idx = self.list_state.selected().unwrap_or_default();
255 let items_count = self.items.len();
256
257 let offset = calculate_offset(idx, items_count, window_height);
258
259 let list_state = &mut self.list_state;
260 *list_state.offset_mut() = offset;
261 }
262
263 self
264 }
265
266 fn toggle_item_in_tree(item: &Item, identifier: &Path) -> Item {
267 let item = item.clone();
268
269 match item {
270 Item::Directory {
271 expanded,
272 path,
273 name,
274 items,
275 } => {
276 let expanded = if path == identifier {
277 !expanded
278 } else {
279 expanded
280 };
281
282 Item::Directory {
283 name,
284 path,
285 expanded,
286 items: items
287 .iter()
288 .map(|child| Self::toggle_item_in_tree(child, identifier))
289 .collect(),
290 }
291 }
292 _ => item,
293 }
294 }
295
296 pub fn select(&mut self) {
297 let Some(selected_item_index) = self.list_state.selected() else {
298 return;
299 };
300
301 let Some(current_item) = self.flat_items.get(selected_item_index) else {
302 return;
303 };
304
305 match current_item {
306 (Item::Directory { path, .. }, _) => {
307 let items: Vec<Item> = self
308 .items
309 .clone()
310 .iter()
311 .map(|item| Self::toggle_item_in_tree(item, path))
312 .collect();
313
314 self.flatten_with_items(&items)
315 }
316 (Item::File(note), _) => {
317 self.selected_note = Some(note.clone());
318 self.selected_item_index = Some(selected_item_index);
319 self.selected_item_path = Some(note.path().to_path_buf());
320 }
321 }
322 }
323
324 pub fn current_item(&self) -> Option<&Item> {
325 let selected_item_index = self.list_state.selected()?;
326 self.flat_items
327 .get(selected_item_index)
328 .map(|(item, _)| item)
329 }
330
331 pub fn selected_path(&self) -> Option<PathBuf> {
332 self.selected_item_path.clone()
333 }
334
335 pub fn is_open(&self) -> bool {
336 matches!(self.visibility, Visibility::Visible | Visibility::FullWidth)
337 }
338
339 pub fn next(&mut self, amount: usize) {
340 let index = self.list_state.selected().map(|i| {
341 i.saturating_add(amount)
342 .min(self.flat_items.len().saturating_sub(1))
343 });
344
345 self.list_state.select(index);
346 }
347
348 pub fn previous(&mut self, amount: usize) {
349 let index = self.list_state.selected().map(|i| i.saturating_sub(amount));
350
351 self.list_state.select(index);
352 }
353}