Compare commits
2 commits
f0538190d9
...
d080bc85af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d080bc85af | ||
|
|
338354c4d0 |
6 changed files with 135 additions and 44 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -1494,8 +1494,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-text"
|
name = "cosmic-text"
|
||||||
version = "0.18.2"
|
version = "0.19.0"
|
||||||
source = "git+https://github.com/pop-os/cosmic-text.git#4d74f795cc771fdcc7ea0f9cacba63fcf036fad6"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
|
|
@ -1513,6 +1512,7 @@ dependencies = [
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
"unicode-script",
|
"unicode-script",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9448,7 +9448,3 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
"winnow 0.7.15",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[patch.unused]]
|
|
||||||
name = "cosmic-text"
|
|
||||||
version = "0.19.0"
|
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ window_clipboard = { path = "/home/lionel/Devels/window_clipboard" }
|
||||||
dnd = { path = "/home/lionel/Devels/window_clipboard/dnd" }
|
dnd = { path = "/home/lionel/Devels/window_clipboard/dnd" }
|
||||||
mime = { path = "/home/lionel/Devels/window_clipboard/mime" }
|
mime = { path = "/home/lionel/Devels/window_clipboard/mime" }
|
||||||
|
|
||||||
[patch.'https://github.com/pop-os/cosmic-text']
|
[patch.'https://github.com/pop-os/cosmic-text.git']
|
||||||
cosmic-text = { path = "/home/lionel/Devels/cosmic-text" }
|
cosmic-text = { path = "/home/lionel/Devels/cosmic-text" }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
|
|
||||||
78
docs/local-performance-and-portal-notes.md
Normal file
78
docs/local-performance-and-portal-notes.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Local performance and portal notes
|
||||||
|
|
||||||
|
Date: 2026-05-05
|
||||||
|
|
||||||
|
This repository carries a local patch aimed at improving initial directory
|
||||||
|
display latency in large folders, plus a system-level workaround for a COSMIC
|
||||||
|
portal FileChooser crash observed with Firefox and Chromium.
|
||||||
|
|
||||||
|
## Directory listing latency
|
||||||
|
|
||||||
|
The slow path was the initial construction of `Item` values in `src/tab.rs`.
|
||||||
|
On a test folder with about 2000 entries, raw filesystem enumeration and stat
|
||||||
|
calls completed in a few milliseconds, while COSMIC Files took multiple seconds
|
||||||
|
before showing the directory.
|
||||||
|
|
||||||
|
The local patch keeps initial item construction cheap:
|
||||||
|
|
||||||
|
- directory child counts are no longer computed synchronously in
|
||||||
|
`item_from_entry` and `item_from_gvfs_info`;
|
||||||
|
- initial MIME detection uses extension-based `mime_guess`;
|
||||||
|
- regular files use a generic file icon during the initial scan instead of
|
||||||
|
resolving the full MIME icon immediately.
|
||||||
|
|
||||||
|
This keeps folders and `.desktop` files special-cased, while avoiding expensive
|
||||||
|
per-file work for ordinary files during first paint.
|
||||||
|
|
||||||
|
Measured locally during investigation:
|
||||||
|
|
||||||
|
- before the MIME/icon changes: about 3.1 seconds for `~/Téléchargements`;
|
||||||
|
- after avoiding full MIME icon resolution during scan: below the temporary
|
||||||
|
100 ms perf-log threshold for the same folder.
|
||||||
|
|
||||||
|
## File chooser portal workaround
|
||||||
|
|
||||||
|
Firefox and Chromium were failing to open `Save As` on the first attempt because
|
||||||
|
`xdg-desktop-portal-cosmic` crashed while handling `FileChooser`.
|
||||||
|
|
||||||
|
Logs showed:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Backend call failed: Remote peer disconnected
|
||||||
|
xdg-desktop-portal-cosmic ... status=11/SEGV
|
||||||
|
```
|
||||||
|
|
||||||
|
The working local system workaround is to remove
|
||||||
|
`org.freedesktop.impl.portal.FileChooser` from:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/share/xdg-desktop-portal/portals/cosmic.portal
|
||||||
|
```
|
||||||
|
|
||||||
|
so the file chooser falls back to GTK. The resulting local `cosmic.portal`
|
||||||
|
keeps COSMIC for the other interfaces:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[portal]
|
||||||
|
DBusName=org.freedesktop.impl.portal.desktop.cosmic
|
||||||
|
Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.ScreenCast
|
||||||
|
UseIn=COSMIC
|
||||||
|
```
|
||||||
|
|
||||||
|
Backups created locally:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/share/xdg-desktop-portal/portals/cosmic.portal.bak-codex-20260505-filechooser
|
||||||
|
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505
|
||||||
|
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505-2
|
||||||
|
```
|
||||||
|
|
||||||
|
After editing portal files, restart:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl --user restart xdg-desktop-portal.service xdg-desktop-portal-gtk.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Package updates may overwrite `/usr/share/xdg-desktop-portal/portals/cosmic.portal`.
|
||||||
|
If `Save As` starts needing two attempts again, re-check that `FileChooser` has
|
||||||
|
not been reintroduced in `cosmic.portal`.
|
||||||
|
|
@ -132,9 +132,10 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
|
||||||
match mime.as_str() {
|
match mime.as_str() {
|
||||||
"text/uri-list" => {
|
"text/uri-list" => {
|
||||||
let text = str::from_utf8(&data)?;
|
let text = str::from_utf8(&data)?;
|
||||||
let lines = text.lines();
|
for line in text.lines().filter(|line| {
|
||||||
|
let line = line.trim();
|
||||||
for line in text.lines() {
|
!line.is_empty() && !line.starts_with('#')
|
||||||
|
}) {
|
||||||
let url = Url::parse(line)?;
|
let url = Url::parse(line)?;
|
||||||
match url.to_file_path() {
|
match url.to_file_path() {
|
||||||
Ok(path) => paths.push(path),
|
Ok(path) => paths.push(path),
|
||||||
|
|
|
||||||
72
src/tab.rs
72
src/tab.rs
|
|
@ -302,6 +302,26 @@ pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Han
|
||||||
.handle()
|
.handle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generic_file_icons(
|
||||||
|
sizes: IconSizes,
|
||||||
|
) -> (
|
||||||
|
widget::icon::Handle,
|
||||||
|
widget::icon::Handle,
|
||||||
|
widget::icon::Handle,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.grid())
|
||||||
|
.handle(),
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.list())
|
||||||
|
.handle(),
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.list_condensed())
|
||||||
|
.handle(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: replace with Path::has_trailing_sep when stable
|
//TODO: replace with Path::has_trailing_sep when stable
|
||||||
fn has_trailing_sep(path: &Path) -> bool {
|
fn has_trailing_sep(path: &Path) -> bool {
|
||||||
path.as_os_str()
|
path.as_os_str()
|
||||||
|
|
@ -665,9 +685,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
folder_icon(&path, sizes.list_condensed()),
|
folder_icon(&path, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive
|
// Keep the initial directory scan cheap. Opening files still
|
||||||
// @todo - expose this as a config option?
|
// recalculates MIME from the real path before launching apps.
|
||||||
let mime = mime_for_path(&path, None, true);
|
let mime = mime_guess::from_path(&path).first_or_octet_stream();
|
||||||
|
|
||||||
//TODO: clean this up, implement for trash
|
//TODO: clean this up, implement for trash
|
||||||
let icon_name_opt = if mime == "application/x-desktop" {
|
let icon_name_opt = if mime == "application/x-desktop" {
|
||||||
|
|
@ -684,28 +704,21 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
generic_file_icons(sizes);
|
||||||
(
|
(
|
||||||
mime.clone(),
|
mime,
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
icon_handle_grid,
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
icon_handle_list,
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
icon_handle_list_condensed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut children_opt = None;
|
let children_opt = None;
|
||||||
let mut dir_size = DirSize::NotDirectory;
|
let mut dir_size = DirSize::NotDirectory;
|
||||||
if is_dir && !remote {
|
if is_dir && !remote {
|
||||||
dir_size = DirSize::Calculating(Controller::default());
|
dir_size = DirSize::Calculating(Controller::default());
|
||||||
//TODO: calculate children in the background (and make it cancellable?)
|
|
||||||
match fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
children_opt = Some(entries.count());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read directory {}: {}", path.display(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);
|
let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);
|
||||||
|
|
@ -807,7 +820,9 @@ pub fn item_from_entry(
|
||||||
folder_icon(&path, sizes.list_condensed()),
|
folder_icon(&path, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let mime = mime_for_path(&path, Some(&metadata), remote);
|
// Keep the initial directory scan cheap. Opening files still
|
||||||
|
// recalculates MIME from the real path before launching apps.
|
||||||
|
let mime = mime_guess::from_path(&path).first_or_octet_stream();
|
||||||
//TODO: clean this up, implement for trash
|
//TODO: clean this up, implement for trash
|
||||||
let icon_name_opt = if mime == "application/x-desktop" {
|
let icon_name_opt = if mime == "application/x-desktop" {
|
||||||
is_desktop = true;
|
is_desktop = true;
|
||||||
|
|
@ -823,28 +838,21 @@ pub fn item_from_entry(
|
||||||
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
generic_file_icons(sizes);
|
||||||
(
|
(
|
||||||
mime.clone(),
|
mime,
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
icon_handle_grid,
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
icon_handle_list,
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
icon_handle_list_condensed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut children_opt = None;
|
let children_opt = None;
|
||||||
let mut dir_size = DirSize::NotDirectory;
|
let mut dir_size = DirSize::NotDirectory;
|
||||||
if metadata.is_dir() && !remote {
|
if metadata.is_dir() && !remote {
|
||||||
dir_size = DirSize::Calculating(Controller::default());
|
dir_size = DirSize::Calculating(Controller::default());
|
||||||
//TODO: calculate children in the background (and make it cancellable?)
|
|
||||||
match fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
children_opt = Some(entries.count());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read directory {}: {}", path.display(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);
|
let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);
|
||||||
|
|
|
||||||
12
src/trash.rs
12
src/trash.rs
|
|
@ -27,7 +27,7 @@ pub trait TrashExt {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
|
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex);
|
||||||
|
|
||||||
fn icon(icon_size: u16) -> widget::icon::Handle {
|
fn icon(icon_size: u16) -> widget::icon::Handle {
|
||||||
widget::icon::from_name(if Self::is_empty() {
|
widget::icon::from_name(if Self::is_empty() {
|
||||||
|
|
@ -142,4 +142,12 @@ impl TrashExt for Trash {
|
||||||
not(target_os = "android")
|
not(target_os = "android")
|
||||||
)
|
)
|
||||||
)))]
|
)))]
|
||||||
impl TrashExt for Trash {}
|
impl TrashExt for Trash {
|
||||||
|
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
|
||||||
|
log::warn!(
|
||||||
|
"searching trash not supported on this platform for pattern {:?}",
|
||||||
|
regex.as_str()
|
||||||
|
);
|
||||||
|
drop(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue