312 lines
9.8 KiB
Rust
Executable File
312 lines
9.8 KiB
Rust
Executable File
use std::path::PathBuf;
|
|
|
|
use ratatui::{
|
|
crossterm::event::{Event, KeyCode},
|
|
style::{Color, Style},
|
|
text::{Line, Span},
|
|
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
|
};
|
|
|
|
use super::Component;
|
|
|
|
pub struct Tree {
|
|
root: Folder,
|
|
// pub selected: PathBuf,
|
|
vertical_scroll: usize,
|
|
vertical_scroll_state: ScrollbarState,
|
|
selected_idx: usize,
|
|
focused: bool,
|
|
}
|
|
|
|
impl Component for Tree {
|
|
fn update(&mut self, mut evt: super::AppEventClient) {
|
|
if evt.is_focused_and_key_pressed(KeyCode::Up, None) {
|
|
evt.mark_as_handled();
|
|
if self.selected_idx > 0 {
|
|
self.selected_idx -= 1;
|
|
}
|
|
} else if evt.is_focused_and_key_pressed(KeyCode::Down, None) {
|
|
evt.mark_as_handled();
|
|
let entries = self.root.get_entries();
|
|
if self.selected_idx < entries.len() - 1 {
|
|
self.selected_idx += 1;
|
|
}
|
|
}
|
|
|
|
if evt.has_focus() {
|
|
evt.register_helper("->", "Expand");
|
|
evt.register_helper("<-", "Collapse");
|
|
evt.register_helper("↑", "Up");
|
|
evt.register_helper("↓", "Down");
|
|
}
|
|
|
|
let mut tmp = self.selected_idx;
|
|
self.root.update(&evt, &mut tmp);
|
|
|
|
self.focused = evt.has_focus();
|
|
}
|
|
|
|
fn render(&mut self, f: &mut ratatui::Frame, rect: ratatui::prelude::Rect) {
|
|
let block = Block::bordered()
|
|
.title("Select Folder")
|
|
.borders(ratatui::widgets::Borders::ALL)
|
|
.border_style(ratatui::style::Style::default().fg(if self.focused {
|
|
ratatui::style::Color::Cyan
|
|
} else {
|
|
ratatui::style::Color::Gray
|
|
}));
|
|
|
|
let inner_area = block.inner(rect);
|
|
f.render_widget(block, rect);
|
|
|
|
let entries = self.root.get_entries();
|
|
let lines: Vec<Line> = entries.iter().map(|entry| entry.get_line()).collect();
|
|
|
|
if self.selected_idx >= self.vertical_scroll + inner_area.height as usize {
|
|
self.vertical_scroll = self.selected_idx - inner_area.height as usize + 1;
|
|
} else if self.selected_idx < self.vertical_scroll as usize {
|
|
self.vertical_scroll = self.selected_idx;
|
|
}
|
|
|
|
self.vertical_scroll_state = self
|
|
.vertical_scroll_state
|
|
.content_length(lines.len())
|
|
.position(self.vertical_scroll as usize);
|
|
|
|
let paragraph = Paragraph::new(lines).scroll((self.vertical_scroll as u16, 0));
|
|
|
|
f.render_widget(paragraph, inner_area);
|
|
f.render_stateful_widget(
|
|
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
|
.begin_symbol(Some("↑"))
|
|
.end_symbol(Some("↓")),
|
|
inner_area,
|
|
&mut self.vertical_scroll_state,
|
|
);
|
|
|
|
// todo!("Implement the Widget trait for Tree");
|
|
}
|
|
}
|
|
|
|
impl Tree {
|
|
pub fn new(initial_path: impl Into<PathBuf>) -> Self {
|
|
let path: PathBuf = initial_path.into();
|
|
let name = path.to_string_lossy().to_string();
|
|
|
|
let mut root = Folder::new(name, path, 0);
|
|
root.selected = true;
|
|
|
|
Self {
|
|
root,
|
|
// selected: PathBuf::new(),
|
|
vertical_scroll: 0,
|
|
vertical_scroll_state: ScrollbarState::default(),
|
|
selected_idx: 0,
|
|
focused: false,
|
|
}
|
|
}
|
|
|
|
pub fn get_selected(&self) -> PathBuf {
|
|
let entries = self.root.get_entries();
|
|
if self.selected_idx < entries.len() {
|
|
return entries[self.selected_idx].path.clone();
|
|
}
|
|
PathBuf::new()
|
|
}
|
|
|
|
// pub fn show(&mut self, ui: &mut egui::Ui) {
|
|
// ui.vertical(|ui| {
|
|
// self.root.show(ui, &mut self.selected);
|
|
// });
|
|
// }
|
|
}
|
|
|
|
pub struct Folder {
|
|
name: String,
|
|
path: PathBuf,
|
|
expanded: bool,
|
|
children: Option<Vec<Folder>>,
|
|
selected: bool,
|
|
depth: usize,
|
|
}
|
|
|
|
impl Folder {
|
|
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>, depth: usize) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
path: path.into(),
|
|
expanded: false,
|
|
selected: false,
|
|
children: Default::default(),
|
|
depth,
|
|
}
|
|
}
|
|
|
|
pub fn expand(&mut self) {
|
|
if self.children.is_none() {
|
|
let mut children_data = vec![];
|
|
// println!("Reading dir: {:?}", self.path);
|
|
std::fs::read_dir(&self.path)
|
|
.map(|entries| {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
// println!(
|
|
// "Found dir: {:?}, file {}, link {}",
|
|
// path,
|
|
// path.is_file(),
|
|
// path.is_symlink()
|
|
// );
|
|
let path2 = path.clone();
|
|
let name = path2.file_name().unwrap().to_string_lossy();
|
|
children_data.push(Folder::new(name, path, self.depth + 1));
|
|
}
|
|
}
|
|
})
|
|
.unwrap_or_else(|err| {
|
|
eprintln!("Failed to read dir: {}", err);
|
|
});
|
|
children_data.sort_by_key(|folder| folder.name.clone());
|
|
self.children.replace(children_data);
|
|
}
|
|
}
|
|
|
|
pub fn update(&mut self, evt: &super::AppEventClient, selected_idx: &mut usize) {
|
|
if *selected_idx == 0 {
|
|
self.selected = true;
|
|
*selected_idx = usize::MAX;
|
|
} else {
|
|
self.selected = false;
|
|
}
|
|
*selected_idx -= 1;
|
|
if evt.has_focus() && self.selected {
|
|
if let Some(evt) = &evt.event {
|
|
if let Event::Key(key) = evt {
|
|
match key.code {
|
|
KeyCode::Right | KeyCode::Enter => {
|
|
self.expand();
|
|
self.expanded = true;
|
|
}
|
|
KeyCode::Left => {
|
|
self.expanded = false;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.expanded {
|
|
if let Some(children) = &mut self.children {
|
|
for child in children {
|
|
child.update(evt, selected_idx);
|
|
// if child.selected {
|
|
// self.selected = true;
|
|
// }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_entries(&self) -> Vec<&Folder> {
|
|
let mut entries: Vec<&Folder> = vec![];
|
|
if self.expanded {
|
|
if let Some(children) = &self.children {
|
|
for child in children {
|
|
entries.extend(child.get_entries());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut res: Vec<&Folder> = vec![self];
|
|
res.extend(entries);
|
|
|
|
res
|
|
}
|
|
|
|
pub fn get_line(&self) -> Line {
|
|
let pad = " ".repeat(self.depth);
|
|
Line::from(vec![Span::styled(
|
|
format!(
|
|
"{}{} {}",
|
|
pad,
|
|
if self.expanded { "▼" } else { "▶" },
|
|
self.name
|
|
),
|
|
if self.selected {
|
|
Style::new().bg(Color::White).fg(Color::Black)
|
|
} else {
|
|
Style::default()
|
|
},
|
|
)])
|
|
}
|
|
|
|
// pub fn show(&mut self, ui: &mut egui::Ui, selected: &mut PathBuf) {
|
|
// let Self {
|
|
// name,
|
|
// path,
|
|
// expanded,
|
|
// children,
|
|
// } = self;
|
|
|
|
// if children.is_none() {
|
|
// let mut children_data = vec![];
|
|
// println!("Reading dir: {:?}", path);
|
|
// std::fs::read_dir(&path)
|
|
// .map(|entries| {
|
|
// for entry in entries.flatten() {
|
|
// let path = entry.path();
|
|
// if path.is_dir() {
|
|
// println!(
|
|
// "Found dir: {:?}, file {}, link {}",
|
|
// path,
|
|
// path.is_file(),
|
|
// path.is_symlink()
|
|
// );
|
|
// let path2 = path.clone();
|
|
// let name = path2.file_name().unwrap().to_string_lossy();
|
|
// children_data.push(Folder::new(name, path));
|
|
// }
|
|
// }
|
|
// })
|
|
// .unwrap_or_else(|err| {
|
|
// eprintln!("Failed to read dir: {}", err);
|
|
// });
|
|
// children_data.sort_by_key(|folder| folder.name.clone());
|
|
// children.replace(children_data);
|
|
// }
|
|
|
|
// ui.horizontal(|ui| {
|
|
// if let Some(children) = children {
|
|
// if !children.is_empty() {
|
|
// let label = if *expanded { "▼" } else { "▶" };
|
|
// let response = ui.selectable_label(false, label);
|
|
|
|
// if response.clicked() && !children.is_empty() {
|
|
// *expanded = !*expanded;
|
|
// }
|
|
// }
|
|
// } else {
|
|
// ui.label("⌛");
|
|
// }
|
|
|
|
// if ui
|
|
// .selectable_label(*selected == *path, name.to_owned())
|
|
// .clicked()
|
|
// {
|
|
// *selected = path.clone();
|
|
// }
|
|
// });
|
|
|
|
// if let Some(children) = children {
|
|
// if *expanded {
|
|
// ui.indent("", |ui| {
|
|
// for child in children {
|
|
// child.show(ui, selected);
|
|
// }
|
|
// });
|
|
// }
|
|
// }
|
|
// }
|
|
}
|