Compare commits

..

35 commits

Author SHA1 Message Date
dcd936f7dd chore: resolve local graphics dependencies 2026-05-25 19:37:22 +02:00
9dbe3a378d chore: use local glyphon lockfile 2026-05-25 18:28:56 +02:00
36ade6cf65 chore: use local COSMIC support crates 2026-05-25 18:10:22 +02:00
d6c245d809 fix(keybinds): adapt key_bind.matches to new 3-arg signature
Upstream libcosmic added an Option<&Physical> third parameter to
KeyBind::matches for physical-key matching. We pass None at every call
site since these paths only have the logical key available.

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

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

Original commits (chronological):

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

Original tip preserved as tag backup/pre-rebase-upstream-20260524.
2026-05-24 21:27:30 +02:00
Mario
784200a253
Fix 24h time format in file dialog (#1812)
* Assign parameter config the proper way
* Don't overwrite military time

---------

Co-authored-by: Rrogntudju <rrogntudju@example.com>
2026-05-18 11:57:04 -06:00
Oleg Bespalov
03e537abad feat: add text preview for thumbnails and gallery
Preview text files (≤8 MiB) in grid and gallery: read up to 256 KiB,
handle invalid UTF-8, skip empty files. Add unit tests.

Lets users peek at .txt without opening; size caps avoid blocking the UI.
2026-05-15 14:09:14 -06:00
Levi Portenier
accb9fd418
fix: don't overwrite military time in Message::Config() (#1803)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x ] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.

Closes #1798
2026-05-12 17:05:30 -06:00
Jeremy Soller
f9b215dbd4 Epoch 1.0.13 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-05-12 12:00:19 -06:00
Michael Murphy
77615fc6b5
i18n: translation update from Hosted Weblate (#1763)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Files](https://hosted.weblate.org/projects/pop-os/cosmic-files/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-files/horizontal-auto.svg)
2026-05-12 17:21:12 +02:00
Jeremy Soller
048ed3187b
Merge pull request #1793 from pop-os/icons
Fix small file icons from alternative icon themes
2026-05-12 09:13:49 -06:00
Rrogntudju
b4df354585 don't overwrite military time 2026-05-11 20:07:35 -04:00
lorduskordus
4b5c6d2c1b fix(i18n): use separate 'rename' keys for menu and dialog confirm button 2026-05-11 16:49:33 -06:00
Jeremy Soller
3548615d40
Merge pull request #1777 from norepro/add-clear-recents-button
feat(files): Add button to clear recents
2026-05-11 15:04:34 -06:00
Hosted Weblate
55c8a54f28
i18n: translation updates from weblate
Co-authored-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Arve Eriksson <031299870@telia.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: BoneNI <bounkirdni@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Feike Donia <feikedonia@proton.me>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Nara Díaz Viñolas <nara.diaz.vinolas@gmail.com>
Co-authored-by: Tadas Misiūnas <tadujo@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: Марко М. Костић <marko.m.kostic@gmail.com>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files
2026-05-11 12:23:52 +02:00
Michael Aaron Murphy
095940e9a9
fix: small grid file icons 2026-05-06 16:40:30 +02:00
Michael Aaron Murphy
6946e95c88
fix: prefer SVG file icons 2026-05-06 16:40:30 +02:00
Jeremy Soller
750c92c841 Epoch 1.0.12 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-05-05 12:03:22 -06:00
Jeremy Soller
b8e02b7df8
Merge pull request #1776 from norepro/network-sidebar-name
fix: Use name for network location
2026-05-04 13:47:08 -06:00
Michael Aaron Murphy
cd48e4fa30 perf: reduce size of DialogPage enum by 1000 bytes 2026-05-01 19:10:23 +02:00
Michael Aaron Murphy
72b40aece3 perf: use Box with tab::Item to reduce message size by 800 bytes 2026-05-01 19:10:23 +02:00
Michael Aaron Murphy
7c47cbbb29 chore: add rust-toolchain to enforce 1.93 2026-05-01 19:10:23 +02:00
Michael Aaron Murphy
f8323171e7 fix: remove match if let guards for Rust 1.93 2026-05-01 19:10:23 +02:00
Jeremy Soller
506f86eff1
Merge pull request #1775 from pop-os/field-codes
fix: improve desktop entry field code handling
2026-04-30 07:49:37 -06:00
Michael Aaron Murphy
37e8734e7b
fix(mime-app): handle no-path command exec 2026-04-30 00:12:28 +02:00
Michael Aaron Murphy
1fcd0dccc8
fix: %F and %U field code handling
Fixes %F path arguments being ignored
2026-04-29 21:52:17 +02:00
Michael Aaron Murphy
d775f3e5e8
fix: improve desktop entry field code handling
- The %f and %u field codes may now be expanded within a word
- Handle field code escapes (%%)
- Support the %c and %k field codes

This will notably fix desktop entries and context menu actions
that pass files as a long argument, such as `--option=%f`.
2026-04-29 17:31:42 +02:00
Vukašin Vojinović
d5dbcc7677 chore: add rustfmt config
Also adds a Zed editor config for automatic formatting with nightly.
2026-04-29 00:53:57 +02:00
Vukašin Vojinović
e91a984da9 chore: clippy 2026-04-29 00:53:57 +02:00
Vukašin Vojinović
93e31d433a chore: update dependencies 2026-04-29 00:53:57 +02:00
Will Sheehan
e7fa2d0fa5 Use existing clear-recents-history string 2026-04-25 00:11:05 -07:00
Will Sheehan
accf5b2ba6 Add button to clear recents 2026-04-25 00:00:29 -07:00
Will Sheehan
38e4fd3ec7 Use name for network location if available 2026-04-24 23:15:31 -07:00
61 changed files with 2760 additions and 1294 deletions

15
.zed/settings.json Normal file
View file

@ -0,0 +1,15 @@
{
"format_on_save": "on",
"lsp": {
"rust-analyzer": {
"initialization_options": {
"check": {
"command": "clippy",
},
"rustfmt": {
"extraArgs": ["+nightly"],
},
},
},
},
}

751
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,17 @@
[package]
name = "cosmic-files"
version = "1.0.11"
version = "1.0.13"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"
rust-version = "1.90"
rust-version = "1.93"
[dependencies]
anyhow = "1"
jiff = "0.2"
jiff-icu = "0.2"
icu = { version = "2.1.1", features = ["compiled_data"] }
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
icu = { version = "2.2.0", features = ["compiled_data"] }
cctk = { path = "../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit", optional = true }
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
dirs = "6.0.0"
gio = { version = "0.21", optional = true }
@ -24,7 +24,7 @@ log = "0.4"
mime_guess = "2"
notify-debouncer-full = "0.7"
notify-rust = { version = "4", optional = true }
open = "5.3.3"
open = "5.3.4"
paste = "1.0"
regex = "1"
rustc-hash = "2.1"
@ -36,15 +36,15 @@ tokio = { version = "1", features = ["process", "sync"] }
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" }
url = "2.5"
walkdir = "2.5.0"
wayland-client = { version = "0.31.13", optional = true }
wayland-client = { version = "0.31.14", optional = true }
xdg = { version = "3.0", optional = true }
xdg-mime = { git = "https://github.com/ebassi/xdg-mime-rs" }
# Compression
bzip2 = { version = "0.6", optional = true } #TODO: replace with pure Rust crate
flate2 = "1.1"
tar = "0.4.44"
tar = "0.4.45"
lzma-rust2 = { version = "0.16", optional = true }
ordermap = { version = "1.1.0", features = ["serde"] }
ordermap = { version = "1.2.0", features = ["serde"] }
# Internationalization
i18n-embed = { version = "0.16", features = [
"fluent-system",
@ -61,10 +61,11 @@ jxl-oxide = { version = "0.12.5", features = ["image"] }
num_cpus = "1.17.0"
filetime = "0.2"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
thiserror = "2.0.18"
atomic_float = "1.1.0"
num_enum = "0.7.6"
bstr = "1.12.1"
# Completion-based IO runtime to enable io_uring / IOCP file IO support.
[dependencies.compio]
@ -72,8 +73,9 @@ version = "0.18"
default-features = false
features = ["fs", "io", "macros", "polling", "runtime"]
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic.git"
# Yoda fork — depend on libcosmic-yoda directly by path (no git/no patch).
[dependencies.libcosmic-yoda]
path = "../libcosmic"
default-features = false
#TODO: a11y feature crashes
features = [
@ -82,7 +84,7 @@ features = [
"autosize",
"multi-window",
"tokio",
"winit",
"wayland",
"surface-message",
]
@ -110,15 +112,15 @@ default = [
"wayland",
"wgpu",
]
dbus-config = ["libcosmic/dbus-config"]
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
dbus-config = ["libcosmic-yoda/dbus-config"]
desktop = ["libcosmic-yoda/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
desktop-applet = []
gvfs = ["dep:gio", "dep:glib"]
io-uring = ["compio/io-uring"]
jemalloc = ["dep:tikv-jemallocator"]
notify = ["dep:notify-rust"]
wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"]
wgpu = ["libcosmic/wgpu"]
wayland = ["libcosmic-yoda/wayland", "dep:cctk", "dep:wayland-client"]
wgpu = ["libcosmic-yoda/wgpu"]
[profile.dev]
opt-level = 1
@ -144,19 +146,12 @@ fastrand = "2"
test-log = "0.2"
tokio = { version = "1", features = ["rt", "macros"] }
# [patch.'https://github.com/pop-os/cosmic-text']
# cosmic-text = { path = "../cosmic-text" }
# Yoda fork — libcosmic dep is now a direct path dep (libcosmic-yoda above),
# 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']
libcosmic = { path = "../libcosmic" }
cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-theme = { path = "../libcosmic/cosmic-theme" }
iced_futures = { path = "../libcosmic/iced/futures" }
iced_winit = { path = "../libcosmic/iced/winit" }
# [patch.'https://github.com/pop-os/smithay-clipboard']
# smithay-clipboard = { path = "../smithay-clipboard" }
[patch.'https://github.com/pop-os/cosmic-text.git']
cosmic-text = { path = "../cosmic-text" }
[workspace]
members = ["cosmic-files-applet"]

View file

@ -1,4 +1,5 @@
use std::{env, fs, path::PathBuf};
use std::path::PathBuf;
use std::{env, fs};
use xdgen::{App, Context, FluentString};
fn main() {

View file

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

12
debian/changelog vendored
View file

@ -1,3 +1,15 @@
cosmic-files (1.0.13) noble; urgency=medium
* Epoch 1.0.13 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 12 May 2026 09:39:14 -0600
cosmic-files (1.0.12) noble; urgency=medium
* Epoch 1.0.12 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 05 May 2026 10:23:57 -0600
cosmic-files (1.0.11) noble; urgency=medium
* Epoch 1.0.11 version update

View 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`.

View file

@ -1,6 +1,8 @@
use cosmic_files::operation::recursive::Method;
use cosmic_files::operation::{Controller, ReplaceResult, recursive::Context};
use std::{error::Error, io, path::PathBuf};
use cosmic_files::operation::recursive::{Context, Method};
use cosmic_files::operation::{Controller, ReplaceResult};
use std::error::Error;
use std::io;
use std::path::PathBuf;
#[compio::main]
async fn main() -> Result<(), Box<dyn Error>> {

View file

@ -1,15 +1,12 @@
use cosmic::{
Application, Element,
app::{self, Core, Settings, Task},
executor,
iced::{Subscription, window},
widget,
};
use cosmic::app::{self, Core, Settings, Task};
use cosmic::iced::{Subscription, window};
use cosmic::{Application, Element, executor, widget};
use cosmic_files::dialog::{
Dialog, DialogChoice, DialogChoiceOption, DialogFilter, DialogFilterPattern, DialogKind,
DialogMessage, DialogResult, DialogSettings,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let log_format = tracing_subscriber::fmt::format()

View file

@ -372,3 +372,14 @@ show-recents = مجلد الحديثة في الشريط الجانبي
clear-recents-history = امحُ التأريخ الحديث
copy-path = انسخ المسار
mixed = مختلط
context-action = إجراء السياق
context-action-confirm-title = شغِّل "{ $name }"؟
context-action-confirm-warning =
سيُشغِّل هذا على { $items } { $items ->
[one] عنصر
[two] عنصرين
[few] عناصر
[many] عنصرًا
*[other] عنصر
}.
run = شغِّل

View file

@ -74,7 +74,7 @@ name-no-slashes = El nom no pot contenir barres.
## Open/Save Dialog
cancel = Cancel·la
cancel = Cancel·lar
create = Crea
open = Obre
open-file = Obre el fixer
@ -352,3 +352,4 @@ sort-newest-first = Primer més recents
sort-oldest-first = Primer més antics
sort-smallest-to-largest = De petit a gran
sort-largest-to-smallest = De gran a petit
run = Executa

View file

@ -17,7 +17,7 @@ size = Velikost
## Empty Trash Dialog
empty-trash = Vysypat koš
empty-trash = Vyprázdnit koš
empty-trash-warning = Položky v koši budou trvale smazány
## New File/Folder Dialog
@ -195,8 +195,8 @@ deleted =
[few] položky
*[other] položek
} z koše
emptying-trash = Vysypávání koše ({ $progress })...
emptied-trash = Koš vysypán
emptying-trash = Vyprazdňování koše ({ $progress })...
emptied-trash = Koš vyprázdněn
restoring =
Obnovování { $items } { $items ->
[one] položky
@ -403,7 +403,7 @@ sort-largest-to-smallest = Od největšího po nejmenší
gallery-preview = Náhled galerie
sort = Řazení
sort-a-z = A-Z
empty-trash-title = Vysypat koš?
empty-trash-title = Vyprázdnit koš?
type-to-search-select = Vybere první shodující se soubor nebo složku
pasted-image = Vložený obrázek
pasted-text = Vložený text
@ -418,3 +418,12 @@ show-recents = Složka „Nedávné“ v postranním panelu
copy-path = Kopírovat cestu
clear-recents-history = Vymazat historii „Nedávné“
mixed = Různé
context-action = Kontextová akce
context-action-confirm-title = Spustit „{ $name }“?
context-action-confirm-warning =
Tato akce se spustí pro { $items } { $items ->
[one] položku
[few] položky
*[other] položek
}.
run = Spustit

View file

@ -429,3 +429,11 @@ remove-from-recents = Aus den zuletzt verwendeten Elementen entfernen
move-to = Verschieben nach...
copy-path = Pfad kopieren
mixed = Gemischt
context-action = Kontextaktion
context-action-confirm-title = „{ $name }“ ausführen?
context-action-confirm-warning =
Dies wird ausgeführt auf { $items } { $items ->
[one] Element
*[other] Elementen
}.
run = Ausführen

View file

@ -1,15 +1,329 @@
empty-folder = Άδειος φάκελος
no-results = Δεν βρέθηκαν αποτελέσματα
trash = Κάδος Ανακύκλωσης
trash = Απορρίμματα
recents = Πρόσφατα
cosmic-files = COSMIC Αρχεία
empty-folder-hidden = Άδειος φάκελος (περιέχει κρυφά αντικείμενα)
cosmic-files = Αρχεία COSMIC
empty-folder-hidden = Άδειος φάκελος (περιέχει κρυφά στοιχεία)
filesystem = Σύστημα αρχείων
home = Προσωπικός φάκελος
networks = Δίκτυο
comment = Αρχεία για το COSMIC περιβάλλον
keywords = Φάκελος;Διαχειριστής;
networks = Δίκτυα
comment = Διαχείριση αρχείων για το περιβάλλον επιφάνειας εργασίας COSMIC
keywords = Αρχείο;Φάκελος;Διαχείριση;Folder;Manager;
rename = Μετονομασία...
close-tab = Κλείσιμο καρτέλας
light = Φωτεινό
dark = Σκοτεινό
light = Ανοιχτόχρωμο
dark = Σκουρόχρωμο
connect = Σύνδεση
dismiss = Απόρριψη μηνύματος
copy_noun = Αντιγραφή
open-file = Άνοιγμα αρχείου
save = Αποθήκευση
password = Κωδικός πρόσβασης
remove = Αφαίρεση
create = Δημιουργία
pause = Παύση
quit = Έξοδος
calculating = Υπολογισμός...
keep = Διατήρηση
edit = Επεξεργασία
connecting = Σύνδεση...
copy = Αντιγραφή
theme = Θέμα
appearance = Εμφάνιση
name = Όνομα
resume = Συνέχιση
username = Όνομα χρήστη
delete = Διαγραφή
repository = Αποθετήριο
support = Υποστήριξη
eject = Εξαγωγή
group = Ομάδα
skip = Παράλειψη
paste = Επικόλληση
menu-settings = Ρυθμίσεις...
view = Προβολή
undo = Αναίρεση
details = Λεπτομέρειες
sort-a-z = Α
extract-here = Αποσυμπίεση
cancel = Ακύρωση
sort-z-a = Ω-Α
open = Άνοιγμα
history = Ιστορικό
domain = Τομέας
sort = Ταξινόμηση
settings = Ρυθμίσεις
pending = Σε εκκρεμότητα
open-folder = Άνοιγμα φακέλου
replace = Αντικατάσταση
cut = Αποκοπή
file = Αρχείο
today = Σήμερα
compress = Συμπίεση...
size = Μέγεθος
related-apps = Σχετικές εφαρμογές
zoom-in = Μεγέθυνση
select-all = Επιλογή όλων
icon-size-and-spacing = Μέγεθος και απόσταση εικονιδίων
new-window = Νέο παράθυρο
zoom-out = Σμίκρυνση
default-size = Προεπιλεγμένο μέγεθος
create-archive = Δημιουργία συμπιεσμένου αρχείου
other-apps = Άλλες εφαρμογές
rename-folder = Μετονομασία φακέλου
folder-name = Όνομα φακέλου
connect-anonymously = Ανώνυμη σύνδεση
replace-with = Αντικατάσταση με
mounted-drives = Προσαρτημένες μονάδες
desktop-view-options = Επιλογές προβολής επιφάνειας εργασίας...
show-on-desktop = Εμφάνιση στην επιφάνεια εργασίας
trash-folder-icon = Εικονίδιο φακέλου απορριμμάτων
open-with = Άνοιγμα με
keep-both = Διατήρηση αμφότερων
icon-size = Μέγεθος εικονιδίων
open-with-title = Πώς θέλετε να ανοίξετε το «{ $name }»;
extract-password-required = Απαιτείται κωδικός πρόσβασης
rename-file = Μετονομασία αρχείου
file-name = Όνομα αρχείου
save-file = Αποθήκευση αρχείου
name-hidden = Τα ονόματα που ξεκινούν με «.» θα αποκρύπτονται
folder-already-exists = Υπάρχει ήδη ένας φάκελος με αυτό το όνομα
empty-trash = Άδειασμα απορριμμάτων
permanently-delete-question = Οριστική διαγραφή;
copy-to-button-label = Αντιγραφή
move-to-button-label = Μετακίνηση
run = Εκτέλεση
copy-to-title = Επιλογή προορισμού αντιγραφής
sort-newest-first = Πρώτα τα νεότερα
default-app = { $name } (προεπιλογή)
renamed = Έγινε μετονομασία από «{ $from }» σε «{ $to }»
read-execute = Ανάγνωση και εκτέλεση
deleted =
Έγινε διαγραφή { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τα { trash }
item-modified = Ημερομηνία τροποποίησης: { $modified }
list-view = Προβολή λίστας
reload-folder = Επαναφόρτωση φακέλου
favorite-path-error = Σφάλμα ανοίγματος καταλόγου
progress = { $percent }%
remove-from-sidebar = Αφαίρεση από την πλαϊνή στήλη
restoring =
Ανάκτηση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τα { trash } ({ $progress })...
network-drive-error = Αδυναμία πρόσβασης σε μονάδα δικτύου
gallery-preview = Προεπισκόπηση συλλογής
sort-smallest-to-largest = Από τα μικρότερα στα μεγαλύτερα
removing-from-recents =
Αφαίρεση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τα { recents }
type-to-search-enter-path = Εισέρχεται στη διαδρομή προς τον κατάλογο ή το αρχείο
emptying-trash = Άδειασμα φακέλου «{ trash }» ({ $progress })...
trashed-on = Ημερομηνία διαγραφής
compressing =
Συμπίεση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τον φάκελο «{ $from }» στο αρχείο «{ $to }» ({ $progress })...
move-to-trash = Μετακίνηση στα απορρίμματα
menu-about = Σχετικά με τα Αρχεία COSMIC...
setting-executable-and-launching = Ορισμός του «{ $name }» ως εκτελέσιμου και εκκίνηση
open-multiple-files = Άνοιγμα πολλαπλών αρχείων
menu-open-with = Άνοιγμα με...
extracted =
Έγινε αποσυμπίεση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από το αρχείο «{ $from }» στον φάκελο «{ $to }»
create-new-folder = Δημιουργία νέου φακέλου
original-file = Πρωτότυπο αρχείο
read-write-execute = Ανάγνωση, εγγραφή και εκτέλεση
set-permissions = Έγινε ορισμός των δικαιωμάτων για το «{ $name }» σε: { $mode }
sort-by-size = Ταξινόμηση κατά μέγεθος
item-size = Μέγεθος: { $size }
permanently-deleting =
Οριστική διαγραφή { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
}
read-write = Ανάγνωση και εγγραφή
none = Κανένα
items = Στοιχεία: { $items }
type = Τύπος: { $mime }
compressed =
Έγινε συμπίεση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τον φάκελο «{ $from }» στο αρχείο «{ $to }»
replace-warning = Θέλετε να το αντικαταστήσετε με αυτό που αποθηκεύετε; Αυτό θα οδηγήσει στην αντικατάσταση του περιεχομένου του.
new-file = Νέο αρχείο...
open-in-terminal = Άνοιγμα σε τερματικό
open-multiple-folders = Άνοιγμα πολλαπλών φακέλων
remember-password = Απομνημόνευση κωδικού πρόσβασης
show-details = Εμφάνιση λεπτομερειών
grid-spacing = Απόσταση πλέγματος
extract-to = Αποσυμπίεση σε...
add-network-drive = Προσθήκη μονάδας δικτύου
copying =
Αντιγραφή { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τον φάκελο «{ $from }» στον φάκελο «{ $to }» ({ $progress })...
sort-oldest-first = Πρώτα τα παλαιότερα
create-new-file = Δημιουργία νέου αρχείου
sort-by-trashed = Ταξινόμηση κατά ημερομηνία διαγραφής
replace-warning-operation = Θέλετε να το αντικαταστήσετε; Αυτό θα οδηγήσει στην αντικατάσταση του περιεχομένου του.
try-again = Δοκιμή ξανά
copied =
Έγινε αντιγραφή { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τον φάκελο «{ $from }» στον φάκελο «{ $to }»
other = Άλλο
open-in-new-window = Άνοιγμα σε νέο παράθυρο
sort-by-modified = Ταξινόμηση κατά ημερομηνία τροποποίησης
list-directories-first = Παράθεση των καταλόγων πρώτα
read-only = Μόνο ανάγνωση
browse-store = Περιήγηση στο { $store }
enter-server-address = Εισαγάγετε τη διεύθυνση διακομιστή
remove-from-recents = Αφαίρεση από τα πρόσφατα
apply-to-all = Εφαρμογή σε όλα
moving =
Μετακίνηση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τον φάκελο «{ $from }» στον φάκελο «{ $to }» ({ $progress })...
change-wallpaper = Αλλαγή ταπετσαρίας...
network-drive-description =
Οι διευθύνσεις διακομιστών αποτελούνται από ένα πρόθεμα πρωτοκόλλου και μια διεύθυνση.
Παραδείγματα: ssh://192.168.0.1, ftp://[2001:db8::1]
deleting =
Διαγραφή { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τα { trash } ({ $progress })...
single-click = Μονό κλικ για άνοιγμα
setting-permissions = Ορισμός των δικαιωμάτων για το «{ $name }» σε: { $mode }
owner = Κάτοχος
creating = Δημιουργία του «{ $name }» στον φάκελο «{ $parent }»
execute-only = Μόνο εκτέλεση
open-item-location = Άνοιγμα τοποθεσίας στοιχείου
set-executable-and-launched = Έγινε ορισμός του «{ $name }» ως εκτελέσιμου και εκκινήθηκε
mount-error = Αδυναμία πρόσβασης στη μονάδα
grid-view = Προβολή πλέγματος
set-and-launch = Ορισμός και εκκίνηση
removed-from-recents =
Έγινε αφαίρεση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τα { recents }
add-to-sidebar = Προσθήκη στην πλαϊνή στήλη
item-created = Ημερομηνία δημιουργίας: { $created }
network-drive-schemes =
Διαθέσιμα πρωτόκολλα,Πρόθεμα
AppleTalk,afp://
File Transfer Protocol,ftp:// ή ftps://
Network File System,nfs://
Server Message Block,smb://
SSH File Transfer Protocol,sftp:// ή ssh://
WebDAV,dav:// ή davs://
set-executable-and-launch = Ορισμός ως εκτελέσιμο και εκκίνηση
restored =
Έγινε ανάκτηση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τα { trash }
type-to-search-recursive = Κάνει αναζήτηση στον τρέχοντα φάκελο και όλους τους υποφακέλους
progress-paused = { $percent }%, σε παύση
cancelled = Ακυρωμένες
new-folder = Νέος φάκελος...
match-desktop = Συμφωνία με την επιφάνεια εργασίας
operations-running-finished =
Εκτέλεση { $running } { $running ->
[one] διεργασίας
*[other] διεργασιών
} ({ $percent }%), { $finished } ολοκληρωμένες...
sort-by-name = Ταξινόμηση κατά όνομα
edit-history = Ιστορικό επεξεργασιών
show-hidden-files = Εμφάνιση κρυφών αρχείων
progress-failed = { $percent }%, απέτυχε
item-accessed = Ημερομηνία πρόσβασης: { $accessed }
extract-to-title = Αποσυμπίεση σε φάκελο
extracting =
Αποσυμπίεση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από το αρχείο «{ $from }» στον φάκελο «{ $to }» ({ $progress })...
permanently-deleted =
Έγινε οριστική διαγραφή { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
}
complete = Ολοκληρωμένες
write-execute = Εγγραφή και εκτέλεση
desktop-folder-content = Περιεχόμενο φακέλου επιφάνειας εργασίας
renaming = Μετονομασία από «{ $from }» σε «{ $to }»
set-executable-and-launch-description = Θέλετε να ορίσετε το «{ $name }» ως εκτελέσιμο και να το εκκινήσετε;
no-history = Δεν υπάρχουν στοιχεία στο ιστορικό.
emptied-trash = Έγινε άδειασμα του φακέλου «{ trash }»
sort-largest-to-smallest = Από τα μεγαλύτερα στα μικρότερα
restore-from-trash = Ανάκτηση από τα απορρίμματα
moved =
Έγινε μετακίνηση { $items } { $items ->
[one] στοιχείου
*[other] στοιχείων
} από τον φάκελο «{ $from }» στον φάκελο «{ $to }»
progress-cancelled = { $percent }%, ακυρώθηκε
open-in-new-tab = Άνοιγμα σε νέα καρτέλα
unknown-folder = άγνωστος φάκελος
created = Έγινε δημιουργία του «{ $name }» στον φάκελο «{ $parent }»
delete-permanently = Οριστική διαγραφή
write-only = Μόνο εγγραφή
display-settings = Ρυθμίσεις οθόνης...
new-tab = Νέα καρτέλα
failed = Αποτυχημένες
modified = Ημερομηνία τροποποίησης
desktop-appearance = Εμφάνιση επιφάνειας εργασίας...
file-already-exists = Υπάρχει ήδη ένα αρχείο με αυτό το όνομα
permanently-delete-warning = Θα διαγραφούν οριστικά τα εξής: { $target }. Δεν είναι δυνατή η αναίρεση αυτής της ενέργειας.
favorite-path-error-description =
Αδυναμία ανοίγματος του «{ $path }»
Το «{ $path }» ενδέχεται να μην υπάρχει ή να μην έχετε το δικαίωμα να το ανοίξετε
Θέλετε να το αφαιρέσετε από την πλαϊνή στήλη;
empty-trash-warning = Τα στοιχεία του φακέλου «Απορρίμματα» θα διαγραφούν οριστικά
empty-trash-title = Άδειασμα απορριμμάτων;
type-to-search = Πληκτρολόγηση για αναζήτηση
notification-in-progress = Βρίσκονται σε εξέλιξη διεργασίες αρχείων
name-no-slashes = Το όνομα δεν μπορεί να περιέχει καθέτους
replace-title = Το «{ $filename }» υπάρχει ήδη σε αυτήν την τοποθεσία
name-invalid = Το όνομα δεν μπορεί να είναι «{ $filename }»
operations-running =
Εκτέλεση { $running } { $running ->
[one] διεργασίας
*[other] διεργασιών
} ({ $percent }%)...
context-action-confirm-title = Εκτέλεση του «{ $name }»;
pasted-image = Επικολλημένη εικόνα
pasted-text = Επικολλημένο κείμενο
pasted-video = Επικολλημένο βίντεο
show-recents = Φάκελος «Πρόσφατα» στην πλαϊνή στήλη
move-to = Μετακίνηση σε...
copy-path = Αντιγραφή διαδρομής
move-to-title = Επιλογή προορισμού μετακίνησης
selected-items = { $items } επιλεγμένα στοιχεία
context-action = Ενέργεια περιβάλλοντος
copy-to = Αντιγραφή σε...
mixed = Μικτό
type-to-search-select = Επιλέγει την πρώτη αντιστοιχία αρχείου ή φακέλου
clear-recents-history = Απαλοιφή ιστορικού πρόσφατων
context-action-confirm-warning =
Θα εκτελεστεί σε { $items } { $items ->
[one] στοιχείο
*[other] στοιχεία
}.

View file

@ -96,6 +96,7 @@ save-file = Save file
## Open With Dialog
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}
other-apps = Other applications
related-apps = Related applications
@ -116,6 +117,7 @@ permanently-delete-warning = {$target} will be permanently deleted. This action
## Rename Dialog
rename-file = Rename file
rename-folder = Rename folder
rename-confirm = Rename
## Replace Dialog
replace = Replace
@ -138,6 +140,11 @@ open-with = Open with
owner = Owner
group = Group
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
### Mode 0
none = None

View file

@ -405,3 +405,12 @@ removed-from-recents =
[one] kohde
*[other] kohdetta
} viimeaikaisista
mixed = Sekoitettu
context-action = Kontekstitoiminto
context-action-confirm-title = Suoritetaanko "{ $name }"?
context-action-confirm-warning =
Tämä suorittaa { $items } { $items ->
[one] kohteen
*[other] kohdetta
}.
run = Suorita

View file

@ -92,6 +92,7 @@ save-file = Enregistrer fichier
## Open With Dialog
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 }
## Permanently delete Dialog
@ -130,6 +131,11 @@ open-with = Ouvrir avec
owner = Propriétaire
group = Groupe
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
@ -437,3 +443,11 @@ show-recents = Dossier Récents dans la barre latérale
copy-path = Copier le chemin
clear-recents-history = Effacer l'historique des Récents
mixed = Mixte
context-action-confirm-title = Exécuter "{ $name }"?
context-action-confirm-warning =
Cela exécutera sur { $items } { $items ->
[one] élément
*[other] éléments
}.
run = Exécuter
context-action = Action contextuelle

View file

@ -434,3 +434,11 @@ show-recents = Fillteán le déanaí sa bharra taoibh
clear-recents-history = Glan stair na n-earraí le déanaí
copy-path = Cóipeáil an chosán
mixed = Measctha
context-action = Gníomh comhthéacsúil
context-action-confirm-title = Rith "{ $name }"?
context-action-confirm-warning =
Rithfidh sé seo ar { $items } { $items ->
[one] mhír
*[other] míreanna
}.
run = Rith

View file

@ -437,3 +437,11 @@ show-recents = Legutóbbiak mappa megjelenítése az oldalsávban
copy-path = Útvonal másolása
clear-recents-history = Legutóbbiak előzményének törlése
mixed = Vegyes
context-action = Helyi művelet
context-action-confirm-title = Futtatod ezt: „{ $name }”?
context-action-confirm-warning =
Ez a művelet { $items } { $items ->
[one] elemen
*[other] elemen
} fog lefutni.
run = Futtatás

View file

@ -319,3 +319,11 @@ show-recents = Map terbaru di bilah sisi
clear-recents-history = Bersihkan riwayat Terbaru
copy-path = Salin jalur
mixed = Bercampur
context-action = Tindakan konteks
context-action-confirm-title = Jalankan "{ $name }"?
run = Jalankan
context-action-confirm-warning =
Ini akan dijalankan pada { $items } { $items ->
[one] item
*[other] item
}.

View file

@ -319,3 +319,11 @@ show-recents = Бүйір панеліндегі «Жуырдағы құжатт
clear-recents-history = Жуырдағылар тарихын өшіру
copy-path = Орналасқан жолын көшіру
mixed = Аралас
context-action = Контекст әрекеті
context-action-confirm-title = "{ $name }" орындау керек пе?
context-action-confirm-warning =
Бұл { $items } орындалады { $items ->
[one] нәрсеге
*[other] нәрсеге
}.
run = Орындау

View file

@ -277,3 +277,4 @@ copy-to-button-label = 복사
move-to-button-label = 이동
clear-recents-history = 최근 기록 비우기
copy-path = 복사 경로
move-to-title = 이동 위치 선택

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

View file

@ -1,5 +1,5 @@
progress = { $percent }%
cosmic-files = Cosmic Files
cosmic-files = COSMIC Failai
empty-folder = Tuščias aplankas
empty-folder-hidden = Tuščias aplankas (turi paslėptų failų)
no-results = Rezultatų nėra
@ -76,7 +76,7 @@ delete = Ištrinti
permanently-delete-warning = { $target } bus ištrintas visam laikui. Šis veiksmas yra negrįžtamas.
rename-file = Pervadinti failą
rename-folder = Pervadinti aplanką
replace = Pakeisti
replace = Keisti
replace-title = „{ $filename }“ jau egzistuoja šioje vietoje
replace-warning = Ar norite pakeisti tai su tuo, kas yra įrašoma? Keičiant bus pakeistas turinys.
replace-warning-operation = Ar norite pakeisti tai? Pakeičiant bus keičiamas turinys.
@ -244,7 +244,7 @@ item-created = Sukurtas: { $created }
item-modified = Modifikuota: { $modified }
item-accessed = Paskutinė prieiga: { $accessed }
calculating = Skaičiuojama...
settings = Nustatymai
settings = Nuostatos
single-click = Vieno paspaudimo atidarymas
appearance = Išvaizda
match-desktop = Pagal darbalaukio temą
@ -280,18 +280,18 @@ quit = Išeiti
edit = Redaguoti
cut = Iškirpti
copy = Kopijuoti
paste = Įklijuoti
select-all = Pažymėti viską
zoom-in = Priartinti
paste = Įti
select-all = Žymėti viską
zoom-in = Artinti
default-size = Numatytas dydis
zoom-out = Nutolinti
zoom-out = Tolinti
view = Rodymas
grid-view = Tinklelio išdėstymas
list-view = Sąrašo išdėstymas
show-hidden-files = Rodyti paslėptus failus
list-directories-first = Pirmiau pateikti aplankus
gallery-preview = Galerijos peržiūra
menu-settings = Nustatymai...
menu-settings = Nuostatos...
menu-about = Apie COSMIC Files...
sort = Rikiuoti
sort-a-z = A-Ž
@ -302,7 +302,7 @@ sort-smallest-to-largest = Nuo mažiausio iki didžiausio
sort-largest-to-smallest = Nuo didžiausio iki mažiausio
dark = Tamsus
light = Šviesus
comment = COSMIC desktop failų tvarkyklė
comment = COSMIC aplinkos failų tvarkyklė
keywords = Aplankas;Tvarkyklė;
copy-to-title = Pasirinkti kopijavimo vietą
copy-to-button-label = Kopijuoti
@ -317,3 +317,4 @@ clear-recents-history = Išvalyti Neseniai naudotų istoriją
copy-to = Kopijuoti į...
move-to = Perkeltiį į...
copy-path = Kopijuoti kelią
theme = Stilius

View file

@ -438,3 +438,11 @@ show-recents = Ostatnie katalogi w panelu bocznym
clear-recents-history = Wyczyść bierzącą historię
copy-path = Skopiuj ścieżkę
mixed = Mieszane
context-action = Akcja sytuacyjna
context-action-confirm-title = Uruchomić "{ $name }"?
context-action-confirm-warning =
Zostanie uruchomionych { $items } { $items ->
[one] element
*[other] elementów
}.
run = Uruchom

View file

@ -437,3 +437,11 @@ show-recents = Pasta de recentes na barra lateral
clear-recents-history = Limpar histórico de recentes
copy-path = Copiar caminho
mixed = Misto
context-action = Ação de contexto
context-action-confirm-title = Executar "{ $name }"?
context-action-confirm-warning =
Isso será executado em { $items } { $items ->
[one] item
*[other] itens
}.
run = Executar

View file

@ -352,3 +352,5 @@ sort-newest-first = Mais recentes primeiro
sort-oldest-first = Mais antigos primeiro
sort-smallest-to-largest = Do menor para o maior
sort-largest-to-smallest = Do maior para o menor
context-action-confirm-title = Executar "{ $name }"?
run = Executar

View file

@ -381,3 +381,11 @@ show-recents = «Недавние документы» в бок. панели
clear-recents-history = Очистить историю недавних
copy-path = Копировать путь
mixed = Смешанные
context-action = Контекстная команда
context-action-confirm-title = Выполнить «{ $name }»?
context-action-confirm-warning =
Команда затронет { $items } { $items ->
[one] элемент
*[other] элем.
}.
run = Выполнить

View file

@ -0,0 +1,329 @@
open-file = Отвори датотеку
quit = Изађи
cancel = Откажи
open = Отвори
run = Покрени
connect = Повежи
save = Сачувај
password = Лозинка
remove = Уклони
appearance = Изглед
username = Корисничко име
light = Светла
dark = Тамна
settings = Подешавања
replace = Замени
size = Величина
sort-newest-first = Најновије прво
default-app = { $name } (подразумевано)
renamed = Преименована „{ $from }“ у „{ $to }“
read-execute = Читање и извршавање
deleted =
Обрисано је { $items } { $items ->
[one] ставка
*[other] ставки
} из { trash }
item-modified = Измењено: { $modified }
dismiss = Одбаци поруку
list-view = Преглед у виду списака
reload-folder = Поново учитај фасциклу
copy_noun = Копија
favorite-path-error = Грешка при отварању директоријума
progress = { $percent }%
remove-from-sidebar = Уклони из бочне траке
related-apps = Повезани програми
restoring =
Враћање { $items } { $items ->
[one] ставке
*[other] ставки
} из { trash } ({ $progress })...
network-drive-error = Немогуће приступити мрежном уређају
gallery-preview = Преглед галерије
sort-smallest-to-largest = Од најмање до највеће
zoom-in = Увећајте приказ
select-all = Означи све
icon-size-and-spacing = Величина и размак иконица
removing-from-recents =
Уклањање { $items } { $items ->
[one] ставке
*[other] ставки
} из { recents }
cosmic-files = Космик датотеке
type-to-search-enter-path = Уноси путању до директоријума или датотеке
trash = Смеће
emptying-trash = Пражњење { trash } ({ $progress })...
trashed-on = Премештено у смеће
new-window = Нови прозор
zoom-out = Умањите приказ
compressing =
Сажимање { $items } { $items ->
[one] ставке
*[other] ставки
} из „{ $from }“ у „{ $to }“ ({ $progress })...
move-to-trash = Премести у смеће
menu-about = О програму Космик Датотеке...
setting-executable-and-launching = Подешавање „{ $name }“ као извршне датотеке и покретање
open-multiple-files = Отвори више датотека
default-size = Подразумевана величина
menu-open-with = Отвори програмом...
extracted =
Извлачено { $items } { $items ->
[one] ставка
*[other] ставки
} из „{ $from }“ у „{ $to }“
create-new-folder = Направи нову фасциклу
original-file = Изворна датотека
create = Направи
create-archive = Направи архиву
read-write-execute = Читање, уписивање и извршавање
other-apps = Остали програми
set-permissions = Овлашћења за „{ $name }“ су постављена на { $mode }
pause = Паузирај
calculating = Израчунавам…
sort-by-size = Поређај по величини
rename = Преименуј...
empty-folder-hidden = Празна фасцикла (има сакривених ставки)
keep = Задржи
item-size = Величина: { $size }
permanently-deleting =
Трајно брисање { $items } { $items ->
[one] ставке
*[other] ставки
}
edit = Уреди
connecting = Повезујем се...
read-write = Читање и уписивање
copy = Умножи
none = Ништа
items = Ставки: { $items }
no-results = Нису пронађени резултати
theme = Тема
type = Врста: { $mime }
compressed =
Сажето { $items } { $items ->
[one] ставка
*[other] ставки
} из „{ $from }“ у „{ $to }“
replace-warning = Да ли желите да га замените оним који снимате? Замена ће преписати његов садржај.
rename-folder = Преименуј фасциклу
new-file = Нова датотека...
close-tab = Затвори језичак
name = Назив
open-in-terminal = Отвори у терминалу
resume = Настави
open-multiple-folders = Отвори више фасцикла
remember-password = Запамти лозинку
show-details = Прикажи детаље
grid-spacing = Размак мреже
extract-to = Распакуј у...
add-network-drive = Додај мрежни уређај
copying =
Умножавање { $items } { $items ->
[one] ставке
*[other] ставки
} из „{ $from }“ у „{ $to }“ ({ $progress })...
delete = Обриши
sort-oldest-first = Најстарије прво
repository = Ризница
create-new-file = Направи нову датотеку
sort-by-trashed = Поређај по времену брисања
replace-warning-operation = Да ли желите да га замените? Замена ће преписати његов садржај.
support = Подршка
try-again = Покушај поново
eject = Избаци
copied =
Умножено { $items } { $items ->
[one] ставке
*[other] ставки
} из „{ $from }“ у „{ $to }“
other = Друго
open-in-new-window = Отвори у новом прозору
empty-folder = Празна фасцикла
sort-by-modified = Поређај по датуму измене
list-directories-first = Прикажи директоријуме прво
read-only = Само за читање
folder-name = Назив фасцикле
browse-store = Разгледај { $store }
enter-server-address = Унесите адресу сервера
remove-from-recents = Уклони из недавних
connect-anonymously = Повежи се анонимно
group = Група
apply-to-all = Примени на све
skip = Прескочи
paste = Залепи
menu-settings = Подешавања...
moving =
Премештање { $items } { $items ->
[one] датотеке
*[other] датотека
} из „{ $from }“ у „{ $to }“ ({ $progress })...
replace-with = Замени са
recents = Недавно
change-wallpaper = Промени позадину...
network-drive-description =
Адресе сервера укључују префикс протокола и адресу.
Примери: ssh://192.168.0.1, ftp://[2001:db8::1]
deleting =
Брише се { $items } { $items ->
[one] ставка
*[other] ставки
} из { trash } ({ $progress })...
single-click = Један клик за отварање
view = Преглед
undo = Опозови
setting-permissions = Подешавање овлашћења за „{ $name }“ на { $mode }
owner = Власник
creating = Правим „{ $name }“ у „{ $parent }“
execute-only = Само извршавање
open-item-location = Отвори локацију ставке
details = Детаљи
set-executable-and-launched = Постављено „{ $name }“ као извршну датотеку и покренуто
mounted-drives = Прикључени дискови
sort-a-z = А
mount-error = Немогуће приступити уређају
extract-here = Извуци
grid-view = Преглед у виду мреже
filesystem = Систем датотека
set-and-launch = Подеси и покрени
removed-from-recents =
Уклоњено { $items } { $items ->
[one] ставке
*[other] ставки
} из { recents }
add-to-sidebar = Додај у страничник
item-created = Направљено: { $created }
network-drive-schemes =
Доступни протоколи,Префикс
ЕплТок,afp://
Протокол за пренос датотека,ftp:// или ftps://
Мрежни систем датотека,nfs://
Серверски блок порука,smb://
SSH протокол за пренос датотека,sftp:// или ssh://
ВебДАВ,dav:// или davs://
home = Лична
set-executable-and-launch = Постави као извршну и покрени
restored =
Враћено { $items } { $items ->
[one] ставка
*[other] ставки
} из { trash }
sort-z-a = Ш-А
type-to-search-recursive = Претражује тренутну фасциклу и све подфасцикле
history = Историјат
progress-paused = { $percent }%, паузирано
desktop-view-options = Могућности приказа радне површине...
show-on-desktop = Прикажи на радној површини
cancelled = Отказано
new-folder = Нова фасцикла...
match-desktop = Прати радну површину
domain = Домен
operations-running-finished =
{ $running } { $running ->
[one] радња покренута
*[other] радње покренуте
} ({ $percent }%), { $finished } завршено...
sort-by-name = Поређај по називу
edit-history = Историјат уређивања
sort = Поређај
show-hidden-files = Прикажи скривене датотеке
progress-failed = { $percent }%, није успело
trash-folder-icon = Иконица фасцикле Смеће
item-accessed = Приступљено: { $accessed }
extract-to-title = Распакуј у фасциклу
open-with = Отвори помоћу
keep-both = Задржи оба
icon-size = Величина иконице
open-with-title = Како желите да отворите „{ $name }“?
extracting =
Извлачење { $items } { $items ->
[one] ставке
*[other] ставки
} из „{ $from }“ у „{ $to }“ ({ $progress })...
permanently-deleted =
Трајно обрисано { $items } { $items ->
[one] ставке
*[other] ставки
}
complete = Завршено
write-execute = Писање и извршавање
extract-password-required = Потребна је лозинка
pending = У току
desktop-folder-content = Садржај фасцикле радне површине
renaming = Преименовање „{ $from }“ у „{ $to }“
set-executable-and-launch-description = Да ли желите да поставите „{ $name }“ као извршну и покренете је?
no-history = Нема ставки у историјату.
open-folder = Отвори фасциклу
emptied-trash = Опражњено { trash }
rename-file = Преименуј датотеку
sort-largest-to-smallest = Од највеће до најмање
restore-from-trash = Врати из смећа
cut = Исеци
moved =
Премештено { $items } { $items ->
[one] датотеке
*[other] датотека
} из „{ $from }“ у „{ $to }“
progress-cancelled = { $percent }%, отказано
open-in-new-tab = Отвори у новом језичку
unknown-folder = непозната фасцикла
file = Датотека
file-name = Назив датотеке
save-file = Сачувај датотеку
created = Направљено „{ $name }“ у „{ $parent }“
delete-permanently = Трајно обриши
networks = Мреже
write-only = Само писање
today = Данас
display-settings = Подешавања екрана...
new-tab = Нови језичак
failed = Неуспешно
modified = Измењено
desktop-appearance = Изглед радне површине...
file-already-exists = Датотека са овим називом већ постоји
name-hidden = Називи који почињу тачком „.“ ће бити сакривени
folder-already-exists = Фасцикла са овим називом већ постоји
permanently-delete-warning = { $target } ће бити трајно обрисано. Ова радња се не може поништити.
favorite-path-error-description =
Не можемо да отворимо „{ $path }“
„{ $path }“ можда не постоји или немате дозволу за његово отварање
Желите ли да га уклоните из бочне површи?
empty-trash-warning = Ставке у смећу биће трајно обрисане
empty-trash = Испразни смеће
empty-trash-title = Испразнити смеће?
type-to-search = Куцајте за претрагу
notification-in-progress = Радње са датотекама су у току
name-no-slashes = Назив не може садржати косе црте
permanently-delete-question = Трајно обриши?
replace-title = „{ $filename }“ већ постоји на овој локацији
name-invalid = Назив не може бити „{ $filename }“
operations-running =
{ $running } { $running ->
[one] радња покренута
*[other] радње покренуте
} ({ $percent }%)...
comment = Управник датотека за Космик радну површину
keywords = Folder;Manager;Фасцикла;Управник;fascikla;upravnik;
copy-to-title = Изабери одредиште умножавања
copy-to-button-label = Умножи
move-to-title = Изабери одредиште премештања
move-to-button-label = Помери
context-action = Контекстна радња
context-action-confirm-title = Покрени „{ $name }“?
context-action-confirm-warning =
Ово ће се извршити на { $items } { $items ->
[one] ставку
*[other] ставки
}.
selected-items = Изабраних { $items } ставки
mixed = Помешано
pasted-image = Убачена слика
pasted-text = Убачен текст
pasted-video = Убачен видео
show-recents = Недавна фасцикла у бочној површи
type-to-search-select = Обира прву подударајућу датотеку или фасциклу
clear-recents-history = Очисти историју недавних
compress = Сажми...
copy-to = Умножи у...
move-to = Помери у...
copy-path = Умножи путању

View file

@ -410,3 +410,11 @@ move-to = Flytta till...
show-recents = Mapp för senast använda filer i sidofältet
clear-recents-history = Töm historik för Senaste
copy-path = Kopiera sökväg
context-action = Kontextåtgärd
context-action-confirm-title = Kör "{ $name }"?
context-action-confirm-warning =
Detta kommer att köras på { $items } { $items ->
[one] objekt
*[other] objekt
}.
run = Kör

View file

@ -5,7 +5,7 @@ filesystem = Файлова система
home = Домівка
trash = Смітник
recents = Нещодавні
undo = Відмінити
undo = Скасувати
# List view
name = Назва
modified = Змінено
@ -86,6 +86,7 @@ copying =
copied =
Скопійовано { $items } { $items ->
[one] елемент
[few] елементи
*[other] елеменів
} з «{ $from }» в «{ $to }»
emptying-trash = Спорожнення { trash } ({ $progress })...
@ -98,7 +99,8 @@ moving =
moved =
Переміщено { $items } { $items ->
[one] елемент
*[other] елементи
[few] елементи
*[other] елементів
} з «{ $from }» в «{ $to }»
renaming = Перейменування «{ $from }» на «{ $to }»
renamed = Перейменовано «{ $from }» на «{ $to }»
@ -110,7 +112,8 @@ restoring =
restored =
Відновлено { $items } { $items ->
[one] елемент
*[other] елементи
[few] елементи
*[other] елементів
} з { trash }
unknown-folder = невідома тека
@ -223,7 +226,7 @@ permanently-delete-question = Остаточно видалити?
delete = Видалити
permanently-delete-warning = { $target } буде остаточно видалено. Цю дію не можна скасувати.
set-executable-and-launch = Зробити виконуваним і запустити
set-executable-and-launch-description = Бажаєте зробити "{ $name }" виконуваним і запустити його?
set-executable-and-launch-description = Бажаєте зробити «{ $name }» виконуваним і запустити його?
set-and-launch = Зробити і запустити
open-with = Відкрити за допомогою
owner = Власник
@ -245,7 +248,7 @@ favorite-path-error-description =
Вилучити з бічної панелі?
keep = Залишити
add-network-drive = Додати мережевий диск
connect = З'єднати
connect = Зєднати
connect-anonymously = З'єднатись анонімно
connecting = З'єднання…
domain = Домен
@ -274,12 +277,13 @@ compressing =
Стиснення { $items } { $items ->
[one] елемента
*[other] елементів
} з "{ $from }" до "{ $to }" ({ $progress })...
} з «{ $from }» до «{ $to }» ({ $progress })...
compressed =
Стиснуто { $items } { $items ->
[one] елемент
*[other] елементи
} з "{ $from }" до "{ $to }"
[few] елементи
*[other] елементів
} з «{ $from }» до «{ $to }»
deleting =
Видалення { $items } { $items ->
[one] елемента
@ -288,6 +292,7 @@ deleting =
deleted =
Видалено { $items } { $items ->
[one] елемент
[few] елементи
*[other] елементи
} з { trash }
extracting =
@ -298,7 +303,8 @@ extracting =
extracted =
Видобуто { $items } { $items ->
[one] елемент
*[other] елементи
[few] елементи
*[other] елементів
} з «{ $from }» в «{ $to }»
setting-executable-and-launching = Надання «{ $name }» прав на виконання та запуск
set-executable-and-launched = «{ $name }» надано права на виконання і відкрито
@ -343,7 +349,8 @@ permanently-deleting =
permanently-deleted =
Остаточно вилучено { $items } { $items ->
[one] елемент
*[other] елементи
[few] елементи
*[other] елементів
}
removing-from-recents =
Вилучення { $items } { $items ->
@ -353,13 +360,14 @@ removing-from-recents =
removed-from-recents =
Вилучено { $items } { $items ->
[one] елемент
*[other] елементи
[few] елементи
*[other] елементів
} з { recents }
empty-trash-title = Спорожити смітник?
type-to-search-select = Вибирає перший відповідний файл або папку
pasted-image = Вставлене Зображення
pasted-text = Вставлений Текст
pasted-video = Вставлене Видиво
pasted-image = Вставлене зображення
pasted-text = Вставлений текст
pasted-video = Вставлене відео
copy-to-button-label = Копіювати
move-to-button-label = Перемістити
copy-to = Копіювати до…
@ -371,3 +379,11 @@ keywords = Тека;Папка;Провідник;Менеджер;Катало
show-recents = Тека «Нещодавні» на бічній панелі
copy-path = Копіювати шлях
clear-recents-history = Очистити нещодавні
context-action-confirm-title = Запустити «{ $name }»?
run = Виконати
context-action-confirm-warning =
Запуститься для { $items } { $items ->
[one] елемента
*[other] елементів
}.
context-action = Контекстна дія

View file

@ -167,7 +167,7 @@ read-write-execute = 读取、写入和执行
## Favorite Path Error Dialog
favorite-path-error = 打开路径时出错
favorite-path-error = 打开目录时出错
favorite-path-error-description =
无法打开 "{ $path }" 。
"{ $path }" 可能不存在或您没有权限打开它。
@ -348,7 +348,7 @@ light = 亮色模式
type-to-search = 输入即可搜索
type-to-search-recursive = 搜索当前文件夹及其所有子文件夹
type-to-search-enter-path = 输入文件夹或文件路径
type-to-search-enter-path = 输入文件夹或文件目录
# Context menu
add-to-sidebar = 加入侧边栏
compress = 压缩…
@ -437,3 +437,11 @@ clear-recents-history = 清除最近访问历史
copy-path = 复制文件路径
show-recents = 侧边栏中的最近访问
mixed = 混合
context-action-confirm-title = 运行 “{ $name }”
run = 运行
context-action-confirm-warning =
该行动将会在 { $items } { $items ->
[one] 项目
*[other] 项目
} 上运行。
context-action = 环境行动

View file

@ -42,7 +42,7 @@ name-no-slashes = 名稱不能包含斜線
## Open/Save Dialog
cancel = 取消
create = 創作
create = 建立
open = 開啟
open-file = 開啟檔案
open-folder = 開啟資料夾
@ -358,7 +358,7 @@ calculating = 計算中...
single-click = 點按以開啟
type-to-search = 輸入進行搜尋
type-to-search-recursive = 搜尋目前資料夾及全部子資料夾
type-to-search-enter-path = 輸入目錄或檔案的路徑
type-to-search-enter-path = 輸入目錄或檔案的目錄
delete-permanently = 永久刪除
eject = 彈出
remove-from-recents = 從最近項目中移除
@ -384,3 +384,11 @@ set-executable-and-launched = 設定「{ $name }」為可以執行並已經啟
setting-permissions = 設定「{ $name }」的權限至 { $mode }
set-permissions = 設定「{ $name }」的權限至 { $mode }
mixed = 混合
context-action = 環境行動
context-action-confirm-title = 執行「{ $name }」嗎?
context-action-confirm-warning =
該行動將會在 { $items } { $items ->
[one] 項目
*[other] 項目
} 上執行。
run = 執行

3
rust-toolchain.toml Normal file
View file

@ -0,0 +1,3 @@
[toolchain]
channel = "1.93.0"
components = ["clippy", "rustfmt"]

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
imports_granularity = "Module"

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,14 @@
use crate::{
mime_icon::mime_for_path,
operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk},
};
use crate::mime_icon::mime_for_path;
use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk};
use cosmic::iced::futures;
use jiff::{Zoned, civil::DateTime, tz::TimeZone};
use std::{
collections::HashSet,
fs,
io::{self, Read, Write},
path::{Path, PathBuf},
time::SystemTime,
};
use jiff::Zoned;
use jiff::civil::DateTime;
use jiff::tz::TimeZone;
use std::collections::HashSet;
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use zip::result::ZipError;
pub const SUPPORTED_ARCHIVE_TYPES: &[&str] = &[
@ -112,7 +110,8 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
password: Option<&str>,
controller: Controller,
) -> zip::result::ZipResult<()> {
use std::{ffi::OsString, fs};
use std::ffi::OsString;
use std::fs;
use zip::result::ZipError;
fn make_writable_dir_all<T: AsRef<Path>>(

View file

@ -1,13 +1,9 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::{
collections::VecDeque,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
/// Create a channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque.
pub fn channel<Message>() -> (Sender<Message>, Receiver<Message>) {

View file

@ -2,12 +2,10 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use std::{
borrow::Cow,
error::Error,
path::{Path, PathBuf},
str,
};
use std::borrow::Cow;
use std::error::Error;
use std::path::{Path, PathBuf};
use std::str;
use url::Url;
#[derive(Clone, Copy, Debug)]
@ -132,9 +130,11 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
match mime.as_str() {
"text/uri-list" => {
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)?;
match url.to_file_path() {
Ok(path) => paths.push(path),

View file

@ -1,20 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-only
use std::{any::TypeId, num::NonZeroU16, path::PathBuf};
use std::any::TypeId;
use std::num::NonZeroU16;
use std::path::PathBuf;
use cosmic::{
Application,
cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry},
iced::Subscription,
theme,
};
use cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry;
use cosmic::cosmic_config::{self, CosmicConfigEntry};
use cosmic::iced::Subscription;
use cosmic::{Application, theme};
use serde::{Deserialize, Serialize};
use crate::{
FxOrderMap,
app::App,
tab::{HeadingOptions, Location, View},
};
use crate::FxOrderMap;
use crate::app::App;
use crate::tab::{HeadingOptions, Location, View};
pub use crate::context_action::{ContextActionPreset, ContextActionSelection};
@ -172,6 +170,11 @@ pub struct Config {
pub show_details: bool,
pub show_recents: bool,
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,
}
@ -236,11 +239,97 @@ impl Default for Config {
show_details: false,
show_recents: true,
tab: TabConfig::default(),
toolbar: default_toolbar(),
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)]
#[serde(default)]
pub struct DesktopConfig {

View file

@ -4,7 +4,8 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{mime_app, spawn_detached::spawn_detached};
use crate::mime_app;
use crate::spawn_detached::spawn_detached;
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum ContextActionSelection {
@ -46,7 +47,7 @@ impl ContextActionPreset {
}
for step in &self.steps {
let Some(commands) = mime_app::exec_to_command(step, paths) else {
let Some(commands) = mime_app::exec_to_command(step, &self.name, None, paths) else {
log::warn!(
"failed to parse context action {:?}: invalid Exec {:?}",
self.name,

View file

@ -1,58 +1,46 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
Application, ApplicationExt, Element,
app::{Core, Task, context_drawer, cosmic::Cosmic},
cosmic_config, cosmic_theme, executor,
iced::core::widget::operation,
iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper},
iced::widget::scrollable::AbsoluteOffset,
iced::{
self, Alignment, Event, Length, Size, Subscription,
core::SmolStr,
event,
futures::{self, SinkExt},
keyboard::{Event as KeyEvent, Key, Modifiers, key::Named},
mouse, stream,
widget::scrollable,
window,
},
theme,
widget::{
self, Operation,
menu::{Action as MenuAction, KeyBind, key_bind::Modifier},
segmented_button,
},
use cosmic::app::cosmic::Cosmic;
use cosmic::app::{Core, Task, context_drawer};
use cosmic::iced::core::SmolStr;
use cosmic::iced::core::widget::operation;
use cosmic::iced::futures::{self, SinkExt};
use cosmic::iced::keyboard::key::Named;
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
use cosmic::iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper};
use cosmic::iced::widget::scrollable;
use cosmic::iced::widget::scrollable::AbsoluteOffset;
use cosmic::iced::{
self, Alignment, Event, Length, Size, Subscription, event, mouse, stream, window,
};
use cosmic::widget::menu::key_bind::Modifier;
use cosmic::widget::menu::{Action as MenuAction, KeyBind};
use cosmic::widget::{self, Operation, segmented_button};
use cosmic::{Application, ApplicationExt, Element, cosmic_config, cosmic_theme, executor, theme};
use mime_guess::{Mime, mime};
use notify_debouncer_full::{
DebouncedEvent, Debouncer, RecommendedCache, new_debouncer,
notify::{self, RecommendedWatcher},
};
use notify_debouncer_full::notify::{self, RecommendedWatcher};
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
use recently_used_xbel::update_recently_used;
use rustc_hash::{FxHashMap, FxHashSet};
use std::{
any::TypeId,
collections::{HashMap, VecDeque},
env, fmt, fs,
path::PathBuf,
time::{self, Instant},
};
use std::any::TypeId;
use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::time::{self, Instant};
use std::{env, fmt, fs};
use crate::{
app::{
use crate::app::{
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
},
config::{Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch},
fl, home_dir,
key_bind::key_binds,
localize::LANGUAGE_SORTER,
menu,
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
tab::{self, ItemMetadata, Location, SearchLocation, Tab},
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
};
use crate::config::{
Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch,
};
use crate::key_bind::key_binds;
use crate::localize::LANGUAGE_SORTER;
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab};
use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};
use crate::{fl, home_dir, menu};
#[derive(Clone, Debug)]
pub struct DialogMessage(cosmic::Action<Message>);
@ -487,7 +475,7 @@ enum Message {
TabMessage(tab::Message),
TabRescan(
Location,
Option<tab::Item>,
Option<Box<tab::Item>>,
Vec<tab::Item>,
Option<Vec<PathBuf>>,
),
@ -587,7 +575,7 @@ impl App {
space_s,
space_l,
..
} = theme::active().cosmic().spacing;
} = theme::spacing();
let is_condensed = self.core().is_condensed();
let mut col = widget::column::with_capacity(2).spacing(space_xxs);
@ -744,11 +732,17 @@ impl App {
fn rescan_tab(&self, selection_paths: Option<Vec<PathBuf>>) -> Task<Message> {
let location = self.tab.location.clone();
let icon_sizes = self.tab.config.icon_sizes;
#[cfg(feature = "gvfs")]
let mounter_items = self.mounter_items.clone();
Task::future(async move {
let location2 = location.clone();
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")]
{
let mounter_paths: Box<[_]> = mounter_items
@ -837,9 +831,10 @@ impl App {
fn update_config(&mut self) -> Task<Message> {
self.core.window.show_context = self.flags.config.dialog.show_details;
self.tab.config = self.flags.config.dialog_tab();
let config = self.flags.config.dialog_tab();
self.tab.config.view = config.view;
self.update_nav_model();
self.update(Message::TabMessage(tab::Message::Config(self.tab.config)))
self.update(Message::TabMessage(tab::Message::Config(config)))
}
fn with_dialog_config<F: Fn(&mut DialogConfig)>(&mut self, f: F) -> Task<Message> {
@ -902,6 +897,8 @@ impl App {
if let Some(path) = favorite.path_opt() {
let name = if matches!(favorite, Favorite::Home) {
fl!("home")
} else if let Favorite::Network { name, .. } = favorite {
name.clone()
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
file_name.to_string()
} else {
@ -1125,7 +1122,7 @@ impl Application for App {
}
fn dialog(&self) -> Option<Element<'_, Message>> {
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();
//TODO: should gallery view just be a dialog?
if self.tab.gallery {
@ -1403,7 +1400,10 @@ impl Application for App {
Message::Config(config) => {
if config != self.flags.config {
log::info!("update config");
// Don't overwrite military time
let military_time = self.flags.config.tab.military_time;
self.flags.config = config;
self.flags.config.tab.military_time = military_time;
return self.update_config();
}
}
@ -1458,14 +1458,14 @@ impl Application for App {
}
Message::Key(modifiers, key, text) => {
for (key_bind, action) in &self.key_binds {
if key_bind.matches(modifiers, &key) {
if key_bind.matches(modifiers, &key, None) {
return self.update(Message::from(action.message()));
}
}
// Check key binds from accept label
if let Some(key_bind) = &self.accept_label.key_bind_opt
&& key_bind.matches(modifiers, &key)
&& key_bind.matches(modifiers, &key, None)
{
return self.update(if self.flags.kind.save() {
Message::Save(false)
@ -2017,7 +2017,7 @@ impl Application for App {
/// Creates a view after each update.
fn view(&self) -> Element<'_, Message> {
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();
let mut col = widget::column::with_capacity(2);

View file

@ -1,11 +1,10 @@
use cosmic::{
iced::core::keyboard::key::Named,
iced::keyboard::Key,
widget::menu::key_bind::{KeyBind, Modifier},
};
use cosmic::iced::core::keyboard::key::Named;
use cosmic::iced::keyboard::Key;
use cosmic::widget::menu::key_bind::{KeyBind, Modifier};
use std::collections::HashMap;
use crate::{app::Action, tab};
use crate::app::Action;
use crate::tab;
//TODO: load from config
pub fn key_binds(mode: &tab::Mode) -> HashMap<KeyBind, Action> {

View file

@ -1,9 +1,7 @@
use cosmic::widget;
use image::ImageReader;
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
/// Bytes per pixel in RGBA format (Red, Green, Blue, Alpha = 4 bytes)
pub const RGBA_BYTES_PER_PIXEL: u64 = 4;

View file

@ -1,15 +1,16 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{app::Settings, iced::Limits};
use std::{env, fs, path::PathBuf, process};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use cosmic::app::Settings;
use cosmic::iced::Limits;
use std::path::PathBuf;
use std::{env, fs, process};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use crate::{
app::{App, Flags},
config::{Config, State},
tab::Location,
};
use crate::app::{App, Flags};
use crate::config::{Config, State};
use crate::tab::Location;
pub mod app;
mod archive;
@ -37,6 +38,7 @@ mod zoom;
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 {
err.to_string()
}

View file

@ -1,11 +1,10 @@
use cosmic::iced::{core as iced_core, widget as iced_widget};
use iced_core::event::Event;
use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::{Operation, Tree};
use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget};
use iced_core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay,
renderer,
};
pub fn loaded_image<'a, Message: 'static, Theme>(
handle: <cosmic::Renderer as iced_core::image::Renderer>::Handle,

View file

@ -1,13 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only
use i18n_embed::{
DefaultLocalizer, LanguageLoader, Localizer,
fluent::{FluentLanguageLoader, fluent_language_loader},
};
use icu::collator::{
Collator, CollatorBorrowed, CollatorPreferences, options::CollatorOptions,
preferences::CollationNumericOrdering,
};
use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};
use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};
use icu::collator::options::CollatorOptions;
use icu::collator::preferences::CollationNumericOrdering;
use icu::collator::{Collator, CollatorBorrowed, CollatorPreferences};
use icu::locale::Locale;
use rust_embed::RustEmbed;
use std::sync::LazyLock;

View file

@ -1,30 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
Element,
app::Core,
iced::{
Alignment, Background, Border, Length, advanced::widget::text::Style as TextStyle,
keyboard::Modifiers,
},
theme,
widget::{
self, Row, button, column, container, divider,
menu::{self, ItemHeight, ItemWidth, MenuBar, key_bind::KeyBind},
responsive_menu_bar, space, text,
},
use cosmic::app::Core;
use cosmic::iced::advanced::widget::text::Style as TextStyle;
use cosmic::iced::keyboard::Modifiers;
use cosmic::iced::{Alignment, Background, Border, Length};
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};
use cosmic::widget::{
self, Row, button, column, container, divider, responsive_menu_bar, space, text,
};
use cosmic::{Element, theme};
#[cfg(feature = "desktop")]
use i18n_embed::LanguageLoader;
use mime_guess::Mime;
use std::{collections::HashMap, sync::LazyLock};
use std::collections::HashMap;
use std::sync::LazyLock;
use crate::{
app::{Action, Message},
config::{Config, ContextActionPreset},
fl,
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
trash::{Trash, TrashExt},
};
use crate::app::{Action, Message};
use crate::config::{Config, ContextActionPreset};
use crate::fl;
use crate::tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab};
use crate::trash::{Trash, TrashExt};
static MENU_ID: LazyLock<cosmic::widget::Id> =
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
@ -38,7 +34,7 @@ macro_rules! menu_button {
.height(Length::Fixed(24.0))
.align_y(Alignment::Center)
)
.padding([theme::active().cosmic().spacing.space_xxs, 16])
.padding([theme::spacing().space_xxs, 16])
.width(Length::Fill)
.class(theme::Button::MenuItem)
);
@ -143,13 +139,12 @@ pub fn context_menu<'a>(
Some(Location::Trash) | Some(Location::Search(SearchLocation::Trash, ..)) => {
selected_trash_only = true
}
Some(Location::Path(path)) => {
Some(Location::Path(path))
if selected == 1
&& path.extension().and_then(|s| s.to_str()) == Some("desktop")
&& path.extension().and_then(|s| s.to_str()) == Some("desktop") =>
{
selected_desktop_entry = Some(&**path);
}
}
_ => (),
}
selected_types.push(item.mime.clone());
@ -196,11 +191,11 @@ pub fn context_menu<'a>(
if !Trash::is_empty() {
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());
#[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(),
));
}
@ -579,7 +574,7 @@ pub fn dialog_menu(
])
.item_height(ItemHeight::Dynamic(40))
.item_width(ItemWidth::Uniform(360))
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
.spacing(theme::spacing().space_xxxs.into())
.into()
}
@ -634,7 +629,7 @@ pub fn menu_bar<'a>(
responsive_menu_bar()
.item_height(ItemHeight::Dynamic(40))
.item_width(ItemWidth::Uniform(360))
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
.spacing(theme::spacing().space_xxxs.into())
.into_element(
core,
key_binds,

View file

@ -1,155 +1,123 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use bstr::{BString, ByteSlice, ByteVec};
#[cfg(feature = "desktop")]
use cosmic::desktop;
use cosmic::widget;
pub use mime_guess::Mime;
use rustc_hash::FxHashMap;
#[cfg(feature = "desktop")]
use std::{cmp::Ordering, fs, io, time::Instant};
use std::{
cmp::Ordering,
ffi::OsStr,
fs, io,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
process,
time::Instant,
};
// Supported exec key field codes
const EXEC_HANDLERS: [&str; 4] = ["%f", "%F", "%u", "%U"];
// Deprecated field codes. The spec advises to ignore these handlers.
const DEPRECATED_HANDLERS: [&str; 6] = ["%d", "%D", "%n", "%N", "%v", "%m"];
pub fn exec_to_command(
exec: &str,
entry_name: &str,
entry_path: Option<&Path>,
path_opt: &[impl AsRef<OsStr>],
) -> Option<Vec<process::Command>> {
let args_vec = shlex::split(exec)?;
let program = args_vec.first()?;
// Skip program to make indexing easier
let args_vec = &args_vec[1..];
let arguments = shlex::split(exec)?;
// Base Command instance(s)
// 1. We may need to launch multiple of the same process.
// 2. Each of those processes will need to be passed args from exec.
// 3. Each of those args may appear in any order.
// 4. Arg order should be preserved.
//
// So, we'll go through exec in two passes. The first pass handles paths (%f etc) and args up
// to the field code followed by the second which passes extra, non-% args to each processes.
//
// While it'd be marginally faster to process everything in one pass, that's problematic:
// 1. path_opt may need to be cloned because it may be moved on each iteration (borrowck
// doesn't know we'll only use it once)
// 2. We have to keep track of which modifier (%f etc) we've used/seen already
// 3. We have to keep track of which processes received non-modifier args which gets messy fast
// 4. `exec` is likely small so looping over it twice is not a big deal
let field_code_pos = args_vec
if arguments.is_empty() {
tracing::error!("command does not contain any arguments");
return None;
}
let mut commands = Vec::new();
let paths = path_opt
.iter()
.position(|arg| EXEC_HANDLERS.contains(&arg.as_str()));
let args_handler = field_code_pos.and_then(|i| args_vec.get(i));
// msrv
// .inspect(|handler| log::trace!("Found paths handler: {handler} for exec: {exec}"));
// Number of args before the field code.
// This won't be an off by one err below because take is not zero indexed.
let field_code_pos = field_code_pos.unwrap_or_default();
let mut processes = match args_handler.map(String::as_str) {
Some("%f") => {
let mut processes = Vec::with_capacity(path_opt.len());
.map(AsRef::as_ref)
.map(Some)
// Add a single `None` if no path was given.
.chain(std::iter::repeat_n(
None,
if path_opt.is_empty() { 1 } else { 0 },
));
for path in paths {
let mut batch_process = false;
let mut args = Vec::with_capacity(arguments.len());
let mut field_code_used = false;
for argument in arguments.iter().skip(1) {
let mut new_argument = BString::new(Vec::with_capacity(argument.capacity()));
let mut chars = argument.chars();
while let Some(char) = chars.next() {
// https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html
if char == '%' {
match chars.next() {
Some('%') => new_argument.push_char(char),
Some('c') => new_argument.push_str(entry_name),
Some('k') => {
if let Some(path) = entry_path {
new_argument.push_str(path.as_os_str().as_bytes());
}
}
// %f and %u behave the same in a file manager.
Some('f' | 'u') => {
if let Some(path) = path
&& !field_code_used
{
// TODO: files on remote file systems should be copied to a temporary local file.
batch_process = true;
field_code_used = true;
new_argument.push_str(path.as_bytes());
}
}
// %F and %U behave the same in a file manager.
Some('F') | Some('U') => {
if !field_code_used && new_argument.is_empty() {
field_code_used = true;
for path in path_opt.iter().map(AsRef::as_ref) {
// TODO: %f and %F need to handle non-file URLs (see spec)
if from_file_or_dir(path).is_none() {
log::warn!("Desktop file expects a file path instead of a URL: {path:?}");
args.push(BString::new(path.as_bytes().to_owned()));
}
}
}
// Passing multiple paths to %f should open an instance per path
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(AsRef::as_ref)
.take(field_code_pos)
.chain(std::iter::once(path)),
);
processes.push(process);
_ => (),
}
} else {
new_argument.push_char(char);
}
}
processes
if !new_argument.is_empty() {
args.push(new_argument);
}
Some("%F") => {
// TODO: %f and %F need to handle non-file URLs (see spec)
for invalid in path_opt
.iter()
.map(AsRef::as_ref)
.filter(|&path| from_file_or_dir(path).is_none())
{
log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}");
}
// Launch one instance with all args
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(OsStr::new)
.take(field_code_pos)
.chain(path_opt.iter().map(AsRef::as_ref)),
);
let mut command = process::Command::new(&arguments[0]);
vec![process]
for arg in args {
match arg.to_os_str() {
Ok(arg) => {
command.arg(arg);
}
Some("%u") => path_opt
.iter()
.map(|path| {
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(OsStr::new)
.take(field_code_pos)
.chain(std::iter::once(path.as_ref())),
);
process
})
.collect(),
Some("%U") => {
let mut process = process::Command::new(program);
process.args(
args_vec
.iter()
.map(OsStr::new)
.take(field_code_pos)
.chain(path_opt.iter().map(AsRef::as_ref)),
);
vec![process]
}
Some(invalid) => unreachable!("All valid variants were checked; got: {invalid}"),
None => vec![process::Command::new(program)],
};
// Pass 2: Add remaining arguments that are not % to each process
for arg in args_vec.iter().skip(field_code_pos) {
match arg.as_str() {
// Consume path field codes or fail on codes we don't handle yet
field_code if arg.starts_with('%') => {
if !EXEC_HANDLERS.contains(&field_code)
&& !DEPRECATED_HANDLERS.contains(&field_code)
{
log::warn!("unsupported Exec code {field_code:?} in {exec:?}");
Err(_) => {
tracing::error!("invalid string encoding in command");
return None;
}
}
arg => {
for process in &mut processes {
process.arg(arg);
}
}
commands.push(command);
if !batch_process {
break;
}
}
#[cfg(debug_assertions)]
for command in &processes {
for command in &commands {
log::debug!(
"Parsed program {} with args: {:?}",
command.get_program().to_string_lossy(),
@ -157,13 +125,7 @@ pub fn exec_to_command(
);
}
Some(processes)
}
fn from_file_or_dir(path: impl AsRef<Path>) -> Option<url::Url> {
url::Url::from_file_path(&path)
.ok()
.or_else(|| url::Url::from_directory_path(&path).ok())
Some(commands)
}
#[derive(Clone, Debug)]
@ -179,7 +141,12 @@ pub struct MimeApp {
impl MimeApp {
//TODO: move to libcosmic, support multiple files
pub fn command<O: AsRef<OsStr>>(&self, path_opt: &[O]) -> Option<Vec<process::Command>> {
exec_to_command(self.exec.as_deref()?, path_opt)
exec_to_command(
self.exec.as_deref()?,
&self.name,
self.path.as_deref(),
path_opt,
)
}
}
@ -399,9 +366,12 @@ impl MimeAppCache {
// The current approach works but might not adhere to the spec (yet)
// Look for and return preferred terminals
//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() {
preference_order.insert(0, id);
@ -475,11 +445,43 @@ impl Default for MimeAppCache {
mod tests {
use super::exec_to_command;
#[test]
fn keys_within_words() {
let exec = "/usr/bin/foo --option=%f";
let paths = ["file1"];
let commands = exec_to_command(exec, "keys_within_words", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
assert_eq!("/usr/bin/foo", command.get_program().to_str().unwrap());
assert_eq!(
"--option=file1",
command.get_args().next().unwrap().to_str().unwrap()
);
}
#[test]
fn no_path_f_field_code() {
let exec = "/usr/bin/foo %f";
let paths: [&str; 0] = [];
let commands = exec_to_command(exec, "no_path_f_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
assert_eq!("/usr/bin/foo", command.get_program().to_str().unwrap());
assert_eq!(0, command.get_args().len());
}
#[test]
fn one_path_f_field_code() {
let exec = "/usr/bin/foo %f";
let paths = ["file1"];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "one_path_f_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
@ -494,31 +496,40 @@ mod tests {
#[test]
#[allow(non_snake_case)]
fn one_path_F_field_code() {
let exec = "/usr/bin/bar %F";
let paths = ["cat"];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let exec = "/usr/bin/cosmic-term -w %F";
let paths = ["/home/user"];
let commands = exec_to_command(exec, "one_path_F_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
let mut args = command.get_args();
assert_eq!("/usr/bin/bar", command.get_program().to_str().unwrap());
assert_eq!("cat", command.get_args().next().unwrap().to_str().unwrap());
assert_eq!(
"/usr/bin/cosmic-term",
command.get_program().to_str().unwrap()
);
assert_eq!("-w", args.next().unwrap().to_str().unwrap());
assert_eq!(paths[0], args.next().unwrap().to_str().unwrap());
}
#[test]
fn one_path_u_field_code() {
let exec = "/usr/bin/foobar %u";
let paths = ["/home/josh/krumpli"];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let exec = "/usr/bin/cosmic-term -w %u";
let paths = ["/home/user"];
let commands = exec_to_command(exec, "one_path_u_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
let mut args = command.get_args();
assert_eq!("/usr/bin/foobar", command.get_program().to_str().unwrap());
assert_eq!(
*paths.first().unwrap(),
command.get_args().next().unwrap().to_str().unwrap()
"/usr/bin/cosmic-term",
command.get_program().to_str().unwrap()
);
assert_eq!("-w", args.next().unwrap().to_str().unwrap());
assert_eq!(paths[0], args.next().unwrap().to_str().unwrap());
}
#[test]
@ -526,7 +537,8 @@ mod tests {
fn one_path_U_field_code() {
let exec = "/usr/bin/rmrfbye %U";
let paths = ["/"];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "one_path_U_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
@ -542,7 +554,8 @@ mod tests {
"/usr/share/games/psp/miku.iso",
"/usr/share/games/psp/eternia.iso",
];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "mult_path_f_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(paths.len(), commands.len());
for (command, path) in commands.into_iter().zip(paths.iter()) {
@ -562,7 +575,8 @@ mod tests {
"/usr/share/games/doom2/hr.wad",
"/usr/share/games/doom2/hrmus.wad",
];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "mult_path_F_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
@ -584,7 +598,8 @@ mod tests {
"https://redox-os.org/",
"https://system76.com/",
];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "mult_path_u_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(paths.len(), commands.len());
for (command, path) in commands.into_iter().zip(paths.iter()) {
@ -607,7 +622,8 @@ mod tests {
"frieren01.mkv",
"rtmp://example.org/this/video/doesnt/exist.avi",
];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "mult_path_U_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
@ -635,7 +651,8 @@ mod tests {
"@@u",
];
let paths = ["file1.rs", "file2.rs"];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "flatpak_style_exec", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
@ -658,7 +675,8 @@ mod tests {
"file:///usr/share/games/roguelike/mods/mod1",
"file:///usr/share/games/roguelike/mods/mod2",
];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "multiple_field_codes", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();
@ -691,7 +709,8 @@ mod tests {
];
let paths = ["rust_game_dev.pdf", "superhero_ferris.epub"];
let args_trailing = ["@@"];
let commands = exec_to_command(exec, &paths).expect("Should parse valid exec");
let commands = exec_to_command(exec, "sandwiched_field_code", None, &paths)
.expect("Should parse valid exec");
assert_eq!(1, commands.len());
let command = commands.first().unwrap();

View file

@ -3,11 +3,9 @@
use cosmic::widget::icon;
use mime_guess::Mime;
use rustc_hash::FxHashMap;
use std::{
fs,
path::Path,
sync::{LazyLock, Mutex},
};
use std::fs;
use std::path::Path;
use std::sync::{LazyLock, Mutex};
pub const FALLBACK_MIME_ICON: &str = "text-x-generic";
@ -47,7 +45,7 @@ impl MimeIconCache {
return None;
}
let icon_name = icon_names.remove(0);
let mut named = icon::from_name(icon_name).size(key.size);
let mut named = icon::from_name(icon_name).prefer_svg(true).size(key.size);
if !icon_names.is_empty() {
let fallback_names =
icon_names.into_iter().map(std::borrow::Cow::from).collect();
@ -114,7 +112,10 @@ pub fn mime_icon(mime: Mime, size: u16) -> icon::Handle {
let mut mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
match mime_icon_cache.get(MimeIconKey { mime, size }) {
Some(handle) => handle,
None => icon::from_name(FALLBACK_MIME_ICON).size(size).handle(),
None => icon::from_name(FALLBACK_MIME_ICON)
.prefer_svg(true)
.size(size)
.handle(),
}
}

View file

@ -1,18 +1,20 @@
use cosmic::{
Task,
iced::{Subscription, futures::SinkExt, stream},
widget,
};
use gio::{glib, prelude::*};
use std::{any::TypeId, cell::Cell, future::pending, hash::Hash, path::PathBuf, sync::Arc};
use cosmic::iced::futures::SinkExt;
use cosmic::iced::{Subscription, stream};
use cosmic::{Task, widget};
use gio::glib;
use gio::prelude::*;
use std::any::TypeId;
use std::cell::Cell;
use std::future::pending;
use std::hash::Hash;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::mpsc;
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
use crate::{
config::IconSizes,
err_str,
tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location},
};
use crate::config::IconSizes;
use crate::err_str;
use crate::tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location};
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";

View file

@ -1,13 +1,13 @@
use cosmic::{Task, iced::Subscription, widget};
use std::{
collections::BTreeMap,
fmt,
path::PathBuf,
sync::{Arc, LazyLock},
};
use cosmic::iced::Subscription;
use cosmic::{Task, widget};
use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::{Arc, LazyLock};
use tokio::sync::mpsc;
use crate::{config::IconSizes, tab};
use crate::config::IconSizes;
use crate::tab;
#[cfg(feature = "gvfs")]
mod gvfs;
@ -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 {
#[cfg(feature = "gvfs")]
Self::Gvfs(item) => item.icon(symbolic),
Self::Gvfs(item) => item.icon(_symbolic),
Self::None => unreachable!(),
}
}
@ -103,6 +103,7 @@ impl MounterItem {
pub type MounterItems = Vec<MounterItem>;
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum MounterMessage {
Items(MounterItems),
MountResult(MounterItem, Result<bool, String>),

View file

@ -3,21 +3,17 @@
use std::time::Instant;
use crate::tab::DOUBLE_CLICK_DURATION;
use cosmic::{
Element, Renderer, Theme,
iced::core::{
Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget,
border::Border,
event::Event,
layout,
mouse::{self, click},
overlay,
renderer::{self, Quad, Renderer as _},
touch,
widget::{Operation, Tree, tree},
},
widget::Id,
use cosmic::iced::core::border::Border;
use cosmic::iced::core::event::Event;
use cosmic::iced::core::mouse::{self, click};
use cosmic::iced::core::renderer::{self, Quad, Renderer as _};
use cosmic::iced::core::widget::{Operation, Tree, tree};
use cosmic::iced::core::{
Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout,
overlay, touch,
};
use cosmic::widget::Id;
use cosmic::{Element, Renderer, Theme};
/// Emit messages on mouse events.
#[allow(missing_debug_implementations)]

View file

@ -1,20 +1,15 @@
use crate::{
app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID},
archive,
config::IconSizes,
fl,
spawn_detached::spawn_detached,
tab,
};
use cosmic::iced::futures::{self, SinkExt, StreamExt, channel::mpsc::Sender, stream};
use std::{
borrow::Cow,
fmt::Formatter,
fs,
io::{self, Read, Write},
path::{Path, PathBuf},
sync::Arc,
};
use crate::app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID};
use crate::config::IconSizes;
use crate::spawn_detached::spawn_detached;
use crate::{archive, fl, tab};
use cosmic::iced::futures::channel::mpsc::Sender;
use cosmic::iced::futures::{self, SinkExt, StreamExt, stream};
use std::borrow::Cow;
use std::fmt::Formatter;
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::{Mutex as TokioMutex, mpsc};
use walkdir::WalkDir;
use zip::AesMode::Aes256;
@ -39,7 +34,7 @@ async fn handle_replace(
conflict_count: usize,
) -> ReplaceResult {
let item_from = match tab::item_from_path(file_from, IconSizes::default()) {
Ok(ok) => ok,
Ok(ok) => Box::new(ok),
Err(err) => {
log::warn!("{err}");
return ReplaceResult::Cancel;
@ -47,7 +42,7 @@ async fn handle_replace(
};
let item_to = match tab::item_from_path(file_to, IconSizes::default()) {
Ok(ok) => ok,
Ok(ok) => Box::new(ok),
Err(err) => {
log::warn!("{err}");
return ReplaceResult::Cancel;
@ -1243,28 +1238,23 @@ fn wrap_compio_spawn_error(err: Box<dyn std::any::Any + Send>) -> OperationError
#[cfg(test)]
mod tests {
use std::{
fs::{self, File},
io,
path::PathBuf,
};
use std::fs::{self, File};
use std::io;
use std::path::PathBuf;
use cosmic::iced::futures::{StreamExt, channel::mpsc, future};
use cosmic::iced::futures::channel::mpsc;
use cosmic::iced::futures::{StreamExt, future};
use log::debug;
use test_log::test;
use tokio::sync;
use super::{Controller, Operation, OperationError, OperationSelection, ReplaceResult};
use crate::{
app::{
DialogPage, Message,
test_utils::{
NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, empty_fs, filter_dirs,
filter_files, simple_fs,
},
},
fl,
use crate::app::test_utils::{
NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, empty_fs, filter_dirs, filter_files,
simple_fs,
};
use crate::app::{DialogPage, Message};
use crate::fl;
/// Simple wrapper around `[Operation::Copy]`
pub async fn operation_copy(

View file

@ -1,4 +1,5 @@
use std::{fs, io, path::Path};
use std::path::Path;
use std::{fs, io};
use crate::operation::OperationError;

View file

@ -6,15 +6,21 @@ use crate::operation::{OperationError, sync_to_disk};
use anyhow::Context as AnyhowContext;
use compio::BufResult;
use compio::buf::{IntoInner, IoBuf};
use compio::driver::{ToSharedFd, op::AsyncifyFd};
use compio::driver::ToSharedFd;
use compio::driver::op::AsyncifyFd;
use compio::io::{AsyncReadAt, AsyncWriteAt};
use cosmic::iced::futures;
#[cfg(feature = "gvfs")]
use futures::{FutureExt, StreamExt};
use std::cell::Cell;
use std::error::Error;
use std::fs;
use std::future::Future;
use std::ops::ControlFlow;
use std::path::PathBuf;
use std::pin::Pin;
use std::rc::Rc;
use std::time::Instant;
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf};
use walkdir::WalkDir;
#[cfg(feature = "gvfs")]
@ -473,14 +479,14 @@ impl Op {
progress.total_bytes = metadata.as_ref().map(|m| m.len());
(ctx.on_progress)(self, &progress);
if let Some(metadata) = metadata.as_ref() {
if let Err(why) = to_file.set_permissions(metadata.permissions()).await {
if let Some(metadata) = metadata.as_ref()
&& let Err(why) = to_file.set_permissions(metadata.permissions()).await
{
// This error is not propagated upwards as some filesystems do not support setting permissions
if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {
tracing::warn!(?why, "failed to set permissions for {}", self.to.display(),);
}
}
}
// Prevent spamming the progress callbacks.
let mut last_progress_update = Instant::now();

View file

@ -1,91 +1,72 @@
#[cfg(feature = "desktop")]
use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};
use cosmic::{
Apply, Element, cosmic_theme, font,
iced::core::{mouse::ScrollDelta, widget::tree},
iced::{
use cosmic::iced::advanced::graphics;
use cosmic::iced::advanced::text::{self, Paragraph};
use cosmic::iced::alignment::Vertical;
use cosmic::iced::clipboard::dnd::DndAction;
use cosmic::iced::core::mouse::ScrollDelta;
use cosmic::iced::core::widget::tree;
use cosmic::iced::futures::{self, SinkExt};
use cosmic::iced::keyboard::Modifiers;
use cosmic::iced::widget::scrollable::{self, AbsoluteOffset, Viewport};
use cosmic::iced::widget::{rule, stack};
use cosmic::iced::{
Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector,
advanced::{
graphics,
text::{self, Paragraph},
},
alignment::Vertical,
clipboard::dnd::DndAction,
futures::{self, SinkExt},
keyboard::Modifiers,
padding, stream,
widget::{
rule,
scrollable::{self, AbsoluteOffset, Viewport},
stack,
},
window,
},
theme,
widget::{
self, DndDestination, DndSource, Id, RcElementWrapper, Widget,
menu::{action::MenuAction, key_bind::KeyBind},
space,
},
padding, stream, window,
};
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::{self, DndDestination, DndSource, Id, RcElementWrapper, Widget, space};
use cosmic::{Apply, Element, cosmic_theme, font, theme};
#[cfg(feature = "desktop")]
use i18n_embed::LanguageLoader;
use icu::{
datetime::{
DateTimeFormatter, DateTimeFormatterPreferences, fieldsets, input::DateTime,
options::TimePrecision,
},
locale::preferences::extensions::unicode::keywords::HourCycle,
};
use icu::datetime::input::DateTime;
use icu::datetime::options::TimePrecision;
use icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences, fieldsets};
use icu::locale::preferences::extensions::unicode::keywords::HourCycle;
use image::{DynamicImage, ImageReader};
use jiff_icu::ConvertFrom;
use mime_guess::{Mime, mime};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::cell::Cell;
use std::cmp::{Ordering, Reverse};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::error::Error;
use std::fmt::{self, Display};
use std::fs::{self, File, Metadata};
use std::hash::Hash;
use std::io::{BufRead, BufReader, Read};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::{
borrow::Cow,
cell::Cell,
cmp::{Ordering, Reverse},
collections::{BTreeMap, BTreeSet, HashMap},
error::Error,
fmt::{self, Display},
fs::{self, File, Metadata},
hash::Hash,
io::{BufRead, BufReader},
path::{self, Path, PathBuf},
sync::{Arc, LazyLock, RwLock, atomic},
time::{Duration, Instant, SystemTime},
};
use std::path::{self, Path, PathBuf};
use std::sync::{Arc, LazyLock, RwLock, atomic};
use std::time::{Duration, Instant, SystemTime};
use tempfile::NamedTempFile;
use tokio::sync::mpsc;
use trash::{TrashItem, TrashItemMetadata, TrashItemSize};
use walkdir::WalkDir;
use crate::{
FxOrderMap,
app::{Action, PreviewItem, PreviewKind},
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
config::{
use crate::app::{Action, PreviewItem, PreviewKind};
use crate::clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste};
use crate::config::{
ContextActionPreset, DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig,
ThumbCfg,
},
dialog::DialogKind,
fl,
large_image::{
};
use crate::dialog::DialogKind;
use crate::large_image::{
LargeImageManager, decode_large_image, exceeds_memory_limit, should_use_dedicated_worker,
should_use_tiling,
},
localize::{LANGUAGE_SORTER, LOCALE},
menu, mime_app,
mime_icon::{mime_for_path, mime_icon},
mounter::MOUNTERS,
mouse_area,
operation::{Controller, OperationError},
thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize},
thumbnailer::thumbnailer,
trash::{Trash, TrashExt},
};
use crate::localize::{LANGUAGE_SORTER, LOCALE};
use crate::mime_icon::{mime_for_path, mime_icon};
use crate::mounter::MOUNTERS;
use crate::operation::{Controller, OperationError};
use crate::thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize};
use crate::thumbnailer::thumbnailer;
use crate::trash::{Trash, TrashExt};
use crate::{FxOrderMap, fl, menu, mime_app, mouse_area};
pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
@ -95,6 +76,11 @@ const MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20);
const MAX_SEARCH_RESULTS: usize = 200;
//TODO: configurable thumbnail size?
const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32);
/// Maximum bytes of text to pass to the editor for preview; caps shaping work to avoid blocking.
/// Files larger than this get a truncated preview (first N bytes only).
const TEXT_PREVIEW_MAX_BYTES: usize = 256 * 1024; // 256 KiB
/// Maximum file size (bytes) to attempt text preview; files larger than this are skipped entirely.
const TEXT_PREVIEW_MAX_FILE_BYTES: u64 = 8 * 1000 * 1000; // 8 MiB
// Thumbnail generation semaphore - limits parallel thumbnail workers
// Uses 4 workers for balanced throughput and memory usage
@ -289,6 +275,7 @@ fn button_style(
pub fn folder_icon(path: &PathBuf, icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(SPECIAL_DIRS.get(path).map_or("folder", |x| *x))
.prefer_svg(true)
.size(icon_size)
.handle()
}
@ -302,6 +289,26 @@ pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Han
.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
fn has_trailing_sep(path: &Path) -> bool {
path.as_os_str()
@ -558,7 +565,7 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
}
#[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
}
@ -577,7 +584,7 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
}
#[cfg(not(feature = "desktop"))]
fn get_desktop_file_icon(path: &Path) -> Option<String> {
fn get_desktop_file_icon(_path: &Path) -> Option<String> {
None
}
@ -601,7 +608,10 @@ fn desktop_icon_handle(icon: &str, size: u16) -> widget::icon::Handle {
if icon_path.is_absolute() && icon_path.exists() {
widget::icon::from_path(icon_path.to_path_buf())
} else {
widget::icon::from_name(icon).size(size).handle()
widget::icon::from_name(icon)
.prefer_svg(true)
.size(size)
.handle()
}
}
@ -665,9 +675,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
folder_icon(&path, sizes.list_condensed()),
)
} else {
// ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive
// @todo - expose this as a config option?
let mime = mime_for_path(&path, None, true);
// 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
let icon_name_opt = if mime == "application/x-desktop" {
@ -684,28 +694,21 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
desktop_icon_handle(&icon_name, sizes.list_condensed()),
)
} else {
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
generic_file_icons(sizes);
(
mime.clone(),
mime_icon(mime.clone(), sizes.grid()),
mime_icon(mime.clone(), sizes.list()),
mime_icon(mime, sizes.list_condensed()),
mime,
icon_handle_grid,
icon_handle_list,
icon_handle_list_condensed,
)
}
};
let mut children_opt = None;
let children_opt = None;
let mut dir_size = DirSize::NotDirectory;
if is_dir && !remote {
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);
@ -759,7 +762,10 @@ pub fn item_from_entry(
sizes: IconSizes,
) -> Item {
let mut is_desktop = false;
#[cfg(feature = "gvfs")]
let mut is_gvfs = false;
#[cfg(not(feature = "gvfs"))]
let is_gvfs = false;
let hidden = name.starts_with('.') || hidden_attribute(&metadata);
@ -807,7 +813,9 @@ pub fn item_from_entry(
folder_icon(&path, sizes.list_condensed()),
)
} 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
let icon_name_opt = if mime == "application/x-desktop" {
is_desktop = true;
@ -823,28 +831,21 @@ pub fn item_from_entry(
desktop_icon_handle(&icon_name, sizes.list_condensed()),
)
} else {
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
generic_file_icons(sizes);
(
mime.clone(),
mime_icon(mime.clone(), sizes.grid()),
mime_icon(mime.clone(), sizes.list()),
mime_icon(mime, sizes.list_condensed()),
mime,
icon_handle_grid,
icon_handle_list,
icon_handle_list_condensed,
)
}
};
let mut children_opt = None;
let children_opt = None;
let mut dir_size = DirSize::NotDirectory;
if metadata.is_dir() && !remote {
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);
@ -958,7 +959,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> {
let mut items = Vec::new();
let mut hidden_files = Box::from([]);
#[cfg(feature = "gvfs")]
let mut remote_scannable = false;
#[cfg(not(feature = "gvfs"))]
let remote_scannable = false;
#[cfg(feature = "gvfs")]
{
@ -1558,7 +1562,7 @@ impl Location {
}
}
pub fn scan(&self, sizes: IconSizes) -> (Option<Item>, Vec<Item>) {
pub fn scan(&self, sizes: IconSizes) -> (Option<Box<Item>>, Vec<Item>) {
let items = match self {
Self::Desktop(path, display, desktop_config) => {
scan_desktop(path, display, *desktop_config, sizes)
@ -1574,7 +1578,7 @@ impl Location {
};
let parent_item_opt = match self.path_opt() {
Some(path) => match item_from_path(path, sizes) {
Ok(item) => Some(item),
Ok(item) => Some(Box::new(item)),
Err(err) => {
log::warn!("failed to get item for {}: {}", path.display(), err);
None
@ -1683,6 +1687,7 @@ pub enum Command {
ContextMenu(Option<Point>, Option<window::Id>),
Delete(Vec<PathBuf>),
DropFiles(PathBuf, ClipboardPaste),
ClearRecents,
EmptyTrash,
#[cfg(feature = "desktop")]
ExecEntryAction(cosmic::desktop::DesktopEntryData, usize),
@ -1722,6 +1727,7 @@ pub enum Message {
EditLocationSubmit,
EditLocationTab,
OpenInNewTab(PathBuf),
ClearRecents,
EmptyTrash,
#[cfg(feature = "desktop")]
ExecEntryAction(Option<PathBuf>, usize),
@ -2084,17 +2090,37 @@ impl ItemThumbnail {
log::warn!("failed to read {}: {}", path.display(), err);
}
}
} else if mime.type_() == mime::TEXT && check_size("text", 8 * 1000 * 1000) {
/*TODO: fix performance issues, widget::text_editor::Content::with_text forces all text to shape, which blocks rendering
match fs::read_to_string(&path) {
Ok(data) => {
return ItemThumbnail::Text(widget::text_editor::Content::with_text(&data));
} else if mime.type_() == mime::TEXT && check_size("text", TEXT_PREVIEW_MAX_FILE_BYTES) {
tried_supported_file = true;
if size > 0 {
// Reuse size from metadata above; cap allocation and read
let read_cap = (size.min(TEXT_PREVIEW_MAX_BYTES as u64)) as usize;
let mut buf = vec![0u8; read_cap];
match File::open(path).and_then(|f| {
let n = Read::read(&mut f.take(read_cap as u64), &mut buf)?;
buf.truncate(n);
Ok(())
}) {
Ok(()) => {
let text = match std::str::from_utf8(&buf) {
Ok(s) => s.to_string(),
Err(e) => {
// Use only the valid UTF-8 prefix (slice is guaranteed valid by valid_up_to())
std::str::from_utf8(&buf[..e.valid_up_to()])
.unwrap_or("")
.to_string()
}
};
if !text.is_empty() {
return Self::Text(widget::text_editor::Content::with_text(&text));
}
}
Err(err) => {
log::warn!("failed to read {}: {}", path.display(), err);
}
}
*/
}
// size == 0: empty file or unknown size; skip read and allocation
}
// If we weren't able to create a thumbnail, but we should have
@ -2278,7 +2304,7 @@ impl Item {
}
fn preview(&self) -> Element<'_, Message> {
let spacing = cosmic::theme::active().cosmic().spacing;
let spacing = cosmic::theme::spacing();
// This loads the image only if thumbnailing worked
let icon = widget::icon::icon(self.icon_handle_grid.clone())
.content_fit(ContentFit::Contain)
@ -2339,7 +2365,7 @@ impl Item {
space_xxxs,
space_m,
..
} = theme::active().cosmic().spacing;
} = theme::spacing();
let mut column = widget::column::with_capacity(4).spacing(space_m);
@ -2517,7 +2543,7 @@ impl Item {
}
pub fn replace_view(&self, heading: String, military_time: bool) -> Element<'_, Message> {
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let cosmic_theme::Spacing { space_xxxs, .. } = theme::spacing();
let mut row = widget::row::with_capacity(2).spacing(space_xxxs);
row = row.push(self.preview());
@ -2657,7 +2683,7 @@ pub struct Tab {
pub sort_name: HeadingOptions,
pub sort_direction: bool,
pub gallery: bool,
pub(crate) parent_item_opt: Option<Item>,
pub(crate) parent_item_opt: Option<Box<Item>>,
pub(crate) items_opt: Option<Vec<Item>>,
pub dnd_hovered: Option<(Location, Instant)>,
pub(crate) scrollable_id: widget::Id,
@ -2991,10 +3017,10 @@ impl Tab {
return None;
};
let search_items = after
.into_iter()
.iter_mut()
.enumerate()
.map(|(i, item)| (i + start, item))
.chain(until.into_iter().enumerate());
.chain(until.iter_mut().enumerate());
if forward {
Self::select_first_prefix_match(prefix_lower, search_items)
@ -3011,7 +3037,7 @@ impl Tab {
items: impl Iterator<Item = (usize, &'a mut Item)>,
) -> Option<usize> {
for (i, item) in items {
if item.name.to_lowercase().starts_with(&prefix) {
if item.name.to_lowercase().starts_with(prefix) {
item.selected = true;
return Some(i);
}
@ -3607,7 +3633,7 @@ impl Tab {
match item_from_path(&path, IconSizes::default()) {
Ok(item) => {
commands.push(Command::Preview(PreviewKind::Custom(
PreviewItem(item),
PreviewItem(Box::new(item)),
)));
}
Err(err) => {
@ -3697,6 +3723,9 @@ impl Tab {
Message::OpenInNewTab(path) => {
commands.push(Command::OpenInNewTab(path));
}
Message::ClearRecents => {
commands.push(Command::ClearRecents);
}
Message::EmptyTrash => {
commands.push(Command::EmptyTrash);
}
@ -4332,7 +4361,7 @@ impl Tab {
Message::ShiftPermissions(path_mode_opt, shift, bits) => match path_mode_opt {
Some((path, mode)) => commands.push(Command::SetPermissions(
path,
set_mode_part(mode, shift, bits.try_into().unwrap()),
set_mode_part(mode, shift, bits),
)),
// Shift permissions on all selected items
None => {
@ -4343,13 +4372,9 @@ impl Tab {
#[cfg(unix)]
if let (Some(path), Some(mode)) = (
item.path_opt(),
item.file_metadata()
.and_then(|metadata| Some(metadata.mode())),
item.file_metadata().map(|metadata| metadata.mode()),
) {
permissions.push((
path.clone(),
set_mode_part(mode, shift, bits.try_into().unwrap()),
));
permissions.push((path.clone(), set_mode_part(mode, shift, bits)));
}
}
commands.push(Command::SetMultiplePermissions(permissions));
@ -4765,7 +4790,7 @@ impl Tab {
space_xs,
space_m,
..
} = theme::active().cosmic().spacing;
} = theme::spacing();
//TODO: display error messages when image not found?
let mut name_opt = None;
@ -4963,7 +4988,7 @@ impl Tab {
space_s,
space_m,
..
} = theme::active().cosmic().spacing;
} = theme::spacing();
let size = self.size_opt.get().unwrap_or(Size::new(0.0, 0.0));
@ -5298,7 +5323,7 @@ impl Tab {
}
pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> {
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();
mouse_area::MouseArea::new(widget::column::with_children([widget::container(
match self.mode {
@ -5338,7 +5363,7 @@ impl Tab {
space_xxs,
space_xxxs,
..
} = theme::active().cosmic().spacing;
} = theme::spacing();
let TabConfig {
show_hidden,
@ -5448,8 +5473,7 @@ impl Tab {
widget::button::custom(
widget::icon::icon(item.icon_handle_grid.clone())
.content_fit(ContentFit::Contain)
.size(icon_sizes.grid())
.width(Length::Shrink),
.size(icon_sizes.grid()),
)
.padding(space_xxxs)
.class(button_style(
@ -5684,7 +5708,7 @@ impl Tab {
) {
let cosmic_theme::Spacing {
space_s, space_xxs, ..
} = theme::active().cosmic().spacing;
} = theme::spacing();
let TabConfig {
show_hidden,
@ -6086,7 +6110,7 @@ impl Tab {
space_xxs,
space_xs,
..
} = theme::active().cosmic().spacing;
} = theme::spacing();
let location_view_opt = if matches!(self.mode, Mode::Desktop) {
None
@ -6216,6 +6240,24 @@ impl Tab {
);
}
}
Location::Recents | Location::Search(SearchLocation::Recents, ..) => {
if let Some(items) = self.items_opt()
&& !items.is_empty()
{
tab_column = tab_column.push(
widget::layer_container(widget::row::with_children([
widget::space::horizontal().into(),
widget::button::standard(fl!("clear-recents-history"))
.on_press(Message::ClearRecents)
.into(),
]))
.padding([space_xxs, space_xs])
.layer(cosmic_theme::Layer::Primary)
.apply(widget::container)
.padding([0, 0, 7, 0]),
);
}
}
Location::Network(uri, _display_name, _path) if uri == "network:///" => {
tab_column = tab_column.push(
widget::layer_container(widget::row::with_children([
@ -6282,7 +6324,7 @@ impl Tab {
space_xxxs,
space_m,
..
} = theme::active().cosmic().spacing;
} = theme::spacing();
let mut column = widget::column::with_capacity(4).spacing(space_m);
@ -6415,12 +6457,12 @@ impl Tab {
let mut settings = Vec::new();
// Only allow modifying open-with if all mime types are the same
if mime_types.len() == 1 {
if let Some(mime) = mime_types
.get(0)
if mime_types.len() == 1
&& let Some(mime) = mime_types
.first()
.and_then(|(mime, _)| mime.parse::<Mime>().ok())
&& 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(&mime);
if !mime_apps.is_empty() {
let mime_closure = mime.clone();
@ -6442,8 +6484,6 @@ impl Tab {
);
}
}
}
}
#[cfg(unix)]
{
@ -7046,21 +7086,25 @@ fn text_editor_class(
#[cfg(test)]
mod tests {
use std::{fs, io, path::PathBuf};
use std::path::PathBuf;
use std::{fs, io};
use cosmic::{iced::mouse::ScrollDelta, iced::runtime::keyboard::Modifiers, widget};
use cosmic::iced::mouse::ScrollDelta;
use cosmic::iced::runtime::keyboard::Modifiers;
use cosmic::widget;
use log::{debug, trace};
use mime_guess::mime;
use tempfile::TempDir;
use test_log::test;
use super::{Location, Message, Tab, respond_to_scroll_direction, scan_path};
use crate::{
app::test_utils::{
use super::{
ItemMetadata, ItemThumbnail, Location, Message, Tab, respond_to_scroll_direction, scan_path,
};
use crate::app::test_utils::{
NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, assert_eq_tab_path, empty_fs,
eq_path_item, filter_dirs, read_dir_sorted, simple_fs, tab_click_new,
},
config::{IconSizes, TabConfig, ThumbCfg},
};
use crate::config::{IconSizes, TabConfig, ThumbCfg};
// Boilerplate for tab tests. Checks if simulated clicks selected items.
fn tab_selects_item(
@ -7473,4 +7517,86 @@ mod tests {
}
}
}
#[test]
fn item_thumbnail_text_preview_small_utf8_returns_text() -> io::Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("preview.txt");
fs::write(&path, "Hello, world!")?;
let metadata = fs::metadata(&path)?;
let item_metadata = ItemMetadata::Path {
metadata,
children_opt: None,
};
let thumb = ItemThumbnail::new(
&path,
item_metadata,
mime::TEXT_PLAIN,
128,
100 * 1024 * 1024,
1,
8,
);
assert!(
matches!(thumb, ItemThumbnail::Text(_)),
"small text file should produce Text thumbnail"
);
Ok(())
}
#[test]
fn item_thumbnail_text_preview_empty_file_returns_not_image() -> io::Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("empty.txt");
fs::File::create(&path)?;
let metadata = fs::metadata(&path)?;
let item_metadata = ItemMetadata::Path {
metadata,
children_opt: None,
};
let thumb = ItemThumbnail::new(
&path,
item_metadata,
mime::TEXT_PLAIN,
128,
100 * 1024 * 1024,
1,
8,
);
assert!(
matches!(thumb, ItemThumbnail::NotImage),
"empty text file should produce NotImage (no read)"
);
Ok(())
}
#[test]
fn item_thumbnail_text_preview_invalid_utf8_uses_valid_prefix() -> io::Result<()> {
let dir = TempDir::new()?;
let path = dir.path().join("invalid_utf8.txt");
// Valid UTF-8 "ab" then invalid byte sequence then "c"
fs::write(&path, b"ab\xff\xfe\xfdc")?;
let metadata = fs::metadata(&path)?;
let item_metadata = ItemMetadata::Path {
metadata,
children_opt: None,
};
let thumb = ItemThumbnail::new(
&path,
item_metadata,
mime::TEXT_PLAIN,
128,
100 * 1024 * 1024,
1,
8,
);
match &thumb {
ItemThumbnail::Text(content) => {
// Text editor content may add a trailing newline
assert_eq!(content.text().trim_end(), "ab");
}
_ => panic!("expected Text thumbnail with valid prefix only, got {:?}", thumb),
}
Ok(())
}
}

View file

@ -1,16 +1,14 @@
use image::DynamicImage;
use md5::{Digest, Md5};
use rustc_hash::FxHashMap;
use std::error::Error;
use std::fs::{self, File};
use std::io::{self, BufReader, BufWriter};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
error::Error,
fs::{self, File},
io::{self, BufReader, BufWriter},
path::{Path, PathBuf},
sync::LazyLock,
time::UNIX_EPOCH,
};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::time::UNIX_EPOCH;
use tempfile::NamedTempFile;
use url::Url;

View file

@ -5,12 +5,12 @@
use cosmic::desktop::fde::GenericEntry;
use mime_guess::Mime;
use rustc_hash::FxHashMap;
#[cfg(feature = "desktop")]
use std::{fs, time::Instant};
use std::{
fs,
path::Path,
process,
sync::{LazyLock, Mutex},
time::Instant,
};
#[derive(Clone, Debug)]

View file

@ -1,11 +1,10 @@
use cosmic::widget;
use regex::Regex;
use std::{collections::HashSet, path::PathBuf};
use std::collections::HashSet;
use std::path::PathBuf;
use crate::{
config::IconSizes,
tab::{Item, SearchItem},
};
use crate::config::IconSizes;
use crate::tab::{Item, SearchItem};
pub trait TrashExt {
fn is_empty() -> bool {
@ -27,7 +26,7 @@ pub trait TrashExt {
Vec::new()
}
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(_callback: F, _regex: &Regex) {}
fn icon(icon_size: u16) -> widget::icon::Handle {
widget::icon::from_name(if Self::is_empty() {
@ -81,7 +80,8 @@ impl TrashExt for Trash {
}
fn scan(sizes: IconSizes) -> Vec<Item> {
use crate::{localize::LANGUAGE_SORTER, tab::item_from_trash_entry};
use crate::localize::LANGUAGE_SORTER;
use crate::tab::item_from_trash_entry;
use std::cmp::Ordering;
let entries = match trash::os_limited::list() {
@ -142,4 +142,12 @@ impl TrashExt for Trash {
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);
}
}

View file

@ -1,6 +1,7 @@
use std::num::NonZeroU16;
use crate::{config::IconSizes, tab::View};
use crate::config::IconSizes;
use crate::tab::View;
static DEFAULT_ZOOM: NonZeroU16 = NonZeroU16::new(100).unwrap();
static MIN_ZOOM: NonZeroU16 = NonZeroU16::new(50).unwrap();