Compare commits

...
Sign in to create a new pull request.

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

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

Weak references used in gio callbacks in case the sender is dropped
by the application.
2026-04-14 17:04:48 +02:00
Michael Aaron Murphy
971374f60b
perf: use atomics for controller progress and state 2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
93dd775f3c
perf: get image dimensions from background thread
This caused the tab subscription to block the tokio executor.
Instead store the image dimensions in the `Item`, which is
created on a background thread.
2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
bb15f30fe5
chore: update Cargo.lock 2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
e2bdcf8da4
fix: use gio file copy fallback to fix MTP unsupported I/O errors 2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
b299f1a172
chore: add methods to Controller to pause and unpause futures
- Use `Controller::until_paused` as a signal in a select to pause futures
- Use `Controller::until_unpaused` to block futures in a select loop
2026-04-14 16:51:51 +02:00
Michael Aaron Murphy
c114759c9e
chore: use tracing for structured logs 2026-04-14 16:51:51 +02:00
sandroid
91243b99b5 feat: repeat a key to cycle through items in type-to-select mode 2026-04-13 16:24:28 -04:00
sandroid
da05a85fc5 feat: start type-to-select search from the current focus 2026-04-13 16:24:28 -04:00
Jeremy Soller
109f83799d
Merge pull request #1711 from pop-os/clipboard-fixes
fix: handle slight delay in availability of clipboard data
2026-04-13 14:09:00 -06:00
Hojjat
fc25260a5f feat: select file name until the extension in rename and save dialog 2026-04-13 12:06:49 -06:00
Ashley Wulber
1c1f8fdf6e fix: retry reading clipboard if data is empty 2026-04-13 11:53:25 -06:00
Ashley Wulber
cf328771c3 fix: handle slight delay in availability of clipboard data
a better fix in the future would probably be an event indicating availability of the data.
2026-04-13 11:53:25 -06:00
Hosted Weblate
e60ae4e41f
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Asier Saratsua Garmendia <asier.sarasua@gmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Jun Hwi Ku <siguning@gmail.com>
Co-authored-by: Otto Domínguez <otto@sistemasorion.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Thomas Worofsky <thomas.worofsky@gmail.com>
Co-authored-by: Tommi Nieminen <translator@legisign.org>
Co-authored-by: Urocissa Caerulea.Tw <urocissa.tw@proton.me>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: defaultUser822 <defaultuser822@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/es_419/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files
2026-04-12 18:49:45 +02:00
Jeremy Soller
175f8ba724
Merge pull request #1728 from darkfated/add-context-actions
Add user-defined context actions
2026-04-10 15:42:42 -06:00
darkfated
ad0e66dceb Fix: remove context actions from open/save dialog 2026-04-10 02:58:20 +03:00
Hojjat
b17f8889a8 chore: update to the latest libcosmic 2026-04-08 15:15:48 -06:00
Ashley Wulber
9547da2b25 fix: reset focus when opening a new tab 2026-04-07 15:39:29 -06:00
Ashley Wulber
d38d55525b chore: update libcosmic 2026-04-06 18:08:44 -06:00
Jeremy Soller
781e99d293 Use row, column with_capacity 2026-04-06 18:08:44 -06:00
Jeremy Soller
11b2617b6c Epoch 1.0.9 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-04-06 18:08:44 -06:00
User
079c82ee43 fix: assign replace dialog button id 2026-04-06 15:12:13 -06:00
Ashley Wulber
635bff7c1e chore: udpate libcosmic 2026-04-06 15:14:56 -04:00
darkfated
39281a6336 Add user-defined context actions 2026-04-06 06:16:27 +03:00
Jonatan Pettersson
23b5d98dcc feat: set placeholder for permission dropdowns 2026-04-02 13:43:03 -06:00
Jonatan Pettersson
4c6f2db5f2 feat: join multiple operations
Allow for joining operations into a single Task that
will produce a single Message:PendingResults message such
that multiple Message::PendingComplete and Message::PendingError
messages can be handled together to, for example, show
a single error dialog with multiple errors.
2026-04-02 13:43:03 -06:00
Jonatan Pettersson
41cdf89604 feat: allow setting permissions in multi preview
This adds a Message::ShiftPermissions to handle setting
permissions for user, group or other for either 1 or
more items and a Command::SetMultiplePermissions to
set permissions on multiple items at the same time.

The permission dropdown will only have a selection if
all selected items have the same permission, otherwise it
will be empty but still allow changing.

Up to 5 owners and groups will be displayed for all
selected items with an ellipses if there are more.

