use std::{ borrow::Borrow, io, path::PathBuf, thread::{self, JoinHandle}, }; use anyhow::Result; 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}; use posix_acl::{ACLEntry, PermSet, PosixACL, Qualifier, ACL_RWX}; use super::Component; pub struct ACLEditor { path: PathBuf, is_changed: bool, available_users: Vec, available_groups: Vec, acl: PosixACL, pub exited: bool, 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 { fn default() -> Self { Self { path: PathBuf::new(), is_changed: false, available_groups: get_groups().unwrap(), available_users: get_users().unwrap(), acl: PosixACL::new(ACL_RWX, ACL_RWX, ACL_RWX), 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 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(_)) => { self.save_thread_ok = Some("Saved successfully!".to_string()); 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 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 { 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() } } } trait PermissionObject: Clone + std::fmt::Debug + Send + Sync { fn get_name(&self) -> String; } impl PermissionObject for User { fn get_name(&self) -> String { self.name.clone() } } impl PermissionObject for Group { fn get_name(&self) -> String { self.name.clone() } } struct SelectUserOrGroupPopup { search: String, available_objects: Vec, selected: Option, exited: bool, table_state: TableState, } impl SelectUserOrGroupPopup { fn new_users() -> Self { Self { search: String::new(), available_objects: get_users().unwrap(), selected: None, table_state: TableState::default(), exited: false, } } } impl SelectUserOrGroupPopup { fn new_groups() -> Self { Self { search: String::new(), available_objects: get_groups().unwrap(), selected: None, table_state: TableState::default(), exited: false, } } } 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 }