Compare commits

..

No commits in common. "deb6000c10d9974c964830a1d13c418af4681226" and "0f6f9509aee88091a51b67fe436aefc4f10692fe" have entirely different histories.

13 changed files with 218 additions and 773 deletions

411
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-files"
version = "1.7.0"
version = "1.6.0"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-files-applet"
version = "1.7.0"
version = "1.6.0"
edition = "2024"
[dependencies]

6
debian/changelog vendored
View file

@ -1,9 +1,3 @@
cosmic-files (1.7.0) noble; urgency=medium
* Epoch 1.7.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 25 Aug 2026 09:20:22 -0600
cosmic-files (1.6.0) noble; urgency=medium
* Epoch 1.6.0 version update

View file

@ -5,7 +5,7 @@ no-results = Fandt ingen resultater
filesystem = Filsystem
home = Hjem
networks = Netværk
notification-in-progress = Fil-operationer er igang
notification-in-progress = Fil-operationer er igang.
trash = Papirkurv
recents = Seneste
undo = Fortryd
@ -69,7 +69,7 @@ name-no-slashes = Navn må ikke indeholde skråstreger
## Open/Save Dialog
cancel = Afbryd
cancel = Annuller
create = Opret
open = Åbn
open-file = Åbn fil
@ -95,7 +95,7 @@ rename-folder = Omdøb mappe
## Replace Dialog
replace = Erstat
replace-title = "{ $filename }" eksisterer allerede i denne placering
replace-title = "{ $filename }" eksisterer allerede i denne placering.
replace-warning = Ønsker du at erstatte den med den, du er ved at gemme? Erstatning af den vil overskrive dens indhold.
replace-warning-operation = Ønsker du at erstatte den? Erstatning af den vil overskrive dens indhold.
original-file = Original fil
@ -253,7 +253,7 @@ light = Lyst
# Context menu
add-to-sidebar = Tilføj til sidebjælke
compress = Komprimer…
extract-here = Uddrag
extract-here = Extract
new-file = Ny fil...
new-folder = Ny mappe...
open-in-terminal = Åbn i terminal
@ -332,7 +332,7 @@ deleted =
Slettede { $items } { $items ->
[one] element
*[other] elementer
} fra { trash }
} fra papirkurven
removing-from-recents =
Fjerner { $items } { $items ->
[one] element
@ -360,7 +360,7 @@ remove = Fjern
context-action = Konteksthandling
context-action-confirm-title = Kør “{ $name }”?
context-action-confirm-warning =
Dette vil køre på { $items } { $items ->
This will run on { $items } { $items ->
[one] item
*[other] items
}.

View file

@ -119,9 +119,6 @@ rename-file = Rename file
rename-folder = Rename folder
rename-confirm = Rename
## Change Sidebar Label Dialog
sidebar-label = Sidebar label
## Replace Dialog
replace = Replace
replace-title = "{$filename}" already exists in this location
@ -302,7 +299,6 @@ unknown-folder = unknown folder
## Open with
menu-open-with = Open with...
default-app = {$name} (default)
search-application = Search by app name
## Show details
show-details = Show details
@ -351,7 +347,6 @@ 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

View file

@ -109,10 +109,6 @@ permanently-delete-warning = Deseja realmente excluir permanentemente { $target
rename-file = Renomear arquivo
rename-folder = Renomear pasta
## Change Sidebar Label Dialog
sidebar-label = Rótulo da barra lateral
## Replace Dialog
replace = Substituir
@ -354,7 +350,7 @@ light = Estilo claro
### Type to Search
type-to-search = Digitar para pesquisar
type-to-search = Digite para pesquisar
type-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas
type-to-search-enter-path = Insere o caminho do diretório ou arquivo
# Context menu
@ -369,7 +365,6 @@ 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

View file

@ -174,7 +174,6 @@ unknown-folder = неизвестная папка
menu-open-with = Открыть с помощью…
default-app = { $name } (по умолчанию)
search-application = Поиск по названию приложения
## Show details

View file

@ -37,7 +37,7 @@ use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::segmented_button::{self, Entity, ReorderEvent};
use cosmic::widget::{self, icon, settings, space};
use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, surface, theme};
use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, style, surface, theme};
use mime_guess::Mime;
use notify_debouncer_full::notify::{self, RecommendedWatcher};
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
@ -428,7 +428,6 @@ pub enum NavMenuAction {
Preview(segmented_button::Entity),
RunContextAction(segmented_button::Entity, usize),
RemoveFromSidebar(segmented_button::Entity),
ChangeSidebarLabel(segmented_button::Entity),
}
impl MenuAction for NavMenuAction {
@ -504,7 +503,6 @@ pub enum Message {
OpenWithBrowse,
OpenWithDialog(Option<Entity>),
OpenWithSelection(usize),
OpenWithSearchClear,
OpenWithToggleDefault(bool),
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
Overlap(window::Id, OverlapNotifyEvent),
@ -693,7 +691,6 @@ pub enum DialogPage {
mime: mime_guess::Mime,
selected: usize,
store_opt: Option<Arc<MimeApp>>,
search_app_name: String,
/// When true, the chosen app is written to mimeapps.list as the
/// default handler for `mime` after it spawns.
set_default: bool,
@ -704,10 +701,6 @@ pub enum DialogPage {
DeleteTrash {
items: Vec<TrashItem>,
},
ChangeSidebarLabel {
entity: Entity,
label: String,
},
RenameItem {
from: PathBuf,
parent: PathBuf,
@ -1963,9 +1956,15 @@ impl App {
for (favorite_i, favorite) in self.config.favorites.iter().enumerate() {
if let Some(path) = favorite.path_opt() {
let name = favorite
.display_name()
.unwrap_or_else(|| fl!("filesystem"));
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")
};
nav_model = nav_model.insert(move |b| {
b.text(name.clone())
.icon(
@ -2602,33 +2601,15 @@ impl App {
.favorites
.iter()
.map(|favorite| {
match favorite {
Favorite::Path(path) => {
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref()))
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)
{
if path.starts_with(from)
&& let Ok(relative) = path.strip_prefix(from)
{
favorites_changed = true;
return Favorite::from_path(to.join(relative));
}
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()
})
@ -2944,11 +2925,6 @@ 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!("change-sidebar-label"),
None,
NavMenuAction::ChangeSidebarLabel(entity),
));
items.push(cosmic::widget::menu::Item::Button(
fl!("remove-from-sidebar"),
None,
@ -3240,8 +3216,7 @@ impl Application for App {
} else {
Favorite::from_path(path)
};
let favorite_path = favorite.path_opt();
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
if !favorites.contains(&favorite) {
favorites.push(favorite);
}
}
@ -3548,18 +3523,10 @@ impl Application for App {
path,
mime,
selected,
search_app_name,
set_default,
..
} => {
let mut available_apps =
self.mime_app_cache.get_apps_for_mime(&mime, true);
available_apps.retain(|(app, _)| {
app.name
.to_lowercase()
.trim()
.contains(search_app_name.to_lowercase().as_str().trim())
});
let available_apps = self.mime_app_cache.get_apps_for_mime(&mime, true);
if let Some((app, _)) = available_apps.get(selected) {
if let Some(mut command) =
@ -3605,18 +3572,6 @@ impl Application for App {
DialogPage::DeleteTrash { items } => {
tasks.push(self.operation(Operation::DeleteTrash { items }));
}
DialogPage::ChangeSidebarLabel { entity, label } => {
if let Some(FavoriteIndex(favorite_i)) =
self.nav_model.data::<FavoriteIndex>(entity)
{
let mut favorites = self.config.favorites.clone();
if let Some(favorite) = favorites.get_mut(*favorite_i) {
*favorite = favorite.with_label(label.trim());
config_set!(favorites, favorites);
tasks.push(self.update_config());
}
}
}
DialogPage::RenameItem {
from, parent, name, ..
} => {
@ -4218,25 +4173,21 @@ impl Application for App {
let Some(path) = item.path_opt() else {
continue;
};
return Task::batch([
self.push_dialog(
DialogPage::OpenWith {
path: path.clone(),
mime: item.mime.clone(),
selected: 0,
store_opt: "x-scheme-handler/mime"
.parse::<mime_guess::Mime>()
.ok()
.and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned()
}),
search_app_name: String::new(),
set_default: false,
},
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
),
widget::text_input::focus(self.dialog_text_input.clone()),
]);
return self.push_dialog(
DialogPage::OpenWith {
path: path.clone(),
mime: item.mime.clone(),
selected: 0,
store_opt: "x-scheme-handler/mime"
.parse::<mime_guess::Mime>()
.ok()
.and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned()
}),
set_default: false,
},
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
);
}
}
}
@ -4252,14 +4203,6 @@ impl Application for App {
*set_default = enabled;
}
}
Message::OpenWithSearchClear => {
if let Some(DialogPage::OpenWith {
search_app_name, ..
}) = self.dialog_pages.front_mut()
{
*search_app_name = String::new();
}
}
Message::Paste(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
@ -4945,8 +4888,7 @@ impl Application for App {
tab::Command::AddToSidebar(path) => {
let mut favorites = self.config.favorites.clone();
let favorite = Favorite::from_path(path);
let favorite_path = favorite.path_opt();
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
if !favorites.contains(&favorite) {
favorites.push(favorite);
}
config_set!(favorites, favorites);
@ -5595,7 +5537,6 @@ impl Application for App {
.and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned()
}),
search_app_name: String::new(),
set_default: false,
},
None,
@ -5731,26 +5672,6 @@ impl Application for App {
return self.update_config();
}
}
NavMenuAction::ChangeSidebarLabel(entity) => {
if let Some(favorite) = self
.nav_model
.data::<FavoriteIndex>(entity)
.and_then(|FavoriteIndex(favorite_i)| {
self.config.favorites.get(*favorite_i)
})
{
let label = favorite
.display_name()
.unwrap_or_else(|| fl!("filesystem"));
return Task::batch([
self.dialog_pages
.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()),
]);
}
}
},
Message::Recents => {
if self.config.show_recents {
@ -6473,9 +6394,7 @@ impl Application for App {
mime,
selected,
store_opt,
search_app_name,
set_default,
..
} => {
let name = match path.file_name() {
Some(file_name) => file_name.to_str(),
@ -6483,13 +6402,7 @@ impl Application for App {
};
let mut column = widget::list_column();
let mut available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
available_apps.retain(|(app, _)| {
app.name
.to_lowercase()
.trim()
.contains(search_app_name.to_lowercase().as_str().trim())
});
let available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
let item_height = 32.0;
let mut displayed_default = false;
let mut last_kind = MimeAppMatch::Exact;
@ -6553,25 +6466,6 @@ impl Application for App {
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
)
.control(
widget::text_input::search_input(
fl!("search-application"),
search_app_name,
)
.id(self.dialog_text_input.clone())
.on_clear(Message::OpenWithSearchClear)
.on_input(move |search_app_name| {
Message::DialogUpdate(DialogPage::OpenWith {
path: path.clone(),
mime: mime.clone(),
selected: *selected,
store_opt: store_opt.clone(),
search_app_name,
set_default: *set_default,
})
})
.on_submit(|_| Message::DialogComplete),
)
.control(widget::scrollable(column).height({
let max_size = self
.size
@ -6660,40 +6554,6 @@ impl Application for App {
target = target
)))
}
DialogPage::ChangeSidebarLabel { entity, label } => {
let entity = *entity;
let complete_maybe = if label.trim().is_empty() {
None
} else {
Some(Message::DialogComplete)
};
widget::dialog()
.title(fl!("change-sidebar-label"))
.primary_action(
widget::button::suggested(fl!("save"))
.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!("sidebar-label")).into(),
widget::text_input("", label.as_str())
.id(self.dialog_text_input.clone())
.on_input(move |label| {
Message::DialogUpdate(DialogPage::ChangeSidebarLabel {
entity,
label,
})
})
.on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))
.into(),
])
.spacing(space_xxs),
)
}
DialogPage::RenameItem {
from,
parent,

View file

@ -70,11 +70,6 @@ pub enum Favorite {
name: String,
path: PathBuf,
},
/// A path with a custom name chosen by the user
Named {
path: PathBuf,
name: String,
},
}
impl Favorite {
@ -103,38 +98,6 @@ 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<String> {
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 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(),
name: name.to_string(),
path: path.clone(),
},
other => match other.path_opt() {
Some(path) => Self::Named {
path,
name: name.to_string(),
},
None => other.clone(),
},
}
}
}
@ -513,66 +476,3 @@ 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_label_converts_path_to_named() {
let favorite = Favorite::Path(PathBuf::from("/some/dir"));
assert_eq!(
favorite.with_label("Custom"),
Favorite::Named {
path: PathBuf::from("/some/dir"),
name: "Custom".to_string(),
}
);
}
#[test]
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_label("Custom"),
Favorite::Network {
uri: "sftp://example.com/".to_string(),
name: "Custom".to_string(),
path: PathBuf::from("/run/mount/example"),
}
);
}
#[test]
fn favorite_with_label_converts_special_folder_to_named() {
let home = dirs::home_dir().unwrap();
assert_eq!(
Favorite::Home.with_label("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())
);
}
}

View file

@ -32,7 +32,9 @@ use std::{env, fmt, fs};
use crate::app::{
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
};
use crate::config::{Config, DialogConfig, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch};
use crate::config::{
Config, DialogConfig, Favorite, 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};
@ -893,7 +895,13 @@ impl App {
for favorite in &self.flags.config.favorites {
if let Some(path) = favorite.path_opt() {
let Some(name) = favorite.display_name() else {
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 {
continue;
};
nav_model = nav_model.insert(move |b| {

View file

@ -18,15 +18,6 @@ use crate::tab::{self, ChecksumState, DirSize, ItemMetadata, ItemThumbnail, Loca
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
// Attributes requested when listing a network directory.
const SCAN_ATTRIBUTES: &str = "standard::name,\
standard::display-name,\
standard::type,\
standard::size,\
standard::icon,\
standard::is-hidden,\
time::modified";
fn resolve_uri(uri: &str) -> (String, gio::File) {
let file = gio::File::for_uri(uri);
// Resolve the target-uri if it exists
@ -128,27 +119,9 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
Box::from([])
};
// `filesystem::remote` belongs to the filesystem namespace, which `enumerate_children`
// never fills in, so it has to be queried once for the directory being listed.
let remote = file
.query_filesystem_info(
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
gio::Cancellable::NONE,
)
.map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
.unwrap_or_else(|err| {
log::warn!("failed to get GIO filesystem info for {uri}: {err}");
// Assume remote, so per-entry work is skipped rather than retried
true
});
let mut items = Vec::new();
for info_res in file
.enumerate_children(
SCAN_ATTRIBUTES,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
)
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
.map_err(err_str)?
{
let info = info_res.map_err(err_str)?;
@ -160,17 +133,13 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
//TODO: what is the best way to resolve shortcuts?
let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
let metadata = if force_dir {
ItemMetadata::SimpleDir { entries: 0 }
} else {
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
let is_dir = matches!(info.file_type(), gio::FileType::Directory);
let size_opt = (!is_dir).then_some(info.size() as u64);
let mut children_opt = None;
// Counting children costs a directory listing per entry, which is far too
// expensive on a remote filesystem
if is_dir && !remote {
if is_dir {
if let Some(path) = file.child(&name).path() {
//TODO: calculate children in the background (and make it cancellable?)
match std::fs::read_dir(&path) {
@ -190,8 +159,9 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
mtime,
size_opt,
children_opt,
is_dir,
}
} else {
ItemMetadata::SimpleDir { entries: 0 }
};
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {

View file

@ -458,9 +458,7 @@ impl<'a> FormatTime<'a> {
impl Display for FormatTime<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Ok(zoned) = jiff::Zoned::try_from(self.time) else {
return Ok(());
};
let zoned = jiff::Zoned::try_from(self.time).unwrap();
let now = jiff::Zoned::now();
let icu_datetime = DateTime::convert_from(zoned.datetime());
if zoned.date() == now.date() {
@ -663,41 +661,6 @@ fn display_name_for_file(path: &Path, name: &str, get_from_gvfs: bool, is_deskto
Item::display_name(name)
}
// Whether a dir lives on a remote filesystem, according to GIO.
#[cfg(feature = "gvfs")]
fn gvfs_dir_is_remote(dir: &Path) -> bool {
static REMOTE_CACHE: LazyLock<RwLock<FxHashMap<PathBuf, bool>>> =
LazyLock::new(|| RwLock::new(FxHashMap::default()));
if let Some(remote) = REMOTE_CACHE.read().unwrap().get(dir) {
return *remote;
}
let remote = match gio::prelude::FileExt::query_filesystem_info(
&gio::File::for_path(dir),
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
gio::Cancellable::NONE,
) {
Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE),
Err(err) => {
log::warn!(
"failed to get GIO filesystem info for {}: {}",
dir.display(),
err
);
//Assume remote so that the "expensive tasks" are rather skipped then actually executed.
true
}
};
REMOTE_CACHE
.write()
.unwrap()
.insert(dir.to_path_buf(), remote);
remote
}
#[cfg(feature = "gvfs")]
pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconSizes) -> Item {
let file_name = file_info
@ -705,7 +668,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
.unwrap_or_default();
let mtime = file_info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
let mut is_desktop = false;
let remote = path.parent().is_none_or(gvfs_dir_is_remote);
let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE);
let is_dir = matches!(file_info.file_type(), gio::FileType::Directory);
let size_opt = (!is_dir).then_some(file_info.size() as u64);
@ -766,7 +729,6 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
mtime,
size_opt,
children_opt,
is_dir,
},
hidden,
image_dimensions: (!remote && mime.type_() == mime::IMAGE)
@ -821,7 +783,23 @@ pub fn item_from_entry(
#[cfg(feature = "gvfs")]
FsKind::Gvfs => {
is_gvfs = true;
path.parent().is_none_or(gvfs_dir_is_remote)
let file = gio::File::for_path(&path);
match gio::prelude::FileExt::query_filesystem_info(
&file,
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
gio::Cancellable::NONE,
) {
Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE),
Err(err) => {
log::warn!(
"failed to get GIO filesystem info for {}: {}",
path.display(),
err
);
true
}
}
}
#[cfg(not(feature = "gvfs"))]
FsKind::Gvfs => {
@ -1046,6 +1024,7 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
// gio crate expects a comma delimited string
let attr_string = [
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(),
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(),
gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(),
gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(),
gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(),
@ -1854,8 +1833,6 @@ pub enum Message {
HighlightDeactivate(usize),
HighlightActivate(usize),
DirectorySize(PathBuf, DirSize),
#[cfg(feature = "gvfs")]
DirectoryChildren(PathBuf, usize),
Checksums(PathBuf, ChecksumState),
CalculateChecksums(PathBuf),
CopyChecksum(String),
@ -1924,7 +1901,6 @@ pub enum ItemMetadata {
mtime: u64,
size_opt: Option<u64>,
children_opt: Option<usize>,
is_dir: bool,
},
}
@ -1939,7 +1915,7 @@ impl ItemMetadata {
Self::SimpleDir { .. } => true,
Self::SimpleFile { .. } => false,
#[cfg(feature = "gvfs")]
Self::GvfsPath { is_dir, .. } => *is_dir,
Self::GvfsPath { children_opt, .. } => children_opt.is_some(),
}
}
@ -1948,7 +1924,7 @@ impl ItemMetadata {
Self::Path { metadata, .. } => metadata.modified().ok(),
#[cfg(feature = "gvfs")]
Self::GvfsPath { mtime, .. } => {
SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(*mtime))
Some(SystemTime::UNIX_EPOCH + Duration::from_secs(*mtime))
}
_ => None,
}
@ -2370,7 +2346,6 @@ impl Item {
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
widget::text::body(name)
.wrapping(text::Wrapping::WordOrGlyph)
.align_x(text::Alignment::Center)
.ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(
3,
)))
@ -4867,20 +4842,6 @@ impl Tab {
}
}
}
#[cfg(feature = "gvfs")]
Message::DirectoryChildren(path, children) => {
if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() {
if item.path_opt() == Some(&path) {
if let ItemMetadata::GvfsPath { children_opt, .. } = &mut item.metadata
{
*children_opt = Some(children);
}
break;
}
}
}
}
Message::Checksums(path, checksum_state) => {
let location = Location::Path(path);
if let Some(ref mut item) = self.parent_item_opt
@ -5042,15 +5003,11 @@ impl Tab {
ItemMetadata::GvfsPath {
size_opt,
children_opt,
is_dir,
..
} => {
if *is_dir {
(true, children_opt.unwrap_or_default() as u64)
} else {
(false, size_opt.unwrap_or_default())
}
}
} => match children_opt {
Some(child_count) => (true, *child_count as u64),
None => (false, size_opt.unwrap_or_default()),
},
};
let (a_is_entry, a_size) = get_size(a.1);
let (b_is_entry, b_size) = get_size(b.1);
@ -6228,21 +6185,17 @@ impl Tab {
ItemMetadata::GvfsPath {
size_opt,
children_opt,
is_dir,
..
} => {
if *is_dir {
// Children are not counted on remote filesystems
match children_opt {
//TODO: translate
Some(1) => "1 item".to_string(),
Some(child_count) => format!("{child_count} items"),
None => String::new(),
} => match children_opt {
Some(child_count) => {
if *child_count == 1 {
format!("{child_count} item")
} else {
format!("{child_count} items")
}
} else {
format_size(size_opt.unwrap_or_default())
}
}
None => format_size(size_opt.unwrap_or_default()),
},
};
let row = if condensed {
@ -7008,74 +6961,6 @@ impl Tab {
Rectangle::new(point, size)
};
// Count the children of visible directories in the background. Doing it while
// scanning costs one directory listing per entry, which stalls remote filesystems.
#[cfg(feature = "gvfs")]
for item in items {
let ItemMetadata::GvfsPath {
children_opt: None,
is_dir: true,
..
} = &item.metadata
else {
continue;
};
// Skip items that are not visible, or have no determined rect
match item.rect_opt.get() {
Some(rect) if rect.intersects(&visible_rect) => {}
_ => continue,
}
let Some(path) = item.path_opt().cloned() else {
continue;
};
struct ChildrenWrapper(PathBuf);
impl Hash for ChildrenWrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
subscriptions.push(Subscription::run_with(
ChildrenWrapper(path),
|ChildrenWrapper(path)| {
let path = path.clone();
stream::channel(
1,
move |mut output: futures::channel::mpsc::Sender<_>| async move {
let message = {
let path = path.clone();
tokio::task::spawn_blocking(move || {
let children = match fs::read_dir(&path) {
Ok(entries) => entries.count(),
Err(err) => {
log::warn!(
"failed to read directory {}: {}",
path.display(),
err
);
0
}
};
Message::DirectoryChildren(path, children)
})
.await
.unwrap()
};
if let Err(err) = output.send(message).await {
log::warn!("failed to send directory children: {err}");
}
std::future::pending().await
},
)
},
));
}
for item in items {
if item.thumbnail_opt.is_some() {
// Skip items that already have a mime type and thumbnail