Compare commits

...

2 Commits

Author SHA1 Message Date
Fabian Stamm
009dd4657f Work on TUI 2025-03-31 19:48:29 +02:00
Fabian Stamm
a9bf713dd5 Fix stuff 2025-03-31 09:29:21 +02:00
7 changed files with 1259 additions and 2601 deletions

2628
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,18 +4,13 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[features] [features]
default = ["wayland", "x11"] default = []
wayland = ["eframe/wayland"]
x11 = ["eframe/x11"]
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
eframe = { version = "0.28.1", features = [ color-eyre = "0.6.3"
"wgpu",
"default_fonts",
], default-features = false }
egui_extras = "0.28.1"
env_logger = { version = "0.11", features = ["auto-color", "humantime"] } env_logger = { version = "0.11", features = ["auto-color", "humantime"] }
posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.5" } posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.6" }
ratatui = { version = "0.29.0", features = ["all-widgets"] }
walkdir = "2" walkdir = "2"

0
Dockerfile Normal file
View File

View File

@ -1,22 +1,21 @@
use anyhow::Result; use anyhow::Result;
use eframe::egui; use ratatui::{
crossterm::event::{self, Event},
DefaultTerminal, Frame,
};
mod helper; mod helper;
mod ui; mod ui;
fn main() -> Result<()> { fn main() -> Result<()> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
hardware_acceleration: eframe::HardwareAcceleration::Preferred,
..Default::default()
};
eframe::run_native(
"ACL Editor",
options,
Box::new(|cc| Ok(Box::new(ui::App::new(&cc.egui_ctx)))),
)
.expect("Failed to run eframe app");
Ok(()) color_eyre::install().expect("failed to install color_eyre");
let mut terminal = ratatui::init();
let result = ui::App::default().run(&mut terminal);
ratatui::restore();
result
.map_err(|e| anyhow::anyhow!("Error: {}", e))
.map(|_| ())
} }

View File

