Compare commits

..

No commits in common. "9bcfe7a1f58580c713e21703e5c2fb2acd397cda" and "91243b99b556586c16c7c9b89872d4784f93c654" have entirely different histories.

42 changed files with 774 additions and 1864 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.11"
version = "1.0.9"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"
@ -14,6 +14,7 @@ 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"
@ -55,16 +56,12 @@ 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]
@ -80,6 +77,7 @@ features = [
"about",
"advanced-shaping",
"autosize",
"desktop",
"multi-window",
"tokio",
"winit",
@ -111,7 +109,7 @@ default = [
"wgpu",
]
dbus-config = ["libcosmic/dbus-config"]
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
desktop = ["dep:cosmic-mime-apps", "dep:xdg"]
desktop-applet = []
gvfs = ["dep:gio", "dep:glib"]
io-uring = ["compio/io-uring"]
@ -129,7 +127,6 @@ debug = true
[target.'cfg(unix)'.dependencies]
fork = "0.7"
uzers = "0.12.2"
[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.18"
@ -147,12 +144,13 @@ 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" }
iced_futures = { path = "../libcosmic/iced/futures" }
iced_winit = { path = "../libcosmic/iced/winit" }
# [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/smithay-clipboard']

View file

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

6
debian/changelog vendored
View file

@ -1,9 +1,3 @@
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,25 +9,9 @@ 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>> {
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();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).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,4 +371,3 @@ 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,4 +417,3 @@ 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 = Gib den Pfad zum Verzeichnis oder zur Datei ein
type-to-search-enter-path = Gibt den Pfad zu einem Verzeichnis oder einer Datei ein
# Kontextmenü
add-to-sidebar = Zur Seitenleiste hinzufügen
compress = Komprimieren...
@ -376,9 +376,9 @@ select-all = Alles auswählen
## Ansicht
zoom-in = Hineinzoomen
zoom-in = Vergrößern
default-size = Standardgröße
zoom-out = Herauszoomen
zoom-out = Verkleinern
view = Ansicht
grid-view = Rasteransicht
list-view = Listenansicht
@ -390,7 +390,7 @@ menu-about = Über COSMIC Dateien...
## Sortieren
sort = Sortierung
sort = Sortieren
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = Neueste zuerst
@ -424,8 +424,3 @@ 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

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 = Liitetyt asemat
mounted-drives = Tiedostojärjestelmään liitetyt kovalevyt
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 = Roskakorikansion kohteet poistetaan pysyvästi
empty-trash-warning = Haluatko varmasti tyhjentää koko roskakorin pysyvästi?
## Mount Error Dialog
@ -73,8 +73,8 @@ save-file = Tallenna tiedosto
## Open With Dialog
open-with-title = Miten haluat avata kohteen "{ $name }"?
browse-store = Selaa { $store }a
open-with-title = Kuinka haluat avata kohteen "{ $name }"?
browse-store = Selaa { $store }
## 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 käyttäen
apply-to-all = Toteuta kaikkiin
replace-with = Korvaa kohteella
apply-to-all = Sovella 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äynnistettäväksi ja käynnistää sen?
set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettvä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 = Yhdistetään…
connecting = Yhdistää…
domain = Verkkotunnus
enter-server-address = Kirjoita palvelimen osoite
enter-server-address = Syötä 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 =
Saatavilla olevat yhteyskäytännöt,Etuliite
Saatavissa olevat protokollat,Etuliite
AppleTalk,afp://
File Transfer Protocol,ftp:// tai ftps://
File Transfer Protocol,ftp:// or ftps://
Network File System,nfs://
Server Message Block,smb://
SSH File Transfer Protocol,sftp:// tai ssh://
WebDav,dav:// tai davs://
network-drive-error = Verkkolevy ei saatavilla
SSH File Transfer Protocol,sftp:// or ssh://
WebDav,dav:// or davs://
network-drive-error = Verkkolevy saavuttamattomissa
password = Salasana
remember-password = Muista salasana
try-again = Yritä uudelleen
username = Käyttäjätunnus
username = Käyttäjänimi
## Operations
edit-history = Muokkaa historiaa
history = Historia
no-history = Historia on tyhjä.
pending = Jonossa
failed = Epäonnistuneet
complete = Valmiit
pending = Odottaa käsittelyä
failed = Epäonnistui
complete = Valmis
compressing =
Pakataan { $items } { $items ->
Tiivistetään { $items } { $items ->
[one] kohde
*[other] kohdetta
} sijainnista "{ $from }" arkistoon "{ $to }" ({ $progress })…
*[other] kohteita
} lähteestä "{ $from }" arkistoon "{ $to }"
compressed =
Pakattu { $items } { $items ->
Tiivistetty { $items } { $items ->
[one] kohde
*[other] kohdetta
} sijainnista "{ $from }" arkistoon "{ $to }"
*[other] kohteet
} lähteestä "{ $from }" arkistoon "{ $to }"
copy_noun = Kopio
creating = Luodaan "{ $name }" kohteen "{ $parent }" alle
created = Luotu "{ $name }" kohteen "{ $parent }" alle
creating = Luodaan kohdetta "{ $name }" kohteen "{ $parent }" alle
created = Luotu kohde "{ $name }" kohteen "{ $parent }" alle
copying =
Kopioidaan { $items } { $items ->
[one] kohde
*[other] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })…
*[other] kohteita
} lähteestä "{ $from }" kohteeseen "{ $to }"
copied =
Kopioitu { $items } { $items ->
[one] kohde
*[other] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }"
emptying-trash = Tyhjennetään { trash } ({ $progress })…
*[other] kohteet
} lähteestä "{ $from }" kohteeseen "{ $to }"
emptying-trash = Tyhjennetään { trash }
emptied-trash = Tyhjennetty { trash }
extracting =
Puretaan { $items } { $items ->
[one] kohde
*[other] kohdetta
} arkistosta "{ $from }" kohteeseen "{ $to }" ({ $progress })…
*[other] kohteet
} arkistosta "{ $from }" kohteeseen "{ $to }"
extracted =
Purettu { $items } { $items ->
[one] kohde
*[other] kohdetta
*[other] kohteet
} 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] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })…
*[other] kohteet
} lähteestä "{ $from }" kohteeseen "{ $to }"
moved =
Siirretty { $items } { $items ->
[one] kohde
*[other] kohdetta
} sijainnista "{ $from }" kohteeseen "{ $to }"
*[other] kohteet
} lähteestä "{ $from }" kohteeseen "{ $to }"
renaming = Nimetään kohde "{ $from }" muotoon "{ $to }"
renamed = Nimetty kohde "{ $from }" muotoon "{ $to }"
restoring =
Palautetaan { $items } { $items ->
[one] kohde
*[other] kohdetta
} roskakorista ({ $progress })…
*[other] kohteet
} { trash }sta
restored =
Palautettu { $items } { $items ->
[one] kohde
*[other] kohdetta
} roskakorista
unknown-folder = tuntematon kansio
*[other] kohteet
} { trash }sta
unknown-folder = Tuntematon kansio
## Open with
menu-open-with = Avaa sovelluksella…
menu-open-with = Avaa ohjelmalla…
default-app = { $name } (oletus)
## Show details
@ -225,14 +225,14 @@ settings = Asetukset
appearance = Ulkoasu
theme = Teema
match-desktop = Sovita työpöytään
match-desktop = Sovita yhteen työpöydän kanssa
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ä muokkausajan mukaan
sort-by-modified = Järjestä muokkauspäivämäärän mukaan
sort-by-size = Järjestä koon mukaan
sort-by-trashed = Järjestä poistamisajan mukaan
sort-by-trashed = Järjestä poistamispäivämäärän 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 esikatselu
gallery-preview = Gallerian esinäkymä
menu-settings = Asetukset…
menu-about = Tietoa COSMICin tiedostonhallinnasta…
menu-about = Tietoa COSMIC Tiedostoista…
## Sort
@ -318,90 +318,3 @@ 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,4 +436,3 @@ 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,4 +433,3 @@ 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,4 +436,3 @@ 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,4 +318,3 @@ 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,322 +1 @@
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,4 +318,3 @@ 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,8 +272,3 @@ 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 = ਵਾਲਪੇਪਰ ਨੂੰ ਬਦਲੋ...
desktop-appearance = ਡੈਸਕਟਾਪ ਦੀ ਦਿੱਖ...
display-settings = ਡਿਸਪਲੇਅ ਸੈਟਿੰਗਾਂ...
change-wallpaper = Change wallpaper...
desktop-appearance = Desktop appearance...
display-settings = 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,4 +437,3 @@ 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 colado
pasted-video = Vídeo colado
pasted-text = Texto copiado
pasted-video = Vídeo copiado
copy-to-title = Selecione o destino da cópia
copy-to-button-label = Copiar
move-to-title = Selecione o destino da movimentação
@ -433,7 +433,6 @@ move-to-button-label = Mover
copy-to = Copiar para...
move-to = Mover para...
keywords = Pasta;Gerenciador;
show-recents = Pasta de recentes na barra lateral
show-recents = Pasta 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,4 +380,3 @@ 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,5 +435,4 @@ comment = COSMIC 桌面的文件管理器
keywords = 文件夹;管理器;
clear-recents-history = 清除最近访问历史
copy-path = 复制文件路径
show-recents = 侧边栏中的最近访问
mixed = 混合
show-recents = 侧边栏显示最近访问

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,112 +275,9 @@ 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,7 +18,9 @@ use cosmic::{
Application, ApplicationExt, Element,
app::{self, Core, Task, context_drawer},
cosmic_config::{self, ConfigSet},
cosmic_theme, executor,
cosmic_theme,
desktop::fde::DesktopEntry,
executor,
iced::core::widget::operation::focusable::unfocus,
iced::runtime::{clipboard, task},
iced::widget::{button::focus, scrollable::AbsoluteOffset},
@ -97,7 +99,6 @@ 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},
};
@ -833,12 +834,9 @@ impl App {
// First launch apps that can be launched directly
if mime == "application/x-desktop" {
#[cfg(feature = "desktop")]
{
// Try opening desktop application
Self::launch_desktop_entries(&paths);
continue;
}
// 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 {
@ -899,10 +897,7 @@ 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() {
@ -1799,7 +1794,7 @@ impl App {
nav_model = nav_model.insert(|b| {
b.text(fl!("trash"))
.icon(icon::icon(Trash::icon_symbolic(16)))
.icon(icon::icon(tab::trash_helpers::trash_icon_symbolic(16)))
.data(Location::Trash)
.divider_above()
});
@ -2660,7 +2655,9 @@ impl Application for App {
));
}
if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() {
if matches!(location_opt, Some(Location::Trash))
&& !trash::os_limited::is_empty().unwrap_or(true)
{
items.push(cosmic::widget::menu::Item::Button(
fl!("empty-trash"),
None,
@ -4240,8 +4237,10 @@ 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(Trash::icon_symbolic(16)));
self.nav_model.icon_set(
entity,
icon::icon(tab::trash_helpers::trash_icon_symbolic(16)),
);
}
return Task::batch([self.rescan_trash(), self.update_desktop()]);
@ -4263,14 +4262,12 @@ impl Application for App {
.collect();
if !selected.is_empty() {
//TODO: batch rename
let mut last_name = String::new();
let tasks: Vec<_> = selected
let tasks = 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,
@ -4278,15 +4275,9 @@ impl Application for App {
dir,
}))
})
.collect();
let tasks = tasks.into_iter().chain([
widget::text_input::focus(self.dialog_text_input.clone()),
widget::text_input::select_until_last(
.chain(std::iter::once(widget::text_input::focus(
self.dialog_text_input.clone(),
&last_name,
'.',
),
]);
)));
return Task::batch(tasks);
}
}
@ -6160,7 +6151,6 @@ 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(),
@ -6804,7 +6794,14 @@ impl Application for App {
},
);
match (watcher_res, Trash::folders()) {
// 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()) {
(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
@ -6961,7 +6958,8 @@ impl Application for App {
|_| {
stream::channel(
1,
move |mut msg_tx: futures::channel::mpsc::Sender<_>| async move {
move |msg_tx: futures::channel::mpsc::Sender<_>| async move {
let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx));
tokio::task::spawn_blocking(move || {
match notify_rust::Notification::new()
.summary(&fl!("notification-in-progress"))
@ -6971,6 +6969,8 @@ 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,8 +189,6 @@ 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)]
{
@ -201,15 +199,11 @@ 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".into(),
));
return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink 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 = target.into_boxed_str();
let target_is_dir_from_archive =
archive.shared.files.contains_key(&target) && is_dir(&target);
let target_path = directory.as_ref().join(OsString::from(target.to_string()));
let target_is_dir = if target_is_dir_from_archive {
true

View file

@ -1,77 +0,0 @@
// 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,8 +441,13 @@ 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)]
@ -595,7 +600,6 @@ 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)),
);
@ -1958,16 +1962,6 @@ 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());
}
}
@ -2037,7 +2031,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,20 +3,14 @@
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;
pub mod config;
mod context_action;
use config::Config;
pub mod config;
pub mod dialog;
mod key_bind;
pub(crate) mod large_image;
@ -29,11 +23,13 @@ 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>;
@ -77,22 +73,7 @@ pub fn is_wayland() -> bool {
/// Runs application in desktop mode
#[rustfmt::skip]
pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
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();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
localize::localize();
@ -127,21 +108,7 @@ 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>> {
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();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
localize::localize();
@ -191,7 +158,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
}
if daemonize {
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
#[cfg(all(unix, not(target_os = "redox")))]
match fork::daemon(true, true) {
Ok(fork::Fork::Child) => (),
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),

View file

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

View file

@ -19,7 +19,6 @@ struct MimeIconKey {
struct MimeIconCache {
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
#[cfg(unix)]
shared_mime_info: xdg_mime::SharedMimeInfo,
}
@ -27,17 +26,10 @@ 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)
@ -61,16 +53,6 @@ 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>,
@ -118,13 +100,8 @@ 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::mpsc;
use tokio::sync::{Mutex, mpsc};
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
use crate::{
@ -199,7 +199,6 @@ 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,
@ -230,10 +229,7 @@ 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: std::sync::Weak<crate::channel::Sender<Event>>,
) -> gio::MountOperation {
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
let mount_op = gio::MountOperation::new();
mount_op.connect_ask_password(
move |mount_op, message, default_user, default_domain, flags| {
@ -256,9 +252,9 @@ fn mount_op(
.then_some(false),
};
let (auth_tx, mut auth_rx) = mpsc::channel(1);
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx));
}
event_tx
.send(Event::NetworkAuth(uri.clone(), auth, auth_tx))
.unwrap();
//TODO: async recv?
if let Some(auth) = auth_rx.blocking_recv() {
if auth.anonymous_opt == Some(true) {
@ -361,45 +357,37 @@ impl Item {
pub struct Gvfs {
command_tx: mpsc::UnboundedSender<Cmd>,
event_rx: Arc<crate::channel::Receiver<Event>>,
event_rx: Arc<Mutex<mpsc::UnboundedReceiver<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) = crate::channel::channel();
let event_tx = Arc::new(event_tx);
let (event_tx, event_rx) = mpsc::unbounded_channel();
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));
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_mount_added(move |_monitor, mount| {
log::info!("mount added {}", MountExt::name(mount));
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_mount_removed(move |_monitor, mount| {
log::info!("mount removed {}", MountExt::name(mount));
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
event_tx.send(Event::Changed).unwrap();
});
}
@ -407,27 +395,21 @@ impl Gvfs {
let event_tx = event_tx.clone();
monitor.connect_volume_changed(move |_monitor, volume| {
log::info!("volume changed {}", VolumeExt::name(volume));
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_volume_added(move |_monitor, volume| {
log::info!("volume added {}", VolumeExt::name(volume));
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
event_tx.send(Event::Changed).unwrap();
});
}
{
let event_tx = event_tx.clone();
monitor.connect_volume_removed(move |_monitor, volume| {
log::info!("volume removed {}", VolumeExt::name(volume));
if let Some(event_tx) = event_tx.upgrade() {
event_tx.send(Event::Changed);
}
event_tx.send(Event::Changed).unwrap();
});
}
@ -437,11 +419,7 @@ impl Gvfs {
items_tx.send(items(&monitor, sizes)).await.unwrap();
}
Cmd::Rescan => {
let Some(event_tx) = event_tx.upgrade() else {
return;
};
event_tx.send(Event::Items(items(&monitor, IconSizes::default())));
event_tx.send(Event::Items(items(&monitor, IconSizes::default()))).unwrap();
}
Cmd::Mount(mounter_item, complete_tx) => {
let MounterItem::Gvfs(ref item) = mounter_item else {
@ -493,9 +471,6 @@ 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(()));
@ -507,7 +482,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}"))
}}
}));
})).unwrap();
},
);
break;
@ -523,9 +498,6 @@ 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(()));
@ -536,7 +508,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}"))
}}
}));
})).unwrap();
}
);
}
@ -560,9 +532,6 @@ 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)
@ -571,7 +540,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}"))
}
}));
})).unwrap();
}
);
} else {
@ -627,7 +596,7 @@ impl Gvfs {
});
Self {
command_tx,
event_rx: Arc::new(event_rx),
event_rx: Arc::new(Mutex::new(event_rx)),
}
}
}
@ -701,7 +670,7 @@ impl Mounter for Gvfs {
let event_rx = self.event_rx.clone();
struct Wrapper {
command_tx: mpsc::UnboundedSender<Cmd>,
event_rx: Arc<crate::channel::Receiver<Event>>,
event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
}
impl Hash for Wrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
@ -725,7 +694,7 @@ impl Mounter for Gvfs {
MounterMessage,
>| async move {
command_tx.send(Cmd::Rescan).unwrap();
while let Some(event) = event_rx.recv().await {
while let Some(event) = event_rx.lock().await.recv().await {
match event {
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
Event::Items(items) => {

View file

@ -1,11 +1,7 @@
use atomic_float::AtomicF32;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::sync::Arc;
use std::sync::atomic::{self, AtomicU16};
use std::sync::{Arc, Mutex};
use tokio::sync::Notify;
#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ControllerState {
Cancelled,
Failed,
@ -15,8 +11,8 @@ pub enum ControllerState {
#[derive(Debug)]
struct ControllerInner {
state: AtomicU16,
progress: AtomicF32,
state: Mutex<ControllerState>,
progress: Mutex<f32>,
notify: Notify,
}
@ -31,8 +27,8 @@ impl Default for Controller {
Self {
primary: true,
inner: Arc::new(ControllerInner {
state: AtomicU16::new(ControllerState::Running.into()),
progress: AtomicF32::new(0.0),
state: Mutex::new(ControllerState::Running),
progress: Mutex::new(0.0),
notify: Notify::new(),
}),
}
@ -54,24 +50,19 @@ impl Controller {
}
pub fn progress(&self) -> f32 {
self.inner.progress.load(atomic::Ordering::Relaxed)
*self.inner.progress.lock().unwrap()
}
pub fn set_progress(&self, progress: f32) {
self.inner
.progress
.swap(progress, atomic::Ordering::Relaxed);
*self.inner.progress.lock().unwrap() = progress;
}
pub fn state(&self) -> ControllerState {
ControllerState::try_from(self.inner.state.load(atomic::Ordering::Relaxed))
.unwrap_or(ControllerState::Failed)
*self.inner.state.lock().unwrap()
}
pub fn set_state(&self, state: ControllerState) {
self.inner
.state
.store(state.into(), atomic::Ordering::Relaxed);
*self.inner.state.lock().unwrap() = state;
self.inner.notify.notify_waiters();
}
@ -95,35 +86,6 @@ 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,9 +22,6 @@ 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;
@ -114,7 +111,7 @@ async fn copy_or_move(
);
// Handle duplicate file names by renaming paths
let from_to_pairs_iter = paths
let mut from_to_pairs: Vec<(PathBuf, PathBuf)> = paths
.into_iter()
.zip(std::iter::repeat(to.as_path()))
.filter_map(|(from, to)| {
@ -132,46 +129,36 @@ 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;
}
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));
//TODO: use compio::fs::rename?
match fs::rename(from, to) {
Ok(()) => {
log::info!("renamed {} to {}", from.display(), to.display());
false
}
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))
}
Err(err) => {
log::info!(
"failed to rename {} to {}, fallback to recursive move: {}",
from.display(),
to.display(),
err
);
true
}
})
.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());
@ -229,7 +216,7 @@ pub async fn sync_to_disk(
}
}))
.buffer_unordered(32)
.collect::<()>()
.collect::<Vec<_>>()
.await;
// Sync directories to disk
@ -239,7 +226,7 @@ pub async fn sync_to_disk(
}
}))
.buffer_unordered(16)
.collect::<()>()
.collect::<Vec<_>>()
.await;
}
@ -1125,9 +1112,7 @@ impl Operation {
#[cfg(target_os = "macos")]
Self::Restore { .. } => {
// TODO: add support for macos
return Err(OperationError::from_msg(
"Restoring from trash is not supported on macos",
));
return OperationError::from_msg("Restoring from trash is not supported on macos");
}
#[cfg(not(target_os = "macos"))]
Self::Restore { items } => {

View file

@ -1,58 +0,0 @@
// 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,33 +1,15 @@
// 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 std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc};
use walkdir::WalkDir;
#[cfg(feature = "gvfs")]
use gio::prelude::FileExtManual;
use crate::operation::{OperationError, sync_to_disk};
#[derive(thiserror::Error, Debug)]
pub enum GioCopyError {
#[error("controller state")]
Controller(OperationError),
#[cfg(feature = "gvfs")]
#[error("gio copy failed")]
GLib(#[from] glib::Error),
}
use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};
pub enum Method {
Copy,
@ -331,28 +313,136 @@ impl Op {
})
}
async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result<bool, Box<dyn Error>> {
async fn run(
&mut self,
ctx: &mut Context,
mut 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 => {
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;
// 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_remove(&self.to);
return result;
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);
}
}
}
}
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? {
@ -430,268 +520,4 @@ 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, font,
Apply, Element, cosmic_theme,
desktop::fde::{DesktopEntry, get_languages_from_env},
font,
iced::core::{mouse::ScrollDelta, widget::tree},
iced::{
Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector,
@ -39,10 +39,9 @@ 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,
@ -53,6 +52,7 @@ 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,12 +557,6 @@ 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)) {
@ -576,12 +570,6 @@ 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,
@ -605,7 +593,6 @@ 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)) {
@ -721,9 +708,6 @@ 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,
@ -859,7 +843,6 @@ pub fn item_from_entry(
},
hidden,
location_opt: Some(Location::Path(path)),
image_dimensions: None,
mime,
icon_handle_grid,
icon_handle_list,
@ -913,9 +896,6 @@ 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,
@ -1181,7 +1161,133 @@ pub fn scan_search<F: Fn(SearchItem) -> bool + Sync>(
}
}
SearchLocation::Trash => {
Trash::scan_search(callback, &regex);
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;
}
}
}
}
}
@ -1319,15 +1425,15 @@ pub fn scan_desktop(
let display_name = Item::display_name(&name);
let metadata = ItemMetadata::SimpleDir {
entries: Trash::entries() as u64,
entries: trash_helpers::trash_entries() as u64,
};
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
(
"inode/directory".parse().unwrap(),
Trash::icon(sizes.grid()),
Trash::icon(sizes.list()),
Trash::icon(sizes.list_condensed()),
trash_helpers::trash_icon(sizes.grid()),
trash_helpers::trash_icon(sizes.list()),
trash_helpers::trash_icon(sizes.list_condensed()),
)
};
@ -1338,7 +1444,6 @@ pub fn scan_desktop(
metadata,
hidden: false,
location_opt: Some(Location::Trash),
image_dimensions: None,
mime,
icon_handle_grid,
icon_handle_list,
@ -1568,7 +1673,7 @@ impl Location {
// Search is done incrementally
Vec::new()
}
Self::Trash => Trash::scan(sizes),
Self::Trash => trash_helpers::scan_trash(sizes),
Self::Recents => scan_recents(sizes),
Self::Network(uri, _, _) => scan_network(uri, sizes),
};
@ -2214,7 +2319,6 @@ 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,
@ -2429,7 +2533,7 @@ impl Item {
let mode = metadata.mode();
let user_name = uzers::get_user_by_uid(metadata.uid())
let user_name = 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();
@ -2452,7 +2556,7 @@ impl Item {
)),
);
let group_name = uzers::get_group_by_gid(metadata.gid())
let group_name = 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();
@ -4340,7 +4444,6 @@ 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()
@ -6358,23 +6461,20 @@ impl Tab {
} else {
total_size = total_size.saturating_add(metadata.len());
}
#[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 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));
}
}
let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect();
@ -6613,13 +6713,13 @@ impl Tab {
// Determine effective memory budget based on image size
let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE {
match item.image_dimensions {
Some((width, height)) => {
match image::image_dimensions(&path) {
Ok((width, height)) => {
let (_use_dedicated, eff_mb, eff_jobs) =
should_use_dedicated_worker(width, height, max_mb, max_jobs);
(eff_mb, eff_jobs)
}
None => (max_mb, max_jobs),
Err(_) => (max_mb, max_jobs),
}
} else {
(max_mb, max_jobs)
@ -6662,10 +6762,6 @@ 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();
@ -6850,8 +6946,9 @@ impl Tab {
.await
.unwrap();
let (watch_tx, mut watch_rx) = tokio::sync::watch::channel(true);
let output = Arc::new(tokio::sync::Mutex::new(output));
{
let output = output.clone();
tokio::task::spawn_blocking(move || {
scan_search(
&search_location,
@ -6879,7 +6976,14 @@ impl Tab {
true
} else {
// Wake up update method
watch_tx.send(false).is_ok()
futures::executor::block_on(async {
output
.lock()
.await
.send(Message::SearchReady(false))
.await
})
.is_ok()
}
}
Err(_) => false,
@ -6892,16 +6996,13 @@ impl Tab {
search_location,
start.elapsed(),
);
});
}
while watch_rx.changed().await.is_ok() {
let is_ready = *watch_rx.borrow_and_update();
let _ = output.send(Message::SearchReady(is_ready)).await;
})
.await
.unwrap();
}
// Send final ready
let _ = output.send(Message::SearchReady(true)).await;
let _ = output.lock().await.send(Message::SearchReady(true)).await;
std::future::pending().await
},

View file

@ -1,12 +1,11 @@
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,
@ -94,7 +93,6 @@ 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)?;
@ -129,7 +127,6 @@ 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,7 +1,6 @@
// 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;

View file

@ -1,145 +0,0 @@
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 {}