The OperationSelection for setting permissions now also
returns the path as selected such that the tab will be re-scanned
and update the dropdown to correctly.
2026-04-02 13:43:03 -06:00
Jonatan Pettersson
17325a5f5a feat: add open-with in multi preview
Adds the ability to set the open-with setting for
multiple selected items. This setting will only appear
if all selected items have the same mime type.
2026-04-02 13:43:03 -06:00
Josh Megnauth
e50c41aa24 fix: Respect show_recents conf in dialog
Closes: #1698
2026-03-27 13:04:03 -06:00
Ashley Wulber
75fe043e73 fix: don't read files if list empty 2026-03-26 16:42:36 +01:00
Levi Portenier
0b7294d4e4
Merge pull request #1699 from jasonrhansen/open-item-location
feat: add 'Open item location' to search
2026-03-24 15:14:12 -06:00
Ashley Wulber
3a88d7fcf7 fix: update cached clipboard 2026-03-24 16:29:56 -04:00
Ashley Wulber
f6ca0cb460 cleanup 2026-03-24 16:29:56 -04:00
Ashley Wulber
34f35842df chore: update deps 2026-03-24 16:29:56 -04:00
Vukašin Vojinović
88bfd76a95 chore: update libcosmic 2026-03-24 11:25:59 -06:00
Vukašin Vojinović
6e2eafe16c chore: clippy 2026-03-24 11:25:59 -06:00
Vukašin Vojinović
3b1bc4430b refactor: use jiff instead of chrono 2026-03-24 11:25:59 -06:00
Vukašin Vojinović
4414d2f4b2 chore: update dependencies
Also utilizes the `jxl-oxide` image decoding hook.
2026-03-24 11:25:59 -06:00
Jason Rodney Hansen
b041feec48 feat: add 'Open item location' to search 2026-03-20 08:41:19 -06:00
68 changed files with 5681 additions and 2585 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"],
},
},
},
},
}