@ -1,13 +1,14 @@
use std::{borrow::Borrow, path::PathBuf, thread::JoinHandle}; use std::{borrow::Borrow, io, path::PathBuf, thread::JoinHandle};
use anyhow::Result; use anyhow::Result;
use eframe::egui; use ratatui::{widgets::Widget, DefaultTerminal, Frame};
use egui_extras::{Column, TableBuilder};
use crate::helper::getent::{get_groups, get_users, Group, User}; use crate::helper::getent::{get_groups, get_users, Group, User};
use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX}; use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX};
use super::Component;
pub struct ACLEditor { pub struct ACLEditor {
pub path: PathBuf, pub path: PathBuf,
pub is_changed: bool, pub is_changed: bool,
@ -22,8 +23,8 @@ pub struct ACLEditor {
pub save_thread_error: Option<String>, pub save_thread_error: Option<String>,
} }
impl ACLEditor { impl Default for ACLEditor {
pub fn new() -> Self { fn default() -> Self {
Self { Self {
path: PathBuf::new(), path: PathBuf::new(),
is_changed: false, is_changed: false,
@ -36,322 +37,368 @@ impl ACLEditor {
save_thread_error: None, save_thread_error: None,
} }
} }
pub fn show(&mut self, ui: &mut egui::Ui, selected_path: &PathBuf) { }
let mut show_save_or_dicard_message = false;
ui.vertical(|ui| {
if self.save_thread.is_some() {
if self.save_thread.as_ref().unwrap().is_finished() {
let save_thread = std::mem::replace(&mut self.save_thread, None);
let result = save_thread.unwrap().join();
match result {
Ok(Ok(_)) => {
ui.label("Saved successfully!");
self.save_thread_error = None;
self.is_changed = false;
}
Ok(Err(e)) => {
self.save_thread_error = Some(format!("Failed to save {:?}", e));
}
Err(e) => {
self.save_thread_error = Some(format!("Failed to join {:?}", e));
}
}
} else {
egui::Frame::default()
.fill(egui::Color32::GREEN)
.inner_margin(4.0)
.show(ui, |ui| {
ui.label("Apply changes...");
});
}
} else if let Some(err) = &self.save_thread_error.clone() {
egui::Frame::default()
.fill(egui::Color32::DARK_RED)
.inner_margin(4.0)
.show(ui, |ui| {
ui.label(err);
if ui.button("OK").clicked() { impl Component for ACLEditor {
self.save_thread_error = None; fn render(&mut self, f: &mut Frame, rect: ratatui::prelude::Rect, evt: super::AppEventClient) {
} let block =
});
} else {
ui.heading(format!("Editing: {:?}", self.path));
ui.separator();
if self.path != *selected_path { // todo!("Implement the rendering logic for ACLEditor");
if !self.is_changed { if let Some(save_thread) = &self.save_thread {
// Load ACLs if save_thread.is_finished() {
self.path = selected_path.clone(); let result = save_thread.join();
self.acl = PosixACL::new_from_file(&self.path, false).unwrap(); match result {
} else { Ok(Ok(_)) => {
show_save_or_dicard_message = true; f.set_title("Saved successfully!");
}
}
self.acl_table(ui);
ui.separator();
ui.label("Add new entry:");
self.new_user(ui);
self.new_group(ui);
ui.separator();
if show_save_or_dicard_message {
egui::Frame::default()
.fill(egui::Color32::YELLOW)
.inner_margin(4.0)
.show(ui, |ui| {
ui.label("You have unsaved changes! Save or discard to continue.");
});
}
ui.horizontal(|ui| {
if ui.button("Save").clicked() {
// TODO: Save ACLs
let path = self.path.clone();
let acl = self.acl.clone();
self.save_thread_error = None; self.save_thread_error = None;
self.save_thread = Some(std::thread::spawn(move || {
crate::helper::acl_writer::write_acl_recursive(path, acl)
}));
}
if ui.button("Discard").clicked() {
self.is_changed = false; self.is_changed = false;
self.path.clear();
} }
}); Ok(Err(e)) => {
self.save_thread_error = Some(format!("Failed to save {:?}", e));
}
Err(e) => {
self.save_thread_error = Some(format!("Failed to join {:?}", e));
}
}
} else {
f.set_title("Saving...");
} }
}); } else if let Some(err) = &self.save_thread_error {
f.set_title(err);
} else {
f.set_title(format!("Editing: {:?}", self.path));
}
} }
}
fn new_group(&mut self, ui: &mut egui::Ui) { impl ACLEditor {
ui.label("Group:"); pub fn new(path: impl Into<PathBuf>) -> Self {
let selected = filter_dropbox(
ui,
"group_popup",
&mut self.search_group,
self.available_groups.iter(),
|group, filter| group.name.contains(filter),
|group| group.name.as_str(),
);
if let Some(selected) = selected { let path: PathBuf = path.into();
self.acl
.entries Self {
.push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX)); path,
self.is_changed = true; ..Self::default()
} }
} }
fn new_user(&mut self, ui: &mut egui::Ui) { // pub fn show(&mut self, ui: &mut egui::Ui, selected_path: &PathBuf) {
ui.label("User:"); // let mut show_save_or_dicard_message = false;
let selected = filter_dropbox( // ui.vertical(|ui| {
ui, // if self.save_thread.is_some() {
"user_popup", // if self.save_thread.as_ref().unwrap().is_finished() {
&mut self.search_user, // let save_thread = std::mem::replace(&mut self.save_thread, None);
self.available_users.iter(), // let result = save_thread.unwrap().join();
|user, filter| user.name.contains(filter), // match result {
|user| user.name.as_str(), // Ok(Ok(_)) => {
); // ui.label("Saved successfully!");
// self.save_thread_error = None;
// self.is_changed = false;
// }
// Ok(Err(e)) => {
// self.save_thread_error = Some(format!("Failed to save {:?}", e));
// }
// Err(e) => {
// self.save_thread_error = Some(format!("Failed to join {:?}", e));
// }
// }
// } else {
// egui::Frame::default()
// .fill(egui::Color32::GREEN)
// .inner_margin(4.0)
// .show(ui, |ui| {
// ui.label("Apply changes...");
// });
// }
// } else if let Some(err) = &self.save_thread_error.clone() {
// egui::Frame::default()
// .fill(egui::Color32::DARK_RED)
// .inner_margin(4.0)
// .show(ui, |ui| {
// ui.label(err);
if let Some(selected) = selected { // if ui.button("OK").clicked() {
self.acl // self.save_thread_error = None;
.entries // }
.push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX)); // });
self.is_changed = true; // } else {
} // ui.heading(format!("Editing: {:?}", self.path));
} // ui.separator();
fn acl_table(&mut self, ui: &mut egui::Ui) { // if self.path != *selected_path {
TableBuilder::new(ui) // if !self.is_changed {
.auto_shrink([false, true]) // // Load ACLs
.striped(true) // self.path = selected_path.clone();
.column(Column::auto().resizable(false)) // self.acl = PosixACL::new_from_file(&self.path, false).unwrap();
.column(Column::remainder().resizable(false)) // } else {
.column(Column::auto().resizable(false)) // show_save_or_dicard_message = true;
.column(Column::auto().resizable(false)) // }
.column(Column::auto().resizable(false)) // }
.column(Column::auto().resizable(false))
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("Type");
});
header.col(|ui| {
ui.heading("Name");
});
header.col(|ui| {
ui.heading("R");
});
header.col(|ui| {
ui.heading("W");
});
header.col(|ui| {
ui.heading("X");
});
header.col(|ui| {
ui.heading("D");
});
})
.body(|mut body| {
let mut to_delete: Vec<Qualifier> = Vec::new();
self.acl.entries.sort(); // self.acl_table(ui);
for entry in &mut self.acl.entries {
body.row(30.0, |mut row| {
row.col(|ui| {
let label = match entry.0 {
Qualifier::User(_) => "U",
Qualifier::Group(_) => "G",
Qualifier::Other => "",
Qualifier::GroupObj => "G",
Qualifier::UserObj => "U",
Qualifier::Mask => "",
};
ui.label(label);
});
row.col(|ui| {
let label = match entry.0 {
Qualifier::User(uid) => self
.available_users
.iter()
.find(|user| user.uid == uid)
.map(|user| user.name.clone())
.unwrap_or_else(|| {
self.available_groups
.iter()
.find(|group| group.gid == uid)
.map(|group| format!("({})", group.name))
.unwrap_or_else(|| format!("Unknown user {}", uid))
}),
Qualifier::Group(gid) => self
.available_groups
.iter()
.find(|group| group.gid == gid)
.map(|group| group.name.clone())
.unwrap_or_else(|| {
self.available_users
.iter()
.find(|user| user.uid == gid)
.map(|user| format!("({})", user.name))
.unwrap_or_else(|| format!("Unknown group {}", gid))
}),
Qualifier::Other => "Other".to_string(),
Qualifier::GroupObj => "GroupObj".to_string(),
Qualifier::UserObj => "UserObj".to_string(),
Qualifier::Mask => "Mask".to_string(),
};
ui.label(label); // ui.separator();
}); // ui.label("Add new entry:");
row.col(|ui| {
let mut checked = entry.1.contains(PermSet::ACL_READ);
if ui.checkbox(&mut checked, "").changed() {
if checked {
entry.1.insert(PermSet::ACL_READ);
self.is_changed = true;
} else {
entry.1.remove(PermSet::ACL_READ);
self.is_changed = true;
}
}
});
row.col(|ui| {
let mut checked = entry.1.contains(PermSet::ACL_WRITE);
if ui.checkbox(&mut checked, "").changed() {
if checked {
entry.1.insert(PermSet::ACL_WRITE);
self.is_changed = true;
} else {
entry.1.remove(PermSet::ACL_WRITE);
self.is_changed = true;
}
}
});
row.col(|ui| {
let mut checked = entry.1.contains(PermSet::ACL_EXECUTE);
if ui.checkbox(&mut checked, "").changed() {
if checked {
entry.1.insert(PermSet::ACL_EXECUTE);
self.is_changed = true;
} else {
entry.1.remove(PermSet::ACL_EXECUTE);
self.is_changed = true;
}
}
});
row.col(|ui| match entry.0 { // self.new_user(ui);
Qualifier::User(_) | Qualifier::Group(_) => { // self.new_group(ui);
if ui.button("🗑").clicked() {
to_delete.push(entry.0.clone());
}
}
_ => {}
});
});
}
for entry in to_delete { // ui.separator();
self.acl.entries.retain(|e| e.0 != entry);
self.is_changed = true; // if show_save_or_dicard_message {
} // egui::Frame::default()
}); // .fill(egui::Color32::YELLOW)
} // .inner_margin(4.0)
// .show(ui, |ui| {
// ui.label("You have unsaved changes! Save or discard to continue.");
// });
// }
// ui.horizontal(|ui| {
// if ui.button("Save").clicked() {
// let path = self.path.clone();
// let mut acl = self.acl.clone();
// acl.set(ACLEntry(Qualifier::Mask, PermSet::all())); // Make sure mask is set!
// self.save_thread_error = None;
// self.save_thread = Some(std::thread::spawn(move || {
// crate::helper::acl_writer::write_acl_recursive(path, acl)
// }));
// }
// if ui.button("Discard").clicked() {
// self.is_changed = false;
// self.path.clear();
// }
// });
// }
// });
// }
// fn new_group(&mut self, ui: &mut egui::Ui) {
// ui.label("Group:");
// let selected = filter_dropbox(
// ui,
// "group_popup",
// &mut self.search_group,
// self.available_groups.iter(),
// |group, filter| group.name.contains(filter),
// |group| group.name.as_str(),
// );
// if let Some(selected) = selected {
// self.acl
// .entries
// .push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX));
// self.is_changed = true;
// }
// }
// fn new_user(&mut self, ui: &mut egui::Ui) {
// ui.label("User:");
// let selected = filter_dropbox(
// ui,
// "user_popup",
// &mut self.search_user,
// self.available_users.iter(),
// |user, filter| user.name.contains(filter),
// |user| user.name.as_str(),
// );
// if let Some(selected) = selected {
// self.acl
// .entries
// .push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX));
// self.is_changed = true;
// }
// }
// fn acl_table(&mut self, ui: &mut egui::Ui) {
// TableBuilder::new(ui)
// .auto_shrink([false, true])
// .striped(true)
// .column(Column::auto().resizable(false))
// .column(Column::remainder().resizable(false))
// .column(Column::auto().resizable(false))
// .column(Column::auto().resizable(false))
// .column(Column::auto().resizable(false))
// .column(Column::auto().resizable(false))
// .header(20.0, |mut header| {
// header.col(|ui| {
// ui.heading("Type");
// });
// header.col(|ui| {
// ui.heading("Name");
// });
// header.col(|ui| {
// ui.heading("R");
// });
// header.col(|ui| {
// ui.heading("W");
// });
// header.col(|ui| {
// ui.heading("X");
// });
// header.col(|ui| {
// ui.heading("D");
// });
// })
// .body(|mut body| {
// let mut to_delete: Vec<Qualifier> = Vec::new();
// self.acl.entries.sort();
// for entry in &mut self.acl.entries {
// body.row(30.0, |mut row| {
// row.col(|ui| {
// let label = match entry.0 {
// Qualifier::User(_) => "U",
// Qualifier::Group(_) => "G",
// Qualifier::Other => "",
// Qualifier::GroupObj => "G",
// Qualifier::UserObj => "U",
// Qualifier::Mask => "",
// };
// ui.label(label);
// });
// row.col(|ui| {
// let label = match entry.0 {
// Qualifier::User(uid) => self
// .available_users
// .iter()
// .find(|user| user.uid == uid)
// .map(|user| user.name.clone())
// .unwrap_or_else(|| {
// self.available_groups
// .iter()
// .find(|group| group.gid == uid)
// .map(|group| format!("({})", group.name))
// .unwrap_or_else(|| format!("Unknown user {}", uid))
// }),
// Qualifier::Group(gid) => self
// .available_groups
// .iter()
// .find(|group| group.gid == gid)
// .map(|group| group.name.clone())
// .unwrap_or_else(|| {
// self.available_users
// .iter()
// .find(|user| user.uid == gid)
// .map(|user| format!("({})", user.name))
// .unwrap_or_else(|| format!("Unknown group {}", gid))
// }),
// Qualifier::Other => "Other".to_string(),
// Qualifier::GroupObj => "GroupObj".to_string(),
// Qualifier::UserObj => "UserObj".to_string(),
// Qualifier::Mask => "Mask".to_string(),
// };
// ui.label(label);
// });
// row.col(|ui| {
// let mut checked = entry.1.contains(PermSet::ACL_READ);
// if ui.checkbox(&mut checked, "").changed() {
// if checked {
// entry.1.insert(PermSet::ACL_READ);
// self.is_changed = true;
// } else {
// entry.1.remove(PermSet::ACL_READ);
// self.is_changed = true;
// }
// }
// });
// row.col(|ui| {
// let mut checked = entry.1.contains(PermSet::ACL_WRITE);
// if ui.checkbox(&mut checked, "").changed() {
// if checked {
// entry.1.insert(PermSet::ACL_WRITE);
// self.is_changed = true;
// } else {
// entry.1.remove(PermSet::ACL_WRITE);
// self.is_changed = true;
// }
// }
// });
// row.col(|ui| {
// let mut checked = entry.1.contains(PermSet::ACL_EXECUTE);
// if ui.checkbox(&mut checked, "").changed() {
// if checked {
// entry.1.insert(PermSet::ACL_EXECUTE);
// self.is_changed = true;
// } else {
// entry.1.remove(PermSet::ACL_EXECUTE);
// self.is_changed = true;
// }
// }
// });
// row.col(|ui| match entry.0 {
// Qualifier::User(_) | Qualifier::Group(_) => {
// if ui.button("🗑").clicked() {
// to_delete.push(entry.0.clone());
// }
// }
// _ => {}
// });
// });
// }
// for entry in to_delete {
// self.acl.entries.retain(|e| e.0 != entry);
// self.is_changed = true;
// }
// });
// }
} }
// fn searchable_dropdown() // fn searchable_dropdown()
fn filter_dropbox< // fn filter_dropbox<
'a, // 'a,
T: PartialEq + 'a, // T: PartialEq + 'a,
I: Iterator<Item = &'a T> + 'a, // I: Iterator<Item = &'a T> + 'a,
F: Fn(&'a T, &str) -> bool, // F: Fn(&'a T, &str) -> bool,
L: Fn(&'a T) -> &str, // L: Fn(&'a T) -> &str,
>( // >(
ui: &mut egui::Ui, // ui: &mut egui::Ui,
popup_id: &str, // popup_id: &str,
search_buf: &mut String, // search_buf: &mut String,
items: I, // items: I,
filter_items: F, // filter_items: F,
get_label: L, // get_label: L,
) -> Option<&'a T> { // ) -> Option<&'a T> {
let search_field = ui.text_edit_singleline(search_buf); // let search_field = ui.text_edit_singleline(search_buf);
let popup_id = ui.make_persistent_id(popup_id); // let popup_id = ui.make_persistent_id(popup_id);
if search_field.gained_focus() { // if search_field.gained_focus() {
ui.memory_mut(|mem| mem.open_popup(popup_id)); // ui.memory_mut(|mem| mem.open_popup(popup_id));
} // }
let mut selected = None; // let mut selected = None;
egui::popup_above_or_below_widget( // egui::popup_above_or_below_widget(
&ui, // &ui,
popup_id, // popup_id,
&search_field, // &search_field,
egui::AboveOrBelow::Below, // egui::AboveOrBelow::Below,
egui::PopupCloseBehavior::CloseOnClick, // egui::PopupCloseBehavior::CloseOnClick,
|ui| { // |ui| {
ui.set_min_width(200.0); // ui.set_min_width(200.0);
ui.set_max_height(300.0); // ui.set_max_height(300.0);
egui::ScrollArea::vertical().show(ui, |ui| { // egui::ScrollArea::vertical().show(ui, |ui| {
let filtered = items.filter(|elm| filter_items(elm, &search_buf)); // let filtered = items.filter(|elm| filter_items(elm, &search_buf));
for user in filtered { // for user in filtered {
let value = ui.selectable_value(&mut selected, Some(user), get_label(user)); // let value = ui.selectable_value(&mut selected, Some(user), get_label(user));
if value.clicked() { // if value.clicked() {
search_field.surrender_focus(); // search_field.surrender_focus();
} // }
} // }
}); // });
}, // },
); // );
if selected.is_some() { // if selected.is_some() {
search_buf.clear(); // search_buf.clear();
} // }
selected // selected
} // }

