Compare commits

...

24 commits

Author SHA1 Message Date
Votre Nom
9bcfe7a1f5 Cargo.toml: patch libcosmic via local path for dev builds
Activates the [patch.'https://github.com/pop-os/libcosmic'] override
pointing at ../libcosmic, enabling local development against a
patched libcosmic checkout (e.g. to pick up
WindowControlsPosition / macOS-style window controls).

This branch is intentionally dev-local: do NOT merge upstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:13:55 +02:00
Jeremy Soller
b3af8bf211
Merge pull request #1753 from pop-os/epoch-update
Epoch 1.0.11 version update
2026-04-17 15:55:04 -06:00
Jeremy Soller
4afacccc8a Fix windows compilation issues 2026-04-17 13:31:55 -06:00
Jeremy Soller
8c57060db2 Only use uzers on unix, do not fork on mac 2026-04-17 12:54:33 -06:00
Jeremy Soller
62bfcc3550 Put libcosmic/desktop behind desktop feature 2026-04-17 12:54:33 -06:00
Jeremy Soller
9c0eb63b82 Refactor trash handling to improve portability 2026-04-17 12:54:33 -06:00
Jeremy Soller
33890633b5 Epoch 1.0.11 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-04-17 12:54:32 -06:00
Jeremy Soller
b895b07bb2
Merge pull request #1742 from pop-os/mtp
Fix unsupported errors when copying large files over MTP
2026-04-16 15:51:33 -06:00
Levi Portenier
afca6aef73
Merge pull request #1735 from hojjatabdollahi/hojjat/select-until-last
feat: select file name until the extension in rename and save dialog
2026-04-15 11:14:40 -06:00
Jeremy Soller
9a89100088
Merge pull request #1679 from weblate/weblate-pop-os-cosmic-files
i18n: translation update from Hosted Weblate
2026-04-14 09:52:34 -06:00
Michael Aaron Murphy
908f30a571
example(dialog): switch logger to fix build 2026-04-14 17:47:50 +02:00
Michael Aaron Murphy
1c4ab75814
fix: gate gio file copy fallback with gvfs feature 2026-04-14 17:15:19 +02:00
Michael Aaron Murphy
e21989aaa3
chore: use compio::fs::rename for quick renames 2026-04-14 17:04:53 +02:00
Michael Aaron Murphy
15e40461e5
fix: do not generate thumbnail if file is being written 2026-04-14 17:04:53 +02:00
Michael Aaron Murphy
0bd20e57e7
refactor: use select macro for gio copy futures 2026-04-14 17:04:53 +02:00
Michael Aaron Murphy
e35d5123f0
perf: avoid holding async mutex guards across await points
tokio recommends using a sync mutex with a notifier instead of the
async mutex where possible. Rust forbids holding a sync mutex guard
across await points so we can prevent a potential deadlock this way.

This adds a custom channel based on the tokio mpmc example for
handling gvfs events from callbacks to avoid the async mutex
requirement. Messages are held in a `VecDeque` behind a sync mutex
and the receiver will get notified via the notifier when a message
is added to the queue.

Weak references used in gio callbacks in case the sender is dropped
by the application.
2026-04-14 17:04:48 +02:00
Michael Aaron Murphy
971374f60b
perf: use atomics for controller progress and state 2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
93dd775f3c
perf: get image dimensions from background thread
This caused the tab subscription to block the tokio executor.
Instead store the image dimensions in the `Item`, which is
created on a background thread.
2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
bb15f30fe5
chore: update Cargo.lock 2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
e2bdcf8da4
fix: use gio file copy fallback to fix MTP unsupported I/O errors 2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
b299f1a172
chore: add methods to Controller to pause and unpause futures
- Use `Controller::until_paused` as a signal in a select to pause futures
- Use `Controller::until_unpaused` to block futures in a select loop
2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
c114759c9e
chore: use tracing for structured logs 2026-04-14 16:51:51 +02:00
Hojjat
fc25260a5f feat: select file name until the extension in rename and save dialog 2026-04-13 12:06:49 -06:00
Hosted Weblate
e60ae4e41f
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Asier Saratsua Garmendia <asier.sarasua@gmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Jun Hwi Ku <siguning@gmail.com>
Co-authored-by: Otto Domínguez <otto@sistemasorion.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Thomas Worofsky <thomas.worofsky@gmail.com>
Co-authored-by: Tommi Nieminen <translator@legisign.org>
Co-authored-by: Urocissa Caerulea.Tw <urocissa.tw@proton.me>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: defaultUser822 <defaultuser822@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/es_419/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files
2026-04-12 18:49:45 +02:00
42 changed files with 1871 additions and 781 deletions

