Compare commits

..

15 commits

Author SHA1 Message Date
f2ca1d37a6 fix: absorb libcosmic theme-v2 API (nav_context_menu, app_popup LiveSettings)
Leyoda 2026 – GPLv3
2026-07-03 13:01:18 +02:00
50e0f5a19c chore: use local COSMIC support crates 2026-06-13 08:39:43 +02:00
Leyoda
f4546017a9 yoda: cosmic-files customizations (squashed 21 commits)
This commit squashes the 21 local commits that customize cosmic-files for
the yoda stack, to allow a clean rebase on upstream/master.

Original commits (chronological):

- 9bcfe7a Cargo.toml: patch libcosmic via local path for dev builds
- 04abd13 yoda: depend on libcosmic-yoda (path) instead of upstream libcosmic
- 02adcc3 lockfile: libcosmic-yoda 0.1.0-yoda -> 0.1.0-yoda.2
- a025fd6 yoda: prefer cosmic-yoterm over upstream cosmic-term in terminal fallback
- e8d62ae yoda: add "Always use this app" toggle to OpenWith dialog
- 8fb2b15 yoda wayland-v5: redirect window_clipboard + cosmic-text to local forks
- 0595296 yoda: Dolphin-style quick actions toolbar under the headerbar
- 4b6d345 yoda: fix missing rename icon in toolbar
- 8b51af1 yoda: use pencil-symbolic for the Rename toolbar button
- 33a5c8f yoda: phase 2 - customizable toolbar (settings toggles per button)
- 1cf17dc yoda: phase 3 - drag-drop toolbar editor in Settings
- 11d4357 yoda: add up/down buttons next to drag handle in toolbar editor
- af843d2 yoda: direct drag-drop reorder on the toolbar itself
- 94c3e6c yoda: toolbar as segmented_button for working drag reorder
- f053819 yoda: toolbar icon-only + clean visual (Control style, 32px squares)
- 338354c Improve initial directory listing latency
- d080bc8 Resolve cosmic-files warnings without masking
- 69c35ab yoda: switch window_clipboard patch to public Forgejo fork
- 35e115f yoda: switch cosmic-text patch to public Forgejo fork
- 6f3adcd chore: clean feature-gated warnings
- 57ab1ec fix: clean files warnings for terminal build

Original tip preserved as tag backup/pre-rebase-upstream-20260524.
2026-06-13 08:39:43 +02:00
Hojjat
55a6f46f73 chore: update libcosmic to fix item highlight in wayland context menu 2026-06-13 08:39:43 +02:00
Ashley Wulber
a84524ef70 fix: cosmic-protocols 2026-06-13 08:39:43 +02:00
Hojjat
2480d5cb0d chore: fixes after libcosmic update 2026-06-13 08:39:43 +02:00
Jeremy Soller
98778333f4 Epoch 1.0.14 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-06-13 08:39:43 +02:00
Hosted Weblate
0331bf550c i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Mattias Eriksson <snaggen@gmail.com>
Co-authored-by: Thomas Worofsky <thomas.worofsky@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: oddib <60390653+oddib@users.noreply.github.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files
2026-06-13 08:39:43 +02:00
dcd936f7dd chore: resolve local graphics dependencies 2026-05-25 19:37:22 +02:00
9dbe3a378d chore: use local glyphon lockfile 2026-05-25 18:28:56 +02:00
36ade6cf65 chore: use local COSMIC support crates 2026-05-25 18:10:22 +02:00
d6c245d809 fix(keybinds): adapt key_bind.matches to new 3-arg signature
Upstream libcosmic added an Option<&Physical> third parameter to
KeyBind::matches for physical-key matching. We pass None at every call
site since these paths only have the logical key available.

Also refresh Cargo.lock to satisfy the libcosmic-yoda squash rebase
absorbed on 2026-05-25.

Leyoda 2026 – GPLv3
2026-05-25 14:27:36 +02:00
17490bfbca chore: use local cosmic-text checkout 2026-05-25 11:34:38 +02:00
54862ba701 yoda: refresh local dbus binding lock and warnings 2026-05-25 09:55:23 +02:00
Leyoda
f1b1f8d799 yoda: cosmic-files customizations (squashed 21 commits)
This commit squashes the 21 local commits that customize cosmic-files for
the yoda stack, to allow a clean rebase on upstream/master.

Original commits (chronological):

- 9bcfe7a Cargo.toml: patch libcosmic via local path for dev builds
- 04abd13 yoda: depend on libcosmic-yoda (path) instead of upstream libcosmic
- 02adcc3 lockfile: libcosmic-yoda 0.1.0-yoda -> 0.1.0-yoda.2
- a025fd6 yoda: prefer cosmic-yoterm over upstream cosmic-term in terminal fallback
- e8d62ae yoda: add "Always use this app" toggle to OpenWith dialog
- 8fb2b15 yoda wayland-v5: redirect window_clipboard + cosmic-text to local forks
- 0595296 yoda: Dolphin-style quick actions toolbar under the headerbar
- 4b6d345 yoda: fix missing rename icon in toolbar
- 8b51af1 yoda: use pencil-symbolic for the Rename toolbar button
- 33a5c8f yoda: phase 2 - customizable toolbar (settings toggles per button)
- 1cf17dc yoda: phase 3 - drag-drop toolbar editor in Settings
- 11d4357 yoda: add up/down buttons next to drag handle in toolbar editor
- af843d2 yoda: direct drag-drop reorder on the toolbar itself
- 94c3e6c yoda: toolbar as segmented_button for working drag reorder
- f053819 yoda: toolbar icon-only + clean visual (Control style, 32px squares)
- 338354c Improve initial directory listing latency
- d080bc8 Resolve cosmic-files warnings without masking
- 69c35ab yoda: switch window_clipboard patch to public Forgejo fork
- 35e115f yoda: switch cosmic-text patch to public Forgejo fork
- 6f3adcd chore: clean feature-gated warnings
- 57ab1ec fix: clean files warnings for terminal build

Original tip preserved as tag backup/pre-rebase-upstream-20260524.
2026-05-24 21:27:30 +02:00
25 changed files with 782 additions and 1686 deletions

868
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.2.0"
version = "1.0.14"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"
@ -11,7 +11,7 @@ anyhow = "1"
jiff = "0.2"
jiff-icu = "0.2"
icu = { version = "2.2.0", features = ["compiled_data"] }
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c253ec1", optional = true }
cctk = { path = "../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit", optional = true }
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
dirs = "6.0.0"
gio = { version = "0.21", optional = true }
@ -33,7 +33,7 @@ shlex = { version = "1.3" }
tempfile = "3"
tikv-jemallocator = { version = "0.6", optional = true }
tokio = { version = "1", features = ["process", "sync"] }
trash = "5.2.6"
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" }
url = "2.5"
walkdir = "2.5.0"
wayland-client = { version = "0.31.14", optional = true }
@ -56,7 +56,6 @@ slotmap = "1.1.1"
recently-used-xbel = "1.2.0"
zip = "8"
md-5 = "0.10.6"
sha2 = "0.10"
png = "0.18"
jxl-oxide = { version = "0.12.5", features = ["image"] }
num_cpus = "1.17.0"
@ -151,17 +150,6 @@ tokio = { version = "1", features = ["rt", "macros"] }
# no [patch] block needed anymore. Keeping the block below would be a no-op
# since nothing in the dep graph still asks for pop-os/libcosmic.git.
# Yoda wayland cut: redirect window_clipboard (x11 gated behind opt-in
# feature) to our public Forgejo fork and cosmic-text to the local checkout
# carrying PR#503 plus the EAW monospace width fix. The window_clipboard patch
# is needed to consolidate the
# upstream pop-os/libcosmic chain (pulled by cosmic-settings-daemon) onto
# the same fork iced uses, otherwise cargo compiles two versions.
[patch.'https://github.com/pop-os/window_clipboard.git']
window_clipboard = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" }
dnd = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" }
mime = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch = "yoda-x11-optional" }
[patch.'https://github.com/pop-os/cosmic-text.git']
cosmic-text = { path = "../cosmic-text" }

View file

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

24
debian/changelog vendored
View file

@ -1,27 +1,3 @@
cosmic-files (1.2.0) noble; urgency=medium
* Epoch 1.2.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 30 Jun 2026 12:21:07 -0600
cosmic-files (1.1.0) noble; urgency=medium
* Epoch 1.1.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 23 Jun 2026 08:47:18 -0600
cosmic-files (1.0.16) noble; urgency=medium
* Epoch 1.0.16 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 09 Jun 2026 10:26:05 -0600
cosmic-files (1.0.15) noble; urgency=medium
* Epoch 1.0.15 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 02 Jun 2026 11:02:29 -0600
cosmic-files (1.0.14) noble; urgency=medium
* Epoch 1.0.14 version update

View file

@ -1,11 +1,11 @@
cosmic-files = Fitxers del COSMIC
empty-folder = Carpeta buida
empty-folder-hidden = Carpeta buida (conté elements ocults)
empty-folder-hidden = Carpeta buida (té elements ocults)
no-results = No s'ha trobat cap resultat
filesystem = Sistema de fitxers
home = Inici
networks = Xarxes
notification-in-progress = Operacions de fitxers en curs
notification-in-progress = Hi ha operacions amb fitxers en curs.
trash = Paperera
recents = Recents
undo = Desfés
@ -29,14 +29,14 @@ details = Detalls
dismiss = Descarta el missatge
operations-running =
{ $running } { $running ->
[one] operació
[one] operacion
*[other] operacions
} en curs ({ $percent }%)...
operations-running-finished =
{ $running }{ $running ->
[one] operació
[one] operacion
*[other] operacions
} en curs ({ $percent }%), { $finished } acabada/es...
} en curs ({ $percent }%), { $finished } acabat...
pause = Pausa
resume = Reprèn
@ -54,7 +54,7 @@ extract-password-required = Cal una contrasenya
## Empty Trash Dialog
empty-trash = Buida la paperera
empty-trash-warning = Els elements dins la Paperera s'eliminaran permanentment
empty-trash-warning = Voleu suprimir permanentment tots els fitxers de la paperera?
## Mount Error Dialog
@ -64,12 +64,12 @@ mount-error = No es pot accedir a la unitat
create-new-file = Crea un nou fitxer
create-new-folder = Crea una nova carpeta
file-name = Nom del fitxer
file-name = Nom del fixer
folder-name = Nom de la carpeta
file-already-exists = Ja existeix un fitxer amb aquest nom
folder-already-exists = Ja existeix una carpeta amb aquest nom
name-hidden = Els noms que comencin amb "." s'ocultaran.
name-invalid = El nom no pot ser "{ $filename }"
file-already-exists = Ja existeix un fitxer amb aquest nom.
folder-already-exists = Ja existeix una carpeta amb aquest nom.
name-hidden = Els noms que comencin amb "." seran ocults.
name-invalid = El nom no pot ser "{ $filename }".
name-no-slashes = El nom no pot contenir barres.
## Open/Save Dialog
@ -353,13 +353,3 @@ sort-oldest-first = Primer més antics
sort-smallest-to-largest = De petit a gran
sort-largest-to-smallest = De gran a petit
run = Executa
extract-to-title = Extreu a una carpeta
empty-trash-title = Voleu buidar la paperera?
extract-to = Extreu a...
delete = Suprimeix
comment = Gestor de fitxers de l'escriptori COSMIC
copy-to-title = Escolliu el destí de la còpia
copy-to-button-label = Copia
move-to-title = Escolliu el destí del trasllat
move-to-button-label = Mou
keywords = Carpeta;Fitxers;Arxius;Gestor;Explorador;