View File

@ -1,68 +1,217 @@
use std::{io, rc::Rc, sync::RwLock};
use anyhow::Result;
use editor::ACLEditor; use editor::ACLEditor;
use eframe::egui::{self}; use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Direction, Layout, Rect},
widgets::Widget,
DefaultTerminal, Frame,
};
use tree::Tree; use tree::Tree;
mod editor; mod editor;
mod tree; mod tree;
pub struct App { pub struct App {
exit: bool,
tree: Tree, tree: Tree,
editor: ACLEditor, editor: Option<ACLEditor>,
} }
impl App { impl Default for App {
pub fn new(ctx: &egui::Context) -> Self { fn default() -> Self {
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"FiraCode".to_owned(),
egui::FontData::from_static(include_bytes!("../../fonts/FiraCodeNerdFont-Regular.ttf")),
);
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "FiraCode".to_owned());
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "FiraCode".to_owned());
ctx.set_fonts(fonts);
// first argument is the initial path or the current working directory
let initial_path = if let Some(path) = std::env::args().nth(1) { let initial_path = if let Some(path) = std::env::args().nth(1) {
std::path::PathBuf::from(path) std::path::PathBuf::from(path)
} else { } else {
std::env::current_dir().unwrap() std::env::current_dir().unwrap()
}; };
App { Self {
exit: false,
tree: Tree::new(initial_path), tree: Tree::new(initial_path),
editor: ACLEditor::new(), editor: None,
} }
} }
} }
impl eframe::App for App { impl App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
egui::SidePanel::right("side_panel") let mut app_event = AppEventHost::default();
.min_width(300.0) while !self.exit {
.max_width(ctx.available_rect().width() - 100.0) terminal.draw(|frame| self.render(frame, app_event))?;
.resizable(true) app_event = self.handle_events()?;
.show(ctx, |ui| { }
self.editor.show(ui, &self.tree.selected); Ok(())
}); }
egui::CentralPanel::default().show(ctx, |ui| { pub fn handle_events(&mut self) -> io::Result<AppEventHost> {
egui::ScrollArea::both() // Handle events here
.auto_shrink([false; 2]) // For example, you can check for key presses and update the state accordingly
.show(ui, |ui| {
self.tree.show(ui); let evt = event::read()?;
}); if let Event::Key(key) = evt {
match key.code {
KeyCode::Char('q') => {
self.exit = true;
}
_ => {}
}
}
Ok(AppEventHost::new(Some(evt)))
}
pub fn render(&mut self, frame: &mut Frame, event: AppEventHost) {
if let Some(evt) = &event.event {
match evt {
Event::Key(key) => match key.code {
KeyCode::Char('q') => {
self.exit = true;
}
KeyCode::Enter | KeyCode::Char('e') => {
self.editor = Some(ACLEditor::new(self.tree.get_selected()));
}
KeyCode::Esc | KeyCode::Char('c') => {
self.editor = None;
}
_ => {}
},
_ => {}
}
}
event.get_client(true).register_helper("q", "Quit");
let layout1 = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(1), Constraint::Length(1)])
.split(frame.area());
let content_area = layout1[0];
let help_area = layout1[1];
let layout2 = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Fill(1)])
.split(content_area);
let content_left_area = layout2[0];
let content_right_area = layout2[1];
self.tree.render(
frame,
content_left_area,
event.get_client(self.editor.is_none()),
);
if let Some(editor) = self.editor.as_mut() {
event
.get_client(true)
.register_helper("Esc", "Close editor");
editor.render(frame, content_right_area, event.get_client(true));
} else {
event.get_client(true).register_helper("Enter", "Select");
frame.render_widget("Select folder from the left", content_right_area);
}
event.render_help(frame, help_area);
}
}
pub struct AppEventHost {
pub event: Option<Event>,
pub registered_helper: Rc<RwLock<Vec<AppEventHelper>>>,
}
impl Default for AppEventHost {
fn default() -> Self {
Self {
event: None,
registered_helper: Rc::from(RwLock::new(vec![])),
}
}
}
impl AppEventHost {
pub fn new(event: Option<Event>) -> Self {
Self {
event: event,
..Default::default()
}
}
fn render_help(&self, frame: &mut Frame, area: Rect) {
let mut help = String::new();
let infos = self.registered_helper.read().unwrap();
for info in infos.iter() {
help.push_str(&format!("{}: {} | \n", info.key, info.help));
}
frame.render_widget(help, area);
}
fn get_client(&self, focus: bool) -> AppEventClient {
AppEventClient {
host: self.registered_helper.clone(),
event: self.event.clone(),
has_focus: focus,
}
}
}
pub struct AppEventClient {
host: Rc<RwLock<Vec<AppEventHelper>>>,
event: Option<Event>,
pub has_focus: bool,
}
impl AppEventClient {
pub fn has_focus(&self) -> bool {
self.has_focus
}
pub fn get_client(&self, focus: bool) -> Self {
Self {
host: self.host.clone(),
event: self.event.clone(),
has_focus: focus,
}
}
pub fn register_helper(&mut self, key: &str, help: &str) {
self.host.write().unwrap().push(AppEventHelper {
key: key.to_string(),
help: help.to_string(),
}); });
} }
} }
pub struct AppEventHelper {
pub key: String,
pub help: String,
}
// impl eframe::App for App {
// fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// egui::SidePanel::right("side_panel")
// .min_width(300.0)
// .max_width(ctx.available_rect().width() - 100.0)
// .resizable(true)
// .show(ctx, |ui| {
// self.editor.show(ui, &self.tree.selected);
// });
// egui::CentralPanel::default().show(ctx, |ui| {
// egui::ScrollArea::both()
// .auto_shrink([false; 2])
// .show(ui, |ui| {
// self.tree.show(ui);
// });
// });
// }
// }
pub trait Component {
fn render(&mut self, f: &mut Frame, rect: Rect, evt: AppEventClient);
}

