First prototype
This commit is contained in:
		
							
								
								
									
										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.
										
									
								
							
							
								
								
									
										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);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user