First prototype
This commit is contained in:
commit
69f1f7d9cb
2
.editorconfig
Normal file
2
.editorconfig
Normal file
@ -0,0 +1,2 @@
|
||||
[*.rs]
|
||||
indent_size = 4
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
45
.vscode/launch.json
vendored
Normal file
45
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
|
||||
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
|
||||
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'ACL_Editor'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=ACL_Editor",
|
||||
"--package=ACL_Editor"
|
||||
],
|
||||
"filter": {
|
||||
"name": "ACL_Editor",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'ACL_Editor'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=ACL_Editor",
|
||||
"--package=ACL_Editor"
|
||||
],
|
||||
"filter": {
|
||||
"name": "ACL_Editor",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
2810
Cargo.lock
generated
Normal file
2810
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "ACL_Editor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
eframe = { version = "0.28.1", features = [
|
||||
"wayland",
|
||||
"wgpu",
|
||||
"default_fonts",
|
||||
], default-features = false }
|
||||
egui_extras = "0.28.1"
|
||||
env_logger = { version = "0.11", features = ["auto-color", "humantime"] }
|
||||
posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.5" }
|
||||
walkdir = "2"
|
BIN
fonts/FiraCodeNerdFont-Regular.ttf
Normal file
BIN
fonts/FiraCodeNerdFont-Regular.ttf
Normal file
Binary file not shown.
3
fonts/FiraCodeNerdFont-Regular.ttf:Zone.Identifier
Normal file
3
fonts/FiraCodeNerdFont-Regular.ttf:Zone.Identifier
Normal file
@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=C:\Users\micro\Downloads\FiraCode.zip
|
24
src/helper/acl_writer.rs
Normal file
24
src/helper/acl_writer.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use posix_acl::PosixACL;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub fn write_acl_recursive<P: AsRef<Path>>(path: P, acl: PosixACL) -> Result<()> {
|
||||
for entry in WalkDir::new(path).min_depth(0).follow_links(false) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
println!("Writing ACL for: {:?} {:?}", path, acl);
|
||||
match entry.file_type().is_dir() {
|
||||
true => {
|
||||
acl.write(&path)?;
|
||||
acl.write_default(&path)?;
|
||||
}
|
||||
false => {
|
||||
acl.write(&path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
49
src/helper/getent.rs
Normal file
49
src/helper/getent.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub uid: u32,
|
||||
}
|
||||
|
||||
pub fn get_users() -> Result<Vec<User>> {
|
||||
let raw = Command::new("getent").arg("passwd").output()?;
|
||||
let raw = String::from_utf8_lossy(&raw.stdout);
|
||||
let users: Vec<User> = raw
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
User {
|
||||
name: parts[0].to_string(),
|
||||
uid: parts[2].parse().unwrap(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Group {
|
||||
pub name: String,
|
||||
pub gid: u32,
|
||||
}
|
||||
|
||||
pub fn get_groups() -> Result<Vec<Group>> {
|
||||
let raw = Command::new("getent").arg("group").output()?;
|
||||
let raw = String::from_utf8_lossy(&raw.stdout);
|
||||
let groups: Vec<Group> = raw
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
Group {
|
||||
name: parts[0].to_string(),
|
||||
gid: parts[2].parse().unwrap(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(groups)
|
||||
}
|
2
src/helper/mod.rs
Normal file
2
src/helper/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod acl_writer;
|
||||
pub mod getent;
|
22
src/main.rs
Normal file
22
src/main.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use anyhow::Result;
|
||||
use eframe::egui;
|
||||
|
||||
mod helper;
|
||||
mod ui;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
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(())
|
||||
}
|
345
src/ui/editor.rs
Normal file
345
src/ui/editor.rs
Normal file
@ -0,0 +1,345 @@
|
||||
use std::{borrow::Borrow, path::PathBuf, thread::JoinHandle};
|
||||
|
||||
use anyhow::Result;
|
||||
use eframe::egui;
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
|
||||
use crate::helper::getent::{get_groups, get_users, Group, User};
|
||||
|
||||
use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX};
|
||||
|
||||
pub struct ACLEditor {
|
||||
pub path: PathBuf,
|
||||
pub is_changed: bool,
|
||||
pub available_users: Vec<User>,
|
||||
pub available_groups: Vec<Group>,
|
||||
pub acl: PosixACL,
|
||||
|
||||
pub search_user: String,
|
||||
pub search_group: String,
|
||||
|
||||
pub save_thread: Option<JoinHandle<Result<()>>>,
|
||||
pub save_thread_error: Option<String>,
|
||||
}
|
||||
|
||||
impl ACLEditor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
path: PathBuf::new(),
|
||||
is_changed: false,
|
||||
available_groups: get_groups().unwrap(),
|
||||
available_users: get_users().unwrap(),
|
||||
acl: PosixACL::new(ACL_RWX, ACL_RWX, ACL_RWX),
|
||||
search_user: String::new(),
|
||||
search_group: String::new(),
|
||||
save_thread: 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() {
|
||||
self.save_thread_error = None;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.heading(format!("Editing: {:?}", self.path));
|
||||
ui.separator();
|
||||
|
||||
if self.path != *selected_path {
|
||||
if !self.is_changed {
|
||||
// Load ACLs
|
||||
self.path = selected_path.clone();
|
||||
self.acl = PosixACL::new_from_file(&self.path, false).unwrap();
|
||||
} else {
|
||||
show_save_or_dicard_message = true;
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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(|| format!("Unknown user {}", uid)),
|
||||
Qualifier::Group(gid) => self
|
||||
.available_groups
|
||||
.iter()
|
||||
.find(|group| group.gid == gid)
|
||||
.map(|group| group.name.clone())
|
||||
.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 filter_dropbox<
|
||||
'a,
|
||||
T: PartialEq + 'a,
|
||||
I: Iterator<Item = &'a T> + 'a,
|
||||
F: Fn(&'a T, &str) -> bool,
|
||||
L: Fn(&'a T) -> &str,
|
||||
>(
|
||||
ui: &mut egui::Ui,
|
||||
popup_id: &str,
|
||||
search_buf: &mut String,
|
||||
items: I,
|
||||
filter_items: F,
|
||||
get_label: L,
|
||||
) -> Option<&'a T> {
|
||||
let search_field = ui.text_edit_singleline(search_buf);
|
||||
let popup_id = ui.make_persistent_id(popup_id);
|
||||
|
||||
if search_field.gained_focus() {
|
||||
ui.memory_mut(|mem| mem.open_popup(popup_id));
|
||||
}
|
||||
|
||||
let mut selected = None;
|
||||
egui::popup_above_or_below_widget(
|
||||
&ui,
|
||||
popup_id,
|
||||
&search_field,
|
||||
egui::AboveOrBelow::Below,
|
||||
egui::PopupCloseBehavior::CloseOnClick,
|
||||
|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
ui.set_max_height(300.0);
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
let filtered = items.filter(|elm| filter_items(elm, &search_buf));
|
||||
|
||||
for user in filtered {
|
||||
let value = ui.selectable_value(&mut selected, Some(user), get_label(user));
|
||||
if value.clicked() {
|
||||
search_field.surrender_focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if selected.is_some() {
|
||||
search_buf.clear();
|
||||
}
|
||||
|
||||
selected
|
||||
}
|
60
src/ui/mod.rs
Normal file
60
src/ui/mod.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use editor::ACLEditor;
|
||||
use eframe::egui::{self};
|
||||
use tree::Tree;
|
||||
|
||||
mod editor;
|
||||
mod tree;
|
||||
|
||||
pub struct App {
|
||||
tree: Tree,
|
||||
editor: ACLEditor,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(ctx: &egui::Context) -> 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);
|
||||
|
||||
App {
|
||||
tree: Tree::new(),
|
||||
editor: ACLEditor::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
122
src/ui/tree.rs
Normal file
122
src/ui/tree.rs
Normal file
@ -0,0 +1,122 @@
|
||||
// Build a tree of folders (not files)
|
||||
// Folders can be expanded and collapsed
|
||||
// Folders can by selected
|
||||
// Folders will fetch their children when expanded
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
pub struct Tree {
|
||||
root: Folder,
|
||||
pub selected: PathBuf,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
root: Folder::new("./tmp", "./tmp"),
|
||||
selected: PathBuf::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_root_path(&mut self, path: impl Into<PathBuf>) {
|
||||
let path: PathBuf = path.into();
|
||||
|
||||
let name = path.to_string_lossy().to_string();
|
||||
|
||||
self.root = Folder::new(name, path);
|
||||
}
|
||||
|
||||
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: Arc<RwLock<PathBuf>>,
|
||||
}
|
||||
|
||||
impl Folder {
|
||||
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
path: path.into(),
|
||||
expanded: false,
|
||||
children: Default::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.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user