View File

@ -3,13 +3,98 @@
// Folders can by selected // Folders can by selected
// Folders will fetch their children when expanded // Folders will fetch their children when expanded
use std::path::PathBuf; use std::{default, fmt::format, path::PathBuf};
use eframe::egui; use ratatui::{
crossterm::event::{Event, KeyCode, KeyEvent},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Widget},
Frame,
};
use super::Component;
// use eframe::egui;
pub struct Tree { pub struct Tree {
root: Folder, root: Folder,
pub selected: PathBuf, // pub selected: PathBuf,
vertical_scroll: usize,
vertical_scroll_state: ScrollbarState,
selected_idx: usize,
}
impl Component for Tree {
fn render(
&mut self,
f: &mut ratatui::Frame,
rect: ratatui::prelude::Rect,
mut evt: super::AppEventClient,
) {
if evt.has_focus() {
if let Some(evt) = &evt.event {
if let Event::Key(key) = evt {
match key.code {
KeyCode::Up => {
if self.selected_idx > 0 {
self.selected_idx -= 1;
}
}
KeyCode::Down => {
let entries = self.root.get_entries();
if self.selected_idx < entries.len() - 1 {
self.selected_idx += 1;
}
}
_ => {}
}
}
}
evt.register_helper("->", "Expand");
evt.register_helper("<-", "Collapse");
evt.register_helper("", "Up");
evt.register_helper("", "Down");
}
let block = Block::bordered()
.title("Select Folder")
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::White));
let inner_area = block.inner(rect);
f.render_widget(block, rect);
let mut tmp = self.selected_idx;
self.root.apply_event(&evt, &mut tmp);
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 { impl Tree {
@ -17,25 +102,31 @@ impl Tree {
let path: PathBuf = initial_path.into(); let path: PathBuf = initial_path.into();
let name = path.to_string_lossy().to_string(); let name = path.to_string_lossy().to_string();
let mut root = Folder::new(name, path, 0);
root.selected = true;
Self { Self {
root: Folder::new(name, path), root,
selected: PathBuf::new(), // selected: PathBuf::new(),
vertical_scroll: 0,
vertical_scroll_state: ScrollbarState::default(),
selected_idx: 0,
} }
} }
pub fn set_root_path(&mut self, path: impl Into<PathBuf>) { pub fn get_selected(&self) -> PathBuf {
let path: PathBuf = path.into(); let entries = self.root.get_entries();
if self.selected_idx < entries.len() {
let name = path.to_string_lossy().to_string(); return entries[self.selected_idx].path.clone();
}
self.root = Folder::new(name, path); PathBuf::new()
} }
pub fn show(&mut self, ui: &mut egui::Ui) { // pub fn show(&mut self, ui: &mut egui::Ui) {
ui.vertical(|ui| { // ui.vertical(|ui| {
self.root.show(ui, &mut self.selected); // self.root.show(ui, &mut self.selected);
}); // });
} // }
} }
pub struct Folder { pub struct Folder {
@ -43,83 +134,186 @@ pub struct Folder {
path: PathBuf, path: PathBuf,
expanded: bool, expanded: bool,
children: Option<Vec<Folder>>, children: Option<Vec<Folder>>,
// selected: Arc<RwLock<PathBuf>>, selected: bool,
depth: usize,
} }
impl Folder { impl Folder {
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self { pub fn new(name: impl Into<String>, path: impl Into<PathBuf>, depth: usize) -> Self {
Self { Self {
name: name.into(), name: name.into(),
path: path.into(), path: path.into(),
expanded: false, expanded: false,
selected: false,
children: Default::default(), children: Default::default(),
depth,
} }
} }
pub fn show(&mut self, ui: &mut egui::Ui, selected: &mut PathBuf) { pub fn expand(&mut self) {
let Self { if self.children.is_none() {
name,
path,
expanded,
children,
} = self;
if children.is_none() {
let mut children_data = vec![]; let mut children_data = vec![];
println!("Reading dir: {:?}", path); // println!("Reading dir: {:?}", self.path);
std::fs::read_dir(&path) std::fs::read_dir(&self.path)
.map(|entries| { .map(|entries| {
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
println!( // println!(
"Found dir: {:?}, file {}, link {}", // "Found dir: {:?}, file {}, link {}",
path, // path,
path.is_file(), // path.is_file(),
path.is_symlink() // path.is_symlink()
); // );
let path2 = path.clone(); let path2 = path.clone();
let name = path2.file_name().unwrap().to_string_lossy(); let name = path2.file_name().unwrap().to_string_lossy();
children_data.push(Folder::new(name, path)); children_data.push(Folder::new(name, path, self.depth + 1));
} }
} }
}) })
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
eprintln!("Failed to read dir: {}", err); eprintln!("Failed to read dir: {}", err);
}); });
children.replace(children_data); children_data.sort_by_key(|folder| folder.name.clone());
self.children.replace(children_data);
} }
}
ui.horizontal(|ui| { pub fn apply_event(&mut self, evt: &super::AppEventClient, selected_idx: &mut usize) {
if let Some(children) = children { if *selected_idx == 0 {
if !children.is_empty() { self.selected = true;
let label = if *expanded { "" } else { "" }; *selected_idx = usize::MAX;
let response = ui.selectable_label(false, label); } else {
self.selected = false;
if response.clicked() && !children.is_empty() { }
*expanded = !*expanded; *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;
}
_ => {}
} }
} }
} else {
ui.label("");
} }
}
if ui if self.expanded {
.selectable_label(*selected == *path, name.to_owned()) if let Some(children) = &mut self.children {
.clicked() for child in children {
{ child.apply_event(evt, selected_idx);
*selected = path.clone(); // if child.selected {
} // self.selected = true;
}); // }
}
if let Some(children) = children {
if *expanded {
ui.indent("", |ui| {
for child in children {
child.show(ui, selected);
}
});
} }
} }
} }
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);
// }
// });
// }
// }
// }
} }