426
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.0.9"
version = "1.0.11"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"
@ -14,7 +14,6 @@ icu = { version = "2.1.1", features = ["compiled_data"] }
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
dirs = "6.0.0"
env_logger = "0.11"
gio = { version = "0.21", optional = true }
glib = { version = "0.21", optional = true }
glob = "0.3"
@ -56,12 +55,16 @@ rust-embed = "8"
slotmap = "1.1.1"
recently-used-xbel = "1.2.0"
zip = "8"
uzers = "0.12.2"
md-5 = "0.10.6"
png = "0.18"
jxl-oxide = { version = "0.12.5", features = ["image"] }
num_cpus = "1.17.0"
filetime = "0.2"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
thiserror = "2.0.18"
atomic_float = "1.1.0"
num_enum = "0.7.6"
# Completion-based IO runtime to enable io_uring / IOCP file IO support.
[dependencies.compio]
@ -77,7 +80,6 @@ features = [
"about",
"advanced-shaping",
"autosize",
"desktop",
"multi-window",
"tokio",
"winit",
@ -109,7 +111,7 @@ default = [
"wgpu",
]
dbus-config = ["libcosmic/dbus-config"]
desktop = ["dep:cosmic-mime-apps", "dep:xdg"]
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
desktop-applet = []
gvfs = ["dep:gio", "dep:glib"]
io-uring = ["compio/io-uring"]
@ -127,6 +129,7 @@ debug = true
[target.'cfg(unix)'.dependencies]
fork = "0.7"
uzers = "0.12.2"
[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.18"
@ -144,13 +147,12 @@ tokio = { version = "1", features = ["rt", "macros"] }
# [patch.'https://github.com/pop-os/cosmic-text']
# cosmic-text = { path = "../cosmic-text" }
# [patch.'https://github.com/pop-os/libcosmic']
# libcosmic = { path = "../libcosmic" }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
[patch.'https://github.com/pop-os/libcosmic']
libcosmic = { path = "../libcosmic" }
cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-theme = { path = "../libcosmic/cosmic-theme" }
iced_futures = { path = "../libcosmic/iced/futures" }
iced_winit = { path = "../libcosmic/iced/winit" }
# [patch.'https://github.com/pop-os/smithay-clipboard']

View file

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

6
debian/changelog vendored
View file

@ -1,3 +1,9 @@
cosmic-files (1.0.11) noble; urgency=medium
* Epoch 1.0.11 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 14 Apr 2026 11:09:44 -0600
cosmic-files (1.0.9) noble; urgency=medium
* Epoch 1.0.9 version update

View file

@ -9,9 +9,25 @@ use cosmic_files::dialog::{
Dialog, DialogChoice, DialogChoiceOption, DialogFilter, DialogFilterPattern, DialogKind,
DialogMessage, DialogResult, DialogSettings,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let log_format = tracing_subscriber::fmt::format()
.pretty()
.without_time()
.with_line_number(true)
.with_file(true)
.with_target(false)
.with_thread_names(true);
let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
.with(log_layer)
.init();
let settings = Settings::default();
app::run::<App>(settings, ())?;

View file

@ -22,10 +22,10 @@ empty-trash-warning = سيتم حذف العناصر الموجودة في مج
## New File/Folder Dialog
create-new-file = إنشاء ملف جديد
create-new-folder = إنشاء مجلد جديد
create-new-file = أنشئ ملف جديد
create-new-folder = أنشئ مجلَّد جديد
file-name = اسم الملف
folder-name = اسم المجلد
folder-name = اسم المجلَّد
file-already-exists = يوجد ملف بهذا الاسم بالفعل
folder-already-exists = يوجد مجلد بهذا الاسم بالفعل
name-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية
@ -34,10 +34,10 @@ name-no-slashes = لا يمكن أن يحتوي الاسم على شرطات م
## Open/Save Dialog
cancel = إلغاء
open = فتح
open-file = فتح ملف
open-folder = افتح مجلد
cancel = ألغِ
open = افتح
open-file = افتح ملف
open-folder = افتح مجلَّد
open-in-new-tab = افتح في لسان جديد
open-in-new-window = افتح في نافذة جديدة
open-multiple-files = افتح عدة ملفات
@ -150,12 +150,12 @@ trashed-on = مهمل
details = التفاصيل
pause = ألبث
resume = استئناف
create-archive = إنشاء أرشيف
create-archive = أنشئ أرشيف
extract-password-required = كلمة السر مطلوبة
extract-to = استخرِج إلى...
extract-to-title = استخرِج إلى مجلّد
mount-error = تعذر الوصول إلى القرص
create = إنشاء
create = أنشئ
open-item-location = افتح مكان العنصر
set-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟
favorite-path-error-description =
@ -259,7 +259,7 @@ related-apps = تطبيقات ذات صلة
selected-items = العناصر { $items } المحدّدة
permanently-delete-question = احذف نهائيًا؟
delete = احذف
permanently-delete-warning = هل أنت متأكد من أنك تريد حذف { $target } نهائيًا؟ لا يمكن التراجع عن هذا الإجراء.
permanently-delete-warning = سيُحذف { $target } نهائيًا. لا يمكن التراجع عن هذا الإجراء.
replace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه.
original-file = الملف الأصلي
replace-with = استبدل بـ
@ -371,3 +371,4 @@ move-to = انقل إلى...
show-recents = مجلد الحديثة في الشريط الجانبي
clear-recents-history = امحُ التأريخ الحديث
copy-path = انسخ المسار
mixed = مختلط

View file

@ -176,7 +176,7 @@ set-executable-and-launch-description = Chcete povolit spouštění souboru „{
set-and-launch = Povolit a spustit
open-with = Otevřít pomocí
other = Ostatní
none = Žádný
none = Žádné
icon-size-and-spacing = Velikost a rozestupy ikon
grid-spacing = Rozestupy mřížky
deleting =
@ -417,3 +417,4 @@ move-to-title = Vyberte cíl přesunutí
show-recents = Složka „Nedávné“ v postranním panelu
copy-path = Kopírovat cestu
clear-recents-history = Vymazat historii „Nedávné“
mixed = Různé

View file

@ -330,7 +330,7 @@ light = Hell
type-to-search = Zum Suchen tippen
type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner
type-to-search-enter-path = Gibt den Pfad zu einem Verzeichnis oder einer Datei ein
type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein
# Kontextmenü
add-to-sidebar = Zur Seitenleiste hinzufügen
compress = Komprimieren...
@ -376,9 +376,9 @@ select-all = Alles auswählen
## Ansicht
zoom-in = Vergrößern
zoom-in = Hineinzoomen
default-size = Standardgröße
zoom-out = Verkleinern
zoom-out = Herauszoomen
view = Ansicht
grid-view = Rasteransicht
list-view = Listenansicht
@ -390,7 +390,7 @@ menu-about = Über COSMIC Dateien...
## Sortieren
sort = Sortieren
sort = Sortierung
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = Neueste zuerst
@ -424,3 +424,8 @@ clear-recents-history = Verlauf zuletzt verwendeter Elemente leeren
comment = Dateimanager für den COSMIC Desktop
keywords = Ordner;Manager;
move-to-button-label = Verschieben
move-to-title = Verschiebeziel auswählen
remove-from-recents = Aus den zuletzt verwendeten Elementen entfernen
move-to = Verschieben nach...
copy-path = Pfad kopieren
mixed = Gemischt

View file

@ -5,7 +5,7 @@ no-results = No se encontraron resultados
filesystem = Sistema de archivos
home = Inicio
networks = Redes
notification-in-progress = Las operaciones de archivo están en progreso.
notification-in-progress = Las operaciones de archivo están en progreso
trash = Papelera
recents = Recientes
undo = Deshacer

0
i18n/eu/cosmic_files.ftl Normal file
View file

View file

@ -5,7 +5,7 @@ no-results = Ei tuloksia
filesystem = Tiedostojärjestelmä
home = Koti
networks = Verkot
notification-in-progress = Tiedostotoimintoja käynnissä.
notification-in-progress = Tiedostotoimintoja käynnissä
trash = Roskakori
recents = Viimeaikaiset
undo = Kumoa
@ -16,7 +16,7 @@ today = Tänään
desktop-view-options = Työpöytänäkymän asetukset…
show-on-desktop = Näytä työpöydällä
desktop-folder-content = Työpöytäkansion sisältö
mounted-drives = Tiedostojärjestelmään liitetyt kovalevyt
mounted-drives = Liitetyt asemat
trash-folder-icon = Roskakorikansion kuvake
icon-size-and-spacing = Kuvakkeen koko ja välistys
icon-size = Kuvakkeen koko
@ -38,7 +38,7 @@ create-archive = Luo arkisto
## Empty Trash Dialog
empty-trash = Tyhjennä roskakori
empty-trash-warning = Haluatko varmasti tyhjentää koko roskakorin pysyvästi?
empty-trash-warning = Roskakorikansion kohteet poistetaan pysyvästi
## Mount Error Dialog
@ -73,8 +73,8 @@ save-file = Tallenna tiedosto
## Open With Dialog
open-with-title = Kuinka haluat avata kohteen "{ $name }"?
browse-store = Selaa { $store }
open-with-title = Miten haluat avata kohteen "{ $name }"?
browse-store = Selaa { $store }a
## Rename Dialog
@ -88,15 +88,15 @@ replace-title = "{ $filename }" on jo olemassa tässä sijainnissa
replace-warning = Haluatko korvata sen tallentamallasi kohteella? Korvaaminen ylikirjoittaa kohteen sisällön.
replace-warning-operation = Haluatko korvata sen? Korvaaminen ylikirjoittaa sen sisällön.
original-file = Alkuperäinen tiedosto
replace-with = Korvaa kohteella
apply-to-all = Sovella kaikkiin
replace-with = Korvaa käyttäen
apply-to-all = Toteuta kaikkiin
keep-both = Pidä molemmat
skip = Ohita
## Set as Executable and Launch Dialog
set-executable-and-launch = Aseta käynnistettäväksi ja käynnistä
set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettväksi ja käynnistää sen?
set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettäväksi ja käynnistää sen?
set-and-launch = Aseta ja käynnistä
## Metadata Dialog
@ -116,9 +116,9 @@ other = Muut
add-network-drive = Lisää verkkolevy
connect = Yhdistä
connect-anonymously = Yhdistä nimettömästi
connecting = Yhdistää…
connecting = Yhdistetään…
domain = Verkkotunnus
enter-server-address = Syötä palvelimen osoite
enter-server-address = Kirjoita palvelimen osoite
network-drive-description =
Palvelinosoitteet sisältävät protokollaetuliitteen sekä osoitteen.
Esimerkkejä: ssh://192.168.0.1, ftp://[2001:db8::1]
@ -126,91 +126,91 @@ network-drive-description =
### Make sure to keep the comma which separates the columns
network-drive-schemes =
Saatavissa olevat protokollat,Etuliite
Saatavilla olevat yhteyskäytännöt,Etuliite
AppleTalk,afp://
File Transfer Protocol,ftp:// or ftps://
File Transfer Protocol,ftp:// tai ftps://
Network File System,nfs://
Server Message Block,smb://
SSH File Transfer Protocol,sftp:// or ssh://
WebDav,dav:// or davs://
network-drive-error = Verkkolevy saavuttamattomissa
SSH File Transfer Protocol,sftp:// tai ssh://
WebDav,dav:// tai davs://
network-drive-error = Verkkolevy ei saatavilla
password = Salasana
remember-password = Muista salasana
try-again = Yritä uudelleen
username = Käyttäjänimi
username = Käyttäjätunnus
## Operations
edit-history = Muokkaa historiaa
history = Historia
no-history = Historia on tyhjä.
pending = Odottaa käsittelyä
failed = Epäonnistui
complete = Valmis
pending = Jonossa
failed = Epäonnistuneet
complete = Valmiit
compressing =
Tiivistetään { $items } { $items ->
Pakataan { $items } { $items ->
[one] kohde
*[other] kohteita
} lähteestä "{ $from }" arkistoon "{ $to }"
*[other] kohdetta
} sijainnista "{ $from }" arkistoon "{ $to }" ({ $progress })…
compressed =
Tiivistetty { $items } { $items ->
Pakattu { $items } { $items ->
[one] kohde
*[other] kohteet
} lähteestä "{ $from }" arkistoon "{ $to }"
*[other] kohdetta
} sijainnista "{ $from }" arkistoon "{ $to }"
copy_noun = Kopio
creating = Luodaan kohdetta "{ $name }" kohteen "{ $parent }" alle
created = Luotu kohde "{ $name }" kohteen "{ $parent }" alle
creating = Luodaan "{ $name }" kohteen "{ $parent }" alle
created = Luotu "{ $name }" kohteen "{ $parent }" alle
copying =
Kopioidaan { $items } { $items ->
[one] kohde
*[other] kohteita
} lähteestä "{ $from }" kohteeseen "{ $to }"
*[other] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })…
copied =
Kopioitu { $items } { $items ->
[one] kohde
*[other] kohteet
} lähteestä "{ $from }" kohteeseen "{ $to }"
emptying-trash = Tyhjennetään { trash }
*[other] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }"
emptying-trash = Tyhjennetään { trash } ({ $progress })…
emptied-trash = Tyhjennetty { trash }
extracting =
Puretaan { $items } { $items ->
[one] kohde
*[other] kohteet
} arkistosta "{ $from }" kohteeseen "{ $to }"
*[other] kohdetta
} arkistosta "{ $from }" kohteeseen "{ $to }" ({ $progress })…
extracted =
Purettu { $items } { $items ->
[one] kohde
*[other] kohteet
*[other] kohdetta
} arkistosta "{ $from }" kohteeseen "{ $to }"
setting-executable-and-launching = Asetetaan "{ $name }" käynnistettäväksi ja käynnistetään
set-executable-and-launched = Asetettu "{ $name }" käynnistettäväksi ja käynnistetty
moving =
Siirretään { $items } { $items ->
[one] kohde
*[other] kohteet
} lähteestä "{ $from }" kohteeseen "{ $to }"
*[other] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })…
moved =
Siirretty { $items } { $items ->
[one] kohde
*[other] kohteet
} lähteestä "{ $from }" kohteeseen "{ $to }"
*[other] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }"
renaming = Nimetään kohde "{ $from }" muotoon "{ $to }"
renamed = Nimetty kohde "{ $from }" muotoon "{ $to }"
restoring =
Palautetaan { $items } { $items ->
[one] kohde
*[other] kohteet
} { trash }sta
*[other] kohdetta
} roskakorista ({ $progress })…
restored =
Palautettu { $items } { $items ->
[one] kohde
*[other] kohteet
} { trash }sta
unknown-folder = Tuntematon kansio
*[other] kohdetta
} roskakorista
unknown-folder = tuntematon kansio
## Open with
menu-open-with = Avaa ohjelmalla…
menu-open-with = Avaa sovelluksella…
default-app = { $name } (oletus)
## Show details
@ -225,14 +225,14 @@ settings = Asetukset
appearance = Ulkoasu
theme = Teema
match-desktop = Sovita yhteen työpöydän kanssa
match-desktop = Sovita työpöytään
dark = Tumma
light = Vaalea
# Context menu
add-to-sidebar = Lisää sivupalkkiin
compress = Pakkaa
compress = Pakkaa…
extract-here = Pura
new-file = Uusi tiedosto…
new-folder = Uusi kansio…
@ -241,9 +241,9 @@ move-to-trash = Siirrä roskakoriin
restore-from-trash = Palauta roskakorista
remove-from-sidebar = Poista sivupalkista
sort-by-name = Järjestä nimen mukaan
sort-by-modified = Järjestä muokkauspäivämäärän mukaan
sort-by-modified = Järjestä muokkausajan mukaan
sort-by-size = Järjestä koon mukaan
sort-by-trashed = Järjestä poistamispäivämäärän mukaan
sort-by-trashed = Järjestä poistamisajan mukaan
## Desktop
@ -281,9 +281,9 @@ grid-view = Ruudukkonäkymä
list-view = Listanäkymä
show-hidden-files = Näytä piilotetut tiedostot
list-directories-first = Näytä kansiot ensin
gallery-preview = Gallerian esinäkymä
gallery-preview = Gallerian esikatselu
menu-settings = Asetukset…
menu-about = Tietoa COSMIC Tiedostoista…
menu-about = Tietoa COSMICin tiedostonhallinnasta…
## Sort
@ -318,3 +318,90 @@ move-to-button-label = Siirrä
clear-recents-history = Tyhjennä viimeaikaisten historia
copy-path = Kopioi polku
dismiss = Hylkää viesti
operations-running =
{ $running } { $running ->
[one] toiminto
*[other] toimintoa
} käynnissä ({ $percent } %)...
operations-running-finished =
{ $running } { $running ->
[one] toiminto
*[other] toimintoa
} käynnissä ({ $percent } %), { $finished } valmistunut…
pause = Keskeytä
extract-to = Pura sijaintiin…
permanently-delete-warning = { $target } tullaan poistamaan pysyvästi. Tätä toimintoa ei voi perua.
execute-only = Vain suoritus
write-only = Vain kirjoitus
write-execute = Kirjoita ja suorita
read-only = Vain luku
read-execute = Lue ja suorita
read-write = Lue ja kirjoita
read-write-execute = Lue, kirjoita sekä suorita
calculating = Lasketaan…
single-click = Yhden napsautuksen avaus
type-to-search = Kirjoita etsiäksesi
type-to-search-recursive = Etsii nykyisestä kansiosta ja kaikista alikansioista
remove-from-recents = Poista viimeaikaisista
selected-items = { $items } valittua kohdetta
show-recents = Viimeaikaisten kansio sivupalkissa
copy-to = Kopioi…
move-to = Siirrä…
details = Yksityiskohdat
grid-spacing = Ruudukkovälit
none = Ei mitään
favorite-path-error = Virhe avattaessa kansiota
favorite-path-error-description =
Polun { $path } avaaminen ei onnistunut
"{ $path }" ei välttämättä ole olemassa tai oikeutesi eivät riitä sen avaamiseen
Haluatko poistaa sen sivupalkista?
keep = Pidä
repository = Tietovarasto
support = Tuki
progress = { $percent } %
progress-cancelled = { $percent } %, peruttu
progress-failed = { $percent } %, epäonnistui
progress-paused = { $percent } %, keskeytetty
setting-permissions = Asetetaan kohteen "{ $name }" käyttöoikeudeksi { $mode }
set-permissions = Asetettu kohteen { $name } käyttöoikeudeksi { $mode }
permanently-deleting =
Poistetaan pysyvästi { $items } { $items ->
[one] kohde
*[other] kohdetta
}
permanently-deleted =
Poistettu pysyvästi { $items } { $items ->
[one] kohde
*[other] kohdetta
}
items = Kohteita: { $items }
item-accessed = Käytetty: { $accessed }
type-to-search-enter-path = Kirjoittaa polun kansioon tai tiedostoon
eject = Poista asemasta
copy-to-title = Valitse mihin kopioidaan
move-to-title = Valitse mihin siirretään
pasted-image = Liitetty kuva
pasted-text = Liitetty teksti
pasted-video = Liitetty video
type-to-search-select = Valitsee ensimmäisen täsmäävän tiedoston tai kansion
deleting =
Poistetaan { $items } { $items ->
[one] kohde
*[other] kohdetta
} roskakorista ({ $progress })…
deleted =
Poistettu { $items } { $items ->
[one] kohde
*[other] kohdetta
} roskakorista
removing-from-recents =
Poistetaan { $items } { $items ->
[one] kohde
*[other] kohdetta
} viimeaikaisista
removed-from-recents =
Poistettu { $items } { $items ->
[one] kohde
*[other] kohdetta
} viimeaikaisista

View file

@ -436,3 +436,4 @@ keywords = Dossier;Gestionnaire;
show-recents = Dossier Récents dans la barre latérale
copy-path = Copier le chemin
clear-recents-history = Effacer l'historique des Récents
mixed = Mixte

View file

@ -433,3 +433,4 @@ keywords = Fillteán;Bainisteoir;
show-recents = Fillteán le déanaí sa bharra taoibh
clear-recents-history = Glan stair na n-earraí le déanaí
copy-path = Cóipeáil an chosán
mixed = Measctha

View file

@ -436,3 +436,4 @@ move-to = Áthelyezés ide…
show-recents = Legutóbbiak mappa megjelenítése az oldalsávban
copy-path = Útvonal másolása
clear-recents-history = Legutóbbiak előzményének törlése
mixed = Vegyes

View file

@ -318,3 +318,4 @@ comment = Pengelola berkas untuk desktop COSMIC
show-recents = Map terbaru di bilah sisi
clear-recents-history = Bersihkan riwayat Terbaru
copy-path = Salin jalur
mixed = Bercampur

View file