View file

@ -309,9 +309,6 @@ item-created = Created: {$created}
item-modified = Modified: {$modified}
item-accessed = Accessed: {$accessed}
calculating = Calculating...
checksum = {$kind} checksum
calculate = Calculate
error = Error
## Settings
settings = Settings

View file

@ -17,7 +17,7 @@ desktop-view-options = Asztali nézet beállításai…
show-on-desktop = Megjelenítés az asztalon
desktop-folder-content = Asztal mappa tartalma
mounted-drives = Csatolt meghajtók
trash-folder-icon = Kuka ikon
trash-folder-icon = Kuka ikonja
icon-size-and-spacing = Ikonméret és távolság
icon-size = Ikonméret
grid-spacing = Rácsköz
@ -57,8 +57,8 @@ extract-to-title = Kibontási cél kiválasztása
## Empty Trash Dialog
empty-trash = A Kuka ürítése
empty-trash-warning = A Kukában lévő összes elem véglegesen törölve lesz
empty-trash = Kuka ürítése
empty-trash-warning = A kukában lévő összes elem véglegesen törölve lesz
## Mount Error Dialog
@ -103,7 +103,7 @@ related-apps = Hasonló alkalmazások
selected-items = A(z) { $items } kijelölt elem
permanently-delete-question = Végleges törlés?
delete = Törlés
permanently-delete-warning = { $target } véglegesen törölve lesz. A művelet nem vonható vissza.
permanently-delete-warning = { $target } véglegesen törölve lesz. A művelet nem visszavonható.
## Rename Dialog
@ -251,13 +251,13 @@ deleting =
{ $items } { $items ->
[one] elem
*[other] elem
} törlése a Kukából ({ $progress })…
} törlése a kukából ({ $progress })…
deleted =
{ $items } { $items ->
[one] elem
*[other] elem
} törölve a Kukából
emptying-trash = { trash } ürítése ({ $progress })…
} törölve a kukából
emptying-trash = { trash } kiürítése ({ $progress })…
emptied-trash = { trash } kiürítve
extracting =
{ $items } { $items ->
@ -309,12 +309,12 @@ restoring =
{ $items } { $items ->
[one] elem
*[other] elem
} visszaállítása a Kukából ({ $progress })…
} visszaállítása a kukából ({ $progress })…
restored =
{ $items } { $items ->
[one] elem
*[other] elem
} visszaállítva a Kukából
} visszaállítva a kukából
unknown-folder = ismeretlen mappa
## Open with
@ -349,7 +349,7 @@ light = Világos
### Type to Search
type-to-search = Gépeléssel keresés
type-to-search-recursive = Keresés a jelenlegi mappában és az almappákban
type-to-search-recursive = Keresés a jelenlegi mappában és almappákban
type-to-search-enter-path = Elérési út megnyitása
# Context menu
add-to-sidebar = Hozzáadás az oldalsávhoz
@ -360,8 +360,8 @@ extract-here = Kibontás
new-file = Új fájl…
new-folder = Új mappa…
open-in-terminal = Megnyitás a terminálban
move-to-trash = Áthelyezés a Kukába
restore-from-trash = Visszaállítás a Kukából
move-to-trash = Kukába helyezés
restore-from-trash = Visszaállítás a kukából
remove-from-sidebar = Eltávolítás az oldalsávról
sort-by-name = Név szerinti rendezés
sort-by-modified = Módosítás szerinti rendezés
@ -372,7 +372,7 @@ remove-from-recents = Eltávolítás a legutóbbiak közül
## Desktop
change-wallpaper = Háttérkép cseréje…
desktop-appearance = Asztal megjelenése
desktop-appearance = Asztali megjelenés…
display-settings = Kijelzőbeállítások…
# Menu
@ -422,7 +422,7 @@ sort-largest-to-smallest = Legnagyobbtól a legkisebbig
repository = Tároló
support = Támogatás
progress-failed = { $percent }%, sikertelen
empty-trash-title = Kiüríted a Kukát?
empty-trash-title = Kuka ürítése?
type-to-search-select = Kijelöli az első egyező fájlt vagy mappát
pasted-image = Beillesztett kép
pasted-text = Beillesztett szöveg

View file

@ -327,4 +327,3 @@ context-action-confirm-warning =
[one] item
*[other] item
}.
rename-confirm = Ganti nama

View file

@ -327,4 +327,3 @@ context-action-confirm-warning =
*[other] нәрсеге
}.
run = Орындау
rename-confirm = Атын өзгерту

View file