1729
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,19 @@
[package]
name = "cosmic-files"
version = "1.0.8"
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"
chrono = { version = "0.4", features = ["unstable-locales"] }
icu = { version = "2.1.1", features = ["compiled_data"] }
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true }
jiff = "0.2"
jiff-icu = "0.2"
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"
env_logger = "0.11"
gio = { version = "0.21", optional = true }
glib = { version = "0.21", optional = true }
glob = "0.3"
@ -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.12", 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"
lzma-rust2 = { version = "0.15.7", optional = true }
ordermap = { version = "1.1.0", features = ["serde"] }
tar = "0.4.45"
lzma-rust2 = { version = "0.16", optional = true }
ordermap = { version = "1.2.0", features = ["serde"] }
# Internationalization
i18n-embed = { version = "0.16", features = [
"fluent-system",
@ -54,13 +54,18 @@ i18n-embed-fl = "0.10"
rust-embed = "8"
slotmap = "1.1.1"
recently-used-xbel = "1.2.0"
zip = "7"
uzers = "0.12.2"
zip = "8"
md-5 = "0.10.6"
png = "0.18"
jxl-oxide = { version = "0.12.5", features = ["image"] }
num_cpus = "1.17.0"
filetime = "0.2"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.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]
@ -68,17 +73,18 @@ 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 = [
"about",
"advanced-shaping",
"autosize",
"desktop",
"multi-window",
"tokio",
"winit",
"wayland",
"surface-message",
]
@ -106,15 +112,15 @@ default = [
"wayland",
"wgpu",
]
dbus-config = ["libcosmic/dbus-config"]
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
@ -124,7 +130,8 @@ inherits = "release"
debug = true
[target.'cfg(unix)'.dependencies]
fork = "0.6"
fork = "0.7"
uzers = "0.12.2"
[target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.18"
@ -139,20 +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" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# [patch.'https://github.com/pop-os/smithay-clipboard']
# 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.8"
version = "1.0.13"
edition = "2024"
[dependencies]

24
debian/changelog vendored
View file

@ -1,3 +1,27 @@
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
-- Jeremy Soller <jeremy@system76.com> Tue, 14 Apr 2026 11:09:44 -0600
cosmic-files (1.0.9) noble; urgency=medium
* Epoch 1.0.9 version update
-- Jeremy Soller <jeremy@system76.com> Mon, 06 Apr 2026 15:10:13 -0600
cosmic-files (1.0.8) noble; urgency=medium
* Epoch 1.0.8 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,17 +1,30 @@
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;
use tracing_subscriber::util::SubscriberInitExt;
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let log_format = tracing_subscriber::fmt::format()
.pretty()
.without_time()
.with_line_number(true)
.with_file(true)
.with_target(false)
.with_thread_names(true);
let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
.with(log_layer)
.init();
let settings = Settings::default();
app::run::<App>(settings, ())?;
@ -148,7 +161,7 @@ impl Application for App {
}
fn view(&self) -> Element<'_, Message> {
let mut column = widget::column().spacing(8).padding(8);
let mut column = widget::column::with_capacity(8).spacing(8).padding(8);
{
let mut button = widget::button::standard("Open File");
if self.dialog_opt.is_none() {

View file

@ -22,10 +22,10 @@ empty-trash-warning = سيتم حذف العناصر الموجودة في مج
## New File/Folder Dialog
create-new-file = إنشاء ملف جديد
create-new-folder = إنشاء مجلد جديد
create-new-file = أنشئ ملف جديد
create-new-folder = أنشئ مجلَّد جديد
file-name = اسم الملف
folder-name = اسم المجلد
folder-name = اسم المجلَّد
file-already-exists = يوجد ملف بهذا الاسم بالفعل
folder-already-exists = يوجد مجلد بهذا الاسم بالفعل
name-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية
@ -34,10 +34,10 @@ name-no-slashes = لا يمكن أن يحتوي الاسم على شرطات م
## Open/Save Dialog
cancel = إلغاء
open = فتح
open-file = فتح ملف
open-folder = افتح مجلد
cancel = ألغِ
open = افتح
open-file = افتح ملف
open-folder = افتح مجلَّد
open-in-new-tab = افتح في لسان جديد
open-in-new-window = افتح في نافذة جديدة
open-multiple-files = افتح عدة ملفات
@ -150,12 +150,12 @@ trashed-on = مهمل
details = التفاصيل
pause = ألبث
resume = استئناف
create-archive = إنشاء أرشيف
create-archive = أنشئ أرشيف
extract-password-required = كلمة السر مطلوبة
extract-to = استخرِج إلى...
extract-to-title = استخرِج إلى مجلّد
mount-error = تعذر الوصول إلى القرص
create = إنشاء
create = أنشئ
open-item-location = افتح مكان العنصر
set-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟
favorite-path-error-description =
@ -259,7 +259,7 @@ related-apps = تطبيقات ذات صلة
selected-items = العناصر { $items } المحدّدة
permanently-delete-question = احذف نهائيًا؟
delete = احذف
permanently-delete-warning = هل أنت متأكد من أنك تريد حذف { $target } نهائيًا؟ لا يمكن التراجع عن هذا الإجراء.
permanently-delete-warning = سيُحذف { $target } نهائيًا. لا يمكن التراجع عن هذا الإجراء.
replace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه.
original-file = الملف الأصلي
replace-with = استبدل بـ
@ -371,3 +371,15 @@ move-to = انقل إلى...
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
@ -176,7 +176,7 @@ set-executable-and-launch-description = Chcete povolit spouštění souboru „{
set-and-launch = Povolit a spustit
open-with = Otevřít pomocí
other = Ostatní
none = Žádný
none = Žádné
icon-size-and-spacing = Velikost a rozestupy ikon
grid-spacing = Rozestupy mřížky
deleting =
@ -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
@ -417,3 +417,13 @@ move-to-title = Vyberte cíl přesunutí
show-recents = Složka „Nedávné“ v postranním panelu
copy-path = Kopírovat cestu
clear-recents-history = Vymazat historii „Nedávné“
mixed = Různé
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

@ -330,7 +330,7 @@ light = Hell
type-to-search = Zum Suchen tippen
type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner
type-to-search-enter-path = Gibt den Pfad zu einem Verzeichnis oder einer Datei ein
type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein
# Kontextmenü
add-to-sidebar = Zur Seitenleiste hinzufügen
compress = Komprimieren...
@ -376,9 +376,9 @@ select-all = Alles auswählen
## Ansicht
zoom-in = Vergrößern
zoom-in = Hineinzoomen
default-size = Standardgröße
zoom-out = Verkleinern
zoom-out = Herauszoomen
view = Ansicht
grid-view = Rasteransicht
list-view = Listenansicht
@ -390,7 +390,7 @@ menu-about = Über COSMIC Dateien...
## Sortieren
sort = Sortieren
sort = Sortierung
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = Neueste zuerst
@ -424,3 +424,16 @@ clear-recents-history = Verlauf zuletzt verwendeter Elemente leeren
comment = Dateimanager für den COSMIC Desktop
keywords = Ordner;Manager;
move-to-button-label = Verschieben
move-to-title = Verschiebeziel auswählen
remove-from-recents = Aus den zuletzt verwendeten Elementen entfernen
move-to = Verschieben nach...
copy-path = Pfad kopieren
mixed = Gemischt
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,9 +96,17 @@ 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
context-action = Context action
context-action-confirm-title = Run "{$name}"?
context-action-confirm-warning = This will run on {$items} {$items ->
[one] item
*[other] items
}.
run = Run
## Permanently delete Dialog
selected-items = The {$items} selected items
@ -109,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
@ -131,6 +140,12 @@ 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
### Mode 1 (unusual)

View file

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

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

View file

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

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

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

@ -318,3 +318,12 @@ comment = Pengelola berkas untuk desktop COSMIC
show-recents = Map terbaru di bilah sisi
clear-recents-history = Bersihkan riwayat Terbaru
copy-path = Salin jalur
mixed = Bercampur
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

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

View file

@ -256,7 +256,7 @@ type-to-search-recursive = Ағымдағы бума мен барлық ішк
type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді
type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды
add-to-sidebar = Бүйірлік панельге қосу
compress = Сығу
compress = Сығу...
delete-permanently = Біржолата өшіру
eject = Шығару
extract-here = Тарқату
@ -318,3 +318,12 @@ keywords = Folder;Manager;Бума;Басқарушы;
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

@ -110,7 +110,7 @@ dismiss = 메시지 무시
copy_noun = 복사
progress = { $percent }%
related-apps = 관련 앱
compress = 압축
compress = 압축...
network-drive-error = 네트워크 드라이브에 접근할 수 없음
icon-size-and-spacing = 아이콘 크기 및 간격
password = 암호
@ -272,3 +272,9 @@ network-drive-schemes =
WebDAV,dav:// 또는 davs://
type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다
comment = COSMIC 데스크톱용 위한 파일 관리자
keywords = 폴더;관리자;
copy-to-button-label = 복사
move-to-button-label = 이동
clear-recents-history = 최근 기록 비우기
copy-path = 복사 경로
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ą
@ -252,7 +252,7 @@ type-to-search = Norint ieškoti, pradėkite rašyti
type-to-search-recursive = Paieška dabartiniame aplanke ir jo poaplankiuose
type-to-search-enter-path = Įvedamas aplanko ar failo kelias
add-to-sidebar = Pridėti į šonjuostę
compress = Suspausti
compress = Suspausti...
delete-permanently = Ištrinti visam laikui
eject = Išstumti
extract-here = Išskleisti
@ -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

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

View file

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

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

@ -195,7 +195,7 @@ dark = Тёмная
light = Светлая
# Context menu
add-to-sidebar = Добавить на боковую панель
compress = Сжать
compress = Сжать...
extract-here = Распаковать
new-file = Новый файл…
new-folder = Новая папка…
@ -380,3 +380,12 @@ keywords = Папка;Менеджер;
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

@ -101,6 +101,7 @@ open-with = Öppna med
owner = Ägare
group = Grupp
other = Andra
mixed = Blandade
# Listvy
name = Namn
modified = Ändrad
@ -409,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 }» надано права на виконання і відкрито
@ -317,7 +323,7 @@ single-click = Відкривати одним клацанням
type-to-search = Введіть для пошуку
type-to-search-recursive = Шукає у поточній теці та всіх підтеках
type-to-search-enter-path = Вводить шлях до каталогу або файлу
compress = Стиснути
compress = Стиснути...
delete-permanently = Остаточно видалити
eject = Безпечно вилучити
extract-here = Видобути
@ -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

@ -11,7 +11,7 @@ recents = 最近访问
undo = 撤销
today = 今天
# Desktop view options
desktop-view-options = 桌面视图选项...
desktop-view-options = 桌面视图选项
show-on-desktop = 在桌面显示
desktop-folder-content = 桌面文件夹内容
mounted-drives = 已装载驱动器
@ -31,7 +31,7 @@ operations-running =
正在进行 { $running } { $running ->
[one] 个操作
*[other] 个操作
}{ $percent }%...
}{ $percent }%
operations-running-finished =
正在进行 { $running } { $running ->
[one] 个操作
@ -50,7 +50,7 @@ create-archive = 创建压缩包
## Extract Dialog
extract-password-required = 需要密码
extract-to = 提取到...
extract-to = 提取到
extract-to-title = 提取到文件夹
## Empty Trash Dialog
@ -167,7 +167,7 @@ read-write-execute = 读取、写入和执行
## Favorite Path Error Dialog
favorite-path-error = 打开路径时出错
favorite-path-error = 打开目录时出错
favorite-path-error-description =
无法打开 "{ $path }" 。
"{ $path }" 可能不存在或您没有权限打开它。
@ -317,7 +317,7 @@ unknown-folder = 未知文件夹
## Open with
menu-open-with = 打开方式...
menu-open-with = 打开方式
default-app = { $name }(默认)
## Show details
@ -329,7 +329,7 @@ item-size = 文件大小:{ $size }
item-created = 创建于:{ $created }
item-modified = 修改于:{ $modified }
item-accessed = 访问于:{ $accessed }
calculating = 计算中...
calculating = 计算中
## Settings
@ -348,15 +348,15 @@ light = 亮色模式
type-to-search = 输入即可搜索
type-to-search-recursive = 搜索当前文件夹及其所有子文件夹
type-to-search-enter-path = 输入文件夹或文件路径
type-to-search-enter-path = 输入文件夹或文件目录
# Context menu
add-to-sidebar = 加入侧边栏
compress = 压缩…
delete-permanently = 永久删除
eject = 弹出
extract-here = 解压到此处
new-file = 新建文件...
new-folder = 新建文件夹...
new-file = 新建文件
new-folder = 新建文件夹
open-in-terminal = 在终端模拟器中打开
move-to-trash = 移动到回收站
restore-from-trash = 从回收站中还原
@ -369,9 +369,9 @@ remove-from-recents = 从最近访问中移除
## Desktop
change-wallpaper = 更改壁纸...
desktop-appearance = 桌面外观...
display-settings = 显示设置...
change-wallpaper = 更改壁纸
desktop-appearance = 桌面外观
display-settings = 显示设置
# Menu
@ -382,7 +382,7 @@ file = 文件
new-tab = 新建标签
new-window = 新建窗口
reload-folder = 刷新文件夹
rename = 重命名...
rename = 重命名
close-tab = 关闭标签
quit = 退出
@ -405,7 +405,7 @@ list-view = 列表视图
show-hidden-files = 显示隐藏文件
list-directories-first = 优先列出目录
gallery-preview = 图库预览
menu-settings = 设置...
menu-settings = 设置
menu-about = 关于 COSMIC 文件…
## Sort
@ -435,4 +435,13 @@ comment = COSMIC 桌面的文件管理器
keywords = 文件夹;管理器;
clear-recents-history = 清除最近访问历史
copy-path = 复制文件路径
show-recents = 侧边栏显示最近访问
show-recents = 侧边栏中的最近访问
mixed = 混合
context-action-confirm-title = 运行 “{ $name }”
run = 运行
context-action-confirm-warning =
该行动将会在 { $items } { $items ->
[one] 项目
*[other] 项目
} 上运行。
context-action = 环境行动

View file

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

1423
src/app.rs

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,14 @@
use crate::{
mime_icon::mime_for_path,
operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk},
};
use chrono::TimeZone;
use chrono::{Datelike, Timelike};
use crate::mime_icon::mime_for_path;
use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk};
use cosmic::iced::futures;
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] = &[
@ -113,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>>(
@ -190,6 +188,8 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
let mut target = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut target)?;
// File no longer needed, drop to allow reading target on windows
drop(file);
#[cfg(unix)]
{
@ -200,11 +200,15 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
#[cfg(windows)]
{
let Ok(target) = String::from_utf8(target) else {
return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target"));
return Err(ZipError::InvalidArchive(
"Invalid UTF-8 as symlink target".into(),
));
};
let target = target.into_boxed_str();
let target_is_dir_from_archive =
archive.shared.files.contains_key(&target) && is_dir(&target);
let target_is_dir_from_archive = match password {
None => archive.by_name(&target),
Some(pwd) => archive.by_name_decrypt(&target, pwd.as_bytes()),
}
.map_or(false, |x| x.is_dir());
let target_path = directory.as_ref().join(OsString::from(target.to_string()));
let target_is_dir = if target_is_dir_from_archive {
true
@ -285,25 +289,25 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
}
fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option<SystemTime> {
let date = chrono::NaiveDate::from_ymd_opt(
date_time.year() as i32,
date_time.month() as u32,
date_time.day() as u32,
)?;
let time = chrono::NaiveTime::from_hms_opt(
date_time.hour() as u32,
date_time.minute() as u32,
date_time.second() as u32,
)?;
let naive = chrono::NaiveDateTime::new(date, time);
chrono::Local
.from_local_datetime(&naive)
.latest()
let dt = DateTime::new(
date_time.year() as i16,
date_time.month() as i8,
date_time.day() as i8,
date_time.hour() as i8,
date_time.minute() as i8,
date_time.second() as i8,
0,
)
.ok()?;
TimeZone::system()
.to_ambiguous_zoned(dt)
.later()
.ok()
.map(SystemTime::from)
}
pub fn system_time_to_zip_date_time(system_time: SystemTime) -> Option<zip::DateTime> {
let date_time: chrono::DateTime<chrono::Local> = system_time.into();
let date_time = Zoned::try_from(system_time).ok()?;
zip::DateTime::from_date_and_time(
date_time.year() as u16,

73
src/channel.rs Normal file
View file

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

View file

@ -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)]
@ -128,10 +126,15 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
// Assume the kind is Copy if not provided by the mime type
let mut kind = ClipboardKind::Copy;
let mut paths = Vec::new();
match mime.as_str() {
"text/uri-list" => {
let text = str::from_utf8(&data)?;
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),
@ -141,6 +144,7 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
}
"x-special/gnome-copied-files" => {
let text = str::from_utf8(&data)?;
for (i, line) in text.lines().enumerate() {
if i == 0 {
kind = match line {

View file

@ -1,20 +1,20 @@
// 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};
pub const CONFIG_VERSION: u64 = 1;
@ -164,11 +164,17 @@ pub struct Config {
pub app_theme: AppTheme,
pub dialog: DialogConfig,
pub desktop: DesktopConfig,
pub context_actions: Vec<ContextActionPreset>,
pub thumb_cfg: ThumbCfg,
pub favorites: Vec<Favorite>,
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,
}
@ -220,6 +226,7 @@ impl Default for Config {
app_theme: AppTheme::System,
desktop: DesktopConfig::default(),
dialog: DialogConfig::default(),
context_actions: Vec::new(),
thumb_cfg: ThumbCfg::default(),
favorites: vec![
Favorite::Home,
@ -232,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 {

80
src/context_action.rs Normal file
View file

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-only
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::mime_app;
use crate::spawn_detached::spawn_detached;
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum ContextActionSelection {
#[default]
#[serde(alias = "any")]
Any,
#[serde(alias = "files")]
Files,
#[serde(alias = "folders")]
Folders,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct ContextActionPreset {
pub name: String,
pub confirm: bool,
pub selection: ContextActionSelection,
pub steps: Vec<String>,
}
impl ContextActionPreset {
pub fn matches_selection(&self, selected: usize, selected_dir: usize) -> bool {
if selected == 0 {
return false;
}
match self.selection {
ContextActionSelection::Any => true,
ContextActionSelection::Files => selected_dir == 0,
ContextActionSelection::Folders => selected_dir == selected,
}
}
pub fn run(&self, paths: &[PathBuf]) {
if self.steps.is_empty() {
log::warn!("context action {:?} has no steps", self.name);
return;
}
for step in &self.steps {
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,
step
);
return;
};
for mut command in commands {
if let Err(err) = spawn_detached(&mut command) {
log::warn!(
"failed to run context action {:?} step {:?}: {}",
self.name,
step,
err
);
return;
}
}
}
}
}
pub fn run(actions: &[ContextActionPreset], action: usize, paths: &[PathBuf]) {
if let Some(preset) = actions.get(action) {
preset.run(paths);
} else {
log::warn!("invalid context action index `{action}`");
}
}

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::{
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,
},
iced_core::widget::operation,
iced_widget::scrollable::AbsoluteOffset,
iced_winit::{self, SurfaceIdWrapper},
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::{
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::app::{
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
};
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>);
@ -203,7 +191,7 @@ impl<T: AsRef<str>> From<T> for DialogLabel {
impl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> {
fn from(label: &'a DialogLabel) -> Self {
let mut iced_spans: Vec<cosmic::iced_core::text::Span<'_, ()>> =
let mut iced_spans: Vec<cosmic::iced::core::text::Span<'_, ()>> =
Vec::with_capacity(label.spans.len());
for span in &label.spans {
iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline));
@ -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);
@ -595,6 +583,7 @@ impl App {
col = col.push(
widget::text_input("", filename)
.id(self.filename_id.clone())
.double_click_select_delimiter('.')
.on_input(Message::Filename)
.on_submit(|_| Message::Save(false)),
);
@ -717,7 +706,7 @@ impl App {
match (selected.next(), selected.next()) {
// At least two selected items
(Some(_), Some(_)) => Some(self.tab.multi_preview_view()),
(Some(_), Some(_)) => Some(self.tab.multi_preview_view(None)),
// Exactly one selected item
(Some(item), None) => Some(item.preview_view(None, military_time)),
// No selected items
@ -743,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
@ -798,7 +793,7 @@ impl App {
};
search_location.map(|search_location| {
return (
(
Location::Search(
search_location,
term,
@ -806,7 +801,7 @@ impl App {
Instant::now(),
),
true,
);
)
})
}
None => match &self.tab.location {
@ -836,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> {
@ -889,16 +885,20 @@ impl App {
fn update_nav_model(&mut self) {
let mut nav_model = segmented_button::ModelBuilder::default();
nav_model = nav_model.insert(|b| {
b.text(fl!("recents"))
.icon(widget::icon::from_name("document-open-recent-symbolic"))
.data(Location::Recents)
});
if self.flags.config.show_recents {
nav_model = nav_model.insert(|b| {
b.text(fl!("recents"))
.icon(widget::icon::from_name("document-open-recent-symbolic"))
.data(Location::Recents)
});
}
for favorite in &self.flags.config.favorites {
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 {
@ -1122,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 {
@ -1206,7 +1206,9 @@ impl Application for App {
.icon(widget::icon::from_name("dialog-question").size(64))
.body(fl!("replace-warning"))
.primary_action(
widget::button::suggested(fl!("replace")).on_press(Message::DialogComplete),
widget::button::suggested(fl!("replace"))
.on_press(Message::DialogComplete)
.id(REPLACE_BUTTON_ID.clone()),
)
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
@ -1398,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();
}
}
@ -1453,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)
@ -1787,7 +1792,7 @@ impl Application for App {
use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{
Anchor, Gravity,
};
use cosmic::iced_runtime::platform_specific::wayland::popup::{
use cosmic::iced::runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner,
};
use cosmic::iced::Rectangle;
@ -1829,6 +1834,7 @@ impl Application for App {
&app.key_binds,
&app.modifiers,
false, // Paste not used in dialogs
&app.flags.config.context_actions,
)
.map(Message::TabMessage)
.map(cosmic::Action::App),
@ -1952,6 +1958,16 @@ impl Application for App {
if self.search_get().is_some() {
return widget::text_input::focus(self.search_id.clone());
}
if let DialogKind::SaveFile { filename } = &self.flags.kind {
return Task::batch([
widget::text_input::focus(self.filename_id.clone()),
widget::text_input::select_until_last(
self.filename_id.clone(),
filename,
'.',
),
]);
}
return widget::text_input::focus(self.filename_id.clone());
}
}
@ -2001,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);
@ -2022,7 +2038,7 @@ impl Application for App {
col = col.push(
self.tab
.view(&self.key_binds, &self.modifiers, false)
.view(&self.key_binds, &self.modifiers, false, &[])
.map(Message::TabMessage),
);

View file

@ -1,11 +1,10 @@
use cosmic::{
iced::keyboard::Key,
iced_core::keyboard::key::Named,
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,23 @@
// 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 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};
use crate::config::{Config, State};
use crate::tab::Location;
use app::{App, Flags};
pub mod app;
mod archive;
pub mod channel;
pub mod clipboard;
use config::Config;
pub mod config;
mod context_action;
pub mod dialog;
mod key_bind;
pub(crate) mod large_image;
@ -22,16 +30,15 @@ mod mounter;
mod mouse_area;
pub mod operation;
mod spawn_detached;
use tab::Location;
mod zoom;
use crate::config::State;
pub mod tab;
mod thumbnail_cacher;
mod thumbnailer;
pub(crate) mod trash;
mod zoom;
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
#[cfg(feature = "gvfs")]
pub(crate) fn err_str<T: ToString>(err: T) -> String {
err.to_string()
}
@ -72,7 +79,22 @@ pub fn is_wayland() -> bool {
/// Runs application in desktop mode
#[rustfmt::skip]
pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let log_format = tracing_subscriber::fmt::format()
.pretty()
.without_time()
.with_line_number(true)
.with_file(true)
.with_target(false)
.with_thread_names(true);
let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
.with(log_layer)
.init();
localize::localize();
@ -107,7 +129,21 @@ pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
/// Runs application with these settings
#[rustfmt::skip]
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let log_format = tracing_subscriber::fmt::format()
.pretty()
.with_line_number(true)
.with_file(true)
.with_target(false)
.with_thread_names(true);
let log_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.event_format(log_format);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(log_layer)
.init();
localize::localize();
@ -157,7 +193,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
}
if daemonize {
#[cfg(all(unix, not(target_os = "redox")))]
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
match fork::daemon(true, true) {
Ok(fork::Fork::Child) => (),
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),

View file

@ -1,11 +1,10 @@
use cosmic::{iced_core, iced_widget};
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

@ -6,5 +6,6 @@ use tikv_jemallocator::Jemalloc;
static GLOBAL: Jemalloc = Jemalloc;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = jxl_oxide::integration::register_image_decoding_hook();
cosmic_files::main()
}

View file

@ -1,29 +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,
fl,
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
};
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"));
@ -37,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)
);
@ -60,6 +57,7 @@ pub fn context_menu<'a>(
key_binds: &HashMap<KeyBind, Action>,
modifiers: &Modifiers,
clipboard_paste_available: bool,
context_actions: &[ContextActionPreset],
) -> Element<'a, tab::Message> {
let find_key = |action: &Action| -> String {
for (key_bind, key_action) in key_binds {
@ -141,12 +139,11 @@ 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")
{
selected_desktop_entry = Some(&**path);
}
&& path.extension().and_then(|s| s.to_str()) == Some("desktop") =>
{
selected_desktop_entry = Some(&**path);
}
_ => (),
}
@ -157,6 +154,14 @@ pub fn context_menu<'a>(
selected_types.sort_unstable();
selected_types.dedup();
selected_trash_only = selected_trash_only && selected == 1;
let context_action_items = |selected: usize, selected_dir: usize| {
context_actions
.iter()
.enumerate()
.filter(|(_, action)| action.matches_selection(selected, selected_dir))
.map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)).into())
.collect::<Vec<Element<'a, tab::Message>>>()
};
// Parse the desktop entry if it is the only selection
#[cfg(feature = "desktop")]
let selected_desktop_entry = selected_desktop_entry.and_then(|path| {
@ -183,14 +188,14 @@ pub fn context_menu<'a>(
) => {
if selected_trash_only {
children.push(menu_item(fl!("open"), Action::Open).into());
if !trash::os_limited::is_empty().unwrap_or(true) {
if !Trash::is_empty() {
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
}
} else if let Some(entry) = selected_desktop_entry {
} 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(),
));
}
@ -204,6 +209,11 @@ pub fn context_menu<'a>(
}
// Should this simply bypass trash and remove the shortcut?
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into());
let action_items = context_action_items(selected, selected_dir);
if !action_items.is_empty() {
children.push(divider::horizontal::light().into());
children.extend(action_items);
}
} else if selected > 0 {
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
children.push(menu_item(fl!("open"), Action::Open).into());
@ -215,7 +225,7 @@ pub fn context_menu<'a>(
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
}
}
if tab.location.is_recents() {
if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) {
children.push(
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
);
@ -226,6 +236,11 @@ pub fn context_menu<'a>(
children
.push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into());
}
let action_items = context_action_items(selected, selected_dir);
if !action_items.is_empty() {
children.push(divider::horizontal::light().into());
children.extend(action_items);
}
children.push(divider::horizontal::light().into());
if selected_mount_point == 0 {
children.push(menu_item(fl!("rename"), Action::Rename).into());
@ -559,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()
}
@ -614,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 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:?}");
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) {
args.push(BString::new(path.as_bytes().to_owned()));
}
}
}
_ => (),
}
} else {
new_argument.push_char(char);
}
// 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);
}
processes
}
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:?}");
if !new_argument.is_empty() {
args.push(new_argument);
}
// 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)),
);
vec![process]
}
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:?}");
let mut command = process::Command::new(&arguments[0]);
for arg in args {
match arg.to_os_str() {
Ok(arg) => {
command.arg(arg);
}
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";
@ -19,6 +17,7 @@ struct MimeIconKey {
struct MimeIconCache {
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
#[cfg(unix)]
shared_mime_info: xdg_mime::SharedMimeInfo,
}
@ -26,10 +25,17 @@ impl MimeIconCache {
pub fn new() -> Self {
Self {
cache: FxHashMap::default(),
#[cfg(unix)]
shared_mime_info: xdg_mime::SharedMimeInfo::new(),
}
}
#[cfg(not(unix))]
pub fn get(&mut self, _key: MimeIconKey) -> Option<icon::Handle> {
None
}
#[cfg(unix)]
pub fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
self.cache
.entry(key)
@ -39,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();
@ -53,6 +59,16 @@ impl MimeIconCache {
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
#[cfg(not(unix))]
pub fn mime_for_path(
path: impl AsRef<Path>,
metadata_opt: Option<&fs::Metadata>,
remote: bool,
) -> Mime {
mime_guess::from_path(path).first_or_octet_stream()
}
#[cfg(unix)]
pub fn mime_for_path(
path: impl AsRef<Path>,
metadata_opt: Option<&fs::Metadata>,
@ -96,12 +112,20 @@ 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(),
}
}
#[cfg(not(unix))]
pub fn parent_mime_types(_mime: &Mime) -> Option<Vec<Mime>> {
None
}
#[cfg(unix)]
pub fn parent_mime_types(mime: &Mime) -> Option<Vec<Mime>> {
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
mime_icon_cache.shared_mime_info.get_parents_aliased(mime)
}

View file

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

View file

@ -1,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)]
@ -395,7 +391,7 @@ where
update(
self,
&event,
event,
layout,
cursor,
shell,
@ -488,7 +484,7 @@ where
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut cosmic::iced_core::clipboard::DndDestinationRectangles,
dnd_rectangles: &mut cosmic::iced::core::clipboard::DndDestinationRectangles,
) {
self.content.as_widget().drag_destinations(
&state.children[0],

View file

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

View file

@ -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;
@ -22,6 +17,9 @@ use zip::AesMode::Aes256;
pub use self::controller::{Controller, ControllerState};
pub mod controller;
pub use notifiers::*;
mod notifiers;
pub use self::reader::OpReader;
pub mod reader;
@ -36,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;
@ -44,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;
@ -111,7 +109,7 @@ async fn copy_or_move(
);
// Handle duplicate file names by renaming paths
let mut from_to_pairs: Vec<(PathBuf, PathBuf)> = paths
let from_to_pairs_iter = paths
.into_iter()
.zip(std::iter::repeat(to.as_path()))
.filter_map(|(from, to)| {
@ -129,36 +127,46 @@ async fn copy_or_move(
//TODO: how to handle from missing file name?
None
}
})
.collect();
});
// Attempt quick and simple renames
//TODO: allow rename to be used for directories in recursive context?
if matches!(method, Method::Move { .. }) {
from_to_pairs.retain(|(from, to)| {
//TODO: show replace dialog here?
if to.exists() {
return true;
}
//TODO: use compio::fs::rename?
match fs::rename(from, to) {
Ok(()) => {
log::info!("renamed {} to {}", from.display(), to.display());
false
let from_to_pairs: Vec<(PathBuf, PathBuf)> = if matches!(method, Method::Move { .. }) {
from_to_pairs_iter
.map(|(from, to)| async move {
//TODO: show replace dialog here?
if to.exists() {
return Some((from, to));
}
Err(err) => {
log::info!(
"failed to rename {} to {}, fallback to recursive move: {}",
from.display(),
to.display(),
err
);
true
match compio::fs::rename(&from, &to).await {
Ok(()) => {
log::info!("renamed {} to {}", from.display(), to.display());
None
}
Err(err) => {
log::info!(
"failed to rename {} to {}, fallback to recursive move: {}",
from.display(),
to.display(),
err
);
Some((from, to))
}
}
}
});
}
})
.collect::<cosmic::iced::futures::stream::FuturesOrdered<_>>()
.fold(Vec::new(), |mut pairs, pair| async move {
if let Some(pair) = pair {
pairs.push(pair);
}
pairs
})
.await
} else {
from_to_pairs_iter.collect()
};
let mut context = Context::new(controller.clone());
@ -216,7 +224,7 @@ pub async fn sync_to_disk(
}
}))
.buffer_unordered(32)
.collect::<Vec<_>>()
.collect::<()>()
.await;
// Sync directories to disk
@ -226,7 +234,7 @@ pub async fn sync_to_disk(
}
}))
.buffer_unordered(16)
.collect::<Vec<_>>()
.collect::<()>()
.await;
}
@ -762,13 +770,12 @@ impl Operation {
OperationError::from_err(e, &controller)
})?;
if let Ok(modified) = metadata.modified() {
if let Some(last_modified) =
if let Ok(modified) = metadata.modified()
&& let Some(last_modified) =
archive::system_time_to_zip_date_time(modified)
{
zip_options =
zip_options.last_modified_time(last_modified);
}
{
zip_options =
zip_options.last_modified_time(last_modified);
}
#[cfg(unix)]
@ -1113,7 +1120,9 @@ impl Operation {
#[cfg(target_os = "macos")]
Self::Restore { .. } => {
// TODO: add support for macos
return OperationError::from_msg("Restoring from trash is not supported on macos");
return Err(OperationError::from_msg(
"Restoring from trash is not supported on macos",
));
}
#[cfg(not(target_os = "macos"))]
Self::Restore { items } => {
@ -1181,8 +1190,10 @@ impl Operation {
.map_err(|s| OperationError::from_state(s, &controller))?;
let controller_clone = controller.clone();
let path_clone = path.clone();
compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {
let controller = controller_clone;
let path = path_clone;
//TODO: what to do on non-Unix systems?
#[cfg(unix)]
{
@ -1197,7 +1208,10 @@ impl Operation {
.await
.map_err(wrap_compio_spawn_error)?
.map_err(|e| OperationError::from_err(e, &controller))?;
Ok(OperationSelection::default())
Ok(OperationSelection {
ignored: Vec::new(),
selected: vec![path],
})
}
};
@ -1224,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

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

View file

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

View file

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

1081
src/tab.rs

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,15 +1,16 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(feature = "desktop")]
use cosmic::desktop::fde::GenericEntry;
use mime_guess::Mime;
use rustc_hash::FxHashMap;
#[cfg(feature = "desktop")]
use std::{fs, time::Instant};
use std::{
fs,
path::Path,
process,
sync::{LazyLock, Mutex},
time::Instant,
};
#[derive(Clone, Debug)]

153
src/trash.rs Normal file
View file

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