@ -1 +1,322 @@
change-wallpaper = Beddel aɣrab n ugdil…
cosmic-files = Ifuyla COSMIC
empty-folder = Akaram d ilem
empty-folder-hidden = Akaram d ilem (yesɛa iferdisen yeffren)
no-results = Ulac igmaḍ yettwafen
home = Agejdan
networks = Iẓeḍwa
notification-in-progress = Timhalin ɣef ifuyla la tteddunt
trash = Iḍumman
recents = Melmi kan
undo = Ssemmet
today = Ass-a
desktop-view-options = Iɣewwaṛen n tmeẓri n tnarit…
show-on-desktop = Sken deg tnarit
desktop-folder-content = Agbur n ukaram n tnarit
trash-folder-icon = Tignit n ukaram n iḍumman
icon-size-and-spacing = Tiddi n tignit akked tallunt
icon-size = Tiddi n tignit
name = Isem
modified = Ittusnifel
trashed-on = Yettwakkes ɣer tqecwalt n yiḍumman
size = Tiddi
details = Talqayt
pause = Serǧu
resume = Kemmel
create-archive = Snulfu-d aɣbaṛ
extract-to = Ssef ɣer...
extract-to-title = Ssef ɣer ukaram
empty-trash = Silem iḍumman
rename-folder = Snifel isem n ukaram
filesystem = Anagraw n yifuyla
dismiss = Zgel izen
empty-trash-title = Silem iḍumman?
empty-trash-warning = Iferdisen n ukaram n iḍumman ad ttwakksen i lebda
create-new-file = Snulfu-d afaylu amaynut
create-new-folder = Snulfu-d akaram amaynut
file-name = Isem n ufaylu
folder-name = Isem n ukaram
file-already-exists = Afaylu s yisem-agi yella yakan
folder-already-exists = Akaram s yisem-agi yella yakan
name-hidden = Ismawen ibeddun s "." ad ttwaffren
name-invalid = Isem ur yezmir ara ad yili "{ $filename }"
cancel = Sefsex
create = Snulfu-d
open = Ldi
open-file = Ldi afaylu
open-folder = Ldi akaram
open-in-new-tab = Ldi deg yiccer amaynut
open-in-new-window = Ldi deg usfaylu amaynut
open-item-location = Ldi adig n uferdis
open-multiple-files = Ldi aget n ifuyla
mounted-drives = imeɣriyen yettuserkben
mount-error = Ulamek anekcum ɣer umeɣri
operations-running =
{ $running } { $running ->
[one] n temhelt la tteddu
*[other] n temhal la tteddunt
} ({ $percent }%)...
operations-running-finished =
{ $running } { $running ->
[one] n temhelt la tteddu
*[other] n temhal la tteddunt
} ({ $percent }%), { $finished } { $finished ->
[one] tfukk
*[other] fukkent
}...
copy-to-title = Fren taɣerwaḍt n unɣel
copy-to-button-label = Nɣel
move-to-title = Fren taɣerwaḍt n usmutti
move-to-button-label = Smutti
comment = Amsefrak n yifuyla i tnarit COSMIC
keywords = Akaram;Amsefrak;
delete = kkes
replace = Semselsi
support = Tallalt
settings = Iɣewwaṛen
appearance = Timeẓri
theme = Asentel
dark = Aɣmayan
light = Aceɛlal
paste = Senteḍ
select-all = Fren akk
zoom-in = Asemɣeṛ
default-size = Tiddi tamezwert
zoom-out = Asemẓi
view = Wali
menu-settings = Iɣewwaṛen…
save = Sekles
match-desktop = Amṣada d tnarit
file = Afaylu
new-window = Asfaylu amaynut
quit = Tuffɣa
edit = Ẓreg
cut = Gzem
copy = Nɣel
repository = Asarsay
pasted-image = Tettwasenṭeḍ tugna
pasted-text = Yettwasenṭeḍ uḍris
pasted-video = Tettwasenṭeḍ tvidyut
compress = Skussem…
grid-spacing = Tallunt n iẓiki
extract-password-required = Awal uffir yettwasra
permanently-delete-question = Kkes s wudem imezgi?
permanently-delete-warning = { $target } ad yettwakkes s wudem imezgi. Tigawt-agi ur tezmir ara ad tettwasefsex.
rename-file = Snifel isem n ufaylu
replace-title = "{ $filename }" yella yakan deg wadig-a
replace-warning = Tebɣiḍ ad t-tsemselsiḍ s win ara teskelseḍ? Asemselsi-ines ad yaru sennig ugbur-is.
replace-warning-operation = Tebɣiḍ ad t-tsemselsiḍ? Asemselsi-ines ad yaru sennig ugbur-is.
original-file = Afaylu aneṣli
replace-with = Semselsi s
apply-to-all = Snes i meṛṛa
keep-both = Eǧǧ-iten deg sin
skip = Zgel
set-executable-and-launch = Sbeddet am umselkam syinna senker
set-executable-and-launch-description = Tebɣiḍ ad tesbeddeḍ "{ $name }" am umselkam syinna ad tessenkreḍ?
set-and-launch = Sbadu sakin senker
add-network-drive = Rnu ameɣri n uẓeṭṭa
connect = Qqen
connect-anonymously = Qqen s wudem udrig
connecting = Tuqqna…
domain = Taɣult
enter-server-address = Sekcem tansa n uqeddac
network-drive-description =
Tansiwin n uqeddac gebrent azwir n uneggaf akked tansa.
Imedyaten: ssh://192.168.0.1, ftp://[2001:db8::1]
network-drive-error = Ur izmir ara ad yekcem ɣer umeɣri n uzeṭṭa
password = Awal uffir
remember-password = Cfu ɣef wawal uffir
try-again = ɛreḍ tikelt nniḍen
username = Isem n useqdac
cancelled = Yettwasefsex
edit-history = Ẓreg amazray
history = Amazray
no-history = Ulac iferdisen deg umazray.
pending = Yettṛaǧu
progress-cancelled = { $percent }%, yettwasefsex
progress-failed = { $percent }%, ur yeddi ara
failed = Ur yeddi ara
renaming = Asnifel n yisem "{ $from }" ɣer "{ $to }"
renamed = Yettwasenfel yisem n "{ $from }" ɣer "{ $to }"
unknown-folder = akaram arussin
menu-open-with = Ldi s…
default-app = { $name } (amezwer)
show-details = Sken talqayt
type = Anaw: { $mime }
items = Iferdisen: { $items }
item-size = Tiddi: { $size }
item-created = Yettwarna: { $created }
item-modified = Ittusnifel: { $modified }
item-accessed = Yettwakcem: { $accessed }
calculating = Asiḍen…
single-click = Asiti asuf i ulday
type-to-search = Aru iwakken ad tnadiḍ
type-to-search-recursive = Nadi akaram amiran akked ikaramen inaddawen meṛṛa
type-to-search-enter-path = Sekcem abrid ɣer ukaram neɣ afaylu
add-to-sidebar = Rnu ɣer ufeggag adisan
delete-permanently = Kkes i lebda
grid-view = Askan s iẓiki
list-view = Askan s tebdart
show-hidden-files = Sken ifuyla uffiren
gallery-preview = Taskant n temidelt
menu-about = Ɣef Ifuyla COSMIC…
sort = Asmizzwer
sort-newest-first = Amaynut d amezwaru
sort-oldest-first = Aqbur d amezwaru
sort-smallest-to-largest = Seg umeẓyan akk ɣer umeqqran
sort-largest-to-smallest = Seg umeqqran akk ɣer umeẓyan
open-multiple-folders = Ldi usgit n ikaramen
save-file = Sekles afaylu
open-with-title = Amek tebɣiḍ ad teldiḍ "{ $name }"?
browse-store = Snirem { $store }
other-apps = Isnasen-nniḍen
emptying-trash = Silem { trash } ({ $progress })…
emptied-trash = D ilem { trash }
set-permissions = Sbadu isirigen i "{ $name }" ɣer { $mode }
eject = Ḍeqqer
extract-here = Ssef
new-file = Afaylu amaynut…
new-folder = Akaram amaynut…
open-in-terminal = Ldi deg yixef
move-to-trash = Smutti ɣer tqecwalt n yiḍumman
restore-from-trash = Err-d seg tqecwalt n yiḍumman
remove-from-sidebar = Kkes seg ugalis adisan
sort-by-name = Smizzwer s yisem
sort-by-modified = Asmizzwer s usnifel
sort-by-size = Asmizzwer s tiddi
sort-by-trashed = Asmizzwer s wakud n tukksa
remove-from-recents = Kkes seg ineggura
desktop-appearance = Timeẓri n tnarit…
display-settings = Iɣewwaṛen n ubeqqeḍ...
new-tab = Iccer amaynut
reload-folder = Ales asali n ukaram
rename = Snifel isem...
close-tab = Mdel iccer
list-directories-first = Sken di tazwara ikaramen
open-with = Ldi s
owner = Bab
group = Agraw
other = Ayen nniḍen
none = Ula Yiwen
execute-only = Selkem kan
write-only = Aru kan
write-execute = Aru u selkem
read-only = I tɣuri kan
read-execute = Ɣeṛ u selkem
read-write = Ɣeṛ u aru
read-write-execute = Ɣeṛ, aru, u selkem
favorite-path-error = Tuccḍa deg ulday n ukaram
remove = Kkes
keep = Eǧǧ
progress = { $percent }%
progress-paused = { $percent }%, ibedd
complete = Immed
copy_noun = Nɣel
creating = Asnulfu n "{ $name }" deg "{ $parent }"
created = Yettwarna "{ $name }" deg "{ $parent }"
setting-executable-and-launching = Asbadu n "{ $name }" am umselkam syin ad yettwasekker
set-executable-and-launched = Sbadu "{ $name }" am umselkam syin sekker-it
setting-permissions = Asbadu n tsirag i "{ $name }" ɣer { $mode }
related-apps = Isnasen icudden
favorite-path-error-description =
Ur izmir ara ad yeldi "{ $path }"
"{ $path }" yezmer lḥal ulac-it neɣ ahat ur tesɛiḍ ara tisirag akken ad t-teldiḍ
Tebɣiḍ ad t-tekkseḍ seg ufeggag adisan?
compressing =
La issekkussum { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
compressed =
Yekussem { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }"
copying =
Anɣal { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
copied =
Yettwanɣel { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }"
deleting =
Tukksa { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg { trash } ({ $progress })...
deleted =
Yettwakkes { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg { trash }
extracting =
Tussfa { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
extracted =
Yettusef { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }"
moving =
Asmutti { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
moved =
Yettwasenkez { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg "{ $from }" ɣer "{ $to }"
permanently-deleting =
Tukksa i lebda { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
}
permanently-deleted =
I lebda yettwakkes { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
}
removing-from-recents =
Tukksa { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg { recents }
removed-from-recents =
Yettwakkes { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg { recents }
restoring =
Tiririt { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg { trash } ({ $progress })…
restored =
Yettwarr-d { $items } { $items ->
[one] n uferdis
*[other] n yiferdisen
} seg { trash }
network-drive-schemes =
Ineggafen iwejden, azwir
AppleTalk,afp://
Aneggaf n usiweḍ n yifuyla,ftp:// neɣ ftps://
Anagraw n yifuyla n uzeṭṭa,nfs://
Iḥder n yizen n uqeddac,smb://
SSH Aneggaf n usiweḍ n yifuyla,sftp:// neɣ ssh://
WebDAV:// neɣ davs://
sort-a-z = A-Ẓ
sort-z-a = Ẓ-A
selected-items = { $items } n yiferdisen yettwafernen
type-to-search-select = Fren afaylu amezwaru neɣ akaram yemṣadan
copy-to = Nɣel ɣer...
move-to = Smutti ɣer…
show-recents = Akaram n melmi kan deg ufeggag adisan
clear-recents-history = Sfeḍ azray n melmi kan
copy-path = Nɣel abrid

View file

@ -256,7 +256,7 @@ type-to-search-recursive = Ағымдағы бума мен барлық ішк
type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді
type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды
add-to-sidebar = Бүйірлік панельге қосу
compress = Сығу
compress = Сығу...
delete-permanently = Біржолата өшіру
eject = Шығару
extract-here = Тарқату
@ -318,3 +318,4 @@ keywords = Folder;Manager;Бума;Басқарушы;
show-recents = Бүйір панеліндегі «Жуырдағы құжаттар» бумасы
clear-recents-history = Жуырдағылар тарихын өшіру
copy-path = Орналасқан жолын көшіру
mixed = Аралас

View file

@ -110,7 +110,7 @@ dismiss = 메시지 무시
copy_noun = 복사
progress = { $percent }%
related-apps = 관련 앱
compress = 압축
compress = 압축...
network-drive-error = 네트워크 드라이브에 접근할 수 없음
icon-size-and-spacing = 아이콘 크기 및 간격
password = 암호
@ -272,3 +272,8 @@ network-drive-schemes =
WebDAV,dav:// 또는 davs://
type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다
comment = COSMIC 데스크톱용 위한 파일 관리자
keywords = 폴더;관리자;
copy-to-button-label = 복사
move-to-button-label = 이동
clear-recents-history = 최근 기록 비우기
copy-path = 복사 경로

View file

@ -252,7 +252,7 @@ type-to-search = Norint ieškoti, pradėkite rašyti
type-to-search-recursive = Paieška dabartiniame aplanke ir jo poaplankiuose
type-to-search-enter-path = Įvedamas aplanko ar failo kelias
add-to-sidebar = Pridėti į šonjuostę
compress = Suspausti
compress = Suspausti...
delete-permanently = Ištrinti visam laikui
eject = Išstumti
extract-here = Išskleisti

View file

@ -32,7 +32,7 @@ pause = ਵਿਰਾਮ
resume = ਮੁੜ-ਚਾਲੂ
create-archive = ਅਕਾਇਵ ਬਣਾਓ
extract-password-required = ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ
extract-to = Extract To...
extract-to = ਖਿਲਾਰੋ...
extract-to-title = ਫੋਲਡਰ ਵਿੱਚ ਖਿਲਾਰੋ
empty-trash = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰੋ
empty-trash-title = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰਨਾ ਹੈ?
@ -113,7 +113,7 @@ replace-title = "{ $filename }" ਪਹਿਲਾਂ ਹੀ ਇਸ ਟਿਕਾਣ
favorite-path-error = ਡਾਇਰੈਕਟਰੀ ਖੋਲ੍ਹਣ ਦੌਰਾਨ ਗਲਤੀ
add-network-drive = ਨੈੱਟਵਰਕ ਡਰਾਇਵ ਜੋੜੋ
connect-anonymously = ਅਣਪਛਾਤੇ ਵਜੋਂ ਕਨੈਕਟ ਕਰੋ
connecting = Connecting...
connecting = ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...
domain = ਡੋਮੇਨ
enter-server-address = ਸਰਵਰ ਦਾ ਸਿਰਨਾਵਾਂ ਦਿਓ
password = ਪਾਸਵਰਡ
@ -138,9 +138,9 @@ sort-by-modified = ਸੋਧ ਨਾਲ ਲੜੀਬੱਧ
sort-by-size = ਆਕਾਰ ਨਾਲ ਲੜੀਬੱਧ
sort-by-trashed = ਹਟਾਉਣ ਸਮੇਂ ਨਾਲ ਲੜੀਬੱਧ
remove-from-recents = ਸੱਜਰਿਆਂ ਵਿੱਚੋਂ ਹਟਾਓ
change-wallpaper = Change wallpaper...
desktop-appearance = Desktop appearance...
display-settings = Display settings...
change-wallpaper = ਵਾਲਪੇਪਰ ਨੂੰ ਬਦਲੋ...
desktop-appearance = ਡੈਸਕਟਾਪ ਦੀ ਦਿੱਖ...
display-settings = ਡਿਸਪਲੇਅ ਸੈਟਿੰਗਾਂ...
file = ਫ਼ਾਇਲ
new-tab = ਨਵੀਂ ਟੈਬ
new-window = ਨਵੀਂ ਵਿੰਡੋ
@ -195,11 +195,11 @@ type-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ
type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ
add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ
compress = ਕੰਪਰੈਸ
copy-to = Copy to...
copy-to = ਇੱਥੇ ਕਾਪੀ ਕਰੋ...
delete-permanently = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਓ
eject = ਬਾਹਰ
extract-here = ਖਿਲਾਰੋ
move-to = Move to...
move-to = ਇੱਥੇ ਭੇਜੋ...
remove-from-sidebar = ਬਾਹੀ ਵਿੱਚੋਂ ਹਟਾਓ
reload-folder = ਫੋਲਡਰ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰੋ
list-view = ਸੂਚੀ ਝਲਕ

View file

@ -437,3 +437,4 @@ move-to = Przenieś do…
show-recents = Ostatnie katalogi w panelu bocznym
clear-recents-history = Wyczyść bierzącą historię
copy-path = Skopiuj ścieżkę
mixed = Mieszane

View file

@ -424,8 +424,8 @@ sort-largest-to-smallest = Do maior para o menor
empty-trash-title = Esvaziar a lixeira?
type-to-search-select = Seleciona o primeiro arquivo ou pasta correspondente
pasted-image = Imagem colada
pasted-text = Texto copiado
pasted-video = Vídeo copiado
pasted-text = Texto colado
pasted-video = Vídeo colado
copy-to-title = Selecione o destino da cópia
copy-to-button-label = Copiar
move-to-title = Selecione o destino da movimentação
@ -433,6 +433,7 @@ move-to-button-label = Mover
copy-to = Copiar para...
move-to = Mover para...
keywords = Pasta;Gerenciador;
show-recents = Pasta Recentes na barra lateral
show-recents = Pasta de recentes na barra lateral
clear-recents-history = Limpar histórico de recentes
copy-path = Copiar caminho
mixed = Misto

View file

@ -195,7 +195,7 @@ dark = Тёмная
light = Светлая
# Context menu
add-to-sidebar = Добавить на боковую панель
compress = Сжать
compress = Сжать...
extract-here = Распаковать
new-file = Новый файл…
new-folder = Новая папка…
@ -380,3 +380,4 @@ keywords = Папка;Менеджер;
show-recents = «Недавние документы» в бок. панели
clear-recents-history = Очистить историю недавних
copy-path = Копировать путь
mixed = Смешанные

View file

@ -317,7 +317,7 @@ single-click = Відкривати одним клацанням
type-to-search = Введіть для пошуку
type-to-search-recursive = Шукає у поточній теці та всіх підтеках
type-to-search-enter-path = Вводить шлях до каталогу або файлу
compress = Стиснути
compress = Стиснути...
delete-permanently = Остаточно видалити
eject = Безпечно вилучити
extract-here = Видобути

View file

@ -11,7 +11,7 @@ recents = 最近访问
undo = 撤销
today = 今天
# Desktop view options
desktop-view-options = 桌面视图选项...
desktop-view-options = 桌面视图选项…
show-on-desktop = 在桌面显示
desktop-folder-content = 桌面文件夹内容
mounted-drives = 已装载驱动器
@ -31,7 +31,7 @@ operations-running =
正在进行 { $running } { $running ->
[one] 个操作
*[other] 个操作
}({ $percent }%)...
}({ $percent }%)…
operations-running-finished =
正在进行 { $running } { $running ->
[one] 个操作
@ -50,7 +50,7 @@ create-archive = 创建压缩包
## Extract Dialog
extract-password-required = 需要密码
extract-to = 提取到...
extract-to = 提取到…
extract-to-title = 提取到文件夹
## Empty Trash Dialog
@ -317,7 +317,7 @@ unknown-folder = 未知文件夹
## Open with
menu-open-with = 打开方式...
menu-open-with = 打开方式…
default-app = { $name }(默认)
## Show details
@ -329,7 +329,7 @@ item-size = 文件大小:{ $size }
item-created = 创建于:{ $created }
item-modified = 修改于:{ $modified }
item-accessed = 访问于:{ $accessed }
calculating = 计算中...
calculating = 计算中…
## Settings
@ -355,8 +355,8 @@ compress = 压缩…
delete-permanently = 永久删除
eject = 弹出
extract-here = 解压到此处
new-file = 新建文件...
new-folder = 新建文件夹...
new-file = 新建文件…
new-folder = 新建文件夹…
open-in-terminal = 在终端模拟器中打开
move-to-trash = 移动到回收站
restore-from-trash = 从回收站中还原
@ -369,9 +369,9 @@ remove-from-recents = 从最近访问中移除
## Desktop
change-wallpaper = 更改壁纸...
desktop-appearance = 桌面外观...
display-settings = 显示设置...
change-wallpaper = 更改壁纸…
desktop-appearance = 桌面外观…
display-settings = 显示设置…
# Menu
@ -382,7 +382,7 @@ file = 文件
new-tab = 新建标签
new-window = 新建窗口
reload-folder = 刷新文件夹
rename = 重命名...
rename = 重命名…
close-tab = 关闭标签
quit = 退出
@ -405,7 +405,7 @@ list-view = 列表视图
show-hidden-files = 显示隐藏文件
list-directories-first = 优先列出目录
gallery-preview = 图库预览
menu-settings = 设置...
menu-settings = 设置…
menu-about = 关于 COSMIC 文件…
## Sort
@ -435,4 +435,5 @@ comment = COSMIC 桌面的文件管理器
keywords = 文件夹;管理器;
clear-recents-history = 清除最近访问历史
copy-path = 复制文件路径
show-recents = 侧边栏显示最近访问
show-recents = 侧边栏中的最近访问
mixed = 混合

View file

@ -1,11 +1,11 @@
cosmic-files = COSMIC 檔案總管
cosmic-files = COSMIC 檔案
empty-folder = 空資料夾
empty-folder-hidden = 空資料夾(包含隱藏項目)
no-results = 找不到結果
filesystem = 檔案系統
home = 主目錄
home = 家目錄
networks = 網路
notification-in-progress = 檔案操作正在進行中。
notification-in-progress = 檔案操作正在進行中
trash = 垃圾桶
recents = 最近使用
undo = 復原
@ -25,7 +25,7 @@ create-archive = 建立壓縮檔案
## Empty Trash Dialog
empty-trash = 清空垃圾桶
empty-trash-warning = 你確定要永久刪除垃圾桶中的所有項目嗎?
empty-trash-warning = 垃圾桶中的項目將被永久刪除
## New File/Folder Dialog
@ -33,16 +33,16 @@ create-new-file = 建立新檔案
create-new-folder = 建立新資料夾
file-name = 檔案名稱
folder-name = 資料夾名稱
file-already-exists = 已存在同名檔案。
folder-already-exists = 已存在同名資料夾。
name-hidden = 以「.」開頭的名稱將會被隱藏。
name-invalid = 名稱不能是 「{ $filename }」。
name-no-slashes = 名稱不能包含斜線。
file-already-exists = 相同名稱的檔案已經存在
folder-already-exists = 相同名稱的資料夾已經存在
name-hidden = 以「.」開頭的名稱將會被隱藏
name-invalid = 名稱不能是「{ $filename }」
name-no-slashes = 名稱不能包含斜線
## Open/Save Dialog
cancel = 取消
create = 建立
create = 創作
open = 開啟
open-file = 開啟檔案
open-folder = 開啟資料夾
@ -62,12 +62,12 @@ rename-folder = 重新命名資料夾
## Replace Dialog
replace = 取代
replace-title = 檔案「{ $filename }」已存在於此位置。
replace-title = 「{ $filename }」已存在於此位置
replace-warning = 你要取代它嗎?取代將覆蓋其內容。
replace-warning-operation = 你要取代它嗎?取代將覆蓋其內容。
original-file = 原始檔案
replace-with = 取代為
apply-to-all = 套用至所有項目
apply-to-all = 套用至全部
keep-both = 保留兩者
skip = 跳過
@ -95,13 +95,13 @@ network-drive-description =
伺服器地址包括協定前綴和地址。
範例:ssh://192.168.0.1, ftp://[2001:db8::1]
network-drive-schemes =
可用協定,前綴
AppleTalk,afp://
檔案傳輸協定,ftp:// 或 ftps://
網路檔案系統,nfs://
伺服器訊息區塊,smb://
SSH 檔案傳輸協定,sftp:// 或 ssh://
WebDav,dav:// 或 davs://
可用協定,前綴
AppleTalk,afp://
檔案傳輸協定,ftp:// 或 ftps://
網路檔案系統,nfs://
伺服器訊息區塊,smb://
SSH 檔案傳輸協定,sftp:// 或 ssh://
WebDav,dav:// 或 davs://
network-drive-error = 無法存取網路磁碟機
password = 密碼
remember-password = 記住密碼
@ -112,7 +112,7 @@ username = 使用者名稱
edit-history = 編輯歷史
history = 歷史紀錄
no-history = 沒有歷史項目。
no-history = 無歷史記錄項目。
pending = 待處理
failed = 失敗
complete = 完成
@ -133,51 +133,51 @@ copying =
正在複製 { $items } { $items ->
[one] 項目
*[other] 項目
} 從「{ $from }」到「{ $to }」({ $progress })...
}從「{ $from }」到「{ $to }」({ $progress })...
copied =
已複製 { $items } { $items ->
[one] 項目
*[other] 項目
}從「{ $from }」到「{ $to }」
emptying-trash = 正在清空{ trash }({ $progress })…
emptied-trash = 已清空{ trash }
emptying-trash = 正在清空 { trash }({ $progress })…
emptied-trash = 已經清空 { trash }
extracting =
正在解壓縮 { $items } 項目 { $items ->
[one] 項目
*[other] 項目
} 從「{ $from }」到「{ $to }」({ $progress })...
}從「{ $from }」至「{ $to }」({ $progress })...
extracted =
已解壓縮 { $items } 項目 { $items ->
[one] 項目
*[other] 項目
} 從 { $from } 到 { $to }
}從「{ $from }」到「{ $to }」
moving =
正在移動 { $items } { $items ->
[one] 項目
*[other] 項目
} 從「{ $from }」到「{ $to }」({ $progress })...
}從「{ $from }」到「{ $to }」({ $progress })...
moved =
已移動 { $items } { $items ->
已經移動 { $items } { $items ->
[one] 項目
*[other] 項目
} 從「{ $from }」到「{ $to }」
renaming = 正在重新命名「{ $from }」為「{ $to }」
renamed = 已重新命名 { $from } 為 { $to }
} 從「{ $from }」至「{ $to }」
renaming = 正在重新命名「{ $from }」至「{ $to }」
renamed = 已經重新命名「{ $from }」至「{ $to }」
restoring =
正在還原 { $items } 項目 { $items ->
[one] 項目
*[other] 項目
} 從{ trash }({ $progress })...
}自 { trash } ({ $progress })...
restored =
已還原 { $items } 項目 { $items ->
已經還原 { $items } 項目 { $items ->
[one] 項目
*[other] 項目
} 從{ trash }
unknown-folder = 未知資料夾
}從 { trash }
unknown-folder = 不明資料夾
## Open with
menu-open-with = 開啟方式...
menu-open-with = 開啟檔案...
default-app = { $name } (預設)
## Show details
@ -192,15 +192,15 @@ settings = 設定
appearance = 外觀
theme = 主題
match-desktop = 與桌面一致
dark = 暗色模式
light = 亮色模式
match-desktop = 符合桌面
dark = 深色
light = 淺色
# Context menu
add-to-sidebar = 加入側邊欄
compress = 壓縮
extract-here = 解壓縮至此
new-file = 新檔案...
new-folder = 新資料夾...
add-to-sidebar = 添加至側邊欄
compress = 壓縮…
extract-here = 解壓縮
new-file = 新建檔案...
new-folder = 新建資料夾...
open-in-terminal = 在終端機中開啟
move-to-trash = 移動至垃圾桶
restore-from-trash = 從垃圾桶還原
@ -215,8 +215,8 @@ sort-by-size = 依大小排序
## File
file = 檔案
new-tab = 新分頁
new-window = 新視窗
new-tab = 新建分頁
new-window = 新建視窗
rename = 重新命名...
close-tab = 關閉分頁
quit = 退出
@ -238,36 +238,36 @@ view = 檢視
grid-view = 網格檢視
list-view = 列表檢視
show-hidden-files = 顯示隱藏檔案
list-directories-first = 優先列出目錄
list-directories-first = 目錄優先列出
menu-settings = 設定...
menu-about = 關於 COSMIC 檔案總管...
menu-about = 關於 COSMIC 檔案...
## Sort
sort = 排序
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = 最新的在前
sort-oldest-first = 最舊的在前
sort-smallest-to-largest = 由小至大
sort-largest-to-smallest = 由大至小
sort-newest-first = 最新優先
sort-oldest-first = 最舊優先
sort-smallest-to-largest = 從小到大
sort-largest-to-smallest = 從大到小
deleted =
已刪除 { $items } { $items ->
已經刪除 { $items } { $items ->
[one] 項目
*[other] 項目
}從{ trash }
}從 { trash }
permanently-deleting =
正在永久刪除 { $items } { $items ->
[one] 项目
*[other] 项目
}
permanently-deleted =
已永久刪除 { $items } { $items ->
已經永久刪除 { $items } { $items ->
[one] 项目
*[other] 项目
}
removing-from-recents =
正在從{ recents }中移除 { $items } { $items ->
正在從 { recents } 中移除 { $items } { $items ->
[one] 项目
*[other] 项目
}
@ -275,9 +275,112 @@ deleting =
正在刪除 { $items } { $items ->
[one] 项目
*[other] 项目
}從{ trash }({ $progress })…
}從 { trash }({ $progress })…
removed-from-recents =
已從{ recents }中移除 { $items } { $items ->
已經從 { recents } 中移除 { $items } { $items ->
[one] 项目
*[other] 项目
}
repository = 軟體庫源
desktop-view-options = 桌面檢視選項...
show-on-desktop = 顯示在桌面
desktop-folder-content = 桌面資料夾內容
mounted-drives = 已經掛載的磁碟機
trash-folder-icon = 垃圾桶圖示
trashed-on = 遺棄時間
icon-size-and-spacing = 圖示大小與間距
icon-size = 圖示大小
grid-spacing = 網格間距
details = 詳情
dismiss = 撤停訊息
delete = 刪除
remove = 移除
support = 支援
cancelled = 已取消
keywords = 資料夾;管理器;
empty-trash-title = 清空垃圾桶?
pause = 暫停
resume = 繼續
extract-password-required = 需要密碼
extract-to = 解壓縮至...
extract-to-title = 解壓縮至資料夾
mount-error = 無法存取磁碟機
open-with-title = 您要如何開啟「{ $name }」?
browse-store = 瀏覽 { $store }
other-apps = 其他應用程式
related-apps = 相關應用程式
permanently-delete-question = 永久刪除?
set-executable-and-launch = 設定為可以執行並啟動
read-only = 唯讀
read-execute = 讀取和執行
read-write = 讀取和寫入
read-write-execute = 讀取、寫入和執行
favorite-path-error = 開啟目錄時發生錯誤
set-executable-and-launch-description = 您是否要將「{ $name }」設為可執行並啟動它?
set-and-launch = 設定並啟動
none = 無
execute-only = 僅執行
write-only = 僅寫入
write-execute = 寫入和執行
operations-running =
{ $running } { $running ->
[one] 個操作
*[other] 個操作
}正在執行({ $percent }%)...
operations-running-finished =
{ $running } { $running ->
[one] 個操作
*[other] 個操作
}正在執行({ $percent }%), { $finished } 個已經完成...
permanently-delete-warning = 「{ $target }」將被永久刪除。此操作無法復原。
open-with = 開啟檔案
selected-items = 已經選定 { $items } 個項目
copy-to-title = 選擇複製目的地
copy-to-button-label = 複製
move-to-title = 選擇移動目的地
move-to-button-label = 移動
keep = 保留
progress = { $percent }%
progress-cancelled = { $percent }%,已經取消
progress-failed = { $percent }%,失敗
progress-paused = { $percent }%,已經暫停
favorite-path-error-description =
無法開啟「{ $path }」
「{ $path }」可能不存在,或您可能沒有權限開啟它。
您是否要將它從側邊欄移除?
comment = COSMIC 桌面檔案管理器
pasted-image = 已經貼上的圖片
pasted-text = 已經貼上的文字
pasted-video = 已經貼上的影片
sort-by-trashed = 依丟入時間排序
calculating = 計算中...
single-click = 點按以開啟
type-to-search = 輸入進行搜尋
type-to-search-recursive = 搜尋目前資料夾及全部子資料夾
type-to-search-enter-path = 輸入目錄或檔案的路徑
delete-permanently = 永久刪除
eject = 彈出
remove-from-recents = 從最近項目中移除
change-wallpaper = 變更桌布...
desktop-appearance = 桌面外觀...
display-settings = 顯示設定...
reload-folder = 重新載入資料夾
gallery-preview = 圖庫預覽
type = 類型:{ $mime }
items = 項目:{ $items }
item-size = 大小:{ $size }
item-created = 建立時間:{ $created }
item-modified = 修改時間:{ $modified }
item-accessed = 存取時間:{ $accessed }
type-to-search-select = 選取第一個符合條件的檔案或資料夾
copy-to = 複製至...
move-to = 移動至...
show-recents = 側邊欄中的最近使用資料夾
clear-recents-history = 清除最近使用歷史記錄
copy-path = 複製路徑
setting-executable-and-launching = 設定「{ $name }」為可以執行並進行啟動
set-executable-and-launched = 設定「{ $name }」為可以執行並已經啟動
setting-permissions = 設定「{ $name }」的權限至 { $mode }
set-permissions = 設定「{ $name }」的權限至 { $mode }
mixed = 混合

View file

@ -18,9 +18,7 @@ use cosmic::{
Application, ApplicationExt, Element,
app::{self, Core, Task, context_drawer},
cosmic_config::{self, ConfigSet},
cosmic_theme,
desktop::fde::DesktopEntry,
executor,
cosmic_theme, executor,
iced::core::widget::operation::focusable::unfocus,
iced::runtime::{clipboard, task},
iced::widget::{button::focus, scrollable::AbsoluteOffset},
@ -99,6 +97,7 @@ use crate::{
self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK,
SearchLocation, Tab,
},
trash::{Trash, TrashExt},
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
};
@ -834,9 +833,12 @@ impl App {
// First launch apps that can be launched directly
if mime == "application/x-desktop" {
// Try opening desktop application
Self::launch_desktop_entries(&paths);
continue;
#[cfg(feature = "desktop")]
{
// Try opening desktop application
Self::launch_desktop_entries(&paths);
continue;
}
} else if mime == "application/x-executable" || mime == "application/vnd.appimage" {
// Try opening executable
for path in paths {
@ -897,7 +899,10 @@ impl App {
Task::batch(tasks)
}
#[cfg(feature = "desktop")]
fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {
use cosmic::desktop::fde::DesktopEntry;
for path in paths.iter().map(AsRef::as_ref) {
match DesktopEntry::from_path::<&str>(path, None) {
Ok(entry) => match entry.exec() {
@ -1794,7 +1799,7 @@ impl App {
nav_model = nav_model.insert(|b| {
b.text(fl!("trash"))
.icon(icon::icon(tab::trash_helpers::trash_icon_symbolic(16)))
.icon(icon::icon(Trash::icon_symbolic(16)))
.data(Location::Trash)
.divider_above()
});
@ -2655,9 +2660,7 @@ impl Application for App {
));
}
if matches!(location_opt, Some(Location::Trash))
&& !trash::os_limited::is_empty().unwrap_or(true)
{
if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() {
items.push(cosmic::widget::menu::Item::Button(
fl!("empty-trash"),
None,
@ -4237,10 +4240,8 @@ impl Application for App {
.is_some_and(|loc| matches!(loc, Location::Trash))
});
if let Some(entity) = maybe_entity {
self.nav_model.icon_set(
entity,
icon::icon(tab::trash_helpers::trash_icon_symbolic(16)),
);
self.nav_model
.icon_set(entity, icon::icon(Trash::icon_symbolic(16)));
}
return Task::batch([self.rescan_trash(), self.update_desktop()]);
@ -4262,12 +4263,14 @@ impl Application for App {
.collect();
if !selected.is_empty() {
//TODO: batch rename
let tasks = selected
let mut last_name = String::new();
let tasks: Vec<_> = selected
.into_iter()
.filter_map(|path| {
let parent = path.parent()?.to_path_buf();
let name = path.file_name()?.to_str()?.to_string();
let dir = path.is_dir();
last_name = name.clone();
Some(self.dialog_pages.push_back(DialogPage::RenameItem {
from: path,
parent,
@ -4275,9 +4278,15 @@ impl Application for App {
dir,
}))
})
.chain(std::iter::once(widget::text_input::focus(
.collect();
let tasks = tasks.into_iter().chain([
widget::text_input::focus(self.dialog_text_input.clone()),
widget::text_input::select_until_last(
self.dialog_text_input.clone(),
)));
&last_name,
'.',
),
]);
return Task::batch(tasks);
}
}
@ -6151,6 +6160,7 @@ impl Application for App {
.into(),
widget::text_input("", name.as_str())
.id(self.dialog_text_input.clone())
.double_click_select_delimiter('.')
.on_input(move |name| {
Message::DialogUpdate(DialogPage::RenameItem {
from: from.clone(),
@ -6794,14 +6804,7 @@ impl Application for App {
},
);
// TODO: Trash watching support for Windows, macOS, and other OSes
#[cfg(all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
))]
match (watcher_res, trash::os_limited::trash_folders()) {
match (watcher_res, Trash::folders()) {
(Ok(mut watcher), Ok(trash_bins)) => {
// Watch the "bins" themselves as well as the files folder where
// trashed items are placed. This allows us to avoid recursively
@ -6958,8 +6961,7 @@ impl Application for App {
|_| {
stream::channel(
1,
move |msg_tx: futures::channel::mpsc::Sender<_>| async move {
let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx));
move |mut msg_tx: futures::channel::mpsc::Sender<_>| async move {
tokio::task::spawn_blocking(move || {
match notify_rust::Notification::new()
.summary(&fl!("notification-in-progress"))
@ -6969,8 +6971,6 @@ impl Application for App {
Ok(notification) => {
let _ = futures::executor::block_on(async {
msg_tx
.lock()
.await
.send(Message::Notification(Arc::new(
Mutex::new(notification),
)))

View file

@ -189,6 +189,8 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
let mut target = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut target)?;
// File no longer needed, drop to allow reading target on windows
drop(file);
#[cfg(unix)]
{
@ -199,11 +201,15 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
#[cfg(windows)]
{
let Ok(target) = String::from_utf8(target) else {
return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target"));
return Err(ZipError::InvalidArchive(
"Invalid UTF-8 as symlink target".into(),
));
};
let target = target.into_boxed_str();
let target_is_dir_from_archive =
archive.shared.files.contains_key(&target) && is_dir(&target);
let target_is_dir_from_archive = match password {
None => archive.by_name(&target),
Some(pwd) => archive.by_name_decrypt(&target, pwd.as_bytes()),
}
.map_or(false, |x| x.is_dir());
let target_path = directory.as_ref().join(OsString::from(target.to_string()));
let target_is_dir = if target_is_dir_from_archive {
true

77
src/channel.rs Normal file
View file

@ -0,0 +1,77 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::{
collections::VecDeque,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
/// Create a channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque.
pub fn channel<Message>() -> (Sender<Message>, Receiver<Message>) {
let channel = Arc::new(Channel {
queue: Mutex::new(VecDeque::default()),
notify: tokio::sync::Notify::const_new(),
closed: AtomicBool::new(false),
});
(Sender(channel.clone()), Receiver(channel))
}
/// A channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque.
struct Channel<Message> {
pub(self) queue: Mutex<VecDeque<Message>>,
/// Set when a new message has been stored.
pub(self) notify: tokio::sync::Notify,
/// Set when the receiver is dropped.
pub(self) closed: AtomicBool,
}
pub struct Sender<Message>(Arc<Channel<Message>>);
impl<Message> Sender<Message> {
pub fn send(&self, message: Message) {
self.0.queue.lock().unwrap().push_back(message);
self.0.notify.notify_one();
}
}
impl<Message> Drop for Sender<Message> {
fn drop(&mut self) {
self.0.closed.store(true, Ordering::SeqCst);
self.0.notify.notify_one();
}
}
pub struct Receiver<Message>(Arc<Channel<Message>>);
impl<Message> Receiver<Message> {
/// Returns a value until the sender is dropped.
pub async fn recv(&self) -> Option<Message> {
loop {
{
let mut queue = self.0.queue.lock().unwrap();
if let Some(value) = queue.pop_front() {
if queue.capacity() - queue.len() > 32 {
let capacity = queue.len().next_power_of_two();
queue.shrink_to(capacity);
}
drop(queue);
return Some(value);
}
}
if self.0.closed.load(Ordering::SeqCst) {
return None;
}
self.0.notify.notified().await;
}
}
pub fn try_recv(&self) -> Option<Message> {
self.0.queue.lock().unwrap().pop_front()
}
}

View file

@ -441,13 +441,8 @@ impl<M: Send + 'static> Dialog<M> {
#[derive(Clone, Debug)]
enum DialogPage {
NewFolder {
parent: PathBuf,
name: String,
},
Replace {
filename: String,
},
NewFolder { parent: PathBuf, name: String },
Replace { filename: String },
}
#[derive(Clone, Debug)]
@ -600,6 +595,7 @@ impl App {
col = col.push(
widget::text_input("", filename)
.id(self.filename_id.clone())
.double_click_select_delimiter('.')
.on_input(Message::Filename)
.on_submit(|_| Message::Save(false)),
);
@ -1962,6 +1958,16 @@ impl Application for App {
if self.search_get().is_some() {
return widget::text_input::focus(self.search_id.clone());
}
if let DialogKind::SaveFile { filename } = &self.flags.kind {
return Task::batch([
widget::text_input::focus(self.filename_id.clone()),
widget::text_input::select_until_last(
self.filename_id.clone(),
filename,
'.',
),
]);
}
return widget::text_input::focus(self.filename_id.clone());
}
}
@ -2031,7 +2037,7 @@ impl Application for App {
}
col = col.push(
self.tab
self.tab
.view(&self.key_binds, &self.modifiers, false, &[])
.map(Message::TabMessage),
);

View file

@ -3,14 +3,20 @@
use cosmic::{app::Settings, iced::Limits};
use std::{env, fs, path::PathBuf, process};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use crate::{
app::{App, Flags},
config::{Config, State},
tab::Location,
};
use app::{App, Flags};
pub mod app;
mod archive;
pub mod channel;
pub mod clipboard;
mod context_action;
use config::Config;
pub mod config;
mod context_action;
pub mod dialog;
mod key_bind;
pub(crate) mod large_image;
@ -23,13 +29,11 @@ mod mounter;
mod mouse_area;
pub mod operation;
mod spawn_detached;
use tab::Location;
mod zoom;
use crate::config::State;
pub mod tab;
mod thumbnail_cacher;
mod thumbnailer;
pub(crate) mod trash;
mod zoom;
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
@ -73,7 +77,22 @@ pub fn is_wayland() -> bool {
/// Runs application in desktop mode
#[rustfmt::skip]
pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let log_format = tracing_subscriber::fmt::format()
.pretty()
.without_time()
.with_line_number(true)
.with_file(true)
.with_target(false)
.with_thread_names(true);
let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
.with(log_layer)
.init();
localize::localize();
@ -108,7 +127,21 @@ pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
/// Runs application with these settings
#[rustfmt::skip]
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let log_format = tracing_subscriber::fmt::format()
.pretty()
.with_line_number(true)
.with_file(true)
.with_target(false)
.with_thread_names(true);
let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(log_layer)
.init();
localize::localize();
@ -158,7 +191,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
}
if daemonize {
#[cfg(all(unix, not(target_os = "redox")))]
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
match fork::daemon(true, true) {
Ok(fork::Fork::Child) => (),
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),

View file

@ -23,6 +23,7 @@ use crate::{
config::{Config, ContextActionPreset},
fl,
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
trash::{Trash, TrashExt},
};
static MENU_ID: LazyLock<cosmic::widget::Id> =
@ -192,7 +193,7 @@ pub fn context_menu<'a>(
) => {
if selected_trash_only {
children.push(menu_item(fl!("open"), Action::Open).into());
if !trash::os_limited::is_empty().unwrap_or(true) {
if !Trash::is_empty() {
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
}
} else if let Some(entry) = selected_desktop_entry {

View file

@ -19,6 +19,7 @@ struct MimeIconKey {
struct MimeIconCache {
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
#[cfg(unix)]
shared_mime_info: xdg_mime::SharedMimeInfo,
}
@ -26,10 +27,17 @@ impl MimeIconCache {
pub fn new() -> Self {
Self {
cache: FxHashMap::default(),
#[cfg(unix)]
shared_mime_info: xdg_mime::SharedMimeInfo::new(),
}
}
#[cfg(not(unix))]
pub fn get(&mut self, _key: MimeIconKey) -> Option<icon::Handle> {
None
}
#[cfg(unix)]
pub fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
self.cache
.entry(key)
@ -53,6 +61,16 @@ impl MimeIconCache {
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
#[cfg(not(unix))]
pub fn mime_for_path(
path: impl AsRef<Path>,
metadata_opt: Option<&fs::Metadata>,
remote: bool,
) -> Mime {
mime_guess::from_path(path).first_or_octet_stream()
}
#[cfg(unix)]
pub fn mime_for_path(
path: impl AsRef<Path>,
metadata_opt: Option<&fs::Metadata>,
@ -100,8 +118,13 @@ pub fn mime_icon(mime: Mime, size: u16) -> icon::Handle {
}
}
#[cfg(not(unix))]
pub fn parent_mime_types(_mime: &Mime) -> Option<Vec<Mime>> {
None
}
#[cfg(unix)]
pub fn parent_mime_types(mime: &Mime) -> Option<Vec<Mime>> {
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
mime_icon_cache.shared_mime_info.get_parents_aliased(mime)
}

View file

@ -5,7 +5,7 @@ use cosmic::{
};
use gio::{glib, prelude::*};
use std::{any::TypeId, cell::Cell, future::pending, hash::Hash, path::PathBuf, sync::Arc};
use tokio::sync::{Mutex, mpsc};
use tokio::sync::mpsc;
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
use crate::{
@ -199,6 +199,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
metadata,
hidden,
location_opt: Some(location),
image_dimensions: None,
mime,
icon_handle_grid,
icon_handle_list,
@ -229,7 +230,10 @@ fn dir_info(uri: &str) -> Result<(String, String, Option<PathBuf>), glib::Error>
Ok((resolved_uri, info.display_name().into(), file.path()))
}
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
fn mount_op(
uri: String,
event_tx: std::sync::Weak<crate::channel::Sender<Event>>,
) -> gio::MountOperation {
let mount_op = gio::MountOperation::new();
mount_op.connect_ask_password(
move |mount_op, message, default_user, default_domain, flags| {
@ -252,9 +256,9 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOp
.then_some(false),
};
let (auth_tx, mut auth_rx) = mpsc::channel(1);
event_tx
.send(Event::NetworkAuth(uri.clone(), auth, auth_tx))
.unwrap();
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx));
}
//TODO: async recv?
if let Some(auth) = auth_rx.blocking_recv() {
if auth.anonymous_opt == Some(true) {
@ -357,37 +361,45 @@ impl Item {
pub struct Gvfs {
command_tx: mpsc::UnboundedSender<Cmd>,
event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
event_rx: Arc<crate::channel::Receiver<Event>>,
}
impl Gvfs {
pub fn new() -> Self {
//TODO: switch to using gvfs-zbus which will better integrate with async rust
let (command_tx, mut command_rx) = mpsc::unbounded_channel();
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (event_tx, event_rx) = crate::channel::channel();
let event_tx = Arc::new(event_tx);
std::thread::spawn(move || {
let main_loop = glib::MainLoop::new(None, false);
main_loop.context().spawn_local(async move {
let event_tx = Arc::downgrade(&event_tx);
let monitor = gio::VolumeMonitor::get();
{
let event_tx = event_tx.clone();
monitor.connect_mount_changed(move |_monitor, mount| {
log::info!("mount changed {}", MountExt::name(mount));
event_tx.send(Event::Changed).unwrap();
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
});
}
{
let event_tx = event_tx.clone();
monitor.connect_mount_added(move |_monitor, mount| {
log::info!("mount added {}", MountExt::name(mount));
event_tx.send(Event::Changed).unwrap();
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
});
}
{
let event_tx = event_tx.clone();
monitor.connect_mount_removed(move |_monitor, mount| {
log::info!("mount removed {}", MountExt::name(mount));
event_tx.send(Event::Changed).unwrap();
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
});
}
@ -395,21 +407,27 @@ impl Gvfs {
let event_tx = event_tx.clone();
monitor.connect_volume_changed(move |_monitor, volume| {
log::info!("volume changed {}", VolumeExt::name(volume));
event_tx.send(Event::Changed).unwrap();
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
});
}
{
let event_tx = event_tx.clone();
monitor.connect_volume_added(move |_monitor, volume| {
log::info!("volume added {}", VolumeExt::name(volume));
event_tx.send(Event::Changed).unwrap();
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
});
}
{
let event_tx = event_tx.clone();
monitor.connect_volume_removed(move |_monitor, volume| {
log::info!("volume removed {}", VolumeExt::name(volume));
event_tx.send(Event::Changed).unwrap();
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
});
}
@ -419,7 +437,11 @@ impl Gvfs {
items_tx.send(items(&monitor, sizes)).await.unwrap();
}
Cmd::Rescan => {
event_tx.send(Event::Items(items(&monitor, IconSizes::default()))).unwrap();
let Some(event_tx) = event_tx.upgrade() else {
return;
};
event_tx.send(Event::Items(items(&monitor, IconSizes::default())));
}
Cmd::Mount(mounter_item, complete_tx) => {
let MounterItem::Gvfs(ref item) = mounter_item else {
@ -471,6 +493,9 @@ impl Gvfs {
.ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
.unwrap_or(true);
}
let Some(event_tx) = event_tx.upgrade() else {
return;
};
event_tx.send(Event::MountResult(updated_item, match res {
Ok(()) => {
_ = complete_tx.send(Ok(()));
@ -482,7 +507,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}"))
}}
})).unwrap();
}));
},
);
break;
@ -498,6 +523,9 @@ impl Gvfs {
gio::Cancellable::NONE,
move |res| {
log::info!("network drive {uri}: result {res:?}");
let Some(event_tx) = event_tx.upgrade() else {
return;
};
event_tx.send(Event::NetworkResult(uri, match res {
Ok(()) => {
_ = result_tx.send(Ok(()));
@ -508,7 +536,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}"))
}}
})).unwrap();
}));
}
);
}
@ -532,6 +560,9 @@ impl Gvfs {
// FIXME sometimes a uri can be mounted and then not recognized as mounted...
// seems to be related to uri with a path
items_tx.blocking_send(network_scan(&uri, sizes)).unwrap();
let Some(event_tx) = event_tx.upgrade() else {
return;
};
event_tx.send(Event::NetworkResult(resolved_uri, match res {
Ok(()) => {
Ok(true)
@ -540,7 +571,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}"))
}
})).unwrap();
}));
}
);
} else {
@ -596,7 +627,7 @@ impl Gvfs {
});
Self {
command_tx,
event_rx: Arc::new(Mutex::new(event_rx)),
event_rx: Arc::new(event_rx),
}
}
}
@ -670,7 +701,7 @@ impl Mounter for Gvfs {
let event_rx = self.event_rx.clone();
struct Wrapper {
command_tx: mpsc::UnboundedSender<Cmd>,
event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
event_rx: Arc<crate::channel::Receiver<Event>>,
}
impl Hash for Wrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
@ -694,7 +725,7 @@ impl Mounter for Gvfs {
MounterMessage,
>| async move {
command_tx.send(Cmd::Rescan).unwrap();
while let Some(event) = event_rx.lock().await.recv().await {
while let Some(event) = event_rx.recv().await {
match event {
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
Event::Items(items) => {

View file

@ -1,7 +1,11 @@
use std::sync::{Arc, Mutex};
use atomic_float::AtomicF32;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::sync::Arc;
use std::sync::atomic::{self, AtomicU16};
use tokio::sync::Notify;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
pub enum ControllerState {
Cancelled,
Failed,
@ -11,8 +15,8 @@ pub enum ControllerState {
#[derive(Debug)]
struct ControllerInner {
state: Mutex<ControllerState>,
progress: Mutex<f32>,
state: AtomicU16,
progress: AtomicF32,
notify: Notify,
}
@ -27,8 +31,8 @@ impl Default for Controller {
Self {
primary: true,
inner: Arc::new(ControllerInner {
state: Mutex::new(ControllerState::Running),
progress: Mutex::new(0.0),
state: AtomicU16::new(ControllerState::Running.into()),
progress: AtomicF32::new(0.0),
notify: Notify::new(),
}),
}
@ -50,19 +54,24 @@ impl Controller {
}
pub fn progress(&self) -> f32 {
*self.inner.progress.lock().unwrap()
self.inner.progress.load(atomic::Ordering::Relaxed)
}
pub fn set_progress(&self, progress: f32) {
*self.inner.progress.lock().unwrap() = progress;
self.inner
.progress
.swap(progress, atomic::Ordering::Relaxed);
}
pub fn state(&self) -> ControllerState {
*self.inner.state.lock().unwrap()
ControllerState::try_from(self.inner.state.load(atomic::Ordering::Relaxed))
.unwrap_or(ControllerState::Failed)
}
pub fn set_state(&self, state: ControllerState) {
*self.inner.state.lock().unwrap() = state;
self.inner
.state
.store(state.into(), atomic::Ordering::Relaxed);
self.inner.notify.notify_waiters();
}
@ -86,6 +95,35 @@ impl Controller {
self.set_state(ControllerState::Paused);
}
/// Returns when the state is paused.
///
/// Use this to pause futures.
pub async fn until_paused(&self) {
loop {
if matches!(self.state(), ControllerState::Paused) {
return;
}
self.inner.notify.notified().await;
}
}
/// Returns when state is neither paused, cancelled, nor failed.
///
/// Use this to resume futures.
pub async fn until_unpaused(&self) {
loop {
if !matches!(
self.state(),
ControllerState::Paused | ControllerState::Cancelled | ControllerState::Failed
) {
return;
}
self.inner.notify.notified().await;
}
}
pub fn unpause(&self) {
if !self.is_cancelled() | !self.is_failed() {
self.set_state(ControllerState::Running);

View file

@ -22,6 +22,9 @@ use zip::AesMode::Aes256;
pub use self::controller::{Controller, ControllerState};
pub mod controller;
pub use notifiers::*;
mod notifiers;
pub use self::reader::OpReader;
pub mod reader;
@ -111,7 +114,7 @@ async fn copy_or_move(
);
// Handle duplicate file names by renaming paths
let mut from_to_pairs: Vec<(PathBuf, PathBuf)> = paths
let from_to_pairs_iter = paths
.into_iter()
.zip(std::iter::repeat(to.as_path()))
.filter_map(|(from, to)| {
@ -129,36 +132,46 @@ async fn copy_or_move(
//TODO: how to handle from missing file name?
None
}
})
.collect();
});
// Attempt quick and simple renames
//TODO: allow rename to be used for directories in recursive context?
if matches!(method, Method::Move { .. }) {
from_to_pairs.retain(|(from, to)| {
//TODO: show replace dialog here?
if to.exists() {
return true;
}
//TODO: use compio::fs::rename?
match fs::rename(from, to) {
Ok(()) => {
log::info!("renamed {} to {}", from.display(), to.display());
false
let from_to_pairs: Vec<(PathBuf, PathBuf)> = if matches!(method, Method::Move { .. }) {
from_to_pairs_iter
.map(|(from, to)| async move {
//TODO: show replace dialog here?
if to.exists() {
return Some((from, to));
}
Err(err) => {
log::info!(
"failed to rename {} to {}, fallback to recursive move: {}",
from.display(),
to.display(),
err
);
true
match compio::fs::rename(&from, &to).await {
Ok(()) => {
log::info!("renamed {} to {}", from.display(), to.display());
None
}
Err(err) => {
log::info!(
"failed to rename {} to {}, fallback to recursive move: {}",
from.display(),
to.display(),
err
);
Some((from, to))
}
}
}
});
}
})
.collect::<cosmic::iced::futures::stream::FuturesOrdered<_>>()
.fold(Vec::new(), |mut pairs, pair| async move {
if let Some(pair) = pair {
pairs.push(pair);
}
pairs
})
.await
} else {
from_to_pairs_iter.collect()
};
let mut context = Context::new(controller.clone());
@ -216,7 +229,7 @@ pub async fn sync_to_disk(
}
}))
.buffer_unordered(32)
.collect::<Vec<_>>()
.collect::<()>()
.await;
// Sync directories to disk
@ -226,7 +239,7 @@ pub async fn sync_to_disk(
}
}))
.buffer_unordered(16)
.collect::<Vec<_>>()
.collect::<()>()
.await;
}
@ -1112,7 +1125,9 @@ impl Operation {
#[cfg(target_os = "macos")]
Self::Restore { .. } => {
// TODO: add support for macos
return OperationError::from_msg("Restoring from trash is not supported on macos");
return Err(OperationError::from_msg(
"Restoring from trash is not supported on macos",
));
}
#[cfg(not(target_os = "macos"))]
Self::Restore { items } => {

View file

@ -0,0 +1,58 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use tokio::sync::Notify;
/// Monitor files which are being written to.
pub struct FileWritingNotifier {
data: Vec<PathBuf>,
notify: Arc<Notify>,
}
static ACTIVELY_WRITING: LazyLock<Mutex<FileWritingNotifier>> = LazyLock::new(|| {
Mutex::new(FileWritingNotifier {
data: Vec::new(),
notify: Arc::new(Notify::new()),
})
});
/// Append path that is being written to.
pub fn actively_writing_add(path: PathBuf) {
ACTIVELY_WRITING.lock().unwrap().data.push(path);
}
/// Remove path to file that has finished writing and notify waiters.
pub fn actively_writing_remove(path: &Path) {
let mut guard = ACTIVELY_WRITING.lock().unwrap();
guard.data.retain(|p| p != path);
guard.notify.notify_waiters();
}
/// Wait until the actively-writing queue is empty or a file has been removed.
pub async fn actively_writing_tick() {
let notify = (|| {
let guard = ACTIVELY_WRITING.lock().unwrap();
if !guard.data.is_empty() {
return Some(guard.notify.clone());
}
None
})();
if let Some(notify) = notify {
notify.notified().await
}
}
/// Check if a file is being written to. Avoid thumbnail generation until after it is finished.
pub fn is_actively_writing_to(path: &Path) -> bool {
ACTIVELY_WRITING
.lock()
.unwrap()
.data
.iter()
.any(|p| p == path)
}

View file

@ -1,15 +1,33 @@
use compio::BufResult;
use compio::buf::{IntoInner, IoBuf};
use compio::io::{AsyncReadAt, AsyncWriteAt};
use std::future::Future;
use std::pin::Pin;
use std::time::Instant;
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc};
use walkdir::WalkDir;
use crate::operation::{OperationError, sync_to_disk};
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};
use crate::operation::{OperationError, sync_to_disk};
use anyhow::Context as AnyhowContext;
use compio::BufResult;
use compio::buf::{IntoInner, IoBuf};
use compio::driver::{ToSharedFd, op::AsyncifyFd};
use compio::io::{AsyncReadAt, AsyncWriteAt};
use cosmic::iced::futures;
use futures::{FutureExt, StreamExt};
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::time::Instant;
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf};
use walkdir::WalkDir;
#[cfg(feature = "gvfs")]
use gio::prelude::FileExtManual;
#[derive(thiserror::Error, Debug)]
pub enum GioCopyError {
#[error("controller state")]
Controller(OperationError),
#[cfg(feature = "gvfs")]
#[error("gio copy failed")]
GLib(#[from] glib::Error),
}
pub enum Method {
Copy,
@ -313,136 +331,28 @@ impl Op {
})
}
async fn run(
&mut self,
ctx: &mut Context,
mut progress: Progress,
) -> Result<bool, Box<dyn Error>> {
async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result<bool, Box<dyn Error>> {
if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) {
return Ok(true);
}
match self.kind {
OpKind::Copy => {
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(self).await? {
ControlFlow::Continue(to) => {
self.to = to;
}
ControlFlow::Break(ret) => {
return Ok(ret);
}
}
crate::operation::actively_writing_add(self.to.clone());
let result = self.copy(ctx, progress).await;
if result.is_err() {
_ = compio::fs::remove_file(&self.to).await;
}
let (from_file, metadata, mut to_file) = cosmic::iced::futures::try_join!(
async {
compio::fs::OpenOptions::new()
.read(true)
.open(&self.from)
.await
},
compio::fs::metadata(&self.from),
// This is atomic and ensures `to` is not created by any other process
async {
compio::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&self.to)
.await
}
)?;
progress.total_bytes = Some(metadata.len());
(ctx.on_progress)(self, &progress);
if let Err(err) = to_file.set_permissions(metadata.permissions()).await {
// This error is not propagated upwards as some filesystems do not support setting permissions
log::warn!(
"failed to set permissions for {}: {}",
self.to.display(),
err
);
}
// Prevent spamming the progress callbacks.
let mut last_progress_update = Instant::now();
// io_uring/IOCP requires transferring ownership of the buffer to the kernel.
let mut buf_in = std::mem::take(&mut ctx.buf);
// Track where the current read/write position is at.
let mut pos = 0;
loop {
let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await;
let count = match result {
Ok(0) => {
ctx.buf = buf_out;
break;
}
Ok(count) => count,
Err(why) => {
ctx.buf = buf_out;
return Err(why.into());
}
};
let BufResult(result, buf_out_slice) =
to_file.write_at(buf_out.slice(..count), pos).await;
let buf_out = buf_out_slice.into_inner();
if let Err(why) = result {
ctx.buf = buf_out;
return Err(why.into());
}
progress.current_bytes += count as u64;
pos += count as u64;
// Avoid spamming progress messages too early.
let current = Instant::now();
if current.duration_since(last_progress_update).as_millis() > 49 {
last_progress_update = current;
(ctx.on_progress)(self, &progress);
// Also check if the progress was cancelled.
if let Err(state) = ctx.controller.check().await {
ctx.buf = buf_out;
return Err(OperationError::from_state(state, &ctx.controller).into());
}
}
buf_in = buf_out;
}
let mut times = fs::FileTimes::new();
{
use std::os::unix::prelude::MetadataExt;
log::info!("{}", metadata.mtime());
}
if let Ok(time) = metadata.modified() {
times = times.set_modified(time);
}
if let Ok(time) = metadata.accessed() {
times = times.set_accessed(time);
}
//TODO: upstream set_times implementation to compio?
{
use compio::driver::{ToSharedFd, op::AsyncifyFd};
let op =
AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| {
BufResult(file.set_times(times).map(|_| 0), ())
});
match compio::runtime::submit(op).await.0.map(|_| ()) {
Ok(()) => {
log::info!("set times for {} to {:?}", self.to.display(), times);
}
Err(err) => {
log::warn!("failed to set times for {}: {}", self.to.display(), err);
}
}
}
crate::operation::actively_writing_remove(&self.to);
return result;
}
OpKind::Move { cross_device_copy } => {
// Do not clean up if cross_device_copy is set
if cross_device_copy {
self.skipped.cleanup.set(true);
}
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(self).await? {
@ -520,4 +430,268 @@ impl Op {
}
Ok(true)
}
async fn copy(
&mut self,
ctx: &mut Context,
mut progress: Progress,
) -> Result<bool, Box<dyn Error>> {
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(self).await? {
ControlFlow::Continue(to) => {
self.to = to;
}
ControlFlow::Break(ret) => {
return Ok(ret);
}
}
}
let (from_file, metadata, to_file) = cosmic::iced::futures::join!(
async {
compio::fs::OpenOptions::new()
.read(true)
.open(&self.from)
.await
.with_context(|| format!("failed to open {} for reading", self.from.display(),))
},
async { compio::fs::metadata(&self.from).await.ok() },
// This is atomic and ensures `to` is not created by any other process
async {
compio::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&self.to)
.await
.with_context(|| format!("failed to open {} for writing", self.to.display()))
}
);
let from_file = from_file?;
let mut to_file = to_file?;
progress.total_bytes = metadata.as_ref().map(|m| m.len());
(ctx.on_progress)(self, &progress);
if let Some(metadata) = metadata.as_ref() {
if let Err(why) = to_file.set_permissions(metadata.permissions()).await {
// This error is not propagated upwards as some filesystems do not support setting permissions
if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {
tracing::warn!(?why, "failed to set permissions for {}", self.to.display(),);
}
}
}
// Prevent spamming the progress callbacks.
let mut last_progress_update = Instant::now();
// io_uring/IOCP requires transferring ownership of the buffer to the kernel.
let mut buf_in = std::mem::take(&mut ctx.buf);
// Track where the current read/write position is at.
let mut pos = 0;
loop {
let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await;
let count = match result {
Ok(0) => {
buf_in = buf_out;
break;
}
Ok(count) => count,
Err(why) => {
ctx.buf = buf_out;
tracing::error!("failed to read: {:?}", why);
_ = futures::future::join(from_file.close(), to_file.close()).await;
return Err(why).context("failed to read")?;
}
};
let BufResult(result, buf_out_slice) =
to_file.write_at(buf_out.slice(..count), pos).await;
let buf_out = buf_out_slice.into_inner();
if let Err(why) = result {
#[cfg(feature = "gvfs")]
if let std::io::ErrorKind::Unsupported = why.kind() {
ctx.buf = buf_out;
_ = futures::future::join(from_file.close(), to_file.close()).await;
return self
.gio_file_copy(ctx, progress)
.await
.map(|_| true)
.map_err(Into::into);
}
tracing::error!("failed to write: {:?}", why);
ctx.buf = buf_out;
_ = futures::future::join(from_file.close(), to_file.close()).await;
return Err(why).context("failed to write")?;
}
progress.current_bytes += count as u64;
pos += count as u64;
// Avoid spamming progress messages too early.
let current = Instant::now();
if current.duration_since(last_progress_update).as_millis() > 49 {
last_progress_update = current;
(ctx.on_progress)(self, &progress);
// Also check if the progress was cancelled.
if let Err(state) = ctx.controller.check().await {
ctx.buf = buf_out;
tracing::warn!(
"operation to copy from {:?} to {:?} cancelled",
self.from,
self.to
);
_ = futures::future::join(from_file.close(), to_file.close()).await;
return Err(OperationError::from_state(state, &ctx.controller).into());
}
}
buf_in = buf_out;
}
ctx.buf = buf_in;
if let Some(metadata) = metadata.as_ref() {
let mut times = fs::FileTimes::new();
if let Ok(time) = metadata.modified() {
times = times.set_modified(time);
}
if let Ok(time) = metadata.accessed() {
times = times.set_accessed(time);
}
//TODO: upstream set_times implementation to compio?
let op = AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| {
BufResult(file.set_times(times).map(|_| 0), ())
});
match compio::runtime::submit(op).await.0.map(|_| ()) {
Ok(()) => {
tracing::info!("set times for {} to {:?}", self.to.display(), times);
}
Err(why) => {
if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {
tracing::error!(?why, "failed to set times for {}", self.to.display());
}
}
}
}
_ = to_file.close().await;
Ok(true)
}
/// Fallback mechanism in the event that unsupported I/O error errors occur.
/// Fixes unsupported errors when copying large files over MTP.
/// TODO: Find what Gio.File does to work around this.
#[cfg(feature = "gvfs")]
async fn gio_file_copy(
&self,
ctx: &mut Context,
mut progress: Progress,
) -> Result<(), GioCopyError> {
_ = compio::fs::remove_file(&self.to).await;
let from = gio::File::for_path(&self.from);
let to = gio::File::for_path(&self.to);
let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel();
let (tx, rx) = tokio::sync::oneshot::channel();
let (pause_tx, mut pause_rx) = tokio::sync::watch::channel(false);
let task = compio::runtime::spawn_blocking(move || {
let glib_context = glib::MainContext::new();
let glib_loop = glib::MainLoop::new(Some(&glib_context), false);
glib_context.with_thread_default(move || {
let glib_loop2 = glib_loop.clone();
glib::MainContext::ref_thread_default().spawn_local(async move {
// Create a future for copying the file with `gio::File`. This also creates a progress stream.
let (gio_copy_fut, mut progress_stream) = from.copy_future(
&to,
gio::FileCopyFlags::OVERWRITE | gio::FileCopyFlags::ALL_METADATA,
glib::Priority::LOW,
);
let mut copy_fut = gio_copy_fut
.map(|result| result.map_err(GioCopyError::GLib))
.fuse();
let progress_fut = std::pin::pin!(async {
while let Some((current_bytes, _)) = progress_stream.next().await {
_ = progress_tx.send(current_bytes);
}
drop(progress_tx);
futures::future::pending::<()>().await;
});
let mut progress_fut = progress_fut.fuse();
let mut pause_rx2 = pause_rx.clone();
loop {
let until_paused = std::pin::pin!(pause_rx.wait_for(|paused| *paused));
futures::select! {
_ = &mut progress_fut => {},
result = &mut copy_fut => {
_ = tx.send(result.map(|_| ()));
glib_loop2.quit();
return;
}
_ = until_paused.fuse() => {
_ = pause_rx2.wait_for(|paused| !*paused).await;
}
}
}
});
glib_loop.run();
})
});
let mut last_progress_update = Instant::now();
let mut task = task.fuse();
let mut rx = rx.fuse();
loop {
let until_paused = std::pin::pin!(ctx.controller.until_paused());
futures::select! {
value = progress_rx.recv().fuse() => {
if let Some(current_bytes) = value {
progress.current_bytes = current_bytes as u64;
let current = Instant::now();
if current.duration_since(last_progress_update).as_millis() > 49 {
last_progress_update = current;
(ctx.on_progress)(self, &progress);
// Also check if the progress was cancelled.
if let Err(state) = ctx.controller.check().await {
tracing::warn!(
"operation to copy from {:?} to {:?} cancelled",
self.from,
self.to
);
return Err::<(), GioCopyError>(GioCopyError::Controller(
OperationError::from_state(state, &ctx.controller),
));
}
}
}
}
result = rx => return result.unwrap(),
_ = task => (),
_ = until_paused.fuse() => {
// Pauses an active copy while the controller state is paused.
_ = pause_tx.send(true);
ctx.controller.until_unpaused().await;
_ = pause_tx.send(false);
}
}
}
}
}

View file

@ -1,7 +1,7 @@
#[cfg(feature = "desktop")]
use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};
use cosmic::{
Apply, Element, cosmic_theme,
desktop::fde::{DesktopEntry, get_languages_from_env},
font,
Apply, Element, cosmic_theme, font,
iced::core::{mouse::ScrollDelta, widget::tree},
iced::{
Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector,
@ -39,9 +39,10 @@ use icu::{
use image::{DynamicImage, ImageReader};
use jiff_icu::ConvertFrom;
use mime_guess::{Mime, mime};
use regex::Regex;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::{
borrow::Cow,
cell::Cell,
@ -52,7 +53,6 @@ use std::{
fs::{self, File, Metadata},
hash::Hash,
io::{BufRead, BufReader},
os::unix::fs::MetadataExt,
path::{self, Path, PathBuf},
sync::{Arc, LazyLock, RwLock, atomic},
time::{Duration, Instant, SystemTime},
@ -84,8 +84,8 @@ use crate::{
operation::{Controller, OperationError},
thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize},
thumbnailer::thumbnailer,
trash::{Trash, TrashExt},
};
use uzers::{get_group_by_gid, get_user_by_uid};
pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
@ -557,6 +557,12 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
FsKind::Local
}
#[cfg(not(feature = "desktop"))]
fn get_desktop_file_display_name(path: &Path) -> Option<String> {
None
}
#[cfg(feature = "desktop")]
fn get_desktop_file_display_name(path: &Path) -> Option<String> {
let locales = get_languages_from_env();
let entry = match DesktopEntry::from_path(path, Some(&locales)) {
@ -570,6 +576,12 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
entry.name(&locales).map(|s| s.into_owned())
}
#[cfg(not(feature = "desktop"))]
fn get_desktop_file_icon(path: &Path) -> Option<String> {
None
}
#[cfg(feature = "desktop")]
fn get_desktop_file_icon(path: &Path) -> Option<String> {
let entry = match DesktopEntry::from_path::<&str>(path, None) {
Ok(ok) => ok,
@ -593,6 +605,7 @@ fn desktop_icon_handle(icon: &str, size: u16) -> widget::icon::Handle {
}
}
#[cfg(feature = "desktop")]
pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
let locales = get_languages_from_env();
let entry = match DesktopEntry::from_path(path, Some(&locales)) {
@ -708,6 +721,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
children_opt,
},
hidden,
image_dimensions: (!remote && mime.type_() == mime::IMAGE)
.then(|| image::image_dimensions(&path).ok())
.flatten(),
location_opt: Some(Location::Path(path)),
mime,
icon_handle_grid,
@ -843,6 +859,7 @@ pub fn item_from_entry(
},
hidden,
location_opt: Some(Location::Path(path)),
image_dimensions: None,
mime,
icon_handle_grid,
icon_handle_list,
@ -896,6 +913,9 @@ pub fn item_from_trash_entry(
metadata: ItemMetadata::Trash { metadata, entry },
hidden: false,
location_opt: None,
image_dimensions: (mime.type_() == mime::IMAGE)
.then(|| image::image_dimensions(&original_path).ok())
.flatten(),
mime,
icon_handle_grid,
icon_handle_list,
@ -1161,133 +1181,7 @@ pub fn scan_search<F: Fn(SearchItem) -> bool + Sync>(
}
}
SearchLocation::Trash => {
trash_helpers::scan_search_trash(callback, &regex);
}
}
}
// This config statement is from trash::os_limited, inverted
#[cfg(not(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
)))]
mod trash_helpers {
use super::*;
pub fn trash_entries() -> usize {
0
}
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name("user-trash")
.size(icon_size)
.handle()
}
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name("user-trash-symbolic")
.size(icon_size)
.handle()
}
pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
log::warn!("viewing trash not supported on this platform");
Vec::new()
}
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
}
// This config statement is from trash::os_limited
#[cfg(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
))]
pub mod trash_helpers {
use super::*;
pub fn trash_entries() -> usize {
match trash::os_limited::list() {
Ok(entries) => entries.len(),
Err(_err) => 0,
}
}
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
"user-trash"
} else {
"user-trash-full"
})
.size(icon_size)
.handle()
}
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
"user-trash-symbolic"
} else {
"user-trash-full-symbolic"
})
.size(icon_size)
.handle()
}
pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
let entries = match trash::os_limited::list() {
Ok(entry) => entry,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return Vec::new();
}
};
let mut items: Vec<_> = entries
.into_iter()
.filter_map(|entry| {
let metadata = trash::os_limited::metadata(&entry)
.inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
})
.ok()?;
Some(item_from_trash_entry(entry, metadata, sizes))
})
.collect();
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
});
items
}
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
let entries = match trash::os_limited::list() {
Ok(entries) => entries,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return;
}
};
for entry in entries {
if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
}) {
let name = entry.name.to_string_lossy();
if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) {
break;
}
}
Trash::scan_search(callback, &regex);
}
}
}
@ -1425,15 +1319,15 @@ pub fn scan_desktop(
let display_name = Item::display_name(&name);
let metadata = ItemMetadata::SimpleDir {
entries: trash_helpers::trash_entries() as u64,
entries: Trash::entries() as u64,
};
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
(
"inode/directory".parse().unwrap(),
trash_helpers::trash_icon(sizes.grid()),
trash_helpers::trash_icon(sizes.list()),
trash_helpers::trash_icon(sizes.list_condensed()),
Trash::icon(sizes.grid()),
Trash::icon(sizes.list()),
Trash::icon(sizes.list_condensed()),
)
};
@ -1444,6 +1338,7 @@ pub fn scan_desktop(
metadata,
hidden: false,
location_opt: Some(Location::Trash),
image_dimensions: None,
mime,
icon_handle_grid,
icon_handle_list,
@ -1673,7 +1568,7 @@ impl Location {
// Search is done incrementally
Vec::new()
}
Self::Trash => trash_helpers::scan_trash(sizes),
Self::Trash => Trash::scan(sizes),
Self::Recents => scan_recents(sizes),
Self::Network(uri, _, _) => scan_network(uri, sizes),
};
@ -2319,6 +2214,7 @@ pub struct Item {
pub hidden: bool,
pub location_opt: Option<Location>,
pub mime: Mime,
pub image_dimensions: Option<(u32, u32)>,
pub icon_handle_grid: widget::icon::Handle,
pub icon_handle_list: widget::icon::Handle,
pub icon_handle_list_condensed: widget::icon::Handle,
@ -2533,7 +2429,7 @@ impl Item {
let mode = metadata.mode();
let user_name = get_user_by_uid(metadata.uid())
let user_name = uzers::get_user_by_uid(metadata.uid())
.and_then(|user| user.name().to_str().map(ToOwned::to_owned))
.unwrap_or_default();
let user_path = path.clone();
@ -2556,7 +2452,7 @@ impl Item {
)),
);
let group_name = get_group_by_gid(metadata.gid())
let group_name = uzers::get_group_by_gid(metadata.gid())
.and_then(|group| group.name().to_str().map(ToOwned::to_owned))
.unwrap_or_default();
let group_path = path.clone();
@ -4444,6 +4340,7 @@ impl Tab {
for item in self.items_opt().map_or(Vec::new(), |items| {
items.iter().filter(|item| item.selected).collect()
}) {
#[cfg(unix)]
if let (Some(path), Some(mode)) = (
item.path_opt(),
item.file_metadata()
@ -6461,20 +6358,23 @@ 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));
#[cfg(unix)]
{
let mode = metadata.mode();
user_name.insert(
uzers::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(
uzers::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();
@ -6713,13 +6613,13 @@ impl Tab {
// Determine effective memory budget based on image size
let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE {
match image::image_dimensions(&path) {
Ok((width, height)) => {
match item.image_dimensions {
Some((width, height)) => {
let (_use_dedicated, eff_mb, eff_jobs) =
should_use_dedicated_worker(width, height, max_mb, max_jobs);
(eff_mb, eff_jobs)
}
Err(_) => (max_mb, max_jobs),
None => (max_mb, max_jobs),
}
} else {
(max_mb, max_jobs)
@ -6762,6 +6662,10 @@ impl Tab {
stream::channel(
1,
move |mut output: futures::channel::mpsc::Sender<_>| async move {
while crate::operation::is_actively_writing_to(&path) {
crate::operation::actively_writing_tick().await;
}
let message = {
let path = path.clone();
@ -6946,9 +6850,8 @@ impl Tab {
.await
.unwrap();
let output = Arc::new(tokio::sync::Mutex::new(output));
let (watch_tx, mut watch_rx) = tokio::sync::watch::channel(true);
{
let output = output.clone();
tokio::task::spawn_blocking(move || {
scan_search(
&search_location,
@ -6976,14 +6879,7 @@ impl Tab {
true
} else {
// Wake up update method
futures::executor::block_on(async {
output
.lock()
.await
.send(Message::SearchReady(false))
.await
})
.is_ok()
watch_tx.send(false).is_ok()
}
}
Err(_) => false,
@ -6996,13 +6892,16 @@ impl Tab {
search_location,
start.elapsed(),
);
})
.await
.unwrap();
});
}
while watch_rx.changed().await.is_ok() {
let is_ready = *watch_rx.borrow_and_update();
let _ = output.send(Message::SearchReady(is_ready)).await;
}
// Send final ready
let _ = output.lock().await.send(Message::SearchReady(true)).await;
let _ = output.send(Message::SearchReady(true)).await;
std::future::pending().await
},

View file

@ -1,11 +1,12 @@
use image::DynamicImage;
use md5::{Digest, Md5};
use rustc_hash::FxHashMap;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
error::Error,
fs::{self, File},
io::{self, BufReader, BufWriter},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
sync::LazyLock,
time::UNIX_EPOCH,
@ -93,6 +94,7 @@ impl ThumbnailCacher {
}
pub fn update_with_temp_file(&self, temp_file: NamedTempFile) -> Result<&Path, Box<dyn Error>> {
#[cfg(unix)]
fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(0o600))?;
self.update_thumbnail_text_metadata(temp_file.path())?;
fs::rename(temp_file.path(), &self.thumbnail_path)?;
@ -127,6 +129,7 @@ impl ThumbnailCacher {
pub fn create_fail_marker(&self) -> Result<(), Box<dyn Error>> {
if let Some(dir) = self.thumbnail_fail_marker_path.parent() {
fs::create_dir_all(dir)?;
#[cfg(unix)]
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
}

View file

@ -1,6 +1,7 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(feature = "desktop")]
use cosmic::desktop::fde::GenericEntry;
use mime_guess::Mime;
use rustc_hash::FxHashMap;

145
src/trash.rs Normal file
View file

@ -0,0 +1,145 @@
use cosmic::widget;
use regex::Regex;
use std::{collections::HashSet, path::PathBuf};
use crate::{
config::IconSizes,
tab::{Item, SearchItem},
};
pub trait TrashExt {
fn is_empty() -> bool {
true
}
fn entries() -> usize {
0
}
fn folders() -> Result<HashSet<PathBuf>, trash::Error> {
Err(trash::Error::Unknown {
description: "reading trash folders not supported on this platform".into(),
})
}
fn scan(_sizes: IconSizes) -> Vec<Item> {
log::warn!("viewing trash not supported on this platform");
Vec::new()
}
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
fn icon(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if Self::is_empty() {
"user-trash"
} else {
"user-trash-full"
})
.size(icon_size)
.handle()
}
fn icon_symbolic(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if Self::is_empty() {
"user-trash-symbolic"
} else {
"user-trash-full-symbolic"
})
.size(icon_size)
.handle()
}
}
pub struct Trash;
// This config statement is from trash::os_limited
#[cfg(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
))]
impl TrashExt for Trash {
fn is_empty() -> bool {
trash::os_limited::is_empty().unwrap_or(true)
}
fn entries() -> usize {
match trash::os_limited::list() {
Ok(entries) => entries.len(),
Err(_err) => 0,
}
}
// Not available on Windows only
#[cfg(not(target_os = "windows"))]
fn folders() -> Result<HashSet<PathBuf>, trash::Error> {
trash::os_limited::trash_folders()
}
fn scan(sizes: IconSizes) -> Vec<Item> {
use crate::{localize::LANGUAGE_SORTER, tab::item_from_trash_entry};
use std::cmp::Ordering;
let entries = match trash::os_limited::list() {
Ok(entry) => entry,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return Vec::new();
}
};
let mut items: Vec<_> = entries
.into_iter()
.filter_map(|entry| {
let metadata = trash::os_limited::metadata(&entry)
.inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
})
.ok()?;
Some(item_from_trash_entry(entry, metadata, sizes))
})
.collect();
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
});
items
}
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
let entries = match trash::os_limited::list() {
Ok(entries) => entries,
Err(err) => {
log::warn!("failed to read trash items: {err}");
return;
}
};
for entry in entries {
if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
}) {
let name = entry.name.to_string_lossy();
if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) {
break;
}
}
}
}
}
// This config statement is from trash::os_limited, inverted
#[cfg(not(any(
target_os = "windows",
all(
unix,
not(target_os = "macos"),
not(target_os = "ios"),
not(target_os = "android")
)
)))]
impl TrashExt for Trash {}