@ -1,330 +0,0 @@
sort-newest-first = Pêşî ya herî nû
connect = Girê bide
deleted =
{ $items } hat jêbirin{ $items ->
[one] hêman
*[other] hêman
} ji { trash }
dismiss = Peyamê paşguh bike
list-view = Dîtina lîsteyê
copy_noun = Jê bigire
favorite-path-error = Şaşetiya vekirina rêgehê
progress = { $percent }%
open-file = Pelê veke
remove-from-sidebar = Ji benda kêlekê rake
related-apps = Bernameyên têkildar
network-drive-error = Nikaribû bigihêje ajokarê torê
gallery-preview = Pêşdîtina galeriyê
sort-smallest-to-largest = Biçûktir bo mezintir
zoom-in = Nêzîk bike
select-all = Hemûyan hilbijêre
removing-from-recents =
{ $items } tê rakirin { $items ->
[one] hêman
*[other] hêman
} ji { recents }
password = Borînpeyv
cosmic-files = Pelên COSMIC
emptying-trash = { trash } tê valakirin({ $progress })...
new-window = Çarçoveya nû
remove = Rake
zoom-out = Dûr bike
compressing =
{ $items } tê guvaştin { $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo "{ $to }" ({ $progress })...
move-to-trash = Bilivîne bo jêbirdankê
menu-about = Derbarê Pelên COSMIC...
file-already-exists = Pelek bi heman navî jixwe heye
setting-executable-and-launching = "{ $name }" wekî pelê xebitandinê tê sazkirin û xebitandin
open-multiple-files = Gelek pelan veke
default-size = Mezinahiya berdest
name-hidden = Navên ku bi "." tê destpêkirin wê werin veşartin
extracted =
{ $items } hate derxistin { $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo "{ $to }"
create-new-folder = Peldanka nû biafrîne
folder-already-exists = Peldankek bi heman navî jixwe heye
create-archive = Erşîvê biafrîne
permanently-delete-warning = { $target } wê bi mayînde were jêbirin û nayê vegerandin.
favorite-path-error-description =
Nikaribû "{ $path }" veke.
"{ $path }" dibe ku tune be yan jî mafê te yê
vekirina wê tune be.
Tu dixwazî wê ji benda kêlekê rakî?
empty-trash-warning = Hêmanên di jêbirdankê de wê bi mayînde werin jêbirin
other-apps = Bernameyên din
set-permissions = Maf hatine sazkirin ji bo "{ $name }" bo { $mode }
quit = Biqedîne
sort-by-size = Li gorî mezinahiyê rêz bike
rename = Navê biguhêrîne...
empty-folder-hidden = Peldanka vala (tiştên veşartî tê de hene)
keep = Bihêle
operations-running =
{ $running } { $running ->
[one] pêvajo
*[other] pêvajo
} dixebite ({ $percent }%)...
permanently-deleting =
{ $items } bi mayînde tê jêbirin { $items ->
[one] hêman
*[other] hêman
}
edit = Biguhêrîne
copy = Jê bigire
none = Ne yek
no-results = Tu encam nehatin dîtin
theme = Rûkar
compressed =
{ $items } hate guvaştin{ $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo "{ $to }"
appearance = Xuyang
rename-folder = Navê peldankê biguherîne
new-file = Pela nû...
close-tab = Rûgerê bigire
open-in-terminal = Di termînalê de veke
open-multiple-folders = Gelek peldankan veke
remember-password = Borînpeyvê bi bîr bîne
username = Navê bikarhêner
light = Ronî
extract-to = Derxîne bo...
add-network-drive = Ajokarê torê tevlî bike
copying =
{ $items } tê jêgirtin{ $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo "{ $to }" ({ $progress })...
create-new-file = Pelê nû biafrîne
sort-by-trashed = Li gorî demê rêz bike
support = Piştgirî
try-again = Dîsa hewl bide
eject = Biavêje
copied =
{ $items } hate jêgirtin { $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo "{ $to }"
open-in-new-window = Di çarçoveyeke nû de veke
empty-folder = Peldanka vala
empty-trash = Jêbirdankê vala bike
sort-by-modified = Li gorî guhertinê rêz bike
list-directories-first = Pêşî rêgehan lîste bike
folder-name = Navê peldankê
browse-store = { $store } bigere
remove-from-recents = Ji dawiyê rake
paste = Pêve bike
empty-trash-title = Jêbirdankê vala bike?
menu-settings = Sazkarî...
moving =
{ $items } tê livandin{ $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo "{ $to }" ({ $progress })...
change-wallpaper = Wêneyê dîwêr biguhêrîne...
network-drive-description =
Di nav navnîşanên rajekarê de pêşgira protokolekê û navnîşanek heye.
Mînak: ssh://192.168.0.1, ftp://[2001:db8::1]
deleting =
{ $items } tê jêbirin { $items ->
[one] hêman
*[other] hêman
} ji { trash } ({ $progress })...
single-click = Tikandina yekane bo vekriinê
view = Dîtin
setting-permissions = Maf tên sazkirin ji bo "{ $name }" bo { $mode }
creating = "{ $name }" tê afirandin di "{ $parent }" de
execute-only = Tenê xebitandin
open-item-location = Cihê hêmanê veke
type-to-search = Bo lêgerînê binivîse
set-executable-and-launched = "{ $name }" wekî pelê xebitandinê hat sazkirin û xebitandin
mounted-drives = Ajokarên siwarkirî
sort-a-z = A-Z
mount-error = Nikare bigihêje ajokerê
extract-here = Derxîne
grid-view = Dîtina torê
add-to-sidebar = Tevlî benda kêlekê bike
dark = Tarî
notification-in-progress = Pêvajoyên pelê didome
home = Rûpela sereke
sort-z-a = Z-A
type-to-search-recursive = Di peldank û binpeldankên heyî de digere
history = Dîrok
progress-paused = { $percent }%, hate rawestandin
desktop-view-options = Vebijêrkên dîtina sermaseyê...
show-on-desktop = Li ser sermaseyê nîşan bide
cancelled = Têkbirî
new-folder = Peldankan nû...
match-desktop = Li gorî sermaseyê
name-no-slashes = Nav nabe ku "/" tê de hebe
operations-running-finished =
{ $running } { $running ->
[one] pêvajo
*[other] pêvajo
} dixebite ({ $percent }%), { $finished } qediya...
sort-by-name = Li gorî navê rêz bike
edit-history = Dîrokê biguhêrînê
show-hidden-files = Pelên veşartî nîşan bide
progress-failed = { $percent }%, têk çû
trash-folder-icon = Îkona peldanka jêbirdankê
extract-to-title = Derxîne bo peldankê
icon-size = Mezinahiya îkonê
open-with-title = Tu çawa dixwazî "{ $name }" vekî?
extracting =
{ $items } tê derxistin { $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo"{ $to }" ({ $progress })...
settings = Sazkarî
complete = Qediya
permanently-delete-question = Bi mayînde jê bibe?
extract-password-required = Borînpeyv pêdivî ye
pending = Li bendê ye
desktop-folder-content = Naveroka peldanka sermaseyê
no-history = Tu hêman di dîrok de tune ne.
open-folder = Peldankê veke
emptied-trash = { trash } hate valakirin
rename-file = Navê pelê biguherîne
sort-largest-to-smallest = Mezintir bo biçûktir
restore-from-trash = Ji jêbirdankê vegerîne
cut = Jê bike
moved =
{ $items } hate livandin { $items ->
[one] hêman
*[other] hêman
} ji "{ $from }" bo "{ $to }"
progress-cancelled = { $percent }%, hat têkbirin
open-in-new-tab = Di rûgerê nû de veke
file = Pel
file-name = Navê pelê
save-file = Pelê tomar bike
created = "{ $name }" hate afirandin di "{ $parent }" de
delete-permanently = Bi mayînde jê bibe
display-settings = Sazkariyên dîmenderê...
new-tab = Rûgera nû
failed = Têk çû
name-invalid = Nav nabe ku \"{ $filename }\" be
desktop-appearance = Xuyanga sermaseyê...
comment = Rêveberê pelê ji bo sermaseya COSMIC
keywords = Rêveber;Peldank;
filesystem = Pergala pelê
networks = Tor
recents = Dawî
open = Veke
cancel = Têk bibe
trash = Jêbirdank
undo = Vegerîne
today = Îro
name = Nav
modified = Guhertî
size = Mezinahî
details = Hûrgilî
pause = Rawestîne
resume = Bidomîne
copy-to-title = Rêgeha jêgirtinê hilbijêre
copy-to-button-label = Jê bigire
move-to-title = Rêgeha livandinê hilbijêre
move-to-button-label = Bilivîne
create = Biafrîne
save = Tomar bike
context-action = Çalakiya naverokê
context-action-confirm-title = "{ $name }" bixebtîne?
context-action-confirm-warning =
Ev ê li ser { $items } { $items ->
[one] hêman
*[other] hêman
} bixebite.
run = Bixebitîne
selected-items = { $items } hêmanên hilbijartî
delete = Jê bibe
rename-confirm = Navê biguherîne
pasted-image = Wêneya pêvekirî
pasted-text = Nivîsa pêvekirî
pasted-video = Vîdiyoya pêvekirî
copy-path = Rêgehê jê bigire
show-recents = Peldanka dawî di benda kêlekê de
clear-recents-history = Dîroka dawî pak bike
compress = Biguvşîne...
copy-to = Jê bigire bo...
move-to = Bilivîne bo...
default-app = { $name } (berdest)
renamed = Nav hate guhertin ji "{ $from }" bo "{ $to }"
read-execute = Xwendin û xebitandin
item-modified = Guhertin: { $modified }
reload-folder = Peldankê ji nû ve bar bike
restoring =
{ $items } tê vegerandin { $items ->
[one] hêman
*[other] hêman
} ji { trash } ({ $progress })...
icon-size-and-spacing = Mezinahiya îkonan û valahiya di navbera wan de
type-to-search-enter-path = Riya rêgehê yan jî pelê têdixîne
menu-open-with = Veke bi...
original-file = Pelê resen
read-write-execute = Xwendin, nivîsandin û xebitandin
calculating = Tê jimartin...
item-size = Mezinahî: { $size }
connecting = Tê girêdan...
read-write = Xwendin û nivîsandin
items = Hêman: { $items }
type = Cûre: { $mime }
replace-warning = Tu dixwazî bi ya ku tu niha tomar dikî biguhêrînî? Naveroka berê wê were jêbirin.
show-details = Hûrgiliyan nîşan bide
grid-spacing = Valahiya torê
sort-oldest-first = Pêşî ya herî kevn
repository = Embar
replace-warning-operation = Tu dixwazî biguherînî? Naveroka berê wê were jêbirin.
other = Ên din
read-only = Tenê xwendin
enter-server-address = Navnîşana rajekarê têxîne
connect-anonymously = Bi awayekî anonîm girê bide
group = Kom
apply-to-all = Bo hemûyan bisepîne
skip = Derbas bike
replace-with = Biguhêrîne bi
owner = Xwedî
set-and-launch = Saz bike û dest pê bike
removed-from-recents =
{ $items } hate rakirin { $items ->
[one] hêman
*[other] hêman
} ji { recents }
item-created = Afirandin:{ $created }
network-drive-schemes =
Protokolên berdest,pêşgirê
AppleTalk,afp://
Protokola şandina pelê,ftp:// an ftps://
Pergala pelê torê,nfs://
Astengiya peyama rajekarê,smb://
SSH protokola şandina pelê,sftp:// an ssh://
WebDAV, dav:// an davs://
set-executable-and-launch = Wekî pelê xebitandinê saz bike û bixebitîne
restored =
{ $items } hate vegerandin { $items ->
[one] hêman
*[other] hêman
} ji { trash }
domain = Navper
sort = Rêz bike
item-accessed = Gihîştin: { $accessed }
open-with = Veke bi
keep-both = Herduyan bihêle
permanently-deleted =
{ $items } bi mayînde hate jêbirin{ $items ->
[one] hêman
*[other] hêman
}
write-execute = Nivîsandin û xebitandin
renaming = Navê ji "{ $from }" biguhêrîne bo "{ $to }"
set-executable-and-launch-description = Tu dixwazî "{ $name }" wekî pelê xebitandinê saz bikî û bixebtînî?
replace = Biguhêrîne
replace-title = "{ $filename }" jixwe di vê cihê de heye
unknown-folder = Peldanka nenas
write-only = Tenê nivîsandin
trashed-on = Li Jêbirdankê hatiye avêtin
mixed = Tevlihev
type-to-search-select = Pel an peldanka yekem a lihevhatî hildibijêre

View file

@ -1,12 +1,12 @@
cosmic-files = Ficheiros COSMIC
comment = Gerenciador de arquivos do COSMIC
keywords = Pasta;Gestor;
keywords = Pastas;Gerenciador;Arquivos;Gestor;Explorer;
empty-folder = Pasta vazia
empty-folder-hidden = Pasta vazia (tem ficheiros ocultos)
no-results = Nenhum resultado encontrado
filesystem = Sistema de ficheiros
home = Pasta Pessoal
notification-in-progress = Operações em curso
notification-in-progress = Operações em curso.
trash = Lixo
undo = Anular
# List view
@ -204,7 +204,7 @@ desktop-view-options = Opções de visualização da área de trabalho...
show-on-desktop = Mostrar na área de trabalho
desktop-folder-content = Conteúdo da pasta da área de trabalho
mounted-drives = Dispositivos montados
trash-folder-icon = Ícone da pasta lixo
trash-folder-icon = Ícone do lixo
icon-size-and-spacing = Tamanho e espaçamento do ícone
icon-size = Tamanho do ícone
grid-spacing = Espaçamento entre ícones

View file

@ -327,4 +327,3 @@ compress = Сажми...
copy-to = Умножи у...
move-to = Помери у...
copy-path = Умножи путању
rename-confirm = Преименуј

View file

@ -388,4 +388,3 @@ context-action-confirm-warning =
}.
context-action = Контекстна дія
rename-confirm = Перейменувати
mixed = Змішано

View file

@ -48,7 +48,7 @@ build-debug *args:
# Compiles with release profile
build-release *args: (build-debug '--release' args)
# Compiles applet with release profile
build-release-applet *args:
cargo build --package {{applet-name}} --release {{args}}
@ -111,7 +111,7 @@ uninstall:
vendor:
#!/usr/bin/env bash
mkdir -p .cargo
cargo vendor --locked --sync Cargo.toml | head -n -1 > .cargo/config.toml
cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config.toml
echo 'directory = "vendor"' >> .cargo/config.toml
echo >> .cargo/config.toml
echo '[env]' >> .cargo/config.toml

View file

@ -73,7 +73,7 @@ use crate::config::{
use crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
use crate::key_bind::key_binds;
use crate::localize::LANGUAGE_SORTER;
use crate::mime_app::{self, MimeApp, MimeAppCache, MimeAppMatch};
use crate::mime_app::{MimeApp, MimeAppCache};
use crate::mounter::{
MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage,
};
@ -260,8 +260,6 @@ pub enum Action {
HistoryPrevious,
ItemDown,
ItemLeft,
ItemPageDown,
ItemPageUp,
ItemRight,
ItemUp,
LocationUp,
@ -335,8 +333,6 @@ impl Action {
Self::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious),
Self::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown),
Self::ItemLeft => Message::TabMessage(entity_opt, tab::Message::ItemLeft),
Self::ItemPageDown => Message::TabMessage(entity_opt, tab::Message::ItemPageDown),
Self::ItemPageUp => Message::TabMessage(entity_opt, tab::Message::ItemPageUp),
Self::ItemRight => Message::TabMessage(entity_opt, tab::Message::ItemRight),
Self::ItemUp => Message::TabMessage(entity_opt, tab::Message::ItemUp),
Self::LocationUp => Message::TabMessage(entity_opt, tab::Message::LocationUp),
@ -526,7 +522,6 @@ pub enum Message {
PendingPauseAll(bool),
PermanentlyDelete(Option<Entity>),
Preview(Option<Entity>),
ReloadMimeAppCache,
ReorderTab(ReorderEvent),
RescanRecents,
RescanTrash,
@ -686,7 +681,7 @@ pub enum DialogPage {
path: PathBuf,
mime: mime_guess::Mime,
selected: usize,
store_opt: Option<Arc<MimeApp>>,
store_opt: Option<MimeApp>,
/// When true, the chosen app is written to mimeapps.list as the
/// default handler for `mime` after it spawns.
set_default: bool,
@ -785,6 +780,13 @@ impl DialogPages {
pub struct FavoriteIndex(usize);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MimeAppMatch {
Exact,
Related,
Other,
}
pub struct MounterData(MounterKey, MounterItem);
#[derive(Clone, Debug)]
@ -1012,6 +1014,7 @@ impl App {
#[cfg(feature = "desktop")]
fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {
use crate::mime_app;
use cosmic::desktop::fde::DesktopEntry;
let locales = cosmic::desktop::fde::get_languages_from_env();
@ -1139,12 +1142,6 @@ impl App {
.and_then(|first| first.as_ref().parent())
.map(Path::to_path_buf)
{
let mut tasks = Vec::new();
if let Some(old_dialog) = self.file_dialog_opt.take() {
let old_id = old_dialog.window_id();
self.windows.remove(&old_id);
tasks.push(window::close(old_id));
}
let (mut dialog, dialog_task) = Dialog::new(
DialogSettings::new()
.kind(DialogKind::OpenFolder)
@ -1161,9 +1158,7 @@ impl App {
))),
);
self.file_dialog_opt = Some(dialog);
tasks.push(set_title_task);
tasks.push(dialog_task);
Task::batch(tasks)
Task::batch([set_title_task, dialog_task])
} else {
Task::none()
}
@ -2574,6 +2569,45 @@ impl App {
col.into()
}
fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> {
let mut results = Vec::new();
let mut dedupe = FxHashSet::default();
// start with exact matches
results.extend(
self.mime_app_cache
.get(mime_type)
.iter()
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Exact)),
);
// grab matches based off of subclass / parent mime type
if let Some(parent_types) = mime_icon::parent_mime_types(mime_type) {
for parent_type in parent_types {
results.extend(
self.mime_app_cache
.get(&parent_type)
.iter()
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Related)),
);
}
}
// Add other apps
results.extend(
self.mime_app_cache
.apps()
.iter()
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Other)),
);
results
}
// Update favorites based on renaming or moving dirs.
fn update_favorites(&mut self, path_changes: &[(impl AsRef<Path>, impl AsRef<Path>)]) -> bool {
let mut favorites_changed = false;
@ -3489,7 +3523,7 @@ impl Application for App {
set_default,
..
} => {
let available_apps = self.mime_app_cache.get_apps_for_mime(&mime, true);
let available_apps = self.get_apps_for_mime(&mime);
if let Some((app, _)) = available_apps.get(selected) {
if let Some(mut command) =
@ -3629,7 +3663,7 @@ impl Application for App {
if self.core.main_window_id() == Some(window_id) || in_surface_ids {
let entity = self.tab_model.active();
for (key_bind, action) in &self.key_binds {
if key_bind.matches(modifiers, &key, Some(&physical_key)) {
if key_bind.matches(modifiers, &key, None) {
return self.update(action.message(Some(entity)));
}
}
@ -4497,9 +4531,6 @@ impl Application for App {
let paths: Box<[_]> = self.selected_paths(entity_opt).collect();
return self.operation(Operation::RemoveFromRecents { paths });
}
Message::ReloadMimeAppCache => {
self.mime_app_cache.reload();
}
Message::RescanRecents => {
return self.rescan_recents();
}
@ -6322,7 +6353,7 @@ impl Application for App {
};
let mut column = widget::list_column();
let available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
let available_apps = self.get_apps_for_mime(mime);
let item_height = 32.0;
let mut displayed_default = false;
let mut last_kind = MimeAppMatch::Exact;
@ -6343,8 +6374,8 @@ impl Application for App {
widget::mouse_area(
widget::button::custom(
widget::row::with_children([
icon(app.icon()).size(32).into(),
if app.is_default(mime) && !displayed_default {
icon(app.icon.clone()).size(32).into(),
if app.is_default && !displayed_default {
displayed_default = true;
widget::text::body(fl!(
"default-app",
@ -7182,22 +7213,6 @@ impl Application for App {
},
)
}),
#[cfg(feature = "desktop")]
Subscription::run(|| {
stream::channel(
1,
|mut output: futures::channel::mpsc::Sender<Message>| async move {
mime_app::watch(move || {
futures::executor::block_on(async {
_ = output.send(Message::ReloadMimeAppCache).await;
})
})
.await;
std::future::pending().await
},
)
}),
Subscription::run_with(TypeId::of::<TrashWatcherSubscription>(), |_| {
stream::channel(
1,

View file

@ -1,5 +1,5 @@
use crate::mime_icon::mime_for_path;
use crate::operation::{Controller, OpReader, OperationError, OperationErrorType};
use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk};
use cosmic::iced::futures;
use jiff::Zoned;
use jiff::civil::DateTime;
@ -45,7 +45,7 @@ pub fn extract(
new_dir: &Path,
password: &Option<String>,
controller: &Controller,
) -> Result<(Vec<PathBuf>, HashSet<PathBuf>), OperationError> {
) -> Result<(), OperationError> {
let mime = mime_for_path(path, None, false);
let password = password.as_deref();
match mime.essence_str() {
@ -55,15 +55,13 @@ pub fn extract(
.map(flate2::read::GzDecoder::new)
.map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default())
.map_err(|e| OperationError::from_err(e, controller))?;
}
"application/x-tar" => OpReader::new(path, controller.clone())
.map(io::BufReader::new)
.map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default()),
.map_err(|e| OperationError::from_err(e, controller))?,
"application/zip" => fs::File::open(path)
.map(io::BufReader::new)
.map(zip::ZipArchive::new)
@ -77,7 +75,7 @@ pub fn extract(
OperationError::from_kind(OperationErrorType::PasswordRequired, controller)
}
_ => OperationError::from_err(e, controller),
}),
})?,
#[cfg(feature = "bzip2")]
"application/x-bzip"
| "application/x-bzip-compressed-tar"
@ -87,8 +85,7 @@ pub fn extract(
.map(bzip2::read::BzDecoder::new)
.map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default()),
.map_err(|e| OperationError::from_err(e, controller))?,
#[cfg(feature = "lzma-rust2")]
"application/x-xz" | "application/x-xz-compressed-tar" => {
OpReader::new(path, controller.clone())
@ -96,14 +93,14 @@ pub fn extract(
.map(|reader| lzma_rust2::XzReader::new(reader, true))
.map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default())
.map_err(|e| OperationError::from_err(e, controller))?;
}
_ => Err(OperationError::from_err(
format!("unsupported mime type {mime:?}"),
controller,
)),
))?,
}
Ok(())
}
// From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added
@ -112,7 +109,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
directory: P,
password: Option<&str>,
controller: Controller,
) -> zip::result::ZipResult<(Vec<PathBuf>, HashSet<PathBuf>)> {
) -> zip::result::ZipResult<()> {
use std::ffi::OsString;
use std::fs;
use zip::result::ZipError;
@ -285,7 +282,10 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
}
}
Ok((written_files, target_dirs))
// Flush files to disk
futures::executor::block_on(async { sync_to_disk(written_files, target_dirs).await });
Ok(())
}
fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option<SystemTime> {

View file

@ -40,7 +40,7 @@ use crate::localize::LANGUAGE_SORTER;
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab};
use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};
use crate::{fl, home_dir, menu, mime_icon};
use crate::{fl, home_dir, menu};
#[derive(Clone, Debug)]
pub struct DialogMessage(cosmic::Action<Message>);
@ -1458,14 +1458,14 @@ impl Application for App {
}
Message::Key(modifiers, key, physical_key, text) => {
for (key_bind, action) in &self.key_binds {
if key_bind.matches(modifiers, &key, Some(&physical_key)) {
if key_bind.matches(modifiers, &key, None) {
return self.update(Message::from(action.message()));
}
}
// Check key binds from accept label
if let Some(key_bind) = &self.accept_label.key_bind_opt
&& key_bind.matches(modifiers, &key, Some(&physical_key))
&& key_bind.matches(modifiers, &key, None)
{
return self.update(if self.flags.kind.save() {
Message::Save(false)
@ -1926,13 +1926,11 @@ impl Application for App {
items.retain(|item| {
// Directories are always shown
item.metadata.is_dir()
// Check for mime type match (first because it is faster)
|| mimes.iter().any(|filter_mime| {
if filter_mime.subtype() == mime::STAR {
filter_mime.type_() == item.mime.type_()
} else {
*filter_mime == item.mime
|| mime_icon::is_mime_subclass_of(&item.mime, filter_mime)
}
})
// Check for glob match (last because it is slower)

View file

@ -30,16 +30,12 @@ pub fn key_binds(mode: &tab::Mode) -> HashMap<KeyBind, Action> {
bind!([], Key::Named(Named::F5), Reload);
bind!([], Key::Named(Named::Home), SelectFirst);
bind!([], Key::Named(Named::End), SelectLast);
bind!([], Key::Named(Named::PageDown), ItemPageDown);
bind!([], Key::Named(Named::PageUp), ItemPageUp);
bind!([Shift], Key::Named(Named::ArrowDown), ItemDown);
bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft);
bind!([Shift], Key::Named(Named::ArrowRight), ItemRight);
bind!([Shift], Key::Named(Named::ArrowUp), ItemUp);
bind!([Shift], Key::Named(Named::Home), SelectFirst);
bind!([Shift], Key::Named(Named::End), SelectLast);
bind!([Shift], Key::Named(Named::PageDown), ItemPageDown);
bind!([Shift], Key::Named(Named::PageUp), ItemPageUp);
bind!([Ctrl, Shift], Key::Character("n".into()), NewFolder);
bind!([], Key::Named(Named::Enter), Open);
bind!([Ctrl], Key::Character(" ".into()), Preview);

View file

@ -19,9 +19,7 @@ use std::sync::LazyLock;
use crate::app::{Action, Message};
use crate::config::{Config, ContextActionPreset};
use crate::fl;
use crate::tab::{
self, HeadingOptions, ItemMetadata, Location, LocationMenuAction, SearchLocation, Tab,
};
use crate::tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab};
use crate::trash::{Trash, TrashExt};
static MENU_ID: LazyLock<cosmic::widget::Id> =
@ -110,25 +108,18 @@ pub fn context_menu<'a>(
let (sort_name, sort_direction, _) = tab.sort_options();
let sort_item = |label, variant| {
let key = find_key(&Action::ToggleSort(variant));
let leading: Element<'a, tab::Message> = if sort_name == variant {
let icon_name = if sort_direction {
"view-sort-ascending-symbolic"
} else {
"view-sort-descending-symbolic"
};
widget::icon::from_name(icon_name).size(14).into()
} else {
space::horizontal().width(Length::Fixed(14.0)).into()
};
menu_button!(
leading,
space::horizontal().width(Length::Fixed(theme::spacing().space_xxs.into())),
text::body(label),
space::horizontal(),
text::body(key).class(theme::Text::Custom(key_style))
menu_item(
format!(
"{} {}",
label,
match (sort_name == variant, sort_direction) {
(true, true) => "\u{2B07}",
(true, false) => "\u{2B06}",
_ => "",
}
),
Action::ToggleSort(variant),
)
.on_press(tab::Message::ContextAction(Action::ToggleSort(variant)))
.into()
};
@ -138,7 +129,6 @@ pub fn context_menu<'a>(
let mut selected_desktop_entry = None;
let mut selected_types: Vec<Mime> = vec![];
let mut selected_mount_point = 0;
let mut any_trash_item = false;
if let Some(items) = tab.items_opt() {
for item in items {
if item.selected {
@ -159,9 +149,6 @@ pub fn context_menu<'a>(
}
_ => (),
}
if matches!(&item.metadata, ItemMetadata::Trash { .. }) {
any_trash_item = true;
}
selected_types.push(item.mime.clone());
}
}
@ -283,38 +270,27 @@ pub fn context_menu<'a>(
//TODO: Print?
children.push(menu_item(fl!("show-details"), Action::Preview).into());
if any_trash_item {
if matches!(tab.mode, tab::Mode::App) {
children.push(divider::horizontal::light().into());
children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
}
children.push(divider::horizontal::light().into());
if tab.location.is_recents() {
children.push(
menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into(),
menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(),
);
children.push(divider::horizontal::light().into());
children.push(menu_item(fl!("delete-permanently"), Action::Delete).into());
} else {
if matches!(tab.mode, tab::Mode::App) {
children.push(divider::horizontal::light().into());
children
.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
}
children.push(divider::horizontal::light().into());
if tab.location.is_recents() {
}
if selected_mount_point == 0 {
if modifiers.shift() && !modifiers.control() {
children.push(
menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(),
menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(),
);
children.push(divider::horizontal::light().into());
}
if selected_mount_point == 0 {
if modifiers.shift() && !modifiers.control() {
children.push(
menu_item(fl!("delete-permanently"), Action::PermanentlyDelete)
.into(),
);
} else {
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
}
} else if selected == 1 {
children.push(menu_item(fl!("eject"), Action::Eject).into());
} else {
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
}
} else if selected == 1 {
children.push(menu_item(fl!("eject"), Action::Eject).into());
}
} else {
//TODO: need better designs for menu with no selection

View file

@ -2,67 +2,19 @@
// SPDX-License-Identifier: GPL-3.0-only
use bstr::{BString, ByteSlice, ByteVec};
#[cfg(feature = "desktop")]
use cosmic::desktop;
use cosmic::widget;
pub use mime_guess::Mime;
use rustc_hash::FxHashMap;
#[cfg(feature = "desktop")]
use notify_debouncer_full::notify;
use rustc_hash::{FxHashMap, FxHashSet};
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, RwLock, atomic};
use std::time::{self, Instant};
use std::{fs, io, process};
#[cfg(feature = "desktop")]
pub async fn watch(mut emitter: impl FnMut() + 'static + Send) {
let watcher_result = notify_debouncer_full::new_debouncer(
time::Duration::from_millis(250),
Some(time::Duration::from_millis(250)),
move |event_res: notify_debouncer_full::DebounceEventResult| {
let Ok(events) = event_res else {
return;
};
if events.iter().any(|event| {
event.kind.is_create() || event.kind.is_modify() || event.kind.is_remove()
}) {
emitter();
}
},
);
if let Ok(mut watcher) = watcher_result {
let system_paths = cosmic_mime_apps::list_paths();
let local_paths = (|| {
let base_dirs = xdg::BaseDirectories::new();
let Some(home) = base_dirs.get_config_home() else {
return Err(std::io::Error::other("XDG config home not set"));
};
let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") else {
return Err(std::io::Error::other("XDG_CURRENT_DESKTOP unset"));
};
let default_mimeapps = home.join("mimeapps.list");
let desktop_mimeapps =
home.join([&desktop.to_ascii_lowercase(), "-mimeapps.list"].concat());
Ok([desktop_mimeapps, default_mimeapps])
})()
.ok();
for path in system_paths
.iter()
.chain(local_paths.as_ref().into_iter().flatten())
{
_ = watcher.watch(path.as_path(), notify::RecursiveMode::NonRecursive);
}
std::future::pending().await
}
}
use std::{cmp::Ordering, fs, io, time::Instant};
use std::{
ffi::OsStr,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
process,
};
pub fn exec_to_command(
exec: &str,
@ -176,23 +128,14 @@ pub fn exec_to_command(
Some(commands)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MimeAppMatch {
Exact,
Related,
Other,
}
#[derive(Clone, Debug)]
pub struct MimeApp {
pub id: String,
pub path: Option<PathBuf>,
pub name: String,
pub exec: Option<String>,
icon_name: Box<str>,
icon: std::sync::OnceLock<widget::icon::Handle>,
is_default: Arc<RwLock<FxHashSet<Box<str>>>>,
no_display: Arc<AtomicBool>,
pub icon: widget::icon::Handle,
pub is_default: bool,
}
impl MimeApp {
@ -205,27 +148,6 @@ impl MimeApp {
path_opt,
)
}
pub fn is_default(&self, mime: &Mime) -> bool {
self.is_default.read().unwrap().contains(mime.essence_str())
}
pub fn no_display(&self) -> bool {
self.no_display.load(atomic::Ordering::Relaxed)
}
pub fn icon(&self) -> widget::icon::Handle {
self.icon
.get_or_init(|| {
let name = &*self.icon_name;
if name.starts_with('/') {
cosmic::widget::icon::from_path(PathBuf::from(name))
} else {
cosmic::widget::icon::from_name(name).size(32).handle()
}
})
.clone()
}
}
// This allows usage of MimeApp in a dropdown
@ -235,10 +157,38 @@ impl AsRef<str> for MimeApp {
}
}
#[cfg(feature = "desktop")]
impl From<&desktop::DesktopEntryData> for MimeApp {
fn from(app: &desktop::DesktopEntryData) -> Self {
Self {
id: app.id.clone(),
path: app.path.clone(),
name: app.name.clone(),
exec: app.exec.clone(),
icon: match &app.icon {
desktop::fde::IconSource::Name(name) => {
widget::icon::from_name(name.as_str()).size(32).handle()
}
desktop::fde::IconSource::Path(path) => widget::icon::from_path(path.clone()),
},
is_default: false,
}
}
}
#[cfg(feature = "desktop")]
fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
path_opt
.as_ref()
.and_then(|path| path.file_name())
.is_some_and(|x| x == filename)
}
pub struct MimeAppCache {
apps: Vec<Arc<MimeApp>>,
cache: FxHashMap<Mime, Vec<Arc<MimeApp>>>,
terminals: Vec<Arc<MimeApp>>,
apps: Vec<MimeApp>,
cache: FxHashMap<Mime, Vec<MimeApp>>,
icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>,
terminals: Vec<MimeApp>,
}
impl MimeAppCache {
@ -246,273 +196,154 @@ impl MimeAppCache {
let mut mime_app_cache = Self {
apps: Vec::new(),
cache: FxHashMap::default(),
icons: FxHashMap::default(),
terminals: Vec::new(),
};
mime_app_cache.reload();
mime_app_cache
}
pub fn get_apps_for_mime(
&self,
mime_type: &Mime,
include_other: bool,
) -> Vec<(&Arc<MimeApp>, MimeAppMatch)> {
let mut results = Vec::new();
let mut dedupe = FxHashSet::default();
// start with exact matches
results.extend(
self.get(mime_type)
.iter()
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Exact)),
);
let include_mime = match mime_type.type_().as_str() {
"audio" => Some("video/mp4".parse::<Mime>().expect("video/mp4 mime")),
"text" => Some(mime_guess::mime::TEXT_PLAIN),
_ => None,
};
if let Some(mime) = include_mime {
results.extend(
self.get(&mime)
.iter()
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Exact)),
);
}
// grab matches based off of subclass / parent mime type
if let Some(parent_types) = crate::mime_icon::parent_mime_types(mime_type) {
for parent_type in parent_types {
results.extend(
self.get(&parent_type)
.iter()
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Related)),
);
}
}
if include_other {
results.extend({
let mut apps = self
.apps()
.iter()
.filter(|mime_app| !mime_app.no_display())
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Other))
.collect::<Vec<_>>();
apps.sort_by(|(a, _), (b, _)| {
crate::localize::LANGUAGE_SORTER.compare(&a.name, &b.name)
});
apps
});
}
results
}
#[cfg(not(feature = "desktop"))]
pub fn reload(&mut self) {}
/// Reload mime types and their known app associations and defaults.
// Only available when using desktop feature of libcosmic, which only works on Unix-likes
#[cfg(feature = "desktop")]
pub fn reload(&mut self) {
use crate::localize::LANGUAGE_SORTER;
use crate::mime_icon;
use cosmic::desktop::fde;
use std::borrow::Cow;
let start = Instant::now();
self.apps.clear();
self.cache.clear();
self.icons.clear();
self.terminals.clear();
//TODO: get proper locale?
let locale = &[];
// Load desktop applications by supported mime types
//TODO: hashmap for all apps by id?
let all_apps: Box<[_]> = desktop::load_applications(locale, false, None).collect();
for app in &all_apps {
//TODO: just collect apps that can be executed with a file argument?
if !app.mime_types.is_empty() {
self.apps.push(MimeApp::from(app));
}
for mime in &app.mime_types {
let apps = self
.cache
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| x.id == app.id) {
apps.push(MimeApp::from(app));
}
}
for category in &app.categories {
if category == "TerminalEmulator" {
self.terminals.push(MimeApp::from(app));
break;
}
}
}
let mut list = cosmic_mime_apps::List::default();
let paths = cosmic_mime_apps::list_paths();
list.load_from_paths(&paths);
let locales = fde::get_languages_from_env();
let desktop_entries = fde::Iter::new(fde::default_paths()).entries(Some(&locales));
let mime_icon_cache = mime_icon::MIME_ICON_CACHE.lock().unwrap();
let shared_mime_info = &mime_icon_cache.shared_mime_info;
let mut aliased_mimes = FxHashMap::default();
for desktop_entry in desktop_entries {
let name = desktop_entry
.name(&locales)
.unwrap_or_else(|| Cow::Borrowed(desktop_entry.id()));
let app = Arc::new(MimeApp {
id: desktop_entry.appid.clone(),
path: Some(desktop_entry.path.clone()),
name: name.into(),
exec: desktop_entry.exec().map(String::from),
icon_name: desktop_entry.icon().unwrap_or_default().into(),
icon: std::sync::OnceLock::new(),
is_default: Arc::new(RwLock::default()),
no_display: Arc::new(AtomicBool::new(false)),
});
tracing::info!(target: "mime-apps", id = app.id, "detected desktop entry");
self.apps.push(app.clone());
if desktop_entry
.categories()
.into_iter()
.flatten()
.any(|c| c == "TerminalEmulator")
{
self.terminals.push(app.clone());
}
// Cache associations defined by the desktop entry.
let mime_types = desktop_entry.mime_type().unwrap_or_else(Vec::new);
let associated_mime_types = mime_types.iter().filter_map(|m| {
m.parse::<Mime>().ok().map(|mime| {
if let Some(unaliased) = shared_mime_info.unalias_mime_type(&mime) {
aliased_mimes.insert(unaliased.clone(), mime);
return unaliased;
}
mime
})
});
for mime in associated_mime_types {
let apps = self.cache.entry(mime.clone()).or_default();
if apps.iter().all(|cached_app| cached_app.id != app.id) {
apps.push(app.clone());
}
}
}
// Cache added associations from mimeapps lists.
for (mut added_mime, added_apps) in &list.added_associations {
let _unaliased;
if let Some(unaliased) = shared_mime_info.unalias_mime_type(added_mime) {
aliased_mimes.insert(unaliased.clone(), added_mime.clone());
_unaliased = unaliased;
added_mime = &_unaliased;
}
for added_app in added_apps {
if let Some(app) = self
.apps
.iter()
.find(|cached| cached.id.as_str() == added_app.as_ref())
{
let apps = self.cache.entry(added_mime.clone()).or_default();
if apps.iter().all(|cached_app| cached_app.id != app.id) {
apps.push(app.clone());
for (mime, filenames) in list
.added_associations
.iter()
.chain(list.default_apps.iter())
{
for filename in filenames {
log::trace!("add {mime}={filename}");
let apps = self
.cache
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
if let Some(app) = all_apps.iter().find(|&x| filename_eq(&x.path, filename)) {
apps.push(MimeApp::from(app));
} else {
log::info!(
"failed to add association for {mime:?}: application {filename:?} not found"
);
}
}
}
}
// Remove associations
for (mut removed_mime, removed_apps) in &list.removed_associations {
let _unaliased;
if let Some(unaliased) = shared_mime_info.unalias_mime_type(removed_mime) {
aliased_mimes.insert(unaliased.clone(), removed_mime.clone());
_unaliased = unaliased;
removed_mime = &_unaliased;
}
for removed_app in removed_apps {
if let Some(app) = self
.apps
.iter()
.find(|cached| cached.id.as_str() == removed_app.as_ref())
&& let Some(apps) = self.cache.get_mut(removed_mime)
{
apps.retain(|cached_app| cached_app.id != app.id);
for (mime, filenames) in list.removed_associations.iter() {
for filename in filenames {
log::trace!("remove {mime}={filename}");
if let Some(apps) = self.cache.get_mut(mime) {
apps.retain(|x| !filename_eq(&x.path, filename));
}
}
}
// Fetch defaults and sort apps by their default precedence.
for (mime, mut apps) in std::mem::take(&mut self.cache).into_iter() {
let defaults = list.default_app_for(&mime);
let aliased_defaults = aliased_mimes
.get(&mime)
.and_then(|mime| list.default_app_for(mime));
let cache = self
.cache
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(apps.len()));
// Sort cached apps for this mime by default precedence.
for default in defaults
.into_iter()
.flatten()
.chain(aliased_defaults.into_iter().flatten())
{
let default = default.strip_suffix(".desktop").unwrap_or(default.as_ref());
let mut found_any = false;
apps.retain(|app| {
let found = app.id.as_str() == default;
for (mime, filenames) in list.default_apps.iter() {
for filename in filenames {
log::trace!("default {mime}={filename}");
if let Some(apps) = self.cache.get_mut(mime) {
let mut found = false;
for app in apps.iter_mut() {
if filename_eq(&app.path, filename) {
app.is_default = true;
found = true;
} else {
app.is_default = false;
}
}
if found {
app.is_default
.write()
.unwrap()
.insert(mime.essence_str().into());
cache.push(app.clone());
found_any = true;
break;
}
!found
});
if !found_any && let Some(app) = self.apps.iter().find(|app| app.id == default) {
app.is_default
.write()
.unwrap()
.insert(mime.essence_str().into());
cache.push(app.clone());
log::debug!(
"failed to set default for {mime:?}: application {filename:?} not found"
);
}
}
// Sort remaining apps by name
apps.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.name, &b.name));
cache.extend_from_slice(&apps);
tracing::debug!(target: "mime-apps", mime = mime.essence_str(), apps = ?(cache.iter().map(|app| &*app.id).collect::<Vec<&str>>()), "mime defaults found")
}
let associated: rustc_hash::FxHashSet<&str> = self
.cache
.values()
.flatten()
.map(|app| app.id.as_str())
.collect();
for app in &self.apps {
app.no_display.store(
!associated.contains(app.id.as_str()),
atomic::Ordering::Relaxed,
);
// Sort apps by name
self.apps
.sort_by(|a, b| match (a.is_default, b.is_default) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => LANGUAGE_SORTER.compare(&a.name, &b.name),
});
for apps in self.cache.values_mut() {
apps.sort_by(|a, b| match (a.is_default, b.is_default) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => LANGUAGE_SORTER.compare(&a.name, &b.name),
});
}
// Copy icons to special cache
//TODO: adjust dropdown API so this is no longer needed
self.icons.extend(self.cache.iter().map(|(mime, apps)| {
(
mime.clone(),
apps.iter().map(|app| app.icon.clone()).collect(),
)
}));
let elapsed = start.elapsed();
tracing::info!(target: "mime-apps", "loaded mime app cache in {elapsed:?}");
log::info!("loaded mime app cache in {elapsed:?}");
}
pub fn apps(&self) -> &[Arc<MimeApp>] {
pub fn apps(&self) -> &[MimeApp] {
&self.apps
}
pub fn get(&self, key: &Mime) -> &[Arc<MimeApp>] {
pub fn get(&self, key: &Mime) -> &[MimeApp] {
self.cache.get(key).map_or(&[], Vec::as_slice)
}
pub fn icons(&self, key: &Mime) -> Vec<widget::icon::Handle> {
self.cache
.get(key)
.map_or_else(Vec::new, |apps| apps.iter().map(|app| app.icon()).collect())
pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
self.icons.get(key).map_or(&[], Box::as_ref)
}
fn get_default_terminal(&self) -> Option<String> {
@ -530,13 +361,11 @@ impl MimeAppCache {
.map(|string| string.trim().replace(".desktop", ""))
}
pub fn terminal(&self) -> Option<&Arc<MimeApp>> {
pub fn terminal(&self) -> Option<&MimeApp> {
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
// The current approach works but might not adhere to the spec (yet)
// Look for and return preferred terminals
//TODO: fallback order beyond cosmic-term?
// Yoda: cosmic-yoterm (our fork) wins over upstream cosmic-term if both
// are installed — useful when xdg-mime default is not set.
let mut preference_order = vec![

View file

@ -15,21 +15,28 @@ struct MimeIconKey {
size: u16,
}
#[derive(Default)]
pub struct MimeIconCache {
struct MimeIconCache {
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
#[cfg(unix)]
pub shared_mime_info: xdg_mime::SharedMimeInfo,
shared_mime_info: xdg_mime::SharedMimeInfo,
}
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)]
fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
pub fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
self.cache
.entry(key)
.or_insert_with_key(|key| {
@ -49,9 +56,8 @@ impl MimeIconCache {
.clone()
}
}
pub static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
LazyLock::new(|| Mutex::new(MimeIconCache::default()));
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
#[cfg(not(unix))]
pub fn mime_for_path(
@ -72,7 +78,6 @@ pub fn mime_for_path(
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
// Try the shared mime info cache first
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
gb.zero_size(false);
if remote {
if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
gb.file_name(file_name);
@ -124,11 +129,3 @@ 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)
}
pub fn is_mime_subclass_of(mime_type: &Mime, base: &Mime) -> bool {
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
mime_icon_cache
.shared_mime_info
.mime_type_subclass(mime_type, base)
}

View file

@ -14,7 +14,7 @@ use tokio::sync::mpsc;
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
use crate::config::IconSizes;
use crate::err_str;
use crate::tab::{self, ChecksumState, DirSize, ItemMetadata, ItemThumbnail, Location};
use crate::tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location};
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
@ -216,7 +216,6 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
//TODO: scan directory size on gvfs mounts?
dir_size: DirSize::NotDirectory,
cut: false,
checksums: ChecksumState::default(),
});
}
Ok(items)

View file

@ -967,13 +967,11 @@ impl Operation {
password,
} => {
let controller_clone = controller.clone();
compio::runtime::spawn(async move {
let extracted = compio::runtime::spawn_blocking(move || {
compio::runtime::spawn_blocking(
move || -> Result<OperationSelection, OperationError> {
let controller = controller_clone;
let total_paths = paths.len();
let mut op_sel = OperationSelection::default();
let mut written_files = Vec::new();
let mut target_dirs = std::collections::HashSet::new();
for (i, path) in paths.iter().enumerate() {
futures::executor::block_on(async {
controller
@ -997,32 +995,16 @@ impl Operation {
op_sel.ignored.push(path.clone());
op_sel.selected.push(new_dir.clone());
let (files, dirs) = crate::archive::extract(
path,
&new_dir,
&password,
&controller,
)?;
written_files.extend(files);
target_dirs.extend(dirs);
crate::archive::extract(path, &new_dir, &password, &controller)?;
}
}
Ok::<_, OperationError>((op_sel, written_files, target_dirs))
})
.await
.map_err(wrap_compio_spawn_error)??;
let (op_sel, written_files, target_dirs) = extracted;
if !written_files.is_empty() || !target_dirs.is_empty() {
sync_to_disk(written_files, target_dirs).await;
}
Ok::<_, OperationError>(op_sel)
})
.await
.map_err(wrap_compio_spawn_error)?
Ok(op_sel)
},
)
}
.await
.map_err(wrap_compio_spawn_error)?,
Self::Move {
paths,
to,
@ -1156,26 +1138,10 @@ impl Operation {
paths.push(item.original_path());
// Items with .trashinfo id use standard restore; sub-items use manual move
if item
.id
.to_str()
.map_or(false, |s| s.ends_with(".trashinfo"))
{
compio::runtime::spawn_blocking(|| trash::os_limited::restore_all([item]))
.await
.map_err(wrap_compio_spawn_error)?
.map_err(|e| OperationError::from_err(e, &controller))?;
} else {
let from = PathBuf::from(&item.id);
let to = item.original_path();
if let Some(parent) = to.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| OperationError::from_err(e, &controller))?;
}
std::fs::rename(&from, &to)
.map_err(|e| OperationError::from_err(e, &controller))?;
}
compio::runtime::spawn_blocking(|| trash::os_limited::restore_all([item]))
.await
.map_err(wrap_compio_spawn_error)?
.map_err(|e| OperationError::from_err(e, &controller))?;
}
Ok(OperationSelection {
ignored: Vec::new(),

View file

@ -29,7 +29,6 @@ use jiff_icu::ConvertFrom;
use mime_guess::{Mime, mime};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::borrow::Cow;
use std::cell::Cell;
use std::cmp::{Ordering, Reverse};
@ -746,7 +745,6 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
overlaps_drag_rect: false,
dir_size,
cut: false,
checksums: ChecksumState::default(),
}
}
@ -876,7 +874,6 @@ pub fn item_from_entry(
overlaps_drag_rect: false,
dir_size,
cut: false,
checksums: ChecksumState::default(),
}
}
@ -889,8 +886,6 @@ pub fn item_from_trash_entry(
let name = entry.name.to_string_lossy().into_owned();
let display_name = Item::display_name(&name);
let location = crate::trash::trash_item_path(&entry).map(Location::Path);
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = match metadata.size
{
trash::TrashItemSize::Entries(_) => (
@ -918,7 +913,7 @@ pub fn item_from_trash_entry(
is_mount_point: false,
metadata: ItemMetadata::Trash { metadata, entry },
hidden: false,
location_opt: location,
location_opt: None,
image_dimensions: (mime.type_() == mime::IMAGE)
.then(|| image::image_dimensions(&original_path).ok())
.flatten(),
@ -935,35 +930,9 @@ pub fn item_from_trash_entry(
overlaps_drag_rect: false,
dir_size: DirSize::NotDirectory,
cut: false,
checksums: ChecksumState::default(),
}
}
fn item_from_trash_child(
path: PathBuf,
name: String,
metadata: fs::Metadata,
sizes: IconSizes,
) -> Option<Item> {
let original_path = crate::trash::original_path_for_trash_child(&path)?;
let entry = trash::TrashItem {
id: path.as_os_str().to_os_string(),
name: std::ffi::OsString::from(&name),
original_parent: original_path.parent()?.to_path_buf(),
time_deleted: 0,
};
let size = if metadata.is_dir() {
trash::TrashItemSize::Entries(0)
} else {
trash::TrashItemSize::Bytes(metadata.len())
};
Some(item_from_trash_entry(
entry,
trash::TrashItemMetadata { size },
sizes,
))
}
fn get_filename_from_path(path: &Path) -> Result<String, String> {
Ok(match path.file_name() {
Some(name_os) => name_os
@ -1082,12 +1051,7 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
})
.ok()?;
let trash = crate::trash::is_trash_path(tab_path);
if trash {
item_from_trash_child(path, name, metadata, sizes)
} else {
Some(item_from_entry(path, name, metadata, sizes))
}
Some(item_from_entry(path, name, metadata, sizes))
})
.collect();
}
@ -1392,7 +1356,6 @@ pub fn scan_desktop(
overlaps_drag_rect: false,
dir_size: DirSize::NotDirectory,
cut: false,
checksums: ChecksumState::default(),
});
}
@ -1776,8 +1739,6 @@ pub enum Message {
GoPrevious,
ItemDown,
ItemLeft,
ItemPageDown,
ItemPageUp,
ItemRight,
ItemUp,
Location(Location),
@ -1814,9 +1775,6 @@ pub enum Message {
HighlightDeactivate(usize),
HighlightActivate(usize),
DirectorySize(PathBuf, DirSize),
Checksums(PathBuf, ChecksumState),
CalculateChecksums(PathBuf),
CopyChecksum(String),
ImageDecoded(PathBuf, u32, u32, Vec<u8>, Option<(u32, u32)>, u64), // path, width, height, pixels, display_size, generation
}
@ -1844,23 +1802,6 @@ pub enum DirSize {
Error(String),
}
/// Checksums computed for a file. Only SHA256 is currently exposed; see
/// [`calculate_checksums`] for how to add more.
#[derive(Clone, Debug, Default)]
pub struct FileChecksums {
pub sha256: String,
}
/// State of checksum computation for a file.
#[derive(Clone, Debug, Default)]
pub enum ChecksumState {
#[default]
NotCalculated,
Calculating,
Calculated(FileChecksums),
Error(String),
}
#[derive(Clone, Debug)]
pub enum ItemMetadata {
Path {
@ -2312,7 +2253,6 @@ pub struct Item {
pub cut: bool,
pub overlaps_drag_rect: bool,
pub dir_size: DirSize,
pub checksums: ChecksumState,
}
impl Item {
@ -2443,24 +2383,20 @@ impl Item {
)));
let mut settings = Vec::new();
if let Some(mime_app_cache) = mime_app_cache_opt {
let mime_apps = mime_app_cache.get_apps_for_mime(&self.mime, false);
let mime_apps = mime_app_cache.get(&self.mime);
if !mime_apps.is_empty() {
let (names, icons) = mime_apps
.iter()
.map(|(app, _)| (Cow::Owned(app.name.clone()), app.icon()))
.collect::<(Vec<_>, Vec<_>)>();
settings.push(
widget::settings::item::builder(fl!("open-with")).control(
Element::from(
widget::dropdown(
names,
mime_apps.iter().position(|(x, _)| x.is_default(&self.mime)),
mime_apps,
mime_apps.iter().position(|x| x.is_default),
move |index| index,
)
.icons(Cow::Owned(icons)),
.icons(Cow::Borrowed(mime_app_cache.icons(&self.mime))),
)
.map(move |index| {
let mime_app = &mime_apps[index].0;
.map(|index| {
let mime_app = &mime_apps[index];
Message::SetOpenWith(self.mime.clone(), mime_app.id.clone())
}),
),
@ -2589,55 +2525,6 @@ impl Item {
}
column = column.push(details);
if let Some(metadata) = self.file_metadata()
&& !metadata.is_dir()
&& let Some(path) = self.path_opt()
{
let control: Element<'_, Message> = match &self.checksums {
ChecksumState::NotCalculated => widget::button::standard(fl!("calculate"))
.on_press(Message::CalculateChecksums(path.clone()))
.into(),
ChecksumState::Calculating => widget::row::with_capacity(2)
.align_y(Alignment::Center)
.spacing(space_xxxs)
.push(widget::indeterminate_circular().size(16.0))
.push(widget::text::body(fl!("calculating")))
.into(),
ChecksumState::Calculated(checksums) => {
let value = checksums.sha256.clone();
// Middle-ellipsize the digest to fit, full value on hover.
let value_text = widget::tooltip(
widget::text::body(value.clone())
.font(cosmic::font::mono())
.width(Length::Fill)
.wrapping(text::Wrapping::None)
.ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(
1,
))),
widget::text::body(value.clone()),
widget::tooltip::Position::Bottom,
);
let copy_button = widget::button::icon(
widget::icon::from_name("edit-copy-symbolic").size(16),
)
.on_press(Message::CopyChecksum(value.clone()))
.tooltip(fl!("copy"));
widget::row::with_capacity(2)
.align_y(Alignment::Center)
.spacing(space_xxxs)
.push(value_text)
.push(copy_button)
.into()
}
ChecksumState::Error(err) => {
widget::text::body(format!("{}: {}", fl!("error"), err)).into()
}
};
settings.push(
widget::settings::item::builder(fl!("checksum", kind = "SHA256")).control(control),
);
}
if let Some(path) = self.path_opt()
&& self.selected
{
@ -2835,32 +2722,6 @@ async fn calculate_dir_size(path: &Path, controller: Controller) -> Result<u64,
Ok(total)
}
/// Calculate file checksums in a single pass over the file. To add another
/// digest, hash it alongside `sha256_hasher` in the loop below and add a field
/// to [`FileChecksums`]; the file is only read once.
async fn calculate_checksums(path: &Path) -> Result<FileChecksums, String> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || {
let mut file = File::open(&path).map_err(|e| e.to_string())?;
let mut sha256_hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let bytes_read = file.read(&mut buffer).map_err(|e| e.to_string())?;
if bytes_read == 0 {
break;
}
sha256_hasher.update(&buffer[..bytes_read]);
}
Ok(FileChecksums {
sha256: format!("{:x}", sha256_hasher.finalize()),
})
})
.await
.map_err(|e| e.to_string())?
}
fn folder_name<P: AsRef<Path>>(path: P) -> (String, bool) {
let path = path.as_ref();
let mut found_home = false;
@ -3280,14 +3141,6 @@ impl Tab {
item.pos_opt.get()
}
fn dehighlight_all(&mut self) {
if let Some(items) = self.items_opt.as_mut() {
for item in items.iter_mut() {
item.highlighted = false;
}
}
}
pub(crate) fn select_focus_scroll(&mut self) -> Option<AbsoluteOffset> {
let items = self.items_opt.as_ref()?;
let item = items.get(self.select_focus?)?;
@ -3323,20 +3176,6 @@ impl Tab {
}
}
fn rows_per_page(&self) -> usize {
let viewport_height = self.item_view_size_opt.get().map_or(0.0, |s| s.height);
let row_height = self
.select_focus
.and_then(|i| self.items_opt.as_ref()?.get(i))
.and_then(|item| item.rect_opt.get())
.map_or(0.0, |r| r.height);
if row_height > 0.0 && viewport_height > 0.0 {
(viewport_height / row_height).floor() as usize
} else {
1
}
}
fn select_range_start_pos_opt(&self) -> Option<(usize, usize)> {
let items = self.items_opt.as_ref()?;
let item = items.get(self.select_range.map(|r| r.0)?)?;
@ -3992,7 +3831,6 @@ impl Tab {
}
}
Message::ItemDown => {
self.dehighlight_all();
if let Some(edit_location) = &mut self.edit_location {
edit_location.select(true);
} else if self.gallery {
@ -4034,95 +3872,7 @@ impl Tab {
}
}
}
Message::ItemPageDown => {
self.dehighlight_all();
if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_last_pos_opt())
{
if self.select_focus.is_none() {
self.select_position(row, col, mod_shift);
}
let rows_per_page = self.rows_per_page().max(1);
let target_row = row.saturating_add(rows_per_page);
if !self.select_position(target_row, col, mod_shift) {
// Fall back to the last item at or before target_row
let best = self.items_opt.as_ref().and_then(|items| {
items
.iter()
.filter_map(|item| item.pos_opt.get())
.filter(|(r, _)| *r <= target_row)
.max()
});
if let Some((best_row, best_col)) = best {
self.select_position(best_row, best_col, mod_shift);
}
}
} else {
self.select_position(0, 0, mod_shift);
}
if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced(
scrollable::scroll_to(
self.scrollable_id.clone(),
AbsoluteOffset {
x: Some(offset.x),
y: Some(offset.y),
},
)
.into(),
));
}
if let Some(id) = self.select_focus_id() {
commands.push(Command::Iced(widget::button::focus(id).into()));
}
}
Message::ItemPageUp => {
self.dehighlight_all();
if let Some((row, col)) =
self.select_focus_pos_opt().or(self.select_first_pos_opt())
{
if self.select_focus.is_none() {
self.select_position(row, col, mod_shift);
}
let rows_per_page = self.rows_per_page().max(1);
let target_row = row.saturating_sub(rows_per_page);
if !self.select_position(target_row, col, mod_shift) {
// Fall back to the first item at or after target_row
let best = self.items_opt.as_ref().and_then(|items| {
items
.iter()
.filter_map(|item| item.pos_opt.get())
.filter(|(r, _)| *r >= target_row)
.min()
});
if let Some((best_row, best_col)) = best {
self.select_position(best_row, best_col, mod_shift);
}
}
} else {
self.select_position(0, 0, mod_shift);
}
if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced(
scrollable::scroll_to(
self.scrollable_id.clone(),
AbsoluteOffset {
x: Some(offset.x),
y: Some(offset.y),
},
)
.into(),
));
}
if let Some(id) = self.select_focus_id() {
commands.push(Command::Iced(widget::button::focus(id).into()));
}
}
Message::ItemLeft => {
self.dehighlight_all();
if self.gallery {
commands.append(&mut self.update(Message::GalleryPrevious, modifiers));
} else {
@ -4181,7 +3931,6 @@ impl Tab {
}
}
Message::ItemRight => {
self.dehighlight_all();
if self.gallery {
commands.append(&mut self.update(Message::GalleryNext, modifiers));
} else {
@ -4223,7 +3972,6 @@ impl Tab {
}
}
Message::ItemUp => {
self.dehighlight_all();
if let Some(edit_location) = &mut self.edit_location {
edit_location.select(false);
} else if self.gallery {
@ -4800,52 +4548,6 @@ impl Tab {
}
}
}
Message::Checksums(path, checksum_state) => {
let location = Location::Path(path);
if let Some(ref mut item) = self.parent_item_opt
&& item.location_opt.as_ref() == Some(&location)
{
item.checksums = checksum_state.clone();
}
if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() {
if item.location_opt.as_ref() == Some(&location) {
item.checksums = checksum_state;
break;
}
}
}
}
Message::CalculateChecksums(path) => {
let location = Location::Path(path.clone());
if let Some(ref mut item) = self.parent_item_opt
&& item.location_opt.as_ref() == Some(&location)
{
item.checksums = ChecksumState::Calculating;
}
if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() {
if item.location_opt.as_ref() == Some(&location) {
item.checksums = ChecksumState::Calculating;
break;
}
}
}
commands.push(Command::Iced(
cosmic::Task::future(async move {
match calculate_checksums(&path).await {
Ok(checksums) => {
Message::Checksums(path, ChecksumState::Calculated(checksums))
}
Err(err) => Message::Checksums(path, ChecksumState::Error(err)),
}
})
.into(),
));
}
Message::CopyChecksum(value) => {
commands.push(Command::Iced(cosmic::iced::clipboard::write(value).into()));
}
}
// Scroll to top if needed
@ -6761,25 +6463,21 @@ impl Tab {
.and_then(|(mime, _)| mime.parse::<Mime>().ok())
&& let Some(mime_app_cache) = mime_app_cache_opt
{
let mime_apps = mime_app_cache.get_apps_for_mime(&mime, false);
let mime_apps = mime_app_cache.get(&mime);
if !mime_apps.is_empty() {
let mime_closure = mime.clone();
let (names, icons) = mime_apps
.iter()
.map(|(app, _)| (Cow::Owned(app.name.clone()), app.icon()))
.collect::<(Vec<_>, Vec<_>)>();
settings.push(
widget::settings::item::builder(fl!("open-with")).control(
Element::from(
widget::dropdown(
names,
mime_apps.iter().position(|(x, _)| x.is_default(&mime)),
mime_apps,
mime_apps.iter().position(|x| x.is_default),
move |index| (index, mime_closure.clone()),
)
.icons(Cow::Owned(icons)),
.icons(Cow::Borrowed(mime_app_cache.icons(&mime))),
)
.map(move |(index, mime)| {
let mime_app = &mime_apps[index].0;
.map(|(index, mime)| {
let mime_app = &mime_apps[index];
Message::SetOpenWith(mime, mime_app.id.clone())
}),
),

View file

@ -1,30 +1,11 @@
use cosmic::widget;
use regex::Regex;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use crate::config::IconSizes;
use crate::tab::{Item, SearchItem};
fn percent_decode(s: &str) -> Option<String> {
let (mut r, mut b) = (String::with_capacity(s.len()), s.bytes());
while let Some(c) = b.next() {
if c == b'%' {
let (hi, lo) = (b.next()?, b.next()?);
let h = |x: u8| match x {
b'0'..=b'9' => Some(x - b'0'),
b'a'..=b'f' => Some(x - b'a' + 10),
b'A'..=b'F' => Some(x - b'A' + 10),
_ => None,
};
r.push((h(hi)? << 4 | h(lo)?) as char);
} else {
r.push(c as char);
}
}
Some(r)
}
pub trait TrashExt {
fn is_empty() -> bool {
true
@ -68,46 +49,6 @@ pub trait TrashExt {
}
}
/// Derive the actual filesystem path of a trashed item from its .trashinfo path
/// (or return the id directly if it's already a filesystem path).
pub fn trash_item_path(item: &trash::TrashItem) -> Option<PathBuf> {
let id_path = Path::new(&item.id);
if id_path.extension().map_or(false, |e| e == "trashinfo") {
let trash_root = id_path.parent()?.parent()?;
let file_name = id_path.file_stem()?;
Some(trash_root.join("files").join(file_name))
} else {
Some(PathBuf::from(&item.id))
}
}
/// For a path inside a trash `files/` directory, reconstruct the original path
/// by reading the parent `.trashinfo` file.
///
/// Given `~/.local/share/Trash/files/folder/sub/file.txt`:
/// - The top-level trashed item is `folder`
/// - Read `~/.local/share/Trash/info/folder.trashinfo` to get the original path
/// - Compute: `<original_path>/sub/file.txt`
pub fn original_path_for_trash_child(p: &Path) -> Option<PathBuf> {
let files = p.ancestors().find(|a| a.ends_with("files"))?;
let root = files.parent()?;
let top = p.strip_prefix(files).ok()?.components().next()?;
let info =
std::fs::read_to_string(root.join("info").join(top).with_extension("trashinfo")).ok()?;
let orig = percent_decode(info.lines().find_map(|l| l.strip_prefix("Path="))?.trim())?;
let rel = p.strip_prefix(files.join(top)).ok()?;
let mut result = PathBuf::from(&orig);
if !rel.as_os_str().is_empty() {
result.push(rel);
}
Some(result)
}
/// Check whether a path is inside any trash `files/` directory.
pub fn is_trash_path(path: &Path) -> bool {
path.ancestors().any(|a| a.ends_with("files"))
}
pub struct Trash;
// This config statement is from trash::os_limited