From 3d520858f388a140ad8565145637e8224ccd3b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Mendon=C3=A7a?= Date: Thu, 9 Jul 2026 09:09:52 -0300 Subject: [PATCH 1/2] feat: allow renaming sidebar favorites to custom names Co-Authored-By: Claude Fable 5 --- i18n/en/cosmic_files.ftl | 4 ++ i18n/pt-BR/cosmic_files.ftl | 5 ++ src/app.rs | 124 +++++++++++++++++++++++++++++++----- src/config.rs | 100 +++++++++++++++++++++++++++++ src/dialog.rs | 12 +--- 5 files changed, 218 insertions(+), 27 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index ea01f7e..2c1a6f5 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -118,6 +118,10 @@ rename-file = Rename file rename-folder = Rename folder rename-confirm = Rename +## Rename Favorite Dialog +rename-favorite = Rename sidebar item +favorite-name = Item name + ## Replace Dialog replace = Replace replace-title = "{$filename}" already exists in this location diff --git a/i18n/pt-BR/cosmic_files.ftl b/i18n/pt-BR/cosmic_files.ftl index 75b071d..38d2b79 100644 --- a/i18n/pt-BR/cosmic_files.ftl +++ b/i18n/pt-BR/cosmic_files.ftl @@ -109,6 +109,11 @@ permanently-delete-warning = Deseja realmente excluir permanentemente { $target rename-file = Renomear arquivo rename-folder = Renomear pasta +## Rename Favorite Dialog + +rename-favorite = Renomear item da barra lateral +favorite-name = Nome do item + ## Replace Dialog replace = Substituir diff --git a/src/app.rs b/src/app.rs index afc22f4..6009089 100644 --- a/src/app.rs +++ b/src/app.rs @@ -321,6 +321,7 @@ pub enum NavMenuAction { Preview(segmented_button::Entity), RunContextAction(segmented_button::Entity, usize), RemoveFromSidebar(segmented_button::Entity), + RenameFavorite(segmented_button::Entity), } impl MenuAction for NavMenuAction { @@ -575,6 +576,10 @@ pub enum DialogPage { DeleteTrash { items: Vec, }, + RenameFavorite { + entity: Entity, + name: String, + }, RenameItem { from: PathBuf, parent: PathBuf, @@ -1769,15 +1774,9 @@ impl App { for (favorite_i, favorite) in self.config.favorites.iter().enumerate() { if let Some(path) = favorite.path_opt() { - let name = if matches!(favorite, Favorite::Home) { - fl!("home") - } else if let Favorite::Network { name, .. } = favorite { - name.clone() - } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) { - file_name.to_string() - } else { - fl!("filesystem") - }; + let name = favorite + .display_name() + .unwrap_or_else(|| fl!("filesystem")); nav_model = nav_model.insert(move |b| { b.text(name.clone()) .icon( @@ -2290,15 +2289,33 @@ impl App { .favorites .iter() .map(|favorite| { - if let Favorite::Path(path) = favorite { - for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) { - if path.starts_with(from) - && let Ok(relative) = path.strip_prefix(from) + match favorite { + Favorite::Path(path) => { + for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) { - favorites_changed = true; - return Favorite::from_path(to.join(relative)); + if path.starts_with(from) + && let Ok(relative) = path.strip_prefix(from) + { + favorites_changed = true; + return Favorite::from_path(to.join(relative)); + } } } + Favorite::Named { path, name } => { + for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) + { + if path.starts_with(from) + && let Ok(relative) = path.strip_prefix(from) + { + favorites_changed = true; + return Favorite::Named { + path: to.join(relative), + name: name.clone(), + }; + } + } + } + _ => {} } favorite.clone() }) @@ -2613,6 +2630,11 @@ impl Application for App { } items.push(cosmic::widget::menu::Item::Divider); if favorite_index_opt.is_some() { + items.push(cosmic::widget::menu::Item::Button( + fl!("rename-confirm"), + None, + NavMenuAction::RenameFavorite(entity), + )); items.push(cosmic::widget::menu::Item::Button( fl!("remove-from-sidebar"), None, @@ -2904,7 +2926,8 @@ impl Application for App { } else { Favorite::from_path(path) }; - if !favorites.contains(&favorite) { + let favorite_path = favorite.path_opt(); + if !favorites.iter().any(|f| f.path_opt() == favorite_path) { favorites.push(favorite); } } @@ -3262,6 +3285,18 @@ impl Application for App { DialogPage::DeleteTrash { items } => { tasks.push(self.operation(Operation::DeleteTrash { items })); } + DialogPage::RenameFavorite { entity, name } => { + if let Some(FavoriteIndex(favorite_i)) = + self.nav_model.data::(entity) + { + let mut favorites = self.config.favorites.clone(); + if let Some(favorite) = favorites.get_mut(*favorite_i) { + *favorite = favorite.with_name(name.trim()); + config_set!(favorites, favorites); + tasks.push(self.update_config()); + } + } + } DialogPage::RenameItem { from, parent, name, .. } => { @@ -4487,7 +4522,8 @@ impl Application for App { tab::Command::AddToSidebar(path) => { let mut favorites = self.config.favorites.clone(); let favorite = Favorite::from_path(path); - if !favorites.contains(&favorite) { + let favorite_path = favorite.path_opt(); + if !favorites.iter().any(|f| f.path_opt() == favorite_path) { favorites.push(favorite); } config_set!(favorites, favorites); @@ -5271,6 +5307,26 @@ impl Application for App { return self.update_config(); } } + + NavMenuAction::RenameFavorite(entity) => { + if let Some(favorite) = self + .nav_model + .data::(entity) + .and_then(|FavoriteIndex(favorite_i)| { + self.config.favorites.get(*favorite_i) + }) + { + let name = favorite + .display_name() + .unwrap_or_else(|| fl!("filesystem")); + return Task::batch([ + self.dialog_pages + .push_back(DialogPage::RenameFavorite { entity, name }), + widget::text_input::focus(self.dialog_text_input.clone()), + widget::text_input::select_all(self.dialog_text_input.clone()), + ]); + } + } }, Message::Recents => { if self.config.show_recents { @@ -6164,6 +6220,40 @@ impl Application for App { target = target ))) } + DialogPage::RenameFavorite { entity, name } => { + let entity = *entity; + let complete_maybe = if name.trim().is_empty() { + None + } else { + Some(Message::DialogComplete) + }; + + widget::dialog() + .title(fl!("rename-favorite")) + .primary_action( + widget::button::suggested(fl!("rename-confirm")) + .on_press_maybe(complete_maybe.clone()), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + .control( + widget::column::with_children([ + widget::text::body(fl!("favorite-name")).into(), + widget::text_input("", name.as_str()) + .id(self.dialog_text_input.clone()) + .on_input(move |name| { + Message::DialogUpdate(DialogPage::RenameFavorite { + entity, + name, + }) + }) + .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone())) + .into(), + ]) + .spacing(space_xxs), + ) + } DialogPage::RenameItem { from, parent, diff --git a/src/config.rs b/src/config.rs index 2138d83..bb8631e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,11 @@ pub enum Favorite { name: String, path: PathBuf, }, + /// A path with a custom name chosen by the user + Named { + path: PathBuf, + name: String, + }, } impl Favorite { @@ -98,6 +103,38 @@ impl Favorite { Self::Videos => dirs::video_dir(), Self::Path(path) => Some(path.clone()), Self::Network { path, .. } => Some(path.clone()), + Self::Named { path, .. } => Some(path.clone()), + } + } + + /// Name shown in the sidebar, or `None` if the path has no usable file name + pub fn display_name(&self) -> Option { + match self { + Self::Home => Some(crate::fl!("home")), + Self::Named { name, .. } | Self::Network { name, .. } => Some(name.clone()), + _ => self + .path_opt()? + .file_name() + .and_then(|x| x.to_str()) + .map(ToString::to_string), + } + } + + /// Return this favorite renamed with a custom name chosen by the user + pub fn with_name(&self, name: &str) -> Self { + match self { + Self::Network { uri, path, .. } => Self::Network { + uri: uri.clone(), + name: name.to_string(), + path: path.clone(), + }, + other => match other.path_opt() { + Some(path) => Self::Named { + path, + name: name.to_string(), + }, + None => other.clone(), + }, } } } @@ -385,3 +422,66 @@ pub const TIME_CONFIG_ID: &str = "com.system76.CosmicAppletTime"; pub struct TimeConfig { pub military_time: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn favorite_with_name_converts_path_to_named() { + let favorite = Favorite::Path(PathBuf::from("/some/dir")); + assert_eq!( + favorite.with_name("Custom"), + Favorite::Named { + path: PathBuf::from("/some/dir"), + name: "Custom".to_string(), + } + ); + } + + #[test] + fn favorite_with_name_updates_network_in_place() { + let favorite = Favorite::Network { + uri: "sftp://example.com/".to_string(), + name: "example.com".to_string(), + path: PathBuf::from("/run/mount/example"), + }; + assert_eq!( + favorite.with_name("Custom"), + Favorite::Network { + uri: "sftp://example.com/".to_string(), + name: "Custom".to_string(), + path: PathBuf::from("/run/mount/example"), + } + ); + } + + #[test] + fn favorite_with_name_converts_special_folder_to_named() { + let home = dirs::home_dir().unwrap(); + assert_eq!( + Favorite::Home.with_name("Custom"), + Favorite::Named { + path: home, + name: "Custom".to_string(), + } + ); + } + + #[test] + fn favorite_display_name() { + assert_eq!( + Favorite::Path(PathBuf::from("/some/dir")).display_name(), + Some("dir".to_string()) + ); + assert_eq!(Favorite::Path(PathBuf::from("/")).display_name(), None); + assert_eq!( + Favorite::Named { + path: PathBuf::from("/some/dir"), + name: "Custom".to_string(), + } + .display_name(), + Some("Custom".to_string()) + ); + } +} diff --git a/src/dialog.rs b/src/dialog.rs index b369b73..315691a 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -32,9 +32,7 @@ use std::{env, fmt, fs}; use crate::app::{ Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID, }; -use crate::config::{ - Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch, -}; +use crate::config::{Config, DialogConfig, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch}; use crate::key_bind::key_binds; use crate::localize::LANGUAGE_SORTER; use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage}; @@ -889,13 +887,7 @@ impl App { for favorite in &self.flags.config.favorites { if let Some(path) = favorite.path_opt() { - let name = if matches!(favorite, Favorite::Home) { - fl!("home") - } else if let Favorite::Network { name, .. } = favorite { - name.clone() - } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) { - file_name.to_string() - } else { + let Some(name) = favorite.display_name() else { continue; }; nav_model = nav_model.insert(move |b| { From 08a637767d43fa1d7521f142213708c64b8ff651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Mendon=C3=A7a?= Date: Thu, 20 Aug 2026 13:53:47 -0300 Subject: [PATCH 2/2] refactor: rename favorite renaming action to "Change sidebar label" Address review feedback on the sidebar favorite renaming feature: - Context menu item and dialog title now share the same "change-sidebar-label" translation key - Rename NavMenuAction::RenameFavorite and DialogPage::RenameFavorite to ChangeSidebarLabel, with the dialog field renamed to "label" - Rename Favorite::with_name to Favorite::with_label - Dialog confirm button now uses the existing "save" key instead of "rename-confirm" - Replace the "rename-favorite"/"favorite-name" keys with "sidebar-label" in en and pt-BR translations Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01YRJDQDBKhCqipbcZm7U1v9 --- i18n/en/cosmic_files.ftl | 6 +++--- i18n/pt-BR/cosmic_files.ftl | 6 +++--- src/app.rs | 38 ++++++++++++++++++------------------- src/config.rs | 16 ++++++++-------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 2c1a6f5..2c57b53 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -118,9 +118,8 @@ rename-file = Rename file rename-folder = Rename folder rename-confirm = Rename -## Rename Favorite Dialog -rename-favorite = Rename sidebar item -favorite-name = Item name +## Change Sidebar Label Dialog +sidebar-label = Sidebar label ## Replace Dialog replace = Replace @@ -345,6 +344,7 @@ move-to = Move to... move-to-trash = Move to trash restore-from-trash = Restore from trash remove-from-sidebar = Remove from sidebar +change-sidebar-label = Change sidebar label sort-by-name = Sort by name sort-by-modified = Sort by modified sort-by-size = Sort by size diff --git a/i18n/pt-BR/cosmic_files.ftl b/i18n/pt-BR/cosmic_files.ftl index 38d2b79..83986f2 100644 --- a/i18n/pt-BR/cosmic_files.ftl +++ b/i18n/pt-BR/cosmic_files.ftl @@ -109,10 +109,9 @@ permanently-delete-warning = Deseja realmente excluir permanentemente { $target rename-file = Renomear arquivo rename-folder = Renomear pasta -## Rename Favorite Dialog +## Change Sidebar Label Dialog -rename-favorite = Renomear item da barra lateral -favorite-name = Nome do item +sidebar-label = Rótulo da barra lateral ## Replace Dialog @@ -370,6 +369,7 @@ open-in-terminal = Abrir no terminal move-to-trash = Mover para a lixeira restore-from-trash = Restaurar da lixeira remove-from-sidebar = Remover da barra lateral +change-sidebar-label = Alterar rótulo na barra lateral sort-by-name = Ordenar por nome sort-by-modified = Ordenar por data de modificação sort-by-size = Ordenar por tamanho diff --git a/src/app.rs b/src/app.rs index 6009089..6a7bf63 100644 --- a/src/app.rs +++ b/src/app.rs @@ -321,7 +321,7 @@ pub enum NavMenuAction { Preview(segmented_button::Entity), RunContextAction(segmented_button::Entity, usize), RemoveFromSidebar(segmented_button::Entity), - RenameFavorite(segmented_button::Entity), + ChangeSidebarLabel(segmented_button::Entity), } impl MenuAction for NavMenuAction { @@ -576,9 +576,9 @@ pub enum DialogPage { DeleteTrash { items: Vec, }, - RenameFavorite { + ChangeSidebarLabel { entity: Entity, - name: String, + label: String, }, RenameItem { from: PathBuf, @@ -2631,9 +2631,9 @@ impl Application for App { items.push(cosmic::widget::menu::Item::Divider); if favorite_index_opt.is_some() { items.push(cosmic::widget::menu::Item::Button( - fl!("rename-confirm"), + fl!("change-sidebar-label"), None, - NavMenuAction::RenameFavorite(entity), + NavMenuAction::ChangeSidebarLabel(entity), )); items.push(cosmic::widget::menu::Item::Button( fl!("remove-from-sidebar"), @@ -3285,13 +3285,13 @@ impl Application for App { DialogPage::DeleteTrash { items } => { tasks.push(self.operation(Operation::DeleteTrash { items })); } - DialogPage::RenameFavorite { entity, name } => { + DialogPage::ChangeSidebarLabel { entity, label } => { if let Some(FavoriteIndex(favorite_i)) = self.nav_model.data::(entity) { let mut favorites = self.config.favorites.clone(); if let Some(favorite) = favorites.get_mut(*favorite_i) { - *favorite = favorite.with_name(name.trim()); + *favorite = favorite.with_label(label.trim()); config_set!(favorites, favorites); tasks.push(self.update_config()); } @@ -5308,7 +5308,7 @@ impl Application for App { } } - NavMenuAction::RenameFavorite(entity) => { + NavMenuAction::ChangeSidebarLabel(entity) => { if let Some(favorite) = self .nav_model .data::(entity) @@ -5316,12 +5316,12 @@ impl Application for App { self.config.favorites.get(*favorite_i) }) { - let name = favorite + let label = favorite .display_name() .unwrap_or_else(|| fl!("filesystem")); return Task::batch([ self.dialog_pages - .push_back(DialogPage::RenameFavorite { entity, name }), + .push_back(DialogPage::ChangeSidebarLabel { entity, label }), widget::text_input::focus(self.dialog_text_input.clone()), widget::text_input::select_all(self.dialog_text_input.clone()), ]); @@ -6220,18 +6220,18 @@ impl Application for App { target = target ))) } - DialogPage::RenameFavorite { entity, name } => { + DialogPage::ChangeSidebarLabel { entity, label } => { let entity = *entity; - let complete_maybe = if name.trim().is_empty() { + let complete_maybe = if label.trim().is_empty() { None } else { Some(Message::DialogComplete) }; widget::dialog() - .title(fl!("rename-favorite")) + .title(fl!("change-sidebar-label")) .primary_action( - widget::button::suggested(fl!("rename-confirm")) + widget::button::suggested(fl!("save")) .on_press_maybe(complete_maybe.clone()), ) .secondary_action( @@ -6239,13 +6239,13 @@ impl Application for App { ) .control( widget::column::with_children([ - widget::text::body(fl!("favorite-name")).into(), - widget::text_input("", name.as_str()) + widget::text::body(fl!("sidebar-label")).into(), + widget::text_input("", label.as_str()) .id(self.dialog_text_input.clone()) - .on_input(move |name| { - Message::DialogUpdate(DialogPage::RenameFavorite { + .on_input(move |label| { + Message::DialogUpdate(DialogPage::ChangeSidebarLabel { entity, - name, + label, }) }) .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone())) diff --git a/src/config.rs b/src/config.rs index bb8631e..f064076 100644 --- a/src/config.rs +++ b/src/config.rs @@ -120,8 +120,8 @@ impl Favorite { } } - /// Return this favorite renamed with a custom name chosen by the user - pub fn with_name(&self, name: &str) -> Self { + /// Return this favorite with a custom sidebar label chosen by the user + pub fn with_label(&self, name: &str) -> Self { match self { Self::Network { uri, path, .. } => Self::Network { uri: uri.clone(), @@ -428,10 +428,10 @@ mod tests { use super::*; #[test] - fn favorite_with_name_converts_path_to_named() { + fn favorite_with_label_converts_path_to_named() { let favorite = Favorite::Path(PathBuf::from("/some/dir")); assert_eq!( - favorite.with_name("Custom"), + favorite.with_label("Custom"), Favorite::Named { path: PathBuf::from("/some/dir"), name: "Custom".to_string(), @@ -440,14 +440,14 @@ mod tests { } #[test] - fn favorite_with_name_updates_network_in_place() { + fn favorite_with_label_updates_network_in_place() { let favorite = Favorite::Network { uri: "sftp://example.com/".to_string(), name: "example.com".to_string(), path: PathBuf::from("/run/mount/example"), }; assert_eq!( - favorite.with_name("Custom"), + favorite.with_label("Custom"), Favorite::Network { uri: "sftp://example.com/".to_string(), name: "Custom".to_string(), @@ -457,10 +457,10 @@ mod tests { } #[test] - fn favorite_with_name_converts_special_folder_to_named() { + fn favorite_with_label_converts_special_folder_to_named() { let home = dirs::home_dir().unwrap(); assert_eq!( - Favorite::Home.with_name("Custom"), + Favorite::Home.with_label("Custom"), Favorite::Named { path: home, name: "Custom".to_string(),