Compare commits
45 commits
f2ca1d37a6
...
d6e1572611
| Author | SHA1 | Date | |
|---|---|---|---|
| d6e1572611 | |||
| fd4a2ba610 | |||
| 2a74748083 | |||
|
|
17fc1cabbb | ||
|
|
43a4228ca4 | ||
|
|
3ef7913abd | ||
|
|
49107187a4 | ||
|
|
50deb28a11 | ||
|
|
8883d7cd64 | ||
|
|
4d6788c9fd | ||
|
|
cd427c746c | ||
|
|
c653b69c75 | ||
|
|
65f5f454be | ||
|
|
53a33dbe74 | ||
|
|
7d3487da7b | ||
|
|
9a3eb1cdea | ||
|
|
e8fdb28a24 | ||
|
|
eda8291678 | ||
|
|
6d4af69318 | ||
|
|
256a6cba19 | ||
|
|
3cd0d0c00a | ||
|
|
64998b37a1 | ||
|
|
48eef3906d | ||
|
|
54f77f95a6 | ||
|
|
42577aa467 | ||
|
|
4ac289f40a | ||
|
|
c6c9eac97e | ||
|
|
af65a6b1c0 | ||
|
|
1d88fd16d6 | ||
|
|
d04c92229c | ||
|
|
814b49114b | ||
|
|
245dfaa9b0 | ||
|
|
7c7970d0c0 | ||
|
|
fc8f2b4000 | ||
|
|
c605dbecaf | ||
|
|
fca0753781 | ||
|
|
bb1ae9d458 | ||
|
|
0b3d8a9f3f | ||
|
|
53cfeebe57 | ||
|
|
bf01bb31a3 | ||
|
|
18eb41930a | ||
|
|
355423a027 | ||
|
|
cdec335ac7 | ||
|
|
2a3c9f12d9 | ||
|
|
5d1c056bed |
45 changed files with 2596 additions and 1168 deletions
1223
Cargo.lock
generated
1223
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
49
Cargo.toml
49
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-files"
|
name = "cosmic-files"
|
||||||
version = "1.0.13"
|
version = "1.2.0"
|
||||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
@ -11,7 +11,7 @@ anyhow = "1"
|
||||||
jiff = "0.2"
|
jiff = "0.2"
|
||||||
jiff-icu = "0.2"
|
jiff-icu = "0.2"
|
||||||
icu = { version = "2.2.0", features = ["compiled_data"] }
|
icu = { version = "2.2.0", features = ["compiled_data"] }
|
||||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
|
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c253ec1", optional = true }
|
||||||
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
|
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
gio = { version = "0.21", optional = true }
|
gio = { version = "0.21", optional = true }
|
||||||
|
|
@ -33,7 +33,7 @@ shlex = { version = "1.3" }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
tikv-jemallocator = { version = "0.6", optional = true }
|
tikv-jemallocator = { version = "0.6", optional = true }
|
||||||
tokio = { version = "1", features = ["process", "sync"] }
|
tokio = { version = "1", features = ["process", "sync"] }
|
||||||
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" }
|
trash = "5.2.6"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
wayland-client = { version = "0.31.14", optional = true }
|
wayland-client = { version = "0.31.14", optional = true }
|
||||||
|
|
@ -56,6 +56,7 @@ slotmap = "1.1.1"
|
||||||
recently-used-xbel = "1.2.0"
|
recently-used-xbel = "1.2.0"
|
||||||
zip = "8"
|
zip = "8"
|
||||||
md-5 = "0.10.6"
|
md-5 = "0.10.6"
|
||||||
|
sha2 = "0.10"
|
||||||
png = "0.18"
|
png = "0.18"
|
||||||
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
||||||
num_cpus = "1.17.0"
|
num_cpus = "1.17.0"
|
||||||
|
|
@ -73,8 +74,9 @@ version = "0.18"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["fs", "io", "macros", "polling", "runtime"]
|
features = ["fs", "io", "macros", "polling", "runtime"]
|
||||||
|
|
||||||
[dependencies.libcosmic]
|
# Yoda fork — depend on libcosmic-yoda directly by path (no git/no patch).
|
||||||
git = "https://github.com/pop-os/libcosmic.git"
|
[dependencies.libcosmic-yoda]
|
||||||
|
path = "../libcosmic"
|
||||||
default-features = false
|
default-features = false
|
||||||
#TODO: a11y feature crashes
|
#TODO: a11y feature crashes
|
||||||
features = [
|
features = [
|
||||||
|
|
@ -83,7 +85,7 @@ features = [
|
||||||
"autosize",
|
"autosize",
|
||||||
"multi-window",
|
"multi-window",
|
||||||
"tokio",
|
"tokio",
|
||||||
"winit",
|
"wayland",
|
||||||
"surface-message",
|
"surface-message",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -111,15 +113,15 @@ default = [
|
||||||
"wayland",
|
"wayland",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
]
|
]
|
||||||
dbus-config = ["libcosmic/dbus-config"]
|
dbus-config = ["libcosmic-yoda/dbus-config"]
|
||||||
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
|
desktop = ["libcosmic-yoda/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
|
||||||
desktop-applet = []
|
desktop-applet = []
|
||||||
gvfs = ["dep:gio", "dep:glib"]
|
gvfs = ["dep:gio", "dep:glib"]
|
||||||
io-uring = ["compio/io-uring"]
|
io-uring = ["compio/io-uring"]
|
||||||
jemalloc = ["dep:tikv-jemallocator"]
|
jemalloc = ["dep:tikv-jemallocator"]
|
||||||
notify = ["dep:notify-rust"]
|
notify = ["dep:notify-rust"]
|
||||||
wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"]
|
wayland = ["libcosmic-yoda/wayland", "dep:cctk", "dep:wayland-client"]
|
||||||
wgpu = ["libcosmic/wgpu"]
|
wgpu = ["libcosmic-yoda/wgpu"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
@ -145,20 +147,23 @@ fastrand = "2"
|
||||||
test-log = "0.2"
|
test-log = "0.2"
|
||||||
tokio = { version = "1", features = ["rt", "macros"] }
|
tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
|
|
||||||
# [patch.'https://github.com/pop-os/cosmic-text']
|
# Yoda fork — libcosmic dep is now a direct path dep (libcosmic-yoda above),
|
||||||
# cosmic-text = { path = "../cosmic-text" }
|
# 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.
|
||||||
|
|
||||||
# [patch.'https://github.com/pop-os/libcosmic']
|
# Yoda wayland cut: redirect window_clipboard (x11 gated behind opt-in
|
||||||
# libcosmic = { path = "../libcosmic" }
|
# feature) to our public Forgejo fork and cosmic-text to the local checkout
|
||||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
# carrying PR#503 plus the EAW monospace width fix. The window_clipboard patch
|
||||||
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
# is needed to consolidate the
|
||||||
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
# upstream pop-os/libcosmic chain (pulled by cosmic-settings-daemon) onto
|
||||||
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
# the same fork iced uses, otherwise cargo compiles two versions.
|
||||||
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
[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']
|
||||||
# [patch.'https://github.com/pop-os/smithay-clipboard']
|
cosmic-text = { path = "../cosmic-text" }
|
||||||
# smithay-clipboard = { path = "../smithay-clipboard" }
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["cosmic-files-applet"]
|
members = ["cosmic-files-applet"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-files-applet"
|
name = "cosmic-files-applet"
|
||||||
version = "1.0.13"
|
version = "1.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
30
debian/changelog
vendored
30
debian/changelog
vendored
|
|
@ -1,3 +1,33 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Jeremy Soller <jeremy@system76.com> Tue, 26 May 2026 08:30:59 -0600
|
||||||
|
|
||||||
cosmic-files (1.0.13) noble; urgency=medium
|
cosmic-files (1.0.13) noble; urgency=medium
|
||||||
|
|
||||||
* Epoch 1.0.13 version update
|
* Epoch 1.0.13 version update
|
||||||
|
|
|
||||||
78
docs/local-performance-and-portal-notes.md
Normal file
78
docs/local-performance-and-portal-notes.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Local performance and portal notes
|
||||||
|
|
||||||
|
Date: 2026-05-05
|
||||||
|
|
||||||
|
This repository carries a local patch aimed at improving initial directory
|
||||||
|
display latency in large folders, plus a system-level workaround for a COSMIC
|
||||||
|
portal FileChooser crash observed with Firefox and Chromium.
|
||||||
|
|
||||||
|
## Directory listing latency
|
||||||
|
|
||||||
|
The slow path was the initial construction of `Item` values in `src/tab.rs`.
|
||||||
|
On a test folder with about 2000 entries, raw filesystem enumeration and stat
|
||||||
|
calls completed in a few milliseconds, while COSMIC Files took multiple seconds
|
||||||
|
before showing the directory.
|
||||||
|
|
||||||
|
The local patch keeps initial item construction cheap:
|
||||||
|
|
||||||
|
- directory child counts are no longer computed synchronously in
|
||||||
|
`item_from_entry` and `item_from_gvfs_info`;
|
||||||
|
- initial MIME detection uses extension-based `mime_guess`;
|
||||||
|
- regular files use a generic file icon during the initial scan instead of
|
||||||
|
resolving the full MIME icon immediately.
|
||||||
|
|
||||||
|
This keeps folders and `.desktop` files special-cased, while avoiding expensive
|
||||||
|
per-file work for ordinary files during first paint.
|
||||||
|
|
||||||
|
Measured locally during investigation:
|
||||||
|
|
||||||
|
- before the MIME/icon changes: about 3.1 seconds for `~/Téléchargements`;
|
||||||
|
- after avoiding full MIME icon resolution during scan: below the temporary
|
||||||
|
100 ms perf-log threshold for the same folder.
|
||||||
|
|
||||||
|
## File chooser portal workaround
|
||||||
|
|
||||||
|
Firefox and Chromium were failing to open `Save As` on the first attempt because
|
||||||
|
`xdg-desktop-portal-cosmic` crashed while handling `FileChooser`.
|
||||||
|
|
||||||
|
Logs showed:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Backend call failed: Remote peer disconnected
|
||||||
|
xdg-desktop-portal-cosmic ... status=11/SEGV
|
||||||
|
```
|
||||||
|
|
||||||
|
The working local system workaround is to remove
|
||||||
|
`org.freedesktop.impl.portal.FileChooser` from:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/share/xdg-desktop-portal/portals/cosmic.portal
|
||||||
|
```
|
||||||
|
|
||||||
|
so the file chooser falls back to GTK. The resulting local `cosmic.portal`
|
||||||
|
keeps COSMIC for the other interfaces:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[portal]
|
||||||
|
DBusName=org.freedesktop.impl.portal.desktop.cosmic
|
||||||
|
Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.ScreenCast
|
||||||
|
UseIn=COSMIC
|
||||||
|
```
|
||||||
|
|
||||||
|
Backups created locally:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/share/xdg-desktop-portal/portals/cosmic.portal.bak-codex-20260505-filechooser
|
||||||
|
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505
|
||||||
|
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505-2
|
||||||
|
```
|
||||||
|
|
||||||
|
After editing portal files, restart:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl --user restart xdg-desktop-portal.service xdg-desktop-portal-gtk.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Package updates may overwrite `/usr/share/xdg-desktop-portal/portals/cosmic.portal`.
|
||||||
|
If `Save As` starts needing two attempts again, re-check that `FileChooser` has
|
||||||
|
not been reintroduced in `cosmic.portal`.
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
cosmic-files = Fitxers del COSMIC
|
cosmic-files = Fitxers del COSMIC
|
||||||
empty-folder = Carpeta buida
|
empty-folder = Carpeta buida
|
||||||
empty-folder-hidden = Carpeta buida (té elements ocults)
|
empty-folder-hidden = Carpeta buida (conté elements ocults)
|
||||||
no-results = No s'ha trobat cap resultat
|
no-results = No s'ha trobat cap resultat
|
||||||
filesystem = Sistema de fitxers
|
filesystem = Sistema de fitxers
|
||||||
home = Inici
|
home = Inici
|
||||||
networks = Xarxes
|
networks = Xarxes
|
||||||
notification-in-progress = Hi ha operacions amb fitxers en curs.
|
notification-in-progress = Operacions de fitxers en curs
|
||||||
trash = Paperera
|
trash = Paperera
|
||||||
recents = Recents
|
recents = Recents
|
||||||
undo = Desfés
|
undo = Desfés
|
||||||
|
|
@ -29,14 +29,14 @@ details = Detalls
|
||||||
dismiss = Descarta el missatge
|
dismiss = Descarta el missatge
|
||||||
operations-running =
|
operations-running =
|
||||||
{ $running } { $running ->
|
{ $running } { $running ->
|
||||||
[one] operacion
|
[one] operació
|
||||||
*[other] operacions
|
*[other] operacions
|
||||||
} en curs ({ $percent }%)...
|
} en curs ({ $percent }%)...
|
||||||
operations-running-finished =
|
operations-running-finished =
|
||||||
{ $running }{ $running ->
|
{ $running }{ $running ->
|
||||||
[one] operacion
|
[one] operació
|
||||||
*[other] operacions
|
*[other] operacions
|
||||||
} en curs ({ $percent }%), { $finished } acabat...
|
} en curs ({ $percent }%), { $finished } acabada/es...
|
||||||
pause = Pausa
|
pause = Pausa
|
||||||
resume = Reprèn
|
resume = Reprèn
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ extract-password-required = Cal una contrasenya
|
||||||
## Empty Trash Dialog
|
## Empty Trash Dialog
|
||||||
|
|
||||||
empty-trash = Buida la paperera
|
empty-trash = Buida la paperera
|
||||||
empty-trash-warning = Voleu suprimir permanentment tots els fitxers de la paperera?
|
empty-trash-warning = Els elements dins la Paperera s'eliminaran permanentment
|
||||||
|
|
||||||
## Mount Error Dialog
|
## 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-file = Crea un nou fitxer
|
||||||
create-new-folder = Crea una nova carpeta
|
create-new-folder = Crea una nova carpeta
|
||||||
file-name = Nom del fixer
|
file-name = Nom del fitxer
|
||||||
folder-name = Nom de la carpeta
|
folder-name = Nom de la carpeta
|
||||||
file-already-exists = Ja existeix un fitxer amb aquest nom.
|
file-already-exists = Ja existeix un fitxer amb aquest nom
|
||||||
folder-already-exists = Ja existeix una carpeta amb aquest nom.
|
folder-already-exists = Ja existeix una carpeta amb aquest nom
|
||||||
name-hidden = Els noms que comencin amb "." seran ocults.
|
name-hidden = Els noms que comencin amb "." s'ocultaran.
|
||||||
name-invalid = El nom no pot ser "{ $filename }".
|
name-invalid = El nom no pot ser "{ $filename }"
|
||||||
name-no-slashes = El nom no pot contenir barres.
|
name-no-slashes = El nom no pot contenir barres.
|
||||||
|
|
||||||
## Open/Save Dialog
|
## Open/Save Dialog
|
||||||
|
|
@ -353,3 +353,13 @@ sort-oldest-first = Primer més antics
|
||||||
sort-smallest-to-largest = De petit a gran
|
sort-smallest-to-largest = De petit a gran
|
||||||
sort-largest-to-smallest = De gran a petit
|
sort-largest-to-smallest = De gran a petit
|
||||||
run = Executa
|
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;
|
||||||
|
|
|
||||||
|
|
@ -427,3 +427,4 @@ context-action-confirm-warning =
|
||||||
*[other] položek
|
*[other] položek
|
||||||
}.
|
}.
|
||||||
run = Spustit
|
run = Spustit
|
||||||
|
rename-confirm = Přejmenovat
|
||||||
|
|
|
||||||
|
|
@ -437,3 +437,4 @@ context-action-confirm-warning =
|
||||||
*[other] Elementen
|
*[other] Elementen
|
||||||
}.
|
}.
|
||||||
run = Ausführen
|
run = Ausführen
|
||||||
|
rename-confirm = Umbenennen
|
||||||
|
|
|
||||||
|
|
@ -327,3 +327,4 @@ context-action-confirm-warning =
|
||||||
[one] στοιχείο
|
[one] στοιχείο
|
||||||
*[other] στοιχεία
|
*[other] στοιχεία
|
||||||
}.
|
}.
|
||||||
|
rename-confirm = Μετονομασία
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ save-file = Save file
|
||||||
|
|
||||||
## Open With Dialog
|
## Open With Dialog
|
||||||
open-with-title = How do you want to open "{$name}"?
|
open-with-title = How do you want to open "{$name}"?
|
||||||
|
open-with-set-default = Always use this app for this file type
|
||||||
browse-store = Browse {$store}
|
browse-store = Browse {$store}
|
||||||
other-apps = Other applications
|
other-apps = Other applications
|
||||||
related-apps = Related applications
|
related-apps = Related applications
|
||||||
|
|
@ -139,6 +140,11 @@ open-with = Open with
|
||||||
owner = Owner
|
owner = Owner
|
||||||
group = Group
|
group = Group
|
||||||
other = Other
|
other = Other
|
||||||
|
toolbar = Toolbar
|
||||||
|
toolbar-available = Available
|
||||||
|
toolbar-empty-hint = No buttons. Drag or add from below.
|
||||||
|
toolbar-reset = Reset to defaults
|
||||||
|
parent-directory = Parent directory
|
||||||
mixed = Mixed
|
mixed = Mixed
|
||||||
### Mode 0
|
### Mode 0
|
||||||
none = None
|
none = None
|
||||||
|
|
@ -303,6 +309,9 @@ item-created = Created: {$created}
|
||||||
item-modified = Modified: {$modified}
|
item-modified = Modified: {$modified}
|
||||||
item-accessed = Accessed: {$accessed}
|
item-accessed = Accessed: {$accessed}
|
||||||
calculating = Calculating...
|
calculating = Calculating...
|
||||||
|
checksum = {$kind} checksum
|
||||||
|
calculate = Calculate
|
||||||
|
error = Error
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
settings = Settings
|
settings = Settings
|
||||||
|
|
|
||||||
|
|
@ -414,3 +414,4 @@ context-action-confirm-warning =
|
||||||
*[other] kohdetta
|
*[other] kohdetta
|
||||||
}.
|
}.
|
||||||
run = Suorita
|
run = Suorita
|
||||||
|
rename-confirm = Nimeä uudelleen
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ save-file = Enregistrer fichier
|
||||||
## Open With Dialog
|
## Open With Dialog
|
||||||
|
|
||||||
open-with-title = Comment souhaitez-vous ouvrir "{ $name }" ?
|
open-with-title = Comment souhaitez-vous ouvrir "{ $name }" ?
|
||||||
|
open-with-set-default = Toujours utiliser cette application pour ce type de fichier
|
||||||
browse-store = Parcourir { $store }
|
browse-store = Parcourir { $store }
|
||||||
|
|
||||||
## Permanently delete Dialog
|
## Permanently delete Dialog
|
||||||
|
|
@ -130,6 +131,11 @@ open-with = Ouvrir avec
|
||||||
owner = Propriétaire
|
owner = Propriétaire
|
||||||
group = Groupe
|
group = Groupe
|
||||||
other = Autre
|
other = Autre
|
||||||
|
toolbar = Barre d'outils
|
||||||
|
toolbar-available = Disponibles
|
||||||
|
toolbar-empty-hint = Aucun bouton. Glisser-déposer ou ajouter depuis la liste ci-dessous.
|
||||||
|
toolbar-reset = Rétablir par défaut
|
||||||
|
parent-directory = Dossier parent
|
||||||
|
|
||||||
### Mode 0
|
### Mode 0
|
||||||
|
|
||||||
|
|
@ -445,3 +451,4 @@ context-action-confirm-warning =
|
||||||
}.
|
}.
|
||||||
run = Exécuter
|
run = Exécuter
|
||||||
context-action = Action contextuelle
|
context-action = Action contextuelle
|
||||||
|
rename-confirm = Renommer
|
||||||
|
|
|
||||||
|
|
@ -442,3 +442,4 @@ context-action-confirm-warning =
|
||||||
*[other] míreanna
|
*[other] míreanna
|
||||||
}.
|
}.
|
||||||
run = Rith
|
run = Rith
|
||||||
|
rename-confirm = Athainmnigh
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ cosmic-files = COSMIC Fájlok
|
||||||
comment = Fájlkezelő a COSMIC asztali környezethez
|
comment = Fájlkezelő a COSMIC asztali környezethez
|
||||||
keywords = mappa;fájl;kezelő;
|
keywords = mappa;fájl;kezelő;
|
||||||
empty-folder = Üres mappa
|
empty-folder = Üres mappa
|
||||||
empty-folder-hidden = Üres mappa (Rejtett elemeket tartalmaz)
|
empty-folder-hidden = Üres mappa (rejtett elemeket tartalmaz)
|
||||||
no-results = Nincs találat
|
no-results = Nincs találat
|
||||||
filesystem = Fájlrendszer
|
filesystem = Fájlrendszer
|
||||||
home = Saját mappa
|
home = Saját mappa
|
||||||
|
|
@ -17,7 +17,7 @@ desktop-view-options = Asztali nézet beállításai…
|
||||||
show-on-desktop = Megjelenítés az asztalon
|
show-on-desktop = Megjelenítés az asztalon
|
||||||
desktop-folder-content = Asztal mappa tartalma
|
desktop-folder-content = Asztal mappa tartalma
|
||||||
mounted-drives = Csatolt meghajtók
|
mounted-drives = Csatolt meghajtók
|
||||||
trash-folder-icon = Kuka ikonja
|
trash-folder-icon = Kuka ikon
|
||||||
icon-size-and-spacing = Ikonméret és távolság
|
icon-size-and-spacing = Ikonméret és távolság
|
||||||
icon-size = Ikonméret
|
icon-size = Ikonméret
|
||||||
grid-spacing = Rácsköz
|
grid-spacing = Rácsköz
|
||||||
|
|
@ -47,18 +47,18 @@ resume = Folytatás
|
||||||
|
|
||||||
## Compress Dialog
|
## Compress Dialog
|
||||||
|
|
||||||
create-archive = Tömörített fájl létrehozása
|
create-archive = Archívum létrehozása
|
||||||
|
|
||||||
## Extract Dialog
|
## Extract Dialog
|
||||||
|
|
||||||
extract-password-required = Jelszó szükséges
|
extract-password-required = Jelszó szükséges
|
||||||
extract-to = Kibontás ide…
|
extract-to = Kibontás ide…
|
||||||
extract-to-title = Kibontás mappába
|
extract-to-title = Kibontási cél kiválasztása
|
||||||
|
|
||||||
## Empty Trash Dialog
|
## Empty Trash Dialog
|
||||||
|
|
||||||
empty-trash = Kuka ürítése
|
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-warning = A Kukában lévő összes elem véglegesen törölve lesz
|
||||||
|
|
||||||
## Mount Error Dialog
|
## Mount Error Dialog
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ save-file = Fájl mentése
|
||||||
|
|
||||||
## Open With Dialog
|
## Open With Dialog
|
||||||
|
|
||||||
open-with-title = Hogyan szeretnéd megnyitni a következőt: „{ $name }”?
|
open-with-title = Hogyan szeretnéd megnyitni a(z) „{ $name }” fájlt?
|
||||||
browse-store = { $store } böngészése
|
browse-store = { $store } böngészése
|
||||||
other-apps = Egyéb alkalmazások
|
other-apps = Egyéb alkalmazások
|
||||||
related-apps = Hasonló alkalmazások
|
related-apps = Hasonló alkalmazások
|
||||||
|
|
@ -103,7 +103,7 @@ related-apps = Hasonló alkalmazások
|
||||||
selected-items = A(z) { $items } kijelölt elem
|
selected-items = A(z) { $items } kijelölt elem
|
||||||
permanently-delete-question = Végleges törlés?
|
permanently-delete-question = Végleges törlés?
|
||||||
delete = Törlés
|
delete = Törlés
|
||||||
permanently-delete-warning = { $target } véglegesen törölve lesz. A művelet nem visszavonható.
|
permanently-delete-warning = { $target } véglegesen törölve lesz. A művelet nem vonható vissza.
|
||||||
|
|
||||||
## Rename Dialog
|
## Rename Dialog
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ rename-folder = Mappa átnevezése
|
||||||
## Replace Dialog
|
## Replace Dialog
|
||||||
|
|
||||||
replace = Csere
|
replace = Csere
|
||||||
replace-title = „{ $filename }” már létezik
|
replace-title = „{ $filename }” már létezik ezen a helyen
|
||||||
replace-warning = Szeretnéd lecserélni a meglévő fájlt a mentendő fájllal? A cserével felülírod annak tartalmát.
|
replace-warning = Szeretnéd lecserélni a meglévő fájlt a mentendő fájllal? A cserével felülírod annak tartalmát.
|
||||||
replace-warning-operation = Szeretnéd lecserélni? A csere felülírja annak tartalmát.
|
replace-warning-operation = Szeretnéd lecserélni? A csere felülírja annak tartalmát.
|
||||||
original-file = Eredeti fájl
|
original-file = Eredeti fájl
|
||||||
|
|
@ -133,7 +133,7 @@ set-and-launch = Beállítás és indítás
|
||||||
open-with = Megnyitás ezzel
|
open-with = Megnyitás ezzel
|
||||||
owner = Tulajdonos
|
owner = Tulajdonos
|
||||||
group = Csoport
|
group = Csoport
|
||||||
other = Többi
|
other = Mások
|
||||||
|
|
||||||
### Mode 0
|
### Mode 0
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ read-write-execute = Olvasás, írás és végrehajtás
|
||||||
favorite-path-error = Hiba a könyvtár megnyitásakor
|
favorite-path-error = Hiba a könyvtár megnyitásakor
|
||||||
favorite-path-error-description =
|
favorite-path-error-description =
|
||||||
Nem sikerült megnyitni ezt: „{ $path }”
|
Nem sikerült megnyitni ezt: „{ $path }”
|
||||||
„{ $path }” lehet nem létezik, vagy nincs jogosultságod a megnyitásához
|
A(z) „{ $path }” útvonal lehet, hogy nem létezik, vagy nincs jogosultságod a megnyitásához
|
||||||
|
|
||||||
Szeretnéd eltávolítani az oldalsávról?
|
Szeretnéd eltávolítani az oldalsávról?
|
||||||
remove = Eltávolítás
|
remove = Eltávolítás
|
||||||
|
|
@ -187,9 +187,9 @@ keep = Megtartás
|
||||||
## Add Network Drive
|
## Add Network Drive
|
||||||
|
|
||||||
add-network-drive = Hálózati meghajtó hozzáadása
|
add-network-drive = Hálózati meghajtó hozzáadása
|
||||||
connect = Csatlakozás
|
connect = Kapcsolódás
|
||||||
connect-anonymously = Csatlakozás névtelenül
|
connect-anonymously = Kapcsolódás névtelenül
|
||||||
connecting = Csatlakozás…
|
connecting = Kapcsolódás…
|
||||||
domain = Tartomány
|
domain = Tartomány
|
||||||
enter-server-address = Add meg a kiszolgáló címét
|
enter-server-address = Add meg a kiszolgáló címét
|
||||||
network-drive-description =
|
network-drive-description =
|
||||||
|
|
@ -209,7 +209,7 @@ network-drive-schemes =
|
||||||
network-drive-error = Nem érhető el a hálózati meghajtó
|
network-drive-error = Nem érhető el a hálózati meghajtó
|
||||||
password = Jelszó
|
password = Jelszó
|
||||||
remember-password = Jelszó megjegyzése
|
remember-password = Jelszó megjegyzése
|
||||||
try-again = Újra
|
try-again = Próbáld újra
|
||||||
username = Felhasználónév
|
username = Felhasználónév
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
@ -217,7 +217,7 @@ username = Felhasználónév
|
||||||
cancelled = Megszakítva
|
cancelled = Megszakítva
|
||||||
edit-history = Fájlműveleti előzmények
|
edit-history = Fájlműveleti előzmények
|
||||||
history = Előzmények
|
history = Előzmények
|
||||||
no-history = Nem találhatók elemek az előzményekben
|
no-history = Nem találhatók elemek az előzményekben.
|
||||||
pending = Függőben
|
pending = Függőben
|
||||||
progress = { $percent }%
|
progress = { $percent }%
|
||||||
progress-cancelled = { $percent }%, megszakítva
|
progress-cancelled = { $percent }%, megszakítva
|
||||||
|
|
@ -251,13 +251,13 @@ deleting =
|
||||||
{ $items } { $items ->
|
{ $items } { $items ->
|
||||||
[one] elem
|
[one] elem
|
||||||
*[other] elem
|
*[other] elem
|
||||||
} törlése a kukából ({ $progress })…
|
} törlése a Kukából ({ $progress })…
|
||||||
deleted =
|
deleted =
|
||||||
{ $items } { $items ->
|
{ $items } { $items ->
|
||||||
[one] elem
|
[one] elem
|
||||||
*[other] elem
|
*[other] elem
|
||||||
} törölve a kukából
|
} törölve a Kukából
|
||||||
emptying-trash = { trash } kiürítése ({ $progress })…
|
emptying-trash = { trash } ürítése ({ $progress })…
|
||||||
emptied-trash = { trash } kiürítve
|
emptied-trash = { trash } kiürítve
|
||||||
extracting =
|
extracting =
|
||||||
{ $items } { $items ->
|
{ $items } { $items ->
|
||||||
|
|
@ -309,12 +309,12 @@ restoring =
|
||||||
{ $items } { $items ->
|
{ $items } { $items ->
|
||||||
[one] elem
|
[one] elem
|
||||||
*[other] elem
|
*[other] elem
|
||||||
} visszaállítása a kukából ({ $progress })…
|
} visszaállítása a Kukából ({ $progress })…
|
||||||
restored =
|
restored =
|
||||||
{ $items } { $items ->
|
{ $items } { $items ->
|
||||||
[one] elem
|
[one] elem
|
||||||
*[other] elem
|
*[other] elem
|
||||||
} visszaállítva a kukából
|
} visszaállítva a Kukából
|
||||||
unknown-folder = ismeretlen mappa
|
unknown-folder = ismeretlen mappa
|
||||||
|
|
||||||
## Open with
|
## Open with
|
||||||
|
|
@ -330,7 +330,7 @@ items = Elemek: { $items }
|
||||||
item-size = Méret: { $size }
|
item-size = Méret: { $size }
|
||||||
item-created = Létrehozva: { $created }
|
item-created = Létrehozva: { $created }
|
||||||
item-modified = Módosítva: { $modified }
|
item-modified = Módosítva: { $modified }
|
||||||
item-accessed = Hozzáférve: { $accessed }
|
item-accessed = Utolsó hozzáférés: { $accessed }
|
||||||
calculating = Számítás…
|
calculating = Számítás…
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
@ -349,8 +349,8 @@ light = Világos
|
||||||
### Type to Search
|
### Type to Search
|
||||||
|
|
||||||
type-to-search = Gépeléssel keresés
|
type-to-search = Gépeléssel keresés
|
||||||
type-to-search-recursive = Keresés a jelenlegi mappában és almappákban
|
type-to-search-recursive = Keresés a jelenlegi mappában és az almappákban
|
||||||
type-to-search-enter-path = Elérési út megadása
|
type-to-search-enter-path = Elérési út megnyitása
|
||||||
# Context menu
|
# Context menu
|
||||||
add-to-sidebar = Hozzáadás az oldalsávhoz
|
add-to-sidebar = Hozzáadás az oldalsávhoz
|
||||||
compress = Tömörítés…
|
compress = Tömörítés…
|
||||||
|
|
@ -360,8 +360,8 @@ extract-here = Kibontás
|
||||||
new-file = Új fájl…
|
new-file = Új fájl…
|
||||||
new-folder = Új mappa…
|
new-folder = Új mappa…
|
||||||
open-in-terminal = Megnyitás a terminálban
|
open-in-terminal = Megnyitás a terminálban
|
||||||
move-to-trash = Kukába helyezés
|
move-to-trash = Áthelyezés a Kukába
|
||||||
restore-from-trash = Visszaállítás a kukából
|
restore-from-trash = Visszaállítás a Kukából
|
||||||
remove-from-sidebar = Eltávolítás az oldalsávról
|
remove-from-sidebar = Eltávolítás az oldalsávról
|
||||||
sort-by-name = Név szerinti rendezés
|
sort-by-name = Név szerinti rendezés
|
||||||
sort-by-modified = Módosítás szerinti rendezés
|
sort-by-modified = Módosítás szerinti rendezés
|
||||||
|
|
@ -372,8 +372,8 @@ remove-from-recents = Eltávolítás a legutóbbiak közül
|
||||||
## Desktop
|
## Desktop
|
||||||
|
|
||||||
change-wallpaper = Háttérkép cseréje…
|
change-wallpaper = Háttérkép cseréje…
|
||||||
desktop-appearance = Asztali megjelenés…
|
desktop-appearance = Asztal megjelenése…
|
||||||
display-settings = Képernyő-beállítások…
|
display-settings = Kijelzőbeállítások…
|
||||||
|
|
||||||
# Menu
|
# Menu
|
||||||
|
|
||||||
|
|
@ -406,7 +406,7 @@ grid-view = Rácsnézet
|
||||||
list-view = Listanézet
|
list-view = Listanézet
|
||||||
show-hidden-files = Rejtett fájlok megjelenítése
|
show-hidden-files = Rejtett fájlok megjelenítése
|
||||||
list-directories-first = Könyvtárak listázása először
|
list-directories-first = Könyvtárak listázása először
|
||||||
gallery-preview = Galéria előnézet
|
gallery-preview = Galéria-előnézet
|
||||||
menu-settings = Beállítások…
|
menu-settings = Beállítások…
|
||||||
menu-about = A COSMIC Fájlok névjegye…
|
menu-about = A COSMIC Fájlok névjegye…
|
||||||
|
|
||||||
|
|
@ -415,14 +415,14 @@ menu-about = A COSMIC Fájlok névjegye…
|
||||||
sort = Rendezés
|
sort = Rendezés
|
||||||
sort-a-z = A-Z
|
sort-a-z = A-Z
|
||||||
sort-z-a = Z-A
|
sort-z-a = Z-A
|
||||||
sort-newest-first = Legújabb előre
|
sort-newest-first = Legújabb elöl
|
||||||
sort-oldest-first = Legrégibb előre
|
sort-oldest-first = Legrégibb elöl
|
||||||
sort-smallest-to-largest = Legkisebbtől a legnagyobbig
|
sort-smallest-to-largest = Legkisebbtől a legnagyobbig
|
||||||
sort-largest-to-smallest = Legnagyobbtól a legkisebbig
|
sort-largest-to-smallest = Legnagyobbtól a legkisebbig
|
||||||
repository = Tároló
|
repository = Tároló
|
||||||
support = Támogatás
|
support = Támogatás
|
||||||
progress-failed = { $percent }%, sikertelen
|
progress-failed = { $percent }%, sikertelen
|
||||||
empty-trash-title = Kuka ürítése?
|
empty-trash-title = Kiüríted a Kukát?
|
||||||
type-to-search-select = Kijelöli az első egyező fájlt vagy mappát
|
type-to-search-select = Kijelöli az első egyező fájlt vagy mappát
|
||||||
pasted-image = Beillesztett kép
|
pasted-image = Beillesztett kép
|
||||||
pasted-text = Beillesztett szöveg
|
pasted-text = Beillesztett szöveg
|
||||||
|
|
@ -445,3 +445,4 @@ context-action-confirm-warning =
|
||||||
*[other] elemen
|
*[other] elemen
|
||||||
} fog lefutni.
|
} fog lefutni.
|
||||||
run = Futtatás
|
run = Futtatás
|
||||||
|
rename-confirm = Átnevezés
|
||||||
|
|
|
||||||
|
|
@ -327,3 +327,4 @@ context-action-confirm-warning =
|
||||||
[one] item
|
[one] item
|
||||||
*[other] item
|
*[other] item
|
||||||
}.
|
}.
|
||||||
|
rename-confirm = Ganti nama
|
||||||
|
|
|
||||||
|
|
@ -327,3 +327,4 @@ context-action-confirm-warning =
|
||||||
*[other] нәрсеге
|
*[other] нәрсеге
|
||||||
}.
|
}.
|
||||||
run = Орындау
|
run = Орындау
|
||||||
|
rename-confirm = Атын өзгерту
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,330 @@
|
||||||
|
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
|
||||||
|
|
@ -98,7 +98,7 @@ permanently-delete-question = Slett permanent
|
||||||
permanently-delete-warning = Er du sukker på om du vil permanent slette { $target }? Dette kan ikke angres.
|
permanently-delete-warning = Er du sukker på om du vil permanent slette { $target }? Dette kan ikke angres.
|
||||||
rename-file = Endre filnavn
|
rename-file = Endre filnavn
|
||||||
rename-folder = Endre mappenavn
|
rename-folder = Endre mappenavn
|
||||||
replace-title = "{ $filename }" finnes allerede på denne plasseringen.
|
replace-title = «{ $filename }» finnes allerede på denne plasseringen
|
||||||
replace-warning = Vil du bytte den ut med filen du lagrer? Dette vil overskrive innholdet.
|
replace-warning = Vil du bytte den ut med filen du lagrer? Dette vil overskrive innholdet.
|
||||||
replace-warning-operation = Vil du erstatte den? Dette vil overskrive innholdet.
|
replace-warning-operation = Vil du erstatte den? Dette vil overskrive innholdet.
|
||||||
original-file = Orginalfil
|
original-file = Orginalfil
|
||||||
|
|
@ -122,8 +122,8 @@ read-write = Kun skrive- og lesbar
|
||||||
read-write-execute = Lesbar, skrivbar og kjørbar
|
read-write-execute = Lesbar, skrivbar og kjørbar
|
||||||
favorite-path-error = Feil ved åpning av mappen
|
favorite-path-error = Feil ved åpning av mappen
|
||||||
favorite-path-error-description =
|
favorite-path-error-description =
|
||||||
Kunne ikke åpne "{ $path }".
|
Klarte ikke åpne «{ $path }».
|
||||||
Den kan ikke eksistere eller så har du ikke tilgang til å åpne den.
|
«{ $path }» finnes ikke eller så har du ikke tilgang til å åpne den.
|
||||||
|
|
||||||
Vil du fjerne den fra sidepanelet?
|
Vil du fjerne den fra sidepanelet?
|
||||||
keep = Behold
|
keep = Behold
|
||||||
|
|
@ -140,7 +140,7 @@ zoom-out = Zoom ut
|
||||||
cut = Klipp ut
|
cut = Klipp ut
|
||||||
set-and-launch = Still inn og start
|
set-and-launch = Still inn og start
|
||||||
domain = Domene
|
domain = Domene
|
||||||
enter-server-address = Angi serveradresse
|
enter-server-address = Angi tjeneradresse
|
||||||
network-drive-description =
|
network-drive-description =
|
||||||
Serveradresser inkluderer ett protokollprefiks og en addresse.
|
Serveradresser inkluderer ett protokollprefiks og en addresse.
|
||||||
Examples: ssh://192.168.0.1, ftp://[2001:db8::1]
|
Examples: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||||
|
|
|
||||||
|
|
@ -446,3 +446,4 @@ context-action-confirm-warning =
|
||||||
*[other] elementów
|
*[other] elementów
|
||||||
}.
|
}.
|
||||||
run = Uruchom
|
run = Uruchom
|
||||||
|
rename-confirm = Zmień nazwę
|
||||||
|
|
|
||||||
|
|
@ -445,3 +445,4 @@ context-action-confirm-warning =
|
||||||
*[other] itens
|
*[other] itens
|
||||||
}.
|
}.
|
||||||
run = Executar
|
run = Executar
|
||||||
|
rename-confirm = Renomear
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
cosmic-files = Ficheiros COSMIC
|
cosmic-files = Ficheiros COSMIC
|
||||||
comment = Gerenciador de arquivos do COSMIC
|
comment = Gerenciador de arquivos do COSMIC
|
||||||
keywords = Pastas;Gerenciador;Arquivos;Gestor;Explorer;
|
keywords = Pasta;Gestor;
|
||||||
empty-folder = Pasta vazia
|
empty-folder = Pasta vazia
|
||||||
empty-folder-hidden = Pasta vazia (tem ficheiros ocultos)
|
empty-folder-hidden = Pasta vazia (tem ficheiros ocultos)
|
||||||
no-results = Nenhum resultado encontrado
|
no-results = Nenhum resultado encontrado
|
||||||
filesystem = Sistema de ficheiros
|
filesystem = Sistema de ficheiros
|
||||||
home = Pasta Pessoal
|
home = Pasta Pessoal
|
||||||
notification-in-progress = Operações em curso.
|
notification-in-progress = Operações em curso
|
||||||
trash = Lixo
|
trash = Lixo
|
||||||
undo = Anular
|
undo = Anular
|
||||||
# List view
|
# 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
|
show-on-desktop = Mostrar na área de trabalho
|
||||||
desktop-folder-content = Conteúdo da pasta da área de trabalho
|
desktop-folder-content = Conteúdo da pasta da área de trabalho
|
||||||
mounted-drives = Dispositivos montados
|
mounted-drives = Dispositivos montados
|
||||||
trash-folder-icon = Ícone do lixo
|
trash-folder-icon = Ícone da pasta lixo
|
||||||
icon-size-and-spacing = Tamanho e espaçamento do ícone
|
icon-size-and-spacing = Tamanho e espaçamento do ícone
|
||||||
icon-size = Tamanho do ícone
|
icon-size = Tamanho do ícone
|
||||||
grid-spacing = Espaçamento entre ícones
|
grid-spacing = Espaçamento entre ícones
|
||||||
|
|
@ -354,3 +354,4 @@ sort-smallest-to-largest = Do menor para o maior
|
||||||
sort-largest-to-smallest = Do maior para o menor
|
sort-largest-to-smallest = Do maior para o menor
|
||||||
context-action-confirm-title = Executar "{ $name }"?
|
context-action-confirm-title = Executar "{ $name }"?
|
||||||
run = Executar
|
run = Executar
|
||||||
|
rename-confirm = Renomear
|
||||||
|
|
|
||||||
|
|
@ -389,3 +389,4 @@ context-action-confirm-warning =
|
||||||
*[other] элем.
|
*[other] элем.
|
||||||
}.
|
}.
|
||||||
run = Выполнить
|
run = Выполнить
|
||||||
|
rename-confirm = Переименовать
|
||||||
|
|
|
||||||
|
|
@ -327,3 +327,4 @@ compress = Сажми...
|
||||||
copy-to = Умножи у...
|
copy-to = Умножи у...
|
||||||
move-to = Помери у...
|
move-to = Помери у...
|
||||||
copy-path = Умножи путању
|
copy-path = Умножи путању
|
||||||
|
rename-confirm = Преименуј
|
||||||
|
|
|
||||||
|
|
@ -418,3 +418,4 @@ context-action-confirm-warning =
|
||||||
*[other] objekt
|
*[other] objekt
|
||||||
}.
|
}.
|
||||||
run = Kör
|
run = Kör
|
||||||
|
rename-confirm = Byt namn
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ copying =
|
||||||
Копіювання { $items } { $items ->
|
Копіювання { $items } { $items ->
|
||||||
[one] елемента
|
[one] елемента
|
||||||
*[other] елементів
|
*[other] елементів
|
||||||
} з «{ $from }» в «{ $to }» ({ $progress })...
|
} з «{ $from }» до «{ $to }» ({ $progress })...
|
||||||
copied =
|
copied =
|
||||||
Скопійовано { $items } { $items ->
|
Скопійовано { $items } { $items ->
|
||||||
[one] елемент
|
[one] елемент
|
||||||
|
|
@ -95,7 +95,7 @@ moving =
|
||||||
Переміщення { $items } { $items ->
|
Переміщення { $items } { $items ->
|
||||||
[one] елемента
|
[one] елемента
|
||||||
*[other] елементів
|
*[other] елементів
|
||||||
} з «{ $from }» в «{ $to }» ({ $progress })...
|
} з «{ $from }» до «{ $to }» ({ $progress })...
|
||||||
moved =
|
moved =
|
||||||
Переміщено { $items } { $items ->
|
Переміщено { $items } { $items ->
|
||||||
[one] елемент
|
[one] елемент
|
||||||
|
|
@ -299,7 +299,7 @@ extracting =
|
||||||
Видобування { $items } { $items ->
|
Видобування { $items } { $items ->
|
||||||
[one] елемента
|
[one] елемента
|
||||||
*[other] елементів
|
*[other] елементів
|
||||||
} з «{ $from }» в «{ $to }» ({ $progress })...
|
} з «{ $from }» до «{ $to }» ({ $progress })...
|
||||||
extracted =
|
extracted =
|
||||||
Видобуто { $items } { $items ->
|
Видобуто { $items } { $items ->
|
||||||
[one] елемент
|
[one] елемент
|
||||||
|
|
@ -364,10 +364,10 @@ removed-from-recents =
|
||||||
*[other] елементів
|
*[other] елементів
|
||||||
} з { recents }
|
} з { recents }
|
||||||
empty-trash-title = Спорожити смітник?
|
empty-trash-title = Спорожити смітник?
|
||||||
type-to-search-select = Вибирає перший відповідний файл або папку
|
type-to-search-select = Вибирає перший відповідний файл або теку
|
||||||
pasted-image = Вставлене зображення
|
pasted-image = Вставлене зображення
|
||||||
pasted-text = Вставлений текст
|
pasted-text = Вставлений текст
|
||||||
pasted-video = Вставлене відео
|
pasted-video = Вставлене видиво
|
||||||
copy-to-button-label = Копіювати
|
copy-to-button-label = Копіювати
|
||||||
move-to-button-label = Перемістити
|
move-to-button-label = Перемістити
|
||||||
copy-to = Копіювати до…
|
copy-to = Копіювати до…
|
||||||
|
|
@ -387,3 +387,5 @@ context-action-confirm-warning =
|
||||||
*[other] елементів
|
*[other] елементів
|
||||||
}.
|
}.
|
||||||
context-action = Контекстна дія
|
context-action = Контекстна дія
|
||||||
|
rename-confirm = Перейменувати
|
||||||
|
mixed = Змішано
|
||||||
|
|
|
||||||
|
|
@ -445,3 +445,4 @@ context-action-confirm-warning =
|
||||||
*[other] 项目
|
*[other] 项目
|
||||||
} 上运行。
|
} 上运行。
|
||||||
context-action = 环境行动
|
context-action = 环境行动
|
||||||
|
rename-confirm = 重命名
|
||||||
|
|
|
||||||
|
|
@ -392,3 +392,4 @@ context-action-confirm-warning =
|
||||||
*[other] 項目
|
*[other] 項目
|
||||||
} 上執行。
|
} 上執行。
|
||||||
run = 執行
|
run = 執行
|
||||||
|
rename-confirm = 重新命名
|
||||||
|
|
|
||||||
2
justfile
2
justfile
|
|
@ -111,7 +111,7 @@ uninstall:
|
||||||
vendor:
|
vendor:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
mkdir -p .cargo
|
mkdir -p .cargo
|
||||||
cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config.toml
|
cargo vendor --locked --sync Cargo.toml | head -n -1 > .cargo/config.toml
|
||||||
echo 'directory = "vendor"' >> .cargo/config.toml
|
echo 'directory = "vendor"' >> .cargo/config.toml
|
||||||
echo >> .cargo/config.toml
|
echo >> .cargo/config.toml
|
||||||
echo '[env]' >> .cargo/config.toml
|
echo '[env]' >> .cargo/config.toml
|
||||||
|
|
|
||||||
587
src/app.rs
587
src/app.rs
|
|
@ -7,6 +7,7 @@ use cosmic::iced::clipboard::dnd::DndAction;
|
||||||
use cosmic::iced::core::SmolStr;
|
use cosmic::iced::core::SmolStr;
|
||||||
use cosmic::iced::core::widget::operation::focusable::unfocus;
|
use cosmic::iced::core::widget::operation::focusable::unfocus;
|
||||||
use cosmic::iced::futures::{self, SinkExt};
|
use cosmic::iced::futures::{self, SinkExt};
|
||||||
|
use cosmic::iced::keyboard::key::Physical;
|
||||||
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
|
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
|
||||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||||
use cosmic::iced::platform_specific::shell::wayland::commands::overlap_notify::overlap_notify;
|
use cosmic::iced::platform_specific::shell::wayland::commands::overlap_notify::overlap_notify;
|
||||||
|
|
@ -41,15 +42,21 @@ use notify_debouncer_full::notify::{self, RecommendedWatcher};
|
||||||
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
|
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use slotmap::Key as SlotMapKey;
|
use slotmap::Key as SlotMapKey;
|
||||||
use std::any::TypeId;
|
#[cfg(feature = "notify")]
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
|
use std::sync::Mutex;
|
||||||
use std::future::Future;
|
use std::{
|
||||||
use std::num::NonZeroU16;
|
any::TypeId,
|
||||||
use std::path::{Path, PathBuf};
|
collections::{BTreeMap, BTreeSet, HashMap, VecDeque},
|
||||||
use std::pin::Pin;
|
env, fmt, fs,
|
||||||
use std::sync::{Arc, LazyLock, Mutex};
|
future::Future,
|
||||||
use std::time::{self, Duration, Instant};
|
io,
|
||||||
use std::{env, fmt, fs, io, process};
|
num::NonZeroU16,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
pin::Pin,
|
||||||
|
process,
|
||||||
|
sync::{Arc, LazyLock},
|
||||||
|
time::{self, Duration, Instant},
|
||||||
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use trash::TrashItem;
|
use trash::TrashItem;
|
||||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||||
|
|
@ -61,12 +68,12 @@ use crate::clipboard::{
|
||||||
};
|
};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
||||||
TimeConfig, TypeToSearch,
|
TimeConfig, ToolbarAction, TypeToSearch, default_toolbar,
|
||||||
};
|
};
|
||||||
use crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
use crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
||||||
use crate::key_bind::key_binds;
|
use crate::key_bind::key_binds;
|
||||||
use crate::localize::LANGUAGE_SORTER;
|
use crate::localize::LANGUAGE_SORTER;
|
||||||
use crate::mime_app::{self, MimeApp, MimeAppCache};
|
use crate::mime_app::{self, MimeApp, MimeAppCache, MimeAppMatch};
|
||||||
use crate::mounter::{
|
use crate::mounter::{
|
||||||
MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage,
|
MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage,
|
||||||
};
|
};
|
||||||
|
|
@ -127,6 +134,105 @@ pub struct Flags {
|
||||||
pub uris: Vec<url::Url>,
|
pub uris: Vec<url::Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yoda phase 3: MIME for the DnD payload carried when a user drags a
|
||||||
|
/// toolbar row in the Settings editor. A single byte = ToolbarAction
|
||||||
|
/// discriminant (see `ToolbarAction::to_u8`).
|
||||||
|
const TOOLBAR_MIME: &str = "application/x-cosmic-files-toolbar-action";
|
||||||
|
|
||||||
|
/// Yoda phase 3: DnD payload wrapping a ToolbarAction discriminant.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ToolbarActionPayload(pub u8);
|
||||||
|
|
||||||
|
impl cosmic::iced::clipboard::mime::AsMimeTypes for ToolbarActionPayload {
|
||||||
|
fn available(&self) -> std::borrow::Cow<'static, [String]> {
|
||||||
|
std::borrow::Cow::Owned(vec![TOOLBAR_MIME.to_owned()])
|
||||||
|
}
|
||||||
|
fn as_bytes(&self, mime_type: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||||
|
if mime_type == TOOLBAR_MIME {
|
||||||
|
Some(std::borrow::Cow::Owned(vec![self.0]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cosmic::iced::clipboard::mime::AllowedMimeTypes for ToolbarActionPayload {
|
||||||
|
fn allowed() -> std::borrow::Cow<'static, [String]> {
|
||||||
|
std::borrow::Cow::Owned(vec![TOOLBAR_MIME.to_owned()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<(Vec<u8>, String)> for ToolbarActionPayload {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from((data, _mime): (Vec<u8>, String)) -> Result<Self, Self::Error> {
|
||||||
|
if data.len() == 1 {
|
||||||
|
Ok(Self(data[0]))
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Yoda phase 3 helper: map a ToolbarAction to its button UI (icon name,
|
||||||
|
/// localized label, app Message). Shared by the toolbar renderer in
|
||||||
|
/// `view()` and by the Settings page row renderer so the two stay in
|
||||||
|
/// sync.
|
||||||
|
fn toolbar_action_ui(a: ToolbarAction) -> (&'static str, String, Message) {
|
||||||
|
match a {
|
||||||
|
ToolbarAction::LocationUp => (
|
||||||
|
"go-up-symbolic",
|
||||||
|
fl!("parent-directory"),
|
||||||
|
Action::LocationUp.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::Reload => (
|
||||||
|
"view-refresh-symbolic",
|
||||||
|
fl!("reload-folder"),
|
||||||
|
Action::Reload.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::NewFolder => (
|
||||||
|
"folder-new-symbolic",
|
||||||
|
fl!("new-folder"),
|
||||||
|
Action::NewFolder.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::NewFile => (
|
||||||
|
"document-new-symbolic",
|
||||||
|
fl!("new-file"),
|
||||||
|
Action::NewFile.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::Rename => (
|
||||||
|
"pencil-symbolic",
|
||||||
|
fl!("rename"),
|
||||||
|
Action::Rename.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::Delete => (
|
||||||
|
"edit-delete-symbolic",
|
||||||
|
fl!("delete"),
|
||||||
|
Action::Delete.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::Cut => ("edit-cut-symbolic", fl!("cut"), Action::Cut.message(None)),
|
||||||
|
ToolbarAction::Copy => (
|
||||||
|
"edit-copy-symbolic",
|
||||||
|
fl!("copy"),
|
||||||
|
Action::Copy.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::Paste => (
|
||||||
|
"edit-paste-symbolic",
|
||||||
|
fl!("paste"),
|
||||||
|
Action::Paste.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::ToggleShowHidden => (
|
||||||
|
"view-reveal-symbolic",
|
||||||
|
fl!("show-hidden-files"),
|
||||||
|
Action::ToggleShowHidden.message(None),
|
||||||
|
),
|
||||||
|
ToolbarAction::OpenTerminal => (
|
||||||
|
"utilities-terminal-symbolic",
|
||||||
|
fl!("open-in-terminal"),
|
||||||
|
Action::OpenTerminal.message(None),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
About,
|
About,
|
||||||
|
|
@ -154,6 +260,8 @@ pub enum Action {
|
||||||
HistoryPrevious,
|
HistoryPrevious,
|
||||||
ItemDown,
|
ItemDown,
|
||||||
ItemLeft,
|
ItemLeft,
|
||||||
|
ItemPageDown,
|
||||||
|
ItemPageUp,
|
||||||
ItemRight,
|
ItemRight,
|
||||||
ItemUp,
|
ItemUp,
|
||||||
LocationUp,
|
LocationUp,
|
||||||
|
|
@ -227,6 +335,8 @@ impl Action {
|
||||||
Self::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious),
|
Self::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious),
|
||||||
Self::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown),
|
Self::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown),
|
||||||
Self::ItemLeft => Message::TabMessage(entity_opt, tab::Message::ItemLeft),
|
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::ItemRight => Message::TabMessage(entity_opt, tab::Message::ItemRight),
|
||||||
Self::ItemUp => Message::TabMessage(entity_opt, tab::Message::ItemUp),
|
Self::ItemUp => Message::TabMessage(entity_opt, tab::Message::ItemUp),
|
||||||
Self::LocationUp => Message::TabMessage(entity_opt, tab::Message::LocationUp),
|
Self::LocationUp => Message::TabMessage(entity_opt, tab::Message::LocationUp),
|
||||||
|
|
@ -355,7 +465,7 @@ pub enum Message {
|
||||||
ExtractToResult(DialogResult),
|
ExtractToResult(DialogResult),
|
||||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||||
Focused(window::Id),
|
Focused(window::Id),
|
||||||
Key(window::Id, Modifiers, Key, Option<SmolStr>),
|
Key(window::Id, Modifiers, Key, Physical, Option<SmolStr>),
|
||||||
LaunchUrl(String),
|
LaunchUrl(String),
|
||||||
MaybeExit,
|
MaybeExit,
|
||||||
ModifiersChanged(window::Id, Modifiers),
|
ModifiersChanged(window::Id, Modifiers),
|
||||||
|
|
@ -389,6 +499,7 @@ pub enum Message {
|
||||||
OpenWithBrowse,
|
OpenWithBrowse,
|
||||||
OpenWithDialog(Option<Entity>),
|
OpenWithDialog(Option<Entity>),
|
||||||
OpenWithSelection(usize),
|
OpenWithSelection(usize),
|
||||||
|
OpenWithToggleDefault(bool),
|
||||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||||
Overlap(window::Id, OverlapNotifyEvent),
|
Overlap(window::Id, OverlapNotifyEvent),
|
||||||
Paste(Option<Entity>),
|
Paste(Option<Entity>),
|
||||||
|
|
@ -415,6 +526,7 @@ pub enum Message {
|
||||||
PendingPauseAll(bool),
|
PendingPauseAll(bool),
|
||||||
PermanentlyDelete(Option<Entity>),
|
PermanentlyDelete(Option<Entity>),
|
||||||
Preview(Option<Entity>),
|
Preview(Option<Entity>),
|
||||||
|
ReloadMimeAppCache,
|
||||||
ReorderTab(ReorderEvent),
|
ReorderTab(ReorderEvent),
|
||||||
RescanRecents,
|
RescanRecents,
|
||||||
RescanTrash,
|
RescanTrash,
|
||||||
|
|
@ -429,6 +541,22 @@ pub enum Message {
|
||||||
SearchInput(String),
|
SearchInput(String),
|
||||||
SetShowDetails(bool),
|
SetShowDetails(bool),
|
||||||
SetShowRecents(bool),
|
SetShowRecents(bool),
|
||||||
|
/// Yoda phase 3 — toolbar editing messages.
|
||||||
|
ToolbarAdd(ToolbarAction),
|
||||||
|
ToolbarRemove(ToolbarAction),
|
||||||
|
ToolbarReorder {
|
||||||
|
src: ToolbarAction,
|
||||||
|
target: ToolbarAction,
|
||||||
|
},
|
||||||
|
/// Move one step up (toward index 0) inside the enabled toolbar list.
|
||||||
|
ToolbarMoveUp(ToolbarAction),
|
||||||
|
/// Move one step down (toward the end) inside the enabled toolbar list.
|
||||||
|
ToolbarMoveDown(ToolbarAction),
|
||||||
|
ToolbarReset,
|
||||||
|
/// Click on a toolbar button (via segmented_button activation).
|
||||||
|
ToolbarTabActivate(segmented_button::Entity),
|
||||||
|
/// Drag-reorder inside the toolbar (via segmented_button drag).
|
||||||
|
ToolbarTabReorder(segmented_button::ReorderEvent),
|
||||||
SetTypeToSearch(TypeToSearch),
|
SetTypeToSearch(TypeToSearch),
|
||||||
SystemThemeModeChange,
|
SystemThemeModeChange,
|
||||||
Size(window::Id, Size),
|
Size(window::Id, Size),
|
||||||
|
|
@ -558,7 +686,10 @@ pub enum DialogPage {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
mime: mime_guess::Mime,
|
mime: mime_guess::Mime,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
store_opt: Option<MimeApp>,
|
store_opt: Option<Arc<MimeApp>>,
|
||||||
|
/// When true, the chosen app is written to mimeapps.list as the
|
||||||
|
/// default handler for `mime` after it spawns.
|
||||||
|
set_default: bool,
|
||||||
},
|
},
|
||||||
PermanentlyDelete {
|
PermanentlyDelete {
|
||||||
paths: Box<[PathBuf]>,
|
paths: Box<[PathBuf]>,
|
||||||
|
|
@ -654,13 +785,6 @@ impl DialogPages {
|
||||||
|
|
||||||
pub struct FavoriteIndex(usize);
|
pub struct FavoriteIndex(usize);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
enum MimeAppMatch {
|
|
||||||
Exact,
|
|
||||||
Related,
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MounterData(MounterKey, MounterItem);
|
pub struct MounterData(MounterKey, MounterItem);
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -716,6 +840,11 @@ pub struct App {
|
||||||
nav_bar_context_id: segmented_button::Entity,
|
nav_bar_context_id: segmented_button::Entity,
|
||||||
nav_model: segmented_button::SingleSelectModel,
|
nav_model: segmented_button::SingleSelectModel,
|
||||||
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
||||||
|
/// Yoda phase 3: segmented_button model mirroring config.toolbar so the
|
||||||
|
/// toolbar row gets free drag-reorder + click activation (same widget
|
||||||
|
/// that powers the tab bar, its reorder is proven to work in this
|
||||||
|
/// setup unlike the generic dnd_source/dnd_destination wrappers).
|
||||||
|
toolbar_model: segmented_button::Model<segmented_button::SingleSelect>,
|
||||||
config_handler: Option<cosmic_config::Config>,
|
config_handler: Option<cosmic_config::Config>,
|
||||||
state_handler: Option<cosmic_config::Config>,
|
state_handler: Option<cosmic_config::Config>,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
|
@ -1010,6 +1139,12 @@ impl App {
|
||||||
.and_then(|first| first.as_ref().parent())
|
.and_then(|first| first.as_ref().parent())
|
||||||
.map(Path::to_path_buf)
|
.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(
|
let (mut dialog, dialog_task) = Dialog::new(
|
||||||
DialogSettings::new()
|
DialogSettings::new()
|
||||||
.kind(DialogKind::OpenFolder)
|
.kind(DialogKind::OpenFolder)
|
||||||
|
|
@ -1026,7 +1161,9 @@ impl App {
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
self.file_dialog_opt = Some(dialog);
|
self.file_dialog_opt = Some(dialog);
|
||||||
Task::batch([set_title_task, dialog_task])
|
tasks.push(set_title_task);
|
||||||
|
tasks.push(dialog_task);
|
||||||
|
Task::batch(tasks)
|
||||||
} else {
|
} else {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -1071,7 +1208,7 @@ impl App {
|
||||||
.sort_by(|a, b| (b.1.width * b.1.height).total_cmp(&(a.1.width * b.1.height)));
|
.sort_by(|a, b| (b.1.width * b.1.height).total_cmp(&(a.1.width * b.1.height)));
|
||||||
|
|
||||||
for (w_id, overlap) in sorted_overlaps {
|
for (w_id, overlap) in sorted_overlaps {
|
||||||
let Some((bl, br, tl, tr, mut size)) = self.layer_sizes.get(w_id).map(|s| {
|
let Some((bl, br, tl, tr, size)) = self.layer_sizes.get(w_id).map(|s| {
|
||||||
(
|
(
|
||||||
Rectangle::new(
|
Rectangle::new(
|
||||||
Point::new(0., s.height / 2.),
|
Point::new(0., s.height / 2.),
|
||||||
|
|
@ -1121,30 +1258,18 @@ impl App {
|
||||||
if tl && !(tr || bl) {
|
if tl && !(tr || bl) {
|
||||||
*top += min_dim.1;
|
*top += min_dim.1;
|
||||||
*left += min_dim.0;
|
*left += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
if tr && !(tl || br) {
|
if tr && !(tl || br) {
|
||||||
*top += min_dim.1;
|
*top += min_dim.1;
|
||||||
*right += min_dim.0;
|
*right += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
if bl && !(br || tl) {
|
if bl && !(br || tl) {
|
||||||
*bottom += min_dim.1;
|
*bottom += min_dim.1;
|
||||||
*left += min_dim.0;
|
*left += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
if br && !(bl || tr) {
|
if br && !(bl || tr) {
|
||||||
*bottom += min_dim.1;
|
*bottom += min_dim.1;
|
||||||
*right += min_dim.0;
|
*right += min_dim.0;
|
||||||
|
|
||||||
size.height -= min_dim.1;
|
|
||||||
size.width -= min_dim.0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.margin = overlaps;
|
self.margin = overlaps;
|
||||||
|
|
@ -1503,12 +1628,18 @@ impl App {
|
||||||
) -> Task<Message> {
|
) -> Task<Message> {
|
||||||
log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}");
|
log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}");
|
||||||
let icon_sizes = self.config.tab.icon_sizes;
|
let icon_sizes = self.config.tab.icon_sizes;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mounter_items = self.mounter_items.clone();
|
let mounter_items = self.mounter_items.clone();
|
||||||
|
|
||||||
Task::future(async move {
|
Task::future(async move {
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
||||||
Ok((parent_item_opt, mut items)) => {
|
Ok((parent_item_opt, items)) => {
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
|
let mut items = items;
|
||||||
|
#[cfg(not(feature = "gvfs"))]
|
||||||
|
let items = items;
|
||||||
|
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
{
|
{
|
||||||
let mounter_paths: Box<[_]> = mounter_items
|
let mounter_paths: Box<[_]> = mounter_items
|
||||||
|
|
@ -1678,6 +1809,7 @@ impl App {
|
||||||
|
|
||||||
fn update_config(&mut self) -> Task<Message> {
|
fn update_config(&mut self) -> Task<Message> {
|
||||||
self.update_nav_model();
|
self.update_nav_model();
|
||||||
|
self.rebuild_toolbar_model();
|
||||||
// Tabs are collected first to placate the borrowck
|
// Tabs are collected first to placate the borrowck
|
||||||
let tabs: Box<[_]> = self.tab_model.iter().collect();
|
let tabs: Box<[_]> = self.tab_model.iter().collect();
|
||||||
// Update main conf and each tab with the new config
|
// Update main conf and each tab with the new config
|
||||||
|
|
@ -1691,6 +1823,52 @@ impl App {
|
||||||
Task::batch(commands)
|
Task::batch(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yoda phase 3: rebuild `toolbar_model` so it matches `config.toolbar`.
|
||||||
|
/// Called on init and on every config update. Each entity carries the
|
||||||
|
/// associated `ToolbarAction` as data so click/reorder handlers can
|
||||||
|
/// round-trip Entity → ToolbarAction without maintaining a side map.
|
||||||
|
///
|
||||||
|
/// We insert ONLY the icon (no `.text()`) so the toolbar renders as a
|
||||||
|
/// clean icon row — user-visible labels stay in Settings/tooltips on
|
||||||
|
/// other surfaces.
|
||||||
|
fn rebuild_toolbar_model(&mut self) {
|
||||||
|
self.toolbar_model.clear();
|
||||||
|
for action in self.config.toolbar.iter().copied() {
|
||||||
|
let (icon_name, _label, _msg) = toolbar_action_ui(action);
|
||||||
|
self.toolbar_model
|
||||||
|
.insert()
|
||||||
|
.icon(widget::icon::from_name(icon_name).size(16).icon())
|
||||||
|
.data::<ToolbarAction>(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Yoda phase 3: after a drag-reorder, sync `config.toolbar` with the
|
||||||
|
/// new entity order in `toolbar_model`. Inlines what `config_set!`
|
||||||
|
/// would do (the macro lives inside update()).
|
||||||
|
fn sync_toolbar_config_from_model(&mut self) -> Task<Message> {
|
||||||
|
let new_toolbar: Vec<ToolbarAction> = self
|
||||||
|
.toolbar_model
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| self.toolbar_model.data::<ToolbarAction>(e).copied())
|
||||||
|
.collect();
|
||||||
|
if new_toolbar == self.config.toolbar {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
match self.config_handler.as_ref() {
|
||||||
|
Some(h) => {
|
||||||
|
if let Err(err) = self.config.set_toolbar(h, new_toolbar) {
|
||||||
|
log::warn!("failed to save toolbar order: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => self.config.toolbar = new_toolbar,
|
||||||
|
}
|
||||||
|
// Don't call update_config() — that would rebuild the
|
||||||
|
// toolbar_model from config and undo the reorder the user just
|
||||||
|
// made. The model already has the new order; config is just
|
||||||
|
// catching up for persistence.
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
fn update_desktop(&mut self) -> Task<Message> {
|
fn update_desktop(&mut self) -> Task<Message> {
|
||||||
let needs_reload: Box<[_]> = (self.tab_model.iter())
|
let needs_reload: Box<[_]> = (self.tab_model.iter())
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
|
|
@ -2268,47 +2446,132 @@ impl App {
|
||||||
.toggler(self.config.show_recents, Message::SetShowRecents)
|
.toggler(self.config.show_recents, Message::SetShowRecents)
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
|
// Yoda phase 3: toolbar editor. Two stacked lists:
|
||||||
|
// - top: enabled buttons in their current order (drag to reorder)
|
||||||
|
// - bottom: available (not-yet-enabled) buttons
|
||||||
|
// Each row's toggle adds/removes; enabled rows are also
|
||||||
|
// drag sources + drop targets.
|
||||||
|
self.toolbar_settings_section(),
|
||||||
])
|
])
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> {
|
/// Yoda phase 3: build the Toolbar settings section.
|
||||||
let mut results = Vec::new();
|
fn toolbar_settings_section(&self) -> Element<'_, Message> {
|
||||||
|
use iced::clipboard::dnd::DndAction;
|
||||||
let mut dedupe = FxHashSet::default();
|
let enabled = &self.config.toolbar;
|
||||||
|
let disabled: Vec<ToolbarAction> = ToolbarAction::ALL
|
||||||
// start with exact matches
|
|
||||||
results.extend(
|
|
||||||
self.mime_app_cache
|
|
||||||
.get(mime_type)
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&mime_app| dedupe.insert(&mime_app.id))
|
.copied()
|
||||||
.map(|mime_app| (mime_app, MimeAppMatch::Exact)),
|
.filter(|a| !enabled.contains(a))
|
||||||
);
|
.collect();
|
||||||
|
|
||||||
// grab matches based off of subclass / parent mime type
|
let space_xxs = theme::active().cosmic().spacing.space_xxs;
|
||||||
if let Some(parent_types) = mime_icon::parent_mime_types(mime_type) {
|
|
||||||
for parent_type in parent_types {
|
let drag_icon = |size: u16| -> Element<'static, Message> {
|
||||||
results.extend(
|
widget::icon::from_name("list-drag-handle-symbolic")
|
||||||
self.mime_app_cache
|
.size(size)
|
||||||
.get(&parent_type)
|
.into()
|
||||||
.iter()
|
};
|
||||||
.filter(|&mime_app| dedupe.insert(&mime_app.id))
|
|
||||||
.map(|mime_app| (mime_app, MimeAppMatch::Related)),
|
let row_enabled =
|
||||||
);
|
|action: ToolbarAction, pos: usize, last: usize| -> Element<'_, Message> {
|
||||||
|
let (icon, label, _msg) = toolbar_action_ui(action);
|
||||||
|
let up_btn =
|
||||||
|
widget::button::icon(widget::icon::from_name("go-up-symbolic").size(14));
|
||||||
|
let up_btn = if pos > 0 {
|
||||||
|
up_btn.on_press(Message::ToolbarMoveUp(action))
|
||||||
|
} else {
|
||||||
|
up_btn
|
||||||
|
};
|
||||||
|
let down_btn =
|
||||||
|
widget::button::icon(widget::icon::from_name("go-down-symbolic").size(14));
|
||||||
|
let down_btn = if pos < last {
|
||||||
|
down_btn.on_press(Message::ToolbarMoveDown(action))
|
||||||
|
} else {
|
||||||
|
down_btn
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_content: Element<_> = widget::row::with_children(vec![
|
||||||
|
drag_icon(14),
|
||||||
|
widget::icon::from_name(icon).size(16).into(),
|
||||||
|
widget::text::body(label).width(Length::Fill).into(),
|
||||||
|
up_btn.into(),
|
||||||
|
down_btn.into(),
|
||||||
|
widget::button::icon(widget::icon::from_name("list-remove-symbolic").size(14))
|
||||||
|
.on_press(Message::ToolbarRemove(action))
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(space_xxs)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let row_container = widget::container(row_content)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(space_xxs);
|
||||||
|
|
||||||
|
// Wrap as DnD source (drags itself) + DnD destination (accepts
|
||||||
|
// drops from other enabled rows; on drop, move the src before
|
||||||
|
// this row).
|
||||||
|
let source = widget::dnd_source::<Message, ToolbarActionPayload>(row_container)
|
||||||
|
.drag_content(move || ToolbarActionPayload(action.to_u8()));
|
||||||
|
widget::dnd_destination(source, vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)])
|
||||||
|
.data_received_for::<ToolbarActionPayload>(
|
||||||
|
move |payload: Option<ToolbarActionPayload>| {
|
||||||
|
match payload.and_then(|p| ToolbarAction::from_u8(p.0)) {
|
||||||
|
Some(src) if src != action => Message::ToolbarReorder {
|
||||||
|
src,
|
||||||
|
target: action,
|
||||||
|
},
|
||||||
|
// No-op if payload missing / malformed / same row.
|
||||||
|
_ => Message::ToolbarReorder {
|
||||||
|
src: action,
|
||||||
|
target: action,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.action(DndAction::Move)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_disabled = |action: ToolbarAction| -> Element<'_, Message> {
|
||||||
|
let (icon, label, _msg) = toolbar_action_ui(action);
|
||||||
|
widget::row::with_children(vec![
|
||||||
|
widget::icon::from_name(icon).size(16).into(),
|
||||||
|
widget::text::body(label).width(Length::Fill).into(),
|
||||||
|
widget::button::icon(widget::icon::from_name("list-add-symbolic").size(14))
|
||||||
|
.on_press(Message::ToolbarAdd(action))
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(space_xxs)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.padding(space_xxs)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut section = widget::settings::section().title(fl!("toolbar"));
|
||||||
|
if enabled.is_empty() {
|
||||||
|
section = section.add(widget::text::body(fl!("toolbar-empty-hint")));
|
||||||
|
} else {
|
||||||
|
let last = enabled.len() - 1;
|
||||||
|
for (pos, a) in enabled.iter().copied().enumerate() {
|
||||||
|
section = section.add(row_enabled(a, pos, last));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add other apps
|
let mut col = widget::column::with_capacity(3).spacing(space_xxs);
|
||||||
results.extend(
|
col = col.push(section);
|
||||||
self.mime_app_cache
|
if !disabled.is_empty() {
|
||||||
.apps()
|
let mut avail = widget::settings::section().title(fl!("toolbar-available"));
|
||||||
.iter()
|
for a in disabled {
|
||||||
.filter(|&mime_app| dedupe.insert(&mime_app.id))
|
avail = avail.add(row_disabled(a));
|
||||||
.map(|mime_app| (mime_app, MimeAppMatch::Other)),
|
}
|
||||||
);
|
col = col.push(avail);
|
||||||
|
}
|
||||||
results
|
col = col
|
||||||
|
.push(widget::button::standard(fl!("toolbar-reset")).on_press(Message::ToolbarReset));
|
||||||
|
col.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update favorites based on renaming or moving dirs.
|
// Update favorites based on renaming or moving dirs.
|
||||||
|
|
@ -2443,6 +2706,7 @@ impl Application for App {
|
||||||
nav_bar_context_id: segmented_button::Entity::null(),
|
nav_bar_context_id: segmented_button::Entity::null(),
|
||||||
nav_model: segmented_button::ModelBuilder::default().build(),
|
nav_model: segmented_button::ModelBuilder::default().build(),
|
||||||
tab_model: segmented_button::ModelBuilder::default().build(),
|
tab_model: segmented_button::ModelBuilder::default().build(),
|
||||||
|
toolbar_model: segmented_button::ModelBuilder::default().build(),
|
||||||
config_handler: flags.config_handler,
|
config_handler: flags.config_handler,
|
||||||
state_handler: flags.state_handler,
|
state_handler: flags.state_handler,
|
||||||
config: flags.config,
|
config: flags.config,
|
||||||
|
|
@ -2553,7 +2817,7 @@ impl Application for App {
|
||||||
.on_middle_press(|entity| {
|
.on_middle_press(|entity| {
|
||||||
cosmic::Action::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity)))
|
cosmic::Action::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity)))
|
||||||
})
|
})
|
||||||
.context_menu(self.nav_context_menu(self.nav_bar_context_id))
|
.context_menu(self.nav_context_menu())
|
||||||
.close_icon(icon::from_name("media-eject-symbolic").size(16).icon())
|
.close_icon(icon::from_name("media-eject-symbolic").size(16).icon())
|
||||||
.into_container();
|
.into_container();
|
||||||
|
|
||||||
|
|
@ -2568,8 +2832,8 @@ impl Application for App {
|
||||||
|
|
||||||
fn nav_context_menu(
|
fn nav_context_menu(
|
||||||
&self,
|
&self,
|
||||||
entity: widget::nav_bar::Id,
|
|
||||||
) -> Option<Vec<widget::menu::Tree<cosmic::Action<Self::Message>>>> {
|
) -> Option<Vec<widget::menu::Tree<cosmic::Action<Self::Message>>>> {
|
||||||
|
let entity = self.nav_bar_context_id;
|
||||||
let favorite_index_opt = self.nav_model.data::<FavoriteIndex>(entity);
|
let favorite_index_opt = self.nav_model.data::<FavoriteIndex>(entity);
|
||||||
let location_opt = self.nav_model.data::<Location>(entity);
|
let location_opt = self.nav_model.data::<Location>(entity);
|
||||||
|
|
||||||
|
|
@ -3222,9 +3486,10 @@ impl Application for App {
|
||||||
path,
|
path,
|
||||||
mime,
|
mime,
|
||||||
selected,
|
selected,
|
||||||
|
set_default,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let available_apps = self.get_apps_for_mime(&mime);
|
let available_apps = self.mime_app_cache.get_apps_for_mime(&mime, true);
|
||||||
|
|
||||||
if let Some((app, _)) = available_apps.get(selected) {
|
if let Some((app, _)) = available_apps.get(selected) {
|
||||||
if let Some(mut command) =
|
if let Some(mut command) =
|
||||||
|
|
@ -3240,6 +3505,11 @@ impl Application for App {
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Yoda: persist as default if the user asked for it.
|
||||||
|
if set_default {
|
||||||
|
self.mime_app_cache
|
||||||
|
.set_default(mime.clone(), app.id.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
|
|
@ -3351,7 +3621,7 @@ impl Application for App {
|
||||||
return dialog.update(dialog_message);
|
return dialog.update(dialog_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Key(window_id, modifiers, key, text) => {
|
Message::Key(window_id, modifiers, key, physical_key, text) => {
|
||||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||||
let in_surface_ids = self.surface_ids.values().any(|id| *id == window_id);
|
let in_surface_ids = self.surface_ids.values().any(|id| *id == window_id);
|
||||||
#[cfg(not(all(feature = "wayland", feature = "desktop-applet")))]
|
#[cfg(not(all(feature = "wayland", feature = "desktop-applet")))]
|
||||||
|
|
@ -3359,7 +3629,7 @@ impl Application for App {
|
||||||
if self.core.main_window_id() == Some(window_id) || in_surface_ids {
|
if self.core.main_window_id() == Some(window_id) || in_surface_ids {
|
||||||
let entity = self.tab_model.active();
|
let entity = self.tab_model.active();
|
||||||
for (key_bind, action) in &self.key_binds {
|
for (key_bind, action) in &self.key_binds {
|
||||||
if key_bind.matches(modifiers, &key) {
|
if key_bind.matches(modifiers, &key, Some(&physical_key)) {
|
||||||
return self.update(action.message(Some(entity)));
|
return self.update(action.message(Some(entity)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3862,6 +4132,7 @@ impl Application for App {
|
||||||
.and_then(|mime| {
|
.and_then(|mime| {
|
||||||
self.mime_app_cache.get(&mime).first().cloned()
|
self.mime_app_cache.get(&mime).first().cloned()
|
||||||
}),
|
}),
|
||||||
|
set_default: false,
|
||||||
},
|
},
|
||||||
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
|
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
|
||||||
);
|
);
|
||||||
|
|
@ -3873,6 +4144,13 @@ impl Application for App {
|
||||||
*selected = index;
|
*selected = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::OpenWithToggleDefault(enabled) => {
|
||||||
|
if let Some(DialogPage::OpenWith { set_default, .. }) =
|
||||||
|
self.dialog_pages.front_mut()
|
||||||
|
{
|
||||||
|
*set_default = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::Paste(entity_opt) => {
|
Message::Paste(entity_opt) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
|
||||||
|
|
@ -4219,6 +4497,9 @@ impl Application for App {
|
||||||
let paths: Box<[_]> = self.selected_paths(entity_opt).collect();
|
let paths: Box<[_]> = self.selected_paths(entity_opt).collect();
|
||||||
return self.operation(Operation::RemoveFromRecents { paths });
|
return self.operation(Operation::RemoveFromRecents { paths });
|
||||||
}
|
}
|
||||||
|
Message::ReloadMimeAppCache => {
|
||||||
|
self.mime_app_cache.reload();
|
||||||
|
}
|
||||||
Message::RescanRecents => {
|
Message::RescanRecents => {
|
||||||
return self.rescan_recents();
|
return self.rescan_recents();
|
||||||
}
|
}
|
||||||
|
|
@ -4349,6 +4630,86 @@ impl Application for App {
|
||||||
config_set!(show_recents, show_recents);
|
config_set!(show_recents, show_recents);
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
}
|
}
|
||||||
|
Message::ToolbarAdd(action) => {
|
||||||
|
let mut tb = self.config.toolbar.clone();
|
||||||
|
if !tb.contains(&action) {
|
||||||
|
tb.push(action);
|
||||||
|
}
|
||||||
|
config_set!(toolbar, tb);
|
||||||
|
return self.update_config();
|
||||||
|
}
|
||||||
|
Message::ToolbarRemove(action) => {
|
||||||
|
let mut tb = self.config.toolbar.clone();
|
||||||
|
tb.retain(|a| a != &action);
|
||||||
|
config_set!(toolbar, tb);
|
||||||
|
return self.update_config();
|
||||||
|
}
|
||||||
|
Message::ToolbarReorder { src, target } => {
|
||||||
|
let mut tb = self.config.toolbar.clone();
|
||||||
|
if let (Some(src_idx), Some(tgt_idx)) = (
|
||||||
|
tb.iter().position(|a| a == &src),
|
||||||
|
tb.iter().position(|a| a == &target),
|
||||||
|
) && src_idx != tgt_idx
|
||||||
|
{
|
||||||
|
// Pull src out, then insert before the target's new position.
|
||||||
|
let item = tb.remove(src_idx);
|
||||||
|
let new_tgt = if src_idx < tgt_idx {
|
||||||
|
tgt_idx - 1
|
||||||
|
} else {
|
||||||
|
tgt_idx
|
||||||
|
};
|
||||||
|
tb.insert(new_tgt, item);
|
||||||
|
config_set!(toolbar, tb);
|
||||||
|
return self.update_config();
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Message::ToolbarMoveUp(action) => {
|
||||||
|
let mut tb = self.config.toolbar.clone();
|
||||||
|
if let Some(i) = tb.iter().position(|a| a == &action)
|
||||||
|
&& i > 0
|
||||||
|
{
|
||||||
|
tb.swap(i, i - 1);
|
||||||
|
config_set!(toolbar, tb);
|
||||||
|
return self.update_config();
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Message::ToolbarMoveDown(action) => {
|
||||||
|
let mut tb = self.config.toolbar.clone();
|
||||||
|
if let Some(i) = tb.iter().position(|a| a == &action)
|
||||||
|
&& i + 1 < tb.len()
|
||||||
|
{
|
||||||
|
tb.swap(i, i + 1);
|
||||||
|
config_set!(toolbar, tb);
|
||||||
|
return self.update_config();
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Message::ToolbarReset => {
|
||||||
|
config_set!(toolbar, default_toolbar());
|
||||||
|
return self.update_config();
|
||||||
|
}
|
||||||
|
Message::ToolbarTabActivate(entity) => {
|
||||||
|
// Dispatch the stored ToolbarAction's message, then clear
|
||||||
|
// the "active" selection so the button doesn't stay
|
||||||
|
// highlighted after a click (we use segmented_button for
|
||||||
|
// layout/drag but toolbar buttons are action-firing, not
|
||||||
|
// a mutual-exclusive choice).
|
||||||
|
let action = self.toolbar_model.data::<ToolbarAction>(entity).copied();
|
||||||
|
self.toolbar_model.deactivate();
|
||||||
|
if let Some(action) = action {
|
||||||
|
let (_, _, msg) = toolbar_action_ui(action);
|
||||||
|
return self.update(msg);
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Message::ToolbarTabReorder(event) => {
|
||||||
|
let _ = self
|
||||||
|
.toolbar_model
|
||||||
|
.reorder(event.dragged, event.target, event.position);
|
||||||
|
return self.sync_toolbar_config_from_model();
|
||||||
|
}
|
||||||
Message::SetTypeToSearch(type_to_search) => {
|
Message::SetTypeToSearch(type_to_search) => {
|
||||||
config_set!(type_to_search, type_to_search);
|
config_set!(type_to_search, type_to_search);
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
|
|
@ -4524,6 +4885,7 @@ impl Application for App {
|
||||||
commands.push(self.update(Message::CheckClipboard));
|
commands.push(self.update(Message::CheckClipboard));
|
||||||
commands.push(self.update(Message::Surface(
|
commands.push(self.update(Message::Surface(
|
||||||
cosmic::surface::action::app_popup(
|
cosmic::surface::action::app_popup(
|
||||||
|
|_: &Self| cosmic::surface::action::LiveSettings::default(),
|
||||||
move |app: &mut Self| -> SctkPopupSettings {
|
move |app: &mut Self| -> SctkPopupSettings {
|
||||||
let anchor_rect = Rectangle {
|
let anchor_rect = Rectangle {
|
||||||
x: point.x as i32,
|
x: point.x as i32,
|
||||||
|
|
@ -5108,6 +5470,7 @@ impl Application for App {
|
||||||
.and_then(|mime| {
|
.and_then(|mime| {
|
||||||
self.mime_app_cache.get(&mime).first().cloned()
|
self.mime_app_cache.get(&mime).first().cloned()
|
||||||
}),
|
}),
|
||||||
|
set_default: false,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
@ -5951,7 +6314,7 @@ impl Application for App {
|
||||||
mime,
|
mime,
|
||||||
selected,
|
selected,
|
||||||
store_opt,
|
store_opt,
|
||||||
..
|
set_default,
|
||||||
} => {
|
} => {
|
||||||
let name = match path.file_name() {
|
let name = match path.file_name() {
|
||||||
Some(file_name) => file_name.to_str(),
|
Some(file_name) => file_name.to_str(),
|
||||||
|
|
@ -5959,7 +6322,7 @@ impl Application for App {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut column = widget::list_column();
|
let mut column = widget::list_column();
|
||||||
let available_apps = self.get_apps_for_mime(mime);
|
let available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
|
||||||
let item_height = 32.0;
|
let item_height = 32.0;
|
||||||
let mut displayed_default = false;
|
let mut displayed_default = false;
|
||||||
let mut last_kind = MimeAppMatch::Exact;
|
let mut last_kind = MimeAppMatch::Exact;
|
||||||
|
|
@ -5980,8 +6343,8 @@ impl Application for App {
|
||||||
widget::mouse_area(
|
widget::mouse_area(
|
||||||
widget::button::custom(
|
widget::button::custom(
|
||||||
widget::row::with_children([
|
widget::row::with_children([
|
||||||
icon(app.icon.clone()).size(32).into(),
|
icon(app.icon()).size(32).into(),
|
||||||
if app.is_default && !displayed_default {
|
if app.is_default(mime) && !displayed_default {
|
||||||
displayed_default = true;
|
displayed_default = true;
|
||||||
widget::text::body(fl!(
|
widget::text::body(fl!(
|
||||||
"default-app",
|
"default-app",
|
||||||
|
|
@ -6036,7 +6399,21 @@ impl Application for App {
|
||||||
} else {
|
} else {
|
||||||
Length::Shrink
|
Length::Shrink
|
||||||
}
|
}
|
||||||
}));
|
}))
|
||||||
|
// Yoda: let the user make this choice stick. A plain row
|
||||||
|
// instead of settings::item::builder because the latter
|
||||||
|
// returns a section Item, not an Element usable in .control().
|
||||||
|
.control(
|
||||||
|
widget::row::with_children([
|
||||||
|
widget::text::body(fl!("open-with-set-default")).into(),
|
||||||
|
widget::space::horizontal().into(),
|
||||||
|
widget::toggler(*set_default)
|
||||||
|
.on_toggle(Message::OpenWithToggleDefault)
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(space_s)
|
||||||
|
.align_y(Alignment::Center),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(app) = store_opt {
|
if let Some(app) = store_opt {
|
||||||
dialog = dialog.tertiary_action(
|
dialog = dialog.tertiary_action(
|
||||||
|
|
@ -6442,7 +6819,10 @@ impl Application for App {
|
||||||
/// Creates a view after each update.
|
/// Creates a view after each update.
|
||||||
fn view(&self) -> Element<'_, Self::Message> {
|
fn view(&self) -> Element<'_, Self::Message> {
|
||||||
let cosmic_theme::Spacing {
|
let cosmic_theme::Spacing {
|
||||||
space_xxs, space_s, ..
|
space_xxs,
|
||||||
|
space_xs,
|
||||||
|
space_s,
|
||||||
|
..
|
||||||
} = theme::spacing();
|
} = theme::spacing();
|
||||||
|
|
||||||
let mut tab_column = widget::column::with_capacity(4);
|
let mut tab_column = widget::column::with_capacity(4);
|
||||||
|
|
@ -6486,6 +6866,36 @@ impl Application for App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yoda phase 3: Dolphin-style quick actions toolbar via
|
||||||
|
// segmented_button::horizontal — the same widget that powers the
|
||||||
|
// tab bar, so its built-in drag reorder works reliably (unlike the
|
||||||
|
// generic dnd_source+dnd_destination pairing we tried earlier).
|
||||||
|
// Short click = action (ToolbarTabActivate → dispatch the stored
|
||||||
|
// ToolbarAction's message). Drag past threshold = reorder
|
||||||
|
// (ToolbarTabReorder → model.reorder + sync to config).
|
||||||
|
if !self.config.toolbar.is_empty() {
|
||||||
|
// Use Control style (no TabBar underline, no bottom border)
|
||||||
|
// and let each button shrink to its icon. Spacing = space_xs
|
||||||
|
// keeps the buttons visually separated so it looks like an
|
||||||
|
// icon toolbar rather than a conjoined segmented control.
|
||||||
|
let toolbar = widget::segmented_button::horizontal(&self.toolbar_model)
|
||||||
|
.style(theme::SegmentedButton::Control)
|
||||||
|
.button_height(32)
|
||||||
|
.button_spacing(space_xs)
|
||||||
|
.button_alignment(Alignment::Center)
|
||||||
|
.minimum_button_width(32)
|
||||||
|
.maximum_button_width(32)
|
||||||
|
.enable_tab_drag(String::from("x-cosmic-files/toolbar-dnd"))
|
||||||
|
.on_reorder(Message::ToolbarTabReorder)
|
||||||
|
.tab_drag_threshold(8.)
|
||||||
|
.on_activate(Message::ToolbarTabActivate);
|
||||||
|
tab_column = tab_column.push(
|
||||||
|
widget::container(toolbar)
|
||||||
|
.width(Length::Shrink)
|
||||||
|
.padding([space_xxs, space_s]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let entity = self.tab_model.active();
|
let entity = self.tab_model.active();
|
||||||
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
||||||
let tab_view = tab
|
let tab_view = tab
|
||||||
|
|
@ -6633,11 +7043,14 @@ impl Application for App {
|
||||||
},
|
},
|
||||||
Event::Keyboard(KeyEvent::KeyPressed {
|
Event::Keyboard(KeyEvent::KeyPressed {
|
||||||
key,
|
key,
|
||||||
|
physical_key,
|
||||||
modifiers,
|
modifiers,
|
||||||
text,
|
text,
|
||||||
..
|
..
|
||||||
}) => match status {
|
}) => match status {
|
||||||
event::Status::Ignored => Some(Message::Key(window_id, modifiers, key, text)),
|
event::Status::Ignored => {
|
||||||
|
Some(Message::Key(window_id, modifiers, key, physical_key, text))
|
||||||
|
}
|
||||||
event::Status::Captured => None,
|
event::Status::Captured => None,
|
||||||
},
|
},
|
||||||
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
||||||
|
|
@ -6769,6 +7182,22 @@ 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>(), |_| {
|
Subscription::run_with(TypeId::of::<TrashWatcherSubscription>(), |_| {
|
||||||
stream::channel(
|
stream::channel(
|
||||||
1,
|
1,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::mime_icon::mime_for_path;
|
use crate::mime_icon::mime_for_path;
|
||||||
use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk};
|
use crate::operation::{Controller, OpReader, OperationError, OperationErrorType};
|
||||||
use cosmic::iced::futures;
|
use cosmic::iced::futures;
|
||||||
use jiff::Zoned;
|
use jiff::Zoned;
|
||||||
use jiff::civil::DateTime;
|
use jiff::civil::DateTime;
|
||||||
|
|
@ -45,7 +45,7 @@ pub fn extract(
|
||||||
new_dir: &Path,
|
new_dir: &Path,
|
||||||
password: &Option<String>,
|
password: &Option<String>,
|
||||||
controller: &Controller,
|
controller: &Controller,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(Vec<PathBuf>, HashSet<PathBuf>), OperationError> {
|
||||||
let mime = mime_for_path(path, None, false);
|
let mime = mime_for_path(path, None, false);
|
||||||
let password = password.as_deref();
|
let password = password.as_deref();
|
||||||
match mime.essence_str() {
|
match mime.essence_str() {
|
||||||
|
|
@ -55,13 +55,15 @@ pub fn extract(
|
||||||
.map(flate2::read::GzDecoder::new)
|
.map(flate2::read::GzDecoder::new)
|
||||||
.map(tar::Archive::new)
|
.map(tar::Archive::new)
|
||||||
.and_then(|mut archive| archive.unpack(new_dir))
|
.and_then(|mut archive| archive.unpack(new_dir))
|
||||||
.map_err(|e| OperationError::from_err(e, controller))?;
|
.map_err(|e| OperationError::from_err(e, controller))
|
||||||
|
.map(|_| Default::default())
|
||||||
}
|
}
|
||||||
"application/x-tar" => OpReader::new(path, controller.clone())
|
"application/x-tar" => OpReader::new(path, controller.clone())
|
||||||
.map(io::BufReader::new)
|
.map(io::BufReader::new)
|
||||||
.map(tar::Archive::new)
|
.map(tar::Archive::new)
|
||||||
.and_then(|mut archive| archive.unpack(new_dir))
|
.and_then(|mut archive| archive.unpack(new_dir))
|
||||||
.map_err(|e| OperationError::from_err(e, controller))?,
|
.map_err(|e| OperationError::from_err(e, controller))
|
||||||
|
.map(|_| Default::default()),
|
||||||
"application/zip" => fs::File::open(path)
|
"application/zip" => fs::File::open(path)
|
||||||
.map(io::BufReader::new)
|
.map(io::BufReader::new)
|
||||||
.map(zip::ZipArchive::new)
|
.map(zip::ZipArchive::new)
|
||||||
|
|
@ -75,7 +77,7 @@ pub fn extract(
|
||||||
OperationError::from_kind(OperationErrorType::PasswordRequired, controller)
|
OperationError::from_kind(OperationErrorType::PasswordRequired, controller)
|
||||||
}
|
}
|
||||||
_ => OperationError::from_err(e, controller),
|
_ => OperationError::from_err(e, controller),
|
||||||
})?,
|
}),
|
||||||
#[cfg(feature = "bzip2")]
|
#[cfg(feature = "bzip2")]
|
||||||
"application/x-bzip"
|
"application/x-bzip"
|
||||||
| "application/x-bzip-compressed-tar"
|
| "application/x-bzip-compressed-tar"
|
||||||
|
|
@ -85,7 +87,8 @@ pub fn extract(
|
||||||
.map(bzip2::read::BzDecoder::new)
|
.map(bzip2::read::BzDecoder::new)
|
||||||
.map(tar::Archive::new)
|
.map(tar::Archive::new)
|
||||||
.and_then(|mut archive| archive.unpack(new_dir))
|
.and_then(|mut archive| archive.unpack(new_dir))
|
||||||
.map_err(|e| OperationError::from_err(e, controller))?,
|
.map_err(|e| OperationError::from_err(e, controller))
|
||||||
|
.map(|_| Default::default()),
|
||||||
#[cfg(feature = "lzma-rust2")]
|
#[cfg(feature = "lzma-rust2")]
|
||||||
"application/x-xz" | "application/x-xz-compressed-tar" => {
|
"application/x-xz" | "application/x-xz-compressed-tar" => {
|
||||||
OpReader::new(path, controller.clone())
|
OpReader::new(path, controller.clone())
|
||||||
|
|
@ -93,14 +96,14 @@ pub fn extract(
|
||||||
.map(|reader| lzma_rust2::XzReader::new(reader, true))
|
.map(|reader| lzma_rust2::XzReader::new(reader, true))
|
||||||
.map(tar::Archive::new)
|
.map(tar::Archive::new)
|
||||||
.and_then(|mut archive| archive.unpack(new_dir))
|
.and_then(|mut archive| archive.unpack(new_dir))
|
||||||
.map_err(|e| OperationError::from_err(e, controller))?;
|
.map_err(|e| OperationError::from_err(e, controller))
|
||||||
|
.map(|_| Default::default())
|
||||||
}
|
}
|
||||||
_ => Err(OperationError::from_err(
|
_ => Err(OperationError::from_err(
|
||||||
format!("unsupported mime type {mime:?}"),
|
format!("unsupported mime type {mime:?}"),
|
||||||
controller,
|
controller,
|
||||||
))?,
|
)),
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added
|
// From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added
|
||||||
|
|
@ -109,7 +112,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
directory: P,
|
directory: P,
|
||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
controller: Controller,
|
controller: Controller,
|
||||||
) -> zip::result::ZipResult<()> {
|
) -> zip::result::ZipResult<(Vec<PathBuf>, HashSet<PathBuf>)> {
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use zip::result::ZipError;
|
use zip::result::ZipError;
|
||||||
|
|
@ -282,10 +285,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush files to disk
|
Ok((written_files, target_dirs))
|
||||||
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> {
|
fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option<SystemTime> {
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,11 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
|
||||||
match mime.as_str() {
|
match mime.as_str() {
|
||||||
"text/uri-list" => {
|
"text/uri-list" => {
|
||||||
let text = str::from_utf8(&data)?;
|
let text = str::from_utf8(&data)?;
|
||||||
let _lines = text.lines();
|
|
||||||
|
|
||||||
for line in text.lines() {
|
for line in text.lines().filter(|line| {
|
||||||
|
let line = line.trim();
|
||||||
|
!line.is_empty() && !line.starts_with('#')
|
||||||
|
}) {
|
||||||
let url = Url::parse(line)?;
|
let url = Url::parse(line)?;
|
||||||
match url.to_file_path() {
|
match url.to_file_path() {
|
||||||
Ok(path) => paths.push(path),
|
Ok(path) => paths.push(path),
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,11 @@ pub struct Config {
|
||||||
pub show_details: bool,
|
pub show_details: bool,
|
||||||
pub show_recents: bool,
|
pub show_recents: bool,
|
||||||
pub tab: TabConfig,
|
pub tab: TabConfig,
|
||||||
|
/// Yoda phase 3: Dolphin-style quick actions toolbar. An ordered list
|
||||||
|
/// of enabled buttons — position in the vec drives the toolbar order.
|
||||||
|
/// Reorder in Settings via drag-drop; items not in the vec are
|
||||||
|
/// hidden. Default = the minimal-6 set from phase 1.
|
||||||
|
pub toolbar: Vec<ToolbarAction>,
|
||||||
pub type_to_search: TypeToSearch,
|
pub type_to_search: TypeToSearch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,11 +239,97 @@ impl Default for Config {
|
||||||
show_details: false,
|
show_details: false,
|
||||||
show_recents: true,
|
show_recents: true,
|
||||||
tab: TabConfig::default(),
|
tab: TabConfig::default(),
|
||||||
|
toolbar: default_toolbar(),
|
||||||
type_to_search: TypeToSearch::Recursive,
|
type_to_search: TypeToSearch::Recursive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yoda phase 3: ordered enum of quick-action toolbar buttons.
|
||||||
|
/// The Config stores `Vec<ToolbarAction>` so the user can pick BOTH
|
||||||
|
/// visibility (just include/exclude the variant) AND order (position in
|
||||||
|
/// the vec). Drag-drop reorder in the Settings page moves items around.
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub enum ToolbarAction {
|
||||||
|
LocationUp,
|
||||||
|
Reload,
|
||||||
|
NewFolder,
|
||||||
|
NewFile,
|
||||||
|
Rename,
|
||||||
|
Delete,
|
||||||
|
Cut,
|
||||||
|
Copy,
|
||||||
|
Paste,
|
||||||
|
ToggleShowHidden,
|
||||||
|
OpenTerminal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolbarAction {
|
||||||
|
/// Stable list of every supported action. Ordered roughly by logical
|
||||||
|
/// grouping (location → create/edit → clipboard → view/misc) so that
|
||||||
|
/// the default enabled set follows a sensible shape and the Settings
|
||||||
|
/// row for a not-yet-enabled action lands in a predictable spot.
|
||||||
|
pub const ALL: &'static [Self] = &[
|
||||||
|
Self::LocationUp,
|
||||||
|
Self::Reload,
|
||||||
|
Self::NewFolder,
|
||||||
|
Self::NewFile,
|
||||||
|
Self::Rename,
|
||||||
|
Self::Delete,
|
||||||
|
Self::Cut,
|
||||||
|
Self::Copy,
|
||||||
|
Self::Paste,
|
||||||
|
Self::ToggleShowHidden,
|
||||||
|
Self::OpenTerminal,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// u8 discriminant used to carry the action over a DnD mime payload.
|
||||||
|
pub const fn to_u8(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::LocationUp => 0,
|
||||||
|
Self::Reload => 1,
|
||||||
|
Self::NewFolder => 2,
|
||||||
|
Self::NewFile => 3,
|
||||||
|
Self::Rename => 4,
|
||||||
|
Self::Delete => 5,
|
||||||
|
Self::Cut => 6,
|
||||||
|
Self::Copy => 7,
|
||||||
|
Self::Paste => 8,
|
||||||
|
Self::ToggleShowHidden => 9,
|
||||||
|
Self::OpenTerminal => 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_u8(v: u8) -> Option<Self> {
|
||||||
|
match v {
|
||||||
|
0 => Some(Self::LocationUp),
|
||||||
|
1 => Some(Self::Reload),
|
||||||
|
2 => Some(Self::NewFolder),
|
||||||
|
3 => Some(Self::NewFile),
|
||||||
|
4 => Some(Self::Rename),
|
||||||
|
5 => Some(Self::Delete),
|
||||||
|
6 => Some(Self::Cut),
|
||||||
|
7 => Some(Self::Copy),
|
||||||
|
8 => Some(Self::Paste),
|
||||||
|
9 => Some(Self::ToggleShowHidden),
|
||||||
|
10 => Some(Self::OpenTerminal),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default set shown on a fresh install — same "minimal 6" as phase 1/2.
|
||||||
|
pub fn default_toolbar() -> Vec<ToolbarAction> {
|
||||||
|
vec![
|
||||||
|
ToolbarAction::NewFolder,
|
||||||
|
ToolbarAction::Rename,
|
||||||
|
ToolbarAction::Delete,
|
||||||
|
ToolbarAction::Cut,
|
||||||
|
ToolbarAction::Copy,
|
||||||
|
ToolbarAction::Paste,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct DesktopConfig {
|
pub struct DesktopConfig {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use cosmic::app::{Core, Task, context_drawer};
|
||||||
use cosmic::iced::core::SmolStr;
|
use cosmic::iced::core::SmolStr;
|
||||||
use cosmic::iced::core::widget::operation;
|
use cosmic::iced::core::widget::operation;
|
||||||
use cosmic::iced::futures::{self, SinkExt};
|
use cosmic::iced::futures::{self, SinkExt};
|
||||||
use cosmic::iced::keyboard::key::Named;
|
use cosmic::iced::keyboard::key::{Named, Physical};
|
||||||
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
|
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
|
||||||
use cosmic::iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper};
|
use cosmic::iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper};
|
||||||
use cosmic::iced::widget::scrollable;
|
use cosmic::iced::widget::scrollable;
|
||||||
|
|
@ -40,7 +40,7 @@ use crate::localize::LANGUAGE_SORTER;
|
||||||
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
|
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
|
||||||
use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab};
|
use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab};
|
||||||
use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};
|
use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};
|
||||||
use crate::{fl, home_dir, menu};
|
use crate::{fl, home_dir, menu, mime_icon};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DialogMessage(cosmic::Action<Message>);
|
pub struct DialogMessage(cosmic::Action<Message>);
|
||||||
|
|
@ -456,7 +456,7 @@ enum Message {
|
||||||
Escape,
|
Escape,
|
||||||
Filename(String),
|
Filename(String),
|
||||||
Filter(usize),
|
Filter(usize),
|
||||||
Key(Modifiers, Key, Option<SmolStr>),
|
Key(Modifiers, Key, Physical, Option<SmolStr>),
|
||||||
ModifiersChanged(Modifiers),
|
ModifiersChanged(Modifiers),
|
||||||
MounterItems(MounterKey, MounterItems),
|
MounterItems(MounterKey, MounterItems),
|
||||||
Mouse(window::Id, mouse::Button),
|
Mouse(window::Id, mouse::Button),
|
||||||
|
|
@ -732,11 +732,17 @@ impl App {
|
||||||
fn rescan_tab(&self, selection_paths: Option<Vec<PathBuf>>) -> Task<Message> {
|
fn rescan_tab(&self, selection_paths: Option<Vec<PathBuf>>) -> Task<Message> {
|
||||||
let location = self.tab.location.clone();
|
let location = self.tab.location.clone();
|
||||||
let icon_sizes = self.tab.config.icon_sizes;
|
let icon_sizes = self.tab.config.icon_sizes;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mounter_items = self.mounter_items.clone();
|
let mounter_items = self.mounter_items.clone();
|
||||||
Task::future(async move {
|
Task::future(async move {
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
||||||
Ok((parent_item_opt, mut items)) => {
|
Ok((parent_item_opt, items)) => {
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
|
let mut items = items;
|
||||||
|
#[cfg(not(feature = "gvfs"))]
|
||||||
|
let items = items;
|
||||||
|
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
{
|
{
|
||||||
let mounter_paths: Box<[_]> = mounter_items
|
let mounter_paths: Box<[_]> = mounter_items
|
||||||
|
|
@ -1450,16 +1456,16 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
return self.rescan_tab(None);
|
return self.rescan_tab(None);
|
||||||
}
|
}
|
||||||
Message::Key(modifiers, key, text) => {
|
Message::Key(modifiers, key, physical_key, text) => {
|
||||||
for (key_bind, action) in &self.key_binds {
|
for (key_bind, action) in &self.key_binds {
|
||||||
if key_bind.matches(modifiers, &key) {
|
if key_bind.matches(modifiers, &key, Some(&physical_key)) {
|
||||||
return self.update(Message::from(action.message()));
|
return self.update(Message::from(action.message()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check key binds from accept label
|
// Check key binds from accept label
|
||||||
if let Some(key_bind) = &self.accept_label.key_bind_opt
|
if let Some(key_bind) = &self.accept_label.key_bind_opt
|
||||||
&& key_bind.matches(modifiers, &key)
|
&& key_bind.matches(modifiers, &key, Some(&physical_key))
|
||||||
{
|
{
|
||||||
return self.update(if self.flags.kind.save() {
|
return self.update(if self.flags.kind.save() {
|
||||||
Message::Save(false)
|
Message::Save(false)
|
||||||
|
|
@ -1795,6 +1801,7 @@ impl Application for App {
|
||||||
let autosize_id = widget::Id::unique();
|
let autosize_id = widget::Id::unique();
|
||||||
commands.push(self.update(Message::Surface(
|
commands.push(self.update(Message::Surface(
|
||||||
cosmic::surface::action::app_popup(
|
cosmic::surface::action::app_popup(
|
||||||
|
|_: &Self| cosmic::surface::action::LiveSettings::default(),
|
||||||
move |app: &mut Self| -> SctkPopupSettings {
|
move |app: &mut Self| -> SctkPopupSettings {
|
||||||
let anchor_rect = Rectangle {
|
let anchor_rect = Rectangle {
|
||||||
x: point.x as i32,
|
x: point.x as i32,
|
||||||
|
|
@ -1919,11 +1926,13 @@ impl Application for App {
|
||||||
items.retain(|item| {
|
items.retain(|item| {
|
||||||
// Directories are always shown
|
// Directories are always shown
|
||||||
item.metadata.is_dir()
|
item.metadata.is_dir()
|
||||||
|
// Check for mime type match (first because it is faster)
|
||||||
|| mimes.iter().any(|filter_mime| {
|
|| mimes.iter().any(|filter_mime| {
|
||||||
if filter_mime.subtype() == mime::STAR {
|
if filter_mime.subtype() == mime::STAR {
|
||||||
filter_mime.type_() == item.mime.type_()
|
filter_mime.type_() == item.mime.type_()
|
||||||
} else {
|
} else {
|
||||||
*filter_mime == item.mime
|
*filter_mime == item.mime
|
||||||
|
|| mime_icon::is_mime_subclass_of(&item.mime, filter_mime)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Check for glob match (last because it is slower)
|
// Check for glob match (last because it is slower)
|
||||||
|
|
@ -2050,11 +2059,14 @@ impl Application for App {
|
||||||
},
|
},
|
||||||
Event::Keyboard(KeyEvent::KeyPressed {
|
Event::Keyboard(KeyEvent::KeyPressed {
|
||||||
key,
|
key,
|
||||||
|
physical_key,
|
||||||
modifiers,
|
modifiers,
|
||||||
text,
|
text,
|
||||||
..
|
..
|
||||||
}) => match status {
|
}) => match status {
|
||||||
event::Status::Ignored => Some(Message::Key(modifiers, key, text)),
|
event::Status::Ignored => {
|
||||||
|
Some(Message::Key(modifiers, key, physical_key, text))
|
||||||
|
}
|
||||||
event::Status::Captured => {
|
event::Status::Captured => {
|
||||||
if key == Key::Named(Named::Escape) {
|
if key == Key::Named(Named::Escape) {
|
||||||
Some(Message::Escape)
|
Some(Message::Escape)
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,16 @@ pub fn key_binds(mode: &tab::Mode) -> HashMap<KeyBind, Action> {
|
||||||
bind!([], Key::Named(Named::F5), Reload);
|
bind!([], Key::Named(Named::F5), Reload);
|
||||||
bind!([], Key::Named(Named::Home), SelectFirst);
|
bind!([], Key::Named(Named::Home), SelectFirst);
|
||||||
bind!([], Key::Named(Named::End), SelectLast);
|
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::ArrowDown), ItemDown);
|
||||||
bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft);
|
bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft);
|
||||||
bind!([Shift], Key::Named(Named::ArrowRight), ItemRight);
|
bind!([Shift], Key::Named(Named::ArrowRight), ItemRight);
|
||||||
bind!([Shift], Key::Named(Named::ArrowUp), ItemUp);
|
bind!([Shift], Key::Named(Named::ArrowUp), ItemUp);
|
||||||
bind!([Shift], Key::Named(Named::Home), SelectFirst);
|
bind!([Shift], Key::Named(Named::Home), SelectFirst);
|
||||||
bind!([Shift], Key::Named(Named::End), SelectLast);
|
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!([Ctrl, Shift], Key::Character("n".into()), NewFolder);
|
||||||
bind!([], Key::Named(Named::Enter), Open);
|
bind!([], Key::Named(Named::Enter), Open);
|
||||||
bind!([Ctrl], Key::Character(" ".into()), Preview);
|
bind!([Ctrl], Key::Character(" ".into()), Preview);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ mod zoom;
|
||||||
|
|
||||||
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
|
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
|
||||||
|
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
||||||
err.to_string()
|
err.to_string()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
src/menu.rs
67
src/menu.rs
|
|
@ -10,6 +10,7 @@ use cosmic::widget::{
|
||||||
self, Row, button, column, container, divider, responsive_menu_bar, space, text,
|
self, Row, button, column, container, divider, responsive_menu_bar, space, text,
|
||||||
};
|
};
|
||||||
use cosmic::{Element, theme};
|
use cosmic::{Element, theme};
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
use i18n_embed::LanguageLoader;
|
use i18n_embed::LanguageLoader;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -18,7 +19,9 @@ use std::sync::LazyLock;
|
||||||
use crate::app::{Action, Message};
|
use crate::app::{Action, Message};
|
||||||
use crate::config::{Config, ContextActionPreset};
|
use crate::config::{Config, ContextActionPreset};
|
||||||
use crate::fl;
|
use crate::fl;
|
||||||
use crate::tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab};
|
use crate::tab::{
|
||||||
|
self, HeadingOptions, ItemMetadata, Location, LocationMenuAction, SearchLocation, Tab,
|
||||||
|
};
|
||||||
use crate::trash::{Trash, TrashExt};
|
use crate::trash::{Trash, TrashExt};
|
||||||
|
|
||||||
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
||||||
|
|
@ -67,17 +70,19 @@ pub fn context_menu<'a>(
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
fn key_style(theme: &cosmic::Theme) -> TextStyle {
|
fn key_style(theme: &cosmic::Theme) -> TextStyle {
|
||||||
let mut color = theme.cosmic().background.component.on;
|
let mut color = theme.cosmic().background(false).component.on;
|
||||||
color.alpha *= 0.75;
|
color.alpha *= 0.75;
|
||||||
TextStyle {
|
TextStyle {
|
||||||
color: Some(color.into()),
|
color: Some(color.into()),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn disabled_style(theme: &cosmic::Theme) -> TextStyle {
|
fn disabled_style(theme: &cosmic::Theme) -> TextStyle {
|
||||||
let mut color = theme.cosmic().background.component.on;
|
let mut color = theme.cosmic().background(false).component.on;
|
||||||
color.alpha *= 0.5;
|
color.alpha *= 0.5;
|
||||||
TextStyle {
|
TextStyle {
|
||||||
color: Some(color.into()),
|
color: Some(color.into()),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,18 +110,25 @@ pub fn context_menu<'a>(
|
||||||
|
|
||||||
let (sort_name, sort_direction, _) = tab.sort_options();
|
let (sort_name, sort_direction, _) = tab.sort_options();
|
||||||
let sort_item = |label, variant| {
|
let sort_item = |label, variant| {
|
||||||
menu_item(
|
let key = find_key(&Action::ToggleSort(variant));
|
||||||
format!(
|
let leading: Element<'a, tab::Message> = if sort_name == variant {
|
||||||
"{} {}",
|
let icon_name = if sort_direction {
|
||||||
label,
|
"view-sort-ascending-symbolic"
|
||||||
match (sort_name == variant, sort_direction) {
|
} else {
|
||||||
(true, true) => "\u{2B07}",
|
"view-sort-descending-symbolic"
|
||||||
(true, false) => "\u{2B06}",
|
};
|
||||||
_ => "",
|
widget::icon::from_name(icon_name).size(14).into()
|
||||||
}
|
} else {
|
||||||
),
|
space::horizontal().width(Length::Fixed(14.0)).into()
|
||||||
Action::ToggleSort(variant),
|
};
|
||||||
|
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))
|
||||||
)
|
)
|
||||||
|
.on_press(tab::Message::ContextAction(Action::ToggleSort(variant)))
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -126,6 +138,7 @@ pub fn context_menu<'a>(
|
||||||
let mut selected_desktop_entry = None;
|
let mut selected_desktop_entry = None;
|
||||||
let mut selected_types: Vec<Mime> = vec![];
|
let mut selected_types: Vec<Mime> = vec![];
|
||||||
let mut selected_mount_point = 0;
|
let mut selected_mount_point = 0;
|
||||||
|
let mut any_trash_item = false;
|
||||||
if let Some(items) = tab.items_opt() {
|
if let Some(items) = tab.items_opt() {
|
||||||
for item in items {
|
for item in items {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
|
|
@ -146,6 +159,9 @@ pub fn context_menu<'a>(
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
if matches!(&item.metadata, ItemMetadata::Trash { .. }) {
|
||||||
|
any_trash_item = true;
|
||||||
|
}
|
||||||
selected_types.push(item.mime.clone());
|
selected_types.push(item.mime.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,11 +206,11 @@ pub fn context_menu<'a>(
|
||||||
if !Trash::is_empty() {
|
if !Trash::is_empty() {
|
||||||
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
|
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
|
||||||
}
|
}
|
||||||
} else if let Some(entry) = selected_desktop_entry {
|
} else if let Some(_entry) = selected_desktop_entry {
|
||||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
{
|
{
|
||||||
children.extend(entry.desktop_actions.into_iter().enumerate().map(
|
children.extend(_entry.desktop_actions.into_iter().enumerate().map(
|
||||||
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
|
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -267,9 +283,18 @@ pub fn context_menu<'a>(
|
||||||
|
|
||||||
//TODO: Print?
|
//TODO: Print?
|
||||||
children.push(menu_item(fl!("show-details"), Action::Preview).into());
|
children.push(menu_item(fl!("show-details"), Action::Preview).into());
|
||||||
|
if any_trash_item {
|
||||||
|
children.push(divider::horizontal::light().into());
|
||||||
|
children.push(
|
||||||
|
menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).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) {
|
if matches!(tab.mode, tab::Mode::App) {
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
|
children
|
||||||
|
.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
|
||||||
}
|
}
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
if tab.location.is_recents() {
|
if tab.location.is_recents() {
|
||||||
|
|
@ -281,7 +306,8 @@ pub fn context_menu<'a>(
|
||||||
if selected_mount_point == 0 {
|
if selected_mount_point == 0 {
|
||||||
if modifiers.shift() && !modifiers.control() {
|
if modifiers.shift() && !modifiers.control() {
|
||||||
children.push(
|
children.push(
|
||||||
menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(),
|
menu_item(fl!("delete-permanently"), Action::PermanentlyDelete)
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
|
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
|
||||||
|
|
@ -289,6 +315,7 @@ pub fn context_menu<'a>(
|
||||||
} else if selected == 1 {
|
} else if selected == 1 {
|
||||||
children.push(menu_item(fl!("eject"), Action::Eject).into());
|
children.push(menu_item(fl!("eject"), Action::Eject).into());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
//TODO: need better designs for menu with no selection
|
//TODO: need better designs for menu with no selection
|
||||||
//TODO: have things like properties but they apply to the folder?
|
//TODO: have things like properties but they apply to the folder?
|
||||||
|
|
@ -415,7 +442,7 @@ pub fn context_menu<'a>(
|
||||||
//TODO: move style to libcosmic
|
//TODO: move style to libcosmic
|
||||||
.style(|theme| {
|
.style(|theme| {
|
||||||
let cosmic = theme.cosmic();
|
let cosmic = theme.cosmic();
|
||||||
let component = &cosmic.background.component;
|
let component = &cosmic.background(false).component;
|
||||||
container::Style {
|
container::Style {
|
||||||
icon_color: Some(component.on.into()),
|
icon_color: Some(component.on.into()),
|
||||||
text_color: Some(component.on.into()),
|
text_color: Some(component.on.into()),
|
||||||
|
|
@ -809,7 +836,7 @@ pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Mess
|
||||||
.padding(1)
|
.padding(1)
|
||||||
.style(|theme| {
|
.style(|theme| {
|
||||||
let cosmic = theme.cosmic();
|
let cosmic = theme.cosmic();
|
||||||
let component = &cosmic.background.component;
|
let component = &cosmic.background(false).component;
|
||||||
container::Style {
|
container::Style {
|
||||||
icon_color: Some(component.on.into()),
|
icon_color: Some(component.on.into()),
|
||||||
text_color: Some(component.on.into()),
|
text_color: Some(component.on.into()),
|
||||||
|
|
|
||||||
470
src/mime_app.rs
470
src/mime_app.rs
|
|
@ -2,18 +2,68 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use bstr::{BString, ByteSlice, ByteVec};
|
use bstr::{BString, ByteSlice, ByteVec};
|
||||||
#[cfg(feature = "desktop")]
|
|
||||||
use cosmic::desktop;
|
|
||||||
use cosmic::widget;
|
use cosmic::widget;
|
||||||
pub use mime_guess::Mime;
|
pub use mime_guess::Mime;
|
||||||
use rustc_hash::FxHashMap;
|
#[cfg(feature = "desktop")]
|
||||||
use std::cmp::Ordering;
|
use notify_debouncer_full::notify;
|
||||||
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Instant;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::{Arc, RwLock, atomic};
|
||||||
|
use std::time::{self, Instant};
|
||||||
use std::{fs, io, process};
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn exec_to_command(
|
pub fn exec_to_command(
|
||||||
exec: &str,
|
exec: &str,
|
||||||
entry_name: &str,
|
entry_name: &str,
|
||||||
|
|
@ -126,14 +176,23 @@ pub fn exec_to_command(
|
||||||
Some(commands)
|
Some(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MimeAppMatch {
|
||||||
|
Exact,
|
||||||
|
Related,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MimeApp {
|
pub struct MimeApp {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub exec: Option<String>,
|
pub exec: Option<String>,
|
||||||
pub icon: widget::icon::Handle,
|
icon_name: Box<str>,
|
||||||
pub is_default: bool,
|
icon: std::sync::OnceLock<widget::icon::Handle>,
|
||||||
|
is_default: Arc<RwLock<FxHashSet<Box<str>>>>,
|
||||||
|
no_display: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MimeApp {
|
impl MimeApp {
|
||||||
|
|
@ -146,6 +205,27 @@ impl MimeApp {
|
||||||
path_opt,
|
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
|
// This allows usage of MimeApp in a dropdown
|
||||||
|
|
@ -155,38 +235,10 @@ 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 {
|
pub struct MimeAppCache {
|
||||||
apps: Vec<MimeApp>,
|
apps: Vec<Arc<MimeApp>>,
|
||||||
cache: FxHashMap<Mime, Vec<MimeApp>>,
|
cache: FxHashMap<Mime, Vec<Arc<MimeApp>>>,
|
||||||
icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>,
|
terminals: Vec<Arc<MimeApp>>,
|
||||||
terminals: Vec<MimeApp>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MimeAppCache {
|
impl MimeAppCache {
|
||||||
|
|
@ -194,154 +246,273 @@ impl MimeAppCache {
|
||||||
let mut mime_app_cache = Self {
|
let mut mime_app_cache = Self {
|
||||||
apps: Vec::new(),
|
apps: Vec::new(),
|
||||||
cache: FxHashMap::default(),
|
cache: FxHashMap::default(),
|
||||||
icons: FxHashMap::default(),
|
|
||||||
terminals: Vec::new(),
|
terminals: Vec::new(),
|
||||||
};
|
};
|
||||||
mime_app_cache.reload();
|
mime_app_cache.reload();
|
||||||
mime_app_cache
|
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"))]
|
#[cfg(not(feature = "desktop"))]
|
||||||
pub fn reload(&mut self) {}
|
pub fn reload(&mut self) {}
|
||||||
|
|
||||||
// Only available when using desktop feature of libcosmic, which only works on Unix-likes
|
/// Reload mime types and their known app associations and defaults.
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
pub fn reload(&mut self) {
|
pub fn reload(&mut self) {
|
||||||
use crate::localize::LANGUAGE_SORTER;
|
use crate::localize::LANGUAGE_SORTER;
|
||||||
|
use crate::mime_icon;
|
||||||
|
use cosmic::desktop::fde;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
self.apps.clear();
|
self.apps.clear();
|
||||||
self.cache.clear();
|
self.cache.clear();
|
||||||
self.icons.clear();
|
|
||||||
self.terminals.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 mut list = cosmic_mime_apps::List::default();
|
||||||
|
|
||||||
let paths = cosmic_mime_apps::list_paths();
|
let paths = cosmic_mime_apps::list_paths();
|
||||||
list.load_from_paths(&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 (mime, filenames) in list
|
for desktop_entry in desktop_entries {
|
||||||
.added_associations
|
let name = desktop_entry
|
||||||
.iter()
|
.name(&locales)
|
||||||
.chain(list.default_apps.iter())
|
.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")
|
||||||
{
|
{
|
||||||
for filename in filenames {
|
self.terminals.push(app.clone());
|
||||||
log::trace!("add {mime}={filename}");
|
}
|
||||||
let apps = self
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
.cache
|
||||||
.entry(mime.clone())
|
.entry(mime.clone())
|
||||||
.or_insert_with(|| Vec::with_capacity(1));
|
.or_insert_with(|| Vec::with_capacity(apps.len()));
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (mime, filenames) in list.removed_associations.iter() {
|
// Sort cached apps for this mime by default precedence.
|
||||||
for filename in filenames {
|
for default in defaults
|
||||||
log::trace!("remove {mime}={filename}");
|
.into_iter()
|
||||||
if let Some(apps) = self.cache.get_mut(mime) {
|
.flatten()
|
||||||
apps.retain(|x| !filename_eq(&x.path, filename));
|
.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| {
|
||||||
for (mime, filenames) in list.default_apps.iter() {
|
let found = app.id.as_str() == default;
|
||||||
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 {
|
if found {
|
||||||
break;
|
app.is_default
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert(mime.essence_str().into());
|
||||||
|
cache.push(app.clone());
|
||||||
|
found_any = true;
|
||||||
}
|
}
|
||||||
log::debug!(
|
|
||||||
"failed to set default for {mime:?}: application {filename:?} not found"
|
!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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
let elapsed = start.elapsed();
|
||||||
log::info!("loaded mime app cache in {elapsed:?}");
|
tracing::info!(target: "mime-apps", "loaded mime app cache in {elapsed:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apps(&self) -> &[MimeApp] {
|
pub fn apps(&self) -> &[Arc<MimeApp>] {
|
||||||
&self.apps
|
&self.apps
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: &Mime) -> &[MimeApp] {
|
pub fn get(&self, key: &Mime) -> &[Arc<MimeApp>] {
|
||||||
self.cache.get(key).map_or(&[], Vec::as_slice)
|
self.cache.get(key).map_or(&[], Vec::as_slice)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
|
pub fn icons(&self, key: &Mime) -> Vec<widget::icon::Handle> {
|
||||||
self.icons.get(key).map_or(&[], Box::as_ref)
|
self.cache
|
||||||
|
.get(key)
|
||||||
|
.map_or_else(Vec::new, |apps| apps.iter().map(|app| app.icon()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_default_terminal(&self) -> Option<String> {
|
fn get_default_terminal(&self) -> Option<String> {
|
||||||
|
|
@ -359,14 +530,19 @@ impl MimeAppCache {
|
||||||
.map(|string| string.trim().replace(".desktop", ""))
|
.map(|string| string.trim().replace(".desktop", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn terminal(&self) -> Option<&MimeApp> {
|
pub fn terminal(&self) -> Option<&Arc<MimeApp>> {
|
||||||
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
|
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
|
||||||
// The current approach works but might not adhere to the spec (yet)
|
// The current approach works but might not adhere to the spec (yet)
|
||||||
|
|
||||||
// Look for and return preferred terminals
|
// Look for and return preferred terminals
|
||||||
//TODO: fallback order beyond cosmic-term?
|
//TODO: fallback order beyond cosmic-term?
|
||||||
|
|
||||||
let mut preference_order = vec!["com.system76.CosmicTerm".to_string()];
|
// 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![
|
||||||
|
"com.aditua.CosmicYoterm".to_string(),
|
||||||
|
"com.system76.CosmicTerm".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
if let Some(id) = self.get_default_terminal() {
|
if let Some(id) = self.get_default_terminal() {
|
||||||
preference_order.insert(0, id);
|
preference_order.insert(0, id);
|
||||||
|
|
|
||||||
|
|
@ -15,28 +15,21 @@ struct MimeIconKey {
|
||||||
size: u16,
|
size: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MimeIconCache {
|
#[derive(Default)]
|
||||||
|
pub struct MimeIconCache {
|
||||||
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
|
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
shared_mime_info: xdg_mime::SharedMimeInfo,
|
pub shared_mime_info: xdg_mime::SharedMimeInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MimeIconCache {
|
impl MimeIconCache {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
cache: FxHashMap::default(),
|
|
||||||
#[cfg(unix)]
|
|
||||||
shared_mime_info: xdg_mime::SharedMimeInfo::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
pub fn get(&mut self, _key: MimeIconKey) -> Option<icon::Handle> {
|
pub fn get(&mut self, _key: MimeIconKey) -> Option<icon::Handle> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
|
fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
|
||||||
self.cache
|
self.cache
|
||||||
.entry(key)
|
.entry(key)
|
||||||
.or_insert_with_key(|key| {
|
.or_insert_with_key(|key| {
|
||||||
|
|
@ -56,8 +49,9 @@ impl MimeIconCache {
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
|
|
||||||
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
|
pub static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
|
||||||
|
LazyLock::new(|| Mutex::new(MimeIconCache::default()));
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
pub fn mime_for_path(
|
pub fn mime_for_path(
|
||||||
|
|
@ -78,6 +72,7 @@ pub fn mime_for_path(
|
||||||
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
|
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
|
||||||
// Try the shared mime info cache first
|
// Try the shared mime info cache first
|
||||||
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
|
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
|
||||||
|
gb.zero_size(false);
|
||||||
if remote {
|
if remote {
|
||||||
if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
|
if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
|
||||||
gb.file_name(file_name);
|
gb.file_name(file_name);
|
||||||
|
|
@ -129,3 +124,11 @@ pub fn parent_mime_types(mime: &Mime) -> Option<Vec<Mime>> {
|
||||||
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
|
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
|
||||||
mime_icon_cache.shared_mime_info.get_parents_aliased(mime)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use tokio::sync::mpsc;
|
||||||
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
||||||
use crate::config::IconSizes;
|
use crate::config::IconSizes;
|
||||||
use crate::err_str;
|
use crate::err_str;
|
||||||
use crate::tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location};
|
use crate::tab::{self, ChecksumState, DirSize, ItemMetadata, ItemThumbnail, Location};
|
||||||
|
|
||||||
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
|
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
|
||||||
|
|
||||||
|
|
@ -216,6 +216,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
//TODO: scan directory size on gvfs mounts?
|
//TODO: scan directory size on gvfs mounts?
|
||||||
dir_size: DirSize::NotDirectory,
|
dir_size: DirSize::NotDirectory,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(items)
|
Ok(items)
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,10 @@ impl MounterItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(&self, symbolic: bool) -> Option<widget::icon::Handle> {
|
pub fn icon(&self, _symbolic: bool) -> Option<widget::icon::Handle> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
Self::Gvfs(item) => item.icon(symbolic),
|
Self::Gvfs(item) => item.icon(_symbolic),
|
||||||
Self::None => unreachable!(),
|
Self::None => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +103,7 @@ impl MounterItem {
|
||||||
pub type MounterItems = Vec<MounterItem>;
|
pub type MounterItems = Vec<MounterItem>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum MounterMessage {
|
pub enum MounterMessage {
|
||||||
Items(MounterItems),
|
Items(MounterItems),
|
||||||
MountResult(MounterItem, Result<bool, String>),
|
MountResult(MounterItem, Result<bool, String>),
|
||||||
|
|
|
||||||
|
|
@ -967,11 +967,13 @@ impl Operation {
|
||||||
password,
|
password,
|
||||||
} => {
|
} => {
|
||||||
let controller_clone = controller.clone();
|
let controller_clone = controller.clone();
|
||||||
compio::runtime::spawn_blocking(
|
compio::runtime::spawn(async move {
|
||||||
move || -> Result<OperationSelection, OperationError> {
|
let extracted = compio::runtime::spawn_blocking(move || {
|
||||||
let controller = controller_clone;
|
let controller = controller_clone;
|
||||||
let total_paths = paths.len();
|
let total_paths = paths.len();
|
||||||
let mut op_sel = OperationSelection::default();
|
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() {
|
for (i, path) in paths.iter().enumerate() {
|
||||||
futures::executor::block_on(async {
|
futures::executor::block_on(async {
|
||||||
controller
|
controller
|
||||||
|
|
@ -995,16 +997,32 @@ impl Operation {
|
||||||
op_sel.ignored.push(path.clone());
|
op_sel.ignored.push(path.clone());
|
||||||
op_sel.selected.push(new_dir.clone());
|
op_sel.selected.push(new_dir.clone());
|
||||||
|
|
||||||
crate::archive::extract(path, &new_dir, &password, &controller)?;
|
let (files, dirs) = crate::archive::extract(
|
||||||
|
path,
|
||||||
|
&new_dir,
|
||||||
|
&password,
|
||||||
|
&controller,
|
||||||
|
)?;
|
||||||
|
written_files.extend(files);
|
||||||
|
target_dirs.extend(dirs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(op_sel)
|
Ok::<_, OperationError>((op_sel, written_files, target_dirs))
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}
|
|
||||||
.await
|
.await
|
||||||
.map_err(wrap_compio_spawn_error)?,
|
.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)?
|
||||||
|
}
|
||||||
Self::Move {
|
Self::Move {
|
||||||
paths,
|
paths,
|
||||||
to,
|
to,
|
||||||
|
|
@ -1138,10 +1156,26 @@ impl Operation {
|
||||||
|
|
||||||
paths.push(item.original_path());
|
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]))
|
compio::runtime::spawn_blocking(|| trash::os_limited::restore_all([item]))
|
||||||
.await
|
.await
|
||||||
.map_err(wrap_compio_spawn_error)?
|
.map_err(wrap_compio_spawn_error)?
|
||||||
.map_err(|e| OperationError::from_err(e, &controller))?;
|
.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))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(OperationSelection {
|
Ok(OperationSelection {
|
||||||
ignored: Vec::new(),
|
ignored: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use compio::driver::ToSharedFd;
|
||||||
use compio::driver::op::AsyncifyFd;
|
use compio::driver::op::AsyncifyFd;
|
||||||
use compio::io::{AsyncReadAt, AsyncWriteAt};
|
use compio::io::{AsyncReadAt, AsyncWriteAt};
|
||||||
use cosmic::iced::futures;
|
use cosmic::iced::futures;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
|
||||||
422
src/tab.rs
422
src/tab.rs
|
|
@ -18,6 +18,7 @@ use cosmic::widget::menu::action::MenuAction;
|
||||||
use cosmic::widget::menu::key_bind::KeyBind;
|
use cosmic::widget::menu::key_bind::KeyBind;
|
||||||
use cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space};
|
use cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space};
|
||||||
use cosmic::{Apply, Element, cosmic_theme, font, theme};
|
use cosmic::{Apply, Element, cosmic_theme, font, theme};
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
use i18n_embed::LanguageLoader;
|
use i18n_embed::LanguageLoader;
|
||||||
use icu::datetime::input::DateTime;
|
use icu::datetime::input::DateTime;
|
||||||
use icu::datetime::options::TimePrecision;
|
use icu::datetime::options::TimePrecision;
|
||||||
|
|
@ -28,6 +29,7 @@ use jiff_icu::ConvertFrom;
|
||||||
use mime_guess::{Mime, mime};
|
use mime_guess::{Mime, mime};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::cmp::{Ordering, Reverse};
|
use std::cmp::{Ordering, Reverse};
|
||||||
|
|
@ -179,7 +181,7 @@ fn button_appearance(
|
||||||
appearance.icon_color = Some(Color::from(cosmic.on_bg_component_color()));
|
appearance.icon_color = Some(Color::from(cosmic.on_bg_component_color()));
|
||||||
appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));
|
appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));
|
||||||
if cut {
|
if cut {
|
||||||
appearance.text_color = Some(Color::from(cosmic.background.component.on_disabled));
|
appearance.text_color = Some(Color::from(cosmic.background(false).component.on_disabled));
|
||||||
} else {
|
} else {
|
||||||
appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));
|
appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));
|
||||||
}
|
}
|
||||||
|
|
@ -190,12 +192,12 @@ fn button_appearance(
|
||||||
appearance.background = Some(Color::from(cosmic.bg_color()).into());
|
appearance.background = Some(Color::from(cosmic.bg_color()).into());
|
||||||
appearance.icon_color = Some(Color::from(cosmic.on_bg_color()));
|
appearance.icon_color = Some(Color::from(cosmic.on_bg_color()));
|
||||||
if cut {
|
if cut {
|
||||||
appearance.text_color = Some(Color::from(cosmic.background.component.disabled));
|
appearance.text_color = Some(Color::from(cosmic.background(false).component.disabled));
|
||||||
} else {
|
} else {
|
||||||
appearance.text_color = Some(Color::from(cosmic.on_bg_color()));
|
appearance.text_color = Some(Color::from(cosmic.on_bg_color()));
|
||||||
}
|
}
|
||||||
} else if cut {
|
} else if cut {
|
||||||
appearance.text_color = Some(Color::from(cosmic.background.component.on_disabled));
|
appearance.text_color = Some(Color::from(cosmic.background(false).component.on_disabled));
|
||||||
}
|
}
|
||||||
if focused && accent {
|
if focused && accent {
|
||||||
appearance.outline_width = 1.0;
|
appearance.outline_width = 1.0;
|
||||||
|
|
@ -288,6 +290,26 @@ pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Han
|
||||||
.handle()
|
.handle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generic_file_icons(
|
||||||
|
sizes: IconSizes,
|
||||||
|
) -> (
|
||||||
|
widget::icon::Handle,
|
||||||
|
widget::icon::Handle,
|
||||||
|
widget::icon::Handle,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.grid())
|
||||||
|
.handle(),
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.list())
|
||||||
|
.handle(),
|
||||||
|
widget::icon::from_name("text-x-generic")
|
||||||
|
.size(sizes.list_condensed())
|
||||||
|
.handle(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: replace with Path::has_trailing_sep when stable
|
//TODO: replace with Path::has_trailing_sep when stable
|
||||||
fn has_trailing_sep(path: &Path) -> bool {
|
fn has_trailing_sep(path: &Path) -> bool {
|
||||||
path.as_os_str()
|
path.as_os_str()
|
||||||
|
|
@ -544,7 +566,7 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "desktop"))]
|
#[cfg(not(feature = "desktop"))]
|
||||||
fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
fn get_desktop_file_display_name(_path: &Path) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,7 +585,7 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "desktop"))]
|
#[cfg(not(feature = "desktop"))]
|
||||||
fn get_desktop_file_icon(path: &Path) -> Option<String> {
|
fn get_desktop_file_icon(_path: &Path) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -654,9 +676,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
folder_icon(&path, sizes.list_condensed()),
|
folder_icon(&path, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive
|
// Keep the initial directory scan cheap. Opening files still
|
||||||
// @todo - expose this as a config option?
|
// recalculates MIME from the real path before launching apps.
|
||||||
let mime = mime_for_path(&path, None, true);
|
let mime = mime_guess::from_path(&path).first_or_octet_stream();
|
||||||
|
|
||||||
//TODO: clean this up, implement for trash
|
//TODO: clean this up, implement for trash
|
||||||
let icon_name_opt = if mime == "application/x-desktop" {
|
let icon_name_opt = if mime == "application/x-desktop" {
|
||||||
|
|
@ -673,28 +695,21 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
generic_file_icons(sizes);
|
||||||
(
|
(
|
||||||
mime.clone(),
|
mime,
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
icon_handle_grid,
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
icon_handle_list,
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
icon_handle_list_condensed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut children_opt = None;
|
let children_opt = None;
|
||||||
let mut dir_size = DirSize::NotDirectory;
|
let mut dir_size = DirSize::NotDirectory;
|
||||||
if is_dir && !remote {
|
if is_dir && !remote {
|
||||||
dir_size = DirSize::Calculating(Controller::default());
|
dir_size = DirSize::Calculating(Controller::default());
|
||||||
//TODO: calculate children in the background (and make it cancellable?)
|
|
||||||
match fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
children_opt = Some(entries.count());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read directory {}: {}", path.display(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);
|
let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);
|
||||||
|
|
@ -731,6 +746,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size,
|
dir_size,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -748,7 +764,10 @@ pub fn item_from_entry(
|
||||||
sizes: IconSizes,
|
sizes: IconSizes,
|
||||||
) -> Item {
|
) -> Item {
|
||||||
let mut is_desktop = false;
|
let mut is_desktop = false;
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mut is_gvfs = false;
|
let mut is_gvfs = false;
|
||||||
|
#[cfg(not(feature = "gvfs"))]
|
||||||
|
let is_gvfs = false;
|
||||||
|
|
||||||
let hidden = name.starts_with('.') || hidden_attribute(&metadata);
|
let hidden = name.starts_with('.') || hidden_attribute(&metadata);
|
||||||
|
|
||||||
|
|
@ -796,7 +815,9 @@ pub fn item_from_entry(
|
||||||
folder_icon(&path, sizes.list_condensed()),
|
folder_icon(&path, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let mime = mime_for_path(&path, Some(&metadata), remote);
|
// Keep the initial directory scan cheap. Opening files still
|
||||||
|
// recalculates MIME from the real path before launching apps.
|
||||||
|
let mime = mime_guess::from_path(&path).first_or_octet_stream();
|
||||||
//TODO: clean this up, implement for trash
|
//TODO: clean this up, implement for trash
|
||||||
let icon_name_opt = if mime == "application/x-desktop" {
|
let icon_name_opt = if mime == "application/x-desktop" {
|
||||||
is_desktop = true;
|
is_desktop = true;
|
||||||
|
|
@ -812,28 +833,21 @@ pub fn item_from_entry(
|
||||||
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
desktop_icon_handle(&icon_name, sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
generic_file_icons(sizes);
|
||||||
(
|
(
|
||||||
mime.clone(),
|
mime,
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
icon_handle_grid,
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
icon_handle_list,
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
icon_handle_list_condensed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut children_opt = None;
|
let children_opt = None;
|
||||||
let mut dir_size = DirSize::NotDirectory;
|
let mut dir_size = DirSize::NotDirectory;
|
||||||
if metadata.is_dir() && !remote {
|
if metadata.is_dir() && !remote {
|
||||||
dir_size = DirSize::Calculating(Controller::default());
|
dir_size = DirSize::Calculating(Controller::default());
|
||||||
//TODO: calculate children in the background (and make it cancellable?)
|
|
||||||
match fs::read_dir(&path) {
|
|
||||||
Ok(entries) => {
|
|
||||||
children_opt = Some(entries.count());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read directory {}: {}", path.display(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);
|
let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);
|
||||||
|
|
@ -862,6 +876,7 @@ pub fn item_from_entry(
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size,
|
dir_size,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -874,6 +889,8 @@ pub fn item_from_trash_entry(
|
||||||
let name = entry.name.to_string_lossy().into_owned();
|
let name = entry.name.to_string_lossy().into_owned();
|
||||||
let display_name = Item::display_name(&name);
|
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
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = match metadata.size
|
||||||
{
|
{
|
||||||
trash::TrashItemSize::Entries(_) => (
|
trash::TrashItemSize::Entries(_) => (
|
||||||
|
|
@ -901,7 +918,7 @@ pub fn item_from_trash_entry(
|
||||||
is_mount_point: false,
|
is_mount_point: false,
|
||||||
metadata: ItemMetadata::Trash { metadata, entry },
|
metadata: ItemMetadata::Trash { metadata, entry },
|
||||||
hidden: false,
|
hidden: false,
|
||||||
location_opt: None,
|
location_opt: location,
|
||||||
image_dimensions: (mime.type_() == mime::IMAGE)
|
image_dimensions: (mime.type_() == mime::IMAGE)
|
||||||
.then(|| image::image_dimensions(&original_path).ok())
|
.then(|| image::image_dimensions(&original_path).ok())
|
||||||
.flatten(),
|
.flatten(),
|
||||||
|
|
@ -918,9 +935,35 @@ pub fn item_from_trash_entry(
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size: DirSize::NotDirectory,
|
dir_size: DirSize::NotDirectory,
|
||||||
cut: false,
|
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> {
|
fn get_filename_from_path(path: &Path) -> Result<String, String> {
|
||||||
Ok(match path.file_name() {
|
Ok(match path.file_name() {
|
||||||
Some(name_os) => name_os
|
Some(name_os) => name_os
|
||||||
|
|
@ -947,7 +990,10 @@ pub fn item_from_path<P: Into<PathBuf>>(path: P, sizes: IconSizes) -> Result<Ite
|
||||||
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let mut hidden_files = Box::from([]);
|
let mut hidden_files = Box::from([]);
|
||||||
|
#[cfg(feature = "gvfs")]
|
||||||
let mut remote_scannable = false;
|
let mut remote_scannable = false;
|
||||||
|
#[cfg(not(feature = "gvfs"))]
|
||||||
|
let remote_scannable = false;
|
||||||
|
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
{
|
{
|
||||||
|
|
@ -1036,7 +1082,12 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
})
|
})
|
||||||
.ok()?;
|
.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();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
@ -1341,6 +1392,7 @@ pub fn scan_desktop(
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size: DirSize::NotDirectory,
|
dir_size: DirSize::NotDirectory,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1724,6 +1776,8 @@ pub enum Message {
|
||||||
GoPrevious,
|
GoPrevious,
|
||||||
ItemDown,
|
ItemDown,
|
||||||
ItemLeft,
|
ItemLeft,
|
||||||
|
ItemPageDown,
|
||||||
|
ItemPageUp,
|
||||||
ItemRight,
|
ItemRight,
|
||||||
ItemUp,
|
ItemUp,
|
||||||
Location(Location),
|
Location(Location),
|
||||||
|
|
@ -1760,6 +1814,9 @@ pub enum Message {
|
||||||
HighlightDeactivate(usize),
|
HighlightDeactivate(usize),
|
||||||
HighlightActivate(usize),
|
HighlightActivate(usize),
|
||||||
DirectorySize(PathBuf, DirSize),
|
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
|
ImageDecoded(PathBuf, u32, u32, Vec<u8>, Option<(u32, u32)>, u64), // path, width, height, pixels, display_size, generation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1787,6 +1844,23 @@ pub enum DirSize {
|
||||||
Error(String),
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ItemMetadata {
|
pub enum ItemMetadata {
|
||||||
Path {
|
Path {
|
||||||
|
|
@ -2238,6 +2312,7 @@ pub struct Item {
|
||||||
pub cut: bool,
|
pub cut: bool,
|
||||||
pub overlaps_drag_rect: bool,
|
pub overlaps_drag_rect: bool,
|
||||||
pub dir_size: DirSize,
|
pub dir_size: DirSize,
|
||||||
|
pub checksums: ChecksumState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
|
|
@ -2368,20 +2443,24 @@ impl Item {
|
||||||
)));
|
)));
|
||||||
let mut settings = Vec::new();
|
let mut settings = Vec::new();
|
||||||
if let Some(mime_app_cache) = mime_app_cache_opt {
|
if let Some(mime_app_cache) = mime_app_cache_opt {
|
||||||
let mime_apps = mime_app_cache.get(&self.mime);
|
let mime_apps = mime_app_cache.get_apps_for_mime(&self.mime, false);
|
||||||
if !mime_apps.is_empty() {
|
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(
|
settings.push(
|
||||||
widget::settings::item::builder(fl!("open-with")).control(
|
widget::settings::item::builder(fl!("open-with")).control(
|
||||||
Element::from(
|
Element::from(
|
||||||
widget::dropdown(
|
widget::dropdown(
|
||||||
mime_apps,
|
names,
|
||||||
mime_apps.iter().position(|x| x.is_default),
|
mime_apps.iter().position(|(x, _)| x.is_default(&self.mime)),
|
||||||
move |index| index,
|
move |index| index,
|
||||||
)
|
)
|
||||||
.icons(Cow::Borrowed(mime_app_cache.icons(&self.mime))),
|
.icons(Cow::Owned(icons)),
|
||||||
)
|
)
|
||||||
.map(|index| {
|
.map(move |index| {
|
||||||
let mime_app = &mime_apps[index];
|
let mime_app = &mime_apps[index].0;
|
||||||
Message::SetOpenWith(self.mime.clone(), mime_app.id.clone())
|
Message::SetOpenWith(self.mime.clone(), mime_app.id.clone())
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -2510,6 +2589,55 @@ impl Item {
|
||||||
}
|
}
|
||||||
column = column.push(details);
|
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()
|
if let Some(path) = self.path_opt()
|
||||||
&& self.selected
|
&& self.selected
|
||||||
{
|
{
|
||||||
|
|
@ -2707,6 +2835,32 @@ async fn calculate_dir_size(path: &Path, controller: Controller) -> Result<u64,
|
||||||
Ok(total)
|
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) {
|
fn folder_name<P: AsRef<Path>>(path: P) -> (String, bool) {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let mut found_home = false;
|
let mut found_home = false;
|
||||||
|
|
@ -3126,6 +3280,14 @@ impl Tab {
|
||||||
item.pos_opt.get()
|
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> {
|
pub(crate) fn select_focus_scroll(&mut self) -> Option<AbsoluteOffset> {
|
||||||
let items = self.items_opt.as_ref()?;
|
let items = self.items_opt.as_ref()?;
|
||||||
let item = items.get(self.select_focus?)?;
|
let item = items.get(self.select_focus?)?;
|
||||||
|
|
@ -3161,6 +3323,20 @@ 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)> {
|
fn select_range_start_pos_opt(&self) -> Option<(usize, usize)> {
|
||||||
let items = self.items_opt.as_ref()?;
|
let items = self.items_opt.as_ref()?;
|
||||||
let item = items.get(self.select_range.map(|r| r.0)?)?;
|
let item = items.get(self.select_range.map(|r| r.0)?)?;
|
||||||
|
|
@ -3816,6 +3992,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::ItemDown => {
|
Message::ItemDown => {
|
||||||
|
self.dehighlight_all();
|
||||||
if let Some(edit_location) = &mut self.edit_location {
|
if let Some(edit_location) = &mut self.edit_location {
|
||||||
edit_location.select(true);
|
edit_location.select(true);
|
||||||
} else if self.gallery {
|
} else if self.gallery {
|
||||||
|
|
@ -3857,7 +4034,95 @@ 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 => {
|
Message::ItemLeft => {
|
||||||
|
self.dehighlight_all();
|
||||||
if self.gallery {
|
if self.gallery {
|
||||||
commands.append(&mut self.update(Message::GalleryPrevious, modifiers));
|
commands.append(&mut self.update(Message::GalleryPrevious, modifiers));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3916,6 +4181,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::ItemRight => {
|
Message::ItemRight => {
|
||||||
|
self.dehighlight_all();
|
||||||
if self.gallery {
|
if self.gallery {
|
||||||
commands.append(&mut self.update(Message::GalleryNext, modifiers));
|
commands.append(&mut self.update(Message::GalleryNext, modifiers));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3957,6 +4223,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::ItemUp => {
|
Message::ItemUp => {
|
||||||
|
self.dehighlight_all();
|
||||||
if let Some(edit_location) = &mut self.edit_location {
|
if let Some(edit_location) = &mut self.edit_location {
|
||||||
edit_location.select(false);
|
edit_location.select(false);
|
||||||
} else if self.gallery {
|
} else if self.gallery {
|
||||||
|
|
@ -4533,6 +4800,52 @@ 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
|
// Scroll to top if needed
|
||||||
|
|
@ -6448,21 +6761,25 @@ impl Tab {
|
||||||
.and_then(|(mime, _)| mime.parse::<Mime>().ok())
|
.and_then(|(mime, _)| mime.parse::<Mime>().ok())
|
||||||
&& let Some(mime_app_cache) = mime_app_cache_opt
|
&& let Some(mime_app_cache) = mime_app_cache_opt
|
||||||
{
|
{
|
||||||
let mime_apps = mime_app_cache.get(&mime);
|
let mime_apps = mime_app_cache.get_apps_for_mime(&mime, false);
|
||||||
if !mime_apps.is_empty() {
|
if !mime_apps.is_empty() {
|
||||||
let mime_closure = mime.clone();
|
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(
|
settings.push(
|
||||||
widget::settings::item::builder(fl!("open-with")).control(
|
widget::settings::item::builder(fl!("open-with")).control(
|
||||||
Element::from(
|
Element::from(
|
||||||
widget::dropdown(
|
widget::dropdown(
|
||||||
mime_apps,
|
names,
|
||||||
mime_apps.iter().position(|x| x.is_default),
|
mime_apps.iter().position(|(x, _)| x.is_default(&mime)),
|
||||||
move |index| (index, mime_closure.clone()),
|
move |index| (index, mime_closure.clone()),
|
||||||
)
|
)
|
||||||
.icons(Cow::Borrowed(mime_app_cache.icons(&mime))),
|
.icons(Cow::Owned(icons)),
|
||||||
)
|
)
|
||||||
.map(|(index, mime)| {
|
.map(move |(index, mime)| {
|
||||||
let mime_app = &mime_apps[index];
|
let mime_app = &mime_apps[index].0;
|
||||||
Message::SetOpenWith(mime, mime_app.id.clone())
|
Message::SetOpenWith(mime, mime_app.id.clone())
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -7580,7 +7897,10 @@ mod tests {
|
||||||
// Text editor content may add a trailing newline
|
// Text editor content may add a trailing newline
|
||||||
assert_eq!(content.text().trim_end(), "ab");
|
assert_eq!(content.text().trim_end(), "ab");
|
||||||
}
|
}
|
||||||
_ => panic!("expected Text thumbnail with valid prefix only, got {:?}", thumb),
|
_ => panic!(
|
||||||
|
"expected Text thumbnail with valid prefix only, got {:?}",
|
||||||
|
thumb
|
||||||
|
),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@
|
||||||
use cosmic::desktop::fde::GenericEntry;
|
use cosmic::desktop::fde::GenericEntry;
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::path::Path;
|
#[cfg(feature = "desktop")]
|
||||||
use std::sync::{LazyLock, Mutex};
|
use std::{fs, time::Instant};
|
||||||
use std::time::Instant;
|
use std::{
|
||||||
use std::{fs, process};
|
path::Path,
|
||||||
|
process,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Thumbnailer {
|
pub struct Thumbnailer {
|
||||||
|
|
|
||||||
71
src/trash.rs
71
src/trash.rs
|
|
@ -1,11 +1,30 @@
|
||||||
use cosmic::widget;
|
use cosmic::widget;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::config::IconSizes;
|
use crate::config::IconSizes;
|
||||||
use crate::tab::{Item, SearchItem};
|
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 {
|
pub trait TrashExt {
|
||||||
fn is_empty() -> bool {
|
fn is_empty() -> bool {
|
||||||
true
|
true
|
||||||
|
|
@ -49,6 +68,46 @@ 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;
|
pub struct Trash;
|
||||||
|
|
||||||
// This config statement is from trash::os_limited
|
// This config statement is from trash::os_limited
|
||||||
|
|
@ -142,4 +201,12 @@ impl TrashExt for Trash {
|
||||||
not(target_os = "android")
|
not(target_os = "android")
|
||||||
)
|
)
|
||||||
)))]
|
)))]
|
||||||
impl TrashExt for Trash {}
|
impl TrashExt for Trash {
|
||||||
|
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
|
||||||
|
log::warn!(
|
||||||
|
"searching trash not supported on this platform for pattern {:?}",
|
||||||
|
regex.as_str()
|
||||||
|
);
|
||||||
|
drop(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue