Compare commits
2 Commits
9e763a91ef
...
009dd4657f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
009dd4657f | ||
![]() |
a9bf713dd5 |
2628
Cargo.lock
generated
2628
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@ -4,18 +4,13 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["wayland", "x11"]
|
default = []
|
||||||
wayland = ["eframe/wayland"]
|
|
||||||
x11 = ["eframe/x11"]
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
eframe = { version = "0.28.1", features = [
|
color-eyre = "0.6.3"
|
||||||
"wgpu",
|
|
||||||
"default_fonts",
|
|
||||||
], default-features = false }
|
|
||||||
egui_extras = "0.28.1"
|
|
||||||
env_logger = { version = "0.11", features = ["auto-color", "humantime"] }
|
env_logger = { version = "0.11", features = ["auto-color", "humantime"] }
|
||||||
posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.5" }
|
posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.6" }
|
||||||
|
ratatui = { version = "0.29.0", features = ["all-widgets"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
0
Dockerfile
Normal file
0
Dockerfile
Normal file
25
src/main.rs
25
src/main.rs
@ -1,22 +1,21 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use eframe::egui;
|
use ratatui::{
|
||||||
|
crossterm::event::{self, Event},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
|
||||||
mod helper;
|
mod helper;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||||
let options = eframe::NativeOptions {
|
|
||||||
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
|
|
||||||
hardware_acceleration: eframe::HardwareAcceleration::Preferred,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
eframe::run_native(
|
|
||||||
"ACL Editor",
|
|
||||||
options,
|
|
||||||
Box::new(|cc| Ok(Box::new(ui::App::new(&cc.egui_ctx)))),
|
|
||||||
)
|
|
||||||
.expect("Failed to run eframe app");
|
|
||||||
|
|
||||||
Ok(())
|
color_eyre::install().expect("failed to install color_eyre");
|
||||||
|
|
||||||
|
let mut terminal = ratatui::init();
|
||||||
|
let result = ui::App::default().run(&mut terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
result
|
||||||
|
.map_err(|e| anyhow::anyhow!("Error: {}", e))
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
641
src/ui/editor.rs
641
src/ui/editor.rs
@ -1,13 +1,14 @@
|
|||||||
use std::{borrow::Borrow, path::PathBuf, thread::JoinHandle};
|
use std::{borrow::Borrow, io, path::PathBuf, thread::JoinHandle};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use eframe::egui;
|
use ratatui::{widgets::Widget, DefaultTerminal, Frame};
|
||||||
use egui_extras::{Column, TableBuilder};
|
|
||||||
|
|
||||||
use crate::helper::getent::{get_groups, get_users, Group, User};
|
use crate::helper::getent::{get_groups, get_users, Group, User};
|
||||||
|
|
||||||
use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX};
|
use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX};
|
||||||
|
|
||||||
|
use super::Component;
|
||||||
|
|
||||||
pub struct ACLEditor {
|
pub struct ACLEditor {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub is_changed: bool,
|
pub is_changed: bool,
|
||||||
@ -22,8 +23,8 @@ pub struct ACLEditor {
|
|||||||
pub save_thread_error: Option<String>,
|
pub save_thread_error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ACLEditor {
|
impl Default for ACLEditor {
|
||||||
pub fn new() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: PathBuf::new(),
|
path: PathBuf::new(),
|
||||||
is_changed: false,
|
is_changed: false,
|
||||||
@ -36,322 +37,368 @@ impl ACLEditor {
|
|||||||
save_thread_error: None,
|
save_thread_error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn show(&mut self, ui: &mut egui::Ui, selected_path: &PathBuf) {
|
}
|
||||||
let mut show_save_or_dicard_message = false;
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
if self.save_thread.is_some() {
|
|
||||||
if self.save_thread.as_ref().unwrap().is_finished() {
|
|
||||||
let save_thread = std::mem::replace(&mut self.save_thread, None);
|
|
||||||
let result = save_thread.unwrap().join();
|
|
||||||
match result {
|
|
||||||
Ok(Ok(_)) => {
|
|
||||||
ui.label("Saved successfully!");
|
|
||||||
self.save_thread_error = None;
|
|
||||||
self.is_changed = false;
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
self.save_thread_error = Some(format!("Failed to save {:?}", e));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.save_thread_error = Some(format!("Failed to join {:?}", e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
egui::Frame::default()
|
|
||||||
.fill(egui::Color32::GREEN)
|
|
||||||
.inner_margin(4.0)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label("Apply changes...");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if let Some(err) = &self.save_thread_error.clone() {
|
|
||||||
egui::Frame::default()
|
|
||||||
.fill(egui::Color32::DARK_RED)
|
|
||||||
.inner_margin(4.0)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label(err);
|
|
||||||
|
|
||||||
if ui.button("OK").clicked() {
|
impl Component for ACLEditor {
|
||||||
self.save_thread_error = None;
|
fn render(&mut self, f: &mut Frame, rect: ratatui::prelude::Rect, evt: super::AppEventClient) {
|
||||||
}
|
let block =
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.heading(format!("Editing: {:?}", self.path));
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if self.path != *selected_path {
|
// todo!("Implement the rendering logic for ACLEditor");
|
||||||
if !self.is_changed {
|
if let Some(save_thread) = &self.save_thread {
|
||||||
// Load ACLs
|
if save_thread.is_finished() {
|
||||||
self.path = selected_path.clone();
|
let result = save_thread.join();
|
||||||
self.acl = PosixACL::new_from_file(&self.path, false).unwrap();
|
match result {
|
||||||
} else {
|
Ok(Ok(_)) => {
|
||||||
show_save_or_dicard_message = true;
|
f.set_title("Saved successfully!");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.acl_table(ui);
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
ui.label("Add new entry:");
|
|
||||||
|
|
||||||
self.new_user(ui);
|
|
||||||
self.new_group(ui);
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if show_save_or_dicard_message {
|
|
||||||
egui::Frame::default()
|
|
||||||
.fill(egui::Color32::YELLOW)
|
|
||||||
.inner_margin(4.0)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label("You have unsaved changes! Save or discard to continue.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Save").clicked() {
|
|
||||||
// TODO: Save ACLs
|
|
||||||
let path = self.path.clone();
|
|
||||||
let acl = self.acl.clone();
|
|
||||||
self.save_thread_error = None;
|
self.save_thread_error = None;
|
||||||
self.save_thread = Some(std::thread::spawn(move || {
|
|
||||||
crate::helper::acl_writer::write_acl_recursive(path, acl)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Discard").clicked() {
|
|
||||||
self.is_changed = false;
|
self.is_changed = false;
|
||||||
self.path.clear();
|
|
||||||
}
|
}
|
||||||
});
|
Ok(Err(e)) => {
|
||||||
|
self.save_thread_error = Some(format!("Failed to save {:?}", e));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.save_thread_error = Some(format!("Failed to join {:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.set_title("Saving...");
|
||||||
}
|
}
|
||||||
});
|
} else if let Some(err) = &self.save_thread_error {
|
||||||
|
f.set_title(err);
|
||||||
|
} else {
|
||||||
|
f.set_title(format!("Editing: {:?}", self.path));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn new_group(&mut self, ui: &mut egui::Ui) {
|
impl ACLEditor {
|
||||||
ui.label("Group:");
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||||
let selected = filter_dropbox(
|
|
||||||
ui,
|
|
||||||
"group_popup",
|
|
||||||
&mut self.search_group,
|
|
||||||
self.available_groups.iter(),
|
|
||||||
|group, filter| group.name.contains(filter),
|
|
||||||
|group| group.name.as_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(selected) = selected {
|
let path: PathBuf = path.into();
|
||||||
self.acl
|
|
||||||
.entries
|
Self {
|
||||||
.push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX));
|
path,
|
||||||
self.is_changed = true;
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_user(&mut self, ui: &mut egui::Ui) {
|
// pub fn show(&mut self, ui: &mut egui::Ui, selected_path: &PathBuf) {
|
||||||
ui.label("User:");
|
// let mut show_save_or_dicard_message = false;
|
||||||
let selected = filter_dropbox(
|
// ui.vertical(|ui| {
|
||||||
ui,
|
// if self.save_thread.is_some() {
|
||||||
"user_popup",
|
// if self.save_thread.as_ref().unwrap().is_finished() {
|
||||||
&mut self.search_user,
|
// let save_thread = std::mem::replace(&mut self.save_thread, None);
|
||||||
self.available_users.iter(),
|
// let result = save_thread.unwrap().join();
|
||||||
|user, filter| user.name.contains(filter),
|
// match result {
|
||||||
|user| user.name.as_str(),
|
// Ok(Ok(_)) => {
|
||||||
);
|
// ui.label("Saved successfully!");
|
||||||
|
// self.save_thread_error = None;
|
||||||
|
// self.is_changed = false;
|
||||||
|
// }
|
||||||
|
// Ok(Err(e)) => {
|
||||||
|
// self.save_thread_error = Some(format!("Failed to save {:?}", e));
|
||||||
|
// }
|
||||||
|
// Err(e) => {
|
||||||
|
// self.save_thread_error = Some(format!("Failed to join {:?}", e));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// egui::Frame::default()
|
||||||
|
// .fill(egui::Color32::GREEN)
|
||||||
|
// .inner_margin(4.0)
|
||||||
|
// .show(ui, |ui| {
|
||||||
|
// ui.label("Apply changes...");
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// } else if let Some(err) = &self.save_thread_error.clone() {
|
||||||
|
// egui::Frame::default()
|
||||||
|
// .fill(egui::Color32::DARK_RED)
|
||||||
|
// .inner_margin(4.0)
|
||||||
|
// .show(ui, |ui| {
|
||||||
|
// ui.label(err);
|
||||||
|
|
||||||
if let Some(selected) = selected {
|
// if ui.button("OK").clicked() {
|
||||||
self.acl
|
// self.save_thread_error = None;
|
||||||
.entries
|
// }
|
||||||
.push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX));
|
// });
|
||||||
self.is_changed = true;
|
// } else {
|
||||||
}
|
// ui.heading(format!("Editing: {:?}", self.path));
|
||||||
}
|
// ui.separator();
|
||||||
|
|
||||||
fn acl_table(&mut self, ui: &mut egui::Ui) {
|
// if self.path != *selected_path {
|
||||||
TableBuilder::new(ui)
|
// if !self.is_changed {
|
||||||
.auto_shrink([false, true])
|
// // Load ACLs
|
||||||
.striped(true)
|
// self.path = selected_path.clone();
|
||||||
.column(Column::auto().resizable(false))
|
// self.acl = PosixACL::new_from_file(&self.path, false).unwrap();
|
||||||
.column(Column::remainder().resizable(false))
|
// } else {
|
||||||
.column(Column::auto().resizable(false))
|
// show_save_or_dicard_message = true;
|
||||||
.column(Column::auto().resizable(false))
|
// }
|
||||||
.column(Column::auto().resizable(false))
|
// }
|
||||||
.column(Column::auto().resizable(false))
|
|
||||||
.header(20.0, |mut header| {
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.heading("Type");
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.heading("Name");
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.heading("R");
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.heading("W");
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.heading("X");
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.heading("D");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.body(|mut body| {
|
|
||||||
let mut to_delete: Vec<Qualifier> = Vec::new();
|
|
||||||
|
|
||||||
self.acl.entries.sort();
|
// self.acl_table(ui);
|
||||||
for entry in &mut self.acl.entries {
|
|
||||||
body.row(30.0, |mut row| {
|
|
||||||
row.col(|ui| {
|
|
||||||
let label = match entry.0 {
|
|
||||||
Qualifier::User(_) => "U",
|
|
||||||
Qualifier::Group(_) => "G",
|
|
||||||
Qualifier::Other => "",
|
|
||||||
Qualifier::GroupObj => "G",
|
|
||||||
Qualifier::UserObj => "U",
|
|
||||||
Qualifier::Mask => "",
|
|
||||||
};
|
|
||||||
ui.label(label);
|
|
||||||
});
|
|
||||||
row.col(|ui| {
|
|
||||||
let label = match entry.0 {
|
|
||||||
Qualifier::User(uid) => self
|
|
||||||
.available_users
|
|
||||||
.iter()
|
|
||||||
.find(|user| user.uid == uid)
|
|
||||||
.map(|user| user.name.clone())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
self.available_groups
|
|
||||||
.iter()
|
|
||||||
.find(|group| group.gid == uid)
|
|
||||||
.map(|group| format!("({})", group.name))
|
|
||||||
.unwrap_or_else(|| format!("Unknown user {}", uid))
|
|
||||||
}),
|
|
||||||
Qualifier::Group(gid) => self
|
|
||||||
.available_groups
|
|
||||||
.iter()
|
|
||||||
.find(|group| group.gid == gid)
|
|
||||||
.map(|group| group.name.clone())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
self.available_users
|
|
||||||
.iter()
|
|
||||||
.find(|user| user.uid == gid)
|
|
||||||
.map(|user| format!("({})", user.name))
|
|
||||||
.unwrap_or_else(|| format!("Unknown group {}", gid))
|
|
||||||
}),
|
|
||||||
Qualifier::Other => "Other".to_string(),
|
|
||||||
Qualifier::GroupObj => "GroupObj".to_string(),
|
|
||||||
Qualifier::UserObj => "UserObj".to_string(),
|
|
||||||
Qualifier::Mask => "Mask".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.label(label);
|
// ui.separator();
|
||||||
});
|
// ui.label("Add new entry:");
|
||||||
row.col(|ui| {
|
|
||||||
let mut checked = entry.1.contains(PermSet::ACL_READ);
|
|
||||||
if ui.checkbox(&mut checked, "").changed() {
|
|
||||||
if checked {
|
|
||||||
entry.1.insert(PermSet::ACL_READ);
|
|
||||||
self.is_changed = true;
|
|
||||||
} else {
|
|
||||||
entry.1.remove(PermSet::ACL_READ);
|
|
||||||
self.is_changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
row.col(|ui| {
|
|
||||||
let mut checked = entry.1.contains(PermSet::ACL_WRITE);
|
|
||||||
if ui.checkbox(&mut checked, "").changed() {
|
|
||||||
if checked {
|
|
||||||
entry.1.insert(PermSet::ACL_WRITE);
|
|
||||||
self.is_changed = true;
|
|
||||||
} else {
|
|
||||||
entry.1.remove(PermSet::ACL_WRITE);
|
|
||||||
self.is_changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
row.col(|ui| {
|
|
||||||
let mut checked = entry.1.contains(PermSet::ACL_EXECUTE);
|
|
||||||
if ui.checkbox(&mut checked, "").changed() {
|
|
||||||
if checked {
|
|
||||||
entry.1.insert(PermSet::ACL_EXECUTE);
|
|
||||||
self.is_changed = true;
|
|
||||||
} else {
|
|
||||||
entry.1.remove(PermSet::ACL_EXECUTE);
|
|
||||||
self.is_changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
row.col(|ui| match entry.0 {
|
// self.new_user(ui);
|
||||||
Qualifier::User(_) | Qualifier::Group(_) => {
|
// self.new_group(ui);
|
||||||
if ui.button("🗑").clicked() {
|
|
||||||
to_delete.push(entry.0.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in to_delete {
|
// ui.separator();
|
||||||
self.acl.entries.retain(|e| e.0 != entry);
|
|
||||||
self.is_changed = true;
|
// if show_save_or_dicard_message {
|
||||||
}
|
// egui::Frame::default()
|
||||||
});
|
// .fill(egui::Color32::YELLOW)
|
||||||
}
|
// .inner_margin(4.0)
|
||||||
|
// .show(ui, |ui| {
|
||||||
|
// ui.label("You have unsaved changes! Save or discard to continue.");
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ui.horizontal(|ui| {
|
||||||
|
// if ui.button("Save").clicked() {
|
||||||
|
// let path = self.path.clone();
|
||||||
|
// let mut acl = self.acl.clone();
|
||||||
|
// acl.set(ACLEntry(Qualifier::Mask, PermSet::all())); // Make sure mask is set!
|
||||||
|
|
||||||
|
// self.save_thread_error = None;
|
||||||
|
// self.save_thread = Some(std::thread::spawn(move || {
|
||||||
|
// crate::helper::acl_writer::write_acl_recursive(path, acl)
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if ui.button("Discard").clicked() {
|
||||||
|
// self.is_changed = false;
|
||||||
|
// self.path.clear();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn new_group(&mut self, ui: &mut egui::Ui) {
|
||||||
|
// ui.label("Group:");
|
||||||
|
// let selected = filter_dropbox(
|
||||||
|
// ui,
|
||||||
|
// "group_popup",
|
||||||
|
// &mut self.search_group,
|
||||||
|
// self.available_groups.iter(),
|
||||||
|
// |group, filter| group.name.contains(filter),
|
||||||
|
// |group| group.name.as_str(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if let Some(selected) = selected {
|
||||||
|
// self.acl
|
||||||
|
// .entries
|
||||||
|
// .push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX));
|
||||||
|
// self.is_changed = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn new_user(&mut self, ui: &mut egui::Ui) {
|
||||||
|
// ui.label("User:");
|
||||||
|
// let selected = filter_dropbox(
|
||||||
|
// ui,
|
||||||
|
// "user_popup",
|
||||||
|
// &mut self.search_user,
|
||||||
|
// self.available_users.iter(),
|
||||||
|
// |user, filter| user.name.contains(filter),
|
||||||
|
// |user| user.name.as_str(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if let Some(selected) = selected {
|
||||||
|
// self.acl
|
||||||
|
// .entries
|
||||||
|
// .push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX));
|
||||||
|
// self.is_changed = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn acl_table(&mut self, ui: &mut egui::Ui) {
|
||||||
|
// TableBuilder::new(ui)
|
||||||
|
// .auto_shrink([false, true])
|
||||||
|
// .striped(true)
|
||||||
|
// .column(Column::auto().resizable(false))
|
||||||
|
// .column(Column::remainder().resizable(false))
|
||||||
|
// .column(Column::auto().resizable(false))
|
||||||
|
// .column(Column::auto().resizable(false))
|
||||||
|
// .column(Column::auto().resizable(false))
|
||||||
|
// .column(Column::auto().resizable(false))
|
||||||
|
// .header(20.0, |mut header| {
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("Type");
|
||||||
|
// });
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("Name");
|
||||||
|
// });
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("R");
|
||||||
|
// });
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("W");
|
||||||
|
// });
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("X");
|
||||||
|
// });
|
||||||
|
// header.col(|ui| {
|
||||||
|
// ui.heading("D");
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
// .body(|mut body| {
|
||||||
|
// let mut to_delete: Vec<Qualifier> = Vec::new();
|
||||||
|
|
||||||
|
// self.acl.entries.sort();
|
||||||
|
// for entry in &mut self.acl.entries {
|
||||||
|
// body.row(30.0, |mut row| {
|
||||||
|
// row.col(|ui| {
|
||||||
|
// let label = match entry.0 {
|
||||||
|
// Qualifier::User(_) => "U",
|
||||||
|
// Qualifier::Group(_) => "G",
|
||||||
|
// Qualifier::Other => "",
|
||||||
|
// Qualifier::GroupObj => "G",
|
||||||
|
// Qualifier::UserObj => "U",
|
||||||
|
// Qualifier::Mask => "",
|
||||||
|
// };
|
||||||
|
// ui.label(label);
|
||||||
|
// });
|
||||||
|
// row.col(|ui| {
|
||||||
|
// let label = match entry.0 {
|
||||||
|
// Qualifier::User(uid) => self
|
||||||
|
// .available_users
|
||||||
|
// .iter()
|
||||||
|
// .find(|user| user.uid == uid)
|
||||||
|
// .map(|user| user.name.clone())
|
||||||
|
// .unwrap_or_else(|| {
|
||||||
|
// self.available_groups
|
||||||
|
// .iter()
|
||||||
|
// .find(|group| group.gid == uid)
|
||||||
|
// .map(|group| format!("({})", group.name))
|
||||||
|
// .unwrap_or_else(|| format!("Unknown user {}", uid))
|
||||||
|
// }),
|
||||||
|
// Qualifier::Group(gid) => self
|
||||||
|
// .available_groups
|
||||||
|
// .iter()
|
||||||
|
// .find(|group| group.gid == gid)
|
||||||
|
// .map(|group| group.name.clone())
|
||||||
|
// .unwrap_or_else(|| {
|
||||||
|
// self.available_users
|
||||||
|
// .iter()
|
||||||
|
// .find(|user| user.uid == gid)
|
||||||
|
// .map(|user| format!("({})", user.name))
|
||||||
|
// .unwrap_or_else(|| format!("Unknown group {}", gid))
|
||||||
|
// }),
|
||||||
|
// Qualifier::Other => "Other".to_string(),
|
||||||
|
// Qualifier::GroupObj => "GroupObj".to_string(),
|
||||||
|
// Qualifier::UserObj => "UserObj".to_string(),
|
||||||
|
// Qualifier::Mask => "Mask".to_string(),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// ui.label(label);
|
||||||
|
// });
|
||||||
|
// row.col(|ui| {
|
||||||
|
// let mut checked = entry.1.contains(PermSet::ACL_READ);
|
||||||
|
// if ui.checkbox(&mut checked, "").changed() {
|
||||||
|
// if checked {
|
||||||
|
// entry.1.insert(PermSet::ACL_READ);
|
||||||
|
// self.is_changed = true;
|
||||||
|
// } else {
|
||||||
|
// entry.1.remove(PermSet::ACL_READ);
|
||||||
|
// self.is_changed = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// row.col(|ui| {
|
||||||
|
// let mut checked = entry.1.contains(PermSet::ACL_WRITE);
|
||||||
|
// if ui.checkbox(&mut checked, "").changed() {
|
||||||
|
// if checked {
|
||||||
|
// entry.1.insert(PermSet::ACL_WRITE);
|
||||||
|
// self.is_changed = true;
|
||||||
|
// } else {
|
||||||
|
// entry.1.remove(PermSet::ACL_WRITE);
|
||||||
|
// self.is_changed = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// row.col(|ui| {
|
||||||
|
// let mut checked = entry.1.contains(PermSet::ACL_EXECUTE);
|
||||||
|
// if ui.checkbox(&mut checked, "").changed() {
|
||||||
|
// if checked {
|
||||||
|
// entry.1.insert(PermSet::ACL_EXECUTE);
|
||||||
|
// self.is_changed = true;
|
||||||
|
// } else {
|
||||||
|
// entry.1.remove(PermSet::ACL_EXECUTE);
|
||||||
|
// self.is_changed = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// row.col(|ui| match entry.0 {
|
||||||
|
// Qualifier::User(_) | Qualifier::Group(_) => {
|
||||||
|
// if ui.button("🗑").clicked() {
|
||||||
|
// to_delete.push(entry.0.clone());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// _ => {}
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for entry in to_delete {
|
||||||
|
// self.acl.entries.retain(|e| e.0 != entry);
|
||||||
|
// self.is_changed = true;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn searchable_dropdown()
|
// fn searchable_dropdown()
|
||||||
|
|
||||||
fn filter_dropbox<
|
// fn filter_dropbox<
|
||||||
'a,
|
// 'a,
|
||||||
T: PartialEq + 'a,
|
// T: PartialEq + 'a,
|
||||||
I: Iterator<Item = &'a T> + 'a,
|
// I: Iterator<Item = &'a T> + 'a,
|
||||||
F: Fn(&'a T, &str) -> bool,
|
// F: Fn(&'a T, &str) -> bool,
|
||||||
L: Fn(&'a T) -> &str,
|
// L: Fn(&'a T) -> &str,
|
||||||
>(
|
// >(
|
||||||
ui: &mut egui::Ui,
|
// ui: &mut egui::Ui,
|
||||||
popup_id: &str,
|
// popup_id: &str,
|
||||||
search_buf: &mut String,
|
// search_buf: &mut String,
|
||||||
items: I,
|
// items: I,
|
||||||
filter_items: F,
|
// filter_items: F,
|
||||||
get_label: L,
|
// get_label: L,
|
||||||
) -> Option<&'a T> {
|
// ) -> Option<&'a T> {
|
||||||
let search_field = ui.text_edit_singleline(search_buf);
|
// let search_field = ui.text_edit_singleline(search_buf);
|
||||||
let popup_id = ui.make_persistent_id(popup_id);
|
// let popup_id = ui.make_persistent_id(popup_id);
|
||||||
|
|
||||||
if search_field.gained_focus() {
|
// if search_field.gained_focus() {
|
||||||
ui.memory_mut(|mem| mem.open_popup(popup_id));
|
// ui.memory_mut(|mem| mem.open_popup(popup_id));
|
||||||
}
|
// }
|
||||||
|
|
||||||
let mut selected = None;
|
// let mut selected = None;
|
||||||
egui::popup_above_or_below_widget(
|
// egui::popup_above_or_below_widget(
|
||||||
&ui,
|
// &ui,
|
||||||
popup_id,
|
// popup_id,
|
||||||
&search_field,
|
// &search_field,
|
||||||
egui::AboveOrBelow::Below,
|
// egui::AboveOrBelow::Below,
|
||||||
egui::PopupCloseBehavior::CloseOnClick,
|
// egui::PopupCloseBehavior::CloseOnClick,
|
||||||
|ui| {
|
// |ui| {
|
||||||
ui.set_min_width(200.0);
|
// ui.set_min_width(200.0);
|
||||||
ui.set_max_height(300.0);
|
// ui.set_max_height(300.0);
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
// egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
let filtered = items.filter(|elm| filter_items(elm, &search_buf));
|
// let filtered = items.filter(|elm| filter_items(elm, &search_buf));
|
||||||
|
|
||||||
for user in filtered {
|
// for user in filtered {
|
||||||
let value = ui.selectable_value(&mut selected, Some(user), get_label(user));
|
// let value = ui.selectable_value(&mut selected, Some(user), get_label(user));
|
||||||
if value.clicked() {
|
// if value.clicked() {
|
||||||
search_field.surrender_focus();
|
// search_field.surrender_focus();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
|
|
||||||
if selected.is_some() {
|
// if selected.is_some() {
|
||||||
search_buf.clear();
|
// search_buf.clear();
|
||||||
}
|
// }
|
||||||
|
|
||||||
selected
|
// selected
|
||||||
}
|
// }
|
||||||
|
235
src/ui/mod.rs
235
src/ui/mod.rs
@ -1,68 +1,217 @@
|
|||||||
|
use std::{io, rc::Rc, sync::RwLock};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use editor::ACLEditor;
|
use editor::ACLEditor;
|
||||||
use eframe::egui::{self};
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::event::{self, Event, KeyCode},
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
widgets::Widget,
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
use tree::Tree;
|
use tree::Tree;
|
||||||
|
|
||||||
mod editor;
|
mod editor;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
exit: bool,
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
editor: ACLEditor,
|
editor: Option<ACLEditor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl Default for App {
|
||||||
pub fn new(ctx: &egui::Context) -> Self {
|
fn default() -> Self {
|
||||||
let mut fonts = egui::FontDefinitions::default();
|
|
||||||
fonts.font_data.insert(
|
|
||||||
"FiraCode".to_owned(),
|
|
||||||
egui::FontData::from_static(include_bytes!("../../fonts/FiraCodeNerdFont-Regular.ttf")),
|
|
||||||
);
|
|
||||||
|
|
||||||
fonts
|
|
||||||
.families
|
|
||||||
.entry(egui::FontFamily::Monospace)
|
|
||||||
.or_default()
|
|
||||||
.insert(0, "FiraCode".to_owned());
|
|
||||||
|
|
||||||
fonts
|
|
||||||
.families
|
|
||||||
.entry(egui::FontFamily::Proportional)
|
|
||||||
.or_default()
|
|
||||||
.insert(0, "FiraCode".to_owned());
|
|
||||||
|
|
||||||
ctx.set_fonts(fonts);
|
|
||||||
|
|
||||||
// first argument is the initial path or the current working directory
|
|
||||||
|
|
||||||
let initial_path = if let Some(path) = std::env::args().nth(1) {
|
let initial_path = if let Some(path) = std::env::args().nth(1) {
|
||||||
std::path::PathBuf::from(path)
|
std::path::PathBuf::from(path)
|
||||||
} else {
|
} else {
|
||||||
std::env::current_dir().unwrap()
|
std::env::current_dir().unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
App {
|
Self {
|
||||||
|
exit: false,
|
||||||
tree: Tree::new(initial_path),
|
tree: Tree::new(initial_path),
|
||||||
editor: ACLEditor::new(),
|
editor: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl App {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||||
egui::SidePanel::right("side_panel")
|
let mut app_event = AppEventHost::default();
|
||||||
.min_width(300.0)
|
while !self.exit {
|
||||||
.max_width(ctx.available_rect().width() - 100.0)
|
terminal.draw(|frame| self.render(frame, app_event))?;
|
||||||
.resizable(true)
|
app_event = self.handle_events()?;
|
||||||
.show(ctx, |ui| {
|
}
|
||||||
self.editor.show(ui, &self.tree.selected);
|
Ok(())
|
||||||
});
|
}
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
pub fn handle_events(&mut self) -> io::Result<AppEventHost> {
|
||||||
egui::ScrollArea::both()
|
// Handle events here
|
||||||
.auto_shrink([false; 2])
|
// For example, you can check for key presses and update the state accordingly
|
||||||
.show(ui, |ui| {
|
|
||||||
self.tree.show(ui);
|
let evt = event::read()?;
|
||||||
});
|
if let Event::Key(key) = evt {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
self.exit = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AppEventHost::new(Some(evt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, frame: &mut Frame, event: AppEventHost) {
|
||||||
|
if let Some(evt) = &event.event {
|
||||||
|
match evt {
|
||||||
|
Event::Key(key) => match key.code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
self.exit = true;
|
||||||
|
}
|
||||||
|
KeyCode::Enter | KeyCode::Char('e') => {
|
||||||
|
self.editor = Some(ACLEditor::new(self.tree.get_selected()));
|
||||||
|
}
|
||||||
|
KeyCode::Esc | KeyCode::Char('c') => {
|
||||||
|
self.editor = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.get_client(true).register_helper("q", "Quit");
|
||||||
|
|
||||||
|
let layout1 = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Fill(1), Constraint::Length(1)])
|
||||||
|
.split(frame.area());
|
||||||
|
|
||||||
|
let content_area = layout1[0];
|
||||||
|
let help_area = layout1[1];
|
||||||
|
|
||||||
|
let layout2 = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Fill(1)])
|
||||||
|
.split(content_area);
|
||||||
|
|
||||||
|
let content_left_area = layout2[0];
|
||||||
|
let content_right_area = layout2[1];
|
||||||
|
|
||||||
|
self.tree.render(
|
||||||
|
frame,
|
||||||
|
content_left_area,
|
||||||
|
event.get_client(self.editor.is_none()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(editor) = self.editor.as_mut() {
|
||||||
|
event
|
||||||
|
.get_client(true)
|
||||||
|
.register_helper("Esc", "Close editor");
|
||||||
|
editor.render(frame, content_right_area, event.get_client(true));
|
||||||
|
} else {
|
||||||
|
event.get_client(true).register_helper("Enter", "Select");
|
||||||
|
frame.render_widget("Select folder from the left", content_right_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.render_help(frame, help_area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppEventHost {
|
||||||
|
pub event: Option<Event>,
|
||||||
|
pub registered_helper: Rc<RwLock<Vec<AppEventHelper>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppEventHost {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
event: None,
|
||||||
|
registered_helper: Rc::from(RwLock::new(vec![])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppEventHost {
|
||||||
|
pub fn new(event: Option<Event>) -> Self {
|
||||||
|
Self {
|
||||||
|
event: event,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_help(&self, frame: &mut Frame, area: Rect) {
|
||||||
|
let mut help = String::new();
|
||||||
|
let infos = self.registered_helper.read().unwrap();
|
||||||
|
for info in infos.iter() {
|
||||||
|
help.push_str(&format!("{}: {} | \n", info.key, info.help));
|
||||||
|
}
|
||||||
|
frame.render_widget(help, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_client(&self, focus: bool) -> AppEventClient {
|
||||||
|
AppEventClient {
|
||||||
|
host: self.registered_helper.clone(),
|
||||||
|
event: self.event.clone(),
|
||||||
|
has_focus: focus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppEventClient {
|
||||||
|
host: Rc<RwLock<Vec<AppEventHelper>>>,
|
||||||
|
event: Option<Event>,
|
||||||
|
pub has_focus: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppEventClient {
|
||||||
|
pub fn has_focus(&self) -> bool {
|
||||||
|
self.has_focus
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client(&self, focus: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
host: self.host.clone(),
|
||||||
|
event: self.event.clone(),
|
||||||
|
has_focus: focus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_helper(&mut self, key: &str, help: &str) {
|
||||||
|
self.host.write().unwrap().push(AppEventHelper {
|
||||||
|
key: key.to_string(),
|
||||||
|
help: help.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppEventHelper {
|
||||||
|
pub key: String,
|
||||||
|
pub help: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl eframe::App for App {
|
||||||
|
// fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// egui::SidePanel::right("side_panel")
|
||||||
|
// .min_width(300.0)
|
||||||
|
// .max_width(ctx.available_rect().width() - 100.0)
|
||||||
|
// .resizable(true)
|
||||||
|
// .show(ctx, |ui| {
|
||||||
|
// self.editor.show(ui, &self.tree.selected);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
// egui::ScrollArea::both()
|
||||||
|
// .auto_shrink([false; 2])
|
||||||
|
// .show(ui, |ui| {
|
||||||
|
// self.tree.show(ui);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub trait Component {
|
||||||
|
fn render(&mut self, f: &mut Frame, rect: Rect, evt: AppEventClient);
|
||||||
|
}
|
||||||
|
318
src/ui/tree.rs
318
src/ui/tree.rs
@ -3,13 +3,98 @@
|
|||||||
// Folders can by selected
|
// Folders can by selected
|
||||||
// Folders will fetch their children when expanded
|
// Folders will fetch their children when expanded
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::{default, fmt::format, path::PathBuf};
|
||||||
|
|
||||||
use eframe::egui;
|
use ratatui::{
|
||||||
|
crossterm::event::{Event, KeyCode, KeyEvent},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Widget},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Component;
|
||||||
|
|
||||||
|
// use eframe::egui;
|
||||||
|
|
||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
root: Folder,
|
root: Folder,
|
||||||
pub selected: PathBuf,
|
// pub selected: PathBuf,
|
||||||
|
vertical_scroll: usize,
|
||||||
|
vertical_scroll_state: ScrollbarState,
|
||||||
|
selected_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Tree {
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
f: &mut ratatui::Frame,
|
||||||
|
rect: ratatui::prelude::Rect,
|
||||||
|
mut evt: super::AppEventClient,
|
||||||
|
) {
|
||||||
|
if evt.has_focus() {
|
||||||
|
if let Some(evt) = &evt.event {
|
||||||
|
if let Event::Key(key) = evt {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
if self.selected_idx > 0 {
|
||||||
|
self.selected_idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
let entries = self.root.get_entries();
|
||||||
|
if self.selected_idx < entries.len() - 1 {
|
||||||
|
self.selected_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.register_helper("->", "Expand");
|
||||||
|
evt.register_helper("<-", "Collapse");
|
||||||
|
evt.register_helper("↑", "Up");
|
||||||
|
evt.register_helper("↓", "Down");
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title("Select Folder")
|
||||||
|
.borders(ratatui::widgets::Borders::ALL)
|
||||||
|
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::White));
|
||||||
|
|
||||||
|
let inner_area = block.inner(rect);
|
||||||
|
f.render_widget(block, rect);
|
||||||
|
|
||||||
|
let mut tmp = self.selected_idx;
|
||||||
|
self.root.apply_event(&evt, &mut tmp);
|
||||||
|
let entries = self.root.get_entries();
|
||||||
|
let lines: Vec<Line> = entries.iter().map(|entry| entry.get_line()).collect();
|
||||||
|
|
||||||
|
if self.selected_idx >= self.vertical_scroll + inner_area.height as usize {
|
||||||
|
self.vertical_scroll = self.selected_idx - inner_area.height as usize + 1;
|
||||||
|
} else if self.selected_idx < self.vertical_scroll as usize {
|
||||||
|
self.vertical_scroll = self.selected_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.vertical_scroll_state = self
|
||||||
|
.vertical_scroll_state
|
||||||
|
.content_length(lines.len())
|
||||||
|
.position(self.vertical_scroll as usize);
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(lines).scroll((self.vertical_scroll as u16, 0));
|
||||||
|
|
||||||
|
f.render_widget(paragraph, inner_area);
|
||||||
|
f.render_stateful_widget(
|
||||||
|
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||||
|
.begin_symbol(Some("↑"))
|
||||||
|
.end_symbol(Some("↓")),
|
||||||
|
inner_area,
|
||||||
|
&mut self.vertical_scroll_state,
|
||||||
|
);
|
||||||
|
|
||||||
|
// todo!("Implement the Widget trait for Tree");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tree {
|
impl Tree {
|
||||||
@ -17,25 +102,31 @@ impl Tree {
|
|||||||
let path: PathBuf = initial_path.into();
|
let path: PathBuf = initial_path.into();
|
||||||
let name = path.to_string_lossy().to_string();
|
let name = path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let mut root = Folder::new(name, path, 0);
|
||||||
|
root.selected = true;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
root: Folder::new(name, path),
|
root,
|
||||||
selected: PathBuf::new(),
|
// selected: PathBuf::new(),
|
||||||
|
vertical_scroll: 0,
|
||||||
|
vertical_scroll_state: ScrollbarState::default(),
|
||||||
|
selected_idx: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_root_path(&mut self, path: impl Into<PathBuf>) {
|
pub fn get_selected(&self) -> PathBuf {
|
||||||
let path: PathBuf = path.into();
|
let entries = self.root.get_entries();
|
||||||
|
if self.selected_idx < entries.len() {
|
||||||
let name = path.to_string_lossy().to_string();
|
return entries[self.selected_idx].path.clone();
|
||||||
|
}
|
||||||
self.root = Folder::new(name, path);
|
PathBuf::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(&mut self, ui: &mut egui::Ui) {
|
// pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.vertical(|ui| {
|
// ui.vertical(|ui| {
|
||||||
self.root.show(ui, &mut self.selected);
|
// self.root.show(ui, &mut self.selected);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Folder {
|
pub struct Folder {
|
||||||
@ -43,83 +134,186 @@ pub struct Folder {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
expanded: bool,
|
expanded: bool,
|
||||||
children: Option<Vec<Folder>>,
|
children: Option<Vec<Folder>>,
|
||||||
// selected: Arc<RwLock<PathBuf>>,
|
selected: bool,
|
||||||
|
depth: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Folder {
|
impl Folder {
|
||||||
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
|
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>, depth: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
expanded: false,
|
expanded: false,
|
||||||
|
selected: false,
|
||||||
children: Default::default(),
|
children: Default::default(),
|
||||||
|
depth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(&mut self, ui: &mut egui::Ui, selected: &mut PathBuf) {
|
pub fn expand(&mut self) {
|
||||||
let Self {
|
if self.children.is_none() {
|
||||||
name,
|
|
||||||
path,
|
|
||||||
expanded,
|
|
||||||
children,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
if children.is_none() {
|
|
||||||
let mut children_data = vec![];
|
let mut children_data = vec![];
|
||||||
println!("Reading dir: {:?}", path);
|
// println!("Reading dir: {:?}", self.path);
|
||||||
std::fs::read_dir(&path)
|
std::fs::read_dir(&self.path)
|
||||||
.map(|entries| {
|
.map(|entries| {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
println!(
|
// println!(
|
||||||
"Found dir: {:?}, file {}, link {}",
|
// "Found dir: {:?}, file {}, link {}",
|
||||||
path,
|
// path,
|
||||||
path.is_file(),
|
// path.is_file(),
|
||||||
path.is_symlink()
|
// path.is_symlink()
|
||||||
);
|
// );
|
||||||
let path2 = path.clone();
|
let path2 = path.clone();
|
||||||
let name = path2.file_name().unwrap().to_string_lossy();
|
let name = path2.file_name().unwrap().to_string_lossy();
|
||||||
children_data.push(Folder::new(name, path));
|
children_data.push(Folder::new(name, path, self.depth + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
eprintln!("Failed to read dir: {}", err);
|
eprintln!("Failed to read dir: {}", err);
|
||||||
});
|
});
|
||||||
children.replace(children_data);
|
children_data.sort_by_key(|folder| folder.name.clone());
|
||||||
|
self.children.replace(children_data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
pub fn apply_event(&mut self, evt: &super::AppEventClient, selected_idx: &mut usize) {
|
||||||
if let Some(children) = children {
|
if *selected_idx == 0 {
|
||||||
if !children.is_empty() {
|
self.selected = true;
|
||||||
let label = if *expanded { "▼" } else { "▶" };
|
*selected_idx = usize::MAX;
|
||||||
let response = ui.selectable_label(false, label);
|
} else {
|
||||||
|
self.selected = false;
|
||||||
if response.clicked() && !children.is_empty() {
|
}
|
||||||
*expanded = !*expanded;
|
*selected_idx -= 1;
|
||||||
|
if evt.has_focus() && self.selected {
|
||||||
|
if let Some(evt) = &evt.event {
|
||||||
|
if let Event::Key(key) = evt {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Right | KeyCode::Enter => {
|
||||||
|
self.expand();
|
||||||
|
self.expanded = true;
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
self.expanded = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ui.label("⌛");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ui
|
if self.expanded {
|
||||||
.selectable_label(*selected == *path, name.to_owned())
|
if let Some(children) = &mut self.children {
|
||||||
.clicked()
|
for child in children {
|
||||||
{
|
child.apply_event(evt, selected_idx);
|
||||||
*selected = path.clone();
|
// if child.selected {
|
||||||
}
|
// self.selected = true;
|
||||||
});
|
// }
|
||||||
|
}
|
||||||
if let Some(children) = children {
|
|
||||||
if *expanded {
|
|
||||||
ui.indent("", |ui| {
|
|
||||||
for child in children {
|
|
||||||
child.show(ui, selected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_entries(&self) -> Vec<&Folder> {
|
||||||
|
let mut entries: Vec<&Folder> = vec![];
|
||||||
|
if self.expanded {
|
||||||
|
if let Some(children) = &self.children {
|
||||||
|
for child in children {
|
||||||
|
entries.extend(child.get_entries());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res: Vec<&Folder> = vec![self];
|
||||||
|
res.extend(entries);
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_line(&self) -> Line {
|
||||||
|
let pad = " ".repeat(self.depth);
|
||||||
|
Line::from(vec![Span::styled(
|
||||||
|
format!(
|
||||||
|
"{}{} {}",
|
||||||
|
pad,
|
||||||
|
if self.expanded { "▼" } else { "▶" },
|
||||||
|
self.name
|
||||||
|
),
|
||||||
|
if self.selected {
|
||||||
|
Style::new().bg(Color::White).fg(Color::Black)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
},
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn show(&mut self, ui: &mut egui::Ui, selected: &mut PathBuf) {
|
||||||
|
// let Self {
|
||||||
|
// name,
|
||||||
|
// path,
|
||||||
|
// expanded,
|
||||||
|
// children,
|
||||||
|
// } = self;
|
||||||
|
|
||||||
|
// if children.is_none() {
|
||||||
|
// let mut children_data = vec![];
|
||||||
|
// println!("Reading dir: {:?}", path);
|
||||||
|
// std::fs::read_dir(&path)
|
||||||
|
// .map(|entries| {
|
||||||
|
// for entry in entries.flatten() {
|
||||||
|
// let path = entry.path();
|
||||||
|
// if path.is_dir() {
|
||||||
|
// println!(
|
||||||
|
// "Found dir: {:?}, file {}, link {}",
|
||||||
|
// path,
|
||||||
|
// path.is_file(),
|
||||||
|
// path.is_symlink()
|
||||||
|
// );
|
||||||
|
// let path2 = path.clone();
|
||||||
|
// let name = path2.file_name().unwrap().to_string_lossy();
|
||||||
|
// children_data.push(Folder::new(name, path));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_else(|err| {
|
||||||
|
// eprintln!("Failed to read dir: {}", err);
|
||||||
|
// });
|
||||||
|
// children_data.sort_by_key(|folder| folder.name.clone());
|
||||||
|
// children.replace(children_data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ui.horizontal(|ui| {
|
||||||
|
// if let Some(children) = children {
|
||||||
|
// if !children.is_empty() {
|
||||||
|
// let label = if *expanded { "▼" } else { "▶" };
|
||||||
|
// let response = ui.selectable_label(false, label);
|
||||||
|
|
||||||
|
// if response.clicked() && !children.is_empty() {
|
||||||
|
// *expanded = !*expanded;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// ui.label("⌛");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if ui
|
||||||
|
// .selectable_label(*selected == *path, name.to_owned())
|
||||||
|
// .clicked()
|
||||||
|
// {
|
||||||
|
// *selected = path.clone();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if let Some(children) = children {
|
||||||
|
// if *expanded {
|
||||||
|
// ui.indent("", |ui| {
|
||||||
|
// for child in children {
|
||||||
|
// child.show(ui, selected);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user