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