diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 8af659c..c639bb4 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -26,6 +26,10 @@ open-multiple-folders = Open multiple folders save = Save save-file = Save file +# Rename Dialog +rename-file = Rename file +rename-folder = Rename folder + # Replace Dialog replace = Replace replace-title = {$filename} already exists in this location. @@ -78,6 +82,7 @@ restore-from-trash = Restore from trash file = File new-tab = New tab new-window = New window +rename = Rename close-tab = Close tab quit = Quit diff --git a/src/app.rs b/src/app.rs index 2309cf4..9da949b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -53,6 +53,7 @@ pub enum Action { Operations, Paste, Properties, + Rename, RestoreFromTrash, SelectAll, Settings, @@ -82,6 +83,7 @@ impl Action { Action::Operations => Message::ToggleContextPage(ContextPage::Operations), Action::Paste => Message::Paste(entity_opt), Action::Properties => Message::ToggleContextPage(ContextPage::Properties), + Action::Rename => Message::Rename(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), Action::SelectAll => Message::SelectAll(entity_opt), Action::Settings => Message::ToggleContextPage(ContextPage::Settings), @@ -123,6 +125,7 @@ pub enum Message { PendingComplete(u64), PendingError(u64, String), PendingProgress(u64, f32), + Rename(Option), RestoreFromTrash(Option), SelectAll(Option), SystemThemeModeChange(cosmic_theme::ThemeMode), @@ -166,6 +169,12 @@ pub enum DialogPage { name: String, dir: bool, }, + RenameItem { + from: PathBuf, + parent: PathBuf, + name: String, + dir: bool, + }, } #[derive(Debug)] @@ -717,6 +726,12 @@ impl Application for App { Operation::NewFile { path } }); } + DialogPage::RenameItem { + from, parent, name, .. + } => { + let to = parent.join(name); + self.operation(Operation::Rename { from, to }); + } } } } @@ -831,6 +846,38 @@ impl Application for App { *progress = new_progress; } } + Message::Rename(entity_opt) => { + let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + if let Some(tab) = self.tab_model.data_mut::(entity) { + if let Location::Path(parent) = &tab.location { + if let Some(items) = &tab.items_opt { + let mut selected = Vec::new(); + for item in items.iter() { + if item.selected { + selected.push(item.path.clone()); + } + } + if !selected.is_empty() { + //TODO: batch rename + for path in selected { + let name = match path.file_name().and_then(|x| x.to_str()) { + Some(some) => some.to_string(), + None => continue, + }; + let dir = path.is_dir(); + self.dialog_pages.push_back(DialogPage::RenameItem { + from: path, + parent: parent.clone(), + name, + dir, + }); + } + return widget::text_input::focus(self.dialog_text_input.clone()); + } + } + } + } + } Message::RestoreFromTrash(entity_opt) => { let mut paths = Vec::new(); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); @@ -1136,6 +1183,81 @@ impl Application for App { .spacing(space_xxs), ) } + DialogPage::RenameItem { + from, + parent, + name, + dir, + } => { + //TODO: combine logic with NewItem + let mut dialog = widget::dialog(if *dir { + fl!("rename-folder") + } else { + fl!("rename-file") + }); + + let complete_maybe = if name.is_empty() { + None + } else if name == "." || name == ".." { + dialog = dialog.tertiary_action(widget::text::body(fl!( + "name-invalid", + filename = name.as_str() + ))); + None + } else if name.contains('/') { + dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes"))); + None + } else { + let path = parent.join(name); + if path.exists() { + if path.is_dir() { + dialog = dialog + .tertiary_action(widget::text::body(fl!("folder-already-exists"))); + } else { + dialog = dialog + .tertiary_action(widget::text::body(fl!("file-already-exists"))); + } + None + } else { + if name.starts_with('.') { + dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden"))); + } + Some(Message::DialogComplete) + } + }; + + dialog + .primary_action( + widget::button::suggested(fl!("rename")) + .on_press_maybe(complete_maybe.clone()), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + .control( + widget::column::with_children(vec![ + widget::text::body(if *dir { + fl!("folder-name") + } else { + fl!("file-name") + }) + .into(), + widget::text_input("", name.as_str()) + .id(self.dialog_text_input.clone()) + .on_input(move |name| { + Message::DialogUpdate(DialogPage::RenameItem { + from: from.clone(), + parent: parent.clone(), + name, + dir: *dir, + }) + }) + .on_submit_maybe(complete_maybe) + .into(), + ]) + .spacing(space_xxs), + ) + } }; Some(dialog.into()) diff --git a/src/menu.rs b/src/menu.rs index 91c6210..c37237c 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -52,6 +52,7 @@ pub fn context_menu<'a>(tab: &Tab) -> Element<'a, tab::Message> { children.push(menu_action(fl!("new-folder"), Action::NewFolder).into()); children.push(horizontal_rule(1).into()); if selected > 0 { + children.push(menu_action(fl!("rename"), Action::Rename).into()); children.push(menu_action(fl!("cut"), Action::Cut).into()); children.push(menu_action(fl!("copy"), Action::Copy).into()); children.push(menu_action(fl!("paste"), Action::Paste).into()); @@ -133,9 +134,14 @@ pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message menu_item(fl!("new-window"), Action::WindowNew), menu_item(fl!("new-file"), Action::NewFile), menu_item(fl!("new-folder"), Action::NewFolder), + //TODO: open + MenuTree::new(horizontal_rule(1)), + menu_item(fl!("rename"), Action::Rename), + //TOOD: add to sidebar, then divider + MenuTree::new(horizontal_rule(1)), + menu_item(fl!("move-to-trash"), Action::MoveToTrash), MenuTree::new(horizontal_rule(1)), menu_item(fl!("close-tab"), Action::TabClose), - MenuTree::new(horizontal_rule(1)), menu_item(fl!("quit"), Action::WindowClose), ], ), diff --git a/src/operation.rs b/src/operation.rs index d049ee0..6fddb08 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -29,6 +29,10 @@ pub enum Operation { NewFolder { path: PathBuf, }, + Rename { + from: PathBuf, + to: PathBuf, + }, /// Restore a path from the trash Restore { paths: Vec, @@ -74,6 +78,13 @@ impl Operation { .map_err(err_str)?; let _ = msg_tx.send(Message::PendingProgress(id, 100.0)).await; } + Self::Rename { from, to } => { + tokio::task::spawn_blocking(|| fs::rename(from, to)) + .await + .map_err(err_str)? + .map_err(err_str)?; + let _ = msg_tx.send(Message::PendingProgress(id, 100.0)).await; + } Self::Restore { paths } => { let total = paths.len(); let mut count = 0;