diff --git a/src/app.rs b/src/app.rs index 402efb5..f1e18c0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4478,6 +4478,13 @@ impl Application for App { tab::Command::SetPermissions(path, mode) => { commands.push(self.operation(Operation::SetPermissions { path, mode })); } + tab::Command::SetMultiplePermissions(permissions) => { + commands.push(Task::batch(permissions.into_iter().map( + |(path, mode)| { + self.operation(Operation::SetPermissions { path, mode }) + }, + ))); + } tab::Command::WindowDrag => { if let Some(window_id) = self.core.main_window_id() { commands.push(window::drag(window_id)); diff --git a/src/operation/mod.rs b/src/operation/mod.rs index fa1aba1..da4a83b 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -1180,8 +1180,10 @@ impl Operation { .map_err(|s| OperationError::from_state(s, &controller))?; let controller_clone = controller.clone(); + let path_clone = path.clone(); compio::runtime::spawn_blocking(move || -> Result<(), OperationError> { let controller = controller_clone; + let path = path_clone; //TODO: what to do on non-Unix systems? #[cfg(unix)] { @@ -1196,7 +1198,10 @@ impl Operation { .await .map_err(wrap_compio_spawn_error)? .map_err(|e| OperationError::from_err(e, &controller))?; - Ok(OperationSelection::default()) + Ok(OperationSelection { + ignored: Vec::new(), + selected: vec![path], + }) } }; diff --git a/src/tab.rs b/src/tab.rs index 77ab0a1..31a82d4 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -46,7 +46,7 @@ use std::{ borrow::Cow, cell::Cell, cmp::{Ordering, Reverse}, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, error::Error, fmt::{self, Display}, fs::{self, File, Metadata}, @@ -1796,6 +1796,7 @@ pub enum Command { Preview(PreviewKind), SetOpenWith(Mime, String), SetPermissions(PathBuf, u32), + SetMultiplePermissions(Vec<(PathBuf, u32)>), SetSort(String, HeadingOptions, bool), WindowDrag, WindowToggleMaximize, @@ -1852,6 +1853,7 @@ pub enum Message { SelectLast, SetOpenWith(Mime, String), SetPermissions(PathBuf, u32), + ShiftPermissions(Option<(PathBuf, u32)>, u32, u32), SetSort(HeadingOptions, bool), TabComplete(PathBuf, Vec<(String, PathBuf)>), Thumbnail(PathBuf, ItemThumbnail), @@ -4335,6 +4337,31 @@ impl Tab { Message::SetPermissions(path, mode) => { commands.push(Command::SetPermissions(path, mode)); } + Message::ShiftPermissions(path_mode_opt, shift, bits) => match path_mode_opt { + Some((path, mode)) => commands.push(Command::SetPermissions( + path, + set_mode_part(mode, shift, bits.try_into().unwrap()), + )), + // Shift permissions on all selected items + None => { + let mut permissions = Vec::new(); + for item in self.items_opt().map_or(Vec::new(), |items| { + items.iter().filter(|item| item.selected).collect() + }) { + if let (Some(path), Some(mode)) = ( + item.path_opt(), + item.file_metadata() + .and_then(|metadata| Some(metadata.mode())), + ) { + permissions.push(( + path.clone(), + set_mode_part(mode, shift, bits.try_into().unwrap()), + )); + } + } + commands.push(Command::SetMultiplePermissions(permissions)); + } + }, Message::SetSort(heading_option, dir) => { if !matches!(self.location, Location::Search(..)) { self.sort_name = heading_option; @@ -6304,6 +6331,11 @@ impl Tab { let mut total_size: u64 = 0; let mut mime_type_counts: BTreeMap = BTreeMap::new(); + let mut user_name: BTreeSet = BTreeSet::new(); + let mut mode_user: BTreeSet = BTreeSet::new(); + let mut group_name: BTreeSet = BTreeSet::new(); + let mut mode_group: BTreeSet = BTreeSet::new(); + let mut mode_other: BTreeSet = BTreeSet::new(); let mut calculating_dir_size = false; let mut dir_size_error: Option = None; @@ -6327,6 +6359,20 @@ impl Tab { } else { total_size = total_size.saturating_add(metadata.len()); } + let mode = metadata.mode(); + user_name.insert( + get_user_by_uid(metadata.uid()) + .and_then(|user| user.name().to_str().map(ToOwned::to_owned)) + .unwrap_or_default(), + ); + mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER)); + group_name.insert( + get_group_by_gid(metadata.gid()) + .and_then(|group| group.name().to_str().map(ToOwned::to_owned)) + .unwrap_or_default(), + ); + mode_group.insert(get_mode_part(mode, MODE_SHIFT_GROUP)); + mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER)); } } let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect(); @@ -6366,6 +6412,7 @@ impl Tab { column = column.push(widget::button::standard(fl!("open")).on_press(Message::Open(None))); let mut settings = Vec::new(); + // Only allow modifying open-with if all mime types are the same if mime_types.len() == 1 { if let Some(mime) = mime_types .get(0) @@ -6396,6 +6443,97 @@ impl Tab { } } + #[cfg(unix)] + { + // The following is a bit of a hack to make the dropdown selector + // fill the available space when selected is None so it can be + // clicked on easier. + fn dropdown_width(use_default: bool) -> Length { + if use_default { + Length::Shrink + } else { + Length::Fill + } + } + + // Only return mode part if it's the only one + fn selected_mode_part(mut modes: BTreeSet) -> Option { + match (modes.pop_first(), modes.pop_first()) { + (Some(mode), None) => Some(mode.try_into().unwrap()), + _ => None, + } + } + + // Convert a limited number of values from a set into a comma separated list + fn join_set(set: BTreeSet) -> String { + let limit = 5; + let mut title = set.into_iter().collect::>(); + if title.len() > limit { + title.truncate(limit); + title.push("...".to_string()); + } + title.join(", ") + } + + let mode_part_user = selected_mode_part(mode_user); + settings.push( + widget::settings::item::builder(join_set(user_name)) + .description(fl!("owner")) + .control( + widget::dropdown( + Cow::Borrowed(MODE_NAMES.as_slice()), + mode_part_user, + move |selected| { + Message::ShiftPermissions( + None, + MODE_SHIFT_USER, + selected.try_into().unwrap(), + ) + }, + ) + .width(dropdown_width(mode_part_user.is_some())), + ), + ); + + let mode_part_group = selected_mode_part(mode_group); + settings.push( + widget::settings::item::builder(join_set(group_name)) + .description(fl!("group")) + .control( + widget::dropdown( + Cow::Borrowed(MODE_NAMES.as_slice()), + mode_part_group, + move |selected| { + Message::ShiftPermissions( + None, + MODE_SHIFT_GROUP, + selected.try_into().unwrap(), + ) + }, + ) + .width(dropdown_width(mode_part_group.is_some())), + ), + ); + + let mode_part_other = selected_mode_part(mode_other); + settings.push( + widget::settings::item::builder(fl!("other")).control( + widget::dropdown( + Cow::Borrowed(MODE_NAMES.as_slice()), + mode_part_other, + move |selected| { + Message::ShiftPermissions( + None, + MODE_SHIFT_OTHER, + selected.try_into().unwrap(), + ) + }, + ) + .width(dropdown_width(mode_part_other.is_some())), + ), + ); + } + if !settings.is_empty() { let mut section = widget::settings::section(); section = section.extend(settings);