Compare commits
No commits in common. "009dd4657ff17cf438140822e46a2827f4eb2ca7" and "9e763a91eff35cd8d3320856ebc0e443af6dbff6" have entirely different histories.
009dd4657f
...
9e763a91ef
2644
Cargo.lock
generated
2644
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@ -4,13 +4,18 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["wayland", "x11"]
|
||||||
|
wayland = ["eframe/wayland"]
|
||||||
|
x11 = ["eframe/x11"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
color-eyre = "0.6.3"
|
eframe = { version = "0.28.1", features = [
|
||||||
|
"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.6" }
|
posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.5" }
|
||||||
ratatui = { version = "0.29.0", features = ["all-widgets"] }
|
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
25
src/main.rs
25
src/main.rs
@ -1,21 +1,22 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ratatui::{
|
use eframe::egui;
|
||||||
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");
|
||||||
|
|
||||||
color_eyre::install().expect("failed to install color_eyre");
|
Ok(())
|
||||||
|
|
||||||
let mut terminal = ratatui::init();
|
|
||||||
let result = ui::App::default().run(&mut terminal);
|
|
||||||
ratatui::restore();
|
|
||||||
result
|
|
||||||
.map_err(|e| anyhow::anyhow!("Error: {}", e))
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
}
|
||||||
|
645
src/ui/editor.rs
645
src/ui/editor.rs
@ -1,14 +1,13 @@
|
|||||||
use std::{borrow::Borrow, io, path::PathBuf, thread::JoinHandle};
|
use std::{borrow::Borrow, path::PathBuf, thread::JoinHandle};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ratatui::{widgets::Widget, DefaultTerminal, Frame};
|
use eframe::egui;
|
||||||
|
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,
|
||||||
@ -23,8 +22,8 @@ pub struct ACLEditor {
|
|||||||
pub save_thread_error: Option<String>,
|
pub save_thread_error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ACLEditor {
|
impl ACLEditor {
|
||||||
fn default() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: PathBuf::new(),
|
path: PathBuf::new(),
|
||||||
is_changed: false,
|
is_changed: false,
|
||||||
@ -37,368 +36,322 @@ impl Default for 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;
|
||||||
impl Component for ACLEditor {
|
ui.vertical(|ui| {
|
||||||
fn render(&mut self, f: &mut Frame, rect: ratatui::prelude::Rect, evt: super::AppEventClient) {
|
if self.save_thread.is_some() {
|
||||||
let block =
|
if self.save_thread.as_ref().unwrap().is_finished() {
|
||||||
|
let save_thread = std::mem::replace(&mut self.save_thread, None);
|
||||||
// todo!("Implement the rendering logic for ACLEditor");
|
let result = save_thread.unwrap().join();
|
||||||
if let Some(save_thread) = &self.save_thread {
|
match result {
|
||||||
if save_thread.is_finished() {
|
Ok(Ok(_)) => {
|
||||||
let result = save_thread.join();
|
ui.label("Saved successfully!");
|
||||||
match result {
|
self.save_thread_error = None;
|
||||||
Ok(Ok(_)) => {
|
self.is_changed = false;
|
||||||
f.set_title("Saved successfully!");
|
}
|
||||||
self.save_thread_error = None;
|
Ok(Err(e)) => {
|
||||||
self.is_changed = false;
|
self.save_thread_error = Some(format!("Failed to save {:?}", e));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.save_thread_error = Some(format!("Failed to join {:?}", e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
} else {
|
||||||
self.save_thread_error = Some(format!("Failed to save {:?}", e));
|
egui::Frame::default()
|
||||||
}
|
.fill(egui::Color32::GREEN)
|
||||||
Err(e) => {
|
.inner_margin(4.0)
|
||||||
self.save_thread_error = Some(format!("Failed to join {:?}", e));
|
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
f.set_title("Saving...");
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if let Some(err) = &self.save_thread_error {
|
});
|
||||||
f.set_title(err);
|
|
||||||
} else {
|
|
||||||
f.set_title(format!("Editing: {:?}", self.path));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl ACLEditor {
|
fn new_group(&mut self, ui: &mut egui::Ui) {
|
||||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
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(),
|
||||||
|
);
|
||||||
|
|
||||||
let path: PathBuf = path.into();
|
if let Some(selected) = selected {
|
||||||
|
self.acl
|
||||||
Self {
|
.entries
|
||||||
path,
|
.push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX));
|
||||||
..Self::default()
|
self.is_changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn show(&mut self, ui: &mut egui::Ui, selected_path: &PathBuf) {
|
fn new_user(&mut self, ui: &mut egui::Ui) {
|
||||||
// let mut show_save_or_dicard_message = false;
|
ui.label("User:");
|
||||||
// ui.vertical(|ui| {
|
let selected = filter_dropbox(
|
||||||
// if self.save_thread.is_some() {
|
ui,
|
||||||
// if self.save_thread.as_ref().unwrap().is_finished() {
|
"user_popup",
|
||||||
// let save_thread = std::mem::replace(&mut self.save_thread, None);
|
&mut self.search_user,
|
||||||
// let result = save_thread.unwrap().join();
|
self.available_users.iter(),
|
||||||
// match result {
|
|user, filter| user.name.contains(filter),
|
||||||
// Ok(Ok(_)) => {
|
|user| user.name.as_str(),
|
||||||
// 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() {
|
if let Some(selected) = selected {
|
||||||
// self.save_thread_error = None;
|
self.acl
|
||||||
// }
|
.entries
|
||||||
// });
|
.push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX));
|
||||||
// } else {
|
self.is_changed = true;
|
||||||
// ui.heading(format!("Editing: {:?}", self.path));
|
}
|
||||||
// ui.separator();
|
}
|
||||||
|
|
||||||
// if self.path != *selected_path {
|
fn acl_table(&mut self, ui: &mut egui::Ui) {
|
||||||
// if !self.is_changed {
|
TableBuilder::new(ui)
|
||||||
// // Load ACLs
|
.auto_shrink([false, true])
|
||||||
// self.path = selected_path.clone();
|
.striped(true)
|
||||||
// self.acl = PosixACL::new_from_file(&self.path, false).unwrap();
|
.column(Column::auto().resizable(false))
|
||||||
// } else {
|
.column(Column::remainder().resizable(false))
|
||||||
// show_save_or_dicard_message = true;
|
.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_table(ui);
|
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.separator();
|
ui.label(label);
|
||||||
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// self.new_user(ui);
|
row.col(|ui| match entry.0 {
|
||||||
// self.new_group(ui);
|
Qualifier::User(_) | Qualifier::Group(_) => {
|
||||||
|
if ui.button("🗑").clicked() {
|
||||||
|
to_delete.push(entry.0.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ui.separator();
|
for entry in to_delete {
|
||||||
|
self.acl.entries.retain(|e| e.0 != entry);
|
||||||
// if show_save_or_dicard_message {
|
self.is_changed = true;
|
||||||
// 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
|
||||||
// }
|
}
|
||||||
|
235
src/ui/mod.rs
235
src/ui/mod.rs
@ -1,217 +1,68 @@
|
|||||||
use std::{io, rc::Rc, sync::RwLock};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use editor::ACLEditor;
|
use editor::ACLEditor;
|
||||||
use ratatui::{
|
use eframe::egui::{self};
|
||||||
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: Option<ACLEditor>,
|
editor: ACLEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for App {
|
impl App {
|
||||||
fn default() -> Self {
|
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);
|
||||||
|
|
||||||
|
// 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()
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
App {
|
||||||
exit: false,
|
|
||||||
tree: Tree::new(initial_path),
|
tree: Tree::new(initial_path),
|
||||||
editor: None,
|
editor: ACLEditor::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl eframe::App for App {
|
||||||
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
let mut app_event = AppEventHost::default();
|
egui::SidePanel::right("side_panel")
|
||||||
while !self.exit {
|
.min_width(300.0)
|
||||||
terminal.draw(|frame| self.render(frame, app_event))?;
|
.max_width(ctx.available_rect().width() - 100.0)
|
||||||
app_event = self.handle_events()?;
|
.resizable(true)
|
||||||
}
|
.show(ctx, |ui| {
|
||||||
Ok(())
|
self.editor.show(ui, &self.tree.selected);
|
||||||
}
|
});
|
||||||
|
|
||||||
pub fn handle_events(&mut self) -> io::Result<AppEventHost> {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
// Handle events here
|
egui::ScrollArea::both()
|
||||||
// For example, you can check for key presses and update the state accordingly
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
let evt = event::read()?;
|
self.tree.show(ui);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
324
src/ui/tree.rs
324
src/ui/tree.rs
@ -3,98 +3,13 @@
|
|||||||
// Folders can by selected
|
// Folders can by selected
|
||||||
// Folders will fetch their children when expanded
|
// Folders will fetch their children when expanded
|
||||||
|
|
||||||
use std::{default, fmt::format, path::PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use ratatui::{
|
use eframe::egui;
|
||||||
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 {
|
||||||
@ -102,31 +17,25 @@ 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,
|
root: Folder::new(name, path),
|
||||||
// selected: PathBuf::new(),
|
selected: PathBuf::new(),
|
||||||
vertical_scroll: 0,
|
|
||||||
vertical_scroll_state: ScrollbarState::default(),
|
|
||||||
selected_idx: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_selected(&self) -> PathBuf {
|
pub fn set_root_path(&mut self, path: impl Into<PathBuf>) {
|
||||||
let entries = self.root.get_entries();
|
let path: PathBuf = path.into();
|
||||||
if self.selected_idx < entries.len() {
|
|
||||||
return entries[self.selected_idx].path.clone();
|
let name = path.to_string_lossy().to_string();
|
||||||
}
|
|
||||||
PathBuf::new()
|
self.root = Folder::new(name, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -134,186 +43,83 @@ pub struct Folder {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
expanded: bool,
|
expanded: bool,
|
||||||
children: Option<Vec<Folder>>,
|
children: Option<Vec<Folder>>,
|
||||||
selected: bool,
|
// selected: Arc<RwLock<PathBuf>>,
|
||||||
depth: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Folder {
|
impl Folder {
|
||||||
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>, depth: usize) -> Self {
|
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> 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 expand(&mut self) {
|
pub fn show(&mut self, ui: &mut egui::Ui, selected: &mut PathBuf) {
|
||||||
if self.children.is_none() {
|
let Self {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
expanded,
|
||||||
|
children,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
if children.is_none() {
|
||||||
let mut children_data = vec![];
|
let mut children_data = vec![];
|
||||||
// println!("Reading dir: {:?}", self.path);
|
println!("Reading dir: {:?}", path);
|
||||||
std::fs::read_dir(&self.path)
|
std::fs::read_dir(&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, self.depth + 1));
|
children_data.push(Folder::new(name, path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
eprintln!("Failed to read dir: {}", err);
|
eprintln!("Failed to read dir: {}", err);
|
||||||
});
|
});
|
||||||
children_data.sort_by_key(|folder| folder.name.clone());
|
children.replace(children_data);
|
||||||
self.children.replace(children_data);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_event(&mut self, evt: &super::AppEventClient, selected_idx: &mut usize) {
|
ui.horizontal(|ui| {
|
||||||
if *selected_idx == 0 {
|
if let Some(children) = children {
|
||||||
self.selected = true;
|
if !children.is_empty() {
|
||||||
*selected_idx = usize::MAX;
|
let label = if *expanded { "▼" } else { "▶" };
|
||||||
} else {
|
let response = ui.selectable_label(false, label);
|
||||||
self.selected = false;
|
|
||||||
}
|
if response.clicked() && !children.is_empty() {
|
||||||
*selected_idx -= 1;
|
*expanded = !*expanded;
|
||||||
if evt.has_focus() && self.selected {
|
|
||||||
if let Some(evt) = &evt.event {
|
|
||||||
if let Event::Key(key) = evt {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Right | KeyCode::Enter => {
|
|
||||||
self.expand();
|
|
||||||
self.expanded = true;
|
|
||||||
}
|
|
||||||
KeyCode::Left => {
|
|
||||||
self.expanded = false;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.expanded {
|
|
||||||
if let Some(children) = &mut self.children {
|
|
||||||
for child in children {
|
|
||||||
child.apply_event(evt, selected_idx);
|
|
||||||
// if child.selected {
|
|
||||||
// self.selected = true;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_entries(&self) -> Vec<&Folder> {
|
|
||||||
let mut entries: Vec<&Folder> = vec![];
|
|
||||||
if self.expanded {
|
|
||||||
if let Some(children) = &self.children {
|
|
||||||
for child in children {
|
|
||||||
entries.extend(child.get_entries());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res: Vec<&Folder> = vec![self];
|
|
||||||
res.extend(entries);
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_line(&self) -> Line {
|
|
||||||
let pad = " ".repeat(self.depth);
|
|
||||||
Line::from(vec![Span::styled(
|
|
||||||
format!(
|
|
||||||
"{}{} {}",
|
|
||||||
pad,
|
|
||||||
if self.expanded { "▼" } else { "▶" },
|
|
||||||
self.name
|
|
||||||
),
|
|
||||||
if self.selected {
|
|
||||||
Style::new().bg(Color::White).fg(Color::Black)
|
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user