diff --git a/.editorconfig b/.editorconfig old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.vscode/launch.json b/.vscode/launch.json old mode 100644 new mode 100755 diff --git a/Cargo.lock b/Cargo.lock old mode 100644 new mode 100755 index 5c74196..cdf6d26 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,8 @@ version = "0.1.0" dependencies = [ "anyhow", "color-eyre", - "env_logger", + "crossbeam", + "log", "posix-acl", "ratatui", "walkdir", @@ -38,71 +39,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys 0.59.0", -] - [[package]] name = "anyhow" version = "1.0.97" @@ -193,12 +135,6 @@ dependencies = [ "tracing-error", ] -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - [[package]] name = "compact_str" version = "0.8.1" @@ -213,6 +149,62 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.28.1" @@ -288,29 +280,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -403,12 +372,6 @@ dependencies = [ "syn", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - [[package]] name = "itertools" version = "0.13.0" @@ -424,30 +387,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jiff" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -589,21 +528,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "portable-atomic" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - [[package]] name = "posix-acl" version = "0.1.6" @@ -669,35 +593,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -967,12 +862,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml old mode 100644 new mode 100755 index d7195a2..11817f6 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ default = [] [dependencies] anyhow = "1" color-eyre = "0.6.3" -env_logger = { version = "0.11", features = ["auto-color", "humantime"] } +crossbeam = "0.8.4" +# env_logger = { version = "0.11", features = ["auto-color", "humantime"] } +log = { version = "0.4.27", features = ["std"] } posix-acl = { git = "https://git.hibas.dev/hibas123/PosixACL", tag = "0.1.6" } ratatui = { version = "0.29.0", features = ["all-widgets"] } walkdir = "2" diff --git a/fonts/FiraCodeNerdFont-Regular.ttf b/fonts/FiraCodeNerdFont-Regular.ttf deleted file mode 100644 index c530364..0000000 Binary files a/fonts/FiraCodeNerdFont-Regular.ttf and /dev/null differ diff --git a/src/helper/acl_writer.rs b/src/helper/acl_writer.rs old mode 100644 new mode 100755 index a5b7447..0ba5654 --- a/src/helper/acl_writer.rs +++ b/src/helper/acl_writer.rs @@ -1,6 +1,7 @@ use std::path::Path; use anyhow::Result; +use log::trace; use posix_acl::PosixACL; use walkdir::WalkDir; @@ -8,7 +9,7 @@ pub fn write_acl_recursive>(path: P, acl: PosixACL) -> Result<()> for entry in WalkDir::new(path).min_depth(0).follow_links(false) { let entry = entry?; let path = entry.path(); - println!("Writing ACL for: {:?} {:?}", path, acl); + trace!("Writing ACL for: {:?} {:?}", path, acl); match entry.file_type().is_dir() { true => { acl.write(&path)?; diff --git a/src/helper/getent.rs b/src/helper/getent.rs old mode 100644 new mode 100755 diff --git a/src/helper/mod.rs b/src/helper/mod.rs old mode 100644 new mode 100755 diff --git a/src/main.rs b/src/main.rs old mode 100644 new mode 100755 index 6732a99..a7b24fa --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,10 @@ use anyhow::Result; -use ratatui::{ - crossterm::event::{self, Event}, - DefaultTerminal, Frame, -}; mod helper; mod ui; 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`). color_eyre::install().expect("failed to install color_eyre"); diff --git a/src/ui/editor.rs b/src/ui/editor.rs old mode 100644 new mode 100755 index cb7180f..26380c2 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -1,7 +1,19 @@ -use std::{borrow::Borrow, io, path::PathBuf, thread::JoinHandle}; +use std::{ + borrow::Borrow, + io, + path::PathBuf, + thread::{self, JoinHandle}, +}; use anyhow::Result; -use ratatui::{widgets::Widget, DefaultTerminal, Frame}; +use ratatui::{ + crossterm::event::{Event, KeyCode, KeyModifiers}, + layout::{Constraint, Flex, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Clear, Paragraph, Row, Table, TableState, Wrap}, + Frame, +}; use crate::helper::getent::{get_groups, get_users, Group, User}; @@ -10,17 +22,24 @@ use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX}; use super::Component; pub struct ACLEditor { - pub path: PathBuf, - pub is_changed: bool, - pub available_users: Vec, - pub available_groups: Vec, - pub acl: PosixACL, + path: PathBuf, + is_changed: bool, + available_users: Vec, + available_groups: Vec, + acl: PosixACL, - pub search_user: String, - pub search_group: String, + pub exited: bool, - pub save_thread: Option>>, - pub save_thread_error: Option, + save_thread: Option>>, + save_thread_error: Option, + save_thread_ok: Option, + + table_state: TableState, + + user_popup: Option>, + group_popup: Option>, + + focused: bool, } impl Default for ACLEditor { @@ -31,25 +50,31 @@ impl Default for ACLEditor { available_groups: get_groups().unwrap(), available_users: get_users().unwrap(), acl: PosixACL::new(ACL_RWX, ACL_RWX, ACL_RWX), - search_user: String::new(), - search_group: String::new(), + exited: false, save_thread: None, save_thread_error: None, + save_thread_ok: None, + table_state: TableState::default(), + user_popup: None, + group_popup: None, + focused: false, } } } impl Component for ACLEditor { - fn render(&mut self, f: &mut Frame, rect: ratatui::prelude::Rect, evt: super::AppEventClient) { - let block = - - // todo!("Implement the rendering logic for ACLEditor"); + fn update(&mut self, mut event: super::AppEventClient) { if let Some(save_thread) = &self.save_thread { + // Mark all events as handled, since this task should not be interrupted + // by any other event + event.mark_as_handled(); + if save_thread.is_finished() { + let save_thread = std::mem::replace(&mut self.save_thread, None).unwrap(); let result = save_thread.join(); match result { Ok(Ok(_)) => { - f.set_title("Saved successfully!"); + self.save_thread_ok = Some("Saved successfully!".to_string()); self.save_thread_error = None; self.is_changed = false; } @@ -60,345 +85,474 @@ impl Component for ACLEditor { 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 if self.save_thread_error.is_some() { + // TODO: Might want to do something, or not + if event.is_focused_and_key_pressed(KeyCode::Char('o'), None) { + event.mark_as_handled(); + self.save_thread_error = None; + } + + if event.has_focus() && self.save_thread_error.is_some() { + event.register_helper("o", "Accept error"); + } + } else if self.save_thread_ok.is_some() { + if event.is_focused_and_key_pressed(KeyCode::Char('o'), None) { + event.mark_as_handled(); + self.save_thread_ok = None; + } + + if event.has_focus() && self.save_thread_ok.is_some() { + event.register_helper("o", "Accept message"); + } } else { - f.set_title(format!("Editing: {:?}", self.path)); + if let Some(user_popup) = &mut self.user_popup { + user_popup.update(event.get_client(true)); + if user_popup.exited() { + if let Some(selected) = user_popup.get_result() { + self.acl + .entries + .push(ACLEntry(Qualifier::User(selected.uid), PermSet::ACL_RWX)); + self.is_changed = true; + } + self.user_popup = None; + } else { + event.has_focus = false; + } + } else if let Some(group_popup) = &mut self.group_popup { + group_popup.update(event.get_client(true)); + if group_popup.exited() { + if let Some(selected) = group_popup.get_result() { + self.acl + .entries + .push(ACLEntry(Qualifier::Group(selected.gid), PermSet::ACL_RWX)); + self.is_changed = true; + } + self.group_popup = None; + } else { + event.has_focus = false; + } + } + + if event.is_focused_and_key_pressed(KeyCode::Esc, None) { + event.mark_as_handled(); + self.exited = true; + event.has_focus = false; + } else if event.is_focused_and_key_pressed(KeyCode::Up, None) { + event.mark_as_handled(); + self.table_state.select_previous(); + } else if event.is_focused_and_key_pressed(KeyCode::Down, None) { + event.mark_as_handled(); + self.table_state.select_next(); + } else if event.is_focused_and_key_pressed(KeyCode::Char('d'), None) { + event.mark_as_handled(); + if let Some(selected) = self.table_state.selected() { + let _entry = self.acl.entries.remove(selected); + self.is_changed = true; + self.table_state.select(None); + } + } else if event.is_focused_and_key_pressed(KeyCode::Char('u'), None) { + event.mark_as_handled(); + let mut popup = SelectUserOrGroupPopup::new_users(); + popup.update(event.get_client(true)); + self.user_popup = Some(popup); + event.has_focus = false; + } else if event.is_focused_and_key_pressed(KeyCode::Char('g'), None) { + event.mark_as_handled(); + let mut popup = SelectUserOrGroupPopup::new_groups(); + popup.update(event.get_client(true)); + self.group_popup = Some(popup); + event.has_focus = false; + } else if event.is_focused_and_key_pressed(KeyCode::Char('a'), None) { + event.mark_as_handled(); + 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_ok = None; + self.save_thread = Some(std::thread::spawn(move || { + // thread::sleep(std::time::Duration::from_secs()); + crate::helper::acl_writer::write_acl_recursive(path, acl) + })); + } + + self.acl.entries.iter_mut().enumerate().for_each(|(i, e)| { + if self.table_state.selected() == Some(i) { + event + .is_focused_and_key_pressed(KeyCode::Char('x'), None) + .then(|| { + event.mark_as_handled(); + if e.1.contains(PermSet::ACL_EXECUTE) { + e.1.remove(PermSet::ACL_EXECUTE); + } else { + e.1.insert(PermSet::ACL_EXECUTE); + } + self.is_changed = true; + }); + + event + .is_focused_and_key_pressed(KeyCode::Char('r'), None) + .then(|| { + event.mark_as_handled(); + if e.1.contains(PermSet::ACL_READ) { + e.1.remove(PermSet::ACL_READ); + } else { + e.1.insert(PermSet::ACL_READ); + } + self.is_changed = true; + }); + + event + .is_focused_and_key_pressed(KeyCode::Char('w'), None) + .then(|| { + event.mark_as_handled(); + if e.1.contains(PermSet::ACL_WRITE) { + e.1.remove(PermSet::ACL_WRITE); + } else { + e.1.insert(PermSet::ACL_WRITE); + } + self.is_changed = true; + }); + } + }); + + if event.has_focus() { + event.register_helper("Esc", "Close editor"); + event.register_helper("↑", "Select previous entry"); + event.register_helper("↓", "Select next entry"); + event.register_helper("r", "Toggle Read permission"); + event.register_helper("w", "Toggle Write permission"); + event.register_helper("x", "Toggle Execute permission"); + event.register_helper("d", "Delete selected entry"); + event.register_helper("u", "Create new user entry"); + event.register_helper("g", "Create new group entry"); + event.register_helper("a", "Apply changes"); + } + } + + self.focused = event.has_focus(); + } + + fn render(&mut self, f: &mut Frame, rect: ratatui::prelude::Rect) { + let block = Block::bordered() + .title(format!("Editing: {:?}", self.path)) + .borders(ratatui::widgets::Borders::ALL) + .border_style(ratatui::style::Style::default().fg(if self.focused { + ratatui::style::Color::Cyan + } else { + ratatui::style::Color::Gray + })); + let inner_area = block.inner(rect); + f.render_widget(block, rect); + + // todo!("Implement the rendering logic for ACLEditor"); + if self.save_thread.is_some() { + let lines = vec![ + Line::from(Span::styled("Saving...", Style::new().fg(Color::Yellow))), + // Line::from("Press any key to cancel..."), + ]; + let pg = Paragraph::new(lines).wrap(Wrap::default()); + f.render_widget(pg, inner_area); + } else if let Some(ok) = &self.save_thread_ok { + let lines = vec![ + Line::from(Span::styled( + "Saved successfully!", + Style::new().fg(Color::Green), + )), + Line::from(ok.as_str()), + // Line::from("Press any key to continue..."), + ]; + let pg = Paragraph::new(lines).wrap(Wrap::default()); + f.render_widget(pg, inner_area); + } else if let Some(err) = &self.save_thread_error { + let lines = vec![ + Line::from(Span::styled( + "Error while saving!", + Style::new().fg(Color::Red), + )), + Line::from(err.as_str()), + // Line::from("Press any key to continue..."), + ]; + let pg = Paragraph::new(lines).wrap(Wrap::default()); + f.render_widget(pg, inner_area); + } else { + let layout = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]); + let rects = layout.split(inner_area); + + let acl_table = Table::new( + self.acl.entries.iter_mut().map(|e| { + let mut cols = vec![]; + let label = match e.0 { + Qualifier::User(_) => "U", + Qualifier::Group(_) => "G", + Qualifier::Other => "", + Qualifier::GroupObj => "G", + Qualifier::UserObj => "U", + Qualifier::Mask => "", + }; + cols.push(label.to_string()); + + let label = match e.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 => "Jeder".to_string(), + Qualifier::GroupObj => "Gruppe".to_string(), + Qualifier::UserObj => "Benutzer".to_string(), + Qualifier::Mask => "Mask".to_string(), + }; + + cols.push(label); + + let r = e.1.contains(PermSet::ACL_READ); + cols.push((if r { "✔" } else { "x" }).to_string()); + let w = e.1.contains(PermSet::ACL_WRITE); + cols.push((if w { "✔" } else { "x" }).to_string()); + let x = e.1.contains(PermSet::ACL_EXECUTE); + cols.push((if x { "✔" } else { "x" }).to_string()); + + Row::new(cols) + }), + [ + Constraint::Length(1), + Constraint::Length(30), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ], + ) + .header(Row::new(vec!["Type", "Name", "R", "W", "X"])) + .row_highlight_style(Style::default().bg(Color::Blue).fg(Color::White)) + .highlight_symbol(">>"); + + if self.table_state.selected().is_none() && self.acl.entries.len() > 0 { + self.table_state.select(Some(0)); + } + + f.render_stateful_widget(acl_table, rects[0], &mut self.table_state); + + if let Some(user_popup) = &mut self.user_popup { + user_popup.render(f, rects[1]); + } else if let Some(group_popup) = &mut self.group_popup { + group_popup.render(f, rects[1]); + } } } } impl ACLEditor { pub fn new(path: impl Into) -> Self { - let path: PathBuf = path.into(); + let acl = PosixACL::new_from_file(&path, false).unwrap(); + Self { path, + acl, ..Self::default() } } - - // pub fn show(&mut self, ui: &mut egui::Ui, selected_path: &PathBuf) { - // let mut show_save_or_dicard_message = false; - // ui.vertical(|ui| { - // if self.save_thread.is_some() { - // if self.save_thread.as_ref().unwrap().is_finished() { - // let save_thread = std::mem::replace(&mut self.save_thread, None); - // let result = save_thread.unwrap().join(); - // match result { - // Ok(Ok(_)) => { - // ui.label("Saved successfully!"); - // self.save_thread_error = None; - // self.is_changed = false; - // } - // Ok(Err(e)) => { - // self.save_thread_error = Some(format!("Failed to save {:?}", e)); - // } - // Err(e) => { - // self.save_thread_error = Some(format!("Failed to join {:?}", e)); - // } - // } - // } else { - // egui::Frame::default() - // .fill(egui::Color32::GREEN) - // .inner_margin(4.0) - // .show(ui, |ui| { - // ui.label("Apply changes..."); - // }); - // } - // } else if let Some(err) = &self.save_thread_error.clone() { - // egui::Frame::default() - // .fill(egui::Color32::DARK_RED) - // .inner_margin(4.0) - // .show(ui, |ui| { - // ui.label(err); - - // if ui.button("OK").clicked() { - // self.save_thread_error = None; - // } - // }); - // } else { - // ui.heading(format!("Editing: {:?}", self.path)); - // ui.separator(); - - // if self.path != *selected_path { - // if !self.is_changed { - // // Load ACLs - // self.path = selected_path.clone(); - // self.acl = PosixACL::new_from_file(&self.path, false).unwrap(); - // } else { - // show_save_or_dicard_message = true; - // } - // } - - // self.acl_table(ui); - - // ui.separator(); - // ui.label("Add new entry:"); - - // self.new_user(ui); - // self.new_group(ui); - - // ui.separator(); - - // if show_save_or_dicard_message { - // egui::Frame::default() - // .fill(egui::Color32::YELLOW) - // .inner_margin(4.0) - // .show(ui, |ui| { - // ui.label("You have unsaved changes! Save or discard to continue."); - // }); - // } - - // ui.horizontal(|ui| { - // if ui.button("Save").clicked() { - // 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 = 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() +trait PermissionObject: Clone + std::fmt::Debug + Send + Sync { + fn get_name(&self) -> String; +} -// fn filter_dropbox< -// 'a, -// T: PartialEq + 'a, -// I: Iterator + 'a, -// F: Fn(&'a T, &str) -> bool, -// L: Fn(&'a T) -> &str, -// >( -// ui: &mut egui::Ui, -// popup_id: &str, -// search_buf: &mut String, -// items: I, -// filter_items: F, -// get_label: L, -// ) -> Option<&'a T> { -// let search_field = ui.text_edit_singleline(search_buf); -// let popup_id = ui.make_persistent_id(popup_id); +impl PermissionObject for User { + fn get_name(&self) -> String { + self.name.clone() + } +} -// if search_field.gained_focus() { -// ui.memory_mut(|mem| mem.open_popup(popup_id)); -// } +impl PermissionObject for Group { + fn get_name(&self) -> String { + self.name.clone() + } +} -// let mut selected = None; -// egui::popup_above_or_below_widget( -// &ui, -// popup_id, -// &search_field, -// egui::AboveOrBelow::Below, -// egui::PopupCloseBehavior::CloseOnClick, -// |ui| { -// ui.set_min_width(200.0); -// ui.set_max_height(300.0); -// egui::ScrollArea::vertical().show(ui, |ui| { -// let filtered = items.filter(|elm| filter_items(elm, &search_buf)); +struct SelectUserOrGroupPopup { + search: String, + available_objects: Vec, + selected: Option, + exited: bool, + table_state: TableState, +} -// for user in filtered { -// let value = ui.selectable_value(&mut selected, Some(user), get_label(user)); -// if value.clicked() { -// search_field.surrender_focus(); -// } -// } -// }); -// }, -// ); +impl SelectUserOrGroupPopup { + fn new_users() -> Self { + Self { + search: String::new(), + available_objects: get_users().unwrap(), + selected: None, + table_state: TableState::default(), + exited: false, + } + } +} -// if selected.is_some() { -// search_buf.clear(); -// } +impl SelectUserOrGroupPopup { + fn new_groups() -> Self { + Self { + search: String::new(), + available_objects: get_groups().unwrap(), + selected: None, + table_state: TableState::default(), + exited: false, + } + } +} -// selected -// } +impl SelectUserOrGroupPopup { + fn exited(&self) -> bool { + self.exited + } + + fn get_result(&self) -> Option { + self.selected.clone() + } +} + +impl SelectUserOrGroupPopup { + fn get_filtered(&self) -> Vec { + let search = self.search.to_lowercase(); + self.available_objects + .iter() + .filter(|obj| obj.get_name().to_lowercase().contains(&search)) + .cloned() + .collect() + } +} + +impl Component for SelectUserOrGroupPopup { + fn update(&mut self, mut event: super::AppEventClient) { + if event.has_focus() && !event.was_handled() { + if event.is_focused_and_key_pressed(KeyCode::Up, None) { + event.mark_as_handled(); + self.table_state.select_previous(); + } else if event.is_focused_and_key_pressed(KeyCode::Down, None) { + event.mark_as_handled(); + self.table_state.select_next(); + } else if event.is_focused_and_key_pressed(KeyCode::Tab, None) { + event.mark_as_handled(); + } else if event.is_focused_and_key_pressed(KeyCode::Enter, None) { + // TODO: Handle selection + event.mark_as_handled(); + self.selected = self + .table_state + .selected() + .and_then(|idx| self.get_filtered().get(idx).cloned()); + event.has_focus = false; + self.exited = true; + } else if let Some(evt) = &event.event { + if let Event::Key(key) = evt { + match key.code { + KeyCode::Esc => { + event.mark_as_handled(); + self.selected = None; + self.exited = true; + event.has_focus = false; + } + KeyCode::Char(c) => { + if key.modifiers == KeyModifiers::empty() { + self.search.push(c); + event.mark_as_handled(); + } + } + KeyCode::Backspace => { + self.search.pop(); + event.mark_as_handled(); + } + KeyCode::Tab => { + event.mark_as_handled(); + } + _ => {} + } + } + } + } + + if event.has_focus() { + event.register_helper("Ctrl + q", "Select user"); + event.register_helper("Esc", "Close popup"); + event.register_helper("↑", "Select previous entry"); + event.register_helper("↓", "Select next entry"); + event.register_helper("Enter", "Select entry"); + event.register_helper("Tab", "Switch focus"); + } + + if self.table_state.selected().is_none() && self.get_filtered().len() > 0 { + self.table_state.select(Some(0)); + } + } + + fn render(&mut self, frame: &mut Frame, _rect: ratatui::prelude::Rect) { + let area = frame.area(); + + let popup_area = popup_area(area, 50, 50); + frame.render_widget(Clear, popup_area); + + let block = Block::bordered() + .title("Select User or Group") + .borders(ratatui::widgets::Borders::ALL) + .border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan)); + let inner_area = block.inner(popup_area); + frame.render_widget(block, popup_area); + + let vertical = Layout::vertical([Constraint::Length(3), Constraint::Fill(1)]); + let [input_area, content_area] = vertical.areas(inner_area); + + let input = Paragraph::new(self.search.as_str()) + .block( + Block::default() + .title("Search") + .borders(ratatui::widgets::Borders::ALL), + ) + .style(Style::default().fg(Color::White).bg(Color::Black)); + + frame.render_widget(input, input_area); + + let content = Table::new( + self.get_filtered().iter().map(|user| { + let mut cols = vec![]; + cols.push(user.get_name()); + Row::new(cols) + }), + [Constraint::Fill(1)], + ) + .header(Row::new(vec!["Name"])) + .row_highlight_style(Style::default().bg(Color::Blue).fg(Color::White)) + .highlight_symbol(">>"); + + frame.render_stateful_widget(content, content_area, &mut self.table_state); + } +} + +fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect { + let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center); + let [area] = vertical.areas(area); + let [area] = horizontal.areas(area); + area +} diff --git a/src/ui/log.rs b/src/ui/log.rs new file mode 100644 index 0000000..a106464 --- /dev/null +++ b/src/ui/log.rs @@ -0,0 +1,84 @@ +use crossbeam::channel::{Receiver, Sender}; +use log::{Level, Metadata, Record}; +use ratatui::{text::Line, widgets::Wrap}; + +use crate::ui::Component; + +pub struct SimpleLogger { + sender: Sender, + pub receiver: crossbeam::channel::Receiver, +} + +impl SimpleLogger { + pub fn new() -> Self { + let (sender, receiver) = crossbeam::channel::unbounded(); + SimpleLogger { sender, receiver } + } + + pub fn init(&self) -> Receiver { + let logger = SimpleLogger::new(); + let receiver = logger.receiver.clone(); + log::set_boxed_logger(Box::new(logger)).unwrap(); + log::set_max_level(log::LevelFilter::Trace); + return receiver; + } +} + +impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Trace + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + self.sender + .send(format!("{} - {}", record.level(), record.args())) + .unwrap(); + } + } + + fn flush(&self) {} +} + +pub struct SimpleLoggerConsumer { + receiver: crossbeam::channel::Receiver, + log: Vec, +} + +impl SimpleLoggerConsumer { + pub fn new(receiver: crossbeam::channel::Receiver) -> Self { + SimpleLoggerConsumer { + receiver, + log: vec![], + } + } +} + +impl Component for SimpleLoggerConsumer { + fn update(&mut self, event: crate::ui::AppEventClient) { + // todo!() + while self.receiver.len() > 0 { + match self.receiver.try_recv() { + Ok(msg) => { + while self.log.len() >= 20 { + self.log.remove(0); + } + self.log.push(msg); + } + Err(_) => break, + } + } + } + + fn render(&mut self, frame: &mut ratatui::Frame, rect: ratatui::prelude::Rect) { + let mut lines: Vec = self.log.iter().map(|l| Line::from(l.as_str())).collect(); + lines.reverse(); + if lines.len() > 20 { + lines.truncate(20); + } + let paragraph = ratatui::widgets::Paragraph::new(lines) + .block(ratatui::widgets::Block::default()) + .wrap(Wrap::default()); + frame.render_widget(paragraph, rect); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs old mode 100644 new mode 100755 index f950341..5b61ba0 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,23 +1,25 @@ -use std::{io, rc::Rc, sync::RwLock}; +use std::{io, rc::Rc, sync::RwLock, time::Duration}; -use anyhow::Result; use editor::ACLEditor; +use log::SimpleLogger; use ratatui::{ - buffer::Buffer, crossterm::event::{self, Event, KeyCode}, layout::{Constraint, Direction, Layout, Rect}, - widgets::Widget, + style::{Modifier, Style}, + text::{Line, Span}, DefaultTerminal, Frame, }; use tree::Tree; mod editor; +pub mod log; mod tree; pub struct App { exit: bool, tree: Tree, editor: Option, + log: crate::ui::log::SimpleLoggerConsumer, } impl Default for App { @@ -28,10 +30,14 @@ impl Default for App { std::env::current_dir().unwrap() }; + let log = SimpleLogger::new().init(); + let log = crate::ui::log::SimpleLoggerConsumer::new(log); + Self { exit: false, tree: Tree::new(initial_path), editor: None, + log, } } } @@ -40,6 +46,7 @@ impl App { pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { let mut app_event = AppEventHost::default(); while !self.exit { + self.update(&mut app_event); terminal.draw(|frame| self.render(frame, app_event))?; app_event = self.handle_events()?; } @@ -50,72 +57,74 @@ impl App { // Handle events here // For example, you can check for key presses and update the state accordingly - 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; + if event::poll(Duration::from_millis(100))? { + let evt = event::read()?; + if let Event::Key(key) = evt { + match key.code { + KeyCode::Char('c') => { + if key.modifiers == event::KeyModifiers::CONTROL { + self.exit = true; + return Ok(AppEventHost::default()); + } } _ => {} - }, - _ => {} + } } + Ok(AppEventHost::new(Some(evt))) + } else { + Ok(AppEventHost::new(None)) + } + } + + pub fn update(&mut self, event: &mut AppEventHost) { + self.log.update(event.get_client(false)); + event.get_client(true).register_helper("Ctrl + C", "Quit"); + + let cc = event.get_client(self.editor.is_none()); + if cc.is_focused_and_key_pressed(KeyCode::Enter, None) + || cc.is_focused_and_key_pressed(KeyCode::Char('e'), None) + { + self.editor = Some(ACLEditor::new(self.tree.get_selected())); } - event.get_client(true).register_helper("q", "Quit"); + if let Some(editor) = &mut self.editor { + editor.update(event.get_client(true)); + if editor.exited { + self.editor = None; + } + } + if self.editor.is_none() { + event.get_client(true).register_helper("Enter", "Select"); + } - let layout1 = Layout::default() + self.tree.update(event.get_client(self.editor.is_none())); + } + + pub fn render(&mut self, frame: &mut Frame, mut event: AppEventHost) { + event.event = None; + let [content_area, log_area, help_area] = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(1)]) - .split(frame.area()); + .constraints([ + Constraint::Fill(1), + Constraint::Length(5), + Constraint::Length(2), + ]) + .areas(frame.area()); - let content_area = layout1[0]; - let help_area = layout1[1]; - - let layout2 = Layout::default() + let [content_left_area, content_right_area] = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(30), Constraint::Fill(1)]) - .split(content_area); + .areas(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()), - ); + self.tree.render(frame, content_left_area); 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)); + editor.render(frame, content_right_area); } else { - event.get_client(true).register_helper("Enter", "Select"); frame.render_widget("Select folder from the left", content_right_area); } + self.log.render(frame, log_area); event.render_help(frame, help_area); } } @@ -123,6 +132,7 @@ impl App { pub struct AppEventHost { pub event: Option, pub registered_helper: Rc>>, + pub was_handled: Rc>, } impl Default for AppEventHost { @@ -130,6 +140,7 @@ impl Default for AppEventHost { Self { event: None, registered_helper: Rc::from(RwLock::new(vec![])), + was_handled: Rc::from(RwLock::new(false)), } } } @@ -143,11 +154,27 @@ impl AppEventHost { } fn render_help(&self, frame: &mut Frame, area: Rect) { - let mut help = String::new(); + let mut spans = vec![]; let infos = self.registered_helper.read().unwrap(); + let mut first = true; for info in infos.iter() { - help.push_str(&format!("{}: {} | \n", info.key, info.help)); + if first { + first = false; + spans.push(Span::from("Help: ")); + } else { + spans.push(Span::from(", ")); + } + + let b = Span::styled(&info.key, Style::default().add_modifier(Modifier::BOLD)); + spans.push(b); + + spans.push(Span::from(" - ")); + let s = Span::styled(&info.help, Style::default()); + spans.push(s); } + + let help = ratatui::widgets::Paragraph::new(Line::from(spans)) + .wrap(ratatui::widgets::Wrap { trim: true }); frame.render_widget(help, area); } @@ -156,6 +183,7 @@ impl AppEventHost { host: self.registered_helper.clone(), event: self.event.clone(), has_focus: focus, + was_handled: self.was_handled.clone(), } } } @@ -163,6 +191,7 @@ impl AppEventHost { pub struct AppEventClient { host: Rc>>, event: Option, + pub was_handled: Rc>, pub has_focus: bool, } @@ -175,11 +204,48 @@ impl AppEventClient { Self { host: self.host.clone(), event: self.event.clone(), + was_handled: self.was_handled.clone(), has_focus: focus, } } - pub fn register_helper(&mut self, key: &str, help: &str) { + pub fn was_handled(&self) -> bool { + *self.was_handled.read().unwrap() + } + + pub fn mark_as_handled(&mut self) { + *self.was_handled.write().unwrap() = true; + } + + pub fn is_key_pressed(&self, key: KeyCode, modifiers: Option) -> bool { + if *self.was_handled.read().unwrap() { + return false; + } + + if let Some(evt) = &self.event { + if let Event::Key(k) = evt { + if let Some(mods) = modifiers { + return k.code == key && k.modifiers == mods; + } + return k.code == key; + } + } + false + } + + pub fn is_focused_and_key_pressed( + &self, + key: KeyCode, + modifiers: Option, + ) -> bool { + if self.has_focus() { + self.is_key_pressed(key, modifiers) + } else { + false + } + } + + pub fn register_helper(&self, key: &str, help: &str) { self.host.write().unwrap().push(AppEventHelper { key: key.to_string(), help: help.to_string(), @@ -192,26 +258,7 @@ pub struct AppEventHelper { 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); + fn update(&mut self, event: AppEventClient); + fn render(&mut self, frame: &mut Frame, rect: Rect); } diff --git a/src/ui/tree.rs b/src/ui/tree.rs old mode 100644 new mode 100755 index e37cdf3..0814c5b --- a/src/ui/tree.rs +++ b/src/ui/tree.rs @@ -1,73 +1,64 @@ -// Build a tree of folders (not files) -// Folders can be expanded and collapsed -// Folders can by selected -// Folders will fetch their children when expanded - -use std::{default, fmt::format, path::PathBuf}; +use std::path::PathBuf; use ratatui::{ - crossterm::event::{Event, KeyCode, KeyEvent}, + crossterm::event::{Event, KeyCode}, style::{Color, Style}, text::{Line, Span}, - widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Widget}, - Frame, + widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, }; use super::Component; -// use eframe::egui; - pub struct Tree { root: Folder, // pub selected: PathBuf, vertical_scroll: usize, vertical_scroll_state: ScrollbarState, selected_idx: usize, + focused: bool, } 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; - } - } - _ => {} - } - } + fn update(&mut self, mut evt: super::AppEventClient) { + if evt.is_focused_and_key_pressed(KeyCode::Up, None) { + evt.mark_as_handled(); + if self.selected_idx > 0 { + self.selected_idx -= 1; } + } else if evt.is_focused_and_key_pressed(KeyCode::Down, None) { + evt.mark_as_handled(); + let entries = self.root.get_entries(); + if self.selected_idx < entries.len() - 1 { + self.selected_idx += 1; + } + } + if evt.has_focus() { evt.register_helper("->", "Expand"); evt.register_helper("<-", "Collapse"); evt.register_helper("↑", "Up"); evt.register_helper("↓", "Down"); } + let mut tmp = self.selected_idx; + self.root.update(&evt, &mut tmp); + + self.focused = evt.has_focus(); + } + + fn render(&mut self, f: &mut ratatui::Frame, rect: ratatui::prelude::Rect) { let block = Block::bordered() .title("Select Folder") .borders(ratatui::widgets::Borders::ALL) - .border_style(ratatui::style::Style::default().fg(ratatui::style::Color::White)); + .border_style(ratatui::style::Style::default().fg(if self.focused { + ratatui::style::Color::Cyan + } else { + ratatui::style::Color::Gray + })); 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 = entries.iter().map(|entry| entry.get_line()).collect(); @@ -111,6 +102,7 @@ impl Tree { vertical_scroll: 0, vertical_scroll_state: ScrollbarState::default(), selected_idx: 0, + focused: false, } } @@ -179,7 +171,7 @@ impl Folder { } } - pub fn apply_event(&mut self, evt: &super::AppEventClient, selected_idx: &mut usize) { + pub fn update(&mut self, evt: &super::AppEventClient, selected_idx: &mut usize) { if *selected_idx == 0 { self.selected = true; *selected_idx = usize::MAX; @@ -207,7 +199,7 @@ impl Folder { if self.expanded { if let Some(children) = &mut self.children { for child in children { - child.apply_event(evt, selected_idx); + child.update(evt, selected_idx); // if child.selected { // self.selected = true; // }